diff --git a/.github/workflows/lockfile-drift-detect.yml b/.github/workflows/lockfile-drift-detect.yml index 20db9441e..cc883b838 100644 --- a/.github/workflows/lockfile-drift-detect.yml +++ b/.github/workflows/lockfile-drift-detect.yml @@ -65,6 +65,11 @@ jobs: set -euo pipefail : > drift-report.tsv printf 'repo\tworkflow\trequested\tlocked\n' >> drift-report.tsv + # Second report from the same clones: workflows that run their own + # gh-actions-lock beside governance's lockfile gate (the copied + # workflow-linter step). Owner decision 2026-10-14: drop them; the + # estate wave works this list. Lock-bearing repos only, as below. + printf 'repo\tworkflow\tpins\n' > copied-lock-step.tsv # Enumerate, then filter. `gh repo list` has a denominator we can # state; `gh search` does not. A count landing on a round number is a @@ -80,7 +85,7 @@ jobs: mv r.tmp repos.txt fi - scanned=0; withlock=0; drifted=0; errors=0 + scanned=0; withlock=0; drifted=0; errors=0; copied=0 while read -r r; do scanned=$((scanned+1)) # Cheap probe first: skip repos with no lockfile (that is failure @@ -107,6 +112,14 @@ jobs: errors=$((errors+1)) echo "::warning::$r: drift check errored (rc=$rc) — see drift-errors.log" fi + rc=0 + bash scripts/check-copied-lock-step.sh _w "$r" >> copied-lock-step.tsv 2>> drift-errors.log || rc=$? + if [ "$rc" -eq 1 ]; then + copied=$((copied+1)) + elif [ "$rc" -ne 0 ]; then + errors=$((errors+1)) + echo "::warning::$r: copied-lock-step check errored (rc=$rc) — see drift-errors.log" + fi rm -rf _w done < repos.txt @@ -122,15 +135,18 @@ jobs: echo "drifted=$drifted" echo "errors=$errors" echo "rows=$rows" + echo "copied=$copied" } >> "$GITHUB_OUTPUT" - echo "scanned=$scanned withlock=$withlock drifted=$drifted errors=$errors rows=$rows" + echo "scanned=$scanned withlock=$withlock drifted=$drifted errors=$errors rows=$rows copied=$copied" - name: Upload report if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 with: name: lockfile-drift-report - path: drift-report.tsv + path: | + drift-report.tsv + copied-lock-step.tsv retention-days: 90 - name: Step summary @@ -151,6 +167,12 @@ jobs: echo '```' head -40 drift-report.tsv echo '```' + echo + echo "Running their own gh-actions-lock (copied linter step): **${{ steps.sweep.outputs.copied }}** repo(s)" + echo + echo '```' + head -40 copied-lock-step.tsv + echo '```' } >> "$GITHUB_STEP_SUMMARY" - name: Open / update the tracking issue @@ -185,6 +207,7 @@ jobs: echo "| **with drift** | **${{ steps.sweep.outputs.drifted }}** |" echo "| check errors (rc≠0,1) | ${{ steps.sweep.outputs.errors }} |" echo "| drifted entries | ${{ steps.sweep.outputs.rows }} |" + echo "| running their own gh-actions-lock | ${{ steps.sweep.outputs.copied }} |" } > issue-body.md body="$(cat issue-body.md)" existing=$(gh issue list --repo "$GITHUB_REPOSITORY" --label lockfile-drift \ diff --git a/scripts/check-copied-lock-step.sh b/scripts/check-copied-lock-step.sh new file mode 100755 index 000000000..600826613 --- /dev/null +++ b/scripts/check-copied-lock-step.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# check-copied-lock-step.sh — find workflows that run their own gh-actions-lock. +# +# The fault: a sweep copied a "Verify immutable action lockfile" step into each +# repo's workflow-linter.yml. It downloads a pinned gh-actions-lock binary +# (v0.1.6) and runs it raw. That gives a second verifier beside governance's +# lockfile gate (scripts/update-actions-lock.sh), with a second version pin that +# drifts from the estate's. v0.1.6 cannot read lockfile v0.0.3, which every +# current `gh actions-lock` writes, so the step failed whenever a current tool +# refreshed the lock (proof-burrower#111, #112). Owner decision 2026-10-14: +# drop the step from copied linters and let governance own lock verification. +# +# This lists what is left. It reports; it never edits. +# +# Usage: check-copied-lock-step.sh REPO_DIR [REPO_SLUG] +# REPO_SLUG default: basename(REPO_DIR); pass it for a throwaway +# clone so column 1 names the repository. +# Output: TSV on stdout, one row per workflow: +# repo workflow version(s) it pins ("unpinned" if none) +# Exit: 0 = none found (or no workflows), 1 = found, 2 = usage error. +# +# standards itself is skipped: its installs ARE the estate pin, and +# scripts/tests/gh-actions-lock-contract-test.sh holds them to one version. +set -uo pipefail + +REPO_DIR="${1:-}" +[ -n "$REPO_DIR" ] && [ -d "$REPO_DIR" ] || { + echo "usage: check-copied-lock-step.sh REPO_DIR [REPO_SLUG]" >&2; exit 2; } +REPO_SLUG="${2:-$(basename "$REPO_DIR")}" +WFDIR="$REPO_DIR/.github/workflows" + +case "$REPO_SLUG" in standards|*/standards) exit 0 ;; esac +[ -d "$WFDIR" ] || exit 0 + +found=0 +for wf in "$WFDIR"/*.yml "$WFDIR"/*.yaml; do + [ -f "$wf" ] || continue + # Comment lines are prose, not a step. A KYAML `run:` is one quoted scalar, + # so the substring test still sees the command inside it. + versions="$(awk ' + /^[[:space:]]*#/ { next } + /gh-actions-lock\/releases\/download\// { + if (match($0, /gh-actions-lock\/releases\/download\/v[0-9]+\.[0-9]+\.[0-9]+/)) + { v = substr($0, RSTART, RLENGTH); sub(/.*\//, "", v); print v } + else print "unpinned" + next + } + /extension[[:space:]]+install[[:space:]]+github\/gh-actions-lock/ { + if (match($0, /--pin[[:space:]]+v[0-9]+\.[0-9]+\.[0-9]+/)) { + v = substr($0, RSTART, RLENGTH); sub(/--pin[[:space:]]+/, "", v); print v + } else print "unpinned" + } + ' "$wf" | sort -u | paste -sd, -)" + if [ -n "$versions" ]; then + printf '%s\t%s\t%s\n' "$REPO_SLUG" "${wf#"$REPO_DIR"/}" "$versions" + found=1 + fi +done +exit "$found" diff --git a/scripts/tests/check-copied-lock-step-test.sh b/scripts/tests/check-copied-lock-step-test.sh new file mode 100644 index 000000000..76a860d4d --- /dev/null +++ b/scripts/tests/check-copied-lock-step-test.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# Proves check-copied-lock-step.sh finds a workflow that runs its own +# gh-actions-lock (raw release download or `gh extension install`), in block +# YAML and KYAML, names the pinned version, and stays quiet for prose, for a +# clean tree and for standards itself. +set -uo pipefail +SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check-copied-lock-step.sh" +WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT + +pass=0; fail=0 +expect() { # expect