From 747e49955d0cadef4afe82dc0c8d383dbbebeada Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:08:25 +0900 Subject: [PATCH 01/12] ci(test): bound Vitest workers on hosted runners --- vitest.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vitest.config.ts b/vitest.config.ts index 198e3dcb8..31c4f5d89 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,6 +3,10 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { include: ["src/**/*.test.ts"], + // GitHub-hosted Ubuntu runners can expose enough logical CPUs for Vitest's + // default forks pool to exceed the job's memory budget. Keep file isolation + // and the complete test set; only bound concurrent worker processes in CI. + maxWorkers: process.env.CI ? 2 : undefined, coverage: { provider: "v8", // ponytail: 커버리지는 헤드리스로 검증 가능한 순수 로직과 mockable Tauri API 래퍼만 측정. From e73c5494d0376c724994294eba3c662b6bcbe73e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:08:36 +0900 Subject: [PATCH 02/12] test(ci): guard Vitest worker budget --- src/lib/vitestWorkerBudgetContract.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/lib/vitestWorkerBudgetContract.test.ts diff --git a/src/lib/vitestWorkerBudgetContract.test.ts b/src/lib/vitestWorkerBudgetContract.test.ts new file mode 100644 index 000000000..319946825 --- /dev/null +++ b/src/lib/vitestWorkerBudgetContract.test.ts @@ -0,0 +1,22 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); + +/** Read the Vitest configuration with stable line endings for worker-budget assertions. */ +function readVitestConfig(): string { + return readFileSync(resolve(repositoryRoot, 'vitest.config.ts'), 'utf8').replace(/\r\n?/g, '\n'); +} + +describe('Vitest hosted-runner worker budget', () => { + it('bounds CI worker concurrency without disabling file isolation or test files', () => { + const config = readVitestConfig(); + + expect(config).toContain('maxWorkers: process.env.CI ? 2 : undefined'); + expect(config).not.toContain('fileParallelism: false'); + expect(config).not.toContain('pool: "threads"'); + expect(config).not.toContain("pool: 'threads'"); + }); +}); From e26d8e3d51f3273b247d1de83814c8461c89ca74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:44:39 +0900 Subject: [PATCH 03/12] ci(actions): bound superseded pull request runs Normalize validation concurrency and keep release work non-cancelling. Add a focused workflow contract. Signed-off-by: Seongho Bae Commit-Message-Assisted-by: Claude (via Claude Code) --- .github/workflows/release.yml | 6 ++---- .github/workflows/test.yml | 4 ++++ .../ci/workflow-concurrency-contract.test.mjs | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 scripts/ci/workflow-concurrency-contract.test.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index daafc942d..abe6b9839 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,10 +14,8 @@ on: workflow_dispatch: concurrency: - group: release-${{ github.workflow }}-${{ github.ref }} - # A fresh first attempt supersedes stale work, while GitHub's explicit - # rerun attempt must not cancel itself inside the same concurrency group. - cancel-in-progress: ${{ github.run_attempt == 1 }} + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: false permissions: contents: read diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b35c94808..99cfb4bfa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: test: runs-on: ubuntu-latest diff --git a/scripts/ci/workflow-concurrency-contract.test.mjs b/scripts/ci/workflow-concurrency-contract.test.mjs new file mode 100644 index 000000000..fc8cb8591 --- /dev/null +++ b/scripts/ci/workflow-concurrency-contract.test.mjs @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +const workflow = (name) => readFileSync(new URL(`../../.github/workflows/${name}`, import.meta.url), "utf8"); + +test("PR validation cancels only superseded heads from the same repository PR", () => { + const source = workflow("test.yml"); + assert.match(source, /group: \$\{\{ github\.workflow \}\}-\$\{\{ github\.repository \}\}-\$\{\{ github\.event\.pull_request\.number \|\| github\.run_id \}\}/); + assert.match(source, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/); +}); + +test("release work is never cancelled", () => { + const source = workflow("release.yml"); + assert.match(source, /group: \$\{\{ github\.workflow \}\}-\$\{\{ github\.repository \}\}-\$\{\{ github\.event\.pull_request\.number \|\| github\.run_id \}\}/); + assert.match(source, /cancel-in-progress: false/); +}); From 23bb1b61e1f2445068856e572feb968ee3bc3ce4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:00:09 +0900 Subject: [PATCH 04/12] fix(ci): keep explicit reruns from cancelling newer PR validation --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99cfb4bfa..f6d7c1df9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ permissions: concurrency: group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' && github.run_attempt == 1 }} jobs: test: From 58b181204b0069a1f1d9e5e94f2c56c6d44d7474 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:00:30 +0900 Subject: [PATCH 05/12] test(ci): cover rerun-safe workflow concurrency --- scripts/ci/workflow-concurrency-contract.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ci/workflow-concurrency-contract.test.mjs b/scripts/ci/workflow-concurrency-contract.test.mjs index fc8cb8591..674eab846 100644 --- a/scripts/ci/workflow-concurrency-contract.test.mjs +++ b/scripts/ci/workflow-concurrency-contract.test.mjs @@ -4,10 +4,10 @@ import test from "node:test"; const workflow = (name) => readFileSync(new URL(`../../.github/workflows/${name}`, import.meta.url), "utf8"); -test("PR validation cancels only superseded heads from the same repository PR", () => { +test("PR validation cancels only superseded first-attempt heads from the same repository PR", () => { const source = workflow("test.yml"); assert.match(source, /group: \$\{\{ github\.workflow \}\}-\$\{\{ github\.repository \}\}-\$\{\{ github\.event\.pull_request\.number \|\| github\.run_id \}\}/); - assert.match(source, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/); + assert.match(source, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' && github\.run_attempt == 1 \}\}/); }); test("release work is never cancelled", () => { From c1973622018d90a52f66f70cf4ddc47281410949 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:00:39 +0900 Subject: [PATCH 06/12] test(ci): run workflow concurrency contract in npm test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index efb3e6b47..51bdc3756 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "test": "svelte-kit sync && vitest run", + "test": "svelte-kit sync && vitest run && node --test scripts/ci/workflow-concurrency-contract.test.mjs", "coverage": "svelte-kit sync && vitest run --coverage", "prepare": "svelte-kit sync || echo ''", "tauri": "tauri" From 04339f96263637cd256ea416e1383e1129fa8db6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:00:47 +0900 Subject: [PATCH 07/12] test(ci): pin Vitest production test scope --- src/lib/vitestWorkerBudgetContract.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/vitestWorkerBudgetContract.test.ts b/src/lib/vitestWorkerBudgetContract.test.ts index 319946825..69e8cf9dc 100644 --- a/src/lib/vitestWorkerBudgetContract.test.ts +++ b/src/lib/vitestWorkerBudgetContract.test.ts @@ -15,6 +15,7 @@ describe('Vitest hosted-runner worker budget', () => { const config = readVitestConfig(); expect(config).toContain('maxWorkers: process.env.CI ? 2 : undefined'); + expect(config).toContain('include: ["src/**/*.test.ts"]'); expect(config).not.toContain('fileParallelism: false'); expect(config).not.toContain('pool: "threads"'); expect(config).not.toContain("pool: 'threads'"); From 756de7b807ed355dffcba11885afc78be7843d05 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:00:57 +0900 Subject: [PATCH 08/12] test(release): align retry contract with non-cancelling builds --- src/lib/releaseWorkflowRetryContract.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/releaseWorkflowRetryContract.test.ts b/src/lib/releaseWorkflowRetryContract.test.ts index db1ef0a31..8be4eceaa 100644 --- a/src/lib/releaseWorkflowRetryContract.test.ts +++ b/src/lib/releaseWorkflowRetryContract.test.ts @@ -11,9 +11,9 @@ function readRepositoryFile(relativePath: string): string { } describe('release workflow retry contract', () => { - it('cancels stale first attempts without self-cancelling explicit reruns', () => { + it('preserves in-flight release builds across newer heads and explicit reruns', () => { const workflow = readRepositoryFile('.github/workflows/release.yml'); - expect(workflow).toContain("cancel-in-progress: ${{ github.run_attempt == 1 }}"); + expect(workflow).toContain('cancel-in-progress: false'); expect(workflow).not.toContain('cancel-in-progress: true'); }); @@ -27,11 +27,11 @@ describe('release workflow retry contract', () => { ).toBe(3); }); - it('documents retry-safe concurrency in authoritative evidence', () => { + it('documents non-cancelling release concurrency in authoritative evidence', () => { const doctoring = readRepositoryFile('docs/doctoring/release-artifact-provenance.md'); const changelog = readRepositoryFile('CHANGELOG.md'); - expect(doctoring).toContain('explicit rerun attempts do not cancel themselves'); - expect(doctoring).toContain('github.run_attempt == 1'); + expect(doctoring).toContain('release concurrency never cancels an in-flight build'); + expect(doctoring).toContain('cancel-in-progress: false'); expect(changelog).toContain('retry-safe release concurrency'); }); -}); \ No newline at end of file +}); From 96ecdbef8eea5c51b45c533e9960fcaad39e154b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:01:30 +0900 Subject: [PATCH 09/12] docs(release): document non-cancelling release concurrency --- docs/doctoring/release-artifact-provenance.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md index 296a0246d..da169b24a 100644 --- a/docs/doctoring/release-artifact-provenance.md +++ b/docs/doctoring/release-artifact-provenance.md @@ -19,7 +19,7 @@ The authoritative implementation is `.github/workflows/release.yml`. The release contract requires all of the following: - checkout binds every platform build to `github.event.pull_request.head.sha` for pull requests and `github.sha` for tags or manual runs, rather than silently treating a generated pull-request merge ref as exact-head evidence; -- release concurrency uses `github.run_attempt == 1`, so a fresh first attempt supersedes stale work while explicit rerun attempts do not cancel themselves inside the same concurrency group; +- release concurrency never cancels an in-flight build: `cancel-in-progress: false` preserves already-started release work across newer heads and explicit reruns, while Test PR supersession is governed separately by `.github/workflows/test.yml`; - the three platform builds upload the exact bundle and operational CLI paths that later jobs consume; - release workflow artifacts use the `release-disksage-*` namespace, which excludes concurrently uploaded `disksage-gpu-*` diagnostic bundles; - attestation and publication downloads preserve each workflow artifact in its own directory instead of flattening archives, so duplicate basenames remain observable and last-writer-wins extraction cannot erase evidence before admission; @@ -61,7 +61,7 @@ Retain the artifact, the downloaded bundle, the release tag, the source commit S The pipeline fails closed when an expected platform bundle, operational CLI, checksum file, or source-bound SBOM is absent or duplicated. Artifact namespaces remain separate during download, so the same required filename contributed by two platform archives remains two filesystem entries and is rejected rather than silently overwritten. It validates checksum-record semantics and digests first so an invalid or redirected record receives the specific actionable diagnostic, then rejects any nineteenth regular file and every non-regular filesystem entry before attestation or publication. This exact-set rule prevents a build step from silently adding an unreviewed diagnostic archive, crash dump, log, secret-bearing output, or unrelated executable to the release. Path-scoped checks distinguish the Windows NSIS installer from the two separately shipped Windows operational CLI executables. The pipeline also fails when a checksum record names a file other than its adjacent operational CLI, contains additional fields or records, or presents a malformed digest. A checksum mismatch or malformed/private-path SBOM stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. -Concurrency cancellation applies only to a first workflow attempt. A newer first attempt may cancel stale work for the same ref, but an explicit rerun has `github.run_attempt > 1` and therefore cannot cancel itself. A rerun remains non-authoritative until every required exact-head job in that attempt completes successfully. +Release concurrency never cancels an in-flight build. `cancel-in-progress: false` deliberately lets a started release attempt finish even if a newer head appears or an explicit rerun is requested. Each attempt remains non-authoritative until every required exact-head job for that run completes successfully; completion of an older attempt does not transfer evidence to a newer head. Attestations bind artifact digests, not mutable filenames. Rebuilding the same version produces different bytes and therefore requires new exact-build attestations. Evidence from an earlier workflow run or commit must never authorize publication of a later head. From b6868c7ebfe25cb547c0cae77943be1875aeb3ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:31:26 +0900 Subject: [PATCH 10/12] fix(release): align Windows artifact directory contract Signed-off-by: Seongho Bae --- .github/scripts/verify-release-artifacts.sh | 2 +- src/lib/releaseWorkflowRetryContract.test.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/scripts/verify-release-artifacts.sh b/.github/scripts/verify-release-artifacts.sh index b35a74651..5d891302e 100644 --- a/.github/scripts/verify-release-artifacts.sh +++ b/.github/scripts/verify-release-artifacts.sh @@ -33,7 +33,7 @@ require_exactly_one_file() { expected_dirs=( "release-disksage-ubuntu-22.04-${run_attempt}" - "release-disksage-windows-latest-${run_attempt}" + "release-disksage-windows-2022-${run_attempt}" "release-disksage-macos-latest-${run_attempt}" ) diff --git a/src/lib/releaseWorkflowRetryContract.test.ts b/src/lib/releaseWorkflowRetryContract.test.ts index 8be4eceaa..e27db0431 100644 --- a/src/lib/releaseWorkflowRetryContract.test.ts +++ b/src/lib/releaseWorkflowRetryContract.test.ts @@ -19,12 +19,15 @@ describe('release workflow retry contract', () => { it('binds upload and download artifact names to the current rerun attempt', () => { const workflow = readRepositoryFile('.github/workflows/release.yml'); + const verifier = readRepositoryFile('.github/scripts/verify-release-artifacts.sh'); expect(workflow).toContain( 'name: release-disksage-${{ matrix.os }}-${{ github.run_attempt }}', ); expect( workflow.split('pattern: release-disksage-*-${{ github.run_attempt }}').length - 1, ).toBe(3); + expect(verifier).toContain('release-disksage-windows-2022-${run_attempt}'); + expect(verifier).not.toContain('release-disksage-windows-latest-${run_attempt}'); }); it('documents non-cancelling release concurrency in authoritative evidence', () => { From 0f4c20b88ad319b44b078b3eec0d7f6baa292380 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 04:37:39 +0900 Subject: [PATCH 11/12] fix(actions): cancel superseded PR release builds Keep tag and manual releases isolated by run ID while replacing stale release validation for the same repository pull request.\n\nCo-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .github/workflows/release.yml | 2 +- docs/doctoring/release-artifact-provenance.md | 4 ++-- scripts/ci/workflow-concurrency-contract.test.mjs | 4 ++-- src/lib/releaseWorkflowRetryContract.test.ts | 13 +++++++------ 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index abe6b9839..b404771cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: false + cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: contents: read diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md index da169b24a..ab254f6b2 100644 --- a/docs/doctoring/release-artifact-provenance.md +++ b/docs/doctoring/release-artifact-provenance.md @@ -19,7 +19,7 @@ The authoritative implementation is `.github/workflows/release.yml`. The release contract requires all of the following: - checkout binds every platform build to `github.event.pull_request.head.sha` for pull requests and `github.sha` for tags or manual runs, rather than silently treating a generated pull-request merge ref as exact-head evidence; -- release concurrency never cancels an in-flight build: `cancel-in-progress: false` preserves already-started release work across newer heads and explicit reruns, while Test PR supersession is governed separately by `.github/workflows/test.yml`; +- release concurrency uses `github.event_name == 'pull_request'`: a newer run for the same repository PR cancels its superseded pull-request build, while tag and manual release runs use unique run IDs and are never cancelled by PR activity; - the three platform builds upload the exact bundle and operational CLI paths that later jobs consume; - release workflow artifacts use the `release-disksage-*` namespace, which excludes concurrently uploaded `disksage-gpu-*` diagnostic bundles; - attestation and publication downloads preserve each workflow artifact in its own directory instead of flattening archives, so duplicate basenames remain observable and last-writer-wins extraction cannot erase evidence before admission; @@ -61,7 +61,7 @@ Retain the artifact, the downloaded bundle, the release tag, the source commit S The pipeline fails closed when an expected platform bundle, operational CLI, checksum file, or source-bound SBOM is absent or duplicated. Artifact namespaces remain separate during download, so the same required filename contributed by two platform archives remains two filesystem entries and is rejected rather than silently overwritten. It validates checksum-record semantics and digests first so an invalid or redirected record receives the specific actionable diagnostic, then rejects any nineteenth regular file and every non-regular filesystem entry before attestation or publication. This exact-set rule prevents a build step from silently adding an unreviewed diagnostic archive, crash dump, log, secret-bearing output, or unrelated executable to the release. Path-scoped checks distinguish the Windows NSIS installer from the two separately shipped Windows operational CLI executables. The pipeline also fails when a checksum record names a file other than its adjacent operational CLI, contains additional fields or records, or presents a malformed digest. A checksum mismatch or malformed/private-path SBOM stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. -Release concurrency never cancels an in-flight build. `cancel-in-progress: false` deliberately lets a started release attempt finish even if a newer head appears or an explicit rerun is requested. Each attempt remains non-authoritative until every required exact-head job for that run completes successfully; completion of an older attempt does not transfer evidence to a newer head. +Release concurrency cancels an existing run only when the incoming event is a pull request in the same workflow, repository, and PR-number group. Tag and manual runs use `github.run_id`, so they cannot cancel each other or be cancelled by PR activity. An explicit PR rerun replaces the prior attempt in that PR group; each attempt remains non-authoritative until every required exact-head job for that run completes successfully. Attestations bind artifact digests, not mutable filenames. Rebuilding the same version produces different bytes and therefore requires new exact-build attestations. Evidence from an earlier workflow run or commit must never authorize publication of a later head. diff --git a/scripts/ci/workflow-concurrency-contract.test.mjs b/scripts/ci/workflow-concurrency-contract.test.mjs index 674eab846..7bdde94c0 100644 --- a/scripts/ci/workflow-concurrency-contract.test.mjs +++ b/scripts/ci/workflow-concurrency-contract.test.mjs @@ -10,8 +10,8 @@ test("PR validation cancels only superseded first-attempt heads from the same re assert.match(source, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' && github\.run_attempt == 1 \}\}/); }); -test("release work is never cancelled", () => { +test("release validation cancels only superseded runs from the same pull request", () => { const source = workflow("release.yml"); assert.match(source, /group: \$\{\{ github\.workflow \}\}-\$\{\{ github\.repository \}\}-\$\{\{ github\.event\.pull_request\.number \|\| github\.run_id \}\}/); - assert.match(source, /cancel-in-progress: false/); + assert.match(source, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/); }); diff --git a/src/lib/releaseWorkflowRetryContract.test.ts b/src/lib/releaseWorkflowRetryContract.test.ts index e27db0431..9ed6ff228 100644 --- a/src/lib/releaseWorkflowRetryContract.test.ts +++ b/src/lib/releaseWorkflowRetryContract.test.ts @@ -11,10 +11,11 @@ function readRepositoryFile(relativePath: string): string { } describe('release workflow retry contract', () => { - it('preserves in-flight release builds across newer heads and explicit reruns', () => { + it('cancels superseded PR builds while preserving tag and manual releases', () => { const workflow = readRepositoryFile('.github/workflows/release.yml'); - expect(workflow).toContain('cancel-in-progress: false'); - expect(workflow).not.toContain('cancel-in-progress: true'); + expect(workflow).toContain( + "cancel-in-progress: ${{ github.event_name == 'pull_request' }}", + ); }); it('binds upload and download artifact names to the current rerun attempt', () => { @@ -30,11 +31,11 @@ describe('release workflow retry contract', () => { expect(verifier).not.toContain('release-disksage-windows-latest-${run_attempt}'); }); - it('documents non-cancelling release concurrency in authoritative evidence', () => { + it('documents trigger-aware release concurrency in authoritative evidence', () => { const doctoring = readRepositoryFile('docs/doctoring/release-artifact-provenance.md'); const changelog = readRepositoryFile('CHANGELOG.md'); - expect(doctoring).toContain('release concurrency never cancels an in-flight build'); - expect(doctoring).toContain('cancel-in-progress: false'); + expect(doctoring).toContain('superseded pull-request build'); + expect(doctoring).toContain("github.event_name == 'pull_request'"); expect(changelog).toContain('retry-safe release concurrency'); }); }); From bbe5cc2375a7f83a4a174a2f799dee904f2c9e4a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 05:12:00 +0900 Subject: [PATCH 12/12] fix(actions): preserve explicit release reruns --- .github/workflows/release.yml | 2 +- docs/doctoring/release-artifact-provenance.md | 2 +- scripts/ci/workflow-concurrency-contract.test.mjs | 4 ++-- src/lib/releaseWorkflowRetryContract.test.ts | 5 +++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b404771cd..b962e9ba1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' && github.run_attempt == 1 }} permissions: contents: read diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md index ab254f6b2..8f479ee40 100644 --- a/docs/doctoring/release-artifact-provenance.md +++ b/docs/doctoring/release-artifact-provenance.md @@ -61,7 +61,7 @@ Retain the artifact, the downloaded bundle, the release tag, the source commit S The pipeline fails closed when an expected platform bundle, operational CLI, checksum file, or source-bound SBOM is absent or duplicated. Artifact namespaces remain separate during download, so the same required filename contributed by two platform archives remains two filesystem entries and is rejected rather than silently overwritten. It validates checksum-record semantics and digests first so an invalid or redirected record receives the specific actionable diagnostic, then rejects any nineteenth regular file and every non-regular filesystem entry before attestation or publication. This exact-set rule prevents a build step from silently adding an unreviewed diagnostic archive, crash dump, log, secret-bearing output, or unrelated executable to the release. Path-scoped checks distinguish the Windows NSIS installer from the two separately shipped Windows operational CLI executables. The pipeline also fails when a checksum record names a file other than its adjacent operational CLI, contains additional fields or records, or presents a malformed digest. A checksum mismatch or malformed/private-path SBOM stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. -Release concurrency cancels an existing run only when the incoming event is a pull request in the same workflow, repository, and PR-number group. Tag and manual runs use `github.run_id`, so they cannot cancel each other or be cancelled by PR activity. An explicit PR rerun replaces the prior attempt in that PR group; each attempt remains non-authoritative until every required exact-head job for that run completes successfully. +Release concurrency cancels an existing run only when the incoming event is a first-attempt pull request (`github.event_name == 'pull_request' && github.run_attempt == 1`) in the same workflow, repository, and PR-number group. Tag and manual runs use `github.run_id`, so they cannot cancel each other or be cancelled by PR activity. Partial reruns are unsupported because platform artifacts are attempt-scoped; use **Re-run all jobs** so every required platform artifact is rebuilt under one attempt. Each attempt remains non-authoritative until every required exact-head job for that run completes successfully. Attestations bind artifact digests, not mutable filenames. Rebuilding the same version produces different bytes and therefore requires new exact-build attestations. Evidence from an earlier workflow run or commit must never authorize publication of a later head. diff --git a/scripts/ci/workflow-concurrency-contract.test.mjs b/scripts/ci/workflow-concurrency-contract.test.mjs index 7bdde94c0..dc652d976 100644 --- a/scripts/ci/workflow-concurrency-contract.test.mjs +++ b/scripts/ci/workflow-concurrency-contract.test.mjs @@ -10,8 +10,8 @@ test("PR validation cancels only superseded first-attempt heads from the same re assert.match(source, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' && github\.run_attempt == 1 \}\}/); }); -test("release validation cancels only superseded runs from the same pull request", () => { +test("release validation cancels only superseded first-attempt runs from the same pull request", () => { const source = workflow("release.yml"); assert.match(source, /group: \$\{\{ github\.workflow \}\}-\$\{\{ github\.repository \}\}-\$\{\{ github\.event\.pull_request\.number \|\| github\.run_id \}\}/); - assert.match(source, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/); + assert.match(source, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' && github\.run_attempt == 1 \}\}/); }); diff --git a/src/lib/releaseWorkflowRetryContract.test.ts b/src/lib/releaseWorkflowRetryContract.test.ts index 9ed6ff228..fc365d6fb 100644 --- a/src/lib/releaseWorkflowRetryContract.test.ts +++ b/src/lib/releaseWorkflowRetryContract.test.ts @@ -14,7 +14,7 @@ describe('release workflow retry contract', () => { it('cancels superseded PR builds while preserving tag and manual releases', () => { const workflow = readRepositoryFile('.github/workflows/release.yml'); expect(workflow).toContain( - "cancel-in-progress: ${{ github.event_name == 'pull_request' }}", + "cancel-in-progress: ${{ github.event_name == 'pull_request' && github.run_attempt == 1 }}", ); }); @@ -35,7 +35,8 @@ describe('release workflow retry contract', () => { const doctoring = readRepositoryFile('docs/doctoring/release-artifact-provenance.md'); const changelog = readRepositoryFile('CHANGELOG.md'); expect(doctoring).toContain('superseded pull-request build'); - expect(doctoring).toContain("github.event_name == 'pull_request'"); + expect(doctoring).toContain("github.event_name == 'pull_request' && github.run_attempt == 1"); + expect(doctoring).toContain('Re-run all jobs'); expect(changelog).toContain('retry-safe release concurrency'); }); });