From 1842318154200c5d6ff8eff7d329bc6136b8d291 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sat, 3 Oct 2026 11:45:57 +1000 Subject: [PATCH 1/5] ci(release): tag-driven release with attested binaries and crates.io trusted publishing (LAB-5117) A version tag on a commit in main builds static Linux (x86_64, aarch64) and Apple silicon binaries on GitHub-hosted runners, tests each target, attests the archives and the binaries in them, creates the GitHub Release, and then publishes the crate through crates.io trusted publishing, so the repo holds no registry token. The README gains an install section: cargo install --locked, and the pinned gh attestation verify command for release binaries. --- .github/workflows/release.yml | 153 ++++++++++++++++++++++++++++++++++ README.md | 40 ++++++++- 2 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a70f59c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,153 @@ +name: Release + +# A version tag (v0.1.0, v0.2.0-rc.1) on a commit in main publishes that +# version: attested binaries on the GitHub Release, then the crate on +# crates.io. Users verify a binary against this file's path, so renaming it +# breaks the verify command in the README. + +on: + push: + tags: ["v*"] + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Tag matches the crate version + run: | + version=$(cargo metadata --locked --no-deps --format-version 1 | jq -r '.packages[0].version') + if [ "$GITHUB_REF_NAME" != "v$version" ]; then + echo "::error::tag $GITHUB_REF_NAME does not match the crate version $version" + exit 1 + fi + + - name: Tagged commit is on main + # Only reviewed code is released: a tag on any other branch fails here. + run: | + git merge-base --is-ancestor "$GITHUB_SHA" origin/main \ + || { echo "::error::$GITHUB_SHA is not on main"; exit 1; } + + - name: Package the crate + # Builds the packaged crate, so the publish job can skip that build. + run: cargo package --locked + + build: + name: build (${{ matrix.target }}) + needs: check + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + matrix: + include: + # musl builds are static, so they run on any Linux distribution. + - { os: ubuntu-latest, target: x86_64-unknown-linux-musl } + - { os: ubuntu-24.04-arm, target: aarch64-unknown-linux-musl } + - { os: macos-latest, target: aarch64-apple-darwin } + env: + TARGET: ${{ matrix.target }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install the musl toolchain + if: runner.os == 'Linux' + # ring compiles C, which needs musl-gcc. + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends musl-tools + + - name: Add the target + run: rustup target add "$TARGET" + + - name: Test the shipped target + run: cargo test --locked --release --target "$TARGET" + + - name: Build + run: cargo build --locked --release --target "$TARGET" + + - name: Archive + run: | + dir="ck-$GITHUB_REF_NAME-$TARGET" + mkdir "$dir" + cp "target/$TARGET/release/ck" LICENSE README.md "$dir/" + tar -czf "$dir.tar.gz" "$dir" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ matrix.target }} + path: ck-*.tar.gz + if-no-files-found: error + + release: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + # Signing happens here rather than in the build jobs, so dependency build + # scripts never run with a token that can sign or publish. + permissions: + contents: write # create the release + id-token: write # sign the attestation + attestations: write # store the attestation + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: dist + merge-multiple: true + + - name: Unpack the binaries + # Attesting the binary too lets a user verify ck after unpacking it. + run: for archive in dist/*.tar.gz; do tar -xzf "$archive" -C dist; done + + - name: Attest the archives and the binaries in them + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: | + dist/*.tar.gz + dist/ck-*/ck + + - name: Create the GitHub Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + prerelease=() + case "$GITHUB_REF_NAME" in *-*) prerelease=(--prerelease) ;; esac + gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes "${prerelease[@]}" \ + dist/*.tar.gz + + publish: + needs: release + runs-on: ubuntu-latest + timeout-minutes: 15 + # crates.io accepts a token minted here only from this workflow in this + # environment: trusted publishing, so the repo holds no registry secret. + environment: release + permissions: + contents: read + id-token: write # exchange for a crates.io token + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 + id: auth + + - name: Publish to crates.io + # The check job already built the packaged crate. A version can never + # be republished, so this runs last, after the GitHub Release exists. + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + run: cargo publish --locked --no-verify diff --git a/README.md b/README.md index 917d591..9ce79d3 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,44 @@ `ck`, the command-line client for [CacheKit](https://cachekit.io). -Pre-release: nothing is published yet. To try it, build from a checkout with -`cargo build --release` and put `target/release/ck` on your `PATH`. +## Install + +There is no install script. Install with Cargo, or download a release binary +and verify where it came from. + +### With Cargo + +```text +cargo install --locked cachekit-cli +``` + +This builds `ck` from the crate on crates.io and puts it in `~/.cargo/bin`. +Each crates.io release is published by this repository's release workflow. + +### Release binaries + +Each [release](https://github.com/cachekit-io/cachekit-cli/releases) carries +static Linux binaries for `x86_64` and `aarch64`, and a macOS binary for Apple +silicon. On other platforms, install with Cargo. Download the archive for your +platform, then verify it with the [GitHub CLI](https://cli.github.com/) before +you unpack it: + +```text +gh attestation verify ck-v0.1.0-x86_64-unknown-linux-musl.tar.gz --repo cachekit-io/cachekit-cli \ + --signer-workflow cachekit-io/cachekit-cli/.github/workflows/release.yml \ + --deny-self-hosted-runners +``` + +The check passes only if the file was built by this repository's release +workflow on a GitHub-hosted runner. `--owner cachekit-io` alone is not enough: +it accepts a file built by any workflow in any `cachekit-io` repository. The +`ck` binary inside the archive is attested too, so the same command verifies it +after unpacking. + +### From a checkout + +Build with `cargo build --release --locked` and put `target/release/ck` on +your `PATH`. ## `ck run` From 1d14ecca279947446569898c9f2f30e996ade8e7 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sat, 3 Oct 2026 11:50:33 +1000 Subject: [PATCH 2/5] ci(release): let a rerun reuse an existing release (LAB-5117) A rerun after a failed publish found the release already there, so gh release create failed and the publish job never ran again. The step now replaces the existing release's archives with the rerun's attested ones and carries on to publish. --- .github/workflows/release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a70f59c..d1a724a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -122,6 +122,13 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} run: | + # A rerun, after a failed upload or publish, finds the release + # already there: replace its archives with this run's attested ones + # so the publish job still runs. + if gh release view "$GITHUB_REF_NAME" > /dev/null 2>&1; then + gh release upload "$GITHUB_REF_NAME" --clobber dist/*.tar.gz + exit + fi prerelease=() case "$GITHUB_REF_NAME" in *-*) prerelease=(--prerelease) ;; esac gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes "${prerelease[@]}" \ From 1de07cd773d0acba8b0c0604e5cd9b56da591331 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sat, 3 Oct 2026 11:53:38 +1000 Subject: [PATCH 3/5] ci(release): describe what the main-ancestry check catches (LAB-5117) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d1a724a..703f8b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: fi - name: Tagged commit is on main - # Only reviewed code is released: a tag on any other branch fails here. + # Catches a version tag pushed on the wrong commit by mistake. run: | git merge-base --is-ancestor "$GITHUB_SHA" origin/main \ || { echo "::error::$GITHUB_SHA is not on main"; exit 1; } From 0372149690c827cf6487ee4e31c642e11e59d84b Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sat, 3 Oct 2026 18:02:38 +1000 Subject: [PATCH 4/5] ci(release): publish a leftover draft release on rerun; note the crates.io prerequisite (LAB-5117) --- .github/workflows/release.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 703f8b0..4cd340f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -124,9 +124,12 @@ jobs: run: | # A rerun, after a failed upload or publish, finds the release # already there: replace its archives with this run's attested ones - # so the publish job still runs. + # so the publish job still runs. gh creates a release with assets as + # a draft and publishes it after the uploads, so a run that died + # mid-upload can leave a draft behind; publish it here. if gh release view "$GITHUB_REF_NAME" > /dev/null 2>&1; then gh release upload "$GITHUB_REF_NAME" --clobber dist/*.tar.gz + gh release edit "$GITHUB_REF_NAME" --draft=false exit fi prerelease=() @@ -149,6 +152,10 @@ jobs: with: persist-credentials: false + # Needs the crate to exist on crates.io with this workflow and the + # `release` environment registered as its trusted publisher. crates.io + # attaches a publisher only to an existing crate, so the first version + # is published by hand with an API token before the first tag. - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 id: auth From be672947cdb2e3cc08965197e96176ea88293cae Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sat, 3 Oct 2026 19:14:30 +1000 Subject: [PATCH 5/5] ci(release): name the hand-published bootstrap version (LAB-5117) --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4cd340f..82cbabd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -154,8 +154,9 @@ jobs: # Needs the crate to exist on crates.io with this workflow and the # `release` environment registered as its trusted publisher. crates.io - # attaches a publisher only to an existing crate, so the first version - # is published by hand with an API token before the first tag. + # attaches a publisher only to an existing crate, so 0.0.0 is published + # by hand with an API token first. Every tagged version comes after it, + # because this job cannot publish a version that already exists. - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 id: auth