Migration: forge credential precedence is now GTB's¶
go/forge v0.8.0 stopped resolving auth.env and auth.keychain itself. Credential
ordering moved out of the module and into whatever configures it, for GTB-based tools,
that is GTB.
If you use GTB's own forge wiring, nothing changes. github.auth.env,
github.auth.keychain and github.auth.value resolve exactly as they did, in the same
order, and every config file that worked before still works. This note exists for tools
that reach past GTB and build forge settings directly.
What changed¶
GTB now composes the chain in pkg/vcs, and it is the single place the order is stated:
| # | Rung | Config holds |
|---|---|---|
| 1 | <forge>.auth.env |
The name of an environment variable |
| 2 | <forge>.auth.keychain |
A service/account keychain reference |
| 3 | <forge>.auth.value |
The credential itself |
| 4 | Well-known fallback | e.g. GITHUB_TOKEN |
// Compose the chain for one forge over an already-scoped config subtree.
sub := vcs.ConfigFromReader(cfg).Sub("github")
credential := vcs.ForgeCredential(sub, "GITHUB_TOKEN")
token, err := credential(ctx) // nothing is read until this call
If you build forge settings yourself¶
Before. The module resolved the chain, so a bare config was enough:
After: supply the chain explicitly, or get only whatever the adapter's own
fallback env var provides. Through the registry, hand it to the factory as an
option (go/forge v0.30.0 and later; the adapters from forge-github v0.25.0,
forge-gitlab v0.24.0, forge-gitea v0.25.0 and forge-bitbucket v0.16.0 honour
it):
forgeCfg := vcs.ConfigFromReader(cfg) // the ROOT configuration
endpoint := forge.Endpoint{Type: "github", Host: "github.com"}
provider, err := factory(ctx, endpoint, forgeCfg,
vcs.CredentialOptions(endpoint, forgeCfg, "GITHUB_TOKEN")...)
A factory given a source consults it and nothing else, so the construction
context bounds the resolution and an error from the chain fails construction
with its reason. Bitbucket authenticates with two halves, and the helper
hands the factory both (forge.WithUsername and forge.WithCredential,
go/forge v0.31.0 and forge-bitbucket v0.17.0): the username and the app
password each walk their own env-reference and literal keys and the one
keychain entry that holds both, read once, so the shipped username.env and
app_password.env defaults resolve the way every other forge's auth.env
does. Building typed settings directly, set the field instead:
The view no longer disguises the chain¶
Between the fix for the bare-CI-image failure (#76) and go/forge v0.30.0,
ConfigFromReader resolved the chain into auth.value and showed auth.env
and auth.keychain as unset, so a factory composing from configuration would
not report GTB's shipped default as stale. That resolution ran without the
caller's context and turned a refused keychain into an empty credential. With
the option in place the view reads every key as configured. A tool that
handed the view to a factory without the option relied on the disguise, and
gets the stale-key report back until it passes vcs.CredentialOptions.
Do not reach for forge.ConfigCredential¶
The module offers forge.ConfigCredential as a config-backed source. GTB deliberately
does not use it, and neither should a GTB-based tool.
Its stale-key report exempts only the single key it was pointed at, and probes the
relative auth.env and auth.keychain, precisely the keys GTB ships defaults for.
Pointed at auth.value while auth.env is set, it reports a working configuration
as stale. TestConfigCredentialIsNotUsed guards its absence from this repository.
Two behaviours worth knowing¶
Resolution is lazy. Nothing is read, and no keychain is touched, until the returned source is called. This is load-bearing rather than incidental: a repository authenticating over SSH must never trigger an OS unlock prompt for a token it does not need.
That is why the keychain rung is composed as a caller-supplied CredentialSource rather
than reached through the config stack. A config.Backend contributes its values from
Load, which runs when the store is built, so a keychain-backed backend would put an
OS unlock prompt on the startup path of every command, including --help. The
framework's default layer set (props.DefaultConfigLayers) is defaults, files,
project, env, flags, with no keychain among them, and a tool declaring its own set
via props.Tool.ConfigLayers chooses from the same list. (That field has since become
props.Tool.Config.Layers, whose order is the precedence; see
the config stack.)
The fallback rung is shadowed for shipped forges. Every forge's embedded bundle sets
<forge>.auth.env: <FORGE>_TOKEN as a default, so rung 1 already reads the variable
rung 4 would. Exporting GITHUB_TOKEN and configuring nothing resolves through
auth.env. Same variable, same value, same outcome, but a diagnostic will name
auth.env, not the fallback.
Checking a tool after upgrading¶
doctor now reports resolution as well as storage, naming the winning rung and never
the value:
$ mytool doctor
[OK] GitHub credential: resolves from auth.env
[!!] GitLab credential: credential configured but does not resolve
A [!!] here means a credential is configured but cannot be read, a malformed keychain
reference, most often. Before this check, that state was indistinguishable from having
no credential at all until something tried to authenticate.
Related¶
- Configure credentials: the storage modes and the full resolution chain
0183-forge-credentials-and-configurable-config-layers0054-credential-storage-hardening