|
1 | 1 | #!/usr/bin/env bash |
2 | 2 | # SPDX-License-Identifier: MPL-2.0 |
3 | | -# set-allowed-actions.sh — apply the canonical Actions allowlist to a repo. |
| 3 | +# set-allowed-actions.sh — apply the canonical Actions allowlist at the level |
| 4 | +# that actually governs a repo (repo -> org -> enterprise fallback). |
4 | 5 | # |
5 | 6 | # This is the ROOT prevention for the "empty-allowlist startup_failure" class: |
6 | | -# when a repo is CREATED or MIGRATED to a new org, its `allowed_actions` allowlist |
7 | | -# is reset (often to empty patterns), so every estate action/reusable is blocked |
8 | | -# and the whole CI dies at startup. Run this on onboard/migrate so the allowlist |
9 | | -# is never empty. This is what the repo-automaton (farm sweep) must call with an |
10 | | -# admin token — the default GITHUB_TOKEN CANNOT change Actions policy. |
| 7 | +# when a repo is CREATED or MIGRATED, its allowlist is reset, blocking every |
| 8 | +# estate action/reusable at once so the whole CI dies at startup. Run this on |
| 9 | +# onboard/migrate so patterns_allowed is never empty. |
11 | 10 | # |
12 | | -# Requires: a token with repo Administration:write (fine-grained) or classic |
13 | | -# `repo` + org owner — i.e. the farm ADMIN PAT, not the workflow GITHUB_TOKEN. |
| 11 | +# The allowlist may be enforced at the REPO, the ORG, or the ENTERPRISE level. A |
| 12 | +# repo-level PUT returns 409 "already set at the organization or enterprise level" |
| 13 | +# when a higher level governs it — so this escalates automatically: |
| 14 | +# repo -> (409) -> org (owner) -> (409) -> enterprise (if slug given) |
| 15 | +# Fixing it at the org/enterprise level repairs ALL repos beneath it at once. |
14 | 16 | # |
15 | | -# Usage: set-allowed-actions.sh <owner/repo> [ALLOWED_ACTIONS_JSON] |
16 | | -set -euo pipefail |
17 | | -REPO="${1:?usage: set-allowed-actions.sh <owner/repo> [allowed-actions.json]}" |
| 17 | +# Requires an admin token for the level reached: repo Administration:write, or |
| 18 | +# admin:org, or admin:enterprise — the farm ADMIN PAT, not the workflow |
| 19 | +# GITHUB_TOKEN. (gh auth refresh -h github.com -s admin:org / admin:enterprise) |
| 20 | +# |
| 21 | +# Usage: set-allowed-actions.sh <owner/repo> [ALLOWED_ACTIONS_JSON] [ENTERPRISE_SLUG] |
| 22 | +# ENTERPRISE=<slug> may be given via env instead of the 3rd arg. |
| 23 | +set -uo pipefail |
| 24 | + |
| 25 | +REPO="${1:?usage: set-allowed-actions.sh <owner/repo> [allowed-actions.json] [enterprise-slug]}" |
18 | 26 | CANON="${2:-rhodium-standard-repositories/actions-allowlist/allowed-actions.json}" |
19 | | -[ -f "$CANON" ] || { echo "!! canonical allowlist not found: $CANON"; exit 2; } |
| 27 | +ENTERPRISE="${3:-${ENTERPRISE:-}}" |
| 28 | +OWNER="${REPO%%/*}" |
| 29 | +[ -f "$CANON" ] || { echo "!! canonical allowlist not found: $CANON" >&2; exit 2; } |
| 30 | +N="$(python3 -c "import json;print(len(json.load(open('$CANON'))['patterns_allowed']))")" |
| 31 | + |
| 32 | +# Apply the patterns at one scope. Returns 0 = applied, 42 = 409 (escalate), 1 = other error. |
| 33 | +apply() { |
| 34 | + local label="$1" prefix="$2" out rc |
| 35 | + out="$(gh api -X PUT "$prefix/actions/permissions/selected-actions" --input "$CANON" 2>&1)"; rc=$? |
| 36 | + if [ $rc -eq 0 ]; then |
| 37 | + echo "✔ applied $N patterns at the $label level ($prefix) — governs everything beneath it" |
| 38 | + return 0 |
| 39 | + fi |
| 40 | + if printf '%s' "$out" | grep -qiE 'organization or enterprise level|enterprise level'; then |
| 41 | + echo " $label level is governed higher up (409) — escalating…" |
| 42 | + return 42 |
| 43 | + fi |
| 44 | + echo "!! $label-level apply failed:" >&2; printf '%s\n' "$out" >&2 |
| 45 | + return 1 |
| 46 | +} |
| 47 | + |
| 48 | +# Best-effort: ensure the repo is on allowed_actions=selected, preserving `enabled`. |
| 49 | +enabled="$(gh api "repos/$REPO/actions/permissions" --jq '.enabled' 2>/dev/null || echo true)" |
| 50 | +gh api -X PUT "repos/$REPO/actions/permissions" -F enabled="$enabled" -f allowed_actions=selected >/dev/null 2>&1 || true |
| 51 | + |
| 52 | +echo "==> repo level: $REPO" |
| 53 | +apply "repo" "repos/$REPO"; rc=$? |
| 54 | +[ $rc -eq 0 ] && exit 0 |
| 55 | +[ $rc -ne 42 ] && exit 1 |
20 | 56 |
|
21 | | -echo "==> ensuring allowed_actions=selected + sha_pinning_required on $REPO" |
22 | | -gh api -X PUT "repos/$REPO/actions/permissions" \ |
23 | | - -F enabled=true -f allowed_actions=selected >/dev/null |
| 57 | +echo "==> org level: $OWNER (fixes every repo in the org)" |
| 58 | +apply "org" "orgs/$OWNER"; rc=$? |
| 59 | +[ $rc -eq 0 ] && exit 0 |
| 60 | +[ $rc -ne 42 ] && exit 1 |
24 | 61 |
|
25 | | -echo "==> applying $(python3 -c "import json,sys;print(len(json.load(open('$CANON'))['patterns_allowed']))") patterns to $REPO" |
26 | | -gh api -X PUT "repos/$REPO/actions/permissions/selected-actions" --input "$CANON" |
| 62 | +if [ -n "$ENTERPRISE" ]; then |
| 63 | + echo "==> enterprise level: $ENTERPRISE (fixes every org in the enterprise)" |
| 64 | + apply "enterprise" "enterprises/$ENTERPRISE"; rc=$? |
| 65 | + [ $rc -eq 0 ] && exit 0 |
| 66 | + exit 1 |
| 67 | +fi |
27 | 68 |
|
28 | | -echo "==> verify" |
29 | | -gh api "repos/$REPO/actions/permissions/selected-actions" \ |
30 | | - --jq '{patterns:(.patterns_allowed|length), github_owned:.github_owned_allowed, verified:.verified_allowed}' |
31 | | -echo "✔ canonical allowlist applied to $REPO" |
| 69 | +cat >&2 <<EOF |
| 70 | +!! Enforced at the ENTERPRISE level. Re-run with the enterprise slug: |
| 71 | + set-allowed-actions.sh $REPO $CANON <enterprise-slug> |
| 72 | + (needs admin:enterprise — gh auth refresh -h github.com -s admin:enterprise) |
| 73 | +EOF |
| 74 | +exit 3 |
0 commit comments