Skip to content

Upgrade to Rust 1.98.1 and refresh dependencies - #366

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

aram356 wants to merge 16 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 takes the semver-compatible dependency updates plus six of the seven majors cargo update alone cannot reach. Only brotli is held back, with the reason and the measurement recorded at the pin.
  • 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 + 5 majors (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
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
simple_logger 4 → 5 Demo only — the root workspace was already on 5, so this is the demo catching up rather than a new decision
redb 4.1.0 → 4.2.0 See the AccessGuard fix above. The declared floor was 4.1.0 while resolving 4.2.0; 4.2 is the release whose assertion caught that bug, so the floor now matches the behaviour the code relies on
spin-sdk ~6.0 → ~7.0 Needs Spin 4.1+ at runtime (below)

brotli stays at 8 — the one major not taken. async-compression 0.4.44 resolves brotli 8 through compression-codecs 0.4.39 (still the current release), so taking 9 on the direct dependency does not replace that copy — it adds a second complete allocator/decompressor stack that every adapter graph compiles. Measured on the demo Spin release build: 7,169,574 bytes with both stacks, 7,129,410 with one — ~40 KB of wasm for no gain, since we call only Decompressor and CompressorWriter, unchanged across the two majors. When async-compression moves to 9 the pin should follow and the copies collapse.

spin-sdk 6 → 7 needed a runtime bump, not a code change. 7.0 imports wasi:http/types@0.3.0; Spin 4.0.x does not provide it, so the component compiles and passes its wasmtime-hosted contract tests and then 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

Spin 4.1.0 ships that interface. Verified end to end rather than by compiling — scripts/smoke_test_kv.sh spin is 8/8 against a real spin up. The demo workspace and the scaffold seed pin spin-sdk separately and move with it, and .tool-versions now names Spin 4.1 as the floor (it still said 3.7).

Worth flagging for reviewers: cargo check and the wasmtime contract suite both pass on a build that cannot boot. Only the smoke test catches this class.

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.

Review follow-ups

Three issues found reviewing the branch, all fixed in the final commit:

  • The demo lockfile still resolved validator_derive 0.20.0 → proc-macro-error2 2.0.1 — the crate behind the rejected by a future version of Rust warning that printed on every demo build. The root lockfile had already moved to 0.20.1 → proc-macro-error3; the demo now matches and the warning is gone.
  • brotli consolidated back to 8 (above).
  • The demo lockfile had also never taken the earlier refresh sweep, and carried anyhow 1.0.102, which has an unsoundness advisory. Both lockfiles are now refreshed and the demo is on 1.0.104.
  • check_shared_dep_pins.sh read the pin with an unanchored match and took the first hit, so a commented-out # validator = "0.19" above the real line became the expected version and the gate reported three violations against three correct files. The match is now anchored to an assignment at line start, and a miss normalises to empty via || true so the missing-pin diagnostics run instead of set -e aborting at the command substitution.

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 on Spin 4.1.0 with spin-sdk 7

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`.
Three issues from review.

The demo lockfile still resolved validator_derive 0.20.0, which pulls
proc-macro-error2 2.0.1 -- the crate behind the
"rejected by a future version of Rust" warning that has been printing
on every demo build. The root lockfile had already moved to 0.20.1 and
proc-macro-error3; the demo now matches, and the warning is gone.

brotli goes back to 8. `async-compression` 0.4.44 resolves brotli 8
through compression-codecs 0.4.39 (the current release), so taking 9
on the direct dependency never replaced that copy -- it added a second
complete allocator/decompressor stack that every adapter graph
compiles. Measured on the demo Spin release build: 7,169,574 bytes with
both, 7,129,410 with one, so ~40 KB of wasm bought nothing. We call
only `Decompressor` and `CompressorWriter`, which are unchanged across
the two majors. This is the same duplicate-stack test that kept sha2
honest; it should have been applied to every major, not just that one.

`check_shared_dep_pins.sh` read the pin with an unanchored match and
took the first hit, so a commented-out `# validator = "0.19"` above the
real line became the expected version and the gate reported three
violations against three correct files. The match is now anchored to an
assignment at line start (optionally inside a Rust string literal, which
is how the scaffold seeds it), and a miss normalises to empty via
`|| true` so the missing-pin diagnostics run instead of `set -e` killing
the script at the command substitution.
spin-sdk moves to `~7.0`. The earlier hold was accurate about the
symptom -- 7.0 imports `wasi:http/types@0.3.0` and the component fails
to link -- but wrong to treat it as a property of the SDK. It is a
property of the runtime that was installed: Spin 4.0.2 does not carry
that interface and Spin 4.1.0 does. Verified end to end rather than by
compiling: `smoke_test_kv.sh spin` is 8/8 against a real `spin up` on
4.1.0. The demo workspace and the scaffold seed pin spin-sdk
separately and move with it; `.tool-versions` now names Spin 4.1 as the
floor instead of the stale 3.7.

`redb` was declared `4.1.0` while resolving 4.2.0. The caret made that
work, but 4.2 is the release whose read-reference assertion caught the
`AccessGuard` bug fixed earlier in this branch, so the declared floor
now matches the version whose behaviour the code depends on.

`check_shared_dep_pins.sh` had a second, unanchored parser for the
generator seed that the earlier fix missed. A commented-out seed above
the real assignment shadowed it, which both failed a correct tree and
passed an incorrect one; a missing seed printed nothing at all,
because `set -e` aborted at the command substitution. It now calls
`pin_in` like the other paths and reports a missing seed by name.

Also refreshes both lockfiles (20 packages in the root, a larger
backlog in the demo, which had never taken the earlier sweep) and
moves the demo off anyhow 1.0.102, which carries an unsoundness
advisory. brotli stays at 8: compression-codecs 0.4.39 is current and
still resolves brotli 8, so taking 9 adds a second allocator and
decompressor rather than replacing the first.
@aram356
aram356 requested review from ChristianPavilonis and prk-Jr and removed request for prk-Jr September 5, 2026 22:53
`npm update` inside the declared ranges; `package.json` is unchanged.
eslint 10.9.1 -> 10.10.0, @types/node 24.12.4 -> 24.13.3, and
brace-expansion 5.0.6 -> 5.0.9 transitively, which is what Dependabot
#353 proposes.

@types/node stays on the 24 line on purpose: it describes the Node
runtime, and `.tool-versions` pins nodejs 24.20.0 (the active LTS).
Taking the 26 line that Dependabot #368 proposes would type against
APIs the pinned runtime does not have.

vitepress, prettier, @eslint/js and typescript-eslint were already at
their latest releases.

`npm audit` still reports three advisories, all the same root:
vitepress 1.6.4 -> vite 5.4.21 -> esbuild 0.21.5, with no fix
available. 1.6.4 is the newest vitepress (2.x is alpha only), and the
advisory is a dev-server issue in a dev-only dependency, so it does not
reach `vitepress build` or the published site.
`docs/guide/adapters/spin.md` still documented the `~6.0` pin in its
schema-coupling note. It now names `~7.0`, matching the workspace.

The prerequisites also listed "Spin CLI" with no floor. spin-sdk 7
imports `wasi:http/types@0.3.0`, which Spin 4.0.x does not provide, and
the failure mode is unhelpful: the component builds, the contract tests
pass under wasmtime, and only `spin up` reports a linker error naming
an interface the reader never wrote. The requirement and that error are
now stated where someone would look before hitting it.

Checked the rest of the published guide for claims this branch
invalidated: the remaining crate mentions are version-agnostic, and the
`sha256` sections describe the envelope format, which the sha2 0.11
move left byte-identical.

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed 40c52e3f68aeef925d395688d34d601fc269ee4f against 593fc9282a1c56e12bae15f91eef2162f4b6a1b7. The dependency and toolchain refresh is well covered by CI and focused local checks. Approving with two medium compatibility and operational follow-ups noted inline.

Comment thread Cargo.toml
Comment thread Cargo.toml
@aram356 aram356 self-assigned this Sep 7, 2026
Review follow-ups on #366.

The Spin 4.1 requirement was documented in the adapter guide but never
reached the code that enforces it, so both directions were wrong:
`smoke_test_config_key_override.sh` accepted anything from 3.7 up, which
let Spin 4.0 through the pre-check and into the opaque wasm linker error
the check exists to prevent; and `VERIFIED_SPIN_MAJOR_RANGE` excluded
major 4, so an operator on the required runtime was told it was
unverified. The threshold is now 4.1 and the range includes 4.

Major 4 is included on evidence, not convenience: the `spin_key_value`
statement at the v4.1.0 tag is byte-identical to the vendored copy
(checked 2026-09-07). Two tests pin the behaviour -- one asserts 4 is
verified and 5 still warns, one parses a real 4.1.0 version string --
and the warning text no longer claims the schema came from 3.x.

Generated `.tool-versions` linked to the Spin installer without naming
a version. It now states the 4.1 floor and why the failure is easy to
miss.

Existing generated applications pin `fastly`, `spin-sdk` and
`validator` themselves, and the adapters hand provider-owned types
straight through, so updating EdgeZero alone resolves two majors of the
same crate and fails with a trait error naming only the application's
own type. `docs/guide/dependency-majors-migration.md` lists what to
move together, the Spin runtime requirement, and how to verify -- the
CI fixture hit exactly this and the error named nothing useful.

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

A three-minor toolchain jump, seven dependency majors, two lockfiles and an edition migration, and every non-mechanical decision in it is justified at the site rather than in the description alone — the brotli hold with a byte count, the redb floor with the assertion it preserves, the inline_modules allow with the CLAUDE.md rule it conflicts with. The AccessGuard bug the redb bump surfaced is a real latent defect, correctly diagnosed as an upstream assertion rather than an upstream regression, and correctly fixed. All 23 checks pass on 9ba4eae.

Two things to resolve before merge, both about the Spin 4.1 floor and its gates rather than about the upgrade itself.

😃 Praise

  • The spin-sdk 6 → 7 write-up is the most useful part of this PR for whoever hits this next: cargo check and the wasmtime contract suite both pass on a component that cannot boot, and only the smoke test catches it. That is a non-obvious property of the toolchain worth having written down.
  • Verifying each CLI pin against the workload CI actually runs it for — Viceroy against the wasip1 suites, wasmtime against the wasip2 contract suite — rather than against --version.
  • Checking cargo tree -i before taking brotli 9 and then not taking it, with the 40 KB measured. A major that does not consolidate is a cost with no benefit, and the reasoning is now pinned where the next person will read it.

Findings

Blocking

  • 🔧 scripts/smoke_test_kv.sh has no Spin 4.1 guard, and its install URL is stale (scripts/smoke_test_kv.sh:62-67) — see below.
  • spin-sdk is the one shared pin with no drift gate (crates/edgezero-cli/src/generator.rs:886) — inline.

Non-blocking

  • ♻️ Redundant drift test (crates/edgezero-cli/src/generator.rs:939) — subsumed by the one at line 916; inline.
  • 🤔 pin_in and extract_version accept different grammars (scripts/check_shared_dep_pins.sh:33) — fails closed, but misdiagnoses a reformatted pin as a missing one; inline.
  • The deploy smoke fixture is the last edition-2021 island (.github/actions/deploy-core/tests/make-smoke-fixture.sh:41) — see below.
  • 📝 simple_logger 4 → 5 is a seventh major (examples/app-demo/Cargo.toml:44) — inline.

🔧 scripts/smoke_test_kv.sh:62-67 — the 4.1 guard is missing from the script the docs now point at

The Spin 4.1 pre-check landed in scripts/smoke_test_config_key_override.sh:451-467, and it is correct — printf "%d%02d" orders 4.0.9 (400) below 4.1.0 (401) and handles 4.10 and 10.1 properly, and the empty-version fall-through is guarded by the -n test.

But this PR also rewrote .tool-versions:9-10 to name a different script as the one operators should run:

Install matching CLI manually (https://spinframework.dev/install) before running smoke_test_kv.sh spin; otherwise set SKIP_SPIN=1.

and the test plan cites scripts/smoke_test_kv.sh spin — 8/8 — as the end-to-end verification for the SDK bump. smoke_test_kv.sh only checks command -v spin, so an operator on Spin 4.0 following that line gets:

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

which is exactly the error the guard exists to prevent, from the exact path the guard was documented against. The PR's own framing — "only the smoke test catches this class" — is what makes the gap matter: the smoke test that catches it is the other one.

Its install URL is also now wrong in a way that points operators at the problem:

echo "Spin CLI is required. Install from https://developer.fermyon.com/spin/v3/install" >&2

A v3 install page, for a 4.1 floor.

Fix: factor the pre-check into a helper both scripts source (the guard is ~12 lines and is going to drift if copied), and update the URL to https://spinframework.dev/install to match .tool-versions. smoke_test_config.sh and smoke_test_secrets.sh are worth a look for the same pattern.

.github/actions/deploy-core/tests/make-smoke-fixture.sh:41 — still edition = "2021"

The PR moves examples/app-demo to 2024 so the reference app stops being the odd one out against the workspace and the scaffold templates. This generated fixture is then the remaining 2021 island, and it takes edgezero-core and edgezero-cli by path — so it is compiled against 2024 crates from a 2021 crate. That works, and nothing here depends on the edition, so this is not a correctness point. But the fixture already turned up once in this PR as the fourth place a pin hides; leaving it on the old edition keeps it slightly out of step with everything it links against.

CI Status

  • fmt: PASS
  • clippy (host + all 4 per-target matrix combinations): PASS
  • tests: PASS
  • 23/23 checks green on 9ba4eae

Not run locally: this repo needs 1.98.1 via asdf and my box has rustup at 1.95, so I reviewed the diff statically rather than reproducing the gates. The note in the PR description about a clippy run exiting 0 on No version is set for command cargo is a good catch and is why I did not report a local run as a pass.

Verified against the tree

Every dependency claim in the description checks out against the lockfiles:

Claim Result
base16ct 1.0.0, no transitive deps ✅ lockfile entry has no dependencies block
brotli consolidated to one copy ✅ single brotli 8.0.4
sha2 three copies → two 0.9.9 (via fastly) and 0.11.0 only
demo anyhow → 1.0.104
demo on proc-macro-error3 proc-macro-error3 / proc-macro-error-attr3 3.1.1
prepare still hoisted out of the batch loop push_sqlite.rs:251 — prepared once, reused across entries
check_shared_dep_pins.sh passes and anchors correctly ✅ ran it: all validator pins agree (0.21); the contains("validator = { version =") line at generator.rs:1209 correctly does not shadow the real seed

base16ct::lower::encode_string is output-identical to format!("{:x}", ..) here — both lowercase, both zero-padded, and the input is a fixed 32-byte digest, so there is no width or truncation difference. The canonical_form_pin_v1 pinned-hash test passing is the load-bearing evidence, since these digests are persisted in config envelopes.

Comment thread crates/edgezero-cli/src/generator.rs
Comment thread crates/edgezero-cli/src/generator.rs Outdated
Comment thread scripts/check_shared_dep_pins.sh Outdated
Comment thread scripts/check_shared_dep_pins.sh
Comment thread crates/edgezero-adapter-axum/src/key_value_store.rs
Comment thread examples/app-demo/Cargo.toml
Review follow-ups.

The 4.1 pre-check landed in `smoke_test_config_key_override.sh`, but
`.tool-versions` and the test plan both name `smoke_test_kv.sh` as the
script to run, and that one only checked `command -v spin` and linked a
v3 install page. An operator following those instructions on Spin 4.0
reached the WIT linker error the check exists to prevent, from the path
it was documented against. `smoke_test_config.sh` and
`smoke_test_secrets.sh` had the same gap.

The check is now `scripts/spin_version_guard.sh`, sourced by all four
rather than copied into each. Verified with stub CLIs across the
boundaries the encoding has to get right: 3.7.1, 4.0.2 and 4.0.9 are
refused, 4.1.0, 4.10.0 and 10.1.0 pass. `smoke_test_kv.sh spin` refuses
4.0.2 with an actionable message and still reports 8/8 on 4.1.0.

`spin-sdk` was the one shared pin with no drift gate. It is spelled
`~7.0` in the workspace and `7` in both the demo and the scaffold seed,
so an exact comparison would have failed on spelling; comparing the
major closes it. That is worth having: a `validator` mismatch fails at
compile time, while a `spin-sdk` mismatch compiles, passes the wasmtime
contract suite, and dies at `spin up`.

Also from review: `pin_in` required `version` to be the first key and
exactly single spaces, so a reordered or differently spaced pin was
reported as missing rather than as itself -- it now accepts the same
grammar as `extract_version`, which is what the comment above it
already claimed. The `rust` drift test is deleted as subsumed by the
one that resolves every pinned tool from `.tool-versions`. The deploy
smoke fixture moves to edition 2024, leaving no 2021 crate linking
against 2024 ones.
@aram356

aram356 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — both blocking findings were real gaps, and the first one is the more embarrassing of the two.

🔧 smoke_test_kv.sh had no 4.1 guard

You put it precisely: the smoke test that catches this class is the other one. I added the guard to smoke_test_config_key_override.sh, then rewrote .tool-versions and the test plan to point operators at smoke_test_kv.sh, which checked only command -v spin and linked a v3 install page for a 4.1 floor. Following my own instructions on Spin 4.0 landed you on the exact WIT linker error the guard exists to prevent.

smoke_test_config.sh and smoke_test_secrets.sh had the same gap and the same stale URL, as you suspected.

Fixed in cfcc6cd5 by factoring the check into scripts/spin_version_guard.sh and sourcing it from all four, rather than copying twelve lines three more times. Verified with stub CLIs across the boundaries the printf "%d%02d" encoding has to get right:

CLI result
3.7.1 refused
4.0.2 refused
4.0.9 refused
4.1.0 allowed
4.10.0 allowed
10.1.0 allowed

Then end to end on the real script: smoke_test_kv.sh spin on 4.0.2 now prints Spin CLI 4.0.2 is too old: spin-sdk 7 needs >= 4.1 for wasi:http/types@0.3.0 and exits, and on 4.1.0 still reports 8/8. No developer.fermyon.com/spin/v3 URLs remain, and command -v spin now appears only inside the helper.

⛏ Edition-2021 fixture

Also taken. make-smoke-fixture.sh emits edition = "2024", and I regenerated the fixture and compiled it to confirm — no 2021 crate linking against 2024 ones.

Not changed

The simple_logger note needs no code change but the majors table should say seven; updating the PR body.

All seven local gates pass on cfcc6cd5: fmt, clippy (host + wasm), workspace tests, demo tests, the pin gate, and the deploy-action suite.

Also — your note about not reporting a local run as a pass because of the asdf/No version is set trap is the right instinct. That false pass cost me real time earlier in this branch.

@aram356
aram356 requested a review from prk-Jr September 14, 2026 18:54

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary

Approving. A large but unusually well-argued upgrade: Rust 1.95.0 → 1.98.1, six dependency majors taken and one (brotli) deliberately held with a measured reason, plus a latent AccessGuard bug in the axum KV store that redb 4.2's new assertion exposed. The dependency graph comes out cleaner than it went in — hashbrown, windows-targets, ahash, zerocopy and the RC wasip3 worlds all de-duplicate — and the new check_shared_dep_pins.sh gate plus the two generator drift tests turn a previously deploy-time failure mode into a lint.

Nothing here blocks. Three items below are worth a follow-up rather than a re-review: two are small fixes, and one is a question I'd like answered in writing because it's the single failure mode this PR cannot self-detect.

😃 Praise

  • The redb 4.2 finding is the highlight. get_bytes was holding an AccessGuard borrowed from the table across drop(table); drop(read_txn) in the lazy-expiry path — silently wrong on 4.1.x. The obvious read of the new panic (assertion failed: !self.read_page_ref_counts…) is "upstream regression, pin back to 4.1"; instead the floor was raised to 4.2.0 so the assertion can't be resolved away, with the reason recorded at the pin (Cargo.toml:69-73).
  • brotli is the discipline highlight. Held at 8 with a measured justification — 7,169,574 vs 7,129,410 bytes on the demo Spin release build — rather than bumped for tidiness, with both the mechanism and the exit condition recorded at Cargo.toml:40-45.
  • check_shared_dep_pins.sh + the two generator drift tests convert a failure that previously only surfaced as E0277: the trait bound MyAppConfig: validator::traits::Validate is not satisfied in a deploy smoke job into a named lint. The comment explaining why cargo cannot do this — four pins, three in workspaces that share no lockfile, one in a manifest that does not exist until CI generates it — is at exactly the right altitude. The anchored ^[[:space:]]*(\\?")?validator[[:space:]]*= match correctly skips "validator".to_owned(), and catches the escaped-literal seed form.
  • "cargo check and the wasmtime contract suite both pass on a build that cannot boot." That sentence, plus "a clean host cargo clippy --all-features does not imply the wasm targets are clean," are the two things a reader most needs from this PR, and both are in the description. Same for the asdf note about clippy exiting 0 on No version is set for command cargo.
  • Edition 2024's env::set_var unsoundness was handled by centralising, not by spreading unsafe. app-demo-core/src/config.rs:188-189 now takes env_lock() + EnvOverride, routing through the workspace's single #[expect(unsafe_code)] module, and the guard restores state even when an assertion panics — strictly better than the env::remove_var before the asserts it replaced.

Findings

Worth fixing before or shortly after merge

  • 🔧 Stale ~6.0 SDK pin in the module that documents the pin guardcrates/edgezero-adapter-spin/src/cli/push_sqlite.rs:26 and :37. Cargo.toml:99 is now spin-sdk = "~7.0". This module's whole purpose is documenting the three-layer guard around Spin's internal SQLite schema, and layer 2 now names a pin that no longer exists — as does the "what the guards CANNOT catch" paragraph. The reference sweep updated the equivalent line in docs/guide/adapters/spin.md:205, so this reads as a miss rather than a policy. ~6.0~7.0 in both lines. (Commented in the body rather than inline because both lines are outside the diff.)

  • 🔧 spin_meets_floor fails open on an unparseable versionscripts/spin_version_guard.sh:36-43, inline below. Dev-only blast radius, two-line fix.

  • Fastly CLI 15.1.0 → 16.0.0: flags were verified, but were the --json payload schemas? The adapter does not just shell out — edgezero-adapter-fastly/src/cli.rs parses CLI JSON output shapes in roughly eight places, and its own diagnostics name this as the expected failure mode: "output is neither a bare array nor an items envelope …; fastly CLI may have changed its schema" (:3526), "entry has no string item_key/item_value fields" (:3546), and "The fastly CLI may have changed its JSON schema in a recent version … Workaround: pin to a known-compatible fastly CLI version" (:4057, :3641, :5218).

    The description's verification line is "Every subcommand the adapter shells out to still exists with the same flags." That covers the command and flag surface, not payload shape — and payload shape is what those parsers and error strings are about. Nothing in CI exercises a real fastly CLI (correctly, per the no-credentials rule), so fastly-installer-check.yml proves only that the pinned archive installs and that the two pin sources agree.

    Concretely: were config-store list --json, config-store-entry list --json, config-store-entry describe --json and service-version list --json compared between 15.1.0 and 16.0.0, even manually against a scratch service? Unlike Spin, there's no VERIFIED_FASTLY_MAJOR_RANGE analogue here, so a schema change surfaces first as a red deploy in a consumer's repo. A one-line note in the description recording that comparison would close it.

Non-blocking

  • 🤔 VERIFIED_SPIN_MAJOR_RANGE = [2,3,4] still blesses majors that cannot boot a spin-sdk 7 component, while four other files now state a hard 4.1 floor (push_sqlite.rs:60, inline).
  • 🤔 prepare_cachedprepare is explained in the description but not at the call site (push_sqlite.rs:252, inline).
  • ♻️ Migration guide's Toolchain section mentions only Rust, but a scaffolded app's .tool-versions also pins fastly/viceroy/nodejs and install-fastly.sh fails closed on a mismatch (docs/guide/dependency-majors-migration.md:55-59, inline).
  • One-line where inside config_store_contract_tests!, where the two sibling macros are wrapped (crates/edgezero-core/src/config_store.rs:45, inline).
  • List continuation un-indented in the new Spin prerequisites bullet (docs/guide/adapters/spin.md:13-15, inline).
  • # shellcheck source= resolves relative to the sourcing file, so it looks for scripts/scripts/… (four smoke scripts, line 5 — grouped inline on one).
  • 🌱 Seed drift test covers 4 of 12 seedssimple_logger is the one this PR actually had to move (crates/edgezero-cli/src/generator.rs:886, inline).
  • 🌱 include_str! reaches outside the package in two tests; harmless for cargo package since both are cfg(test), but would break cargo test on a vendored copy (generator.rs:884, :954, inline).
  • 🤔 48-site inline_trait_bounds rewrite rides along with the dependency change, while inline_modules (77 sites) and pub_with_shorthand (44 sites) were allowed with reasons instead. The rationale for each choice is sound and documented; the observation is only that a large mechanical churn now shares a diff with the toolchain and dependency work. Noting that -D warnings on the new clippy meant something had to happen in the same PR, so this is a preference, not a defect.

Dependency review

Both lockfiles diffed at package level (root: 7 added / 7 removed / ~150 changed; demo: 9 / 9 / ~145). Nothing yanked or unvetted.

  • New direct dep base16ct 1.0.0 (unconditional in edgezero-core and edgezero-adapter-fastly) — right call: sha2 0.11's Array dropped LowerHex, so format!("{:x}", …) no longer compiles, and base16ct is RustCrypto's own encoder — no_std, no dependencies of its own, WASM-clean. Unconditional placement matches sha2, also unconditional in both. Digest output verified unchanged by the retained canonical_form_pin_v1 pinned-hash test, which is what actually matters since these digests are persisted in config envelopes.
  • RustCrypto 0.11 line pulled in wholesaledigest 0.10→0.11.3, crypto-common 0.1→0.2.2, block-buffer 0.10→0.12.1, new hybrid-array 0.4.14 and const-oid 0.10.2. All no_std, all WASM-safe.
  • libsqlite3-sys 0.30.1 → 0.38.2 (via rusqlite 0.40) is the largest single jump, but host-only and CLI-only: dep:rusqlite sits behind the spin adapter's cli feature (Cargo.toml:16, :44, optional = true), so no WASM target ever sees bundled SQLite. Written-file compatibility is covered by the byte-equality schema test plus write_batch_round_trips_through_spin_schema.
  • New transitives, all benignbstr 1.13.1, zlib-rs 0.6.7 (pure Rust), proc-macro-error3 3.1.1 replacing proc-macro-error2 (which is the fix for the "rejected by a future version of Rust" warning), rand_pcg 0.10.2 (demo only).
  • Net de-duplicationahash, hashlink, lazy_static, zerocopy gone entirely; hashbrown 3 copies → 1; windows-targets/windows_* collapse to a single 0.52.6; getrandom 0.3.4 dropped; wasm-encoder/wasmparser/wit-* each 2 → 1.
  • wasip3 0.4.0/0.6.0-rc → 0.7.1+wasi-0.3.0 is consistent with the spin-sdk 7 story and the claimed Spin 4.1 floor — the RC WIT worlds are gone and the released 0.3.0 interface is what's imported.
  • docs/package-lock.jsonpackage.json unchanged, so a pure in-range npm update. Every addition is dev: true (cacheable/hashery/qified under eslint's flat-cache) plus one optional, os: ["linux"] rollup binary. Nothing reaches the built site.

📌 Out of scope

  • No schema-shape regression fixture for the Fastly CLI's --json payloads, the way vendored_schema_matches_upstream_byte_for_byte exists for Spin's SQLite schema. A recorded-fixture test over the four parsed payloads would turn the ❓ above into a CI gate for every future CLI bump — worth a separate issue.
  • docs/superpowers/plans/** keeps ~15 references to Rust 1.95.0 / edition 2021 / spin-sdk ~6.0 / viceroy 0.17.0. The description declares these out of scope as dated design records and they're srcExcluded from the published site — agreed, noted so a later grep sweep doesn't re-litigate it.

CI Status

Taken from the GitHub checks rather than re-run locally.

  • fmt: PASS
  • clippy: PASS
  • tests: PASS

# MAJOR.MINOR only; a pre-release suffix on the patch is ignored.
encoded=$(printf '%s' "$version" | awk -F'.' '{printf "%d%02d", $1, $2}')

if [ -n "$encoded" ] && [ "$encoded" -lt "$SPIN_MIN_ENCODED" ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Fails open on an unparseable version.

If spin --version produces no parseable output — empty stdout, a non-zero exit swallowed by 2>/dev/null, or a future format change where the version is not field 2 — then version is empty, awk gets no records, the action block never runs, and encoded is "". The [ -n "$encoded" ] guard short-circuits and the function returns 0, reporting "present and new enough" for a CLI whose version is unknown. The smoke test then proceeds to spin up and dies with exactly the opaque wasi:http/types@0.3.0 … not found in the linker error this guard exists to pre-empt.

That contradicts the helper's own contract at lines 20-21 ("Returns 0 when the CLI is present and new enough, 1 otherwise"). Garbage-but-non-empty output does fail correctly (%d of unknown → 0 → below the floor); only the empty case escapes.

Blast radius is dev-only, but the logic was just extracted into a shared helper that four scripts now depend on, so this is the moment to close it:

  if [ -z "$encoded" ]; then
    echo "Could not parse a version from \`spin --version\` (got: '${version}'); need >= ${SPIN_MIN_DISPLAY}." >&2
    return 1
  fi
  if [ "$encoded" -lt "$SPIN_MIN_ENCODED" ]; then

/// verification to be repeated and this constant updated. Until then,
/// an operator running a Spin CLI outside this range gets a
/// `log::warn!` on first SQLite-direct push.
const VERIFIED_SPIN_MAJOR_RANGE: &[u32] = &[2, 3, 4];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 This range still blesses majors that cannot boot the component.

.tool-versions, docs/guide/adapters/spin.md:10, scripts/spin_version_guard.sh:25 and the new migration guide all now state Spin 4.1+ as a hard floor. An operator on Spin 3.x gets no warning from verify_spin_runtime_compat, even though their runtime definitively cannot run a spin-sdk 7 component.

I follow the intent — the range is scoped to the SQLite schema, a CLI-side concern that still works on 3.x, and config push is legitimately usable there. But the two facts now read as contradictory within the same repo. Either narrow to &[4], or add a sentence here distinguishing "schema verified for" from "runtime supported".

verified_range_covers_the_spin_majors_the_sdk_supports asserts contains(&4) and !contains(&5) and says nothing about 2/3, so narrowing wouldn't break it.

{
let mut statement = transaction
.prepare_cached(SPIN_KV_SET)
.prepare(SPIN_KV_SET)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 The reason for prepare over prepare_cached lives only in the PR body.

The description explains it (rusqlite 0.40 dropped prepare_cached from Transaction, and prepare still prepares once and reuses across the batch — correct, the statement is hoisted outside the for loop). Every other constraint in this file is recorded inline, and this is the sort of line a future reader "optimises" back:

// rusqlite 0.40 dropped `prepare_cached` from `Transaction`; `prepare`
// still prepares once and reuses the statement across the batch below.

Install Spin 4.1 or newer ([install](https://spinframework.dev/install))
before upgrading the SDK. `cargo check` cannot detect this.

## Toolchain

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ The Toolchain section covers Rust but not the CLI pins.

A scaffolded app's .tool-versions (see generator.rs:657-666) also pins fastly, viceroy and nodejs, and this PR moves all three (16.0.0 / 0.21.0 / 24.20.0). For Fastly apps that's operational, not cosmetic: install-fastly.sh fails closed when .tool-versions and versions.json disagree, so an app that moves the Rust pin and not the Fastly pin gets a red deploy action. Worth a short second paragraph or a second table row.

use $crate::config_store::ConfigStore;

fn run<Fut: ::std::future::Future>(future: Fut) -> Fut::Output {
fn run<Fut>(future: Fut) -> Fut::Output where Fut: ::std::future::Future {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ This where clause is on one line, while the equivalents in key_value_store.rs:95 and secret_store.rs:50 are wrapped. rustfmt doesn't reach inside macro_rules! bodies, so it slipped through — it's the only site in the 48-site sweep that reads differently.

- Spin CLI **4.1 or newer** ([install](https://spinframework.dev/install)).
The workspace's `spin-sdk` imports `wasi:http/types@0.3.0`, which Spin
4.0.x does not provide — on an older runtime the component builds and
its tests pass, then `spin up` fails with `component imports instance

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ Lines 14-15 start at column 0 inside a bullet whose other lines are indented 2. It renders via lazy continuation and prettier accepts it, but it reads as a broken list in the source. Indent both by 2, or lift the error text into a fenced block below the bullet — the way the migration guide and spin_version_guard.sh both do.

set -euo pipefail

# Spin CLI floor check, shared with the other smoke tests.
# shellcheck source=scripts/spin_version_guard.sh

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# shellcheck source= path won't resolve (same line in smoke_test_config_key_override.sh, smoke_test_kv.sh and smoke_test_secrets.sh).

shellcheck resolves source= relative to the sourcing file's directory, so from inside scripts/ this looks for scripts/scripts/spin_version_guard.sh. Harmless today — the shellcheck job in deploy-action.yml only covers .github/actions/** — so it's dead weight rather than a break. Use # shellcheck source=./spin_version_guard.sh or # shellcheck source-path=SCRIPTDIR.

// shared crates are checked against the root manifest here.
let root_manifest = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../Cargo.toml"));
let seeds = seed_workspace_dependencies();
for crate_name in ["validator", "axum", "fastly", "worker"] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 Seed drift test covers 4 of 12 seeds.

["validator", "axum", "fastly", "worker"] plus the spin-sdk major. simple_logger, clap, tokio, serde, log, tracing and once_cell are unchecked. simple_logger is the interesting one — this PR moved the demo from 4 to 5 to catch up with the root workspace, i.e. exactly the drift class this test exists to prevent, and it's a one-word addition to the array.

// resolves. If the two majors drift apart the generated app fails to
// compile with a confusing "trait bound not satisfied" error, so the
// shared crates are checked against the root manifest here.
let root_manifest = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../Cargo.toml"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../Cargo.toml")) reaches outside the package (same pattern at :954 for .tool-versions).

Fine in-tree, and no problem for cargo package — both are inside #[cfg(test)], so the publish verify build never evaluates them. But cargo test on a vendored or published copy of edgezero-cli would fail to compile. Recording it rather than asking for a change: if this crate is ever published, these two need a #[cfg] escape or an env-var-driven skip.


if let Some(entry) = table
// redb asserts that no read reference outlives its transaction, so the
// `AccessGuard` borrow has to end before the drops below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😃 The best thing in this PR.

get_bytes was holding an AccessGuard borrowed from the table across drop(table); drop(read_txn) in the lazy-expiry path — silently wrong on redb 4.1.x. The obvious read of the new panic (assertion failed: !self.read_page_ref_counts…) is "upstream regression, pin back to 4.1"; instead the floor was raised to 4.2.0 specifically so the assertion can't be resolved away, with the reason recorded at the pin (Cargo.toml:69-73).

The rewrite here is also the right shape: copy the value out inside the match, then handle the expired path with the read txn already dropped — and the TOCTOU re-check inside the write txn is preserved verbatim.

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

3 participants