Skip to content

feat(onboarding): decide auth per account and per host, and add owners after the first run - #71

Merged
LeTuR merged 3 commits into
mainfrom
feat/add-things-after-onboarding
Sep 11, 2026
Merged

feat(onboarding): decide auth per account and per host, and add owners after the first run#71
LeTuR merged 3 commits into
mainfrom
feat/add-things-after-onboarding

Conversation

@LeTuR

@LeTuR LeTuR commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Intent

Fleet's setup only answered honestly on a first run, and only told the truth about credentials it happened to find on the default host. Two halves, from a real complaint on this operator's machine.

Half one: preflight reported a working setup as broken, twice, both times by reading an all-or-nothing exit code as an answer about one credential. glab auth status exits non-zero when ANY configured instance fails, so this operator — authenticated to gitlab.metyis.technology where 32 of their clones live, and not to gitlab.com which they never use — read "missing glab auth" beside a remedy they had already run. That contradicts fleet's own forge seam (scripts/lib/forge.py), where which hosts a CLI owns comes from that CLI's own variable and a self-hosted instance is the ORDINARY case rather than a special one. gh auth status has the identical shape: with three gh accounts, one expired token makes it exit non-zero, and gh auth is a REQUIRED row, so one lapsed credential of three reported the whole setup as unrunnable.

The decision: make each row go through a seam rather than the CLI's exit code. gh auth reuses scripts/lib/gh-accounts.sh, which an earlier task landed for the registry and discovery — explicitly reusing that seam rather than writing a second enumeration, as the brief required — and reads "N of M accounts", passing the seam's own stderr warning about a skipped login through verbatim rather than swallowing it (an account that goes missing in silence is the failure that file exists for). glab auth gets a new counterpart, scripts/lib/glab-hosts.sh, which asks per host: GITLAB_HOST decides when set (glab's own variable, so a host named there is the one that must work), otherwise one working credential among the configured instances is enough and the row names which. Both keep their old single-answer path as the documented fallback for a CLI too old to enumerate — that keeps the floor exactly where it was. glab's mechanics were verified against the real glab 1.117.0 on this machine before anything was written.

Half two: onboarding is a seven-step FIRST run that converges on a re-run, but there was no path for the thing that actually happens later — the operator gains an owner, a repo, or a whole gh/glab account and wants the map and the checks to catch up. New scripts/add-owner.sh names the owners the current gh accounts reach that registry/owners.txt does not list, grouped BY THE ACCOUNT THAT REACHES THEM, because after a gh auth login that is the shape of the question. With --all or a named list it appends them, then syncs and reports what MOVED — owners added, repositories gained or lost, totals before and after — rather than printing the whole map.

Constraints that were deliberate and may look surprising in the diff:

  • Nothing installs a credential or logs anybody in. gh auth login and glab auth login are interactive and the operator's; this only reports, offers and syncs.
  • registry/owners.txt's comment header and its ENTRY ORDER are preserved on every write, because the order decides the generated map's order. Appending is right; reshuffling is churn. A duplicate is refused and nothing is written — including a name given twice on one command line.
  • What counts as an owner is NOT widened. registry/owners.txt holds GitHub owners read by gh. A GitLab group is not one, so an authenticated GitLab host is reported as evidence (it changes what preflight says and what a task can target through the forge seam) and never contributes an owner. A name containing anything but [A-Za-z0-9-] is refused.
  • add-owner.sh refuses a clone with no owners file and points back at discover-owners.sh: a first run belongs to the fleet-onboarding skill, where candidates come from three sources rather than one.
  • registry/owners.txt and registry/repos.generated.yaml are gitignored and stay that way; nothing hand-writes the generated map.
  • A newly authenticated account's OWN login is offered as an owner alongside its orgs. That is deliberate and matches discover-owners.sh, which counts a login as a candidate for the same reason — an early test expectation that assumed only the org was wrong and was corrected, not the code.

Verification: the gate is ./scripts/check.sh and it is green end to end, including the sync check that arrived on main during this work. The new behaviour is covered in scripts/onboarding-selftest.sh sections 6, 7 and 8, which run OFFLINE against stubbed gh and glab on a PATH built from scratch — no network, no live gh/glab, no touching the operator's real config or map, which is that selftest's standing rule. The three glab cases the brief named are all covered (self-hosted-only reads ok and names the host; GITLAB_HOST pointing at a host with no credential reads missing with a remedy naming that host; nothing authenticated reads missing), plus the one-expired-token gh case and the incremental add on a machine that already has owners and a map. Both halves were also verified against this operator's real machine: the glab row now reads "ok glab auth gitlab.metyis.technology" and the gh row "3 of 3 accounts", and add-owner.sh's report was driven against their real accounts in a scratch root so nothing of theirs was written.

Documentation: the fleet-onboarding skill's Re-running section was EXTENDED rather than given an eighth step, because this is not a step of the first run — that was an explicit instruction. AGENTS.md and README.md name the new script and the two seam changes.

Note on history: a first pipeline run failed mid-review when its own agent hit a usage limit. Its preserved head was my commit rebased onto a newer main and carried no pipeline fix commits, so nothing was lost; the branch has since been rebased onto that same base by hand and re-gated.

What Changed

  • scripts/preflight.sh's two authentication rows no longer read a CLI's all-or-nothing exit code as an answer about one credential. gh auth now goes through the existing scripts/lib/gh-accounts.sh seam and reports "N of M accounts" with the logins named, passing that seam's warning about a skipped login through to stderr verbatim; glab auth goes through a new scripts/lib/glab-hosts.sh, which answers per host — GITLAB_HOST decides when set (and its remedy names that host), otherwise one working credential among glab auth status --all's instances reads ok and names it. Both rows keep the bare status call as the documented fallback for a CLI that cannot enumerate.
  • New scripts/add-owner.sh covers the incremental case onboarding had no path for: it lists the owners the current gh accounts reach that registry/owners.txt does not, grouped by the account that reaches them, and with --all or named owners appends them, runs the registry sync, and reports what moved (owners added, repositories gained or lost, totals before and after). It writes preserving the file's comment header and entry order, refuses duplicates and non-[A-Za-z0-9-] names without writing, refuses a clone with no owners file and points back at discover-owners.sh, logs nobody in, and treats an authenticated GitLab host as reported evidence rather than an owner.
  • scripts/onboarding-selftest.sh gains sections 6, 7 and 8 covering the glab-per-host row, the gh-per-account row (including one expired token of three and the no-enumeration fallback) and add-owner.sh, all offline against stubbed gh/glab on a PATH built from scratch. check.sh's header, AGENTS.md, README.md and the fleet-onboarding skill's Re-running section document the new script and the two seams.

Risk Assessment

✅ Low: The change is well-bounded operator tooling with no production or data path, every intent constraint is implemented and covered by offline selftests that fail for the right reason, and the two remaining issues are a cosmetic input-validation gap and a test that is weaker than its label.

Testing

The configured gate ./scripts/check.sh had already passed as baseline. On top of that I drove both halves live on the reporting operator's own machine: base-commit preflight reproduces missing glab auth and a single-account gh auth row, and the target commit turns those into ok glab auth gitlab.metyis.technology and ok gh auth 3 of 3 accounts, with the GITLAB_HOST decider exercised in both directions against the real instances and both rows shown to still fail when they should (empty gh config dir → missing gh auth, exit 1). add-owner.sh was driven end to end against the real accounts in a scratch root — report, append plus real sync with a what-moved summary, and every refusal path including --all with no account answering — with the files md5-checked as untouched after each refusal. The repo's own offline sections 6–8 were run as coverage, and three mutations (reworded totals line, dropped --paginate, GITLAB_HOST removed from the selftest's environment boundary) confirm that coverage fails when the behaviour regresses — but an offline selftest section is not a live drive, so the mixed-token N of M case that only sections 7a/7b cover is reported untested rather than passed. Two scenarios remain undriven: that mixed multi-account token state, and an account belonging to more than 30 orgs. This is a CLI-only change, so the reviewer-visible evidence is CLI transcripts rather than screenshots. Temp roots were removed and the worktree is clean.

  • Live validation: ✅ go - 15 of 17 scenarios driven live against the product
Scenario Result Live Evidence
A self-hosted-only GitLab operator runs preflight and is no longer told their working setup is broken ✅ pass live /tmp/fleet-base/scripts/preflight.sh (base 209a66c) prints missing glab auth; ./scripts/preflight.sh on the target prints ok glab auth gitlab.metyis.technology — same machine, same real glab 1…
GITLAB_HOST naming a host with no credential still reads missing, and the remedy names that host ✅ pass live GITLAB_HOST=gitlab.com ./scripts/preflight.sh --tier recommendedmissing glab auth with install: glab auth login --hostname gitlab.com (gitlab.com genuinely has no token here)
GITLAB_HOST naming the authenticated host reads ok and names it ✅ pass live GITLAB_HOST=gitlab.metyis.technology ./scripts/preflight.sh --tier recommendedok glab auth gitlab.metyis.technology
A missing GitLab credential is reported but never fatal ✅ pass live GITLAB_HOST=gitlab.com ./scripts/preflight.sh full run exits 0 while the glab row reads missing
The gh auth row reads every account rather than the active one alone ✅ pass live ./scripts/preflight.shok gh auth 3 of 3 accounts — LeTuR, <client-account>, ArthurCesareHerriau, against the base commit's ok gh auth LeTuR
Adversarial: with no gh account authenticating at all, the required row still fails the preflight ✅ pass live GH_CONFIG_DIR=/tmp/gh-empty ./scripts/preflight.sh with the real gh → missing gh auth, install: gh auth login, exit 1
One expired token among several reads N of M and names the login that did not answer ⏸️ untested no The prior run established this only through the repo's offline ./scripts/onboarding-selftest.sh §7a/§7b, which is a stubbed selftest section and not a drive of the real gh; no mixed healthy/expire…
add-owner report names what each gh account reaches that the map does not cover, and writes nothing ✅ pass live ./scripts/add-owner.sh in a scratch root against the three real accounts — owners grouped per account with */+ marks, per-account new counts, a 9-owner summary truncated at 8 with +1 more, and…
An authenticated GitLab host is reported as evidence and never offered as an owner to add ✅ pass live Same live report: a GITLAB — evidence, never an owner block names gitlab.metyis.technology, while the not in registry/owners.txt line contains only GitHub owners
Adding a named owner appends under the preserved header and order, syncs, and reports what moved ✅ pass live ./scripts/add-owner.sh Thurbeen — real sync-registry.sh run, MAP CHANGED with owners added, repositories gained (truncated +21 more) and `67 repos across 2 owners (was 38 repos across 1 owners…
Adversarial: every refusal path leaves owners.txt and the generated map untouched ✅ pass live Duplicate, a name given twice, group/subgroup, gitlab.metyis.technology/group, .leading-dot, and a valid name beside an invalid one all exit 1 with no write — md5 of owners.txt and repos.generat…
Adversarial: --all on a machine where no gh account answers refuses instead of calling the map complete ✅ pass live GH_CONFIG_DIR=/tmp/gh-empty ./scripts/add-owner.sh --all → "No gh account answered...", exit 1, nothing written; with the real accounts and every owner already listed the same flag prints "Nothing n…
A clone that was never onboarded is refused and pointed back at the first-run path ✅ pass live ./scripts/add-owner.sh in this worktree (no registry/owners.txt) → exit 1 naming ./scripts/discover-owners.sh and the owners.example.txt copy
The three invocations the fleet-onboarding skill hands the operator all behave as documented, and --no-sync is gone ✅ pass live Report, --all and <owner> forms all driven above; --help prints the header, --no-sync and --wat both exit 2 with the usage line
An operator who exports GITLAB_HOST does not break the repo's own gate ✅ pass live GITLAB_HOST=gitlab.com ./scripts/check.sh onboarding is green on this branch; the same tree with GITLAB_HOST removed from the selftest's environment boundary FAILs 6a — the reported regression repro…
The new offline coverage fails when the behaviour it claims regresses ✅ pass live Mutating add-owner's before/after totals line makes §8d FAIL; dropping --paginate from the user/orgs call makes §8a/8b/8d/8e FAIL
An account belonging to more than 30 orgs is enumerated completely by the paginated user/orgs call ⏸️ untested no Needs a gh account that belongs to more than 30 GitHub organizations, or a stand-in GitHub API endpoint gh can be pointed at (GH_HOST with a served TLS cert); no account on this machine crosses GitHub…
Evidence: preflight before/after on the real machine (the reported bug, and the fix)

BEFORE — base commit 209a66c: ok gh auth LeTuR missing glab auth reading a merge request needs a credential for the host it lives on install: glab auth login # GITLAB_HOST=... for a self-hosted instance AFTER — target commit 7dfae4d, same machine, same credentials: ok gh auth 3 of 3 accounts — LeTuR, <client-account>, ArthurCesareHerriau ok glab auth gitlab.metyis.technology (exit 0) $ GITLAB_HOST=gitlab.com ./scripts/preflight.sh --tier recommended missing glab auth a merge request is read with a credential for ITS host, not for every host glab knows install: glab auth login --hostname gitlab.com

PREFLIGHT ON THE REPORTING OPERATOR'S REAL MACHINE
real gh 2.100.0 (3 accounts on github.com), real glab 1.117.0
(glab has two instances configured: gitlab.com with NO token, gitlab.metyis.technology authenticated)

$ glab auth status --all        # what the CLI itself says
gitlab.com
  x gitlab.com: API call failed: GET https://gitlab.com/api/v4/user: 401 {message: 401 Unauthorized}
  ✓ Git operations for gitlab.com configured to use ssh protocol.
  ✓ API calls for gitlab.com are made over https protocol.
  ✓ REST API Endpoint: https://gitlab.com/api/v4/
  ✓ GraphQL Endpoint: https://gitlab.com/api/graphql/
  ! No token found (checked config file, keyring, and environment variables).
gitlab.metyis.technology
  ✓ Logged in to gitlab.metyis.technology as arthur.cesare-herriau (~/.config/glab-cli/config.yml)
  ... X could not authenticate to one or more of the configured GitLab instances.  (exit non-zero)

================ BEFORE — base commit 209a66c ================
$ ./scripts/preflight.sh
  ok       gh auth        LeTuR
  ok       jq             1.6
--
  missing  glab auth      reading a merge request needs a credential for the host it lives on
                          install: glab auth login   # GITLAB_HOST=... for a self-hosted instance


================ AFTER — target commit 7dfae4d ================
$ ./scripts/preflight.sh
  ok       gh auth        3 of 3 accounts — LeTuR, <client-account>, ArthurCesareHerriau
  ok       jq             1.6
--
  ok       glab auth      gitlab.metyis.technology

exit=0

---- GITLAB_HOST decides, both directions (real glab, real hosts) ----
$ GITLAB_HOST=gitlab.com ./scripts/preflight.sh --tier recommended
  missing  glab auth      a merge request is read with a credential for ITS host, not for every host glab knows
                          install: glab auth login --hostname gitlab.com

$ GITLAB_HOST=gitlab.metyis.technology ./scripts/preflight.sh --tier recommended
  ok       glab auth      gitlab.metyis.technology


---- the REQUIRED gh row is still failable (real gh, empty config dir) ----
$ GH_CONFIG_DIR=/tmp/gh-empty ./scripts/preflight.sh --tier required
  missing  gh auth        the registry sync reads GitHub as you — EVERY account, no PAT, no CI secret
                          install: gh auth login
  ok       jq             1.6
full-run exit=1 (required gap, non-zero)
Evidence: add-owner.sh driven against the operator's real gh accounts (report, add, every refusal)

$ ./scripts/add-owner.sh ACCOUNTS gh HOLDS — and the GitHub owners each one reaches LeTuR * LeTuR, + sobri-it, + Thurbeen (2 new) <client-account> + <client-account>, + <client-org> (2 new) ArthurCesareHerriau + ArthurCesareHerriau, + Metyis-Porto, + Metyis-Global, + Metyis-Global-Test, + Climbing-District (5 new) * already in registry/owners.txt + not in it yet GITLAB — evidence, never an owner authenticated: gitlab.metyis.technology ... It adds no owner here: registry/owners.txt is a list of GITHUB owners, read by gh. 9 owners not in registry/owners.txt: sobri-it, Thurbeen, ..., +1 more $ ./scripts/add-owner.sh Thurbeen added to registry/owners.txt: Thurbeen fetching every accessible repository ... MAP CHANGED owners added Thurbeen repositories gained Thurbeen/fleet, Thurbeen/thurbox, ..., +21 more totals 67 repos across 2 owners (was 38 repos across 1 owners)

add-owner.sh DRIVEN AGAINST THE OPERATOR'S REAL gh ACCOUNTS
(scratch root /tmp/fleet-scratch — the operator's own owners.txt and map were never touched;
 client account and org names redacted for a public PR)

=== report mode: writes nothing, groups owners by the account that reaches them ===
$ ./scripts/add-owner.sh
ACCOUNTS gh HOLDS — and the GitHub owners each one reaches

  LeTuR                          * LeTuR, + sobri-it, + Thurbeen   (2 new)
  <client-account>  + <client-account>, + <client-org>   (2 new)
  ArthurCesareHerriau            + ArthurCesareHerriau, + Metyis-Porto, + Metyis-Global, + Metyis-Global-Test, + Climbing-District   (5 new)

  * already in registry/owners.txt      + not in it yet

GITLAB — evidence, never an owner

  authenticated: gitlab.metyis.technology

  That changes what ./scripts/preflight.sh reports and what a task can target
  through the forge seam in scripts/lib/forge.py. It adds no owner here:
  registry/owners.txt is a list of GITHUB owners, read by gh.

9 owners not in registry/owners.txt: sobri-it, Thurbeen, <client-account>, <client-org>, ArthurCesareHerriau, Metyis-Porto, Metyis-Global, Metyis-Global-Test, +1 more

  ./scripts/add-owner.sh --all       add every one of them, then sync
  ./scripts/add-owner.sh sobri-it    or name the ones you want

=== add mode: append + real sync, and it reports what MOVED ===
$ ./scripts/add-owner.sh Thurbeen
added to registry/owners.txt: Thurbeen

fetching every accessible repository ...
wrote /tmp/fleet-scratch/registry/repos.generated.yaml (67 repos across 2 owners)

MAP CHANGED
  owners added        Thurbeen
  repositories gained Thurbeen/peon-tactics, Thurbeen/fleet, Thurbeen/homebrew-thurbox, Thurbeen/thurbox, Thurbeen/gdcruiser, Thurbeen/thurview, Thurbeen/.github-private, Thurbeen/.github, +21 more
  totals              67 repos across 2 owners (was 38 repos across 1 owners)

A repository that is in the map and still unexplained belongs in
registry/context/<repo>.md, which is where the judgement about a project lives.

registry/owners.txt afterwards (header kept, new owner APPENDED, order untouched):
# The tracked EXAMPLE. Copy it to `registry/owners.txt`, which is yours and is
# gitignored — `/fleet-onboarding` does that for you.
  [...header...]
  LeTuR
  Thurbeen

=== guards — every one of these wrote nothing ===

$ ./scripts/add-owner.sh Thurbeen
error: 'Thurbeen' is already in registry/owners.txt. Nothing was written.
exit=1

$ ./scripts/add-owner.sh sobri-it sobri-it
error: 'sobri-it' was named twice. Nothing was written.
exit=1

$ ./scripts/add-owner.sh group/subgroup
error: 'group/subgroup' is not a GitHub owner — it holds usernames and orgs, one per line.
       A GitLab group is not one: a task targets a GitLab repository by host
       and path, through the seam in scripts/lib/forge.py.
exit=1

$ ./scripts/add-owner.sh gitlab.metyis.technology/group
error: 'gitlab.metyis.technology/group' is not a GitHub owner — it holds usernames and orgs, one per line.
       A GitLab group is not one: a task targets a GitLab repository by host
       and path, through the seam in scripts/lib/forge.py.
exit=1

$ ./scripts/add-owner.sh sobri-it bad/name
error: 'bad/name' is not a GitHub owner — it holds usernames and orgs, one per line.
       A GitLab group is not one: a task targets a GitLab repository by host
       and path, through the seam in scripts/lib/forge.py.
exit=1

$ GH_CONFIG_DIR=/tmp/gh-empty ./scripts/add-owner.sh --all      # no gh account answers
No gh account answered, so there is nothing to compare your map against.

  gh auth login          then run this again
  ./scripts/preflight.sh reads the same accounts and says which are broken
exit=1

$ ./scripts/add-owner.sh --no-sync                              # the flag dropped in review
usage: ./scripts/add-owner.sh [--all] [owner ...]
exit=2

owners.txt and repos.generated.yaml: UNCHANGED by every refusal above

=== a clone that was never onboarded (the real worktree, which has no owners.txt) ===
$ ./scripts/add-owner.sh
error: no ~/.no-mistakes/worktrees/d1be5edfebb8/01M28M661E692G91ZR3SCHTN3N/registry/owners.txt yet — this is the path for a map that already exists.
       A first run belongs to the fleet-onboarding skill, which asks:
         ./scripts/discover-owners.sh
         cp registry/owners.example.txt registry/owners.txt
exit=1
Evidence: offline sections 6-8 green, and three mutations proving they are not vacuous

GITLAB_HOST exported by the operator: without the fix: FAIL 6a a credential on one configured host is not reported missing with the fix: ok onboarding: scripts/onboarding-selftest.sh add-owner totals line reworded: FAIL 8d the map's own owner count moved --paginate dropped from user/orgs: FAIL 8a/8b/8d/8e

THE REPO'S OWN OFFLINE DRIVER, AND THREE MUTATIONS THAT PROVE IT IS NOT VACUOUS

$ ./scripts/onboarding-selftest.sh      (sections 6-8, the ones this change adds)
§6 preflight — the glab row is per HOST, and a self-hosted instance is ordinary
  ok    6a a credential on one configured host is not reported missing
  ok    6a and the row names the host that answered
  ok    6b GITLAB_HOST naming a host with no credential is missing
  ok    6b and the remedy names that host, not a bare login
  ok    6c GITLAB_HOST naming the host that answers is not missing
  ok    6d no host authenticated is missing
  ok    6e a missing GitLab credential is reported, never fatal

§7 preflight — the gh row is per ACCOUNT, and one expired token is not the answer
  ok    7a one expired token among three working logins is not a failed preflight
  ok    7a the gh auth row is not reported missing
  ok    7a it says how many accounts authenticated
  ok    7a and names them
  ok    7b the login that did not is named, so a thinner answer is never silent
  ok    7c not one account authenticating is still missing
  ok    7c with the remedy
  ok    7c and a required gap is still a non-zero exit
  ok    7d a gh too old for --json still passes on the active session
  ok    7d and is not reported missing
  ok    7d naming the account that answered

§8 add-owner — the path for what the operator gains AFTER the first run
  ok    8a asking what is new exits 0
  ok    8a the newly authenticated account is named
  ok    8a with the owner it reaches that the map does not cover
  ok    8a and it says how many are not in the map
  ok    8a the report hands over the command that adds them
  ok    8b the summary names the new owner
  ok    8b and the new account's own namespace, which is an owner as well
  ok    8b an owner already in the file is not offered twice
  ok    8b nor is the operator's own account
  ok    8c an authenticated GitLab host is reported
  ok    8c but never as something to add
  ok    8d adding every new owner exits 0
  ok    8d it says which owner it added
  ok    8d the map's own owner count moved
  ok    8d a repository the new account reaches is gained
  ok    8d and one that disappeared is reported lost
  ok    8d the whole map is not printed back
  ok    8e the comment header survives the write
  ok    8e the new owner is APPENDED, and the existing order is untouched
  ok    8e and the map now carries it
  ok    8f a re-run once everything is added exits 0
  ok    8f and says there is nothing new rather than offering the same list
  ok    8f no owner is proposed a second time
  ok    8g adding an owner already in the file is refused
  ok    8g and says which one
  ok    8g the file is not touched
  ok    8h a path is not a GitHub owner
  ok    8h and the refusal says what the file holds
  ok    8h nothing was written
  ok    8h nor is a host-qualified name
  ok    8i a clone with no owners file is refused, not onboarded from here
  ok    8i and it points at what does own a first run
  ok    8j --all with no account that answered is refused
  ok    8j and says so rather than calling the map complete
  ok    8j it does not claim every owner is already covered
  ok    8j the file is not touched

onboarding selftest: everything passed

=== mutation 1: an operator who exports GITLAB_HOST must not decide the gate ===
# without the fix (GITLAB_HOST dropped from the selftest environment boundary):
$ GITLAB_HOST=gitlab.com scripts/onboarding-selftest.sh
  FAIL  6a a credential on one configured host is not reported missing
  FAIL  6a and the row names the host that answered
onboarding selftest: failures above
# with the fix, on this branch:
$ GITLAB_HOST=gitlab.com ./scripts/check.sh onboarding
ok    onboarding: scripts/onboarding-selftest.sh

=== mutation 2: break add-owner's before/after totals line — 8d must notice ===
  FAIL  8d the map's own owner count moved
onboarding selftest: failures above

=== mutation 3: drop --paginate from the user/orgs call — section 8 must notice ===
  FAIL  8a with the owner it reaches that the map does not cover
  FAIL  8a and it says how many are not in the map
  FAIL  8b the summary names the new owner
  FAIL  8d it says which owner it added
  FAIL  8d the map's own owner count moved
  FAIL  8d a repository the new account reaches is gained
  FAIL  8e the new owner is APPENDED, and the existing order is untouched
  FAIL  8e and the map now carries it
onboarding selftest: failures above

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ⚠️ scripts/add-owner.sh:285 - --all prints a false claim when no gh account answered — round 1 selected this for fix and the target commit does not contain it. Trace: gh is installed but every credential is expired or logged out (exactly the machine this change is about). gh_accounts returns nothing, the ask_account &#34;&#34; fallback fails on gh api user and is swallowed by || true (line 151), so ACCOUNT_ORDER and NEW_ORDER are both empty. --all then reaches line 284 and prints "Nothing new: every owner these accounts reach is already in registry/owners.txt." and exits 0 — asserting the map is complete when nothing was compared at all. Report mode handles the same state correctly (report() lines 210-215 prints "No gh account answered, so there is nothing to compare your map against." plus the gh auth login remedy); the --all branch is the one place that asymmetry leaks a wrong statement. Remedy: gate line 284 on ${#ACCOUNT_ORDER[@]} -eq 0 first and emit the same "no account answered" message the report emits.
  • ⚠️ scripts/onboarding-selftest.sh:77 - Ambient GITLAB_HOST decides §6's answers, so the new glab coverage is environment-dependent. Line 77 unsets GH_TOKEN GITHUB_TOKEN GH_HOST (and lines 66-72 clear GIT_CONFIG_*) precisely because a caller's environment must not decide a stubbed probe's answer, but this change makes GITLAB_HOST a first-class decider at scripts/preflight.sh:263 and never adds it to that boundary. §6a (line 901) and §6d/§6e run plain_preflight with no GITLAB_HOST prefix, inheriting whatever the operator exports. Concrete failure: with GITLAB_HOST=gitlab.com exported, §6a's GLAB_HOSTS=&#34;gitlab.com gitlab.metyis.technology&#34; GLAB_OK=&#34;gitlab.metyis.technology&#34; run takes the GITLAB_HOST branch, glab_host_ok gitlab.com exits 1, preflight records missing, and refute &#34;6a a credential on one configured host is not reported missing&#34; fails — so ./scripts/check.sh onboarding (and therefore the repo's own gate) breaks on a machine that has done nothing wrong. With GITLAB_HOST set to a self-hosted host instead, §6a passes through the wrong branch and never exercises the per-host enumeration it claims to cover. Remedy is the file's existing pattern: add GITLAB_HOST to the unset at line 77.
  • ⚠️ scripts/add-owner.sh:65 - Simplification: --no-sync is a new mode the stated intent does not require, and round 1 selected its removal — the target commit still carries it. The intent specifies exactly three behaviors for this script: report ("names the owners the current gh accounts reach that registry/owners.txt does not list"), and "With --all or a named list it APPENDS them ... then syncs and reports what MOVED". There is no requirement for a form that writes owners.txt and deliberately leaves the map stale; it also creates the one state the repo's own rules warn about (an owners file the generated map no longer matches), which the script then has to explain away at line 330. Recommend removing the flag and its branch (lines 60, 65, the usage string at line 71, lines 329-332, the --no-sync line in the header at line 38, and the --no-sync sentence added to .agents/skills/fleet-onboarding/SKILL.md) so add mode always converges on a synced map.
  • ℹ️ scripts/add-owner.sh:122 - gh_api_as &#34;$token&#34; user/orgs --jq &#39;.[].login&#39; is unpaginated, so GitHub's default per_page=30 caps it — round 1 selected this for fix and it is not in the target commit. An operator who belongs to more than 30 orgs on one account gets a silently truncated candidate set: NEW_ORDER omits the 31st org onward, so the report can say "Nothing new: every owner these accounts reach is already in registry/owners.txt" while an uncovered org exists, and --all then adds an incomplete set — a wrong answer with no error. Remedy is mechanical: gh_api_as &#34;$token&#34; --paginate user/orgs --jq &#39;.[].login&#39;. Note the §8 gh stub dispatches on case &#34;$1 $2&#34;, so the fixture's &#34;api user/orgs&#34;) arm needs to become the --paginate shape alongside it. The identical unpaginated call exists at scripts/discover-owners.sh:109 (pre-existing, unchanged here), so fixing only this one leaves the two paths disagreeing; scripts/sync-registry.sh:93 already paginates its endpoint.
  • ℹ️ scripts/onboarding-selftest.sh:1189 - expect &#34;8d the map&#39;s own owner count moved&#34; &#34;owners&#34; &#34;$out&#34; is vacuous, and round 1's selected fix is not in the target commit. $out always contains the literal owners added ... line printed unconditionally by scripts/add-owner.sh:351, so the substring owners matches regardless of whether the map's owner total changed at all. The label claims coverage the assertion does not provide, and it would keep passing if the totals line (add-owner.sh:363) regressed or disappeared. In this fixture the map goes from 3 repos across 2 owners to 3 repos across 3 owners, so the honest assertion is that phrase — e.g. across 3 owners (was — which scripts/add-owner.sh:363 emits from map_totals.
  • ℹ️ scripts/add-owner.sh:306 - Simplification: the rule for what a GitHub owner name is has two definitions back to back. The first case (lines 306-309) requires a leading alphanumeric; the second (lines 310-316) rejects any character outside [A-Za-z0-9-]. For every input that can reach here the second subsumes the first — .foo, group/subgroup and gitlab.example.com/group all fail both — and the first's only unique coverage is a leading -, which the argument parser already intercepts at line 70 with a usage error (exit 2) and which no GitHub login can be. Collapsing to the single *[!A-Za-z0-9-]* case removes the dead branch and leaves the more informative refusal message as the one an operator sees.

🔧 Fix applied.
2 infos still open:

  • ℹ️ scripts/add-owner.sh:314 - Collapsing the two owner-name checks into *[!A-Za-z0-9-]* dropped the one input the removed [A-Za-z0-9]* case uniquely covered: the EMPTY string. Verified glob semantics — case &#34;&#34; in *[!A-Za-z0-9-]*) does not match, so an empty name is accepted, while the old leading-alphanumeric case refused it. Reachable path: ./scripts/add-owner.sh &#34;$OWNER&#34; with OWNER unset or empty (the argument parser sends "" to WANTED via the *) arm at line 71, since only -* is intercepted). It then passes the duplicate check, printf &#39;%s\n&#39; &#34;&#34; appends a blank line to the operator's registry/owners.txt at line 330, line 331 prints added to registry/owners.txt: naming nothing (list_some skips empty items and returns early), and a full sync-registry run follows. The written file is still parseable — both add-owner.sh:93-97 and sync-registry.sh:49-55 skip blank lines — so the damage is a stray line plus a success message for an owner the operator never named, rather than a broken map. Remedy is one token inside the branch the prior round already touched: case &#34;$owner&#34; in &#34;&#34; | *[!A-Za-z0-9-]*).
  • ℹ️ scripts/onboarding-selftest.sh:923 - §6c is the only new preflight case with no positive assertion, so it cannot distinguish the behavior it claims to cover from preflight emitting no glab row at all. Its single refute &#34;missing glab auth&#34; passes whenever that string is absent — including if record recommended &#34;glab auth&#34; ok &#34;$GITLAB_HOST&#34; (scripts/preflight.sh:266) were deleted outright, or if preflight died before reaching the glab section. That branch — GITLAB_HOST naming a host that DOES authenticate — is covered by no other case: 6a takes the enumeration path, 6b takes the GITLAB_HOST-fails path, 6d the no-credential path. §6a and §7d both pair their refute with a positive expect for exactly this reason. Remedy: add expect &#34;6c and the row names the host GITLAB_HOST pointed at&#34; &#34;gitlab.metyis.technology&#34; &#34;$out&#34; alongside the existing refute.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 15 of 17 scenarios driven live against the product
Scenario Result Live Evidence
A self-hosted-only GitLab operator runs preflight and is no longer told their working setup is broken ✅ pass live /tmp/fleet-base/scripts/preflight.sh (base 209a66c) prints missing glab auth; ./scripts/preflight.sh on the target prints ok glab auth gitlab.metyis.technology — same machine, same real glab 1…
GITLAB_HOST naming a host with no credential still reads missing, and the remedy names that host ✅ pass live GITLAB_HOST=gitlab.com ./scripts/preflight.sh --tier recommendedmissing glab auth with install: glab auth login --hostname gitlab.com (gitlab.com genuinely has no token here)
GITLAB_HOST naming the authenticated host reads ok and names it ✅ pass live GITLAB_HOST=gitlab.metyis.technology ./scripts/preflight.sh --tier recommendedok glab auth gitlab.metyis.technology
A missing GitLab credential is reported but never fatal ✅ pass live GITLAB_HOST=gitlab.com ./scripts/preflight.sh full run exits 0 while the glab row reads missing
The gh auth row reads every account rather than the active one alone ✅ pass live ./scripts/preflight.shok gh auth 3 of 3 accounts — LeTuR, &lt;client-account&gt;, ArthurCesareHerriau, against the base commit's ok gh auth LeTuR
Adversarial: with no gh account authenticating at all, the required row still fails the preflight ✅ pass live GH_CONFIG_DIR=/tmp/gh-empty ./scripts/preflight.sh with the real gh → missing gh auth, install: gh auth login, exit 1
One expired token among several reads N of M and names the login that did not answer ⏸️ untested no The prior run established this only through the repo's offline ./scripts/onboarding-selftest.sh §7a/§7b, which is a stubbed selftest section and not a drive of the real gh; no mixed healthy/expire…
add-owner report names what each gh account reaches that the map does not cover, and writes nothing ✅ pass live ./scripts/add-owner.sh in a scratch root against the three real accounts — owners grouped per account with */+ marks, per-account new counts, a 9-owner summary truncated at 8 with +1 more, and…
An authenticated GitLab host is reported as evidence and never offered as an owner to add ✅ pass live Same live report: a GITLAB — evidence, never an owner block names gitlab.metyis.technology, while the not in registry/owners.txt line contains only GitHub owners
Adding a named owner appends under the preserved header and order, syncs, and reports what moved ✅ pass live ./scripts/add-owner.sh Thurbeen — real sync-registry.sh run, MAP CHANGED with owners added, repositories gained (truncated +21 more) and `67 repos across 2 owners (was 38 repos across 1 owners…
Adversarial: every refusal path leaves owners.txt and the generated map untouched ✅ pass live Duplicate, a name given twice, group/subgroup, gitlab.metyis.technology/group, .leading-dot, and a valid name beside an invalid one all exit 1 with no write — md5 of owners.txt and repos.generat…
Adversarial: --all on a machine where no gh account answers refuses instead of calling the map complete ✅ pass live GH_CONFIG_DIR=/tmp/gh-empty ./scripts/add-owner.sh --all → "No gh account answered...", exit 1, nothing written; with the real accounts and every owner already listed the same flag prints "Nothing n…
A clone that was never onboarded is refused and pointed back at the first-run path ✅ pass live ./scripts/add-owner.sh in this worktree (no registry/owners.txt) → exit 1 naming ./scripts/discover-owners.sh and the owners.example.txt copy
The three invocations the fleet-onboarding skill hands the operator all behave as documented, and --no-sync is gone ✅ pass live Report, --all and &lt;owner&gt; forms all driven above; --help prints the header, --no-sync and --wat both exit 2 with the usage line
An operator who exports GITLAB_HOST does not break the repo's own gate ✅ pass live GITLAB_HOST=gitlab.com ./scripts/check.sh onboarding is green on this branch; the same tree with GITLAB_HOST removed from the selftest's environment boundary FAILs 6a — the reported regression repro…
The new offline coverage fails when the behaviour it claims regresses ✅ pass live Mutating add-owner's before/after totals line makes §8d FAIL; dropping --paginate from the user/orgs call makes §8a/8b/8d/8e FAIL
An account belonging to more than 30 orgs is enumerated completely by the paginated user/orgs call ⏸️ untested no Needs a gh account that belongs to more than 30 GitHub organizations, or a stand-in GitHub API endpoint gh can be pointed at (GH_HOST with a served TLS cert); no account on this machine crosses GitHub…
  • ./scripts/check.sh
  • /tmp/fleet-base/scripts/preflight.sh (base commit 209a66c) against the real gh/glab — reproduces missing glab auth
  • ./scripts/preflight.sh on the same machine — ok gh auth 3 of 3 accounts, ok glab auth gitlab.metyis.technology, exit 0
  • GITLAB_HOST=gitlab.com ./scripts/preflight.sh --tier recommended — missing, remedy glab auth login --hostname gitlab.com, full run still exit 0 (recommended)
  • GITLAB_HOST=gitlab.metyis.technology ./scripts/preflight.sh --tier recommended — ok, names the host
  • GH_CONFIG_DIR=/tmp/gh-empty ./scripts/preflight.sh with the real gh — missing gh auth, exit 1
  • ./scripts/add-owner.sh in a scratch root against the three real gh accounts (report mode)
  • ./scripts/add-owner.sh Thurbeen — real append + real sync-registry.sh, 38→67 repos across 1→2 owners
  • ./scripts/add-owner.sh Thurbeen / LeTuR / sobri-it sobri-it / group/subgroup / gitlab.metyis.technology/group / .leading-dot / sobri-it bad/name — refusals, files md5-verified unchanged
  • GH_CONFIG_DIR=/tmp/gh-empty ./scripts/add-owner.sh --all — "No gh account answered", exit 1, nothing written
  • ./scripts/add-owner.sh --all with every reachable owner already listed — "Nothing new", exit 0
  • ./scripts/add-owner.sh in this worktree (no registry/owners.txt) — refused, points at discover-owners.sh
  • ./scripts/add-owner.sh --help and --no-sync / --wat — help prints, unknown flags exit 2
  • ./scripts/check.sh onboarding and ./scripts/onboarding-selftest.sh (§6, §7, §8 all green) — offline coverage, not a live drive
  • GITLAB_HOST=gitlab.com ./scripts/check.sh onboarding — green, versus the same tree with GITLAB_HOST removed from the selftest environment boundary, which FAILs 6a
  • mutation: add-owner's totals line reworded — §8d FAILs; mutation: --paginate dropped from the user/orgs call — §8a/8b/8d/8e FAIL
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…er the first run

Preflight called a working setup broken twice over, both times by reading an
all-or-nothing exit code as an answer about one credential:

- `glab auth status` exits non-zero when ANY configured instance fails, so an
  operator authenticated to their self-hosted GitLab and not to gitlab.com —
  which they never use — read `missing glab auth` beside a remedy they had
  already run. That contradicts the forge seam, where which hosts a CLI owns
  comes from that CLI's own variable and a self-hosted instance is ordinary.
- `gh auth status` has the same shape: one expired token among three logins
  made a REQUIRED row read `missing` with two working accounts sitting there.

Both rows now go through a seam. `gh auth` reuses scripts/lib/gh-accounts.sh
and reads `N of M accounts`, naming the login that failed on stderr rather than
swallowing it; `glab auth` gets scripts/lib/glab-hosts.sh, which asks per host
— GITLAB_HOST decides when set, otherwise one working credential is enough and
the row names it. Each keeps its old single-answer path as the fallback for a
CLI too old to enumerate.

The second half is the path that did not exist: what an operator gains AFTER a
first run. scripts/add-owner.sh names the owners the current gh accounts reach
that registry/owners.txt does not list, grouped by the account that reaches
them — the shape of the question after a `gh auth login`. With `--all` or a
named list it appends them, keeping the file's comment header and its order,
refusing a duplicate, then syncs and reports what moved rather than the map. It
logs nobody in, refuses a clone with no owners file, and a GitLab host is
reported as evidence and never becomes an owner.

Covered offline in scripts/onboarding-selftest.sh §6-§8 against stubbed gh and
glab on a PATH built from scratch, including the self-hosted-only case, the
expired-token case, and an incremental add on a machine that already has a map.
@LeTuR
LeTuR merged commit e008e9f into main Sep 11, 2026
11 checks passed
@LeTuR
LeTuR deleted the feat/add-things-after-onboarding branch September 11, 2026 17:03
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