Files
fips/testing/check-action-pins.sh
Johnathan Corgan c7a74a88a0 Pin the nextest and nightly toolchain actions by commit SHA
The three cargo-nextest install steps in ci.yml and the nightly toolchain
step in package-openwrt.yml were the last action references left on
mutable refs. Their owners can move a tag or branch to different code at
any time, and the install-action v2 tag has already moved off the commit
this repository pins for cargo-ndk.

The nextest steps now use that same install-action commit, so there is one
install-action pin to bump rather than two. That lineage declares the
`tool` input required, so each step passes `tool: nextest`; its manifest
installs cargo-nextest 0.9.143, which stays fixed until the pin is bumped
by hand. The toolchain step is pinned to the head of rust-toolchain's
nightly branch and names `toolchain: nightly` explicitly, so a later bump
to a commit whose input has no default still resolves the same channel.
Pinning that action does not pin the toolchain: rustup still resolves
nightly when the step runs.

With every reference pinned, the action pin guard no longer needs its
list of individually allowed mutable refs or the function that matched
against it. Removing both, rather than leaving an empty list, also avoids
expanding an empty array under `set -u`, which bash releases before 4.4
treat as an unbound variable. The comment that justified the exceptions is
replaced by how to pin an action that selects its tool or toolchain from
the ref name: pin the SHA and pass the selection as an explicit `with:`
input. The success message drops "or justified".
2026-10-01 22:40:40 +00:00

129 lines
5.8 KiB
Bash
Executable File

#!/bin/bash
# ── GitHub Action pinning guard ─────────────────────────────────────────────
# Every third-party action this repository invokes must be referenced by a
# 40-character commit SHA, with its human-readable tag in a trailing comment.
#
# A tag is a mutable pointer. Whoever controls an action's repository can move
# `v6` to different code at any time, and several of the jobs here are worth
# moving it for: aur-publish.yml and aur-publish-git.yml hand an action
# AUR_SSH_PRIVATE_KEY, and the OpenWrt release jobs run with HIVE_CI_NSEC in
# the environment. A SHA is content-addressed and cannot be repointed. The
# trailing comment is required rather than optional so the pin stays legible:
# a bare 40-hex string tells a reader nothing about which release it is, and a
# pin nobody can read is a pin nobody updates.
#
# What counts as a violation: any `uses:` reference that is not
# * `owner/repo@<40 hex> # <tag>` — the required form, comment mandatory; or
# * a local action, `./path` or `docker://...`.
#
# WHAT THIS GUARD DOES NOT COVER, so a green run is not read as "the workflows
# fetch nothing unverified":
# * the actions that the pinned actions themselves invoke. Pinning
# KSXGitHub/github-actions-deploy-aur removes the retag vector; it does not
# constrain what that action does with the SSH key it is given by design
# (aur-publish.yml, aur-publish-git.yml).
# * `pip3 install --quiet pyyaml` in ci.yml's ci-parity job, which holds
# `checks: write`. Unpinned entirely, version and hash both.
# * `cargo install cargo-zigbuild --version 0.19.8 --locked` in
# package-openwrt.yml. Version-pinned, not hash-pinned.
# * anything a workflow downloads at run time. The zig tarball and the nak
# binary are SHA-256 checked in their own steps; nothing here enforces that.
#
# Exit 0 = clean. Exit 1 = an unpinned reference. Exit 2 = the guard could not
# run; never treated as a pass.
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$SCRIPT_DIR/.."
# The one accepted form for a third-party action. The comment is mandatory
# rather than optional: an optional comment would let the checker accept a pin
# it cannot describe, and a pin nobody can read is a pin nobody updates.
PINNED_RE='^[^@]+@[0-9a-f]{40} +#.*$'
# There are no exceptions. Some actions select what they install from the ref
# they are called at: a per-tool `taiki-e/install-action` tag, or a
# `dtolnay/rust-toolchain` channel branch, sets the selection input's default
# in that ref's action.yml. Pin such an action by SHA and pass the selection as
# an explicit `with:` input (`tool:`, `toolchain:`). That form works at a SHA
# from any of the action's refs; a bare SHA does not, because their `v2` and
# `master` trees declare the input required with no default.
if ! command -v git >/dev/null 2>&1; then
echo "check-action-pins: git not available, cannot sweep" >&2
exit 2
fi
if [[ ! -d "$REPO_ROOT/.github" ]]; then
echo "check-action-pins: $REPO_ROOT/.github missing, refusing to pass" >&2
exit 2
fi
# Tracked files only. Workflows plus any composite/local action definition:
# a future .yaml extension and a future .github/actions/ tree both have to be
# swept, or the guard silently narrows as the repository grows.
if ! tracked="$(git -C "$REPO_ROOT" ls-files -- '.github/workflows/*.yml' '.github/workflows/*.yaml' '.github/actions/*.yml' '.github/actions/*.yaml')"; then
echo "check-action-pins: git ls-files failed, refusing to pass" >&2
exit 2
fi
if [[ -z "$tracked" ]]; then
echo "check-action-pins: no tracked workflow or action files, refusing to pass" >&2
exit 2
fi
mapfile -t files < <(printf '%s\n' "$tracked")
if [[ ${#files[@]} -eq 0 ]]; then
echo "check-action-pins: empty file list, refusing to pass" >&2
exit 2
fi
violations=0
checked=0
for f in "${files[@]}"; do
[[ -f "$REPO_ROOT/$f" ]] || continue
while IFS= read -r hit; do
n="${hit%%:*}"
text="${hit#*:}"
# A commented-out step is describing a reference, not resolving it.
[[ "$text" =~ ^[[:space:]]*# ]] && continue
# Everything after `uses:`, with surrounding whitespace and any quoting
# removed. The trailing comment is part of the ref text on purpose:
# the accepted form requires it.
ref="${text#*uses:}"
ref="${ref#"${ref%%[![:space:]]*}"}"
ref="${ref%"${ref##*[![:space:]]}"}"
checked=$((checked + 1))
# A local action or a container image is not a mutable upstream tag.
[[ "$ref" == ./* ]] && continue
[[ "$ref" == docker://* ]] && continue
[[ "$ref" =~ $PINNED_RE ]] && continue
echo "$f:$n: $ref"
violations=$((violations + 1))
done < <(grep -nE '^[[:space:]]*(- )?uses:' "$REPO_ROOT/$f" 2>/dev/null)
done
if [[ $checked -eq 0 ]]; then
echo "check-action-pins: no uses: references found at all, refusing to pass" >&2
exit 2
fi
if [[ $violations -gt 0 ]]; then
echo ""
echo "check-action-pins: $violations action reference(s) are not pinned to a commit SHA."
echo "Required form: uses: owner/repo@<40-hex-commit-sha> # <tag>"
echo "Resolve one with:"
echo " git ls-remote https://github.com/owner/repo 'refs/tags/<tag>^{}' refs/tags/<tag>"
echo "and use the peeled (^{}) SHA when the tag is annotated."
echo "A tag is a mutable pointer its owner can repoint; several of these jobs"
echo "hold a signing key or an SSH deploy key while the action runs."
exit 1
fi
echo "check-action-pins: all $checked action reference(s) pinned"
exit 0