dcdc8d1ddd
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环): - 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口) - 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入 parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截 - 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过): * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id) * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配 * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数 * 回滚 job payload 支持 prior_execution_id(strict 解码) - 三通道发布+回滚演练实测:publish 4/4 verified,检索命中, rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false - 前端 A·Data-console 风格全站统一,120 用例通过 - 门禁:go vet / go test 全包通过,gofmt 干净
156 lines
6.6 KiB
Go
156 lines
6.6 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
)
|
|
|
|
// ErrReleaseAuditChainNotFound is returned for an unknown execution id.
|
|
var ErrReleaseAuditChainNotFound = errors.New("release audit chain not found")
|
|
|
|
const releaseAuditChainHeadSQL = `
|
|
SELECT e.id, e.operation, e.state, e.failure_level,
|
|
COALESCE(e.release_batch_id,''), COALESCE(b.name,''), COALESCE(b.created_by,''), COALESCE(b.sealed_by,''),
|
|
COALESCE(e.authorized_by,''), COALESCE(e.release_change_set_id::text,''),
|
|
COALESCE(rcs.name,''), COALESCE(rcs.number,0), COALESCE(rcs.state,''),
|
|
COALESCE(rcs.created_by,''), COALESCE(rcs.confirmed_by,''), rcs.confirmed_at,
|
|
COALESCE(rcs.manifest_sha256,''), COALESCE(e.prior_execution_id,''),
|
|
COALESCE((SELECT rb.id FROM release_executions AS rb
|
|
WHERE rb.prior_execution_id = e.id AND rb.operation = 'rollback'
|
|
ORDER BY rb.created_at DESC LIMIT 1),'')
|
|
FROM release_executions AS e
|
|
LEFT JOIN release_batches AS b ON b.id = e.release_batch_id
|
|
LEFT JOIN release_change_sets AS rcs ON rcs.id = e.release_change_set_id
|
|
WHERE e.id = $1
|
|
`
|
|
|
|
const releaseAuditSnapshotLinksSQL = `
|
|
SELECT snap.id, COALESCE(conn.name,''), snap.node_count, snap.file_count, snap.observed_at
|
|
FROM release_change_set_snapshot_links AS link
|
|
JOIN source_catalog_snapshots AS snap ON snap.id = link.snapshot_id
|
|
LEFT JOIN source_connections AS conn ON conn.id = snap.source_connection_id
|
|
WHERE link.change_set_id = $1
|
|
ORDER BY snap.id
|
|
`
|
|
|
|
const releaseAuditCatalogLinksSQL = `
|
|
SELECT cset.id, cset.state, cset.add_count, cset.update_count,
|
|
cset.move_count, cset.delete_count
|
|
FROM release_change_set_catalog_links AS link
|
|
JOIN source_catalog_change_sets AS cset ON cset.id = link.catalog_change_set_id
|
|
WHERE link.change_set_id = $1
|
|
ORDER BY cset.id
|
|
`
|
|
|
|
const releaseAuditItemsSQL = `
|
|
SELECT COALESCE(batch_item.publication_id, item.publication_id),
|
|
COALESCE(version_title.title, ''),
|
|
COALESCE(item.action, batch_item.action),
|
|
COALESCE(item.state, 'pending'),
|
|
COALESCE(batch_item.review_id, ''),
|
|
COALESCE(batch_item.acceptance_run_id, ''),
|
|
COALESCE(version.source_item_id, ''),
|
|
COALESCE(batch_item.source_item_version_id, ''),
|
|
COALESCE(item.production_revision, ''),
|
|
COALESCE(item.error_code, '')
|
|
FROM release_item_executions AS item
|
|
JOIN test_publications AS publication ON publication.id = item.publication_id
|
|
JOIN source_item_versions AS version ON version.id = publication.source_item_version_id
|
|
JOIN source_items AS version_title ON version_title.id = version.source_item_id
|
|
LEFT JOIN release_batch_items AS batch_item
|
|
ON batch_item.release_batch_id = $2 AND batch_item.publication_id = item.publication_id
|
|
WHERE item.release_execution_id = $1
|
|
ORDER BY item.id
|
|
`
|
|
|
|
// GetReleaseAuditChain projects the complete R6 evidence chain for one
|
|
// execution: execution -> batch -> release change set -> bound source
|
|
// snapshots and catalog change sets -> per-item review/acceptance/source
|
|
// identity, plus the compensating rollback row when one exists. Every link is
|
|
// read through real foreign keys (migration 30), not denormalized JSON. Only
|
|
// safe identifiers, labels, counts and timestamps are selected — never bundle
|
|
// paths, credentials, base URLs, or signed object URLs.
|
|
func (s *Store) GetReleaseAuditChain(ctx context.Context, executionID string) (governance.ReleaseAuditChain, error) {
|
|
if !validShortID(executionID) {
|
|
return governance.ReleaseAuditChain{}, ErrReleaseAuditChainNotFound
|
|
}
|
|
var chain governance.ReleaseAuditChain
|
|
err := s.db.QueryRow(ctx, releaseAuditChainHeadSQL, executionID).Scan(
|
|
&chain.ExecutionID, &chain.Operation, &chain.State, &chain.FailureLevel,
|
|
&chain.BatchID, &chain.BatchName, &chain.CreatedBy, &chain.SealedBy,
|
|
&chain.AuthorizedBy, &chain.ReleaseChangeSetID,
|
|
&chain.ChangeSetName, &chain.ChangeSetNumber, &chain.ChangeSetState,
|
|
&chain.ChangeSetCreatedBy, &chain.ChangeSetConfirmedBy, &chain.ChangeSetConfirmedAt,
|
|
&chain.ManifestSHA256, &chain.PriorExecutionID,
|
|
&chain.RollbackExecutionID,
|
|
)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return governance.ReleaseAuditChain{}, ErrReleaseAuditChainNotFound
|
|
}
|
|
if err != nil {
|
|
return governance.ReleaseAuditChain{}, fmt.Errorf("load release audit chain: %w", err)
|
|
}
|
|
chain.Snapshots = []governance.AuditSnapshot{}
|
|
chain.CatalogChangeSets = []governance.AuditCatalogSet{}
|
|
chain.Items = []governance.AuditChainItem{}
|
|
if chain.ReleaseChangeSetID != "" {
|
|
rows, err := s.db.Query(ctx, releaseAuditSnapshotLinksSQL, chain.ReleaseChangeSetID)
|
|
if err != nil {
|
|
return governance.ReleaseAuditChain{}, fmt.Errorf("list release snapshot links: %w", err)
|
|
}
|
|
for rows.Next() {
|
|
var snap governance.AuditSnapshot
|
|
if err := rows.Scan(&snap.ID, &snap.SourceName, &snap.NodeCount, &snap.FileCount, &snap.ObservedAt); err != nil {
|
|
rows.Close()
|
|
return governance.ReleaseAuditChain{}, fmt.Errorf("scan release snapshot link: %w", err)
|
|
}
|
|
chain.Snapshots = append(chain.Snapshots, snap)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
rows.Close()
|
|
return governance.ReleaseAuditChain{}, fmt.Errorf("iterate release snapshot links: %w", err)
|
|
}
|
|
rows.Close()
|
|
rows, err = s.db.Query(ctx, releaseAuditCatalogLinksSQL, chain.ReleaseChangeSetID)
|
|
if err != nil {
|
|
return governance.ReleaseAuditChain{}, fmt.Errorf("list release catalog links: %w", err)
|
|
}
|
|
for rows.Next() {
|
|
var set governance.AuditCatalogSet
|
|
if err := rows.Scan(&set.ID, &set.State, &set.AddCount, &set.UpdateCount, &set.MoveCount, &set.DeleteCount); err != nil {
|
|
rows.Close()
|
|
return governance.ReleaseAuditChain{}, fmt.Errorf("scan release catalog link: %w", err)
|
|
}
|
|
chain.CatalogChangeSets = append(chain.CatalogChangeSets, set)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
rows.Close()
|
|
return governance.ReleaseAuditChain{}, fmt.Errorf("iterate release catalog links: %w", err)
|
|
}
|
|
rows.Close()
|
|
}
|
|
rows, err := s.db.Query(ctx, releaseAuditItemsSQL, executionID, chain.BatchID)
|
|
if err != nil {
|
|
return governance.ReleaseAuditChain{}, fmt.Errorf("list release audit items: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var item governance.AuditChainItem
|
|
if err := rows.Scan(&item.PublicationID, &item.Title, &item.Action, &item.ItemState,
|
|
&item.ReviewID, &item.AcceptanceRunID, &item.SourceItemID, &item.SourceVersionID,
|
|
&item.ProductionRevision, &item.ErrorCode); err != nil {
|
|
return governance.ReleaseAuditChain{}, fmt.Errorf("scan release audit item: %w", err)
|
|
}
|
|
chain.Items = append(chain.Items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return governance.ReleaseAuditChain{}, fmt.Errorf("iterate release audit items: %w", err)
|
|
}
|
|
return chain, nil
|
|
}
|