diff --git a/dist/index.js b/dist/index.js index 2ab5f49..32ee7ff 100644 --- a/dist/index.js +++ b/dist/index.js @@ -41181,16 +41181,30 @@ async function run() { const packInput = getInput('pack') === 'true'; const isolated = getInput('isolated') === 'true'; const auditReportInput = getInput('audit-report').trim(); - // Pass github-token input to APM subprocess as GITHUB_TOKEN and GITHUB_APM_PAT. + // Pass github-token input to APM subprocess as GITHUB_TOKEN. // GitHub Actions does not auto-export input values as env vars — // without this, APM runs unauthenticated (rate-limited, no private repo access). - // Use ??= so values already in the environment (e.g., a PAT set via job-level - // `env:`) are not clobbered by the action's default github.token. + // Use ??= so a GITHUB_TOKEN already in the environment (e.g., a PAT set via + // job-level `env:`) is not clobbered by the action's default github.token. + // + // GITHUB_APM_PAT is only forwarded when GITHUB_TOKEN was NOT already present. + // When a caller provides GITHUB_TOKEN via step/job-level env: (e.g., a GitHub + // App token from gh-aw), that token carries higher-specificity auth than the + // action's default github.token. Since APM's token precedence is + // GITHUB_APM_PAT > GITHUB_TOKEN > GH_TOKEN + // auto-setting GITHUB_APM_PAT to the default github.token would shadow the + // caller's intentional GITHUB_TOKEN, causing auth failures for cross-org or + // private-repo access. const githubToken = getInput('github-token'); if (githubToken) { core_setSecret(githubToken); - process.env.GITHUB_TOKEN ??= githubToken; - process.env.GITHUB_APM_PAT ??= githubToken; + const callerProvidedToken = !!process.env.GITHUB_TOKEN; + if (!process.env.GITHUB_TOKEN) { + process.env.GITHUB_TOKEN = githubToken; + } + if (!callerProvidedToken) { + process.env.GITHUB_APM_PAT ??= githubToken; + } } // Validate inputs before touching the filesystem. if (bundleInput && packInput) { diff --git a/src/__tests__/runner.test.ts b/src/__tests__/runner.test.ts index 952e7d9..4a3c256 100644 --- a/src/__tests__/runner.test.ts +++ b/src/__tests__/runner.test.ts @@ -466,7 +466,9 @@ describe('run', () => { mockExec.mockResolvedValue(0); const prevToken = process.env.GITHUB_TOKEN; + const prevApmPat = process.env.GITHUB_APM_PAT; process.env.GITHUB_TOKEN = 'ghp_userProvidedPAT'; + delete process.env.GITHUB_APM_PAT; try { mockGetInput.mockImplementation((name: unknown) => { @@ -489,12 +491,76 @@ describe('run', () => { expect(mockSetFailed).not.toHaveBeenCalled(); // User's PAT should be preserved, not overwritten by the action default expect(process.env.GITHUB_TOKEN).toBe('ghp_userProvidedPAT'); + // GITHUB_APM_PAT must NOT be set to the default token — doing so would + // shadow the caller's intentional GITHUB_TOKEN in APM's precedence chain + expect(process.env.GITHUB_APM_PAT).toBeUndefined(); } finally { if (prevToken === undefined) { delete process.env.GITHUB_TOKEN; } else { process.env.GITHUB_TOKEN = prevToken; } + if (prevApmPat === undefined) { + delete process.env.GITHUB_APM_PAT; + } else { + process.env.GITHUB_APM_PAT = prevApmPat; + } + } + }); + + it('does not shadow caller GITHUB_TOKEN with GITHUB_APM_PAT (gh-aw app-token scenario)', async () => { + // Reproduces the gh-aw bug: gh-aw sets GITHUB_TOKEN to a GitHub App token + // (cross-org access) via step env:, while the action's github-token input + // defaults to github.token (scoped to the workflow repo only). + // Before the fix, GITHUB_APM_PAT was set to the default token, which + // shadowed the App token in APM's precedence chain. + fs.writeFileSync(path.join(tmpDir, 'apm.yml'), 'name: test\nversion: 1.0.0\n'); + fs.mkdirSync(path.join(tmpDir, '.github'), { recursive: true }); + mockExec.mockResolvedValue(0); + + const prevToken = process.env.GITHUB_TOKEN; + const prevApmPat = process.env.GITHUB_APM_PAT; + // Simulate gh-aw: step env sets GITHUB_TOKEN to the minted App token + process.env.GITHUB_TOKEN = 'ghs_crossOrgAppToken_abc123'; + delete process.env.GITHUB_APM_PAT; + + try { + mockGetInput.mockImplementation((name: unknown) => { + switch (name) { + case 'working-directory': return tmpDir; + case 'dependencies': return '- some-org/private-marketplace/plugins/essentials'; + case 'isolated': return 'true'; + case 'bundle': return ''; + case 'pack': return 'true'; + case 'compile': return 'false'; + case 'script': return ''; + case 'audit-report': return ''; + case 'target': return 'copilot'; + case 'archive': return 'true'; + // This is the default github.token — NOT the App token + case 'github-token': return 'ghs_workflowDefaultToken_xyz789'; + default: return ''; + } + }); + + await run(); + + // GITHUB_TOKEN must remain the App token (not overwritten) + expect(process.env.GITHUB_TOKEN).toBe('ghs_crossOrgAppToken_abc123'); + // GITHUB_APM_PAT must NOT be set — if it were, APM would use it + // (higher precedence) instead of the correct App token + expect(process.env.GITHUB_APM_PAT).toBeUndefined(); + } finally { + if (prevToken === undefined) { + delete process.env.GITHUB_TOKEN; + } else { + process.env.GITHUB_TOKEN = prevToken; + } + if (prevApmPat === undefined) { + delete process.env.GITHUB_APM_PAT; + } else { + process.env.GITHUB_APM_PAT = prevApmPat; + } } }); @@ -535,4 +601,54 @@ describe('run', () => { } } }); + + it('treats empty-string GITHUB_TOKEN as not-provided and forwards token correctly', async () => { + // Edge case: GITHUB_TOKEN is set to '' (empty string). The ??= operator + // treats '' as not-nullish, so it wouldn't overwrite it. We must treat + // empty-string as "not provided" to ensure APM gets a usable token. + fs.writeFileSync(path.join(tmpDir, 'apm.yml'), 'name: test\nversion: 1.0.0\n'); + fs.mkdirSync(path.join(tmpDir, '.github'), { recursive: true }); + mockExec.mockResolvedValue(0); + + const prevToken = process.env.GITHUB_TOKEN; + const prevApmPat = process.env.GITHUB_APM_PAT; + process.env.GITHUB_TOKEN = ''; + delete process.env.GITHUB_APM_PAT; + + try { + mockGetInput.mockImplementation((name: unknown) => { + switch (name) { + case 'working-directory': return tmpDir; + case 'dependencies': return ''; + case 'isolated': return 'false'; + case 'bundle': return ''; + case 'pack': return 'false'; + case 'compile': return 'false'; + case 'script': return ''; + case 'audit-report': return ''; + case 'github-token': return 'ghs_validToken123'; + default: return ''; + } + }); + + await run(); + + expect(mockSetFailed).not.toHaveBeenCalled(); + // Empty GITHUB_TOKEN should be overwritten with the input token + expect(process.env.GITHUB_TOKEN).toBe('ghs_validToken123'); + // GITHUB_APM_PAT should also be set (no "real" caller token existed) + expect(process.env.GITHUB_APM_PAT).toBe('ghs_validToken123'); + } finally { + if (prevToken === undefined) { + delete process.env.GITHUB_TOKEN; + } else { + process.env.GITHUB_TOKEN = prevToken; + } + if (prevApmPat === undefined) { + delete process.env.GITHUB_APM_PAT; + } else { + process.env.GITHUB_APM_PAT = prevApmPat; + } + } + }); }); diff --git a/src/runner.ts b/src/runner.ts index 88dbb0e..687d21a 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -27,16 +27,30 @@ export async function run(): Promise { const isolated = core.getInput('isolated') === 'true'; const auditReportInput = core.getInput('audit-report').trim(); - // Pass github-token input to APM subprocess as GITHUB_TOKEN and GITHUB_APM_PAT. + // Pass github-token input to APM subprocess as GITHUB_TOKEN. // GitHub Actions does not auto-export input values as env vars — // without this, APM runs unauthenticated (rate-limited, no private repo access). - // Use ??= so values already in the environment (e.g., a PAT set via job-level - // `env:`) are not clobbered by the action's default github.token. + // Use ??= so a GITHUB_TOKEN already in the environment (e.g., a PAT set via + // job-level `env:`) is not clobbered by the action's default github.token. + // + // GITHUB_APM_PAT is only forwarded when GITHUB_TOKEN was NOT already present. + // When a caller provides GITHUB_TOKEN via step/job-level env: (e.g., a GitHub + // App token from gh-aw), that token carries higher-specificity auth than the + // action's default github.token. Since APM's token precedence is + // GITHUB_APM_PAT > GITHUB_TOKEN > GH_TOKEN + // auto-setting GITHUB_APM_PAT to the default github.token would shadow the + // caller's intentional GITHUB_TOKEN, causing auth failures for cross-org or + // private-repo access. const githubToken = core.getInput('github-token'); if (githubToken) { core.setSecret(githubToken); - process.env.GITHUB_TOKEN ??= githubToken; - process.env.GITHUB_APM_PAT ??= githubToken; + const callerProvidedToken = !!process.env.GITHUB_TOKEN; + if (!process.env.GITHUB_TOKEN) { + process.env.GITHUB_TOKEN = githubToken; + } + if (!callerProvidedToken) { + process.env.GITHUB_APM_PAT ??= githubToken; + } } // Validate inputs before touching the filesystem.