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:
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.
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