Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2156e19368 | |||
| b24274086f | |||
| fee1f7d63c | |||
| 6bda5bc141 | |||
| b04413352d | |||
| 5cbc65624c | |||
| b4f638f48e | |||
| efd9eb0674 | |||
| 340567cb0e | |||
| 5c1e00d1af | |||
| 64cd9c9a45 | |||
| 29824e69f5 | |||
| b212e6a739 | |||
| 8efbf9bf0f | |||
| 7e473efe3c | |||
| dce3be8b28 | |||
| 5480f4325a | |||
| d90aa53233 | |||
| 55e2a4b2cc | |||
| cc6f999683 | |||
| 40fa6b52b3 | |||
| 396e5022f0 | |||
| f48f2eb932 | |||
| b7f83f6278 | |||
| 2a47cfc9ef | |||
| 1ff8cc40cc | |||
| b96fde71c0 | |||
| 6701853927 | |||
| bef1eaf1c0 | |||
| 14350b6aaa | |||
| 6cf7b5f22a | |||
| 22e6bc6b49 | |||
| 3b3f07c3d1 | |||
| 7b860611c2 | |||
| a84c82dc20 | |||
| 81e8c224df | |||
| 7434028a6d | |||
| 46f9f788b8 | |||
| 3c69395e16 | |||
| 55b8c24e8d | |||
| cbc2930e9b |
@@ -12,6 +12,6 @@ jobs:
|
||||
id-token: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Publish Immutable Action
|
||||
uses: actions/publish-immutable-action@v0.0.4
|
||||
|
||||
@@ -18,14 +18,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# build local version to create token
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: 'npm'
|
||||
node-version-file: package.json
|
||||
|
||||
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# This workflow warns and then closes issues that have had no activity for a specified amount of time.
|
||||
# https://github.com/actions/stale
|
||||
|
||||
name: Stale
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# 00:00 UTC on Mondays
|
||||
- cron: '0 0 * * 1'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
DAYS_BEFORE_STALE: 180
|
||||
DAYS_BEFORE_CLOSE: 60
|
||||
STALE_LABEL: 'stale'
|
||||
STALE_LABEL_URL: ${{github.server_url}}/${{github.repository}}/labels/stale
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
operations-per-run: 100
|
||||
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
|
||||
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
|
||||
stale-issue-label: ${{ env.STALE_LABEL }}
|
||||
stale-pr-label: ${{ env.STALE_LABEL }}
|
||||
stale-issue-message: 'This issue has been marked ${{ env.STALE_LABEL_URL }} because it has been open for ${{ env.DAYS_BEFORE_STALE }} days with no activity. Please close this issue if it is no longer needed. If this issue is still relevant and you would like it to remain open, simply update it within the next ${{ env.DAYS_BEFORE_CLOSE }} days.'
|
||||
stale-pr-message: 'This pull request has been marked ${{ env.STALE_LABEL_URL }} because it has been open for ${{ env.DAYS_BEFORE_STALE }} days with no activity. Please close this pull request if it is no longer needed. If this pull request is still relevant and you would like it to remain open, simply update it within the next ${{ env.DAYS_BEFORE_CLOSE }} days.'
|
||||
+10
-11
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -16,30 +17,28 @@ permissions:
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
name: Integration
|
||||
name: integration
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: 'npm'
|
||||
node-version-file: package.json
|
||||
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
end-to-end:
|
||||
name: End-to-End
|
||||
name: end-to-end
|
||||
runs-on: ubuntu-latest
|
||||
# do not run from forks, as forks don’t have access to repository secrets
|
||||
if: github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
|
||||
if: github.event_name == 'merge_group' || github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
node-version-file: package.json
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- uses: ./ # Uses the action in the root directory
|
||||
|
||||
@@ -13,21 +13,30 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-permission-inputs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
COMMIT_MESSAGE: 'feat: update permission inputs'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: 'npm'
|
||||
node-version-file: package.json
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run permission inputs update script
|
||||
run: node scripts/update-permission-inputs.js
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1
|
||||
id: auto-commit
|
||||
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
|
||||
with:
|
||||
commit_message: 'feat: update permission inputs'
|
||||
commit_message: ${{ env.COMMIT_MESSAGE }}
|
||||
- name: Update PR title
|
||||
if: github.event_name == 'pull_request' && steps.auto-commit.outputs.changes_detected == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr edit ${{ github.event.pull_request.number }} --title "${{ env.COMMIT_MESSAGE }}"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.env
|
||||
coverage
|
||||
node_modules/
|
||||
.DS_Store
|
||||
@@ -1 +0,0 @@
|
||||
20.9.0
|
||||
@@ -195,6 +195,28 @@ jobs:
|
||||
body: "Hello, World!"
|
||||
```
|
||||
|
||||
### Create a token for an enterprise installation
|
||||
|
||||
```yaml
|
||||
on: [workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
hello-world:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
enterprise-slug: my-enterprise-slug
|
||||
- name: Call enterprise management REST API with gh
|
||||
run: |
|
||||
gh api /enterprises/my-enterprise-slug/apps/installable_organizations
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
```
|
||||
|
||||
### Create a token with specific permissions
|
||||
|
||||
> [!NOTE]
|
||||
@@ -335,6 +357,13 @@ steps:
|
||||
> [!NOTE]
|
||||
> If `owner` is set and `repositories` is empty, access will be scoped to all repositories in the provided repository owner's installation. If `owner` and `repositories` are empty, access will be scoped to only the current repository.
|
||||
|
||||
### `enterprise-slug`
|
||||
|
||||
**Optional:** The slug of the enterprise to generate a token for enterprise-level app installations.
|
||||
|
||||
> [!NOTE]
|
||||
> The `enterprise-slug` input is mutually exclusive with `owner` and `repositories`. GitHub Apps can be installed on enterprise accounts with permissions that let them call enterprise management APIs. Enterprise installations do not grant access to organization or repository resources.
|
||||
|
||||
### `permission-<permission name>`
|
||||
|
||||
**Optional:** The permissions to grant to the token. By default, the token inherits all of the installation's permissions. We recommend to explicitly list the permissions that are required for a use case. This follows GitHub's own recommendation to [control permissions of `GITHUB_TOKEN` in workflows](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token). The documentation also lists all available permissions, just prefix the permission key with `permission-` (e.g., `pull-requests` → `permission-pull-requests`).
|
||||
|
||||
+8
-1
@@ -17,6 +17,9 @@ inputs:
|
||||
repositories:
|
||||
description: "Comma or newline-separated list of repositories to install the GitHub App on (defaults to current repository if owner is unset)"
|
||||
required: false
|
||||
enterprise-slug:
|
||||
description: "Enterprise slug for enterprise-level app installations (cannot be used with 'owner' or 'repositories')"
|
||||
required: false
|
||||
skip-token-revoke:
|
||||
description: "If true, the token will not be revoked when the current job is complete"
|
||||
required: false
|
||||
@@ -37,12 +40,16 @@ inputs:
|
||||
description: "The level of permission to grant the access token to create, edit, delete, and list Codespaces. Can be set to 'read' or 'write'."
|
||||
permission-contents:
|
||||
description: "The level of permission to grant the access token for repository contents, commits, branches, downloads, releases, and merges. Can be set to 'read' or 'write'."
|
||||
permission-custom-properties-for-organizations:
|
||||
description: "The level of permission to grant the access token to view and edit custom properties for an organization, when allowed by the property. Can be set to 'read' or 'write'."
|
||||
permission-dependabot-secrets:
|
||||
description: "The level of permission to grant the access token to manage Dependabot secrets. Can be set to 'read' or 'write'."
|
||||
permission-deployments:
|
||||
description: "The level of permission to grant the access token for deployments and deployment statuses. Can be set to 'read' or 'write'."
|
||||
permission-email-addresses:
|
||||
description: "The level of permission to grant the access token to manage the email addresses belonging to a user. Can be set to 'read' or 'write'."
|
||||
permission-enterprise-custom-properties-for-organizations:
|
||||
description: "The level of permission to grant the access token for organization custom properties management at the enterprise level. Can be set to 'read', 'write', or 'admin'."
|
||||
permission-environments:
|
||||
description: "The level of permission to grant the access token for managing repository environments. Can be set to 'read' or 'write'."
|
||||
permission-followers:
|
||||
@@ -68,7 +75,7 @@ inputs:
|
||||
permission-organization-custom-org-roles:
|
||||
description: "The level of permission to grant the access token for custom organization roles management. Can be set to 'read' or 'write'."
|
||||
permission-organization-custom-properties:
|
||||
description: "The level of permission to grant the access token for custom property management. Can be set to 'read', 'write', or 'admin'."
|
||||
description: "The level of permission to grant the access token for repository custom properties management at the organization level. Can be set to 'read', 'write', or 'admin'."
|
||||
permission-organization-custom-roles:
|
||||
description: "The level of permission to grant the access token for custom repository roles management. Can be set to 'read' or 'write'."
|
||||
permission-organization-events:
|
||||
|
||||
Vendored
+16822
-16400
File diff suppressed because one or more lines are too long
Vendored
+15646
-12132
File diff suppressed because one or more lines are too long
+103
-42
@@ -4,6 +4,7 @@ import pRetry from "p-retry";
|
||||
/**
|
||||
* @param {string} appId
|
||||
* @param {string} privateKey
|
||||
* @param {string} enterpriseSlug
|
||||
* @param {string} owner
|
||||
* @param {string[]} repositories
|
||||
* @param {undefined | Record<string, string>} permissions
|
||||
@@ -15,58 +16,70 @@ import pRetry from "p-retry";
|
||||
export async function main(
|
||||
appId,
|
||||
privateKey,
|
||||
enterpriseSlug,
|
||||
owner,
|
||||
repositories,
|
||||
permissions,
|
||||
core,
|
||||
createAppAuth,
|
||||
request,
|
||||
skipTokenRevoke
|
||||
skipTokenRevoke,
|
||||
|
||||
) {
|
||||
// Validate mutual exclusivity of enterprise-slug with owner/repositories
|
||||
if (enterpriseSlug && (owner || repositories.length > 0)) {
|
||||
throw new Error("Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs");
|
||||
}
|
||||
|
||||
let parsedOwner = "";
|
||||
let parsedRepositoryNames = [];
|
||||
|
||||
// If neither owner nor repositories are set, default to current repository
|
||||
if (!owner && repositories.length === 0) {
|
||||
const [owner, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
|
||||
parsedOwner = owner;
|
||||
parsedRepositoryNames = [repo];
|
||||
// Skip owner/repository parsing if enterprise-slug is set
|
||||
if (!enterpriseSlug) {
|
||||
// If neither owner nor repositories are set, default to current repository
|
||||
if (!owner && repositories.length === 0) {
|
||||
const [owner, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
|
||||
parsedOwner = owner;
|
||||
parsedRepositoryNames = [repo];
|
||||
|
||||
core.info(
|
||||
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner}/${repo}).`
|
||||
);
|
||||
}
|
||||
core.info(
|
||||
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner}/${repo}).`
|
||||
);
|
||||
}
|
||||
|
||||
// If only an owner is set, default to all repositories from that owner
|
||||
if (owner && repositories.length === 0) {
|
||||
parsedOwner = owner;
|
||||
// If only an owner is set, default to all repositories from that owner
|
||||
if (owner && repositories.length === 0) {
|
||||
parsedOwner = owner;
|
||||
|
||||
core.info(
|
||||
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.`
|
||||
);
|
||||
}
|
||||
core.info(
|
||||
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.`
|
||||
);
|
||||
}
|
||||
|
||||
// If repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER`
|
||||
if (!owner && repositories.length > 0) {
|
||||
parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER);
|
||||
parsedRepositoryNames = repositories;
|
||||
// If repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER`
|
||||
if (!owner && repositories.length > 0) {
|
||||
parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER);
|
||||
parsedRepositoryNames = repositories;
|
||||
|
||||
core.info(
|
||||
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories
|
||||
.map((repo) => `\n- ${parsedOwner}/${repo}`)
|
||||
.join("")}`
|
||||
);
|
||||
}
|
||||
core.info(
|
||||
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories
|
||||
.map((repo) => `\n- ${parsedOwner}/${repo}`)
|
||||
.join("")}`
|
||||
);
|
||||
}
|
||||
|
||||
// If both owner and repositories are set, use those values
|
||||
if (owner && repositories.length > 0) {
|
||||
parsedOwner = owner;
|
||||
parsedRepositoryNames = repositories;
|
||||
// If both owner and repositories are set, use those values
|
||||
if (owner && repositories.length > 0) {
|
||||
parsedOwner = owner;
|
||||
parsedRepositoryNames = repositories;
|
||||
|
||||
core.info(
|
||||
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
|
||||
core.info(
|
||||
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
|
||||
${repositories.map((repo) => `\n- ${parsedOwner}/${repo}`).join("")}`
|
||||
);
|
||||
);
|
||||
}
|
||||
} else {
|
||||
core.info(`Creating enterprise installation token for enterprise "${enterpriseSlug}".`);
|
||||
}
|
||||
|
||||
const auth = createAppAuth({
|
||||
@@ -76,9 +89,22 @@ export async function main(
|
||||
});
|
||||
|
||||
let authentication, installationId, appSlug;
|
||||
// If at least one repository is set, get installation ID from that repository
|
||||
|
||||
if (parsedRepositoryNames.length > 0) {
|
||||
|
||||
// If enterprise-slug is set, get installation ID from the enterprise
|
||||
if (enterpriseSlug) {
|
||||
({ authentication, installationId, appSlug } = await pRetry(
|
||||
() => getTokenFromEnterprise(request, auth, enterpriseSlug, permissions),
|
||||
{
|
||||
shouldRetry: (error) => error.status >= 500,
|
||||
onFailedAttempt: (error) => {
|
||||
core.info(
|
||||
`Failed to create token for enterprise "${enterpriseSlug}" (attempt ${error.attemptNumber}): ${error.message}`
|
||||
);
|
||||
},
|
||||
retries: 3,
|
||||
}
|
||||
));
|
||||
} else if (parsedRepositoryNames.length > 0) {
|
||||
({ authentication, installationId, appSlug } = await pRetry(
|
||||
() =>
|
||||
getTokenFromRepository(
|
||||
@@ -89,12 +115,12 @@ export async function main(
|
||||
permissions
|
||||
),
|
||||
{
|
||||
shouldRetry: (error) => error.status >= 500,
|
||||
onFailedAttempt: (error) => {
|
||||
shouldRetry: ({ error }) => error.status >= 500,
|
||||
onFailedAttempt: (context) => {
|
||||
core.info(
|
||||
`Failed to create token for "${parsedRepositoryNames.join(
|
||||
","
|
||||
)}" (attempt ${error.attemptNumber}): ${error.message}`
|
||||
)}" (attempt ${context.attemptNumber}): ${context.error.message}`
|
||||
);
|
||||
},
|
||||
retries: 3,
|
||||
@@ -105,9 +131,9 @@ export async function main(
|
||||
({ authentication, installationId, appSlug } = await pRetry(
|
||||
() => getTokenFromOwner(request, auth, parsedOwner, permissions),
|
||||
{
|
||||
onFailedAttempt: (error) => {
|
||||
onFailedAttempt: (context) => {
|
||||
core.info(
|
||||
`Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}`
|
||||
`Failed to create token for "${parsedOwner}" (attempt ${context.attemptNumber}): ${context.error.message}`
|
||||
);
|
||||
},
|
||||
retries: 3,
|
||||
@@ -181,3 +207,38 @@ async function getTokenFromRepository(
|
||||
|
||||
return { authentication, installationId, appSlug };
|
||||
}
|
||||
|
||||
async function getTokenFromEnterprise(request, auth, enterpriseSlug, permissions) {
|
||||
// Get all installations and find the enterprise one
|
||||
// https://docs.github.com/rest/apps/apps#list-installations-for-the-authenticated-app
|
||||
// Note: Currently we do not have a way to get the installation for an enterprise directly,
|
||||
// so as a workaround we need to list all installations and filter for the enterprise one.
|
||||
const response = await request("GET /app/installations", {
|
||||
request: {
|
||||
hook: auth.hook,
|
||||
},
|
||||
});
|
||||
|
||||
// Find the enterprise installation
|
||||
const enterpriseInstallation = response.data.find(
|
||||
installation => installation.target_type === "Enterprise" &&
|
||||
installation.account?.slug === enterpriseSlug
|
||||
);
|
||||
|
||||
/* c8 ignore next 3 */
|
||||
if (!enterpriseInstallation) {
|
||||
throw new Error(`No enterprise installation found matching the name ${enterpriseSlug}. Available installations: ${response.data.map(i => `${i.target_type}:${i.account?.login || 'N/A'}`).join(', ')}`);
|
||||
}
|
||||
|
||||
// Get token for the enterprise installation
|
||||
const authentication = await auth({
|
||||
type: "installation",
|
||||
installationId: enterpriseInstallation.id,
|
||||
permissions,
|
||||
});
|
||||
|
||||
const installationId = enterpriseInstallation.id;
|
||||
const appSlug = enterpriseInstallation["app_slug"];
|
||||
|
||||
return { authentication, installationId, appSlug };
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import core from "@actions/core";
|
||||
import * as core from "@actions/core";
|
||||
import { request } from "@octokit/request";
|
||||
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
import core from "@actions/core";
|
||||
import * as core from "@actions/core";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
|
||||
import { getPermissionsFromInputs } from "./lib/get-permissions-from-inputs.js";
|
||||
@@ -17,6 +17,7 @@ if (!process.env.GITHUB_REPOSITORY_OWNER) {
|
||||
|
||||
const appId = core.getInput("app-id");
|
||||
const privateKey = core.getInput("private-key");
|
||||
const enterpriseSlug = core.getInput("enterprise-slug");
|
||||
const owner = core.getInput("owner");
|
||||
const repositories = core
|
||||
.getInput("repositories")
|
||||
@@ -32,6 +33,7 @@ const permissions = getPermissionsFromInputs(process.env);
|
||||
export default main(
|
||||
appId,
|
||||
privateKey,
|
||||
enterpriseSlug,
|
||||
owner,
|
||||
repositories,
|
||||
permissions,
|
||||
@@ -40,7 +42,10 @@ export default main(
|
||||
request,
|
||||
skipTokenRevoke,
|
||||
).catch((error) => {
|
||||
/* c8 ignore next 3 */
|
||||
/* c8 ignore next 5 */
|
||||
console.error(error);
|
||||
core.setFailed(error.message);
|
||||
// Don't set failed in test mode (when GITHUB_OUTPUT is undefined)
|
||||
if (process.env.GITHUB_OUTPUT !== undefined) {
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
Generated
+275
-533
File diff suppressed because it is too large
Load Diff
+16
-12
@@ -2,8 +2,12 @@
|
||||
"name": "create-github-app-token",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "2.1.3",
|
||||
"version": "2.2.2",
|
||||
"description": "GitHub Action for creating a GitHub App Installation Access Token",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"packageManager": "npm@10.9.4",
|
||||
"scripts": {
|
||||
"build": "esbuild main.js post.js --bundle --outdir=dist --out-extension:.js=.cjs --platform=node --target=node20.0.0 --packages=bundle",
|
||||
"test": "c8 --100 ava tests/index.js",
|
||||
@@ -12,22 +16,22 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@octokit/auth-app": "^7.2.1",
|
||||
"@octokit/request": "^10.0.3",
|
||||
"p-retry": "^6.2.1",
|
||||
"undici": "^7.16.0"
|
||||
"@actions/core": "^3.0.0",
|
||||
"@octokit/auth-app": "^8.2.0",
|
||||
"@octokit/request": "^10.0.8",
|
||||
"p-retry": "^7.1.1",
|
||||
"undici": "^7.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/openapi": "^19.1.0",
|
||||
"@sinonjs/fake-timers": "^14.0.0",
|
||||
"@octokit/openapi": "^21.0.0",
|
||||
"@sinonjs/fake-timers": "^15.1.0",
|
||||
"ava": "^6.4.1",
|
||||
"c8": "^10.1.3",
|
||||
"dotenv": "^17.2.1",
|
||||
"esbuild": "^0.25.8",
|
||||
"execa": "^9.6.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"esbuild": "^0.27.3",
|
||||
"execa": "^9.6.1",
|
||||
"open-cli": "^8.0.0",
|
||||
"yaml": "^2.8.1"
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
import core from "@actions/core";
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { post } from "./lib/post.js";
|
||||
import request from "./lib/request.js";
|
||||
|
||||
@@ -187,6 +187,14 @@
|
||||
"write"
|
||||
]
|
||||
},
|
||||
"custom_properties_for_organizations": {
|
||||
"type": "string",
|
||||
"description": "The level of permission to grant the access token to view and edit custom properties for an organization, when allowed by the property.",
|
||||
"enum": [
|
||||
"read",
|
||||
"write"
|
||||
]
|
||||
},
|
||||
"members": {
|
||||
"type": "string",
|
||||
"description": "The level of permission to grant the access token for organization teams and members.",
|
||||
@@ -221,7 +229,7 @@
|
||||
},
|
||||
"organization_custom_properties": {
|
||||
"type": "string",
|
||||
"description": "The level of permission to grant the access token for custom property management.",
|
||||
"description": "The level of permission to grant the access token for repository custom properties management at the organization level.",
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
@@ -384,6 +392,15 @@
|
||||
"read",
|
||||
"write"
|
||||
]
|
||||
},
|
||||
"enterprise_custom_properties_for_organizations": {
|
||||
"type": "string",
|
||||
"description": "The level of permission to grant the access token for organization custom properties management at the enterprise level.",
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { test } from "./main.js";
|
||||
|
||||
|
||||
// Verify `main` handles when no enterprise installation is found.
|
||||
await test((mockPool) => {
|
||||
delete process.env.INPUT_OWNER;
|
||||
delete process.env.INPUT_REPOSITORIES;
|
||||
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
|
||||
|
||||
|
||||
// Mock the /app/installations endpoint to return only non-enterprise installations
|
||||
mockPool
|
||||
.intercept({
|
||||
path: "/app/installations",
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/vnd.github.v3+json",
|
||||
"user-agent": "actions/create-github-app-token",
|
||||
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
|
||||
},
|
||||
})
|
||||
.reply(
|
||||
200,
|
||||
[
|
||||
{
|
||||
id: "111111",
|
||||
app_slug: "github-actions",
|
||||
target_type: "Organization",
|
||||
account: { login: "some-org" }
|
||||
},
|
||||
{
|
||||
id: "222222",
|
||||
app_slug: "github-actions",
|
||||
target_type: "User",
|
||||
account: { login: "some-user" }
|
||||
}
|
||||
],
|
||||
{ headers: { "content-type": "application/json" } }
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { DEFAULT_ENV } from "./main.js";
|
||||
|
||||
// Verify `main` exits with an error when `enterprise-slug` is used with both `owner` and `repositories` inputs.
|
||||
try {
|
||||
// Set up environment with enterprise-slug, owner, and repositories all set
|
||||
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
|
||||
process.env.INPUT_OWNER = "test-owner";
|
||||
process.env.INPUT_REPOSITORIES = "repo1,repo2";
|
||||
|
||||
await import("../main.js");
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { DEFAULT_ENV } from "./main.js";
|
||||
|
||||
// Verify `main` exits with an error when `enterprise-slug` is used with `owner` input.
|
||||
try {
|
||||
// Set up environment with enterprise-slug and owner set
|
||||
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
|
||||
process.env.INPUT_OWNER = "test-owner";
|
||||
|
||||
await import("../main.js");
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { DEFAULT_ENV } from "./main.js";
|
||||
|
||||
// Verify `main` exits with an error when `enterprise-slug` is used with `repositories` input.
|
||||
try {
|
||||
// Set up environment with enterprise-slug and repositories set
|
||||
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
|
||||
process.env.INPUT_REPOSITORIES = "repo1,repo2";
|
||||
|
||||
await import("../main.js");
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { test } from "./main.js";
|
||||
|
||||
// Verify `main` successfully obtains a token when only the `enterprise-slug` input is set.
|
||||
await test((mockPool) => {
|
||||
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
|
||||
delete process.env.INPUT_OWNER;
|
||||
delete process.env.INPUT_REPOSITORIES;
|
||||
|
||||
// Mock the /app/installations endpoint to return an enterprise installation
|
||||
const mockInstallationId = "123456";
|
||||
const mockAppSlug = "github-actions";
|
||||
mockPool
|
||||
.intercept({
|
||||
path: "/app/installations",
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/vnd.github.v3+json",
|
||||
"user-agent": "actions/create-github-app-token",
|
||||
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
|
||||
},
|
||||
})
|
||||
.reply(
|
||||
200,
|
||||
[
|
||||
{
|
||||
id: mockInstallationId,
|
||||
app_slug: mockAppSlug,
|
||||
target_type: "Enterprise",
|
||||
account: { login: "test-enterprise", slug: "test-enterprise" }
|
||||
}
|
||||
],
|
||||
{ headers: { "content-type": "application/json" } }
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { test } from "./main.js";
|
||||
|
||||
// Verify `main` successfully generates enterprise token with basic functionality.
|
||||
await test((mockPool) => {
|
||||
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
|
||||
delete process.env.INPUT_OWNER;
|
||||
delete process.env.INPUT_REPOSITORIES;
|
||||
|
||||
// Mock the /app/installations endpoint to return an enterprise installation
|
||||
const mockInstallationId = "123456";
|
||||
const mockAppSlug = "github-actions";
|
||||
mockPool
|
||||
.intercept({
|
||||
path: "/app/installations",
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/vnd.github.v3+json",
|
||||
"user-agent": "actions/create-github-app-token",
|
||||
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
|
||||
},
|
||||
})
|
||||
.reply(
|
||||
200,
|
||||
[
|
||||
{
|
||||
id: mockInstallationId,
|
||||
app_slug: mockAppSlug,
|
||||
target_type: "Enterprise",
|
||||
account: { login: "test-enterprise", slug: "test-enterprise" }
|
||||
}
|
||||
],
|
||||
{ headers: { "content-type": "application/json" } }
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { test } from "./main.js";
|
||||
|
||||
// Verify `main` successfully generates enterprise token with specific permissions.
|
||||
await test((mockPool) => {
|
||||
process.env["INPUT_ENTERPRISE-SLUG"] = "test-enterprise";
|
||||
delete process.env.INPUT_OWNER;
|
||||
delete process.env.INPUT_REPOSITORIES;
|
||||
process.env["INPUT_PERMISSION-ENTERPRISE-ORGANIZATIONS"] = "read";
|
||||
process.env["INPUT_PERMISSION-ENTERPRISE-PEOPLE"] = "write";
|
||||
|
||||
// Mock the /app/installations endpoint to return an enterprise installation
|
||||
const mockInstallationId = "123456";
|
||||
const mockAppSlug = "github-actions";
|
||||
mockPool
|
||||
.intercept({
|
||||
path: "/app/installations",
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/vnd.github.v3+json",
|
||||
"user-agent": "actions/create-github-app-token",
|
||||
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
|
||||
},
|
||||
})
|
||||
.reply(
|
||||
200,
|
||||
[
|
||||
{
|
||||
id: mockInstallationId,
|
||||
app_slug: mockAppSlug,
|
||||
target_type: "Enterprise",
|
||||
account: { login: "test-enterprise", slug: "test-enterprise" }
|
||||
}
|
||||
],
|
||||
{ headers: { "content-type": "application/json" } }
|
||||
);
|
||||
});
|
||||
@@ -39,6 +39,139 @@ Generated by [AVA](https://avajs.dev).
|
||||
POST /api/v3/app/installations/123456/access_tokens␊
|
||||
{"repositories":["create-github-app-token"]}`
|
||||
|
||||
## main-enterprise-installation-not-found.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
`Error: No enterprise installation found matching the name test-enterprise. Available installations: Organization:some-org, User:some-user␊
|
||||
at getTokenFromEnterprise (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:230:11)␊
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)␊
|
||||
at async pRetry (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/node_modules/p-retry/index.js:197:19)␊
|
||||
at async main (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:95:52)␊
|
||||
at async test (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main.js:111:3)␊
|
||||
at async file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main-enterprise-installation-not-found.test.js:5:1`
|
||||
|
||||
> stdout
|
||||
|
||||
`Creating enterprise installation token for enterprise "test-enterprise".␊
|
||||
Failed to create token for enterprise "test-enterprise" (attempt 1): undefined␊
|
||||
--- REQUESTS ---␊
|
||||
GET /app/installations`
|
||||
|
||||
## main-enterprise-mutual-exclusivity-both.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
`Error: Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs␊
|
||||
at main (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:31:11)␊
|
||||
at file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/main.js:33:16␊
|
||||
at ModuleJob.run (node:internal/modules/esm/module_job:430:25)␊
|
||||
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:639:26)␊
|
||||
at async file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main-enterprise-mutual-exclusivity-both.test.js:13:3`
|
||||
|
||||
> stdout
|
||||
|
||||
''
|
||||
|
||||
## main-enterprise-mutual-exclusivity-owner.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
`Error: Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs␊
|
||||
at main (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:31:11)␊
|
||||
at file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/main.js:33:16␊
|
||||
at ModuleJob.run (node:internal/modules/esm/module_job:430:25)␊
|
||||
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:639:26)␊
|
||||
at async file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main-enterprise-mutual-exclusivity-owner.test.js:12:3`
|
||||
|
||||
> stdout
|
||||
|
||||
''
|
||||
|
||||
## main-enterprise-mutual-exclusivity-repositories.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
`Error: Cannot use 'enterprise-slug' input with 'owner' or 'repositories' inputs␊
|
||||
at main (file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/lib/main.js:31:11)␊
|
||||
at file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/main.js:33:16␊
|
||||
at ModuleJob.run (node:internal/modules/esm/module_job:430:25)␊
|
||||
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:639:26)␊
|
||||
at async file:///Users/parkerbxyz/.copilot/worktrees/create-github-app-token/pr-263/tests/main-enterprise-mutual-exclusivity-repositories.test.js:12:3`
|
||||
|
||||
> stdout
|
||||
|
||||
''
|
||||
|
||||
## main-enterprise-only-success.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
''
|
||||
|
||||
> stdout
|
||||
|
||||
`Creating enterprise installation token for enterprise "test-enterprise".␊
|
||||
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
␊
|
||||
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
␊
|
||||
::set-output name=installation-id::123456␊
|
||||
␊
|
||||
::set-output name=app-slug::github-actions␊
|
||||
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
::save-state name=expiresAt::2016-07-11T22:14:10Z␊
|
||||
--- REQUESTS ---␊
|
||||
GET /app/installations␊
|
||||
POST /app/installations/123456/access_tokens␊
|
||||
null`
|
||||
|
||||
## main-enterprise-token-success.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
''
|
||||
|
||||
> stdout
|
||||
|
||||
`Creating enterprise installation token for enterprise "test-enterprise".␊
|
||||
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
␊
|
||||
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
␊
|
||||
::set-output name=installation-id::123456␊
|
||||
␊
|
||||
::set-output name=app-slug::github-actions␊
|
||||
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
::save-state name=expiresAt::2016-07-11T22:14:10Z␊
|
||||
--- REQUESTS ---␊
|
||||
GET /app/installations␊
|
||||
POST /app/installations/123456/access_tokens␊
|
||||
null`
|
||||
|
||||
## main-enterprise-token-with-permissions.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
''
|
||||
|
||||
> stdout
|
||||
|
||||
`Creating enterprise installation token for enterprise "test-enterprise".␊
|
||||
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
␊
|
||||
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
␊
|
||||
::set-output name=installation-id::123456␊
|
||||
␊
|
||||
::set-output name=app-slug::github-actions␊
|
||||
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
|
||||
::save-state name=expiresAt::2016-07-11T22:14:10Z␊
|
||||
--- REQUESTS ---␊
|
||||
GET /app/installations␊
|
||||
POST /app/installations/123456/access_tokens␊
|
||||
{"permissions":{"enterprise_organizations":"read","enterprise_people":"write"}}`
|
||||
|
||||
## main-missing-owner.test.js
|
||||
|
||||
> stderr
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user