Repository navigation
Description
Overview
Regression in #292 (merge 475d292e0), filed the same day it shipped. GitHub runs a run: block as /usr/bin/bash -e {0}; repo_settings.yml sets set -uo pipefail and never disables -e, so the per-repo read aborts the whole script on the first unreadable repo — silently, because its stderr goes to /dev/null. The unreadable-repo handler, and with it the in-org/out-of-org failure-policy split #292 added, is dead code in production.
The weekly schedule fires Sunday 06:40, so this fails every week until fixed.
Plan
- Disable
-eexplicitly in the block and comment why: the script's whole design is to survive a per-repo failure and report it, which GitHub's default flag defeats. - Make
visit()never return non-zero spuriously — the[ ... ] && failed=1trailing compound would abort on the first out-of-org repo. - Add the regression test that was missing: extract the workflow's
run:block and assert the unreadable path underbash -e, matching GitHub's invocation. - Sibling sweep already done at filing: scope is one file (details below).
Detailed implementation plan
Affected Repositories
- PyAutoBrain (primary)
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./PyAutoBrain | main | clean |
Suggested branch: feature/repo-settings-bash-e-abort
The defect
before=$(gh api "repos/$slug" --jq '.delete_branch_on_merge' 2>/dev/null)Non-zero for an unreadable repo → under -e the assignment aborts the script → 2>/dev/null makes it silent. The case "$before" in ... *) branch can never run. The PATCH path is fine (if gh api ...; then is -e-safe).
Latent second instance: [ "$hard" -eq 1 ] && failed=1 is the last statement of visit() in two branches; with hard=0 it returns 1, so the function returns 1 and -e aborts on the first out-of-org repo.
Evidence (confirmed, not inferred)
- Run 32992618192 (
mode=audit) failed in 20s with no stdout at all between##[endgroup]and##[error]Process completed with exit code 1. The log's env block printsshell: /usr/bin/bash -e {0}. - Reproduced locally by extracting the block and stubbing
ghso one read fails:bash body.sh→::warning::+::error::, exit 1 (graceful, as designed);bash -e body.sh→ exit 1, zero output, byte-for-byte the CI signature. - Corollary: at least one repo read genuinely failed in that run. Which one is still unknown — a second finding hiding behind this one. The fix must surface it.
Why it shipped
#292 was control-tested across 7 scenarios before merge. The harness ran bash <script>; GitHub runs bash -e <script>. The harness did not match production, so it validated a shell mode that never executes.
Sibling sweep — done at filing
Every run: block in PyAutoBrain audited. branch_archive.yml and branch_sweep_all.yml explicitly set -e and use no 2>/dev/null — abort-on-error is intended there, so they are consistent, not defective. branch_sweep.yml, brain_board.yml, docs.yml, nightly-release.yml show no matching pattern. repo_settings.yml is the only block combining no e-flag statement with swallowed stderr. Scope is one file.
Implementation Steps
.github/workflows/repo_settings.yml—set +eimmediately afterset -uo pipefail, commented with the reason. Preferred over|| trueon the read, because the abort risk applies to every guarded command in the block, not just that one.- Same file — both
[ "$hard" -eq 1 ] && failed=1occurrences becomeifblocks. tests/test_repo_settings_workflow.py(new) — parse the workflow, extract therun:block, write a stubghinto a tmp dir onPATH, and run the block withbash -e. Assert: unreadable in-org repo →::warning::emitted and exit 1; unreadable out-of-org → warning and exit 0; all-on → exit 0. stdlib + PyYAML only, no network.
Key Files
.github/workflows/repo_settings.yml— the defecttests/test_repo_settings_workflow.py— new; the missingbash -eleg
Acceptance
Re-dispatch repo_settings.yml in audit mode after merge. It must go green and name any repo whose read fails. A green run naming nothing is acceptable only if all 46 reads genuinely succeeded.
Original Prompt
Click to expand starting prompt
repo_settings.yml aborts silently under GitHub's bash -e
Type: bug
Target: pyautobrain
Repos:
- PyAutoBrain
Difficulty: small
Autonomy: supervised
Priority: high
Status: formalised
Filed: 2026-08-26
Regression in PyAutoBrain#292 (merge 475d292e0), filed the same day it shipped.
The weekly schedule fires Sunday 06:40, so it fails every week until fixed.
The bug
GitHub runs a run: block as /usr/bin/bash -e {0}. repo_settings.yml sets
set -uo pipefail and never disables -e. The per-repo read is:
before=$(gh api "repos/$slug" --jq '.delete_branch_on_merge' 2>/dev/null)
For an unreadable repo that assignment returns non-zero, so under -e the
script aborts instantly, and because stderr goes to /dev/null it aborts
silently. The case "$before" in ... *) unreadable handler is dead code in
production, and with it the whole in-org (error) vs out-of-org (warning)
failure-policy split that #292 was partly written to add.
Latent second instance: [ "$hard" -eq 1 ] && failed=1 is the last statement of
visit() in two branches. With hard=0 the compound returns 1, so the function
returns 1 — which under -e aborts on the first out-of-org repo.
The PATCH path is unaffected: if gh api ...; then is -e-safe.
Evidence (confirmed, not inferred)
- Run https://github.com/PyAutoLabs/PyAutoBrain/actions/runs/32992618192
(mode=audit) failed in 20s with no stdout at all between
##[endgroup]and##[error]Process completed with exit code 1. The log's own
env block printsshell: /usr/bin/bash -e {0}. - Reproduced locally: extract the
run:block, stubghso one repo read
fails.bash body.shprints the::warning::then::error::and exits 1
(graceful, as designed).bash -e body.shexits 1 with zero output —
byte-for-byte the CI signature.
Corollary: at least one repo read genuinely failed in that run. The fixed
version must name it; we still do not know which repo, and that is a second
finding hiding behind this one.
Why it shipped — the actual lesson
#292 was control-tested, across 7 scenarios, before merge. The harness ran
bash <script>; GitHub runs bash -e <script>. The harness did not match
production, so it validated a shell mode that never executes. A control test
that does not reproduce the caller's invocation proves nothing about the caller.
Sibling sweep — already done at filing
Audited every run: block in PyAutoBrain. branch_archive.yml and
branch_sweep_all.yml both explicitly set -e and use no 2>/dev/null;
abort-on-error is what they want, so they are consistent, not defective.
branch_sweep.yml, brain_board.yml, docs.yml, nightly-release.yml have no
matching pattern. repo_settings.yml is the only block with no e-flag
statement combined with a swallowed stderr. Scope is one file.
Fix
set +eimmediately afterset -uo pipefail, commented with why — GitHub's
default-ewould abort on the first unreadable repo and skip this script's
own per-repo reporting. Preferred over sprinkling|| true, because the
script's whole design is to survive per-repo failure and report it.- Both
[ "$hard" -eq 1 ] && failed=1occurrences becomeifblocks, so
visit()never returns non-zero spuriously. - Regression-proof it: a test that extracts the workflow's
run:block and
asserts the unreadable-repo path emits its::warning::and the right exit
code underbash -e, matching GitHub's invocation. This is the leg that
was missing. stdlib + PyYAML only, no network — stubghonPATH.
Acceptance
Re-dispatch repo_settings.yml in audit mode after merge. It must go green
and name any repo whose read fails. A green run that names nothing is only
acceptable if every one of the 46 reads genuinely succeeded.
Activity
Jammy2211 commentedon Aug 26, 2026
Shipped and verified — #294 merged (
8b48a93bd)CI green on both legs at
ceb425e0. Head auto-deleted on merge.Acceptance test passed, and it answered the open question. Re-dispatched
repo_settings.ymlinauditmode (run 32994175330) — green, and it named the repos the silence had been hiding:That is the whole failure explained end to end. The loop sweeps in-org repos first; in
auditmode with all 44 alreadytrueit prints nothing, so the run got 20s in with no output, reached the first out-of-org repo, and-ekilled it there. Both are personal-account repos thatPAT_PYAUTOLABScannot read — precisely the case the in-org/out-of-org split was written for, and precisely the case-emade invisible.Now-visible limitation, not a regression: those two repos are outside the sweep's reach. They are currently
true(set by hand on 2026-08-26), but nothing maintains them. Either widen the PAT to cover them, or accept them as manually-managed — a human decision, and now at least a visible one rather than a silent abort.The lesson, recorded: #292 was control-tested across seven scenarios and still shipped this, because the harness ran
bash <script>while GitHub runsbash -e <script>.tests/test_repo_settings_workflow.pynow runs the extracted block the way the runner does, and pins that assumption withtest_the_step_does_not_override_the_shell. The tests were verified to fail against the pre-fix workflow (3 failed / 3 passed) and pass against the fix.