Skip to content

Commit 4ea5cda

Browse files
Shubham HibareShubham Hibare
authored andcommitted
handle pr comments
1 parent d0ad22b commit 4ea5cda

6 files changed

Lines changed: 194 additions & 79 deletions

File tree

internal/clients/github/client.go

Lines changed: 64 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"context"
2424
"crypto/rsa"
2525
"encoding/json"
26-
"errors"
2726
"fmt"
2827
"net/http"
2928
"net/url"
@@ -68,11 +67,16 @@ type Client struct {
6867
httpClient *http.Client
6968
installations *installationCache
7069
installationSF singleflight.Group
71-
contentsTokens *tokenCache
70+
tokens *tokenCache
7271
tokenMintSF singleflight.Group
7372
tokenReadyDelay time.Duration
7473
}
7574

75+
// mintOpts controls mintToken behavior.
76+
type mintOpts struct {
77+
skipCache bool // skip cache and mint a fresh token
78+
}
79+
7680
// ghClient returns a go-github client authenticated with the given token.
7781
func (c *Client) ghClient(token string) (*gogithub.Client, error) {
7882
gh := gogithub.NewClient(c.httpClient).WithAuthToken(token)
@@ -92,7 +96,7 @@ func (c *Client) RequestToken(ctx context.Context, req *TokenRequest) (*TokenRes
9296
return nil, err
9397
}
9498

95-
id, err := c.getInstallationID(ctx, owner, repo)
99+
id, err := c.getInstallationID(ctx, owner)
96100
if err != nil {
97101
return nil, fmt.Errorf("getting installation ID: %w", err)
98102
}
@@ -148,9 +152,12 @@ func (c *Client) GetContents(ctx context.Context, repository string, path string
148152
return nil, err
149153
}
150154

151-
// Retry once: if a cached token yields 401/403, evict it and mint a fresh one.
155+
perms := map[string]string{"contents": "read"}
156+
var opts *mintOpts
157+
158+
// Retry once on 401/403 in case the token was stale; the second attempt mints fresh.
152159
for attempt := range 2 {
153-
tok, cached, err := c.contentsToken(ctx, repository, owner, repo)
160+
tok, err := c.mintToken(ctx, owner, "", perms, opts)
154161
if err != nil {
155162
return nil, err
156163
}
@@ -162,8 +169,8 @@ func (c *Client) GetContents(ctx context.Context, repository string, path string
162169

163170
fileContent, _, resp, err := gh.Repositories.GetContents(ctx, owner, repo, path, nil)
164171
if err != nil {
165-
if attempt == 0 && cached && resp != nil && (resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden) {
166-
c.contentsTokens.delete(repository)
172+
if attempt == 0 && resp != nil && (resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden) {
173+
opts = &mintOpts{skipCache: true}
167174
continue
168175
}
169176
if resp != nil && resp.StatusCode == http.StatusNotFound {
@@ -181,44 +188,41 @@ func (c *Client) GetContents(ctx context.Context, repository string, path string
181188
}
182189
return []byte(content), nil
183190
}
184-
return nil, errors.New("unexpected: exhausted GetContents retry")
191+
return nil, fmt.Errorf("unexpected: exhausted GetContents retries for %s/%s", repository, path)
185192
}
186193

187-
// contentsToken returns a repo-scoped contents:read token, serving from
188-
// cache when possible. On a cache miss, concurrent callers for the same
189-
// repository are coalesced so only one mint + replication delay occurs.
190-
// The bool indicates whether the token was served from cache.
191-
func (c *Client) contentsToken(ctx context.Context, repository, owner, repo string) (string, bool, error) {
192-
if tok := c.contentsTokens.get(repository); tok != "" {
193-
return tok, true, nil
194+
// mintToken returns a cached or freshly minted installation token.
195+
// When repo is non-empty the token is scoped to that single repository;
196+
// when empty the token covers every repo the installation can access.
197+
// Tokens are keyed by owner[/repo]+permissions.
198+
func (c *Client) mintToken(ctx context.Context, owner, repo string, perms map[string]string, opts *mintOpts) (string, error) {
199+
key := owner
200+
if repo != "" {
201+
key = owner + "/" + repo
194202
}
203+
key += "|" + permissionsKey(perms)
195204

196-
perms := map[string]string{"contents": "read"}
197-
tok, err := c.mintToken(ctx, repository, owner, repo, perms, c.contentsTokens)
198-
if err != nil {
199-
return "", false, err
200-
}
201-
return tok, false, nil
202-
}
205+
skipCache := opts != nil && opts.skipCache
203206

204-
// mintToken creates a new installation token with the given permissions,
205-
// waits for edge replication, and stores the result in cache. Concurrent
206-
// calls for the same repository+permissions share a single in-flight
207-
// mint via singleflight.
208-
func (c *Client) mintToken(ctx context.Context, repository, owner, repo string, perms map[string]string, cache *tokenCache) (string, error) {
209-
sfKey := repository + "|" + permissionsKey(perms)
207+
if skipCache {
208+
c.tokens.delete(key)
209+
} else if tok := c.tokens.get(key); tok != "" {
210+
return tok, nil
211+
}
210212

211-
v, err, _ := c.tokenMintSF.Do(sfKey, func() (any, error) {
212-
if tok := cache.get(repository); tok != "" {
213-
return tok, nil
213+
v, err, _ := c.tokenMintSF.Do(key, func() (any, error) {
214+
if !skipCache {
215+
if tok := c.tokens.get(key); tok != "" {
216+
return tok, nil
217+
}
214218
}
215219

216-
id, err := c.getInstallationID(ctx, owner, repo)
220+
id, err := c.getInstallationID(ctx, owner)
217221
if err != nil {
218222
return nil, fmt.Errorf("getting installation ID: %w", err)
219223
}
220224

221-
token, err := c.createInstallationToken(ctx, id, repo, perms)
225+
resp, err := c.createInstallationToken(ctx, id, repo, perms)
222226
if err != nil {
223227
return nil, fmt.Errorf("minting installation token: %w", err)
224228
}
@@ -227,27 +231,24 @@ func (c *Client) mintToken(ctx context.Context, repository, owner, repo string,
227231
return nil, err
228232
}
229233

230-
cache.set(repository, token.Token, token.ExpiresAt)
231-
return token.Token, nil
234+
c.tokens.set(key, resp.Token, resp.ExpiresAt)
235+
return resp.Token, nil
232236
})
233237
if err != nil {
234238
return "", err
235239
}
236240
return v.(string), nil //nolint:errcheck // singleflight guarantees string on nil error
237241
}
238242

239-
// getInstallationID returns the app installation ID for owner. Installations
240-
// are per-account, so cache and singleflight are keyed by owner; repo is
241-
// only used for the API lookup on cache miss.
242-
func (c *Client) getInstallationID(ctx context.Context, owner, repo string) (int64, error) {
243-
key := owner
244-
245-
if id := c.installations.get(key); id != 0 {
243+
// getInstallationID returns the app installation ID for the given owner
244+
// (organization or user account). Cache and singleflight are keyed by owner.
245+
func (c *Client) getInstallationID(ctx context.Context, owner string) (int64, error) {
246+
if id := c.installations.get(owner); id != 0 {
246247
return id, nil
247248
}
248249

249-
v, err, _ := c.installationSF.Do(key, func() (any, error) {
250-
if id := c.installations.get(key); id != 0 {
250+
v, err, _ := c.installationSF.Do(owner, func() (any, error) {
251+
if id := c.installations.get(owner); id != 0 {
251252
return id, nil
252253
}
253254

@@ -261,15 +262,19 @@ func (c *Client) getInstallationID(ctx context.Context, owner, repo string) (int
261262
return nil, err
262263
}
263264

264-
installation, resp, err := gh.Apps.FindRepositoryInstallation(ctx, owner, repo)
265+
// Try organization first, fall back to user on 404.
266+
installation, resp, err := gh.Apps.FindOrganizationInstallation(ctx, owner)
267+
if err != nil && resp != nil && resp.StatusCode == http.StatusNotFound {
268+
installation, resp, err = gh.Apps.FindUserInstallation(ctx, owner)
269+
}
265270
if err != nil {
266271
if resp != nil && resp.StatusCode == http.StatusNotFound {
267-
return nil, fmt.Errorf("%w: %s/%s", ErrRepositoryNotFound, owner, repo)
272+
return nil, fmt.Errorf("%w: %s", ErrInstallationNotFound, owner)
268273
}
269274
return nil, fmt.Errorf("fetching installation ID: %w", err)
270275
}
271276

272-
c.installations.set(key, installation.GetID())
277+
c.installations.set(owner, installation.GetID())
273278
return installation.GetID(), nil
274279
})
275280
if err != nil {
@@ -278,8 +283,9 @@ func (c *Client) getInstallationID(ctx context.Context, owner, repo string) (int
278283
return v.(int64), nil //nolint:errcheck // singleflight guarantees int64 on nil error
279284
}
280285

281-
// createInstallationToken creates a repository-scoped installation
282-
// access token with the given permissions.
286+
// createInstallationToken creates an installation access token with the
287+
// given permissions. When repository is non-empty the token is scoped to
288+
// that single repo; when empty it covers all repos the installation can access.
283289
func (c *Client) createInstallationToken(ctx context.Context, id int64, repository string, permissions map[string]string) (*TokenResponse, error) {
284290
appJWT, err := c.generateAppJWT()
285291
if err != nil {
@@ -296,21 +302,20 @@ func (c *Client) createInstallationToken(ctx context.Context, id int64, reposito
296302
return nil, fmt.Errorf("converting permissions: %w", err)
297303
}
298304

299-
if repository == "" {
300-
return nil, ErrRepositoryRequired
301-
}
302-
303305
opts := &gogithub.InstallationTokenOptions{
304-
Permissions: installationPermissions,
305-
Repositories: []string{repository},
306+
Permissions: installationPermissions,
307+
}
308+
if repository != "" {
309+
opts.Repositories = []string{repository}
306310
}
307311

308312
token, resp, err := gh.Apps.CreateInstallationToken(ctx, id, opts)
309313
if err != nil {
310-
// 404 (no installation) and 422 (repo outside installation scope)
311-
// match getInstallationID's ErrRepositoryNotFound contract.
312314
if resp != nil && (resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusUnprocessableEntity) {
313-
return nil, fmt.Errorf("%w: %s", ErrRepositoryNotFound, repository)
315+
if repository != "" {
316+
return nil, fmt.Errorf("%w: %s", ErrRepositoryNotFound, repository)
317+
}
318+
return nil, fmt.Errorf("%w (installation %d)", ErrInstallationNotFound, id)
314319
}
315320
return nil, fmt.Errorf("creating installation token: %w", err)
316321
}
@@ -404,7 +409,7 @@ func newClient(opts Options) (ClientIface, error) {
404409
baseURL: strings.TrimRight(opts.BaseURL, "/"),
405410
httpClient: &http.Client{Timeout: opts.Timeout, Transport: transport},
406411
installations: newInstallationCache(),
407-
contentsTokens: newTokenCache(),
412+
tokens: newTokenCache(),
408413
tokenReadyDelay: DefaultTokenReadyDelay,
409414
}, nil
410415
}

0 commit comments

Comments
 (0)