Files
pikasTech-unidesk/scripts/src/cicd-delivery-authority.ts
T
2026-07-11 11:42:16 +02:00

389 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<CicdDeliveryAuthority, { kind: "legacy-manual" }>;
}
| {
readonly allowed: false;
readonly authority: Exclude<CicdDeliveryAuthority, { kind: "legacy-manual" }>;
readonly result: Record<string, unknown>;
};
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 已迁移到 PaCGitHub PR merge 是唯一交付触发,CLI 不执行人工推进。"
: "无法从 owning YAML 唯一确认 delivery authorityCLI 已 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<string, unknown> {
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<string, unknown> {
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<string, unknown> {
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<string, unknown> {
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<CicdDeliveryAuthority, { kind: "unknown" }>["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<string, unknown> {
return materializeYamlComposition(readYamlRecord<Record<string, unknown>>(rootPath(...path.split("/")), kind), { label: path }).value;
}
function record(value: unknown, path: string): Record<string, unknown> {
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must be an object`);
return value as Record<string, unknown>;
}
function recordArray(value: unknown, path: string): Record<string, unknown>[] {
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<T>(items: T[], path: string): T {
if (items.length !== 1) throw new Error(`${path} must resolve exactly one row; got ${items.length}`);
return items[0];
}