Skip to content

Commit 85eb8dd

Browse files
parkerbxyzCopilot
andauthored
feat: support full repository names in repositories input (#372)
The `repositories` input currently treats values like `${{ github.repository }}` as a repository name, which can produce a duplicated owner in the installation lookup. This changes repository target resolution so entries may be bare repository names or full `owner/repository` names, while preserving the existing resolved owner behavior. Full repository names are accepted only when their owner matches the `owner` input, or the current repository owner when `owner` is unset. The action still creates a single installation token for one owner, and generated `dist` artifacts are left unchanged for release. Fixes: #177 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c9aabb8 commit 85eb8dd

11 files changed

Lines changed: 232 additions & 8 deletions

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ jobs:
173173
body: "Hello, World!"
174174
```
175175

176+
You can include the current repository in the list with `${{ github.repository }}`:
177+
178+
```yaml
179+
repositories: ${{ github.repository }},repo2
180+
```
181+
176182
### Create a token for all repositories in another owner's installation
177183

178184
```yaml
@@ -377,6 +383,8 @@ steps:
377383

378384
> [!NOTE]
379385
> 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.
386+
>
387+
> 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.
380388

381389
### `enterprise`
382390

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ inputs:
1919
description: "The owner of the GitHub App installation (defaults to current repository owner)"
2020
required: false
2121
repositories:
22-
description: "Comma or newline-separated list of repositories to install the GitHub App on (defaults to current repository if owner is unset)"
22+
description: "Comma or newline-separated list of repositories to grant the token access to (defaults to current repository if owner is unset)"
2323
required: false
2424
enterprise:
2525
description: "The slug of the enterprise account where the GitHub App is installed (cannot be used with 'owner' or 'repositories')"

lib/main.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,29 +86,67 @@ function resolveInstallationTarget(enterprise, owner, repositories, core) {
8686
return { type: "owner", owner };
8787
}
8888

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

9191
if (!owner) {
9292
core.info(
93-
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories
94-
.map((repo) => `\n- ${parsedOwner}/${repo}`)
93+
`No 'owner' input provided. Using default owner '${target.owner}' to create token for the following repositories:${target.repositories
94+
.map((repo) => `\n- ${target.owner}/${repo}`)
9595
.join("")}`
9696
);
9797
} else {
9898
core.info(
99-
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:${repositories
100-
.map((repo) => `\n- ${parsedOwner}/${repo}`)
99+
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:${target.repositories
100+
.map((repo) => `\n- ${target.owner}/${repo}`)
101101
.join("")}`
102102
);
103103
}
104104

105105
return {
106106
type: "repository",
107+
owner: target.owner,
108+
repositories: target.repositories,
109+
};
110+
}
111+
112+
function normalizeRepositoryTarget(owner, repositories) {
113+
const parsedOwner = owner || String(process.env.GITHUB_REPOSITORY_OWNER);
114+
const parsedRepositories = repositories.map(parseRepositoryInput);
115+
116+
const mismatchedRepository = parsedRepositories.find(
117+
(repository) =>
118+
repository.owner &&
119+
repository.owner.toLowerCase() !== parsedOwner.toLowerCase()
120+
);
121+
122+
if (mismatchedRepository) {
123+
throw new Error(
124+
`Repository '${mismatchedRepository.input}' includes owner '${mismatchedRepository.owner}', which does not match the resolved owner '${parsedOwner}'.`
125+
);
126+
}
127+
128+
return {
107129
owner: parsedOwner,
108-
repositories,
130+
repositories: parsedRepositories.map((repository) => repository.name),
109131
};
110132
}
111133

134+
function parseRepositoryInput(input) {
135+
const parts = input.split("/");
136+
137+
if (parts.length === 1 && parts[0]) {
138+
return { input, owner: "", name: parts[0] };
139+
}
140+
141+
if (parts.length === 2 && parts[0] && parts[1]) {
142+
return { input, owner: parts[0], name: parts[1] };
143+
}
144+
145+
throw new Error(
146+
`Invalid repository '${input}'. Expected 'repository' or 'owner/repository'.`
147+
);
148+
}
149+
112150
function getTokenRetryDescription(target) {
113151
switch (target.type) {
114152
case "enterprise":

tests/index.js.snapshot

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,42 @@ POST /app/installations/123456/access_tokens
296296
{"repositories":["failed-repo"]}
297297
`;
298298

299+
exports[`main-token-get-owner-set-repo-full-name.test.js > stdout 1`] = `
300+
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
301+
- actions/create-github-app-token
302+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
303+
304+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
305+
306+
::set-output name=installation-id::123456
307+
308+
::set-output name=app-slug::github-actions
309+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
310+
::save-state name=expiresAt::2016-07-11T22:14:10Z
311+
--- REQUESTS ---
312+
GET /repos/actions/create-github-app-token/installation
313+
POST /app/installations/123456/access_tokens
314+
{"repositories":["create-github-app-token"]}
315+
`;
316+
317+
exports[`main-token-get-owner-set-repo-invalid-format.test.js > stderr 1`] = `
318+
Error: Invalid repository 'octocat/hello-world/extra'. Expected 'repository' or 'owner/repository'.
319+
at parseRepositoryInput (file://<cwd>/lib/main.js:<line>:<column>)
320+
at Array.map (<anonymous>)
321+
at normalizeRepositoryTarget (file://<cwd>/lib/main.js:<line>:<column>)
322+
at resolveInstallationTarget (file://<cwd>/lib/main.js:<line>:<column>)
323+
at main (file://<cwd>/lib/main.js:<line>:<column>)
324+
at run (file://<cwd>/main.js:<line>:<column>)
325+
at file://<cwd>/main.js:<line>:<column>
326+
at ModuleJob.run (node:internal/modules/esm/module_job:<line>:<column>)
327+
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:<line>:<column>)
328+
at async file://<cwd>/tests/main-token-get-owner-set-repo-invalid-format.test.js:<line>:<column>
329+
`;
330+
331+
exports[`main-token-get-owner-set-repo-invalid-format.test.js > stdout 1`] = `
332+
::error::Invalid repository 'octocat/hello-world/extra'. Expected 'repository' or 'owner/repository'.
333+
`;
334+
299335
exports[`main-token-get-owner-set-repo-network-error.test.js > stdout 1`] = `
300336
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
301337
- actions/network-repo
@@ -316,6 +352,41 @@ POST /app/installations/123456/access_tokens
316352
{"repositories":["network-repo"]}
317353
`;
318354

355+
exports[`main-token-get-owner-set-repo-non-current-full-name.test.js > stdout 1`] = `
356+
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
357+
- actions/toolkit
358+
- actions/checkout
359+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
360+
361+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
362+
363+
::set-output name=installation-id::123456
364+
365+
::set-output name=app-slug::github-actions
366+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
367+
::save-state name=expiresAt::2016-07-11T22:14:10Z
368+
--- REQUESTS ---
369+
GET /repos/actions/toolkit/installation
370+
POST /app/installations/123456/access_tokens
371+
{"repositories":["toolkit","checkout"]}
372+
`;
373+
374+
exports[`main-token-get-owner-set-repo-owner-mismatch.test.js > stderr 1`] = `
375+
Error: Repository 'octocat/hello-world' includes owner 'octocat', which does not match the resolved owner 'actions'.
376+
at normalizeRepositoryTarget (file://<cwd>/lib/main.js:<line>:<column>)
377+
at resolveInstallationTarget (file://<cwd>/lib/main.js:<line>:<column>)
378+
at main (file://<cwd>/lib/main.js:<line>:<column>)
379+
at run (file://<cwd>/main.js:<line>:<column>)
380+
at file://<cwd>/main.js:<line>:<column>
381+
at ModuleJob.run (node:internal/modules/esm/module_job:<line>:<column>)
382+
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:<line>:<column>)
383+
at async file://<cwd>/tests/main-token-get-owner-set-repo-owner-mismatch.test.js:<line>:<column>
384+
`;
385+
386+
exports[`main-token-get-owner-set-repo-owner-mismatch.test.js > stdout 1`] = `
387+
::error::Repository 'octocat/hello-world' includes owner 'octocat', which does not match the resolved owner 'actions'.
388+
`;
389+
319390
exports[`main-token-get-owner-set-repo-set-to-many-newline.test.js > stdout 1`] = `
320391
Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
321392
- actions/create-github-app-token
@@ -391,6 +462,41 @@ POST /app/installations/123456/access_tokens
391462
null
392463
`;
393464

465+
exports[`main-token-get-owner-unset-repo-full-name-and-bare.test.js > stdout 1`] = `
466+
No 'owner' input provided. Using default owner 'actions' to create token for the following repositories:
467+
- actions/create-github-app-token
468+
- actions/toolkit
469+
::add-mask::ghs_16C7e42F292c6912E7710c838347Ae178B4a
470+
471+
::set-output name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
472+
473+
::set-output name=installation-id::123456
474+
475+
::set-output name=app-slug::github-actions
476+
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a
477+
::save-state name=expiresAt::2016-07-11T22:14:10Z
478+
--- REQUESTS ---
479+
GET /repos/actions/create-github-app-token/installation
480+
POST /app/installations/123456/access_tokens
481+
{"repositories":["create-github-app-token","toolkit"]}
482+
`;
483+
484+
exports[`main-token-get-owner-unset-repo-owner-mismatch.test.js > stderr 1`] = `
485+
Error: Repository 'octocat/hello-world' includes owner 'octocat', which does not match the resolved owner 'actions'.
486+
at normalizeRepositoryTarget (file://<cwd>/lib/main.js:<line>:<column>)
487+
at resolveInstallationTarget (file://<cwd>/lib/main.js:<line>:<column>)
488+
at main (file://<cwd>/lib/main.js:<line>:<column>)
489+
at run (file://<cwd>/main.js:<line>:<column>)
490+
at file://<cwd>/main.js:<line>:<column>
491+
at ModuleJob.run (node:internal/modules/esm/module_job:<line>:<column>)
492+
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:<line>:<column>)
493+
at async file://<cwd>/tests/main-token-get-owner-unset-repo-owner-mismatch.test.js:<line>:<column>
494+
`;
495+
496+
exports[`main-token-get-owner-unset-repo-owner-mismatch.test.js > stdout 1`] = `
497+
::error::Repository 'octocat/hello-world' includes owner 'octocat', which does not match the resolved owner 'actions'.
498+
`;
499+
394500
exports[`main-token-get-owner-unset-repo-set.test.js > stdout 1`] = `
395501
No 'owner' input provided. Using default owner 'actions' to create token for the following repositories:
396502
- actions/create-github-app-token
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { test } from "./main.js";
2+
3+
// Verify `main` successfully obtains a token when the `owner` and `repositories` inputs are set, and `repositories` contains a full repository name.
4+
await test(() => {
5+
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
6+
process.env.INPUT_REPOSITORIES = process.env.GITHUB_REPOSITORY;
7+
});
8+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { DEFAULT_ENV } from "./main.js";
2+
3+
// Verify `main` exits with an error when a repository entry is neither a repository name nor an owner/repository name.
4+
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
5+
process.env[key] = value;
6+
}
7+
8+
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
9+
process.env.INPUT_REPOSITORIES = "octocat/hello-world/extra";
10+
11+
const { default: promise } = await import("../main.js");
12+
await promise;
13+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { DEFAULT_ENV, test } from "./main.js";
2+
3+
// Verify `main` normalizes full repository names before installation lookup and token scoping.
4+
await test(
5+
() => {},
6+
{
7+
...DEFAULT_ENV,
8+
INPUT_OWNER: DEFAULT_ENV.GITHUB_REPOSITORY_OWNER,
9+
INPUT_REPOSITORIES: `${DEFAULT_ENV.GITHUB_REPOSITORY_OWNER}/toolkit,checkout`,
10+
},
11+
);
12+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { DEFAULT_ENV } from "./main.js";
2+
3+
// Verify `main` exits with an error when a full repository name does not match the `owner` input.
4+
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
5+
process.env[key] = value;
6+
}
7+
8+
process.env.INPUT_OWNER = process.env.GITHUB_REPOSITORY_OWNER;
9+
process.env.INPUT_REPOSITORIES = "octocat/hello-world";
10+
11+
const { default: promise } = await import("../main.js");
12+
await promise;
13+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { test } from "./main.js";
2+
3+
// Verify `main` successfully obtains a token when `owner` is omitted and `repositories` mixes a full repository name with bare repository names.
4+
await test(() => {
5+
delete process.env.INPUT_OWNER;
6+
process.env.INPUT_REPOSITORIES = `${process.env.GITHUB_REPOSITORY},toolkit`;
7+
});
8+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { DEFAULT_ENV } from "./main.js";
2+
3+
// Verify `main` exits with an error when a full repository name does not match the default owner.
4+
for (const [key, value] of Object.entries(DEFAULT_ENV)) {
5+
process.env[key] = value;
6+
}
7+
8+
delete process.env.INPUT_OWNER;
9+
process.env.INPUT_REPOSITORIES = "octocat/hello-world";
10+
11+
const { default: promise } = await import("../main.js");
12+
await promise;
13+

0 commit comments

Comments
 (0)