release, ci, docs, skills: bump, check and publish the crates with the app release #198

Merged
natsukium merged 1 commit from release/crates-publish into main 2026-10-19 23:51:15 +09:00
Owner

Part of #160 (release automation). Stacked on #197; merge that first (this branch carries its commit).

Goal: a forgotten or too-small library bump stops the release, and publishing needs no long-lived credential. This is the minimal design by choice. Rare cases are left to the review step in the release skill.

  • Prepare: just release-prepare runs release-plz update. It bumps changed libraries (patch, or minor when cargo-semver-checks finds a break) and raises the requirements on them. release-plz.toml excludes felis-cli, felis-client and felis-daemon so the app version never moves. release-plz and cargo-semver-checks join the dev shell.

  • Check (tag gate, verify-tag): tools/release/crates.py check refuses the tag when:

    • a requirement is not its crate's version;
    • cargo semver-checks fails for a library or felis-daemon's library target;
    • release-plz update would still change the tree.

    Crates not yet on crates.io skip the last two. The tag build job also runs just publish-dry-run.

  • Publish (GitHub release-mirror.yml, new crates job): runs after the mirror job, for final tags only. Steps:

    1. crates.py publish-plan lists the crates whose version is not on crates.io. An empty list ends the job; a never-published crate fails it.
    2. cargo publish --dry-run builds them with no credential present.
    3. rust-lang/crates-io-auth-action mints a token through trusted publishing, in the crates-io environment.
    4. cargo publish --no-verify uploads them.
  • Procedure: new skill .agents/skills/release/SKILL.md. It covers the release PR, the manual review points, tag and watch, and the one-time setup: tag protection on both forges, the crates-io environment, and the first publish for v0.3.0.

  • Docs: workspace.md ("Versioning", "Release gate"), implementation.md (why GitHub and trusted publishing, why release-plz only prepares), testing.md, tools/README.md.

Verified:

  • crates.py --self-test passes.
  • crates.py check passes on this tree.
  • publish-plan against real crates.io names all 12 crates as never published, as expected.
  • In an ubuntu:24.04 container with stable Rust and only libfontconfig1-dev and pkg-config installed, cargo publish --workspace --dry-run verifies all 12 crates.
  • actionlint is clean on release-mirror.yml.

Not verifiable before the first publish: release-plz and semver-checks against a real published baseline, and trusted publishing itself. They first run when preparing the release after v0.3.0.

Reviews (pi, sol):

  • The fuller design passed after six rounds. You chose the minimal one, which passed in one round.
  • The implementation passed in one round.

One-time actions for you (also in the skill):

  • Protect v* tags on Forgejo and GitHub.
  • Create the crates-io environment on GitHub.
  • At v0.3.0: the first publish by hand, then the trusted publisher on each crate.
Part of #160 (release automation). **Stacked on #197; merge that first** (this branch carries its commit). **Goal:** a forgotten or too-small library bump stops the release, and publishing needs no long-lived credential. This is the minimal design by choice. Rare cases are left to the review step in the release skill. - **Prepare:** `just release-prepare` runs `release-plz update`. It bumps changed libraries (patch, or minor when cargo-semver-checks finds a break) and raises the requirements on them. `release-plz.toml` excludes felis-cli, felis-client and felis-daemon so the app version never moves. release-plz and cargo-semver-checks join the dev shell. - **Check (tag gate, `verify-tag`):** `tools/release/crates.py check` refuses the tag when: - a requirement is not its crate's version; - `cargo semver-checks` fails for a library or felis-daemon's library target; - `release-plz update` would still change the tree. Crates not yet on crates.io skip the last two. The tag build job also runs `just publish-dry-run`. - **Publish (GitHub `release-mirror.yml`, new `crates` job):** runs after the mirror job, for final tags only. Steps: 1. `crates.py publish-plan` lists the crates whose version is not on crates.io. An empty list ends the job; a never-published crate fails it. 2. `cargo publish --dry-run` builds them with no credential present. 3. `rust-lang/crates-io-auth-action` mints a token through trusted publishing, in the `crates-io` environment. 4. `cargo publish --no-verify` uploads them. - **Procedure:** new skill `.agents/skills/release/SKILL.md`. It covers the release PR, the manual review points, tag and watch, and the one-time setup: tag protection on both forges, the `crates-io` environment, and the first publish for v0.3.0. - **Docs:** workspace.md ("Versioning", "Release gate"), implementation.md (why GitHub and trusted publishing, why release-plz only prepares), testing.md, tools/README.md. **Verified:** - `crates.py --self-test` passes. - `crates.py check` passes on this tree. - `publish-plan` against real crates.io names all 12 crates as never published, as expected. - In an `ubuntu:24.04` container with stable Rust and only `libfontconfig1-dev` and `pkg-config` installed, `cargo publish --workspace --dry-run` verifies all 12 crates. - actionlint is clean on `release-mirror.yml`. **Not verifiable before the first publish:** release-plz and semver-checks against a real published baseline, and trusted publishing itself. They first run when preparing the release after v0.3.0. **Reviews (pi, sol):** - The fuller design passed after six rounds. You chose the minimal one, which passed in one round. - The implementation passed in one round. **One-time actions for you (also in the skill):** - Protect `v*` tags on Forgejo and GitHub. - Create the `crates-io` environment on GitHub. - At v0.3.0: the first publish by hand, then the trusted publisher on each crate.
workspace, ci, docs: make every crate publishable and package them in CI
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
windows / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 46s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m12s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 51s
pr / nix flake check (pull_request) Successful in 27s
windows / cargo nextest (Windows) (pull_request) Successful in 2m46s
windows / frontend smoke (Windows) (pull_request) Successful in 43s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m8s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 26s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
1b2e030396
The #160 amendment publishes every crate under crates/, with the
libraries on their own versions and the binary crates on the app
version. Until now each member inherited publish = false and the
workspace version, so nothing could be packaged and a packaging break
would surface only at the first real publish.

The libraries move to 0.1.0, and the internal workspace dependencies
carry that version next to their paths. Dev-dependencies on felis
crates name the path alone, so cargo package drops them; a versioned
one would leave the felis-client-core <-> felis-daemon cycle
unpackageable. The new publish-dry-run gate packages all twelve crates
and builds each against the others in publish order.

Assisted-by: Claude Code
release, ci, docs, skills: bump, check and publish the crates with the app release
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 14s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 53s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 4m36s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 3m48s
pr / nix flake check (pull_request) Successful in 1m16s
windows / cargo nextest (Windows) (pull_request) Successful in 5m51s
windows / frontend smoke (Windows) (pull_request) Successful in 3m42s
pr / cargo build / clippy / test / deny (pull_request) Successful in 6m52s
pr / wire schema is compatible with the base (pull_request) Successful in 8m44s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 5m15s
windows / cargo clippy (Windows cross) (pull_request) Successful in 4m44s
1d0e745430
Library crates now carry their own versions, and nothing bumped,
checked or published them. A forgotten bump was silent: the PR dry-run
overlays local sources, so a library changed after its publish passed
every check while crates.io kept serving the old code.

release-plz prepares the bumps (`just release-prepare`), with the
binaries excluded so it never moves the app version. The tag gate's
crate check refuses a tree it would still change, a bump smaller than
cargo-semver-checks asks for, or a requirement that is not its crate's
version. A GitHub job publishes through trusted publishing after the
Forgejo release, so no long-lived token exists; verification builds run
before the token is minted. Rare cases (breaks reaching consumers
through a dependency's types, platform-only APIs) are left to the
release skill's review step.

Assisted-by: Claude Code
natsukium deleted branch release/crates-publish 2026-10-19 23:51:16 +09:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis!198
No description provided.