// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. cd-trigger module for scripts/src/hwlab-g14.ts. // Moved mechanically from scripts/src/hwlab-g14.ts:9508-10162 for #903. import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { createHash, randomBytes } from "node:crypto"; import { repoRoot, rootPath, type Config } from "../config"; import { runCommand } from "../command"; import { cancelJob, listJobs, readJob, startJob } from "../jobs"; import { hwlabRequiredNoProxyEntries, hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "../hwlab-node-lanes"; import type { CommandJsonResult, G14MonitorOptions, OpenPullRequest } from "./types"; import { runRuntimeLaneControlPlane, runV02ControlPlane, runtimeLaneControlPlaneStatus, v02ControlPlaneStatus } from "./control-plane"; import { runG14GitMirror, runGitMirrorStatus } from "./git-mirror"; import { activeV02PipelineRuns, commentRuntimeLanePullRequest, commentV02PullRequest, durationSeconds, reportRuntimeLaneAutomationIssue, runtimeLaneCdFailed, runtimeLaneCdPassed, sourceCommitFromMergeResult, summarizeRuntimeLaneCdStatus, summarizeV02CdStatus, v02CdFailed, v02CdPassed, v02PipelineSucceeded, waitForV02Head } from "./pr-monitor"; import { commandData, compactCommandResult, isCommandSuccess, nested, printEvent, printRuntimeLanePrMonitorProgress, printV02PrMonitorProgress, record, sleep, stringOrNull } from "./remote"; import { resolveRuntimeLaneHead, runtimeLanePipelineRunName, v02PipelineRunName } from "./source"; export function triggerV02Current(timeoutSeconds: number): Record { return runV02ControlPlane({ action: "trigger-current", lane: "v02", dryRun: false, confirm: true, wait: true, allowLiveDbRead: false, timeoutSeconds, minAgeMinutes: 30, limit: 20, }); } export function flushV02GitMirrorIfNeeded(status: Record, timeoutSeconds: number): Record { const pendingFlush = nested(status, ["gitMirror", "summary", "pendingFlush"]); if (pendingFlush !== true) return { ok: true, skipped: true, reason: "git-mirror-already-flushed" }; return runG14GitMirror({ action: "flush", lane: "v02", confirm: true, dryRun: false, wait: true, timeoutSeconds, }); } export function triggerRuntimeLaneCurrent(spec: HwlabRuntimeLaneSpec, timeoutSeconds: number): Record { return runRuntimeLaneControlPlane(spec, { action: "trigger-current", lane: spec.lane, dryRun: false, confirm: true, wait: true, allowLiveDbRead: false, timeoutSeconds, minAgeMinutes: 30, limit: 20, history: false, }); } export function runtimeLaneStatusWithGitMirror(spec: HwlabRuntimeLaneSpec, sourceCommit: string): Record { const status = runtimeLaneControlPlaneStatus(spec, { sourceCommit, mode: "source-commit" }); const gitMirrorStatus = runGitMirrorStatus(spec.lane); return { ...status, gitMirror: record(gitMirrorStatus.cache), gitMirrorStatus, }; } export function runtimeLaneGitopsSynced(spec: HwlabRuntimeLaneSpec, status: Record): boolean { const summary = record(nested(status, ["gitMirror", "summary"])); const inSync = spec.lane === "v03" ? summary.v03GitopsInSync : summary.githubInSync; return summary.pendingFlush !== true && inSync === true; } export function runtimeLaneCdPassedWithMirror(spec: HwlabRuntimeLaneSpec, status: Record): boolean { return runtimeLaneCdPassed(status) && runtimeLaneGitopsSynced(spec, status); } export function flushRuntimeLaneGitMirrorIfNeeded(spec: HwlabRuntimeLaneSpec, status: Record, timeoutSeconds: number): Record { if (runtimeLaneGitopsSynced(spec, status)) return { ok: true, skipped: true, reason: `${spec.lane}-git-mirror-already-flushed` }; return runG14GitMirror({ action: "flush", lane: spec.lane, confirm: true, dryRun: false, wait: true, timeoutSeconds, }); } export function pullRequestFromMergeCommand(merge: CommandJsonResult): Record { const data = commandData(merge); const direct = record(data.pullRequest); if (Object.keys(direct).length > 0) return direct; return record(nested(data, ["details", "details", "pullRequest"])); } export function mergeCommandRaceState(merge: CommandJsonResult): "merged" | "closed" | null { const pullRequest = pullRequestFromMergeCommand(merge); if (pullRequest.merged === true || pullRequest.stateDetail === "merged") return "merged"; if (pullRequest.closed === true || pullRequest.state === "closed") return "closed"; return null; } export async function waitForRuntimeLaneHead(spec: HwlabRuntimeLaneSpec, expectedCommit: string | null, timeoutSeconds: number): Promise> { const started = Date.now(); let head: string | null = null; while (Date.now() - started < timeoutSeconds * 1000) { head = resolveRuntimeLaneHead(spec).sourceCommit; if (expectedCommit === null) { if (head !== null) return { ok: true, sourceCommit: head, expectedCommit, matched: true, waitedSeconds: Math.round((Date.now() - started) / 1000) }; } else if (head === expectedCommit) { return { ok: true, sourceCommit: head, expectedCommit, matched: true, waitedSeconds: Math.round((Date.now() - started) / 1000) }; } printEvent(`${spec.lane}.source.wait`, { lane: spec.lane, expectedCommit, head, waitedSeconds: Math.round((Date.now() - started) / 1000) }); await sleep(5_000); } return { ok: false, sourceCommit: head, expectedCommit, matched: false, waitedSeconds: Math.round((Date.now() - started) / 1000), degradedReason: `${spec.lane}-source-head-not-aligned-after-merge` }; } export async function waitForV02Cd(sourceCommit: string, timeoutSeconds: number): Promise> { const started = Date.now(); const startedAt = new Date(started).toISOString(); const pipelineRun = v02PipelineRunName(sourceCommit); let lastStatus: Record = {}; let firstPassedAt: string | null = null; while (Date.now() - started < timeoutSeconds * 1000) { lastStatus = v02ControlPlaneStatus({ sourceCommit, mode: "source-commit" }); const summary = summarizeV02CdStatus(lastStatus); printEvent("v02.cd.status", { sourceCommit, pipelineRun, summary }); printV02PrMonitorProgress({ stage: "cd-status", status: "running", sourceCommit, pipelineRun, targetValidationState: summary.targetValidationState ?? null, pipelineStatus: summary.pipelineStatus ?? null, pendingFlush: summary.pendingFlush ?? null }); if (v02PipelineSucceeded(lastStatus) && summary.pendingFlush === true) { const flush = flushV02GitMirrorIfNeeded(lastStatus, Math.min(timeoutSeconds, 600)); if (record(flush).ok !== true) { return { ok: false, phase: "git-mirror-flush", startedAt, finishedAt: new Date().toISOString(), sourceCommit, pipelineRun, status: summary, rawStatus: lastStatus, flush, }; } const finalStatus = v02ControlPlaneStatus({ sourceCommit, mode: "source-commit" }); const finalSummary = summarizeV02CdStatus(finalStatus); if (v02CdPassed(finalStatus)) { printV02PrMonitorProgress({ stage: "git-mirror-flush", status: "succeeded", sourceCommit, pipelineRun, pendingFlush: finalSummary.pendingFlush ?? null }); return { ok: true, phase: "cd-passed", startedAt, finishedAt: new Date().toISOString(), sourceCommit, pipelineRun, status: finalSummary, rawStatus: finalStatus, flush, }; } lastStatus = finalStatus; printEvent("v02.cd.after-flush", { sourceCommit, pipelineRun, flushOk: record(flush).ok, summary: finalSummary }); printV02PrMonitorProgress({ stage: "git-mirror-flush", status: record(flush).ok === true ? "succeeded" : "failed", sourceCommit, pipelineRun, pendingFlush: finalSummary.pendingFlush ?? null }); } if (v02CdPassed(lastStatus)) { firstPassedAt ??= new Date().toISOString(); const flush = flushV02GitMirrorIfNeeded(lastStatus, Math.min(timeoutSeconds, 600)); const finalStatus = v02ControlPlaneStatus({ sourceCommit, mode: "source-commit" }); printV02PrMonitorProgress({ stage: "cd-status", status: v02CdPassed(finalStatus) ? "succeeded" : "failed", sourceCommit, pipelineRun, targetValidationState: nested(finalStatus, ["targetValidation", "state"]) ?? null }); return { ok: v02CdPassed(finalStatus), phase: v02CdPassed(finalStatus) ? "cd-passed" : "git-mirror-flush", startedAt, finishedAt: new Date().toISOString(), sourceCommit, pipelineRun, status: summarizeV02CdStatus(finalStatus), rawStatus: finalStatus, flush, }; } if (v02CdFailed(lastStatus)) { printV02PrMonitorProgress({ stage: "cd-status", status: "failed", sourceCommit, pipelineRun, pipelineStatus: summary.pipelineStatus ?? null, pipelineReason: summary.pipelineReason ?? null }); return { ok: false, phase: "cd-failed", startedAt, finishedAt: new Date().toISOString(), sourceCommit, pipelineRun, status: summary, rawStatus: lastStatus, }; } await sleep(30_000); } return { ok: false, phase: "timeout", startedAt, finishedAt: new Date().toISOString(), sourceCommit, pipelineRun, timeoutSeconds, firstPassedAt, status: summarizeV02CdStatus(lastStatus), rawStatus: lastStatus, }; } export async function waitForRuntimeLaneCd(spec: HwlabRuntimeLaneSpec, sourceCommit: string, timeoutSeconds: number): Promise> { const started = Date.now(); const startedAt = new Date(started).toISOString(); let pipelineRun = runtimeLanePipelineRunName(spec, sourceCommit); let lastStatus: Record = {}; let firstPassedAt: string | null = null; while (Date.now() - started < timeoutSeconds * 1000) { lastStatus = runtimeLaneStatusWithGitMirror(spec, sourceCommit); const summary = summarizeRuntimeLaneCdStatus(spec, lastStatus); pipelineRun = stringOrNull(summary.pipelineRun) ?? pipelineRun; printEvent(`${spec.lane}.cd.status`, { lane: spec.lane, sourceCommit, pipelineRun, summary }); printRuntimeLanePrMonitorProgress(spec, { stage: "cd-status", status: "running", sourceCommit, pipelineRun, pipelineStatus: summary.pipelineStatus ?? null, pipelineReason: summary.pipelineReason ?? null, argoSync: summary.argoSync ?? null, argoHealth: summary.argoHealth ?? null, publicProbesOk: summary.publicProbesOk ?? null, pendingFlush: summary.pendingFlush ?? null, }); if (runtimeLaneCdPassed(lastStatus)) { firstPassedAt ??= new Date().toISOString(); const flush = flushRuntimeLaneGitMirrorIfNeeded(spec, lastStatus, Math.min(timeoutSeconds, 600)); if (record(flush).ok !== true) { return { ok: false, phase: "git-mirror-flush", startedAt, finishedAt: new Date().toISOString(), sourceCommit, pipelineRun, status: summary, rawStatus: lastStatus, flush, }; } const finalStatus = runtimeLaneStatusWithGitMirror(spec, sourceCommit); const finalSummary = summarizeRuntimeLaneCdStatus(spec, finalStatus); printRuntimeLanePrMonitorProgress(spec, { stage: "git-mirror-flush", status: runtimeLaneCdPassedWithMirror(spec, finalStatus) ? "succeeded" : "failed", sourceCommit, pipelineRun, pendingFlush: finalSummary.pendingFlush ?? null, }); return { ok: runtimeLaneCdPassedWithMirror(spec, finalStatus), phase: runtimeLaneCdPassedWithMirror(spec, finalStatus) ? "cd-passed" : "git-mirror-flush", startedAt, finishedAt: new Date().toISOString(), sourceCommit, pipelineRun, status: finalSummary, rawStatus: finalStatus, flush, }; } if (runtimeLaneCdFailed(lastStatus)) { printRuntimeLanePrMonitorProgress(spec, { stage: "cd-status", status: "failed", sourceCommit, pipelineRun, pipelineStatus: summary.pipelineStatus ?? null, pipelineReason: summary.pipelineReason ?? null }); return { ok: false, phase: "cd-failed", startedAt, finishedAt: new Date().toISOString(), sourceCommit, pipelineRun, status: summary, rawStatus: lastStatus, }; } await sleep(30_000); } return { ok: false, phase: "timeout", startedAt, finishedAt: new Date().toISOString(), sourceCommit, pipelineRun, timeoutSeconds, firstPassedAt, status: summarizeRuntimeLaneCdStatus(spec, lastStatus), rawStatus: lastStatus, }; } export async function runV02PrAutoCd(pr: OpenPullRequest, preflight: Record, merge: CommandJsonResult, options: G14MonitorOptions, startedAt: string): Promise> { const mergeRaceState = isCommandSuccess(merge) ? null : mergeCommandRaceState(merge); if (!isCommandSuccess(merge) && mergeRaceState !== "merged") { printV02PrMonitorProgress({ stage: "merge", status: "failed", pr: pr.number, mergeRaceState }); const comment = commentV02PullRequest({ pr, phase: "merge", state: "merge-failed", startedAt, observedAt: new Date().toISOString(), elapsedSeconds: durationSeconds(startedAt, new Date().toISOString()), preflight, merge: compactCommandResult(merge), dryRun: options.dryRun, message: mergeRaceState === "closed" ? "PR preflight 曾通过,但自动合并前 PR 已被关闭且未确认 merged;本轮不会触发 CD。" : "PR preflight 已通过,但自动合并失败;需要处理 GitHub 返回的 merge blocker 后重试。", }); return { ok: mergeRaceState === "closed", phase: "merge", action: "merge-race-closed", pr, merge, comment }; } const expectedCommit = options.dryRun ? null : sourceCommitFromMergeResult(merge, pr.number); if (!options.dryRun && expectedCommit === null) { printV02PrMonitorProgress({ stage: "source-head", status: "failed", pr: pr.number, sourceCommit: null, pipelineRun: null, degradedReason: "merge-commit-unresolved" }); const comment = commentV02PullRequest({ pr, phase: "merge-commit", state: "cd-failed", startedAt, observedAt: new Date().toISOString(), elapsedSeconds: durationSeconds(startedAt, new Date().toISOString()), preflight, merge: commandData(merge), sourceCommit: null, pipelineRun: null, dryRun: false, message: "PR 已合并,但 UniDesk 未能从 GitHub merge/read 结果解析 merge commit;为避免触发错误 source head,本轮未启动 CD。", }); return { ok: false, phase: "merge-commit", pr, merge: commandData(merge), comment, degradedReason: "merge-commit-unresolved" }; } const headWait = options.dryRun ? { ok: true, sourceCommit: null, expectedCommit, matched: true } : await waitForV02Head(expectedCommit, 120); const sourceCommit = typeof record(headWait).sourceCommit === "string" ? String(record(headWait).sourceCommit) : null; const pipelineRun = sourceCommit === null ? null : v02PipelineRunName(sourceCommit); printV02PrMonitorProgress({ stage: "merge", status: "succeeded", pr: pr.number, sourceCommit, pipelineRun, mergeRaceState }); const startedComment = commentV02PullRequest({ pr, phase: options.dryRun ? "dry-run" : "cd-trigger", state: "cd-started", startedAt, observedAt: new Date().toISOString(), elapsedSeconds: durationSeconds(startedAt, new Date().toISOString()), preflight, merge: commandData(merge), sourceCommit, pipelineRun, dryRun: options.dryRun, message: options.dryRun ? "dry-run:PR 已达到自动合并条件;本轮不会写 GitHub merge 或 CD。" : mergeRaceState === "merged" ? "PR 在本轮 merge 前已被合并;worker 继续对齐 merge commit 并驱动 v0.2 CD。后续成功、失败或超时会继续在本 PR 下回复。" : "PR 已自动合并,v0.2 CD 准备开始;其他 commit 的运行中 PipelineRun 不会阻塞本轮 CI,旧 run 若发现 source head 已推进会以 superseded/no-op 收口。后续成功、superseded、失败或超时会继续在本 PR 下回复。", }); if (!options.dryRun && record(startedComment).ok !== true) { return { ok: false, phase: "pr-comment", pr, sourceCommit, pipelineRun, comment: startedComment }; } if (options.dryRun) return { ok: true, action: "dry-run-merge", pr, merge: commandData(merge), comment: startedComment }; if (sourceCommit === null || record(headWait).ok !== true) { printV02PrMonitorProgress({ stage: "source-head", status: "failed", pr: pr.number, sourceCommit, pipelineRun, expectedCommit }); const comment = commentV02PullRequest({ pr, phase: "v02-source-head", state: "cd-timeout", startedAt, observedAt: new Date().toISOString(), elapsedSeconds: durationSeconds(startedAt, new Date().toISOString()), preflight, merge: commandData(merge), sourceCommit, pipelineRun, dryRun: false, message: "PR 已合并,但 G14 v0.2 CI/CD source repo 没有在等待窗口内对齐 merge commit;为避免触发旧 head,本轮未启动 CD。", }); return { ok: false, phase: "v02-head", pr, merge: commandData(merge), expectedCommit, headWait, comment: record(comment).ok === true ? comment : startedComment, degradedReason: "v02-head-unresolved-after-merge" }; } const before = v02ControlPlaneStatus({ sourceCommit, mode: "source-commit" }); const beforeSummary = summarizeV02CdStatus(before); const activeRuns = activeV02PipelineRuns(before); if (activeRuns.length > 0) { printEvent("v02.cd.active-runs-observed", { pr: pr.number, sourceCommit, pipelineRun, activeCount: activeRuns.length, activeRuns: activeRuns.slice(0, 5), latestOnlyPolicy: "do-not-wait-or-cancel-old-runs" }); } const trigger = v02CdPassed(before) ? { ok: true, skipped: true, reason: "source-commit-already-deployed" } : triggerV02Current(Math.min(options.timeoutSeconds, 600)); printEvent("v02.cd.trigger", { pr: pr.number, sourceCommit, pipelineRun, ok: record(trigger).ok, skipped: record(trigger).skipped ?? false, degradedReason: record(trigger).degradedReason ?? null }); printV02PrMonitorProgress({ stage: "cd-trigger", status: record(trigger).ok === true ? "succeeded" : "failed", pr: pr.number, sourceCommit, pipelineRun, activeCount: activeRuns.length, skipped: record(trigger).skipped ?? false, degradedReason: record(trigger).degradedReason ?? null }); if (record(trigger).ok !== true) { const comment = commentV02PullRequest({ pr, phase: "cd-trigger", state: "cd-failed", startedAt, observedAt: new Date().toISOString(), elapsedSeconds: durationSeconds(startedAt, new Date().toISOString()), preflight, merge: commandData(merge), sourceCommit, pipelineRun, cd: beforeSummary, dryRun: false, message: "PR 已合并,但 v0.2 CD 触发失败;需要查看 trigger 阶段的 degradedReason。", }); return { ok: false, phase: "cd-trigger", pr, sourceCommit, pipelineRun, trigger, comment }; } const cd = await waitForV02Cd(sourceCommit, options.timeoutSeconds); const cdStatus = record(cd.status); const flush = record(cd.flush); const cdOk = cd.ok === true; const cdPhase = String(cd.phase ?? ""); const cdSuperseded = cdOk && cdStatus.targetValidationState === "superseded"; const finalComment = commentV02PullRequest({ pr, phase: cdPhase, state: cdOk ? (cdSuperseded ? "cd-superseded" : "cd-succeeded") : cdPhase === "timeout" ? "cd-timeout" : "cd-failed", startedAt, observedAt: new Date().toISOString(), elapsedSeconds: durationSeconds(startedAt, new Date().toISOString()), preflight, merge: commandData(merge), sourceCommit, pipelineRun, cd: cdStatus, flush, dryRun: false, message: cdOk ? cdSuperseded ? "v0.2 自动 CD 已收口:本 merge head 的 PipelineRun 已完成,但 source head 已被后续提交推进,本轮按 latest-only 语义 superseded/no-op,未回写旧 GitOps revision。" : "v0.2 自动 CD 已完成:PipelineRun、Argo/runtime、公开探针和 Git mirror flush 收口均已检查。" : cdPhase === "timeout" ? "v0.2 自动 CD 超时未收口;本评论保留最后一次 targetValidation / PipelineRun / Git mirror 状态用于接续排障。" : "v0.2 自动 CD 失败;本评论保留失败阶段和最后一次状态用于接续排障。", }); return { ok: cdOk && record(finalComment).ok === true, action: cdOk ? (cdSuperseded ? "merged-and-superseded-v02" : "merged-and-rolled-v02") : "v02-cd-failed", phase: cdOk ? "cd-passed" : cdPhase, pr, sourceCommit, pipelineRun, trigger, cd, comment: finalComment, }; } export async function runRuntimeLanePrAutoCd(spec: HwlabRuntimeLaneSpec, pr: OpenPullRequest, preflight: Record, merge: CommandJsonResult, options: G14MonitorOptions, startedAt: string): Promise> { const mergeRaceState = isCommandSuccess(merge) ? null : mergeCommandRaceState(merge); if (!isCommandSuccess(merge) && mergeRaceState !== "merged") { printRuntimeLanePrMonitorProgress(spec, { stage: "merge", status: "failed", pr: pr.number, mergeRaceState }); const observedAt = new Date().toISOString(); const comment = commentRuntimeLanePullRequest(spec, { pr, phase: "merge", state: "merge-failed", startedAt, observedAt, elapsedSeconds: durationSeconds(startedAt, observedAt), preflight, merge: compactCommandResult(merge), dryRun: options.dryRun, message: mergeRaceState === "closed" ? `PR preflight 曾通过,但自动合并前 PR 已被关闭且未确认 merged;本轮不会触发 ${spec.version} CD。` : "PR preflight 已通过,但自动合并失败;需要处理 GitHub 返回的 merge blocker 后重试。", }); const issue = reportRuntimeLaneAutomationIssue({ spec, pr, state: "merge-failed", phase: "merge", startedAt, observedAt, preflight, details: { merge: compactCommandResult(merge), mergeRaceState }, message: "自动合并失败,v0.3 monitor 已暂停 CD 触发。", dryRun: options.dryRun, }); return { ok: mergeRaceState === "closed" && record(comment).ok === true && record(issue).ok === true, phase: "merge", action: "merge-race-closed", pr, merge, comment, issue }; } const expectedCommit = options.dryRun ? null : sourceCommitFromMergeResult(merge, pr.number); if (!options.dryRun && expectedCommit === null) { const observedAt = new Date().toISOString(); printRuntimeLanePrMonitorProgress(spec, { stage: "source-head", status: "failed", pr: pr.number, sourceCommit: null, pipelineRun: null, degradedReason: "merge-commit-unresolved" }); const comment = commentRuntimeLanePullRequest(spec, { pr, phase: "merge-commit", state: "cd-failed", startedAt, observedAt, elapsedSeconds: durationSeconds(startedAt, observedAt), preflight, merge: commandData(merge), sourceCommit: null, pipelineRun: null, dryRun: false, message: "PR 已合并,但 UniDesk 未能从 GitHub merge/read 结果解析 merge commit;为避免触发错误 source head,本轮未启动 CD。", }); const issue = reportRuntimeLaneAutomationIssue({ spec, pr, state: "cd-failed", phase: "merge-commit", startedAt, observedAt, preflight, details: { merge: commandData(merge) }, message: "PR 已合并但 merge commit 不可解析,v0.3 CD 未启动。", dryRun: false, }); return { ok: false, phase: "merge-commit", pr, merge: commandData(merge), comment, issue, degradedReason: "merge-commit-unresolved" }; } const headWait = options.dryRun ? { ok: true, sourceCommit: null, expectedCommit, matched: true } : await waitForRuntimeLaneHead(spec, expectedCommit, 120); const sourceCommit = typeof record(headWait).sourceCommit === "string" ? String(record(headWait).sourceCommit) : null; const pipelineRun = sourceCommit === null ? null : runtimeLanePipelineRunName(spec, sourceCommit); printRuntimeLanePrMonitorProgress(spec, { stage: "merge", status: "succeeded", pr: pr.number, sourceCommit, pipelineRun, mergeRaceState }); const startedComment = commentRuntimeLanePullRequest(spec, { pr, phase: options.dryRun ? "dry-run" : "cd-trigger", state: "cd-started", startedAt, observedAt: new Date().toISOString(), elapsedSeconds: durationSeconds(startedAt, new Date().toISOString()), preflight, merge: commandData(merge), sourceCommit, pipelineRun, dryRun: options.dryRun, message: options.dryRun ? `dry-run:PR 已达到 ${spec.version} 自动合并条件;本轮不会写 GitHub merge 或 CD。` : mergeRaceState === "merged" ? `PR 在本轮 merge 前已被合并;worker 继续对齐 merge commit 并驱动 ${spec.version} CD。后续成功、失败或超时会继续在本 PR 下回复。` : `PR 已自动合并,${spec.version} CD 准备开始;后续成功、失败或超时会继续在本 PR 下回复。`, }); if (!options.dryRun && record(startedComment).ok !== true) { return { ok: false, phase: "pr-comment", pr, sourceCommit, pipelineRun, comment: startedComment }; } if (options.dryRun) return { ok: true, action: "dry-run-merge", pr, merge: commandData(merge), comment: startedComment }; if (sourceCommit === null || record(headWait).ok !== true) { const observedAt = new Date().toISOString(); printRuntimeLanePrMonitorProgress(spec, { stage: "source-head", status: "failed", pr: pr.number, sourceCommit, pipelineRun, expectedCommit }); const comment = commentRuntimeLanePullRequest(spec, { pr, phase: `${spec.lane}-source-head`, state: "cd-timeout", startedAt, observedAt, elapsedSeconds: durationSeconds(startedAt, observedAt), preflight, merge: commandData(merge), sourceCommit, pipelineRun, dryRun: false, message: `PR 已合并,但 G14 ${spec.version} CI/CD source repo 没有在等待窗口内对齐 merge commit;为避免触发旧 head,本轮未启动 CD。`, }); const issue = reportRuntimeLaneAutomationIssue({ spec, pr, state: "cd-timeout", phase: `${spec.lane}-source-head`, startedAt, observedAt, preflight, sourceCommit, pipelineRun, details: { expectedCommit, headWait }, message: `${spec.version} source head 未在等待窗口内对齐 merge commit,CD 未启动。`, dryRun: false, }); return { ok: false, phase: `${spec.lane}-head`, pr, merge: commandData(merge), expectedCommit, headWait, comment: record(comment).ok === true ? comment : startedComment, issue, degradedReason: `${spec.lane}-head-unresolved-after-merge` }; } const before = runtimeLaneStatusWithGitMirror(spec, sourceCommit); const beforeSummary = summarizeRuntimeLaneCdStatus(spec, before); const trigger = runtimeLaneCdPassedWithMirror(spec, before) ? { ok: true, skipped: true, reason: "source-commit-already-deployed" } : triggerRuntimeLaneCurrent(spec, Math.min(options.timeoutSeconds, 600)); printEvent(`${spec.lane}.cd.trigger`, { pr: pr.number, sourceCommit, pipelineRun, ok: record(trigger).ok, skipped: record(trigger).skipped ?? false, degradedReason: record(trigger).degradedReason ?? null }); printRuntimeLanePrMonitorProgress(spec, { stage: "cd-trigger", status: record(trigger).ok === true ? "succeeded" : "failed", pr: pr.number, sourceCommit, pipelineRun, skipped: record(trigger).skipped ?? false, degradedReason: record(trigger).degradedReason ?? null }); if (record(trigger).ok !== true) { const observedAt = new Date().toISOString(); const comment = commentRuntimeLanePullRequest(spec, { pr, phase: "cd-trigger", state: "cd-failed", startedAt, observedAt, elapsedSeconds: durationSeconds(startedAt, observedAt), preflight, merge: commandData(merge), sourceCommit, pipelineRun, cd: beforeSummary, dryRun: false, message: `PR 已合并,但 ${spec.version} CD 触发失败;需要查看 trigger 阶段的 degradedReason。`, }); const issue = reportRuntimeLaneAutomationIssue({ spec, pr, state: "cd-failed", phase: "cd-trigger", startedAt, observedAt, preflight, sourceCommit, pipelineRun, cd: beforeSummary, details: { trigger }, message: `${spec.version} CD 触发失败,PipelineRun 未进入可等待状态。`, dryRun: false, }); return { ok: false, phase: "cd-trigger", pr, sourceCommit, pipelineRun, trigger, comment, issue }; } const cd = await waitForRuntimeLaneCd(spec, sourceCommit, options.timeoutSeconds); const cdStatus = record(cd.status); const flush = record(cd.flush); const cdOk = cd.ok === true; const cdPhase = String(cd.phase ?? ""); const cdPipelineRun = stringOrNull(cdStatus.pipelineRun) ?? pipelineRun; const observedAt = new Date().toISOString(); const finalComment = commentRuntimeLanePullRequest(spec, { pr, phase: cdPhase, state: cdOk ? "cd-succeeded" : cdPhase === "timeout" ? "cd-timeout" : "cd-failed", startedAt, observedAt, elapsedSeconds: durationSeconds(startedAt, observedAt), preflight, merge: commandData(merge), sourceCommit, pipelineRun: cdPipelineRun, cd: cdStatus, flush, dryRun: false, message: cdOk ? `${spec.version} 自动 CD 已完成:PipelineRun、Argo/runtime、公开探针和 Git mirror flush 收口均已检查。` : cdPhase === "timeout" ? `${spec.version} 自动 CD 超时未收口;本评论保留最后一次 PipelineRun / Argo / public probe / Git mirror 状态用于接续排障。` : `${spec.version} 自动 CD 失败;本评论保留失败阶段和最后一次状态用于接续排障。`, }); const issue = cdOk ? { ok: true, skipped: true, reason: "cd-succeeded" } : reportRuntimeLaneAutomationIssue({ spec, pr, state: cdPhase === "timeout" ? "cd-timeout" : "cd-failed", phase: cdPhase || "cd", startedAt, observedAt, preflight, sourceCommit, pipelineRun: cdPipelineRun, cd: cdStatus, details: { cd }, message: cdPhase === "timeout" ? `${spec.version} 自动 CD 超时。` : `${spec.version} 自动 CD 失败。`, dryRun: false, }); return { ok: cdOk && record(finalComment).ok === true && record(issue).ok === true, action: cdOk ? `merged-and-rolled-${spec.lane}` : `${spec.lane}-cd-failed`, phase: cdOk ? "cd-passed" : cdPhase, pr, sourceCommit, pipelineRun: cdPipelineRun, trigger, cd, comment: finalComment, issue, }; }