diff --git a/scripts/fix-actions-policy.sh b/scripts/fix-actions-policy.sh new file mode 100755 index 0000000..b788f06 --- /dev/null +++ b/scripts/fix-actions-policy.sh @@ -0,0 +1,258 @@ +#!/bin/bash +# SPDX-License-Identifier: MPL-2.0 +# +# fix-actions-policy.sh — repair the estate-wide "Actions-policy CI outage" +# +# Driven by gitbot-fleet#362. An onboarding stamp set +# `allowed_actions=selected` + `sha_pinning_required=true` but never +# populated `patterns_allowed`, leaving it `[]`. An empty selected-list +# rejects every non-github-owned `uses:` at workflow-parse time, so the run +# dies as `startup_failure` with ZERO jobs — indistinguishable from "CI is +# broken" but actually a settings fault. +# +# MEASURED 2026-07-21: 80 of 306 scanned hyperpolymath repos are in this +# state (BROKEN-M1). `hyperpolymath` is a USER account, so there is no org +# lever and each repo needs its own settings PUT. `metadatastician` IS an +# Organization, where a single org-level PUT fixes every repo by +# inheritance (requires the `admin:org` token scope). +# +# ── Why this fixer is shaped differently to every other fix-*.sh ────────── +# Every other script in this directory takes a REPO_PATH and edits files, so +# its blast radius is a diff that a human reviews in a PR. This one writes +# repository *settings* through the API: there is no diff, no branch and no +# PR. Per #362 that means it must be report-by-default and gated. Hence: +# +# * default mode is --list (READ ONLY — never writes) +# * --apply is required to write, and is refused without an explicit mode +# * the HYPATIA_AUTOMATION kill switch halts writes instantly +# * the estate exclusion registry is consulted, FAIL-CLOSED, before writes +# * every PUT is GET-verified, and the sweep ABORTS if sha_pinning was +# silently reset (never trade pinning away for permissiveness) +# +# ── Target posture ─────────────────────────────────────────────────────── +# #362's decision of record was `allowed_actions=all` + sha_pinning. A +# parallel remediation (llm-coding-configs/claude-code/notification-storm- +# remediation/FINDINGS.md, 2026-07-21) established a STRICTLY SAFER route +# that fixes the same 98 repos WITHOUT widening anything: +# +# keep allowed_actions=selected, keep sha_pinning_required=true, +# and simply POPULATE the empty patterns_allowed list. +# +# Its evidence is a natural experiment over 426 repos: +# allowed_actions=all 286 repos -> wrappers run +# selected + `hyperpolymath/*` present 42 repos -> wrappers run +# selected + patterns_allowed=[] 98 repos -> ALL startup_failure +# i.e. the breakage is caused by the list being EMPTY, not by it existing. +# Populating it therefore restores CI while KEEPING the owner allowlist as a +# real control — so `selected` is the default here, and `--mode all` is the +# opt-in fallback rather than the target. +# +# NEVER disable sha_pinning_required. +# +# Idempotent: healthy repos are reported `ok` and skipped. +# +# Usage: +# fix-actions-policy.sh [--list|--apply] [--mode all|selected] +# [--limit N] [--jsonl FILE] [--repo NAME] +# +# Exit codes: 0 ok · 2 usage · 3 sha_pinning reset (ABORT) · 4 kill switch +# 5 exclusion registry unavailable while applying + +set -euo pipefail + +OWNER="" +ACTION="list" # list | apply (report-by-default) +MODE="selected" # selected (default, non-widening) | all (opt-in fallback) +ALLOWLIST="" # curated patterns_allowed JSON; defaults to standards' +LIMIT=500 +JSONL="" +ONE_REPO="" +FINDING_FILE="" # dispatch-runner contract: $2 is the finding JSON +STD_REPO="hyperpolymath/standards" + +die() { echo "fix-actions-policy: $*" >&2; exit "${2:-2}"; } + +while [ $# -gt 0 ]; do + case "$1" in + --list) ACTION="list" ;; + --apply) ACTION="apply" ;; + --mode) MODE="${2:-}"; shift ;; + --allowlist) ALLOWLIST="${2:-}"; shift ;; + --limit) LIMIT="${2:-}"; shift ;; + --jsonl) JSONL="${2:-}"; shift ;; + --repo) ONE_REPO="${2:-}"; shift ;; + -h|--help) sed -n '2,50p' "$0"; exit 0 ;; + -*) die "unknown flag: $1" ;; + *) if [ -z "$OWNER" ]; then OWNER="$1" + elif [ -z "$FINDING_FILE" ]; then FINDING_FILE="$1" # dispatch-runner passes finding JSON as $2 + else die "unexpected arg: $1"; fi ;; + esac + shift +done + +# ── Dual invocation contract ───────────────────────────────────────────── +# scripts/dispatch-runner.sh calls every fixer as `