Repository navigation
CI resilience (per-PR): detect Actions-policy startup_failure instead of mis-attributing it #15
Description
Context — the estate-wide "Actions-policy CI outage"
Two failure modes recur across the estate: mode-1 (allowed_actions=selected + empty patterns_allowed → external uses: rejected → startup_failure) and mode-2 (sha_pinning_required=true + tag-pinned workflow files → github-owned actions fail the pin rule). The squabbler is the per-PR / per-repo reactive lane for a single stuck gate — but today it mis-attributes this class:
- a
governance-reusablestartup_failure→AssignGateOwner{OwnedUpstream:standards}(wrong: the reusable is fine; the local Actions policy blocks it); - a tag-pinned
codeql.ymlstartup_failure→EscalateToExpert{Security}(wrong: a scan won't fix a tag-vs-SHA rejection).
The symptom already lands in the Gate (fetch.rs::parse_rollup maps STARTUP_FAILURE → CheckRun::Failed); what's missing is the why-probe. This is a real added rule inside the existing diagnose→classify→move pipeline. Fleet-scale remediation stays out of scope (escalate to ExpertGroup::HypatiaFleet → gitbot-fleet).
Decision of record
Estate default = allowed_actions=all + sha_pinning_required=true. The squabbler's settings-move should propose that posture (or an explicit patterns_allowed superset) while keeping sha_pinning_required=true — never disable pinning. squabble ≠ bypass still holds: these moves only let a required check start; they drop no required context and weaken no check.
Changes
-
crates/squabble-cli/src/fetch.rs— add an Actions-policy why-probe. When any required context resolves toSTARTUP_FAILURE(already handled atparse_rollup, ~L171–184), additionally callgh api repos/{o}/{r}/actions/permissions+.../selected-actionsand thread{allowed_actions, sha_pinning_required, patterns_allowed}into the fight as classification input (it already fetches the ruleset viagh api .../rules/branches/{b}). -
crates/squabble-fight/src/workflows.rs— extendWorkflowInfowithtag_pinned_uses: Vec<String>andexternal_uses: Vec<String>(parse inparse_workflowalongsidereusable_repo()). Inclassify(), when the emitting workflow uses an external reusable/action absent from aselectedallowlist (mode-1) or a tag-pinneduses:undersha_pinning_required(mode-2), return the NEW move instead of the currentOwnedUpstream/Escalate-Securitymis-attribution. -
crates/squabble-core/src/moves.rs— addMovevariants:PinWorkflowActions { workflow, refs }(self-win, SHA-pin at source; twin of the existingRepinReusableWorkflow) andReconcileActionsPolicy { blocked_ref, add_patterns }/SetActionsAllowedAll(a legitimate non-bypass settings move). Add them toevery_move_is_legitimate_by_constructionanddescribe()coverage so the SPARK invariant still holds. -
crates/squabble-core/src/lib.rs— no structural change;diagnose_with_hintsis the existing plug seam — feed it the new hints from the host. -
crates/squabble-fight/src/lib.rs::structural_scan()— add a probe surfacing "allowed_actions=selectedwith empty/insufficientpatterns_allowedwhile an external reusable/action is used" and "tag-pinneduses:undersha_pinning_required" even before the check has (yet)startup_failure'd, mirroring the existing well-known/path-filter/AGPL probes. -
examples/actions-policy-deadlock.json— new offline gate fixture (mirroringexamples/gate-deadlock.json) with a required check asmissing/startup_failurefor regression coverage. The squabbler's own repo currently shows aSecret Scannerstartup_failure— an in-repo dogfood target for.github/workflows/dogfood-gate.yml. -
docs/CHARTER.adoc— add the "Actions-policystartup_failuredeadlock" class to the v0.1 fight playbook, stating the tradeoff explicitly (preferallowed_actions=allwhile KEEPINGsha_pinning_required=true, plus SHA-pin the workflow refs).
DO-NOT pointer: .github/settings.yml (probot/settings) manages branch protection/labels/merge only and cannot set Actions permissions — the fix goes via the REST actions/permissions + .../selected-actions endpoints (single-repo from the squabbler, fleet-wide from gitbot-fleet), never via settings.yml.
Acceptance criteria
- A required-context
STARTUP_FAILUREtriggers the why-probe; the fight receives{allowed_actions, sha_pinning_required, patterns_allowed}. classify()returnsPinWorkflowActionsfor a tag-pinned-under-sha-pinning gate andReconcileActionsPolicy/SetActionsAllowedAllfor an empty-allowlist gate — notOwnedUpstream{standards}/Escalate{Security}.- New
Movevariants passevery_move_is_legitimate_by_construction(SPARK invariant intact) and havedescribe()text. examples/actions-policy-deadlock.jsonis diagnosed to the new move in a unit test.
Model tier
Opus — Rust across three crates plus the SPARK-legitimacy invariant; the classification changes are subtle (distinguishing "policy blocks the check from starting" from genuine upstream/out-of-lane reds).
Related
Fleet-scale detection/remediation belong to hyperpolymath/hypatia + hyperpolymath/gitbot-fleet (the squabbler escalates the fleet dimension to ExpertGroup::HypatiaFleet); source-of-truth allowlist in hyperpolymath/standards.
Activity
hyperpolymath commentedon Jul 11, 2026
Part of the estate-wide CI-resilience remediation set (Actions-policy
startup_failure, root-caused by the 2026-07-11 estate scan). One owner per responsibility:Decision of record: estate default =
allowed_actions=all+sha_pinning_required=true. The one-off sweep of the 91 broken hyperpolymath repos is intentionally deferred to gitbot-fleet#362 (self-healing behind the kill-switch), not a manual PUT.ci: clear OSSF Scorecard startup_failure (#19)
fix(ci): restore reachable standards workflow pins (#65)
fix: green main (#105) + classic-protection reading (fixes #100) + Ac…
hyperpolymath commentedon Oct 8, 2026
Closing as implemented and verified — the work landed in #110 (merged 2026-09-27), which referenced this issue but did not use a closing keyword, so it stayed open.
#110 was authored without a Rust toolchain ("compilation + tests delegated to CI"), so I re-verified locally rather than trusting the PR body.
Verified on
main@a4cd088(worktree, rustc 1.101.0-nightly via the repo'smise.toml):cargo test --workspace --all-targetscargo clippy --workspace --all-targets -- -D warningsthiserrorinsquabble-core, not a lint failure)cargo fmt --all -- --checkAcceptance criteria, checked against code and named tests:
STARTUP_FAILURE.fetch::fetch_actions_policy(fetch.rs:429) readsactions/permissionsand.../selected-actions. It is threaded into the fight asActionsPolicyFacts(fight.rs:58). Tests:only_required_contexts_with_startup_failure_trigger_the_probeandstartup_failure_still_classifies_failed_in_the_gate.classify()returns the new moves, not the mis-attributions. workflows.rs:244–259. Tests:empty_selected_allowlist_classifies_to_set_allowed_all,selected_allowlist_gap_classifies_to_reconcile_with_the_missing_patternandtag_pinned_under_sha_pinning_classifies_to_pin_not_escalate_security.PinWorkflowActions,ReconcileActionsPolicyandSetActionsAllowedAllvariants are inevery_move_is_legitimate_by_construction, havedescribe()arms, and passactions_policy_moves_serialise_round_trip.the_offline_fixture_diagnoses_to_the_new_movesloadsexamples/actions-policy-deadlock.jsonand assertsSetActionsAllowedAllandPinWorkflowActions{codeql.yml}. It also asserts the absence ofOwnedUpstream{standards}andEscalate{Security}. The negative controlthe_same_fixture_without_the_probe_shows_the_old_misattributionproves the probe inputs are what carries the redirect.Changes 5 and 7 are also present: the structural-scan probe (
latent_policy_deadlocks_surface_before_any_startup_failure) and the CHARTER playbook entry (docs/CHARTER.adoc:112–130).Horizon of this verification: unit and fixture level on
main. Not exercised live against a real deadlocked PR. The estate's mode-1 outage has largely been repaired since (see gitbot-fleet#362), so no live specimen was at hand.