Skip to content

Commit 981ba04

Browse files
authored
fix: Fix for GHES to allow variations in GitHub Actions bot user id (#222)
* refactor: remove GITHUB_ACTIONS_BOT_USER_ID constant and update related logic; add getGitHubActionsBotEmail function for dynamic email retrieval * refactor: enhance GitHub API integration by adding user retrieval and updating related logic in tests
1 parent 78b75c7 commit 981ba04

File tree

11 files changed

+95
-49
lines changed

11 files changed

+95
-49
lines changed

__tests__/helpers/octokit.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type EndpointNames = {
1919
issues: 'createComment' | 'deleteComment' | 'listComments';
2020
pulls: 'listCommits' | 'listFiles';
2121
repos: 'getCommit' | 'listTags' | 'listReleases' | 'createRelease' | 'deleteRelease';
22+
users: 'getByUsername';
2223
};
2324

2425
// Type guard that ensures the method name is valid for the given namespace K.
@@ -164,12 +165,26 @@ export function resetMockStore() {
164165
headers: {},
165166
},
166167
},
168+
users: {
169+
getByUsername: {
170+
data: {
171+
login: 'github-actions[bot]',
172+
id: 41898282,
173+
type: 'Bot',
174+
site_admin: false,
175+
},
176+
status: 200,
177+
url: 'https://api.github.com/users/github-actions[bot]',
178+
headers: {},
179+
},
180+
},
167181
},
168182
implementations: {
169183
git: {},
170184
issues: {},
171185
pulls: {},
172186
repos: {},
187+
users: {},
173188
},
174189
};
175190
}
@@ -302,6 +317,9 @@ export function createDefaultOctokitMock(): OctokitRestApi {
302317
createRelease: vi.fn().mockImplementation(() => getMockResponse('repos.createRelease')),
303318
deleteRelease: vi.fn().mockImplementation(() => getMockResponse('repos.deleteRelease')),
304319
},
320+
users: {
321+
getByUsername: vi.fn().mockImplementation((params) => getMockResponse('users.getByUsername', params)),
322+
},
305323
},
306324
paginate: {
307325
iterator: <T>(

__tests__/pull-request.test.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@ import type { TerraformModule } from '@/terraform-module';
55
import { stubOctokitImplementation, stubOctokitReturnData } from '@/tests/helpers/octokit';
66
import { createMockTerraformModule } from '@/tests/helpers/terraform-module';
77
import type { GitHubRelease } from '@/types';
8-
import {
9-
BRANDING_COMMENT,
10-
GITHUB_ACTIONS_BOT_USER_ID,
11-
PR_RELEASE_MARKER,
12-
PR_SUMMARY_MARKER,
13-
WIKI_STATUS,
14-
} from '@/utils/constants';
8+
import { BRANDING_COMMENT, PR_RELEASE_MARKER, PR_SUMMARY_MARKER, WIKI_STATUS } from '@/utils/constants';
159
import { debug, endGroup, info, startGroup } from '@actions/core';
1610
import { RequestError } from '@octokit/request-error';
1711
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -55,25 +49,14 @@ describe('pull-request', () => {
5549
context.useMockOctokit();
5650
});
5751

58-
it('should return false when release marker is found in comments from non github-actions user', async () => {
52+
it('should return true when release marker is found in comments', async () => {
5953
stubOctokitReturnData('issues.listComments', {
6054
data: [
6155
{ user: { id: 123 }, body: 'Some comment' },
6256
{ user: { id: 123 }, body: PR_RELEASE_MARKER },
6357
{ user: { id: 123 }, body: 'Another comment' },
6458
],
6559
});
66-
expect(await hasReleaseComment()).toBe(false);
67-
});
68-
69-
it('should return true when release marker is found in comments from github-actions user', async () => {
70-
stubOctokitReturnData('issues.listComments', {
71-
data: [
72-
{ user: { id: 123 }, body: 'Some comment' },
73-
{ user: { id: GITHUB_ACTIONS_BOT_USER_ID }, body: PR_RELEASE_MARKER },
74-
{ user: { id: 123 }, body: 'Another comment' },
75-
],
76-
});
7760
expect(await hasReleaseComment()).toBe(true);
7861
});
7962

__tests__/utils/constants.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import {
22
BRANDING_COMMENT,
33
BRANDING_WIKI,
4-
GITHUB_ACTIONS_BOT_EMAIL,
54
GITHUB_ACTIONS_BOT_NAME,
5+
GITHUB_ACTIONS_BOT_USERNAME,
66
PROJECT_URL,
77
PR_RELEASE_MARKER,
88
PR_SUMMARY_MARKER,
99
WIKI_TITLE_REPLACEMENTS,
1010
} from '@/utils/constants';
1111
import { describe, expect, it } from 'vitest';
1212

13-
describe('constants', () => {
13+
describe('utils/constants', () => {
1414
it('should have the correct GitHub Actions bot name', () => {
1515
expect(GITHUB_ACTIONS_BOT_NAME).toBe('GitHub Actions');
1616
});
1717

18-
it('should have the correct GitHub Actions bot email', () => {
19-
expect(GITHUB_ACTIONS_BOT_EMAIL).toBe('41898282+github-actions[bot]@users.noreply.github.com');
18+
it('should have the correct GitHub Actions bot username', () => {
19+
expect(GITHUB_ACTIONS_BOT_USERNAME).toBe('github-actions[bot]');
2020
});
2121

2222
it('should have the correct PR summary marker', () => {

__tests__/utils/github.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { context } from '@/mocks/context';
2+
import { getGitHubActionsBotEmail } from '@/utils/github';
3+
import { beforeAll, describe, expect, it } from 'vitest';
4+
5+
describe('utils/github', () => {
6+
describe('getGitHubActionsBotEmail - real API queries', () => {
7+
beforeAll(async () => {
8+
if (!process.env.GITHUB_TOKEN) {
9+
throw new Error('GITHUB_TOKEN environment variable must be set for these tests');
10+
}
11+
await context.useRealOctokit();
12+
});
13+
14+
it('should return the correct email format for GitHub.com public API', async () => {
15+
// This test uses the real GitHub API and expects the standard GitHub.com user ID
16+
// for the github-actions[bot] user, which is 41898282
17+
18+
const result = await getGitHubActionsBotEmail();
19+
20+
// Assert
21+
expect(result).toBe('41898282+github-actions[bot]@users.noreply.github.com');
22+
});
23+
});
24+
});

__tests__/wiki.test.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ describe('wiki', async () => {
205205

206206
afterAll(() => {
207207
vi.mocked(execFileSync).mockImplementation(vi.fn());
208+
vi.resetAllMocks(); // Unclears the console.log
208209
});
209210

210211
it('should generate all required wiki files', async () => {
@@ -259,11 +260,16 @@ describe('wiki', async () => {
259260
});
260261

261262
describe('commitAndPushWikiChanges()', () => {
262-
it('should commit and push changes when changes are detected', () => {
263+
beforeAll(() => {
264+
// Ensure we're using the mock octokit, not real one
265+
context.useMockOctokit();
266+
});
267+
268+
it('should commit and push changes when changes are detected', async () => {
263269
// Mock git status to indicate changes exist
264270
vi.mocked(execFileSync).mockImplementationOnce(() => Buffer.from('M _Sidebar.md\n'));
265271

266-
commitAndPushWikiChanges();
272+
await commitAndPushWikiChanges();
267273

268274
// Verify git commands were called in correct order
269275
const gitCalls = vi.mocked(execFileSync).mock.calls.map((call) => call?.[1]?.join(' ') || '');
@@ -284,11 +290,11 @@ describe('wiki', async () => {
284290
expect(endGroup).toHaveBeenCalled();
285291
});
286292

287-
it('should skip commit and push when no changes are detected', () => {
293+
it('should skip commit and push when no changes are detected', async () => {
288294
// Mock git status to indicate no changes
289295
vi.mocked(execFileSync).mockImplementationOnce(() => Buffer.from(''));
290296

291-
commitAndPushWikiChanges();
297+
await commitAndPushWikiChanges();
292298

293299
// Verify only status check was called
294300
const gitCalls = vi.mocked(execFileSync).mock.calls.map((call) => call?.[1]?.join(' ') || '');
@@ -301,23 +307,23 @@ describe('wiki', async () => {
301307
expect(endGroup).toHaveBeenCalled();
302308
});
303309

304-
it('should handle git command failures gracefully', () => {
310+
it('should handle git command failures gracefully', async () => {
305311
// Mock git status to indicate changes exist but make add command fail
306312
vi.mocked(execFileSync)
307313
.mockImplementationOnce(() => Buffer.from('M _Sidebar.md\n'))
308314
.mockImplementationOnce(() => {
309315
throw new Error('Git command failed');
310316
});
311317

312-
expect(() => commitAndPushWikiChanges()).toThrow('Git command failed');
318+
await expect(commitAndPushWikiChanges()).rejects.toThrow('Git command failed');
313319

314320
expect(startGroup).toHaveBeenCalledWith('Committing and pushing changes to wiki');
315321
expect(info).toHaveBeenCalledWith('Checking for changes in wiki repository');
316322
expect(info).toHaveBeenCalledWith('git status output: M _Sidebar.md');
317323
expect(endGroup).toHaveBeenCalled();
318324
});
319325

320-
it('should not use complete PR information in commit message', () => {
326+
it('should not use complete PR information in commit message', async () => {
321327
// Set up PR context with multiline body
322328
context.set({
323329
prBody: 'Line 1\nLine 2\nLine 3',
@@ -328,7 +334,7 @@ describe('wiki', async () => {
328334
// Mock git status to indicate changes exist
329335
vi.mocked(execFileSync).mockImplementationOnce(() => Buffer.from('M _Sidebar.md\n'));
330336

331-
commitAndPushWikiChanges();
337+
await commitAndPushWikiChanges();
332338

333339
// Verify commit message format
334340
const commitCall = vi.mocked(execFileSync).mock.calls.find((call) => call?.[1]?.includes('commit'));

src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ async function handlePullRequestMergedEvent(
7878
ensureTerraformDocsConfigDoesNotExist();
7979
checkoutWiki();
8080
await generateWikiFiles(terraformModules);
81-
commitAndPushWikiChanges();
81+
await commitAndPushWikiChanges();
8282
}
8383
}
8484

src/pull-request.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,7 @@ import { config } from '@/config';
33
import { context } from '@/context';
44
import { TerraformModule } from '@/terraform-module';
55
import type { CommitDetails, GitHubRelease, WikiStatusResult } from '@/types';
6-
import {
7-
BRANDING_COMMENT,
8-
GITHUB_ACTIONS_BOT_USER_ID,
9-
PROJECT_URL,
10-
PR_RELEASE_MARKER,
11-
PR_SUMMARY_MARKER,
12-
WIKI_STATUS,
13-
} from '@/utils/constants';
6+
import { BRANDING_COMMENT, PROJECT_URL, PR_RELEASE_MARKER, PR_SUMMARY_MARKER, WIKI_STATUS } from '@/utils/constants';
147

158
import { getWikiLink } from '@/wiki';
169
import { debug, endGroup, info, startGroup } from '@actions/core';
@@ -38,7 +31,7 @@ export async function hasReleaseComment(): Promise<boolean> {
3831

3932
for await (const { data } of iterator) {
4033
for (const comment of data) {
41-
if (comment.user?.id === GITHUB_ACTIONS_BOT_USER_ID && comment.body?.includes(PR_RELEASE_MARKER)) {
34+
if (comment.body?.includes(PR_RELEASE_MARKER)) {
4235
return true;
4336
}
4437
}

src/releases.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { config } from '@/config';
77
import { context } from '@/context';
88
import { TerraformModule } from '@/terraform-module';
99
import type { GitHubRelease } from '@/types';
10-
import { GITHUB_ACTIONS_BOT_EMAIL, GITHUB_ACTIONS_BOT_NAME } from '@/utils/constants';
10+
import { GITHUB_ACTIONS_BOT_NAME } from '@/utils/constants';
1111
import { copyModuleContents } from '@/utils/file';
12+
import { getGitHubActionsBotEmail } from '@/utils/github';
1213
import { debug, endGroup, info, startGroup } from '@actions/core';
1314
import type { RestEndpointMethodTypes } from '@octokit/plugin-rest-endpoint-methods';
1415
import { RequestError } from '@octokit/request-error';
@@ -139,13 +140,14 @@ export async function createTaggedReleases(terraformModules: TerraformModule[]):
139140
// Git operations: commit the changes and tag the release
140141
const commitMessage = `${module.getReleaseTag()}\n\n${prTitle}\n\n${prBody}`.trim();
141142
const gitPath = await which('git');
143+
const githubActionsBotEmail = await getGitHubActionsBotEmail();
142144

143145
// Execute git commands in temp directory without inheriting stdio to avoid output pollution
144146
const gitOpts: ExecSyncOptions = { cwd: tmpDir };
145147

146148
for (const cmd of [
147149
['config', '--local', 'user.name', GITHUB_ACTIONS_BOT_NAME],
148-
['config', '--local', 'user.email', GITHUB_ACTIONS_BOT_EMAIL],
150+
['config', '--local', 'user.email', githubActionsBotEmail],
149151
['add', '.'],
150152
['commit', '-m', commitMessage.trim()],
151153
['tag', releaseTag],

src/utils/constants.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,8 @@ export const WIKI_HOME_FILENAME = 'Home.md';
4646
export const WIKI_SIDEBAR_FILENAME = '_Sidebar.md';
4747
export const WIKI_FOOTER_FILENAME = '_Footer.md';
4848

49-
export const GITHUB_ACTIONS_BOT_USER_ID = 41898282;
5049
export const GITHUB_ACTIONS_BOT_NAME = 'GitHub Actions';
51-
export const GITHUB_ACTIONS_BOT_EMAIL = '41898282+github-actions[bot]@users.noreply.github.com';
50+
export const GITHUB_ACTIONS_BOT_USERNAME = 'github-actions[bot]';
5251

5352
export const PR_SUMMARY_MARKER = '<!-- techpivot/terraform-module-releaser — pr-summary-marker -->';
5453
export const PR_RELEASE_MARKER = '<!-- techpivot/terraform-module-releaser — release-marker -->';

src/utils/github.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { context } from '@/context';
2+
import { GITHUB_ACTIONS_BOT_USERNAME } from '@/utils/constants';
3+
4+
/**
5+
* Retrieves the GitHub Actions bot email address dynamically by querying the GitHub API.
6+
* This function handles both GitHub.com and GitHub Enterprise Server environments.
7+
*
8+
* The email format follows GitHub's standard: {user_id}+{username}@users.noreply.github.com
9+
*
10+
* @returns {Promise<string>} The GitHub Actions bot email address
11+
* @throws {Error} If the API request fails or the user information cannot be retrieved
12+
*/
13+
export async function getGitHubActionsBotEmail(): Promise<string> {
14+
const response = await context.octokit.rest.users.getByUsername({
15+
username: GITHUB_ACTIONS_BOT_USERNAME,
16+
});
17+
18+
return `${response.data.id}+${GITHUB_ACTIONS_BOT_USERNAME}@users.noreply.github.com`;
19+
}

src/wiki.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { TerraformModule } from '@/terraform-module';
1212
import type { ExecSyncError, WikiStatusResult } from '@/types';
1313
import {
1414
BRANDING_WIKI,
15-
GITHUB_ACTIONS_BOT_EMAIL,
1615
GITHUB_ACTIONS_BOT_NAME,
1716
PROJECT_URL,
1817
WIKI_FOOTER_FILENAME,
@@ -22,6 +21,7 @@ import {
2221
WIKI_TITLE_REPLACEMENTS,
2322
} from '@/utils/constants';
2423
import { removeDirectoryContents } from '@/utils/file';
24+
import { getGitHubActionsBotEmail } from '@/utils/github';
2525
import { endGroup, info, startGroup } from '@actions/core';
2626
import pLimit from 'p-limit';
2727
import which from 'which';
@@ -558,9 +558,9 @@ export async function generateWikiFiles(terraformModules: TerraformModule[]): Pr
558558
* pushes them to the remote wiki repository. If no changes are detected, it logs a
559559
* message and exits gracefully without creating a commit.
560560
*
561-
* @returns {void}
561+
* @returns {Promise<void>}
562562
*/
563-
export function commitAndPushWikiChanges(): void {
563+
export async function commitAndPushWikiChanges(): Promise<void> {
564564
startGroup('Committing and pushing changes to wiki');
565565

566566
try {
@@ -583,9 +583,11 @@ export function commitAndPushWikiChanges(): void {
583583

584584
if (status !== null && status.toString().trim() !== '') {
585585
// There are changes, commit and push
586+
const githubActionsBotEmail = await getGitHubActionsBotEmail();
587+
586588
for (const cmd of [
587589
['config', '--local', 'user.name', GITHUB_ACTIONS_BOT_NAME],
588-
['config', '--local', 'user.email', GITHUB_ACTIONS_BOT_EMAIL],
590+
['config', '--local', 'user.email', githubActionsBotEmail],
589591
['add', '.'],
590592
['commit', '-m', commitMessage.trim()],
591593
['push', 'origin'],

0 commit comments

Comments
 (0)