0c014070f9
Fixes #72 If an Actions job is long enough, more than an hour can pass between creating and revoking the App token in the post-job clean up step. Since the token itself is used to authenticate with the revoke API, an expired token will fail to be revoked. This PR saves the token expiration in the actions state and uses that in the post step to determine if the token can be revoked. I've also added error handling to the revoke token API call, as it's unlikely that users would want their job to fail if the token can't be revoked.
156 lines
4.6 KiB
JavaScript
156 lines
4.6 KiB
JavaScript
import pRetry from "p-retry";
|
|
// @ts-check
|
|
|
|
/**
|
|
* @param {string} appId
|
|
* @param {string} privateKey
|
|
* @param {string} owner
|
|
* @param {string} repositories
|
|
* @param {import("@actions/core")} core
|
|
* @param {import("@octokit/auth-app").createAppAuth} createAppAuth
|
|
* @param {import("@octokit/request").request} request
|
|
* @param {boolean} skipTokenRevoke
|
|
*/
|
|
export async function main(
|
|
appId,
|
|
privateKey,
|
|
owner,
|
|
repositories,
|
|
core,
|
|
createAppAuth,
|
|
request,
|
|
skipTokenRevoke
|
|
) {
|
|
let parsedOwner = "";
|
|
let parsedRepositoryNames = "";
|
|
|
|
// If neither owner nor repositories are set, default to current repository
|
|
if (!owner && !repositories) {
|
|
[parsedOwner, parsedRepositoryNames] = String(
|
|
process.env.GITHUB_REPOSITORY
|
|
).split("/");
|
|
|
|
core.info(
|
|
`owner and repositories not set, creating token for the current repository ("${parsedRepositoryNames}")`
|
|
);
|
|
}
|
|
|
|
// If only an owner is set, default to all repositories from that owner
|
|
if (owner && !repositories) {
|
|
parsedOwner = owner;
|
|
|
|
core.info(
|
|
`repositories not set, creating token for all repositories for given owner "${owner}"`
|
|
);
|
|
}
|
|
|
|
// If repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER`
|
|
if (!owner && repositories) {
|
|
parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER);
|
|
parsedRepositoryNames = repositories;
|
|
|
|
core.info(
|
|
`owner not set, creating owner for given repositories "${repositories}" in current owner ("${parsedOwner}")`
|
|
);
|
|
}
|
|
|
|
// If both owner and repositories are set, use those values
|
|
if (owner && repositories) {
|
|
parsedOwner = owner;
|
|
parsedRepositoryNames = repositories;
|
|
|
|
core.info(
|
|
`owner and repositories set, creating token for repositories "${repositories}" owned by "${owner}"`
|
|
);
|
|
}
|
|
|
|
const auth = createAppAuth({
|
|
appId,
|
|
privateKey,
|
|
request,
|
|
});
|
|
|
|
let authentication;
|
|
// If at least one repository is set, get installation ID from that repository
|
|
|
|
if (parsedRepositoryNames) {
|
|
authentication = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames), {
|
|
onFailedAttempt: (error) => {
|
|
core.info(
|
|
`Failed to create token for "${parsedRepositoryNames}" (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 = await pRetry(() => getTokenFromOwner(request, auth, parsedOwner), {
|
|
onFailedAttempt: (error) => {
|
|
core.info(
|
|
`Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}`
|
|
);
|
|
},
|
|
retries: 3,
|
|
});
|
|
}
|
|
|
|
// Register the token with the runner as a secret to ensure it is masked in logs
|
|
core.setSecret(authentication.token);
|
|
|
|
core.setOutput("token", authentication.token);
|
|
|
|
// Make token accessible to post function (so we can invalidate it)
|
|
if (!skipTokenRevoke) {
|
|
core.saveState("token", authentication.token);
|
|
core.setOutput("expiresAt", authentication.expiresAt);
|
|
}
|
|
}
|
|
|
|
async function getTokenFromOwner(request, auth, parsedOwner) {
|
|
// https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-organization-installation-for-the-authenticated-app
|
|
const response = await request("GET /orgs/{org}/installation", {
|
|
org: parsedOwner,
|
|
request: {
|
|
hook: auth.hook,
|
|
},
|
|
}).catch((error) => {
|
|
/* c8 ignore next */
|
|
if (error.status !== 404) throw error;
|
|
|
|
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app
|
|
return request("GET /users/{username}/installation", {
|
|
username: parsedOwner,
|
|
request: {
|
|
hook: auth.hook,
|
|
},
|
|
});
|
|
});
|
|
|
|
// Get token for for all repositories of the given installation
|
|
const authentication = await auth({
|
|
type: "installation",
|
|
installationId: response.data.id,
|
|
});
|
|
return authentication;
|
|
}
|
|
|
|
async function getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames) {
|
|
// 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", {
|
|
owner: parsedOwner,
|
|
repo: parsedRepositoryNames.split(",")[0],
|
|
request: {
|
|
hook: auth.hook,
|
|
},
|
|
});
|
|
|
|
// Get token for given repositories
|
|
const authentication = await auth({
|
|
type: "installation",
|
|
installationId: response.data.id,
|
|
repositoryNames: parsedRepositoryNames.split(","),
|
|
});
|
|
|
|
return authentication;
|
|
} |