From 60f953888603811e2bb6c38549d95f51a4aff7a4 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 21 Sep 2026 21:09:53 +0300 Subject: [PATCH] fix(ci): resume verification of immutable release images TD-009: retry verification of an existing digest without republishing. Paired backend workflow fix: 82eccc3. --- .gitea/workflows/image.yml | 14 +++--- scripts/check-image-publication.cjs | 75 +++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 scripts/check-image-publication.cjs diff --git a/.gitea/workflows/image.yml b/.gitea/workflows/image.yml index 67ce36c..d802cea 100644 --- a/.gitea/workflows/image.yml +++ b/.gitea/workflows/image.yml @@ -45,15 +45,17 @@ jobs: run: | VERSION="${RELEASE_TAG#v}" if docker manifest inspect "$IMAGE:$VERSION" >/dev/null 2>manifest-error.log; then - echo 'Refusing to overwrite an existing release tag'; exit 1 + DIGEST="$(docker buildx imagetools inspect "$IMAGE:$VERSION" --format '{{.Manifest.Digest}}')" + else + if ! grep -Eq 'manifest unknown|no such manifest' manifest-error.log; then + echo 'Cannot prove tag absence; refusing publication'; exit 1 + fi + docker buildx build --target production --build-arg VERSION="$VERSION" --build-arg REVISION="$REVISION" --tag "$IMAGE:$VERSION" --metadata-file image-metadata.json --push . + DIGEST="$(node --input-type=commonjs -p 'require("./image-metadata.json")["containerimage.digest"]')" fi - if ! grep -Eq 'manifest unknown|no such manifest' manifest-error.log; then - echo 'Cannot prove tag absence; refusing publication'; exit 1 - fi - docker buildx build --target production --build-arg VERSION="$VERSION" --build-arg REVISION="$REVISION" --tag "$IMAGE:$VERSION" --metadata-file image-metadata.json --push . - DIGEST="$(node --input-type=commonjs -p 'require("./image-metadata.json")["containerimage.digest"]')" docker pull "$IMAGE@$DIGEST" test "$(docker image inspect "$IMAGE@$DIGEST" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$REVISION" + test "$(docker image inspect "$IMAGE@$DIGEST" --format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = "$VERSION" docker run --rm --entrypoint sh "$IMAGE@$DIGEST" -c 'test -s /usr/share/nginx/html/index.html && nginx -t && nginx && wget -q -O /dev/null http://127.0.0.1:8080/' printf 'Frontend image: %s@%s\n' "$IMAGE" "$DIGEST" - name: Remove registry session diff --git a/scripts/check-image-publication.cjs b/scripts/check-image-publication.cjs new file mode 100644 index 0000000..d0fda81 --- /dev/null +++ b/scripts/check-image-publication.cjs @@ -0,0 +1,75 @@ +const { test } = require("node:test"); +const assert = require("node:assert/strict"); +const { + mkdtempSync, + readFileSync, + writeFileSync, + chmodSync, + rmSync, +} = require("node:fs"); +const { tmpdir } = require("node:os"); +const { join, resolve } = require("node:path"); +const EXECUTABLE_MODE = 0o755; +const { spawnSync } = require("node:child_process"); + +test("immutable publication resumes exact digest checks after post-push failure", () => { + const directory = mkdtempSync(join(tmpdir(), "wl-image-retry-")); + const workflow = readFileSync( + resolve(__dirname, "../.gitea/workflows/image.yml"), + "utf8", + ); + const block = workflow + .split(" - name: Publish immutable release tag\n run: |\n")[1] + .split(" - name:")[0] + .replace(/^ {10}/gm, ""); + const double = `#!${process.execPath} +const fs = require('node:fs'); +const args = process.argv.slice(2).join(' '); +fs.appendFileSync('calls', args + '\\n'); +if (args.startsWith('manifest inspect')) { + if (!fs.existsSync('published')) { console.error('manifest unknown'); process.exit(1); } +} else if (args.startsWith('buildx imagetools inspect')) { + console.log('sha256:exact'); +} else if (args.startsWith('buildx build')) { + fs.writeFileSync('published', 'yes'); + fs.writeFileSync('image-metadata.json', JSON.stringify({'containerimage.digest': 'sha256:exact'})); +} else if (args.startsWith('pull')) { + if (!fs.existsSync('pull-failed')) { fs.writeFileSync('pull-failed', 'yes'); process.exit(1); } +} else if (args.startsWith('image inspect')) { + console.log(args.includes('image.version') ? '1.2.3' : process.env.BAD_REVISION ? 'other' : 'commit'); +} else if (args.startsWith('run') && process.env.BAD_SMOKE) process.exit(1); +`; + try { + for (const name of ["docker", "npm"]) { + writeFileSync(join(directory, name), double); + chmodSync(join(directory, name), EXECUTABLE_MODE); + } + const run = (extra = {}) => + spawnSync("/bin/sh", ["-ec", block], { + cwd: directory, + encoding: "utf8", + env: { + ...process.env, + PATH: `${directory}:${process.env.PATH}`, + IMAGE: "synthetic/image", + RELEASE_TAG: "v1.2.3", + REVISION: "commit", + ...extra, + }, + }); + assert.notEqual(run().status, 0); + const retry = run(); + assert.equal(retry.status, 0, retry.stdout + retry.stderr); + assert.notEqual(run({ BAD_REVISION: "1" }).status, 0); + assert.notEqual(run({ BAD_SMOKE: "1" }).status, 0); + const calls = readFileSync(join(directory, "calls"), "utf8"); + assert.equal( + calls.split("\n").filter((line) => line.startsWith("buildx build")) + .length, + 1, + ); + assert.ok(calls.includes("synthetic/image@sha256:exact")); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +});