Skip to content

Ship self-update

Four things have to line up: the metadata, the published assets, the trusted keys, and a --version flag on your binary. Getting three of four gives you a tool that downloads an update and refuses to install it.

Set the release source

use rtb_app::metadata::ReleaseSource;

ToolMetadata::builder()
    .name("mytool")
    .summary("does the thing")
    .release_source(ReleaseSource::Gitlab {
        project: "myorg/group/mytool".into(),
        host: "gitlab.com".into(),
    })
    .update_public_keys(vec![
        "RW…".to_string(),          // base64 minisign public key
    ])
    .build()

Without release_source, both update check and update run fail immediately. Without update_public_keys, update run refuses before touching the network — a tool that cannot verify a signature does not download one.

For a private release source, set release_credential as well; the token is resolved through the credential chain at update time, and a NotFound result just means the provider runs unauthenticated.

Publish assets the selector can find

The default asset-name pattern is {name}-{version}-{target}{ext}, so a v1.2.3 release for x86-64 Linux must be published as:

mytool-1.2.3-x86_64-unknown-linux-gnu.tar.gz
mytool-1.2.3-x86_64-unknown-linux-gnu.tar.gz.minisig

If your pipeline names things differently, set update_asset_pattern rather than renaming your releases. Placeholders are {name}, {version}, {target}, {os}, {arch} and {ext}; unknown ones pass through verbatim.

Three constraints on the archive itself:

  • Format must be .tar.gz, .tgz or .zip. Anything else fails extraction.
  • The binary inside must be named exactly <tool name> (or <tool name>.exe). Nesting it in a directory is fine; renaming it is not.
  • The signature must be <asset>.minisig<asset>.sig is also accepted, but .minisig is preferred and matches what minisign produces.

Optionally publish a sha256sum-format checksums file and name it in update_checksums_asset. Without that field set, no checksum is verified at all; the signature is the only integrity gate.

Sign with the prehashed variant

Only minisign's prehashed ED signatures are accepted. A legacy pure Ed signature is refused even when the trusted key could verify it, because cargo-binstall rejects legacy too and one published signature has to satisfy both installers. Modern minisign produces prehashed signatures by default.

Pin the same base64 public key string in both places — update_public_keys here and pubkey under [package.metadata.binstall.signing] — so the two installers cannot end up trusting different things.

Ship two keys during a rotation. Any one verifying is enough, so a release trusting {old, new} spans the change with no dual-signing window.

Give your binary a root --version flag

This is the step that catches people. Before swapping, update run executes <staged-binary> --version and requires exit 0 with the release tag in stdout.

rtb-cli does not declare a root --version flag. A tool built straight on Application::builder answers mytool --version with command not found: --version and a non-zero exit, so its own self-test fails and every update stops with rtb::update::self_test_failed.

Make --version work before you enable self-update, and check it by hand:

$ ./mytool --version
mytool 1.2.3
$ echo $?
0

There is also no timeout on the self-test, so make sure --version cannot block.

Test the flow without replacing anything

$ mytool update check
new version available: 1.2.2 -> 1.2.3

$ mytool update run --dry-run --progress
update: checking…
update: downloading 4194304/8388608
update: verifying signature…
update: self-testing staged binary…
update: done — now at 1.2.3
dry run: staged 1.2.2 -> 1.2.3 at /home/you/.cache/mytool/update/v1.2.3/bin/mytool

--dry-run performs every step except the swap and tells you where the staged binary is, so you can run it yourself. --progress prints each phase to stderr, which is how you find out which step is slow or failing.

Nothing cleans the cache directory up. Each release leaves an archive and an extracted binary behind.

Decide about automatic checks — carefully

ToolMetadata::update_policy defaults to Disabled. Prompt prints a notice on stderr when a newer release exists; Enabled performs the update before running the user's command.

Under Enabled, any update failure fails the command the user actually asked for, and there is no environment variable, config key or flag a user can set to opt out. Prefer Prompt unless you have a specific reason not to. update_check_interval throttles both, defaulting to 24 hours.