diff --git a/scripts/tests/actions-lock-update-test.sh b/scripts/tests/actions-lock-update-test.sh index 158b4c38..8ca997b5 100755 --- a/scripts/tests/actions-lock-update-test.sh +++ b/scripts/tests/actions-lock-update-test.sh @@ -29,7 +29,28 @@ if [ "${2:-}" = "--verify-local" ]; then .github/workflows/actions.lock sed -i 's#uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1#uses: actions/checkout@v7.0.1#' \ .github/workflows/ci.yml - exit + case "${FAKE_VERIFY_FINDING:-}" in + reusable-exact) + printf '%s\n' '{"valid":false,"findings":[{"workflow":".github/workflows/reusable.yml","category":"stale","dependency":"hyperpolymath/standards@abc123"}]}' + exit 1 + ;; + reusable-wrong-ref) + printf '%s\n' '{"valid":false,"findings":[{"workflow":".github/workflows/reusable.yml","category":"stale","dependency":"hyperpolymath/standards@wrong456"}]}' + exit 1 + ;; + reusable-non-stale) + printf '%s\n' '{"valid":false,"findings":[{"workflow":".github/workflows/reusable.yml","category":"missing","dependency":"hyperpolymath/standards@abc123"}]}' + exit 1 + ;; + malformed-success) + printf '%s\n' 'not valid JSON' + exit 0 + ;; + *) + printf '%s\n' '{"valid":true,"findings":[]}' + exit + ;; + esac fi sed -i '1i# This workflow is managed by gh actions-lock.' .github/workflows/ci.yml sed -i 's#actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1#actions/checkout@v7.0.1#' \ @@ -75,6 +96,41 @@ cmp -s "$WORK/ci.before-verify" .github/workflows/ci.yml cmp -s "$WORK/lock.before-verify" .github/workflows/actions.lock echo "PASS: Actions lock verification restores tool-authored workflow edits" +cat > .github/workflows/reusable.yml <<'EOF' +# SPDX-License-Identifier: MPL-2.0 +name: Reusable caller +on: push +permissions: {} +jobs: + governance: + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@abc123 +EOF + +FAKE_VERIFY_FINDING=reusable-exact GH_BIN="$WORK/bin/fake-gh" \ + bash "$UPDATE" --verify-local .github/workflows >/dev/null +echo "PASS: exact reusable-workflow dependency is accepted" + +if FAKE_VERIFY_FINDING=reusable-wrong-ref GH_BIN="$WORK/bin/fake-gh" \ + bash "$UPDATE" --verify-local .github/workflows >/dev/null 2>&1; then + echo "FAIL: wrong reusable-workflow ref was accepted" >&2 + exit 1 +fi +echo "PASS: wrong reusable-workflow ref remains blocking" + +if FAKE_VERIFY_FINDING=reusable-non-stale GH_BIN="$WORK/bin/fake-gh" \ + bash "$UPDATE" --verify-local .github/workflows >/dev/null 2>&1; then + echo "FAIL: non-stale reusable-workflow finding was accepted" >&2 + exit 1 +fi +echo "PASS: non-stale reusable-workflow finding remains blocking" + +if FAKE_VERIFY_FINDING=malformed-success GH_BIN="$WORK/bin/fake-gh" \ + bash "$UPDATE" --verify-local .github/workflows >/dev/null 2>&1; then + echo "FAIL: malformed successful verifier output was accepted" >&2 + exit 1 +fi +echo "PASS: malformed verifier output fails closed" + # A failed refresh must restore both authored workflows and the previous # lockfile; this is the production failure mode that left the original checkout # half-rewritten when DNS resolution failed. diff --git a/scripts/update-actions-lock.sh b/scripts/update-actions-lock.sh index cc5c3f87..ed9bd1ea 100755 --- a/scripts/update-actions-lock.sh +++ b/scripts/update-actions-lock.sh @@ -26,6 +26,71 @@ restore_workflows() { done } +workflow_references_reusable_dependency() { + workflow=$1 + dependency=$2 + repo=${dependency%@*} + ref=${dependency#*@} + + [[ -f "$workflow" ]] || return 1 + awk -v prefix="$repo/.github/workflows/" -v suffix="@$ref" ' + /^[[:space:]]*uses:[[:space:]]*/ { + value = $0 + sub(/^[[:space:]]*uses:[[:space:]]*/, "", value) + sub(/[[:space:]]*#.*/, "", value) + sub(/[[:space:]]*$/, "", value) + if (index(value, prefix) == 1 && + length(value) >= length(suffix) && + substr(value, length(value) - length(suffix) + 1) == suffix) { + found = 1 + } + } + END { exit(found ? 0 : 1) } + ' "$workflow" +} + +verify_lock_coverage() { + # gh-actions-lock v0.1.6 does not recognise reusable-workflow `uses:` + # paths. GitHub's startup enforcement nevertheless requires callers to + # carry the reusable repository and its transitive actions in actions.lock. + # Accept only the tool's `stale` false positive when the named workflow + # contains the exact owner/repo/.github/workflows/file@ref dependency. + # Every other finding, including a wrong ref, remains blocking. + command -v jq >/dev/null 2>&1 || { + "$GH_BIN" actions-lock --verify-local + return + } + + set +e + result=$("$GH_BIN" actions-lock --verify-local --json=valid,findings) + status=$? + set -e + + if ! printf '%s' "$result" | jq -e '.valid != null and (.findings | type == "array")' >/dev/null 2>&1; then + printf '%s\n' "$result" + [[ "$status" -ne 0 ]] && return "$status" + return 1 + fi + if printf '%s' "$result" | jq -e '.valid == true and (.findings | length == 0)' >/dev/null; then + return 0 + fi + + remaining=0 + while IFS=$'\t' read -r category workflow dependency; do + if [[ "$category" = stale ]] && + workflow_references_reusable_dependency "$workflow" "$dependency"; then + echo "Accepted reusable-workflow lock coverage: $workflow -> $dependency" + else + remaining=$((remaining + 1)) + fi + done < <(printf '%s' "$result" | jq -r '.findings[] | [.category, .workflow, .dependency] | @tsv') + + if [[ "$remaining" -ne 0 ]]; then + printf '%s\n' "$result" + return 1 + fi +} + cleanup() { status=$? if [ "$COMPLETE" != true ]; then @@ -62,6 +127,6 @@ fi # Despite its name, --verify-local can migrate local `./` action paths to an # invalid `$/` spelling. Treat verification as mutating and restore authored # workflow bytes afterward too. -"$GH_BIN" actions-lock --verify-local +verify_lock_coverage restore_workflows COMPLETE=true