mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
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".
129 lines
5.8 KiB
Bash
Executable File
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
|