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 { 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 { return sentinelReadOnlyNextForSpec(state.spec, state.sentinelId, authority); } export function publishCurrentNext(state: SentinelCicdState): Record { 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 `, 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", }; }