Tool metadata¶
ToolMetadata is the static description of a tool: what it is called, what it does,
where its releases come from, and where users go for help. It is built once at
startup and never mutated.
ToolMetadata fields¶
Built with a bon typestate builder. name and summary are
required at compile time — leaving either out is a type error, not a runtime
panic, and the crate carries a trybuild fixture proving it stays that way. String
fields accept &str directly because the builder is declared with
#[builder(on(String, into))].
| Field | Type | Default | Set it when |
|---|---|---|---|
name |
String |
required | Always. Used in --help, the banner, and the update asset pattern. |
summary |
String |
required | Always. The one-line description in --help. |
description |
String |
"" |
You want long-form text under --help. |
release_source |
Option<ReleaseSource> |
None |
The tool can update itself. Required in practice whenever Feature::Update is enabled. |
update_policy |
UpdatePolicy |
UpdatePolicy::Disabled |
You want automatic pre-run update checks. |
update_check_interval |
Duration |
24 hours | The default throttle is wrong for your release cadence. |
release_credential |
Option<CredentialRef> |
None |
Releases live in a private repository. |
help |
HelpChannel |
HelpChannel::None |
You want a support line under every error diagnostic. |
update_public_keys |
Vec<String> |
vec![] |
The tool self-updates — an empty list makes rtb-update refuse to run. |
update_checksums_asset |
Option<&'static str> |
None |
Your release publishes a SHA-256 checksums asset. |
update_asset_pattern |
Option<&'static str> |
None |
Your release assets are not named {name}-{version}-{target}{ext}. |
telemetry_notice |
Option<&'static str> |
None |
You want a tool-specific privacy notice instead of the generic one. |
let metadata = ToolMetadata::builder()
.name("mytool")
.summary("does the thing")
.help(HelpChannel::Url { url: "https://example.com/support".into() })
.build();
Which metadata fields survive a config file¶
ToolMetadata derives Serialize and Deserialize, but four fields carry
#[serde(skip)] and are therefore compile-time-only. They cannot be set from a
config file, and they do not appear when the struct is serialised:
update_public_keys, update_checksums_asset, update_asset_pattern,
telemetry_notice.
That is deliberate for the keys — trusted signing keys pinned in a file a user can
edit would not be trusted keys. It has a sharp edge, though, because the struct is
also #[serde(deny_unknown_fields)]. Putting a skipped field in a YAML file does not
silently do nothing; it fails the whole parse:
unknown field `update_public_keys`, expected one of `name`, `summary`,
`description`, `release_source`, `update_policy`, `update_check_interval`,
`release_credential`, `help`
Those eight names are the complete set of keys a ToolMetadata document may carry.
release_credential is a ninth special case: it deserialises but carries
#[serde(skip_serializing)], so it can be read from a file and never written back.
CredentialRef wraps a secret, and secrets do not round-trip out through
Serialize.
How update_check_interval is written in a config file¶
Duration has no bespoke serde format here — it uses serde's own representation,
which is a two-field struct. A bare number is rejected:
# rejected: invalid type: integer `3600`, expected struct Duration
update_check_interval: 3600
# accepted
update_check_interval:
secs: 3600
nanos: 0
Both secs and nanos are required; omitting either fails with
missing field "nanos". A serialised ToolMetadata with default settings looks like
this in full:
name: mytool
summary: does things
description: ''
release_source: null
update_policy: disabled
update_check_interval:
secs: 86400
nanos: 0
help:
kind: none
ReleaseSource — the six variants¶
Where the tool's releases live. Serialised with a type: discriminator in
lowercase, #[non_exhaustive], and deny_unknown_fields on every variant.
| Variant | Required fields | host default |
|---|---|---|
Github |
owner, repo |
github.com |
Gitlab |
project (full path, e.g. group/subgroup/project) |
gitlab.com |
Bitbucket |
workspace, repo_slug |
api.bitbucket.org/2.0 |
Gitea |
owner, repo, host |
none — omitting it fails with missing field "host" |
Codeberg |
owner, repo |
not applicable — the variant has no host field at all |
Direct |
url_template |
not applicable |
Gitea is the one to watch: it is the only variant with a mandatory host, because
there is no public Gitea instance to default to. Codeberg exists as its own variant
rather than as a Gitea preset precisely so nobody has to remember
host: codeberg.org.
Direct takes a template rather than a URL. The placeholders it accepts are resolved
by rtb-update, not by this crate:
Because ReleaseSource is #[non_exhaustive], a match over it in your own code
needs a _ => arm. That is what lets a new hosting provider be added in a minor
release without breaking you.
UpdatePolicy — automatic update checking¶
| Value | YAML | Behaviour |
|---|---|---|
Disabled |
disabled |
Default. Never checks automatically. |
Prompt |
prompt |
Checks (throttled); prompts the user when a newer version exists. |
Enabled |
enabled |
Checks (throttled); updates before running when a newer version exists. |
Three things this policy does not govern, each of which trips people up:
- The
updatesubcommand is always available regardless of the policy. The policy governs only the automatic pre-run check. - The policy is inert unless
Feature::Updateis enabled andrelease_sourceis set. Setting it toEnabledon a tool with no release source changes nothing. update_check_intervalis consulted only forPromptandEnabled. A check that falls within that window of the previous one is skipped.
Default is Disabled so that a tool built on the toolkit makes no unsolicited
network call and pays no pre-run cost unless its author opted in.
HelpChannel — the support footer¶
rtb-cli reads this off ToolMetadata, formats it with footer(), and installs the
result so every error diagnostic ends with the same support pointer.
| Variant | Fields | footer() output |
|---|---|---|
None (default) |
— | None — no footer is printed |
Slack |
team, channel (no leading #) |
support: slack #cli-tools (in platform) |
Teams |
team, channel |
support: Teams → SRE / oncall |
Url |
url |
support: https://support.example.com |
Serialised with a kind: discriminator in lowercase, #[non_exhaustive], and
deny_unknown_fields:
footer() returns Option<String>; HelpChannel::None yields None, which the
error hook treats as "no footer".
Related¶
- Point your tool at its releases
- Features —
Feature::Updategates all of the update fields