Compare commits

...

2 Commits

Author SHA1 Message Date
Parker Brown a13b8e8f0b Merge branch 'main' into parkerbxyz/fix-repositories-github-repository 2026-05-08 17:19:14 -07:00
Parker Brown 7890ff190e Support full repository names
Allow repositories entries to include an owner when it matches the resolved owner. This supports values like github.repository while preserving the existing owner/default-owner behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-08 17:10:38 -07:00
9 changed files with 197 additions and 9 deletions
+9 -1
View File
@@ -173,6 +173,12 @@ jobs:
body: "Hello, World!"
```
The `repositories` input accepts comma or newline-separated repository names. It also accepts full repository names, 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
@@ -373,10 +379,12 @@ steps:
### `repositories`
**Optional:** Comma or newline-separated list of repositories to grant access to.
**Optional:** Comma or newline-separated list of repositories to grant access to. Entries can be repository names, such as `repo1`, or full repository names, such as `owner/repo1`.
> [!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.
>
> The owner portion of any full repository name in `repositories` must match the `owner` input, or the current repository owner if `owner` is unset.
### `enterprise`
+1 -1
View File
@@ -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 repository names or owner/repository names to install the GitHub App on (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')"
+45 -7
View File
@@ -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: parsedOwner,
repositories,
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(
(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: 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":
+87
View File
@@ -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
@@ -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
@@ -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
@@ -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;
});
@@ -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;
@@ -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;
@@ -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`;
});
@@ -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;