Files
shared-actions/scripts/check-actions.sh
T
lukaszraczylo 1aece66c7f feat: validate workflows and actions, and prepare Renovate bumps
scripts/check-actions.sh runs actionlint and checks that every external
uses: reference resolves. The validate workflow runs it, then runs the
go-test and node-build actions and the node-pr workflow against small
fixtures, so a bump of an action they use is exercised before it merges.

renovate.json bumps the actions this repository uses. Minor and patch
bumps merge themselves once validate is green. Major bumps wait for a
person, because every consumer calls these actions at @main. The same
script runs as a Renovate post-upgrade task and marks a failing branch so
no PR opens.
2026-10-03 22:22:22 +01:00

116 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Static checks for the workflows and composite actions in this repository:
# 1. actionlint (schema, expressions, and shell steps through shellcheck when present)
# 2. every external `uses:` reference resolves to a real tag or branch
#
# Usage: scripts/check-actions.sh [--ci | --renovate]
# --ci exit 1 when a check fails (default)
# --renovate exit 0 and write .validation-failed when a check fails, so the
# Renovate runner deletes the branch before a PR can open
#
# Needs: bash, git, curl, tar, sha256sum or shasum. No other tools: actionlint is
# downloaded at a pinned version and checked against a pinned SHA-256.
set -u
mode=--ci
case "${1:-}" in
"" | --ci) mode=--ci ;;
--renovate) mode=--renovate ;;
*) echo "usage: $0 [--ci | --renovate]" >&2; exit 2 ;;
esac
ACTIONLINT_VERSION=1.7.12
ACTIONLINT_SHA256_AMD64=8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
ACTIONLINT_SHA256_ARM64=325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6
marker=.validation-failed
cd "$(git rev-parse --show-toplevel)" || exit 2
fail=0
problem() {
echo "::error::$*" >&2
fail=1
}
sha256_of() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi
}
install_actionlint() {
local os arch want url tmp
case "$(uname -s)" in Linux) os=linux ;; Darwin) os=darwin ;; *) return 1 ;; esac
case "$(uname -m)" in
x86_64 | amd64) arch=amd64; want=$ACTIONLINT_SHA256_AMD64 ;;
aarch64 | arm64) arch=arm64; want=$ACTIONLINT_SHA256_ARM64 ;;
*) return 1 ;;
esac
# The pinned checksums are for the linux builds only.
[ "$os" = linux ] || return 1
tmp=$(mktemp -d)
url="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${os}_${arch}.tar.gz"
curl -fsSL --retry 3 -o "$tmp/a.tgz" "$url" || return 1
[ "$(sha256_of "$tmp/a.tgz")" = "$want" ] || { echo "actionlint checksum mismatch" >&2; return 1; }
tar -xzf "$tmp/a.tgz" -C "$tmp" actionlint || return 1
echo "$tmp/actionlint"
}
run_actionlint() {
local bin
if command -v actionlint >/dev/null 2>&1; then
bin=$(command -v actionlint)
else
bin=$(install_actionlint) || { problem "cannot install actionlint ${ACTIONLINT_VERSION}"; return; }
fi
# -shellcheck= disables the shellcheck pass when the binary is missing.
local opts=()
command -v shellcheck >/dev/null 2>&1 || opts+=(-shellcheck=)
# Info and style findings from shellcheck (for example SC2086 on intentional word
# splitting) do not fail the gate. Warnings and errors do. Composite actions are
# not linted by actionlint 1.7; workflows are.
opts+=(-ignore 'shellcheck reported issue.+SC[0-9]+:(info|style):')
if ! "$bin" "${opts[@]}" -color .github/workflows/*.y*ml; then
problem "actionlint reported problems"
fi
}
# Resolves every `uses: owner/repo[/path]@ref` that points outside this repository.
check_refs() {
local files uses ref repo path checked=""
files=$(find .github -type f \( -name '*.yml' -o -name '*.yaml' \))
uses=$(grep -hoE '^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*[^[:space:]#]+' $files | sed -E 's/^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*//' | sort -u)
while IFS= read -r u; do
[ -n "$u" ] || continue
case "$u" in ./* | docker://*) continue ;; esac
repo=${u%%@*}
ref=${u#*@}
[ "$repo" != "$u" ] || { problem "no @ref in 'uses: $u'"; continue; }
# owner/repo/sub/path -> owner/repo
path=$(echo "$repo" | cut -d/ -f1,2)
# This repository's own composite actions are tracked at @main by design.
[ "$path" = lukaszraczylo/shared-actions ] && continue
case " $checked " in *" $path@$ref "*) continue ;; esac
checked="$checked $path@$ref"
if ! git ls-remote --exit-code "https://github.com/$path" "refs/tags/$ref" "refs/heads/$ref" >/dev/null 2>&1; then
# A full commit SHA is not listed by ls-remote; accept it when the repository answers at all.
if echo "$ref" | grep -qE '^[0-9a-f]{40}$' && git ls-remote --exit-code "https://github.com/$path" HEAD >/dev/null 2>&1; then
continue
fi
problem "'uses: $u' does not resolve: no tag or branch $ref in $path"
fi
done <<<"$uses"
}
rm -f "$marker"
run_actionlint
check_refs
if [ "$fail" -ne 0 ]; then
if [ "$mode" = --renovate ]; then
echo "validation failed; writing $marker" >&2
printf 'actions validation failed\n' >"$marker"
exit 0
fi
exit 1
fi
echo "actions validation passed"