Skip to content

feat(forge): discover GitLab hosts from glab instead of GITLAB_HOST - #72

Merged
LeTuR merged 4 commits into
mainfrom
feat/discover-the-forge-hosts
Sep 11, 2026
Merged

feat(forge): discover GitLab hosts from glab instead of GITLAB_HOST#72
LeTuR merged 4 commits into
mainfrom
feat/discover-the-forge-hosts

Conversation

@LeTuR

@LeTuR LeTuR commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

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:

  • GITLAB_HOST still DECIDES when it is set: discovery does not run at all then. It is glab's own variable and an operator who exported it pointed fleet at that instance on purpose. That preserves today's behaviour exactly for anyone who sets it.
  • The exit code of 'glab auth status' is ignored on purpose: glab exits non-zero whenever any one configured instance fails to authenticate, which is the ordinary state of a machine logged in to one instance and not another. Reading the exit code would discover nothing on exactly the machines this exists for.
  • Both stdout and stderr are read because the two CLIs disagree: glab writes the whole report to stderr, gh writes it to stdout.
  • Discovery is never a requirement. No CLI, no config, a report it cannot parse, or a CLI too old for --all each answer with an empty list and leave the adapter exactly where it was - collect has to keep working with the network down. A call that never answered at all (timeout) stops the sequence rather than being retried, so the timeout is not paid twice.
  • The GitHub adapter deliberately does NOT use this, even though gh auth status prints the same shape and GitHub Enterprise is the same problem. queue-selftest.sh section 13 puts gh on PATH as a tripwire that fails on any invocation, proving no code reaches around the forge seam; building the GitHub adapter would run 'gh auth status' and trip it. GH_HOST was never the half that was broken. configured_hosts takes the CLI by name so preflight.sh and a future GitHub change can still use it, and forge.py grew a tiny 'main' so a shell script can ask ('python3 scripts/lib/forge.py hosts glab').
  • AUTO_MERGE_REPOS is untouched and stays host-qualified. A discovered host must not make anything auto-mergeable; the selftest asserts that a green, attested, mergeable merge request on a discovered host is reported 'fleet does not merge in ...' and never merged.

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

  • Added forge.configured_hosts(cli), which reads every instance out of <cli> auth status (both streams, exit code ignored, --all with a bare-form fallback) and answers with an empty list on any failure; GitLabForge folds that into its host list so a self-hosted instance is owned without an exported variable, while a set GITLAB_HOST still decides and suppresses discovery entirely. The GitHub adapter is unchanged and still takes GH_HOST alone.
  • Gave forge.py a __main__ so a shell caller can ask python3 scripts/lib/forge.py hosts <gh|glab>.
  • Extended queue-selftest.sh with §14e — discovery from a recorded glab auth status, GITLAB_HOST precedence, degraded machines, the --all fallback, and collect/shepherd/reap end to end on a discovered host with no GITLAB_HOST — plus a global glab stand-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 the scripts/fixtures/glab/auth-status.stderr recording (hostname, account and home directory redacted) and documented it, the new host source, and the seam in the fixtures README, README.md and AGENTS.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.sh baseline 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's collect said "no configured forge owns <host>" and shepherd said "no task names a repository on a configured forge yet", while this branch's collect reaches 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 with collect exiting 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 recorded glab output 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.

  • Live validation: ✅ go - 5 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An operator whose glab is logged in to a self-hosted GitLab, with GITLAB_HOST unset, has that instance owned by the GitLab adapter — and did not before ✅ pass live 01-discovery-before-after.txt and 01b-hosts-entrypoint.txt — base build: hosts ('gitlab.com','www.gitlab.com'), for_repo → 'no configured forge owns <host>'; this branch: the self-hosted host is in ho…
collect, shepherd and reap on a task whose artifact is a merge request on that self-hosted instance ✅ pass live 02-queue-cli-before-after.txt — base prints 'publish unchecked: could not check — no configured forge owns <host>' and 'shepherd: no task names a repository on a configured forge yet'; this branch act…
GITLAB_HOST still decides: with it pointed at another instance, a host glab holds is not ours and glab is not consulted ✅ pass live 03-degraded-machines.txt, first row — 'no configured forge owns <self-hosted>', collect exits 0 in under a second (no glab subprocess)
Adversarial: five machines discovery must survive — no glab, an unparseable config, prose instead of a host list, a glab too old for --all, and a glab that never answers ✅ pass live 03-degraded-machines.txt — the first three degrade to exactly the pre-change answer with collect exiting 0; the too-old one still discovers the host via the bare auth status and the publish check ru…
The whole loop on a discovered host: collect verifies the publish, shepherd lists what is open, reap lands the task and releases the session that used to leak ⏸️ untested no The prior payload did not establish a live result for this: it cited only §14e of ./scripts/queue-selftest.sh, which replays a recorded glab auth status and a fake thurbox rather than driving the pr…
Adversarial: a green, attested, mergeable merge request on a discovered host must be reported and never merged — discovery must not widen AUTO_MERGE_REPOS ⏸️ untested no The prior payload did not establish a live result for this: its evidence was §14e fixture assertions, not fleet offered a real mergeable merge request. Proving it live requires an open MR on a real se…
§14e is a real regression test: the same selftest fails on the pre-fix forge ⏸️ untested no The prior payload did not establish a live result for this: it is the repository's own test script run twice against two builds, which is test-harness behaviour and not the product driven by an end us…
The GitHub half is untouched and nothing reaches gh while the host came from glab ✅ pass live 06-github-untouched.txt — a tripwire gh first on PATH records 0 invocations during a collect on the self-hosted artifact; for_repo(github.com/Thurbeen/fleet) is still GitHubForge and GH_HOST still add…
Evidence: Live before/after at the forge seam, on this machine's own self-hosted GitLab
\### Scenario 1 — a merge request on this machine's own self-hosted GitLab, GITLAB_HOST unset

$ env -u GITLAB_HOST python3 <ask the forge seam> <self-hosted host>

=== BASE 209a66c (before the change) ===
GitLabForge.hosts             : ('gitlab.com', 'www.gitlab.com')
for_repo(gitlab.self-hosted.example/acme/group/widgets)
   -> None: no configured forge owns gitlab.self-hosted.example
for_change_url(.../merge_requests/306)
   -> None: no configured forge owns gitlab.self-hosted.example

=== TARGET b4ee798 (this branch) ===
GitLabForge.hosts             : ('gitlab.com', 'www.gitlab.com', 'gitlab.self-hosted.example')
for_repo(gitlab.self-hosted.example/acme/group/widgets)
   -> GitLabForge
for_change_url(.../merge_requests/306)
   -> GitLabForge, change https://gitlab.self-hosted.example/acme/group/widgets/-/merge_requests/306
Evidence: The shell entrypoint AGENTS.md advertises, driven live
\### Scenario 1b — the shell entrypoint AGENTS.md advertises, on the real machine

$ env -u GITLAB_HOST python3 scripts/lib/forge.py hosts glab
gitlab.com
gitlab.self-hosted.example
  exit=0

$ python3 scripts/lib/forge.py hosts nosuchcli        # a CLI that is not installed is an ANSWER
  exit=0 (no output above)

$ python3 scripts/lib/forge.py                        # misuse
usage: forge.py hosts <gh|glab>
  exit=2

$ python3 scripts/lib/forge.py hosts gh               # gh is on this machine too
github.com
  exit=0
Evidence: collect / shepherd / reap, same task, base vs this branch
\### Scenario 2 — collect / shepherd / reap driven on the operator's own machine
#   one task, artifact = a merge request URL on this machine's self-hosted GitLab,
#   GITLAB_HOST unset everywhere, the real `glab` config as discovery input.
#   (the project acme/group/widgets does not exist there — 404 is the instance ANSWERING)

================ BASE 209a66c (before the change) ================
$ ./scripts/queue.sh collect            # BASE
    live-discovery/01-shipped  shipped  https://gitlab.self-hosted.example/acme/group/widgets/-/merge_requests/306  [publish unchecked: could not check — no configured forge owns gitlab.self-hosted.example]
        Shipped it.
collect: 1 result(s) read
    run log updated: /tmp/fleet-test-0j3Ckq/q-base-runs/2026-09-11-live-discovery.md

$ ./scripts/queue.sh shepherd --dry-run  # BASE
shepherd: no task names a repository on a configured forge yet

$ ./scripts/queue.sh reap --dry-run      # BASE
reap: nothing has landed and no finished task is still holding a session

================ TARGET b4ee798 (this branch) ===================
$ ./scripts/queue.sh collect            # TARGET
    live-discovery/01-shipped  shipped  https://gitlab.self-hosted.example/acme/group/widgets/-/merge_requests/306  [publish unchecked: could not check — glab mr view failed: failed to get merge request 306: 404 Not Found]
        Shipped it.
collect: 1 result(s) read
         1 of them left an open pull request. Run
         `queue.sh shepherd --dry-run` — a PR can go bad long
         after the worker that wrote it stopped.
    run log updated: /tmp/fleet-test-0j3Ckq/q-target-runs/2026-09-11-live-discovery.md

$ ./scripts/queue.sh shepherd --dry-run  # TARGET
shepherd: could not read the pull requests on acme/group/widgets on gitlab.self-hosted.example: 404 Not Found — nothing there was touched

$ ./scripts/queue.sh reap --dry-run      # TARGET
reap: nothing has landed and no finished task is still holding a session
Evidence: GITLAB_HOST precedence and five degraded glab machines, through the real collect
\### Scenarios 3 & 4 — the machines discovery must not break, driven through the real `collect`
#   each row: same task, same self-hosted merge-request URL, one machine's worth of glab
#   (acme/group/widgets does not exist on the instance, so '404 Not Found' means the CALL WAS MADE)

--- GITLAB_HOST names ANOTHER instance (it must still DECIDE)
    c/01-shipped  shipped  https://gitlab.self-hosted.example/acme/group/widgets/-/merge_requests/306  [publish unchecked: could not check — no configured forge owns gitlab.self-hosted.example]
collect: 1 result(s) read
    [collect exited 0 after 0s]
--- no glab on the machine at all
    c/01-shipped  shipped  https://gitlab.self-hosted.example/acme/group/widgets/-/merge_requests/306  [publish unchecked: could not check — no configured forge owns gitlab.self-hosted.example]
collect: 1 result(s) read
    [collect exited 0 after 0s]
--- glab whose config it cannot parse
    c/01-shipped  shipped  https://gitlab.self-hosted.example/acme/group/widgets/-/merge_requests/306  [publish unchecked: could not check — no configured forge owns gitlab.self-hosted.example]
collect: 1 result(s) read
    [collect exited 0 after 0s]
--- glab that answers something that is not a host list
    c/01-shipped  shipped  https://gitlab.self-hosted.example/acme/group/widgets/-/merge_requests/306  [publish unchecked: could not check — no configured forge owns gitlab.self-hosted.example]
collect: 1 result(s) read
    [collect exited 0 after 0s]
--- glab too old for --all (must still discover, via the bare form)
    c/01-shipped  shipped  https://gitlab.self-hosted.example/acme/group/widgets/-/merge_requests/306  [publish unchecked: could not check — glab mr view failed: gitlab.com]
collect: 1 result(s) read
    [collect exited 0 after 1s]
--- glab that never answers at all (timeout, paid once)
    c/01-shipped  shipped  https://gitlab.self-hosted.example/acme/group/widgets/-/merge_requests/306  [publish unchecked: could not check — no configured forge owns gitlab.self-hosted.example]
collect: 1 result(s) read
    [collect exited 0 after 10s]
Evidence: queue-selftest.sh section 14e transcript (test-harness coverage, not a live drive)
  ok    the probe proves a GitLab host against GitLab's own welcome
  ok    and it asked THAT host, not github.com
  ok    a self-hosted instance on a port is asked on that port
  ok    GitHub's own banner still passes, unchanged
  ok    a repo whose origin cannot be read says THAT, not 'no credentials'
  ok    both instances in the recording are read out of it
  ok    so a self-hosted instance is ours with no GITLAB_HOST set
  ok    and its merge request is a change request fleet can be asked about
  ok    an instance nothing on this machine holds is still not ours
  ok    GITLAB_HOST set is still the answer, scheme and all
  ok    and it DECIDES: an instance glab holds is not ours while it is set
  ok    a machine with no glab at all is exactly where it was
  ok    nor does a configuration glab cannot read move it
  ok    nor a glab that answers something that is not a host list
  ok    a glab too old for --all is asked again without it
run log opened: /tmp/tmp.uQ2JpA8yYv/runs/2026-09-11-discovered.md
  ok    collect verifies a publish on an instance only glab knew about
  ok    and does not report it as on no configured forge
  ok    which is not the same as landing it
  ok    shepherd lists what is open on the discovered instance
  ok    and a mergeable, attested one there is handed back, not merged
  ok    because the merge set is exactly the three repos it always was
  ok    so nothing on a discovered host was merged
  ok    reap lands a task on a discovered host
  ok    and releases the session that used to leak with it
  ok    no code path ran `gh` while the host came from glab
  ok    a title that cannot become a session name is refused at add
  ok    and the refusal names the offending character
  ok    and says it is the SESSION NAME that cannot carry it
  ok    and quotes the name thurbox would have been asked to create
  ok    and creates nothing, so the repair is one re-run and not an edit
Evidence: The same selftest against the pre-fix forge.py — 7 failures (test-harness coverage, not a live drive)
\### Scenario 5 — §14e is a real regression test: the SAME selftest against the pre-fix forge.py
$ ./scripts/queue-selftest.sh     # target tree, scripts/lib/forge.py from 209a66c

679:  FAIL  collect verifies a publish on an instance only glab knew about
688:  FAIL  and does not report it as on no configured forge
698:  FAIL  shepherd lists what is open on the discovered instance
702:  FAIL  and a mergeable, attested one there is handed back, not merged
706:  FAIL  because the merge set is exactly the three repos it always was
711:  FAIL  reap lands a task on a discovered host
716:  FAIL  and releases the session that used to leak with it

(target tree, unmodified: exit 0, 'queue-selftest: every claim holds')
Evidence: GitHub half untouched, gh tripwire never fired
\### Scenario 6 — the GitHub half is untouched, and no code path reaches gh for a GitLab host

$ PATH=<tripwire gh first> ./scripts/queue.sh collect   # artifact on the self-hosted GitLab
    g/01-shipped  shipped  https://gitlab.self-hosted.example/acme/group/widgets/-/merge_requests/306  [publish unchecked: could not check — glab mr view failed: failed to get merge request 306: 404 Not Found]
collect: 1 result(s) read
gh invocations recorded: 0

$ which forge owns github.com/Thurbeen/fleet, and what does GH_HOST still do?
  for_repo(github.com/Thurbeen/fleet)  -> GitHubForge
  GitHubForge.hosts, GH_HOST unset     -> ('github.com', 'www.github.com')
  GitHubForge.hosts, GH_HOST set       -> ('github.com', 'www.github.com', 'github.acme.example')
Evidence: The defect and its fix, one task, one machine, GITLAB_HOST unset
BASE 209a66c
$ ./scripts/queue.sh collect
.../01-shipped shipped https://gitlab.self-hosted.example/... [publish unchecked: could not check — no configured forge owns gitlab.self-hosted.example]
$ ./scripts/queue.sh shepherd --dry-run
shepherd: no task names a repository on a configured forge yet

TARGET b4ee798
$ ./scripts/queue.sh collect
.../01-shipped shipped https://gitlab.self-hosted.example/... [publish unchecked: could not check — glab mr view failed: failed to get merge request 306: 404 Not Found]
1 of them left an open pull request. Run `queue.sh shepherd --dry-run`
$ ./scripts/queue.sh shepherd --dry-run
shepherd: could not read the pull requests on acme/group/widgets on gitlab.self-hosted.example: 404 Not Found — nothing there was touched

(the project acme/group/widgets does not exist on that instance, so the 404 is the
self-hosted instance ANSWERING — the call now reaches it at all)
- Outcome: ⚠️ 1 info across 1 run (18m8s)

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.

⚠️ **Review** - 1 info
  • ⚠️ scripts/lib/forge.py:278 - scripts/lib/forge.py:278-280 — the IT NEVER FAILS paragraph 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: a glab stub that refuses --all with unknown flag: --all and then prints the recording is expected to end with owns_host(&#34;gitlab.example.com&#34;) == 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: drop or a CLI too old for --all from 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 ## Refreshing section still says "Re-run the commands above", and "the commands above" now includes the new auth-status.stderr table 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 the mr-* 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 the mr-* commands above" and note that auth-status.stderr is re-recorded by hand from a machine that has the instances, then sanitized as its bullet describes.
⚠️ **Test** - 1 info
  • ℹ️ 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; if forge.configured_hosts is 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.
  • Live validation: ✅ go - 5 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An operator whose glab is logged in to a self-hosted GitLab, with GITLAB_HOST unset, has that instance owned by the GitLab adapter — and did not before ✅ pass live 01-discovery-before-after.txt and 01b-hosts-entrypoint.txt — base build: hosts ('gitlab.com','www.gitlab.com'), for_repo → 'no configured forge owns <host>'; this branch: the self-hosted host is in ho…
collect, shepherd and reap on a task whose artifact is a merge request on that self-hosted instance ✅ pass live 02-queue-cli-before-after.txt — base prints 'publish unchecked: could not check — no configured forge owns <host>' and 'shepherd: no task names a repository on a configured forge yet'; this branch act…
GITLAB_HOST still decides: with it pointed at another instance, a host glab holds is not ours and glab is not consulted ✅ pass live 03-degraded-machines.txt, first row — 'no configured forge owns <self-hosted>', collect exits 0 in under a second (no glab subprocess)
Adversarial: five machines discovery must survive — no glab, an unparseable config, prose instead of a host list, a glab too old for --all, and a glab that never answers ✅ pass live 03-degraded-machines.txt — the first three degrade to exactly the pre-change answer with collect exiting 0; the too-old one still discovers the host via the bare auth status and the publish check ru…
The whole loop on a discovered host: collect verifies the publish, shepherd lists what is open, reap lands the task and releases the session that used to leak ⏸️ untested no The prior payload did not establish a live result for this: it cited only §14e of ./scripts/queue-selftest.sh, which replays a recorded glab auth status and a fake thurbox rather than driving the pr…
Adversarial: a green, attested, mergeable merge request on a discovered host must be reported and never merged — discovery must not widen AUTO_MERGE_REPOS ⏸️ untested no The prior payload did not establish a live result for this: its evidence was §14e fixture assertions, not fleet offered a real mergeable merge request. Proving it live requires an open MR on a real se…
§14e is a real regression test: the same selftest fails on the pre-fix forge ⏸️ untested no The prior payload did not establish a live result for this: it is the repository's own test script run twice against two builds, which is test-harness behaviour and not the product driven by an end us…
The GitHub half is untouched and nothing reaches gh while the host came from glab ✅ pass live 06-github-untouched.txt — a tripwire gh first on PATH records 0 invocations during a collect on the self-hosted artifact; for_repo(github.com/Thurbeen/fleet) is still GitHubForge and GH_HOST still add…
  • ./scripts/check.sh
  • env -u GITLAB_HOST python3 scripts/lib/forge.py hosts glab — the advertised shell entrypoint, against this machine's real glab config (also hosts gh, hosts nosuchcli, and misuse → exit 2)
  • ./scripts/queue.sh topic add + add + collect + shepherd --dry-run + reap --dry-run on 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 carrying git show 209a66c:scripts/lib/forge.py
  • GITLAB_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 collect under 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.sh against a tree with the pre-fix scripts/lib/forge.py — expected and got 7 §14e FAILs, exit 1
  • PATH=&lt;tripwire gh first&gt; ./scripts/queue.sh collect on the self-hosted artifact — 0 gh invocations recorded; for_repo(github.com/Thurbeen/fleet) and GH_HOST behaviour unchanged
⚠️ **Document** - 1 info
  • ℹ️ scripts/preflight.sh:215 - scripts/preflight.sh still reports glab auth as a single row and still hints GITLAB_HOST=... for a self-hosted instance, which is now a weaker story than the code tells — forge.configured_hosts reads the instances off glab itself. Left as-is on purpose: the stated intent scopes preflight out and names another task in the same topic as its owner, with configured_hosts as 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.

…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.
@LeTuR
LeTuR merged commit 50c70e0 into main Sep 11, 2026
11 checks passed
@LeTuR
LeTuR deleted the feat/discover-the-forge-hosts branch September 11, 2026 17:18
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