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
13 changed files with 435 additions and 9 deletions
+81
View File
@@ -0,0 +1,81 @@
name: release
on:
workflow_dispatch:
push:
branches:
- "*.x"
- main
- beta
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
name: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ./
id: app-token
with:
app-id: ${{ vars.RELEASER_APP_ID }}
private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }}
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
id: release-please
with:
token: ${{ steps.app-token.outputs.token }}
config-file: ${{ github.ref_name == 'beta' && 'release-please-config.beta.json' || 'release-please-config.json' }}
manifest-file: .release-please-manifest.json
target-branch: ${{ github.ref_name }}
- uses: actions/checkout@v6
if: steps.release-please.outputs.prs_created == 'true'
with:
ref: ${{ fromJSON(steps.release-please.outputs.pr).headBranchName }}
token: ${{ steps.app-token.outputs.token }}
- uses: actions/setup-node@v6
if: steps.release-please.outputs.prs_created == 'true'
with:
node-version-file: package.json
- run: npm ci
if: steps.release-please.outputs.prs_created == 'true'
- run: npm run build
if: steps.release-please.outputs.prs_created == 'true'
- uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
if: steps.release-please.outputs.prs_created == 'true'
with:
commit_author: "${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>"
commit_message: "chore: update dist files"
file_pattern: dist/**
- name: Update major version tag
id: update-major-tag
if: steps.release-please.outputs.release_created == 'true' && github.ref_name != 'beta'
uses: octokit/request-action@b91aabaa861c777dcdb14e2387e30eddf04619ae # v3.0.0
continue-on-error: true
with:
route: PATCH /repos/${{ github.repository }}/git/refs/tags/v${{ steps.release-please.outputs.major }}
sha: ${{ steps.release-please.outputs.sha }}
force: true
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Create major version tag
if: steps.release-please.outputs.release_created == 'true' && github.ref_name != 'beta' && steps.update-major-tag.outcome == 'failure'
uses: octokit/request-action@b91aabaa861c777dcdb14e2387e30eddf04619ae # v3.0.0
with:
route: POST /repos/${{ github.repository }}/git/refs
ref: refs/tags/v${{ steps.release-please.outputs.major }}
sha: ${{ steps.release-please.outputs.sha }}
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
+34
View File
@@ -0,0 +1,34 @@
# This workflow warns and then closes issues that have had no activity for a specified amount of time.
# https://github.com/actions/stale
name: Stale
on:
workflow_dispatch:
schedule:
# 00:00 UTC on Mondays
- cron: '0 0 * * 1'
permissions:
issues: write
pull-requests: write
env:
DAYS_BEFORE_STALE: 180
DAYS_BEFORE_CLOSE: 60
STALE_LABEL: 'stale'
STALE_LABEL_URL: ${{github.server_url}}/${{github.repository}}/labels/stale
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
operations-per-run: 100
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
stale-issue-label: ${{ env.STALE_LABEL }}
stale-pr-label: ${{ env.STALE_LABEL }}
stale-issue-message: 'This issue has been marked ${{ env.STALE_LABEL_URL }} because it has been open for ${{ env.DAYS_BEFORE_STALE }} days with no activity. Please close this issue if it is no longer needed. If this issue is still relevant and you would like it to remain open, simply update it within the next ${{ env.DAYS_BEFORE_CLOSE }} days.'
stale-pr-message: 'This pull request has been marked ${{ env.STALE_LABEL_URL }} because it has been open for ${{ env.DAYS_BEFORE_STALE }} days with no activity. Please close this pull request if it is no longer needed. If this pull request is still relevant and you would like it to remain open, simply update it within the next ${{ env.DAYS_BEFORE_CLOSE }} days.'
+81
View File
@@ -0,0 +1,81 @@
name: test
on:
push:
branches:
- main
- beta
pull_request:
merge_group:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
integration:
name: integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- run: npm ci
- run: npm test
end-to-end:
name: end-to-end
runs-on: ubuntu-latest
# do not run from forks, as forks dont have access to repository secrets
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- run: npm ci
- run: npm run build
- uses: ./ # Uses the action in the root directory
id: test
with:
app-id: ${{ vars.TEST_APP_ID }}
private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }}
- uses: octokit/request-action@v2.x
id: get-repository
env:
GITHUB_TOKEN: ${{ steps.test.outputs.token }}
with:
route: GET /installation/repositories
- run: echo '${{ steps.get-repository.outputs.data }}'
end-to-end-proxy:
name: end-to-end with unreachable proxy
runs-on: ubuntu-latest
# do not run from forks, as forks dont have access to repository secrets
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: package.json
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: ./ # Uses the action in the root directory
continue-on-error: true
id: test
env:
NODE_USE_ENV_PROXY: "1"
https_proxy: http://127.0.0.1:9
with:
app-id: ${{ vars.TEST_APP_ID }}
private-key: ${{ secrets.TEST_APP_PRIVATE_KEY }}
- name: Assert action failed through unreachable proxy
run: test "${{ steps.test.outcome }}" = "failure"
@@ -0,0 +1,42 @@
name: Update Permission Inputs
on:
pull_request:
paths:
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
jobs:
update-permission-inputs:
runs-on: ubuntu-latest
env:
COMMIT_MESSAGE: 'feat: update permission inputs'
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: package.json
- name: Install dependencies
run: npm ci
- name: Run permission inputs update script
run: node scripts/update-permission-inputs.js
- name: Commit changes
id: auto-commit
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: ${{ env.COMMIT_MESSAGE }}
- name: Update PR title
if: github.event_name == 'pull_request' && steps.auto-commit.outputs.changes_detected == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr edit ${{ github.event.pull_request.number }} --title "${{ env.COMMIT_MESSAGE }}"
+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;