fix: 收敛 PaC 自动交付提示

This commit is contained in:
Codex
2026-07-11 11:42:16 +02:00
parent b12dd85375
commit b607c3a996
55 changed files with 2953 additions and 1472 deletions
+2 -2
View File
@@ -202,7 +202,7 @@ export function nodeRuntimeBaseImageCommand(scoped: ReturnType<typeof parseNodeS
: "node-runtime-image-seed-failed",
next: scoped.dryRun
? { preload: `bun scripts/cli.ts hwlab nodes control-plane runtime-image preload --node ${scoped.node} --lane ${scoped.lane} --confirm` }
: { triggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${scoped.node} --lane ${scoped.lane} --confirm --rerun` },
: { status: `bun scripts/cli.ts hwlab nodes control-plane runtime-image status --node ${scoped.node} --lane ${scoped.lane}` },
};
}
@@ -416,7 +416,7 @@ export function nodeRuntimeApply(scoped: ReturnType<typeof parseNodeScopedDelega
next: scoped.dryRun
? { apply: `bun scripts/cli.ts hwlab nodes control-plane apply --node ${scoped.node} --lane ${scoped.lane} --confirm` }
: {
triggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${scoped.node} --lane ${scoped.lane} --confirm`,
status: `bun scripts/cli.ts hwlab nodes control-plane status --node ${scoped.node} --lane ${scoped.lane}`,
...(sshTcpPoolDiagnostics === null ? {} : { sshPoolStatus: `bun scripts/cli.ts debug ssh-pool ${scoped.node}` }),
},
};
@@ -0,0 +1,92 @@
import {
decideCicdDeliveryMutation,
gitRepositoryIdentity,
pacAutomaticDeliveryFix,
pacReadOnlyNext,
resolveCicdDeliveryAuthority,
type CicdDeliveryAuthority,
type CicdDeliveryMutationDecision,
} from "../cicd-delivery-authority";
import { hwlabRuntimeLaneConfigPath, type HwlabRuntimeLaneSpec } from "../hwlab-node-lanes";
export interface HwlabNodeDeliveryTarget {
readonly node: string;
readonly lane: string;
readonly spec: HwlabRuntimeLaneSpec;
}
export function hwlabNodeDeliveryAuthority(target: HwlabNodeDeliveryTarget): CicdDeliveryAuthority {
return resolveCicdDeliveryAuthority({
node: target.node,
lane: target.lane,
sourceRepository: gitRepositoryIdentity(target.spec.gitUrl),
sourceBranch: target.spec.sourceBranch,
declaredSourceAuthorityMode: target.spec.sourceAuthority?.mode ?? null,
declaredConfigRef: hwlabRuntimeLaneConfigPath(),
});
}
export function hwlabNodeDeliveryMutationDecision(
target: HwlabNodeDeliveryTarget,
command: string,
action: string,
): CicdDeliveryMutationDecision {
return decideCicdDeliveryMutation(hwlabNodeDeliveryAuthority(target), {
command,
statusCommand: `bun scripts/cli.ts hwlab nodes control-plane status --node ${target.node} --lane ${target.lane}`,
action,
node: target.node,
lane: target.lane,
});
}
export function hwlabNodeDeliveryNext(
target: HwlabNodeDeliveryTarget,
authority: CicdDeliveryAuthority,
runtimeStatusFull: string,
): Record<string, unknown> {
if (authority.kind === "pac-pr-merge") {
return {
runtimeStatusFull,
...pacReadOnlyNext(authority.consumer.node, authority.consumer.consumerId),
};
}
if (authority.kind === "unknown") {
return {
runtimeStatusFull,
fixAutomaticDelivery: pacAutomaticDeliveryFix(authority),
valuesPrinted: false,
};
}
return {
full: runtimeStatusFull,
plan: `bun scripts/cli.ts hwlab nodes control-plane plan --node ${target.node} --lane ${target.lane}`,
triggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${target.node} --lane ${target.lane} --confirm`,
gitMirrorFlush: `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${target.node} --lane ${target.lane} --confirm --wait`,
webProbe: `bun scripts/cli.ts web-probe run --node ${target.node} --lane ${target.lane}`,
};
}
export function hwlabNodePlanNext(
target: HwlabNodeDeliveryTarget,
authority: CicdDeliveryAuthority,
): Record<string, unknown> {
const status = `bun scripts/cli.ts hwlab nodes control-plane status --node ${target.node} --lane ${target.lane}`;
if (authority.kind === "pac-pr-merge") {
return pacReadOnlyNext(authority.consumer.node, authority.consumer.consumerId);
}
if (authority.kind === "unknown") {
return {
status,
history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${target.node} --limit 10`,
fixAutomaticDelivery: pacAutomaticDeliveryFix(authority),
valuesPrinted: false,
};
}
return {
status,
platformMaintenanceHelp: "bun scripts/cli.ts hwlab nodes control-plane platform-maintenance --help",
legacyCicdHelp: "bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help",
valuesPrinted: false,
};
}
+16 -1
View File
@@ -46,6 +46,7 @@ import { emitNodeRuntimeStatusWorkerEvent, isNodeRuntimeStatusWorker, readNodeRu
import { legacyHwlabNodeWebProbeUnsupported } from "../web-probe";
import { startNodeDelegatedJob } from "./web-probe";
import { assertKnownOptions } from "./web-probe-observe";
import { hwlabNodeDeliveryMutationDecision } from "./delivery-authority";
export { hwlabNodeHelp, hwlabNodeWebProbeHelp, hwlabNodeObservabilityHelp } from "../hwlab-node-help";
@@ -571,7 +572,10 @@ export async function runHwlabNodeCommand(_config: Config, args: string[]): Prom
if (args.length === 1 || args.includes("--help") || args.includes("-h") || args[1] === "help") return hwlabNodeObservabilityHelp();
return runNodeObservability(parseNodeObservabilityOptions(args.slice(1)));
}
if (args.includes("--help") || args.includes("-h")) return hwlabNodeHelp();
if (args.includes("--help") || args.includes("-h")) {
const scope = args.includes("legacy-cicd") ? "legacy-cicd" : args.includes("platform-maintenance") ? "platform-maintenance" : null;
return hwlabNodeHelp(scope);
}
if (domain === "control-plane" || domain === "git-mirror") {
return runNodeDelegatedDomain(_config, domain, args.slice(1));
}
@@ -614,6 +618,17 @@ export function parseNodeObservabilityOptions(args: string[]): NodeObservability
export async function runNodeDelegatedDomain(config: Config, domain: DelegatedNodeDomain, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
const scoped = parseNodeScopedDelegatedOptions(domain, args);
const guardedDeliveryAction = domain === "git-mirror" && (scoped.action === "sync" || scoped.action === "flush")
|| domain === "control-plane" && (scoped.action === "trigger-current" || scoped.action === "refresh" || scoped.action === "sync")
|| domain === "control-plane" && scoped.action === "source-workspace" && scoped.originalArgs[1] === "sync";
if (guardedDeliveryAction) {
const decision = hwlabNodeDeliveryMutationDecision(
scoped,
`hwlab nodes ${domain} ${scoped.action}${scoped.action === "source-workspace" ? " sync" : ""}`,
scoped.action === "source-workspace" ? "source-workspace-sync" : scoped.action,
);
if (!decision.allowed) return decision.result;
}
if (domain === "control-plane" && scoped.action === "public-exposure") {
return runNodePublicExposure({
action: "public-exposure",
+4 -8
View File
@@ -38,9 +38,11 @@ import { compactCommandResultRedacted, record, shellQuote } from "./utils";
import { readBootstrapAdminPasswordMaterial } from "./web-probe";
import { webProbeCredential } from "./web-probe-observe";
import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes";
import { hwlabNodeDeliveryAuthority, hwlabNodePlanNext } from "./delivery-authority";
export function nodeRuntimeControlPlanePlan(scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): Record<string, unknown> {
const activeExternalPostgres = hwlabRuntimeActiveExternalPostgres(scoped.spec);
const deliveryAuthority = hwlabNodeDeliveryAuthority(scoped);
return {
ok: true,
command: `hwlab nodes control-plane plan --node ${scoped.node} --lane ${scoped.lane}`,
@@ -48,6 +50,7 @@ export function nodeRuntimeControlPlanePlan(scoped: ReturnType<typeof parseNodeS
mutation: false,
node: scoped.node,
lane: scoped.lane,
deliveryAuthority,
expected: nodeRuntimeExpected(scoped.spec),
checks: {
nodeScopedTargetConfigured: true,
@@ -58,14 +61,7 @@ export function nodeRuntimeControlPlanePlan(scoped: ReturnType<typeof parseNodeS
localPostgresExpectedAbsent: nodeRuntimeLocalPostgresExpectedAbsent(scoped.spec),
publicExposureDeclared: scoped.spec.publicExposure !== null,
},
next: {
infraStatus: `bun scripts/cli.ts hwlab nodes control-plane infra status --node ${scoped.node} --lane ${scoped.lane}`,
status: `bun scripts/cli.ts hwlab nodes control-plane status --node ${scoped.node} --lane ${scoped.lane}`,
platformDbStatus: activeExternalPostgres === undefined
? null
: `bun scripts/cli.ts platform-db postgres status --config ${activeExternalPostgres.configRef}`,
publicExposure: scoped.spec.publicExposure === null ? null : `bun scripts/cli.ts hwlab nodes control-plane public-exposure --node ${scoped.node} --lane ${scoped.lane} --confirm`,
},
next: hwlabNodePlanNext(scoped, deliveryAuthority),
};
}
@@ -525,7 +525,6 @@ export function runNodePublicExposure(options: NodePublicExposureOptions): Recor
valuesRedacted: true,
next: ok
? {
triggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${options.node} --lane ${options.lane} --confirm`,
status: `bun scripts/cli.ts hwlab nodes control-plane status --node ${options.node} --lane ${options.lane}`,
}
: { retry: `bun scripts/cli.ts hwlab nodes control-plane public-exposure --node ${options.node} --lane ${options.lane} --confirm` },
+47 -18
View File
@@ -42,6 +42,8 @@ import { readNodeRuntimeStatusPolicy, runNodeRuntimeStatusProbe, skippedNodeRunt
import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes";
import { nodeRuntimeDeployYamlOverlayShellScript } from "./deploy-overlay";
import { pipelineProvenanceAnnotationKeys } from "../pipeline-provenance";
import { hwlabNodeDeliveryAuthority, hwlabNodeDeliveryNext } from "./delivery-authority";
import type { CicdDeliveryAuthority } from "../cicd-delivery-authority";
const runtimeGitopsObservabilityNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-observability.mjs"), "utf8").trimEnd();
const runtimeGitopsPipelineGuardNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-pipeline-guard.mjs"), "utf8").trimEnd();
@@ -346,6 +348,9 @@ export function nodeRuntimeControlPlaneStatus(scoped: ReturnType<typeof parseNod
runtimeDegradedReason,
publicReady,
});
const deliveryAuthority = hwlabNodeDeliveryAuthority(scoped);
const visiblePipelineRunDiagnostics = nodeRuntimeVisiblePipelineRunDiagnostics(pipelineRunDiagnostics, deliveryAuthority);
const runtimeStatusFull = `${nodeRuntimeStatusCommand(scoped)} --full`;
const fullStatus = {
ok: controlPlaneReady && runtimeReady && argoReady && pipelineRunReady && publicReady && gitMirrorReady,
command: `hwlab nodes control-plane status --node ${scoped.node} --lane ${scoped.lane}`,
@@ -389,7 +394,7 @@ export function nodeRuntimeControlPlaneStatus(scoped: ReturnType<typeof parseNod
result: compactRuntimeCommand(argo),
},
pipelineRun: pipelineRunProbe,
pipelineRunDiagnostics,
pipelineRunDiagnostics: visiblePipelineRunDiagnostics,
runtime: {
ready: runtimeReady,
skipped: !statusPolicy.probes.runtime,
@@ -420,17 +425,32 @@ export function nodeRuntimeControlPlaneStatus(scoped: ReturnType<typeof parseNod
postgresObjects: postgresObjects === null ? null : compactRuntimeCommand(postgresObjects),
},
degradedReason,
next: {
plan: `bun scripts/cli.ts hwlab nodes control-plane plan --node ${scoped.node} --lane ${scoped.lane}`,
apply: `bun scripts/cli.ts hwlab nodes control-plane apply --node ${scoped.node} --lane ${scoped.lane} --confirm`,
triggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${scoped.node} --lane ${scoped.lane} --confirm`,
},
deliveryAuthority,
next: hwlabNodeDeliveryNext(scoped, deliveryAuthority, runtimeStatusFull),
};
const summary = summarizeNodeRuntimeControlPlaneStatus(fullStatus, scoped);
const summary = summarizeNodeRuntimeControlPlaneStatus(fullStatus, scoped, deliveryAuthority);
if (scoped.originalArgs.includes("--full") || scoped.originalArgs.includes("--raw")) return { ...fullStatus, summary };
return summary;
}
export function nodeRuntimeVisiblePipelineRunDiagnostics(
diagnostics: Record<string, unknown> | null,
deliveryAuthority: CicdDeliveryAuthority,
): Record<string, unknown> | null {
if (deliveryAuthority.kind === "legacy-manual" || diagnostics === null) return diagnostics;
const failureSummary = record(diagnostics.failureSummary);
return {
...diagnostics,
failureSummary: Object.keys(failureSummary).length === 0
? diagnostics.failureSummary ?? null
: {
...failureSummary,
nextAction: "检查失败 TaskRun 与有界日志,修复 owning YAML、controller 或源码中的自动链缺陷,再以新的正常 GitHub PR merge 验收;不得人工 rerun。",
},
next: null,
};
}
export function parseNodeRuntimeWorkloadReadiness(text: string): Array<Record<string, unknown>> {
return text.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => {
const [ref = "", counts = ""] = line.split(/\t/u);
@@ -724,7 +744,11 @@ export function nodeRuntimePipelinePendingTaskRunSummaries(
});
}
export function summarizeNodeRuntimeControlPlaneStatus(status: Record<string, unknown>, scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): Record<string, unknown> {
export function summarizeNodeRuntimeControlPlaneStatus(
status: Record<string, unknown>,
scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>,
deliveryAuthority: CicdDeliveryAuthority = hwlabNodeDeliveryAuthority(scoped),
): Record<string, unknown> {
const pipelineRun = record(status.pipelineRun);
const pipelineRunDiagnostics = record(status.pipelineRunDiagnostics);
const argo = record(status.argo);
@@ -770,7 +794,7 @@ export function summarizeNodeRuntimeControlPlaneStatus(status: Record<string, un
pendingTaskRuns: pipelineRunDiagnostics.pendingTaskRuns ?? [],
unscheduledPods: pipelineRunDiagnostics.unscheduledPods ?? [],
schedulingMessages: pipelineRunDiagnostics.schedulingMessages ?? [],
next: pipelineRunDiagnostics.next ?? null,
next: deliveryAuthority.kind === "legacy-manual" ? pipelineRunDiagnostics.next ?? null : null,
},
},
argo: {
@@ -842,14 +866,9 @@ export function summarizeNodeRuntimeControlPlaneStatus(status: Record<string, un
flushNeeded: gitMirrorCompact.flushNeeded === true,
githubInSync: gitMirrorCompact.githubInSync === true,
},
nextAction: nodeRuntimeStatusNextAction(status, scoped),
next: {
full: `${nodeRuntimeStatusCommand(scoped)} --full`,
plan: `bun scripts/cli.ts hwlab nodes control-plane plan --node ${scoped.node} --lane ${scoped.lane}`,
triggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${scoped.node} --lane ${scoped.lane} --confirm`,
gitMirrorFlush: `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${scoped.node} --lane ${scoped.lane} --confirm --wait`,
webProbe: `bun scripts/cli.ts web-probe run --node ${scoped.node} --lane ${scoped.lane}`,
},
deliveryAuthority,
nextAction: nodeRuntimeStatusNextAction(status, scoped, deliveryAuthority),
next: hwlabNodeDeliveryNext(scoped, deliveryAuthority, `${nodeRuntimeStatusCommand(scoped)} --full`),
};
}
@@ -967,8 +986,18 @@ function argoEventRows(text: string): Record<string, unknown>[] {
}).filter((item): item is Record<string, unknown> => item !== null);
}
export function nodeRuntimeStatusNextAction(status: Record<string, unknown>, scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): string {
export function nodeRuntimeStatusNextAction(
status: Record<string, unknown>,
scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>,
deliveryAuthority: CicdDeliveryAuthority = hwlabNodeDeliveryAuthority(scoped),
): string {
const reason = typeof status.degradedReason === "string" ? status.degradedReason : null;
if (deliveryAuthority.kind === "pac-pr-merge") {
return reason === null
? `${nodeRuntimeStatusCommand(scoped)} --full`
: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${deliveryAuthority.consumer.node} --consumer ${deliveryAuthority.consumer.consumerId}`;
}
if (deliveryAuthority.kind === "unknown") return `${nodeRuntimeStatusCommand(scoped)} --full`;
if (reason === null) return `${nodeRuntimeStatusCommand(scoped)} --full`;
if (reason === "control-plane-not-ready" || reason === "runtime-namespace-missing") {
return `bun scripts/cli.ts hwlab nodes control-plane apply --node ${scoped.node} --lane ${scoped.lane} --confirm`;
+6 -2
View File
@@ -197,7 +197,6 @@ export function sshTcpPoolDiagnosticsFromCommand(spec: HwlabRuntimeLaneSpec, res
poolStatus: `bun scripts/cli.ts debug ssh-pool ${spec.nodeId}`,
retrySmoke: `trans ${spec.nodeId} argv true`,
retryApply: `bun scripts/cli.ts hwlab nodes control-plane apply --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
retryTriggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
},
};
}
@@ -210,7 +209,12 @@ export function nodeRuntimeUnsupportedAction(scoped: ReturnType<typeof parseNode
lane: scoped.lane,
mutation: false,
degradedReason: "unsupported-node-scoped-runtime-action",
message: "node-scoped runtime currently supports plan/status/apply/refresh/sync/trigger-current/cleanup-runs/cleanup-released-pvs/cleanup-legacy-docker-images/cleanup-legacy-docker-registry-volume/runtime-image/runtime-migration/source-workspace",
message: "node-scoped runtime default help exposes read-only plan/status; delivery mutations are available only from explicit legacy-cicd help, and platform maintenance from platform-maintenance help",
next: {
status: `bun scripts/cli.ts hwlab nodes control-plane status --node ${scoped.node} --lane ${scoped.lane}`,
legacyHelp: "bun scripts/cli.ts hwlab nodes help legacy-cicd",
maintenanceHelp: "bun scripts/cli.ts hwlab nodes help platform-maintenance",
},
expected: nodeRuntimeExpected(scoped.spec),
};
}
@@ -0,0 +1,207 @@
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",
};
}
+15 -2
View File
@@ -37,6 +37,7 @@ import { parseJsonObject, record, shellQuote, statusText } from "./utils";
import { webObserveTable } from "./web-observe-render";
import { nodeRuntimeGitMirrorTarget, printNodeRuntimeTriggerProgress, sleepSync } from "./web-probe";
import { webObserveShort, webObserveText } from "./web-probe-observe";
import { hwlabNodeDeliveryAuthority, hwlabNodeDeliveryNext } from "./delivery-authority";
type NodeScopedDelegatedOptions = ReturnType<typeof parseNodeScopedDelegatedOptions>;
export type NodeRuntimeGitMirrorRunOptions = {
@@ -61,8 +62,20 @@ export function nodeRuntimeExternalPostgresSecretRows(secrets: Record<string, un
}
export function nodeRuntimeGitMirrorStatus(scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): Record<string, unknown> {
if (scoped.spec.sourceAuthority?.mode === "giteaSnapshot") return nodeRuntimeGiteaSourceStatus(scoped);
return nodeRuntimeLegacyGitMirrorStatus(scoped);
const result = scoped.spec.sourceAuthority?.mode === "giteaSnapshot"
? nodeRuntimeGiteaSourceStatus(scoped)
: nodeRuntimeLegacyGitMirrorStatus(scoped);
const deliveryAuthority = hwlabNodeDeliveryAuthority(scoped);
if (deliveryAuthority.kind === "legacy-manual") return { ...result, deliveryAuthority };
return {
...result,
deliveryAuthority,
next: hwlabNodeDeliveryNext(
scoped,
deliveryAuthority,
`bun scripts/cli.ts hwlab nodes control-plane status --node ${scoped.node} --lane ${scoped.lane} --full`,
),
};
}
function nodeRuntimeGiteaSourceStatus(scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): Record<string, unknown> {
+24 -5
View File
@@ -52,6 +52,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
&& sentinelActionRaw !== "image"
&& sentinelActionRaw !== "control-plane"
&& sentinelActionRaw !== "publish-current"
&& sentinelActionRaw !== "pac-publish-current"
&& sentinelActionRaw !== "validate"
&& sentinelActionRaw !== "maintenance"
&& sentinelActionRaw !== "dashboard"
@@ -60,7 +61,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
&& sentinelActionRaw !== "inspect-url"
&& sentinelActionRaw !== "inspect-id"
) {
throw new Error("web-probe sentinel usage: sentinel plan|status|image|control-plane|publish-current|validate|maintenance|dashboard|error|report|inspect-url|inspect-id --node NODE --lane vNN [--dry-run|--confirm]");
throw new Error("web-probe sentinel usage: sentinel plan|status|image status|control-plane status|validate|maintenance|dashboard|error|report|inspect-url|inspect-id --node NODE --lane vNN; migrated delivery is triggered only by GitHub PR merge");
}
assertKnownOptions(args, new Set([
"--node",
@@ -131,19 +132,37 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
sentinel = { kind: "config", action: sentinelActionRaw, node, lane, sentinelId, dryRun };
} else if (sentinelActionRaw === "image") {
const imageAction = args[1];
if (imageAction !== "status" && imageAction !== "build") throw new Error("web-probe sentinel image usage: image status|build --node NODE --lane vNN [--dry-run|--confirm]");
if (imageAction !== "status" && imageAction !== "build") throw new Error("web-probe sentinel image usage: image status --node NODE --lane vNN; migrated image delivery is triggered only by GitHub PR merge");
sentinel = { kind: "image", action: imageAction, node, lane, sentinelId, dryRun: imageAction === "build" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds, sourceOverride };
} else if (sentinelActionRaw === "control-plane") {
const controlPlaneAction = args[1];
if (controlPlaneAction !== "plan" && controlPlaneAction !== "apply" && controlPlaneAction !== "status" && controlPlaneAction !== "trigger-current") {
throw new Error("web-probe sentinel control-plane usage: control-plane plan|apply|status|trigger-current --node NODE --lane vNN [--dry-run|--confirm]");
throw new Error("web-probe sentinel control-plane usage: control-plane plan|status --node NODE --lane vNN; delivery writes are unavailable for migrated PaC consumers");
}
sentinel = { kind: "control-plane", action: controlPlaneAction, node, lane, sentinelId, dryRun: controlPlaneAction === "apply" || controlPlaneAction === "trigger-current" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds, rerun: args.includes("--rerun"), sourceOverride };
} else if (sentinelActionRaw === "publish-current") {
} else if (sentinelActionRaw === "publish-current" || sentinelActionRaw === "pac-publish-current") {
const manualRecovery = args.includes("--manual-recovery") || args.includes("--recovery");
const reason = optionValue(args, "--reason") ?? null;
if (manualRecovery && (reason === null || reason.trim().length < 8)) throw new Error("web-probe sentinel publish-current manual recovery requires --reason with at least 8 characters");
sentinel = { kind: "publish", action: "publish-current", node, lane, sentinelId, dryRun: dryRun || !confirm, confirm, wait: args.includes("--wait"), timeoutSeconds, rerun: args.includes("--rerun"), manualRecovery, reason, sourceOverride };
if (sentinelActionRaw === "pac-publish-current" && (manualRecovery || args.includes("--rerun"))) {
throw new Error("web-probe sentinel pac-publish-current does not accept recovery or rerun options");
}
sentinel = {
kind: "publish",
action: "publish-current",
node,
lane,
sentinelId,
dryRun: dryRun || !confirm,
confirm,
wait: args.includes("--wait"),
timeoutSeconds,
rerun: args.includes("--rerun"),
manualRecovery,
reason,
internalExecution: sentinelActionRaw === "pac-publish-current" ? "pac-controller" : null,
sourceOverride,
};
} else if (sentinelActionRaw === "maintenance") {
const maintenanceAction = args[1];
if (maintenanceAction !== "status" && maintenanceAction !== "start" && maintenanceAction !== "stop") {