From 23fb8dc2b241bf33dddfca8f765263670a5bea7c Mon Sep 17 00:00:00 2001 From: Wessel Verheij Date: Tue, 28 Jul 2026 01:43:30 +0200 Subject: [PATCH] ci: enforce the action pinning rules in the shared hygiene workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0012 settled two pinning rules and nothing ever checked either one. ADR-0021 then found three repositories doing two different things, which is what an unenforced rule looks like after a few months. The `action pins` step encodes both rules exactly: anything outside beatrax-app/ needs a full 40-character SHA, and a first-party reusable workflow needs its major-version tag. Local composite actions are exempt — they carry no supply-chain risk of their own. It lives in `hygiene` so every repository that already calls the shared workflow picks it up without adding a job name to its ruleset. Spec: GOV-R12 Signed-off-by: Wessel Verheij --- .github/workflows/hygiene.yml | 40 ++++++++++++++++++++ 00-overview/decisions/0012-action-pinning.md | 12 ++++++ 2 files changed, 52 insertions(+) diff --git a/.github/workflows/hygiene.yml b/.github/workflows/hygiene.yml index 937db70..3c85a28 100644 --- a/.github/workflows/hygiene.yml +++ b/.github/workflows/hygiene.yml @@ -20,6 +20,46 @@ jobs: - name: actionlint uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 + # The org's two pinning rules are the kind that decay silently: nothing + # fails when a `uses:` line drifts, it just quietly widens the blast + # radius of someone else's repository. This is the only thing enforcing + # them, so it runs everywhere hygiene runs. + - name: action pins (ADR-0012, ADR-0021) + run: | + fail=0 + while IFS= read -r line; do + file=${line%%:*} + rest=${line#*:} + lineno=${rest%%:*} + ref=$(printf '%s' "$line" | sed -E 's/.*uses:[[:space:]]*//; s/[[:space:]]*(#.*)?$//' | tr -d "\"'") + + # A local composite action carries no supply-chain risk of its own. + case $ref in + ./*|'') continue ;; + esac + + if [ "${ref%%/*}" = "beatrax-app" ]; then + # First-party: the moving major-version tag is the contract. + if ! printf '%s' "$ref" | grep -qE '@v[0-9]+$'; then + echo "$file:$lineno: first-party reusable workflow must ride a major-version tag (@v1), got: $ref" + fail=1 + fi + continue + fi + + if ! printf '%s' "$ref" | grep -qE '@[0-9a-f]{40}$'; then + echo "$file:$lineno: third-party action must be pinned to a full 40-character commit SHA, got: $ref" + fail=1 + fi + done < <(grep -rnE '^[[:space:]]*(-[[:space:]]*)?uses:' .github/workflows/ 2>/dev/null) + + if [ "$fail" -ne 0 ]; then + echo + echo "See ADR-0012 (third-party SHA pinning) and ADR-0021 (first-party @vN tags)." + exit 1 + fi + echo "action pins OK" + typos: runs-on: ubuntu-latest steps: diff --git a/00-overview/decisions/0012-action-pinning.md b/00-overview/decisions/0012-action-pinning.md index 64a24c2..cfa708d 100644 --- a/00-overview/decisions/0012-action-pinning.md +++ b/00-overview/decisions/0012-action-pinning.md @@ -46,6 +46,18 @@ Two rules, deliberately different: Dependency automation watches the actions ecosystem weekly so the inline version comments do not rot. +## Enforcement + +The `action pins` step in the shared `hygiene` workflow fails the build on any +`uses:` line that breaks either rule — a third-party action without a full +40-character SHA, or a first-party reusable workflow off its major-version tag. +It runs in every repository that calls `hygiene`. + +This is deliberate rather than incidental. The second rule of this ADR went +unfollowed from the day it was written until [ADR-0021](0021-reusable-workflow-version-tags.md) +found three repositories doing two different things, because nothing checked. +An unenforced pinning rule is a comment. + ## Alternatives considered | Option | Why it lost |