diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index 6a644a70..9d9048ea 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -5,7 +5,7 @@ "profile": "dev", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", - "commitId": "b9b625c", + "commitId": "42338c0", "artifactState": "contract-skeleton", "publish": { "ciPublished": false, @@ -66,9 +66,9 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -82,9 +82,9 @@ }, { "serviceId": "hwlab-cloud-web", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -98,9 +98,9 @@ }, { "serviceId": "hwlab-agent-mgr", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -114,9 +114,9 @@ }, { "serviceId": "hwlab-agent-worker", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -130,9 +130,9 @@ }, { "serviceId": "hwlab-gateway", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -146,9 +146,9 @@ }, { "serviceId": "hwlab-gateway-simu", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -162,9 +162,9 @@ }, { "serviceId": "hwlab-box-simu", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -178,9 +178,9 @@ }, { "serviceId": "hwlab-patch-panel", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -194,9 +194,9 @@ }, { "serviceId": "hwlab-router", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-router:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-router:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -210,9 +210,9 @@ }, { "serviceId": "hwlab-tunnel-client", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -226,9 +226,9 @@ }, { "serviceId": "hwlab-edge-proxy", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -242,9 +242,9 @@ }, { "serviceId": "hwlab-cli", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -258,9 +258,9 @@ }, { "serviceId": "hwlab-agent-skills", - "commitId": "b9b625c", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:b9b625c", - "imageTag": "b9b625c", + "commitId": "42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:42338c0", + "imageTag": "42338c0", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", diff --git a/deploy/deploy.json b/deploy/deploy.json index 5ce550d3..32c8bbbe 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -1,7 +1,7 @@ { "manifestVersion": "v1", "environment": "dev", - "commitId": "b9b625c", + "commitId": "42338c0", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", "health": { @@ -181,7 +181,7 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -189,9 +189,9 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667", - "HWLAB_COMMIT_ID": "b9b625c", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b9b625c", - "HWLAB_IMAGE_TAG": "b9b625c", + "HWLAB_COMMIT_ID": "42338c0", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:42338c0", + "HWLAB_IMAGE_TAG": "42338c0", "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy", "HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url", "HWLAB_CLOUD_DB_SSL_MODE": "disable", @@ -210,7 +210,7 @@ }, { "serviceId": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -218,14 +218,14 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "HWLAB_COMMIT_ID": "b9b625c", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b9b625c", - "HWLAB_IMAGE_TAG": "b9b625c" + "HWLAB_COMMIT_ID": "42338c0", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:42338c0", + "HWLAB_IMAGE_TAG": "42338c0" } }, { "serviceId": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -237,7 +237,7 @@ }, { "serviceId": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -249,7 +249,7 @@ }, { "serviceId": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -261,7 +261,7 @@ }, { "serviceId": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -281,7 +281,7 @@ }, { "serviceId": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -300,7 +300,7 @@ }, { "serviceId": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -321,7 +321,7 @@ }, { "serviceId": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-router:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -333,7 +333,7 @@ }, { "serviceId": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -347,7 +347,7 @@ }, { "serviceId": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -359,7 +359,7 @@ }, { "serviceId": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -370,13 +370,13 @@ }, { "serviceId": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:42338c0", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", "replicas": 1, "env": { - "HWLAB_SKILLS_COMMIT_ID": "b9b625c" + "HWLAB_SKILLS_COMMIT_ID": "42338c0" } } ], diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index 39824804..0c839894 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -31,7 +31,7 @@ "containers": [ { "name": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:42338c0", "ports": [ { "name": "http", @@ -49,15 +49,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "b9b625c" + "value": "42338c0" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b9b625c" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:42338c0" }, { "name": "HWLAB_IMAGE_TAG", - "value": "b9b625c" + "value": "42338c0" }, { "name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN", @@ -175,7 +175,7 @@ "containers": [ { "name": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:42338c0", "ports": [ { "name": "http", @@ -189,15 +189,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "b9b625c" + "value": "42338c0" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b9b625c" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:42338c0" }, { "name": "HWLAB_IMAGE_TAG", - "value": "b9b625c" + "value": "42338c0" } ], "readinessProbe": { @@ -247,7 +247,7 @@ "containers": [ { "name": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:42338c0", "ports": [ { "name": "http", @@ -303,7 +303,7 @@ "containers": [ { "name": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:42338c0", "env": [ { "name": "HWLAB_AGENT_SESSION_MODE", @@ -346,7 +346,7 @@ "containers": [ { "name": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:42338c0", "ports": [ { "name": "http", @@ -407,7 +407,7 @@ "containers": [ { "name": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:42338c0", "ports": [ { "name": "http", @@ -480,7 +480,7 @@ "containers": [ { "name": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:42338c0", "ports": [ { "name": "http", @@ -548,7 +548,7 @@ "containers": [ { "name": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:42338c0", "ports": [ { "name": "http", @@ -612,7 +612,7 @@ "containers": [ { "name": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-router:42338c0", "ports": [ { "name": "http", @@ -672,7 +672,7 @@ "containers": [ { "name": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:42338c0", "ports": [ { "name": "http", @@ -744,7 +744,7 @@ "containers": [ { "name": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:42338c0", "ports": [ { "name": "http", @@ -805,7 +805,7 @@ "containers": [ { "name": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:42338c0", "env": [ { "name": "HWLAB_CLI_ENDPOINT", @@ -847,7 +847,7 @@ "containers": [ { "name": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:42338c0", "ports": [ { "name": "http", @@ -857,7 +857,7 @@ "env": [ { "name": "HWLAB_SKILLS_COMMIT_ID", - "value": "b9b625c" + "value": "42338c0" } ], "readinessProbe": { diff --git a/scripts/deploy-desired-state-plan.test.mjs b/scripts/deploy-desired-state-plan.test.mjs index 8b491415..8a66916d 100644 --- a/scripts/deploy-desired-state-plan.test.mjs +++ b/scripts/deploy-desired-state-plan.test.mjs @@ -1,11 +1,23 @@ import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; +import { promisify } from "node:util"; import { buildDesiredStatePlan } from "./src/deploy-desired-state-plan.mjs"; +const execFileAsync = promisify(execFile); + +async function git(repoRoot, args) { + const result = await execFileAsync("git", args, { + cwd: repoRoot, + maxBuffer: 1024 * 1024 + }); + return result.stdout.trim(); +} + async function makeFixture({ serviceId = "hwlab-cloud-api", commitId = "abc1234", @@ -289,6 +301,44 @@ test("target check blocks when current desired-state is uniformly older", async )); }); +test("target check accepts accepted first-parent main on the checked-out merge commit", async () => { + const repoRoot = await makeFixture(); + await git(repoRoot, ["init", "-b", "main"]); + await git(repoRoot, ["config", "user.email", "test@example.invalid"]); + await git(repoRoot, ["config", "user.name", "HWLAB Test"]); + await git(repoRoot, ["add", "."]); + await git(repoRoot, ["commit", "-m", "base"]); + + const acceptedMain = await git(repoRoot, ["rev-parse", "HEAD"]); + const acceptedTag = acceptedMain.slice(0, 7); + await git(repoRoot, ["checkout", "-b", "desired-state-refresh"]); + + for (const relativePath of [ + "deploy/deploy.json", + "deploy/artifact-catalog.dev.json", + "deploy/k8s/base/workloads.yaml" + ]) { + const filePath = path.join(repoRoot, relativePath); + const raw = await readFile(filePath, "utf8"); + await writeFile(filePath, raw.replaceAll("abc1234", acceptedTag)); + } + await git(repoRoot, ["add", "."]); + await git(repoRoot, ["commit", "-m", "refresh desired-state"]); + await git(repoRoot, ["checkout", "main"]); + await git(repoRoot, ["merge", "--no-ff", "desired-state-refresh", "-m", "merge desired-state refresh"]); + + const mergeCommit = await git(repoRoot, ["rev-parse", "HEAD"]); + const plan = await buildDesiredStatePlan({ repoRoot, targetRef: "HEAD", requireTargetConvergence: true }); + + assert.equal(plan.status, "pass"); + assert.equal(plan.target.commitId, mergeCommit); + assert.equal(plan.target.acceptedCurrentMain.commitId, acceptedMain); + assert.equal(plan.target.comparisonCommitId, acceptedMain); + assert.equal(plan.target.comparisonTag, acceptedTag); + assert.equal(plan.target.convergence.state, "accepted_main_promoted"); + assert.deepEqual(plan.diagnostics, []); +}); + test("promotion commit check blocks when current desired-state is uniformly older", async () => { const repoRoot = await makeFixture(); const plan = await buildDesiredStatePlan({ repoRoot, promotionCommit: "def5678" }); diff --git a/scripts/src/deploy-desired-state-plan.mjs b/scripts/src/deploy-desired-state-plan.mjs index 8cb9ba25..acae3004 100644 --- a/scripts/src/deploy-desired-state-plan.mjs +++ b/scripts/src/deploy-desired-state-plan.mjs @@ -471,6 +471,7 @@ async function resolveTarget(repoRoot, { targetRef, targetTag, promotionCommit, if (!targetRef && !targetTag && !promotionCommit) return null; let commitId = null; let shortCommitId = null; + let acceptedCurrentMain = null; if (targetRef) { try { const result = await execFileAsync("git", ["rev-parse", "--verify", `${targetRef}^{commit}`], { cwd: repoRoot }); @@ -504,6 +505,11 @@ async function resolveTarget(repoRoot, { targetRef, targetTag, promotionCommit, addMismatch(ctx, "promotion_tag_mismatch", "--target-tag", "promotion target tag must be the short promotion commit", promotionCommit.slice(0, 7), targetTag); } const requireConvergence = Boolean(promotionCommit || requireTargetConvergence); + if (targetRef && commitId && requireConvergence && !promotionCommit && !targetTag) { + acceptedCurrentMain = await resolveAcceptedCurrentMain(repoRoot, commitId); + } + const comparisonCommitId = acceptedCurrentMain?.commitId ?? commitId; + const comparisonTag = acceptedCurrentMain?.tag ?? tag; return { targetRef, promotionCommit, @@ -511,12 +517,40 @@ async function resolveTarget(repoRoot, { targetRef, targetTag, promotionCommit, shortCommitId, tag, tagSource: targetTag ? "target-tag" : promotionCommit ? "promotion-commit-short" : "target-ref-short-commit", + acceptedCurrentMain, + comparisonCommitId, + comparisonTag, requireConvergence, convergenceRequirement: promotionCommit ? "promotion-commit" : requireTargetConvergence ? "target-check" : "plan-only" }; } +async function resolveAcceptedCurrentMain(repoRoot, targetCommitId) { + try { + const [{ stdout: headStdout }, { stdout: parentsStdout }] = await Promise.all([ + execFileAsync("git", ["rev-parse", "--verify", "HEAD^{commit}"], { cwd: repoRoot }), + execFileAsync("git", ["rev-list", "--parents", "-n", "1", targetCommitId], { cwd: repoRoot }) + ]); + const headCommitId = headStdout.trim(); + if (headCommitId !== targetCommitId) return null; + + const [commitId, firstParentId, secondParentId] = parentsStdout.trim().split(/\s+/u); + if (commitId !== targetCommitId || !firstParentId || !secondParentId) return null; + + return { + mode: "target-ref-head-merge-first-parent", + reason: "A merged main commit cannot contain desired-state files pinned to its own content hash; compare against the accepted first-parent main commit.", + commitId: firstParentId, + shortCommitId: firstParentId.slice(0, 7), + tag: firstParentId.slice(0, 7) + }; + } catch { + return null; + } +} + function targetLabel(target) { + if (target.acceptedCurrentMain) return `${target.targetRef} accepted first parent (${target.acceptedCurrentMain.tag})`; if (target.targetRef && target.tag) return `${target.targetRef} (${target.tag})`; if (target.promotionCommit) return target.promotionCommit; if (target.tag) return `tag ${target.tag}`; @@ -526,13 +560,13 @@ function targetLabel(target) { function observationMatchesTarget(observation, target) { if (!target) return null; if (observation.kind === "commit") { - return target.commitId ? commitEquivalent(observation.value, target.commitId) : null; + return target.comparisonCommitId ? commitEquivalent(observation.value, target.comparisonCommitId) : null; } if (observation.kind === "tag") { - return target.tag ? observation.value === target.tag : null; + return target.comparisonTag ? observation.value === target.comparisonTag : null; } if (observation.kind === "image") { - return target.tag ? parseImageRef(observation.value).tag === target.tag : null; + return target.comparisonTag ? parseImageRef(observation.value).tag === target.comparisonTag : null; } return null; } @@ -580,8 +614,8 @@ function buildTargetConvergence(ctx, target) { ? "desired-state fields do not match the required promotion commit/tag" : `stale desired-state is not pinned to ${targetLabel(target)}; explicit target checks require current-main/commit-pinned deploy, catalog, workload, and runtime mirror fields`, expected: { - commitId: target.commitId, - imageTag: target.tag + commitId: target.comparisonCommitId, + imageTag: target.comparisonTag }, actual: { pending: pending.length, @@ -593,7 +627,9 @@ function buildTargetConvergence(ctx, target) { const state = partial ? "partial_drift" : pending.length === 0 - ? "already_promoted" + ? target.acceptedCurrentMain + ? "accepted_main_promoted" + : "already_promoted" : promotionRequired ? target.promotionCommit ? "promotion_mismatch" @@ -641,16 +677,17 @@ function reportHints(report, desiredCommitId) { } function servicePromotion(service, target) { - if (!target?.tag) return null; + const targetTag = target?.comparisonTag ?? target?.tag; + if (!targetTag) return null; return { - targetCommitId: target.commitId, - targetImageTag: target.tag, - deployImage: service.deploy?.image ? replaceImageTag(service.deploy.image, target.tag) : null, - catalogImage: service.catalog?.image ? replaceImageTag(service.catalog.image, target.tag) : null, + targetCommitId: target.comparisonCommitId ?? target.commitId, + targetImageTag: targetTag, + deployImage: service.deploy?.image ? replaceImageTag(service.deploy.image, targetTag) : null, + catalogImage: service.catalog?.image ? replaceImageTag(service.catalog.image, targetTag) : null, workloadImages: (service.workloads ?? []).map((workload) => ({ workload: workload.name, container: workload.container, - image: workload.image ? replaceImageTag(workload.image, target.tag) : null + image: workload.image ? replaceImageTag(workload.image, targetTag) : null })) }; } @@ -926,7 +963,7 @@ export async function buildDesiredStatePlan(options = {}) { prod: false, devLiveClaim: false }, - checkSemantics: "--check exits non-zero for missing/invalid desired-state sources, internal commit/image mirror drift, invalid target tags, partial target drift, or any mismatch with an explicit --target-ref, --target-tag, or --promotion-commit. Without --check, a uniform older desired-state under --target-ref/--target-tag is a read-only promotion plan.", + checkSemantics: "--check exits non-zero for missing/invalid desired-state sources, internal commit/image mirror drift, invalid target tags, partial target drift, or any mismatch with an explicit --target-ref, --target-tag, or --promotion-commit. When the checked target ref is the current checked-out merge commit, the comparable current-main target is its accepted first parent because a commit cannot embed its own content hash. Without --check, a uniform older desired-state under --target-ref/--target-tag is a read-only promotion plan.", summary: { desiredCommitId, desiredImageTag,