Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions internal/cli/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ var githubAPIBaseURL = "https://api.github.com"
var githubHTTPClient = &http.Client{Timeout: 30 * time.Second}

// lookupAppID fetches the numeric app ID for a public GitHub App by slug.
// It makes an unauthenticated GET request to the GitHub API.
// When GH_TOKEN or GITHUB_TOKEN is set in the environment, the request is
// authenticated (5,000 requests/hour). Otherwise it falls back to an
// unauthenticated request (60 requests/hour, shared by source IP).
func lookupAppID(ctx context.Context, slug string) (int, error) {
url := githubAPIBaseURL + "/apps/" + url.PathEscape(slug)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
Expand All @@ -164,6 +166,16 @@ func lookupAppID(ctx context.Context, slug string) (int, error) {
}
req.Header.Set("Accept", "application/vnd.github+json")

// Authenticate if a token is available, lifting the rate limit from
// 60/hour (unauthenticated, shared by IP) to 5,000/hour.
token := os.Getenv("GH_TOKEN")
if token == "" {
token = os.Getenv("GITHUB_TOKEN")
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}

resp, err := githubHTTPClient.Do(req)
if err != nil {
return 0, fmt.Errorf("looking up app %s: %w", slug, err)
Expand All @@ -177,7 +189,10 @@ func lookupAppID(ctx context.Context, slug string) (int, error) {
return 0, fmt.Errorf("GitHub App %q not found — ensure the app exists and is publicly visible", slug)
}
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusTooManyRequests {
return 0, fmt.Errorf("GitHub API rate limit exceeded for app %s — unauthenticated requests are limited to 60/hour; try again later", slug)
if token != "" {
return 0, fmt.Errorf("GitHub API rate limit exceeded for app %s — try again later", slug)
}
return 0, fmt.Errorf("GitHub API rate limit exceeded for app %s — unauthenticated requests are limited to 60/hour; set GH_TOKEN or GITHUB_TOKEN and try again", slug)
}
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("GitHub API returned %d for app %s", resp.StatusCode, slug)
Expand Down
89 changes: 89 additions & 0 deletions internal/cli/mint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2286,8 +2286,12 @@ func TestMintDeployCmd_NoWarningForCorrectPlatformFlags(t *testing.T) {
// --- lookupAppID tests ---

func TestLookupAppID_Success(t *testing.T) {
t.Setenv("GH_TOKEN", "")
t.Setenv("GITHUB_TOKEN", "")

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/apps/fullsend-ai-coder", r.URL.Path)
assert.Empty(t, r.Header.Get("Authorization"), "unauthenticated request should have no Authorization header")
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"id": 12345, "slug": "fullsend-ai-coder", "client_id": "Iv1.abc123"}`)
}))
Expand Down Expand Up @@ -2358,6 +2362,9 @@ func TestLookupAppID_RateLimit(t *testing.T) {
{"TooManyRequests", http.StatusTooManyRequests},
} {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("GH_TOKEN", "")
t.Setenv("GITHUB_TOKEN", "")

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(tc.code)
}))
Expand All @@ -2370,10 +2377,92 @@ func TestLookupAppID_RateLimit(t *testing.T) {
_, err := lookupAppID(context.Background(), "some-app")
require.Error(t, err)
assert.Contains(t, err.Error(), "rate limit")
assert.Contains(t, err.Error(), "set GH_TOKEN or GITHUB_TOKEN")
})
}
}

func TestLookupAppID_AuthenticatedWithGHToken(t *testing.T) {
t.Setenv("GH_TOKEN", "ghp_test_token_123")
t.Setenv("GITHUB_TOKEN", "")

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer ghp_test_token_123", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"id": 99}`)
}))
defer srv.Close()

orig := githubAPIBaseURL
githubAPIBaseURL = srv.URL
defer func() { githubAPIBaseURL = orig }()

appID, err := lookupAppID(context.Background(), "test-app")
require.NoError(t, err)
assert.Equal(t, 99, appID)
}

func TestLookupAppID_AuthenticatedWithGITHUBToken(t *testing.T) {
t.Setenv("GH_TOKEN", "")
t.Setenv("GITHUB_TOKEN", "ghs_fallback_token")

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer ghs_fallback_token", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"id": 77}`)
}))
defer srv.Close()

orig := githubAPIBaseURL
githubAPIBaseURL = srv.URL
defer func() { githubAPIBaseURL = orig }()

appID, err := lookupAppID(context.Background(), "test-app")
require.NoError(t, err)
assert.Equal(t, 77, appID)
}

func TestLookupAppID_GHTokenTakesPrecedence(t *testing.T) {
t.Setenv("GH_TOKEN", "ghp_primary")
t.Setenv("GITHUB_TOKEN", "ghs_secondary")

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer ghp_primary", r.Header.Get("Authorization"),
"GH_TOKEN should take precedence over GITHUB_TOKEN")
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"id": 55}`)
}))
defer srv.Close()

orig := githubAPIBaseURL
githubAPIBaseURL = srv.URL
defer func() { githubAPIBaseURL = orig }()

appID, err := lookupAppID(context.Background(), "test-app")
require.NoError(t, err)
assert.Equal(t, 55, appID)
}

func TestLookupAppID_RateLimitAuthenticated(t *testing.T) {
t.Setenv("GH_TOKEN", "ghp_some_token")
t.Setenv("GITHUB_TOKEN", "")

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusTooManyRequests)
}))
defer srv.Close()

orig := githubAPIBaseURL
githubAPIBaseURL = srv.URL
defer func() { githubAPIBaseURL = orig }()

_, err := lookupAppID(context.Background(), "some-app")
require.Error(t, err)
assert.Contains(t, err.Error(), "rate limit")
assert.NotContains(t, err.Error(), "set GH_TOKEN or GITHUB_TOKEN",
"authenticated rate limit error should not suggest setting a token")
}

// --- verifyPEMMatchesApp tests ---

func TestVerifyPEMMatchesApp_Success(t *testing.T) {
Expand Down
Loading