diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32112cb..d8b33d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,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: Compute build identifier (FR-005) + # GITHUB_RUN_NUMBER.GITHUB_RUN_ATTEMPT -- deterministic, and + # (GITHUB_RUN_NUMBER, GITHUB_RUN_ATTEMPT) strictly increases + # across successful releases (SC-003). Exposed as a step output + # (not a job-level env var like TARGET) since it's produced by a + # script rather than a static value. + id: build_id + # A bare `echo "value=$(cmd)"` would swallow a failing `cmd`: + # the compound command's exit status is echo's, not the command + # substitution's, so GitHub Actions' default `bash -e {0}` (no + # `pipefail`) would never notice and the step would pass green + # with an empty value -- silently producing a malformed, + # unidentified release artifact instead of failing the job. A + # separate assignment does propagate the failure correctly. + run: | + build_id="$(.github/scripts/build-identifier.sh)" + echo "value=${build_id}" >> "$GITHUB_OUTPUT" + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 with: @@ -62,14 +80,24 @@ jobs: run: cargo build --release -p iklo-cli --locked - name: Package release binary (FR-003) - # Stages a target-triple-named copy under a dedicated dist/ - # directory for T011 (checksums) and T012 (release creation) to - # consume. Per plan.md's Key Design Decision #7, the actual GitHub - # Release is created once, atomically, in T012 -- this step only - # prepares the asset; it does not create or upload to a Release. + # Stages a target-triple-and-build-identifier-named copy under a + # dedicated dist/ directory for T011 (checksums) and T012 + # (release creation) to consume. Per plan.md's Key Design + # Decision #7, the actual GitHub Release is created once, + # atomically, in T012 -- this step only prepares the asset; it + # does not create or upload to a Release. BUILD_ID comes in via + # step-level env (a YAML value, not spliced into the run: script + # body), the same safe pattern as everywhere else in this file -- + # GITHUB_RUN_NUMBER/GITHUB_RUN_ATTEMPT are GitHub-controlled, not + # attacker-influenced like a tag name, but there's no reason to + # treat step outputs differently from job-level env vars here. + env: + BUILD_ID: ${{ steps.build_id.outputs.value }} run: | mkdir -p dist - cp target/release/iklo "dist/iklo-${GITHUB_REF_NAME}-${TARGET}" + cp target/release/iklo "dist/iklo-${GITHUB_REF_NAME}-${TARGET}-${BUILD_ID}" - name: Generate SHA-256 checksums (FR-012) - run: .github/scripts/generate-checksums.sh "dist/iklo-${GITHUB_REF_NAME}-${TARGET}" + env: + BUILD_ID: ${{ steps.build_id.outputs.value }} + run: .github/scripts/generate-checksums.sh "dist/iklo-${GITHUB_REF_NAME}-${TARGET}-${BUILD_ID}" diff --git a/specs/005-ci-release-versioning/tasks.md b/specs/005-ci-release-versioning/tasks.md index 3915f1c..b7c1230 100644 --- a/specs/005-ci-release-versioning/tasks.md +++ b/specs/005-ci-release-versioning/tasks.md @@ -208,8 +208,9 @@ created with the packaged CLI binary attached. atomic release-creation call, per plan.md's Key Design Decision #7 -- this task only stages the named artifact. **Done 2026-08-13**: `release.yml` copies `target/release/iklo` to - `dist/iklo-${GITHUB_REF_NAME}-x86_64-unknown-linux-gnu` -- single - platform for now, per spec.md's Assumptions. Per plan.md's Key Design + `dist/iklo-${GITHUB_REF_NAME}-x86_64-unknown-linux-gnu` (later extended + with the build identifier in T015) -- single platform for now, per + spec.md's Assumptions. Per plan.md's Key Design Decision #7, this step only *stages* the named artifact; it does not create or upload to a GitHub Release -- the Release itself is created atomically in T012, after checksums (T011) and release notes (T014) @@ -369,8 +370,28 @@ progression and generated notes reflect `previous_tag..current_tag` commits. correctly by `set -e` -- verified empirically that a failing call propagates as a nonzero exit with the child script's own clear error message, no additional handling needed). -- [ ] **T015** [US3] Wire T006's build identifier into release metadata and +- [x] **T015** [US3] Wire T006's build identifier into release metadata and artifact naming (FR-005). + **Done 2026-08-15**: `release.yml` gained a "Compute build identifier + (FR-005)" step right after tag validation, calling + `.github/scripts/build-identifier.sh` and exposing the result as a + step output (`steps.build_id.outputs.value`) -- a step output rather + than job-level `env` like `TARGET`, since it's computed by a script + rather than a static value. Threaded into artifact naming: the staged + binary and its checksum file are now + `dist/iklo---` (e.g. + `iklo-v0.1.0-x86_64-unknown-linux-gnu-42.1`), via `BUILD_ID` passed as + step-level `env` (a YAML value, not spliced into the `run:` script + body -- the same safe pattern used for the tag/target throughout this + file, even though `GITHUB_RUN_NUMBER`/`GITHUB_RUN_ATTEMPT` are + GitHub-controlled rather than attacker-influenced like a tag name). + "Release metadata" (the other half of FR-005) is completed by T012, + which will read the same `steps.build_id.outputs.value` when it + composes the atomic release-creation call -- not yet built. Verified + locally: built the real release binary, computed a build identifier + with `GITHUB_RUN_NUMBER=42 GITHUB_RUN_ATTEMPT=1` standing in for the + real env vars, staged and checksummed the correctly-named artifact, + confirmed `shasum -a 256 -c` reports `OK`. - [ ] **T016** [US3] Verify SC-003/SC-004 end-to-end: two consecutive release runs show a strictly increasing build identifier, and each release's notes include exactly the commits since the previous tag (no