Skip to content

[pull] trunk from cli:trunk - #327

Merged
pull[bot] merged 40 commits into
All-Blockchains:trunkfrom
cli:trunk
Sep 2, 2026
Merged

[pull] trunk from cli:trunk#327
pull[bot] merged 40 commits into
All-Blockchains:trunkfrom
cli:trunk

Conversation

@pull

@pullpullBot commented Sep 2, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

williammartinand others added 30 commits September 2, 2026 15:28
gist had no acceptance scripts, so its commands were never exercised
against a real host. Cover create, view and delete in one script, and
edit, rename and list in another.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
search-issues flakes because gh search reads a separate index that lags
issue creation, and five seconds was not reliably enough for it to catch
up.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The variable took a single script name, so running a chosen subset meant
one go test invocation per script. Accept a comma separated list and
select the ones belonging to the command directory under test.
A filter that matches nothing in a directory now skips that directory
rather than falling back to running all of it, so a mistyped script name
reports as a skip instead of silently passing a full run.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A call site that builds an absolute api.github.com URL and calls
httpClient.Do bypasses any central resolution, and no existing test
notices, because api.github.com is reachable from CI. That makes the
routing claim unfalsifiable.
This harness makes a bypass fail loudly. It runs the real gh binary
against a recording TLS reverse proxy, with api.github.com blackholed
inside the container, so a request that honours api_host reaches the
gateway and a request that ignores it cannot connect at all.
Results are recorded by name, and acceptance scripts run one per
invocation, so a change that fixes a single call site is visible as that
specific assertion turning green.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
go-gh gains per-host API endpoint overrides, so a host can route its API
traffic through a gateway. That immediately breaks authentication: gh
resolves tokens from the hostname in the request URL, and after the
override that hostname is the gateway, which gh has never logged in to
and holds no token for.
Map the gateway back to the host it stands in for and send that host's
token. The fallback only applies when the hostname has no token of its
own, so a host we do authenticate keeps resolving exactly as before and
an api_host mapping cannot hijack real credentials.
This is the credential half only. Requests still have to reach the
gateway to benefit, and most call sites do not yet.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
gh api builds its request URLs from the hostname directly rather than
going through the shared client, so a host's api_host had no effect on
it: gh api repos/cli/cli still went to api.github.com even when the host
was configured to route elsewhere.
Resolve api_host when building the URL for a relative path or graphql.
Absolute URLs are deliberately left alone, both because the user asked
for that exact URL and because paginating on a rewritten Link header
depends on following the gateway's own URLs unchanged.
This means api_host is now resolved in two places, which is a smell
worth being honest about rather than hiding: gh api takes its path
verbatim from the user, so it cannot use the shared client that resolves
api_host for everything else.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Being on api.Client is not the same as being routed. RenameRepo already
used client.REST, but handed it an absolute URL built from
ghinstance.RESTPrefix, so the host was decided before the client saw the
request and a configured api_host was ignored.
Pass a relative path and let the client resolve the host, as it does for
every other call. No new capability is needed here, only the removal of
a hardcoded prefix.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Many call sites could not move onto api.Client because REST decodes into
a receiver, and they need the response itself: a streaming body, a body
that is not JSON, a response header, or the status code of a success.
Having no way to express that, they built absolute api.github.com URLs
and called httpClient.Do, which decides the host before any central
resolution can apply.
Add Request and RequestWithContext, which return the response for the
caller to consume, and migrate those call sites. Non-2xx responses still
become an HTTPError, so callers only handle the success path.
Add UnexpectedStatusError for callers that require one specific status.
Since every non-2xx is already an error, such a caller can only be
surprised by a different 2xx, and handing a success to an error parser
would be wrong.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Some endpoints do not say which OAuth scopes they require, so gh supplies the
answer itself: it calls EndpointNeedsScopes on the response before turning it
into an error, which folds the scope into the suggestion the user is shown.
That only works while the call site holds the response. A call site that hands
request making to the shared client never sees a failed response, because the
client has already converted it into an error and closed the body. So the
scope has to travel with the request instead.
Add WithEndpointScopes, and apply it on the error path: the scope is added to
the error's headers before the suggestion is generated, which is the same
mechanism as before, moved to where the response still exists.
gh gist create is the first caller, and needed this to keep telling a user
with an under-scoped token that they are missing the gist scope.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6dd1d13e-90bd-44b8-8bec-261795e282c3
gh repo delete is the reason this option exists, and the reason it built its
own client rather than sharing one. Deleting a repository that has since been
renamed returns a 301, and Go's default redirect policy turns a DELETE into a
GET when it follows one. The user would be told the delete succeeded while
nothing had been deleted, so the command copied the http.Client, set
CheckRedirect on the copy, and made the request itself.
Making the request itself is also how it came to name api.github.com and
ignore a host's api_host. Add WithoutFollowingRedirects so the policy can be
stated per request, and the destination can go back to being the client's
business.
The option is deliberately REST-only. GraphQL does not meet redirects in
practice, and offering it there would suggest a guarantee that is not tested.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6dd1d13e-90bd-44b8-8bec-261795e282c3
The remaining call sites that build their own request do so for a header,
usually an Accept naming a preview media type or a raw representation. Building
the request also meant building the URL, which is how these came to name
api.github.com and ignore a host's api_host.
Add WithHeader, so a call site can say which header it needs without also
taking ownership of where the request goes. Headers set this way take
precedence over the transport's own, which is what a caller asking for a
specific representation means.
GraphQL takes options too, for one reason: gh auth login validates a token the
client has not been configured with yet, so it must pass an explicit
Authorization header.
This is the last of the three capabilities, and with it the gateway harness is
fully green.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6dd1d13e-90bd-44b8-8bec-261795e282c3
These two call sites are the last outside gh api that send a request through a
raw http.Client, and they resisted the shared request surface for a real
reason. Uploading an asset sets ContentLength and GetBody so a retry can rewind
the file, and neither is expressible as a method, path, body or header.
Add DoRequest, which takes a request the caller has built and applies the same
error handling as Request. It also sends the request through the client held by
api.Client, so a CheckRedirect set on that client survives, which Request
cannot promise because go-gh builds a client of its own from the transport.
This changes no behaviour. Both sites use absolute URLs the API returned, and
those URLs already point wherever the request that produced them went, so
routing was never wrong here. What changes is that api.Client is now the single
place a request leaves gh, so a later change to how a destination is resolved
reaches these two without anyone remembering they exist.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6dd1d13e-90bd-44b8-8bec-261795e282c3
Note that Go's default redirect policy rewrites any non-GET/HEAD method
(not just DELETE) to GET on 301/302/303, and document why repo delete
opts out of following redirects to avoid a phantom success.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
HostForAPIHost iterated c.Hosts() in undefined order, so when several
hosts share one api_host the winner was unstable. Sort a clone of the
host list first to make the first-match resolution deterministic.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
Replace errors.As plus a separate variable declaration with the generic
errors.AsType across the call sites introduced by the api client rollout.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
Add Request and RequestWithContext to the SafeURL path construction query so
their URL arguments must flow through safeurl. Exclude the client's own
internal delegation between these methods, which forwards the caller's already
checked path and would otherwise be reported as a hand built URL.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
…oken
Converge cli/cli's origin-comparison policy onto go-gh's host-only policy so
both token layers ignore the port when deciding whether a request is same-host.
go-gh compares req.URL.Hostname() while cli/cli previously compared the full
host including port, making the two layers inconsistent.
Rename getHost to getHostname and strip any port from the host so redirect
comparison, token lookup, and enterprise detection all key on the hostname
alone, matching how gh stores config host keys (auth login rejects a port).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Signed-off-by: Babak K. Shandiz <babakks@github.com>
Hold an api.Client on Uploader and post assets with DoRequest instead of
calling http.Client.Do directly. DoRequest is built for requests that must
set fields Request cannot express, such as ContentLength and GetBody when
uploading an asset, and it already turns a non-2xx response into an HTTPError,
so the hand-rolled status check and HandleHTTPError call are no longer needed.
NewUploader keeps its *http.Client parameter and wraps it, so callers are
unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
The go-gh per-host api_host branch was rebased onto its latest trunk, so
update the pin to its new HEAD.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb7763a9-2086-461a-91a4-86ea4a2d078d
@pullpullBot locked and limited conversation to collaborators Sep 2, 2026
@pullpullBot added the ⤵️ pull label Sep 2, 2026
@pull
pullBot merged commit adda317 into All-Blockchains:trunkSep 2, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@williammartin@babakks@sergiou87