From 81d09c3c0b8d44a42c28ac184a053a6747bf1395 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:36:12 +0000 Subject: [PATCH 1/5] Initial plan From 6b7f8f2846f5d0e91e61f1ec1a33c02632a09b9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:46:02 +0000 Subject: [PATCH 2/5] Initial plan: enterprise-ready fixes for hardcoded github.com in safe output JS Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup-cli/install.sh | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/actions/setup-cli/install.sh b/actions/setup-cli/install.sh index 58d73c6805d..e1ac3479a12 100755 --- a/actions/setup-cli/install.sh +++ b/actions/setup-cli/install.sh @@ -239,17 +239,8 @@ fi if [ "$TRY_GH_INSTALL" = true ] && command -v gh &> /dev/null; then print_info "Attempting to install gh-aw using 'gh extension install'..." - # Call gh extension install directly to avoid command injection - install_result=0 - if [ -n "$VERSION" ] && [ "$VERSION" != "latest" ]; then - gh extension install "$REPO" --force --pin "$VERSION" 2>&1 | tee /tmp/gh-install.log - install_result=${PIPESTATUS[0]} - else - gh extension install "$REPO" --force 2>&1 | tee /tmp/gh-install.log - install_result=${PIPESTATUS[0]} - fi - - if [ $install_result -eq 0 ]; then + # Try to install using gh + if gh extension install "$REPO" --force 2>&1 | tee /tmp/gh-install.log; then # Verify the installation succeeded if gh aw version &> /dev/null; then INSTALLED_VERSION=$(gh aw version 2>&1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1) @@ -258,7 +249,7 @@ if [ "$TRY_GH_INSTALL" = true ] && command -v gh &> /dev/null; then # Set output for GitHub Actions if [ -n "${GITHUB_OUTPUT}" ]; then - echo "installed_version=${INSTALLED_VERSION}" >> "${GITHUB_OUTPUT}" + echo "installed_version=${VERSION}" >> "${GITHUB_OUTPUT}" fi exit 0 From 77224e107bbc484952ec66c0e373a689ba303d2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:03:00 +0000 Subject: [PATCH 3/5] fix: enterprise-ready fixes for hardcoded github.com in safe output JS Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup-cli/install.sh | 15 +++++-- .../setup/js/autofix_code_scanning_alert.cjs | 2 +- .../setup/js/check_workflow_timestamp_api.cjs | 13 +++--- actions/setup/js/create_project.cjs | 2 +- actions/setup/js/extra_empty_commit.cjs | 4 +- actions/setup/js/extra_empty_commit.test.cjs | 40 +++++++++++++++++++ .../js/merge_remote_agent_github_folder.cjs | 3 +- actions/setup/js/safe_outputs_handlers.cjs | 13 +++++- .../setup/js/safe_outputs_handlers.test.cjs | 32 +++++++++++++++ actions/setup/js/validate_secrets.cjs | 10 +++-- 10 files changed, 116 insertions(+), 18 deletions(-) diff --git a/actions/setup-cli/install.sh b/actions/setup-cli/install.sh index e1ac3479a12..58d73c6805d 100755 --- a/actions/setup-cli/install.sh +++ b/actions/setup-cli/install.sh @@ -239,8 +239,17 @@ fi if [ "$TRY_GH_INSTALL" = true ] && command -v gh &> /dev/null; then print_info "Attempting to install gh-aw using 'gh extension install'..." - # Try to install using gh - if gh extension install "$REPO" --force 2>&1 | tee /tmp/gh-install.log; then + # Call gh extension install directly to avoid command injection + install_result=0 + if [ -n "$VERSION" ] && [ "$VERSION" != "latest" ]; then + gh extension install "$REPO" --force --pin "$VERSION" 2>&1 | tee /tmp/gh-install.log + install_result=${PIPESTATUS[0]} + else + gh extension install "$REPO" --force 2>&1 | tee /tmp/gh-install.log + install_result=${PIPESTATUS[0]} + fi + + if [ $install_result -eq 0 ]; then # Verify the installation succeeded if gh aw version &> /dev/null; then INSTALLED_VERSION=$(gh aw version 2>&1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1) @@ -249,7 +258,7 @@ if [ "$TRY_GH_INSTALL" = true ] && command -v gh &> /dev/null; then # Set output for GitHub Actions if [ -n "${GITHUB_OUTPUT}" ]; then - echo "installed_version=${VERSION}" >> "${GITHUB_OUTPUT}" + echo "installed_version=${INSTALLED_VERSION}" >> "${GITHUB_OUTPUT}" fi exit 0 diff --git a/actions/setup/js/autofix_code_scanning_alert.cjs b/actions/setup/js/autofix_code_scanning_alert.cjs index 319e4c79d68..880237bc2e4 100644 --- a/actions/setup/js/autofix_code_scanning_alert.cjs +++ b/actions/setup/js/autofix_code_scanning_alert.cjs @@ -100,7 +100,7 @@ async function main(config = {}) { headers: { "X-GitHub-Api-Version": "2022-11-28" }, }); - const autofixUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/security/code-scanning/${alertNumber}`; + const autofixUrl = `${process.env.GITHUB_SERVER_URL || "https://github.com"}/${context.repo.owner}/${context.repo.repo}/security/code-scanning/${alertNumber}`; core.info(`✓ Successfully created autofix for code scanning alert ${alertNumber}: ${autofixUrl}`); processedAutofixes.push({ diff --git a/actions/setup/js/check_workflow_timestamp_api.cjs b/actions/setup/js/check_workflow_timestamp_api.cjs index f77d5cd46f3..dd09b778fc3 100644 --- a/actions/setup/js/check_workflow_timestamp_api.cjs +++ b/actions/setup/js/check_workflow_timestamp_api.cjs @@ -31,6 +31,7 @@ async function main() { const { owner, repo } = context.repo; const ref = context.sha; + const githubServerUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; // Helper function to get the last commit for a file async function getLastCommitForFile(path) { @@ -151,10 +152,10 @@ async function main() { .addRaw("**Files:**\n") .addRaw(`- Source: \`${workflowMdPath}\`\n`) .addRaw(` - Last commit: ${workflowTimestamp}\n`) - .addRaw(` - Commit SHA: [\`${workflowCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${workflowCommit.sha})\n`) + .addRaw(` - Commit SHA: [\`${workflowCommit.sha.substring(0, 7)}\`](${githubServerUrl}/${owner}/${repo}/commit/${workflowCommit.sha})\n`) .addRaw(`- Lock: \`${lockFilePath}\`\n`) .addRaw(` - Last commit: ${lockTimestamp}\n`) - .addRaw(` - Commit SHA: [\`${lockCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${lockCommit.sha})\n\n`) + .addRaw(` - Commit SHA: [\`${lockCommit.sha.substring(0, 7)}\`](${githubServerUrl}/${owner}/${repo}/commit/${lockCommit.sha})\n\n`) .addRaw("**Action Required:** Run `gh aw compile` to regenerate the lock file.\n\n"); await summary.write(); @@ -179,11 +180,11 @@ async function main() { .addRaw("**Files:**\n") .addRaw(`- Source: \`${workflowMdPath}\`\n`) .addRaw(` - Last commit: ${workflowTimestamp}\n`) - .addRaw(` - Commit SHA: [\`${workflowCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${workflowCommit.sha})\n`) + .addRaw(` - Commit SHA: [\`${workflowCommit.sha.substring(0, 7)}\`](${githubServerUrl}/${owner}/${repo}/commit/${workflowCommit.sha})\n`) .addRaw(` - Frontmatter hash: \`${hashComparison.recomputedHash.substring(0, 12)}...\`\n`) .addRaw(`- Lock: \`${lockFilePath}\`\n`) .addRaw(` - Last commit: ${lockTimestamp}\n`) - .addRaw(` - Commit SHA: [\`${lockCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${lockCommit.sha})\n`) + .addRaw(` - Commit SHA: [\`${lockCommit.sha.substring(0, 7)}\`](${githubServerUrl}/${owner}/${repo}/commit/${lockCommit.sha})\n`) .addRaw(` - Stored hash: \`${hashComparison.storedHash.substring(0, 12)}...\`\n\n`) .addRaw("**Action Required:** Run `gh aw compile` to regenerate the lock file.\n\n"); @@ -223,11 +224,11 @@ async function main() { .addRaw("**Files:**\n") .addRaw(`- Source: \`${workflowMdPath}\`\n`) .addRaw(` - Last commit: ${workflowTimestamp}\n`) - .addRaw(` - Commit SHA: [\`${workflowCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${workflowCommit.sha})\n`) + .addRaw(` - Commit SHA: [\`${workflowCommit.sha.substring(0, 7)}\`](${githubServerUrl}/${owner}/${repo}/commit/${workflowCommit.sha})\n`) .addRaw(` - Frontmatter hash: \`${hashComparison.recomputedHash.substring(0, 12)}...\`\n`) .addRaw(`- Lock: \`${lockFilePath}\`\n`) .addRaw(` - Last commit: ${lockTimestamp}\n`) - .addRaw(` - Commit SHA: [\`${lockCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${lockCommit.sha})\n`) + .addRaw(` - Commit SHA: [\`${lockCommit.sha.substring(0, 7)}\`](${githubServerUrl}/${owner}/${repo}/commit/${lockCommit.sha})\n`) .addRaw(` - Stored hash: \`${hashComparison.storedHash.substring(0, 12)}...\`\n\n`) .addRaw("**Action Required:** Run `gh aw compile` to regenerate the lock file.\n\n"); diff --git a/actions/setup/js/create_project.cjs b/actions/setup/js/create_project.cjs index e5dceffca4c..ebe8f461785 100644 --- a/actions/setup/js/create_project.cjs +++ b/actions/setup/js/create_project.cjs @@ -375,7 +375,7 @@ async function main(config = {}, githubClient = null) { if (resolved && resolved.repo && resolved.number) { // Build the proper GitHub issue URL - const resolvedUrl = `https://github.com/${resolved.repo}/issues/${resolved.number}`; + const resolvedUrl = `${process.env.GITHUB_SERVER_URL || "https://github.com"}/${resolved.repo}/issues/${resolved.number}`; core.info(`Resolved temporary ID ${tempIdStr} in item_url to ${resolvedUrl}`); item_url = resolvedUrl; } else { diff --git a/actions/setup/js/extra_empty_commit.cjs b/actions/setup/js/extra_empty_commit.cjs index de58a9cb4a1..648b21b8d29 100644 --- a/actions/setup/js/extra_empty_commit.cjs +++ b/actions/setup/js/extra_empty_commit.cjs @@ -127,7 +127,9 @@ async function pushExtraEmptyCommit({ branchName, repoOwner, repoName, commitMes core.info(`Cycle check passed: ${emptyCommitCount} empty commit(s) in last ${COMMITS_TO_CHECK} (limit: ${MAX_EMPTY_COMMITS})`); // Configure git remote with the token for authentication - const remoteUrl = `https://x-access-token:${token}@github.com/${repoOwner}/${repoName}.git`; + const githubServerUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; + const serverHostStripped = githubServerUrl.replace(/^https?:\/\//, ""); + const remoteUrl = `https://x-access-token:${token}@${serverHostStripped}/${repoOwner}/${repoName}.git`; // Add a temporary remote with the token try { diff --git a/actions/setup/js/extra_empty_commit.test.cjs b/actions/setup/js/extra_empty_commit.test.cjs index 1521b79e121..e9a4609100a 100644 --- a/actions/setup/js/extra_empty_commit.test.cjs +++ b/actions/setup/js/extra_empty_commit.test.cjs @@ -6,10 +6,12 @@ describe("extra_empty_commit.cjs", () => { let pushExtraEmptyCommit; let originalEnv; let originalGithubRepo; + let originalGithubServerUrl; beforeEach(() => { originalEnv = process.env.GH_AW_CI_TRIGGER_TOKEN; originalGithubRepo = process.env.GITHUB_REPOSITORY; + originalGithubServerUrl = process.env.GITHUB_SERVER_URL; // Set GITHUB_REPOSITORY to match the default test owner/repo so the // cross-repo guard doesn't interfere with unrelated tests. process.env.GITHUB_REPOSITORY = "test-owner/test-repo"; @@ -44,6 +46,11 @@ describe("extra_empty_commit.cjs", () => { } else { delete process.env.GITHUB_REPOSITORY; } + if (originalGithubServerUrl !== undefined) { + process.env.GITHUB_SERVER_URL = originalGithubServerUrl; + } else { + delete process.env.GITHUB_SERVER_URL; + } delete global.core; delete global.exec; vi.clearAllMocks(); @@ -154,6 +161,39 @@ describe("extra_empty_commit.cjs", () => { expect(removeRemoteCalls.length).toBeGreaterThanOrEqual(1); }); + it("should use github.com by default when GITHUB_SERVER_URL is not set", async () => { + delete process.env.GITHUB_SERVER_URL; + delete require.cache[require.resolve("./extra_empty_commit.cjs")]; + ({ pushExtraEmptyCommit } = require("./extra_empty_commit.cjs")); + + await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + const addRemote = mockExec.exec.mock.calls.find(c => c[0] === "git" && c[1] && c[1][0] === "remote" && c[1][1] === "add"); + expect(addRemote).toBeDefined(); + expect(addRemote[1][3]).toContain("github.com/test-owner/test-repo.git"); + }); + + it("should use GITHUB_SERVER_URL hostname for GitHub Enterprise", async () => { + process.env.GITHUB_SERVER_URL = "https://github.example.com"; + delete require.cache[require.resolve("./extra_empty_commit.cjs")]; + ({ pushExtraEmptyCommit } = require("./extra_empty_commit.cjs")); + + await pushExtraEmptyCommit({ + branchName: "feature-branch", + repoOwner: "test-owner", + repoName: "test-repo", + }); + + const addRemote = mockExec.exec.mock.calls.find(c => c[0] === "git" && c[1] && c[1][0] === "remote" && c[1][1] === "add"); + expect(addRemote).toBeDefined(); + expect(addRemote[1][3]).toContain("github.example.com/test-owner/test-repo.git"); + expect(addRemote[1][3]).not.toContain("github.com/test-owner/test-repo.git"); + }); + it("should use default commit message when none provided", async () => { await pushExtraEmptyCommit({ branchName: "feature-branch", diff --git a/actions/setup/js/merge_remote_agent_github_folder.cjs b/actions/setup/js/merge_remote_agent_github_folder.cjs index 2e026ff61f2..20bed192e63 100644 --- a/actions/setup/js/merge_remote_agent_github_folder.cjs +++ b/actions/setup/js/merge_remote_agent_github_folder.cjs @@ -184,7 +184,8 @@ function sparseCheckoutGithubFolder(owner, repo, ref, tempDir) { validateGitParameter(repo, "repo"); validateGitParameter(ref, "ref"); - const repoUrl = `https://github.com/${owner}/${repo}.git`; + const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; + const repoUrl = `${serverUrl}/${owner}/${repo}.git`; try { // Initialize git repository diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 312143ca78c..9c16d1e3ce9 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -160,7 +160,18 @@ function createHandlers(server, appendSafeOutput, config = {}) { const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; const repo = process.env.GITHUB_REPOSITORY || "owner/repo"; - const url = `${githubServer.replace("github.com", "raw.githubusercontent.com")}/${repo}/${normalizedBranchName}/${targetFileName}`; + let url; + try { + const serverHostname = new URL(githubServer).hostname; + if (serverHostname === "github.com") { + url = `https://raw.githubusercontent.com/${repo}/${normalizedBranchName}/${targetFileName}`; + } else { + // GitHub Enterprise Server - raw content is served from the same host with /raw/ path + url = `${githubServer}/${repo}/raw/${normalizedBranchName}/${targetFileName}`; + } + } catch { + url = `${githubServer}/${repo}/raw/${normalizedBranchName}/${targetFileName}`; + } // Create entry for safe outputs const entry = { diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index 285e74c48c2..844f57917cd 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -138,6 +138,38 @@ describe("safe_outputs_handlers", () => { }); describe("uploadAssetHandler", () => { + it("should generate raw.githubusercontent.com URL for github.com", () => { + process.env.GH_AW_ASSETS_BRANCH = "test-branch"; + process.env.GITHUB_SERVER_URL = "https://github.com"; + process.env.GITHUB_REPOSITORY = "myorg/myrepo"; + + const testFile = path.join(testWorkspaceDir, "test.png"); + fs.writeFileSync(testFile, "test content"); + + handlers.uploadAssetHandler({ path: testFile }); + + const entry = mockAppendSafeOutput.mock.calls[0][0]; + expect(entry.url).toContain("raw.githubusercontent.com"); + expect(entry.url).toContain("myorg/myrepo"); + }); + + it("should generate enterprise URL for GitHub Enterprise Server", () => { + process.env.GH_AW_ASSETS_BRANCH = "test-branch"; + process.env.GITHUB_SERVER_URL = "https://github.example.com"; + process.env.GITHUB_REPOSITORY = "myorg/myrepo"; + + const testFile = path.join(testWorkspaceDir, "test2.png"); + fs.writeFileSync(testFile, "test content"); + + handlers = createHandlers(mockServer, mockAppendSafeOutput); + handlers.uploadAssetHandler({ path: testFile }); + + const entry = mockAppendSafeOutput.mock.calls[0][0]; + expect(entry.url).toContain("github.example.com"); + expect(entry.url).toContain("/raw/"); + expect(entry.url).not.toContain("raw.githubusercontent.com"); + }); + it("should validate and process valid asset upload", () => { process.env.GH_AW_ASSETS_BRANCH = "test-branch"; diff --git a/actions/setup/js/validate_secrets.cjs b/actions/setup/js/validate_secrets.cjs index 4e8da2f78af..88b4650b12d 100644 --- a/actions/setup/js/validate_secrets.cjs +++ b/actions/setup/js/validate_secrets.cjs @@ -97,7 +97,8 @@ async function testGitHubRESTAPI(token, owner, repo) { } try { - const result = await makeRequest("api.github.com", `/repos/${owner}/${repo}`, { + const apiUrl = new URL(process.env.GITHUB_API_URL || "https://api.github.com"); + const result = await makeRequest(apiUrl.hostname, `${apiUrl.pathname.replace(/\/$/, "")}/repos/${owner}/${repo}`, { "User-Agent": "gh-aw-secret-validation", Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", @@ -159,9 +160,10 @@ async function testGitHubGraphQLAPI(token, owner, repo) { try { const result = await new Promise((resolve, reject) => { const postData = JSON.stringify({ query }); + const graphqlUrl = new URL(process.env.GITHUB_GRAPHQL_URL || "https://api.github.com/graphql"); const options = { - hostname: "api.github.com", - path: "/graphql", + hostname: graphqlUrl.hostname, + path: graphqlUrl.pathname, method: "POST", headers: { "User-Agent": "gh-aw-secret-validation", @@ -564,7 +566,7 @@ function generateMarkdownReport(results) { report += `> - [\`${secret}\`](${docsLink})\n`; }); report += `>\n`; - report += `> Configure these secrets in [repository settings](https://github.com/${repository}/settings/secrets/actions) if needed.\n\n`; + report += `> Configure these secrets in [repository settings](${process.env.GITHUB_SERVER_URL || "https://github.com"}/${repository}/settings/secrets/actions) if needed.\n\n`; } } From 09fb33edac4bb2a0aa8b54daf1cfe1c3e81f591b Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Wed, 4 Mar 2026 17:49:11 +0000 Subject: [PATCH 4/5] Add changeset for GHES safe output fix [skip-ci] --- .changeset/patch-enterprise-safe-output-url.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/patch-enterprise-safe-output-url.md diff --git a/.changeset/patch-enterprise-safe-output-url.md b/.changeset/patch-enterprise-safe-output-url.md new file mode 100644 index 00000000000..aa350da6917 --- /dev/null +++ b/.changeset/patch-enterprise-safe-output-url.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Ensure the safe output handlers and helpers build URLs from the configured GitHub server (e.g., GITHUB_SERVER_URL) so enterprise hosts no longer hit https://github.com references that break authentication. From 2a0754c6cb0c5127bdc286952fd0f01141a135bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Mar 2026 17:50:57 +0000 Subject: [PATCH 5/5] ci: trigger checks