Skip to content

Commit 72247c8

Browse files
authored
feat: export git user environment variables (#1)
1 parent c234e63 commit 72247c8

File tree

5 files changed

+163
-10
lines changed

5 files changed

+163
-10
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ jobs:
5050
- `org` - *(optional)* The organization for an org-scoped token
5151
- `owner` - *(optional)* The repository owner for a repo-scoped token
5252
- `repo` - *(optional)* The repository name for a repo-scoped token
53+
- `export-git-user` - *(optional)* [Export environment variables][git-env-variables]
54+
which set the git user to the GitHub app user
5355

5456
### Outputs
5557

@@ -60,3 +62,4 @@ jobs:
6062
MIT
6163

6264
[generating-cred-bundle]: https://github.com/electron/github-app-auth#generating-credentials
65+
[git-env-variables]: https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables

__tests__/index.test.ts

+107
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import {
66
} from '@electron/github-app-auth'
77

88
import * as index from '../src/index'
9+
import { GitHub } from '@actions/github/lib/utils'
910

1011
jest.mock('@actions/core', () => {
1112
return {
13+
exportVariable: jest.fn(),
14+
getBooleanInput: jest.fn(),
1215
getInput: jest.fn(),
1316
getState: jest.fn(),
1417
info: jest.fn(),
@@ -28,15 +31,35 @@ jest.mock('@actions/github', () => {
2831
}
2932
}
3033
})
34+
jest.mock('@actions/github/lib/utils')
3135
jest.mock('@electron/github-app-auth')
3236

3337
jest
3438
.mocked(appCredentialsFromString)
3539
.mockReturnValue({ appId: '12345', privateKey: 'private' })
3640

41+
const getAuthenticated = jest.fn()
42+
const getByUsername = jest.fn()
43+
44+
;(GitHub as unknown as jest.Mock).mockReturnValue({
45+
rest: {
46+
apps: {
47+
getAuthenticated
48+
},
49+
users: {
50+
getByUsername
51+
}
52+
}
53+
})
54+
3755
// Spy the action's entrypoint
3856
const runSpy = jest.spyOn(index, 'run')
3957

58+
const slug = 'my-app'
59+
const userId = 12345
60+
const username = `${slug}[bot]`
61+
const email = `${userId}+${slug}[bot]@users.noreply.github.com`
62+
4063
describe('action', () => {
4164
beforeEach(() => {
4265
jest.clearAllMocks()
@@ -166,6 +189,49 @@ describe('action', () => {
166189
expect(core.saveState).toHaveBeenLastCalledWith('token', token)
167190
})
168191

192+
it('can export a git user with repo token', async () => {
193+
const token = 'repo-token'
194+
jest.mocked(core.getBooleanInput).mockReturnValue(true)
195+
jest.mocked(core.getInput).mockImplementation((name: string) => {
196+
switch (name) {
197+
case 'creds':
198+
return 'foobar'
199+
case 'owner':
200+
return 'electron'
201+
case 'repo':
202+
return 'fake-repo'
203+
default:
204+
return ''
205+
}
206+
})
207+
jest.mocked(getTokenForRepo).mockResolvedValue(token)
208+
jest.mocked(getAuthenticated).mockResolvedValue({ data: { slug } })
209+
jest.mocked(getByUsername).mockResolvedValue({
210+
data: {
211+
id: userId
212+
}
213+
})
214+
215+
await index.run()
216+
expect(runSpy).toHaveReturned()
217+
218+
// Exports git user environment variables
219+
expect(core.exportVariable).toHaveBeenCalledTimes(4)
220+
expect(core.exportVariable).toHaveBeenCalledWith(
221+
'GIT_AUTHOR_NAME',
222+
username
223+
)
224+
expect(core.exportVariable).toHaveBeenCalledWith('GIT_AUTHOR_EMAIL', email)
225+
expect(core.exportVariable).toHaveBeenCalledWith(
226+
'GIT_COMMITTER_NAME',
227+
username
228+
)
229+
expect(core.exportVariable).toHaveBeenCalledWith(
230+
'GIT_COMMITTER_EMAIL',
231+
email
232+
)
233+
})
234+
169235
it('generates an org token', async () => {
170236
const token = 'org-token'
171237
jest.mocked(core.getInput).mockImplementation((name: string) => {
@@ -202,6 +268,47 @@ describe('action', () => {
202268
expect(core.saveState).toHaveBeenLastCalledWith('token', token)
203269
})
204270

271+
it('can export a git user with org token', async () => {
272+
const token = 'org-token'
273+
jest.mocked(core.getBooleanInput).mockReturnValue(true)
274+
jest.mocked(core.getInput).mockImplementation((name: string) => {
275+
switch (name) {
276+
case 'creds':
277+
return 'foobar'
278+
case 'org':
279+
return 'electron'
280+
default:
281+
return ''
282+
}
283+
})
284+
jest.mocked(getTokenForOrg).mockResolvedValue(token)
285+
jest.mocked(getAuthenticated).mockResolvedValue({ data: { slug } })
286+
jest.mocked(getByUsername).mockResolvedValue({
287+
data: {
288+
id: userId
289+
}
290+
})
291+
292+
await index.run()
293+
expect(runSpy).toHaveReturned()
294+
295+
// Exports git user environment variables
296+
expect(core.exportVariable).toHaveBeenCalledTimes(4)
297+
expect(core.exportVariable).toHaveBeenCalledWith(
298+
'GIT_AUTHOR_NAME',
299+
username
300+
)
301+
expect(core.exportVariable).toHaveBeenCalledWith('GIT_AUTHOR_EMAIL', email)
302+
expect(core.exportVariable).toHaveBeenCalledWith(
303+
'GIT_COMMITTER_NAME',
304+
username
305+
)
306+
expect(core.exportVariable).toHaveBeenCalledWith(
307+
'GIT_COMMITTER_EMAIL',
308+
email
309+
)
310+
})
311+
205312
it('handles token generate failure', async () => {
206313
jest.mocked(core.getInput).mockImplementation((name: string) => {
207314
switch (name) {

action.yml

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ inputs:
1515
repo:
1616
description: 'The repo name for a repo-scoped token'
1717
required: false
18+
export-git-user:
19+
description: 'Export environment variables which set the git user to the GitHub app user'
20+
required: false
1821

1922
outputs:
2023
token:

dist/index.js

+26-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

+24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import * as core from '@actions/core'
22
import * as github from '@actions/github'
3+
import { GitHub } from '@actions/github/lib/utils'
34
import {
45
appCredentialsFromString,
6+
getAuthOptionsForOrg,
7+
getAuthOptionsForRepo,
58
getTokenForOrg,
69
getTokenForRepo
710
} from '@electron/github-app-auth'
@@ -24,6 +27,7 @@ export async function run(): Promise<void> {
2427
const org = core.getInput('org')
2528
let owner = core.getInput('owner')
2629
let repo = core.getInput('repo')
30+
const exportGitUser = core.getBooleanInput('export-git-user')
2731

2832
if (org && (owner || repo)) {
2933
core.setFailed('Invalid inputs')
@@ -54,6 +58,26 @@ export async function run(): Promise<void> {
5458

5559
// Save token to state so the post function can invalidate
5660
core.saveState('token', token)
61+
62+
if (exportGitUser) {
63+
const authOpts = await (org
64+
? getAuthOptionsForOrg(org, appCreds)
65+
: getAuthOptionsForRepo({ owner, name: repo }, appCreds))
66+
67+
const appOctokit = new GitHub({ ...authOpts })
68+
69+
const { data: app } = await appOctokit.rest.apps.getAuthenticated()
70+
const username = `${app.slug}[bot]`
71+
const { data: user } = await appOctokit.rest.users.getByUsername({
72+
username
73+
})
74+
const email = `${user.id}+${app.slug}[bot]@users.noreply.github.com`
75+
76+
core.exportVariable('GIT_AUTHOR_NAME', username)
77+
core.exportVariable('GIT_AUTHOR_EMAIL', email)
78+
core.exportVariable('GIT_COMMITTER_NAME', username)
79+
core.exportVariable('GIT_COMMITTER_EMAIL', email)
80+
}
5781
} catch (error) {
5882
// Fail the workflow run if an error occurs
5983
if (error instanceof Error) core.setFailed(error.message)

0 commit comments

Comments
 (0)