feat: allow repositories input to be comma or newline-separated (#169)

Resolves https://github.com/actions/create-github-app-token/issues/106

- Fixes the parsing to cope with whitespace in the input string.
- Allows the input to be comma or newline-separated. (I've done this for
all array-type inputs in my own actions, but I'm happy to remove this if
you only want to support comma-separated.)
- Added tests for parsing comma and newline-separated inputs.
This commit is contained in:
Peter Evans
2024-09-11 21:54:50 +01:00
committed by GitHub
parent 3378cda945
commit 796b88dc58
10 changed files with 75 additions and 37 deletions
+4 -2
View File
@@ -163,7 +163,9 @@ jobs:
app-id: ${{ vars.APP_ID }} app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }} private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }} owner: ${{ github.repository_owner }}
repositories: "repo1,repo2" repositories: |
repo1
repo2
- uses: peter-evans/create-or-update-comment@v3 - uses: peter-evans/create-or-update-comment@v3
with: with:
token: ${{ steps.app-token.outputs.token }} token: ${{ steps.app-token.outputs.token }}
@@ -302,7 +304,7 @@ steps:
### `repositories` ### `repositories`
**Optional:** Comma-separated list of repositories to grant access to. **Optional:** Comma or newline-separated list of repositories to grant access to.
> [!NOTE] > [!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. > 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.
+1 -1
View File
@@ -23,7 +23,7 @@ inputs:
description: "The owner of the GitHub App installation (defaults to current repository owner)" description: "The owner of the GitHub App installation (defaults to current repository owner)"
required: false required: false
repositories: repositories:
description: "Repositories to install the GitHub App on (defaults to current repository if owner is unset)" description: "Comma or newline-separated list of repositories to install the GitHub App on (defaults to current repository if owner is unset)"
required: false required: false
skip-token-revoke: skip-token-revoke:
description: "If truthy, the token will not be revoked when the current job is complete" description: "If truthy, the token will not be revoked when the current job is complete"
+16 -14
View File
@@ -39700,33 +39700,35 @@ async function pRetry(input, options) {
// lib/main.js // lib/main.js
async function main(appId2, privateKey2, owner2, repositories2, core3, createAppAuth2, request2, skipTokenRevoke2) { async function main(appId2, privateKey2, owner2, repositories2, core3, createAppAuth2, request2, skipTokenRevoke2) {
let parsedOwner = ""; let parsedOwner = "";
let parsedRepositoryNames = ""; let parsedRepositoryNames = [];
if (!owner2 && !repositories2) { if (!owner2 && repositories2.length === 0) {
[parsedOwner, parsedRepositoryNames] = String( const [owner3, repo] = String(
process.env.GITHUB_REPOSITORY process.env.GITHUB_REPOSITORY
).split("/"); ).split("/");
parsedOwner = owner3;
parsedRepositoryNames = [repo];
core3.info( core3.info(
`owner and repositories not set, creating token for the current repository ("${parsedRepositoryNames}")` `owner and repositories not set, creating token for the current repository ("${repo}")`
); );
} }
if (owner2 && !repositories2) { if (owner2 && repositories2.length === 0) {
parsedOwner = owner2; parsedOwner = owner2;
core3.info( core3.info(
`repositories not set, creating token for all repositories for given owner "${owner2}"` `repositories not set, creating token for all repositories for given owner "${owner2}"`
); );
} }
if (!owner2 && repositories2) { if (!owner2 && repositories2.length > 0) {
parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER); parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER);
parsedRepositoryNames = repositories2; parsedRepositoryNames = repositories2;
core3.info( core3.info(
`owner not set, creating owner for given repositories "${repositories2}" in current owner ("${parsedOwner}")` `owner not set, creating owner for given repositories "${repositories2.join(",")}" in current owner ("${parsedOwner}")`
); );
} }
if (owner2 && repositories2) { if (owner2 && repositories2.length > 0) {
parsedOwner = owner2; parsedOwner = owner2;
parsedRepositoryNames = repositories2; parsedRepositoryNames = repositories2;
core3.info( core3.info(
`owner and repositories set, creating token for repositories "${repositories2}" owned by "${owner2}"` `owner and repositories set, creating token for repositories "${repositories2.join(",")}" owned by "${owner2}"`
); );
} }
const auth5 = createAppAuth2({ const auth5 = createAppAuth2({
@@ -39735,11 +39737,11 @@ async function main(appId2, privateKey2, owner2, repositories2, core3, createApp
request: request2 request: request2
}); });
let authentication, installationId, appSlug; let authentication, installationId, appSlug;
if (parsedRepositoryNames) { if (parsedRepositoryNames.length > 0) {
({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromRepository(request2, auth5, parsedOwner, parsedRepositoryNames), { ({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromRepository(request2, auth5, parsedOwner, parsedRepositoryNames), {
onFailedAttempt: (error) => { onFailedAttempt: (error) => {
core3.info( core3.info(
`Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` `Failed to create token for "${parsedRepositoryNames.join(",")}" (attempt ${error.attemptNumber}): ${error.message}`
); );
}, },
retries: 3 retries: 3
@@ -39789,7 +39791,7 @@ async function getTokenFromOwner(request2, auth5, parsedOwner) {
async function getTokenFromRepository(request2, auth5, parsedOwner, parsedRepositoryNames) { async function getTokenFromRepository(request2, auth5, parsedOwner, parsedRepositoryNames) {
const response = await request2("GET /repos/{owner}/{repo}/installation", { const response = await request2("GET /repos/{owner}/{repo}/installation", {
owner: parsedOwner, owner: parsedOwner,
repo: parsedRepositoryNames.split(",")[0], repo: parsedRepositoryNames[0],
request: { request: {
hook: auth5.hook hook: auth5.hook
} }
@@ -39797,7 +39799,7 @@ async function getTokenFromRepository(request2, auth5, parsedOwner, parsedReposi
const authentication = await auth5({ const authentication = await auth5({
type: "installation", type: "installation",
installationId: response.data.id, installationId: response.data.id,
repositoryNames: parsedRepositoryNames.split(",") repositoryNames: parsedRepositoryNames
}); });
const installationId = response.data.id; const installationId = response.data.id;
const appSlug = response.data["app_slug"]; const appSlug = response.data["app_slug"];
@@ -39847,7 +39849,7 @@ if (!privateKey) {
throw new Error("Input required and not supplied: private-key"); throw new Error("Input required and not supplied: private-key");
} }
var owner = import_core2.default.getInput("owner"); var owner = import_core2.default.getInput("owner");
var repositories = import_core2.default.getInput("repositories"); var repositories = import_core2.default.getInput("repositories").split(/[\n,]+/).map((s) => s.trim()).filter((x) => x !== "");
var skipTokenRevoke = Boolean( var skipTokenRevoke = Boolean(
import_core2.default.getInput("skip-token-revoke") || import_core2.default.getInput("skip_token_revoke") import_core2.default.getInput("skip-token-revoke") || import_core2.default.getInput("skip_token_revoke")
); );
+17 -15
View File
@@ -5,7 +5,7 @@ import pRetry from "p-retry";
* @param {string} appId * @param {string} appId
* @param {string} privateKey * @param {string} privateKey
* @param {string} owner * @param {string} owner
* @param {string} repositories * @param {string[]} repositories
* @param {import("@actions/core")} core * @param {import("@actions/core")} core
* @param {import("@octokit/auth-app").createAppAuth} createAppAuth * @param {import("@octokit/auth-app").createAppAuth} createAppAuth
* @param {import("@octokit/request").request} request * @param {import("@octokit/request").request} request
@@ -22,21 +22,23 @@ export async function main(
skipTokenRevoke skipTokenRevoke
) { ) {
let parsedOwner = ""; let parsedOwner = "";
let parsedRepositoryNames = ""; let parsedRepositoryNames = [];
// If neither owner nor repositories are set, default to current repository // If neither owner nor repositories are set, default to current repository
if (!owner && !repositories) { if (!owner && repositories.length === 0) {
[parsedOwner, parsedRepositoryNames] = String( const [owner, repo] = String(
process.env.GITHUB_REPOSITORY process.env.GITHUB_REPOSITORY
).split("/"); ).split("/");
parsedOwner = owner;
parsedRepositoryNames = [repo];
core.info( core.info(
`owner and repositories not set, creating token for the current repository ("${parsedRepositoryNames}")` `owner and repositories not set, creating token for the current repository ("${repo}")`
); );
} }
// If only an owner is set, default to all repositories from that owner // If only an owner is set, default to all repositories from that owner
if (owner && !repositories) { if (owner && repositories.length === 0) {
parsedOwner = owner; parsedOwner = owner;
core.info( core.info(
@@ -45,22 +47,22 @@ export async function main(
} }
// If repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER` // If repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER`
if (!owner && repositories) { if (!owner && repositories.length > 0) {
parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER); parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER);
parsedRepositoryNames = repositories; parsedRepositoryNames = repositories;
core.info( core.info(
`owner not set, creating owner for given repositories "${repositories}" in current owner ("${parsedOwner}")` `owner not set, creating owner for given repositories "${repositories.join(',')}" in current owner ("${parsedOwner}")`
); );
} }
// If both owner and repositories are set, use those values // If both owner and repositories are set, use those values
if (owner && repositories) { if (owner && repositories.length > 0) {
parsedOwner = owner; parsedOwner = owner;
parsedRepositoryNames = repositories; parsedRepositoryNames = repositories;
core.info( core.info(
`owner and repositories set, creating token for repositories "${repositories}" owned by "${owner}"` `owner and repositories set, creating token for repositories "${repositories.join(',')}" owned by "${owner}"`
); );
} }
@@ -73,11 +75,11 @@ export async function main(
let authentication, installationId, appSlug; let authentication, installationId, appSlug;
// If at least one repository is set, get installation ID from that repository // If at least one repository is set, get installation ID from that repository
if (parsedRepositoryNames) { if (parsedRepositoryNames.length > 0) {
({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames), { ({ authentication, installationId, appSlug } = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames), {
onFailedAttempt: (error) => { onFailedAttempt: (error) => {
core.info( core.info(
`Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}` `Failed to create token for "${parsedRepositoryNames.join(',')}" (attempt ${error.attemptNumber}): ${error.message}`
); );
}, },
retries: 3, retries: 3,
@@ -144,7 +146,7 @@ async function getTokenFromRepository(request, auth, parsedOwner, parsedReposito
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app // https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
const response = await request("GET /repos/{owner}/{repo}/installation", { const response = await request("GET /repos/{owner}/{repo}/installation", {
owner: parsedOwner, owner: parsedOwner,
repo: parsedRepositoryNames.split(",")[0], repo: parsedRepositoryNames[0],
request: { request: {
hook: auth.hook, hook: auth.hook,
}, },
@@ -154,11 +156,11 @@ async function getTokenFromRepository(request, auth, parsedOwner, parsedReposito
const authentication = await auth({ const authentication = await auth({
type: "installation", type: "installation",
installationId: response.data.id, installationId: response.data.id,
repositoryNames: parsedRepositoryNames.split(","), repositoryNames: parsedRepositoryNames,
}); });
const installationId = response.data.id; const installationId = response.data.id;
const appSlug = response.data['app_slug']; const appSlug = response.data['app_slug'];
return { authentication, installationId, appSlug }; return { authentication, installationId, appSlug };
} }
+4 -1
View File
@@ -25,7 +25,10 @@ if (!privateKey) {
throw new Error("Input required and not supplied: private-key"); throw new Error("Input required and not supplied: private-key");
} }
const owner = core.getInput("owner"); const owner = core.getInput("owner");
const repositories = core.getInput("repositories"); const repositories = core.getInput("repositories")
.split(/[\n,]+/)
.map(s => s.trim())
.filter(x => x !== '');
const skipTokenRevoke = Boolean( const skipTokenRevoke = Boolean(
core.getInput("skip-token-revoke") || core.getInput("skip_token_revoke") core.getInput("skip-token-revoke") || core.getInput("skip_token_revoke")
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "create-github-app-token", "name": "create-github-app-token",
"version": "1.10.3", "version": "1.10.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "create-github-app-token", "name": "create-github-app-token",
"version": "1.10.3", "version": "1.10.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
@@ -0,0 +1,9 @@
import { test } from "./main.js";
// Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set (and the latter is a list of repos).
await test(() => {
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
const currentRepoName = process.env.GITHUB_REPOSITORY.split("/")[1];
// Intentional unnecessary whitespace to test parsing to array
process.env.INPUT_REPOSITORIES = `\n ${currentRepoName}\ntoolkit \n\n checkout \n`;
});
@@ -4,5 +4,6 @@ import { test } from "./main.js";
await test(() => { await test(() => {
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER; process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
const currentRepoName = process.env.GITHUB_REPOSITORY.split("/")[1]; const currentRepoName = process.env.GITHUB_REPOSITORY.split("/")[1];
process.env.INPUT_REPOSITORIES = `${currentRepoName},toolkit`; // Intentional unnecessary whitespace to test parsing to array
process.env.INPUT_REPOSITORIES = ` ${currentRepoName}, toolkit ,checkout`;
}); });
+20 -1
View File
@@ -134,6 +134,25 @@ Generated by [AVA](https://avajs.dev).
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ ::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z` ::save-state name=expiresAt::2016-07-11T22:14:10Z`
## main-token-get-owner-set-repo-set-to-many-newline.test.js
> stderr
''
> stdout
`owner and repositories set, creating token for repositories "create-github-app-token,toolkit,checkout" owned by "actions"␊
::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`
## main-token-get-owner-set-repo-set-to-many.test.js ## main-token-get-owner-set-repo-set-to-many.test.js
> stderr > stderr
@@ -142,7 +161,7 @@ Generated by [AVA](https://avajs.dev).
> stdout > stdout
`owner and repositories set, creating token for repositories "create-github-app-token,toolkit" owned by "actions"␊ `owner and repositories set, creating token for repositories "create-github-app-token,toolkit,checkout" owned by "actions"␊
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ ::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊ ::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
Binary file not shown.