952a2a7073
This pull request adds support for generating GitHub App installation
tokens for enterprise-level installations.
### What changed
- Added a new `enterprise` input to `action.yml`.
- Wired `enterprise` through `main.js` and `lib/main.js`.
- Added validation so `enterprise` cannot be combined with `owner` or
`repositories`.
- Implemented enterprise installation lookup using the direct GitHub API
route `GET /enterprises/{enterprise}/installation`, then used the
returned installation ID to mint an installation token through
`@octokit/auth-app`.
- Updated `README.md` with enterprise installation usage and input
documentation.
- Updated `dist/main.cjs` for the bundled action.
- Shared token creation retry behavior across repository, owner, and
enterprise paths so server errors and transient network errors are
retried, while client errors fail immediately.
### Tests
Added focused test coverage for:
- enterprise token creation
- enterprise token creation with explicit permissions
- enterprise installation not found
- mutual exclusivity with `owner`
- mutual exclusivity with `repositories`
- owner installation client errors are not retried
- transient network errors are retried during token creation
### Notes
- This keeps the existing repository-scoped token behavior unchanged.
- Owner, repository, and enterprise token creation now share the same
retry policy: server errors and recognized transient network errors are
retried, while client errors fail immediately. This intentionally fixes
the previous owner-path behavior that retried client errors.
Refs:
-
https://github.blog/changelog/2025-07-01-enterprise-level-access-for-github-apps-and-installation-automation-apis/
-
https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-enterprise-installation-for-the-authenticated-app
---------
Co-authored-by: Parker Brown <17183625+parkerbxyz@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
73 lines
2.1 KiB
JavaScript
73 lines
2.1 KiB
JavaScript
import { readdirSync } from "node:fs";
|
||
import { execFile } from "node:child_process";
|
||
import { promisify } from "node:util";
|
||
|
||
import { snapshot, test } from "node:test";
|
||
|
||
const execFileAsync = promisify(execFile);
|
||
|
||
// Serialize strings as-is so multiline output is human-readable in snapshots
|
||
snapshot.setDefaultSnapshotSerializers([
|
||
(value) => (typeof value === "string" ? value : undefined),
|
||
]);
|
||
|
||
function normalizeStderr(stderr) {
|
||
return stderr
|
||
.replaceAll(/\u001B\[[0-9;]*m/g, "")
|
||
.replaceAll(process.cwd(), "<cwd>")
|
||
.replaceAll(/:\d+:\d+/g, ":<line>:<column>");
|
||
}
|
||
|
||
// Get all files in tests directory
|
||
const files = readdirSync("tests");
|
||
|
||
// Files to ignore
|
||
const ignore = ["index.js", "index.js.snapshot", "main.js", "README.md"];
|
||
|
||
const testFiles = files.filter((file) => !ignore.includes(file)).sort();
|
||
|
||
// Throw an error if there is a file that does not end with test.js in the tests directory
|
||
for (const file of testFiles) {
|
||
if (!file.endsWith(".test.js")) {
|
||
throw new Error(`File ${file} does not end with .test.js`);
|
||
}
|
||
test(file, async (t) => {
|
||
// Override Actions environment variables that change `core`’s behavior
|
||
const {
|
||
GITHUB_OUTPUT,
|
||
GITHUB_STATE,
|
||
HTTP_PROXY,
|
||
HTTPS_PROXY,
|
||
http_proxy,
|
||
https_proxy,
|
||
NO_PROXY,
|
||
no_proxy,
|
||
NODE_OPTIONS,
|
||
NODE_USE_ENV_PROXY,
|
||
...env
|
||
} = process.env;
|
||
let stderr, stdout;
|
||
try {
|
||
({ stderr, stdout } = await execFileAsync("node", [`tests/${file}`], {
|
||
env,
|
||
}));
|
||
} catch (error) {
|
||
if (!(error instanceof Error) || !("stderr" in error) || !("stdout" in error)) {
|
||
throw error;
|
||
}
|
||
|
||
({ stderr, stdout } = error);
|
||
}
|
||
const trimmedStderr = normalizeStderr(stderr).replace(/\r?\n$/, "");
|
||
const trimmedStdout = stdout.replace(/\r?\n$/, "");
|
||
await t.test("stderr", (t) => {
|
||
if (trimmedStderr) t.assert.snapshot(trimmedStderr);
|
||
else t.assert.strictEqual(trimmedStderr, "");
|
||
});
|
||
await t.test("stdout", (t) => {
|
||
if (trimmedStdout) t.assert.snapshot(trimmedStdout);
|
||
else t.assert.strictEqual(trimmedStdout, "");
|
||
});
|
||
});
|
||
}
|