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.
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:
usernameset, token supplied → authenticated.usernameunset, token supplied → request is sent anonymously, silently.usernameset, 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::OS — linux, macos, windows, … |
{arch} |
std::env::consts::ARCH — x86_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
versionkey. Anything else isProviderError::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:
This is reachable if you call a backend's factory directly rather than going
through lookup(cfg.source_type()).