Repository navigation
Conversation
Bump github.com/cli/go-gh/v2 and github.com/cli/oauth to the pseudo-versions that expose the refreshable-credential and OAuth refresh APIs used by the token refresh support that follows. Co-authored-by: Copilot <[email protected]> Copilot-Session: 2dc3f23c-61a6-43a7-85cd-90aa872a7fc6
Add the gh.Credential type, its IsRefreshable helper, and the token source constants, and change the AuthConfig interface so ActiveToken and TokenForUser return a Credential instead of a (token, source) pair. This is the foundational type the refreshable-credential work builds on. Also un-ignore the internal/gh directory by anchoring the gh ignore rule to the repo root (/gh), so the built binary is still ignored but package files are not. Co-authored-by: Copilot <[email protected]> Copilot-Session: 2dc3f23c-61a6-43a7-85cd-90aa872a7fc6
Update the config AuthConfig implementation (ActiveTokenType, ActiveToken, HasActiveToken, TokenForUser, SwitchUser) and every caller and test double to the Credential-returning API introduced in the previous commit. This is a mechanical, behavior-preserving migration with no refreshable logic yet. Co-authored-by: Copilot <[email protected]> Copilot-Session: 2dc3f23c-61a6-43a7-85cd-90aa872a7fc6
Extend the file lock with a blocking acquire that retries until it succeeds or a timeout elapses, used to serialize concurrent token-refresh attempts. Co-authored-by: Copilot <[email protected]> Copilot-Session: 2dc3f23c-61a6-43a7-85cd-90aa872a7fc6
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The exported HTTP client contract becomes unusable externally, and lock acquisition can succeed after context cancellation.
Get a fresh assessment by requesting another Copilot review.
Review tier: Balanced
Findings: 1
Open (2)
What changed in this PR
Introduces the credential and locking foundations needed for refreshable OAuth tokens without intentionally changing user-facing behavior.
Changes:
- Adds
gh.Credentialand migrates authentication storage consumers. - Adds timeout-bounded file-lock acquisition and tests.
- Updates OAuth dependencies and related fixtures.
| File | Description |
|---|---|
.gitignore |
Limits the gh ignore rule to the repository root. |
api/http_client.go |
Migrates HTTP authentication to credentials. |
api/http_client_test.go |
Updates HTTP client test configuration. |
go.mod |
Pins refresh-support dependency revisions. |
go.sum |
Updates dependency checksums. |
internal/attachments/client_test.go |
Updates attachment test authentication. |
internal/authflow/flow.go |
Adapts OAuth flow configuration. |
internal/config/auth_config_test.go |
Migrates authentication storage assertions. |
internal/config/config.go |
Returns credentials from authentication storage. |
internal/flock/flock.go |
Adds blocking lock acquisition. |
internal/flock/flock_test.go |
Tests blocking, timeout, and error behavior. |
internal/gh/gh.go |
Defines the credential model and interface. |
internal/ghcmd/cmd.go |
Adapts authentication recovery logic. |
pkg/cmd/agent-task/agent_task.go |
Uses credential fields and source constants. |
pkg/cmd/agent-task/shared/capi.go |
Extracts the CAPI access token. |
pkg/cmd/auth/gitcredential/helper.go |
Migrates Git credential lookup. |
pkg/cmd/auth/gitcredential/helper_test.go |
Updates the credential-helper fake. |
pkg/cmd/auth/logout/logout_test.go |
Updates logout token assertions. |
pkg/cmd/auth/refresh/refresh.go |
Migrates refresh token lookups. |
pkg/cmd/auth/refresh/refresh_test.go |
Updates refresh assertions. |
pkg/cmd/auth/shared/writeable.go |
Uses credential source metadata. |
pkg/cmd/auth/status/status.go |
Migrates status credential lookup. |
pkg/cmd/auth/token/token.go |
Migrates token output lookup. |
pkg/cmd/config/get/get.go |
Extracts tokens from credentials. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| type config interface { | ||
| ActiveToken(string) (string, string) | ||
| ActiveToken(string) gh.Credential |
There was a problem hiding this comment.
Some packages this will be a problem for:
| } | ||
|
|
||
| if user, err := c.ActiveUser(hostname); err == nil { | ||
| if credential, err := c.TokenForUser(hostname, user); err == nil { |
There was a problem hiding this comment.
This isn't quite behaviour-preserving. Previously the active-user lookup only checked the per-user keyring slot (TokenFromKeyringForUser). Going through TokenForUser also checks hosts.<host>.users.<user>.oauth_token in the config file, ahead of the unkeyed keyring fallback.
I checked it with a scratch test: active user u, users.u.oauth_token: per-user-config, an unkeyed keyring token legacy-keyring, and no host-level oauth_token:
trunk: token="legacy-keyring" source="keyring"
PR: token="per-user-config" source="oauth_token"
It should be rare in practice because activateUser always mirrors the per-user insecure token into the host-level slot, and the new result is arguably more correct. But the description says any behavioural difference is a bug, so can we either call this out as intentional (I assume it's groundwork for the refreshable slot in #14451) with a test pinning it, or keep the old lookup order here?
| // ActiveToken retrieves the active credential for the given hostname, searching environment variables, general | ||
| // configuration, and finally encrypted storage. It returns the last stored token as-is, including the current | ||
| // access token of a refreshable credential, and never contacts the token endpoint to refresh it. A caller that | ||
| // needs a token valid for API calls should use ActiveTokenWithRefresh instead. |
There was a problem hiding this comment.
ActiveTokenWithRefresh doesn't exist at this point in the stack. Same for TokenForUserWithRefresh below at L246, and the matching comments in internal/config/config.go (L257, L569). Fine if this only ever merges with #14451, but if the PRs land separately these docs point at nothing.
| TokenSourceOAuthToken = "oauth_token" | ||
| // TokenSourceRefreshableOAuthToken indicates the token was read from the refreshable_oauth_token entry in the | ||
| // config file, which holds a refreshable credential and is kept distinct from the non-expiring oauth_token entry. | ||
| TokenSourceRefreshableOAuthToken = "refreshable_oauth_token" |
There was a problem hiding this comment.
Nothing in this PR uses TokenSourceRefreshableOAuthToken.
Resolve go.mod/go.sum conflicts by keeping trunk's dependency versions and re-pinning go-gh to v2.16.2-0.20260914233729-2ce792192e43. The old pseudo-version was v2.16.1-0.*, which sorts below the v2.16.1 release. The cli/oauth pin is unchanged. Co-authored-by: Copilot App <[email protected]>


Part of #14449.
Description
Foundation for refreshable (short-lived) OAuth tokens. This PR carries no user-facing behavior; it prepares the ground so the later PRs can add the refresh machinery without re-threading call sites or churning unrelated code.
It does three things:
gh.Credentialtype and migrates the auth storage interface (and its callers) to speak that type instead of a bare token string.cli/go-ghandcli/oauthto pick up the refreshable-credential support this feature builds on.internal/flock, used later to serialize single-use refresh-token exchanges across processes.How did you test this change?
This feature is verified end to end as a whole rather than per PR. End-to-end tests should pass.
Key points
gh.Credentialtype now. Until now the auth storage layer spoke bare token strings, which is all a non-expiring token needs. A refreshable credential is more than a string: it carries a refresh token and expiry timestamps that must travel together with the access token through every layer that reads or writes it. Introducing the struct and migrating the interface in one mechanical, behavior-preserving step here means the later PRs only add fields and behavior, rather than re-plumbing every call site while also introducing new logic. It keeps the risky, reviewable logic changes out of a large mechanical diff.ghprocesses cannot spend the same single-use refresh token at once. The existing lock primitive was a non-blocking try-lock, which would force a refresh to fail immediately if a peer held the lock. This PR adds a blocking acquire with a timeout: a refresh waits briefly for a peer to finish (and then observes the peer's rotated credential on reload) instead of failing, while the timeout guarantees it can never hang forever. The timeout value and the refresh-specific naming land with the refresh path in PR 2.cli/go-ghandcli/oauth, repin to the released versions, and re-rungo mod tidy. This is tracked in the envelope issue.Notes for reviewers
Start with the
gh.Credentialtype and the storage-interface change, then the caller migration (mechanical), then the flock timeout addition. The migration should not change any observable behavior; if you spot a behavioral difference, that is a bug.Commits (review in order):
build(deps): bump go-gh and oauth for refreshable credentialsfix(gh): introduce gh.Credential and switch auth storage interface to itfix(config): migrate auth storage layer and callers to gh.Credentialfix(flock): add blocking lock acquisition with timeoutAuthorship and follow-up
Who wrote this:
Who answers review comments: