feat: permissions (#168)

- Load `app-permissions` from schema exported by `@octokit/openapi`
- Update documentation in README.md
- Implement the `permissions_*` inputs in the action code

---------

Co-authored-by: Parker Brown <17183625+parkerbxyz@users.noreply.github.com>
This commit is contained in:
Gregor Martynus
2025-03-27 12:00:54 -07:00
committed by GitHub
parent f577941506
commit 0e0aa99a86
21 changed files with 821 additions and 80 deletions

View File

@@ -17,3 +17,14 @@ or with npm
```
npm test
```
## How the tests work
The output from the tests is captured into a snapshot ([tests/snapshots/index.js.md](snapshots/index.js.md)). It includes all requests sent by our scripts to verify it's working correctly and to prevent regressions.
## How to add a new test
We have tests both for the `main.js` and `post.js` scripts.
- If you do not expect an error, take [main-token-permissions-set.test.js](tests/main-token-permissions-set.test.js) as a starting point.
- If your test has an expected error, take [main-missing-app-id.test.js](tests/main-missing-app-id.test.js) as a starting point.

View File

@@ -4,10 +4,10 @@ import { install } from "@sinonjs/fake-timers";
// Verify `main` retry when the clock has drifted.
await test((mockPool) => {
process.env.INPUT_OWNER = 'actions'
process.env.INPUT_REPOSITORIES = 'failed-repo';
const owner = process.env.INPUT_OWNER
const repo = process.env.INPUT_REPOSITORIES
process.env.INPUT_OWNER = "actions";
process.env.INPUT_REPOSITORIES = "failed-repo";
const owner = process.env.INPUT_OWNER;
const repo = process.env.INPUT_REPOSITORIES;
const mockInstallationId = "123456";
const mockAppSlug = "github-actions";
@@ -25,20 +25,23 @@ await test((mockPool) => {
})
.reply(({ headers }) => {
const [_, jwt] = (headers.authorization || "").split(" ");
const payload = JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString());
const payload = JSON.parse(
Buffer.from(jwt.split(".")[1], "base64").toString(),
);
if (payload.iat < 0) {
return {
statusCode: 401,
data: {
message: "'Issued at' claim ('iat') must be an Integer representing the time that the assertion was issued."
message:
"'Issued at' claim ('iat') must be an Integer representing the time that the assertion was issued.",
},
responseOptions: {
headers: {
"content-type": "application/json",
"date": new Date(Date.now() + 30000).toUTCString()
}
}
date: new Date(Date.now() + 30000).toUTCString(),
},
},
};
}
@@ -46,13 +49,14 @@ await test((mockPool) => {
statusCode: 200,
data: {
id: mockInstallationId,
"app_slug": mockAppSlug
app_slug: mockAppSlug,
},
responseOptions: {
headers: {
"content-type": "application/json"
}
}
"content-type": "application/json",
},
},
};
}).times(2);
})
.times(2);
});

View File

@@ -10,7 +10,7 @@ await test((mockPool) => {
const mockAppSlug = "github-actions";
mockPool
.intercept({
path: `/users/${process.env.INPUT_OWNER}/installation`,
path: `/users/smockle/installation`,
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
@@ -21,7 +21,7 @@ await test((mockPool) => {
.reply(500, "GitHub API not available");
mockPool
.intercept({
path: `/users/${process.env.INPUT_OWNER}/installation`,
path: `/users/smockle/installation`,
method: "GET",
headers: {
accept: "application/vnd.github.v3+json",
@@ -32,6 +32,6 @@ await test((mockPool) => {
.reply(
200,
{ id: mockInstallationId, app_slug: mockAppSlug },
{ headers: { "content-type": "application/json" } }
{ headers: { "content-type": "application/json" } },
);
});

View File

@@ -33,7 +33,7 @@ await test((mockPool) => {
})
.reply(
200,
{ id: mockInstallationId, "app_slug": mockAppSlug },
{ headers: { "content-type": "application/json" } }
{ id: mockInstallationId, app_slug: mockAppSlug },
{ headers: { "content-type": "application/json" } },
);
});

View File

@@ -21,6 +21,6 @@ await test((mockPool) => {
.reply(
200,
{ id: mockInstallationId, app_slug: mockAppSlug },
{ headers: { "content-type": "application/json" } }
{ headers: { "content-type": "application/json" } },
);
});

View File

@@ -20,7 +20,7 @@ await test((mockPool) => {
})
.reply(
200,
{ id: mockInstallationId, "app_slug": mockAppSlug },
{ headers: { "content-type": "application/json" } }
{ id: mockInstallationId, app_slug: mockAppSlug },
{ headers: { "content-type": "application/json" } },
);
});

View File

@@ -0,0 +1,7 @@
import { test } from "./main.js";
// Verify `main` successfully sets permissions
await test(() => {
process.env.INPUT_PERMISSION_ISSUES = `write`;
process.env.INPUT_PERMISSION_PULL_REQUESTS = `read`;
});

View File

@@ -47,7 +47,7 @@ export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) {
// Set up mocking
const baseUrl = new URL(env["INPUT_GITHUB-API-URL"]);
const basePath = baseUrl.pathname === "/" ? "" : baseUrl.pathname;
const mockAgent = new MockAgent();
const mockAgent = new MockAgent({ enableCallHistory: true });
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
const mockPool = mockAgent.get(baseUrl.origin);
@@ -60,8 +60,9 @@ export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) {
const owner = env.INPUT_OWNER ?? env.GITHUB_REPOSITORY_OWNER;
const currentRepoName = env.GITHUB_REPOSITORY.split("/")[1];
const repo = encodeURIComponent(
(env.INPUT_REPOSITORIES ?? currentRepoName).split(",")[0]
(env.INPUT_REPOSITORIES ?? currentRepoName).split(",")[0],
);
mockPool
.intercept({
path: `${basePath}/repos/${owner}/${repo}/installation`,
@@ -75,13 +76,14 @@ export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) {
.reply(
200,
{ id: mockInstallationId, app_slug: mockAppSlug },
{ headers: { "content-type": "application/json" } }
{ headers: { "content-type": "application/json" } },
);
// Mock installation access token request
const mockInstallationAccessToken =
"ghs_16C7e42F292c6912E7710c838347Ae178B4a"; // This token is invalidated. Its from https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app.
const mockExpiresAt = "2016-07-11T22:14:10Z";
mockPool
.intercept({
path: `${basePath}/app/installations/${mockInstallationId}/access_tokens`,
@@ -95,12 +97,26 @@ export async function test(cb = (_mockPool) => {}, env = DEFAULT_ENV) {
.reply(
201,
{ token: mockInstallationAccessToken, expires_at: mockExpiresAt },
{ headers: { "content-type": "application/json" } }
{ headers: { "content-type": "application/json" } },
);
// Run the callback
cb(mockPool);
// Run the main script
await import("../main.js");
const { default: promise } = await import("../main.js");
await promise;
console.log("--- REQUESTS ---");
const calls = mockAgent
.getCallHistory()
.calls()
.map((call) => {
const route = `${call.method} ${call.path}`;
if (call.method === "GET") return route;
return `${route}\n${call.body}`;
});
console.log(calls.join("\n"));
}

View File

@@ -33,7 +33,11 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---␊
GET /api/v3/repos/actions/create-github-app-token/installation␊
POST /api/v3/app/installations/123456/access_tokens␊
{"repositories":["create-github-app-token"]}`
## main-missing-app-id.test.js
@@ -92,7 +96,11 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::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"]}`
## main-repo-skew.test.js
@@ -112,7 +120,12 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---␊
GET /repos/actions/failed-repo/installation␊
GET /repos/actions/failed-repo/installation␊
POST /app/installations/123456/access_tokens␊
{"repositories":["failed-repo"]}`
## main-token-get-owner-set-fail-response.test.js
@@ -132,7 +145,12 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---␊
GET /users/smockle/installation␊
GET /users/smockle/installation␊
POST /app/installations/123456/access_tokens␊
null`
## main-token-get-owner-set-repo-fail-response.test.js
@@ -152,7 +170,12 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---␊
GET /repos/actions/failed-repo/installation␊
GET /repos/actions/failed-repo/installation␊
POST /app/installations/123456/access_tokens␊
{"repositories":["failed-repo"]}`
## main-token-get-owner-set-repo-set-to-many-newline.test.js
@@ -171,7 +194,11 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::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","checkout"]}`
## main-token-get-owner-set-repo-set-to-many.test.js
@@ -190,7 +217,11 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::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","checkout"]}`
## main-token-get-owner-set-repo-set-to-one.test.js
@@ -209,7 +240,11 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::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"]}`
## main-token-get-owner-set-repo-unset.test.js
@@ -228,7 +263,11 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::save-state name=expiresAt::2016-07-11T22:14:10Z
--- REQUESTS ---␊
GET /users/actions/installation␊
POST /app/installations/123456/access_tokens␊
null`
## main-token-get-owner-unset-repo-set.test.js
@@ -247,7 +286,11 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::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"]}`
## main-token-get-owner-unset-repo-unset.test.js
@@ -266,7 +309,34 @@ Generated by [AVA](https://avajs.dev).
::set-output name=app-slug::github-actions␊
::save-state name=token::ghs_16C7e42F292c6912E7710c838347Ae178B4a␊
::save-state name=expiresAt::2016-07-11T22:14:10Z`
::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"]}`
## main-token-permissions-set.test.js
> stderr
''
> stdout
`owner and repositories not set, creating token for the current repository ("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"],"permissions":{"issues":"write","pull_requests":"read"}}`
## post-revoke-token-fail-response.test.js

Binary file not shown.