Skip to content

Commit 07b7f46

Browse files
authored
fix: handle revocation 401 gracefully, add retry on 400/404, improve policy error messages, fix docs (#4)
* fix: handle revocation 401 gracefully, add retry on 400/404, improve policy error messages, fix docs * fix: handle revocation 401 gracefully, add retry on 400/404, improve policy error messages, fix docs
1 parent 9bd0c70 commit 07b7f46

6 files changed

Lines changed: 41 additions & 18 deletions

File tree

deployments/terraform/scripts/aws/push-github-apps-to-aws-secrets-manager.sh

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,14 @@ TERRAFORM INTEGRATION:
120120
(See Terraform module README for details)
121121
122122
GITHUB APPS JSON FORMAT:
123-
{
124-
"apps": [
123+
[
125124
{
126125
"client_id": "Iv1...",
127-
"private_key_path": "/path/to/key.pem",
126+
"private_key": "<PEM-ENCODED-PRIVATE-KEY>",
128127
"organization": "my-org"
129128
}
130-
]
131-
}
129+
]
130+
132131
133132
EOF
134133
}

internal/clients/github/client.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,12 @@ func (c *Client) RevokeToken(ctx context.Context, token string) error {
126126
if err != nil {
127127
return err
128128
}
129-
_, err = gh.Apps.RevokeInstallationToken(ctx)
129+
resp, err := gh.Apps.RevokeInstallationToken(ctx)
130130
if err != nil {
131+
// 401 means the token is already invalid (expired or revoked),
132+
if resp != nil && resp.StatusCode == http.StatusUnauthorized {
133+
return nil
134+
}
131135
return fmt.Errorf("revoking installation token: %w", err)
132136
}
133137
return nil

internal/clients/github/client_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ func TestRevokeToken_Success(t *testing.T) {
441441
assert.Equal(t, "/installation/token", gotPath)
442442
}
443443

444-
func TestRevokeToken_Error(t *testing.T) {
444+
func TestRevokeToken_UnauthorizedTreatedAsSuccess(t *testing.T) {
445445
t.Parallel()
446446
_, keyPEM := testutil.GenerateRSAKey(t)
447447

@@ -453,7 +453,23 @@ func TestRevokeToken_Error(t *testing.T) {
453453
c, err := New(Options{ClientID: "client-1", PrivateKey: keyPEM, BaseURL: server.URL})
454454
require.NoError(t, err)
455455

456-
err = c.RevokeToken(t.Context(), "ghs_invalid_token")
456+
err = c.RevokeToken(t.Context(), "ghs_expired_token")
457+
require.NoError(t, err, "401 means token is already invalid, which achieves revocation")
458+
}
459+
460+
func TestRevokeToken_ServerError(t *testing.T) {
461+
t.Parallel()
462+
_, keyPEM := testutil.GenerateRSAKey(t)
463+
464+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
465+
http.Error(w, `{"message":"Internal Server Error"}`, http.StatusInternalServerError)
466+
}))
467+
t.Cleanup(server.Close)
468+
469+
c, err := New(Options{ClientID: "client-1", PrivateKey: keyPEM, BaseURL: server.URL})
470+
require.NoError(t, err)
471+
472+
err = c.RevokeToken(t.Context(), "ghs_test_token")
457473
require.Error(t, err)
458474
assert.Contains(t, err.Error(), "revoking installation token")
459475
}

internal/clients/github/retry.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,18 @@ func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
102102
return lastResp, nil
103103
}
104104

105-
// shouldRetry returns true for 5xx, 429, 408, or nil response (retry on transient failure).
105+
var retryableStatusCodes = map[int]bool{
106+
http.StatusBadRequest: true,
107+
http.StatusNotFound: true,
108+
http.StatusRequestTimeout: true,
109+
http.StatusTooManyRequests: true,
110+
}
111+
106112
func shouldRetry(resp *http.Response) bool {
107113
if resp == nil {
108114
return true
109115
}
110-
if resp.StatusCode >= http.StatusInternalServerError {
111-
return true
112-
}
113-
return resp.StatusCode == http.StatusTooManyRequests ||
114-
resp.StatusCode == http.StatusRequestTimeout
116+
return resp.StatusCode >= http.StatusInternalServerError || retryableStatusCodes[resp.StatusCode]
115117
}
116118

117119
// isTransient returns true for timeouts, DNS/connection errors, and connection reset/refused/EOF.

internal/clients/github/retry_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ func TestRetryTransport_StatusBehavior(t *testing.T) {
5858
{"success_first_attempt", http.StatusOK, 0, http.StatusOK, 1},
5959
{"retries_on_5xx", http.StatusBadGateway, 3, http.StatusOK, 3},
6060
{"retries_on_429", http.StatusTooManyRequests, 2, http.StatusOK, 2},
61-
{"no_retry_on_4xx", http.StatusNotFound, 999, http.StatusNotFound, 1},
61+
{"retries_on_400", http.StatusBadRequest, 2, http.StatusOK, 2},
62+
{"retries_on_404", http.StatusNotFound, 2, http.StatusOK, 2},
63+
{"no_retry_on_other_4xx", http.StatusForbidden, 999, http.StatusForbidden, 1},
6264
{"exhausts_max_attempts", http.StatusServiceUnavailable, 999, http.StatusServiceUnavailable, 3},
6365
}
6466
for _, tt := range tests {
@@ -155,10 +157,10 @@ func TestShouldRetry(t *testing.T) {
155157
}{
156158
{"200 OK", http.StatusOK, false},
157159
{"201 Created", http.StatusCreated, false},
158-
{"400 Bad Request", http.StatusBadRequest, false},
160+
{"400 Bad Request", http.StatusBadRequest, true},
159161
{"401 Unauthorized", http.StatusUnauthorized, false},
160162
{"403 Forbidden", http.StatusForbidden, false},
161-
{"404 Not Found", http.StatusNotFound, false},
163+
{"404 Not Found", http.StatusNotFound, true},
162164
{"408 Request Timeout", http.StatusRequestTimeout, true},
163165
{"429 Too Many Requests", http.StatusTooManyRequests, true},
164166
{"500 Internal Server Error", http.StatusInternalServerError, true},

internal/sts/authorizer/policy_fetch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func (a *Authorizer) fetchPolicyFromGitHub(ctx context.Context, repository, reso
9090
}
9191
}
9292

93-
return nil, fmt.Errorf("%w at %s", ErrPolicyFileNotFound, paths[0])
93+
return nil, fmt.Errorf("%w at %s", ErrPolicyFileNotFound, strings.Join(paths, ", "))
9494
}
9595

9696
// extensionVariants returns the paths to try when fetching a trust policy.

0 commit comments

Comments
 (0)