Rust in Practice #8 Distribution: Publishing to crates.io and Automating GitHub Releases

4 min read

The final part. The tool is complete (Parts 1–5), tests stand guard (Part 6), and we can build binaries for any platform (Part 7). What remains is putting it into the world, and there are two roads: crates.io for Rust developers, and GitHub Releases for users who don’t have Rust.

crates.io: the world of cargo install #

crates.io is the same registry we pulled dependencies from in basics Part 9 — publishing goes to the same place. First, fill in the Cargo.toml metadata.

Cargo.toml
[package]
name = "loglens"
version = "0.1.0"
edition = "2024"
description = "Access log stats, top paths, and error extraction CLI"
license = "MIT OR Apache-2.0"
repository = "https://github.com/example/loglens"
keywords = ["log", "cli", "nginx"]
categories = ["command-line-utilities"]

description and license are required for publishing. The dual MIT OR Apache-2.0 license is the Rust ecosystem’s customary default — absent a specific reason, following it is the safe move. The publishing procedure starts with a rehearsal.

Publish
cargo publish --dry-run   # package and validate only
cargo publish             # actually publish (crates.io login required)

One rule to know before publishing: versions on crates.io are permanent. No deletion, no overwriting; a problematic version can only be marked with cargo yank, which blocks new usage (users who already pinned it in a lock file can still fetch it — a design that refuses to break dependency graphs). The fix for a bad version is publishing the next version, which is why dry-run matters as a rehearsal. Once published, the user experience is one line:

Install
cargo install loglens

A release build runs on the user’s machine and lands in ~/.cargo/bin. It is the same shape as publishing to PyPI from the Python packaging series — minus the virtual environments and runtime-version worries, which is the compiled language’s advantage.

GitHub Releases: for users without Rust #

cargo install demands a Rust toolchain from the user. You cannot tell a server administrator “first, install rustup” — so there must be a path to download finished binaries. The customary place is GitHub Releases, and the files that go there are exactly Part 7’s target list (Linux musl, two Macs, Windows).

You could build four times by hand and upload, but anything repeated per release is automation material. The matrix from GitHub Actions course Part 4 is precisely the tool for this problem.

.github/workflows/release.yml
# .github/workflows/release.yml (the core)
on:
  push:
    tags: ["v*"]

jobs:
  build:
    strategy:
      matrix:
        include:
          - { os: ubuntu-latest,  target: x86_64-unknown-linux-musl }
          - { os: macos-latest,   target: aarch64-apple-darwin }
          - { os: macos-latest,   target: x86_64-apple-darwin }
          - { os: windows-latest, target: x86_64-pc-windows-msvc }
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - run: rustup target add ${{ matrix.target }}
      - run: cargo test
      - run: cargo build --release --target ${{ matrix.target }}
      # then: compress artifacts → upload to the release

Read it like this: pushing a tag like v0.1.0 wakes the workflow, and the four matrix combinations build in parallel, each on its own OS runner. As Part 7 promised, each OS builds its own share, so the cross-compilation linker problem shrinks to roughly the Linux musl target. And cargo test standing before the build is no accident: Part 6’s tests must pass on all three operating systems before a release exists — a pipeline from verification to distribution, triggered by one tag.

Closing the series #

Summarizing the eight-part journey as the tool’s résumé: its interface is declared as types (clap), its failures are designed (anyhow·thiserror), its memory use is predictable (BufReader streaming), it uses every core (rayon), its behavior is documented by tests (assert_cmd), it ships as a single binary that runs anywhere (musl), and it deploys automatically on a tag (Actions). Decision by decision, “code that works” became “a tool you can recommend.”

If the basics course was the skeleton of the grammar, this course was the muscle. As a next step, take one repetitive task from your own work and tool it up along the same frame — clap, error design, core logic, tests, distribution. The second tool will come together surprisingly fast.

Summary #

  • crates.io publishing is metadata (description and license required) → --dry-run rehearsal → cargo publish. Published versions are permanent; yank only blocks new usage.
  • cargo install loglens is the install experience for Rust developers — no runtime concerns, the compiled language’s advantage.
  • Users without Rust get finished binaries on GitHub Releases. Part 7’s target list is the file list, verbatim.
  • Tag push → matrix build → release upload is the automation skeleton. Each OS runner building its own share also shrinks the cross-compilation burden.
  • A pipeline where tests stand before the build completes the picture: verification to distribution on a single tag. That concludes the practice course.
X