@@ -2,13 +2,19 @@ import { createAppAuth } from '@octokit/auth-app';
22import { StrategyOptions } from '@octokit/auth-app/dist-types/types' ;
33import { request } from '@octokit/request' ;
44import { RequestInterface , RequestParameters } from '@octokit/types' ;
5- import { getParameters } from '@aws-github-runner/aws-ssm-util' ;
5+ import {
6+ getGitHubAppCredentialsStore ,
7+ type GitHubAppCredential ,
8+ type GitHubAppCredentialsStore ,
9+ } from '@aws-github-runner/storage-providers' ;
610import { generateKeyPairSync } from 'node:crypto' ;
711import * as nock from 'nock' ;
812
913import {
1014 createGithubAppAuth ,
1115 createOctokitClient ,
16+ getAppCount ,
17+ getAppId ,
1218 getStoredInstallationId ,
1319 onRateLimit ,
1420 onSecondaryRateLimit ,
@@ -25,24 +31,27 @@ type MockProxy<T> = T & {
2531// eslint-disable-next-line @typescript-eslint/no-explicit-any
2632const mock = < T > ( implementation ?: any ) : MockProxy < T > => vi . fn ( implementation ) as any ;
2733
28- vi . mock ( '@aws-github-runner/aws-ssm-util' ) ;
34+ vi . mock ( '@aws-github-runner/storage-providers' , ( ) => ( {
35+ getGitHubAppCredentialsStore : vi . fn ( ) ,
36+ } ) ) ;
2937vi . mock ( '@octokit/auth-app' ) ;
3038
3139const cleanEnv = process . env ;
32- const ENVIRONMENT = 'dev' ;
33- const GITHUB_APP_ID = '1' ;
34- const PARAMETER_GITHUB_APP_ID_NAME = `/actions-runner/${ ENVIRONMENT } /github_app_id` ;
35- const PARAMETER_GITHUB_APP_KEY_BASE64_NAME = `/actions-runner/${ ENVIRONMENT } /github_app_key_base64` ;
40+ const GITHUB_APP_ID = 1 ;
3641
37- const mockedGetParameters = vi . mocked ( getParameters ) ;
42+ const mockedGetGitHubAppCredentialsStore = vi . mocked ( getGitHubAppCredentialsStore ) ;
43+ const mockCredentialsGet = vi . fn < GitHubAppCredentialsStore [ 'get' ] > ( ) ;
44+ const credentialsStore = {
45+ get : mockCredentialsGet ,
46+ } satisfies GitHubAppCredentialsStore ;
3847
3948beforeEach ( ( ) => {
4049 vi . resetModules ( ) ;
4150 vi . clearAllMocks ( ) ;
51+ mockCredentialsGet . mockReset ( ) ;
4252 resetAppCredentialsCache ( ) ;
4353 process . env = { ...cleanEnv } ;
44- process . env . PARAMETER_GITHUB_APP_ID_NAME = PARAMETER_GITHUB_APP_ID_NAME ;
45- process . env . PARAMETER_GITHUB_APP_KEY_BASE64_NAME = PARAMETER_GITHUB_APP_KEY_BASE64_NAME ;
54+ mockedGetGitHubAppCredentialsStore . mockReturnValue ( credentialsStore ) ;
4655 nock . disableNetConnect ( ) ;
4756} ) ;
4857
@@ -80,38 +89,18 @@ describe('Test createGithubAppAuth', () => {
8089 const authType = 'app' ;
8190 const token = '123456' ;
8291 const decryptedValue = 'decryptedValue' ;
83- const b64 = Buffer . from ( decryptedValue , 'binary' ) . toString ( 'base64' ) ;
84-
85- beforeEach ( ( ) => {
86- process . env . ENVIRONMENT = ENVIRONMENT ;
87- } ) ;
8892
89- it ( 'Throws early when PARAMETER_GITHUB_APP_ID_NAME is not set' , async ( ) => {
90- delete process . env . PARAMETER_GITHUB_APP_ID_NAME ;
93+ it ( 'Propagates errors from the credential store' , async ( ) => {
94+ const error = new Error ( 'Unable to load GitHub App credentials' ) ;
95+ mockCredentialsGet . mockRejectedValueOnce ( error ) ;
9196
92- await expect ( createGithubAppAuth ( installationId ) ) . rejects . toThrow (
93- 'Environment variable PARAMETER_GITHUB_APP_ID_NAME is not set' ,
94- ) ;
95- expect ( mockedGetParameters ) . not . toHaveBeenCalled ( ) ;
96- } ) ;
97-
98- it ( 'Throws early when PARAMETER_GITHUB_APP_KEY_BASE64_NAME is not set' , async ( ) => {
99- delete process . env . PARAMETER_GITHUB_APP_KEY_BASE64_NAME ;
100-
101- await expect ( createGithubAppAuth ( installationId ) ) . rejects . toThrow (
102- 'Environment variable PARAMETER_GITHUB_APP_KEY_BASE64_NAME is not set' ,
103- ) ;
104- expect ( mockedGetParameters ) . not . toHaveBeenCalled ( ) ;
97+ await expect ( createGithubAppAuth ( installationId ) ) . rejects . toBe ( error ) ;
98+ expect ( mockCredentialsGet ) . toHaveBeenCalledOnce ( ) ;
10599 } ) ;
106100
107101 it ( 'Creates auth object with createJwt callback including jti claim' , async ( ) => {
108102 // Arrange
109- mockedGetParameters . mockResolvedValueOnce (
110- new Map ( [
111- [ PARAMETER_GITHUB_APP_ID_NAME , GITHUB_APP_ID ] ,
112- [ PARAMETER_GITHUB_APP_KEY_BASE64_NAME , b64 ] ,
113- ] ) ,
114- ) ;
103+ mockCredentialsGet . mockResolvedValueOnce ( [ { appId : GITHUB_APP_ID , privateKey : decryptedValue } ] ) ;
115104
116105 const mockedAuth = vi . fn ( ) ;
117106 mockedAuth . mockResolvedValue ( { token } ) ;
@@ -124,7 +113,7 @@ describe('Test createGithubAppAuth', () => {
124113 // Assert
125114 expect ( mockedCreatAppAuth ) . toBeCalledTimes ( 1 ) ;
126115 const callArgs = mockedCreatAppAuth . mock . calls [ 0 ] [ 0 ] as Record < string , unknown > ;
127- expect ( callArgs . appId ) . toBe ( parseInt ( GITHUB_APP_ID ) ) ;
116+ expect ( callArgs . appId ) . toBe ( GITHUB_APP_ID ) ;
128117 expect ( callArgs . createJwt ) . toBeTypeOf ( 'function' ) ;
129118 expect ( callArgs ) . not . toHaveProperty ( 'privateKey' ) ;
130119 expect ( callArgs . installationId ) . toBe ( installationId ) ;
@@ -137,14 +126,7 @@ describe('Test createGithubAppAuth', () => {
137126 privateKeyEncoding : { type : 'pkcs8' , format : 'pem' } ,
138127 publicKeyEncoding : { type : 'spki' , format : 'pem' } ,
139128 } ) ;
140- const b64Key = Buffer . from ( privateKey as string ) . toString ( 'base64' ) ;
141-
142- mockedGetParameters . mockResolvedValueOnce (
143- new Map ( [
144- [ PARAMETER_GITHUB_APP_ID_NAME , GITHUB_APP_ID ] ,
145- [ PARAMETER_GITHUB_APP_KEY_BASE64_NAME , b64Key ] ,
146- ] ) ,
147- ) ;
129+ mockCredentialsGet . mockResolvedValueOnce ( [ { appId : GITHUB_APP_ID , privateKey : privateKey as string } ] ) ;
148130
149131 let capturedCreateJwt : ( appId : string | number , timeDifference ?: number ) => Promise < { jwt : string } > ;
150132 mockedCreatAppAuth . mockImplementation ( ( opts : StrategyOptions ) => {
@@ -173,41 +155,9 @@ describe('Test createGithubAppAuth', () => {
173155 expect ( payload ) . toHaveProperty ( 'iss' ) ;
174156 } ) ;
175157
176- it ( 'Creates auth object with line breaks in SSH key.' , async ( ) => {
177- // Arrange
178- const b64PrivateKeyWithLineBreaks = Buffer . from ( decryptedValue + '\n' + decryptedValue , 'binary' ) . toString (
179- 'base64' ,
180- ) ;
181- mockedGetParameters . mockResolvedValueOnce (
182- new Map ( [
183- [ PARAMETER_GITHUB_APP_ID_NAME , GITHUB_APP_ID ] ,
184- [ PARAMETER_GITHUB_APP_KEY_BASE64_NAME , b64PrivateKeyWithLineBreaks ] ,
185- ] ) ,
186- ) ;
187-
188- const mockedAuth = vi . fn ( ) ;
189- mockedAuth . mockResolvedValue ( { token } ) ;
190- const mockWithHook = Object . assign ( mockedAuth , { hook : vi . fn ( ) } ) ;
191- mockedCreatAppAuth . mockReturnValue ( mockWithHook ) ;
192-
193- // Act
194- const result = await createGithubAppAuth ( installationId ) ;
195-
196- // Assert
197- expect ( getParameters ) . toBeCalledWith ( [ PARAMETER_GITHUB_APP_ID_NAME , PARAMETER_GITHUB_APP_KEY_BASE64_NAME ] ) ;
198- expect ( mockedCreatAppAuth ) . toBeCalledTimes ( 1 ) ;
199- expect ( mockedAuth ) . toBeCalledWith ( { type : authType } ) ;
200- expect ( result . token ) . toBe ( token ) ;
201- } ) ;
202-
203158 it ( 'Creates auth object for public GitHub' , async ( ) => {
204159 // Arrange
205- mockedGetParameters . mockResolvedValueOnce (
206- new Map ( [
207- [ PARAMETER_GITHUB_APP_ID_NAME , GITHUB_APP_ID ] ,
208- [ PARAMETER_GITHUB_APP_KEY_BASE64_NAME , b64 ] ,
209- ] ) ,
210- ) ;
160+ mockCredentialsGet . mockResolvedValueOnce ( [ { appId : GITHUB_APP_ID , privateKey : decryptedValue } ] ) ;
211161
212162 const mockedAuth = vi . fn ( ) ;
213163 mockedAuth . mockResolvedValue ( { token } ) ;
@@ -218,11 +168,9 @@ describe('Test createGithubAppAuth', () => {
218168 const result = await createGithubAppAuth ( installationId ) ;
219169
220170 // Assert
221- expect ( getParameters ) . toBeCalledWith ( [ PARAMETER_GITHUB_APP_ID_NAME , PARAMETER_GITHUB_APP_KEY_BASE64_NAME ] ) ;
222-
223171 expect ( mockedCreatAppAuth ) . toBeCalledTimes ( 1 ) ;
224172 const callArgs = mockedCreatAppAuth . mock . calls [ 0 ] [ 0 ] as Record < string , unknown > ;
225- expect ( callArgs . appId ) . toBe ( parseInt ( GITHUB_APP_ID ) ) ;
173+ expect ( callArgs . appId ) . toBe ( GITHUB_APP_ID ) ;
226174 expect ( callArgs . createJwt ) . toBeTypeOf ( 'function' ) ;
227175 expect ( callArgs . installationId ) . toBe ( installationId ) ;
228176 expect ( mockedAuth ) . toBeCalledWith ( { type : authType } ) ;
@@ -238,12 +186,7 @@ describe('Test createGithubAppAuth', () => {
238186 ( ) => mockedRequestInterface as RequestInterface < object & RequestParameters > ,
239187 ) ;
240188
241- mockedGetParameters . mockResolvedValueOnce (
242- new Map ( [
243- [ PARAMETER_GITHUB_APP_ID_NAME , GITHUB_APP_ID ] ,
244- [ PARAMETER_GITHUB_APP_KEY_BASE64_NAME , b64 ] ,
245- ] ) ,
246- ) ;
189+ mockCredentialsGet . mockResolvedValueOnce ( [ { appId : GITHUB_APP_ID , privateKey : decryptedValue } ] ) ;
247190 const mockedAuth = vi . fn ( ) ;
248191 mockedAuth . mockResolvedValue ( { token } ) ;
249192 // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -255,11 +198,9 @@ describe('Test createGithubAppAuth', () => {
255198 const result = await createGithubAppAuth ( installationId , githubServerUrl ) ;
256199
257200 // Assert
258- expect ( getParameters ) . toBeCalledWith ( [ PARAMETER_GITHUB_APP_ID_NAME , PARAMETER_GITHUB_APP_KEY_BASE64_NAME ] ) ;
259-
260201 expect ( mockedCreatAppAuth ) . toBeCalledTimes ( 1 ) ;
261202 const callArgs = mockedCreatAppAuth . mock . calls [ 0 ] [ 0 ] as Record < string , unknown > ;
262- expect ( callArgs . appId ) . toBe ( parseInt ( GITHUB_APP_ID ) ) ;
203+ expect ( callArgs . appId ) . toBe ( GITHUB_APP_ID ) ;
263204 expect ( callArgs . createJwt ) . toBeTypeOf ( 'function' ) ;
264205 expect ( callArgs . installationId ) . toBe ( installationId ) ;
265206 expect ( callArgs . request ) . toBeDefined ( ) ;
@@ -278,12 +219,7 @@ describe('Test createGithubAppAuth', () => {
278219
279220 const installationId = undefined ;
280221
281- mockedGetParameters . mockResolvedValueOnce (
282- new Map ( [
283- [ PARAMETER_GITHUB_APP_ID_NAME , GITHUB_APP_ID ] ,
284- [ PARAMETER_GITHUB_APP_KEY_BASE64_NAME , b64 ] ,
285- ] ) ,
286- ) ;
222+ mockCredentialsGet . mockResolvedValueOnce ( [ { appId : GITHUB_APP_ID , privateKey : decryptedValue } ] ) ;
287223 const mockedAuth = vi . fn ( ) ;
288224 mockedAuth . mockResolvedValue ( { token } ) ;
289225 const mockWithHook = Object . assign ( mockedAuth , { hook : vi . fn ( ) } ) ;
@@ -293,11 +229,9 @@ describe('Test createGithubAppAuth', () => {
293229 const result = await createGithubAppAuth ( installationId , githubServerUrl ) ;
294230
295231 // Assert
296- expect ( getParameters ) . toBeCalledWith ( [ PARAMETER_GITHUB_APP_ID_NAME , PARAMETER_GITHUB_APP_KEY_BASE64_NAME ] ) ;
297-
298232 expect ( mockedCreatAppAuth ) . toBeCalledTimes ( 1 ) ;
299233 const callArgs = mockedCreatAppAuth . mock . calls [ 0 ] [ 0 ] as Record < string , unknown > ;
300- expect ( callArgs . appId ) . toBe ( parseInt ( GITHUB_APP_ID ) ) ;
234+ expect ( callArgs . appId ) . toBe ( GITHUB_APP_ID ) ;
301235 expect ( callArgs . createJwt ) . toBeTypeOf ( 'function' ) ;
302236 expect ( callArgs ) . not . toHaveProperty ( 'installationId' ) ;
303237 expect ( callArgs . request ) . toBeDefined ( ) ;
@@ -330,98 +264,48 @@ describe('Test throttling retry caps', () => {
330264 } ) ;
331265} ) ;
332266
333- describe ( 'Test getStoredInstallationId' , ( ) => {
334- const decryptedValue = 'decryptedValue' ;
335- const b64 = Buffer . from ( decryptedValue , 'binary' ) . toString ( 'base64' ) ;
336-
337- beforeEach ( ( ) => {
338- const mockedAuth = vi . fn ( ) ;
339- mockedAuth . mockResolvedValue ( { token : 'token' } ) ;
340- const mockWithHook = Object . assign ( mockedAuth , { hook : vi . fn ( ) } ) ;
341- vi . mocked ( createAppAuth ) . mockReturnValue ( mockWithHook ) ;
342- } ) ;
343-
267+ describe ( 'Test GitHub App credential accessors' , ( ) => {
344268 it ( 'returns stored installation ID when configured' , async ( ) => {
345- const installationIdParam = `/actions-runner/${ ENVIRONMENT } /github_app_installation_id` ;
346- process . env . PARAMETER_GITHUB_APP_INSTALLATION_ID_NAME = installationIdParam ;
347- mockedGetParameters . mockResolvedValueOnce (
348- new Map ( [
349- [ PARAMETER_GITHUB_APP_ID_NAME , GITHUB_APP_ID ] ,
350- [ PARAMETER_GITHUB_APP_KEY_BASE64_NAME , b64 ] ,
351- [ installationIdParam , '12345' ] ,
352- ] ) ,
353- ) ;
269+ mockCredentialsGet . mockResolvedValueOnce ( [
270+ { appId : GITHUB_APP_ID , privateKey : 'private-key' , installationId : 12345 } ,
271+ ] ) ;
354272
355273 const result = await getStoredInstallationId ( 0 ) ;
356274 expect ( result ) . toBe ( 12345 ) ;
357275 } ) ;
358276
359- it ( 'returns undefined when installation ID param is empty' , async ( ) => {
360- process . env . PARAMETER_GITHUB_APP_INSTALLATION_ID_NAME = '' ;
361- mockedGetParameters . mockResolvedValueOnce (
362- new Map ( [
363- [ PARAMETER_GITHUB_APP_ID_NAME , GITHUB_APP_ID ] ,
364- [ PARAMETER_GITHUB_APP_KEY_BASE64_NAME , b64 ] ,
365- ] ) ,
366- ) ;
367-
368- const result = await getStoredInstallationId ( 0 ) ;
369- expect ( result ) . toBeUndefined ( ) ;
370- } ) ;
371-
372- it ( 'returns undefined when env var is not set' , async ( ) => {
373- delete process . env . PARAMETER_GITHUB_APP_INSTALLATION_ID_NAME ;
374- mockedGetParameters . mockResolvedValueOnce (
375- new Map ( [
376- [ PARAMETER_GITHUB_APP_ID_NAME , GITHUB_APP_ID ] ,
377- [ PARAMETER_GITHUB_APP_KEY_BASE64_NAME , b64 ] ,
378- ] ) ,
379- ) ;
277+ it ( 'returns undefined when the credential has no installation ID' , async ( ) => {
278+ mockCredentialsGet . mockResolvedValueOnce ( [ { appId : GITHUB_APP_ID , privateKey : 'private-key' } ] ) ;
380279
381280 const result = await getStoredInstallationId ( 0 ) ;
382281 expect ( result ) . toBeUndefined ( ) ;
383282 } ) ;
384283
385284 it ( 'returns undefined for out-of-bounds appIndex' , async ( ) => {
386- process . env . PARAMETER_GITHUB_APP_INSTALLATION_ID_NAME = '' ;
387- mockedGetParameters . mockResolvedValueOnce (
388- new Map ( [
389- [ PARAMETER_GITHUB_APP_ID_NAME , GITHUB_APP_ID ] ,
390- [ PARAMETER_GITHUB_APP_KEY_BASE64_NAME , b64 ] ,
391- ] ) ,
392- ) ;
285+ mockCredentialsGet . mockResolvedValueOnce ( [ { appId : GITHUB_APP_ID , privateKey : 'private-key' } ] ) ;
393286
394287 const result = await getStoredInstallationId ( 99 ) ;
395288 expect ( result ) . toBeUndefined ( ) ;
396289 } ) ;
397290
398- it ( 'loads installation IDs for multi-app setup' , async ( ) => {
399- const app1IdParam = `/actions-runner/${ ENVIRONMENT } /github_app_id` ;
400- const app2IdParam = `/actions-runner/${ ENVIRONMENT } /additional_github_app_0_id` ;
401- const app1KeyParam = `/actions-runner/${ ENVIRONMENT } /github_app_key_base64` ;
402- const app2KeyParam = `/actions-runner/${ ENVIRONMENT } /additional_github_app_0_key_base64` ;
403- const app2InstallParam = `/actions-runner/${ ENVIRONMENT } /additional_github_app_0_installation_id` ;
404-
405- process . env . PARAMETER_GITHUB_APP_ID_NAME = `${ app1IdParam } :${ app2IdParam } ` ;
406- process . env . PARAMETER_GITHUB_APP_KEY_BASE64_NAME = `${ app1KeyParam } :${ app2KeyParam } ` ;
407- process . env . PARAMETER_GITHUB_APP_INSTALLATION_ID_NAME = `:${ app2InstallParam } ` ;
408-
409- mockedGetParameters . mockResolvedValueOnce (
410- new Map ( [
411- [ app1IdParam , '1' ] ,
412- [ app1KeyParam , b64 ] ,
413- [ app2IdParam , '2' ] ,
414- [ app2KeyParam , b64 ] ,
415- [ app2InstallParam , '67890' ] ,
416- ] ) ,
417- ) ;
291+ it ( 'loads multi-app credentials once and exposes values by index' , async ( ) => {
292+ const credentials : GitHubAppCredential [ ] = [
293+ { appId : 1 , privateKey : 'private-key-1' } ,
294+ { appId : 2 , privateKey : 'private-key-2' , installationId : 67890 } ,
295+ ] ;
296+ mockCredentialsGet . mockResolvedValueOnce ( credentials ) ;
297+
298+ await expect ( getAppCount ( ) ) . resolves . toBe ( 2 ) ;
299+ await expect ( getAppId ( ) ) . resolves . toBe ( '1' ) ;
300+ await expect ( getAppId ( 1 ) ) . resolves . toBe ( '2' ) ;
301+ await expect ( getStoredInstallationId ( 0 ) ) . resolves . toBeUndefined ( ) ;
302+ await expect ( getStoredInstallationId ( 1 ) ) . resolves . toBe ( 67890 ) ;
303+ expect ( mockCredentialsGet ) . toHaveBeenCalledOnce ( ) ;
304+ } ) ;
418305
419- // Primary app (index 0) has no stored installation ID
420- const result0 = await getStoredInstallationId ( 0 ) ;
421- expect ( result0 ) . toBeUndefined ( ) ;
306+ it ( 'throws a clear error for an out-of-bounds app ID index' , async ( ) => {
307+ mockCredentialsGet . mockResolvedValueOnce ( [ { appId : GITHUB_APP_ID , privateKey : 'private-key' } ] ) ;
422308
423- // Additional app (index 1) has stored installation ID
424- const result1 = await getStoredInstallationId ( 1 ) ;
425- expect ( result1 ) . toBe ( 67890 ) ;
309+ await expect ( getAppId ( 99 ) ) . rejects . toThrow ( 'GitHub App credential at index 99 not found' ) ;
426310 } ) ;
427311} ) ;
0 commit comments