From 31b9f2d616c77870af5fc287716b411fbe6f3de4 Mon Sep 17 00:00:00 2001 From: Victor Solano Date: Tue, 15 Sep 2026 12:57:50 +0200 Subject: [PATCH 1/2] test(release): fail if the action-pin check inspects nothing The pin check enforced a 40-hex SHA over whatever grep handed it and never asked whether grep handed it anything. Break the extraction, leave every real pin in place, and it printed success over zero lines. Count the extracted uses: lines and refuse below a floor of 20, and refuse an unmatched workflow glob. A fixture whose only uses: is a flow-style mapping, which the extractor does not read, has to fail. Closes #407 --- CHANGELOG.md | 2 + tests/release/release-rehearsal.test.sh | 90 ++++++++++++++++++++----- 2 files changed, 75 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ec44bfe..9588aadd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ Releases before `0.2.5` predate the public launch; their notes live in the - Discover release/e2e shell guards in local CI instead of maintaining a stale list. Report skipped required Postgres validation in the final summary and prefer Podman when both container runtimes are available (#346). +- The release-rehearsal pin check now fails when it extracted too few `uses:` + lines, instead of reporting the invariant holding over an empty set (#407). ## [0.15.0] — 2026-09-10 diff --git a/tests/release/release-rehearsal.test.sh b/tests/release/release-rehearsal.test.sh index eb94d9c7..7014d905 100755 --- a/tests/release/release-rehearsal.test.sh +++ b/tests/release/release-rehearsal.test.sh @@ -90,23 +90,79 @@ grep -Eq 'needs: \[release\]' "$release_workflow" # @stable, @main, per-tool tags like @cargo-nextest, and short SHAs), across # all workflows — not just the publishing one — for a uniform supply-chain # posture that cannot silently drift. -for workflow in "${repo_root}"/.github/workflows/*.yml; do - while IFS= read -r uses_line; do - # A reusable workflow in this same repository is referenced by path and - # cannot carry a SHA at all: GitHub resolves `./...` at the caller's own - # commit, so it is pinned by construction and always to this tree. The - # exemption is deliberately anchored to `./` so a third-party - # `owner/repo/.github/workflows/x.yml@ref` still has to be pinned. - if printf '%s\n' "$uses_line" | grep -Eq 'uses:[[:space:]]+\./'; then - continue - fi - if ! printf '%s\n' "$uses_line" | grep -Eq 'uses:[[:space:]]+[^@[:space:]]+@[0-9a-f]{40}([[:space:]]|$)'; then - printf 'FAIL: %s action is not pinned to a 40-hex SHA: %s\n' \ - "$(basename "$workflow")" "$uses_line" >&2 - exit 1 - fi - done < <(grep -E '^[[:space:]]*(-[[:space:]]+)?uses:' "$workflow") -done +# +# The extraction has to say how many lines it saw. A broken grep, an unmatched +# glob, or a spelling the regex does not read all used to print +# "Release rehearsal contract passed." over nothing. See #407. +# +# The floor is 20. This tree currently has 55 `uses:` lines, 19 of them +# actions/checkout. A regex that only matches checkout, or that only matches +# the `- uses:` spelling (one hit, in docs.yml), falls below it. Adding a +# workflow cannot trip it; extracting a subset can. +assert_action_pins() { + local workflows_dir="$1" + local min_uses="$2" + local old_nullglob uses_count workflow uses_line + old_nullglob="$(shopt -p nullglob || true)" + shopt -s nullglob + local -a workflows=("$workflows_dir"/*.yml "$workflows_dir"/*.yaml) + eval "$old_nullglob" + + ((${#workflows[@]})) || { + printf 'FAIL: no workflow files matched under %s\n' "$workflows_dir" >&2 + return 1 + } + + uses_count=0 + for workflow in "${workflows[@]}"; do + while IFS= read -r uses_line; do + [ -n "$uses_line" ] || continue + uses_count=$((uses_count + 1)) + # A reusable workflow in this same repository is referenced by path + # and cannot carry a SHA at all: GitHub resolves `./...` at the + # caller's own commit, so it is pinned by construction and always + # to this tree. The exemption is deliberately anchored to `./` so a + # third-party `owner/repo/.github/workflows/x.yml@ref` still has to + # be pinned. + if printf '%s\n' "$uses_line" | grep -Eq 'uses:[[:space:]]+\./'; then + continue + fi + if ! printf '%s\n' "$uses_line" | grep -Eq 'uses:[[:space:]]+[^@[:space:]]+@[0-9a-f]{40}([[:space:]]|$)'; then + printf 'FAIL: %s action is not pinned to a 40-hex SHA: %s\n' \ + "$(basename "$workflow")" "$uses_line" >&2 + return 1 + fi + done < <(grep -E '^[[:space:]]*(-[[:space:]]+)?uses:' "$workflow" || true) + done + + if [ "$uses_count" -lt "$min_uses" ]; then + printf 'FAIL: extracted %s uses: line(s) under %s; need at least %s (extraction is broken, not the workflows)\n' \ + "$uses_count" "$workflows_dir" "$min_uses" >&2 + return 1 + fi +} + +assert_action_pins "${repo_root}/.github/workflows" 20 + +# Negative twin: a workflow whose only `uses:` is a flow-style mapping, which +# the extractor above does not read. Before the floor this check printed +# success over zero lines. +pin_fixture="$(mktemp -d)" +trap 'rm -rf "$pin_fixture"' EXIT +mkdir -p "$pin_fixture/workflows" +cat > "$pin_fixture/workflows/missed.yml" <<'EOF' +on: push +jobs: + x: + runs-on: ubuntu-latest + steps: + - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 } +EOF +if pin_output="$(assert_action_pins "$pin_fixture/workflows" 1 2>&1)"; then + printf 'FAIL: pin check passed over a uses: spelling the extraction does not read\n' >&2 + exit 1 +fi +grep -Fq 'extraction is broken, not the workflows' <<<"$pin_output" if grep -Fq -- '--no-verify' "$release_workflow"; then printf 'FAIL: release publication skips generated crate verification\n' >&2 exit 1 From 3d3575aee9dcba819a8d2fc84fc8c8420014b560 Mon Sep 17 00:00:00 2001 From: Vladimir Rotariu Date: Tue, 15 Sep 2026 11:59:12 -0600 Subject: [PATCH 2/2] chore(changelog): move the #407 entry to Unreleased v0.16.0 was tagged and published while this branch was open, so the section this entry was written against is now a released one. Git merges the entry into it cleanly, because the surrounding lines did not move, and the result would have claimed the published v0.16.0 contains a fix it does not. The [0.16.0] section is byte-identical to the tag after this move. --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 410a3cfd..b353c5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ Releases before `0.2.5` predate the public launch; their notes live in the ## [Unreleased] +### Fixed + +- The release-rehearsal pin check now fails when it extracted too few `uses:` + lines, instead of reporting the invariant holding over an empty set (#407). + ## [0.16.0] — 2026-09-15 ### Added @@ -38,8 +43,6 @@ Releases before `0.2.5` predate the public launch; their notes live in the - Discover release/e2e shell guards in local CI instead of maintaining a stale list. Report skipped required Postgres validation in the final summary and prefer Podman when both container runtimes are available (#346). -- The release-rehearsal pin check now fails when it extracted too few `uses:` - lines, instead of reporting the invariant holding over an empty set (#407). ## [0.15.0] — 2026-09-10