diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b7865e..db6ab80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,11 +5,11 @@ on: tags: - 'v[0-9]*' -# Least-privilege: this phase (T008) only validates the tag against the -# workspace version and writes nothing. `contents: write` is deferred until -# T012, the task that actually creates the GitHub Release -- granting it -# here would hand a compromised validation step needless write access to -# the repo via the default token. +# Least-privilege: through T009 this workflow only reads the repo -- it +# validates the tag, tests, and builds, but publishes nothing. +# `contents: write` is deferred until T012, the task that actually creates +# the GitHub Release -- granting it here would hand a compromised build +# step needless write access to the repo via the default token. permissions: contents: read @@ -33,3 +33,24 @@ jobs: - name: Validate release tag against workspace version (FR-002, FR-004, FR-010) run: .github/scripts/validate-release-tag.sh "${GITHUB_REF_NAME}" + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 + with: + toolchain: stable + + - name: Cache cargo dependencies and build output + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + + - name: Test (cargo test --locked) + # --locked, not `make test`'s plain `cargo test`: an unlocked test + # run silently rewrites a drifted Cargo.lock, so by the time the + # --locked release build below runs, the lock would already be + # "fixed" and the drift it's meant to catch would never surface. + run: cargo test --locked + + - name: Build release binary (FR-002) + # --locked: this is a release artifact users download -- fail loud + # if Cargo.lock has drifted from the manifests rather than silently + # building against dependency versions the lock file never recorded. + run: cargo build --release -p iklo-cli --locked diff --git a/specs/005-ci-release-versioning/tasks.md b/specs/005-ci-release-versioning/tasks.md index ce9ceb2..7bf24d2 100644 --- a/specs/005-ci-release-versioning/tasks.md +++ b/specs/005-ci-release-versioning/tasks.md @@ -181,9 +181,28 @@ created with the packaged CLI binary attached. packaging, checksums, and the atomic release-creation step are separate tasks (T009-T012) — this workflow does nothing on a real tag push yet beyond validating it. -- [ ] **T009** [US2] Build the `iklo` executable in release mode +- [x] **T009** [US2] Build the `iklo` executable in release mode (`cargo build --release -p iklo-cli`) only after `make test` passes (FR-002). + **Done 2026-08-13**: added toolchain install + `Swatinem/rust-cache` + (same pinned SHAs as `ci.yml`, confirmed byte-identical) to + `release.yml`, then `make test`, then `cargo build --release -p + iklo-cli --locked` as the final step. `--locked` added after + self-review: a release artifact should fail loud on `Cargo.lock` drift + rather than silently building against unrecorded dependency versions. + No `make build` (debug) step — T009's scope is only the release build, + gated on tests passing, not a redundant debug build first. Verified + locally: `make test` exits 0, `cargo build --release -p iklo-cli + --locked` exits 0 and produces `target/release/iklo`. Also updated the + workflow's least-privilege header comment, which still described the + T008-only state before this task added test/build steps. + Follow-up fix + after cubic-dev-ai review: the test step now runs `cargo test --locked` + directly rather than `make test`'s plain `cargo test` -- an unlocked + test run silently rewrites a drifted `Cargo.lock`, which would have + defeated the release build's own `--locked` guarantee by the time it + ran. Scoped to `release.yml` only, not `Makefile`/`ci.yml` (those are a + separate concern about local-dev/PR-check strictness, not this task). - [ ] **T010** [US2] Package and upload the built executable as a GitHub Release asset (FR-003, SC-002). - [ ] **T011** [US2] Generate and publish SHA-256 checksums for every