Skip to content

feat(registry): read every gh account when building the repo map and discovering owners - #69

Merged
LeTuR merged 4 commits into
mainfrom
feat/read-every-gh-account
Sep 11, 2026
Merged

feat(registry): read every gh account when building the repo map and discovering owners#69
LeTuR merged 4 commits into
mainfrom
feat/read-every-gh-account

Conversation

@LeTuR

@LeTuR LeTuR commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Intent

Make the fleet control plane's repo map cover every gh account the machine holds, not only the active one, in both places that ask GitHub who the operator is.

The problem: 'gh api user/repos' (scripts/sync-registry.sh) and 'gh api user/orgs' (scripts/discover-owners.sh) answer for whichever gh account is ACTIVE. On a machine with three logins reaching disjoint sets of repositories, the generated map held one set at a time, and every owner the other logins reached produced 'warning: no accessible repos for owner ' — the SAME warning a mistyped owner produces, so the failure was indistinguishable from a typo. discover-owners.sh had the same blindness one level earlier and it cost more: its candidate list is the single question onboarding asks the operator, so four orgs were never offered, and an owner never offered never reaches the map.

The mechanism, verified on this machine: 'gh auth status --json hosts' lists every account for a host with a state and an active flag, and 'gh auth token --hostname --user ' hands over one account's token WITHOUT switching the active one.

Constraints and decisions:

  • Do NOT switch the operator's active gh account. Tokens are read by name; 'auth switch' is never called.
  • An account whose credential fails is reported by name and skipped, never fatal — one expired login must cost its own repos and not the whole map. But every account failing is a different thing and must NOT overwrite the operator's map with an empty one; the sync refuses there and leaves the file.
  • A repository two accounts both reach is ONE repository, deduped by full_name.
  • The 'no accessible repos for owner' warning MUST still be printed for an owner no account reaches: it is the typo signal, and the point of the change is that it now means only that.
  • Both scripts keep working with no jq-parseable account list at all — an older gh with no 'auth status --json', or a GH_TOKEN/GITHUB_TOKEN already in the environment. That falls back to the active session alone, the old behaviour.
  • The account enumeration is deliberately not inlined twice: a follow-up task (adding a gh or glab account after the first onboarding) is blocked on this one and will build on the seam, so it lives in a new sourced library scripts/lib/gh-accounts.sh, which also owns gh_api_as — scoping an account's token to the 'gh' call itself as a command prefix on an external command, deliberately not 'VAR=x some_shell_function' (bash leaves that set in the shell afterwards) and deliberately not a subshell (which would throw away the candidates discover-owners.sh collects in an associative array).
  • scripts/check.sh's shell check now runs 'shellcheck -x' and includes scripts/lib/*.sh, because without -x every source of the new library is an SC1091.
  • The new selftest sections were appended at the END of scripts/onboarding-selftest.sh rather than filed beside the existing section about the same script, so a concurrently-planned follow-up task's sections do not interleave with these in the diff.
  • No real account, org or host name from this operator goes into a fixture, a comment or a doc: this repository is public. All fixtures are generic.
  • registry/repos.generated.yaml stays generated and gitignored.

Verification: the selftest sections were written first and watched to fail for the right reason, then the scripts were fixed. Offline with a stubbed gh and no network the selftest covers: every owner any account reaches present in the map, the typo warning still printed for an owner nothing reaches and for no other owner, a repo two accounts both reach counted once, the totals counting the merged set, no account switching, an expired credential and an unreadable token both named and skipped without aborting, every account failing refusing rather than erasing the map, and an older gh with no --json still writing a map from the active account alone. On the real machine: the map went from 67 repos across 2 owners with two spurious unreachable warnings to 90 repos across 4 owners with none, with the active gh account unchanged; discovery went from 4 candidates to 10. ./scripts/check.sh is green.

A previous run of this pipeline failed at the document step because its own agent hit a usage limit, not because of a finding; its review fix commit is already on the branch.

What Changed

  • New sourced library scripts/lib/gh-accounts.sh enumerates every gh login for a host from gh auth status --json hosts, reads each one's token BY NAME with gh auth token --user (never gh auth switch), and runs a call as that account by putting the token on the gh command itself so nothing leaks into the calling shell. An account whose state is not success is named on stderr and skipped; with no jq-parseable account list at all — an older gh, or GH_TOKEN/GITHUB_TOKEN already set — it prints nothing and callers fall back to the active session, the previous behaviour.
  • scripts/sync-registry.sh now runs one user/repos listing per account and merges them deduped by full_name, so a repo two logins both reach is counted once. A token it cannot read or a listing that fails is named and skipped rather than fatal; if not one account could be listed it dies and leaves registry/repos.generated.yaml untouched instead of writing an empty map. scripts/discover-owners.sh folds each login's gh api user and user/orgs into the candidate list, gates on command -v gh instead of gh auth status (which exits non-zero when any account anywhere has issues), names a login that cannot be read, and defers the read:org scope hint until every account has answered. The no accessible repos for owner warning is unchanged and now signals only a typo or an owner no login can see.
  • scripts/onboarding-selftest.sh gains §4 and §5, appended at the end of the file, driving both scripts offline against a stubbed gh in a sandbox repo root: every owner any account reaches present in the map, the unreachable warning printed for the typo owner and no other, the shared repo counted once, merged totals, an expired credential and an unreadable token both named without aborting, all accounts failing refusing without erasing the map, an older gh writing from the active account alone, and gh auth switch wired as a tripwire. scripts/check.sh switches the shell check to shellcheck -x over scripts/*.sh scripts/lib/*.sh; AGENTS.md, README.md, CONTRIBUTING.md and the fleet-onboarding skill describe the multi-login read.

Risk Assessment

✅ Low: The change is well-bounded to two scripts plus a new sourced seam, every intent constraint is source-verifiable and satisfied, the one destructive failure mode (overwriting the operator's map with an empty one) is explicitly guarded in both code paths and covered by an offline regression test, and tokens are read by name without export, logging, or any switch of the active account.

Testing

The configured ./scripts/check.sh baseline had already passed; on top of it I drove the two changed scripts as an operator runs them, live against real GitHub on a machine that genuinely holds three gh logins, and compared each run against the base commit's version of the same script. The map grew from 29 repos across 1 owner (with two real owners reported by the same warning a typo produces) to 52 repos across 3 owners with only the typo owner warned, and the candidate list grew from 3 to 10 — with the active gh account and hosts.yml unchanged after every run. Adversarial runs injected faults at the network through a local CONNECT proxy and through an isolated gh config carrying one lapsed login: total listing failure refuses and leaves the map byte-identical, partial failure names the account and still publishes the rest, a login that cannot say who it is is named and non-fatal, and the lapsed login no longer costs discovery every healthy account (base printed a false "gh is not authenticated" and zero candidates, HEAD printed all ten). Two scenarios were not driven live at all: dedup across two accounts that both reach one repository, and the unreadable-token / older-gh-without---json fallbacks. These three logins reach strictly disjoint repo sets, and producing either case would mean writing to the operator's real GitHub org or altering their gh credentials and installed gh binary, so both are reported untested — the offline selftest exercises them with a stubbed gh, which is not a live result. This change has no UI surface; CLI transcripts are the reviewer-visible evidence, and all of them are redacted because this repository is public and the logins and orgs are the operator's own.

  • Live validation: ✅ go - 9 of 11 scenarios driven live against the product
Scenario Result Live Evidence
An operator syncs the map on a machine with three gh logins and gets every owner any login reaches, not just the active one's ✅ pass live ./scripts/sync-registry.sh against real GitHub: base commit wrote 29 repos across 1 owner, HEAD wrote 52 repos across 3 owners — sync-registry-live.txt
The "no accessible repos for owner" warning now means only a typo: printed for the bogus owner, for no owner some account reaches ✅ pass live Same live run: base warned on two real owners plus the typo, HEAD warned on 'no-such-owner-typo-xyz' alone — sync-registry-live.txt
Reading every account does not switch the operator's active gh account or touch its credential store ✅ pass live gh auth status --json hosts active login and md5sum ~/.config/gh/hosts.yml identical before and after repeated sync and discovery runs — sync-registry-live.txt / discover-owners-live.txt
Onboarding's candidate list offers the orgs only a non-active login can see ✅ pass live ./scripts/discover-owners.sh <empty-root>: 3 candidates at base commit, 10 at HEAD (two further logins and their orgs) — discover-owners-live.txt
A login that has lapsed on another host does not cost discovery every healthy login ✅ pass live Isolated GH_CONFIG_DIR with the three real logins plus one unusable login elsewhere: plain gh auth status exits 1; base printed "No candidate owners found" and a false "gh is not authenticated", HEA…
An account that cannot say who it is is named by name, skipped, and never fatal to discovery ✅ pass live Two of three logins' gh api user calls refused at the network: both warned by name, the healthy login's candidates still printed, exit 0 — adversarial-live.txt §D
One account's listing failing costs only that account's repos, and the map is still written from the rest ✅ pass live Only one listing allowed through the fault-injecting proxy: the other two accounts named, map written with 29 repos across 1 owner, exit 0 — adversarial-live.txt §B
Every account failing refuses rather than overwriting the operator's map with an empty one ✅ pass live All three real accounts listed but every listing refused: error naming all 3 accounts, exit 1, map md5 byte-identical to before, no "wrote ..." line — adversarial-live.txt §A
A GH_TOKEN already in the environment falls back to that one session, the pre-change behaviour ✅ pass live GH_TOKEN=<non-active login token> on both scripts: map written from that session alone (20 repos / 1 owner) with the unreachable owners warned again, discovery lists only that login and its orgs — e…
A repository two accounts both reach is counted once in the map ⏸️ untested no Never driven against the live product: the only evidence gathered was ./scripts/onboarding-selftest.sh §4c/§4d, which runs the sync script against a stubbed gh, so no live result was established. It…
An account whose credential is expired or whose token cannot be read is named and skipped, and an older gh without auth status --json still writes a map from the active account alone ⏸️ untested no Never driven against the live product: the only evidence gathered was ./scripts/onboarding-selftest.sh §4f/§4g/§5c/§5d, which drives the scripts against a stubbed gh, so no live result was establish…
Evidence: sync-registry driven live: base (29 repos / 1 owner, two false warnings) vs HEAD (52 repos / 3 owners, typo only)
# Live drive on a machine with three real gh logins (names redacted; this repo is public)

$ gh auth status --json hosts | jq -c ".hosts[\"github.com\"] | map({login,state,active})"
[{"login":"<login-a>","state":"success","active":true},{"login":"<login-b>","state":"success","active":false},{"login":"<login-c>","state":"success","active":false}]

$ cat registry/owners.txt   # three owners, one per account, plus a deliberate typo
<org-a>
<org-b>
<org-c>
no-such-owner-typo-xyz

=== BEFORE (base commit e559816) — sync-registry.sh asks the ACTIVE account only ===
$ ./scripts/sync-registry.sh
fetching every accessible repository ...
warning: no accessible repos for owner '<org-b>'
warning: no accessible repos for owner '<org-c>'
warning: no accessible repos for owner 'no-such-owner-typo-xyz'
wrote <tmp>/base-sandbox/registry/repos.generated.yaml (29 repos across 1 owners)

  ^ two REAL owners reported with the very same warning the typo produces

=== AFTER (HEAD a1480c7) — every gh account is asked ===
$ ./scripts/sync-registry.sh
fetching every accessible repository ...
warning: no accessible repos for owner 'no-such-owner-typo-xyz'
wrote /home/<home>/.no-mistakes/worktrees/d1be5edfebb8/01M282PWN4E0T1VF5BM7K5T9J1/registry/repos.generated.yaml (52 repos across 3 owners)

$ grep -E "^  - name: |^  repos: |^  owners: " registry/repos.generated.yaml
  - name: <org-a>
  - name: <org-b>
  - name: <org-c>
  repos: 52
  owners: 3

=== the active gh account was not switched ===
$ gh auth status --json hosts | jq -r ".hosts[\"github.com\"][] | select(.active) | .login"
<login-a>
Evidence: discover-owners driven live: 3 candidates before, 10 after, active account unchanged
# discover-owners.sh driven live against the machine's three real gh logins
# (clone scan pointed at an empty root so only the gh evidence differs; names redacted)

=== BEFORE (base commit e559816) — only the ACTIVE login is asked ===
$ ./scripts/discover-owners.sh <empty-root>
CANDIDATE OWNERS — for registry/owners.txt, which is a list of GITHUB owners

    <login-a>                  gh account
    <user-n>               gh org
  * <org-a>               gh org

  * already in registry/owners.txt

=== AFTER (HEAD a1480c7) — every login is asked ===
$ ./scripts/discover-owners.sh <empty-root>
CANDIDATE OWNERS — for registry/owners.txt, which is a list of GITHUB owners

    <login-a>                  gh account
    <user-n>               gh org
  * <org-a>               gh org
    <login-b> gh account
  * <org-b>             gh org
    <login-c>    gh account
    <org-d>           gh org
  * <org-c>          gh org
    <org-c-test>     gh org
    <org-e>      gh org

  * already in registry/owners.txt

  3 candidates -> 10. The seven new ones are the two other logins and their orgs;
  an owner never offered never reaches the map.

=== the active gh account was not switched by discovery either ===
<login-a>
Evidence: Adversarial live runs: total listing failure refuses and leaves the map byte-identical; partial failure names the account; a lapsed login no longer wipes discovery; two logins failing are named and non-fatal
# Adversarial live runs — real gh, real GitHub, faults injected at the network
# (a local CONNECT proxy allows the account-list call and refuses the listings)

--- the operator's map, as it stands
  repos: 52
  owners: 3
md5 2a49c535e90f19a7c32276a4aecca258

=== A. EVERY account's listing fails (outage / offline) — must refuse, not erase ===
$ ./scripts/sync-registry.sh
fetching every accessible repository ...
Get "https://api.github.com/user/repos?per_page=100&affiliation=owner,collaborator,organization_member": Service Unavailable
warning: could not list repositories for gh account '<login-a>' — its repositories are not in this map
Get "https://api.github.com/user/repos?per_page=100&affiliation=owner,collaborator,organization_member": Service Unavailable
warning: could not list repositories for gh account '<login-b>' — its repositories are not in this map
Get "https://api.github.com/user/repos?per_page=100&affiliation=owner,collaborator,organization_member": Service Unavailable
warning: could not list repositories for gh account '<login-c>' — its repositories are not in this map
error: not one of the 3 gh accounts above could be listed.
       /home/<home>/.no-mistakes/worktrees/d1be5edfebb8/01M282PWN4E0T1VF5BM7K5T9J1/registry/repos.generated.yaml is left exactly as it was rather than
       overwritten with an empty map.
exit=1
map now:   repos: 52 / md5 2a49c535e90f19a7c32276a4aecca258
MAP BYTE-IDENTICAL — nothing was published over it

=== B. ONE account lists, the other two fail — named, skipped, map still written ===
$ ./scripts/sync-registry.sh
fetching every accessible repository ...
Get "https://api.github.com/user/repos?per_page=100&affiliation=owner,collaborator,organization_member": Service Unavailable
warning: could not list repositories for gh account '<login-b>' — its repositories are not in this map
Get "https://api.github.com/user/repos?per_page=100&affiliation=owner,collaborator,organization_member": Service Unavailable
warning: could not list repositories for gh account '<login-c>' — its repositories are not in this map
warning: no accessible repos for owner '<org-b>'
warning: no accessible repos for owner '<org-c>'
warning: no accessible repos for owner 'no-such-owner-typo-xyz'
wrote /home/<home>/.no-mistakes/worktrees/d1be5edfebb8/01M282PWN4E0T1VF5BM7K5T9J1/registry/repos.generated.yaml (29 repos across 1 owners)
exit=0
  - name: <org-a>
  repos: 29
  owners: 1

=== C. a login that has LAPSED on another host — discovery must not lose the healthy ones ===
(isolated GH_CONFIG_DIR: the three real github.com logins plus one unusable login on another host)
$ gh auth status ; echo $?
gh auth status exit=1   <- the gate the old code trusted
$ ./scripts/discover-owners.sh   # BASE commit e559816
No candidate owners found.

  gh is not authenticated, so the account and its orgs could not be read: gh auth login
  Nothing on this machine names a GitHub owner: no gh session, no
  github.user, and no clone with a github.com remote under the roots
  scanned. Name a root to scan, or write registry/owners.txt by hand
  from registry/owners.example.txt.
$ ./scripts/discover-owners.sh   # HEAD a1480c7
CANDIDATE OWNERS — for registry/owners.txt, which is a list of GITHUB owners

    <login-a>                  gh account
    <user-n>               gh org
  * <org-a>               gh org
    <login-b> gh account
  * <org-b>             gh org
    <login-c>    gh account
    <org-d>           gh org
  * <org-c>          gh org
    <org-c-test>     gh org
    <org-e>      gh org

  * already in registry/owners.txt

=== D. two of three logins cannot say who they are — named by name, never fatal ===
$ ./scripts/discover-owners.sh
warning: gh account '<login-b>' could not say who it is — its orgs are not below
warning: gh account '<login-c>' could not say who it is — its orgs are not below
CANDIDATE OWNERS — for registry/owners.txt, which is a list of GITHUB owners

    <login-a>                  gh account
    <user-n>               gh org
  * <org-a>               gh org

  * already in registry/owners.txt
exit=0
Evidence: GH_TOKEN environment fallback: both scripts ask that one session alone, the pre-change behaviour
# The documented fallback: a GH_TOKEN/GITHUB_TOKEN already in the environment
# overrides every stored account, so both scripts ask that ONE session — the old behaviour.

$ GH_TOKEN=<a non-active login's token> ./scripts/sync-registry.sh
fetching every accessible repository ...
warning: no accessible repos for owner '<org-a>'
warning: no accessible repos for owner '<org-b>'
warning: no accessible repos for owner 'no-such-owner-typo-xyz'
wrote /home/<home>/.no-mistakes/worktrees/d1be5edfebb8/01M282PWN4E0T1VF5BM7K5T9J1/registry/repos.generated.yaml (20 repos across 1 owners)
exit=0
  - name: <org-c>
  repos: 20
  owners: 1

  ^ one session's answer only, and the owners it cannot reach are warned about again,
    exactly as before this change.

$ GH_TOKEN=<same token> ./scripts/discover-owners.sh <empty-root>
CANDIDATE OWNERS — for registry/owners.txt, which is a list of GITHUB owners

    <login-c>    gh account
    <org-d>           gh org
  * <org-c>          gh org
    <org-c-test>     gh org
    <org-e>      gh org

  * already in registry/owners.txt
exit=0

--- the active account, after every run above
<login-a>
Evidence: Offline onboarding selftest §4/§5 (dedup, expired credential, unreadable token, older gh without --json) — stubbed gh, not a live run

§1 preflight — the table is honest, and every gap carries a remedy
  ok    1a a fully equipped machine exits 0
  ok    1a it says so in one line
  ok    1a --commands prints nothing when there is nothing to install
  ok    1b a missing required tool is a non-zero exit
  ok    1b it names gh
  ok    1b and jq in the same pass
  ok    1b gh is named too
  ok    1b it says how many are missing
  ok    1b every gap carries an install line
  ok    1b --commands hands over the lines to run
  ok    1b --commands never prints a 'see <url>' as if it were a command
  ok    1e --tier required leaves the recommended lines out
  ok    1e and a filtered view still fails on a required gap
  ok    1e --tier gate shows only that tier
  ok    1c a thurbox below the floor fails preflight
  ok    1c it is reported stale, not missing
  ok    1c and the remedy names the floor from the manifest
  ok    1d a missing recommended or gate tool is reported, never fatal
  ok    1d quota-axi is named
  ok    1d and it says what degrades without it
  ok    1d the gate tools are their own tier
  ok    1f signing on with no key is reported
  ok    1f and it names what it costs
  ok    1f with a remedy that is either half of the fix
  ok    1f the signing row is reported at all
  ok    1f signing with a key is not reported as a gap

§2 discover-owners — three sources, and GitLab is not one of them
  ok    2a discovery exits 0 when it found something
  ok    2a the gh account is a candidate
  ok    2a so are its orgs
  ok    2a each candidate carries its evidence
  ok    2b an ssh host ALIAS clone is found
  ok    2b with its clone count
  ok    2b a plain https remote too
  ok    2b an ssh:// URL with a port names the owner, not the port
  ok    2b a fork's origin names its owner
  ok    2d a GitLab remote is reported as evidence
  ok    2d under the host it lives on
  ok    2b a port is never an owner
  ok    2b a fork's upstream is not an owner the operator has repos under
  ok    2b a vendored vim plugin's author is not an owner
  ok    2b nor is an npm package's
  ok    2d a GitLab namespace never reaches the candidate list
  ok    2e a machine with no gh session still discovers owners
  ok    2e the noreply commit email names the account
  ok    2e and the missing gh session is said out loud
  ok    2f a machine that says nothing exits 1 rather than inventing an owner
  ok    2f and points at the file to write by hand

§3 place-pane — safe, idempotent, or refused
  ok    3a an unplaced pane is exit 1 from --check
  ok    3a and it says what that costs
  ok    3a --dry-run exits 0
  ok    3a --dry-run names the file it would edit
  ok    3a --dry-run changed nothing
  ok    3a placing it exits 0
  ok    3a it says which side it chose
  ok    3a and --check now agrees
  ok    3b the default places the column to the RIGHT of the terminal
  ok    3c the block carries the panels.shown guard
  ok    3c and the slot the pane declares
  ok    3c a pane declaring another slot places exit 0
  ok    3c the block carries the slot the PANE declares
  ok    3c and never one the writer spells itself
  ok    3d a second run exits 0
  ok    3d and says it changed nothing
  ok    3d the file is untouched
  ok    3e the edit left a backup beside the original
  ok    3f --left exits 0
  ok    3f --left places the column before the terminal
  ok    3g an unrecognised arrangement is refused
  ok    3g and the block is printed for the operator instead
  ok    3g the file was not touched
  ok    3h a missing layout.lua exits 2 and says thurbox writes one
  ok    3h with the reason
  ok    3j a layout missing the helpers the block calls is refused
  ok    3j and it names the one it could not find
  ok    3j the file was not touched
  ok    3k a commented-out block is not a placement
  ok    3k and --check says what that costs
  ok    3k placing it over the comment exits 0
  ok    3k the file now carves exactly one live column for the slot
  ok    3i the edited layout still parses as Lua

§4 sync-registry — the map covers every gh account, not just the active one
  ok    4a a sync across five accounts exits 0
  ok    4a the map carries owner 'octo', reached by one of the accounts
  ok    4a the map carries owner 'acme-org', reached by one of the accounts
  ok    4a the map carries owner 'client-org', reached by one of the accounts
  ok    4a the map carries owner 'employer-org', reached by one of the accounts
  ok    4b an owner no account reaches is still reported
  ok    4b 'octo' is not reported unreachable — some account reaches it
  ok    4b 'acme-org' is not reported unreachable — some account reaches it
  ok    4b 'client-org' is not reported unreachable — some account reaches it
  ok    4b 'employer-org' is not reported unreachable — some account reaches it
  ok    4c a repo two accounts both reach appears once
  ok    4d the totals count every merged repo
  ok    4d across every owner that resolved
  ok    4f an expired credential is named
  ok    4f a login whose token cannot be read is named too
  ok    4f and the map was still written
  ok    4g a gh without --json still writes a map
  ok    4g from the active account alone
  ok    4g and the owners it cannot reach are back to being warned about
  ok    4h every account failing refuses instead of publishing
  ok    4h the map that was already there survives untouched
  ok    4h and nothing claims a map was written

§5 discover-owners — every login is asked, not only the active one
  ok    5a discovery across several logins exits 0
  ok    5a the active account is a candidate
  ok    5a so is a login that is not active
  ok    5a and so is the third
  ok    5b the active login's orgs are there
  ok    5b and the orgs only another login can see
  ok    5b including the third login's
  ok    5c an expired login is not offered as a candidate
  ok    5c but it is reported
  ok    5e a login that cannot say who it is is named
  ok    5e and it is not offered as a candidate
  ok    5f a login with no orgs is still a candidate
  ok    5f but an otherwise complete answer carries no scope advice
  ok    5d a gh without --json still discovers
  ok    5d from the active session alone
  ok    5d with its orgs

onboarding selftest: everything passed
Evidence: The map before and after the change, on the real machine
BASE e559816: wrote registry/repos.generated.yaml (29 repos across 1 owners)
warning: no accessible repos for owner '<org-b>' <- a REAL owner
warning: no accessible repos for owner '<org-c>' <- a REAL owner
warning: no accessible repos for owner 'no-such-owner-typo-xyz'

HEAD a1480c7: wrote registry/repos.generated.yaml (52 repos across 3 owners)
warning: no accessible repos for owner 'no-such-owner-typo-xyz' <- the typo, and only the typo

active gh account before = <login-a>, after = <login-a>; ~/.config/gh/hosts.yml md5 unchanged

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ scripts/onboarding-selftest.sh:601 - The new §4 and §5 sections do not scrub the gh environment they depend on, so ./scripts/check.sh onboarding fails on an ordinary developer machine and silently exercises the wrong path where it does not fail. gh_accounts short-circuits on two inherited variables (scripts/lib/gh-accounts.sh:40 [ -z &#34;${GH_TOKEN:-}${GITHUB_TOKEN:-}&#34; ] || return 0, and scripts/sync-registry.sh:67 / scripts/discover-owners.sh:140 derive the host from GH_HOST), but lines 601, 669, 701, 780 and 820 override only PATH, HOME and FIXTURES. Concrete sequence with GH_TOKEN exported (a common setup, and one the intent itself names as a supported fallback): gh_accounts returns nothing, sync-registry takes the active-session branch, the §4 stub evaluates ${GH_TOKEN:-tok-octo} to the operator's real token, hits its *) exit 1 arm, set -e aborts the sync, and §4a reports exit != 0 with §4d/§4f failing on fixture-unrelated errors — while §4g and §5d still pass, but for the wrong reason (they are exercising the GH_TOKEN short-circuit, not the gh too old for --json path they claim to cover). GH_HOST=&lt;self-hosted&gt; produces the same divergence via the .hosts[$h] filter. Remedy: unset GH_TOKEN GITHUB_TOKEN GH_HOST once at the same boundary the file already clears the git environment for exactly this reason (scripts/onboarding-selftest.sh:66-71), and have §4g/§5d set GH_TOKEN explicitly if they want to cover that half of the fallback.
  • ℹ️ scripts/discover-owners.sh:88 - The comment asserts "The token never reaches this shell: gh_api_as scopes it to the gh call itself", but line 150 stores it in the caller's shell: tok=&#34;$(gh_account_token &#34;$gh_host&#34; &#34;$acct&#34;)&#34;, and ask_gh then holds it again in local token. The load-bearing and accurate part of the claim is that no assignment persists after the call and no subshell is needed (which is why note's associative array survives); the absolute "never reaches this shell" is wrong and, on a security-adjacent property, is the kind of comment AGENTS.md's shell guidance calls worse than none. Remedy: narrow the sentence to what is true — the token is never exported and the assignment lives exactly as long as the one gh call.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 9 of 11 scenarios driven live against the product
Scenario Result Live Evidence
An operator syncs the map on a machine with three gh logins and gets every owner any login reaches, not just the active one's ✅ pass live ./scripts/sync-registry.sh against real GitHub: base commit wrote 29 repos across 1 owner, HEAD wrote 52 repos across 3 owners — sync-registry-live.txt
The "no accessible repos for owner" warning now means only a typo: printed for the bogus owner, for no owner some account reaches ✅ pass live Same live run: base warned on two real owners plus the typo, HEAD warned on 'no-such-owner-typo-xyz' alone — sync-registry-live.txt
Reading every account does not switch the operator's active gh account or touch its credential store ✅ pass live gh auth status --json hosts active login and md5sum ~/.config/gh/hosts.yml identical before and after repeated sync and discovery runs — sync-registry-live.txt / discover-owners-live.txt
Onboarding's candidate list offers the orgs only a non-active login can see ✅ pass live ./scripts/discover-owners.sh &lt;empty-root&gt;: 3 candidates at base commit, 10 at HEAD (two further logins and their orgs) — discover-owners-live.txt
A login that has lapsed on another host does not cost discovery every healthy login ✅ pass live Isolated GH_CONFIG_DIR with the three real logins plus one unusable login elsewhere: plain gh auth status exits 1; base printed "No candidate owners found" and a false "gh is not authenticated", HEA…
An account that cannot say who it is is named by name, skipped, and never fatal to discovery ✅ pass live Two of three logins' gh api user calls refused at the network: both warned by name, the healthy login's candidates still printed, exit 0 — adversarial-live.txt §D
One account's listing failing costs only that account's repos, and the map is still written from the rest ✅ pass live Only one listing allowed through the fault-injecting proxy: the other two accounts named, map written with 29 repos across 1 owner, exit 0 — adversarial-live.txt §B
Every account failing refuses rather than overwriting the operator's map with an empty one ✅ pass live All three real accounts listed but every listing refused: error naming all 3 accounts, exit 1, map md5 byte-identical to before, no "wrote ..." line — adversarial-live.txt §A
A GH_TOKEN already in the environment falls back to that one session, the pre-change behaviour ✅ pass live GH_TOKEN=&lt;non-active login token&gt; on both scripts: map written from that session alone (20 repos / 1 owner) with the unreachable owners warned again, discovery lists only that login and its orgs — e…
A repository two accounts both reach is counted once in the map ⏸️ untested no Never driven against the live product: the only evidence gathered was ./scripts/onboarding-selftest.sh §4c/§4d, which runs the sync script against a stubbed gh, so no live result was established. It…
An account whose credential is expired or whose token cannot be read is named and skipped, and an older gh without auth status --json still writes a map from the active account alone ⏸️ untested no Never driven against the live product: the only evidence gathered was ./scripts/onboarding-selftest.sh §4f/§4g/§5c/§5d, which drives the scripts against a stubbed gh, so no live result was establish…
  • ./scripts/check.sh
  • ./scripts/sync-registry.sh against real GitHub with three live gh logins and an owners.txt spanning one owner per account plus a deliberate typo owner
  • git show e559816:scripts/sync-registry.sh run in a sandbox root against the same real accounts, to reproduce the pre-change one-account map
  • ./scripts/discover-owners.sh &lt;empty-root&gt; at HEAD vs the base-commit copy, clone scan pointed at an empty directory so only the gh evidence differs
  • HTTPS_PROXY=&lt;fault-injecting CONNECT proxy&gt; ./scripts/sync-registry.sh with every repo listing refused (total-failure refusal) and with only one listing allowed (partial failure)
  • HTTPS_PROXY=&lt;fault-injecting CONNECT proxy&gt; ./scripts/discover-owners.sh with two of three logins failing their gh api user call
  • GH_CONFIG_DIR=&lt;isolated copy + one lapsed login on another host&gt; driving both the base and HEAD discover-owners.sh, with gh auth status exiting 1
  • GH_TOKEN=&lt;non-active login token&gt; ./scripts/sync-registry.sh and ./scripts/discover-owners.sh (documented environment-token fallback)
  • md5sum ~/.config/gh/hosts.yml and the active flag from gh auth status --json hosts before and after every run (no account switching)
  • ./scripts/check.sh onboarding and ./scripts/onboarding-selftest.sh (offline; §4 and §5 cover dedup, unreadable token, expired credential, older gh without --json)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…vering owners

`gh api user/repos` and `gh api user/orgs` answer for whichever account is
ACTIVE. On a machine with several logins reaching disjoint sets, the map held
one set at a time and every owner the other logins reached produced `warning:
no accessible repos for owner '<x>'` — the same warning a mistyped owner
produces, so the two were indistinguishable. Discovery had it one level
earlier and it cost more: the candidate list is the single question onboarding
asks the operator, and an owner never offered never reaches the map at all.

Both scripts now enumerate every account for the host and ask each one with
its own token, through the new `scripts/lib/gh-accounts.sh`. Tokens are read
by name with `gh auth token --user`, so nothing switches the account the
operator's `gh` points at. A repo two accounts both reach is one repo. An
account whose credential fails is named and skipped. No account list — an old
`gh` with no `auth status --json`, or a `GH_TOKEN` already in the environment —
falls back to the active session alone, which is the old behaviour.

`scripts/check.sh shell` now runs `shellcheck -x` and covers `scripts/lib/*.sh`.
@LeTuR
LeTuR merged commit 154c925 into main Sep 11, 2026
11 checks passed
@LeTuR
LeTuR deleted the feat/read-every-gh-account branch September 11, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant