Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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!"
```

The `repositories` input also accepts entries with an owner, which is useful when passing `${{ github.repository }}`:

```yaml
repositories: ${{ github.repository }},generic-submodule
```

### 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)"
Comment thread
parkerbxyz marked this conversation as resolved.
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
87 changes: 87 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,22 @@ POST /app/installations/123456/access_tokens
{"repositories":["network-repo"]}
`;

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 +443,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;

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;

Loading