diff --git a/README.md b/README.md index 5c88697..209e4ac 100644 --- a/README.md +++ b/README.md @@ -135,12 +135,45 @@ jobs: category: apm-audit ``` +## Private repo authentication + +By default, `github-token` (which defaults to `${{ github.token }}`) is automatically forwarded to APM as `GITHUB_APM_PAT`. This means same-org private repos work with zero config. + +```yaml +# Same-org private repos: zero config +- uses: microsoft/apm-action@v1 +``` + +For cross-org private repos, pass a PAT with broader scope via the `github-token` input: + +```yaml +# Cross-org private repos: pass a broader-scoped PAT +- uses: microsoft/apm-action@v1 + with: + github-token: ${{ secrets.APM_PAT }} +``` + +For multi-org or multi-platform scenarios, use the `env:` block for full control. An explicit `GITHUB_APM_PAT` in `env:` always wins over the auto-forwarded value: + +```yaml +# Multi-org / multi-platform: full control via env block +- uses: microsoft/apm-action@v1 + env: + GITHUB_APM_PAT: ${{ secrets.APM_PAT }} + GITHUB_APM_PAT_CONTOSO: ${{ secrets.APM_PAT_CONTOSO }} + ADO_APM_PAT: ${{ secrets.ADO_PAT }} + ARTIFACTORY_APM_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }} +``` + +> **Note:** GitHub Actions forbids secrets named with the `GITHUB_` prefix, so you cannot create a secret called `GITHUB_APM_PAT` directly. The auto-forward from `github-token` covers the common case. For cross-org tokens, name your secret something like `APM_PAT` and pass it via `github-token` or `env: GITHUB_APM_PAT`. + ## Inputs | Input | Required | Default | Description | |---|---|---|---| | `working-directory` | No | `.` | Working directory for execution. Must exist in non-isolated mode (with your `apm.yml`). In `isolated`, `pack`, or `bundle` modes the directory is created automatically. | | `apm-version` | No | `latest` | APM version to install | +| `github-token` | No | `${{ github.token }}` | GitHub token for API calls. Auto-forwarded as `GITHUB_APM_PAT` so same-org private repos work with zero config. Pass a broader-scoped PAT for cross-org access. | | `script` | No | | APM script to run after install | | `dependencies` | No | | YAML array of extra dependencies to install (additive to apm.yml) | | `isolated` | No | `false` | Ignore apm.yml and clear pre-existing primitive dirs — install only inline dependencies | diff --git a/dist/index.js b/dist/index.js index 0786283..2ab5f49 100644 --- a/dist/index.js +++ b/dist/index.js @@ -41181,15 +41181,16 @@ 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. + // Pass github-token input to APM subprocess as GITHUB_TOKEN and GITHUB_APM_PAT. // GitHub Actions does not auto-export input values as env vars — // without this, APM runs unauthenticated (rate-limited, no private repo access). - // 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. + // 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. const githubToken = getInput('github-token'); if (githubToken) { core_setSecret(githubToken); process.env.GITHUB_TOKEN ??= githubToken; + 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 cf801c1..952e7d9 100644 --- a/src/__tests__/runner.test.ts +++ b/src/__tests__/runner.test.ts @@ -365,13 +365,15 @@ describe('run', () => { expect(mockSetOutput).not.toHaveBeenCalledWith('audit-report-path', expect.anything()); }); - it('passes github-token input as GITHUB_TOKEN env var', async () => { + it('passes github-token input as GITHUB_TOKEN and GITHUB_APM_PAT env vars', async () => { 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; delete process.env.GITHUB_TOKEN; + delete process.env.GITHUB_APM_PAT; try { mockGetInput.mockImplementation((name: unknown) => { @@ -394,6 +396,7 @@ describe('run', () => { expect(mockSetFailed).not.toHaveBeenCalled(); // Token should be set in process.env for subprocess inheritance expect(process.env.GITHUB_TOKEN).toBe('ghs_fakeToken123'); + expect(process.env.GITHUB_APM_PAT).toBe('ghs_fakeToken123'); // Token should be masked in logs expect(mockSetSecret).toHaveBeenCalledWith('ghs_fakeToken123'); } finally { @@ -402,16 +405,23 @@ describe('run', () => { } 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 set GITHUB_TOKEN when github-token input is empty', async () => { + it('does not set GITHUB_TOKEN or GITHUB_APM_PAT when github-token input is empty', async () => { 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; delete process.env.GITHUB_TOKEN; + delete process.env.GITHUB_APM_PAT; try { mockGetInput.mockImplementation((name: unknown) => { @@ -432,8 +442,9 @@ describe('run', () => { await run(); expect(mockSetFailed).not.toHaveBeenCalled(); - // Token should NOT be set when input is empty + // Tokens should NOT be set when input is empty expect(process.env.GITHUB_TOKEN).toBeUndefined(); + expect(process.env.GITHUB_APM_PAT).toBeUndefined(); expect(mockSetSecret).not.toHaveBeenCalled(); } finally { if (prevToken === undefined) { @@ -441,6 +452,11 @@ describe('run', () => { } else { process.env.GITHUB_TOKEN = prevToken; } + if (prevApmPat === undefined) { + delete process.env.GITHUB_APM_PAT; + } else { + process.env.GITHUB_APM_PAT = prevApmPat; + } } }); @@ -481,4 +497,42 @@ describe('run', () => { } } }); + + it('does not clobber existing GITHUB_APM_PAT from job-level env', async () => { + 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 prevApmPat = process.env.GITHUB_APM_PAT; + process.env.GITHUB_APM_PAT = 'ghp_userProvidedApmPAT'; + + 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_defaultActionToken'; + default: return ''; + } + }); + + await run(); + + expect(mockSetFailed).not.toHaveBeenCalled(); + // User's explicitly-set GITHUB_APM_PAT should be preserved + expect(process.env.GITHUB_APM_PAT).toBe('ghp_userProvidedApmPAT'); + } finally { + 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 ea74ed9..88dbb0e 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -27,15 +27,16 @@ 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. + // Pass github-token input to APM subprocess as GITHUB_TOKEN and GITHUB_APM_PAT. // GitHub Actions does not auto-export input values as env vars — // without this, APM runs unauthenticated (rate-limited, no private repo access). - // 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. + // 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. const githubToken = core.getInput('github-token'); if (githubToken) { core.setSecret(githubToken); process.env.GITHUB_TOKEN ??= githubToken; + process.env.GITHUB_APM_PAT ??= githubToken; } // Validate inputs before touching the filesystem.