diff --git a/scripts/hwlab-g14-contract-test.ts b/scripts/hwlab-g14-contract-test.ts index 5353e50c..1f7c6a72 100644 --- a/scripts/hwlab-g14-contract-test.ts +++ b/scripts/hwlab-g14-contract-test.ts @@ -1,4 +1,4 @@ -import { gitMirrorFlushJobManifest, gitMirrorStatusSummary, gitMirrorSyncJobManifest, gitMirrorV02SyncRequirement, hwlabG14MonitorStateFileName, parseGitMirrorStatusRefs, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets, v02ControlPlaneRenderScript, v02FalseGreenGuard, v02PipelineServiceIds } from "./src/hwlab-g14"; +import { gitMirrorFlushJobManifest, gitMirrorStatusSummary, gitMirrorSyncJobManifest, gitMirrorV02SyncRequirement, hwlabG14MonitorStateFileName, parseGitMirrorStatusRefs, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets, v02CommitAlignment, v02ControlPlaneRenderScript, v02FalseGreenGuard, v02PipelineServiceIds } from "./src/hwlab-g14"; function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); @@ -44,7 +44,7 @@ const gitMirrorStatusRaw = [ 'lastSync={"ok":true}', "lastWrite=", "lastFlush=", - 'refs={"refs":{"localV02":"0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc","localG14":"1111111111111111111111111111111111111111","localGitops":"2222222222222222222222222222222222222222","githubGitops":"3333333333333333333333333333333333333333"},"pendingFlush":true}', + 'refs={"refs":{"localV02":"0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc","githubV02":"0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc","localG14":"1111111111111111111111111111111111111111","githubG14":"1111111111111111111111111111111111111111","localGitops":"2222222222222222222222222222222222222222","githubGitops":"3333333333333333333333333333333333333333"},"pendingFlush":true}', ].join("\n"); const parsedGitMirrorRefs = parseGitMirrorStatusRefs(gitMirrorStatusRaw); assertCondition( @@ -53,6 +53,11 @@ assertCondition( parsedGitMirrorRefs, ); assertCondition(parsedGitMirrorRefs.pendingFlush === true, "git mirror status parser must preserve pending flush signal", parsedGitMirrorRefs); +assertCondition( + parsedGitMirrorRefs.refs.githubV02 === "0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc", + "git mirror status parser must expose GitHub source branch staging ref", + parsedGitMirrorRefs, +); assertCondition( gitMirrorV02SyncRequirement("0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc", gitMirrorStatusRaw).required === false, "trigger-current must not sync mirror when local v0.2 already matches source commit", @@ -68,15 +73,16 @@ assertCondition( gitMirrorSummary, ); assertCondition(gitMirrorSummary.githubInSync === false, "git mirror status summary must expose GitHub GitOps drift", gitMirrorSummary); +assertCondition(gitMirrorSummary.sourceInSync === true && gitMirrorSummary.gitopsInSync === false, "git mirror status must split source and gitops GitHub sync state", gitMirrorSummary); const renderScript = v02ControlPlaneRenderScript("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); assertCondition( - renderScript.includes("git worktree add --detach") && renderScript.includes("/tmp/hwlab-v02-control-plane-source-aaaaaaaaaaaa"), - "v0.2 control-plane render must use a detached temp worktree", + renderScript.includes("git --git-dir=\"$cicd_repo\" worktree add --detach") && renderScript.includes("/tmp/hwlab-v02-control-plane-source-aaaaaaaaaaaa"), + "v0.2 control-plane render must use a detached temp worktree from the CI/CD repo", renderScript, ); assertCondition( - !renderScript.includes("git merge --ff-only origin/v0.2") && !renderScript.includes("git checkout v0.2"), - "v0.2 control-plane render must not require fixed workspace checkout/merge and clean status", + renderScript.includes("cicd_repo='/root/hwlab-v02-cicd.git'") && !renderScript.includes("git -C /root/hwlab-v02") && !renderScript.includes("git checkout v0.2"), + "v0.2 control-plane render must not use the fixed workspace checkout or its clean status", renderScript, ); assertCondition( @@ -85,6 +91,29 @@ assertCondition( v02PipelineServiceIds(), ); +const staleSuccessAlignment = v02CommitAlignment({ + expectedSourceHead: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + sourceHeads: { + cicdRepo: "/root/hwlab-v02-cicd.git", + cicdSourceHead: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + originHead: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + workspace: { path: "/root/hwlab-v02", head: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", originHead: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", dirty: true, dirtyCount: 1 }, + }, + gitMirrorSummary: { localV02: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", githubV02: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", sourceInSync: false, gitopsInSync: true }, + pipelineRun: { pipelineRun: "hwlab-v02-ci-poll-aaaaaaaaaaaa", status: null }, + recentPipelineRuns: { items: [{ name: "hwlab-v02-ci-poll-bbbbbbbbbbbb", sourceCommit: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", status: "True", reason: "Completed" }] }, + runtimeWorkloads: { items: [] }, + webAssets: { apiRevision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }, +}); +assertCondition( + staleSuccessAlignment.aligned === false + && staleSuccessAlignment.state === "stale-success" + && JSON.stringify(staleSuccessAlignment.staleReasons).includes("mirror-source-stale") + && JSON.stringify(staleSuccessAlignment.workspaceWarnings).includes("workspace-dirty-but-isolated-from-cicd"), + "v0.2 commit alignment must call out stale-success while keeping dirty workspace isolated from CI source selection", + staleSuccessAlignment, +); + const falseGreenPassed = v02FalseGreenGuard({ sourceCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", pipelineRun: { exists: true, status: "True" }, @@ -261,8 +290,9 @@ console.log(JSON.stringify({ "git mirror sync is a manual devops-infra Job, not a CronJob", "git mirror flush is a manual devops-infra Job, not a CronJob", "trigger-current can decide whether v0.2 git mirror pre-sync is required", - "git mirror status exposes pending flush and controlled flush command", - "v0.2 control-plane render uses a detached temp worktree", + "git mirror status exposes source and gitops GitHub sync state plus controlled flush command", + "v0.2 control-plane render uses a detached temp worktree from a CI/CD dedicated bare repo", + "v0.2 status alignment reports stale-success without coupling CI to dirty workspace state", "v0.2 PipelineRun service matrix excludes hwlab-cli", "v0.2 false-green guard checks build TaskRuns, runtime artifact source commits, and reuse provenance", "rollout brief includes natural-language changelog before automatic diff summary", diff --git a/scripts/src/hwlab-g14.ts b/scripts/src/hwlab-g14.ts index 18ec62bf..a8891777 100644 --- a/scripts/src/hwlab-g14.ts +++ b/scripts/src/hwlab-g14.ts @@ -11,6 +11,7 @@ const G14_PROVIDER = "G14"; const G14_WORKSPACE = "/root/hwlab"; const V02_SOURCE_BRANCH = "v0.2"; const V02_WORKSPACE = "/root/hwlab-v02"; +const V02_CICD_REPO = "/root/hwlab-v02-cicd.git"; const DEV_NAMESPACE = "hwlab-dev"; const CI_NAMESPACE = "hwlab-ci"; const ARGO_NAMESPACE = "argocd"; @@ -488,16 +489,39 @@ function precheckWorkspace(): CommandJsonResult { return cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "script", "--", "pwd; git fetch origin G14 --prune; git status --short --branch; git remote -v | sed -n '1,4p'"], 120_000); } -function v02WorkspaceScript(script: string, timeoutMs = 120_000): CommandJsonResult { - return cliJson(["ssh", `${G14_PROVIDER}:${V02_WORKSPACE}`, "script", "--", script], timeoutMs); +function g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult { + return cliJson(["ssh", G14_PROVIDER, "script", "--", script], timeoutMs); } function g14K3s(args: string[], timeoutMs = 60_000): CommandJsonResult { return cliJson(["ssh", `${G14_PROVIDER}:k3s`, ...args], timeoutMs); } +function v02CicdRepoEnsureScript(): string { + return [ + `cicd_repo=${shellQuote(V02_CICD_REPO)}`, + `cicd_url=${shellQuote(V02_GIT_URL)}`, + "mkdir -p \"$(dirname \"$cicd_repo\")\"", + "if [ -d \"$cicd_repo/objects\" ] && [ -f \"$cicd_repo/HEAD\" ]; then", + " :", + "elif [ -e \"$cicd_repo\" ]; then", + " echo \"v0.2 CI/CD repo path exists but is not a bare git repo: $cicd_repo\" >&2", + " exit 41", + "else", + " git clone --bare \"$cicd_url\" \"$cicd_repo\"", + "fi", + "git --git-dir=\"$cicd_repo\" remote set-url origin \"$cicd_url\" 2>/dev/null || git --git-dir=\"$cicd_repo\" remote add origin \"$cicd_url\"", + "git --git-dir=\"$cicd_repo\" config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'", + "git --git-dir=\"$cicd_repo\" fetch origin '+refs/heads/v0.2:refs/remotes/origin/v0.2' --prune", + ].join("\n"); +} + function getV02Head(): string | null { - const result = v02WorkspaceScript("git fetch origin v0.2 --prune >/dev/null 2>&1; git rev-parse origin/v0.2", 120_000); + const result = g14HostScript([ + "set -eu", + v02CicdRepoEnsureScript(), + "git --git-dir=\"$cicd_repo\" rev-parse refs/remotes/origin/v0.2", + ].join("\n"), 180_000); if (!isCommandSuccess(result)) return null; const output = String(nested(result.parsed, ["data", "stdout"]) ?? result.stdout).trim(); const match = /[0-9a-f]{40}/iu.exec(output); @@ -693,7 +717,7 @@ function shellSectionOk(section: ShellSection | undefined): boolean { function v02ControlPlaneStatusBundle(sourceCommit: string | null | undefined): CommandJsonResult { const sourceCommitLine = sourceCommit === undefined - ? `source_commit=$(git -C ${shellQuote(V02_WORKSPACE)} rev-parse origin/v0.2 2>/dev/null || true)` + ? `source_commit=$(git --git-dir=${shellQuote(V02_CICD_REPO)} rev-parse refs/remotes/origin/v0.2 2>/dev/null || true)` : `source_commit=${shellQuote(sourceCommit ?? "")}`; const script = [ "set +e", @@ -709,6 +733,7 @@ function v02ControlPlaneStatusBundle(sourceCommit: string | null | undefined): C " printf '\\n__UNIDESK_SECTION_END__ %s exit=%s\\n' \"$name\" \"$code\"", "}", "section sourceCommit printf '%s\\n' \"$source_commit\"", + `section sourceHeads sh -c ${shellQuote(v02SourceHeadsProbeScript())}`, "section queryNow date -u +%Y-%m-%dT%H:%M:%SZ", `section controlPlane kubectl get pipeline,role,rolebinding,serviceaccount -n ${shellQuote(CI_NAMESPACE)} -l hwlab.pikastech.local/gitops-target=v02 -o name`, `section obsoleteCronJobs kubectl get cronjob -n ${shellQuote(CI_NAMESPACE)} ${shellQuote(V02_POLLER)} ${shellQuote(V02_RECONCILER)} --ignore-not-found -o name`, @@ -718,11 +743,31 @@ function v02ControlPlaneStatusBundle(sourceCommit: string | null | undefined): C `if [ -n "$pipeline_run" ]; then section planArtifacts sh -c ${shellQuote(v02PlanArtifactsLogScript())} plan-artifacts "$pipeline_run"; else section planArtifacts sh -c 'true'; fi`, `section runtimeWorkloads kubectl get deploy,statefulset,job -n hwlab-v02 -l hwlab.pikastech.local/gitops-target=v02 -o ${shellQuote(v02RuntimeWorkloadsColumns())} --no-headers`, `section recentPipelineRuns kubectl get pipelinerun -n ${shellQuote(CI_NAMESPACE)} -l hwlab.pikastech.local/gitops-target=v02 -o ${shellQuote(pipelineRunRowsJsonPath())}`, + `section gitMirrorCache kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc ${shellQuote(gitMirrorCacheProbeScript())}`, `section webAssets sh -c ${shellQuote(v02WebAssetsProbeScript())}`, ].join("\n"); return g14K3s(["script", "--", script], 60_000); } +function v02SourceHeadsProbeScript(): string { + return [ + "set +e", + `cicd_repo=${shellQuote(V02_CICD_REPO)}`, + `workspace=${shellQuote(V02_WORKSPACE)}`, + "rev_cicd() { git --git-dir=\"$cicd_repo\" rev-parse \"$1\" 2>/dev/null || true; }", + "rev_workspace() { git -C \"$workspace\" rev-parse \"$1\" 2>/dev/null || true; }", + "printf 'cicdRepo\\t%s\\n' \"$cicd_repo\"", + "printf 'cicdRepoExists\\t%s\\n' \"$([ -d \"$cicd_repo/objects\" ] && printf yes || printf no)\"", + "printf 'cicdSourceHead\\t%s\\n' \"$(rev_cicd refs/remotes/origin/v0.2)\"", + "printf 'originHead\\t%s\\n' \"$(rev_cicd refs/remotes/origin/v0.2)\"", + "printf 'workspacePath\\t%s\\n' \"$workspace\"", + "printf 'workspaceBranch\\t%s\\n' \"$(git -C \"$workspace\" rev-parse --abbrev-ref HEAD 2>/dev/null || true)\"", + "printf 'workspaceHead\\t%s\\n' \"$(rev_workspace HEAD)\"", + "printf 'workspaceOriginHead\\t%s\\n' \"$(rev_workspace refs/remotes/origin/v0.2)\"", + "printf 'workspaceDirtyCount\\t%s\\n' \"$(git -C \"$workspace\" status --porcelain=v1 2>/dev/null | wc -l | tr -d ' ')\"", + ].join("\n"); +} + function v02PlanArtifactsLogScript(): string { return [ "pipeline_run=\"$1\"", @@ -981,6 +1026,38 @@ function v02RuntimeWorkloadsFromText(text: string, commandOk: boolean, exitCode: }; } +function keyValueLinesFromText(text: string): Record { + const fields: Record = {}; + for (const line of text.split(/\r?\n/u)) { + const [key = "", ...rest] = line.split("\t"); + if (key.trim().length > 0) fields[key.trim()] = rest.join("\t").trim(); + } + return fields; +} + +function v02SourceHeadsFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string): Record { + const fields = keyValueLinesFromText(text); + const dirtyCount = numericField(fields.workspaceDirtyCount); + return { + ok: commandOk, + cicdRepo: fields.cicdRepo || V02_CICD_REPO, + cicdRepoExists: fields.cicdRepoExists === "yes", + cicdSourceHead: fields.cicdSourceHead || null, + originHead: fields.originHead || null, + workspace: { + path: fields.workspacePath || V02_WORKSPACE, + branch: fields.workspaceBranch || null, + head: fields.workspaceHead || null, + originHead: fields.workspaceOriginHead || null, + dirtyCount, + dirty: typeof dirtyCount === "number" ? dirtyCount > 0 : null, + isolatedFromCicd: true, + }, + exitCode, + stderr: commandOk ? "" : stderr.trim().slice(0, 2000), + }; +} + export function v02FalseGreenGuard(input: { sourceCommit: string | null; pipelineRun: Record | null; @@ -1057,6 +1134,118 @@ export function v02FalseGreenGuard(input: { }; } +export function v02CommitAlignment(input: { + expectedSourceHead: string | null; + sourceHeads: Record; + gitMirrorSummary: Record; + pipelineRun: Record | null; + recentPipelineRuns: Record; + planArtifacts?: Record; + runtimeWorkloads: Record; + webAssets: Record; +}): Record { + const expectedSourceHead = input.expectedSourceHead; + const cicdSourceHead = stringOrNull(input.sourceHeads.cicdSourceHead); + const originHead = stringOrNull(input.sourceHeads.originHead) ?? expectedSourceHead; + const workspace = record(input.sourceHeads.workspace); + const workspaceHead = stringOrNull(workspace.head); + const workspaceOriginHead = stringOrNull(workspace.originHead); + const mirrorSourceHead = stringOrNull(input.gitMirrorSummary.localV02); + const mirrorGithubSourceHead = stringOrNull(input.gitMirrorSummary.githubV02); + const recentItems = Array.isArray(input.recentPipelineRuns.items) + ? input.recentPipelineRuns.items.map((item) => record(item)) + : []; + const latestPipelineRun = recentItems[0] ?? null; + const latestPipelineSourceCommit = latestPipelineRun === null ? null : stringOrNull(latestPipelineRun.sourceCommit); + const currentPipelineStatus = stringOrNull(input.pipelineRun?.status); + const apiRevision = stringOrNull(input.webAssets.apiRevision); + const planArtifacts = record(input.planArtifacts); + const planSourceCommit = stringOrNull(planArtifacts.sourceCommitId); + const rolloutServices = stringArray(planArtifacts.rolloutServices); + const apiRevisionRequired = expectedSourceHead !== null && planSourceCommit === expectedSourceHead && rolloutServices.includes("hwlab-cloud-api"); + const workloadItems = Array.isArray(input.runtimeWorkloads.items) + ? input.runtimeWorkloads.items.map((item) => record(item)) + : []; + const serviceSourceCommits = Object.fromEntries(workloadItems + .filter((item) => typeof item.serviceId === "string" && item.serviceId.length > 0) + .map((item) => [String(item.serviceId), stringOrNull(item.sourceCommit) ?? stringOrNull(item.artifactSourceCommit)])); + const staleReasons: string[] = []; + if (expectedSourceHead === null) staleReasons.push("expected-source-head-unresolved"); + if (expectedSourceHead !== null && originHead !== null && originHead !== expectedSourceHead) staleReasons.push("origin-head-mismatch"); + if (expectedSourceHead !== null && cicdSourceHead !== null && cicdSourceHead !== expectedSourceHead) staleReasons.push("cicd-source-repo-stale"); + if (expectedSourceHead !== null && mirrorSourceHead !== expectedSourceHead) staleReasons.push("mirror-source-stale"); + if (expectedSourceHead !== null && latestPipelineSourceCommit !== expectedSourceHead) staleReasons.push("latest-pipelinerun-not-current"); + if (apiRevisionRequired && apiRevision !== expectedSourceHead) staleReasons.push("runtime-api-revision-stale"); + const workspaceWarnings: string[] = []; + if (expectedSourceHead !== null && workspaceHead !== null && workspaceHead !== expectedSourceHead) workspaceWarnings.push("workspace-head-differs-from-latest-source-but-isolated"); + if (expectedSourceHead !== null && workspaceOriginHead !== null && workspaceOriginHead !== expectedSourceHead) workspaceWarnings.push("workspace-origin-ref-stale-but-isolated"); + if (workspace.dirty === true) workspaceWarnings.push("workspace-dirty-but-isolated-from-cicd"); + const runtimeWarnings: string[] = []; + if (!apiRevisionRequired && expectedSourceHead !== null && apiRevision !== null && apiRevision !== expectedSourceHead) { + runtimeWarnings.push("api-revision-differs-without-current-cloud-api-rollout"); + } + const latestPipelineSucceeded = latestPipelineRun !== null && latestPipelineRun.status === "True"; + const aligned = staleReasons.length === 0; + const state = aligned + ? "aligned" + : latestPipelineSucceeded + ? "stale-success" + : currentPipelineStatus === "Unknown" + ? "in-progress" + : "stale"; + return { + aligned, + state, + expectedSourceHead, + originHead, + cicdSourceHead, + cicdRepo: input.sourceHeads.cicdRepo ?? V02_CICD_REPO, + mirrorSourceHead, + mirrorGithubSourceHead, + latestPipelineRun: latestPipelineRun === null + ? null + : { + name: latestPipelineRun.name ?? null, + sourceCommit: latestPipelineSourceCommit, + status: latestPipelineRun.status ?? null, + reason: latestPipelineRun.reason ?? null, + createdAt: latestPipelineRun.createdAt ?? null, + durationSeconds: latestPipelineRun.durationSeconds ?? null, + }, + latestPipelineSourceCommit, + currentPipelineRun: input.pipelineRun === null + ? null + : { + name: input.pipelineRun.pipelineRun ?? null, + status: input.pipelineRun.status ?? null, + reason: input.pipelineRun.reason ?? null, + }, + runtimeSourceCommit: apiRevision, + apiRevision, + apiRevisionRequired, + planSourceCommit, + rolloutServices, + serviceSourceCommits, + sourceInSync: input.gitMirrorSummary.sourceInSync ?? null, + gitopsInSync: input.gitMirrorSummary.gitopsInSync ?? null, + staleReasons, + workspace: { + path: workspace.path ?? V02_WORKSPACE, + branch: workspace.branch ?? null, + head: workspaceHead, + originHead: workspaceOriginHead, + dirty: workspace.dirty ?? null, + dirtyCount: workspace.dirtyCount ?? null, + isolatedFromCicd: true, + }, + workspaceWarnings, + runtimeWarnings, + summary: aligned + ? `v0.2 CI/CD source, mirror, PipelineRun, and runtime align with ${shortSha(expectedSourceHead ?? "")}` + : `v0.2 CI/CD alignment state=${state}; staleReasons=${staleReasons.join(",")}`, + }; +} + function webAssetsRevisionNote(apiRevision: string | null, sourceCommit: string | null, activePipelineRuns: unknown[]): string | null { if (!apiRevision || !sourceCommit || apiRevision === sourceCommit) return null; const activeItems = activePipelineRuns.map((item) => record(item)); @@ -1380,23 +1569,24 @@ export function v02ControlPlaneRenderScript(sourceCommit: string): string { const worktreeDir = v02RenderWorktreeDir(sourceCommit); return [ "set -eu", + v02CicdRepoEnsureScript(), `render_dir=${shellQuote(renderDir)}`, `worktree_dir=${shellQuote(worktreeDir)}`, - "cleanup_render_worktree() { git worktree remove --force \"$worktree_dir\" >/dev/null 2>&1 || rm -rf \"$worktree_dir\"; }", + "cleanup_render_worktree() { git --git-dir=\"$cicd_repo\" worktree remove --force \"$worktree_dir\" >/dev/null 2>&1 || rm -rf \"$worktree_dir\"; }", "trap cleanup_render_worktree EXIT", - "git fetch origin v0.2 --prune", - `test "$(git rev-parse origin/v0.2)" = ${shellQuote(sourceCommit)}`, + `test "$(git --git-dir="$cicd_repo" rev-parse refs/remotes/origin/v0.2)" = ${shellQuote(sourceCommit)}`, "cleanup_render_worktree", + "git --git-dir=\"$cicd_repo\" worktree prune >/dev/null 2>&1 || true", "rm -rf \"$render_dir\"", "mkdir -p \"$render_dir\" \"$(dirname \"$worktree_dir\")\"", - `git worktree add --detach "$worktree_dir" ${shellQuote(sourceCommit)}`, + `git --git-dir="$cicd_repo" worktree add --detach "$worktree_dir" ${shellQuote(sourceCommit)}`, "cd \"$worktree_dir\"", `node scripts/g14-gitops-render.mjs --lane v02 --source-revision ${shellQuote(sourceCommit)} --out "$render_dir"`, ].join("\n"); } function runV02RenderToTemp(sourceCommit: string): CommandJsonResult { - return v02WorkspaceScript(v02ControlPlaneRenderScript(sourceCommit), 180_000); + return g14HostScript(v02ControlPlaneRenderScript(sourceCommit), 180_000); } function v02RenderDir(sourceCommit: string): string { @@ -1563,6 +1753,7 @@ function v02ControlPlaneStatus(sourceCommitInput?: string | null): Record line.trim()).filter(Boolean), @@ -1675,12 +1898,12 @@ function v02ControlPlaneStatus(sourceCommitInput?: string | null): Record { if (options.action === "cleanup-runs") return runControlPlaneCleanup(options); if (options.action === "cleanup-released-pvs") return runControlPlaneReleasedPvCleanup(options); - const sourceCommit = options.action === "status" ? undefined : getV02Head(); + const sourceCommit = getV02Head(); if (sourceCommit === null) { - return { ok: false, command: `hwlab g14 control-plane ${options.action} --lane v02`, degradedReason: "v02-head-unresolved", workspace: V02_WORKSPACE }; + return { ok: false, command: `hwlab g14 control-plane ${options.action} --lane v02`, degradedReason: "v02-head-unresolved", sourceRepo: V02_CICD_REPO, workspace: V02_WORKSPACE }; } if (options.action === "runtime-migration") return runV02RuntimeMigration(options, sourceCommit); - if (options.action === "status") return v02ControlPlaneStatus(); + if (options.action === "status") return v02ControlPlaneStatus(sourceCommit); if (options.action === "apply") { const render = runV02RenderToTemp(sourceCommit); if (!isCommandSuccess(render)) { @@ -1933,7 +2156,9 @@ export function parseGitMirrorStatusRefs(raw: string): { refs: Record { const lastSync = parseLabeledJsonLine(raw, "lastSync"); const lastWrite = parseLabeledJsonLine(raw, "lastWrite"); const lastFlush = parseLabeledJsonLine(raw, "lastFlush"); + const localV02 = refs.refs.localV02 ?? null; + const githubV02 = refs.refs.githubV02 ?? null; const localGitops = refs.refs.localGitops ?? null; const githubGitops = refs.refs.githubGitops ?? null; + const sourceInSync = Boolean(localV02 && githubV02 && localV02 === githubV02); + const gitopsInSync = Boolean(localGitops && githubGitops && localGitops === githubGitops); return { - localV02: refs.refs.localV02 ?? null, + localV02, + githubV02, localG14: refs.refs.localG14 ?? null, + githubG14: refs.refs.githubG14 ?? null, localGitops, githubGitops, pendingFlush: refs.pendingFlush, @@ -1986,7 +2217,9 @@ export function gitMirrorStatusSummary(raw: string): Record { }, flushNeeded: refs.pendingFlush === true, flushCommand: refs.pendingFlush === true ? "bun scripts/cli.ts hwlab g14 git-mirror flush --confirm" : null, - githubInSync: Boolean(localGitops && githubGitops && localGitops === githubGitops), + sourceInSync, + gitopsInSync, + githubInSync: sourceInSync && gitopsInSync, }; } @@ -2018,7 +2251,9 @@ function compactGitMirrorStatus(status: Record, sourceCommit: s required: requirement.required, sourceCommit, localV02: requirement.localV02 ?? null, + githubV02: requirement.refs.githubV02 ?? null, pendingFlush: requirement.pendingFlush ?? null, + sourceInSync: Boolean(requirement.refs.localV02 && requirement.refs.githubV02 && requirement.refs.localV02 === requirement.refs.githubV02), reason: requirement.reason, cacheOk: nested(status, ["cache", "ok"]) === true, cacheStderr: tailText(nested(status, ["cache", "stderr"]), 1000), @@ -2042,6 +2277,39 @@ function compactGitMirrorSync(sync: Record): Record/dev/null || true", + "const { execFileSync } = require('node:child_process');", + "const repo = '/cache/pikasTech/HWLAB.git';", + "function rev(ref) {", + " try {", + " return execFileSync('git', ['--git-dir=' + repo, 'rev-parse', ref], { encoding: 'utf8' }).trim();", + " } catch {", + " return null;", + " }", + "}", + "const localGitops = rev('refs/heads/v0.2-gitops');", + "const githubGitops = rev('refs/mirror-stage/heads/v0.2-gitops');", + "console.log(JSON.stringify({", + " refs: {", + " localV02: rev('refs/heads/v0.2'),", + " githubV02: rev('refs/mirror-stage/heads/v0.2'),", + " localG14: rev('refs/heads/G14'),", + " githubG14: rev('refs/mirror-stage/heads/G14'),", + " localGitops,", + " githubGitops,", + " },", + " pendingFlush: Boolean(localGitops && githubGitops && localGitops !== githubGitops),", + "}));", + "NODE", + ].join("\n"); +} + function preSyncV02GitMirror(sourceCommit: string, options: Pick): Record { const statusStartMs = Date.now(); printProgressEvent("hwlab.v02.trigger.progress", { stage: "git-mirror-pre-sync-status", status: "started", sourceCommit }); @@ -2113,34 +2381,6 @@ function preSyncV02GitMirror(sourceCommit: string, options: Pick { const startedAtMs = Date.now(); - const cacheScript = [ - "printf 'lastSync='; cat /cache/HWLAB.last-sync.json 2>/dev/null || true; printf '\\n'", - "printf 'lastWrite='; cat /cache/HWLAB.last-write.json 2>/dev/null || true; printf '\\n'", - "printf 'lastFlush='; cat /cache/HWLAB.last-flush.json 2>/dev/null || true; printf '\\n'", - "printf 'refs='", - "node - <<'NODE' 2>/dev/null || true", - "const { execFileSync } = require('node:child_process');", - "const repo = '/cache/pikasTech/HWLAB.git';", - "function rev(ref) {", - " try {", - " return execFileSync('git', ['--git-dir=' + repo, 'rev-parse', ref], { encoding: 'utf8' }).trim();", - " } catch {", - " return null;", - " }", - "}", - "const localGitops = rev('refs/heads/v0.2-gitops');", - "const githubGitops = rev('refs/mirror-stage/heads/v0.2-gitops');", - "console.log(JSON.stringify({", - " refs: {", - " localV02: rev('refs/heads/v0.2'),", - " localG14: rev('refs/heads/G14'),", - " localGitops,", - " githubGitops,", - " },", - " pendingFlush: Boolean(localGitops && githubGitops && localGitops !== githubGitops),", - "}));", - "NODE", - ].join("\n"); const script = [ "set +e", "section() {", @@ -2176,7 +2416,7 @@ function runGitMirrorStatus(): Record { `-n ${shellQuote(GIT_MIRROR_NAMESPACE)}`, "deploy/git-mirror-http", "-- sh -lc", - shellQuote(cacheScript), + shellQuote(gitMirrorCacheProbeScript()), ].join(" "), ].join("\n"); const bundle = g14K3s(["script", "--", script], 60_000); @@ -2226,7 +2466,7 @@ function runGitMirrorStatus(): Record { function runGitMirrorApply(options: G14GitMirrorOptions): Record { const sourceCommit = getV02Head(); if (sourceCommit === null) { - return { ok: false, command: "hwlab g14 git-mirror apply", degradedReason: "v02-head-unresolved", workspace: V02_WORKSPACE }; + return { ok: false, command: "hwlab g14 git-mirror apply", degradedReason: "v02-head-unresolved", sourceRepo: V02_CICD_REPO, workspace: V02_WORKSPACE }; } const render = runV02RenderToTemp(sourceCommit); if (!isCommandSuccess(render)) {