A self-managed host connects, and says which half is wrong when it does not - #116
Merged
Merged
Conversation
…es not Two things, found by connecting a project on a corporate GitLab. Connecting one answered "Internal server error". `GitLabAdapter.api()` called fetch unguarded, and fetch rejects with a plain TypeError in exactly the two situations a self-managed instance produces: a bare host is not a URL (WHATWG reads it as a scheme), and an unreachable host fails at the transport with the reason on `cause.code`. A TypeError is not an HttpException, so it sailed past the controller's `instanceof VcsError` catch — the catch whose own docstring says it exists to stop "your token is wrong" reading as "Internal server error". gitlab.com always resolves, so the gap only ever opened on self-managed. `normalizeInstanceUrl` now reads a bare host as https and refuses what it cannot use, at the point the URL is stored so the complaint lands on the field somebody typed it into; `describeTransportFailure` turns cause.code into the sentence for that cause — VPN, DNS, refused, timeout, an untrusted internal CA pointed at NODE_EXTRA_CA_CERTS. The same guard goes on the GitHub adapter, which has the identical unguarded fetch and hides it behind api.github.com always being up. The instance URL keeps its path, deliberately. Stripping to the origin would be a kindness to someone pasting a project URL bought by breaking every GitLab served from a relative URL root, and only one of those is a deployment somebody chose. A pasted project URL instead 404s at the API base, and `describeApiBase404` says which half to drop. Local mode could not open a merge request on such a host at all: `detectHost` refuses to guess what software a self-managed host runs (0020, property 2), and `glab` is not on most corporate machines. But that reasoning forbids *guessing* the host, not being *told* it. So a local-mode project may now carry an optional review credential — provider, instance URL and token, on the `vcs` connection it already owns, which needed no migration because that row's encrypted_secret and settings.instanceUrl were both unused for provider 'local'. The token opens the review and nothing else: it never reads a file, lists a tree, clones or pushes, and git still pushes with the machine's own credentials. Absent, which is the default, local mode behaves exactly as before, down to the wording of the hint. It is proved at connect time with verify() on either adapter, the way JiraAdapter.verify() already worked, so a wrong token fails in the wizard rather than as a merge request that never appears. 0020 carries the amendment, including the honest version of the cost: local mode's promise narrows from "specd holds no credential for your host" to "unless you give it one, and then only to open reviews".
unitypark
force-pushed
the
feat/local-mode-reaches-a-self-managed-host
branch
from
August 17, 2026 16:29
bcb14b1 to
e957c43
Compare
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.
Found by connecting a project on a self-managed, corporate GitLab: a host that
is not gitlab.com, and a project two groups deep. Two problems, one root: specd could not reach a self-managed host, and would not
tell you why.
Grounded in
knowledge/decisions/0020-local-mode-borrows-the-host-cli.md(amended here) and
knowledge/glossary.md.1 · "Internal server error" on connect
GitLabAdapter.api()calledfetchunguarded.fetchrejects with a plainTypeErrorin exactly the two situations a self-managed instance produces —and a
TypeErroris not anHttpException, so Nest rendered both as a 500:It sailed straight past the controller's
instanceof VcsErrorcatch — thecatch whose own docstring says it exists so that "your token is wrong" never
reads as "Internal server error".
gitlab.comalways resolves, so the gaponly ever opened on self-managed.
normalizeInstanceUrlreads a bare host as https, strips a trailingslash, and refuses what it cannot use — applied where the URL is stored, so
the complaint lands on the field you just typed into rather than on the
repository listing one call later.
describeTransportFailureturnscause.codeinto the sentence for thatcause: VPN/DNS, connection refused, timeout, or an untrusted internal CA
pointed at
NODE_EXTRA_CA_CERTSrather than at disabling verification.GitHubAdapter.api(), which has the identicalunguarded
fetchand hides it behind api.github.com always being up. Fixingone instance of a bug and leaving its twin is not fixing the bug.
The instance URL keeps its path, on purpose
My first attempt reduced the URL to its origin, so a pasted project URL would
"just work". That was wrong and is reverted: GitLab supports being served from
a relative URL root (
external_url 'https://host/gitlab'), where the APIreally is at
{origin}/gitlab/api/v4. Stripping the path would trade adeployment somebody chose for a typo somebody made.
A pasted project URL instead 404s at the API base, and
describeApiBase404names which half to drop — while telling you to keep it if GitLab genuinely
lives on that subpath. Both readings are legitimate; only the operator knows
which is theirs.
2 · Local mode had no way to open a merge request on such a host
Two independent blockers:
detectHostrecognised only github.com andgitlab.com, and there was no field anywhere to supply a token in local mode.
Decision 0020 refused to guess a self-managed host's software from its URL,
because guessing wrong means running
ghagainst someone's private git server.That still holds — but it forbids guessing the host, not being told it. A
token both names the software and authorises the call, so the restriction lifts
exactly when a credential is present.
The local step now takes an optional review credential:
vcsconnection the project already owns —that row's
encrypted_secretandsettings.instanceUrlwere both unused forprovider: 'local'.settings.reviewProvideris the switch.verify()on either adapter (mirroringJiraAdapter.verify()), reporting the account it belongs to — so a bad tokenfails in the wizard, not as a merge request that never appears.
0020 specified, down to the wording of the hint.
The boundary
The token opens reviews and nothing else — it never reads a file, lists a
tree, clones, or pushes. Your own git credentials still do the push; specd
still reads the repository from disk. That narrowness is what makes it
tolerable in the mode whose promise is that specd holds nothing, and 0020's
amendment states the cost plainly rather than glossing it:
Verified against the shape that broke
A test pushes a branch to a real local bare repo, then opens the MR against a
stubbed API, asserting the request:
The nested group is percent-encoded whole — splitting it would address
acme/services, which is a group and not a project. The project path isderived from
origin, so it is never typed twice, and a subpath-hostedinstance has its prefix removed. The same shapes are covered for GitLab mode,
including that
verify()hits the configured instance rather than gitlab.com.pnpm typecheck,pnpm test(473 API tests),pnpm buildandpnpm site:checkall pass. 15 new tests.
For the reviewer
stubbed transports and reproduced failure modes, not a real corporate GitLab.
The failure paths are all soft — worst case is the previous branch-only
behaviour with a note — but a live check is worth doing.
glab's flags remain unverified from the earlier PR (Local mode opens a real PR, setup keeps your AGENTS.md, builds name their branch #115). Unchangedhere, and now less load-bearing: a configured credential bypasses the CLI
entirely, which is the path a corporate machine will actually take.