fix: 收敛 PaC 自动交付提示
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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` },
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user