e6bd4e6970
GitHub now recommends using a GitHub App's Client ID for authentication.
This PR adds a first-class `client-id` input, keeps `app-id` available
for compatibility, and makes the migration path explicit in both runtime
behavior and documentation.
### Action inputs
- Adds a new `client-id` input
- Removes `required` from `app-id`
- Marks `app-id` as deprecated in `action.yml`
### Runtime behavior
- Updates input parsing to prefer `client-id`
- Falls back to `app-id` for existing workflows
- Adds a clear error when neither `client-id` nor `app-id` is provided
### Docs
- Updates the README to recommend `client-id`
- Switches usage examples to `client-id`
- Documents that `app-id` is deprecated and that `client-id` takes
precedence if both are set
### Regression coverage
- Adds a focused test proving a client-ID-shaped value works through the
new `client-id` input
- Adds coverage for the missing-ID validation path
- Updates snapshots to lock in the new metadata and runtime behavior
### Resulting usage
Users can migrate to the new input name directly:
```yaml
- uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
```
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: parkerbxyz <17183625+parkerbxyz@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
184 lines
5.4 KiB
JavaScript
184 lines
5.4 KiB
JavaScript
import pRetry from "p-retry";
|
|
// @ts-check
|
|
|
|
/**
|
|
* @param {string} clientId
|
|
* @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
|
|
* @param {boolean} skipTokenRevoke
|
|
*/
|
|
export async function main(
|
|
clientId,
|
|
privateKey,
|
|
owner,
|
|
repositories,
|
|
permissions,
|
|
core,
|
|
createAppAuth,
|
|
request,
|
|
skipTokenRevoke
|
|
) {
|
|
let parsedOwner = "";
|
|
let parsedRepositoryNames = [];
|
|
|
|
// If neither owner nor repositories are set, default to current repository
|
|
if (!owner && repositories.length === 0) {
|
|
const [owner, repo] = String(process.env.GITHUB_REPOSITORY).split("/");
|
|
parsedOwner = owner;
|
|
parsedRepositoryNames = [repo];
|
|
|
|
core.info(
|
|
`Inputs 'owner' and 'repositories' are not set. Creating token for this repository (${owner}/${repo}).`
|
|
);
|
|
}
|
|
|
|
// If only an owner is set, default to all repositories from that owner
|
|
if (owner && repositories.length === 0) {
|
|
parsedOwner = owner;
|
|
|
|
core.info(
|
|
`Input 'repositories' is not set. Creating token for all repositories owned by ${owner}.`
|
|
);
|
|
}
|
|
|
|
// If repositories are set, but no owner, default to `GITHUB_REPOSITORY_OWNER`
|
|
if (!owner && repositories.length > 0) {
|
|
parsedOwner = String(process.env.GITHUB_REPOSITORY_OWNER);
|
|
parsedRepositoryNames = repositories;
|
|
|
|
core.info(
|
|
`No 'owner' input provided. Using default owner '${parsedOwner}' to create token for the following repositories:${repositories
|
|
.map((repo) => `\n- ${parsedOwner}/${repo}`)
|
|
.join("")}`
|
|
);
|
|
}
|
|
|
|
// If both owner and repositories are set, use those values
|
|
if (owner && repositories.length > 0) {
|
|
parsedOwner = owner;
|
|
parsedRepositoryNames = repositories;
|
|
|
|
core.info(
|
|
`Inputs 'owner' and 'repositories' are set. Creating token for the following repositories:
|
|
${repositories.map((repo) => `\n- ${parsedOwner}/${repo}`).join("")}`
|
|
);
|
|
}
|
|
|
|
const auth = createAppAuth({
|
|
appId: clientId,
|
|
privateKey,
|
|
request,
|
|
});
|
|
|
|
let authentication, installationId, appSlug;
|
|
// If at least one repository is set, get installation ID from that repository
|
|
|
|
if (parsedRepositoryNames.length > 0) {
|
|
({ authentication, installationId, appSlug } = await pRetry(
|
|
() =>
|
|
getTokenFromRepository(
|
|
request,
|
|
auth,
|
|
parsedOwner,
|
|
parsedRepositoryNames,
|
|
permissions
|
|
),
|
|
{
|
|
shouldRetry: ({ error }) => error.status >= 500,
|
|
onFailedAttempt: (context) => {
|
|
core.info(
|
|
`Failed to create token for "${parsedRepositoryNames.join(
|
|
","
|
|
)}" (attempt ${context.attemptNumber}): ${context.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, permissions),
|
|
{
|
|
onFailedAttempt: (context) => {
|
|
core.info(
|
|
`Failed to create token for "${parsedOwner}" (attempt ${context.attemptNumber}): ${context.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);
|
|
core.setOutput("installation-id", installationId);
|
|
core.setOutput("app-slug", appSlug);
|
|
|
|
// Make token accessible to post function (so we can invalidate it)
|
|
if (!skipTokenRevoke) {
|
|
core.saveState("token", authentication.token);
|
|
core.saveState("expiresAt", authentication.expiresAt);
|
|
}
|
|
}
|
|
|
|
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", {
|
|
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,
|
|
permissions,
|
|
});
|
|
|
|
const installationId = response.data.id;
|
|
const appSlug = response.data["app_slug"];
|
|
|
|
return { authentication, installationId, appSlug };
|
|
}
|
|
|
|
async function getTokenFromRepository(
|
|
request,
|
|
auth,
|
|
parsedOwner,
|
|
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", {
|
|
owner: parsedOwner,
|
|
repo: parsedRepositoryNames[0],
|
|
request: {
|
|
hook: auth.hook,
|
|
},
|
|
});
|
|
|
|
// Get token for given repositories
|
|
const authentication = await auth({
|
|
type: "installation",
|
|
installationId: response.data.id,
|
|
repositoryNames: parsedRepositoryNames,
|
|
permissions,
|
|
});
|
|
|
|
const installationId = response.data.id;
|
|
const appSlug = response.data["app_slug"];
|
|
|
|
return { authentication, installationId, appSlug };
|
|
}
|