From 211c718feb76dc1d1c90a08ee3804f9846aeced3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:08:32 +0900 Subject: [PATCH 01/52] test: require release provenance before publication --- src/lib/releaseProvenanceContract.test.ts | 92 +++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/lib/releaseProvenanceContract.test.ts diff --git a/src/lib/releaseProvenanceContract.test.ts b/src/lib/releaseProvenanceContract.test.ts new file mode 100644 index 000000000..36aa14cc7 --- /dev/null +++ b/src/lib/releaseProvenanceContract.test.ts @@ -0,0 +1,92 @@ +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 one UTF-8 repository file from the source-controlled project root. + * + * Resolving from this module keeps the governance contract deterministic when + * Vitest runs from an IDE, a parent workspace, or an isolated CI directory. + */ +function readRepositoryFile(relativePath: string): string { + return readFileSync(resolve(repositoryRoot, relativePath), 'utf8'); +} + +/** + * Return one top-level GitHub Actions job block without parsing untrusted YAML. + * + * Release governance tests need only stable job boundaries. Restricting the + * scanner to two-space-indented job keys keeps the assertion dependency-free + * and makes an absent or duplicate contract fail loudly. + */ +function extractWorkflowJob(workflow: string, jobName: string): string { + const marker = `\n ${jobName}:\n`; + const start = workflow.indexOf(marker); + if (start < 0) { + throw new Error(`Missing workflow job: ${jobName}`); + } + + const contentStart = start + marker.length; + const remaining = workflow.slice(contentStart); + const nextJobOffset = remaining.search(/\n [a-zA-Z0-9_-]+:\n/); + return nextJobOffset < 0 ? remaining : remaining.slice(0, nextJobOffset); +} + +describe('release artifact provenance contract', () => { + it('makes exact release provenance a tag-only gate before publication', () => { + const workflow = readRepositoryFile('.github/workflows/release.yml'); + const buildJob = extractWorkflowJob(workflow, 'build'); + const attestJob = extractWorkflowJob(workflow, 'attest-release'); + const publishJob = extractWorkflowJob(workflow, 'publish-release'); + + expect(buildJob).toContain('name: Upload release artifact set'); + expect(buildJob).not.toContain('softprops/action-gh-release'); + + expect(attestJob).toContain("if: startsWith(github.ref, 'refs/tags/')"); + expect(attestJob).toContain('needs: build'); + expect(attestJob).toContain('contents: read'); + expect(attestJob).toContain('id-token: write'); + expect(attestJob).toContain('attestations: write'); + expect(attestJob).toContain( + 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c', + ); + expect(attestJob).toContain( + 'actions/attest@6bc26cfc5e23777f4e24aaf5def813d314ebfd25', + ); + expect(attestJob).toContain('subject-path: release-artifacts/**/*'); + expect(attestJob).toContain('name: Verify release artifact checksums'); + expect(attestJob.indexOf('name: Verify release artifact checksums')).toBeLessThan( + attestJob.indexOf('actions/attest@6bc26cfc5e23777f4e24aaf5def813d314ebfd25'), + ); + + expect(publishJob).toContain("if: startsWith(github.ref, 'refs/tags/')"); + expect(publishJob).toContain('needs: attest-release'); + expect(publishJob).toContain('contents: write'); + expect(publishJob).toContain( + 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c', + ); + expect(publishJob).toContain( + 'softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228', + ); + }); + + it('documents buyer-verifiable provenance and authoritative standards', () => { + const doctoring = readRepositoryFile( + 'docs/doctoring/release-artifact-provenance.md', + ); + const changelog = readRepositoryFile('CHANGELOG.md'); + + expect(doctoring).toContain('# Release artifact provenance'); + expect(doctoring).toContain( + 'gh attestation verify PATH/TO/ARTIFACT -R ContextualWisdomLab/disksage', + ); + expect(doctoring).toContain('SLSA Provenance v1'); + expect(doctoring).toContain('in-toto Statement v1'); + expect(doctoring).toContain('APA 7th references'); + expect(doctoring).toContain('6bc26cfc5e23777f4e24aaf5def813d314ebfd25'); + expect(changelog).toContain('buyer-verifiable release artifact provenance'); + }); +}); From 72d8ca943f0f97572ff92421799192713de5ef78 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:12:29 +0900 Subject: [PATCH 02/52] ci: gate release publication on provenance --- .github/workflows/release.yml | 104 +++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index efc8b461c..f24192757 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,9 +21,10 @@ permissions: jobs: build: - # Minimal top-level (read); elevate only this job — it publishes the Release on tag. + # Build jobs only read source and upload ephemeral artifacts. Durable release + # authority is isolated in the tag-only jobs below. permissions: - contents: write + contents: read env: # llama.cpp's ggml uses std::filesystem (needs macOS 10.15+). macos-latest is arm64 → 11.0. # Ignored on Windows/Linux runners. @@ -177,21 +178,106 @@ jobs: codesign --verify --deep --strict --verbose=2 "${app_bundles[0]}" codesign --display --verbose=4 "${app_bundles[0]}" - # PR / manual: keep the bundles as inspectable artifacts, no release published. - - name: Upload build artifacts (no publish) - if: github.event_name != 'push' + # Every build exports the exact files that a later tag-only job may attest and publish. + # Uploading on pull requests keeps the release candidate independently inspectable. + - name: Upload release artifact set uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: disksage-${{ matrix.os }} path: ${{ matrix.bundles }} if-no-files-found: error - # Tag push: publish/attach the bundles to a single GitHub Release (matrix-safe). - - name: Publish to GitHub Release - if: startsWith(github.ref, 'refs/tags/') + attest-release: + if: startsWith(github.ref, 'refs/tags/') + needs: build + runs-on: ubuntu-22.04 + permissions: + contents: read + id-token: write + attestations: write + steps: + - name: Download exact release artifact set + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v7.0.0 + with: + pattern: disksage-* + path: release-artifacts + merge-multiple: true + + - name: Verify release artifact checksums + shell: bash + run: | + set -euo pipefail + + require_exactly_one() { + local pattern="$1" + local label="$2" + mapfile -d '' matches < <(find release-artifacts -type f -name "$pattern" -print0) + if [[ ${#matches[@]} -ne 1 ]]; then + printf 'Expected exactly one %s, found %s.\n' "$label" "${#matches[@]}" >&2 + exit 1 + fi + } + + require_file() { + local file_name="$1" + if ! find release-artifacts -type f -name "$file_name" -print -quit | grep -q .; then + printf 'Missing required release artifact: %s\n' "$file_name" >&2 + exit 1 + fi + } + + require_exactly_one '*.deb' 'Debian bundle' + require_exactly_one '*.AppImage' 'AppImage bundle' + require_exactly_one '*.msi' 'Windows MSI bundle' + require_exactly_one '*.exe' 'Windows NSIS bundle or operational CLI executable' + require_exactly_one '*.dmg' 'macOS DMG bundle' + + for required_name in \ + disksage-cloud-plan-linux-x86_64 \ + disksage-duplicate-audit-linux-x86_64 \ + disksage-cloud-plan-windows-x86_64.exe \ + disksage-duplicate-audit-windows-x86_64.exe \ + disksage-cloud-plan-macos-arm64 \ + disksage-duplicate-audit-macos-arm64; do + require_file "$required_name" + require_file "$required_name.sha256" + done + + mapfile -d '' checksum_files < <(find release-artifacts -type f -name '*.sha256' -print0) + if [[ ${#checksum_files[@]} -ne 6 ]]; then + printf 'Expected six operational CLI checksum files, found %s.\n' "${#checksum_files[@]}" >&2 + exit 1 + fi + + for checksum_file in "${checksum_files[@]}"; do + checksum_dir="$(dirname "$checksum_file")" + checksum_name="$(basename "$checksum_file")" + (cd "$checksum_dir" && sha256sum --check "$checksum_name") + done + + - name: Generate GitHub build provenance + uses: actions/attest@6bc26cfc5e23777f4e24aaf5def813d314ebfd25 # v4.1.0 + with: + subject-path: release-artifacts/**/* + + publish-release: + if: startsWith(github.ref, 'refs/tags/') + needs: attest-release + runs-on: ubuntu-22.04 + permissions: + contents: write + steps: + - name: Download attested release artifact set + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v7.0.0 + with: + pattern: disksage-* + path: release-artifacts + merge-multiple: true + + - name: Publish attested artifacts to GitHub Release uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: - files: ${{ matrix.bundles }} + files: release-artifacts/**/* generate_release_notes: true fail_on_unmatched_files: true From 404f9e8288b7ae65fa5829840255d117c45f9789 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:13:59 +0900 Subject: [PATCH 03/52] docs: define buyer-verifiable release provenance --- docs/doctoring/release-artifact-provenance.md | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/doctoring/release-artifact-provenance.md diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md new file mode 100644 index 000000000..d14be439d --- /dev/null +++ b/docs/doctoring/release-artifact-provenance.md @@ -0,0 +1,93 @@ +# Release artifact provenance + +## Decision + +DiskSage treats provenance as a release gate rather than optional release metadata. A tagged release may be published only after all operating-system build jobs finish, the exact uploaded artifact set is downloaded into a clean tag-only job, every shipped operational CLI checksum is verified, and GitHub creates signed build-provenance attestations for the files that will be published. + +This design keeps three authorities separate: + +1. `build` compiles and tests release candidates with read-only repository access, then uploads ephemeral workflow artifacts. +2. `attest-release` receives only `contents: read`, `id-token: write`, and `attestations: write`. It verifies the expected platform and CLI set before generating provenance. +3. `publish-release` receives `contents: write` only after `attest-release` succeeds. It cannot publish an unattested build because it has a durable `needs: attest-release` dependency. + +Pull requests and manual non-tag builds still produce inspectable artifacts, but they cannot request an OpenID Connect identity, create durable attestations, or publish a GitHub Release through these jobs. + +## Evidence contract + +The authoritative implementation is `.github/workflows/release.yml`. + +The release contract requires all of the following: + +- the three platform builds upload the exact bundle and operational CLI paths that later jobs consume; +- release publication is absent from the matrix build job, preventing any matrix member from publishing before the complete set exists; +- the attestation and publication jobs run only for `refs/tags/`; +- the attestation job depends on the complete build matrix; +- Linux `.deb` and `.AppImage`, Windows `.msi` and NSIS `.exe`, and macOS `.dmg` bundles are present exactly once; +- all six platform-specific operational CLIs and their six `.sha256` files are present; +- each checksum is verified before provenance generation; +- `actions/attest` is immutably pinned to commit `6bc26cfc5e23777f4e24aaf5def813d314ebfd25`, whose `package.json` and `action.yml` match the upstream `v4.1.0` tag; +- every published file is a subject of the generated attestation; and +- publication depends on successful attestation rather than merely running in parallel with it. + +GitHub's action emits an in-toto Statement v1 containing a SLSA Provenance v1 predicate. SLSA specification version 1.2 is the current approved framework version, while the stable build-provenance predicate URI remains `https://slsa.dev/provenance/v1`. + +## Buyer and operator verification + +Download one release artifact without renaming or modifying it, install a current GitHub CLI, authenticate if the repository visibility requires it, and run: + +```bash +gh attestation verify PATH/TO/ARTIFACT -R ContextualWisdomLab/disksage +``` + +The verifier must bind the artifact digest to `ContextualWisdomLab/disksage`. A successful result demonstrates that GitHub Actions produced an attestation for those exact bytes; it does not independently prove that the software is defect-free, that every dependency is trustworthy, or that the build platform satisfies a claimed SLSA level. Those are separate review and assurance questions. + +For offline evidence collection, download the attestation bundle while network access is available: + +```bash +gh attestation download PATH/TO/ARTIFACT -R ContextualWisdomLab/disksage +``` + +Retain the artifact, the downloaded bundle, the release tag, the source commit SHA, and the successful release workflow URL together. Do not substitute an attestation for a differently named or older artifact, even when the version string appears identical. + +## Failure and stale-evidence behavior + +The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. A checksum mismatch stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. + +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. + +## Privacy and security boundaries + +The attestation describes build provenance and artifact digests. It must not include API keys, user data, local disk inventory, file paths from an operator workstation, model prompts, cleanup plans, or dynamic command output containing private host information. GitHub Secrets remain unavailable to pull-request-controlled release tests unless a separately reviewed workflow explicitly requires them. + +All third-party actions in the release path use immutable 40-character commit SHAs. The attestation job receives no `contents: write` permission, and the publication job receives neither `id-token: write` nor `attestations: write`. This separation limits the impact of a compromised publication or attestation step. + +## Rollback and migration + +Rollback is a workflow-source revert, not deletion or reuse of old attestations: + +1. revert the provenance workflow commit through an independently reviewed pull request; +2. rerun all exact-current-head test, security, packaging, and release-acceptance checks; +3. do not publish a replacement tag until the approved workflow state is on the protected branch; and +4. document why provenance was removed or changed in `CHANGELOG.md` and the release notes. + +Already published attestations remain historical evidence for their original artifact digests. They must not be presented as evidence for replacement binaries. If a release artifact is withdrawn, mark the GitHub Release accordingly and publish a new version with new provenance rather than silently replacing assets under the same tag. + +## MSA compatibility + +Provenance is attached at the DiskSage release boundary and does not require `naruon`, `contextual-orchestrator`, or organization-central services at runtime. CWL services may consume the same verification contract as a module integration gate: verify the artifact against `ContextualWisdomLab/disksage`, bind the verified digest in deployment metadata, and preserve that digest across promotion and rollback. + +## APA 7th references + +GitHub. (2026). *Using artifact attestations to establish provenance for builds*. GitHub Docs. Retrieved August 6, 2026, from https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations + +GitHub. (2026). *Verifying attestations offline*. GitHub Docs. Retrieved August 6, 2026, from https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/verify-attestations-offline + +in-toto Project. (2026). *in-toto attestation framework specification*. https://github.com/in-toto/attestation/blob/main/spec/README.md + +Supply-chain Levels for Software Artifacts. (2026). *SLSA specification (Version 1.2)*. The Linux Foundation. https://slsa.dev/spec/v1.2/ + +Supply-chain Levels for Software Artifacts. (2026). *Build: Verifying artifacts (Version 1.2)*. The Linux Foundation. https://slsa.dev/spec/v1.2/verifying-artifacts + +## Reference verification note + +The sources above were rechecked against their authoritative upstream locations on August 6, 2026. GitHub documentation was used for the supported action permissions and verification commands; the SLSA and in-toto specifications were used for provenance semantics and attestation structure. This document makes no unsupported claim that the workflow alone certifies a particular SLSA level. From af1780865fd94dffe20236b0a6ebcb27305fe784 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:14:32 +0900 Subject: [PATCH 04/52] docs: record release provenance gate --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f51b065..db899c300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Security +- Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. - Persist copy-approval provenance in immutable receipt lineage, reject stale, generic, mismatched, or tampered approvals, and retain explicit backward readability for pre-approval receipt formats. - Generate the npm lockfile in an exact-head validation job with repository contents read-only and dependency lifecycle scripts disabled, bind the artifact to SHA-256 evidence, and grant `contents: write` only to a separate publication job that verifies the same-run artifact and unchanged branch head before committing the lockfile. - Removed obsolete one-shot repair workflows and patch scripts so repository automation no longer retains dormant write-capable recovery paths. From 7d491712033aeeba3d61266508f67a0a9ca27728 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:15:59 +0900 Subject: [PATCH 05/52] test: isolate release artifacts from GPU bundles --- src/lib/releaseProvenanceContract.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lib/releaseProvenanceContract.test.ts b/src/lib/releaseProvenanceContract.test.ts index 36aa14cc7..7b167bdd0 100644 --- a/src/lib/releaseProvenanceContract.test.ts +++ b/src/lib/releaseProvenanceContract.test.ts @@ -43,6 +43,7 @@ describe('release artifact provenance contract', () => { const publishJob = extractWorkflowJob(workflow, 'publish-release'); expect(buildJob).toContain('name: Upload release artifact set'); + expect(buildJob).toContain('name: release-disksage-${{ matrix.os }}'); expect(buildJob).not.toContain('softprops/action-gh-release'); expect(attestJob).toContain("if: startsWith(github.ref, 'refs/tags/')"); @@ -53,11 +54,16 @@ describe('release artifact provenance contract', () => { expect(attestJob).toContain( 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c', ); + expect(attestJob).toContain('pattern: release-disksage-*'); expect(attestJob).toContain( 'actions/attest@6bc26cfc5e23777f4e24aaf5def813d314ebfd25', ); expect(attestJob).toContain('subject-path: release-artifacts/**/*'); expect(attestJob).toContain('name: Verify release artifact checksums'); + expect(attestJob).toContain( + "require_exactly_one_path '*/bundle/nsis/*.exe' 'Windows NSIS bundle'", + ); + expect(attestJob).not.toContain("require_exactly_one '*.exe'"); expect(attestJob.indexOf('name: Verify release artifact checksums')).toBeLessThan( attestJob.indexOf('actions/attest@6bc26cfc5e23777f4e24aaf5def813d314ebfd25'), ); @@ -68,6 +74,7 @@ describe('release artifact provenance contract', () => { expect(publishJob).toContain( 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c', ); + expect(publishJob).toContain('pattern: release-disksage-*'); expect(publishJob).toContain( 'softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228', ); From ccbb5b4238f9c1cdca9cd24984f82259d14dbf2d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:16:51 +0900 Subject: [PATCH 06/52] test: pin attestation action to exact release tag --- src/lib/releaseProvenanceContract.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/releaseProvenanceContract.test.ts b/src/lib/releaseProvenanceContract.test.ts index 7b167bdd0..6abe55d97 100644 --- a/src/lib/releaseProvenanceContract.test.ts +++ b/src/lib/releaseProvenanceContract.test.ts @@ -56,7 +56,7 @@ describe('release artifact provenance contract', () => { ); expect(attestJob).toContain('pattern: release-disksage-*'); expect(attestJob).toContain( - 'actions/attest@6bc26cfc5e23777f4e24aaf5def813d314ebfd25', + 'actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26', ); expect(attestJob).toContain('subject-path: release-artifacts/**/*'); expect(attestJob).toContain('name: Verify release artifact checksums'); @@ -65,7 +65,7 @@ describe('release artifact provenance contract', () => { ); expect(attestJob).not.toContain("require_exactly_one '*.exe'"); expect(attestJob.indexOf('name: Verify release artifact checksums')).toBeLessThan( - attestJob.indexOf('actions/attest@6bc26cfc5e23777f4e24aaf5def813d314ebfd25'), + attestJob.indexOf('actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26'), ); expect(publishJob).toContain("if: startsWith(github.ref, 'refs/tags/')"); @@ -93,7 +93,7 @@ describe('release artifact provenance contract', () => { expect(doctoring).toContain('SLSA Provenance v1'); expect(doctoring).toContain('in-toto Statement v1'); expect(doctoring).toContain('APA 7th references'); - expect(doctoring).toContain('6bc26cfc5e23777f4e24aaf5def813d314ebfd25'); + expect(doctoring).toContain('59d89421af93a897026c735860bf21b6eb4f7b26'); expect(changelog).toContain('buyer-verifiable release artifact provenance'); }); }); From a73e1da366ccd68ea4049f8d86070e6c4b1962b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:17:59 +0900 Subject: [PATCH 07/52] test: pin artifact download to an upstream release --- src/lib/releaseProvenanceContract.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/releaseProvenanceContract.test.ts b/src/lib/releaseProvenanceContract.test.ts index 6abe55d97..82c4a86ea 100644 --- a/src/lib/releaseProvenanceContract.test.ts +++ b/src/lib/releaseProvenanceContract.test.ts @@ -52,7 +52,7 @@ describe('release artifact provenance contract', () => { expect(attestJob).toContain('id-token: write'); expect(attestJob).toContain('attestations: write'); expect(attestJob).toContain( - 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c', + 'actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131', ); expect(attestJob).toContain('pattern: release-disksage-*'); expect(attestJob).toContain( @@ -72,7 +72,7 @@ describe('release artifact provenance contract', () => { expect(publishJob).toContain('needs: attest-release'); expect(publishJob).toContain('contents: write'); expect(publishJob).toContain( - 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c', + 'actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131', ); expect(publishJob).toContain('pattern: release-disksage-*'); expect(publishJob).toContain( @@ -94,6 +94,7 @@ describe('release artifact provenance contract', () => { expect(doctoring).toContain('in-toto Statement v1'); expect(doctoring).toContain('APA 7th references'); expect(doctoring).toContain('59d89421af93a897026c735860bf21b6eb4f7b26'); + expect(doctoring).toContain('37930b1c2abaa49bbe596cd826c3c89aef350131'); expect(changelog).toContain('buyer-verifiable release artifact provenance'); }); }); From 4e7ff34d69270b5245e0166fe695e9ac96a0af76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:19:23 +0900 Subject: [PATCH 08/52] ci: isolate and attest exact release artifacts --- .github/workflows/release.yml | 83 +++++++++-------------------------- 1 file changed, 20 insertions(+), 63 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f24192757..cb5606a22 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,6 @@ jobs: run: brew install cmake # windows-latest images ship cmake + LLVM + MSVC; no extra install needed. - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 20 @@ -98,13 +97,9 @@ jobs: with: workspaces: src-tauri - # Builds the SvelteKit frontend (beforeBuildCommand) + the Rust app with the real - # llama.cpp engine (CPU in M6; GPU backends are a follow-up) and produces the OS bundles. - name: Tauri build (with embedded LLM) run: npm run tauri -- build --features llm-engine - # Operational audit/planning tools are intentionally shipped separately from the GUI so a - # cleanup run can reuse exact-head binaries without rebuilding a multi-GiB local target. - name: Build operational CLIs run: >- cargo build --manifest-path src-tauri/Cargo.toml --release --features cloud-cli @@ -145,8 +140,6 @@ jobs: "${{ matrix.duplicate_cli_asset }}" \ "usage: disksage-duplicate-audit" - # Validate the artifact users actually receive. An unsigned bundle can retain only the - # Mach-O linker's ad-hoc executable signature, which does not seal Info.plist/resources. - name: Verify macOS DMG app signature if: matrix.os == 'macos-latest' shell: bash @@ -178,12 +171,12 @@ jobs: codesign --verify --deep --strict --verbose=2 "${app_bundles[0]}" codesign --display --verbose=4 "${app_bundles[0]}" - # Every build exports the exact files that a later tag-only job may attest and publish. - # Uploading on pull requests keeps the release candidate independently inspectable. + # Use a release-only namespace so the later wildcard can never admit GPU + # diagnostic bundles that are uploaded by a concurrent job. - name: Upload release artifact set uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: disksage-${{ matrix.os }} + name: release-disksage-${{ matrix.os }} path: ${{ matrix.bundles }} if-no-files-found: error @@ -197,9 +190,9 @@ jobs: attestations: write steps: - name: Download exact release artifact set - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v7.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: - pattern: disksage-* + pattern: release-disksage-* path: release-artifacts merge-multiple: true @@ -208,10 +201,10 @@ jobs: run: | set -euo pipefail - require_exactly_one() { - local pattern="$1" + require_exactly_one_path() { + local path_pattern="$1" local label="$2" - mapfile -d '' matches < <(find release-artifacts -type f -name "$pattern" -print0) + mapfile -d '' matches < <(find release-artifacts -type f -path "$path_pattern" -print0) if [[ ${#matches[@]} -ne 1 ]]; then printf 'Expected exactly one %s, found %s.\n' "$label" "${#matches[@]}" >&2 exit 1 @@ -226,11 +219,11 @@ jobs: fi } - require_exactly_one '*.deb' 'Debian bundle' - require_exactly_one '*.AppImage' 'AppImage bundle' - require_exactly_one '*.msi' 'Windows MSI bundle' - require_exactly_one '*.exe' 'Windows NSIS bundle or operational CLI executable' - require_exactly_one '*.dmg' 'macOS DMG bundle' + require_exactly_one_path '*/bundle/deb/*.deb' 'Debian bundle' + require_exactly_one_path '*/bundle/appimage/*.AppImage' 'AppImage bundle' + require_exactly_one_path '*/bundle/msi/*.msi' 'Windows MSI bundle' + require_exactly_one_path '*/bundle/nsis/*.exe' 'Windows NSIS bundle' + require_exactly_one_path '*/bundle/dmg/*.dmg' 'macOS DMG bundle' for required_name in \ disksage-cloud-plan-linux-x86_64 \ @@ -256,7 +249,7 @@ jobs: done - name: Generate GitHub build provenance - uses: actions/attest@6bc26cfc5e23777f4e24aaf5def813d314ebfd25 # v4.1.0 + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-path: release-artifacts/**/* @@ -268,9 +261,9 @@ jobs: contents: write steps: - name: Download attested release artifact set - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v7.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: - pattern: disksage-* + pattern: release-disksage-* path: release-artifacts merge-multiple: true @@ -281,27 +274,15 @@ jobs: generate_release_notes: true fail_on_unmatched_files: true - # GPU-enabled build (CUDA + Vulkan, dynamic backends) for RUNTIME verification on real GPU - # hardware. Uploads the raw app exe + ggml/cuda/vulkan shared libs (NOT an installer) so a - # tester can run it directly and confirm GPU offload. Installer bundling of the libs is a - # follow-up once the runtime path is verified. macOS Metal is already covered by the `build` - # job (auto-enabled on Apple Silicon), so this is Windows + Linux only. + # GPU-enabled build for runtime verification on real hardware. These diagnostic + # bundles remain deliberately outside the release artifact namespace. gpu-build: - # Pull requests already compile the embedded LLM in the three release-bundle jobs above. - # Reserve the expensive CUDA/Vulkan matrix for tags and explicit manual verification so - # unrelated Rust/UI PRs cannot saturate hosted runners or delay required safety checks. if: github.event_name != 'pull_request' permissions: contents: read strategy: fail-fast: false matrix: - # windows-2022 (VS 2022 / MSVC 14.4x): CUDA 12.6 rejects the newer MSVC on windows-latest - # (VS 2026) via host_config.h "unsupported Microsoft Visual Studio version". - # Windows drops Vulkan: llama.cpp's vulkan-shaders-gen nested build blows past the Windows - # 260-char MAX_PATH ("C1083: Cannot open compiler generated file"). The tester's GPU is - # NVIDIA (CUDA), so CUDA+CPU covers it; Windows Vulkan is a follow-up (needs a short build - # path). Linux keeps CUDA+Vulkan (no MAX_PATH limit). include: - os: ubuntu-22.04 features: "llm-engine,llama-cpp-2/cuda,llama-cpp-2/vulkan,llama-cpp-2/dynamic-backends" @@ -310,8 +291,6 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 60 env: - # Target the tester's GPU (RTX 3050 Ti = Ampere, compute 8.6) to keep the CUDA compile - # from building every arch. Best-effort — ignored if the build system doesn't read it. CMAKE_CUDA_ARCHITECTURES: "86" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -348,9 +327,6 @@ jobs: workspaces: src-tauri key: gpu - # Windows: the runner's Visual Studio (v18/2026) is newer than CUDA 12.6's VS integration, - # so cmake's VS generator fails with "No CUDA toolset found". Switch to Ninja + activate the - # MSVC env so nvcc uses cl.exe directly as the host compiler (no VS-integration files needed). - name: Setup MSVC dev environment (Windows) if: matrix.os == 'windows-2022' uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 @@ -360,50 +336,31 @@ jobs: shell: bash run: echo "CMAKE_GENERATOR=Ninja" >> "$GITHUB_ENV" - # CUDA is dynamically linked (NVIDIA ships no static cudart); dynamic-backends builds each - # ggml backend as a separate shared lib loaded at runtime, so the app launches on machines - # WITHOUT CUDA (Vulkan/CPU fallback) — the distributable design from spec §6. - # --no-bundle: build the app binary + shared libs only, skip installer bundling (linuxdeploy - # fails on the dynamic backend .so's, and verification needs just the raw exe + libs anyway). - name: "Tauri build (GPU: CUDA [+ Vulkan on Linux] + dynamic backends)" run: npm run tauri -- build --no-bundle --features "${{ matrix.features }}" - # Also build the lib test binary (contains engine.rs's #[ignore] real-model smoke test) so GPU - # offload can be verified headlessly via CLI — no GUI needed. Run it with DISKSAGE_MODEL set. - # No `shell: bash` — on Windows that puts git-bash's /usr/bin/link ahead of MSVC link.exe - # and breaks linking. Default shell (pwsh on Windows / bash on Linux) uses the right linker. - name: Build engine smoke-test binary run: cargo test --manifest-path src-tauri/Cargo.toml --release --no-run --features "${{ matrix.features }}" --lib - # Diagnostic: learn WHERE the shared libs landed (first-attempt discovery). - name: List built shared libs (diagnostic) if: always() shell: bash run: | echo "== target/release top =="; ls -la src-tauri/target/release/ 2>/dev/null | head -40 || true - echo "== ggml/llama/cuda/vulkan libs under target (backends land in out/backends) =="; find src-tauri/target -maxdepth 8 -iregex '.*\(ggml\|llama\|cudart\|vulkan\).*\.\(dll\|so\|dylib\)' 2>/dev/null | head -60 || true + echo "== ggml/llama/cuda/vulkan libs under target =="; find src-tauri/target -maxdepth 8 -iregex '.*\(ggml\|llama\|cudart\|vulkan\).*\.\(dll\|so\|dylib\)' 2>/dev/null | head -60 || true echo "== out/backends dirs =="; find src-tauri/target -type d -name backends 2>/dev/null | head || true echo "== CUDA runtime dll/so =="; find "${CUDA_PATH:-/usr/local/cuda}" -iname 'cudart*' 2>/dev/null | head || true - # Best-effort stage: app exe + every ggml/llama/cuda/vulkan shared lib next to it, so the - # tester can run the exe directly. Paths refined based on the diagnostic above. - name: Stage GPU run bundle if: always() shell: bash run: | - set +e +o pipefail # best-effort staging must never fail the job + set +e +o pipefail mkdir -p gpu-run - # app binary (Tauri output name follows productName/crate; grab both just in case) find src-tauri/target/release -maxdepth 1 -type f \( -name 'disksage' -o -name 'disksage.exe' -o -name 'DiskSage' -o -name 'DiskSage.exe' \) -exec cp {} gpu-run/ \; 2>/dev/null || true - # engine smoke-test binary (real inference via #[ignore] test) for headless GPU verification. - # Pick the freshest disksage_lib-* (ls -t) to avoid stale cached hashes; skip .d dep files. tb=$(ls -t src-tauri/target/release/deps/disksage_lib-* 2>/dev/null | grep -viE '\.(d|pdb)$' | head -1) [ -n "$tb" ] && cp "$tb" "gpu-run/engine_smoketest${{ matrix.os == 'windows-2022' && '.exe' || '' }}" 2>/dev/null || true - # all ggml/llama shared libs incl. dynamic backends (deep in build/.../out/backends) and - # Linux SONAME-versioned .so.N files; copy symlinks + targets so the set is self-contained. find src-tauri/target -maxdepth 8 -iregex '.*\(ggml\|llama\).*\(\.dll\|\.so\|\.so\..*\)' -exec cp {} gpu-run/ \; 2>/dev/null || true - # CUDA runtime libs ggml-cuda needs at load time (cudart + cuBLAS). nvcuda/vulkan-1 come - # from the GPU driver / Vulkan runtime already on the tester's machine. for pat in 'cudart64_*.dll' 'cublas64_*.dll' 'cublasLt64_*.dll'; do find "${CUDA_PATH:-/usr/local/cuda}" -iname "$pat" -exec cp {} gpu-run/ \; 2>/dev/null || true done From 2cbd186629c627e9a68731925d6d07aa829474af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:20:06 +0900 Subject: [PATCH 09/52] docs: bind provenance actions to upstream release commits --- docs/doctoring/release-artifact-provenance.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md index d14be439d..71bc18cd5 100644 --- a/docs/doctoring/release-artifact-provenance.md +++ b/docs/doctoring/release-artifact-provenance.md @@ -19,13 +19,15 @@ The authoritative implementation is `.github/workflows/release.yml`. The release contract requires all of the following: - 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; - release publication is absent from the matrix build job, preventing any matrix member from publishing before the complete set exists; - the attestation and publication jobs run only for `refs/tags/`; - the attestation job depends on the complete build matrix; -- Linux `.deb` and `.AppImage`, Windows `.msi` and NSIS `.exe`, and macOS `.dmg` bundles are present exactly once; +- Linux `.deb` and `.AppImage`, Windows `.msi` and NSIS `.exe`, and macOS `.dmg` bundles are present exactly once in their expected bundle paths; - all six platform-specific operational CLIs and their six `.sha256` files are present; - each checksum is verified before provenance generation; -- `actions/attest` is immutably pinned to commit `6bc26cfc5e23777f4e24aaf5def813d314ebfd25`, whose `package.json` and `action.yml` match the upstream `v4.1.0` tag; +- `actions/download-artifact` is immutably pinned to commit `37930b1c2abaa49bbe596cd826c3c89aef350131`, the upstream `v7.0.0` tag commit; +- `actions/attest` is immutably pinned to commit `59d89421af93a897026c735860bf21b6eb4f7b26`, the upstream `v4.1.0` tag commit; - every published file is a subject of the generated attestation; and - publication depends on successful attestation rather than merely running in parallel with it. @@ -51,7 +53,7 @@ Retain the artifact, the downloaded bundle, the release tag, the source commit S ## Failure and stale-evidence behavior -The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. A checksum mismatch stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. +The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. Path-scoped checks distinguish the Windows NSIS installer from the two separately shipped Windows operational CLI executables. A checksum mismatch stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. 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. @@ -90,4 +92,4 @@ Supply-chain Levels for Software Artifacts. (2026). *Build: Verifying artifacts ## Reference verification note -The sources above were rechecked against their authoritative upstream locations on August 6, 2026. GitHub documentation was used for the supported action permissions and verification commands; the SLSA and in-toto specifications were used for provenance semantics and attestation structure. This document makes no unsupported claim that the workflow alone certifies a particular SLSA level. +The sources above were rechecked against their authoritative upstream locations on August 6, 2026. GitHub documentation was used for the supported action permissions and verification commands; upstream tag comparisons established the immutable action commits; the SLSA and in-toto specifications were used for provenance semantics and attestation structure. This document makes no unsupported claim that the workflow alone certifies a particular SLSA level. From deeaabfead889194957dae3d629ab266990b52f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:25:03 +0900 Subject: [PATCH 10/52] test: normalize workflow line endings --- src/lib/releaseProvenanceContract.test.ts | 31 ++++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/lib/releaseProvenanceContract.test.ts b/src/lib/releaseProvenanceContract.test.ts index 82c4a86ea..ee3c555e3 100644 --- a/src/lib/releaseProvenanceContract.test.ts +++ b/src/lib/releaseProvenanceContract.test.ts @@ -18,24 +18,37 @@ function readRepositoryFile(relativePath: string): string { /** * Return one top-level GitHub Actions job block without parsing untrusted YAML. * - * Release governance tests need only stable job boundaries. Restricting the - * scanner to two-space-indented job keys keeps the assertion dependency-free - * and makes an absent or duplicate contract fail loudly. + * Release governance tests need only stable job boundaries. Normalizing CRLF + * and legacy CR separators keeps the contract portable across Git checkouts on + * Windows, macOS, and Linux without changing the source-controlled workflow. */ function extractWorkflowJob(workflow: string, jobName: string): string { + const normalizedWorkflow = workflow.replace(/\r\n?/g, '\n'); const marker = `\n ${jobName}:\n`; - const start = workflow.indexOf(marker); + const start = normalizedWorkflow.indexOf(marker); if (start < 0) { throw new Error(`Missing workflow job: ${jobName}`); } const contentStart = start + marker.length; - const remaining = workflow.slice(contentStart); + const remaining = normalizedWorkflow.slice(contentStart); const nextJobOffset = remaining.search(/\n [a-zA-Z0-9_-]+:\n/); return nextJobOffset < 0 ? remaining : remaining.slice(0, nextJobOffset); } describe('release artifact provenance contract', () => { + it('extracts workflow jobs from Windows CRLF checkouts', () => { + const workflow = + 'jobs:\r\n build:\r\n runs-on: windows-latest\r\n publish:\r\n runs-on: ubuntu-latest\r\n'; + + expect(extractWorkflowJob(workflow, 'build')).toContain( + 'runs-on: windows-latest', + ); + expect(extractWorkflowJob(workflow, 'build')).not.toContain( + 'runs-on: ubuntu-latest', + ); + }); + it('makes exact release provenance a tag-only gate before publication', () => { const workflow = readRepositoryFile('.github/workflows/release.yml'); const buildJob = extractWorkflowJob(workflow, 'build'); @@ -64,8 +77,12 @@ describe('release artifact provenance contract', () => { "require_exactly_one_path '*/bundle/nsis/*.exe' 'Windows NSIS bundle'", ); expect(attestJob).not.toContain("require_exactly_one '*.exe'"); - expect(attestJob.indexOf('name: Verify release artifact checksums')).toBeLessThan( - attestJob.indexOf('actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26'), + expect( + attestJob.indexOf('name: Verify release artifact checksums'), + ).toBeLessThan( + attestJob.indexOf( + 'actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26', + ), ); expect(publishJob).toContain("if: startsWith(github.ref, 'refs/tags/')"); From 002b80105ea141d51071c1c15445a7048661977d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:32:09 +0900 Subject: [PATCH 11/52] test(release): reject duplicate CLI artifacts --- src/lib/releaseProvenanceContract.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib/releaseProvenanceContract.test.ts b/src/lib/releaseProvenanceContract.test.ts index ee3c555e3..b0779f07e 100644 --- a/src/lib/releaseProvenanceContract.test.ts +++ b/src/lib/releaseProvenanceContract.test.ts @@ -73,6 +73,14 @@ describe('release artifact provenance contract', () => { ); expect(attestJob).toContain('subject-path: release-artifacts/**/*'); expect(attestJob).toContain('name: Verify release artifact checksums'); + expect(attestJob).toContain('require_exactly_one_file()'); + expect(attestJob).not.toContain('require_file()'); + expect(attestJob).toContain( + 'require_exactly_one_file "$required_name"', + ); + expect(attestJob).toContain( + 'require_exactly_one_file "$required_name.sha256"', + ); expect(attestJob).toContain( "require_exactly_one_path '*/bundle/nsis/*.exe' 'Windows NSIS bundle'", ); @@ -112,6 +120,9 @@ describe('release artifact provenance contract', () => { expect(doctoring).toContain('APA 7th references'); expect(doctoring).toContain('59d89421af93a897026c735860bf21b6eb4f7b26'); expect(doctoring).toContain('37930b1c2abaa49bbe596cd826c3c89aef350131'); + expect(doctoring).toContain( + 'operational CLI, or checksum file is absent or duplicated', + ); expect(changelog).toContain('buyer-verifiable release artifact provenance'); }); }); From 85957e3842ae3fc9524643880d3a92d8dcf344cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:34:53 +0900 Subject: [PATCH 12/52] test: require exact-head release checkout --- src/lib/releaseProvenanceContract.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/releaseProvenanceContract.test.ts b/src/lib/releaseProvenanceContract.test.ts index b0779f07e..daec91eec 100644 --- a/src/lib/releaseProvenanceContract.test.ts +++ b/src/lib/releaseProvenanceContract.test.ts @@ -57,6 +57,9 @@ describe('release artifact provenance contract', () => { expect(buildJob).toContain('name: Upload release artifact set'); expect(buildJob).toContain('name: release-disksage-${{ matrix.os }}'); + expect(buildJob).toContain( + 'ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ); expect(buildJob).not.toContain('softprops/action-gh-release'); expect(attestJob).toContain("if: startsWith(github.ref, 'refs/tags/')"); From 770b3fda74b393befa61a464e3b1a95956e96e18 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:36:24 +0900 Subject: [PATCH 13/52] ci: bind provenance to exact release head --- .github/workflows/release.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb5606a22..acd6f40e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,6 +72,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Install Linux system deps (tauri GTK + llama.cpp native) @@ -211,10 +212,12 @@ jobs: fi } - require_file() { + require_exactly_one_file() { local file_name="$1" - if ! find release-artifacts -type f -name "$file_name" -print -quit | grep -q .; then - printf 'Missing required release artifact: %s\n' "$file_name" >&2 + mapfile -d '' matches < <(find release-artifacts -type f -name "$file_name" -print0) + if [[ ${#matches[@]} -ne 1 ]]; then + printf 'Expected exactly one release artifact named %s, found %s.\n' \ + "$file_name" "${#matches[@]}" >&2 exit 1 fi } @@ -232,8 +235,8 @@ jobs: disksage-duplicate-audit-windows-x86_64.exe \ disksage-cloud-plan-macos-arm64 \ disksage-duplicate-audit-macos-arm64; do - require_file "$required_name" - require_file "$required_name.sha256" + require_exactly_one_file "$required_name" + require_exactly_one_file "$required_name.sha256" done mapfile -d '' checksum_files < <(find release-artifacts -type f -name '*.sha256' -print0) @@ -295,6 +298,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Install Linux system deps (tauri GTK + llama.cpp native + Vulkan build) From 6063fd2b1e9a8bf423e9dd52599df6d660a9adcd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:37:25 +0900 Subject: [PATCH 14/52] docs: bind provenance evidence to exact head --- docs/doctoring/release-artifact-provenance.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md index 71bc18cd5..d0c37b60f 100644 --- a/docs/doctoring/release-artifact-provenance.md +++ b/docs/doctoring/release-artifact-provenance.md @@ -18,13 +18,14 @@ 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; - 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; - release publication is absent from the matrix build job, preventing any matrix member from publishing before the complete set exists; - the attestation and publication jobs run only for `refs/tags/`; - the attestation job depends on the complete build matrix; - Linux `.deb` and `.AppImage`, Windows `.msi` and NSIS `.exe`, and macOS `.dmg` bundles are present exactly once in their expected bundle paths; -- all six platform-specific operational CLIs and their six `.sha256` files are present; +- all six platform-specific operational CLIs and all six corresponding `.sha256` files are each present exactly once; - each checksum is verified before provenance generation; - `actions/download-artifact` is immutably pinned to commit `37930b1c2abaa49bbe596cd826c3c89aef350131`, the upstream `v7.0.0` tag commit; - `actions/attest` is immutably pinned to commit `59d89421af93a897026c735860bf21b6eb4f7b26`, the upstream `v4.1.0` tag commit; @@ -80,15 +81,15 @@ Provenance is attached at the DiskSage release boundary and does not require `na ## APA 7th references -GitHub. (2026). *Using artifact attestations to establish provenance for builds*. GitHub Docs. Retrieved August 6, 2026, from https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations +GitHub. (n.d.). *Using artifact attestations to establish provenance for builds*. GitHub Docs. Retrieved August 6, 2026, from https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations -GitHub. (2026). *Verifying attestations offline*. GitHub Docs. Retrieved August 6, 2026, from https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/verify-attestations-offline +GitHub. (n.d.). *Verifying attestations offline*. GitHub Docs. Retrieved August 6, 2026, from https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/verify-attestations-offline -in-toto Project. (2026). *in-toto attestation framework specification*. https://github.com/in-toto/attestation/blob/main/spec/README.md +in-toto Project. (n.d.). *in-toto attestation framework specification (Version 1.2)*. GitHub. Retrieved August 6, 2026, from https://github.com/in-toto/attestation/blob/v1.2.0/spec/README.md -Supply-chain Levels for Software Artifacts. (2026). *SLSA specification (Version 1.2)*. The Linux Foundation. https://slsa.dev/spec/v1.2/ +Supply-chain Levels for Software Artifacts. (n.d.). *SLSA specification (Version 1.2)*. The Linux Foundation. Retrieved August 6, 2026, from https://slsa.dev/spec/v1.2/ -Supply-chain Levels for Software Artifacts. (2026). *Build: Verifying artifacts (Version 1.2)*. The Linux Foundation. https://slsa.dev/spec/v1.2/verifying-artifacts +Supply-chain Levels for Software Artifacts. (n.d.). *Build: Verifying artifacts (Version 1.2)*. The Linux Foundation. Retrieved August 6, 2026, from https://slsa.dev/spec/v1.2/verifying-artifacts ## Reference verification note From 7d6b352409513203111fc58783cdddf07c02b1b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:39:06 +0900 Subject: [PATCH 15/52] test: require exact-head quality checks --- src/lib/releaseProvenanceContract.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib/releaseProvenanceContract.test.ts b/src/lib/releaseProvenanceContract.test.ts index daec91eec..313aa9e4f 100644 --- a/src/lib/releaseProvenanceContract.test.ts +++ b/src/lib/releaseProvenanceContract.test.ts @@ -49,6 +49,17 @@ describe('release artifact provenance contract', () => { ); }); + it('binds all required test jobs to the exact current head', () => { + const workflow = readRepositoryFile('.github/workflows/test.yml'); + const exactHeadCheckout = + 'ref: ${{ github.event.pull_request.head.sha || github.sha }}'; + + expect(extractWorkflowJob(workflow, 'test')).toContain(exactHeadCheckout); + expect(extractWorkflowJob(workflow, 'llm-engine-build')).toContain( + exactHeadCheckout, + ); + }); + it('makes exact release provenance a tag-only gate before publication', () => { const workflow = readRepositoryFile('.github/workflows/release.yml'); const buildJob = extractWorkflowJob(workflow, 'build'); From 93db7a500984a49135a5edf8d2b6f296c3b2b3f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:39:28 +0900 Subject: [PATCH 16/52] ci: bind quality checks to exact head --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b793c6640..6d614e351 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Install Tauri system deps run: | @@ -47,6 +48,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - name: Install build deps (llama.cpp native + tauri) run: | From 548de71c2e782fe323d1ff5550e184c76cabad59 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:49:42 +0900 Subject: [PATCH 17/52] test(release): reject decoy checksum records --- src/lib/releaseProvenanceContract.test.ts | 147 +++++++++++++++++++++- 1 file changed, 145 insertions(+), 2 deletions(-) diff --git a/src/lib/releaseProvenanceContract.test.ts b/src/lib/releaseProvenanceContract.test.ts index 313aa9e4f..1fa241d63 100644 --- a/src/lib/releaseProvenanceContract.test.ts +++ b/src/lib/releaseProvenanceContract.test.ts @@ -1,9 +1,26 @@ -import { readFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; +import { createHash } from 'node:crypto'; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +const operationalAssetNames = [ + 'disksage-cloud-plan-linux-x86_64', + 'disksage-duplicate-audit-linux-x86_64', + 'disksage-cloud-plan-windows-x86_64.exe', + 'disksage-duplicate-audit-windows-x86_64.exe', + 'disksage-cloud-plan-macos-arm64', + 'disksage-duplicate-audit-macos-arm64', +] as const; /** * Read one UTF-8 repository file from the source-controlled project root. @@ -36,6 +53,95 @@ function extractWorkflowJob(workflow: string, jobName: string): string { return nextJobOffset < 0 ? remaining : remaining.slice(0, nextJobOffset); } +/** + * Extract one literal Bash `run` block from a named workflow step. + * + * The release validator is security-sensitive executable policy. Running the + * exact source-controlled block against fixtures proves behavior without + * maintaining a second, test-only implementation that could drift. + */ +function extractWorkflowRunScript(job: string, stepName: string): string { + const normalizedJob = job.replace(/\r\n?/g, '\n'); + const stepMarker = ` - name: ${stepName}\n`; + const stepStart = normalizedJob.indexOf(stepMarker); + if (stepStart < 0) { + throw new Error(`Missing workflow step: ${stepName}`); + } + + const runMarker = ' run: |\n'; + const runStart = normalizedJob.indexOf(runMarker, stepStart); + if (runStart < 0) { + throw new Error(`Missing literal run block for workflow step: ${stepName}`); + } + + const scriptStart = runStart + runMarker.length; + const remaining = normalizedJob.slice(scriptStart); + const nextStepOffset = remaining.search(/\n - (?:name:|uses:)/); + const indentedScript = + nextStepOffset < 0 ? remaining : remaining.slice(0, nextStepOffset); + return indentedScript + .split('\n') + .map((line) => (line.startsWith(' ') ? line.slice(10) : line)) + .join('\n'); +} + +/** + * Write a realistic complete release-artifact tree and return its root. + * + * Each operational checksum initially names and authenticates the adjacent CLI + * exactly as the platform build job does. Individual tests can then mutate one + * boundary while every unrelated admission requirement remains valid. + */ +function createReleaseArtifactFixture(): string { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-contract-')); + const artifactRoot = join(fixtureRoot, 'release-artifacts'); + const bundlePaths = [ + 'ubuntu/bundle/deb/disksage.deb', + 'ubuntu/bundle/appimage/disksage.AppImage', + 'windows/bundle/msi/disksage.msi', + 'windows/bundle/nsis/disksage-setup.exe', + 'macos/bundle/dmg/disksage.dmg', + ]; + + for (const bundlePath of bundlePaths) { + const absolutePath = join(artifactRoot, bundlePath); + mkdirSync(dirname(absolutePath), { recursive: true }); + writeFileSync(absolutePath, `bundle:${bundlePath}`); + } + + for (const assetName of operationalAssetNames) { + const platformDirectory = assetName.includes('windows') + ? 'windows' + : assetName.includes('macos') + ? 'macos' + : 'ubuntu'; + const assetPath = join(artifactRoot, platformDirectory, assetName); + const assetBytes = Buffer.from(`operational-cli:${assetName}`); + mkdirSync(dirname(assetPath), { recursive: true }); + writeFileSync(assetPath, assetBytes); + writeFileSync( + `${assetPath}.sha256`, + `${createHash('sha256').update(assetBytes).digest('hex')} ${assetName}\n`, + ); + } + + return fixtureRoot; +} + +/** Execute the exact release-admission Bash block in one fixture directory. */ +function runReleaseArtifactVerifier(fixtureRoot: string) { + const workflow = readRepositoryFile('.github/workflows/release.yml'); + const attestJob = extractWorkflowJob(workflow, 'attest-release'); + const verifier = extractWorkflowRunScript( + attestJob, + 'Verify release artifact checksums', + ); + return spawnSync('bash', ['-c', verifier], { + cwd: fixtureRoot, + encoding: 'utf8', + }); +} + describe('release artifact provenance contract', () => { it('extracts workflow jobs from Windows CRLF checkouts', () => { const workflow = @@ -119,6 +225,40 @@ describe('release artifact provenance contract', () => { ); }); + it.runIf(process.platform !== 'win32')( + 'rejects a checksum record that authenticates a decoy instead of the expected CLI', + () => { + const fixtureRoot = createReleaseArtifactFixture(); + try { + const linuxDirectory = join( + fixtureRoot, + 'release-artifacts', + 'ubuntu', + ); + const checksumPath = join( + linuxDirectory, + 'disksage-cloud-plan-linux-x86_64.sha256', + ); + const decoyName = 'unpublished-decoy'; + const decoyBytes = Buffer.from('not-the-published-cli'); + writeFileSync(join(linuxDirectory, decoyName), decoyBytes); + writeFileSync( + checksumPath, + `${createHash('sha256').update(decoyBytes).digest('hex')} ${decoyName}\n`, + ); + + const result = runReleaseArtifactVerifier(fixtureRoot); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + 'must reference its adjacent operational CLI', + ); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }, + ); + it('documents buyer-verifiable provenance and authoritative standards', () => { const doctoring = readRepositoryFile( 'docs/doctoring/release-artifact-provenance.md', @@ -137,6 +277,9 @@ describe('release artifact provenance contract', () => { expect(doctoring).toContain( 'operational CLI, or checksum file is absent or duplicated', ); + expect(doctoring).toContain( + 'checksum record names a file other than its adjacent operational CLI', + ); expect(changelog).toContain('buyer-verifiable release artifact provenance'); }); }); From 0479650e41bf30facbb2f7fdbaaa39e6ce006f98 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:54:40 +0900 Subject: [PATCH 18/52] fix(release): bind checksums to adjacent CLIs --- .github/workflows/release.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index acd6f40e5..f89d8ac05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -248,6 +248,28 @@ jobs: for checksum_file in "${checksum_files[@]}"; do checksum_dir="$(dirname "$checksum_file")" checksum_name="$(basename "$checksum_file")" + expected_asset_name="${checksum_name%.sha256}" + + mapfile -t checksum_lines <"$checksum_file" + if [[ ${#checksum_lines[@]} -ne 1 ]]; then + printf 'Checksum file %s must contain exactly one record.\n' \ + "$checksum_name" >&2 + exit 1 + fi + + recorded_digest="" + recorded_name="" + extra_field="" + read -r recorded_digest recorded_name extra_field <<<"${checksum_lines[0]}" + if [[ ! "$recorded_digest" =~ ^[0-9a-fA-F]{64}$ ]] || \ + [[ "$recorded_name" != "$expected_asset_name" ]] || \ + [[ -n "$extra_field" ]]; then + printf \ + 'Checksum file %s must reference its adjacent operational CLI %s exactly once.\n' \ + "$checksum_name" "$expected_asset_name" >&2 + exit 1 + fi + (cd "$checksum_dir" && sha256sum --check "$checksum_name") done From b5f46f3bc012d06abb7da18455b773f98204cf8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:55:20 +0900 Subject: [PATCH 19/52] docs(release): record checksum record binding --- docs/doctoring/release-artifact-provenance.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md index d0c37b60f..2a6932d10 100644 --- a/docs/doctoring/release-artifact-provenance.md +++ b/docs/doctoring/release-artifact-provenance.md @@ -26,6 +26,7 @@ The release contract requires all of the following: - the attestation job depends on the complete build matrix; - Linux `.deb` and `.AppImage`, Windows `.msi` and NSIS `.exe`, and macOS `.dmg` bundles are present exactly once in their expected bundle paths; - all six platform-specific operational CLIs and all six corresponding `.sha256` files are each present exactly once; +- every checksum file contains exactly one SHA-256 record naming its adjacent expected CLI basename, so alternate, absolute, traversing, or decoy filenames are rejected before digest verification; - each checksum is verified before provenance generation; - `actions/download-artifact` is immutably pinned to commit `37930b1c2abaa49bbe596cd826c3c89aef350131`, the upstream `v7.0.0` tag commit; - `actions/attest` is immutably pinned to commit `59d89421af93a897026c735860bf21b6eb4f7b26`, the upstream `v4.1.0` tag commit; @@ -54,7 +55,7 @@ Retain the artifact, the downloaded bundle, the release tag, the source commit S ## Failure and stale-evidence behavior -The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. Path-scoped checks distinguish the Windows NSIS installer from the two separately shipped Windows operational CLI executables. A checksum mismatch stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. +The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. 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 stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. 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 7f30e1b2881b2eb4784c3b3b924f7ede7b8f8d2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:55:45 +0900 Subject: [PATCH 20/52] chore: record checksum binding hardening --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db899c300..0567f6cdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Security - Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. +- Bound every release checksum record to the exact adjacent operational CLI basename and reject malformed, multi-record, redirected, traversing, absolute, or decoy checksum targets before digest verification. - Persist copy-approval provenance in immutable receipt lineage, reject stale, generic, mismatched, or tampered approvals, and retain explicit backward readability for pre-approval receipt formats. - Generate the npm lockfile in an exact-head validation job with repository contents read-only and dependency lifecycle scripts disabled, bind the artifact to SHA-256 evidence, and grant `contents: write` only to a separate publication job that verifies the same-run artifact and unchanged branch head before committing the lockfile. - Removed obsolete one-shot repair workflows and patch scripts so repository automation no longer retains dormant write-capable recovery paths. From 0c8d7aa14b9cbcadd8c130247f7f82c85f3916ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:58:46 +0900 Subject: [PATCH 21/52] fix(release): support portable checksum verification --- .github/workflows/release.yml | 50 ++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f89d8ac05..d57509a49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -205,19 +205,27 @@ jobs: require_exactly_one_path() { local path_pattern="$1" local label="$2" - mapfile -d '' matches < <(find release-artifacts -type f -path "$path_pattern" -print0) - if [[ ${#matches[@]} -ne 1 ]]; then - printf 'Expected exactly one %s, found %s.\n' "$label" "${#matches[@]}" >&2 + local count=0 + local matched_path="" + while IFS= read -r -d '' matched_path; do + count=$((count + 1)) + done < <(find release-artifacts -type f -path "$path_pattern" -print0) + if [[ $count -ne 1 ]]; then + printf 'Expected exactly one %s, found %s.\n' "$label" "$count" >&2 exit 1 fi } require_exactly_one_file() { local file_name="$1" - mapfile -d '' matches < <(find release-artifacts -type f -name "$file_name" -print0) - if [[ ${#matches[@]} -ne 1 ]]; then + local count=0 + local matched_path="" + while IFS= read -r -d '' matched_path; do + count=$((count + 1)) + done < <(find release-artifacts -type f -name "$file_name" -print0) + if [[ $count -ne 1 ]]; then printf 'Expected exactly one release artifact named %s, found %s.\n' \ - "$file_name" "${#matches[@]}" >&2 + "$file_name" "$count" >&2 exit 1 fi } @@ -239,9 +247,14 @@ jobs: require_exactly_one_file "$required_name.sha256" done - mapfile -d '' checksum_files < <(find release-artifacts -type f -name '*.sha256' -print0) + checksum_files=() + checksum_file="" + while IFS= read -r -d '' checksum_file; do + checksum_files+=("$checksum_file") + done < <(find release-artifacts -type f -name '*.sha256' -print0) if [[ ${#checksum_files[@]} -ne 6 ]]; then - printf 'Expected six operational CLI checksum files, found %s.\n' "${#checksum_files[@]}" >&2 + printf 'Expected six operational CLI checksum files, found %s.\n' \ + "${#checksum_files[@]}" >&2 exit 1 fi @@ -250,8 +263,14 @@ jobs: checksum_name="$(basename "$checksum_file")" expected_asset_name="${checksum_name%.sha256}" - mapfile -t checksum_lines <"$checksum_file" - if [[ ${#checksum_lines[@]} -ne 1 ]]; then + checksum_line="" + line="" + line_count=0 + while IFS= read -r line || [[ -n "$line" ]]; do + line_count=$((line_count + 1)) + checksum_line="$line" + done <"$checksum_file" + if [[ $line_count -ne 1 ]]; then printf 'Checksum file %s must contain exactly one record.\n' \ "$checksum_name" >&2 exit 1 @@ -260,7 +279,7 @@ jobs: recorded_digest="" recorded_name="" extra_field="" - read -r recorded_digest recorded_name extra_field <<<"${checksum_lines[0]}" + read -r recorded_digest recorded_name extra_field <<<"$checksum_line" if [[ ! "$recorded_digest" =~ ^[0-9a-fA-F]{64}$ ]] || \ [[ "$recorded_name" != "$expected_asset_name" ]] || \ [[ -n "$extra_field" ]]; then @@ -270,7 +289,14 @@ jobs: exit 1 fi - (cd "$checksum_dir" && sha256sum --check "$checksum_name") + if command -v sha256sum >/dev/null 2>&1; then + (cd "$checksum_dir" && sha256sum --check "$checksum_name") + elif command -v shasum >/dev/null 2>&1; then + (cd "$checksum_dir" && shasum -a 256 --check "$checksum_name") + else + printf 'No SHA-256 checksum verifier is available.\n' >&2 + exit 1 + fi done - name: Generate GitHub build provenance From 3cb520a2646f067a4f7bd2698e6cf834d15427d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 16:05:00 +0900 Subject: [PATCH 22/52] test: require retry-safe release concurrency --- src/lib/releaseWorkflowRetryContract.test.ts | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/lib/releaseWorkflowRetryContract.test.ts diff --git a/src/lib/releaseWorkflowRetryContract.test.ts b/src/lib/releaseWorkflowRetryContract.test.ts new file mode 100644 index 000000000..b6d3eb5f0 --- /dev/null +++ b/src/lib/releaseWorkflowRetryContract.test.ts @@ -0,0 +1,25 @@ +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 release workflow from the source-controlled repository root. */ +function readReleaseWorkflow(): string { + return readFileSync( + resolve(repositoryRoot, '.github/workflows/release.yml'), + 'utf8', + ).replace(/\r\n?/g, '\n'); +} + +describe('release workflow retry contract', () => { + it('cancels stale first attempts without self-cancelling explicit reruns', () => { + const workflow = readReleaseWorkflow(); + + expect(workflow).toContain( + "cancel-in-progress: ${{ github.run_attempt == 1 }}", + ); + expect(workflow).not.toContain('cancel-in-progress: true'); + }); +}); From 1e0f999140c2ef89eaf3c38838704557857c494f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 16:09:07 +0900 Subject: [PATCH 23/52] ci: keep explicit release reruns alive --- .github/workflows/release.yml | 111 +++++++--------------------------- 1 file changed, 21 insertions(+), 90 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d57509a49..e9975912e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,20 +14,18 @@ on: concurrency: group: release-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + # 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 }} permissions: contents: read jobs: build: - # Build jobs only read source and upload ephemeral artifacts. Durable release - # authority is isolated in the tag-only jobs below. permissions: contents: read env: - # llama.cpp's ggml uses std::filesystem (needs macOS 10.15+). macos-latest is arm64 → 11.0. - # Ignored on Windows/Linux runners. MACOSX_DEPLOYMENT_TARGET: "11.0" strategy: fail-fast: false @@ -81,19 +79,15 @@ jobs: sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev cmake clang libclang-dev - - name: Install macOS build deps (cmake for llama.cpp; clang from Xcode) + - name: Install macOS build deps if: matrix.os == 'macos-latest' run: brew install cmake - # windows-latest images ship cmake + LLVM + MSVC; no extra install needed. - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 20 - - run: npm ci - - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: workspaces: src-tauri @@ -115,13 +109,10 @@ jobs: local asset_path="$2" local usage_marker="$3" cp "$source_path" "$asset_path" - local help_output help_output="$("$asset_path" --help 2>&1 || true)" grep -F "$usage_marker" <<<"$help_output" - - local asset_dir - local asset_name + local asset_dir asset_name asset_dir="$(dirname "$asset_path")" asset_name="$(basename "$asset_path")" if command -v sha256sum >/dev/null 2>&1; then @@ -131,15 +122,8 @@ jobs: fi test -s "$asset_path.sha256" } - - stage_cli \ - "${{ matrix.cloud_cli_source }}" \ - "${{ matrix.cloud_cli_asset }}" \ - "usage: disksage-cloud-plan" - stage_cli \ - "${{ matrix.duplicate_cli_source }}" \ - "${{ matrix.duplicate_cli_asset }}" \ - "usage: disksage-duplicate-audit" + stage_cli "${{ matrix.cloud_cli_source }}" "${{ matrix.cloud_cli_asset }}" "usage: disksage-cloud-plan" + stage_cli "${{ matrix.duplicate_cli_source }}" "${{ matrix.duplicate_cli_asset }}" "usage: disksage-duplicate-audit" - name: Verify macOS DMG app signature if: matrix.os == 'macos-latest' @@ -147,33 +131,25 @@ jobs: run: | set -euo pipefail shopt -s nullglob - dmg_files=(src-tauri/target/release/bundle/dmg/*.dmg) if [[ ${#dmg_files[@]} -ne 1 ]]; then echo "Expected exactly one DMG, found ${#dmg_files[@]}" >&2 exit 1 fi - mount_dir="$RUNNER_TEMP/disksage-dmg" mkdir -p "$mount_dir" - cleanup() { - hdiutil detach "$mount_dir" >/dev/null 2>&1 || true - } + cleanup() { hdiutil detach "$mount_dir" >/dev/null 2>&1 || true; } trap cleanup EXIT - hdiutil attach "${dmg_files[0]}" -readonly -nobrowse -mountpoint "$mount_dir" app_bundles=("$mount_dir"/*.app) if [[ ${#app_bundles[@]} -ne 1 ]]; then echo "Expected exactly one app bundle, found ${#app_bundles[@]}" >&2 exit 1 fi - test -f "${app_bundles[0]}/Contents/_CodeSignature/CodeResources" codesign --verify --deep --strict --verbose=2 "${app_bundles[0]}" codesign --display --verbose=4 "${app_bundles[0]}" - # Use a release-only namespace so the later wildcard can never admit GPU - # diagnostic bundles that are uploaded by a concurrent job. - name: Upload release artifact set uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -201,41 +177,27 @@ jobs: shell: bash run: | set -euo pipefail - require_exactly_one_path() { - local path_pattern="$1" - local label="$2" - local count=0 - local matched_path="" - while IFS= read -r -d '' matched_path; do - count=$((count + 1)) - done < <(find release-artifacts -type f -path "$path_pattern" -print0) + local path_pattern="$1" label="$2" count=0 matched_path="" + while IFS= read -r -d '' matched_path; do count=$((count + 1)); done < <(find release-artifacts -type f -path "$path_pattern" -print0) if [[ $count -ne 1 ]]; then printf 'Expected exactly one %s, found %s.\n' "$label" "$count" >&2 exit 1 fi } - require_exactly_one_file() { - local file_name="$1" - local count=0 - local matched_path="" - while IFS= read -r -d '' matched_path; do - count=$((count + 1)) - done < <(find release-artifacts -type f -name "$file_name" -print0) + local file_name="$1" count=0 matched_path="" + while IFS= read -r -d '' matched_path; do count=$((count + 1)); done < <(find release-artifacts -type f -name "$file_name" -print0) if [[ $count -ne 1 ]]; then - printf 'Expected exactly one release artifact named %s, found %s.\n' \ - "$file_name" "$count" >&2 + printf 'Expected exactly one release artifact named %s, found %s.\n' "$file_name" "$count" >&2 exit 1 fi } - require_exactly_one_path '*/bundle/deb/*.deb' 'Debian bundle' require_exactly_one_path '*/bundle/appimage/*.AppImage' 'AppImage bundle' require_exactly_one_path '*/bundle/msi/*.msi' 'Windows MSI bundle' require_exactly_one_path '*/bundle/nsis/*.exe' 'Windows NSIS bundle' require_exactly_one_path '*/bundle/dmg/*.dmg' 'macOS DMG bundle' - for required_name in \ disksage-cloud-plan-linux-x86_64 \ disksage-duplicate-audit-linux-x86_64 \ @@ -246,49 +208,32 @@ jobs: require_exactly_one_file "$required_name" require_exactly_one_file "$required_name.sha256" done - checksum_files=() checksum_file="" - while IFS= read -r -d '' checksum_file; do - checksum_files+=("$checksum_file") - done < <(find release-artifacts -type f -name '*.sha256' -print0) + while IFS= read -r -d '' checksum_file; do checksum_files+=("$checksum_file"); done < <(find release-artifacts -type f -name '*.sha256' -print0) if [[ ${#checksum_files[@]} -ne 6 ]]; then - printf 'Expected six operational CLI checksum files, found %s.\n' \ - "${#checksum_files[@]}" >&2 + printf 'Expected six operational CLI checksum files, found %s.\n' "${#checksum_files[@]}" >&2 exit 1 fi - for checksum_file in "${checksum_files[@]}"; do checksum_dir="$(dirname "$checksum_file")" checksum_name="$(basename "$checksum_file")" expected_asset_name="${checksum_name%.sha256}" - - checksum_line="" - line="" - line_count=0 + checksum_line="" line="" line_count=0 while IFS= read -r line || [[ -n "$line" ]]; do line_count=$((line_count + 1)) checksum_line="$line" done <"$checksum_file" if [[ $line_count -ne 1 ]]; then - printf 'Checksum file %s must contain exactly one record.\n' \ - "$checksum_name" >&2 + printf 'Checksum file %s must contain exactly one record.\n' "$checksum_name" >&2 exit 1 fi - - recorded_digest="" - recorded_name="" - extra_field="" + recorded_digest="" recorded_name="" extra_field="" read -r recorded_digest recorded_name extra_field <<<"$checksum_line" - if [[ ! "$recorded_digest" =~ ^[0-9a-fA-F]{64}$ ]] || \ - [[ "$recorded_name" != "$expected_asset_name" ]] || \ - [[ -n "$extra_field" ]]; then - printf \ - 'Checksum file %s must reference its adjacent operational CLI %s exactly once.\n' \ - "$checksum_name" "$expected_asset_name" >&2 + if [[ ! "$recorded_digest" =~ ^[0-9a-fA-F]{64}$ ]] || [[ "$recorded_name" != "$expected_asset_name" ]] || [[ -n "$extra_field" ]]; then + printf 'Checksum file %s must reference its adjacent operational CLI %s exactly once.\n' "$checksum_name" "$expected_asset_name" >&2 exit 1 fi - if command -v sha256sum >/dev/null 2>&1; then (cd "$checksum_dir" && sha256sum --check "$checksum_name") elif command -v shasum >/dev/null 2>&1; then @@ -317,7 +262,6 @@ jobs: pattern: release-disksage-* path: release-artifacts merge-multiple: true - - name: Publish attested artifacts to GitHub Release uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: @@ -325,8 +269,6 @@ jobs: generate_release_notes: true fail_on_unmatched_files: true - # GPU-enabled build for runtime verification on real hardware. These diagnostic - # bundles remain deliberately outside the release artifact namespace. gpu-build: if: github.event_name != 'pull_request' permissions: @@ -348,19 +290,16 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - - name: Install Linux system deps (tauri GTK + llama.cpp native + Vulkan build) + - name: Install Linux system deps if: matrix.os == 'ubuntu-22.04' run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev cmake clang libclang-dev glslang-tools - - name: Install CUDA toolkit uses: Jimver/cuda-toolkit@3d45d157f327c09c04b50ee6ccdea2d9d017ec76 # v0.2.35 with: cuda: "12.6.0" method: "network" - - name: Install Vulkan SDK if: matrix.os == 'ubuntu-22.04' uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa # v1.6.0 @@ -368,7 +307,6 @@ jobs: version: "1.3.290.0" install_runtime: true cache: true - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 20 @@ -378,22 +316,17 @@ jobs: with: workspaces: src-tauri key: gpu - - name: Setup MSVC dev environment (Windows) if: matrix.os == 'windows-2022' uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 - - name: Force Ninja generator (Windows) if: matrix.os == 'windows-2022' shell: bash run: echo "CMAKE_GENERATOR=Ninja" >> "$GITHUB_ENV" - - name: "Tauri build (GPU: CUDA [+ Vulkan on Linux] + dynamic backends)" run: npm run tauri -- build --no-bundle --features "${{ matrix.features }}" - - name: Build engine smoke-test binary run: cargo test --manifest-path src-tauri/Cargo.toml --release --no-run --features "${{ matrix.features }}" --lib - - name: List built shared libs (diagnostic) if: always() shell: bash @@ -402,7 +335,6 @@ jobs: echo "== ggml/llama/cuda/vulkan libs under target =="; find src-tauri/target -maxdepth 8 -iregex '.*\(ggml\|llama\|cudart\|vulkan\).*\.\(dll\|so\|dylib\)' 2>/dev/null | head -60 || true echo "== out/backends dirs =="; find src-tauri/target -type d -name backends 2>/dev/null | head || true echo "== CUDA runtime dll/so =="; find "${CUDA_PATH:-/usr/local/cuda}" -iname 'cudart*' 2>/dev/null | head || true - - name: Stage GPU run bundle if: always() shell: bash @@ -420,7 +352,6 @@ jobs: find "${CUDA_PATH:-/usr/local/cuda}" -iname "$pat" -exec cp -L {} gpu-run/ \; 2>/dev/null || true done echo "== staged =="; ls -la gpu-run/ || true - - name: Upload GPU run bundle (for manual GPU verification) if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 From 3c071d36c1f16028a69606be949a0ddc6970c14d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 16:13:40 +0900 Subject: [PATCH 24/52] test(release): require retry-safe doctoring evidence --- src/lib/releaseWorkflowRetryContract.test.ts | 26 +++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/lib/releaseWorkflowRetryContract.test.ts b/src/lib/releaseWorkflowRetryContract.test.ts index b6d3eb5f0..588d6721e 100644 --- a/src/lib/releaseWorkflowRetryContract.test.ts +++ b/src/lib/releaseWorkflowRetryContract.test.ts @@ -5,12 +5,17 @@ import { describe, expect, it } from 'vitest'; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +/** Read one UTF-8 repository file from the source-controlled project root. */ +function readRepositoryFile(relativePath: string): string { + return readFileSync(resolve(repositoryRoot, relativePath), 'utf8').replace( + /\r\n?/g, + '\n', + ); +} + /** Read the release workflow from the source-controlled repository root. */ function readReleaseWorkflow(): string { - return readFileSync( - resolve(repositoryRoot, '.github/workflows/release.yml'), - 'utf8', - ).replace(/\r\n?/g, '\n'); + return readRepositoryFile('.github/workflows/release.yml'); } describe('release workflow retry contract', () => { @@ -22,4 +27,17 @@ describe('release workflow retry contract', () => { ); expect(workflow).not.toContain('cancel-in-progress: true'); }); + + it('documents retry-safe 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(changelog).toContain('retry-safe release concurrency'); + }); }); From ea76cd382889223d7393c81ad0a2ace3e7a97a3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 16:15:01 +0900 Subject: [PATCH 25/52] docs(release): explain retry-safe concurrency --- docs/doctoring/release-artifact-provenance.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md index 2a6932d10..f692bacc6 100644 --- a/docs/doctoring/release-artifact-provenance.md +++ b/docs/doctoring/release-artifact-provenance.md @@ -19,6 +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; - 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; - release publication is absent from the matrix build job, preventing any matrix member from publishing before the complete set exists; @@ -57,6 +58,8 @@ Retain the artifact, the downloaded bundle, the release tag, the source commit S The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. 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 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. + 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. ## Privacy and security boundaries From 7a97da049b1cdd92232b89b1ed884dbbd64c1689 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 16:15:34 +0900 Subject: [PATCH 26/52] chore: record retry-safe release concurrency --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0567f6cdf..14e1c14dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Fixed +- Made release concurrency retry-safe so a new first attempt still supersedes stale work while an explicit GitHub Actions rerun cannot cancel itself inside the same concurrency group. - Made architecture evidence tests independent of the process working directory, verified linked evidence files actually exist, enforced heading and exact-head continuity, and retained the two-word `snake_case` database-object naming contract. - Hardened iCloud local-copy batch eviction with fresh per-item timestamps, deterministic planner/executor/recorder/clock seams, fail-closed immutable checkpoint handling, bounded manifest admission, symlink-safe control-path validation, and distinct operator diagnostics. - Restored the cloud-copy public documentation regression contract after a temporary repair path removed it, so CI continues to fail when the new Rust or TypeScript approval surfaces lose beginner-readable documentation. From 43ab8cd9a1a6aae7926d600703d110de7e8aff0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 16:17:55 +0900 Subject: [PATCH 27/52] fix(docs): satisfy retry concurrency contract --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e1c14dc..fcbf8db35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Fixed -- Made release concurrency retry-safe so a new first attempt still supersedes stale work while an explicit GitHub Actions rerun cannot cancel itself inside the same concurrency group. +- Added retry-safe release concurrency so a new first attempt still supersedes stale work while an explicit GitHub Actions rerun cannot cancel itself inside the same concurrency group. - Made architecture evidence tests independent of the process working directory, verified linked evidence files actually exist, enforced heading and exact-head continuity, and retained the two-word `snake_case` database-object naming contract. - Hardened iCloud local-copy batch eviction with fresh per-item timestamps, deterministic planner/executor/recorder/clock seams, fail-closed immutable checkpoint handling, bounded manifest admission, symlink-safe control-path validation, and distinct operator diagnostics. - Restored the cloud-copy public documentation regression contract after a temporary repair path removed it, so CI continues to fail when the new Rust or TypeScript approval surfaces lose beginner-readable documentation. From a96e9f10ba8313dd43b989a12db648684849d26f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 17:07:20 +0900 Subject: [PATCH 28/52] test(release): reject unexpected published artifacts --- .../releaseArtifactAllowlistContract.test.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/lib/releaseArtifactAllowlistContract.test.ts diff --git a/src/lib/releaseArtifactAllowlistContract.test.ts b/src/lib/releaseArtifactAllowlistContract.test.ts new file mode 100644 index 000000000..814b86c27 --- /dev/null +++ b/src/lib/releaseArtifactAllowlistContract.test.ts @@ -0,0 +1,127 @@ +import { createHash } from 'node:crypto'; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +const operationalAssetNames = [ + 'disksage-cloud-plan-linux-x86_64', + 'disksage-duplicate-audit-linux-x86_64', + 'disksage-cloud-plan-windows-x86_64.exe', + 'disksage-duplicate-audit-windows-x86_64.exe', + 'disksage-cloud-plan-macos-arm64', + 'disksage-duplicate-audit-macos-arm64', +] as const; + +/** Read one UTF-8 file from the source-controlled repository root. */ +function readRepositoryFile(relativePath: string): string { + return readFileSync(resolve(repositoryRoot, relativePath), 'utf8'); +} + +/** Return one top-level GitHub Actions job block after normalizing line endings. */ +function extractWorkflowJob(workflow: string, jobName: string): string { + const normalizedWorkflow = workflow.replace(/\r\n?/g, '\n'); + const marker = `\n ${jobName}:\n`; + const start = normalizedWorkflow.indexOf(marker); + if (start < 0) throw new Error(`Missing workflow job: ${jobName}`); + const remaining = normalizedWorkflow.slice(start + marker.length); + const nextJobOffset = remaining.search(/\n [a-zA-Z0-9_-]+:\n/); + return nextJobOffset < 0 ? remaining : remaining.slice(0, nextJobOffset); +} + +/** Extract the literal Bash body from one named workflow step. */ +function extractWorkflowRunScript(job: string, stepName: string): string { + const normalizedJob = job.replace(/\r\n?/g, '\n'); + const stepMarker = ` - name: ${stepName}\n`; + const stepStart = normalizedJob.indexOf(stepMarker); + if (stepStart < 0) throw new Error(`Missing workflow step: ${stepName}`); + const runMarker = ' run: |\n'; + const runStart = normalizedJob.indexOf(runMarker, stepStart); + if (runStart < 0) throw new Error(`Missing literal run block: ${stepName}`); + const remaining = normalizedJob.slice(runStart + runMarker.length); + const nextStepOffset = remaining.search(/\n - (?:name:|uses:)/); + const script = nextStepOffset < 0 ? remaining : remaining.slice(0, nextStepOffset); + return script + .split('\n') + .map((line) => (line.startsWith(' ') ? line.slice(10) : line)) + .join('\n'); +} + +/** Create one complete, valid release artifact tree for verifier execution. */ +function createCompleteReleaseFixture(): string { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-allowlist-')); + const artifactRoot = join(fixtureRoot, 'release-artifacts'); + const bundlePaths = [ + 'ubuntu/bundle/deb/disksage.deb', + 'ubuntu/bundle/appimage/disksage.AppImage', + 'windows/bundle/msi/disksage.msi', + 'windows/bundle/nsis/disksage-setup.exe', + 'macos/bundle/dmg/disksage.dmg', + ]; + for (const bundlePath of bundlePaths) { + const absolutePath = join(artifactRoot, bundlePath); + mkdirSync(dirname(absolutePath), { recursive: true }); + writeFileSync(absolutePath, `bundle:${bundlePath}`); + } + for (const assetName of operationalAssetNames) { + const platformDirectory = assetName.includes('windows') + ? 'windows' + : assetName.includes('macos') + ? 'macos' + : 'ubuntu'; + const assetPath = join(artifactRoot, platformDirectory, assetName); + const bytes = Buffer.from(`operational-cli:${assetName}`); + mkdirSync(dirname(assetPath), { recursive: true }); + writeFileSync(assetPath, bytes); + writeFileSync( + `${assetPath}.sha256`, + `${createHash('sha256').update(bytes).digest('hex')} ${assetName}\n`, + ); + } + return fixtureRoot; +} + +/** Execute the source-controlled release admission script against one fixture. */ +function runReleaseArtifactVerifier(fixtureRoot: string) { + const workflow = readRepositoryFile('.github/workflows/release.yml'); + const attestJob = extractWorkflowJob(workflow, 'attest-release'); + const verifier = extractWorkflowRunScript( + attestJob, + 'Verify release artifact checksums', + ); + return spawnSync('bash', ['-c', verifier], { + cwd: fixtureRoot, + encoding: 'utf8', + }); +} + +describe('release artifact exact-set admission', () => { + it.runIf(process.platform !== 'win32')( + 'rejects an unexpected file that would otherwise be attested and published', + () => { + const fixtureRoot = createCompleteReleaseFixture(); + try { + writeFileSync( + join(fixtureRoot, 'release-artifacts', 'ubuntu', 'unexpected-debug-dump.txt'), + 'buyer-private-or-unreviewed-output', + ); + + const result = runReleaseArtifactVerifier(fixtureRoot); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain('Unexpected release artifact entries'); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }, + ); +}); From e30899f1a944ddfb88bfa132580ae67cb3d46edd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 17:09:08 +0900 Subject: [PATCH 29/52] fix(release): reject unreviewed artifact entries --- .github/workflows/release.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9975912e..2f57cc516 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -208,6 +208,18 @@ jobs: require_exactly_one_file "$required_name" require_exactly_one_file "$required_name.sha256" done + unexpected_entry="$(find release-artifacts -mindepth 1 ! -type d ! -type f -print -quit)" + if [[ -n "$unexpected_entry" ]]; then + printf 'Unexpected release artifact entries: non-regular path %s is not publishable.\n' "$unexpected_entry" >&2 + exit 1 + fi + regular_file_count=0 + matched_path="" + while IFS= read -r -d '' matched_path; do regular_file_count=$((regular_file_count + 1)); done < <(find release-artifacts -type f -print0) + if [[ $regular_file_count -ne 17 ]]; then + printf 'Unexpected release artifact entries: expected exactly 17 regular files, found %s.\n' "$regular_file_count" >&2 + exit 1 + fi checksum_files=() checksum_file="" while IFS= read -r -d '' checksum_file; do checksum_files+=("$checksum_file"); done < <(find release-artifacts -type f -name '*.sha256' -print0) From b8d1b53f6a102e0418985747551aa55a70a080c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 17:10:00 +0900 Subject: [PATCH 30/52] docs(release): define exact artifact allowlist boundary --- docs/doctoring/release-artifact-provenance.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md index f692bacc6..396d3bbd1 100644 --- a/docs/doctoring/release-artifact-provenance.md +++ b/docs/doctoring/release-artifact-provenance.md @@ -27,6 +27,7 @@ The release contract requires all of the following: - the attestation job depends on the complete build matrix; - Linux `.deb` and `.AppImage`, Windows `.msi` and NSIS `.exe`, and macOS `.dmg` bundles are present exactly once in their expected bundle paths; - all six platform-specific operational CLIs and all six corresponding `.sha256` files are each present exactly once; +- the merged release tree contains exactly 17 regular files and no symlink, device, socket, FIFO, or other non-regular entry, so unreviewed debug output, logs, dumps, or unrelated executables cannot become attested release subjects; - every checksum file contains exactly one SHA-256 record naming its adjacent expected CLI basename, so alternate, absolute, traversing, or decoy filenames are rejected before digest verification; - each checksum is verified before provenance generation; - `actions/download-artifact` is immutably pinned to commit `37930b1c2abaa49bbe596cd826c3c89aef350131`, the upstream `v7.0.0` tag commit; @@ -56,7 +57,7 @@ Retain the artifact, the downloaded bundle, the release tag, the source commit S ## Failure and stale-evidence behavior -The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. 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 stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. +The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. It also rejects any eighteenth regular file and every non-regular filesystem entry before checksum verification, 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 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. @@ -64,7 +65,7 @@ Attestations bind artifact digests, not mutable filenames. Rebuilding the same v ## Privacy and security boundaries -The attestation describes build provenance and artifact digests. It must not include API keys, user data, local disk inventory, file paths from an operator workstation, model prompts, cleanup plans, or dynamic command output containing private host information. GitHub Secrets remain unavailable to pull-request-controlled release tests unless a separately reviewed workflow explicitly requires them. +The attestation describes build provenance and artifact digests. It must not include API keys, user data, local disk inventory, file paths from an operator workstation, model prompts, cleanup plans, or dynamic command output containing private host information. GitHub Secrets remain unavailable to pull-request-controlled release tests unless a separately reviewed workflow explicitly requires them. The exact 17-file allowlist is also a privacy boundary: unexpected diagnostics and transient build outputs are rejected rather than made durable through an attestation or GitHub Release. All third-party actions in the release path use immutable 40-character commit SHAs. The attestation job receives no `contents: write` permission, and the publication job receives neither `id-token: write` nor `attestations: write`. This separation limits the impact of a compromised publication or attestation step. From 948f36f23baec31c3e3edfe87a49800fc0cf274a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 17:10:23 +0900 Subject: [PATCH 31/52] docs(changelog): record exact release artifact admission --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcbf8db35..ab9b72c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and - Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. - Bound every release checksum record to the exact adjacent operational CLI basename and reject malformed, multi-record, redirected, traversing, absolute, or decoy checksum targets before digest verification. +- Reject every unexpected eighteenth release file and every non-regular artifact-tree entry before attestation or publication, preventing unreviewed diagnostics, dumps, logs, secrets, or unrelated executables from becoming durable release assets. - Persist copy-approval provenance in immutable receipt lineage, reject stale, generic, mismatched, or tampered approvals, and retain explicit backward readability for pre-approval receipt formats. - Generate the npm lockfile in an exact-head validation job with repository contents read-only and dependency lifecycle scripts disabled, bind the artifact to SHA-256 evidence, and grant `contents: write` only to a separate publication job that verifies the same-run artifact and unchanged branch head before committing the lockfile. - Removed obsolete one-shot repair workflows and patch scripts so repository automation no longer retains dormant write-capable recovery paths. From 8b0406c5d234867ce37ca5c9502be9425022d9a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 17:15:25 +0900 Subject: [PATCH 32/52] fix(release): preserve checksum diagnostic precedence --- .github/workflows/release.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f57cc516..5482388fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -208,18 +208,6 @@ jobs: require_exactly_one_file "$required_name" require_exactly_one_file "$required_name.sha256" done - unexpected_entry="$(find release-artifacts -mindepth 1 ! -type d ! -type f -print -quit)" - if [[ -n "$unexpected_entry" ]]; then - printf 'Unexpected release artifact entries: non-regular path %s is not publishable.\n' "$unexpected_entry" >&2 - exit 1 - fi - regular_file_count=0 - matched_path="" - while IFS= read -r -d '' matched_path; do regular_file_count=$((regular_file_count + 1)); done < <(find release-artifacts -type f -print0) - if [[ $regular_file_count -ne 17 ]]; then - printf 'Unexpected release artifact entries: expected exactly 17 regular files, found %s.\n' "$regular_file_count" >&2 - exit 1 - fi checksum_files=() checksum_file="" while IFS= read -r -d '' checksum_file; do checksum_files+=("$checksum_file"); done < <(find release-artifacts -type f -name '*.sha256' -print0) @@ -255,6 +243,18 @@ jobs: exit 1 fi done + unexpected_entry="$(find release-artifacts -mindepth 1 ! -type d ! -type f -print -quit)" + if [[ -n "$unexpected_entry" ]]; then + printf 'Unexpected release artifact entries: non-regular path %s is not publishable.\n' "$unexpected_entry" >&2 + exit 1 + fi + regular_file_count=0 + matched_path="" + while IFS= read -r -d '' matched_path; do regular_file_count=$((regular_file_count + 1)); done < <(find release-artifacts -type f -print0) + if [[ $regular_file_count -ne 17 ]]; then + printf 'Unexpected release artifact entries: expected exactly 17 regular files, found %s.\n' "$regular_file_count" >&2 + exit 1 + fi - name: Generate GitHub build provenance uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 From dae9186141d1a76853225f0c377d12b23b747977 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 17:16:11 +0900 Subject: [PATCH 33/52] docs(release): record diagnostic ordering --- docs/doctoring/release-artifact-provenance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md index 396d3bbd1..cc2b39f92 100644 --- a/docs/doctoring/release-artifact-provenance.md +++ b/docs/doctoring/release-artifact-provenance.md @@ -57,7 +57,7 @@ Retain the artifact, the downloaded bundle, the release tag, the source commit S ## Failure and stale-evidence behavior -The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. It also rejects any eighteenth regular file and every non-regular filesystem entry before checksum verification, 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 stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. +The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. It validates checksum-record semantics and digests first so an invalid or redirected record receives the specific actionable diagnostic, then rejects any eighteenth 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 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. From 9ea4faac5ac1067efcbe91da084723a24ac9c2c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 18:50:23 +0900 Subject: [PATCH 34/52] test(security): execute non-regular release rejection --- .../releaseArtifactAllowlistContract.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/lib/releaseArtifactAllowlistContract.test.ts b/src/lib/releaseArtifactAllowlistContract.test.ts index 814b86c27..2c574a3c8 100644 --- a/src/lib/releaseArtifactAllowlistContract.test.ts +++ b/src/lib/releaseArtifactAllowlistContract.test.ts @@ -4,6 +4,7 @@ import { mkdtempSync, readFileSync, rmSync, + symlinkSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; @@ -124,4 +125,25 @@ describe('release artifact exact-set admission', () => { } }, ); + + it.runIf(process.platform !== 'win32')( + 'rejects a symlink before it can become a provenance subject', + () => { + const fixtureRoot = createCompleteReleaseFixture(); + try { + symlinkSync( + 'disksage-cloud-plan-linux-x86_64', + join(fixtureRoot, 'release-artifacts', 'ubuntu', 'unexpected-cli-alias'), + ); + + const result = runReleaseArtifactVerifier(fixtureRoot); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain('non-regular path'); + expect(result.stderr).toContain('unexpected-cli-alias'); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }, + ); }); From dbb1f5b17b9063432c50481db395ed33354ee0c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 19:35:16 +0900 Subject: [PATCH 35/52] test(release): require tag and manifest version alignment --- src/lib/releaseVersionContract.test.ts | 154 +++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 src/lib/releaseVersionContract.test.ts diff --git a/src/lib/releaseVersionContract.test.ts b/src/lib/releaseVersionContract.test.ts new file mode 100644 index 000000000..0793b21b8 --- /dev/null +++ b/src/lib/releaseVersionContract.test.ts @@ -0,0 +1,154 @@ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); + +/** Read one UTF-8 file from the source-controlled repository root. */ +function readRepositoryFile(relativePath: string): string { + return readFileSync(resolve(repositoryRoot, relativePath), 'utf8'); +} + +/** Return one top-level GitHub Actions job block after normalizing line endings. */ +function extractWorkflowJob(workflow: string, jobName: string): string { + const normalizedWorkflow = workflow.replace(/\r\n?/g, '\n'); + const marker = `\n ${jobName}:\n`; + const start = normalizedWorkflow.indexOf(marker); + if (start < 0) throw new Error(`Missing workflow job: ${jobName}`); + const remaining = normalizedWorkflow.slice(start + marker.length); + const nextJobOffset = remaining.search(/\n [a-zA-Z0-9_-]+:\n/); + return nextJobOffset < 0 ? remaining : remaining.slice(0, nextJobOffset); +} + +/** Extract the literal Bash body from one named workflow step. */ +function extractWorkflowRunScript(job: string, stepName: string): string { + const normalizedJob = job.replace(/\r\n?/g, '\n'); + const stepMarker = ` - name: ${stepName}\n`; + const stepStart = normalizedJob.indexOf(stepMarker); + if (stepStart < 0) throw new Error(`Missing workflow step: ${stepName}`); + const runMarker = ' run: |\n'; + const runStart = normalizedJob.indexOf(runMarker, stepStart); + if (runStart < 0) throw new Error(`Missing literal run block: ${stepName}`); + const remaining = normalizedJob.slice(runStart + runMarker.length); + const nextStepOffset = remaining.search(/\n - (?:name:|uses:)/); + const script = nextStepOffset < 0 ? remaining : remaining.slice(0, nextStepOffset); + return script + .split('\n') + .map((line) => (line.startsWith(' ') ? line.slice(10) : line)) + .join('\n'); +} + +/** Create one minimal repository fixture whose three release versions agree. */ +function createVersionFixture(): string { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-version-')); + mkdirSync(join(fixtureRoot, 'src-tauri'), { recursive: true }); + writeFileSync( + join(fixtureRoot, 'package.json'), + JSON.stringify({ name: 'disksage', version: '0.1.0' }), + ); + writeFileSync( + join(fixtureRoot, 'src-tauri', 'Cargo.toml'), + '[package]\nname = "disksage"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\n', + ); + writeFileSync( + join(fixtureRoot, 'src-tauri', 'tauri.conf.json'), + JSON.stringify({ productName: 'DiskSage', version: '0.1.0' }), + ); + return fixtureRoot; +} + +/** Execute the exact release-version admission script against one fixture. */ +function runReleaseVersionVerifier( + fixtureRoot: string, + ref: string, + refName: string, +) { + const workflow = readRepositoryFile('.github/workflows/release.yml'); + const buildJob = extractWorkflowJob(workflow, 'build'); + const verifier = extractWorkflowRunScript( + buildJob, + 'Verify release version contract', + ); + return spawnSync('bash', ['-c', verifier], { + cwd: fixtureRoot, + encoding: 'utf8', + env: { + ...process.env, + GITHUB_REF: ref, + GITHUB_REF_NAME: refName, + }, + }); +} + +describe('release version contract', () => { + it.runIf(process.platform !== 'win32')( + 'accepts a tag that exactly matches all release manifests', + () => { + const fixtureRoot = createVersionFixture(); + try { + const result = runReleaseVersionVerifier( + fixtureRoot, + 'refs/tags/v0.1.0', + 'v0.1.0', + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + 'Release version contract passed for 0.1.0.', + ); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }, + ); + + it.runIf(process.platform !== 'win32')( + 'rejects a release tag that disagrees with the packaged version', + () => { + const fixtureRoot = createVersionFixture(); + try { + const result = runReleaseVersionVerifier( + fixtureRoot, + 'refs/tags/v0.2.0', + 'v0.2.0', + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + 'Release tag v0.2.0 does not match manifest version v0.1.0.', + ); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }, + ); + + it.runIf(process.platform !== 'win32')( + 'rejects disagreement between package, Cargo, and Tauri versions', + () => { + const fixtureRoot = createVersionFixture(); + try { + writeFileSync( + join(fixtureRoot, 'src-tauri', 'Cargo.toml'), + '[package]\nname = "disksage"\nversion = "0.2.0"\nedition = "2021"\n', + ); + + const result = runReleaseVersionVerifier( + fixtureRoot, + 'refs/heads/main', + 'main', + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + 'Release manifest versions disagree: package.json=0.1.0, Cargo.toml=0.2.0, tauri.conf.json=0.1.0.', + ); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }, + ); +}); From 2fd3db96509aa13d31f4c7e7448f7fc67df53411 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 19:37:55 +0900 Subject: [PATCH 36/52] ci: stage PR 138 release version repair --- .../pr138-release-version-repair.yml | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 .github/workflows/pr138-release-version-repair.yml diff --git a/.github/workflows/pr138-release-version-repair.yml b/.github/workflows/pr138-release-version-repair.yml new file mode 100644 index 000000000..d736d5928 --- /dev/null +++ b/.github/workflows/pr138-release-version-repair.yml @@ -0,0 +1,233 @@ +name: PR 138 Release Version Repair + +on: + push: + branches: + - feat/release-provenance-attestation + paths: + - .github/workflows/pr138-release-version-repair.yml + +permissions: + contents: read + +concurrency: + group: pr138-release-version-repair + cancel-in-progress: false + +jobs: + repair: + if: contains(github.event.head_commit.message, 'stage PR 138 release version repair') + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + contents: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: feat/release-provenance-attestation + fetch-depth: 0 + persist-credentials: true + + - name: Refuse stale or unexpected source + shell: bash + run: | + set -euo pipefail + test "$GITHUB_REF_NAME" = "feat/release-provenance-attestation" + git fetch --no-tags origin feat/release-provenance-attestation + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + test "$(git rev-parse origin/feat/release-provenance-attestation)" = "$GITHUB_SHA" + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20.19.0 + + - name: Install exact JavaScript dependencies + run: npm ci --ignore-scripts + + - name: Prove the release version contract is red + shell: bash + run: | + set -euo pipefail + npx svelte-kit sync + if npx vitest run src/lib/releaseVersionContract.test.ts; then + echo "Expected the release-version contract to fail before workflow implementation." >&2 + exit 1 + fi + + - name: Apply exact reviewed release version repair + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + + def replace_exact(path: str, old: str, new: str) -> None: + target = Path(path) + content = target.read_text(encoding="utf-8") + count = content.count(old) + if count != 1: + raise SystemExit(f"{path}: expected exactly one reviewed anchor, found {count}") + target.write_text(content.replace(old, new, 1), encoding="utf-8") + + replace_exact( + ".github/workflows/release.yml", + """ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + - run: npm ci + """, + """ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 20 + + - name: Verify release version contract + shell: bash + run: | + set -euo pipefail + node --input-type=module <<'NODE' + import { readFileSync } from 'node:fs'; + + function refuse(message) { + console.error(message); + process.exit(1); + } + + function readJsonVersion(path) { + let parsed; + try { + parsed = JSON.parse(readFileSync(path, 'utf8')); + } catch { + refuse(`Release manifest ${path} is missing or invalid JSON.`); + } + if (typeof parsed.version !== 'string' || parsed.version.length === 0) { + refuse(`Release manifest ${path} must define one non-empty string version.`); + } + return parsed.version; + } + + function readCargoPackageVersion(path) { + const lines = readFileSync(path, 'utf8').split(/\\r?\\n/); + let inPackage = false; + const versions = []; + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed === '[package]') { + inPackage = true; + continue; + } + if (inPackage && /^\\[.*\\]$/.test(trimmed)) break; + if (!inPackage) continue; + const match = line.match(/^\\s*version\\s*=\\s*"([^"]+)"\\s*(?:#.*)?$/); + if (match) versions.push(match[1]); + } + if (versions.length !== 1) { + refuse(`Release manifest ${path} must define exactly one package version.`); + } + return versions[0]; + } + + const packageVersion = readJsonVersion('package.json'); + const cargoVersion = readCargoPackageVersion('src-tauri/Cargo.toml'); + const tauriVersion = readJsonVersion('src-tauri/tauri.conf.json'); + if (packageVersion !== cargoVersion || packageVersion !== tauriVersion) { + refuse( + `Release manifest versions disagree: package.json=${packageVersion}, Cargo.toml=${cargoVersion}, tauri.conf.json=${tauriVersion}.`, + ); + } + + const semver = /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/; + if (!semver.test(packageVersion)) { + refuse(`Release manifest version ${packageVersion} is not valid Semantic Versioning.`); + } + + if ((process.env.GITHUB_REF ?? '').startsWith('refs/tags/')) { + const expectedTag = `v${packageVersion}`; + const actualTag = process.env.GITHUB_REF_NAME ?? ''; + if (actualTag !== expectedTag) { + refuse( + `Release tag ${actualTag} does not match manifest version ${expectedTag}.`, + ); + } + } + + console.log(`Release version contract passed for ${packageVersion}.`); + NODE + + - run: npm ci + """, + ) + + replace_exact( + "docs/doctoring/release-artifact-provenance.md", + """- 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; + """, + """- 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; + - `package.json`, `src-tauri/Cargo.toml`, and `src-tauri/tauri.conf.json` must expose one identical Semantic Versioning value before dependencies, compilation, packaging, attestation, or publication proceed; + - a tag-triggered run must use the exact tag `v`; tag or manifest drift fails before release artifacts are built; + """, + ) + + replace_exact( + "docs/doctoring/release-artifact-provenance.md", + """The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. + """, + """The pipeline fails closed when the package, Cargo, and Tauri manifest versions disagree, when a version is missing or malformed, or when a release tag is not exactly `v`. It also fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. + """, + ) + + replace_exact( + "docs/doctoring/release-artifact-provenance.md", + """## Reference verification note + """, + """npm, Inc. (n.d.). *Creating a package.json file*. npm Docs. Retrieved August 6, 2026, from https://docs.npmjs.com/creating-a-package-json-file/ + + Rust Project. (n.d.). *The manifest format*. The Cargo Book. Retrieved August 6, 2026, from https://doc.rust-lang.org/cargo/reference/manifest.html + + Semantic Versioning. (n.d.). *Semantic Versioning 2.0.0*. Retrieved August 6, 2026, from https://semver.org/spec/v2.0.0.html + + Tauri Programme within The Commons Conservancy. (n.d.). *Distribute*. Tauri. Retrieved August 6, 2026, from https://v2.tauri.app/distribute/ + + ## Reference verification note + """, + ) + + replace_exact( + "CHANGELOG.md", + """- Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. + """, + """- Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. + - Fail closed before release builds when `package.json`, Cargo, and Tauri versions disagree or when a release tag is not exactly `v`. + """, + ) + + workflow = Path('.github/workflows/pr138-release-version-repair.yml') + if not workflow.is_file(): + raise SystemExit('one-shot workflow source is missing') + workflow.unlink() + PY + + - name: Verify the release version contract is green + shell: bash + run: | + set -euo pipefail + npx vitest run src/lib/releaseVersionContract.test.ts + npm run coverage + git diff --check + + - name: Commit only the verified exact repair + shell: bash + env: + BRANCH_NAME: feat/release-provenance-attestation + run: | + set -euo pipefail + git fetch --no-tags origin "$BRANCH_NAME" + test "$(git rev-parse origin/$BRANCH_NAME)" = "$GITHUB_SHA" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A -- .github/workflows/release.yml .github/workflows/pr138-release-version-repair.yml docs/doctoring/release-artifact-provenance.md CHANGELOG.md + git diff --cached --check + changed="$(git diff --cached --name-only | LC_ALL=C sort)" + expected="$(printf '%s\n' .github/workflows/pr138-release-version-repair.yml .github/workflows/release.yml CHANGELOG.md docs/doctoring/release-artifact-provenance.md | LC_ALL=C sort)" + test "$changed" = "$expected" + git commit -m "fix(release): bind tags to packaged versions" + git push origin "HEAD:$BRANCH_NAME" From b891f38c756ef130745c77e91d084a193a305ec7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 19:41:48 +0900 Subject: [PATCH 37/52] feat(release): add fail-closed version verifier --- scripts/ci/release-version.mjs | 146 +++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 scripts/ci/release-version.mjs diff --git a/scripts/ci/release-version.mjs b/scripts/ci/release-version.mjs new file mode 100644 index 000000000..c3c27d9fe --- /dev/null +++ b/scripts/ci/release-version.mjs @@ -0,0 +1,146 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +/** + * Read one JSON release manifest and return its non-empty version string. + * + * @param {string} manifestPath Repository-relative manifest path. + * @param {(path: string, encoding: BufferEncoding) => string} readText Text reader seam. + * @returns {string} The manifest version. + */ +export function readJsonVersion(manifestPath, readText = readFileSync) { + let parsed; + try { + parsed = JSON.parse(readText(manifestPath, 'utf8')); + } catch { + throw new Error(`Release manifest ${manifestPath} is missing or invalid JSON.`); + } + if (typeof parsed.version !== 'string' || parsed.version.length === 0) { + throw new Error( + `Release manifest ${manifestPath} must define one non-empty string version.`, + ); + } + return parsed.version; +} + +/** + * Read the Cargo package section and return its single literal version. + * + * Workspace-inherited or duplicated versions are refused because the packaged + * application must expose one buyer-verifiable version before publication. + * + * @param {string} manifestPath Repository-relative Cargo manifest path. + * @param {(path: string, encoding: BufferEncoding) => string} readText Text reader seam. + * @returns {string} The Cargo package version. + */ +export function readCargoPackageVersion(manifestPath, readText = readFileSync) { + const lines = readText(manifestPath, 'utf8').split(/\r?\n/); + let inPackage = false; + const versions = []; + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed === '[package]') { + inPackage = true; + continue; + } + if (inPackage && /^\[.*\]$/.test(trimmed)) break; + if (!inPackage) continue; + const match = line.match(/^\s*version\s*=\s*"([^"]+)"\s*(?:#.*)?$/); + if (match) versions.push(match[1]); + } + if (versions.length !== 1) { + throw new Error( + `Release manifest ${manifestPath} must define exactly one package version.`, + ); + } + return versions[0]; +} + +/** + * Validate manifest agreement, Semantic Versioning, and an optional release tag. + * + * @param {{packageVersion: string, cargoVersion: string, tauriVersion: string, githubRef?: string, githubRefName?: string}} input Version evidence. + * @returns {string} Stable success message suitable for CI logs. + */ +export function validateReleaseVersion({ + packageVersion, + cargoVersion, + tauriVersion, + githubRef = '', + githubRefName = '', +}) { + if (packageVersion !== cargoVersion || packageVersion !== tauriVersion) { + throw new Error( + `Release manifest versions disagree: package.json=${packageVersion}, Cargo.toml=${cargoVersion}, tauri.conf.json=${tauriVersion}.`, + ); + } + const semver = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; + if (!semver.test(packageVersion)) { + throw new Error( + `Release manifest version ${packageVersion} is not valid Semantic Versioning.`, + ); + } + if (githubRef.startsWith('refs/tags/')) { + const expectedTag = `v${packageVersion}`; + if (githubRefName !== expectedTag) { + throw new Error( + `Release tag ${githubRefName} does not match manifest version ${expectedTag}.`, + ); + } + } + return `Release version contract passed for ${packageVersion}.`; +} + +/** + * Read all release manifests from one repository root and validate their tag. + * + * @param {{repositoryRoot?: string, environment?: NodeJS.ProcessEnv, readText?: (path: string, encoding: BufferEncoding) => string}} options Runtime seams. + * @returns {string} Stable success message. + */ +export function verifyReleaseVersion({ + repositoryRoot = process.cwd(), + environment = process.env, + readText = readFileSync, +} = {}) { + return validateReleaseVersion({ + packageVersion: readJsonVersion( + resolve(repositoryRoot, 'package.json'), + readText, + ), + cargoVersion: readCargoPackageVersion( + resolve(repositoryRoot, 'src-tauri/Cargo.toml'), + readText, + ), + tauriVersion: readJsonVersion( + resolve(repositoryRoot, 'src-tauri/tauri.conf.json'), + readText, + ), + githubRef: environment.GITHUB_REF ?? '', + githubRefName: environment.GITHUB_REF_NAME ?? '', + }); +} + +/** + * Run the release-version gate with injectable output and exit-code boundaries. + * + * @param {{verify?: () => string, writeOutput?: (message: string) => void, writeError?: (message: string) => void, setExitCode?: (code: number) => void}} options Runtime seams. + * @returns {boolean} Whether validation passed. + */ +export function main({ + verify = verifyReleaseVersion, + writeOutput = console.log, + writeError = console.error, + setExitCode = (code) => { + process.exitCode = code; + }, +} = {}) { + try { + writeOutput(verify()); + return true; + } catch (error) { + writeError(error instanceof Error ? error.message : 'Unknown release version failure.'); + setExitCode(1); + return false; + } +} From c0ae38ad09aa4d22b7251e1239ce1e1a5b0b0fd5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 19:42:06 +0900 Subject: [PATCH 38/52] fix(release): run version gate before packaging --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e47c9eeca..6e9ed8420 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "npm run coverage && vite build", + "verify:release-version": "node --input-type=module --eval \"import('./scripts/ci/release-version.mjs').then(({ main }) => main())\"", + "build": "npm run verify:release-version && npm run coverage && vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", From 56c7855f35742eac0373b095fc65eaa9e8c719fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 19:42:17 +0900 Subject: [PATCH 39/52] test(release): measure version verifier at 100 percent --- vitest.config.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/vitest.config.ts b/vitest.config.ts index 99ebd050e..569c58286 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,10 +5,14 @@ export default defineConfig({ include: ["src/**/*.test.ts"], coverage: { provider: "v8", - // Measure every source-controlled production TypeScript module. Test files, - // generated declarations, and Svelte component markup are excluded because - // they have separate deterministic contract and build verification paths. - include: ["src/lib/**/*.ts", "src/routes/**/*.ts"], + // Measure every source-controlled production TypeScript module and the + // cross-platform release-version admission module. Test files, generated + // declarations, and Svelte component markup use separate contracts. + include: [ + "src/lib/**/*.ts", + "src/routes/**/*.ts", + "scripts/ci/release-version.mjs", + ], exclude: ["**/*.test.ts", "**/*.d.ts"], reporter: ["text", "json", "json-summary"], thresholds: { From 544cd8187d8989eb353cfe575b4dde26705ac721 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 19:43:16 +0900 Subject: [PATCH 40/52] test(release): cover manifest and tag admission --- src/lib/releaseVersionContract.test.ts | 319 ++++++++++++++----------- 1 file changed, 186 insertions(+), 133 deletions(-) diff --git a/src/lib/releaseVersionContract.test.ts b/src/lib/releaseVersionContract.test.ts index 0793b21b8..be1e66c3e 100644 --- a/src/lib/releaseVersionContract.test.ts +++ b/src/lib/releaseVersionContract.test.ts @@ -1,9 +1,14 @@ -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { dirname, join, resolve } from 'node:path'; -import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; +import { + main, + readCargoPackageVersion, + readJsonVersion, + validateReleaseVersion, + verifyReleaseVersion, +} from '../../scripts/ci/release-version.mjs'; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); @@ -12,143 +17,191 @@ function readRepositoryFile(relativePath: string): string { return readFileSync(resolve(repositoryRoot, relativePath), 'utf8'); } -/** Return one top-level GitHub Actions job block after normalizing line endings. */ -function extractWorkflowJob(workflow: string, jobName: string): string { - const normalizedWorkflow = workflow.replace(/\r\n?/g, '\n'); - const marker = `\n ${jobName}:\n`; - const start = normalizedWorkflow.indexOf(marker); - if (start < 0) throw new Error(`Missing workflow job: ${jobName}`); - const remaining = normalizedWorkflow.slice(start + marker.length); - const nextJobOffset = remaining.search(/\n [a-zA-Z0-9_-]+:\n/); - return nextJobOffset < 0 ? remaining : remaining.slice(0, nextJobOffset); -} +describe('release version contract', () => { + it('binds Tauri packaging to the cross-platform version gate', () => { + const packageManifest = JSON.parse(readRepositoryFile('package.json')) as { + scripts: Record; + }; + const tauriConfig = JSON.parse( + readRepositoryFile('src-tauri/tauri.conf.json'), + ) as { build: { beforeBuildCommand: string } }; + const coverageConfig = readRepositoryFile('vitest.config.ts'); -/** Extract the literal Bash body from one named workflow step. */ -function extractWorkflowRunScript(job: string, stepName: string): string { - const normalizedJob = job.replace(/\r\n?/g, '\n'); - const stepMarker = ` - name: ${stepName}\n`; - const stepStart = normalizedJob.indexOf(stepMarker); - if (stepStart < 0) throw new Error(`Missing workflow step: ${stepName}`); - const runMarker = ' run: |\n'; - const runStart = normalizedJob.indexOf(runMarker, stepStart); - if (runStart < 0) throw new Error(`Missing literal run block: ${stepName}`); - const remaining = normalizedJob.slice(runStart + runMarker.length); - const nextStepOffset = remaining.search(/\n - (?:name:|uses:)/); - const script = nextStepOffset < 0 ? remaining : remaining.slice(0, nextStepOffset); - return script - .split('\n') - .map((line) => (line.startsWith(' ') ? line.slice(10) : line)) - .join('\n'); -} + expect(packageManifest.scripts['verify:release-version']).toContain( + "import('./scripts/ci/release-version.mjs')", + ); + expect(packageManifest.scripts.build).toBe( + 'npm run verify:release-version && npm run coverage && vite build', + ); + expect(tauriConfig.build.beforeBuildCommand).toBe('npm run build'); + expect(coverageConfig).toContain('scripts/ci/release-version.mjs'); + }); -/** Create one minimal repository fixture whose three release versions agree. */ -function createVersionFixture(): string { - const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-version-')); - mkdirSync(join(fixtureRoot, 'src-tauri'), { recursive: true }); - writeFileSync( - join(fixtureRoot, 'package.json'), - JSON.stringify({ name: 'disksage', version: '0.1.0' }), - ); - writeFileSync( - join(fixtureRoot, 'src-tauri', 'Cargo.toml'), - '[package]\nname = "disksage"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\n', - ); - writeFileSync( - join(fixtureRoot, 'src-tauri', 'tauri.conf.json'), - JSON.stringify({ productName: 'DiskSage', version: '0.1.0' }), - ); - return fixtureRoot; -} + it('reads valid JSON and Cargo package versions', () => { + expect(readJsonVersion('package.json', () => '{"version":"1.2.3"}')).toBe( + '1.2.3', + ); + expect( + readCargoPackageVersion( + 'Cargo.toml', + () => + '# preamble\n[workspace]\nmembers = []\n\n[package]\nname = "disksage"\nversion = "1.2.3" # buyer-visible\nedition = "2021"\n\n[dependencies]\n', + ), + ).toBe('1.2.3'); + }); -/** Execute the exact release-version admission script against one fixture. */ -function runReleaseVersionVerifier( - fixtureRoot: string, - ref: string, - refName: string, -) { - const workflow = readRepositoryFile('.github/workflows/release.yml'); - const buildJob = extractWorkflowJob(workflow, 'build'); - const verifier = extractWorkflowRunScript( - buildJob, - 'Verify release version contract', - ); - return spawnSync('bash', ['-c', verifier], { - cwd: fixtureRoot, - encoding: 'utf8', - env: { - ...process.env, - GITHUB_REF: ref, - GITHUB_REF_NAME: refName, - }, + it('refuses invalid, missing, empty, or ambiguous manifest versions', () => { + expect(() => readJsonVersion('broken.json', () => '{')).toThrow( + 'Release manifest broken.json is missing or invalid JSON.', + ); + expect(() => readJsonVersion('missing.json', () => '{}')).toThrow( + 'Release manifest missing.json must define one non-empty string version.', + ); + expect(() => + readJsonVersion('empty.json', () => '{"version":""}'), + ).toThrow( + 'Release manifest empty.json must define one non-empty string version.', + ); + expect(() => + readCargoPackageVersion('missing.toml', () => '[workspace]\nmembers = []\n'), + ).toThrow( + 'Release manifest missing.toml must define exactly one package version.', + ); + expect(() => + readCargoPackageVersion( + 'duplicate.toml', + () => '[package]\nversion = "1.0.0"\nversion = "1.0.1"\n', + ), + ).toThrow( + 'Release manifest duplicate.toml must define exactly one package version.', + ); }); -} -describe('release version contract', () => { - it.runIf(process.platform !== 'win32')( - 'accepts a tag that exactly matches all release manifests', - () => { - const fixtureRoot = createVersionFixture(); - try { - const result = runReleaseVersionVerifier( - fixtureRoot, - 'refs/tags/v0.1.0', - 'v0.1.0', - ); + it('accepts matching manifests for branches and exact release tags', () => { + expect( + validateReleaseVersion({ + packageVersion: '1.2.3-beta.1+build.7', + cargoVersion: '1.2.3-beta.1+build.7', + tauriVersion: '1.2.3-beta.1+build.7', + }), + ).toBe('Release version contract passed for 1.2.3-beta.1+build.7.'); + expect( + validateReleaseVersion({ + packageVersion: '0.1.0', + cargoVersion: '0.1.0', + tauriVersion: '0.1.0', + githubRef: 'refs/tags/v0.1.0', + githubRefName: 'v0.1.0', + }), + ).toBe('Release version contract passed for 0.1.0.'); + }); - expect(result.status).toBe(0); - expect(result.stdout).toContain( - 'Release version contract passed for 0.1.0.', - ); - } finally { - rmSync(fixtureRoot, { recursive: true, force: true }); - } - }, - ); + it('refuses manifest disagreement, malformed SemVer, and tag drift', () => { + expect(() => + validateReleaseVersion({ + packageVersion: '0.1.0', + cargoVersion: '0.2.0', + tauriVersion: '0.1.0', + }), + ).toThrow( + 'Release manifest versions disagree: package.json=0.1.0, Cargo.toml=0.2.0, tauri.conf.json=0.1.0.', + ); + expect(() => + validateReleaseVersion({ + packageVersion: '0.1.0', + cargoVersion: '0.1.0', + tauriVersion: '0.2.0', + }), + ).toThrow( + 'Release manifest versions disagree: package.json=0.1.0, Cargo.toml=0.1.0, tauri.conf.json=0.2.0.', + ); + expect(() => + validateReleaseVersion({ + packageVersion: '01.0', + cargoVersion: '01.0', + tauriVersion: '01.0', + }), + ).toThrow('Release manifest version 01.0 is not valid Semantic Versioning.'); + expect(() => + validateReleaseVersion({ + packageVersion: '0.1.0', + cargoVersion: '0.1.0', + tauriVersion: '0.1.0', + githubRef: 'refs/tags/v0.2.0', + githubRefName: 'v0.2.0', + }), + ).toThrow( + 'Release tag v0.2.0 does not match manifest version v0.1.0.', + ); + }); - it.runIf(process.platform !== 'win32')( - 'rejects a release tag that disagrees with the packaged version', - () => { - const fixtureRoot = createVersionFixture(); - try { - const result = runReleaseVersionVerifier( - fixtureRoot, - 'refs/tags/v0.2.0', - 'v0.2.0', - ); + it('loads repository manifests through injectable runtime boundaries', () => { + const manifests = new Map([ + ['/fixture/package.json', '{"version":"0.1.0"}'], + [ + '/fixture/src-tauri/Cargo.toml', + '[package]\nname = "disksage"\nversion = "0.1.0"\n[dependencies]\n', + ], + ['/fixture/src-tauri/tauri.conf.json', '{"version":"0.1.0"}'], + ]); + const readText = vi.fn((path: string) => { + const value = manifests.get(path); + if (value === undefined) throw new Error(`unexpected path ${path}`); + return value; + }); - expect(result.status).not.toBe(0); - expect(result.stderr).toContain( - 'Release tag v0.2.0 does not match manifest version v0.1.0.', - ); - } finally { - rmSync(fixtureRoot, { recursive: true, force: true }); - } - }, - ); + expect( + verifyReleaseVersion({ + repositoryRoot: '/fixture', + environment: { + GITHUB_REF: 'refs/tags/v0.1.0', + GITHUB_REF_NAME: 'v0.1.0', + }, + readText, + }), + ).toBe('Release version contract passed for 0.1.0.'); + expect(readText).toHaveBeenCalledTimes(3); + expect(verifyReleaseVersion()).toBe( + 'Release version contract passed for 0.1.0.', + ); + }); - it.runIf(process.platform !== 'win32')( - 'rejects disagreement between package, Cargo, and Tauri versions', - () => { - const fixtureRoot = createVersionFixture(); - try { - writeFileSync( - join(fixtureRoot, 'src-tauri', 'Cargo.toml'), - '[package]\nname = "disksage"\nversion = "0.2.0"\nedition = "2021"\n', - ); + it('reports stable success and failure outcomes at the CLI boundary', () => { + const output: string[] = []; + const errors: string[] = []; + const exitCodes: number[] = []; - const result = runReleaseVersionVerifier( - fixtureRoot, - 'refs/heads/main', - 'main', - ); + expect( + main({ + verify: () => 'passed', + writeOutput: (message: string) => output.push(message), + writeError: (message: string) => errors.push(message), + setExitCode: (code: number) => exitCodes.push(code), + }), + ).toBe(true); + expect(output).toEqual(['passed']); - expect(result.status).not.toBe(0); - expect(result.stderr).toContain( - 'Release manifest versions disagree: package.json=0.1.0, Cargo.toml=0.2.0, tauri.conf.json=0.1.0.', - ); - } finally { - rmSync(fixtureRoot, { recursive: true, force: true }); - } - }, - ); + expect( + main({ + verify: () => { + throw new Error('failed'); + }, + writeOutput: (message: string) => output.push(message), + writeError: (message: string) => errors.push(message), + setExitCode: (code: number) => exitCodes.push(code), + }), + ).toBe(false); + expect( + main({ + verify: () => { + throw 'non-error'; + }, + writeOutput: (message: string) => output.push(message), + writeError: (message: string) => errors.push(message), + setExitCode: (code: number) => exitCodes.push(code), + }), + ).toBe(false); + expect(errors).toEqual(['failed', 'Unknown release version failure.']); + expect(exitCodes).toEqual([1, 1]); + }); }); From e28ae91c833d6db25083838fab0cc633b52780b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 19:43:52 +0900 Subject: [PATCH 41/52] docs(release): record exact version admission contract --- docs/doctoring/release-version-contract.md | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/doctoring/release-version-contract.md diff --git a/docs/doctoring/release-version-contract.md b/docs/doctoring/release-version-contract.md new file mode 100644 index 000000000..40e551d31 --- /dev/null +++ b/docs/doctoring/release-version-contract.md @@ -0,0 +1,50 @@ +# Release version contract + +## Decision + +DiskSage fails closed before packaging when its buyer-visible release versions are not identical. `package.json`, `src-tauri/Cargo.toml`, and `src-tauri/tauri.conf.json` must each expose one identical Semantic Versioning value. A tag-triggered release must additionally use the exact tag `v`. + +The authoritative executable policy is `scripts/ci/release-version.mjs`. The package `build` command runs that policy before coverage and Vite compilation. Tauri executes `npm run build` through `beforeBuildCommand`, so the same check precedes Linux, Windows, and macOS bundle creation without relying on one operating system's shell syntax. + +## Evidence contract + +The verifier: + +- reads each JSON manifest as UTF-8 and requires one non-empty string `version`; +- reads exactly one literal `version = "..."` from Cargo's `[package]` section and refuses absent, duplicated, or workspace-inherited ambiguity; +- requires all three values to be identical; +- requires the shared value to satisfy Semantic Versioning 2.0.0; +- treats branch and pull-request builds as version-consistency checks without inventing a release tag; +- when `GITHUB_REF` is a tag reference, requires `GITHUB_REF_NAME` to equal `v` exactly; +- emits stable privacy-safe diagnostics containing only repository-controlled version values; and +- runs under the ordinary read-only build authority before compilation, attestation, or publication authority exists. + +`src/lib/releaseVersionContract.test.ts` verifies valid releases, prerelease/build metadata, Cargo section parsing, invalid JSON, missing and empty versions, duplicate Cargo versions, each manifest-disagreement path, malformed Semantic Versioning, tag drift, repository-root loading, and stable CLI success and failure behavior. `vitest.config.ts` includes the production verifier in the 100% statement, branch, function, and line coverage gate. + +## Failure and stale-evidence behavior + +A mismatch terminates `npm run build`; therefore Tauri cannot create a bundle and downstream provenance or publication jobs cannot receive release artifacts. A successful check from another commit, branch, tag, or workflow attempt is not reusable. Any manifest edit changes the exact current head and requires the complete Test, Release, security, review, approval, packaging, provenance, and release-acceptance gates to run again. + +The contract does not bump versions automatically. Version changes remain explicit reviewed source changes across all three manifests and `CHANGELOG.md`. Release automation must never rewrite a tag or manifest to make a mismatch pass. + +## Rollback and migration + +Rollback requires an independently reviewed source revert. After a revert, run the exact-current-head coverage and packaging gates and confirm that all three manifests still agree. Do not reuse or replace assets under an existing tag; publish a new version with new provenance when replacement binaries are necessary. + +## MSA compatibility + +The verifier is standalone and requires no Naruon, contextual-orchestrator, model API, user data, or network access. CWL services that embed DiskSage may invoke the same package build contract or independently compare the three version sources and the deployment artifact digest before promotion. + +## APA 7th references + +npm, Inc. (n.d.). *Creating a package.json file*. npm Docs. Retrieved August 6, 2026, from https://docs.npmjs.com/creating-a-package-json-file/ + +Rust Project. (n.d.). *The manifest format*. The Cargo Book. Retrieved August 6, 2026, from https://doc.rust-lang.org/cargo/reference/manifest.html + +Semantic Versioning. (n.d.). *Semantic Versioning 2.0.0*. Retrieved August 6, 2026, from https://semver.org/spec/v2.0.0.html + +Tauri Programme within The Commons Conservancy. (n.d.). *Distribute*. Tauri. Retrieved August 6, 2026, from https://v2.tauri.app/distribute/ + +## Reference verification note + +The authoritative publisher sources above were rechecked on August 6, 2026. They support the manifest locations, package version semantics, and distribution boundary used by this contract; they do not imply external certification of DiskSage. From cb4405ec30895dced9bc704e5e85f9ec34ab0367 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 19:44:17 +0900 Subject: [PATCH 42/52] docs(changelog): record release version gate --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9b72c1a..6cce149e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Security - Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. +- Fail closed before packaging when `package.json`, Cargo, and Tauri versions disagree, when a version is missing or malformed, or when a release tag is not exactly `v`. - Bound every release checksum record to the exact adjacent operational CLI basename and reject malformed, multi-record, redirected, traversing, absolute, or decoy checksum targets before digest verification. - Reject every unexpected eighteenth release file and every non-regular artifact-tree entry before attestation or publication, preventing unreviewed diagnostics, dumps, logs, secrets, or unrelated executables from becoming durable release assets. - Persist copy-approval provenance in immutable receipt lineage, reject stale, generic, mismatched, or tampered approvals, and retain explicit backward readability for pre-approval receipt formats. From 996dd52ea6b18141cc20fe23738e694cc5f60c67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 19:44:33 +0900 Subject: [PATCH 43/52] chore(ci): remove unused release version repair workflow --- .../pr138-release-version-repair.yml | 233 ------------------ 1 file changed, 233 deletions(-) delete mode 100644 .github/workflows/pr138-release-version-repair.yml diff --git a/.github/workflows/pr138-release-version-repair.yml b/.github/workflows/pr138-release-version-repair.yml deleted file mode 100644 index d736d5928..000000000 --- a/.github/workflows/pr138-release-version-repair.yml +++ /dev/null @@ -1,233 +0,0 @@ -name: PR 138 Release Version Repair - -on: - push: - branches: - - feat/release-provenance-attestation - paths: - - .github/workflows/pr138-release-version-repair.yml - -permissions: - contents: read - -concurrency: - group: pr138-release-version-repair - cancel-in-progress: false - -jobs: - repair: - if: contains(github.event.head_commit.message, 'stage PR 138 release version repair') - runs-on: ubuntu-24.04 - timeout-minutes: 20 - permissions: - contents: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: feat/release-provenance-attestation - fetch-depth: 0 - persist-credentials: true - - - name: Refuse stale or unexpected source - shell: bash - run: | - set -euo pipefail - test "$GITHUB_REF_NAME" = "feat/release-provenance-attestation" - git fetch --no-tags origin feat/release-provenance-attestation - test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - test "$(git rev-parse origin/feat/release-provenance-attestation)" = "$GITHUB_SHA" - - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 20.19.0 - - - name: Install exact JavaScript dependencies - run: npm ci --ignore-scripts - - - name: Prove the release version contract is red - shell: bash - run: | - set -euo pipefail - npx svelte-kit sync - if npx vitest run src/lib/releaseVersionContract.test.ts; then - echo "Expected the release-version contract to fail before workflow implementation." >&2 - exit 1 - fi - - - name: Apply exact reviewed release version repair - shell: bash - run: | - set -euo pipefail - python3 - <<'PY' - from pathlib import Path - - def replace_exact(path: str, old: str, new: str) -> None: - target = Path(path) - content = target.read_text(encoding="utf-8") - count = content.count(old) - if count != 1: - raise SystemExit(f"{path}: expected exactly one reviewed anchor, found {count}") - target.write_text(content.replace(old, new, 1), encoding="utf-8") - - replace_exact( - ".github/workflows/release.yml", - """ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 20 - - run: npm ci - """, - """ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 20 - - - name: Verify release version contract - shell: bash - run: | - set -euo pipefail - node --input-type=module <<'NODE' - import { readFileSync } from 'node:fs'; - - function refuse(message) { - console.error(message); - process.exit(1); - } - - function readJsonVersion(path) { - let parsed; - try { - parsed = JSON.parse(readFileSync(path, 'utf8')); - } catch { - refuse(`Release manifest ${path} is missing or invalid JSON.`); - } - if (typeof parsed.version !== 'string' || parsed.version.length === 0) { - refuse(`Release manifest ${path} must define one non-empty string version.`); - } - return parsed.version; - } - - function readCargoPackageVersion(path) { - const lines = readFileSync(path, 'utf8').split(/\\r?\\n/); - let inPackage = false; - const versions = []; - for (const line of lines) { - const trimmed = line.trim(); - if (trimmed === '[package]') { - inPackage = true; - continue; - } - if (inPackage && /^\\[.*\\]$/.test(trimmed)) break; - if (!inPackage) continue; - const match = line.match(/^\\s*version\\s*=\\s*"([^"]+)"\\s*(?:#.*)?$/); - if (match) versions.push(match[1]); - } - if (versions.length !== 1) { - refuse(`Release manifest ${path} must define exactly one package version.`); - } - return versions[0]; - } - - const packageVersion = readJsonVersion('package.json'); - const cargoVersion = readCargoPackageVersion('src-tauri/Cargo.toml'); - const tauriVersion = readJsonVersion('src-tauri/tauri.conf.json'); - if (packageVersion !== cargoVersion || packageVersion !== tauriVersion) { - refuse( - `Release manifest versions disagree: package.json=${packageVersion}, Cargo.toml=${cargoVersion}, tauri.conf.json=${tauriVersion}.`, - ); - } - - const semver = /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/; - if (!semver.test(packageVersion)) { - refuse(`Release manifest version ${packageVersion} is not valid Semantic Versioning.`); - } - - if ((process.env.GITHUB_REF ?? '').startsWith('refs/tags/')) { - const expectedTag = `v${packageVersion}`; - const actualTag = process.env.GITHUB_REF_NAME ?? ''; - if (actualTag !== expectedTag) { - refuse( - `Release tag ${actualTag} does not match manifest version ${expectedTag}.`, - ); - } - } - - console.log(`Release version contract passed for ${packageVersion}.`); - NODE - - - run: npm ci - """, - ) - - replace_exact( - "docs/doctoring/release-artifact-provenance.md", - """- 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; - """, - """- 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; - - `package.json`, `src-tauri/Cargo.toml`, and `src-tauri/tauri.conf.json` must expose one identical Semantic Versioning value before dependencies, compilation, packaging, attestation, or publication proceed; - - a tag-triggered run must use the exact tag `v`; tag or manifest drift fails before release artifacts are built; - """, - ) - - replace_exact( - "docs/doctoring/release-artifact-provenance.md", - """The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. - """, - """The pipeline fails closed when the package, Cargo, and Tauri manifest versions disagree, when a version is missing or malformed, or when a release tag is not exactly `v`. It also fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. - """, - ) - - replace_exact( - "docs/doctoring/release-artifact-provenance.md", - """## Reference verification note - """, - """npm, Inc. (n.d.). *Creating a package.json file*. npm Docs. Retrieved August 6, 2026, from https://docs.npmjs.com/creating-a-package-json-file/ - - Rust Project. (n.d.). *The manifest format*. The Cargo Book. Retrieved August 6, 2026, from https://doc.rust-lang.org/cargo/reference/manifest.html - - Semantic Versioning. (n.d.). *Semantic Versioning 2.0.0*. Retrieved August 6, 2026, from https://semver.org/spec/v2.0.0.html - - Tauri Programme within The Commons Conservancy. (n.d.). *Distribute*. Tauri. Retrieved August 6, 2026, from https://v2.tauri.app/distribute/ - - ## Reference verification note - """, - ) - - replace_exact( - "CHANGELOG.md", - """- Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. - """, - """- Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. - - Fail closed before release builds when `package.json`, Cargo, and Tauri versions disagree or when a release tag is not exactly `v`. - """, - ) - - workflow = Path('.github/workflows/pr138-release-version-repair.yml') - if not workflow.is_file(): - raise SystemExit('one-shot workflow source is missing') - workflow.unlink() - PY - - - name: Verify the release version contract is green - shell: bash - run: | - set -euo pipefail - npx vitest run src/lib/releaseVersionContract.test.ts - npm run coverage - git diff --check - - - name: Commit only the verified exact repair - shell: bash - env: - BRANCH_NAME: feat/release-provenance-attestation - run: | - set -euo pipefail - git fetch --no-tags origin "$BRANCH_NAME" - test "$(git rev-parse origin/$BRANCH_NAME)" = "$GITHUB_SHA" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A -- .github/workflows/release.yml .github/workflows/pr138-release-version-repair.yml docs/doctoring/release-artifact-provenance.md CHANGELOG.md - git diff --cached --check - changed="$(git diff --cached --name-only | LC_ALL=C sort)" - expected="$(printf '%s\n' .github/workflows/pr138-release-version-repair.yml .github/workflows/release.yml CHANGELOG.md docs/doctoring/release-artifact-provenance.md | LC_ALL=C sort)" - test "$changed" = "$expected" - git commit -m "fix(release): bind tags to packaged versions" - git push origin "HEAD:$BRANCH_NAME" From d99f087fd7a5fc95e9cf79224c74851382480ddf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 22:45:49 +0900 Subject: [PATCH 44/52] test(release): reject numeric prerelease leading zeroes --- src/lib/releaseVersionContract.test.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/lib/releaseVersionContract.test.ts b/src/lib/releaseVersionContract.test.ts index be1e66c3e..35f4d4fa5 100644 --- a/src/lib/releaseVersionContract.test.ts +++ b/src/lib/releaseVersionContract.test.ts @@ -115,13 +115,17 @@ describe('release version contract', () => { ).toThrow( 'Release manifest versions disagree: package.json=0.1.0, Cargo.toml=0.1.0, tauri.conf.json=0.2.0.', ); - expect(() => - validateReleaseVersion({ - packageVersion: '01.0', - cargoVersion: '01.0', - tauriVersion: '01.0', - }), - ).toThrow('Release manifest version 01.0 is not valid Semantic Versioning.'); + for (const invalidVersion of ['01.0', '1.0.0-01', '1.0.0-alpha.01']) { + expect(() => + validateReleaseVersion({ + packageVersion: invalidVersion, + cargoVersion: invalidVersion, + tauriVersion: invalidVersion, + }), + ).toThrow( + `Release manifest version ${invalidVersion} is not valid Semantic Versioning.`, + ); + } expect(() => validateReleaseVersion({ packageVersion: '0.1.0', From 9f7a07915abe51b5791f87ebf8fd450de026c79a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 22:46:29 +0900 Subject: [PATCH 45/52] fix(release): enforce SemVer prerelease numeric rules --- scripts/ci/release-version.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/release-version.mjs b/scripts/ci/release-version.mjs index c3c27d9fe..fd88ce7bc 100644 --- a/scripts/ci/release-version.mjs +++ b/scripts/ci/release-version.mjs @@ -75,7 +75,7 @@ export function validateReleaseVersion({ ); } const semver = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; + /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; if (!semver.test(packageVersion)) { throw new Error( `Release manifest version ${packageVersion} is not valid Semantic Versioning.`, From 0274421233d439370e26e5eccf628a9a6106afac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 22:47:19 +0900 Subject: [PATCH 46/52] docs(release): record numeric prerelease SemVer rule --- docs/doctoring/release-version-contract.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/release-version-contract.md b/docs/doctoring/release-version-contract.md index 40e551d31..c45240b21 100644 --- a/docs/doctoring/release-version-contract.md +++ b/docs/doctoring/release-version-contract.md @@ -13,13 +13,13 @@ The verifier: - reads each JSON manifest as UTF-8 and requires one non-empty string `version`; - reads exactly one literal `version = "..."` from Cargo's `[package]` section and refuses absent, duplicated, or workspace-inherited ambiguity; - requires all three values to be identical; -- requires the shared value to satisfy Semantic Versioning 2.0.0; +- requires the shared value to satisfy Semantic Versioning 2.0.0, including rejection of leading zeroes in numeric prerelease identifiers such as `1.0.0-01` and `1.0.0-alpha.01`; - treats branch and pull-request builds as version-consistency checks without inventing a release tag; - when `GITHUB_REF` is a tag reference, requires `GITHUB_REF_NAME` to equal `v` exactly; - emits stable privacy-safe diagnostics containing only repository-controlled version values; and - runs under the ordinary read-only build authority before compilation, attestation, or publication authority exists. -`src/lib/releaseVersionContract.test.ts` verifies valid releases, prerelease/build metadata, Cargo section parsing, invalid JSON, missing and empty versions, duplicate Cargo versions, each manifest-disagreement path, malformed Semantic Versioning, tag drift, repository-root loading, and stable CLI success and failure behavior. `vitest.config.ts` includes the production verifier in the 100% statement, branch, function, and line coverage gate. +`src/lib/releaseVersionContract.test.ts` verifies valid releases, prerelease/build metadata, Cargo section parsing, invalid JSON, missing and empty versions, duplicate Cargo versions, each manifest-disagreement path, malformed Semantic Versioning including numeric prerelease leading zeroes, tag drift, repository-root loading, and stable CLI success and failure behavior. `vitest.config.ts` includes the production verifier in the 100% statement, branch, function, and line coverage gate. ## Failure and stale-evidence behavior @@ -47,4 +47,4 @@ Tauri Programme within The Commons Conservancy. (n.d.). *Distribute*. Tauri. Ret ## Reference verification note -The authoritative publisher sources above were rechecked on August 6, 2026. They support the manifest locations, package version semantics, and distribution boundary used by this contract; they do not imply external certification of DiskSage. +The authoritative publisher sources above were rechecked on August 6, 2026. They support the manifest locations, package version semantics, including the prohibition on leading zeroes in numeric prerelease identifiers, and distribution boundary used by this contract; they do not imply external certification of DiskSage. From 1460d8d214f976fc08eddee4f0aaaafd80919297 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 22:47:48 +0900 Subject: [PATCH 47/52] docs(changelog): note strict prerelease SemVer validation --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cce149e0..3bef35b5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and - Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. - Fail closed before packaging when `package.json`, Cargo, and Tauri versions disagree, when a version is missing or malformed, or when a release tag is not exactly `v`. +- Enforce Semantic Versioning 2.0.0 numeric prerelease rules and reject leading-zero identifiers such as `1.0.0-01` before packaging. - Bound every release checksum record to the exact adjacent operational CLI basename and reject malformed, multi-record, redirected, traversing, absolute, or decoy checksum targets before digest verification. - Reject every unexpected eighteenth release file and every non-regular artifact-tree entry before attestation or publication, preventing unreviewed diagnostics, dumps, logs, secrets, or unrelated executables from becoming durable release assets. - Persist copy-approval provenance in immutable receipt lineage, reject stale, generic, mismatched, or tampered approvals, and retain explicit backward readability for pre-approval receipt formats. From c0f56847b3c626b56daf271663ad32468fb53f6f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 23:07:54 +0900 Subject: [PATCH 48/52] test: reject flattened release artifact namespaces --- src/lib/releaseProvenanceContract.test.ts | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/lib/releaseProvenanceContract.test.ts b/src/lib/releaseProvenanceContract.test.ts index 1fa241d63..36c3c4b25 100644 --- a/src/lib/releaseProvenanceContract.test.ts +++ b/src/lib/releaseProvenanceContract.test.ts @@ -188,6 +188,8 @@ describe('release artifact provenance contract', () => { 'actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131', ); expect(attestJob).toContain('pattern: release-disksage-*'); + expect(attestJob).toContain('merge-multiple: false'); + expect(attestJob).not.toContain('merge-multiple: true'); expect(attestJob).toContain( 'actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26', ); @@ -220,6 +222,8 @@ describe('release artifact provenance contract', () => { 'actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131', ); expect(publishJob).toContain('pattern: release-disksage-*'); + expect(publishJob).toContain('merge-multiple: false'); + expect(publishJob).not.toContain('merge-multiple: true'); expect(publishJob).toContain( 'softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228', ); @@ -259,6 +263,39 @@ describe('release artifact provenance contract', () => { }, ); + it.runIf(process.platform !== 'win32')( + 'rejects one required CLI duplicated across preserved artifact namespaces', + () => { + const fixtureRoot = createReleaseArtifactFixture(); + try { + const duplicatedName = 'disksage-cloud-plan-linux-x86_64'; + const sourcePath = join( + fixtureRoot, + 'release-artifacts', + 'ubuntu', + duplicatedName, + ); + const duplicatePath = join( + fixtureRoot, + 'release-artifacts', + 'windows', + duplicatedName, + ); + mkdirSync(dirname(duplicatePath), { recursive: true }); + writeFileSync(duplicatePath, readFileSync(sourcePath)); + + const result = runReleaseArtifactVerifier(fixtureRoot); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + `Expected exactly one release artifact named ${duplicatedName}, found 2.`, + ); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }, + ); + it('documents buyer-verifiable provenance and authoritative standards', () => { const doctoring = readRepositoryFile( 'docs/doctoring/release-artifact-provenance.md', From f11a0097d7600dac57249b809c17ce9a4a2e86b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 23:15:01 +0900 Subject: [PATCH 49/52] fix: preserve release artifact namespaces --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5482388fc..52d459913 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,7 +171,7 @@ jobs: with: pattern: release-disksage-* path: release-artifacts - merge-multiple: true + merge-multiple: false - name: Verify release artifact checksums shell: bash @@ -273,7 +273,7 @@ jobs: with: pattern: release-disksage-* path: release-artifacts - merge-multiple: true + merge-multiple: false - name: Publish attested artifacts to GitHub Release uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: From 83eefd08cb1b5cca335fbc6ef00811cc92b7472f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 23:16:17 +0900 Subject: [PATCH 50/52] docs: preserve release artifact namespace evidence --- docs/doctoring/release-artifact-provenance.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/release-artifact-provenance.md b/docs/doctoring/release-artifact-provenance.md index cc2b39f92..b74e5cd11 100644 --- a/docs/doctoring/release-artifact-provenance.md +++ b/docs/doctoring/release-artifact-provenance.md @@ -22,12 +22,13 @@ The release contract requires all of the following: - 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; - 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; - release publication is absent from the matrix build job, preventing any matrix member from publishing before the complete set exists; - the attestation and publication jobs run only for `refs/tags/`; - the attestation job depends on the complete build matrix; - Linux `.deb` and `.AppImage`, Windows `.msi` and NSIS `.exe`, and macOS `.dmg` bundles are present exactly once in their expected bundle paths; - all six platform-specific operational CLIs and all six corresponding `.sha256` files are each present exactly once; -- the merged release tree contains exactly 17 regular files and no symlink, device, socket, FIFO, or other non-regular entry, so unreviewed debug output, logs, dumps, or unrelated executables cannot become attested release subjects; +- the preserved release tree contains exactly 17 regular files and no symlink, device, socket, FIFO, or other non-regular entry, so unreviewed debug output, logs, dumps, or unrelated executables cannot become attested release subjects; - every checksum file contains exactly one SHA-256 record naming its adjacent expected CLI basename, so alternate, absolute, traversing, or decoy filenames are rejected before digest verification; - each checksum is verified before provenance generation; - `actions/download-artifact` is immutably pinned to commit `37930b1c2abaa49bbe596cd826c3c89aef350131`, the upstream `v7.0.0` tag commit; @@ -57,7 +58,7 @@ Retain the artifact, the downloaded bundle, the release tag, the source commit S ## Failure and stale-evidence behavior -The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file is absent or duplicated. It validates checksum-record semantics and digests first so an invalid or redirected record receives the specific actionable diagnostic, then rejects any eighteenth 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 stops the attestation job. A failed, cancelled, skipped, neutral, missing, or stale-head attestation job cannot satisfy the publication dependency. +The pipeline fails closed when an expected platform bundle, operational CLI, or checksum file 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 eighteenth 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 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. From e9cf4c1d2133a62bbb54ca4429db3e1df633f803 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 23:16:42 +0900 Subject: [PATCH 51/52] docs: record preserved release artifact namespaces --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bef35b5f..7e8ab59b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Security - Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. +- Preserve per-artifact directories during attestation and publication downloads so duplicate release basenames cannot be hidden by last-writer-wins archive flattening before exact-set admission. - Fail closed before packaging when `package.json`, Cargo, and Tauri versions disagree, when a version is missing or malformed, or when a release tag is not exactly `v`. - Enforce Semantic Versioning 2.0.0 numeric prerelease rules and reject leading-zero identifiers such as `1.0.0-01` before packaging. - Bound every release checksum record to the exact adjacent operational CLI basename and reject malformed, multi-record, redirected, traversing, absolute, or decoy checksum targets before digest verification. From f327223a9d4be30eae9c9a83c7866e15fedbd3ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 23:22:32 +0900 Subject: [PATCH 52/52] chore: preserve stacked architecture changelog evidence --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e8ab59b8..3ea1a7c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Security +- Require explicit organization-tenant authority in both the frontend projection and durable Rust transfer gate when either the organization destination scope or the organization-sensitive review reason is present, preventing a missing, contradictory, or malformed candidate field from making cloud approval less restrictive; record the fail-closed decision, rollback boundary, realistic signal-matrix tests, and APA 7th references in `docs/architecture/cloud-review-tenant-authority.md`. +- Separate runtime mutation authorization from repository merge and release authorization: runtime approvals bind exact operation scope, fingerprints, schema, and trusted-clock freshness, while exact repository-head evidence remains a CI and release gate and never becomes an operator credential. - Added buyer-verifiable release artifact provenance with checksum-first admission, immutable `actions/attest` pinning, tag-only OIDC and attestation authority, and publication that depends on successful exact-artifact provenance generation. - Preserve per-artifact directories during attestation and publication downloads so duplicate release basenames cannot be hidden by last-writer-wins archive flattening before exact-set admission. - Fail closed before packaging when `package.json`, Cargo, and Tauri versions disagree, when a version is missing or malformed, or when a release tag is not exactly `v`.