Skip to content

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

type Endpoint struct {
    Type string
    Host string
    Name string
}

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

const DefaultAuthKey = "auth.value"

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