diff --git a/README.md b/README.md index c3f17b7fd6..41e5c97234 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ All inputs are **optional**. If not set, sensible defaults will be used. | `path` | Relative path under `GITHUB_WORKSPACE` to the repository. | `GITHUB_WORKSPACE` | | `add-paths` | A comma or newline-separated list of file paths to commit. Paths should follow git's [pathspec](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec) syntax. See [Add specific paths](#add-specific-paths). | If no paths are specified, all new and modified files are added. | | `commit-message` | The message to use when committing changes. See [commit-message](#commit-message). | `[create-pull-request] automated change` | -| `committer` | The committer name and email address in the format `Display Name `. Defaults to the GitHub Actions bot user on github.com. | `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>` | +| `committer` | The committer name and email address in the format `Display Name `. Defaults to the identity of the `token` | `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>` for the GitHub Actions bot user on github.com | | `author` | The author name and email address in the format `Display Name `. Defaults to the user who triggered the workflow run. | `${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>` | | `signoff` | Add [`Signed-off-by`](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff) line by the committer at the end of the commit log message. | `false` | | `branch` | The pull request branch name. | `create-pull-request/patch` | diff --git a/__test__/utils.unit.test.ts b/__test__/utils.unit.test.ts index 2381b98e30..4f44d5ccb4 100644 --- a/__test__/utils.unit.test.ts +++ b/__test__/utils.unit.test.ts @@ -117,6 +117,38 @@ describe('utils tests', () => { ) } }) + + test('composeNoReplyIdentity produces the unchanged github.com identity', async () => { + const identity = utils.composeNoReplyIdentity( + 'github-actions[bot]', + 41898282, + 'github.com' + ) + expect(identity).toEqual( + 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>' + ) + const parsed = utils.parseDisplayNameEmail(identity) + expect(parsed.name).toEqual('github-actions[bot]') + expect(parsed.email).toEqual( + '41898282+github-actions[bot]@users.noreply.github.com' + ) + }) + + test('composeNoReplyIdentity derives a server-aware identity on GHES', async () => { + const identity = utils.composeNoReplyIdentity( + 'github-actions[bot]', + 5, + 'ghe.example.com' + ) + expect(identity).toEqual( + 'github-actions[bot] <5+github-actions[bot]@users.noreply.ghe.example.com>' + ) + const parsed = utils.parseDisplayNameEmail(identity) + expect(parsed.name).toEqual('github-actions[bot]') + expect(parsed.email).toEqual( + '5+github-actions[bot]@users.noreply.ghe.example.com' + ) + }) }) describe('retryWithBackoff', () => { diff --git a/action.yml b/action.yml index e09c1f3ba9..fecbacdd5b 100644 --- a/action.yml +++ b/action.yml @@ -23,13 +23,12 @@ inputs: committer: description: > The committer name and email address in the format `Display Name `. - Defaults to the GitHub Actions bot user. - default: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>' + Defaults to the identity of the `token` (e.g. `github-actions[bot]` for `GITHUB_TOKEN`, + your app's bot for a GitHub App token, or the user for a PAT). author: description: > The author name and email address in the format `Display Name `. - Defaults to the user who triggered the workflow run. - default: '${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>' + Defaults to the user who triggered the workflow run, with a server-aware no-reply email. signoff: description: 'Add `Signed-off-by` line by the committer at the end of the commit log message.' default: false diff --git a/dist/index.js b/dist/index.js index c06637d71d..00f9e29cda 100644 --- a/dist/index.js +++ b/dist/index.js @@ -491,6 +491,31 @@ function createPullRequest(inputs) { core.info(`Pull request branch to create or update set to '${inputs.branch}'`); // Configure the committer and author core.startGroup('Configuring the committer and author'); + // Resolve the default committer from the token identity + if (!inputs.committer) { + try { + const user = yield ghBranch.getServerUser(); + inputs.committer = utils.composeNoReplyIdentity(user.login, user.id, baseRemote.hostname); + core.info(`Committer not supplied; derived from token as '${inputs.committer}'`); + } + catch (e) { + core.warning(`Failed to derive committer from token: ${utils.getErrorMessage(e)}`); + inputs.committer = + 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>'; + } + } + // Resolve the author default from the triggering actor, with a server-aware + // no-reply email domain. + if (!inputs.author) { + const actor = process.env['GITHUB_ACTOR']; + const actorId = process.env['GITHUB_ACTOR_ID']; + if (actor && actorId) { + inputs.author = utils.composeNoReplyIdentity(actor, actorId, baseRemote.hostname); + } + else { + inputs.author = inputs.committer; + } + } const parsedAuthor = utils.parseDisplayNameEmail(inputs.author); const parsedCommitter = utils.parseDisplayNameEmail(inputs.committer); git.setIdentityGitOptions([ @@ -1406,6 +1431,21 @@ class GitHubHelper { repo: repo }; } + getServerUser() { + return __awaiter(this, void 0, void 0, function* () { + var _a, _b; + // Resolve the identity behind the authenticated token. + // The GraphQL `viewer` resolves installation tokens (the default + // `GITHUB_TOKEN` and GitHub App tokens) as well as PAT tokens. + // REST `users.getAuthenticated()` is NOT used since it does not work for + // installation tokens. + const resp = yield this.octokit.graphql(`query { viewer { login databaseId } }`); + if (!((_a = resp === null || resp === void 0 ? void 0 : resp.viewer) === null || _a === void 0 ? void 0 : _a.login) || !((_b = resp === null || resp === void 0 ? void 0 : resp.viewer) === null || _b === void 0 ? void 0 : _b.databaseId)) { + throw new Error('GraphQL viewer did not return login and databaseId'); + } + return { login: resp.viewer.login, id: resp.viewer.databaseId }; + }); + } getPullNumber(baseRepository, headBranch, baseBranch) { return __awaiter(this, void 0, void 0, function* () { var _a, e_1, _b, _c; @@ -1929,6 +1969,7 @@ exports.getRepoPath = getRepoPath; exports.getRemoteUrl = getRemoteUrl; exports.secondsSinceEpoch = secondsSinceEpoch; exports.randomString = randomString; +exports.composeNoReplyIdentity = composeNoReplyIdentity; exports.parseDisplayNameEmail = parseDisplayNameEmail; exports.fileExistsSync = fileExistsSync; exports.readFile = readFile; @@ -1980,6 +2021,11 @@ function secondsSinceEpoch() { function randomString() { return Math.random().toString(36).substr(2, 7); } +function composeNoReplyIdentity(login, id, hostname) { + // The GitHub no-reply commit identity, e.g. + // `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>`. + return `${login} <${id}+${login}@users.noreply.${hostname}>`; +} function parseDisplayNameEmail(displayNameEmail) { // Parse the name and email address from a string in the following format // Display Name diff --git a/src/create-pull-request.ts b/src/create-pull-request.ts index 2e3f0e0a29..9c814bd160 100644 --- a/src/create-pull-request.ts +++ b/src/create-pull-request.ts @@ -168,6 +168,41 @@ export async function createPullRequest(inputs: Inputs): Promise { // Configure the committer and author core.startGroup('Configuring the committer and author') + // Resolve the default committer from the token identity + if (!inputs.committer) { + try { + const user = await ghBranch.getServerUser() + inputs.committer = utils.composeNoReplyIdentity( + user.login, + user.id, + baseRemote.hostname + ) + core.info( + `Committer not supplied; derived from token as '${inputs.committer}'` + ) + } catch (e) { + core.warning( + `Failed to derive committer from token: ${utils.getErrorMessage(e)}` + ) + inputs.committer = + 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>' + } + } + // Resolve the author default from the triggering actor, with a server-aware + // no-reply email domain. + if (!inputs.author) { + const actor = process.env['GITHUB_ACTOR'] + const actorId = process.env['GITHUB_ACTOR_ID'] + if (actor && actorId) { + inputs.author = utils.composeNoReplyIdentity( + actor, + actorId, + baseRemote.hostname + ) + } else { + inputs.author = inputs.committer + } + } const parsedAuthor = utils.parseDisplayNameEmail(inputs.author) const parsedCommitter = utils.parseDisplayNameEmail(inputs.committer) git.setIdentityGitOptions([ diff --git a/src/github-helper.ts b/src/github-helper.ts index 26ef65cb87..1469fbfc87 100644 --- a/src/github-helper.ts +++ b/src/github-helper.ts @@ -37,6 +37,11 @@ interface CommitResponse { verified: boolean } +interface TokenIdentity { + login: string + id: number +} + type TreeObject = { path: string mode: '100644' | '100755' | '040000' | '160000' | '120000' @@ -70,6 +75,21 @@ export class GitHubHelper { } } + async getServerUser(): Promise { + // Resolve the identity behind the authenticated token. + // The GraphQL `viewer` resolves installation tokens (the default + // `GITHUB_TOKEN` and GitHub App tokens) as well as PAT tokens. + // REST `users.getAuthenticated()` is NOT used since it does not work for + // installation tokens. + const resp = await this.octokit.graphql<{ + viewer: {login: string; databaseId: number} + }>(`query { viewer { login databaseId } }`) + if (!resp?.viewer?.login || !resp?.viewer?.databaseId) { + throw new Error('GraphQL viewer did not return login and databaseId') + } + return {login: resp.viewer.login, id: resp.viewer.databaseId} + } + private async getPullNumber( baseRepository: string, headBranch: string, diff --git a/src/utils.ts b/src/utils.ts index a7f825d8b3..84c89efb69 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -65,6 +65,16 @@ interface DisplayNameEmail { email: string } +export function composeNoReplyIdentity( + login: string, + id: number | string, + hostname: string +): string { + // The GitHub no-reply commit identity, e.g. + // `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>`. + return `${login} <${id}+${login}@users.noreply.${hostname}>` +} + export function parseDisplayNameEmail( displayNameEmail: string ): DisplayNameEmail {