feat(cli): log token scope at start of fullsend run#1333
Conversation
At the start of every fullsend run, introspect the GH_TOKEN by calling GET /installation/repositories and log which repos the token is scoped to. This surfaces cross-org token scoping issues immediately rather than requiring post-hoc debugging. The check is non-fatal: if the token isn't an installation token or the API call fails, a warning is logged and the run continues normally. Refs: #1321 Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
Site previewPreview: https://b10815fa-site.fullsend-ai.workers.dev Commit: |
PATs and GITHUB_TOKENs return 401/403 on /installation/repositories. Treat these as "not an installation token" and skip silently instead of logging a warning on every run. Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
ReviewReason: stale-head The review agent reviewed commit |
waynesun09
left a comment
There was a problem hiding this comment.
Review Squad (10 agents) — 1 HIGH, 4 MEDIUM findings
Useful diagnostic feature with solid test coverage. The HIGH finding (pagination truncation at 100 repos) is the most actionable — large orgs will get silently incomplete output, defeating the debugging purpose. The hardcoded base URL, missing context.Context, empty repo list handling, and package-level client are standard code quality improvements.
| return nil, nil | ||
| } | ||
|
|
||
| url := baseURL + "/installation/repositories?per_page=100" |
There was a problem hiding this comment.
HIGH — Installations with >100 repos silently truncated
per_page=100 with no pagination and no total_count check. Large orgs with >100 repos in the installation scope will silently show only the first 100, with no indication the list is incomplete. This is the exact scenario (large multi-org deployments) this debugging feature is meant to help with.
Suggest parsing total_count from the response and appending a suffix when truncated:
if result.TotalCount > len(repos) {
repos = append(repos, fmt.Sprintf("... and %d more (%d total)",
result.TotalCount-len(repos), result.TotalCount))
}This avoids pagination complexity while ensuring operators know the list is incomplete.
9/10 review agents flagged this (near-unanimous)
| // Non-fatal: if the check fails (e.g., non-installation token), log a | ||
| // warning and continue. | ||
| if ghToken := os.Getenv("GH_TOKEN"); ghToken != "" { | ||
| repos, err := fetchTokenScope(ghToken, "https://api.github.com") |
There was a problem hiding this comment.
MEDIUM — Hardcoded api.github.com breaks GHES
GitHub Actions sets GITHUB_API_URL for GHES runners. If GHES support is ever added, this call will silently hit the wrong API.
apiURL := os.Getenv("GITHUB_API_URL")
if apiURL == "" {
apiURL = "https://api.github.com"
}
repos, err := fetchTokenScope(ghToken, apiURL)5/10 review agents flagged this
| // GET /installation/repositories and returning the full_name of each | ||
| // accessible repo. Returns (nil, nil) if the token is empty. | ||
| func fetchTokenScope(token, baseURL string) ([]string, error) { | ||
| if token == "" { |
There was a problem hiding this comment.
MEDIUM — No context.Context parameter — request is uncancellable
Uses http.NewRequest instead of http.NewRequestWithContext. The 10s http.Client.Timeout is the only deadline mechanism — the request can't be cancelled if runAgent is interrupted (e.g., SIGTERM). The rest of the codebase uses context.Context throughout (e.g., forge/github.LiveClient.do()).
func fetchTokenScope(ctx context.Context, token, baseURL string) ([]string, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
...
}4/10 review agents flagged this
| repos, err := fetchTokenScope(ghToken, "https://api.github.com") | ||
| if err != nil { | ||
| printer.StepWarn("Token scope check: " + err.Error()) | ||
| } else if len(repos) > 0 { |
There was a problem hiding this comment.
MEDIUM — Empty repo list silently ignored
When fetchTokenScope returns a non-nil empty slice (installation token with 0 repos), the len(repos) > 0 guard silently skips it. An installation token scoped to zero repos is a significant misconfiguration that will cause downstream agent failures.
} else if len(repos) > 0 {
printer.KeyValue("Token scoped to", strings.Join(repos, ", "))
} else {
printer.StepWarn("Token is an installation token but has access to 0 repositories")
}2/10 review agents flagged this
| ) | ||
|
|
||
| var tokenScopeClient = &http.Client{Timeout: 10 * time.Second} | ||
|
|
There was a problem hiding this comment.
MEDIUM — Package-level mutable HTTP client
tokenScopeClient is a package-level var, making the function harder to test in isolation and inconsistent with the rest of the codebase where HTTP clients are struct fields (forge/github.LiveClient.http, mint.Server.httpClient). Consider passing *http.Client as a parameter instead.
3/10 review agents flagged this
Summary
fullsend run, introspectGH_TOKENviaGET /installation/repositoriesand log which repos the token is scoped to.Refs: #1321
Test plan
TestFetchTokenScope_ReturnsRepoNames— happy pathTestFetchTokenScope_EmptyToken— skips gracefullyTestFetchTokenScope_APIError— returns errorTestFetchTokenScope_NonInstallationToken— 403 treated as error🤖 Generated with Claude Code