From f0800ca0d96c630d14040122c3ea0f3b5c456b61 Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sun, 24 May 2026 04:58:14 +0000 Subject: [PATCH] fix: allow CI artifact publish outside CD transaction --- scripts/dev-artifact-publish.mjs | 8 ++--- scripts/src/dev-cd-apply.test.mjs | 10 +++--- scripts/src/dev-cd-transaction-guard.mjs | 45 ++++++++++++++++++++++++ scripts/src/dev-deploy-apply.test.mjs | 40 +++++++++++++++++++++ 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/scripts/dev-artifact-publish.mjs b/scripts/dev-artifact-publish.mjs index 102385cd..491cc59b 100644 --- a/scripts/dev-artifact-publish.mjs +++ b/scripts/dev-artifact-publish.mjs @@ -16,7 +16,7 @@ import { resolveDevArtifactServices, serviceInventoryFromServices } from "./src/dev-artifact-services.mjs"; -import { requireDevCdTransactionForSideEffect } from "./src/dev-cd-transaction-guard.mjs"; +import { requireDevArtifactPublishIdentity } from "./src/dev-cd-transaction-guard.mjs"; import { runDevBaseImagePreflight } from "./src/dev-base-image-preflight.mjs"; import { probeRegistryCapabilities } from "./src/registry-capabilities.mjs"; import { inspectCloudWebDistFreshness } from "../web/hwlab-cloud-web/scripts/dist-contract.mjs"; @@ -1429,12 +1429,12 @@ async function main() { return; } if (args.mode === "publish") { - const transactionGuard = requireDevCdTransactionForSideEffect({ + const identityGuard = requireDevArtifactPublishIdentity({ script: "scripts/dev-artifact-publish.mjs", mode: "--publish" }); - if (transactionGuard) { - console.log(JSON.stringify(transactionGuard, null, 2)); + if (identityGuard) { + console.log(JSON.stringify(identityGuard, null, 2)); process.exitCode = 2; return; } diff --git a/scripts/src/dev-cd-apply.test.mjs b/scripts/src/dev-cd-apply.test.mjs index 5e3f3e6d..013cf42f 100644 --- a/scripts/src/dev-cd-apply.test.mjs +++ b/scripts/src/dev-cd-apply.test.mjs @@ -1598,20 +1598,22 @@ test("live verify summarizes 16666 and 16667 health identity", async () => { assert.equal(result.endpoints.every((endpoint) => endpoint.commitMatches), true); }); -test("legacy artifact publish CLI rejects side effects without transaction env", async () => { +test("artifact publish CLI rejects side effects without CI or transaction identity", async () => { await assert.rejects( execFileAsync(process.execPath, ["scripts/dev-artifact-publish.mjs", "--publish", "--no-report"], { cwd: repoRoot, env: { - ...process.env, - HWLAB_CD_TRANSACTION_ID: "" + HWLAB_CD_TRANSACTION_ID: "", + HWLAB_CI_ARTIFACT_RUN_ID: "" } }), (error) => { assert.equal(error.code, 2); const payload = JSON.parse(error.stdout); - assert.equal(payload.error, "cd-transaction-required"); + assert.equal(payload.error, "artifact-publish-identity-required"); assert.equal(payload.script, "scripts/dev-artifact-publish.mjs"); + assert.equal(payload.cdLockRequired, false); + assert.equal(payload.rolloutAttempted, false); assert.equal(payload.mutationAttempted, false); return true; } diff --git a/scripts/src/dev-cd-transaction-guard.mjs b/scripts/src/dev-cd-transaction-guard.mjs index adb74715..5b34dd14 100644 --- a/scripts/src/dev-cd-transaction-guard.mjs +++ b/scripts/src/dev-cd-transaction-guard.mjs @@ -6,10 +6,26 @@ const transactionEnvNames = [ "HWLAB_CD_LOCK_NAME" ]; +const ciArtifactEnvNames = [ + "HWLAB_CI_ARTIFACT_RUN_ID", + "GITHUB_RUN_ID", + "CI_PIPELINE_ID", + "BUILDKITE_BUILD_ID" +]; + +const artifactPublishIdentityEnvNames = [ + ...ciArtifactEnvNames, + ...transactionEnvNames +]; + export function hasDevCdTransactionEnv(env = process.env) { return typeof env.HWLAB_CD_TRANSACTION_ID === "string" && env.HWLAB_CD_TRANSACTION_ID.trim().length > 0; } +export function hasCiArtifactPublishEnv(env = process.env) { + return ciArtifactEnvNames.some((name) => typeof env[name] === "string" && env[name].trim().length > 0); +} + export function devCdTransactionGuardFailure({ script, mode, @@ -32,7 +48,36 @@ export function devCdTransactionGuardFailure({ }; } +export function devArtifactPublishIdentityGuardFailure({ + script, + mode, + requiredEnv = "HWLAB_CI_ARTIFACT_RUN_ID or HWLAB_CD_TRANSACTION_ID" +}) { + return { + ok: false, + status: "blocked", + error: "artifact-publish-identity-required", + code: "artifact-publish-identity-required", + script, + mode, + requiredEnv, + acceptedEnv: artifactPublishIdentityEnvNames, + entrypoint: `HWLAB_CI_ARTIFACT_RUN_ID= node scripts/dev-artifact-publish.mjs --publish --report ${tempReportPath("dev-artifacts.json")}`, + summary: `${script} ${mode} is a DEV artifact publish side-effect step and must run under CI artifact identity, or the legacy DEV CD transaction path.`, + devOnly: true, + prodTouched: false, + cdLockRequired: false, + rolloutAttempted: false, + mutationAttempted: false + }; +} + export function requireDevCdTransactionForSideEffect({ env = process.env, script, mode }) { if (hasDevCdTransactionEnv(env)) return null; return devCdTransactionGuardFailure({ script, mode }); } + +export function requireDevArtifactPublishIdentity({ env = process.env, script, mode }) { + if (hasCiArtifactPublishEnv(env) || hasDevCdTransactionEnv(env)) return null; + return devArtifactPublishIdentityGuardFailure({ script, mode }); +} diff --git a/scripts/src/dev-deploy-apply.test.mjs b/scripts/src/dev-deploy-apply.test.mjs index 706479f9..7f21d0dc 100644 --- a/scripts/src/dev-deploy-apply.test.mjs +++ b/scripts/src/dev-deploy-apply.test.mjs @@ -15,8 +15,11 @@ import { resolveDevKubeconfigSelection } from "./dev-deploy-apply.mjs"; import { + devArtifactPublishIdentityGuardFailure, devCdTransactionGuardFailure, + hasCiArtifactPublishEnv, hasDevCdTransactionEnv, + requireDevArtifactPublishIdentity, requireDevCdTransactionForSideEffect } from "./dev-cd-transaction-guard.mjs"; @@ -141,6 +144,43 @@ test("DEV CD transaction guard rejects direct legacy side-effect calls", () => { ); }); +test("DEV artifact publish guard allows CI identity without CD transaction", () => { + assert.equal(hasCiArtifactPublishEnv({}), false); + assert.equal(hasCiArtifactPublishEnv({ HWLAB_CI_ARTIFACT_RUN_ID: "ci-123" }), true); + + const failure = requireDevArtifactPublishIdentity({ + env: {}, + script: "scripts/dev-artifact-publish.mjs", + mode: "--publish" + }); + assert.deepEqual(failure, devArtifactPublishIdentityGuardFailure({ + script: "scripts/dev-artifact-publish.mjs", + mode: "--publish" + })); + assert.equal(failure.error, "artifact-publish-identity-required"); + assert.equal(failure.cdLockRequired, false); + assert.equal(failure.rolloutAttempted, false); + assert.equal(failure.mutationAttempted, false); + assert.match(failure.entrypoint, /HWLAB_CI_ARTIFACT_RUN_ID=.*dev-artifact-publish\.mjs/u); + + assert.equal( + requireDevArtifactPublishIdentity({ + env: { HWLAB_CI_ARTIFACT_RUN_ID: "ci-123" }, + script: "scripts/dev-artifact-publish.mjs", + mode: "--publish" + }), + null + ); + assert.equal( + requireDevArtifactPublishIdentity({ + env: { HWLAB_CD_TRANSACTION_ID: "tx-123" }, + script: "scripts/dev-artifact-publish.mjs", + mode: "--publish" + }), + null + ); +}); + test("matching allowlisted suspended template Job image does not replace", () => { const image = "127.0.0.1:5000/hwlab/hwlab-cli:73b379f"; const decision = decideDevTemplateJobReplacement({