Skip to content

CI resilience (prevention/enforcement): wire allowlist preflight into the governance gate + live-policy detector #486

Description

@hyperpolymath

Context — the estate-wide "Actions-policy CI outage"

A single onboarding stamp sets the restrictive frame (allowed_actions=selected + sha_pinning_required=true) but omits the allowlist-population step, leaving patterns_allowed=[]. With an empty selected-list, any uses: that is not github-owned/verified is rejected at workflow parse → startup_failure (0 jobs, 0s). This took out CI across 91 hyperpolymath repos + (until an org-level PUT) all 8 metadatastician repos. A second, independent stamp (sha_pinning_required=true left on while workflow files still carry tag pins, e.g. the default codeql.yml @v4) fails github-owned actions purely on the tag-vs-SHA rule.

standards already owns the source of truth and the applier tool for this, but the detection is authored-yet-unwired. This issue closes that.

Decision of record (owner: maintainer)

Estate go-forward default = allowed_actions=all + sha_pinning_required=true. With SHA-pinning on, all still admits only exact-SHA actions, so it is equally safe and removes the empty-allowlist failure by construction. selected + the canonical vetted allowlist is reserved for a designated high-sensitivity tier (release/publish/signing repos). Never disable sha_pinning_required.

Changes

  1. [HIGHEST VALUE] Wire the existing tree detector into the governance gate. scripts/check-allowed-actions.sh is currently referenced by no workflow. Add an allowlist-preflight job to .github/workflows/governance-reusable.yml (mirror the language-policy job's sparse-checkout-of-standards/scripts pattern, ~L218–227) that runs it. Use only actions/checkout (github-owned, always permitted, already SHA-pinned to 9c091bb…) so the preflight itself can never startup_failure — turning opaque dark-CI into one legible red check.

  2. NEW scripts/check-actions-policy.sh — live-API detector. The tree-only checks assume the live policy equals canonical and so cannot see an empty live allowlist or pinning-off. Add a script that reads repos/<r>/actions/permissions + .../selected-actions and asserts allowed_actions ∈ {all, selected-with-superset} AND sha_pinning_required==true (needs administration:read). Wire as a second preflight step.

  3. Enforce the RSR requirement. scripts/check-rsr-profile.sh and scripts/rsr-selfaudit.sh currently have zero allowlist references. Add allowlist + sha_pinning assertions so RSR self-audit fails a repo whose live policy is empty/unpinned.

  4. Extend scripts/set-allowed-actions.sh. It applies only the allowlist today (documented gap). Extend it to also PUT {sha_pinning_required:true} on the permissions object in the same call, and support the allowed_actions=all posture — with a follow-up GET-verify that aborts if sha_pinning_required reset to false (see open question below). Keep the repo→org→enterprise 409-escalation.

  5. NEW .github/workflows/allowlist-preflight-reusable.yml so rsr-template-repo can seed a one-line caller (the actions-allowlist README promises it; standards ships none).

  6. tasks/Justfile: add check-allowlist / set-allowlist recipes (none today).

Draft artifact

# governance-reusable.yml: new job (mirror language-policy sparse-checkout ~L218-227)
  allowlist-preflight:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb...   # github-owned, always permitted, SHA-pinned
      - name: sparse-checkout standards scripts + allowlist json
        run: |  # (same pattern as language-policy job)
      - name: Preflight allowlist coverage (tree)
        run: bash scripts/check-allowed-actions.sh rhodium-standard-repositories/actions-allowlist/allowed-actions.json .github/workflows
      - name: Preflight live policy (API)
        env: { GH_TOKEN: ${{ secrets.SCAN_PAT }} }   # administration:read
        run: bash scripts/check-actions-policy.sh ${{ github.repository }}
# NEW scripts/check-actions-policy.sh
#!/usr/bin/env bash
set -euo pipefail; R=$1
AL=$(gh api repos/$R/actions/permissions --jq .allowed_actions)
SP=$(gh api repos/$R/actions/permissions --jq .sha_pinning_required)
[ "$SP" = true ] || { echo "::error::sha_pinning_required is OFF"; exit 1; }
if [ "$AL" = selected ]; then
  N=$(gh api repos/$R/actions/permissions/selected-actions --jq '.patterns_allowed|length')
  [ "$N" -gt 0 ] || { echo "::error::allowed_actions=selected with EMPTY patterns_allowed (FAILURE MODE 1)"; exit 1; }
fi
echo "policy ok: allowed=$AL sha_pin=$SP"

Acceptance criteria

  • governance-reusable.yml runs check-allowed-actions.sh + check-actions-policy.sh in a preflight job that uses only github-owned SHA-pinned actions.
  • check-actions-policy.sh exits non-zero on (a) empty selected allowlist and (b) sha_pinning_required=false; exits 0 on all + sha=true.
  • check-rsr-profile.sh / rsr-selfaudit.sh fail an empty/unpinned repo; pass a compliant one.
  • set-allowed-actions.sh co-sets sha_pinning_required=true and GET-verifies.

Open question to resolve during implementation

Does PUT /actions/permissions with allowed_actions=all preserve sha_pinning_required when omitted, or reset it? Confirm empirically; every all path must co-set + GET-verify + abort on reset. If it cannot be co-set safely, fall back to PUT only /selected-actions with the canonical list (never touches the permissions object).

Model tier

Sonnet — bash + workflow wiring over existing, exemplary patterns; no open design.

Related

Detection lands in hyperpolymath/hypatia; actuation in hyperpolymath/gitbot-fleet; birth-time default in hyperpolymath/rsr-template-repo; per-PR lane in hyperpolymath/cicd-squabbler. Source-of-truth allowed-actions.json lives here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions