Skip to content

Commit 4d338c2

Browse files
committed
feat: support prod and staging GitHub Apps
1 parent 20e83fc commit 4d338c2

File tree

5 files changed

+174
-50
lines changed

5 files changed

+174
-50
lines changed

.gitleaksignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
/github/workspace/src/auth.ts:generic-api-key:8
1+
/github/workspace/src/auth.ts:generic-api-key:16
2+
/github/workspace/src/auth.ts:generic-api-key:22

__tests__/auth.test.ts

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
authenticate,
3-
GITHUB_AUTH_API_KEY,
4-
GITHUB_AUTH_SERVICE_URL
5-
} from '../src/auth'
1+
import { authenticate, PRODUCTION_SERVICE, STAGING_SERVICE } from '../src/auth'
62
import { jest } from '@jest/globals'
73

84
describe('authenticate', () => {
@@ -13,24 +9,54 @@ describe('authenticate', () => {
139
mockFetch.mockClear()
1410
})
1511

16-
it('should use GitHub App authentication when app is installed', async () => {
12+
it('should use production GitHub App when installed', async () => {
1713
mockFetch.mockResolvedValue({
1814
ok: true,
1915
status: 200,
20-
json: async () => Promise.resolve({ token: 'ghs_app_123' })
16+
json: async () => Promise.resolve({ token: 'ghs_prod_token' })
2117
} as Response)
2218

2319
const result = await authenticate(
2420
{ owner: 'dunder-mifflin', repo: 'website' },
2521
'ghp_default_token'
2622
)
2723

28-
expect(result).toBe('ghs_app_123')
24+
expect(result).toBe('ghs_prod_token')
25+
expect(mockFetch).toHaveBeenCalledTimes(1)
2926
expect(mockFetch).toHaveBeenCalledWith(
30-
`${GITHUB_AUTH_SERVICE_URL}/github/dunder-mifflin/website/installation-token`,
27+
`${PRODUCTION_SERVICE.url}/github/dunder-mifflin/website/installation-token`,
3128
expect.objectContaining({
3229
method: 'POST',
33-
headers: { Authorization: `Bearer ${GITHUB_AUTH_API_KEY}` }
30+
headers: { Authorization: `Bearer ${PRODUCTION_SERVICE.key}` }
31+
})
32+
)
33+
})
34+
35+
it('should use staging GitHub App when production not installed', async () => {
36+
mockFetch
37+
.mockResolvedValueOnce({
38+
ok: false,
39+
status: 404,
40+
json: async () => ({ error: 'Not installed' })
41+
} as Response)
42+
.mockResolvedValueOnce({
43+
ok: true,
44+
status: 200,
45+
json: async () => ({ token: 'ghs_staging_token' })
46+
} as Response)
47+
48+
const result = await authenticate(
49+
{ owner: 'dunder-mifflin', repo: 'website' },
50+
'ghp_default_token'
51+
)
52+
53+
expect(result).toBe('ghs_staging_token')
54+
expect(mockFetch).toHaveBeenCalledTimes(2)
55+
expect(mockFetch).toHaveBeenCalledWith(
56+
`${STAGING_SERVICE.url}/github/dunder-mifflin/website/installation-token`,
57+
expect.objectContaining({
58+
method: 'POST',
59+
headers: { Authorization: `Bearer ${STAGING_SERVICE.key}` }
3460
})
3561
)
3662
})
@@ -48,6 +74,7 @@ describe('authenticate', () => {
4874
)
4975

5076
expect(result).toBe('ghp_default_token')
77+
expect(mockFetch).toHaveBeenCalledTimes(2)
5178
})
5279

5380
it('should fall back to standard authentication when service is unavailable', async () => {
@@ -65,11 +92,54 @@ describe('authenticate', () => {
6592
const customPAT = 'ghp_custom_pat'
6693

6794
const result = await authenticate(
68-
{ owner: 'owner', repo: 'repo' },
95+
{ owner: 'dunder-mifflin', repo: 'website' },
6996
customPAT
7097
)
7198

7299
expect(result).toBe(customPAT)
73100
expect(mockFetch).not.toHaveBeenCalled()
74101
})
102+
103+
it('should handle all services returning errors', async () => {
104+
mockFetch
105+
.mockResolvedValueOnce({
106+
ok: false,
107+
status: 401,
108+
json: async () => ({ error: 'Unauthorized' })
109+
} as Response)
110+
.mockResolvedValueOnce({
111+
ok: false,
112+
status: 500,
113+
json: async () => ({ error: 'Server error' })
114+
} as Response)
115+
116+
const result = await authenticate(
117+
{ owner: 'dunder-mifflin', repo: 'website' },
118+
'ghp_default_token'
119+
)
120+
121+
expect(result).toBe('ghp_default_token')
122+
expect(mockFetch).toHaveBeenCalledTimes(2)
123+
})
124+
125+
it('should handle unexpected status codes', async () => {
126+
mockFetch
127+
.mockResolvedValueOnce({
128+
ok: false,
129+
status: 403,
130+
json: async () => ({ error: 'Forbidden' })
131+
} as Response)
132+
.mockResolvedValueOnce({
133+
ok: false,
134+
status: 403,
135+
json: async () => ({ error: 'Forbidden' })
136+
} as Response)
137+
138+
const result = await authenticate(
139+
{ owner: 'dunder-mifflin', repo: 'website' },
140+
'ghp_default_token'
141+
)
142+
143+
expect(result).toBe('ghp_default_token')
144+
})
75145
})

dist/index.js

Lines changed: 36 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/auth.ts

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
import * as core from '@actions/core'
22
import type { Context } from '@actions/github/lib/context'
33

4-
// TODO: check production URL?
5-
export const GITHUB_AUTH_SERVICE_URL =
6-
'https://github-auth.staging.code-pushup.dev'
7-
8-
export const GITHUB_AUTH_API_KEY =
9-
'18850f2513adad10662e85f4f085a9714e64cef7793fc2ffe903b5ddcd62de42'
10-
114
type TokenResponse = {
125
token: string
136
}
147

8+
type AuthService = {
9+
url: string
10+
key: string
11+
name: string
12+
}
13+
14+
export const STAGING_SERVICE: AuthService = {
15+
url: 'https://github-auth.staging.code-pushup.dev',
16+
key: '18850f2513adad10662e85f4f085a9714e64cef7793fc2ffe903b5ddcd62de42',
17+
name: 'Code PushUp (staging)'
18+
}
19+
20+
export const PRODUCTION_SERVICE: AuthService = {
21+
url: 'https://github-auth.code-pushup.dev',
22+
key: 'b2585352366ceead1323a1f3a7cf6b9212387ea6d2d8aeb397e7950aaa3ba776',
23+
name: 'Code PushUp'
24+
}
25+
1526
export async function authenticate(
1627
{ owner, repo }: Context['repo'],
1728
token: string
@@ -20,29 +31,47 @@ export async function authenticate(
2031
core.info('Using user-provided PAT')
2132
return token
2233
}
34+
const productionResult = await tryService(PRODUCTION_SERVICE, owner, repo)
35+
if (productionResult) {
36+
core.info(`Using ${PRODUCTION_SERVICE.name} GitHub App installation token`)
37+
return productionResult.token
38+
}
39+
const stagingResult = await tryService(STAGING_SERVICE, owner, repo)
40+
if (stagingResult) {
41+
core.info(`Using ${STAGING_SERVICE.name} GitHub App installation token`)
42+
return stagingResult.token
43+
}
44+
core.info('Using default GITHUB_TOKEN')
45+
return token
46+
}
47+
48+
async function tryService(
49+
service: AuthService,
50+
owner: string,
51+
repo: string
52+
): Promise<{ token: string; service: AuthService } | null> {
2353
try {
2454
const response = await fetch(
25-
`${GITHUB_AUTH_SERVICE_URL}/github/${owner}/${repo}/installation-token`,
55+
`${service.url}/github/${owner}/${repo}/installation-token`,
2656
{
2757
method: 'POST',
2858
headers: {
29-
Authorization: `Bearer ${GITHUB_AUTH_API_KEY}`
59+
Authorization: `Bearer ${service.key}`
3060
}
3161
}
3262
)
3363
const data = await response.json()
3464
if (response.ok && isTokenResponse(data)) {
35-
core.info('Using Code PushUp GitHub App installation token')
36-
return data.token
65+
return { token: data.token, service }
3766
}
38-
handleErrorResponse(response.status)
67+
handleErrorResponse(response.status, service.name)
3968
} catch (error) {
40-
core.warning(
41-
`Unable to contact Code PushUp authentication service: ${error}`
69+
core.debug(
70+
`Unable to contact ${service.name} authentication service: ${error}`
4271
)
72+
return null
4373
}
44-
core.info('Using default GITHUB_TOKEN')
45-
return token
74+
return null
4675
}
4776

4877
function isTokenResponse(res: unknown): res is TokenResponse {
@@ -54,18 +83,22 @@ function isTokenResponse(res: unknown): res is TokenResponse {
5483
)
5584
}
5685

57-
function handleErrorResponse(status: number): void {
86+
function handleErrorResponse(status: number, serviceName: string): void {
5887
switch (status) {
5988
case 404:
60-
core.debug('Code PushUp GitHub App not installed on this repository')
89+
core.debug(`${serviceName} GitHub App not installed on this repository`)
6190
break
6291
case 401:
63-
core.warning('Code PushUp authentication service authorization failed')
92+
core.warning(`${serviceName} authentication service authorization failed`)
6493
break
6594
case 500:
66-
core.warning('Code PushUp authentication service temporarily unavailable')
95+
core.warning(
96+
`${serviceName} authentication service temporarily unavailable`
97+
)
6798
break
6899
default:
69-
core.debug(`Code PushUp authentication service returned status ${status}`)
100+
core.warning(
101+
`${serviceName} authentication service returned unexpected status: ${status}`
102+
)
70103
}
71104
}

0 commit comments

Comments
 (0)