Skip to content

Commit a489664

Browse files
owenniblockCopilot
andcommitted
Route github.localhost to split-host URLs for local dotcom dev
A local dotcom checkout serves its APIs from api.github.localhost, uploads.github.localhost and raw.github.localhost — the same split-host shape as production. github.localhost didn't match the github.com or *.ghe.com branches in parseAPIHost though, so it fell through to newGHESHost, which then tried /api/v3 and /api/graphql against the apex and got 404/422. Add a github.localhost branch that mirrors the dotcom layout so the MCP server can talk to a local dotcom stack without anyone patching parseAPIHost by hand. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1654d32 commit a489664

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

pkg/utils/api.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,55 @@ func parseAPIHost(s string) (APIHost, error) {
239239
return newDotcomHost()
240240
}
241241

242+
if u.Hostname() == "github.localhost" || strings.HasSuffix(u.Hostname(), ".github.localhost") {
243+
return newLocalDevHost(u.Scheme)
244+
}
245+
242246
if u.Hostname() == "ghe.com" || strings.HasSuffix(u.Hostname(), ".ghe.com") {
243247
return newGHECHost(s)
244248
}
245249

246250
return newGHESHost(s)
247251
}
252+
253+
// newLocalDevHost handles a local dotcom checkout served from github.localhost.
254+
// Local dev mirrors production's split-host layout (api., uploads., raw.) but
255+
// uses a plain hostname that doesn't match the github.com / ghe.com branches,
256+
// so without this it falls through to newGHESHost and tries /api/v3 paths that
257+
// the local stack doesn't serve.
258+
func newLocalDevHost(scheme string) (APIHost, error) {
259+
const root = "github.localhost"
260+
261+
restURL, err := url.Parse(fmt.Sprintf("%s://api.%s/", scheme, root))
262+
if err != nil {
263+
return APIHost{}, fmt.Errorf("failed to parse local dev REST URL: %w", err)
264+
}
265+
266+
gqlURL, err := url.Parse(fmt.Sprintf("%s://api.%s/graphql", scheme, root))
267+
if err != nil {
268+
return APIHost{}, fmt.Errorf("failed to parse local dev GraphQL URL: %w", err)
269+
}
270+
271+
uploadURL, err := url.Parse(fmt.Sprintf("%s://uploads.%s/", scheme, root))
272+
if err != nil {
273+
return APIHost{}, fmt.Errorf("failed to parse local dev Upload URL: %w", err)
274+
}
275+
276+
rawURL, err := url.Parse(fmt.Sprintf("%s://raw.%s/", scheme, root))
277+
if err != nil {
278+
return APIHost{}, fmt.Errorf("failed to parse local dev Raw URL: %w", err)
279+
}
280+
281+
authorizationServerURL, err := url.Parse(fmt.Sprintf("%s://%s/login/oauth", scheme, root))
282+
if err != nil {
283+
return APIHost{}, fmt.Errorf("failed to parse local dev Authorization Server URL: %w", err)
284+
}
285+
286+
return APIHost{
287+
restURL: restURL,
288+
gqlURL: gqlURL,
289+
uploadURL: uploadURL,
290+
rawURL: rawURL,
291+
authorizationServerURL: authorizationServerURL,
292+
}, nil
293+
}

pkg/utils/api_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,21 @@ func TestParseAPIHost(t *testing.T) {
5454
input: "https://myghe.com",
5555
wantRestURL: "https://myghe.com/api/v3/",
5656
},
57+
{
58+
name: "github.localhost (local dotcom dev) routes to api.github.localhost",
59+
input: "http://github.localhost",
60+
wantRestURL: "http://api.github.localhost/",
61+
},
62+
{
63+
name: "subdomain of github.localhost also routes to api.github.localhost",
64+
input: "http://www.github.localhost",
65+
wantRestURL: "http://api.github.localhost/",
66+
},
67+
{
68+
name: "hostname ending in github.localhost but not a subdomain",
69+
input: "http://notgithub.localhost",
70+
wantRestURL: "http://notgithub.localhost/api/v3/",
71+
},
5772
{
5873
name: "missing scheme",
5974
input: "github.com",

0 commit comments

Comments
 (0)