Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 20 additions & 32 deletions .githooks/validate-actions-lock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,14 @@ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
# quietly widen to cover an accident. An entry that stops being used is
# reported as stale, so it cannot rot either.
EXPECTED_ABSENT=(
# Empty: the denoland/setup-deno exception was removed 2026-09-22 after
# the last consumer went away (the validator itself reported it stale).
# Estate doctrine is still bun-only; if a deno ref ever returns, the
# cure is to remove the consumer, not to re-add an exception.
# Estate doctrine is bun-only; deno is banned. Keying it would make a
# BANNED runtime a required lockfile key for every caller of
# governance-reusable.yml. The cure is to remove the consumer -- port the
# governance scripts to bun -- not to satisfy it.
"denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed"
# A2ML is dead. security-gate-pr-target.yml still calls its
# secrets-check-action; locking it in would connect new machinery to it.
"hyperpolymath/a2ml-ecosystem@f7a40a4d5cc82b2e73f861119baa6818d77a448d"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the nonexistent A2ML exception

In the reviewed commit tree, a repo-wide git grep finds no use of this action and security-gate-pr-target.yml does not exist, so this newly added entry immediately produces a stale-exception warning on every successful validator run. Because stale entries only warn rather than increment ERRORS, it also remains as an allowlist that would silently exempt this exact dependency if it were later reintroduced; omit it until there is an actual consumer.

Useful? React with 👍 / 👎.

)

if [ ! -f "$LOCKFILE" ]; then
Expand Down Expand Up @@ -100,6 +104,11 @@ if [ "${#LOCK_KEYS[@]}" -eq 0 ]; then
exit 1
fi

# Look up whether a single 'owner/repo@sha' key is present in LOCK_KEYS.
# Arguments:
# $1 - the lowercase 'owner/repo@sha' key to search for.
# Returns:
# 0 if the key is found in LOCK_KEYS, 1 otherwise.
lock_has() {
local needle="$1" k
for k in "${LOCK_KEYS[@]}"; do
Expand All @@ -113,34 +122,13 @@ CHECKED=0
declare -a SEEN_ABSENT=()

# Collect every SHA-pinned uses: ref across all workflow files.
#
# Read with yq, never grep (YAML-POLICY Y-1). A line grep for `uses:` only
# sees block style: on a KYAML file (`uses: "owner/repo@sha", # v1`) it
# captured the quote and comma into the ref and reported a pinned, locked
# action as missing. The parser returns the scalar VALUE whatever the style,
# and never matches a `uses:` that is text inside a `run:` body.
# Measured 2026-10-01: same 29-ref set as the old grep on the block tree.
if ! command -v yq >/dev/null 2>&1; then
echo -e "${RED}[validate-actions-lock] ERROR: yq not found -- it reads the workflows (YAML-POLICY Y-1)${NC}" >&2
exit 1
fi
declare -a UNPARSED=()
mapfile -t RAW < <(
for f in "$WORKFLOW_DIR"/*.yml "$WORKFLOW_DIR"/*.yaml \
"$ACTIONS_DIR"/*/action.yml "$ACTIONS_DIR"/*/action.yaml; do
[ -f "$f" ] || continue
yq -r '.. | select(tag == "!!map") | select(has("uses")) | .uses | select(tag == "!!str")' "$f" \
|| printf '\001UNPARSED\001%s\n' "$f"
done | grep -E $'@[0-9a-fA-F]{40}$|^\001UNPARSED\001' | sort -u
grep -rhoE '^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*[^[:space:]#]+@[0-9a-fA-F]{40}' \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore parser-based extraction for YAML refs

When a workflow or composite uses valid KYAML flow syntax such as steps: [{uses: "owner/action@<sha>"}], this anchored regex does not see the ref; quoted block scalars retain their opening quote and are falsely reported missing. I reproduced the flow-mapping case against this commit: a deliberately unkeyed action was ignored and the validator exited 0. This regresses the parser-based handling required by 3-practice/YAML-POLICY.adoc:55-70 and allows the required lock-coverage gate to pass without checking every action.

Useful? React with 👍 / 👎.

"$WORKFLOW_DIR"/*.yml "$WORKFLOW_DIR"/*.yaml \
"$ACTIONS_DIR"/*/action.yml "$ACTIONS_DIR"/*/action.yaml 2>/dev/null \
| sed -E 's/^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*//' \
| sort -u
)
# A file yq cannot parse is a file whose refs went unchecked: fail closed.
for ref in "${RAW[@]}"; do
case "$ref" in $'\001UNPARSED\001'*) UNPARSED+=("${ref#$'\001UNPARSED\001'}") ;; esac
done
if [ "${#UNPARSED[@]}" -gt 0 ]; then
echo -e "${RED}[validate-actions-lock] ERROR: yq could not parse: ${UNPARSED[*]}${NC}" >&2
exit 1
fi

# A zero-input pass is the classic fake green: if ref extraction ever breaks,
# this script would report success having checked nothing. If the lockfile
Expand Down Expand Up @@ -174,7 +162,7 @@ for ref in "${RAW[@]}"; do
norm="$(printf '%s/%s@%s' "$owner" "$repo" "$sha" | tr '[:upper:]' '[:lower:]')"

skip=0
for ex in ${EXPECTED_ABSENT[@]+"${EXPECTED_ABSENT[@]}"}; do
for ex in "${EXPECTED_ABSENT[@]}"; do
ex_lc="$(printf '%s' "$ex" | tr '[:upper:]' '[:lower:]')"
if [ "$norm" = "$ex_lc" ]; then
SEEN_ABSENT+=("$ex_lc")
Expand All @@ -194,7 +182,7 @@ done

# A doctrine exception that is no longer used must be removed, or the list
# becomes a place where real coverage gaps can hide.
for ex in ${EXPECTED_ABSENT[@]+"${EXPECTED_ABSENT[@]}"}; do
for ex in "${EXPECTED_ABSENT[@]}"; do
ex_lc="$(printf '%s' "$ex" | tr '[:upper:]' '[:lower:]')"
found=0
for s in ${SEEN_ABSENT[@]+"${SEEN_ABSENT[@]}"}; do
Expand Down
Loading