diff --git a/scripts/src/cicd-branch-follower.ts b/scripts/src/cicd-branch-follower.ts index d63bd737..f49236eb 100644 --- a/scripts/src/cicd-branch-follower.ts +++ b/scripts/src/cicd-branch-follower.ts @@ -34,6 +34,7 @@ import { prioritizedTaskRunItems } from "./cicd-taskruns"; import { runBranchFollowerTaskRunDrillDown } from "./cicd-taskrun-drilldown"; import { runBranchFollowerJobDrillDown, runBranchFollowerRuntimeDrillDown } from "./cicd-job-runtime-drilldown"; import { runBranchFollowerGate } from "./cicd-gates"; +import { buildCicdHelp } from "./cicd-help"; import { attachReconcileTimeline, compactReconcileTimeline, finishReconcileStep, finishReconcileTimeline, startReconcileStep, startReconcileTimeline } from "./cicd-reconcile-timeline"; import { orderFollowersForControllerCloseout, shouldYieldAfterAutomaticTrigger } from "./cicd-reconcile-scheduler"; import type { AdapterSummary, BranchFollowerAction, BranchFollowerDebugStep, BranchFollowerGate, BranchFollowerPhase, BranchFollowerRegistry, ControllerSpec, FollowerSpec, FollowerState, K8sFollowerStateRead, K8sStateRead, NativeCloseoutWaitResult, NativeK8sJobResult, NativeStatusSpec, NativeWorkloadSpec, OutputMode, ParsedOptions, StageTiming, TriggerResult } from "./cicd-types"; @@ -55,30 +56,7 @@ const SPEC_REF = "PJ2026-01060703"; const SPEC_VERSION = "draft-2026-07-03-p0-branch-follower"; export function cicdHelp(): unknown { - return { - command: "cicd branch-follower plan|apply|status|run-once|debug-step|cleanup-state|events|logs|taskrun|job|runtime|gate", - output: "text by default; use --json, --raw, or -o json|yaml for machine output", - usage: [ - "bun scripts/cli.ts cicd branch-follower plan", - "bun scripts/cli.ts cicd branch-follower apply --confirm --wait", - "bun scripts/cli.ts cicd branch-follower status", - "bun scripts/cli.ts cicd branch-follower status --live", - "bun scripts/cli.ts cicd branch-follower run-once --all --dry-run", - "bun scripts/cli.ts cicd branch-follower run-once --follower hwlab-jd01-v03 --confirm --wait", - "bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel-master --step controller-source", - "bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel-master --step state-read", - "bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel-master --step state-write --confirm", - "bun scripts/cli.ts cicd branch-follower cleanup-state --follower web-probe-sentinel-master --confirm", - "bun scripts/cli.ts cicd branch-follower events --follower agentrun-jd01-v02", - "bun scripts/cli.ts cicd branch-follower logs --follower web-probe-sentinel-master", - "bun scripts/cli.ts cicd branch-follower taskrun --follower hwlab-jd01-v03 --taskrun runtime-ready --logs-tail 120 --json", - "bun scripts/cli.ts cicd branch-follower job --follower agentrun-jd01-v02 --source-commit --job image-build --json", - "bun scripts/cli.ts cicd branch-follower gate --follower agentrun-jd01-v02 --gate reuse-plan --source-commit --json", - ], - config: DEFAULT_CONFIG_PATH, - spec: `${SPEC_REF} ${SPEC_VERSION}`, - description: "Deploy and inspect the YAML-first Kubernetes branch follower that follows HWLAB v0.3, AgentRun v0.2, and the selected web-probe sentinel master lane without using host worktrees as source authority.", - }; + return buildCicdHelp(DEFAULT_CONFIG_PATH, `${SPEC_REF} ${SPEC_VERSION}`); } export async function runCicdCommand(_config: UniDeskConfig | null, args: string[]): Promise { diff --git a/scripts/src/cicd-help.ts b/scripts/src/cicd-help.ts new file mode 100644 index 00000000..f49e90eb --- /dev/null +++ b/scripts/src/cicd-help.ts @@ -0,0 +1,29 @@ +// SPEC: PJ2026-01060703 CI/CD branch follower help text. +// Responsibility: bounded CLI help payloads for the branch-follower entrypoint. + +export function buildCicdHelp(configPath: string, spec: string): unknown { + return { + command: "cicd branch-follower plan|apply|status|run-once|debug-step|cleanup-state|events|logs|taskrun|job|runtime|gate", + output: "text by default; use --json, --raw, or -o json|yaml for machine output", + usage: [ + "bun scripts/cli.ts cicd branch-follower plan", + "bun scripts/cli.ts cicd branch-follower apply --confirm --wait", + "bun scripts/cli.ts cicd branch-follower status", + "bun scripts/cli.ts cicd branch-follower status --live", + "bun scripts/cli.ts cicd branch-follower run-once --all --dry-run", + "bun scripts/cli.ts cicd branch-follower run-once --follower hwlab-jd01-v03 --confirm --wait", + "bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel-master --step controller-source", + "bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel-master --step state-read", + "bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel-master --step state-write --confirm", + "bun scripts/cli.ts cicd branch-follower cleanup-state --follower web-probe-sentinel-master --confirm", + "bun scripts/cli.ts cicd branch-follower events --follower agentrun-jd01-v02", + "bun scripts/cli.ts cicd branch-follower logs --follower web-probe-sentinel-master", + "bun scripts/cli.ts cicd branch-follower taskrun --follower hwlab-jd01-v03 --taskrun runtime-ready --logs-tail 120 --json", + "bun scripts/cli.ts cicd branch-follower job --follower agentrun-jd01-v02 --source-commit --job image-build --json", + "bun scripts/cli.ts cicd branch-follower gate --follower agentrun-jd01-v02 --gate reuse-plan --source-commit --json", + ], + config: configPath, + spec, + description: "Deploy and inspect the YAML-first Kubernetes branch follower that follows HWLAB v0.3, AgentRun v0.2, and the selected web-probe sentinel master lane without using host worktrees as source authority.", + }; +} diff --git a/scripts/src/cicd-reconcile-scheduler.ts b/scripts/src/cicd-reconcile-scheduler.ts index d8b0d50f..5ae188f3 100644 --- a/scripts/src/cicd-reconcile-scheduler.ts +++ b/scripts/src/cicd-reconcile-scheduler.ts @@ -23,8 +23,30 @@ export function shouldYieldAfterAutomaticTrigger(options: ParsedOptions, state: function followerPriority(state: Record | undefined): number { if (state === undefined) return 2; const phase = typeof state.phase === "string" ? state.phase : null; + if (hasStoredSourceTargetMismatch(state)) return 0; + if (hasUnfinishedObservedSource(state)) return 0; if (phase !== null && CLOSEOUT_PHASES.has(phase)) return 0; if (typeof state.inFlightJob === "string" && state.inFlightJob.trim() !== "") return 0; if (phase === "PendingTrigger" || phase === "Superseded") return 1; return 2; } + +function hasStoredSourceTargetMismatch(state: Record): boolean { + const observedSha = nestedString(state, "source", "observedSha"); + const targetSha = nestedString(state, "target", "targetSha"); + return observedSha !== null && targetSha !== null && observedSha !== targetSha; +} + +function hasUnfinishedObservedSource(state: Record): boolean { + const observedSha = nestedString(state, "source", "observedSha"); + const lastSucceededSha = typeof state.lastSucceededSha === "string" ? state.lastSucceededSha : null; + if (observedSha === null) return false; + return lastSucceededSha === null || lastSucceededSha !== observedSha; +} + +function nestedString(source: Record, parentKey: string, childKey: string): string | null { + const parent = source[parentKey]; + if (parent === null || typeof parent !== "object" || Array.isArray(parent)) return null; + const value = (parent as Record)[childKey]; + return typeof value === "string" && value.trim() !== "" ? value : null; +}