release: publish the library crates to crates.io with versions decoupled from the app #160

Closed
opened 2026-10-18 12:55:35 +09:00 by natsukium · 3 comments
Owner

Why now

The library crates have outside consumers: spoor uses felis-vt / felis-grid through a git tag, and adroit, a desktop app, wants to embed felis-vt / felis-grid in-process in place of alacritty_terminal (GitHub mirror: https://github.com/felis-terminal/felis/issues/1, https://github.com/felis-terminal/felis/issues/2, https://github.com/felis-terminal/felis/issues/3, https://github.com/felis-terminal/felis/issues/4). crates.io rejects git dependencies, so a consumer that publishes itself cannot depend on felis until felis publishes. The Rust APIs are still unstable, and the release has to say so rather than wait for them to settle.

Policy

  • Published set: felis-protocol, felis-vt, felis-grid, plus the daemon-free crates split out for the embedder requests (the key encoder, the Kitty graphics dispatch) when they land. felis-transport, felis-client-core, felis-shaping, felis-render-wgpu and felis-daemon stay unpublished until an outside consumer needs one. Binaries are not published; distribution stays Nix, Homebrew and the release archives.
  • Versions decoupled from the app. Each published library carries its own version, starting at 0.1.0, instead of [workspace.package] version. Lockstep would force an app minor release for every library API break, and bump libraries that did not change.
  • Published with the app release. A felis release publishes the libraries that changed since their last publish; cargo semver-checks against the last published version decides patch vs minor. One release cadence, independent numbers.
  • Unstable by declaration. 0.x, and a minor bump may break the API (Cargo's 0.x rule); no 1.0 commitment. Each crate's README / crate-level rustdoc says so, and says the wire contract is PROTOCOL_MINOR and the minor ledger, never the crate version. felis-protocol needs this most: its wire is stable while its Rust API is not.
  • Light API pass, not a shrink-first rewrite. Daemon-only internals get pub(crate) or #[doc(hidden)]; what embedders use gets rustdoc. spoor's and adroit's usage is the floor.

Work

  • Per-crate version and publish in the published crates' Cargo.toml; workspace publish = false stays for the rest. Intra-workspace dependencies carry version alongside path.
  • README and crate-level rustdoc per published crate with the stability statement.
  • API pass on felis-protocol, felis-vt, felis-grid.
  • CI: cargo publish --dry-run and cargo semver-checks for the published set.
  • Release automation: first publish by hand in dependency order (protocol → vt → grid). For later releases, check whether crates.io trusted publishing can run from the GitHub release-mirror.yml (it supports GitHub Actions; Forgejo Actions is assumed unsupported, to verify), else a scoped token in the Forgejo release workflow.
  • Docs: docs/reference/workspace.md ("none is published", crate map, a reuse path for in-process embedders), docs/explanation/implementation.md "Why the crate version carries no wire information" plus a section on the decoupled versioning and its rejected lockstep alternative, docs/reference/testing.md "CI shape", CONTRIBUTING.md release steps, CHANGELOG.md.

Timing

First publish with the release that lands the embedder splits (expected v0.3.0). Until then consumers pin the GitHub tag.

## Why now The library crates have outside consumers: spoor uses felis-vt / felis-grid through a git tag, and adroit, a desktop app, wants to embed felis-vt / felis-grid in-process in place of alacritty_terminal (GitHub mirror: https://github.com/felis-terminal/felis/issues/1, https://github.com/felis-terminal/felis/issues/2, https://github.com/felis-terminal/felis/issues/3, https://github.com/felis-terminal/felis/issues/4). crates.io rejects git dependencies, so a consumer that publishes itself cannot depend on felis until felis publishes. The Rust APIs are still unstable, and the release has to say so rather than wait for them to settle. ## Policy - **Published set:** felis-protocol, felis-vt, felis-grid, plus the daemon-free crates split out for the embedder requests (the key encoder, the Kitty graphics dispatch) when they land. felis-transport, felis-client-core, felis-shaping, felis-render-wgpu and felis-daemon stay unpublished until an outside consumer needs one. Binaries are not published; distribution stays Nix, Homebrew and the release archives. - **Versions decoupled from the app.** Each published library carries its own version, starting at 0.1.0, instead of `[workspace.package] version`. Lockstep would force an app minor release for every library API break, and bump libraries that did not change. - **Published with the app release.** A felis release publishes the libraries that changed since their last publish; `cargo semver-checks` against the last published version decides patch vs minor. One release cadence, independent numbers. - **Unstable by declaration.** 0.x, and a minor bump may break the API (Cargo's 0.x rule); no 1.0 commitment. Each crate's README / crate-level rustdoc says so, and says the wire contract is `PROTOCOL_MINOR` and the minor ledger, never the crate version. felis-protocol needs this most: its wire is stable while its Rust API is not. - **Light API pass, not a shrink-first rewrite.** Daemon-only internals get `pub(crate)` or `#[doc(hidden)]`; what embedders use gets rustdoc. spoor's and adroit's usage is the floor. ## Work - [ ] Per-crate `version` and `publish` in the published crates' `Cargo.toml`; workspace `publish = false` stays for the rest. Intra-workspace dependencies carry `version` alongside `path`. - [ ] README and crate-level rustdoc per published crate with the stability statement. - [ ] API pass on felis-protocol, felis-vt, felis-grid. - [ ] CI: `cargo publish --dry-run` and `cargo semver-checks` for the published set. - [ ] Release automation: first publish by hand in dependency order (protocol → vt → grid). For later releases, check whether crates.io trusted publishing can run from the GitHub `release-mirror.yml` (it supports GitHub Actions; Forgejo Actions is assumed unsupported, to verify), else a scoped token in the Forgejo release workflow. - [ ] Docs: `docs/reference/workspace.md` ("none is published", crate map, a reuse path for in-process embedders), `docs/explanation/implementation.md` "Why the crate version carries no wire information" plus a section on the decoupled versioning and its rejected lockstep alternative, `docs/reference/testing.md` "CI shape", `CONTRIBUTING.md` release steps, `CHANGELOG.md`. ## Timing First publish with the release that lands the embedder splits (expected v0.3.0). Until then consumers pin the GitHub tag.
Author
Owner

Two updates from the renderer split (#170–#173):

  • New crate felis-render-core (palette, box-drawing rasterizer, glyph index, cell painter and its display list; depends on felis-protocol, felis-grid, felis-shaping). By this issue's policy it belongs with felis-shaping and felis-render-wgpu: unpublished until an outside consumer needs it. Publishing it later pulls felis-shaping into the published set. The crates.io name is free as of today.
  • Dependency guards that would ship in the packages. crates/felis-protocol/tests/crate_purity.rs and crates/felis-grid/tests/no_render_deps.rs shell out to cargo tree over the workspace; once those crates are published, the tests ship in the package and fail outside the workspace. They move into deny.toml [bans] (wrappers entries, as #171 did for wgpu/winit) in a separate PR before the first publish.
Two updates from the renderer split (#170–#173): - **New crate `felis-render-core`** (palette, box-drawing rasterizer, glyph index, cell painter and its display list; depends on felis-protocol, felis-grid, felis-shaping). By this issue's policy it belongs with felis-shaping and felis-render-wgpu: unpublished until an outside consumer needs it. Publishing it later pulls felis-shaping into the published set. The crates.io name is free as of today. - **Dependency guards that would ship in the packages.** `crates/felis-protocol/tests/crate_purity.rs` and `crates/felis-grid/tests/no_render_deps.rs` shell out to `cargo tree` over the workspace; once those crates are published, the tests ship in the package and fail outside the workspace. They move into `deny.toml` `[bans]` (`wrappers` entries, as #171 did for `wgpu`/`winit`) in a separate PR before the first publish.
Author
Owner

Policy amendment (2026-10-19)

This overrides "Published set" in the body and the felis-render-core note in the first comment.

  • Published set: every crate under crates/, the binaries included (felis-cli, felis-client, felis-daemon); the tests/ member (felis-workspace-tests) stays unpublished. cargo install felis-cli felis-client felis-daemon becomes a way to get felis, with no support claim: no CI gate runs on it, and it lacks what the archives and Nix ship besides the binaries (terminfo, desktop entry, man pages, completions). felis-cli looks for felis-client next to its own executable first, so a cargo install of all three finds it. felis is held by an unrelated crate, so the front door is felis-cli.
  • Versions. Library crates keep their own versions, starting at 0.1.0, as in the body. The three binary crates carry the app version ([workspace.package] version), the one the release tag and felis version name; felis-daemon's library API, which embedders use, breaks with an app minor.
  • Work changes.
    • tools/release/verify.py (and its self-test) requires only the three binary crates to inherit the workspace version; today it refuses any crate that does not.
    • Publish order follows the dependency graph across all crates. CI runs cargo publish --workspace --dry-run, which packages and builds the members in that order against each other rather than against crates.io, so it passes before the first publish. cargo semver-checks covers the library crates and felis-daemon once each has a published baseline.
    • felis-grid answers XTVERSION with a hardcoded felis and its own CARGO_PKG_VERSION, and DA3 with the hardcoded unit ID felis-1 (crates/felis-grid/src/sink.rs). The identity is the host's, not the grid's: under independent versions a 0.3.0 app would reply felis 0.1.0, and an embedder would answer as felis. The grid keeps answering both, in order with its other replies, from an identity the host supplies (the daemon: felis <app version> and felis-1, the replies it sends today). With none set, XTVERSION stays silent and DA3 answers xterm's all-zero unit ID. The daemon sets it on grids restored by an in-place upgrade too, since a dump from 0.2.0 carries none; test harnesses that pin the replies (the protocol coverage matrix, the vttest smoke) set it explicitly.
    • Dev-dependencies between felis crates stay path-only, with no version, so cargo package drops them. The body's "intra-workspace dependencies carry version" applies to normal dependencies only. felis-client-core and felis-daemon dev-depend on each other; with version on those entries neither can be packaged before the other is on crates.io, and cargo publish --workspace --dry-run fails on the cycle. The tests that need such a dev-dependency do not build from the extracted package, like the next item's.
    • A test that reads a file outside its own crate (e.g. felis-protocol's ledger checks against docs/reference/ipc.md) fails in an extracted package; it moves to the tests/ member or out of the package.
    • The docs item adds the cargo install path to docs/reference/workspace.md as unsupported.
## Policy amendment (2026-10-19) This overrides "Published set" in the body and the `felis-render-core` note in the first comment. - **Published set: every crate under `crates/`**, the binaries included (felis-cli, felis-client, felis-daemon); the `tests/` member (`felis-workspace-tests`) stays unpublished. `cargo install felis-cli felis-client felis-daemon` becomes a way to get felis, with no support claim: no CI gate runs on it, and it lacks what the archives and Nix ship besides the binaries (terminfo, desktop entry, man pages, completions). felis-cli looks for felis-client next to its own executable first, so a `cargo install` of all three finds it. `felis` is held by an unrelated crate, so the front door is `felis-cli`. - **Versions.** Library crates keep their own versions, starting at 0.1.0, as in the body. The three binary crates carry the app version (`[workspace.package] version`), the one the release tag and `felis version` name; felis-daemon's library API, which embedders use, breaks with an app minor. - **Work changes.** - `tools/release/verify.py` (and its self-test) requires only the three binary crates to inherit the workspace version; today it refuses any crate that does not. - Publish order follows the dependency graph across all crates. CI runs `cargo publish --workspace --dry-run`, which packages and builds the members in that order against each other rather than against crates.io, so it passes before the first publish. `cargo semver-checks` covers the library crates and felis-daemon once each has a published baseline. - felis-grid answers XTVERSION with a hardcoded `felis` and its own `CARGO_PKG_VERSION`, and DA3 with the hardcoded unit ID `felis-1` (`crates/felis-grid/src/sink.rs`). The identity is the host's, not the grid's: under independent versions a 0.3.0 app would reply `felis 0.1.0`, and an embedder would answer as felis. The grid keeps answering both, in order with its other replies, from an identity the host supplies (the daemon: `felis <app version>` and `felis-1`, the replies it sends today). With none set, XTVERSION stays silent and DA3 answers xterm's all-zero unit ID. The daemon sets it on grids restored by an in-place upgrade too, since a dump from 0.2.0 carries none; test harnesses that pin the replies (the protocol coverage matrix, the vttest smoke) set it explicitly. - Dev-dependencies between felis crates stay path-only, with no `version`, so `cargo package` drops them. The body's "intra-workspace dependencies carry `version`" applies to normal dependencies only. felis-client-core and felis-daemon dev-depend on each other; with `version` on those entries neither can be packaged before the other is on crates.io, and `cargo publish --workspace --dry-run` fails on the cycle. The tests that need such a dev-dependency do not build from the extracted package, like the next item's. - A test that reads a file outside its own crate (e.g. felis-protocol's ledger checks against `docs/reference/ipc.md`) fails in an extracted package; it moves to the `tests/` member or out of the package. - The docs item adds the `cargo install` path to `docs/reference/workspace.md` as unsupported.
Author
Owner

Shipped in v0.3.0: every crate is on crates.io, with trusted publishing from the GitHub mirror.

Shipped in v0.3.0: every crate is on crates.io, with trusted publishing from the GitHub mirror.
Sign in to join this conversation.
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#160
No description provided.