Skip to content

Commit af31d0b

Browse files
authored
Expand parser GitHub host unit coverage and harden host-repo assertions (#46926)
1 parent bdd171e commit af31d0b

7 files changed

Lines changed: 174 additions & 51 deletions

File tree

pkg/cli/engine_secrets.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -408,19 +408,6 @@ func buildGenericPATCreationURL() string {
408408
return buildPATCreationURL(nil)
409409
}
410410

411-
// isAnyGitHubHostEnvVarSet returns true when any of the environment variables
412-
// consumed by getGitHubHost() is explicitly set. When at least one is present
413-
// the caller has made an explicit host choice and the git-remote fallback should
414-
// not be consulted.
415-
func isAnyGitHubHostEnvVarSet() bool {
416-
for _, envVar := range []string{"GITHUB_SERVER_URL", "GITHUB_ENTERPRISE_HOST", "GITHUB_HOST", "GH_HOST"} {
417-
if os.Getenv(envVar) != "" { //nolint:osgetenvlibrary
418-
return true
419-
}
420-
}
421-
return false
422-
}
423-
424411
func buildPATCreationURL(values url.Values) string {
425412
hostURL := getGitHubHost()
426413
// Only consult the git remote when the caller has not made an explicit host

pkg/cli/github.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ func getGitHubHostForRepo(repo string) string {
3333
// For all other repositories, use the configured GitHub host
3434
return getGitHubHost()
3535
}
36+
37+
// isAnyGitHubHostEnvVarSet returns true when at least one of the environment
38+
// variables consulted by getGitHubHost is explicitly set to a non-empty value.
39+
// Delegates to parser.IsAnyGitHubHostEnvVarSet() for the shared implementation.
40+
func isAnyGitHubHostEnvVarSet() bool {
41+
return parser.IsAnyGitHubHostEnvVarSet()
42+
}

pkg/cli/import_url_fetcher.go

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"mime"
99
"net/http"
1010
"net/url"
11-
"os"
1211
"strings"
1312
"time"
1413

@@ -305,26 +304,22 @@ func logResponseBodyVerbose(resp *http.Response) {
305304
}
306305

307306
func importAuthGHHost() string {
308-
ghHost := os.Getenv("GH_HOST") //nolint:osgetenvlibrary
309-
if ghHost == "" {
307+
// Use unified resolution (GITHUB_SERVER_URL > GITHUB_ENTERPRISE_HOST > GITHUB_HOST > GH_HOST).
308+
// Return "" when no host env var is set so that callers do not add a
309+
// redundant entry for github.com, which is already in defaultImportAuthHosts.
310+
if !isAnyGitHubHostEnvVarSet() {
310311
return ""
311312
}
312-
// GH_HOST may carry a scheme prefix; extract just the hostname.
313-
if u, parseErr := url.Parse(ghHost); parseErr == nil && u.Host != "" {
314-
return strings.ToLower(u.Hostname())
315-
}
316-
// No scheme present — treat the whole value as a bare hostname (possibly
317-
// with port). Strip any accidental scheme prefix or trailing path.
318-
bare := strings.TrimPrefix(ghHost, "https://")
319-
bare = strings.TrimPrefix(bare, "http://")
320-
if idx := strings.IndexByte(bare, '/'); idx != -1 {
321-
bare = bare[:idx]
322-
}
323-
parsed, err := url.Parse("https://" + bare)
324-
if err == nil && parsed.Host != "" {
325-
return strings.ToLower(parsed.Hostname())
313+
// getGitHubHost (defined in this package) returns a normalized https://… URL;
314+
// url.Parse is used only to extract the hostname so that port numbers are stripped.
315+
resolved := getGitHubHost()
316+
u, parseErr := url.Parse(resolved)
317+
if parseErr != nil || u.Hostname() == "" {
318+
// getGitHubHost always returns a well-formed URL; an error here is unexpected.
319+
importURLFetcherLog.Printf("importAuthGHHost: unexpected url.Parse failure for %q: %v", resolved, parseErr)
320+
return ""
326321
}
327-
return strings.ToLower(bare)
322+
return strings.ToLower(u.Hostname())
328323
}
329324

330325
// sanitizeHTTPError strips the request URL from a *url.Error (the error type

pkg/cli/import_url_fetcher_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,9 @@ func TestAttachImportAuthHeader_DocsGitHub_NoToken(t *testing.T) {
336336
func TestAttachImportAuthHeader_GHE_BareHostname(t *testing.T) {
337337
t.Setenv("GITHUB_TOKEN", "ghe-token")
338338
t.Setenv("GH_TOKEN", "")
339+
t.Setenv("GITHUB_SERVER_URL", "")
340+
t.Setenv("GITHUB_ENTERPRISE_HOST", "")
341+
t.Setenv("GITHUB_HOST", "")
339342
t.Setenv("GH_HOST", "ghe.example.com")
340343

341344
req, _ := http.NewRequest(http.MethodGet, "https://ghe.example.com/owner/repo/raw/main/wf.md", nil)
@@ -347,6 +350,9 @@ func TestAttachImportAuthHeader_GHE_BareHostname(t *testing.T) {
347350
func TestAttachImportAuthHeader_GHE_HTTPSScheme(t *testing.T) {
348351
t.Setenv("GITHUB_TOKEN", "ghe-token")
349352
t.Setenv("GH_TOKEN", "")
353+
t.Setenv("GITHUB_SERVER_URL", "")
354+
t.Setenv("GITHUB_ENTERPRISE_HOST", "")
355+
t.Setenv("GITHUB_HOST", "")
350356
t.Setenv("GH_HOST", "https://ghe.example.com")
351357

352358
req, _ := http.NewRequest(http.MethodGet, "https://ghe.example.com/owner/repo/raw/main/wf.md", nil)
@@ -358,6 +364,9 @@ func TestAttachImportAuthHeader_GHE_HTTPSScheme(t *testing.T) {
358364
func TestAttachImportAuthHeader_GHE_HTTPSchemePrefix(t *testing.T) {
359365
t.Setenv("GITHUB_TOKEN", "ghe-token")
360366
t.Setenv("GH_TOKEN", "")
367+
t.Setenv("GITHUB_SERVER_URL", "")
368+
t.Setenv("GITHUB_ENTERPRISE_HOST", "")
369+
t.Setenv("GITHUB_HOST", "")
361370
t.Setenv("GH_HOST", "http://ghe.example.com")
362371

363372
// HTTPS request → token sent.
@@ -375,6 +384,9 @@ func TestAttachImportAuthHeader_GHE_HTTPSchemePrefix(t *testing.T) {
375384
func TestAttachImportAuthHeader_GHE_DifferentHost(t *testing.T) {
376385
t.Setenv("GITHUB_TOKEN", "ghe-token")
377386
t.Setenv("GH_TOKEN", "")
387+
t.Setenv("GITHUB_SERVER_URL", "")
388+
t.Setenv("GITHUB_ENTERPRISE_HOST", "")
389+
t.Setenv("GITHUB_HOST", "")
378390
t.Setenv("GH_HOST", "ghe.example.com")
379391

380392
req, _ := http.NewRequest(http.MethodGet, "https://other.example.com/workflow.md", nil)
@@ -386,13 +398,58 @@ func TestAttachImportAuthHeader_GHE_DifferentHost(t *testing.T) {
386398
func TestAttachImportAuthHeader_GitHubAlongsideGHE(t *testing.T) {
387399
t.Setenv("GITHUB_TOKEN", "dual-token")
388400
t.Setenv("GH_TOKEN", "")
401+
t.Setenv("GITHUB_SERVER_URL", "")
402+
t.Setenv("GITHUB_ENTERPRISE_HOST", "")
403+
t.Setenv("GITHUB_HOST", "")
389404
t.Setenv("GH_HOST", "ghe.example.com")
390405

391406
req, _ := http.NewRequest(http.MethodGet, "https://github.com/owner/repo/raw/main/wf.md", nil)
392407
attachImportAuthHeader(req, "https://github.com/owner/repo/raw/main/wf.md")
393408
assert.Equal(t, "Bearer dual-token", req.Header.Get("Authorization"), "github.com must still be allowed when GH_HOST is also set")
394409
}
395410

411+
// GITHUB_ENTERPRISE_HOST resolves the auth host (unified resolution).
412+
func TestAttachImportAuthHeader_GHE_EnterpriseHostEnvVar(t *testing.T) {
413+
t.Setenv("GITHUB_TOKEN", "ent-token")
414+
t.Setenv("GH_TOKEN", "")
415+
t.Setenv("GITHUB_SERVER_URL", "")
416+
t.Setenv("GITHUB_ENTERPRISE_HOST", "ent.example.com")
417+
t.Setenv("GITHUB_HOST", "other.example.com")
418+
t.Setenv("GH_HOST", "gh.example.com")
419+
420+
req, _ := http.NewRequest(http.MethodGet, "https://ent.example.com/owner/repo/raw/main/wf.md", nil)
421+
attachImportAuthHeader(req, "https://ent.example.com/owner/repo/raw/main/wf.md")
422+
assert.Equal(t, "Bearer ent-token", req.Header.Get("Authorization"), "GITHUB_ENTERPRISE_HOST must resolve as the auth host")
423+
}
424+
425+
// GITHUB_SERVER_URL resolves the auth host (highest priority).
426+
func TestAttachImportAuthHeader_GHE_ServerURLEnvVar(t *testing.T) {
427+
t.Setenv("GITHUB_TOKEN", "srv-token")
428+
t.Setenv("GH_TOKEN", "")
429+
t.Setenv("GITHUB_SERVER_URL", "https://srv.example.com")
430+
t.Setenv("GITHUB_ENTERPRISE_HOST", "ent.example.com")
431+
t.Setenv("GITHUB_HOST", "")
432+
t.Setenv("GH_HOST", "")
433+
434+
req, _ := http.NewRequest(http.MethodGet, "https://srv.example.com/owner/repo/raw/main/wf.md", nil)
435+
attachImportAuthHeader(req, "https://srv.example.com/owner/repo/raw/main/wf.md")
436+
assert.Equal(t, "Bearer srv-token", req.Header.Get("Authorization"), "GITHUB_SERVER_URL must resolve as the auth host")
437+
}
438+
439+
// GITHUB_HOST resolves the auth host (third-highest priority, above GH_HOST).
440+
func TestAttachImportAuthHeader_GHE_GitHubHostEnvVar(t *testing.T) {
441+
t.Setenv("GITHUB_TOKEN", "gh-host-token")
442+
t.Setenv("GH_TOKEN", "")
443+
t.Setenv("GITHUB_SERVER_URL", "")
444+
t.Setenv("GITHUB_ENTERPRISE_HOST", "")
445+
t.Setenv("GITHUB_HOST", "ghhost.example.com")
446+
t.Setenv("GH_HOST", "lowpriority.example.com")
447+
448+
req, _ := http.NewRequest(http.MethodGet, "https://ghhost.example.com/owner/repo/raw/main/wf.md", nil)
449+
attachImportAuthHeader(req, "https://ghhost.example.com/owner/repo/raw/main/wf.md")
450+
assert.Equal(t, "Bearer "+"gh-host-token", req.Header.Get("Authorization"), "GITHUB_HOST must resolve as the auth host")
451+
}
452+
396453
// TestBuildRequestLogString_RedactsAuthorization verifies that the request formatter
397454
// never exposes the raw token and shows the correct redacted form.
398455
func TestBuildRequestLogString_RedactsAuthorization(t *testing.T) {

pkg/cli/init.go

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -337,32 +337,25 @@ func isGHESHost(host string) bool {
337337
// detectGHESDeployment returns the GHES host if the current repository's git
338338
// remote points to a GitHub Enterprise Server instance, or "" if it does not.
339339
// Detection uses the following sources in priority order:
340-
// 1. GITHUB_SERVER_URL environment variable (set automatically inside GitHub Actions)
341-
// 2. GH_HOST environment variable (set by the gh CLI)
342-
// 3. The hostname extracted from the git origin remote URL
340+
// 1. GITHUB_SERVER_URL, GITHUB_ENTERPRISE_HOST, GITHUB_HOST, GH_HOST environment variables
341+
// 2. The hostname extracted from the git origin remote URL
343342
func detectGHESDeployment() string {
344-
// Check GITHUB_SERVER_URL first (set inside GitHub Actions runners)
345-
if serverURL := os.Getenv("GITHUB_SERVER_URL"); serverURL != "" { //nolint:osgetenvlibrary
346-
// serverURL is like "https://ghes.example.com", extract just the host.
347-
host := serverURL
348-
for _, scheme := range []string{"https://", "http://"} {
349-
host = strings.TrimPrefix(host, scheme)
343+
// Check env vars in unified priority order (mirrors GetGitHubHost):
344+
// GITHUB_SERVER_URL > GITHUB_ENTERPRISE_HOST > GITHUB_HOST > GH_HOST
345+
for _, envVar := range []string{"GITHUB_SERVER_URL", "GITHUB_ENTERPRISE_HOST", "GITHUB_HOST", "GH_HOST"} {
346+
rawValue := os.Getenv(envVar) //nolint:osgetenvlibrary
347+
if rawValue == "" {
348+
continue
350349
}
350+
host := strings.TrimPrefix(rawValue, "https://")
351+
host = strings.TrimPrefix(host, "http://")
351352
host = strings.TrimSuffix(host, "/")
352353
if isGHESHost(host) {
353-
initLog.Printf("Detected GHES deployment from GITHUB_SERVER_URL: %s", host)
354+
initLog.Printf("Detected GHES deployment from %s: %s", envVar, host)
354355
return host
355356
}
356357
}
357358

358-
// Check GH_HOST (set when using the gh CLI against an enterprise instance)
359-
if ghHost := os.Getenv("GH_HOST"); ghHost != "" { //nolint:osgetenvlibrary
360-
if isGHESHost(ghHost) {
361-
initLog.Printf("Detected GHES deployment from GH_HOST: %s", ghHost)
362-
return ghHost
363-
}
364-
}
365-
366359
// Fall back to detecting the host from the git origin remote
367360
host := getHostFromOriginRemote()
368361
if isGHESHost(host) {

pkg/parser/github.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ func GetGitHubHost() string {
4040
return defaultHost
4141
}
4242

43+
// IsAnyGitHubHostEnvVarSet returns true when at least one of the environment
44+
// variables consulted by GetGitHubHost is explicitly set to a non-empty value.
45+
// This indicates the caller has made an explicit host choice and automatic
46+
// fallback heuristics (such as git-remote detection) should not be consulted.
47+
func IsAnyGitHubHostEnvVarSet() bool {
48+
for _, envVar := range []string{"GITHUB_SERVER_URL", "GITHUB_ENTERPRISE_HOST", "GITHUB_HOST", "GH_HOST"} {
49+
if os.Getenv(envVar) != "" { //nolint:osgetenvlibrary
50+
return true
51+
}
52+
}
53+
return false
54+
}
55+
4356
// GetGitHubHostForRepo returns the GitHub host URL for a specific repository.
4457
// Repositories under the github, githubnext, and microsoft organizations are
4558
// fetched from public GitHub (https://github.com) in cross-host contexts.

pkg/parser/github_host_test.go

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,73 @@ package parser
55
import (
66
"testing"
77

8-
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
99
)
1010

11+
func TestGetGitHubHost(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
serverURL string
15+
enterpriseHost string
16+
githubHost string
17+
ghHost string
18+
expectedHost string
19+
}{
20+
{
21+
name: "GITHUB_SERVER_URL wins over others",
22+
serverURL: "acme.ghe.com/redacted",
23+
enterpriseHost: "enterprise.ghe.com",
24+
githubHost: "github-host.ghe.com",
25+
ghHost: "gh-host.ghe.com",
26+
expectedHost: "https://acme.ghe.com/redacted",
27+
},
28+
{
29+
name: "GITHUB_ENTERPRISE_HOST wins over GITHUB_HOST and GH_HOST",
30+
serverURL: "",
31+
enterpriseHost: "acme.ghe.com",
32+
githubHost: "github-host.ghe.com",
33+
ghHost: "gh-host.ghe.com",
34+
expectedHost: "https://acme.ghe.com",
35+
},
36+
{
37+
name: "GITHUB_HOST wins over GH_HOST",
38+
serverURL: "",
39+
enterpriseHost: "",
40+
githubHost: "acme.ghe.com/",
41+
ghHost: "gh-host.ghe.com",
42+
expectedHost: "https://acme.ghe.com",
43+
},
44+
{
45+
name: "GH_HOST used when others are empty",
46+
serverURL: "",
47+
enterpriseHost: "",
48+
githubHost: "",
49+
ghHost: "acme.ghe.com",
50+
expectedHost: "https://acme.ghe.com",
51+
},
52+
{
53+
name: "all vars empty falls back to github.com",
54+
serverURL: "",
55+
enterpriseHost: "",
56+
githubHost: "",
57+
ghHost: "",
58+
expectedHost: "https://github.com",
59+
},
60+
}
61+
62+
for _, tt := range tests {
63+
t.Run(tt.name, func(t *testing.T) {
64+
t.Setenv("GITHUB_SERVER_URL", tt.serverURL)
65+
t.Setenv("GITHUB_ENTERPRISE_HOST", tt.enterpriseHost)
66+
t.Setenv("GITHUB_HOST", tt.githubHost)
67+
t.Setenv("GH_HOST", tt.ghHost)
68+
69+
host := GetGitHubHost()
70+
require.Equal(t, tt.expectedHost, host)
71+
})
72+
}
73+
}
74+
1175
func TestGetGitHubHostForRepo_PublicOrgFallback(t *testing.T) {
1276
tests := []struct {
1377
name string
@@ -23,6 +87,13 @@ func TestGetGitHubHostForRepo_PublicOrgFallback(t *testing.T) {
2387
gheHost: "myorg.ghe.com",
2488
expectedHost: "https://myorg.ghe.com",
2589
},
90+
{
91+
name: "empty gheHost falls back to public for non-fallback owner",
92+
owner: "acme",
93+
repo: "repo",
94+
gheHost: "",
95+
expectedHost: "https://github.com",
96+
},
2697
{
2798
name: "github owner uses public host",
2899
owner: "github",
@@ -54,7 +125,7 @@ func TestGetGitHubHostForRepo_PublicOrgFallback(t *testing.T) {
54125
t.Setenv("GH_HOST", "")
55126

56127
host := GetGitHubHostForRepo(tt.owner, tt.repo)
57-
assert.Equal(t, tt.expectedHost, host, "GetGitHubHostForRepo(%q, %q)", tt.owner, tt.repo)
128+
require.Equal(t, tt.expectedHost, host, "GetGitHubHostForRepo(%q, %q)", tt.owner, tt.repo)
58129
})
59130
}
60131
}

0 commit comments

Comments
 (0)