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

@@ -0,0 +1,23 @@
/**
* Finds all permissions passed via `permision-*` inputs and turns them into an object.
*
* @see https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#inputs
* @param {NodeJS.ProcessEnv} env
* @returns {undefined | Record<string, string>}
*/
export function getPermissionsFromInputs(env) {
return Object.entries(env).reduce((permissions, [key, value]) => {
if (!key.startsWith("INPUT_PERMISSION_")) return permissions;
const permission = key.slice("INPUT_PERMISSION_".length).toLowerCase();
if (permissions === undefined) {
return { [permission]: value };
}
return {
// @ts-expect-error - needs to be typed correctly
...permissions,
[permission]: value,
};
}, undefined);
}

View File

@@ -6,6 +6,7 @@ import pRetry from "p-retry";
* @param {string} privateKey
* @param {string} owner
* @param {string[]} repositories
* @param {undefined | Record<string, string>} permissions
* @param {import("@actions/core")} core
* @param {import("@octokit/auth-app").createAppAuth} createAppAuth
* @param {import("@octokit/request").request} request
@@ -16,10 +17,11 @@ export async function main(
privateKey,
owner,
repositories,
permissions,
core,
createAppAuth,
request,
skipTokenRevoke
skipTokenRevoke,
) {
let parsedOwner = "";
let parsedRepositoryNames = [];
@@ -31,7 +33,7 @@ export async function main(
parsedRepositoryNames = [repo];
core.info(
`owner and repositories not set, creating token for the current repository ("${repo}")`
`owner and repositories not set, creating token for the current repository ("${repo}")`,
);
}
@@ -40,7 +42,7 @@ export async function main(
parsedOwner = owner;
core.info(
`repositories not set, creating token for all repositories for given owner "${owner}"`
`repositories not set, creating token for all repositories for given owner "${owner}"`,
);
}
@@ -51,8 +53,8 @@ export async function main(
core.info(
`owner not set, creating owner for given repositories "${repositories.join(
","
)}" in current owner ("${parsedOwner}")`
",",
)}" in current owner ("${parsedOwner}")`,
);
}
@@ -63,8 +65,8 @@ export async function main(
core.info(
`owner and repositories set, creating token for repositories "${repositories.join(
","
)}" owned by "${owner}"`
",",
)}" owned by "${owner}"`,
);
}
@@ -84,31 +86,32 @@ export async function main(
request,
auth,
parsedOwner,
parsedRepositoryNames
parsedRepositoryNames,
permissions,
),
{
onFailedAttempt: (error) => {
core.info(
`Failed to create token for "${parsedRepositoryNames.join(
","
)}" (attempt ${error.attemptNumber}): ${error.message}`
",",
)}" (attempt ${error.attemptNumber}): ${error.message}`,
);
},
retries: 3,
}
},
));
} else {
// Otherwise get the installation for the owner, which can either be an organization or a user account
({ authentication, installationId, appSlug } = await pRetry(
() => getTokenFromOwner(request, auth, parsedOwner),
() => getTokenFromOwner(request, auth, parsedOwner, permissions),
{
onFailedAttempt: (error) => {
core.info(
`Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}`
`Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}`,
);
},
retries: 3,
}
},
));
}
@@ -126,7 +129,7 @@ export async function main(
}
}
async function getTokenFromOwner(request, auth, parsedOwner) {
async function getTokenFromOwner(request, auth, parsedOwner, permissions) {
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app
// This endpoint works for both users and organizations
const response = await request("GET /users/{username}/installation", {
@@ -140,6 +143,7 @@ async function getTokenFromOwner(request, auth, parsedOwner) {
const authentication = await auth({
type: "installation",
installationId: response.data.id,
permissions,
});
const installationId = response.data.id;
@@ -152,7 +156,8 @@ async function getTokenFromRepository(
request,
auth,
parsedOwner,
parsedRepositoryNames
parsedRepositoryNames,
permissions,
) {
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
const response = await request("GET /repos/{owner}/{repo}/installation", {
@@ -168,6 +173,7 @@ async function getTokenFromRepository(
type: "installation",
installationId: response.data.id,
repositoryNames: parsedRepositoryNames,
permissions,
});
const installationId = response.data.id;

View File

@@ -17,7 +17,7 @@ const proxyUrl =
const proxyFetch = (url, options) => {
const urlHost = new URL(url).hostname;
const noProxy = (process.env.no_proxy || process.env.NO_PROXY || "").split(
","
",",
);
if (!noProxy.includes(urlHost)) {