From 987a214e245120152223a6ae5211a96ea412c6ed Mon Sep 17 00:00:00 2001 From: Commanderx-code Date: Fri, 25 Sep 2026 13:33:08 -0400 Subject: [PATCH 1/2] Sign build provenance for release packages Release assets had checksums but no signature. SHA256SUMS sits on the same release as the packages, so it detects damaged downloads but not a replaced release. A tag-only attest job now runs after every build and install job passes, downloads the packages and Arch artifacts, rechecks their checksums and signs SLSA build provenance with actions/attest. It is the only job with signing permissions. release:draft verifies each package's attestation (this repository's linux-packages.yml, this tag and commit, a GitHub-hosted runner) before creating a draft, and the release notes tell users how to check a download with gh attestation verify. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/linux-packages.yml | 33 ++++++++++++++++++++++++++++ docs/installation.md | 8 +++++++ docs/releases.md | 3 ++- scripts/release-draft.mjs | 4 +++- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linux-packages.yml b/.github/workflows/linux-packages.yml index 2f2219f..0da0efd 100644 --- a/.github/workflows/linux-packages.yml +++ b/.github/workflows/linux-packages.yml @@ -178,3 +178,36 @@ jobs: name: arch-package path: /arch-release/* if-no-files-found: error + + # Sign SLSA build provenance for the exact packages a release is drafted from, once they + # have built and passed every install test. Only tag runs attest, and only this job gets + # the signing permissions. release:draft verifies these attestations before drafting. + attest: + if: github.ref_type == 'tag' + needs: [build, install-deb, install-rpm, arch] + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: packages + path: packages + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: arch-package + path: arch-package + - name: Verify checksums + run: | + (cd packages && sha256sum --check --strict SHA256SUMS) + (cd arch-package && sha256sum --check --strict SHA256SUMS) + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: | + packages/*.deb + packages/*.rpm + arch-package/*.pkg.tar.zst diff --git a/docs/installation.md b/docs/installation.md index 62313b1..65f4b74 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -16,6 +16,14 @@ sha256sum --check --ignore-missing SHA256SUMS Each downloaded package must report `OK`. The `--ignore-missing` option lets you download only the package you need. +Checksums catch a damaged download. To also confirm a package was built by this repository's CI from its release tag, and not replaced afterwards, verify its signed build provenance with the [GitHub CLI](https://cli.github.com/) (0.7.2 and later): + +```bash +gh attestation verify ./command-center__amd64.deb --repo Commanderx-code/command-center +``` + +It must report that verification succeeded and name the `.github/workflows/linux-packages.yml` workflow and the release tag. + On a compatible Debian/Ubuntu system: ```bash diff --git a/docs/releases.md b/docs/releases.md index f7f622a..2912ea7 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -30,7 +30,7 @@ git push origin v0.7.1 npm run release:draft -- v0.7.1 ``` -The draft command requires an authenticated GitHub CLI (`gh`). It checks for a clean working tree and a matching local/remote tag and runs the checks and tests. It then finds the successful **Linux packages** run for the tagged commit, downloads that run's `packages` and `arch-package` artifacts, verifies their checksums, writes one `SHA256SUMS` covering the `.deb`, `.rpm`, and Arch package, and creates an **unpublished** GitHub release. The release notes are docs/release-notes.md plus a build-and-validation section linking the workflow run. If CI has not passed for the tag, no draft is created. The command never builds release packages locally: a build on a newer distribution such as Garuda would require a newer glibc than the packages declare. It does not push tags or publish the draft. Download and verify the hosted assets and test the app on your machine before publishing. +The draft command requires an authenticated GitHub CLI (`gh`). It checks for a clean working tree and a matching local/remote tag and runs the checks and tests. It then finds the successful **Linux packages** run for the tagged commit, downloads that run's `packages` and `arch-package` artifacts, verifies their checksums, verifies each package's build provenance attestation (signed by that run's `attest` job for this tag and commit, on a GitHub-hosted runner), writes one `SHA256SUMS` covering the `.deb`, `.rpm`, and Arch package, and creates an **unpublished** GitHub release. The release notes are docs/release-notes.md plus a build-and-validation section linking the workflow run. If CI has not passed for the tag, no draft is created. The command never builds release packages locally: a build on a newer distribution such as Garuda would require a newer glibc than the packages declare. It does not push tags or publish the draft. Download and verify the hosted assets and test the app on your machine before publishing. The app discovers only published releases. Building a package, creating a tag, or preparing a draft does not publish a release. @@ -45,6 +45,7 @@ The **Linux packages** workflow runs these jobs: | `install-deb` | Installs that `.deb` on Ubuntu 22.04, Debian 12, and Ubuntu 24.04 and runs a 20-second launch check under a virtual display as an ordinary user. | | `install-rpm` | Installs that `.rpm` on Fedora 43 and runs the same launch check. | | `arch` | Builds `packaging/aur/PKGBUILD` in a clean `archlinux` container, lints the recipe and package with namcap, installs, and launches it. Tag pushes build the recipe unmodified from its release tag (the pkgver must match the tag) and upload the `arch-package` artifact; other pushes build the pushed commit. | +| `attest` | Tag pushes only, after every build and install job passes: downloads the `packages` and `arch-package` artifacts, rechecks their checksums, and signs SLSA build provenance for the `.deb`, `.rpm`, and Arch package with `actions/attest`. It is the only job with signing permissions. | These are installation and launch checks, not end-to-end validation of system-changing workflows. diff --git a/scripts/release-draft.mjs b/scripts/release-draft.mjs index ab2cd89..d21ab32 100644 --- a/scripts/release-draft.mjs +++ b/scripts/release-draft.mjs @@ -38,8 +38,10 @@ if(!readFileSync(join(archDir,'SHA256SUMS'),'utf8').includes(` ${arch}\n`))thro run('sha256sum',['--check','--strict','SHA256SUMS'],archDir); copyFileSync(join(archDir,arch),join(out,arch)); const assets=[...packages,arch]; +// Each package must carry build provenance signed by this repository's packages workflow for this tag. +for(const asset of assets)run('gh',['attestation','verify',join(out,asset),'--repo',repo,'--signer-workflow',`${repo}/.github/workflows/linux-packages.yml`,'--source-ref',`refs/tags/${tag}`,'--source-digest',head,'--deny-self-hosted-runners']); writeFileSync(join(out,'SHA256SUMS'),run('sha256sum',assets,out)+'\n'); const notes=join(mkdtempSync(join(tmpdir(),'command-center-release-')),'notes.md'); -writeFileSync(notes,`${readFileSync(new URL('docs/release-notes.md',root),'utf8').trimEnd()}\n\n### Build and validation\n\nBoth packages were built once on Ubuntu 22.04 (glibc 2.35), then installed and launched as a normal user on Ubuntu 22.04, Debian 12, Ubuntu 24.04 and Fedora 43. The Arch package was built from the release tag with packaging/aur/PKGBUILD in a clean container, linted with namcap, installed and launched. Workflow run: ${passed.url}\n`); +writeFileSync(notes,`${readFileSync(new URL('docs/release-notes.md',root),'utf8').trimEnd()}\n\n### Build and validation\n\nBoth packages were built once on Ubuntu 22.04 (glibc 2.35), then installed and launched as a normal user on Ubuntu 22.04, Debian 12, Ubuntu 24.04 and Fedora 43. The Arch package was built from the release tag with packaging/aur/PKGBUILD in a clean container, linted with namcap, installed and launched. Workflow run: ${passed.url}\n\nEach package carries signed build provenance from that workflow. To confirm a download was built by this repository's CI from the ${tag} tag, run \`gh attestation verify --repo ${repo}\`.\n`); console.log(run('gh',['release','create',tag,...assets.map(a=>join(out,a)),join(out,'SHA256SUMS'),'--repo',repo,'--verify-tag','--draft','--title',`Command Center ${tag}`,'--notes-file',notes])); console.log(`Draft created from ${passed.url}. Download and verify the hosted assets, test the app on your machine, then publish on GitHub.`); From 5fc103b0d6b06640068ad2f4eb994d7cf7c6de81 Mon Sep 17 00:00:00 2001 From: Commanderx-code Date: Fri, 25 Sep 2026 13:38:56 -0400 Subject: [PATCH 2/2] Use the current download-artifact pin in the attest job Co-Authored-By: Claude Opus 5.5 --- .github/workflows/linux-packages.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linux-packages.yml b/.github/workflows/linux-packages.yml index bfdc3a7..cc2cf87 100644 --- a/.github/workflows/linux-packages.yml +++ b/.github/workflows/linux-packages.yml @@ -193,11 +193,11 @@ jobs: attestations: write artifact-metadata: write steps: - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: packages path: packages - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: arch-package path: arch-package