From 5ead002c844a109289172ef8d67842e6c6ecf0c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:40:20 +0000 Subject: [PATCH 1/9] Initial plan From 38626fd46a4c78c190ad20b23aaf4d3cfe07fdb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:47:09 +0000 Subject: [PATCH 2/9] Add GitHub Enterprise host support to Go files Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/download_workflow.go | 18 +++++++++----- pkg/cli/git.go | 17 ++++++++----- pkg/cli/pr_command.go | 3 ++- pkg/cli/spec.go | 10 +++++--- pkg/cli/trial_command.go | 9 ++++--- pkg/cli/update_actions.go | 3 ++- pkg/parser/remote_fetch.go | 46 +++++++++++++++++++++++++++++++++--- 7 files changed, 83 insertions(+), 23 deletions(-) diff --git a/pkg/cli/download_workflow.go b/pkg/cli/download_workflow.go index e9c51d531a8..606a7d6e946 100644 --- a/pkg/cli/download_workflow.go +++ b/pkg/cli/download_workflow.go @@ -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 := getGitHubHost() + repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // List all tags cmd := exec.Command("git", "ls-remote", "--tags", repoURL) @@ -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 := getGitHubHost() + repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // List all branches and check if ref matches cmd := exec.Command("git", "ls-remote", "--heads", repoURL) @@ -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 := getGitHubHost() + 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)) @@ -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 := getGitHubHost() + repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // Get HEAD to find default branch cmd := exec.Command("git", "ls-remote", "--symref", repoURL, "HEAD") @@ -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 := getGitHubHost() + repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // git archive command: git archive --remote= cmd := exec.Command("git", "archive", "--remote="+repoURL, ref, path) @@ -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 := getGitHubHost() + repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // Initialize git repository initCmd := exec.Command("git", "-C", tmpDir, "init") diff --git a/pkg/cli/git.go b/pkg/cli/git.go index 8a0e1f9a8f6..fbce963830f 100644 --- a/pkg/cli/git.go +++ b/pkg/cli/git.go @@ -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 } diff --git a/pkg/cli/pr_command.go b/pkg/cli/pr_command.go index dbe32334468..ee1f44c1a1a 100644 --- a/pkg/cli/pr_command.go +++ b/pkg/cli/pr_command.go @@ -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) diff --git a/pkg/cli/spec.go b/pkg/cli/spec.go index a5722519dbd..0731d3bfa28 100644 --- a/pkg/cli/spec.go +++ b/pkg/cli/spec.go @@ -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) @@ -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]) diff --git a/pkg/cli/trial_command.go b/pkg/cli/trial_command.go index 5507e4098ac..fc109293038 100644 --- a/pkg/cli/trial_command.go +++ b/pkg/cli/trial_command.go @@ -465,7 +465,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 @@ -571,7 +572,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, @@ -596,7 +598,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 diff --git a/pkg/cli/update_actions.go b/pkg/cli/update_actions.go index 226cf831de1..3ef45daa475 100644 --- a/pkg/cli/update_actions.go +++ b/pkg/cli/update_actions.go @@ -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 := getGitHubHost() + repoURL := fmt.Sprintf("%s/%s.git", githubHost, baseRepo) // List all tags cmd := exec.Command("git", "ls-remote", "--tags", repoURL) diff --git a/pkg/parser/remote_fetch.go b/pkg/parser/remote_fetch.go index dd191380efb..3a4c51f1e8e 100644 --- a/pkg/parser/remote_fetch.go +++ b/pkg/parser/remote_fetch.go @@ -21,6 +21,43 @@ import ( var remoteLog = logger.New("parser:remote_fetch") +// 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 != "" { + remoteLog.Printf("Resolved GitHub host from %s: %s", envVar, value) + return normalizeGitHubHostURL(value) + } + } + + defaultHost := "https://github.com" + remoteLog.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 +} + // isUnderWorkflowsDirectory checks if a file path is a top-level workflow file (not in shared subdirectory) func isUnderWorkflowsDirectory(filePath string) bool { // Normalize the path to use forward slashes @@ -306,7 +343,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 := getGitHubHost() + repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo) // Try to resolve the ref using git ls-remote // Format: git ls-remote @@ -397,7 +435,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 := getGitHubHost() + repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo) // git archive command: git archive --remote= cmd := exec.Command("git", "archive", "--remote="+repoURL, ref, path) @@ -432,7 +471,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 := getGitHubHost() + 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) From 2ef57fb8e06910dbc61db701e4cab48fa7e806e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:50:25 +0000 Subject: [PATCH 3/9] Update project URL validation to support GitHub Enterprise Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_project_status_update.cjs | 2 +- actions/setup/js/update_project.cjs | 4 ++-- pkg/workflow/safe_output_validation_config.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/create_project_status_update.cjs b/actions/setup/js/create_project_status_update.cjs index e971340f860..f40f71ec490 100644 --- a/actions/setup/js/create_project_status_update.cjs +++ b/actions/setup/js/create_project_status_update.cjs @@ -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).`); } diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index 2fc17bcdcc5..e2b4a781400 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -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).`); } @@ -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).`); } diff --git a/pkg/workflow/safe_output_validation_config.go b/pkg/workflow/safe_output_validation_config.go index 04d4eb6cb84..2a587dd4847 100644 --- a/pkg/workflow/safe_output_validation_config.go +++ b/pkg/workflow/safe_output_validation_config.go @@ -255,7 +255,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ "update_project": { DefaultMax: 10, Fields: map[string]FieldValidation{ - "project": {Required: true, Type: "string", Sanitize: true, MaxLength: 512, Pattern: "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", PatternError: "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)"}, + "project": {Required: true, Type: "string", Sanitize: true, MaxLength: 512, Pattern: "^https://[^/]+/(orgs|users)/[^/]+/projects/\\d+", PatternError: "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)"}, "content_type": {Type: "string", Enum: []string{"issue", "pull_request", "draft_issue"}}, "content_number": {IssueNumberOrTemporaryID: true}, "issue": {OptionalPositiveInteger: true}, // Legacy @@ -277,7 +277,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ "create_project_status_update": { DefaultMax: 10, Fields: map[string]FieldValidation{ - "project": {Required: true, Type: "string", Sanitize: true, MaxLength: 512, Pattern: "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", PatternError: "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)"}, + "project": {Required: true, Type: "string", Sanitize: true, MaxLength: 512, Pattern: "^https://[^/]+/(orgs|users)/[^/]+/projects/\\d+", PatternError: "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)"}, "body": {Required: true, Type: "string", Sanitize: true, MaxLength: 65536}, "status": {Type: "string", Enum: []string{"INACTIVE", "ON_TRACK", "AT_RISK", "OFF_TRACK", "COMPLETE"}}, "start_date": {Type: "string", Pattern: "^\\d{4}-\\d{2}-\\d{2}$", PatternError: "must be in YYYY-MM-DD format"}, From 23245bd4f3679e15e9dc52337af5b6d598bb431a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:03:40 +0000 Subject: [PATCH 4/9] Move getGitHubHost to pkg/parser/github.go Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/github.go | 37 ++++++++++++++++++++++++++++++++ pkg/parser/remote_fetch.go | 43 +++----------------------------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/pkg/parser/github.go b/pkg/parser/github.go index 962e0d8edca..addd0f99939 100644 --- a/pkg/parser/github.go +++ b/pkg/parser/github.go @@ -13,6 +13,43 @@ import ( 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 := "https://github.com" + 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 +} + // GetGitHubToken attempts to get GitHub token from environment or gh CLI func GetGitHubToken() (string, error) { githubLog.Print("Getting GitHub token") diff --git a/pkg/parser/remote_fetch.go b/pkg/parser/remote_fetch.go index 3a4c51f1e8e..2a7269a6e68 100644 --- a/pkg/parser/remote_fetch.go +++ b/pkg/parser/remote_fetch.go @@ -21,43 +21,6 @@ import ( var remoteLog = logger.New("parser:remote_fetch") -// 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 != "" { - remoteLog.Printf("Resolved GitHub host from %s: %s", envVar, value) - return normalizeGitHubHostURL(value) - } - } - - defaultHost := "https://github.com" - remoteLog.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 -} - // isUnderWorkflowsDirectory checks if a file path is a top-level workflow file (not in shared subdirectory) func isUnderWorkflowsDirectory(filePath string) bool { // Normalize the path to use forward slashes @@ -343,7 +306,7 @@ 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) - githubHost := getGitHubHost() + githubHost := GetGitHubHost() repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo) // Try to resolve the ref using git ls-remote @@ -435,7 +398,7 @@ 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 - githubHost := getGitHubHost() + githubHost := GetGitHubHost() repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo) // git archive command: git archive --remote= @@ -471,7 +434,7 @@ func downloadFileViaGitClone(owner, repo, path, ref string) ([]byte, error) { } defer os.RemoveAll(tmpDir) - githubHost := getGitHubHost() + githubHost := GetGitHubHost() repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo) // Check if ref is a SHA (40 hex characters) From 3bd26142a68ab277ff8923533a732cd1c00dbe69 Mon Sep 17 00:00:00 2001 From: runner Date: Tue, 17 Feb 2026 15:37:27 +0000 Subject: [PATCH 5/9] Add changeset [skip-ci] --- .changeset/patch-github-enterprise-host.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/patch-github-enterprise-host.md diff --git a/.changeset/patch-github-enterprise-host.md b/.changeset/patch-github-enterprise-host.md new file mode 100644 index 00000000000..b3eb9616fe2 --- /dev/null +++ b/.changeset/patch-github-enterprise-host.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Normalize GitHub host detection and URL parsing so GitHub Enterprise deployments honor `GITHUB_SERVER_URL`, `GITHUB_ENTERPRISE_HOST`, `GITHUB_HOST`, and `GH_HOST`. Remote imports now use the normalized host when calling `git`/`gh`, and the parser handles enterprise-styled run/PR/file URLs. From aa5a379fd86fb9bcf3b56b5babeb587e1c8089b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:59:14 +0000 Subject: [PATCH 6/9] Merge main and recompile workflows Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-project.lock.yml | 4 ++-- .github/workflows/test-project-url-default.lock.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index 3fb0a66b621..837c5b8ee2b 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -857,7 +857,7 @@ jobs: "type": "string", "sanitize": true, "maxLength": 512, - "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", + "pattern": "^https://[^/]+/(orgs|users)/[^/]+/projects/\\d+", "patternError": "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)" }, "start_date": { @@ -978,7 +978,7 @@ jobs: "type": "string", "sanitize": true, "maxLength": 512, - "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", + "pattern": "^https://[^/]+/(orgs|users)/[^/]+/projects/\\d+", "patternError": "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)" }, "pull_request": { diff --git a/.github/workflows/test-project-url-default.lock.yml b/.github/workflows/test-project-url-default.lock.yml index e85489a7f8b..f838342ec4e 100644 --- a/.github/workflows/test-project-url-default.lock.yml +++ b/.github/workflows/test-project-url-default.lock.yml @@ -633,7 +633,7 @@ jobs: "type": "string", "sanitize": true, "maxLength": 512, - "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", + "pattern": "^https://[^/]+/(orgs|users)/[^/]+/projects/\\d+", "patternError": "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)" }, "start_date": { @@ -725,7 +725,7 @@ jobs: "type": "string", "sanitize": true, "maxLength": 512, - "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", + "pattern": "^https://[^/]+/(orgs|users)/[^/]+/projects/\\d+", "patternError": "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)" }, "pull_request": { From 0ef11c6022b4f0ac21a63ab31f107955a924729e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:03:25 +0000 Subject: [PATCH 7/9] Ensure github/gh-aw repo always uses public GitHub Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/download_workflow.go | 12 ++++++------ pkg/cli/github.go | 15 +++++++++++++++ pkg/cli/update_actions.go | 2 +- pkg/parser/github.go | 15 +++++++++++++++ pkg/parser/remote_fetch.go | 6 +++--- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/pkg/cli/download_workflow.go b/pkg/cli/download_workflow.go index 606a7d6e946..7196a0c82bc 100644 --- a/pkg/cli/download_workflow.go +++ b/pkg/cli/download_workflow.go @@ -23,7 +23,7 @@ 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))) } - githubHost := getGitHubHost() + githubHost := getGitHubHostForRepo(repo) repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // List all tags @@ -103,7 +103,7 @@ 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) - githubHost := getGitHubHost() + githubHost := getGitHubHostForRepo(repo) repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // List all branches and check if ref matches @@ -169,7 +169,7 @@ 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))) } - githubHost := getGitHubHost() + githubHost := getGitHubHostForRepo(repo) repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // Get the SHA for the specific branch @@ -239,7 +239,7 @@ 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))) } - githubHost := getGitHubHost() + githubHost := getGitHubHostForRepo(repo) repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // Get HEAD to find default branch @@ -330,7 +330,7 @@ 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 - githubHost := getGitHubHost() + githubHost := getGitHubHostForRepo(repo) repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // git archive command: git archive --remote= @@ -371,7 +371,7 @@ func downloadWorkflowContentViaGitClone(repo, path, ref string, verbose bool) ([ } defer os.RemoveAll(tmpDir) - githubHost := getGitHubHost() + githubHost := getGitHubHostForRepo(repo) repoURL := fmt.Sprintf("%s/%s.git", githubHost, repo) // Initialize git repository diff --git a/pkg/cli/github.go b/pkg/cli/github.go index b90fe77a666..1ea24d697af 100644 --- a/pkg/cli/github.go +++ b/pkg/cli/github.go @@ -45,3 +45,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 "https://github.com" + } + + // For all other repositories, use the configured GitHub host + return getGitHubHost() +} diff --git a/pkg/cli/update_actions.go b/pkg/cli/update_actions.go index 3ef45daa475..20c82a4411b 100644 --- a/pkg/cli/update_actions.go +++ b/pkg/cli/update_actions.go @@ -271,7 +271,7 @@ func getLatestActionReleaseViaGit(repo, currentVersion string, allowMajor, verbo baseRepo := extractBaseRepo(repo) updateLog.Printf("Using base repository: %s for action: %s (git fallback)", baseRepo, repo) - githubHost := getGitHubHost() + githubHost := getGitHubHostForRepo(baseRepo) repoURL := fmt.Sprintf("%s/%s.git", githubHost, baseRepo) // List all tags diff --git a/pkg/parser/github.go b/pkg/parser/github.go index addd0f99939..4c4f19648c0 100644 --- a/pkg/parser/github.go +++ b/pkg/parser/github.go @@ -50,6 +50,21 @@ 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(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 "https://github.com" + } + + // 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") diff --git a/pkg/parser/remote_fetch.go b/pkg/parser/remote_fetch.go index 2a7269a6e68..7e0f89104f1 100644 --- a/pkg/parser/remote_fetch.go +++ b/pkg/parser/remote_fetch.go @@ -306,7 +306,7 @@ 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) - githubHost := GetGitHubHost() + githubHost := GetGitHubHostForRepo(owner, repo) repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo) // Try to resolve the ref using git ls-remote @@ -398,7 +398,7 @@ 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 - githubHost := GetGitHubHost() + githubHost := GetGitHubHostForRepo(owner, repo) repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo) // git archive command: git archive --remote= @@ -434,7 +434,7 @@ func downloadFileViaGitClone(owner, repo, path, ref string) ([]byte, error) { } defer os.RemoveAll(tmpDir) - githubHost := GetGitHubHost() + githubHost := GetGitHubHostForRepo(owner, repo) repoURL := fmt.Sprintf("%s/%s/%s.git", githubHost, owner, repo) // Check if ref is a SHA (40 hex characters) From 281dc00bbdc35bde767ad3e143cc2bea064af391 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:32:35 +0000 Subject: [PATCH 8/9] Move public GitHub URL to constants.PublicGitHubHost Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/github.go | 5 +++-- pkg/constants/constants.go | 5 +++++ pkg/parser/github.go | 5 +++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/cli/github.go b/pkg/cli/github.go index 1ea24d697af..33dba0315ef 100644 --- a/pkg/cli/github.go +++ b/pkg/cli/github.go @@ -4,6 +4,7 @@ import ( "os" "strings" + "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" ) @@ -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 } @@ -54,7 +55,7 @@ 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 "https://github.com" + return string(constants.PublicGitHubHost) } // For all other repositories, use the configured GitHub host diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 01b45a5b3ba..83c903e469a 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -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" diff --git a/pkg/parser/github.go b/pkg/parser/github.go index 4c4f19648c0..10bbd6c020e 100644 --- a/pkg/parser/github.go +++ b/pkg/parser/github.go @@ -8,6 +8,7 @@ import ( "os/exec" "strings" + "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" ) @@ -32,7 +33,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 } @@ -58,7 +59,7 @@ 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 "https://github.com" + return string(constants.PublicGitHubHost) } // For all other repositories, use the configured GitHub host From 8d04351a9a13dba38daba0611c918f0a885471f6 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Tue, 17 Feb 2026 18:19:31 +0000 Subject: [PATCH 9/9] Add changeset [skip-ci] --- .changeset/patch-normalize-gh-host-detection.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changeset/patch-normalize-gh-host-detection.md diff --git a/.changeset/patch-normalize-gh-host-detection.md b/.changeset/patch-normalize-gh-host-detection.md new file mode 100644 index 00000000000..c590158b3da --- /dev/null +++ b/.changeset/patch-normalize-gh-host-detection.md @@ -0,0 +1,4 @@ +--- +"gh-aw": patch +--- +Normalize GitHub host detection and URL parsing so GitHub Enterprise deployments honor GITHUB_SERVER_URL, GITHUB_ENTERPRISE_HOST, GITHUB_HOST, and GH_HOST when compiling workflows and CLI commands.