Skip to content

Release source configuration

ReleaseSourceConfig is the serde-tagged enum a tool embeds in its own config file. The tag key is source_type; each value selects a variant with its own typed parameter struct.

release:
  source_type: github
  owner: phpboyscout
  repo: rust-tool-base

Because it is a typed enum, a missing required key fails at deserialise time with a serde error naming the field (missing field `owner`) — not at update time with a 404. An unknown key is not caught: none of the parameter structs set #[serde(deny_unknown_fields)], so a mistyped optional key (privaet: for private:) is silently ignored and the default is used, with no diagnostic.

Keys shared by the forge backends

github, gitlab, gitea and codeberg all accept these. Bitbucket accepts all but owner/repo (it uses workspace/repo_slug); direct accepts only timeout_seconds.

Key Type Default Meaning
private bool false Currently inert — see below.
timeout_seconds u64 30 Per-request timeout. 0 disables the timeout entirely.

private does not do anything

private deserialises and round-trips, and the API docs describe it as "true when auth is required even for read operations". No backend reads it. Whether a request carries credentials depends solely on whether a token was passed to the factory:

let provider = factory(&cfg, Some(token))?;   // authenticated
let provider = factory(&cfg, None)?;          // anonymous

Setting private: true without supplying a token does not make requests fail early, and setting private: false with a token does not suppress the Authorization header. Treat the key as reserved.

timeout_seconds: 0 removes the timeout

The value is passed to reqwest::ClientBuilder::timeout only when it is greater than zero. At 0 no timeout is configured at all, so a request against an unresponsive host can hang for as long as the OS keeps the socket open. There is no separate connect timeout.

source_type: github

Key Required Default Notes
host no api.github.com API root. GitHub Enterprise takes the instance host.
owner yes User or organisation.
repo yes Repository name, no .git suffix.
private no false Inert.
timeout_seconds no 30

How host is normalised for GitHub

The factory rewrites host before use. Any leading https:// or http:// and any trailing / are stripped first, then:

You write Provider uses
api.github.com api.github.com
https://api.github.com/ api.github.com
github.example.com github.example.com/api/v3
api.github.example.com github.example.com/api/v3
github.example.com/api/v3 github.example.com/api/v3
anything containing /api/ left alone

A bare Enterprise hostname is promoted to /api/v3; you do not write the API path yourself. A host that already contains /api/ is passed through unchanged, which is the escape hatch for an instance on a non-standard API path.

GitHub validation errors

All are ProviderError::InvalidConfig:

Condition Message
host empty or whitespace github host must not be empty
host begins http:// github host must be https; got <host>
owner or repo empty github owner and repo must not be empty

The GitHub backend rejects an http:// host unconditionally — unlike GitLab, Gitea and Bitbucket, it does not consult the test-only insecure escape hatch when doing so. Point a GitHub provider at a local mock with a bare 127.0.0.1:PORT host, not an http://-prefixed one.

source_type: gitlab

Key Required Default Notes
host no gitlab.com Bare host. /api/v4 is appended by the backend.
owner yes Group or user namespace.
repo yes Project slug.
private no false Inert.
timeout_seconds no 30

host only has its scheme and trailing slash stripped — there is no promotion step. The project is addressed by URL-encoded path (owner%2Frepo), so you never need to look up a numeric project ID. A nested subgroup goes in owner verbatim (group/subgroup); the slash is percent-encoded for you.

GitLab validation errors

Condition Message
host empty or whitespace gitlab host must not be empty
host begins http:// gitlab host must be https; got <host>
owner or repo empty gitlab owner and repo must not be empty

source_type: gitea

Key Required Default Notes
host yes No default. Omitting it is a serde error.
owner yes
repo yes
private no false Inert.
timeout_seconds no 30

host is the only forge parameter in the crate with no default, because there is no canonical Gitea instance. /api/v1 is appended by the backend.

Gitea validation errors

Condition Message
host empty or whitespace gitea host must not be empty
host begins http:// gitea host must be https; got <host>
owner or repo empty gitea owner and repo must not be empty

source_type: codeberg

Key Required Default Notes
owner yes
repo yes
private no false Inert.
timeout_seconds no 30

There is deliberately no host key. The host is the associated constant CodebergParams::HOST, fixed at codeberg.org. Adding a host: key to a codeberg block is a serde error. To reach a different Gitea instance, use source_type: gitea and set host there.

Codeberg surfaces exactly the Gitea backend's behaviour — same endpoints, same Authorization: token header, same 50-item list cap.

source_type: bitbucket

Key Required Default Notes
host no api.bitbucket.org/2.0 Includes the API version segment.
workspace yes Workspace slug.
repo_slug yes Repository slug.
username no null Required for authenticated requests — see below.
private no false Inert.
timeout_seconds no 30

Bitbucket auth needs username and a token

Bitbucket Cloud uses HTTP Basic auth with an App Password. The username comes from config, the app password comes from the SecretString handed to the factory. The provider only sets an Authorization header when both are present:

  • username set, token supplied → authenticated.
  • username unset, token supplied → request is sent anonymously, silently.
  • username set, no token → request is sent anonymously.

There is no validation error for the mismatched cases. A private repository configured without username fails as a 404 from Bitbucket, surfaced as ProviderError::NotFound, not as an auth error.

Bitbucket validation errors

Condition Message
host empty or whitespace bitbucket host must not be empty
host begins http:// bitbucket host must be https; got <host>
workspace or repo_slug empty bitbucket workspace and repo_slug must not be empty

source_type: direct

For an S3 bucket, a private mirror, or anything else that serves files over HTTPS without a forge API.

Key Required Default Notes
version_url yes Fully-qualified URL returning the current version.
asset_url_template yes URL template. Must contain {version}.
pinned_version no null When set, version_url is never fetched.
timeout_seconds no 30

asset_url_template placeholders

Substituted by simple string replacement, in this order:

Placeholder Substituted with
{version} The discovered or pinned version, verbatim
{target} Rust host triple for the running binary
{os} std::env::consts::OSlinux, macos, windows, …
{arch} std::env::consts::ARCHx86_64, aarch64, …
{ext} .zip on Windows, .tar.gz everywhere else

{target} is resolved from a fixed table of six triples:

OS / arch {target}
linux / x86_64 x86_64-unknown-linux-gnu
linux / aarch64 aarch64-unknown-linux-gnu
macos / x86_64 x86_64-apple-darwin
macos / aarch64 aarch64-apple-darwin
windows / x86_64 x86_64-pc-windows-msvc
windows / aarch64 aarch64-pc-windows-msvc

Any other host combination substitutes the empty string. On, say, linux/armv7, a template of https://example.com/{version}/tool-{target}{ext} renders as https://example.com/1.2.3/tool-.tar.gz — a plausible-looking URL that 404s. It does not leave {target} in place, and it does not raise a configuration error. If you support platforms outside that table, build the URL from {os} and {arch} instead.

Only the five placeholders above are substituted. Any other {...} sequence is left in the URL verbatim.

What version_url may return

Fetched with a GET. The response is treated as JSON when the Content-Type contains application/json or the body's first non-whitespace character is {; otherwise it is treated as plain text.

  • JSON: the root object must have a string version key. Anything else is ProviderError::MalformedResponse("direct version_url JSON missing.versionstring").
  • Plain text: the whole body, trimmed, is the version. An empty body is ProviderError::MalformedResponse("direct version_url returned empty body").

Direct validation errors

Condition Message
version_url empty or whitespace direct version_url must not be empty
version_url has no http:///https:// scheme direct version_url must be a fully-qualified URL; got <url>
version_url begins http:// direct version_url must be https; got <url>
asset_url_template empty or whitespace direct asset_url_template must not be empty
asset_url_template lacks {version} direct asset_url_template must contain the {version} placeholder

asset_url_template is not validated for scheme. Only version_url is checked, so a template pointing at http:// is accepted at construction and then rejected by the HTTPS-only reqwest client at download time as a transport error.

source_type: custom

The escape hatch for a backend your tool registers itself.

release:
  source_type: custom
  type: internal-mirror
  params:
    endpoint: https://releases.corp.internal
    channel: stable
Key Type Meaning
type string The discriminator your factory registered under. Must not collide with a built-in.
params map of string to string Freeform. Your factory parses and validates it.

Note the shape: the enum's tag is source_type: custom, and the actual discriminator is the nested type key. ReleaseSourceConfig::source_type() returns the nested value (internal-mirror), which is what rtb_forge::lookup expects.

params values are all strings — there is no nested structure and no typed coercion. A number or boolean in YAML will fail to deserialise unless quoted.

What happens when the config does not match the factory

Each factory checks that it was handed its own variant and returns ProviderError::InvalidConfig otherwise, with a message of the form:

github factory called with non-github config: source_type=gitlab

This is reachable if you call a backend's factory directly rather than going through lookup(cfg.source_type()).