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
24 changes: 19 additions & 5 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
116 changes: 116 additions & 0 deletions src/__tests__/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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;
}
}
});

Expand Down Expand Up @@ -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;
}
}
});
});
24 changes: 19 additions & 5 deletions src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,30 @@ export async function run(): Promise<void> {
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.
Expand Down
Loading