From c33d79a40cc24a5c1111a19e5925d2e9ca553fcf Mon Sep 17 00:00:00 2001 From: Derek Cormier Date: Mon, 1 Jan 2024 20:22:27 -0800 Subject: [PATCH] fix: properly attribute the github-actions[bot] as a commit author --- e2e/e2e.spec.ts | 48 ++++++++++++++++++++++++++++++++++++ e2e/helpers/fixture.ts | 6 ++--- e2e/stubs/fake-github.ts | 2 +- src/infrastructure/github.ts | 15 +++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/e2e/e2e.spec.ts b/e2e/e2e.spec.ts index 3f4dee1..35996af 100644 --- a/e2e/e2e.spec.ts +++ b/e2e/e2e.spec.ts @@ -635,6 +635,54 @@ describe("e2e tests", () => { expect(messages[0].subject).toEqual(`Publish to BCR`); }); + + test("commits are properly attributed to the github-actions[bot] when it is the releaser", async () => { + const repo = Fixture.Versioned; + const tag = "v1.0.0"; + await setupLocalRemoteRulesetRepo(repo, tag, { + login: "committer", + email: "committer@test.org", + }); + + fakeGitHub.mockUser({ login: "github-actions[bot]" }); + fakeGitHub.mockRepository(testOrg, repo); + fakeGitHub.mockRepository( + testOrg, + "bazel-central-registry", + "bazelbuild", + "bazel-central-registry" + ); + fakeGitHub.mockAppInstallation(testOrg, repo); + fakeGitHub.mockAppInstallation(testOrg, "bazel-central-registry"); + + const releaseArchive = await makeReleaseTarball(repo, "versioned-1.0.0"); + await fakeGitHub.mockReleaseArchive( + `/${testOrg}/${repo}/archive/refs/tags/${tag}.tar.gz`, + releaseArchive + ); + + const response = await publishReleaseEvent( + cloudFunctions.getBaseUrl(), + secrets.webhookSecret, + { + owner: testOrg, + repo, + tag, + releaser: { login: "github-actions[bot]" }, + } + ); + + expect(response.status).toEqual(200); + + const git = getBcr(); + const entryBranch = await getLatestBranch(git); + const logs = await git.log({ maxCount: 1, from: entryBranch }); + + expect(logs.latest?.author_email).toEqual( + "41898282+github-actions[bot]@users.noreply.github.com" + ); + expect(logs.latest?.author_name).toEqual("github-actions[bot]"); + }); }); const testReleaseArchives: string[] = []; diff --git a/e2e/helpers/fixture.ts b/e2e/helpers/fixture.ts index 34cb43a..7273161 100644 --- a/e2e/helpers/fixture.ts +++ b/e2e/helpers/fixture.ts @@ -31,7 +31,7 @@ export enum Fixture { export async function setupLocalRemoteRulesetRepo( fixture: Fixture, tag: string, - releaser: Partial + commitAuthor: Partial ): Promise { const gitRepoPath = path.join(PREPARED_FIXTURES_PATH, fixture); let git: SimpleGit; @@ -44,8 +44,8 @@ export async function setupLocalRemoteRulesetRepo( git = simpleGit(gitRepoPath); await git.init(); await git.add("./*"); - await git.addConfig("user.name", releaser.login!); - await git.addConfig("user.email", releaser.email!); + await git.addConfig("user.name", commitAuthor.login!); + await git.addConfig("user.email", commitAuthor.email!); await git.commit("first commit"); } else { git = simpleGit(gitRepoPath); diff --git a/e2e/stubs/fake-github.ts b/e2e/stubs/fake-github.ts index e70fb5d..a56edc1 100644 --- a/e2e/stubs/fake-github.ts +++ b/e2e/stubs/fake-github.ts @@ -202,7 +202,7 @@ export class FakeGitHub implements StubbedServer { const pattern = /\/users\/([^/]+)\/repos/; await this.server.forGet(pattern).thenCallback((request) => { const match = request.path.match(pattern); - const owner = match![1]; + const owner = decodeURIComponent(match![1]); if (this.ownedRepos.has(owner)) { return { diff --git a/src/infrastructure/github.ts b/src/infrastructure/github.ts index 8079ff1..6af5424 100644 --- a/src/infrastructure/github.ts +++ b/src/infrastructure/github.ts @@ -10,6 +10,18 @@ export class MissingRepositoryInstallationError extends Error { } export class GitHubClient { + // The GitHub API does not return a name or an email for the github-actions[bot]. + // See https://api.github.com/users/github-actions%5Bbot%5D + // Yet, an email and a name are implicitly set when the bot is an author of a + // commit. Hardcode the (stable) name and email so that we can also author commits + // as the GitHub actions bot. + // See https://github.com/orgs/community/discussions/26560#discussioncomment-3252340. + public static readonly GITHUB_ACTIONS_BOT: User = { + username: "github-actions[bot]", + name: "github-actions[bot]", + email: "41898282+github-actions[bot]@users.noreply.github.com", + }; + // Cache installation tokens as they expire after an hour, which is more than // enough time for a cloud function to run. private readonly _installationTokenCache: any = {}; @@ -109,6 +121,9 @@ export class GitHubClient { username: string, repository: Repository ): Promise { + if (username === GitHubClient.GITHUB_ACTIONS_BOT.username) { + return GitHubClient.GITHUB_ACTIONS_BOT; + } const octokit = await this.getRepoAuthorizedOctokit(repository); const { data } = await octokit.rest.users.getByUsername({ username }); return { name: data.name, username, email: data.email };