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

177 lines
7.4 KiB
TypeScript

// Responsibility: fail-closed delivery authority and safe navigation for the retired branch-follower surface.
import {
pacAutomaticDeliveryFix,
pacNodeReadOnlyNext,
resolveCicdDeliveryAuthority,
type CicdDeliveryAuthority,
} from "./cicd-delivery-authority";
import type { BranchFollowerRegistry, FollowerSpec, ParsedOptions } from "./cicd-types";
export type BranchFollowerDeliveryAuthority =
| {
readonly kind: "retired-readonly";
readonly mutationHintsAllowed: false;
readonly followers: readonly string[];
readonly pacAuthorities: readonly Extract<CicdDeliveryAuthority, { kind: "pac-pr-merge" }>[];
readonly reason: "all-selected-followers-retired-to-pac";
}
| {
readonly kind: "legacy-manual";
readonly mutationHintsAllowed: true;
readonly followers: readonly string[];
readonly reason: "all-selected-followers-explicit-legacy-manual";
}
| {
readonly kind: "unknown";
readonly mutationHintsAllowed: false;
readonly followers: readonly string[];
readonly reason: "authority-not-declared" | "authority-mixed" | "retired-pac-identity-invalid" | "legacy-conflicts-with-pac";
readonly details: readonly Record<string, unknown>[];
};
export function branchFollowerDeliveryAuthority(
registry: BranchFollowerRegistry,
options: ParsedOptions,
): BranchFollowerDeliveryAuthority {
const followers = selectedFollowers(registry, options);
const rows = followers.map(resolveFollowerAuthority);
if (rows.every((row) => row.kind === "retired-readonly")) {
return {
kind: "retired-readonly",
mutationHintsAllowed: false,
followers: followers.map((follower) => follower.id),
pacAuthorities: rows.map((row) => row.pacAuthority).filter((item): item is Extract<CicdDeliveryAuthority, { kind: "pac-pr-merge" }> => item !== null),
reason: "all-selected-followers-retired-to-pac",
};
}
if (rows.every((row) => row.kind === "legacy-manual")) {
return {
kind: "legacy-manual",
mutationHintsAllowed: true,
followers: followers.map((follower) => follower.id),
reason: "all-selected-followers-explicit-legacy-manual",
};
}
const reason = rows.some((row) => row.reason === "retired-pac-identity-invalid")
? "retired-pac-identity-invalid"
: rows.some((row) => row.reason === "legacy-conflicts-with-pac")
? "legacy-conflicts-with-pac"
: rows.some((row) => row.kind === "unknown")
? "authority-not-declared"
: "authority-mixed";
return {
kind: "unknown",
mutationHintsAllowed: false,
followers: followers.map((follower) => follower.id),
reason,
details: rows.map((row) => ({ follower: row.follower, kind: row.kind, reason: row.reason, authority: row.pacAuthority })),
};
}
export function branchFollowerMutationGuard(
registry: BranchFollowerRegistry,
options: ParsedOptions,
): Record<string, unknown> | null {
const deliveryMutation = options.action === "apply"
|| options.action === "run-once"
|| options.action === "job"
|| options.action === "gate"
|| (options.action === "debug-step" && options.debugStep === "state-write");
const retiredCleanup = options.action === "cleanup-state";
if (!deliveryMutation && !retiredCleanup) return null;
const authority = branchFollowerDeliveryAuthority(registry, options);
if (deliveryMutation && authority.kind === "legacy-manual" && options.scope === "legacy-cicd") return null;
if (retiredCleanup && authority.kind === "retired-readonly" && options.scope === "retired-maintenance") return null;
return {
ok: false,
action: options.action,
mutation: false,
mode: authority.kind === "retired-readonly"
? "retired-branch-follower-mutation-blocked"
: authority.kind === "legacy-manual"
? "legacy-branch-follower-scope-required"
: "branch-follower-delivery-authority-unknown",
reason: retiredCleanup
? "退役状态清理只允许通过 retired-maintenance scoped command;默认入口不修改控制器状态。"
: "该 branch-follower 已退役或 authority 无法唯一确认;CLI 不执行交付写操作。",
deliveryAuthority: authority,
next: branchFollowerReadOnlyNext(registry, options, authority),
valuesPrinted: false,
};
}
export function branchFollowerReadOnlyNext(
registry: BranchFollowerRegistry,
options: ParsedOptions,
authority = branchFollowerDeliveryAuthority(registry, options),
): Record<string, unknown> {
const suffix = options.followerId === null ? "" : ` --follower ${options.followerId}`;
const nodes = authority.kind === "retired-readonly"
? Array.from(new Set(authority.pacAuthorities.map((item) => item.consumer.node)))
: [];
const followerNext = {
status: `bun scripts/cli.ts cicd branch-follower status${suffix}`,
events: `bun scripts/cli.ts cicd branch-follower events${suffix}`,
logs: `bun scripts/cli.ts cicd branch-follower logs${suffix}`,
fixAutomaticDelivery: authority.kind === "unknown"
? pacAutomaticDeliveryFix(authorityFromUnknown(authority))
: { reference: "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界", mutation: false },
valuesPrinted: false,
};
if (nodes.length !== 1) return followerNext;
const pacNext = pacNodeReadOnlyNext(nodes[0] as string);
return {
...followerNext,
pacStatus: pacNext.status,
pacHistory: pacNext.history,
};
}
function selectedFollowers(registry: BranchFollowerRegistry, options: ParsedOptions): FollowerSpec[] {
const selected = options.followerId === null
? registry.followers
: registry.followers.filter((follower) => follower.id === options.followerId);
if (selected.length === 0) throw new Error(`unknown follower ${options.followerId ?? "-"}`);
return selected;
}
function resolveFollowerAuthority(follower: FollowerSpec): {
follower: string;
kind: "retired-readonly" | "legacy-manual" | "unknown";
reason: string;
pacAuthority: Extract<CicdDeliveryAuthority, { kind: "pac-pr-merge" }> | null;
} {
const pacAuthority = resolveCicdDeliveryAuthority({
node: follower.target.node,
lane: follower.target.lane,
sourceRepository: follower.source.repository,
sourceBranch: follower.source.branch,
});
if (follower.deliveryAuthority === "retired-readonly") {
return pacAuthority.kind === "pac-pr-merge"
? { follower: follower.id, kind: "retired-readonly", reason: "exact-pac-consumer", pacAuthority }
: { follower: follower.id, kind: "unknown", reason: "retired-pac-identity-invalid", pacAuthority: null };
}
if (follower.deliveryAuthority === "legacy-manual") {
return pacAuthority.kind === "pac-pr-merge"
? { follower: follower.id, kind: "unknown", reason: "legacy-conflicts-with-pac", pacAuthority }
: { follower: follower.id, kind: "legacy-manual", reason: "explicit-legacy-manual", pacAuthority: null };
}
return { follower: follower.id, kind: "unknown", reason: "authority-not-declared", pacAuthority: null };
}
function authorityFromUnknown(authority: Extract<BranchFollowerDeliveryAuthority, { kind: "unknown" }>): Extract<CicdDeliveryAuthority, { kind: "unknown" }> {
return {
kind: "unknown",
migrated: false,
mutationHintsAllowed: false,
consumer: null,
reason: "authority-identity-mismatch",
detail: authority.reason,
configRefs: ["config/cicd-branch-followers.yaml", "config/platform-infra/pipelines-as-code.yaml"],
valuesPrinted: false,
};
}