Skip to content

Upgrade to Rust 1.98.1 and refresh dependencies - #366

Open
aram356 wants to merge 9 commits into
mainfrom
chore/upgrade-rust-1.98-deps
Open

Upgrade to Rust 1.98.1 and refresh dependencies#366
aram356 wants to merge 9 commits into
mainfrom
chore/upgrade-rust-1.98-deps

Conversation

@aram356

@aram356 aram356 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Moves the pinned toolchain from 1.95.0 to 1.98.1 (current stable) and brings every direct dependency to its latest release — the 162 semver-compatible updates plus seven majors that cargo update alone cannot reach.
  • Fixes the new clippy restriction lints in code where a fix exists, and migrates examples/app-demo to edition 2024 so it matches the main workspace and the scaffold templates.
  • Fixes a latent bug in the persistent KV store that redb 4.2.0's new assertion caught (details below).

Changes

Crate / File Change
.tool-versions, examples/app-demo/.../rust-toolchain.toml Rust 1.95.01.98.1
crates/edgezero-cli/src/generator.rs Scaffold pins 1.98.1 and the current tool/crate versions, so new apps match this repo; drift tests assert both against the real files
Cargo.toml, Cargo.lock 162 package updates + 7 majors (brotli, similar, validator, fastly, log-fastly, rusqlite, sha2); inline_modules allow
crates/edgezero-adapter-axum/src/key_value_store.rs get_bytes copies the value out before dropping the read txn
crates/edgezero-core/src/key_value_store.rs MIN_TTL uses Duration::from_mins(1); stale #[expect] removed
-core/src/canonical_form.rs, -adapter-fastly/src/chunked_config.rs Digest hex via base16ct::lower::encode_string
edgezero-core, -adapter, -adapter-fastly, -adapter-axum, -cli, -macros Generic bounds moved to where clauses (48 sites in total, incl. the wasm-only ones below)
crates/edgezero-core/src/{app_config,test_env}.rs, -adapter-fastly/src/cli.rs #[expect] for the unsatisfiable pin_drop lint
crates/edgezero-cli/src/templates/root/Cargo.toml.hbs Generated apps get the inline_modules allow too
examples/app-demo/** Edition 2024; EnvOverride replaces unsafe env access; let-chain collapse
-adapter-{cloudflare,spin,fastly} wasm paths + tests/contract.rs 7 of those 48 sit in code only the wasm targets compile
CLAUDE.md, deploy-core scripts/tests Toolchain references updated to 1.98.1
.tool-versions, deploy-fastly/versions.json Fastly CLI 15.1.0→16.0.0 (+ real checksum), Viceroy 0.17.0→0.21.0, wasmtime 44.0.1→48.0.1, Node 24.12.0→24.20.0

New clippy lints

Three releases' worth arrive at once, because the workspace enables the entire restriction group at deny.

Fixed in code:

  • inline_trait_bounds (48 sites) — bounds moved to where clauses.
  • duration_suboptimal_unitsDuration::from_mins is const-stable as of 1.98, so MIN_TTL uses it and its #[expect] is dropped.

Allowed, with reasons:

  • inline_modules flags every #[cfg(test)] mod tests block — 77 of them. Complying means splitting each into a sibling file, which contradicts CLAUDE.md's "colocate tests with implementation modules". Colocation is also what std, tokio and serde do. Allowed in the workspace, the demo, and the scaffold template so generated apps don't inherit the problem.
  • missing_trait_methods now wants Drop::pin_drop on three RAII guards. That method sits behind the unstable pin_ergonomics feature, and the compiler rejects implementing it alongside drop (conflicting implementations of Drop::drop and Drop::pin_drop) — no stable code satisfies the lint. Each site gets an #[expect], which will itself fail once the method stabilises, prompting a revisit.

redb 4.2.0 caught a real bug

redb 4.2.0 initially failed ttl_expires_entry and contract_ttl_expires with a panic pointing into redb's own page manager:

assertion failed: !self.read_page_ref_counts.lock().unwrap().contains_key(&page)

This reads like an upstream regression but isn't. redb added an assertion that read references must not outlive their transaction, and it caught get_bytes holding an AccessGuard (borrowed from the table) across the drop(table); drop(read_txn) in its lazy-expiry path — silently wrong on 4.1.x. The value is now copied out before the transaction ends, so the update goes in rather than being pinned back. Other drop(table) sites in that file are write transactions with no live read guard.

CLI tooling

Each pin was verified against the workload CI runs it for, not just installed:

Tool Bump Verification
Fastly CLI 15.1.0 → 16.0.0 Major carries no breaking changes (starter-kit bounds fix + dep bumps). Every subcommand the adapter shells out to still exists with the same flags.
Viceroy 0.17.0 → 0.21.0 Runs the Fastly wasip1 suites — 6 contract + 88 runtime unit tests pass.
wasmtime 44.0.1 → 48.0.1 Runs the Spin wasip2 contract suite — 12 tests, same result as 44.0.1.
Node.js 24.12.0 → 24.20.0 Stays on the Krypton LTS line rather than jumping to 26.x. Docs site lints and builds.

deploy-fastly/versions.json moves in lockstep with a real SHA-256 for the v16.0.0 linux-amd64 archive, cross-checked against the fastly_v16.0.0_SHA256SUMS release asset. This matters: install-fastly.sh fails closed when versions.json and .tool-versions disagree, and the real-install job downloads and verifies the archive for real — a wrong digest breaks CI rather than shipping.

The scaffold generator pins all four, so generated apps get the same set, and its drift test covers nodejs/fastly/viceroy alongside rust. I confirmed it fails, naming the offending tool, when a pin is changed in only one of the two files.

Host clippy does not cover the wasm targets

cargo clippy --fix runs against the host target, so it never linted code behind target_arch = "wasm32" or the adapters' wasm-only tests/contract.rs. CI's per-target clippy matrix caught 7 surviving inline_trait_bounds sites there. Worth knowing for future lint sweeps: a clean cargo clippy --workspace --all-features on the host does not imply the wasm targets are clean.

Dependency majors

cargo update only moves within the ranges the manifests allow, so seven crates sat a major behind. Six upgrade:

Crate Bump Notes
brotli 8 → 9 No source changes
similar 2 → 3 No source changes
validator 0.20 → 0.21 Both workspaces — the demo pins it separately
fastly, log-fastly 0.12 → 0.13 88 runtime + 6 contract tests pass under Viceroy
rusqlite 0.32 → 0.40 prepare_cached is no longer on Transaction; the one call site uses prepare, which still prepares once and reuses across the batch
redb 4.1.0 → 4.2.0 See the AccessGuard fix above

spin-sdk stays at ~6.0. 7.0 compiles clean on wasm32-wasip2 and its 12 contract tests pass under wasmtime — but it imports wasi:http/types@0.3.0, which Spin 4.0.2 does not provide, so the component fails to link at spin up:

Error: component imports instance `wasi:http/types@0.3.0`, but a
matching implementation was not found in the linker

Neither cargo check nor the wasmtime-hosted tests can see this — only the real runtime. The pin comment now records the error verbatim so the next attempt starts from evidence rather than repeating the experiment. Reverting restores smoke_test_kv.sh spin to 8/8.

The scaffold generator seeds validator and fastly, and its own comment requires the validator major to match this workspace or a generated crate fails against edgezero-core's re-derived Validate. Both seeds move, and a new test asserts the shared seeds track the root manifest — confirmed to fail, naming both versions, when they drift.

sha2 0.10 → 0.11

finalize()/digest() now return hybrid_array::Array, which — unlike 0.10's GenericArray — does not implement LowerHex, so both format!("{:x}", ..) call sites stop compiling.

Hand-rolling the hex is a trap: each version trips a different restriction lint in turn (indexing_slicing for a lookup table, then as-truncation, then arithmetic_side_effects for nibble maths). base16ct is RustCrypto's own encoder — same org as sha2, no_std, zero dependencies of its own — and lower::encode_string restores both sites to a one-liner.

Taking 0.11 on our direct dependency drops sha2 0.10 from the tree. One older copy remains — fastly 0.13 pulls sha2 0.9 transitively — so this goes from three copies to two, not to one.

Output is unchanged, which matters because these digests are persisted in config envelopes: canonical_form_pin_v1 still pins 903a0e4a…, and the 51 chunked_config tests — including the writer/resolver round trip that reads an envelope back by its embedded digest — pass under Viceroy on wasm32-wasip1.

A fourth validator pin, and a gate for it

The first push of the validator 0.20 → 0.21 bump turned six deploy smoke jobs red:

error[E0277]: the trait bound `FixtureAppConfig: validator::traits::Validate`
is not satisfied

make-smoke-fixture.sh generates an app-owned CLI at CI time that takes edgezero-core by path but pins validator itself, so the derive expanded against one Validate while the crate imported another. Rebuilding the fixture locally reproduces the error at 0.20 and builds clean at 0.21.

That was the pin turning up in a fourth place, one failure at a time — this workspace, the excluded demo workspace, the scaffold seeds, and a manifest that does not exist until CI generates it. scripts/check_shared_dep_pins.sh now compares all four against the root manifest and runs beside the existing gates in test.yml; it is verified to report each location, by name and version, when only one is changed. Cargo cannot do this itself for any of the three satellites.

Toolchain references

Updated to 1.98.1 in CLAUDE.md, the two toolchain-file parsers' illustrative versions, and the deploy-core test-harness defaults. Two deliberate exclusions:

  • docs/superpowers/** keeps its 1.95 mentions — dated design records describing the stack as it was when each was written, and srcExcluded from the published site.
  • Two literals in deploy-core/tests/run.sh pair with a 1.60.0 fixture to assert an app's .tool-versions beats the deployer's; the values are arbitrary, only precedence matters.

The pub_with_shorthand comment claimed 6 offending items (verified on 1.95). Re-running the check on 1.98 reports 44, so the note was corrected rather than renumbered.

Note for reviewers

Rust here is managed by asdf, not rustup. After pulling this branch, run asdf install rust 1.98.1 followed by rustup target add wasm32-wasip1 wasm32-wasip2 wasm32-unknown-unknown. Without the asdf install, cargo fails with No version is set for command cargo — and a clippy run can exit 0 on that message alone, which looks like a pass.

Closes

Closes #365

Test plan

Verified on rustc 1.98.1 (48a229cea 2026-09-01):

  • cargo test --workspace --all-targets — 1424 passed, 0 failed
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • WASM builds: wasm32-wasip1 (Fastly) / wasm32-wasip2 (Spin) / wasm32-unknown-unknown (Cloudflare)
  • Per-target clippy, all 4 CI matrix combinations (cloudflare, fastly, fastly+cli, spin)
  • .github/actions/deploy-core/tests/run.sh — 260 passed, 0 failed
  • Fastly wasm suites under Viceroy 0.21.0 and Spin contract suite under wasmtime 48.0.1
  • examples/app-demo workspace: 33 passed, 0 failed, plus clippy clean on edition 2024
  • Docs build — npm ci && npm run lint && npm run build on Node 24.20.0
  • Manual testing via edgezero serve --adapter axum — not run; covered by the axum adapter's test suite
  • Other: pinned-hash regression test canonical_form_pin_v1 passes, confirming config-envelope hashes are unchanged
  • scripts/smoke_test_kv.sh spin — 8/8 against a real spin up (this is what caught the spin-sdk 7 blocker)

Checklist

  • Changes follow CLAUDE.md conventions
  • No Tokio deps added to core or adapter crates
  • Route params use {id} syntax (not :id)
  • Types imported from edgezero_core (not http crate)
  • Store wiring goes through KvRegistry / ConfigRegistry / SecretRegistry (not the legacy single-handle setters)
  • New code has tests — two generator drift tests (tool versions, seeded crates) plus check_shared_dep_pins.sh wired into CI
  • No secrets or credentials committed

Move the pinned toolchain from 1.95.0 to 1.98.1 (current stable) and
take every semver-compatible dependency update.

Toolchain pins move together so a generated app builds on the same
release this repo does: `.tool-versions`, the demo's Fastly
`rust-toolchain.toml`, and the CLI scaffold generator. A new test
asserts the generator's pin matches `.tool-versions` -- previously
only a doc comment asked the two to be bumped in step, and they had
already drifted.

Three releases of new clippy `restriction` lints land with the bump:

- `inline_trait_bounds` (~42 sites) -- generic bounds moved to `where`
  clauses.
- `duration_suboptimal_units` -- `Duration::from_mins` is const-stable
  as of 1.98, so `MIN_TTL` uses it and its `#[expect]` is dropped.
- `inline_modules` -- allowed workspace-wide, in the demo, and in the
  scaffold template. It flags every `#[cfg(test)] mod tests` block;
  colocated tests are the mainstream Rust idiom and what CLAUDE.md
  mandates.
- `missing_trait_methods` now wants `Drop::pin_drop` on three RAII
  guards. That method is behind the unstable `pin_ergonomics` feature
  and the compiler rejects implementing it alongside `drop`, so no
  stable code satisfies the lint; each site gets an `#[expect]` that
  will fail once the method stabilises.

redb 4.2.0 added an assertion that read references not outlive their
transaction, which caught a latent bug in the persistent KV store:
`get_bytes` held an `AccessGuard` borrowed from the table across the
`drop(table); drop(read_txn)` in its lazy-expiry path. The value is now
copied out before the transaction ends, so the update goes in rather
than being pinned back.

The demo workspace moves to edition 2024, matching the main workspace
and the scaffold templates. `cargo fix --edition` wrapped the config
test's `env::set_var`/`remove_var` in `unsafe` blocks with FIXMEs; the
demo denies `unsafe_code`, so the test uses the existing
`edgezero_core::test_env::EnvOverride` guard instead, which also
restores the variable when an assertion panics. Edition 2024 let-chains
collapse a nested `if let` in the demo handlers.

All five CI gates pass on 1.98.1, and the three wasm targets
(wasip1/wasip2/unknown-unknown) compile.
The `inline_trait_bounds` sweep ran on the host target, so it never
linted code behind `target_arch = "wasm32"` or the adapters' wasm-only
`tests/contract.rs`. CI's per-target clippy matrix caught seven
remaining sites across the three adapters; all move to `where` clauses.

Toolchain references outside `.tool-versions` now read 1.98.1: the
CLAUDE.md toolchain table, the illustrative version in the two
toolchain-file parsers, and the deploy-core test harness defaults that
stand in for a real deploy.

Two literals in `deploy-core/tests/run.sh` stay at 1.95.0 on purpose --
they pair with a 1.60.0 fixture to assert that an app's own
`.tool-versions` wins over the deployer's, so the values are arbitrary
and only their precedence matters.

The `pub_with_shorthand` note claimed 6 offending items, verified on
clippy 1.95. Re-running the check on 1.98 with the allow removed
reports 44, so the comment now says 44 rather than carrying a stale
count forward.

`docs/superpowers/**` keeps its 1.95 mentions: those are dated design
records describing the stack as it was when each was written, and the
VitePress config excludes them from the published site.
Moves the tools `.tool-versions` pins to current releases, each
verified against the workload CI actually runs it for:

- Fastly CLI 15.1.0 -> 16.0.0. The major carries no breaking changes
  (a starter-kit bounds fix and dependency bumps); every subcommand the
  adapter shells out to -- `compute serve`, `config-store list --json`,
  `config-store-entry list`, `service`, `service-version`,
  `resource-link` -- still exists with the same flags.
- Viceroy 0.17.0 -> 0.21.0. Runs the Fastly wasip1 suites: 6 contract
  tests and 88 runtime unit tests pass.
- wasmtime 44.0.1 -> 48.0.1. Runs the Spin wasip2 contract suite: 12
  tests, same result as on 44.0.1.
- Node.js 24.12.0 -> 24.20.0, staying on the Krypton LTS line rather
  than jumping to 26.x. The docs site lints and builds on it.

`deploy-fastly/versions.json` moves in lockstep with a real checksum
for the v16.0.0 linux-amd64 archive, cross-checked against the
`fastly_v16.0.0_SHA256SUMS` release asset. `install-fastly.sh` fails
closed when versions.json and `.tool-versions` disagree, and the
`real-install` job downloads and verifies the archive for real, so a
wrong digest here breaks CI rather than shipping.

The scaffold generator pins all four tools, so generated apps get the
same set. Its drift test now covers nodejs, fastly and viceroy
alongside rust -- confirmed to fail, with the offending tool named,
when a pin is changed in only one of the two files.

Two 15.1.0/1.95.0 literals in the deploy-core tests stay put: they are
self-contained fixtures asserting resolution precedence and
checksum-mismatch rejection, where only the relationship between the
values matters.
sha2 0.11 returns `hybrid_array::Array` from `finalize()`/`digest()`.
Unlike 0.10's `GenericArray` it does not implement `LowerHex`, so the
two `format!("{:x}", ..)` call sites no longer compile.

Hand-rolling the hex is the wrong answer here: every version of it
trips a different `restriction` lint in turn (`indexing_slicing` for a
lookup table, `cast_possible_truncation` for `as`, then
`arithmetic_side_effects` for the nibble maths). `base16ct` is
RustCrypto's own encoder -- same org as sha2, `no_std`, and with no
dependencies of its own, so it stays WASM-safe -- and
`lower::encode_string` restores both call sites to a one-liner.

Taking 0.11 on our direct dependency drops sha2 0.10 from the tree
entirely; the workspace now resolves a single sha2.

Output is unchanged: `canonical_form_pin_v1` still pins
903a0e4a..., and the 51 `chunked_config` tests -- including the
writer/resolver round trip that reads back an envelope by its embedded
digest -- pass under Viceroy on wasm32-wasip1.
`cargo update` only moves within the ranges the manifests allow, so
seven crates were still held a major behind. Six of them upgrade:

- brotli 8 -> 9, similar 2 -> 3, validator 0.20 -> 0.21 (both
  workspaces, since the demo pins it separately) -- no source changes.
- fastly and log-fastly 0.12 -> 0.13. The Fastly wasip1 suites pass
  under Viceroy: 88 runtime unit tests and 6 contract tests.
- rusqlite 0.32 -> 0.40. `prepare_cached` is no longer on
  `Transaction`; the one call site in the Spin KV writer uses
  `prepare` instead, which prepares the statement once and reuses it
  across the batch exactly as before. The push_sqlite suite still
  passes, including the round trip through Spin's vendored schema.

spin-sdk stays at ~6.0. 7.0 compiles clean on wasm32-wasip2 and its 12
contract tests pass under wasmtime, but it imports
`wasi:http/types@0.3.0`, which Spin 4.0.2 does not provide -- the
component fails to link at `spin up`, which neither `cargo check` nor
the wasmtime-hosted tests can see. The pin comment now records that
error verbatim so the next attempt starts from the evidence. Reverting
restores `smoke_test_kv.sh spin` to 8/8.

The scaffold generator seeds `validator` and `fastly` for generated
apps, and its own comment requires the validator major to match this
workspace or a generated crate fails to compile against
`edgezero-core`'s re-derived `Validate`. Both seeds move with the
workspace, and a new test asserts the shared seeds track the root
manifest -- confirmed to fail, naming both versions, when they drift.
The six deploy smoke jobs failed on the validator 0.20 -> 0.21 bump:

  error[E0277]: the trait bound `FixtureAppConfig: validator::traits::Validate`
  is not satisfied

`make-smoke-fixture.sh` generates an app-owned CLI that takes
`edgezero-core` by path but pinned `validator` itself. Once the
workspace moved to 0.21 the fixture still asked for 0.20, so the
derive expanded against one `Validate` while the crate imported
another. Confirmed by rebuilding the fixture locally: 0.20 reproduces
the CI error exactly, 0.21 builds.

That made three copies of the same pin found one failure at a time --
the workspace, the demo, the scaffold seeds, and now the fixture --
so `check_shared_dep_pins.sh` compares all of them against the root
manifest and runs beside the existing gates in test.yml. Verified it
reports each location, by name and version, when that pin is changed
in only one file.

Cargo cannot catch this itself: the demo workspace is excluded, the
scaffold seed is a string literal, and the fixture manifest does not
exist until CI generates it.
Self-review of the earlier commits.

`get_bytes` copied the value bytes on every path, including the expired
one where they are dropped two statements later in favour of
`Ok(None)`. It also asked `is_expired` twice about the same value and
left `Ok(Some(value))` depending on an invariant the reader has to
reconstruct -- that the expired branch always returns first, so `value`
cannot be `None` there. Expiry is now decided once, while the
`AccessGuard` is still alive, and the bytes are copied only when they
will be returned; the delete path falls through instead of nesting.

The app-demo env test's comment still described mutating process env
in place and leaned on a sibling test's `env_overlay: false` for
safety. Both stopped being true when it moved to `env_lock` +
`EnvOverride`; the comment now names the lock and the restore-on-drop.

`check_shared_dep_pins.sh` matched only the table form of the pin. It
fails closed on the bare form (`validator = "0.21"`) rather than
passing silently, but that turns a harmless reformat into a red build,
so `pin_in` now accepts either spelling.
The `base16ct` pin carried five lines explaining that sha2 0.11's
`finalize()` stopped implementing `LowerHex`. That is why the change
was made, not something a later reader has to know to avoid breaking
anything, and the commit that introduced it already says so. The pin
now reads like every other entry in the list.

The redb comment spent its second half restating what the two
following statements plainly do. What is worth keeping is the part a
reader cannot see: redb asserts that no read reference outlives its
transaction, so the borrow has to end before the drops. Without that
the code looks needlessly roundabout and the obvious simplification
reintroduces the panic.

The `inline_modules` and `spin-sdk` comments stay. Every other entry
in the clippy allow-list carries its reason, so a bare allow would be
the odd one out and an easy deletion; and the spin-sdk note is what
stops the next attempt rediscovering that 7.0 compiles, passes the
contract tests, and only then fails to link at `spin up`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrade to Rust 1.98.1 and refresh dependencies

1 participant