Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions pkg/cli/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,23 @@ func getGitHubHost() string {
}

// getGitHubHostForRepo returns the GitHub host URL for a specific repository.
// The gh-aw repository (github/gh-aw) always uses public GitHub (https://github.com)
// regardless of enterprise GitHub host settings, since gh-aw itself is only available
// on public GitHub. For all other repositories, it uses getGitHubHost().
// The gh-aw repository (github/gh-aw) and the agentics workflow library
// (githubnext/agentics) always use public GitHub (https://github.com)
// regardless of enterprise GitHub host settings, since these repositories are
// only available on public GitHub. For all other repositories, it uses getGitHubHost().
func getGitHubHostForRepo(repo string) string {
// The gh-aw repository is always on public GitHub
if repo == "github/gh-aw" || strings.HasPrefix(repo, "github/gh-aw/") {
githubLog.Print("Using public GitHub host for github/gh-aw repository")
return string(constants.PublicGitHubHost)
}

// The agentics workflow library is always on public GitHub
if repo == "githubnext/agentics" || strings.HasPrefix(repo, "githubnext/agentics/") {
githubLog.Print("Using public GitHub host for githubnext/agentics repository")
return string(constants.PublicGitHubHost)
}
Comment on lines 38 to +54
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getGitHubHostForRepo now treats githubnext/agentics as always hosted on public GitHub, but there are no unit tests covering this function (pkg/cli/github_test.go only tests getGitHubHost). Add tests that set GH_HOST to a GHE host and verify getGitHubHostForRepo("githubnext/agentics") (and prefixed paths) returns constants.PublicGitHubHost while non-allowlisted repos return the configured enterprise host.

Copilot uses AI. Check for mistakes.

// For all other repositories, use the configured GitHub host
return getGitHubHost()
}
66 changes: 66 additions & 0 deletions pkg/cli/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,69 @@ func TestGetGitHubHost(t *testing.T) {
})
}
}

func TestGetGitHubHostForRepo(t *testing.T) {
tests := []struct {
name string
repo string
gheHost string
expectedHost string
}{
{
name: "non-allowlisted repo uses configured public host",
repo: "owner/repo",
gheHost: "",
expectedHost: "https://github.com",
},
{
name: "non-allowlisted repo uses GHE host",
repo: "owner/repo",
gheHost: "myorg.ghe.com",
expectedHost: "https://myorg.ghe.com",
},
{
name: "github/gh-aw always uses public GitHub",
repo: "github/gh-aw",
gheHost: "myorg.ghe.com",
expectedHost: "https://github.com",
},
{
name: "github/gh-aw with path always uses public GitHub",
repo: "github/gh-aw/workflows/ci-doctor.md",
gheHost: "myorg.ghe.com",
expectedHost: "https://github.com",
},
{
name: "githubnext/agentics always uses public GitHub",
repo: "githubnext/agentics",
gheHost: "myorg.ghe.com",
expectedHost: "https://github.com",
},
{
name: "githubnext/agentics with path always uses public GitHub",
repo: "githubnext/agentics/workflows/daily-plan.md",
gheHost: "myorg.ghe.com",
expectedHost: "https://github.com",
},
{
name: "githubnext/agentics without GHE host uses public GitHub",
repo: "githubnext/agentics",
gheHost: "",
expectedHost: "https://github.com",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("GITHUB_SERVER_URL", "")
t.Setenv("GITHUB_ENTERPRISE_HOST", tt.gheHost)
t.Setenv("GITHUB_HOST", "")
t.Setenv("GH_HOST", "")

host := getGitHubHostForRepo(tt.repo)
if host != tt.expectedHost {
t.Errorf("getGitHubHostForRepo(%q) = %q, want %q", tt.repo, host, tt.expectedHost)
}
})
}
}
20 changes: 18 additions & 2 deletions pkg/cli/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,16 +293,31 @@ func parseWorkflowSpec(spec string) (*WorkflowSpec, error) {
return nil, fmt.Errorf("invalid workflow specification: '%s/%s' does not look like a valid GitHub repository", owner, repo)
}

repoSlug := fmt.Sprintf("%s/%s", owner, repo)

// Determine the API host for this repo. getGitHubHostForRepo returns the canonical
// host, which for well-known public-only repos (githubnext/agentics, github/gh-aw)
// is always public GitHub regardless of GHE configuration. If the repo's canonical
// host differs from the configured host, record the explicit hostname so API fetches
// target the correct server.
var explicitHost string
if repoHost := getGitHubHostForRepo(repoSlug); repoHost != getGitHubHost() {
if u, parseErr := url.Parse(repoHost); parseErr == nil && u.Host != "" {
explicitHost = u.Host
}
}

// Check if this is a wildcard specification (owner/repo/*)
if workflowPath == "*" {
return &WorkflowSpec{
RepoSpec: RepoSpec{
RepoSlug: fmt.Sprintf("%s/%s", owner, repo),
RepoSlug: repoSlug,
Version: version,
},
WorkflowPath: "*",
WorkflowName: "*",
IsWildcard: true,
Host: explicitHost,
}, nil
}

Expand All @@ -321,11 +336,12 @@ func parseWorkflowSpec(spec string) (*WorkflowSpec, error) {

return &WorkflowSpec{
RepoSpec: RepoSpec{
RepoSlug: fmt.Sprintf("%s/%s", owner, repo),
RepoSlug: repoSlug,
Version: version,
},
WorkflowPath: workflowPath,
WorkflowName: strings.TrimSuffix(filepath.Base(workflowPath), ".md"),
Host: explicitHost,
}, nil
}

Expand Down
87 changes: 87 additions & 0 deletions pkg/cli/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,93 @@ func TestParseWorkflowSpec(t *testing.T) {
}
}

// TestParseWorkflowSpecGHEHostPinning verifies that well-known public-only repos
// (githubnext/agentics, github/gh-aw) always get Host pinned to "github.com"
// when a GHE environment is detected, while other repos use an empty host.
func TestParseWorkflowSpecGHEHostPinning(t *testing.T) {
tests := []struct {
name string
spec string
wantHost string
wantNoHost bool // expect empty host
}{
{
name: "githubnext/agentics three-part spec gets github.com in GHE mode",
spec: "githubnext/agentics/daily-plan",
wantHost: "github.com",
},
{
name: "githubnext/agentics wildcard gets github.com in GHE mode",
spec: "githubnext/agentics/*",
wantHost: "github.com",
},
{
name: "github/gh-aw three-part spec gets github.com in GHE mode",
spec: "github/gh-aw/my-workflow",
wantHost: "github.com",
},
{
name: "non-allowlisted repo has empty host in GHE mode",
spec: "owner/repo/workflow",
wantNoHost: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Simulate a GHE environment
t.Setenv("GITHUB_SERVER_URL", "")
t.Setenv("GITHUB_ENTERPRISE_HOST", "myorg.ghe.com")
t.Setenv("GITHUB_HOST", "")
t.Setenv("GH_HOST", "")

spec, err := parseWorkflowSpec(tt.spec)
if err != nil {
t.Fatalf("parseWorkflowSpec(%q) unexpected error: %v", tt.spec, err)
}

if tt.wantNoHost {
if spec.Host != "" {
t.Errorf("parseWorkflowSpec(%q) host = %q, want empty", tt.spec, spec.Host)
}
} else {
if spec.Host != tt.wantHost {
t.Errorf("parseWorkflowSpec(%q) host = %q, want %q", tt.spec, spec.Host, tt.wantHost)
}
}
})
}
}

// TestParseWorkflowSpecNoGHEHostPinning verifies that on public github.com the
// Host field is always empty for short-form specs (no pinning needed).
func TestParseWorkflowSpecNoGHEHostPinning(t *testing.T) {
// Clear all GHE env vars to simulate standard github.com environment
t.Setenv("GITHUB_SERVER_URL", "")
t.Setenv("GITHUB_ENTERPRISE_HOST", "")
t.Setenv("GITHUB_HOST", "")
t.Setenv("GH_HOST", "")

specs := []string{
"githubnext/agentics/daily-plan",
"githubnext/agentics/*",
"github/gh-aw/my-workflow",
"owner/repo/workflow",
}

for _, s := range specs {
t.Run(s, func(t *testing.T) {
spec, err := parseWorkflowSpec(s)
if err != nil {
t.Fatalf("parseWorkflowSpec(%q) unexpected error: %v", s, err)
}
if spec.Host != "" {
t.Errorf("parseWorkflowSpec(%q) host = %q, want empty (no pinning on public GitHub)", s, spec.Host)
}
})
}
}

func TestParseLocalWorkflowSpec(t *testing.T) {
// Clear the repository slug cache to ensure clean test state
ClearCurrentRepoSlugCache()
Expand Down
Loading