Configuration¶
Every value forge reads, where it comes from, what it defaults to, and what
happens when it is wrong.
Per-provider keys are listed with each provider in providers; this page covers the shared surface and gathers the environment variables in one place.
Which configuration surfaces exist¶
Three, and they are deliberately separate. Confusing them is the most common reason a value appears to be ignored.
| Surface | Type | Set by | Holds |
|---|---|---|---|
| Endpoint | forge.Endpoint |
your code, per source | which provider, which instance, which configured source |
| Configuration reader | forge.Config |
your config stack | credentials and endpoint overrides, by key |
| Options | forge.Option |
the factory call | cross-cutting settings with no config subtree: the logger, and the connection rungs below |
forge reads no configuration file, no environment variable and no flag on its
own. It reads the keys you hand it, from the Config you hand it. Precedence
lives in your configuration stack. See
authenticate.
What goes in an Endpoint¶
Every field is connection identity. What a provider operates on (an owner, a repository) is a parameter of the operation, and lives on the method that needs it rather than on the address.
| Field | Default | What it does | When it is wrong |
|---|---|---|---|
Type |
"" |
The registry key. forge.Lookup takes the same string, and the endpoint carries it into the factory. |
Required. An unregistered value returns ErrProviderNotFound, whose hint lists the registered types, which usually means a missing blank import. Empty fails Validate with ErrInvalidEndpoint. |
Host |
"" |
The forge instance. Empty means the provider's own default: gitlab.com, github.com, codeberg.org. |
A bare host is fine: providers normalise it with NormalizeHostURL. A host with no scheme that reaches HostTrusted unnormalised can never match, so the credential is silently never attached. direct ignores Host entirely. |
Name |
"" |
Which configured source of that type this is, and the configuration subtree it reads: <type> when empty, <type>.<name> otherwise. |
Must not contain a dot. Validate rejects it, because a dotted name nests deeper than intended in any backend treating dots as path separators. Two sources of one type that share a Name share a configuration subtree, which is rarely what was meant. |
Endpoint is comparable, which is the point: a consumer reusing providers can
key on it directly rather than deriving a hash from a wider struct.
Never serialise an endpoint into one string
Keep the three fields separate end to end: separate configuration keys,
separate flags, separate struct fields. Host legitimately carries : for a
port and / for a path, so any inline type:host:name spelling collides
with its own payload.
Which options exist¶
| Option | Carries | Default when unset |
|---|---|---|
WithLogger(l) |
where diagnostics go, wrapped in RedactingHandler |
discards; never slog.Default, and nil discards |
WithHTTPTransport(rt) |
a transport for providers to build clients on, so several share one connection pool | each provider builds its own |
WithHTTPClient(c) |
a whole client for providers to use as-is | each provider builds its own |
What a supplied client is NOT used for
All four first-party adapters honour both options for their own API requests, and none uses a supplied client to fetch a release asset.
An asset URL comes from author-controlled release metadata, and whether a
credential can leave the pinned host is decided by the fetching client's
redirect behaviour. A supplied client carries your policy, and may carry your
credentials in headers the standard library does not know to strip. It
protects Authorization, Cookie and WWW-Authenticate on a cross-host
redirect, and nothing else. An adapter cannot inspect a client to find out,
so it builds its own for that hop.
A supplied transport is used everywhere, so the pooling benefit is complete either way. Another reason to prefer it.
A nil passed to any of them is ignored rather than treated as an instruction to
unset, and WithHTTPTransport ignores a typed nil too. An
http.RoundTripper holding a nil *http.Transport is not equal to nil, and
would otherwise panic at the first request rather than at the call that
introduced it.
The two connection rungs are not equivalent
WithHTTPTransport shares the expensive part (the connection pool and TLS
session cache both live in the transport) while each provider still builds
its own client, and so keeps its own redirect and sensitive-header policy.
Prefer this one.
WithHTTPClient hands over the whole client, and the redirect policy with
it. A provider that attaches a credential by hand relies on that policy to
stop the credential following a redirect off the host it pinned, and
GitLab's PRIVATE-TOKEN is that case here. Supply a client carrying an equivalent
policy, or accept that the guarantee is yours to keep.
Which configuration key holds the credential¶
forge.ConfigCredential(cfg, "") reads DefaultAuthKey. Any other key you pass
is read verbatim against whatever Config you hand in. There is no scoping
rule, so both of these work:
forge.ConfigCredential(sub, "") // gitlab.auth.value, via Sub
forge.ConfigCredential(root, "platforms.gitlab.token") // your own layout
| Situation | Result |
|---|---|
cfg is nil |
("", nil), absent rather than an error. Config-free public lookups depend on this. |
| The key is absent or blank | ("", nil), and FirstCredential tries the next source. |
The key is absent and auth.env or auth.keychain is present |
ErrStaleAuthKeys, with a hint naming which. |
Keys this module no longer reads¶
auth.env and auth.keychain were rungs of a resolution ladder that has been
removed. They are reported rather than ignored, because configuration that looks
correct and yields no credential fails later as an unexplained 401.
| Removed key | Replace it with |
|---|---|
auth.env: FOO |
an env layer in your config stack, or forge.EnvCredential("FOO") |
auth.keychain: svc/acct |
a config-keychain layer, or a CredentialSource of your own |
ErrStaleAuthKeys is not fatal on its own. FirstCredential retains it and
returns it only when nothing else produced a credential, so a stale key
sitting beside a working environment variable stays quiet.
forge-bitbucket removed its equivalents the same way: bitbucket.username.env
and bitbucket.app_password.env are no longer read and are reported as stale.
Which environment variables forge reads¶
The core module reads none. forge.EnvCredential(name) reads whatever
variable you name, and an empty name reads nothing rather than guessing.
The well-known fallbacks below are constants declared by each provider module and composed into its default credential source. They are consulted last, after the configured key.
| Variable | Provider module | Used for |
|---|---|---|
GITHUB_TOKEN |
go/forge-github |
API token (github.DefaultTokenEnv) |
GITHUB_CLIENT_ID |
go/forge-github |
OAuth client ID for device login (github.DefaultClientIDEnv) |
GITLAB_TOKEN |
go/forge-gitlab |
API token (gitlab.DefaultTokenEnv) |
GITLAB_CLIENT_ID |
go/forge-gitlab |
OAuth client ID for device login (gitlab.DefaultClientIDEnv) |
GITEA_TOKEN |
go/forge-gitea |
API token, source type gitea |
CODEBERG_TOKEN |
go/forge-gitea |
API token, source type codeberg |
BITBUCKET_USERNAME |
go/forge-bitbucket |
Basic-auth username |
BITBUCKET_APP_PASSWORD |
go/forge-bitbucket |
Basic-auth app password |
DIRECT_TOKEN |
go/forge/direct |
Bearer token (direct.DefaultTokenEnv) |
A provider composing this default is doing it in its config adapter, in the open.
Set Settings.Credential and you replace the whole composition, environment
fallback included.
Which configuration keys the direct provider accepts¶
direct has no API to ask, so everything it needs comes from its configuration
subtree: direct for the default source, direct.<name> for a named one. Keys
not listed here are ignored.
| Key | Required | Default | What it does |
|---|---|---|---|
url_template |
yes | — | The asset URL. Construction fails with a hint if it is empty. |
tool_name |
no | tool |
Expands {tool} in every template. |
pinned_version |
one of | — | A fixed version, returned without any request. Wins over version_url. |
version_url |
one of | — | An endpoint naming the latest version. |
version_format |
no | auto-detected | json, yaml, xml; anything else parses as plain text. Overrides Content-Type detection. |
version_key |
no | tag_name, then version |
The field or element holding the version, for structured formats. |
checksum_url_template |
no | unset | Opts into ChecksumProvider. Unset returns ErrNotSupported. |
signature_url_template |
no | unset | Opts into SignatureProvider. Unset returns ErrNotSupported. |
Configure neither pinned_version nor version_url and GetLatestRelease
returns direct.ErrVersionUnknown. Assuming a version silently would be worse.
Placeholders in a direct template¶
The same expansion applies to all three templates.
| Placeholder | Expands to | Example |
|---|---|---|
{version} |
The version as given | v1.2.3 |
{version_bare} |
The version without a leading v |
1.2.3 |
{os} |
runtime.GOOS, title-cased |
Linux |
{arch} |
runtime.GOARCH, with amd64 rewritten to x86_64 |
x86_64 |
{tool} |
the tool_name key in the source's configuration subtree, or tool when unset |
mytool |
{ext} |
Always tar.gz |
tar.gz |
{ext} does not vary. There is no .zip branch, so a source publishing Windows
archives as .zip has to write the extension into the template literally.
What the size bounds default to¶
Every bounded read takes the limit as a parameter: the caller owns the policy, the provider enforces it. These constants are suggested values, not mandates.
| Constant | Value | Bounds |
|---|---|---|
forge.DefaultMaxChecksumsSize |
1 MiB | ChecksumProvider.DownloadChecksumManifest |
forge.DefaultMaxSignatureSize |
1 MiB | SignatureProvider.DownloadSignature |
forge.DefaultMaxFileSize |
1 MiB | Contents.GetFile |
One bound is not yours to choose: the direct provider caps its version
endpoint response at 1 MiB internally, and no param changes it.
How a factory reads them¶
forge.NewOptions(opts...) normalises the set, so a factory can use
Options.Logger without a nil check, and can test Options.HTTPTransport and
Options.HTTPClient for nil to decide which rung it was handed.
What happens when a value is wrong¶
| Symptom | Likely cause | Fix |
|---|---|---|
ErrProviderNotFound, hint lists other types |
Missing blank import for the provider module | Import it for side effects |
ErrAlreadyRegistered at start-up |
Two modules registering the same source type, usually a duplicate blank import | forge.Unregister first, or drop one import |
ErrStaleAuthKeys |
Config still carries auth.env or auth.keychain |
Move the credential into a config layer |
| A 401 or a low rate limit with a token set | The credential resolved but was never attached: HostTrusted failed closed, commonly because the base URL has no scheme |
Normalise the host with NormalizeHostURL at construction |
direct.ErrVersionUnknown |
Neither pinned_version nor version_url set |
Set one |
version key not found in response |
The document has no tag_name or version field |
Set version_key |
| A configuration key that appears to do nothing | A typo, or a key that provider does not read | Check the table above; nothing validates the subtree. If it is a direct key, check it is not still under release_source.params, which is no longer read |
| A private repository fails as a late 404 | No provider refuses to build for want of a credential, because whether a repository is private is not knowable at construction | Check for a credential yourself before constructing. forge-bitbucket adds guidance to the refusal, which is a better error rather than an earlier one |
Related¶
- Providers: per-provider keys and capabilities
- Errors: every sentinel and what to do with it
- Authenticate to a forge: composing credential sources
- Log what a provider does: the logging option in full