Skip to content

Distribution: publish to crates.io, ship a container image, and automate releases with release-plz #469

Description

@mimi1vx

Why

mtui reaches users through exactly two channels today:

  • an OBS/IBS RPM, built and published from the Build Service by a maintainer
    walking the seven-step recipe in docs/src/installation.md § Cutting a
    release
    ;
  • a GitHub release tarball, built by .github/workflows/release.yml when an
    unprefixed version tag is pushed (x86_64-unknown-linux-musl +
    aarch64-apple-darwin).

Neither serves a Rust developer who wants cargo install, nor someone who wants
to run mtui-mcp --transport http next to an LLM client without building it,
nor the maintainer — who still hand-bumps the workspace version, hand-writes the
CHANGELOG section, tags, and then drives OBS by hand.

Three separable pieces of work, filed as one issue because their inputs
interlock: the crates.io name, the tag shape, and the version scheme are shared
decisions.

Current state (verified, not assumed)

  • release.yml fires on [0-9]+.[0-9]+.[0-9]+* — unprefixed tags. Every
    real tag matches (18.2.1 … 26.1.2); workspace version is 26.2.0.
  • Docs drift: § Cutting a release says git tag v1.2.0. No such tag has
    ever existed. The _service versionrewrite tolerates both, release.yml's
    filter effectively does too, but this must be settled before automating —
    a release tool needs one answer.
  • Releases are cut on per-minor branches (26.0.x, 26.1.x) that fork from
    main and are never merged back. release.yml's release-notes-base walk
    exists precisely because consecutive tags are siblings, not ancestors. Any
    release automation has to survive that topology.
  • 8 publishable crates; xtask is already publish = false; fuzz/ is its own
    detached workspace, so neither is in the way.
  • cargo publish -p mtui-types --dry-run → packages fine, but
    warning: manifest has no description. crates.io rejects an upload with
    no description, so this is a hard blocker wearing a warning's clothes.
  • cargo publish -p mtui-core --dry-run → hard error:
    all dependencies must have a version requirement specified when publishing; dependency 'mtui-config' does not specify a version. Every internal path dep
    in [workspace.dependencies] is path-only.
  • The crates.io name mtui is taken. It is
    an unrelated Modbus TUI
    (inowattio/MTUI), at 0.9.6, last published 2026-07-24, owner
    CosminPerRam. Actively maintained — not squatted, so there is no name-policy
    claim to make. Every other name we need (mtui-types, mtui-config,
    mtui-hosts, mtui-datasources, mtui-testreport, mtui-core, mtui-cli,
    mtui-mcp) is free right now.

Plan

Phase 1 — make the workspace publishable (publishes nothing)

  1. Add the crates.io-required metadata: description (per crate — it is the one
    field that cannot be meaningfully inherited), plus readme, keywords,
    categories, homepage/documentation inherited from
    [workspace.package] where they are genuinely common.
  2. Give every internal dep a version requirement alongside its path
    (mtui-types = { path = "crates/mtui-types", version = "=26.2.0" } — exact
    vs caret is Q4).
  3. Audit each package payload with cargo package --list. The tests/
    fixtures are the golden authority for the wire contracts (lock format,
    history format, testreport export, MCP schemas) — decide per crate whether
    they ship (reproducible cargo test for a downstream) or are excluded
    (smaller tarball). mtui-datasources/mtui-testreport fixtures are the
    large ones (~288K/200K).
  4. Add a CI job running cargo package --workspace --locked so metadata rot
    fails a PR instead of a release.

Done when: cargo package --workspace is clean and the new CI job is green.
No behaviour change, no publish.

Phase 2 — crates.io

  • Publish order (a dependency walk, not a preference):
    mtui-types → mtui-config → mtui-hosts → mtui-datasources →
    mtui-testreport → mtui-core → {mtui-cli, mtui-mcp}.
  • Reserve the names with a first publish early — all 8 are free today and
    the one we actually wanted is already gone.
  • Auth via crates.io Trusted Publishing (OIDC from the release workflow)
    rather than a long-lived CARGO_REGISTRY_TOKEN; the repo runs Scorecard and
    pins every action by SHA, a standing publish token is the odd one out.
  • Documentation must be honest about what this channel is not:
    cargo install mtui-cli gives you the mtui binary and nothing else — no
    completions, no man pages, no dist/terms/*.sh launchers (so terms/switch
    degrade), and mtui-mcp needs --features mcp to be a server at all (Q5).
    The RPM stays the complete install.

Phase 3 — container image (docker + podman)

  • What it is for: primarily mtui-mcp --transport http running beside an
    LLM client; secondarily a throwaway mtui REPL.
  • Base image is the load-bearing choice (Q6): scratch/distroless with
    the static musl binary is smallest but has no svn, so SVN testreport
    checkout is simply absent; registry.opensuse.org/opensuse/bci/bci-base can
    zypper in subversion openssh-clients, is idiomatic for an openSUSE project,
    and costs ~50 MB.
  • Credentials are mounted, never baked: ~/.ssh (pubkey-only by design),
    oscrc, openQA client.conf, mtui.toml. Document the bind mounts with
    :ro (and ,Z for SELinux). Run as a non-root user, read-only rootfs where
    possible.
  • terms/switch are inert in a container (no terminal emulator). The
    graceful-degradation contract already covers the behaviour; the docs must say
    it out loud so it is not read as a bug.
  • Multi-arch amd64 + arm64. The current release matrix has no
    aarch64-unknown-linux-musl leg — adding one also yields an arm64 Linux
    tarball, which is useful on its own.
  • Containerfile at the repo root (docker builds it unchanged), plus a
    documented podman run invocation. Tags X.Y.Z, X.Y, latest; cosign
    signature + build-provenance attestation.
  • Registry choice is Q7.

Phase 4 — release-plz

What it buys: a Release PR that bumps the version and writes the CHANGELOG
section from conventional commits; on merge it tags, publishes to crates.io, and
can create the GitHub release.

Friction that must be resolved before adopting it — none of it fatal, all of it
a decision:

  • Version scheme. release-plz infers semver bumps from conventional commits.
    mtui's majors (18 → 19 → 26) are not commit-derived. Either accept inferred
    bumps, or run it with the maintainer setting the version and release-plz doing
    only changelog + tag + publish (Q8).
  • Branch topology. release-plz assumes one release branch; mtui cuts on
    26.N.x branches that never merge back. Release PR on main + cherry-picks
    is the usual shape, but it needs deciding (Q8).
  • CHANGELOG. CHANGELOG.md is hand-curated Keep-a-Changelog prose with real
    explanatory paragraphs; release-plz emits git-cliff output. Either let it
    generate a draft and hand-edit it inside the Release PR (realistic), or keep
    the changelog fully manual and disable that feature. Note CHANGELOG has no sections for released 26.0.2 and 26.0.3; blocks curated release notes #401 — two released
    versions have no section at all — is a practical prerequisite either way.
  • Tag name. git_tag_name = "{{ version }}" to preserve unprefixed tags;
    _service's @PARENT_TAG@ + versionrewrite and release.yml's tag filter
    both depend on the current shape.
  • Division of labour with release.yml (Q9): keeping release.yml as the
    tag-triggered artifact builder and letting release-plz only bump/tag/publish
    preserves the working matrix + notes-base logic.

Questions

Q1 — crates.io name. mtui is taken by an active unrelated project.
(a) publish the binary crate as mtui-cli; the installed binary is still
mtui, no coordination needed — recommended;
(b) ask the owner to transfer — unlikely, and slow;
(c) rename the published umbrella (mtui-rs, suse-mtui).
This also decides the container image name and the docs wording.

Q2 — publish surface. crates.io requires every dependency to be published,
so shipping the binaries means shipping all 8 crates. Do we accept publishing
the internal library crates with an explicit "no API stability promise; the
supported interface is the mtui CLI and the MCP tool surface" disclaimer?
(Recommended: yes.)

Q3 — lockstep versions. Every crate bumps to 26.x even when unchanged.
Keep the single workspace version (consistent with the RPM's one-version story),
or let release-plz version crates independently? (Recommended: lockstep.)

Q4 — internal dep requirement: version = "=26.2.0" (exact, matches
lockstep and makes a mixed-version install impossible) vs "26.2" caret.
(Recommended: exact.)

Q5 — mtui-mcp's mcp feature. Off by default so the mtui build never
pulls rmcp/axum. For a published crate that means cargo install mtui-mcp
yields a binary that is not a server. Make the feature default-on for the
published mtui-mcp package, or document --features mcp at every install
site?

Q6 — container base. Which workflows must work inside the container? If
SVN testreport checkout must work, it is BCI + subversion; if the container is
MCP-only and the Gitea checkout path suffices, scratch + static musl is on the
table.

Q7 — registry. ghcr.io/openSUSE/mtui (CI-native, immediate, free) vs
registry.opensuse.org via an OBS container build (consistent with the RPM
channel and its signing, more setup). Both?

Q8 — release-plz control model. Maintainer-set version or
conventional-commit inference? And: run it on main only with cherry-picks to
26.N.x, or per release branch?

Q9 — who creates the GitHub release, release-plz or the existing
release.yml on the tag? (Recommended: release.yml keeps it — the matrix and
notes-base logic already work and are non-obvious.
)

Q10 — crates.io auth. Trusted Publishing (OIDC) needs a crates.io-side
config per crate (8 of them) and a named workflow/environment. Acceptable, or
start with a repository secret and migrate?

Q11 — ownership. Which crates.io account(s) own the crates? More than one
owner, so the names are not stranded behind a bus factor of 1.

Out of scope

Definition of done

  • Phase 1: cargo package --workspace --locked clean; CI job guarding it green.
  • Phase 2: all 8 crates on crates.io at the current version, published by CI
    from a tag, with cargo install mtui-cli documented and verified end to end.
  • Phase 3: signed multi-arch image pullable by both docker pull and
    podman pull, with a documented run invocation for mtui-mcp --transport http
    that mounts credentials read-only and runs as non-root; docs/src/installation.md
    gains a container section stating what does not work in it.
  • Phase 4: a version bump produces a Release PR; merging it tags, publishes, and
    triggers the existing artifact build — with the maintainer's answers to Q8/Q9
    encoded in release-plz.toml, and the docs' tag-prefix drift fixed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions