Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/verify-release-artifacts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
)

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' && github.run_attempt == 1 }}

jobs:
test:
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions docs/doctoring/release-artifact-provenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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;
Expand Down Expand Up @@ -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 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.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
17 changes: 17 additions & 0 deletions scripts/ci/workflow-concurrency-contract.test.mjs
Original file line number Diff line number Diff line change
@@ -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 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' && github\.run_attempt == 1 \}\}/);
});

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' && github\.run_attempt == 1 \}\}/);
});
19 changes: 12 additions & 7 deletions src/lib/releaseWorkflowRetryContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,32 @@ function readRepositoryFile(relativePath: string): string {
}

describe('release workflow retry contract', () => {
it('cancels stale first attempts without self-cancelling 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: ${{ github.run_attempt == 1 }}");
expect(workflow).not.toContain('cancel-in-progress: true');
expect(workflow).toContain(
"cancel-in-progress: ${{ github.event_name == 'pull_request' && github.run_attempt == 1 }}",
);
});

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}');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

it('documents retry-safe 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('explicit rerun attempts do not cancel themselves');
expect(doctoring).toContain('github.run_attempt == 1');
expect(doctoring).toContain('superseded pull-request build');
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');
});
});
});
23 changes: 23 additions & 0 deletions src/lib/vitestWorkerBudgetContract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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).toContain('include: ["src/**/*.test.ts"]');
expect(config).not.toContain('fileParallelism: false');
expect(config).not.toContain('pool: "threads"');
expect(config).not.toContain("pool: 'threads'");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
4 changes: 4 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 래퍼만 측정.
Expand Down
Loading