@@ -28,6 +28,7 @@ import (
28
28
"io"
29
29
"net/http"
30
30
"strings"
31
+ "sync"
31
32
"time"
32
33
33
34
"golang.org/x/oauth2"
@@ -42,6 +43,9 @@ type App struct {
42
43
appID string
43
44
privateKey * rsa.PrivateKey
44
45
46
+ installationCache map [string ](func () (* AppInstallation , error ))
47
+ installationCacheLock sync.Mutex
48
+
45
49
baseURL string
46
50
httpClient * http.Client
47
51
}
@@ -99,8 +103,9 @@ func NewApp[T *rsa.PrivateKey | string | []byte](appID string, privateKeyT T, op
99
103
}
100
104
101
105
app := & App {
102
- appID : appID ,
103
- privateKey : privateKey ,
106
+ appID : appID ,
107
+ privateKey : privateKey ,
108
+ installationCache : make (map [string ](func () (* AppInstallation , error )), 8 ),
104
109
105
110
baseURL : defaultBaseURL ,
106
111
httpClient : & http.Client {
@@ -200,58 +205,81 @@ func (i *AppInstallation) App() *App {
200
205
201
206
// InstallationForID returns an AccessTokensURLFunc that gets the access token
202
207
// url for the given installation.
208
+ //
209
+ // The initial invocation will make an API call to GitHub to get the access
210
+ // token URL for the installation; future calls will return the cached
211
+ // installation.
203
212
func (a * App ) InstallationForID (ctx context.Context , installationID string ) (* AppInstallation , error ) {
204
- u , err := a .accessTokenURL (ctx , fmt . Sprintf ( "%s /app/installations/%s" , a . baseURL , installationID ))
213
+ i , err := a .withInstallationCaching (ctx , "i:" + installationID , " /app/installations/" + installationID )( )
205
214
if err != nil {
206
215
return nil , fmt .Errorf ("failed to get access token url for installation %s: %w" , installationID , err )
207
216
}
208
-
209
- return & AppInstallation {
210
- app : a ,
211
- accessTokenURL : u ,
212
- }, nil
217
+ return i , nil
213
218
}
214
219
215
220
// InstallationForOrg returns an AccessTokensURLFunc that gets the access token url for the
216
221
// given org context.
222
+ //
223
+ // The initial invocation will make an API call to GitHub to get the access
224
+ // token URL for the installation; future calls will return the cached
225
+ // installation.
217
226
func (a * App ) InstallationForOrg (ctx context.Context , org string ) (* AppInstallation , error ) {
218
- u , err := a .accessTokenURL (ctx , fmt . Sprintf ( "%s /orgs/%s /installation", a . baseURL , org ) )
227
+ i , err := a .withInstallationCaching (ctx , "org:" + org , " /orgs/" + org + " /installation")( )
219
228
if err != nil {
220
229
return nil , fmt .Errorf ("failed to get access token url for org %s: %w" , org , err )
221
230
}
222
-
223
- return & AppInstallation {
224
- app : a ,
225
- accessTokenURL : u ,
226
- }, nil
231
+ return i , nil
227
232
}
228
233
229
234
// InstallationForRepo returns an AccessTokensURLFunc that gets the access token url for the
230
235
// given repo context.
236
+ //
237
+ // The initial invocation will make an API call to GitHub to get the access
238
+ // token URL for the installation; future calls will return the cached
239
+ // installation.
231
240
func (a * App ) InstallationForRepo (ctx context.Context , org , repo string ) (* AppInstallation , error ) {
232
- u , err := a .accessTokenURL (ctx , fmt . Sprintf ( "%s/ repos/%s/%s /installation", a . baseURL , org , repo ) )
241
+ i , err := a .withInstallationCaching (ctx , "repo:" + org + "/" + repo , "/ repos/" + org + "/" + repo + " /installation")( )
233
242
if err != nil {
234
243
return nil , fmt .Errorf ("failed to get access token url for repo %s/%s: %w" , org , repo , err )
235
244
}
236
-
237
- return & AppInstallation {
238
- app : a ,
239
- accessTokenURL : u ,
240
- }, nil
245
+ return i , nil
241
246
}
242
247
243
248
// InstallationForUser returns an AccessTokensURLFunc that gets the access token url for the
244
249
// given user context.
250
+ //
251
+ // The initial invocation will make an API call to GitHub to get the access
252
+ // token URL for the installation; future calls will return the cached
253
+ // installation.
245
254
func (a * App ) InstallationForUser (ctx context.Context , user string ) (* AppInstallation , error ) {
246
- u , err := a .accessTokenURL (ctx , fmt . Sprintf ( "%s /users/%s /installation", a . baseURL , user ) )
255
+ i , err := a .withInstallationCaching (ctx , "user:" + user , " /users/" + user + " /installation")( )
247
256
if err != nil {
248
257
return nil , fmt .Errorf ("failed to get access token url for user %s: %w" , user , err )
249
258
}
259
+ return i , nil
260
+ }
250
261
251
- return & AppInstallation {
252
- app : a ,
253
- accessTokenURL : u ,
254
- }, nil
262
+ // withInstallationCaching returns a closure that caches the app installation by
263
+ // key.
264
+ func (a * App ) withInstallationCaching (ctx context.Context , cacheKey , tokenPath string ) func () (* AppInstallation , error ) {
265
+ a .installationCacheLock .Lock ()
266
+ defer a .installationCacheLock .Unlock ()
267
+
268
+ entry , ok := a .installationCache [cacheKey ]
269
+ if ! ok {
270
+ entry = sync .OnceValues (func () (* AppInstallation , error ) {
271
+ u , err := a .accessTokenURL (ctx , a .baseURL + tokenPath )
272
+ if err != nil {
273
+ return nil , err
274
+ }
275
+
276
+ return & AppInstallation {
277
+ app : a ,
278
+ accessTokenURL : u ,
279
+ }, nil
280
+ })
281
+ }
282
+ return entry
255
283
}
256
284
257
285
// AccessToken calls the GitHub API to generate a new access token for this
0 commit comments