From bf4d5c1e027f48b701d65052d1b82533eb93df7b Mon Sep 17 00:00:00 2001 From: owkwo-bot Date: Thu, 30 Jul 2026 15:14:57 +0200 Subject: [PATCH 1/2] feat(release): add build-identifier script (epic 005 T006) Test-first per Constitution I. GITHUB_RUN_NUMBER.GITHUB_RUN_ATTEMPT, validated as present and base-10 numeric (not just concatenated blindly) so SC-003's "strictly increasing" check has a numeric pair to compare, not two arbitrary strings. --- .github/scripts/build-identifier.sh | 21 +++++++ .../scripts/tests/test-build-identifier.sh | 59 +++++++++++++++++++ specs/005-ci-release-versioning/tasks.md | 5 +- 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/build-identifier.sh create mode 100755 .github/scripts/tests/test-build-identifier.sh diff --git a/.github/scripts/build-identifier.sh b/.github/scripts/build-identifier.sh new file mode 100755 index 0000000..71cd1ec --- /dev/null +++ b/.github/scripts/build-identifier.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Computes the deterministic build identifier for a release run (FR-005): +# GITHUB_RUN_NUMBER.GITHUB_RUN_ATTEMPT, both required to be present and +# numeric so "strictly increase" can later be evaluated numerically as the +# pair (GITHUB_RUN_NUMBER, GITHUB_RUN_ATTEMPT) (SC-003). +set -euo pipefail + +run_number="${GITHUB_RUN_NUMBER:-}" +run_attempt="${GITHUB_RUN_ATTEMPT:-}" + +if [ -z "$run_number" ] || [ -z "$run_attempt" ]; then + echo "error: GITHUB_RUN_NUMBER and GITHUB_RUN_ATTEMPT must both be set" >&2 + exit 1 +fi + +if ! [[ "$run_number" =~ ^[0-9]+$ ]] || ! [[ "$run_attempt" =~ ^[0-9]+$ ]]; then + echo "error: GITHUB_RUN_NUMBER ('$run_number') and GITHUB_RUN_ATTEMPT ('$run_attempt') must both be base-10 integers" >&2 + exit 1 +fi + +echo "${run_number}.${run_attempt}" diff --git a/.github/scripts/tests/test-build-identifier.sh b/.github/scripts/tests/test-build-identifier.sh new file mode 100755 index 0000000..bc5f323 --- /dev/null +++ b/.github/scripts/tests/test-build-identifier.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Fixture-based tests for build-identifier.sh (T006, Constitution I). Run +# manually: +# bash .github/scripts/tests/test-build-identifier.sh +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +script="$script_dir/../build-identifier.sh" + +pass=0 +fail=0 + +check_eq() { + local desc="$1" expected="$2" actual="$3" + if [ "$expected" == "$actual" ]; then + pass=$((pass + 1)) + else + fail=$((fail + 1)) + echo "FAIL: $desc (expected '$expected', got '$actual')" + fi +} + +assert_fail() { + local desc="$1"; shift + if "$@" >/dev/null 2>&1; then + fail=$((fail + 1)) + echo "FAIL (expected failure but succeeded): $desc" + else + pass=$((pass + 1)) + fi +} + +out="$(GITHUB_RUN_NUMBER=42 GITHUB_RUN_ATTEMPT=1 bash "$script")" +check_eq "basic case" "42.1" "$out" + +out="$(GITHUB_RUN_NUMBER=1 GITHUB_RUN_ATTEMPT=3 bash "$script")" +check_eq "retry attempt" "1.3" "$out" + +assert_fail "missing GITHUB_RUN_NUMBER fails" env -u GITHUB_RUN_NUMBER GITHUB_RUN_ATTEMPT=1 bash "$script" +assert_fail "missing GITHUB_RUN_ATTEMPT fails" env GITHUB_RUN_NUMBER=1 -u GITHUB_RUN_ATTEMPT bash "$script" +assert_fail "non-numeric run number fails" env GITHUB_RUN_NUMBER=abc GITHUB_RUN_ATTEMPT=1 bash "$script" +assert_fail "non-numeric run attempt fails" env GITHUB_RUN_NUMBER=1 GITHUB_RUN_ATTEMPT=abc bash "$script" +assert_fail "negative number fails (not base-10 unsigned per FR-005)" env GITHUB_RUN_NUMBER=-1 GITHUB_RUN_ATTEMPT=1 bash "$script" + +# Numeric-pair ordering sanity (SC-003's "strictly increasing" comparison +# basis) -- not exercised by the script itself, but documents the contract +# callers must use rather than lexicographic string comparison. +a_run=9; a_attempt=9 +b_run=10; b_attempt=1 +if [ "$a_run" -lt "$b_run" ]; then + pass=$((pass + 1)) +else + fail=$((fail + 1)) + echo "FAIL: numeric run-number comparison sanity check" +fi + +echo "----" +echo "pass=$pass fail=$fail" +[ "$fail" -eq 0 ] diff --git a/specs/005-ci-release-versioning/tasks.md b/specs/005-ci-release-versioning/tasks.md index 4b57293..75d0036 100644 --- a/specs/005-ci-release-versioning/tasks.md +++ b/specs/005-ci-release-versioning/tasks.md @@ -96,10 +96,13 @@ shared by both release publishing (US2) and versioning/notes (US3). Also implements `previous_release_tag` selection (FR-011): nearest reachable prior SemVer tag via `git describe --tags --match "v[0-9]*" --abbrev=0 ^`. -- [ ] **T006** Implement build-identifier computation: deterministic +- [x] **T006** Implement build-identifier computation: deterministic `GITHUB_RUN_NUMBER.GITHUB_RUN_ATTEMPT` (FR-005), evaluated numerically as `(GITHUB_RUN_NUMBER, GITHUB_RUN_ATTEMPT)` for "strictly increasing" checks (SC-003). + **Done 2026-07-30**: `.github/scripts/build-identifier.sh`, with fixture + tests under `.github/scripts/tests/test-build-identifier.sh` covering + missing/non-numeric env vars, not just the happy path. **Checkpoint**: Version/tag validation and build-identifier logic available for both release publishing and notes generation. From 761a1585de69ac876dbd866d96067b4878efeec9 Mon Sep 17 00:00:00 2001 From: owkwo-bot Date: Fri, 31 Jul 2026 21:02:45 +0200 Subject: [PATCH 2/2] fix(release): correct env arg order, use tie-break vars instead of suppressing lint - The GITHUB_RUN_ATTEMPT-missing test had `env`'s `-u` option after a NAME=VALUE assignment. env stops parsing options at the first assignment, so it tried to exec a program literally named "-u" and failed with "No such file or directory" -- the test still passed, but for the wrong reason (env failing, not the script's own validation). Verified: reproduced the bug locally (env: -u: No such file or directory), confirmed the fix actually reaches the script's own "must both be set" error instead. - a_attempt/b_attempt were assigned but never used (ShellCheck noise). Rather than prefix-and-ignore, added a real second assertion that exercises them: same run-number, attempt breaks the tie -- a more useful test than a suppressed warning. --- .github/scripts/tests/test-build-identifier.sh | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/scripts/tests/test-build-identifier.sh b/.github/scripts/tests/test-build-identifier.sh index bc5f323..5988193 100755 --- a/.github/scripts/tests/test-build-identifier.sh +++ b/.github/scripts/tests/test-build-identifier.sh @@ -37,14 +37,16 @@ out="$(GITHUB_RUN_NUMBER=1 GITHUB_RUN_ATTEMPT=3 bash "$script")" check_eq "retry attempt" "1.3" "$out" assert_fail "missing GITHUB_RUN_NUMBER fails" env -u GITHUB_RUN_NUMBER GITHUB_RUN_ATTEMPT=1 bash "$script" -assert_fail "missing GITHUB_RUN_ATTEMPT fails" env GITHUB_RUN_NUMBER=1 -u GITHUB_RUN_ATTEMPT bash "$script" +assert_fail "missing GITHUB_RUN_ATTEMPT fails" env -u GITHUB_RUN_ATTEMPT GITHUB_RUN_NUMBER=1 bash "$script" assert_fail "non-numeric run number fails" env GITHUB_RUN_NUMBER=abc GITHUB_RUN_ATTEMPT=1 bash "$script" assert_fail "non-numeric run attempt fails" env GITHUB_RUN_NUMBER=1 GITHUB_RUN_ATTEMPT=abc bash "$script" assert_fail "negative number fails (not base-10 unsigned per FR-005)" env GITHUB_RUN_NUMBER=-1 GITHUB_RUN_ATTEMPT=1 bash "$script" # Numeric-pair ordering sanity (SC-003's "strictly increasing" comparison -# basis) -- not exercised by the script itself, but documents the contract -# callers must use rather than lexicographic string comparison. +# basis, (GITHUB_RUN_NUMBER, GITHUB_RUN_ATTEMPT)) -- not exercised by the +# script itself, but documents the contract callers must use rather than +# lexicographic string comparison: run-number is the primary key, and a +# tie is broken by attempt (a same-run retry still counts as "later"). a_run=9; a_attempt=9 b_run=10; b_attempt=1 if [ "$a_run" -lt "$b_run" ]; then @@ -54,6 +56,15 @@ else echo "FAIL: numeric run-number comparison sanity check" fi +a_run=5; a_attempt=1 +b_run=5; b_attempt=2 +if [ "$a_run" -eq "$b_run" ] && [ "$a_attempt" -lt "$b_attempt" ]; then + pass=$((pass + 1)) +else + fail=$((fail + 1)) + echo "FAIL: same-run-number, attempt-breaks-tie sanity check" +fi + echo "----" echo "pass=$pass fail=$fail" [ "$fail" -eq 0 ]