diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7702f7..c08767e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,24 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + # Pinned, and kept in step with `devVersion` in flake.nix, so CI and + # the dev shell agree and a new stable release cannot turn a lint + # failure into an unrelated red build. + - uses: dtolnay/rust-toolchain@1.99.1 with: components: rustfmt, clippy - run: cargo build --all-targets - run: cargo test - run: cargo fmt --check - run: cargo clippy --all-targets -- -D warnings + + msrv: + name: Build on the declared MSRV + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Must match `rust-version` in Cargo.toml and `msrvVersion` in + # flake.nix. Builds only — lint and format output varies by release, + # and pinning those to the oldest supported toolchain is not useful. + - uses: dtolnay/rust-toolchain@1.88.0 + - run: cargo test diff --git a/CHANGELOG.md b/CHANGELOG.md index 209d90d..bf8b4b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- A declared minimum supported Rust version of **1.88**, verified by running + the test suite against it. The floor comes from the dependency graph + (`reqwest` → `url` → `idna` → `icu_*`), not from this crate's own code. + CI builds on both the MSRV and the pinned dev toolchain. - Wrappers for every one of the API's 34 `Route::resource` groups, plus `users` — 265 methods. See `COVERAGE.md` for the per-endpoint inventory. - `COVERAGE.md`, a detailed record of what is wrapped, what is not, and why, diff --git a/CLAUDE.md b/CLAUDE.md index 43fa234..03bdb09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,6 +14,36 @@ This project uses Nix for toolchain management (Rust via `rust-overlay`): nix develop ``` +### Rust version + +Two versions are pinned, and **three places must agree** when either moves: + +| | Version | Declared in | +| --- | --- | --- | +| Dev toolchain | 1.99.1 | `devVersion` in `flake.nix`, `test` job in `ci.yml` | +| MSRV | 1.88 | `msrvVersion` in `flake.nix`, `rust-version` in `Cargo.toml`, `msrv` job in `ci.yml` | + +The MSRV is set by the **dependency graph**, not by this crate's code: +`reqwest` pulls in `url` → `idna` → `idna_adapter` → `icu_*`, and the `icu` +crates require 1.88. Recheck the floor after a dependency bump with: + +```sh +cargo metadata --format-version 1 \ + | jq -r '.packages[] | select(.rust_version) | "\(.rust_version)\t\(.name)"' \ + | sort -V | tail -5 +``` + +Verify a candidate MSRV actually builds before declaring it — the number +above was confirmed by running the suite, not inferred: + +```sh +CARGO_TARGET_DIR=/tmp/msrv-target nix develop .#msrv --command cargo test +``` + +Don't reach for a newer std API without checking it against the MSRV; a +convenience like `Option::is_none_or` (1.82) is rarely worth raising the +floor for. + ## Common commands ```sh diff --git a/Cargo.toml b/Cargo.toml index 444aaf5..ae6eaee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,11 @@ name = "invoiceninjars" version = "0.1.0" edition = "2021" +# Set by the dependency graph rather than by this crate's own code: reqwest +# pulls in url -> idna -> idna_adapter -> icu_*, and the icu crates require +# 1.88. Verified by running the test suite against it; see the `msrv` +# devShell in flake.nix. +rust-version = "1.88" description = "An async Rust client for the Invoice Ninja v5 API" license = "MIT" repository = "https://github.com/bryant/invoiceninjars" diff --git a/README.md b/README.md index 8166505..b23d63d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ An async Rust client for the [Invoice Ninja](https://www.invoiceninja.com/) v5 API. +Requires Rust 1.88 or newer. + ## Development Enter the dev shell (Rust toolchain via `rust-overlay`): @@ -10,6 +12,14 @@ Enter the dev shell (Rust toolchain via `rust-overlay`): nix develop ``` +The dev toolchain is pinned in `flake.nix` rather than tracking latest +stable, so it only moves deliberately. To check the crate still builds on +the declared minimum of 1.88: + +```sh +nix develop .#msrv --command cargo test +``` + ## Usage ```rust diff --git a/flake.nix b/flake.nix index c3f49ee..007ff6f 100644 --- a/flake.nix +++ b/flake.nix @@ -16,19 +16,36 @@ overlays = [ rust-overlay.overlays.default ]; }; - rustToolchain = pkgs.rust-bin.stable.latest.default.override { - extensions = [ "rust-src" "rust-analyzer" "clippy" "rustfmt" ]; + # Pinned rather than `stable.latest`, so the toolchain only moves + # when this line does. `latest` drifts with the flake inputs, which + # means a lint can start failing on an unrelated change. + devVersion = "1.99.1"; + + # Must match `rust-version` in Cargo.toml. The floor comes from the + # dependency graph, not from this crate's own code: reqwest pulls in + # url -> idna -> idna_adapter -> icu_*, and the icu crates require + # 1.88. Check with: + # cargo metadata --format-version 1 \ + # | jq -r '.packages[] | select(.rust_version) | .rust_version' \ + # | sort -V | tail -1 + msrvVersion = "1.88.0"; + + toolchainFor = version: extensions: + pkgs.rust-bin.stable.${version}.default.override { inherit extensions; }; + + rustToolchain = toolchainFor devVersion [ "rust-src" "rust-analyzer" "clippy" "rustfmt" ]; + msrvToolchain = toolchainFor msrvVersion [ ]; + + shellFor = toolchain: pkgs.mkShell { + packages = [ toolchain pkgs.pkg-config pkgs.openssl ]; + RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library"; }; in { - devShells.default = pkgs.mkShell { - packages = [ - rustToolchain - pkgs.pkg-config - pkgs.openssl - ]; + devShells.default = shellFor rustToolchain; - RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; - }; + # Verifies the declared MSRV still builds: + # nix develop .#msrv --command cargo test + devShells.msrv = shellFor msrvToolchain; }); }