Providers¶
A provider teaches forge to talk to one release backend. Each ships as its own
module — except direct, which is not a forge — so a tool links only the
clients it actually uses.
Enable one with a blank import; the registry does the rest:
import (
"gitlab.com/phpboyscout/go/forge"
_ "gitlab.com/phpboyscout/go/forge-gitlab"
)
factory, err := forge.Lookup("gitlab")
Your code never names a provider type. See backend agnosticism.
Capability matrix¶
| Provider | Source types | Module | Vendor SDK |
|---|---|---|---|
| GitHub | github |
go/forge-github |
go-github/v88 |
| GitLab | gitlab |
go/forge-gitlab |
gitlab-org/api/client-go/v2 |
| Gitea / Codeberg | gitea, codeberg |
go/forge-gitea |
gitea.dev/sdk |
| Bitbucket | bitbucket |
go/forge-bitbucket |
none |
| Direct | direct |
ships in go/forge |
none |
| Provider | GetReleaseByTag |
ListReleases |
ChecksumProvider |
SignatureProvider |
Can return a redirect |
|---|---|---|---|---|---|
| GitHub | ✅ | ✅ | — | — | yes (CDN) |
| GitLab | ✅ | ✅ | — | — | no |
| Gitea / Codeberg | ✅ | ✅ | — | — | no |
| Bitbucket | ❌ ErrNotSupported |
❌ ErrNotSupported |
✅ | ✅ | no |
| Direct | ✅ (synthesised) | ❌ ErrNotSupported |
✅ (opt-in) | ✅ (opt-in) | no |
The redirect column matters because callers are told to refuse a non-empty
redirect from DownloadReleaseAsset rather than follow it. Only GitHub ever
populates it, when its API redirects an asset request to a CDN.
What the contract flattens¶
A single interface over five platforms cannot preserve every distinction. Two worth knowing, because code branching on them misbehaves silently rather than failing:
| Accessor | Caveat |
|---|---|
GetDraft() |
Always false on GitLab — the platform has no draft-release concept, so there is nothing to report. GitHub and Gitea return the real value. |
GetLatestRelease() |
GitHub, GitLab and Gitea have a dedicated endpoint. Bitbucket and Direct synthesise a release, so "latest" means newest by the rule in each section below. |
See also the honest limits in backend agnosticism.
A dash means the provider does not implement the optional interface, so callers fall back to locating the file by name among the release assets — which is how most releases publish checksums. That fallback is the normal path, not a degraded one. See optional capabilities.
GitLab¶
Registers the source type gitlab. Works against gitlab.com and any
self-hosted instance.
Configuration¶
| Key | Purpose |
|---|---|
gitlab.auth.env |
Name of an environment variable holding the token |
gitlab.auth.keychain |
OS keychain reference, service/account |
gitlab.auth.value |
Literal token — weakest tier, avoid in committed config |
gitlab.url.api |
Override the API endpoint outright |
GITLAB_TOKEN |
Well-known fallback, consulted last |
The endpoint is derived from ReleaseSource.Host:
Host |
API base |
|---|---|
| (empty) | https://gitlab.com/api/v4 |
git.example.com |
https://git.example.com/api/v4 |
gitlab.url.api overrides both. See
authenticate for the resolution order.
Credentials are optional¶
A public project resolves unauthenticated. A token raises rate limits and is required for private projects.
Security¶
The token travels as a PRIVATE-TOKEN header, and is attached only to asset
downloads on the configured instance — host, port and scheme must all match.
Asset URLs come from release metadata that a release author controls, so an
unpinned credential is an exfiltration primitive. See
credential pinning.
GitHub¶
Registers the source type github. Works against github.com and GitHub
Enterprise.
Configuration¶
| Key | Purpose |
|---|---|
github.auth.env |
Name of an environment variable holding the token |
github.auth.keychain |
OS keychain reference, service/account |
github.auth.value |
Literal token — weakest tier |
github.url.api |
Override the API endpoint |
github.url.upload |
Override the asset-upload endpoint |
GITHUB_TOKEN |
Well-known fallback |
For Enterprise, set ReleaseSource.Host and both endpoints are derived
(/api/v3/ and /api/uploads/). Setting url.api alone still derives a
matching upload endpoint, so assets never target the wrong host.
Credentials are optional; public repositories resolve unauthenticated.
Releases only
GitHub's wider API — pull requests, repository creation, SSH keys, file contents, device login — exists in go-tool-base and was deliberately not extracted. Shipping it for one forge would give GitHub capabilities the others lack, which is the disparity this module's design exists to prevent. Restoring those across every provider in lockstep is specified separately.
Bitbucket¶
Registers the source type bitbucket, over the Downloads API — a file
bucket rather than a release system. It is a deliberately partial provider:
there are no tags and no release list, so GetReleaseByTag and ListReleases
return ErrNotSupported and callers fall back.
It is the only provider implementing both optional interfaces, locating
checksums.txt and checksums.txt.sig by exact filename — because without a
release object there is nothing else to hang a manifest on.
Configuration¶
Bitbucket needs two credentials, not a single token:
| Key | Purpose |
|---|---|
bitbucket.username.env / bitbucket.app_password.env |
Names of environment variables holding each field |
bitbucket.keychain |
OS keychain entry holding a JSON blob with both fields |
bitbucket.username / bitbucket.app_password |
Literal values — weakest tier |
BITBUCKET_USERNAME / BITBUCKET_APP_PASSWORD |
Well-known fallbacks |
One keychain entry rather than two, because a username and app password are
useless apart. Version detection reads the version from asset filenames;
override with the filename_pattern param.
Credentials travel as basic auth, which makes the scheme check in credential pinning matter more here than elsewhere.
Gitea and Codeberg¶
Registers two source types from one implementation. Codeberg is a Gitea instance, so they differ only in a pre-seeded host and which variable supplies the token:
| Source type | Host | Token fallback |
|---|---|---|
gitea |
from ReleaseSource.Host |
GITEA_TOKEN |
codeberg |
pre-seeded https://codeberg.org |
CODEBERG_TOKEN |
Splitting them into separate modules would mean duplicating the provider to change two constants.
Configuration¶
| Key | Purpose |
|---|---|
gitea.auth.env |
Name of an environment variable holding the token |
gitea.auth.keychain |
OS keychain reference, service/account |
gitea.auth.value |
Literal token — weakest tier |
gitea.url.api |
Override the API endpoint |
GITEA_TOKEN / CODEBERG_TOKEN |
Well-known fallbacks |
ReleaseSource.Host selects the instance. Credentials are optional; public
repositories resolve unauthenticated.
api_version is inert
An earlier hand-rolled client built the API path itself, so the api_version
param could pin an instance to a non-v1 API. The provider now uses the
official SDK, which targets v1 — the only version Gitea has ever shipped —
so the param is ignored rather than rejected.
Security¶
The token travels as an Authorization header, attached only to asset downloads
on the configured instance. See
credential pinning.
Direct¶
Registers the source type direct. Not a forge: a plain download source —
a web server, an artefact store, a CDN — with releases but no API to ask about
them. It ships inside the core module because it will never need a vendor SDK,
and doubles as the contract's reference implementation.
Because there is no API, the URLs are composed from templates and the latest version has to be told to it. Full setup is in use the direct provider; the keys are:
| Params key | Required | Purpose |
|---|---|---|
url_template |
yes | Asset URL, with {version} {version_bare} {os} {arch} {tool} {ext} |
pinned_version |
one of | A fixed version |
version_url |
one of | Endpoint naming the latest version |
version_format |
json, yaml, xml; plain text when omitted |
|
version_key |
Field holding the version, for structured formats | |
checksum_url_template |
Opts into ChecksumProvider |
|
signature_url_template |
Opts into SignatureProvider |
| Config key | Purpose |
|---|---|
direct.auth.env / .keychain / .value |
Standard credential chain |
direct.token |
Legacy literal — still honoured, superseded by auth.* |
DIRECT_TOKEN |
Well-known fallback |
Neither pinned_version nor version_url set means a version check returns
ErrVersionUnknown — assuming a version silently would be worse. The token
travels as a Bearer header.
ReleaseSource.Private¶
Setting Private: true makes a missing credential a fast failure rather
than an unauthenticated attempt. Leave it false and a provider with no resolvable
token proceeds anonymously, which is correct for a public repository and gives a
clearer error than a late 404 when the repository is private.
Registry API¶
forge.RegisteredTypes() // sorted []string of every registered source type
forge.Registered("gitlab") // is this type registered?
forge.Unregister("gitlab") // deliberately replace a built-in
RegisteredTypes is what Lookup's error message lists when a type is unknown —
usually diagnosing a missing blank import. Constants exist for the first-party
set (forge.SourceTypeGitHub and friends), but the registry accepts any string,
so they are a convenience rather than a constraint.
Adding your own¶
All first-party providers ship: GitHub, GitLab, Gitea, Codeberg, Bitbucket and plain download sources.
Your own is a first-class peer, not a workaround — the registry takes any string key, and nothing needs contributing here. See author a provider.