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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ jobs:
body: "Hello, World!"
```

You can include the current repository in the list with `${{ github.repository }}`:

```yaml
repositories: ${{ github.repository }},repo2
```

### Create a token for all repositories in another owner's installation

```yaml
Expand Down Expand Up @@ -377,6 +383,8 @@ 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.
>
> Repository entries may include an owner, for example `owner/repo1`. The owner portion must match the `owner` input, or the current repository owner if `owner` is unset.

### `enterprise`

Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ inputs:
description: "The owner of the GitHub App installation (defaults to current repository owner)"
required: false
repositories:
description: "Comma or newline-separated list of repositories to install the GitHub App on (defaults to current repository if owner is unset)"
description: "Comma or newline-separated list of repositories to grant the token access to (defaults to current repository if owner is unset)"
required: false
enterprise:
description: "The slug of the enterprise account where the GitHub App is installed (cannot be used with 'owner' or 'repositories')"
Expand Down
50 changes: 44 additions & 6 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,29 +86,67 @@ function resolveInstallationTarget(enterprise, owner, repositories, core) {
return { type: "owner", owner };
}

const parsedOwner = owner || String(process.env.GITHUB_REPOSITORY_OWNER);
const target = normalizeRepositoryTarget(owner, repositories);

if (!owner) {
core.info(
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories
.map((repo) => `\n- ${parsedOwner}/${repo}`)
`No 'owner' input provided. Using default owner '${target.owner}' to create token for the following repositories:${target.repositories
.map((repo) => `\n- ${target.owner}/${repo}`)
.join("")}`
);
} else {
core.info(
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:${repositories
.map((repo) => `\n- ${parsedOwner}/${repo}`)
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:${target.repositories
.map((repo) => `\n- ${target.owner}/${repo}`)
.join("")}`
);
}

return {
type: "repository",
owner: target.owner,
repositories: target.repositories,
};
}

function normalizeRepositoryTarget(owner, repositories) {
const parsedOwner = owner || String(process.env.GITHUB_REPOSITORY_OWNER);
const parsedRepositories = repositories.map(parseRepositoryInput);

const mismatchedRepository = parsedRepositories.find(
Comment thread
parkerbxyz marked this conversation as resolved.
(repository) =>
repository.owner &&
repository.owner.toLowerCase() !== parsedOwner.toLowerCase()
);

if (mismatchedRepository) {
throw new Error(
`Repository '${mismatchedRepository.input}' includes owner '${mismatchedRepository.owner}', which does not match the resolved owner '${parsedOwner}'.`
);
}

return {
owner: parsedOwner,
repositories,
repositories: parsedRepositories.map((repository) => repository.name),
};
}

function parseRepositoryInput(input) {
const parts = input.split("/");

if (parts.length === 1 && parts[0]) {
return { input, owner: "", name: parts[0] };
}

if (parts.length === 2 && parts[0] && parts[1]) {
return { input, owner: parts[0], name: parts[1] };
}

throw new Error(
`Invalid repository '${input}'. Expected 'repository' or 'owner/repository'.`
);
}

function getTokenRetryDescription(target) {
switch (target.type) {
case "enterprise":
Expand Down
106 changes: 106 additions & 0 deletions tests/index.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,42 @@ POST /app/installations/123456/access_tokens
{"repositories":["failed-repo"]}
`;

exports[`main-token-get-owner-set-repo-full-name.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token
::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 /repos/actions/create-github-app-token/installation
POST /app/installations/123456/access_tokens
{"repositories":["create-github-app-token"]}
`;

exports[`main-token-get-owner-set-repo-invalid-format.test.js > stderr 1`] = `
Error: Invalid repository 'octocat/hello-world/extra'. Expected 'repository' or 'owner/repository'.
at parseRepositoryInput (file://<cwd>/lib/main.js:<line>:<column>)
at Array.map (<anonymous>)
at normalizeRepositoryTarget (file://<cwd>/lib/main.js:<line>:<column>)
at resolveInstallationTarget (file://<cwd>/lib/main.js:<line>:<column>)
at main (file://<cwd>/lib/main.js:<line>:<column>)
at run (file://<cwd>/main.js:<line>:<column>)
at file://<cwd>/main.js:<line>:<column>
at ModuleJob.run (node:internal/modules/esm/module_job:<line>:<column>)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:<line>:<column>)
at async file://<cwd>/tests/main-token-get-owner-set-repo-invalid-format.test.js:<line>:<column>
`;

exports[`main-token-get-owner-set-repo-invalid-format.test.js > stdout 1`] = `
::error::Invalid repository 'octocat/hello-world/extra'. Expected 'repository' or 'owner/repository'.
`;

exports[`main-token-get-owner-set-repo-network-error.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/network-repo
Expand All @@ -316,6 +352,41 @@ POST /app/installations/123456/access_tokens
{"repositories":["network-repo"]}
`;

exports[`main-token-get-owner-set-repo-non-current-full-name.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/toolkit
- actions/checkout
::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 /repos/actions/toolkit/installation
POST /app/installations/123456/access_tokens
{"repositories":["toolkit","checkout"]}
`;

exports[`main-token-get-owner-set-repo-owner-mismatch.test.js > stderr 1`] = `
Error: Repository 'octocat/hello-world' includes owner 'octocat', which does not match the resolved owner 'actions'.
at normalizeRepositoryTarget (file://<cwd>/lib/main.js:<line>:<column>)
at resolveInstallationTarget (file://<cwd>/lib/main.js:<line>:<column>)
at main (file://<cwd>/lib/main.js:<line>:<column>)
at run (file://<cwd>/main.js:<line>:<column>)
at file://<cwd>/main.js:<line>:<column>
at ModuleJob.run (node:internal/modules/esm/module_job:<line>:<column>)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:<line>:<column>)
at async file://<cwd>/tests/main-token-get-owner-set-repo-owner-mismatch.test.js:<line>:<column>
`;

exports[`main-token-get-owner-set-repo-owner-mismatch.test.js > stdout 1`] = `
::error::Repository 'octocat/hello-world' includes owner 'octocat', which does not match the resolved owner 'actions'.
`;

exports[`main-token-get-owner-set-repo-set-to-many-newline.test.js > stdout 1`] = `
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
- actions/create-github-app-token
Expand Down Expand Up @@ -391,6 +462,41 @@ POST /app/installations/123456/access_tokens
null
`;

exports[`main-token-get-owner-unset-repo-full-name-and-bare.test.js > stdout 1`] = `
No 'owner' input provided. Using default owner 'actions' to create token for the following repositories:
- actions/create-github-app-token
- actions/toolkit
::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 /repos/actions/create-github-app-token/installation
POST /app/installations/123456/access_tokens
{"repositories":["create-github-app-token","toolkit"]}
`;

exports[`main-token-get-owner-unset-repo-owner-mismatch.test.js > stderr 1`] = `
Error: Repository 'octocat/hello-world' includes owner 'octocat', which does not match the resolved owner 'actions'.
at normalizeRepositoryTarget (file://<cwd>/lib/main.js:<line>:<column>)
at resolveInstallationTarget (file://<cwd>/lib/main.js:<line>:<column>)
at main (file://<cwd>/lib/main.js:<line>:<column>)
at run (file://<cwd>/main.js:<line>:<column>)
at file://<cwd>/main.js:<line>:<column>
at ModuleJob.run (node:internal/modules/esm/module_job:<line>:<column>)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:<line>:<column>)
at async file://<cwd>/tests/main-token-get-owner-unset-repo-owner-mismatch.test.js:<line>:<column>
`;

exports[`main-token-get-owner-unset-repo-owner-mismatch.test.js > stdout 1`] = `
::error::Repository 'octocat/hello-world' includes owner 'octocat', which does not match the resolved owner 'actions'.
`;

exports[`main-token-get-owner-unset-repo-set.test.js > stdout 1`] = `
No 'owner' input provided. Using default owner 'actions' to create token for the following repositories:
- actions/create-github-app-token
Expand Down
8 changes: 8 additions & 0 deletions tests/main-token-get-owner-set-repo-full-name.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { test } from "./main.js";

// Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set, and `repositories` contains a full repository name.
await test(() => {
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
process.env.INPUT_REPOSITORIES = process.env.GITHUB_REPOSITORY;
});

13 changes: 13 additions & 0 deletions tests/main-token-get-owner-set-repo-invalid-format.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DEFAULT_ENV } from "./main.js";

// Verify `main` exits with an error when a repository entry is neither a repository name nor an owner/repository name.
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
process.env[key] = value;
}

process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
process.env.INPUT_REPOSITORIES = "octocat/hello-world/extra";

const { default: promise } = await import("../main.js");
await promise;

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { DEFAULT_ENV, test } from "./main.js";

// Verify `main` normalizes full repository names before installation lookup and token scoping.
await test(
() => {},
{
...DEFAULT_ENV,
INPUT_OWNER: DEFAULT_ENV.GITHUB_REPOSITORY_OWNER,
INPUT_REPOSITORIES: `${DEFAULT_ENV.GITHUB_REPOSITORY_OWNER}/toolkit,checkout`,
},
);

13 changes: 13 additions & 0 deletions tests/main-token-get-owner-set-repo-owner-mismatch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DEFAULT_ENV } from "./main.js";

// Verify `main` exits with an error when a full repository name does not match the `owner` input.
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
process.env[key] = value;
}

process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
process.env.INPUT_REPOSITORIES = "octocat/hello-world";

const { default: promise } = await import("../main.js");
await promise;

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { test } from "./main.js";

// Verify `main` successfully obtains a token when `owner` is omitted and `repositories` mixes a full repository name with bare repository names.
await test(() => {
delete process.env.INPUT_OWNER;
process.env.INPUT_REPOSITORIES = `${process.env.GITHUB_REPOSITORY},toolkit`;
});

13 changes: 13 additions & 0 deletions tests/main-token-get-owner-unset-repo-owner-mismatch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DEFAULT_ENV } from "./main.js";

// Verify `main` exits with an error when a full repository name does not match the default owner.
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
process.env[key] = value;
}

delete process.env.INPUT_OWNER;
process.env.INPUT_REPOSITORIES = "octocat/hello-world";

const { default: promise } = await import("../main.js");
await promise;

7 changes: 6 additions & 1 deletion tests/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@ export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) {
const mockAppSlug = "github-actions";
const owner = env.INPUT_OWNER ?? env.GITHUB_REPOSITORY_OWNER;
const currentRepoName = env.GITHUB_REPOSITORY.split("/")[1];
const firstRepositoryInput =
(env.INPUT_REPOSITORIES ?? currentRepoName)
.split(/[\n,]+/)
.map((repository) => repository.trim())
.find(Boolean) ?? currentRepoName;
const repo = encodeURIComponent(
(env.INPUT_REPOSITORIES ?? currentRepoName).split(",")[0]
firstRepositoryInput.split("/").pop() || firstRepositoryInput
);

mockPool
Expand Down
Loading