Skip to content

Commit 175e699

Browse files
hyperpolymathclaude
andcommitted
ci: allowlist escalation (repo->org->enterprise) + preflight canary
Follow-up to #482. Two additions the live gossamer/metadatastician fix surfaced: - scripts/set-allowed-actions.sh: escalate the apply repo -> org (owner) -> enterprise. A repo-level PUT that 409s "already set at the organization or enterprise level" now falls back to the governing level — the metadatastician case (allowlist enforced at the ORG level, so the repo PUT 409s). - rhodium-standard-repositories/.github/workflows/allowlist-preflight.yml: a template workflow (seeded into every RSR repo) that runs check-allowed-actions using only actions/checkout, so it can never itself startup-fail. Turns an opaque estate-wide startup_failure blackout into one legible red check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9a36c90 commit 175e699

3 files changed

Lines changed: 111 additions & 22 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Allowlist Preflight — surfaces an Actions-allowlist gap as an ORDINARY red
3+
# check instead of an opaque, estate-wide `startup_failure` blackout.
4+
#
5+
# Under `allowed_actions: selected`, a workflow that `uses:` an un-allowlisted
6+
# action/reusable dies at STARTUP with no job and no log. This job uses ONLY
7+
# actions/checkout (github-owned, always permitted), so it can NEVER itself
8+
# startup-fail — it is the canary that stays runnable and reports which OTHER
9+
# workflows would die, and why. Ship it in every RSR repo (seeded from this
10+
# template) so a future org migration shows a legible red check, not a blackout.
11+
12+
name: Allowlist Preflight
13+
14+
on:
15+
pull_request:
16+
push:
17+
branches: [main, master]
18+
workflow_dispatch:
19+
20+
permissions:
21+
contents: read
22+
23+
concurrency:
24+
group: allowlist-preflight-${{ github.ref }}
25+
cancel-in-progress: true
26+
27+
jobs:
28+
allowlist:
29+
name: Actions allowlist coverage
30+
runs-on: ubuntu-latest
31+
timeout-minutes: 5
32+
steps:
33+
- name: Checkout
34+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
35+
36+
- name: Every workflow `uses:` must be on the canonical allowlist
37+
run: |
38+
# Fetch the estate detector; it fetches the canonical allowlist itself.
39+
curl -fsSL https://raw.githubusercontent.com/hyperpolymath/standards/main/scripts/check-allowed-actions.sh -o "${RUNNER_TEMP}/check-allowed-actions.sh"
40+
bash "${RUNNER_TEMP}/check-allowed-actions.sh"

rhodium-standard-repositories/actions-allowlist/README.adoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,15 @@ adopts a new action; the check below then enforces it everywhere.
4545

4646
| **Robot-repo-automaton** (onboarding)
4747
| `scripts/set-allowed-actions.sh`
48-
| The ROOT prevention: applies the canonical allowlist to a repo. The farm/onboarding process MUST run this (with an admin PAT) whenever a repo is created or migrated, so `patterns_allowed` is never empty. Rhodibot (only `contents`/`pull-requests: write`) cannot set Actions policy, so it *detects + reports* via the check; the admin PAT *sets* it.
48+
| The ROOT prevention: applies the canonical allowlist. The farm/onboarding process MUST run this (with an admin PAT) whenever a repo is created or migrated, so `patterns_allowed` is never empty. It **escalates repo → org → enterprise**: a repo-level PUT that returns 409 "already set at the organization or enterprise level" falls back to the org (owner), then the enterprise — because the allowlist is often enforced one or two levels up (as it is for `metadatastician`, governed at the org level). Rhodibot (only `contents`/`pull-requests: write`) cannot set Actions policy, so it *detects + reports* via the check; the admin PAT *sets* it.
4949
|===
5050

51+
An **allowlist preflight** workflow (`.github/workflows/allowlist-preflight.yml`,
52+
seeded from the RSR template) makes this a first-class red check: it uses only
53+
`actions/checkout` (github-owned, always permitted) so it can never itself
54+
startup-fail, and runs the check on every push/PR — turning "the whole CI went
55+
dark" into "one legible red check that names the offending `uses:`".
56+
5157
== RSR requirement
5258

5359
A conforming RSR repo MUST:

scripts/set-allowed-actions.sh

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,74 @@
11
#!/usr/bin/env bash
22
# 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).
45
#
56
# 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.
1110
#
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.
1416
#
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]}"
1826
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
2056

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
2461

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
2768

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

Comments
 (0)