Merge pull request #259 from pikasTech/fix/deploy-desired-state-postmerge-main

fix: keep desired-state gate valid after main merge
This commit is contained in:
Lyon
2026-05-23 13:59:52 +08:00
committed by GitHub
5 changed files with 181 additions and 94 deletions
+40 -40
View File
@@ -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",
+21 -21
View File
@@ -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"
}
}
],
+20 -20
View File
@@ -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": {
@@ -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" });
+50 -13
View File
@@ -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,