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
5 changes: 5 additions & 0 deletions .changeset/patch-github-enterprise-host.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .changeset/patch-normalize-gh-host-detection.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .github/workflows/smoke-project.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .github/workflows/test-project-url-default.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion actions/setup/js/create_project_status_update.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function parseProjectUrl(projectUrl) {
throw new Error(`Invalid project input: expected string, got ${typeof projectUrl}. The "project" field is required and must be a full GitHub project URL.`);
}

const match = projectUrl.match(/github\.com\/(users|orgs)\/([^/]+)\/projects\/(\d+)/);
const match = projectUrl.match(/^https:\/\/[^/]+\/(users|orgs)\/([^/]+)\/projects\/(\d+)/);
if (!match) {
throw new Error(`Invalid project URL: "${projectUrl}". The "project" field must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/123).`);
}
Expand Down
4 changes: 2 additions & 2 deletions actions/setup/js/update_project.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function parseProjectInput(projectUrl) {
throw new Error(`Invalid project input: expected string, got ${typeof projectUrl}. The "project" field is required and must be a full GitHub project URL.`);
}

const urlMatch = projectUrl.match(/github\.com\/(?:users|orgs)\/[^/]+\/projects\/(\d+)/);
const urlMatch = projectUrl.match(/^https:\/\/[^/]+\/(?:users|orgs)\/[^/]+\/projects\/(\d+)/);
if (!urlMatch) {
throw new Error(`Invalid project URL: "${projectUrl}". The "project" field must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/123).`);
}
Expand All @@ -96,7 +96,7 @@ function parseProjectUrl(projectUrl) {
throw new Error(`Invalid project input: expected string, got ${typeof projectUrl}. The "project" field is required and must be a full GitHub project URL.`);
}

const match = projectUrl.match(/github\.com\/(users|orgs)\/([^/]+)\/projects\/(\d+)/);
const match = projectUrl.match(/^https:\/\/[^/]+\/(users|orgs)\/([^/]+)\/projects\/(\d+)/);
if (!match) {
throw new Error(`Invalid project URL: "${projectUrl}". The "project" field must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/123).`);
}
Expand Down
18 changes: 12 additions & 6 deletions pkg/cli/download_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ func resolveLatestReleaseViaGit(repo, currentRef string, allowMajor, verbose boo
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Fetching latest release for %s via git ls-remote (current: %s, allow major: %v)", repo, currentRef, allowMajor)))
}

repoURL := fmt.Sprintf("https://github.com/%s.git", repo)
githubHost := getGitHubHostForRepo(repo)
repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo)

// List all tags
cmd := exec.Command("git", "ls-remote", "--tags", repoURL)
Expand Down Expand Up @@ -102,7 +103,8 @@ func resolveLatestReleaseViaGit(repo, currentRef string, allowMajor, verbose boo
func isBranchRefViaGit(repo, ref string) (bool, error) {
downloadLog.Printf("Attempting git ls-remote to check if ref is branch: %s@%s", repo, ref)

repoURL := fmt.Sprintf("https://github.com/%s.git", repo)
githubHost := getGitHubHostForRepo(repo)
repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo)

// List all branches and check if ref matches
cmd := exec.Command("git", "ls-remote", "--heads", repoURL)
Expand Down Expand Up @@ -167,7 +169,8 @@ func resolveBranchHeadViaGit(repo, branch string, verbose bool) (string, error)
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Fetching latest commit for branch %s in %s via git ls-remote", branch, repo)))
}

repoURL := fmt.Sprintf("https://github.com/%s.git", repo)
githubHost := getGitHubHostForRepo(repo)
repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo)

// Get the SHA for the specific branch
cmd := exec.Command("git", "ls-remote", repoURL, fmt.Sprintf("refs/heads/%s", branch))
Expand Down Expand Up @@ -236,7 +239,8 @@ func resolveDefaultBranchHeadViaGit(repo string, verbose bool) (string, error) {
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Fetching default branch for %s via git ls-remote", repo)))
}

repoURL := fmt.Sprintf("https://github.com/%s.git", repo)
githubHost := getGitHubHostForRepo(repo)
repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo)

// Get HEAD to find default branch
cmd := exec.Command("git", "ls-remote", "--symref", repoURL, "HEAD")
Expand Down Expand Up @@ -326,7 +330,8 @@ func downloadWorkflowContentViaGit(repo, path, ref string, verbose bool) ([]byte
downloadLog.Printf("Attempting git fallback for downloading workflow content: %s/%s@%s", repo, path, ref)

// Use git archive to get the file content without cloning
repoURL := fmt.Sprintf("https://github.com/%s.git", repo)
githubHost := getGitHubHostForRepo(repo)
repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo)

// git archive command: git archive --remote=<repo> <ref> <path>
cmd := exec.Command("git", "archive", "--remote="+repoURL, ref, path)
Expand Down Expand Up @@ -366,7 +371,8 @@ func downloadWorkflowContentViaGitClone(repo, path, ref string, verbose bool) ([
}
defer os.RemoveAll(tmpDir)

repoURL := fmt.Sprintf("https://github.com/%s.git", repo)
githubHost := getGitHubHostForRepo(repo)
repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo)

// Initialize git repository
initCmd := exec.Command("git", "-C", tmpDir, "init")
Expand Down
17 changes: 11 additions & 6 deletions pkg/cli/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,27 @@ func findGitRootForPath(path string) (string, error) {

// parseGitHubRepoSlugFromURL extracts owner/repo from a GitHub URL
// Supports both HTTPS (https://github.com/owner/repo) and SSH (git@github.com:owner/repo) formats
// Also supports GitHub Enterprise URLs
func parseGitHubRepoSlugFromURL(url string) string {
gitLog.Printf("Parsing GitHub repo slug from URL: %s", url)

// Remove .git suffix if present
url = strings.TrimSuffix(url, ".git")

// Handle HTTPS URLs: https://github.com/owner/repo
if strings.HasPrefix(url, "https://github.com/") {
slug := strings.TrimPrefix(url, "https://github.com/")
githubHost := getGitHubHost()
githubHostWithoutScheme := strings.TrimPrefix(strings.TrimPrefix(githubHost, "https://"), "http://")

// Handle HTTPS URLs: https://github.com/owner/repo or https://enterprise.github.com/owner/repo
if strings.HasPrefix(url, githubHost+"/") {
slug := strings.TrimPrefix(url, githubHost+"/")
gitLog.Printf("Extracted slug from HTTPS URL: %s", slug)
return slug
}

// Handle SSH URLs: git@github.com:owner/repo
if strings.HasPrefix(url, "git@github.com:") {
slug := strings.TrimPrefix(url, "git@github.com:")
// Handle SSH URLs: git@github.com:owner/repo or git@enterprise.github.com:owner/repo
sshPrefix := "git@" + githubHostWithoutScheme + ":"
if strings.HasPrefix(url, sshPrefix) {
slug := strings.TrimPrefix(url, sshPrefix)
gitLog.Printf("Extracted slug from SSH URL: %s", slug)
return slug
}
Expand Down
18 changes: 17 additions & 1 deletion pkg/cli/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"strings"

"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/logger"
)

Expand All @@ -28,7 +29,7 @@ func getGitHubHost() string {
}
}

defaultHost := "https://github.com"
defaultHost := string(constants.PublicGitHubHost)
githubLog.Printf("No GitHub host environment variable set, using default: %s", defaultHost)
return defaultHost
}
Expand All @@ -45,3 +46,18 @@ func normalizeGitHubHostURL(rawHostURL string) string {

return normalized
}

// 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().
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)
}

// For all other repositories, use the configured GitHub host
return getGitHubHost()
}
3 changes: 2 additions & 1 deletion pkg/cli/pr_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,8 @@ func createTransferPR(targetOwner, targetRepo string, prInfo *PRInfo, branchName

// Add fork as remote if not already present
remoteName := "fork"
forkRepoURL := fmt.Sprintf("https://github.com/%s/%s.git", forkOwner, forkRepo)
githubHost := getGitHubHost()
forkRepoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, forkOwner, forkRepo)

// Check if fork remote exists
checkRemoteCmd := exec.Command("git", "remote", "get-url", remoteName)
Expand Down
10 changes: 7 additions & 3 deletions pkg/cli/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,14 @@ func parseRepoSpec(repoSpec string) (*RepoSpec, error) {
specLog.Printf("Version specified: %s", version)
}

githubHost := getGitHubHost()
githubHostPrefix := githubHost + "/"
githubHostHTTPPrefix := "http://" + strings.TrimPrefix(githubHost, "https://") + "/"

// Check if this is a GitHub URL
if strings.HasPrefix(repo, "https://github.com/") || strings.HasPrefix(repo, "http://github.com/") {
if strings.HasPrefix(repo, githubHostPrefix) || strings.HasPrefix(repo, githubHostHTTPPrefix) {
specLog.Print("Detected GitHub URL format")
// Parse GitHub URL: https://github.com/owner/repo
// Parse GitHub URL: https://github.com/owner/repo or https://enterprise.github.com/owner/repo
repoURL, err := url.Parse(repo)
if err != nil {
specLog.Printf("Failed to parse GitHub URL: %v", err)
Expand All @@ -125,7 +129,7 @@ func parseRepoSpec(repoSpec string) (*RepoSpec, error) {
pathParts := strings.Split(strings.Trim(repoURL.Path, "/"), "/")
if len(pathParts) != 2 || pathParts[0] == "" || pathParts[1] == "" {
specLog.Printf("Invalid GitHub URL path parts: %v", pathParts)
return nil, fmt.Errorf("invalid GitHub URL: must be https://github.com/owner/repo. Example: https://github.com/github/gh-aw")
return nil, fmt.Errorf("invalid GitHub URL: must be %s/owner/repo. Example: %s/github/gh-aw", githubHost, githubHost)
}

repo = fmt.Sprintf("%s/%s", pathParts[0], pathParts[1])
Expand Down
9 changes: 6 additions & 3 deletions pkg/cli/trial_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,8 @@ func RunWorkflowTrials(ctx context.Context, workflowSpecs []string, opts TrialOp
}

// Generate workflow run URL
workflowRunURL := fmt.Sprintf("https://github.com/%s/actions/runs/%s", hostRepoSlug, runID)
githubHost := getGitHubHost()
workflowRunURL := fmt.Sprintf("%s/%s/actions/runs/%s", githubHost, hostRepoSlug, runID)
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Workflow run started with ID: %s (%s)", runID, workflowRunURL)))

// Wait for workflow completion
Expand Down Expand Up @@ -581,7 +582,8 @@ func RunWorkflowTrials(ctx context.Context, workflowSpecs []string, opts TrialOp
if opts.DeleteHostRepo {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Host repository will be cleaned up"))
} else {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Host repository preserved: https://github.com/%s", hostRepoSlug)))
githubHost := getGitHubHost()
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Host repository preserved: %s/%s", githubHost, hostRepoSlug)))
}
},
UseStderr: true,
Expand All @@ -606,7 +608,8 @@ func getCurrentGitHubUsername() (string, error) {

// showTrialConfirmation displays a confirmation prompt to the user using parsed workflow specs
func showTrialConfirmation(parsedSpecs []*WorkflowSpec, logicalRepoSlug, cloneRepoSlug, hostRepoSlug string, deleteHostRepo bool, forceDeleteHostRepo bool, autoMergePRs bool, repeatCount int, directTrialMode bool, engineOverride string) error {
hostRepoSlugURL := fmt.Sprintf("https://github.com/%s", hostRepoSlug)
githubHost := getGitHubHost()
hostRepoSlugURL := fmt.Sprintf("%s/%s", githubHost, hostRepoSlug)

var sections []string

Expand Down
3 changes: 2 additions & 1 deletion pkg/cli/update_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ func getLatestActionReleaseViaGit(repo, currentVersion string, allowMajor, verbo
baseRepo := extractBaseRepo(repo)
updateLog.Printf("Using base repository: %s for action: %s (git fallback)", baseRepo, repo)

repoURL := fmt.Sprintf("https://github.com/%s.git", baseRepo)
githubHost := getGitHubHostForRepo(baseRepo)
repoURL := fmt.Sprintf("%s/%s.git", githubHost, baseRepo)

// List all tags
cmd := exec.Command("git", "ls-remote", "--tags", repoURL)
Expand Down
5 changes: 5 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ const (
// DefaultMCPRegistryURL is the default MCP registry URL.
const DefaultMCPRegistryURL URL = "https://api.mcp.github.com/v0.1"

// PublicGitHubHost is the public GitHub host URL.
// This is used as the default GitHub host and for the gh-aw repository itself,
// which is always hosted on public GitHub regardless of enterprise host settings.
const PublicGitHubHost URL = "https://github.com"

// GitHubCopilotMCPDomain is the domain for the hosted GitHub MCP server.
// Used when github tool is configured with mode: remote.
const GitHubCopilotMCPDomain = "api.githubcopilot.com"
Expand Down
53 changes: 53 additions & 0 deletions pkg/parser/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,64 @@ import (
"os/exec"
"strings"

"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/logger"
)

var githubLog = logger.New("parser:github")

// GetGitHubHost returns the GitHub host URL from environment variables.
// Environment variables are checked in priority order for GitHub Enterprise support:
// 1. GITHUB_SERVER_URL - GitHub Actions standard (e.g., https://MYORG.ghe.com)
// 2. GITHUB_ENTERPRISE_HOST - GitHub Enterprise standard (e.g., MYORG.ghe.com)
// 3. GITHUB_HOST - GitHub Enterprise standard (e.g., MYORG.ghe.com)
// 4. GH_HOST - GitHub CLI standard (e.g., MYORG.ghe.com)
// 5. Defaults to https://github.com if none are set
//
// The function normalizes the URL by adding https:// if missing and removing trailing slashes.
func GetGitHubHost() string {
envVars := []string{"GITHUB_SERVER_URL", "GITHUB_ENTERPRISE_HOST", "GITHUB_HOST", "GH_HOST"}

for _, envVar := range envVars {
if value := os.Getenv(envVar); value != "" {
githubLog.Printf("Resolved GitHub host from %s: %s", envVar, value)
return normalizeGitHubHostURL(value)
}
}

defaultHost := string(constants.PublicGitHubHost)
githubLog.Printf("No GitHub host environment variable set, using default: %s", defaultHost)
return defaultHost
}

// normalizeGitHubHostURL ensures the host URL has https:// scheme and no trailing slashes
func normalizeGitHubHostURL(rawHostURL string) string {
// Remove all trailing slashes
normalized := strings.TrimRight(rawHostURL, "/")

// Add https:// scheme if no scheme is present
if !strings.HasPrefix(normalized, "https://") && !strings.HasPrefix(normalized, "http://") {
normalized = "https://" + normalized
}

return normalized
}

// 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().
func GetGitHubHostForRepo(owner, repo string) string {
// The gh-aw repository is always on public GitHub
if owner == "github" && repo == "gh-aw" {
githubLog.Print("Using public GitHub host for github/gh-aw repository")
return string(constants.PublicGitHubHost)
}

// For all other repositories, use the configured GitHub host
return GetGitHubHost()
}

// GetGitHubToken attempts to get GitHub token from environment or gh CLI
func GetGitHubToken() (string, error) {
githubLog.Print("Getting GitHub token")
Expand Down
9 changes: 6 additions & 3 deletions pkg/parser/remote_fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,8 @@ func downloadIncludeFromWorkflowSpec(spec string, cache *ImportCache) (string, e
func resolveRefToSHAViaGit(owner, repo, ref string) (string, error) {
remoteLog.Printf("Attempting git ls-remote fallback for ref resolution: %s/%s@%s", owner, repo, ref)

repoURL := fmt.Sprintf("https://github.com/%s/%s.git", owner, repo)
githubHost := GetGitHubHostForRepo(owner, repo)
repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo)

// Try to resolve the ref using git ls-remote
// Format: git ls-remote <repo> <ref>
Expand Down Expand Up @@ -397,7 +398,8 @@ func downloadFileViaGit(owner, repo, path, ref string) ([]byte, error) {

// Use git archive to get the file content without cloning
// This works for public repositories without authentication
repoURL := fmt.Sprintf("https://github.com/%s/%s.git", owner, repo)
githubHost := GetGitHubHostForRepo(owner, repo)
repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo)

// git archive command: git archive --remote=<repo> <ref> <path>
cmd := exec.Command("git", "archive", "--remote="+repoURL, ref, path)
Expand Down Expand Up @@ -432,7 +434,8 @@ func downloadFileViaGitClone(owner, repo, path, ref string) ([]byte, error) {
}
defer os.RemoveAll(tmpDir)

repoURL := fmt.Sprintf("https://github.com/%s/%s.git", owner, repo)
githubHost := GetGitHubHostForRepo(owner, repo)
repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo)

// Check if ref is a SHA (40 hex characters)
isSHA := len(ref) == 40 && gitutil.IsHexString(ref)
Expand Down
Loading