feat(forge): discover GitLab hosts from glab instead of GITLAB_HOST - #72
Merged
Conversation
…variable `GITLAB_HOST` is the documented way to tell fleet about a self-hosted GitLab and nothing exports it, so a merge request on a self-hosted instance was on no configured forge: `shepherd` never listed it, `collect` could not verify a publish there, and `reap` could never land the task — leaking its session and worktree, once per task. `forge.configured_hosts` now reads every instance out of `glab auth status`, which is where the operator's answer already lived. `GITLAB_HOST` still decides when it is set, and no glab, no config or a report it cannot parse each leave the adapter where it was. `AUTO_MERGE_REPOS` is untouched: a discovered host is watched, classified and reported, never merged.
… cache and stub log
… fix stale fixture and section prose
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Make fleet discover which GitLab hosts it can reach instead of waiting for a GITLAB_HOST variable nothing exports.
The problem, measured on this machine on 2026-09-11 with a real merge request open on a self-hosted GitLab: with GITLAB_HOST unset, scripts/lib/forge.py's GitLab adapter owned only gitlab.com, so a merge request on a self-hosted instance was on no configured forge. shepherd printed 'no task names a repository on a configured forge yet', collect recorded 'unknown - no configured forge owns ' for the publish check, and reap could therefore never move the task to landed - which means its session and worktree were never released, once per task, with no upper bound.
What was done: forge.configured_hosts(cli) reads every instance out of ' auth status' - the CLI's own config, which is where the operator's answer already lived - and GitLabForge folds that into its host list. Deliberate decisions a reviewer cannot see in the diff:
Verification: queue-selftest.sh gained section 14e, which proves all of the above offline - discovery from a recorded 'glab auth status', GITLAB_HOST precedence, the three degraded machines, the --all fallback, and collect/shepherd/reap driven end to end on a discovered host with no GITLAB_HOST anywhere. The recording is scripts/fixtures/glab/auth-status.stderr; it is real glab 1.117.0 output with the operator's hostname, account and home directory replaced, because this repository is public - the fixtures README says so. The selftest also gained a global 'glab' stand-in on PATH so that no section reaches the operator's real glab or the network, section 13 included; it is a stand-in rather than a tripwire because the adapter now legitimately invokes glab in every section.
A previous run of this pipeline already applied a review fix round on top (commit 'chore: no-mistakes review - tighten 14e assertions, drop unused host cache and stub log') and then failed in the test step because its agent hit the account's usage limit, not because of anything in the change. That fix round is part of the branch now: it dropped a redundant per-process host cache (the registry already caches the one adapter), corrected the selftest header to call the glab stub a stand-in rather than a tripwire, and tightened 14e's collect assertion to key on 'publish verified' so it fails on a discovery-broken build.
Out of scope deliberately: scripts/preflight.sh still reports 'glab auth' as one row and still hints at GITLAB_HOST. Another task in the same topic owns making it report per host, and configured_hosts is what it will use.
What Changed
forge.configured_hosts(cli), which reads every instance out of<cli> auth status(both streams, exit code ignored,--allwith a bare-form fallback) and answers with an empty list on any failure;GitLabForgefolds that into its host list so a self-hosted instance is owned without an exported variable, while a setGITLAB_HOSTstill decides and suppresses discovery entirely. The GitHub adapter is unchanged and still takesGH_HOSTalone.forge.pya__main__so a shell caller can askpython3 scripts/lib/forge.py hosts <gh|glab>.queue-selftest.shwith §14e — discovery from a recordedglab auth status,GITLAB_HOSTprecedence, degraded machines, the--allfallback, andcollect/shepherd/reapend to end on a discovered host with noGITLAB_HOST— plus a globalglabstand-in on PATH so no section reaches the real CLI or the network, and asserted that a discovered host is still not auto-mergeable. Added thescripts/fixtures/glab/auth-status.stderrrecording (hostname, account and home directory redacted) and documented it, the new host source, and the seam in the fixtures README,README.mdandAGENTS.md.Risk Assessment
✅ Low: The discovery seam is additive, fails closed to the previous host list on every degraded path I traced (no CLI, unparseable report, garbled output, timeout), preserves GITLAB_HOST precedence by short-circuit, leaves AUTO_MERGE_REPOS untouched, and is covered by an offline §14e whose assertions I confirmed would fail on a discovery-broken build; the only surviving finding is a documentation provenance nit.
Testing
The configured./scripts/check.shbaseline was already green. On top of that I drove the real product rather than its tests for the discovery seam itself: this machine's glab is logged in to a self-hosted GitLab with GITLAB_HOST unset, so the defect the intent describes is reproducible live, and it was — base commit'scollectsaid "no configured forge owns <host>" andshepherdsaid "no task names a repository on a configured forge yet", while this branch'scollectreaches the instance and reports what the instance answered. I then attacked the guards live: GITLAB_HOST set to another instance still decides (and skips discovery entirely), and five degraded glab installs each degrade to exactly the pre-change answer withcollectexiting 0 — the one that never answers costs a single bounded 10s timeout. What I could NOT drive live is the far half of the loop: verifying a publish against a real open merge request, shepherd reporting-but-not-merging a green attested mergeable MR, and reap landing and releasing the session on a discovered host. That needs a scratch project on a self-hosted GitLab with an open, mergeable, attested MR and a token permitted to merge it; this run had no such project and no merge rights, so those three scenarios are reported untested rather than passed. §14e of queue-selftest.sh covers them against recordedglaboutput and a fake thurbox, and that is test-harness coverage, not the live product, so it does not upgrade them. No UI surface exists in this change; evidence is CLI transcripts, with the operator's real hostname replaced by gitlab.self-hosted.example in every artifact because this repository is public. Nothing was left in the working tree and all scratch copies were removed.auth statusand the publish check ru…glab auth statusand a fake thurbox rather than driving the pr…Evidence: Live before/after at the forge seam, on this machine's own self-hosted GitLab
Evidence: The shell entrypoint AGENTS.md advertises, driven live
Evidence: collect / shepherd / reap, same task, base vs this branch
Evidence: GITLAB_HOST precedence and five degraded glab machines, through the real collect
Evidence: queue-selftest.sh section 14e transcript (test-harness coverage, not a live drive)
Evidence: The same selftest against the pre-fix forge.py — 7 failures (test-harness coverage, not a live drive)
Evidence: GitHub half untouched, gh tripwire never fired
Evidence: The defect and its fix, one task, one machine, GITLAB_HOST unset
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
🔧 **Rebase** - 1 issue found → auto-fixed ✅
scripts/queue-selftest.sh- merge conflict rebasing onto origin/main🔧 Fix applied.
✅ Re-checked - no issues remain.
scripts/lib/forge.py:278- scripts/lib/forge.py:278-280 — theIT NEVER FAILSparagraph lists "a CLI too old for--all" among the cases that "answer with an empty list ... which leaves every caller exactly where it was". That is the opposite of what the code does. The loop at line 308 tries[cli, auth, status, --all]and, when it comes back with no hosts, falls through to the bare[cli, auth, status]— and the comment at 300-307 explains that fallback as deliberate. scripts/queue-selftest.sh:5238-5243 asserts the fallback path: aglabstub that refuses--allwithunknown flag: --alland then prints the recording is expected to end withowns_host("gitlab.example.com") == True, not with an empty list. So the docstring sentence describes a behaviour the code does not have and the test explicitly forbids. Concrete trace: an operator on a glab too old for--all, logged in to gitlab.example.com — the code discovers the host and fleet reaps the task; the docstring tells the next reader the adapter stayed at gitlab.com and the task still leaks, which is exactly the wrong conclusion to reach while debugging this seam. scripts/** review instructions say "Comments explain why, never what. A stale comment is worse than none." Remedy is mechanical: dropor a CLI too old for --allfrom that sentence (AGENTS.md:64-66 already lists only "no CLI, no config, or a report it cannot read" and is correct as written). Note the same inconsistency exists in the stated intent, which says both "a CLI too old for --all [answers] with an empty list" and that 14e proves "the --all fallback"; the code follows the second, more specific clause, so no behavioural change is needed.🔧 Fix applied.
1 info still open:
scripts/fixtures/glab/README.md:79- scripts/fixtures/glab/README.md:79 — the## Refreshingsection still says "Re-run the commands above", and "the commands above" now includes the newauth-status.stderrtable row (glab auth status --all). That command is not re-runnable to reproduce this file: it prints whatever the running machine is configured for, and the committed bytes were additionally sanitized (hostname, account, home directory). The preamble at :8-13 was already corrected in an earlier round to scope repeatability to themr-*rows, but the Refreshing section is the other half of the same provenance contract and was not updated, so it now contradicts the preamble it was fixed to agree with. Remedy is mechanical: scope it, e.g. "Re-run themr-*commands above" and note thatauth-status.stderris re-recorded by hand from a machine that has the instances, then sanitized as its bullet describes.scripts/queue-selftest.sh:5228- scripts/queue-selftest.sh §14e feeds eight assertions through a Python block that prints PASS/FAIL rows into a TSV the shell then reads; ifforge.configured_hostsis ever renamed or removed, that block dies with an AttributeError and the eight rows silently vanish instead of failing (observed exactly this when running §14e against the pre-fix forge.py). The end-to-end assertions further down still fail loudly, so the section cannot silently pass overall — noting it only because a row-count assertion on the TSV would close the gap cheaply. No action needed for this change.auth statusand the publish check ru…glab auth statusand a fake thurbox rather than driving the pr…./scripts/check.shenv -u GITLAB_HOST python3 scripts/lib/forge.py hosts glab— the advertised shell entrypoint, against this machine's real glab config (alsohosts gh,hosts nosuchcli, and misuse → exit 2)./scripts/queue.sh topic add+add+collect+shepherd --dry-run+reap --dry-runon a throwaway FLEET_QUEUE_DIR, artifact = a merge-request URL on this machine's self-hosted GitLab, GITLAB_HOST unset — run once against this branch and once against a copy of the tree carryinggit show 209a66c:scripts/lib/forge.pyGITLAB_HOST=https://gitlab.other.example/ ./scripts/queue.sh collect— precedence: the discovered host must stop being ours, and glab must not be asked (returned in 0s)./scripts/queue.sh collectunder five hand-built glab installs on stub PATHs: none at all, one that cannot parse its config, one that answers prose, one that refuses--all, one that sleeps 120s./scripts/queue-selftest.sh— the repo's own end-to-end coverage, §14e included (694 claims, exit 0)./scripts/queue-selftest.shagainst a tree with the pre-fixscripts/lib/forge.py— expected and got 7 §14e FAILs, exit 1PATH=<tripwire gh first> ./scripts/queue.sh collecton the self-hosted artifact — 0 gh invocations recorded;for_repo(github.com/Thurbeen/fleet)and GH_HOST behaviour unchangedscripts/preflight.sh:215-scripts/preflight.shstill reportsglab authas a single row and still hintsGITLAB_HOST=... for a self-hosted instance, which is now a weaker story than the code tells —forge.configured_hostsreads the instances offglabitself. Left as-is on purpose: the stated intent scopes preflight out and names another task in the same topic as its owner, withconfigured_hostsas the seam it will use. No action here; recorded so the deferral is visible rather than looking like an oversight.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.