Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support tokens scoped to multiple repositories within organization #46

Merged
merged 29 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
12bf248
support token scope to multiple repos
timreimherr Sep 20, 2023
9c2fe6b
update documentation
timreimherr Sep 20, 2023
4e0d015
if owner not set default to current repo owner
timreimherr Sep 21, 2023
151c72e
default to all repos if none are supplied
timreimherr Sep 21, 2023
84c746a
tests for lib/main.js
timreimherr Sep 21, 2023
98d3657
remove jest test
timreimherr Sep 21, 2023
63a98a7
Update documentation
timreimherr Sep 21, 2023
8f5a382
Merge branch 'main' into main
gr2m Sep 21, 2023
a12bbe4
Merge branch 'main' into main
gr2m Sep 21, 2023
fb1cbf7
allow for 'owner' to be empty
timreimherr Sep 22, 2023
c21d2ca
scope according to input
timreimherr Sep 22, 2023
6d39deb
Update documentation
timreimherr Sep 22, 2023
90239ca
clarify documentation
timreimherr Sep 22, 2023
3cfbd0e
change action name for publishing
timreimherr Sep 22, 2023
e8a138f
fix action name
timreimherr Sep 25, 2023
7c7676d
build: dist/main.cjs
gr2m Sep 29, 2023
4b133dc
Merge branch 'main' into main
parkerbxyz Oct 3, 2023
68894b6
update main
gr2m Sep 29, 2023
02c936f
build update for testing
gr2m Oct 3, 2023
73f98bd
Update README.md
gr2m Oct 3, 2023
91b880c
Update action.yml
gr2m Oct 3, 2023
80484a9
build(package): lock file
gr2m Oct 3, 2023
aa7595e
build files for testing (after updating dependencies)
gr2m Oct 3, 2023
2df34b8
Use sentence case in comments for consistency
parkerbxyz Oct 3, 2023
13b24f0
Remove language codes from GitHub Docs URLs
parkerbxyz Oct 3, 2023
9dcf16e
Move note to a dedicated section
parkerbxyz Oct 3, 2023
dad2c36
Reword step 1
parkerbxyz Oct 3, 2023
0a057cb
Update example usage headers
parkerbxyz Oct 3, 2023
7c0311c
Update lib/main.js
gr2m Oct 4, 2023
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
79 changes: 77 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ In order to use this action, you need to:
2. [Store the App's ID in your repository environment variables](https://docs.github.com/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows) (example: `APP_ID`)
3. [Store the App's private key in your repository secrets](https://docs.github.com/actions/security-guides/encrypted-secrets?tool=webui#creating-encrypted-secrets-for-a-repository) (example: `PRIVATE_KEY`)

### Minimal usage
### Create token for current repository

```yaml
on: [issues]
Expand Down Expand Up @@ -57,6 +57,73 @@ jobs:
github_token: ${{ steps.app-token.outputs.token }}
```

### Create token for all repositories in current owner's installation

```yaml
on: [workflow_dispatch]

jobs:
hello-world:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app_id: ${{ vars.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- uses: peter-evans/create-or-update-comment@v3
with:
token: ${{ steps.app-token.outputs.token }}
issue-number: ${{ github.event.issue.number }}
body: "Hello, World!"
```

### Create a token for selected repositories in current owner's installation

```yaml
on: [issues]

jobs:
hello-world:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app_id: ${{ vars.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: "repo1,repo2"
- uses: peter-evans/create-or-update-comment@v3
with:
token: ${{ steps.app-token.outputs.token }}
issue-number: ${{ github.event.issue.number }}
body: "Hello, World!"
```

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

```yaml
on: [issues]

jobs:
hello-world:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app_id: ${{ vars.APP_ID }}
private_key: ${{ secrets.PRIVATE_KEY }}
owner: another-owner
- uses: peter-evans/create-or-update-comment@v3
with:
token: ${{ steps.app-token.outputs.token }}
issue-number: ${{ github.event.issue.number }}
body: "Hello, World!"
```

## Inputs

### `app_id`
Expand All @@ -67,6 +134,14 @@ jobs:

**Required:** GitHub App private key.

### `owner`

**Optional:** GitHub App installation owner. If empty, defaults to the current repository owner.

### `repositories`

**Optional:** Comma-separated list of repositories to grant access to. If 'owner' is set and 'repositories' is empty then the access will be scoped to all repositories in the current repository owner's installation. If 'owner' and 'repositories' are empty then the access will be scoped to the current repository.
gr2m marked this conversation as resolved.
Show resolved Hide resolved

## Outputs

### `token`
Expand All @@ -77,7 +152,7 @@ GitHub App installation access token.

The action creates an installation access token using [the `POST /app/installations/{installation_id}/access_tokens` endpoint](https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app). By default,

1. The token is scoped to the current repository.
1. The token is scoped to the current repository or the repositories given.
2. The token inherits all the installation's permissions.
3. The token is set as output `token` which can be used in subsequent steps.
4. The token is revoked in the `post` step of the action, which means it cannot be passed to another job.
Expand Down
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ inputs:
private_key:
description: "GitHub App private key"
required: true
owner:
description: "GitHub App owner (defaults to current repository owner)"
required: false
repositories:
description: "Repositories to install the GitHub App on (defaults to current repository)"
gr2m marked this conversation as resolved.
Show resolved Hide resolved
required: false
outputs:
token:
description: "GitHub installation access token"
Expand Down
52 changes: 37 additions & 15 deletions dist/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14795,7 +14795,7 @@ var require_dist_node12 = __commonJS({
data: {
token,
expires_at: expiresAt,
repositories,
repositories: repositories2,
permissions: permissionsOptional,
repository_selection: repositorySelectionOptional,
single_file: singleFileName
Expand All @@ -14814,8 +14814,8 @@ var require_dist_node12 = __commonJS({
});
const permissions = permissionsOptional || {};
const repositorySelection = repositorySelectionOptional || "all";
const repositoryIds = repositories ? repositories.map((r) => r.id) : void 0;
const repositoryNames = repositories ? repositories.map((repo) => repo.name) : void 0;
const repositoryIds = repositories2 ? repositories2.map((r) => r.id) : void 0;
const repositoryNames = repositories2 ? repositories2.map((repo) => repo.name) : void 0;
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
await set(state.cache, optionsWithInstallationTokenFromState, {
token,
Expand Down Expand Up @@ -15043,8 +15043,18 @@ var import_core = __toESM(require_core(), 1);
var import_auth_app = __toESM(require_dist_node12(), 1);

// lib/main.js
async function main(appId2, privateKey2, repository2, core2, createAppAuth2, request2) {
const [owner, repo] = repository2.split("/");
async function main(appId2, privateKey2, owner2, repositories2, core2, createAppAuth2, request2) {
let org = "";
if (owner2.length == 0) {
org = process.env.GITHUB_REPOSITORY_OWNER || "";
}
if (owner2.length == 0 && repositories2.length == 0) {
repositories2 = process.env.GITHUB_REPOSITORY?.split("/")[1] || "";
}
let repos = [];
if (repositories2.trim() != "") {
repos = repositories2.split(",").map((repo) => repo.trim());
}
const auth = createAppAuth2({
appId: appId2,
privateKey: privateKey2,
Expand All @@ -15054,20 +15064,27 @@ async function main(appId2, privateKey2, repository2, core2, createAppAuth2, req
type: "app"
});
const { data: installation } = await request2(
"GET /repos/{owner}/{repo}/installation",
"GET /orgs/{org}/installation",
{
owner,
repo,
org,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

org is always empty here if owner2 is passed to this function

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dist/* files are compiled build files. Better comment on the main.js and lib/main.js

I think that my PR here addresses your concern timreimherr#1

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an issue I ran into when trying to use your fork as an action. It might be that the dist file wasn't up to date at the time I made the comment, I just assumed it would be.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah makes sense, thank you for clarifying

headers: {
authorization: `bearer ${appAuthentication.token}`
}
}
);
const authentication = await auth({
type: "installation",
installationId: installation.id,
repositoryNames: [repo]
});
let authentication;
if (repositories2.length == 0) {
authentication = await auth({
type: "installation",
installationId: installation.id
});
} else {
authentication = await auth({
type: "installation",
installationId: installation.id,
repositoryNames: repos
});
}
core2.setSecret(authentication.token);
core2.setOutput("token", authentication.token);
core2.saveState("token", authentication.token);
Expand All @@ -15086,13 +15103,18 @@ var request_default = import_request.request.defaults({
if (!process.env.GITHUB_REPOSITORY) {
throw new Error("GITHUB_REPOSITORY missing, must be set to '<owner>/<repo>'");
}
if (!process.env.GITHUB_REPOSITORY_OWNER) {
throw new Error("GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'");
}
var appId = import_core.default.getInput("app_id");
var privateKey = import_core.default.getInput("private_key");
var repository = process.env.GITHUB_REPOSITORY;
var owner = import_core.default.getInput("owner");
var repositories = import_core.default.getInput("repositories");
main(
appId,
privateKey,
repository,
owner,
repositories,
import_core.default,
import_auth_app.createAppAuth,
request_default.defaults({
Expand Down
4 changes: 3 additions & 1 deletion dist/post.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2956,8 +2956,10 @@ var import_core = __toESM(require_core(), 1);
// lib/post.js
async function post(core2, request2) {
const token = core2.getState("token");
if (!token)
if (!token) {
core2.info("Token is not set");
return;
}
await request2("DELETE /installation/token", {
headers: {
authorization: `token ${token}`
Expand Down
46 changes: 34 additions & 12 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,35 @@
/**
* @param {string} appId
* @param {string} privateKey
* @param {string} repository
* @param {string} owner
* @param {string} repositories
* @param {import("@actions/core")} core
* @param {import("@octokit/auth-app").createAppAuth} createAppAuth
* @param {import("@octokit/request").request} request
*/
export async function main(
appId,
privateKey,
repository,
owner,
repositories,
core,
createAppAuth,
request
) {
// Get owner and repo name from GITHUB_REPOSITORY
const [owner, repo] = repository.split("/");

let org = "";
if (owner.length == 0) {
org = process.env.GITHUB_REPOSITORY_OWNER || "";
}

if (owner.length == 0 && repositories.length == 0) {
repositories = process.env.GITHUB_REPOSITORY?.split("/")[1] || "";
}

let repos = [];
if (repositories.trim() != "") {
repos = repositories.split(",").map((repo) => repo.trim());
}

const auth = createAppAuth({
appId,
Expand All @@ -32,23 +46,31 @@ export async function main(
// Get the installation ID
// https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
const { data: installation } = await request(
"GET /repos/{owner}/{repo}/installation",
"GET /orgs/{org}/installation",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we need to get the installation ID for the current owner, we should keep doing it using GET /repos/{owner}/{repo}/installation. Because if the owner is set to something else, we need to check two endpoints, because the owner can be either an org or a user:

  1. GET /orgs/{org}/installation
  2. GET /users/{username}/installation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my suggested change at timreimherr#1

{
owner,
repo,
org,
headers: {
authorization: `bearer ${appAuthentication.token}`,
},
}
);

// Create a new installation token
const authentication = await auth({
type: "installation",
installationId: installation.id,
repositoryNames: [repo],
});
let authentication;

if (repositories.length == 0) {
authentication = await auth({
type: "installation",
installationId: installation.id,
});
} else {
authentication = await auth({
type: "installation",
installationId: installation.id,
repositoryNames: repos,
});
}

// Register the token with the runner as a secret to ensure it is masked in logs
core.setSecret(authentication.token);

Expand Down
11 changes: 8 additions & 3 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@ if (!process.env.GITHUB_REPOSITORY) {
throw new Error("GITHUB_REPOSITORY missing, must be set to '<owner>/<repo>'");
}

if (!process.env.GITHUB_REPOSITORY_OWNER) {
throw new Error("GITHUB_REPOSITORY_OWNER missing, must be set to '<owner>'");
}

const appId = core.getInput("app_id");
const privateKey = core.getInput("private_key");

const repository = process.env.GITHUB_REPOSITORY;
const owner = core.getInput("owner");
const repositories = core.getInput("repositories");

main(
appId,
privateKey,
repository,
owner,
repositories,
core,
createAppAuth,
request.defaults({
Expand Down