Skip to content

How to Release mif-rs

Create, monitor, and — if needed — roll back a release of the mif-rs workspace: 12 crates (mif-core, mif-problem, mif-schema, mif-frontmatter, mif-ontology, mif-embed, mif-store, mif-cli, mif-mcp, mif-rh, mif-rh-cli, mif-rh-mcp) published independently to crates.io, plus attested binaries for the four binary crates (mif-cli, mif-mcp, mif-rh-cli, mif-rh-mcp) and one container image per binary crate.

Prefer the /release skill. Releases are orchestrated end-to-end by the /release skill (.github/skills/release/SKILL.md): release-prep PR, tag, monitoring of every workflow chain, and independent workstation verification. The manual procedure below is the same process the skill drives.

All 12 crates share one workspace version (version.workspace = true in every crate’s Cargo.toml, set once in [workspace.package]). This project follows Semantic Versioning 2.0.0:

Change type Version bump Example When to use
Breaking API change in any published crate MAJOR 1.0.0 -> 2.0.0 Removed public types, changed function signatures
New feature (backward-compatible) MINOR 0.1.0 -> 0.2.0 New public functions, new optional fields
Bug fix (backward-compatible) PATCH 0.1.0 -> 0.1.1 Fix incorrect behavior, performance improvement

Pre-1.0 policy: while on 0.x.y, MINOR bumps may include breaking changes. Document these clearly in commit messages with BREAKING CHANGE: in the body.


All 12 crates, including mif-rh, mif-rh-cli, and mif-rh-mcp (added later, alongside the rest of the workspace), are now published on crates.io. None of the 12 carries a publish = false line, so publish.yml’s guard job (which reads .packages[] | select(.publish != []) from cargo metadata, not a hardcoded crate list) treats every workspace member as publishable — no publish-gate flag to flip, and no per-crate onboarding step remains outstanding.

Republishing the next release requires a version bump in two places, both in the root Cargo.toml: bump [workspace.package].version, and bump the version field in each of the 8 [workspace.dependencies.mif-*] entries (mif-core, mif-schema, mif-ontology, mif-problem, mif-frontmatter, mif-embed, mif-store, mif-rh) to match. Both matter: cargo publish rewrites each path = "..." version = "..." dependency down to just the version requirement when it packages a crate, so a crate whose internal mif-* deps still point at the old version will fail to build against what’s actually live on crates.io the moment any of those internal APIs changed (confirmed directly: cargo publish --dry-run on a crate depending on a stale-versioned mif-ontology fails to compile once the local trait surface has moved past what’s published). See Pre-Release Checklist below for both edits. Confirm the current published state before releasing:

Terminal window
cargo metadata --no-deps --locked --format-version 1 \
| jq -r '.packages[] | select(.publish != []) | "\(.name) \(.version)"'

Configure on crates.io and in GitHub repository settings (Settings > Secrets and variables > Actions):

Item Purpose How to set up
crates.io Trusted Publishing (one-time, per crate) Publish via OIDC — no long-lived token On crates.io, for each of the 12 published crates: crate Settings > Trusted Publishing > add repo modeled-information-format/mif-rs, workflow publish.yml, environment release. This should be configured for all 12, including mif-rh, mif-rh-cli, and mif-rh-mcp — verify per-crate configuration on crates.io rather than inferring it from publication status alone. A brand-new crate not yet on crates.io still needs its first publish done manually (cargo login + cargo publish -p <crate>, in dependency order) before Trusted Publishing can be added for it
HOMEBREW_TAP_TOKEN (secret, optional) Push formula updates to the Homebrew tap Fine-grained PAT with write access to {owner}/homebrew-tap
HOMEBREW_TAP_REPO (variable, optional) Override the tap repo name (default homebrew-tap) Settings > Secrets and variables > Actions > Variables
GITHUB_TOKEN Provided automatically No setup needed

publish.yml, release.yml, and package-homebrew.yml all gate on the release GitHub Environment. Configure real protection rules (at minimum a required reviewer) on it in Settings > Environments before arming external publish channels.

  • GitHub Packages (Docker): Settings > Actions > General > Workflow permissions > “Read and write permissions”

Run through this checklist before every release.

  • All CI checks pass on main (check Actions)
  • Update the workspace version in the root Cargo.tomlboth places:
    [workspace.package]
    version = "X.Y.Z" # New version — every crate inherits it via version.workspace = true
    # And each of these 8 (mif-core, mif-schema, mif-ontology, mif-problem,
    # mif-frontmatter, mif-embed, mif-store, mif-rh) — cargo publish rewrites
    # path+version deps down to version-only, so a stale pin here ships a
    # crate that depends on the OLD, already-published version of another
    # workspace crate:
    [workspace.dependencies.mif-core]
    path = "crates/mif-core"
    version = "X.Y.Z"
  • Run the full local check suite:
    Terminal window
    just check # fmt-check + lint + test + doc-build + deny
    just msrv # cargo +1.95 check --all-features
  • Build all four release binaries locally to verify:
    Terminal window
    cargo build --release -p mif-cli -p mif-mcp -p mif-rh-cli -p mif-rh-mcp
  • Review CHANGELOG.md and recent commits since the last tag:
    Terminal window
    git log $(git describe --tags --abbrev=0)..HEAD --oneline
  • Verify conventional commit messages are correct (they drive changelog generation)
  • If breaking changes exist, confirm a MAJOR version bump and BREAKING CHANGE: in commit bodies
  • Commit the version bump separately:
    Terminal window
    cargo check # regenerates Cargo.lock for the new version — never hand-edit it
    git add Cargo.toml Cargo.lock
    git commit -m "chore: bump version to X.Y.Z"
    git push

Step-by-Step: Create and Push a Release Tag

Section titled “Step-by-Step: Create and Push a Release Tag”
Terminal window
git tag -a vX.Y.Z -m "Release vX.Y.Z"
Terminal window
git push origin vX.Y.Z

This single push triggers all release automation.

Pushing a v*.*.* tag triggers these workflows in parallel:

Workflow File What it does
Release release.yml Builds every binary crate (mif-cli, mif-mcp, mif-rh-cli, mif-rh-mcp) across 5 platforms each ({bin}-{version}-{platform}), resolved dynamically from cargo metadata — with SLSA build provenance, generates + attests a CycloneDX SBOM, verifies every attestation fail-closed, then creates the GitHub Release with auto-generated notes
Publish publish.yml Publishes every publishable workspace member (all 12 crates: mif-core, mif-problem, mif-schema, mif-frontmatter, mif-ontology, mif-embed, mif-store, mif-cli, mif-mcp, mif-rh, mif-rh-cli, mif-rh-mcp) to crates.io independently, in dependency order, each via its own crates.io Trusted Publishing (OIDC) config — then downloads each registry-served .crate, byte-compares it, and attests it. All 12 crates, including mif-rh/mif-rh-cli/mif-rh-mcp, are expected to have Trusted Publishing configured and publish cleanly — verify per-crate configuration (see Prerequisites) rather than assuming it from publication status alone
Pipeline (container) pipeline.yml Builds and pushes one multi-platform container image (linux/amd64, linux/arm64) per binary crate to ghcr.io/modeled-information-format/mif-rs/{bin} (mif-cli, mif-mcp, mif-rh-cli, mif-rh-mcp) — the bin matrix is resolved dynamically from cargo metadata — each signed/attested by the central signer workflow and verified fail-closed

After the Release workflow completes, package-homebrew.yml fires via workflow_run and regenerates the tap formula(e) in {owner}/homebrew-tap — but only when that workflow is enabled and the tap repo exists; as of this writing it is disabled_manually and no tap repo exists, so no Homebrew run is expected (see Post-Release Verification).

Never re-run release.yml against an existing tag. Builds are not reproducible; a re-run would overwrite published assets with different bytes, violating the immutability the attestations protect.


Terminal window
gh run list --limit 10
gh run watch <run-id>
gh run view <run-id> --log-failed
Stage Expected duration Common failure point
Build Binaries (4 bins x 5 platforms) ~15-25 min The macos-amd64 leg (cross-target on macos-latest), for any binary
Test + Cargo Audit gates ~3 min New advisory in Cargo.lock (audit scans the raw lockfile; deny may not have flagged it)
SBOM (generate + attest) ~1 min Attestation permissions (id-token, attestations)
Verify Attestations <1 min Fail-closed gate: any missing/unverifiable attestation blocks the release
Create Release ~1 min Only runs on tags, after verify passes
Publish (crates.io, 12 crates) ~5 min Trusted Publishing misconfigured or revoked for one of the crates (see Prerequisites); dependency-order failure blocking a downstream crate
Docker chain (pipeline) ~5-10 min Buildx multi-platform, central signer/verify
Homebrew (after Release) ~2 min workflow_run trigger, tap token

Run through this after all workflows complete.

  • GitHub Release exists with correct version:
    Terminal window
    gh release view vX.Y.Z
  • Binary assets are attached for all four binary crates, on every platform ({bin}-{version}-{platform} per binary — mif-cli, mif-mcp, mif-rh-cli, mif-rh-mcp each across linux-amd64, linux-arm64, macos-amd64, macos-arm64, windows-amd64.exe), plus SBOM and checksum assets:
    Terminal window
    gh release view vX.Y.Z --json assets --jq '.assets[].name'
  • Attestations verify from an independent machine (full reference: SECURITY.md):
    Terminal window
    gh release download vX.Y.Z --repo modeled-information-format/mif-rs
    for BIN in mif-cli mif-mcp mif-rh-cli mif-rh-mcp; do
    gh attestation verify "${BIN}-X.Y.Z-linux-amd64" \
    --repo modeled-information-format/mif-rs
    gh attestation verify "${BIN}-X.Y.Z-linux-amd64" \
    --repo modeled-information-format/mif-rs \
    --predicate-type https://cyclonedx.org/bom
    done
    shasum -a 256 -c *-X.Y.Z-checksums.txt
  • Release notes are generated correctly
  • Container images are pushed — one per binary crate:
    Terminal window
    for BIN in mif-cli mif-mcp mif-rh-cli mif-rh-mcp; do
    docker pull ghcr.io/modeled-information-format/mif-rs/${BIN}:vX.Y.Z
    docker pull ghcr.io/modeled-information-format/mif-rs/${BIN}:latest
    done
  • crates.io — each of the 12 crates is updated, and each served .crate attestation verifies:
    Terminal window
    for NAME in mif-core mif-problem mif-schema mif-frontmatter mif-ontology mif-embed mif-store mif-cli mif-mcp mif-rh mif-rh-cli mif-rh-mcp; do
    curl -fsSL -A 'release-check' \
    -O "https://static.crates.io/crates/${NAME}/${NAME}-X.Y.Z.crate"
    gh attestation verify "${NAME}-X.Y.Z.crate" \
    --repo modeled-information-format/mif-rs
    done
  • Homebrew formula — only if the Homebrew chain is armed, i.e. the package-homebrew.yml workflow is enabled (gh api repos/modeled-information-format/mif-rs/actions/workflows/package-homebrew.yml --jq .state reports active) and the {owner}/homebrew-tap repo exists: a package-homebrew.yml run appeared after Release completed and the tap formula shows the new version. If the workflow is disabled or there is no tap repo (the current state — the workflow is disabled_manually and no tap exists), no run is expected; mark this item skipped and move on
  • Install and test each binary crate on at least one platform:
    Terminal window
    cargo install mif-cli --locked
    cargo install mif-mcp --locked
    cargo install mif-rh-cli --locked
    cargo install mif-rh-mcp --locked
    mif-cli --version
    mif-mcp --version
    mif-rh-cli --version
    mif-rh-mcp --version

Terminal window
gh release delete vX.Y.Z --yes
git push --delete origin vX.Y.Z
git tag -d vX.Y.Z

You cannot unpublish from crates.io. Your options, per affected crate:

  1. Yank the version (prevents new projects from depending on it — this is a workspace with multiple published crates, so -p is required):
    Terminal window
    cargo yank --version X.Y.Z -p <crate-name>
  2. Publish a fix as a patch release, bumping the shared workspace version (see Hotfix Release Process below).

GHCR images are immutable by tag. To mitigate:

  1. Point users to a previous version (per binary crate):
    Terminal window
    for BIN in mif-cli mif-mcp mif-rh-cli mif-rh-mcp; do
    docker pull ghcr.io/modeled-information-format/mif-rs/${BIN}:vPREVIOUS
    done
  2. Delete the package version via GitHub UI, for each affected package (mif-rs/mif-cli, mif-rs/mif-mcp, mif-rs/mif-rh-cli, mif-rs/mif-rh-mcp): Packages > <package> > Package versions > Delete.
  3. Re-tag latest to the previous good version by re-pushing a known-good tag.

When a critical bug or security issue is found in the latest release:

Terminal window
git checkout -b hotfix/vX.Y.(Z+1) vX.Y.Z
Terminal window
just check
just msrv

The workspace version lives in one place, so a hotfix touches [workspace.package].version and the 8 [workspace.dependencies.mif-*] version pins, all in the root Cargo.toml (see Pre-Release Checklist above for why both matter):

Terminal window
# Edit Cargo.toml: version = "X.Y.(Z+1)"
cargo check # regenerates Cargo.lock
git add -A
git commit -m "fix: <description of the critical fix>"
Terminal window
git checkout main
git merge hotfix/vX.Y.(Z+1)
git push origin main
git tag -a vX.Y.(Z+1) -m "Release vX.Y.(Z+1)"
git push origin vX.Y.(Z+1)
Terminal window
# Yank the bad version from each affected crate
cargo yank --version X.Y.Z -p <crate-name>
# The hotfix tag push triggers an automatic re-publish of X.Y.(Z+1) for
# every crate, not just the one that changed — all 12 share one version.

CHANGELOG.md (workspace root, covering all 12 crates under the shared version) is maintained by hand (Keep a Changelog format) and updated before tagging: the release-prep step moves the ## [Unreleased] entries under a new ## [X.Y.Z] - <date> heading and updates the compare links (the /release skill does this in the prep PR). GitHub Release notes are auto-generated by the Release workflow (generate_release_notes: true).

Conventional commit prefixes map onto changelog sections:

Commit prefix Changelog section
feat: Added
fix: Fixed
docs: Documentation
perf: Performance
refactor: Refactored
test: Testing
chore: Miscellaneous

Best practices:

  • Use scoped prefixes for clarity: feat(mif-schema): add citation validator
  • Include BREAKING CHANGE: in the commit body for breaking changes
  • A release with an empty [Unreleased] section is a red flag — stop and confirm what the release contains

  • Registry: ghcr.io/modeled-information-format/mif-rs/{bin} — one image per binary crate (mif-cli, mif-mcp, mif-rh-cli, mif-rh-mcp)
  • Platforms: linux/amd64, linux/arm64
  • Base image: chainguard/glibc-dynamic (minimal attack surface, rebuilt from source continuously)
  • User: nonroot:nonroot (unprivileged)
  • Tags: vX.Y.Z, X.Y, X, latest, sha-<commit>
  • Packages: mif-core, mif-problem, mif-schema, mif-frontmatter, mif-ontology, mif-embed, mif-store, mif-cli, mif-mcp, mif-rh, mif-rh-cli, mif-rh-mcp — each published independently, each via its own Trusted Publishing (OIDC) config on crates.io (see Prerequisites above). A failure on one crate does not block the others’ channels, but does block anything published after it in the dependency order.
  • Tap: {owner}/homebrew-tap (override with the HOMEBREW_TAP_REPO variable)
  • Formula: generated from Cargo.toml metadata after each release
Terminal window
# From GitHub release (Linux)
wget https://github.com/modeled-information-format/mif-rs/releases/download/vX.Y.Z/mif-cli-X.Y.Z-linux-amd64
chmod +x mif-cli-X.Y.Z-linux-amd64
# From crates.io
cargo install mif-cli
cargo install mif-mcp
# From source
cargo install --git https://github.com/modeled-information-format/mif-rs mif-cli

See CI-TROUBLESHOOTING.md § Release workflow failures and § Docker build failures for the full table of failure modes and fixes. The release process is complete once every checkbox in Post-Release Verification passes.