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

208 lines
9.3 KiB
TypeScript

import {
PAC_AUTOMATIC_DELIVERY_REFERENCE,
decideCicdDeliveryMutation,
readCicdDeliveryAuthorityCatalog,
resolveCicdDeliveryAuthority,
resolveCicdDeliveryAuthorityFromCatalog,
type CicdDeliveryAuthority,
type CicdDeliveryAuthorityCatalog,
type CicdDeliveryMutationDecision,
} from "../cicd-delivery-authority";
import { authorizeSentinelPacInternalPublish } from "../sentinel-pac-execution-context";
import {
record,
rendered,
sentinelCliSuffix,
text,
type SentinelCicdState,
type WebProbeSentinelOptions,
type WebProbeSentinelSourceAuthority,
} from "../hwlab-node-web-sentinel-cicd-shared";
import { hwlabRuntimeLaneConfigPath, type HwlabRuntimeLaneSpec } from "../hwlab-node-lanes";
import type { RenderedCliResult } from "../output";
export function webProbeSentinelDeliveryAuthority(spec: HwlabRuntimeLaneSpec): CicdDeliveryAuthority {
try {
return webProbeSentinelDeliveryAuthorityFromCatalog(spec, readCicdDeliveryAuthorityCatalog());
} catch {
return resolveCicdDeliveryAuthority({
consumerId: sentinelPacConsumerId(spec),
node: spec.nodeId,
lane: spec.lane,
});
}
}
export function webProbeSentinelDeliveryAuthorityFromCatalog(
spec: HwlabRuntimeLaneSpec,
catalog: CicdDeliveryAuthorityCatalog,
): CicdDeliveryAuthority {
const consumerId = sentinelPacConsumerId(spec);
const exactConsumer = resolveCicdDeliveryAuthorityFromCatalog(catalog, {
consumerId,
node: spec.nodeId,
lane: spec.lane,
});
if (exactConsumer.kind === "pac-pr-merge") return exactConsumer;
const stableSentinelIdentity = spec.sourceAuthority?.mode === "giteaSnapshot"
|| catalog.consumers.some((consumer) => consumer.consumerId.toLowerCase() === consumerId.toLowerCase())
|| catalog.consumers.some((consumer) => consumer.consumerId.toLowerCase().startsWith("sentinel-")
&& consumer.node.toLowerCase() === spec.nodeId.toLowerCase()
&& consumer.lane.toLowerCase() === spec.lane.toLowerCase());
if (stableSentinelIdentity) return exactConsumer;
return resolveCicdDeliveryAuthorityFromCatalog(catalog, {
declaredSourceAuthorityMode: spec.sourceAuthority?.mode ?? null,
declaredConfigRef: hwlabRuntimeLaneConfigPath(),
});
}
export function sentinelPacConsumerId(spec: HwlabRuntimeLaneSpec): string {
return `sentinel-${spec.nodeId.toLowerCase()}-${spec.lane}`;
}
export function webProbeSentinelDeliveryMutationDecision(
spec: HwlabRuntimeLaneSpec,
command: string,
action: string,
): CicdDeliveryMutationDecision {
return decideCicdDeliveryMutation(webProbeSentinelDeliveryAuthority(spec), {
command,
statusCommand: `bun scripts/cli.ts web-probe sentinel control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
action,
node: spec.nodeId,
lane: spec.lane,
});
}
export function guardedSentinelDeliveryAction(options: WebProbeSentinelOptions): { command: string; action: string } | null {
if (options.kind === "image" && options.action === "build") {
return { command: "web-probe sentinel image build", action: "sentinel-image-build" };
}
if (options.kind === "control-plane" && options.action === "trigger-current") {
return { command: "web-probe sentinel control-plane trigger-current", action: "sentinel-control-plane-trigger-current" };
}
if (options.kind === "publish" && (options.internalExecution === "pac-controller" || options.manualRecovery)) {
return { command: "web-probe sentinel publish-current", action: "sentinel-publish-current" };
}
return null;
}
export function renderBlockedSentinelDeliveryMutation(
spec: HwlabRuntimeLaneSpec,
options: WebProbeSentinelOptions,
command: string,
action: string,
): RenderedCliResult | null {
const decision = webProbeSentinelDeliveryMutationDecision(spec, command, action);
if (decision.allowed) return null;
const executionAuthorization = options.kind === "publish" && options.internalExecution === "pac-controller"
? authorizeSentinelPacInternalPublish(options, decision.authority)
: null;
const result = {
...decision.result,
...(executionAuthorization === null ? {} : {
mode: executionAuthorization.mode,
reason: executionAuthorization.reason,
capability: executionAuthorization.capability,
}),
executionAuthorization,
next: sentinelReadOnlyNextForSpec(spec, options.sentinelId, decision.authority),
sentinelId: options.sentinelId,
valuesRedacted: true,
};
const next = record(result.next);
const fixReference = typeof next.fixAutomaticDelivery === "string"
? next.fixAutomaticDelivery
: record(next.fixAutomaticDelivery).reference ?? PAC_AUTOMATIC_DELIVERY_REFERENCE;
return Object.assign(
rendered(false, command, [
"WEB SENTINEL DELIVERY BLOCKED",
`mode=${text(result.mode)} mutation=false authority=${text(record(result.deliveryAuthority).kind)}`,
`status=${text(next.status)}`,
`history=${text(next.history ?? next.pacHistory)}`,
`fix=${text(fixReference)}`,
"GitHub PR merge 是唯一正式交付触发;修复自动链后用新的正常合并事件验收。",
].join("\n")),
result,
);
}
export function sentinelReadOnlyNextForSpec(
spec: HwlabRuntimeLaneSpec,
sentinelId: string | null,
authority: CicdDeliveryAuthority,
): Record<string, string> {
const suffix = sentinelId === null ? "" : ` --sentinel ${sentinelId}`;
const controlPlaneStatus = `bun scripts/cli.ts web-probe sentinel control-plane status --node ${spec.nodeId} --lane ${spec.lane}${suffix}`;
const base = {
status: controlPlaneStatus,
controlPlaneStatus,
fixAutomaticDelivery: PAC_AUTOMATIC_DELIVERY_REFERENCE,
};
if (authority.kind !== "pac-pr-merge") return base;
const pacHistory = `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${authority.consumer.node} --consumer ${authority.consumer.consumerId} --limit 10`;
return {
...base,
pacStatus: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${authority.consumer.node} --consumer ${authority.consumer.consumerId}`,
pacHistory,
history: pacHistory,
};
}
export function sentinelReadOnlyNext(
state: SentinelCicdState,
authority: CicdDeliveryAuthority = webProbeSentinelDeliveryAuthority(state.spec),
): Record<string, string> {
return sentinelReadOnlyNextForSpec(state.spec, state.sentinelId, authority);
}
export function publishCurrentNext(state: SentinelCicdState): Record<string, string> {
const deliveryAuthority = webProbeSentinelDeliveryAuthority(state.spec);
if (deliveryAuthority.kind !== "legacy-manual") return sentinelReadOnlyNext(state, deliveryAuthority);
const node = state.spec.nodeId;
const lane = state.spec.lane;
const suffix = sentinelCliSuffix(state);
const consumer = sentinelPacConsumerId(state.spec);
const sourceCommit = state.sourceHead.commit === null ? "" : ` --source-commit ${state.sourceHead.commit}`;
return {
pacCloseout: `bun scripts/cli.ts platform-infra pipelines-as-code closeout --target ${node} --consumer ${consumer}${sourceCommit} --wait`,
pacStatus: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${node} --consumer ${consumer}`,
pacHistory: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${node} --consumer ${consumer} --limit 10`,
manualRecovery: `bun scripts/cli.ts web-probe sentinel publish-current --node ${node} --lane ${lane}${suffix} --confirm --wait --manual-recovery --reason <reason>`,
controlPlaneStatus: `bun scripts/cli.ts web-probe sentinel control-plane status --node ${node} --lane ${lane}${suffix}`,
dashboardVerify: `bun scripts/cli.ts web-probe sentinel dashboard verify --node ${node} --lane ${lane}${suffix}`,
gitMirrorStatus: `bun scripts/cli.ts hwlab nodes git-mirror status --node ${node} --lane ${lane}`,
gitMirrorSync: `bun scripts/cli.ts hwlab nodes git-mirror sync --node ${node} --lane ${lane} --confirm`,
gitMirrorFlush: `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${node} --lane ${lane} --confirm --wait`,
};
}
export function sentinelSourceResolveMode(options: WebProbeSentinelOptions): "cached" | "sync" {
if (options.kind === "image" && options.action === "build" && options.confirm && options.wait) return "sync";
if (options.kind === "control-plane" && options.action === "trigger-current" && options.confirm && options.wait) return "sync";
if (options.kind === "publish" && options.confirm && options.wait) return "sync";
return "cached";
}
export interface SentinelSourceOverride {
readonly commit: string;
readonly stageRef?: string | null;
readonly mirrorCommit?: string | null;
readonly sourceAuthority: WebProbeSentinelSourceAuthority;
}
export function sentinelSourceOverrideFromOptions(options: WebProbeSentinelOptions): SentinelSourceOverride | null {
if (options.kind !== "image" && options.kind !== "control-plane" && options.kind !== "publish") return null;
const override = options.sourceOverride;
if (override.sourceCommit === null && override.sourceStageRef === null && override.sourceMirrorCommit === null && override.sourceAuthority === null) return null;
if (override.sourceCommit === null) throw new Error("--source-commit is required when overriding web-probe sentinel source");
return {
commit: override.sourceCommit,
stageRef: override.sourceStageRef,
mirrorCommit: override.sourceMirrorCommit,
sourceAuthority: override.sourceAuthority ?? "git-mirror-snapshot",
};
}