import { rootPath } from "./config"; import { readYamlRecord } from "./platform-infra-ops-library"; import { materializeYamlComposition } from "./yaml-composition"; const pacConfigPath = "config/platform-infra/pipelines-as-code.yaml"; const giteaConfigPath = "config/platform-infra/gitea.yaml"; export const PAC_AUTOMATIC_DELIVERY_REFERENCE = "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界"; export interface PacDeliveryConsumerAuthority { readonly consumerId: string; readonly node: string; readonly lane: string; readonly namespace: string; readonly pipelineRunPrefix: string; readonly repositoryRef: string; readonly sourceRepository: string; readonly sourceBranch: string; readonly sourceSnapshotPrefix: string; readonly giteaOwner: string; readonly giteaRepository: string; readonly giteaRepositoryUrl: string; readonly pacControllerVersion: string; readonly configRefs: readonly [string, string]; } export interface CicdDeliveryAuthorityCatalog { readonly consumers: readonly PacDeliveryConsumerAuthority[]; readonly configRefs: readonly [string, string]; } export interface CicdDeliveryAuthorityQuery { readonly consumerId?: string | null; readonly node?: string | null; readonly lane?: string | null; readonly sourceRepository?: string | null; readonly sourceBranch?: string | null; readonly declaredSourceAuthorityMode?: string | null; readonly declaredConfigRef?: string | null; } export interface CicdDeliveryMutationContext { readonly command: string; readonly statusCommand: string; readonly action: string; readonly node: string; readonly lane: string; } export type CicdDeliveryMutationDecision = | { readonly allowed: true; readonly authority: Extract; } | { readonly allowed: false; readonly authority: Exclude; readonly result: Record; }; export type CicdDeliveryAuthority = | { readonly kind: "pac-pr-merge"; readonly migrated: true; readonly mutationHintsAllowed: false; readonly consumer: PacDeliveryConsumerAuthority; readonly reason: "exact-pac-consumer"; readonly valuesPrinted: false; } | { readonly kind: "legacy-manual"; readonly migrated: false; readonly mutationHintsAllowed: true; readonly consumer: null; readonly reason: "explicit-legacy-source-authority"; readonly declaredSourceAuthorityMode: string; readonly declaredConfigRef: string; readonly valuesPrinted: false; } | { readonly kind: "unknown"; readonly migrated: false; readonly mutationHintsAllowed: false; readonly consumer: null; readonly reason: "authority-config-invalid" | "authority-identity-mismatch" | "authority-not-declared" | "authority-ambiguous"; readonly detail: string; readonly configRefs: readonly string[]; readonly valuesPrinted: false; }; interface PacRepositoryRow { readonly id: string; readonly owner: string; readonly repo: string; readonly sourceBranch: string; readonly sourceSnapshotPrefix: string; readonly url: string; } interface GiteaRepositoryRow { readonly targetId: string; readonly owner: string; readonly repo: string; readonly sourceRepository: string; readonly sourceBranch: string; } const legacyAuthorityModes = new Set(["gitMirrorSnapshot", "git-mirror-snapshot", "k8s-git-mirror-snapshot"]); export function readCicdDeliveryAuthorityCatalog(): CicdDeliveryAuthorityCatalog { const pacRoot = composedYaml(pacConfigPath, "platform-infra-pipelines-as-code"); const giteaRoot = composedYaml(giteaConfigPath, "platform-infra-gitea"); const release = record(pacRoot.release, `${pacConfigPath}#release`); const pacControllerVersion = requiredString(release.version, `${pacConfigPath}#release.version`); const pacRepositories = recordArray(pacRoot.repositories, `${pacConfigPath}#repositories`).map((item, index) => { const path = `${pacConfigPath}#repositories[${index}]`; const params = record(item.params, `${path}.params`); return { id: requiredString(item.id ?? item.name, `${path}.id`), owner: requiredString(item.owner, `${path}.owner`), repo: requiredString(item.repo, `${path}.repo`), sourceBranch: requiredString(params.source_branch, `${path}.params.source_branch`), sourceSnapshotPrefix: requiredString(params.source_snapshot_prefix, `${path}.params.source_snapshot_prefix`), url: requiredString(item.url, `${path}.url`), } satisfies PacRepositoryRow; }); const sourceAuthority = record(giteaRoot.sourceAuthority, `${giteaConfigPath}#sourceAuthority`); const giteaRepositories = recordArray(sourceAuthority.repositories, `${giteaConfigPath}#sourceAuthority.repositories`).map((item, index) => { const path = `${giteaConfigPath}#sourceAuthority.repositories[${index}]`; const upstream = record(item.upstream, `${path}.upstream`); const gitea = record(item.gitea, `${path}.gitea`); return { targetId: requiredString(item.targetId, `${path}.targetId`), owner: requiredString(gitea.owner, `${path}.gitea.owner`), repo: requiredString(gitea.name, `${path}.gitea.name`), sourceRepository: requiredString(upstream.repository, `${path}.upstream.repository`), sourceBranch: requiredString(upstream.branch, `${path}.upstream.branch`), } satisfies GiteaRepositoryRow; }); const consumers = recordArray(pacRoot.consumers, `${pacConfigPath}#consumers`).map((item, index) => { const path = `${pacConfigPath}#consumers[${index}]`; const consumerId = requiredString(item.id, `${path}.id`); const node = requiredString(item.node, `${path}.node`); const lane = requiredString(item.lane, `${path}.lane`); const namespace = requiredString(item.namespace, `${path}.namespace`); const pipelineRunPrefix = requiredString(item.pipelineRunPrefix, `${path}.pipelineRunPrefix`); const repositoryRef = requiredString(item.repositoryRef, `${path}.repositoryRef`); const pacRepository = exactlyOne( pacRepositories.filter((candidate) => same(candidate.id, repositoryRef)), `${path}.repositoryRef=${repositoryRef}`, ); const authorityRepository = exactlyOne( giteaRepositories.filter((candidate) => same(candidate.targetId, node) && same(candidate.owner, pacRepository.owner) && same(candidate.repo, pacRepository.repo) && same(candidate.sourceBranch, pacRepository.sourceBranch)), `${path} source authority ${node}/${pacRepository.owner}/${pacRepository.repo}@${pacRepository.sourceBranch}`, ); return { consumerId, node, lane, namespace, pipelineRunPrefix, repositoryRef, sourceRepository: authorityRepository.sourceRepository, sourceBranch: authorityRepository.sourceBranch, sourceSnapshotPrefix: pacRepository.sourceSnapshotPrefix, giteaOwner: pacRepository.owner, giteaRepository: pacRepository.repo, giteaRepositoryUrl: pacRepository.url, pacControllerVersion, configRefs: [pacConfigPath, giteaConfigPath], } satisfies PacDeliveryConsumerAuthority; }); const duplicateIds = consumers.filter((item, index) => consumers.findIndex((candidate) => same(candidate.consumerId, item.consumerId)) !== index); if (duplicateIds.length > 0) throw new Error(`${pacConfigPath} contains duplicate consumer ids: ${duplicateIds.map((item) => item.consumerId).join(", ")}`); return { consumers, configRefs: [pacConfigPath, giteaConfigPath] }; } export function resolveCicdDeliveryAuthority(query: CicdDeliveryAuthorityQuery): CicdDeliveryAuthority { try { return resolveCicdDeliveryAuthorityFromCatalog(readCicdDeliveryAuthorityCatalog(), query); } catch (error) { return { kind: "unknown", migrated: false, mutationHintsAllowed: false, consumer: null, reason: "authority-config-invalid", detail: error instanceof Error ? error.message : String(error), configRefs: [pacConfigPath, giteaConfigPath], valuesPrinted: false, }; } } export function resolveCicdDeliveryAuthorityFromCatalog(catalog: CicdDeliveryAuthorityCatalog, query: CicdDeliveryAuthorityQuery): CicdDeliveryAuthority { const suppliedIdentity = [query.consumerId, query.node, query.lane, query.sourceRepository, query.sourceBranch] .some((value) => normalized(value) !== null); const candidates = suppliedIdentity ? catalog.consumers.filter((consumer) => matchesQuery(consumer, query)) : []; const stableCandidates = stableIdentityCandidates(catalog, query); if (candidates.length === 1) { return { kind: "pac-pr-merge", migrated: true, mutationHintsAllowed: false, consumer: candidates[0], reason: "exact-pac-consumer", valuesPrinted: false, }; } if (candidates.length > 1) { return unknownAuthority("authority-ambiguous", `identity matches multiple PaC consumers: ${candidates.map((item) => item.consumerId).join(", ")}`, catalog.configRefs); } if (suppliedIdentity && query.consumerId !== undefined && query.consumerId !== null) { return unknownAuthority("authority-identity-mismatch", `consumer identity does not match ${query.consumerId}`, catalog.configRefs); } if (stableCandidates.length > 0) { return unknownAuthority( stableCandidates.length > 1 ? "authority-ambiguous" : "authority-identity-mismatch", `stable PaC identity conflicts with source details: ${stableCandidates.map((item) => item.consumerId).join(", ")}`, catalog.configRefs, ); } const declaredMode = normalized(query.declaredSourceAuthorityMode); const declaredConfigRef = normalized(query.declaredConfigRef); if (declaredMode !== null && legacyAuthorityModes.has(declaredMode) && declaredConfigRef !== null) { return { kind: "legacy-manual", migrated: false, mutationHintsAllowed: true, consumer: null, reason: "explicit-legacy-source-authority", declaredSourceAuthorityMode: declaredMode, declaredConfigRef, valuesPrinted: false, }; } return unknownAuthority( suppliedIdentity ? "authority-identity-mismatch" : "authority-not-declared", suppliedIdentity ? "no exact PaC consumer or explicit legacy authority matched" : "delivery authority identity is not declared", catalog.configRefs, ); } function stableIdentityCandidates(catalog: CicdDeliveryAuthorityCatalog, query: CicdDeliveryAuthorityQuery): readonly PacDeliveryConsumerAuthority[] { const consumerId = normalized(query.consumerId); if (consumerId !== null) return catalog.consumers.filter((consumer) => same(consumer.consumerId, consumerId)); const node = normalized(query.node); const lane = normalized(query.lane); if (node === null || lane === null) return []; return catalog.consumers.filter((consumer) => same(consumer.node, node) && same(consumer.lane, lane)); } export function decideCicdDeliveryMutation(authority: CicdDeliveryAuthority, context: CicdDeliveryMutationContext): CicdDeliveryMutationDecision { if (authority.kind === "legacy-manual") return { allowed: true, authority }; const next = authority.kind === "pac-pr-merge" ? pacReadOnlyNext(authority.consumer.node, authority.consumer.consumerId) : { status: context.statusCommand, fixAutomaticDelivery: pacAutomaticDeliveryFix(authority), valuesPrinted: false, }; return { allowed: false, authority, result: { ok: false, command: context.command, mode: authority.kind === "pac-pr-merge" ? "pac-pr-merge-mutation-blocked" : "delivery-authority-unknown", mutation: false, requestedAction: context.action, node: context.node, lane: context.lane, reason: authority.kind === "pac-pr-merge" ? "该 lane 已迁移到 PaC;GitHub PR merge 是唯一交付触发,CLI 不执行人工推进。" : "无法从 owning YAML 唯一确认 delivery authority;CLI 已 fail-closed。", deliveryAuthority: authority, next, valuesPrinted: false, }, }; } export function gitRepositoryIdentity(remoteUrl: string): string | null { const value = remoteUrl.trim(); if (value.length === 0) return null; const scp = value.match(/^[^@\s]+@[^:\s]+:([^\s?#]+?)(?:\.git)?$/u); if (scp !== null) return normalizedRepositoryPath(scp[1]); try { const url = new URL(value); return normalizedRepositoryPath(url.pathname); } catch { return normalizedRepositoryPath(value); } } export function pacAutomaticDeliveryFix(authority: CicdDeliveryAuthority): Record { const configRefs = authority.kind === "pac-pr-merge" ? authority.consumer.configRefs : authority.kind === "unknown" ? authority.configRefs : [authority.declaredConfigRef]; return automaticDeliveryFix(configRefs, authority.kind === "unknown" ? "fix-delivery-authority" : "fix-automatic-delivery"); } export function pacReadOnlyNext(targetId: string, consumerId: string): Record { return { status: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId} --consumer ${consumerId}`, history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${targetId} --consumer ${consumerId}`, fixAutomaticDelivery: automaticDeliveryFix([pacConfigPath, giteaConfigPath], "fix-automatic-delivery"), valuesPrinted: false, }; } export function pacNodeReadOnlyNext(targetId: string): Record { return { status: `bun scripts/cli.ts cicd status --node ${targetId}`, history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${targetId} --limit 10`, fixAutomaticDelivery: automaticDeliveryFix([pacConfigPath, giteaConfigPath], "fix-automatic-delivery"), valuesPrinted: false, }; } function automaticDeliveryFix(configRefs: readonly string[], code: "fix-automatic-delivery" | "fix-delivery-authority"): Record { return { code, reference: PAC_AUTOMATIC_DELIVERY_REFERENCE, configRefs, instruction: "修复 owning YAML、controller 或源码中的自动链缺陷,并以新的正常 GitHub PR merge 验收;不得人工补齐当前交付。", mutation: false, valuesPrinted: false, }; } function normalizedRepositoryPath(value: string): string | null { const result = value.replace(/^\/+|\/+$/gu, "").replace(/\.git$/iu, "").trim(); if (!/^[^/\s]+\/[^/\s]+$/u.test(result)) return null; return result; } function matchesQuery(consumer: PacDeliveryConsumerAuthority, query: CicdDeliveryAuthorityQuery): boolean { return matches(consumer.consumerId, query.consumerId) && matches(consumer.node, query.node) && matches(consumer.lane, query.lane) && matches(consumer.sourceRepository, query.sourceRepository) && matches(consumer.sourceBranch, query.sourceBranch); } function matches(actual: string, expected: string | null | undefined): boolean { const value = normalized(expected); return value === null || same(actual, value); } function same(left: string, right: string): boolean { return left.toLowerCase() === right.toLowerCase(); } function normalized(value: string | null | undefined): string | null { if (typeof value !== "string") return null; const result = value.trim(); return result.length === 0 ? null : result; } function unknownAuthority(reason: Extract["reason"], detail: string, configRefs: readonly string[]): CicdDeliveryAuthority { return { kind: "unknown", migrated: false, mutationHintsAllowed: false, consumer: null, reason, detail, configRefs, valuesPrinted: false }; } function composedYaml(path: string, kind: string): Record { return materializeYamlComposition(readYamlRecord>(rootPath(...path.split("/")), kind), { label: path }).value; } function record(value: unknown, path: string): Record { if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must be an object`); return value as Record; } function recordArray(value: unknown, path: string): Record[] { if (!Array.isArray(value)) throw new Error(`${path} must be an array`); return value.map((item, index) => record(item, `${path}[${index}]`)); } function requiredString(value: unknown, path: string): string { if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path} must be a non-empty string`); return value.trim(); } function exactlyOne(items: T[], path: string): T { if (items.length !== 1) throw new Error(`${path} must resolve exactly one row; got ${items.length}`); return items[0]; }