Skip to content

Bind Fastly stores per deployment environment - #381

Merged
aram356 merged 72 commits into
mainfrom
fix/fastly-environment-store-selectors
Oct 8, 2026
Merged

aram356 merged 72 commits into
mainfrom
fix/fastly-environment-store-selectors

Conversation

@aram356

@aram356 aram356 commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem and resulting behavior

PR #344 made Fastly store selection depend on a shared edgezero_runtime_env Config Store and service/version-scoped descriptor keys. That added mutable shared state, put Fastly service IDs into configuration names, and made application runtime code participate in deployment-target selection.

This change removes that descriptor architecture. Applications keep stable logical store IDs in edgezero.toml. The selected deployment environment chooses physical Config, KV, and optional Secret Stores through canonical EDGEZERO__STORES__...__NAME variables. Before publication, managed Fastly deployment binds those physical resources to the logical aliases on the unpublished service version.

Production and staging can select the same or different physical stores while deploying the same immutable application release. A logical Config Store always uses its logical ID as the entry key. For example, production and staging can select config-prod and config-stage, while both versions expose the alias and key app_config.

Changes

  • Remove edgezero_runtime_env, runtime descriptors, service/version selector keys, staging selector stores, EDGEZERO_FASTLY_BUILD_SETTINGS, and all compatibility behavior for the Support optional typed secret paths and Fastly store mappings #344 contract.
  • Resolve physical stores from canonical __NAME variables with parent environment > manifest default > logical ID precedence for deploy, push, diff, GC, and provision. Normalize recognized selector casing before merging layers, reject noncanonical action inputs, and fail on present blank or invalid selectors before provider I/O. config gc --no-env still forces the logical fallback.
  • Reconcile declared Fastly resource links by kind and logical ID while preserving undeclared recognized Config, KV, and Secret Store links. Unknown provider resource types fail closed. Parse the live API types config, kv-store, and secret-store.
  • Keep first-service Fastly setup accurate: [setup] is written only when physical and logical names match. A different physical store requires a selected existing service; provision creates the physical resource and reports the logical resource-link operation without writing a misleading setup alias.
  • Invoke Fastly CLI through the supported service resource-link ... and service version ... hierarchy, retaining the reviewed Fastly CLI 15.1.0 pin.
  • Use authoritative Fastly environment records for staging state. A staging record's shadow service ID is accepted; its exact environment name, active version, and global uniqueness determine state.
  • Reject orphan editable drafts beside staged or retired sources because their ownership cannot be proven. Treat an empty scaffold service_id as unselected rather than as an invalid configured ID.
  • Replace the Fastly version self-diff dependency with normalized reads of versioned domains, backends, health checks, logging endpoints, and settings. Canonicalize JSON object-key order recursively before sorting collection snapshots. Logging snapshots use the live REST endpoint kinds, including pubsub, logentries, and s3.
  • Revalidate links, package identity, source state, draft state, and protected Compute configuration immediately before publication, with direct regression coverage for every failure barrier.
  • Keep the application CLI, complete edgezero.toml, package, and selected adapter manifest byte-identical across publishers and targets. A Fastly release includes its adapter manifest at the relative path declared in edgezero.toml.
  • Scope targeted config push/diff --adapter <name> validation to the selected adapter while retaining whole-project config validate.
  • Keep immutable-release packaging and verification in provider-neutral cores. Member digests are streamed through a bounded buffer; release.json is checked as a confined regular file before opening, and lifecycle actions copy the archive once before authenticating, inspecting, and extracting that same copy.
  • Move application CLI invocation into the existing provider-neutral deploy-core/scripts/run-app-cli.sh; thin Fastly wrappers own Fastly arguments, credentials, and output policy.
  • Keep store-free manifest-command compatibility, including its provider-owned --package argument. Managed immutable-release deployment rejects every --package / -p spelling because the release owns the package path, and adapters without managed-release support reject --application-release instead of ignoring it.
  • Rename the archive produced by build-app-cli to app-cli.tar while preserving the configurable artifact name, application binary/package names, and immutable member cli/app-cli.tar.
  • Retain uploaded application CLI and release artifacts for 14 days, document the recovery window, and exercise the public Fastly release-packaging action in the deployment smoke workflow.
  • Provide the provider-neutral setup-rust-build-cache action. Its required app-name scopes Cargo sources, sccache, and optional target artifacts; the target key is stable for an application, runner, architecture, and lockfile, and a two-job smoke proves a real restore.
  • Restore the executable ambiguous-publication recovery path: query the exact live version through the authenticated application release, then run the immutable-release rollback action to the captured previous version.
  • Serialize duplicate deploy-action workflow runs, scan all of .github with Zizmor, restore the required cargo test aggregate check, install every generated-project WASM target in CI, minimize workflow credentials, and keep action tooling free of Python and pip.
  • Limit file-based config-push cleanliness checks to the exact tracked config file, so an independently authenticated untracked release download does not block publication.
  • Document producer/consumer migration, all removed deploy inputs, custom Fastly entrypoint logging setup, edgezero_runtime_env cutover, staging config re-push under the fixed logical key, optional selector exports, cross-repository release retrieval, mutable-config recovery, and the distinction between a real application domain and its GitHub Environment name.
  • Build the application CLI for the runner's explicit native target and package that target's executable, so a restored Cargo cache cannot supply a stale CLI when the app configures build.target.
  • Read Axum dev-server secrets from EDGEZERO__SECRETS__<KEY>, with the key in ASCII uppercase, instead of from a variable named exactly like the key. config validate/push rejects keys outside ASCII letters, digits, and _, and distinct keys that collide once uppercased.
  • Restore the guide corrections from Align guide docs with the code on main #373 that the main merge dropped, and correct the Wrangler, Fastly, and Spin local secret commands.

Fastly does not expose a publication compare-and-swap token. Deployments and other version mutators for one service must share caller-side serialization. The source/clone and final snapshot checks reject observed interference; serialization closes the remaining provider time-of-check/time-of-use window.

Downstream consumers of the build action must consume app-cli.tar.

Local Axum runs must set secrets as EDGEZERO__SECRETS__<KEY>; a variable named exactly like the key is no longer read.

Closes #380

Validation

  • cargo test --workspace --all-targets
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin
  • cargo fmt --all -- --check and git diff --check
  • .github/actions/deploy-core/tests/run.sh — 639 passed, 0 failed, 5 expected platform skips
  • .github/actions/setup-rust-build-cache/tests/run.sh
  • .github/actions/package-application-release-fastly/tests/run.sh — exercised on Linux CI; non-Linux runners skip it
  • Actionlint, ShellCheck, and zizmor --offline .github
  • Documentation Prettier, ESLint, and VitePress build
  • rg 'edgezero-cli\\.tar' returns no product-contract references
  • Action and CI paths contain no Python setup, interpreter call, or pip invocation

@aram356 aram356 self-assigned this Sep 16, 2026
@aram356 aram356 changed the title fix(fastly): apply canonical store selectors at deploy feat(fastly): scope runtime configuration to service versions Sep 17, 2026
@aram356

aram356 commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the remaining review findings in b4cf30c9 and added the exact mixed-case managed-deploy regression in 0a450fdb.

The non-inline config-push blocker is fixed as well: file-based push now validates and confines the selected config file, then requires that exact file to be tracked, committed, and unchanged. An unrelated untracked authenticated application-release download no longer makes the checkout fail the config-push preflight. The action suite includes that layout as an executable regression.

I also merged current main in 9da604f1, updated the PR and issue descriptions, replied to every open thread with its concrete fix, and resolved the addressed threads.

Local validation after the merge and fixes:

  • cargo test --workspace --all-targets
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo check --workspace --all-targets --features "fastly cloudflare spin"
  • cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin
  • .github/actions/deploy-core/tests/run.sh — 634 passed, 0 failed, 5 expected platform skips
  • Actionlint, ShellCheck, Zizmor, Rust formatting, documentation formatting/lint/build, and diff checks

The Linux-only release-packaging script test remains platform-gated on this macOS checkout and is running in CI through the public composite-action smoke jobs.

@aram356

aram356 commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: the public-packager smoke handoff is fixed in 3c82f338. The fake Fastly provider now receives the packager's authenticated package digest directly rather than looking for the removed sidecar file, and all four consuming jobs have structural contract coverage.

The complete PR check rollup is now green, including production, staging, recovery, store-free deployment, config push, the required cargo test context, Fastly/Cloudflare/Spin WASM tests, static checks, and CodeQL. The local action suite is 638 passed, 0 failed, with 5 expected non-Linux skips.

@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.

🔧 Reviewed PR #381 at 3c82f33. Requesting changes for one reproduced artifact correctness issue: build-app-cli can publish an older cached executable instead of the executable Cargo just built.

😃 The immutable-release verifier has strong adversarial coverage for member digest mismatches, symlinks, extra members, and loaded-manifest identity. The final publication barriers also verify package and resource-link identity before publication.

📝 Verification: formatting, all-features Clippy, workspace tests (1,492 passed; 1 ignored), Fastly/Cloudflare/Spin feature compilation, and Spin wasm32-wasip2 compilation passed locally. The workspace tests required loopback networking permission. The action harness passed 638 tests with 5 platform skips; build-cache tests, Actionlint, ShellCheck, and documentation format/lint/build passed. All GitHub checks were successful on the reviewed head. The stale-artifact reproduction used real Cargo on macOS, with only Linux runner detection and toolchain installation shimmed.

Comment thread .github/actions/build-app-cli/scripts/build-app-cli.sh
A configured Cargo target (CARGO_BUILD_TARGET or build.target) moves the
fresh executable to <target-dir>/<triple>/release, but build-app-cli always
packaged <target-dir>/release. With a restored cache, an older root-level
executable still passed --help and was published as the current CLI.

Pass the toolchain's host triple as --target, which overrides any configured
target, and package the executable from that triple's directory.
@aram356
aram356 requested a review from prk-Jr October 2, 2026 02:06

@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.

📝 Reviewed PR #381 at c2e862f. Two introduced migration-runbook errors prevent the documented Cloudflare and Axum secret setup from working. No additional blocking runtime or action defects were confirmed.

😃 The deployment wrapper preserves independently valid recovery outputs when another output contract fails, and the tests cover provider exit-status preservation. This helps operators reconcile a deployment that may already have mutated provider state.

📝 Validation: all five required Rust gates passed (workspace tests: 1,492 passed, 1 ignored); action contract suite: 639 passed, 0 failed, 5 macOS platform skips; release verification, cache/environment tests, actionlint, ShellCheck, and docs format/lint/build passed. Linux-only packaging and live provider behavior were not exercised locally. Reported GitHub checks passed.

Comment thread docs/guide/blob-app-config-migration.md Outdated
Comment thread docs/guide/blob-app-config-migration.md Outdated
Merging main (9da604f) resolved five guide files to this branch's side
and dropped the corrections #373 made against the code. Restore each one
that still matches the code here: the Fastly manifest version, provision
behavior, build/serve commands, proxy import path, and streaming note;
Cloudflare local push by binding; the serve fallbacks, healthcheck
outputs, push outputs, and manifest/service-id rows in the CLI reference;
and the bare-handle constructors and `wrangler kv key put` spelling in
the store migration guide.

Also correct the local secret commands that never worked: `wrangler
secret put` takes no `--binding`, Fastly CLI 15.1.0 reads secret values
from `--stdin` or `--file`, and Spin reads `SPIN_VARIABLE_<NAME>` rather
than the bare variable name. Note the runner 2.327.1 requirement for the
Node 24 producer actions and drop stale overlay references from the
Fastly request rustdoc.
The example returned `text(...)`, which does not exist, so uncommenting it
in a generated app failed with E0425. Return `Text<&'static str>` like the
template's other handlers.
The Axum dev server looked a secret key up as an environment variable of
the same name, so `api_token = "demo_api_token"` read `demo_api_token`.
That breaks env-var conventions and lets any inherited variable whose
name matches a key answer for it silently.

Secret key `k` now resolves from `EDGEZERO__SECRETS__` plus `k` in ASCII
uppercase, e.g. `EDGEZERO__SECRETS__DEMO_API_TOKEN`. One rule in
`secret_env` serves both the runtime store and `config validate`/`push`:
a key must be ASCII letters, digits, or `_`, and two different keys that
differ only in case are rejected because they would share a variable.
The bare variable is no longer read.

Update the Axum guide, CLI reference, migration guide, the generated
README and app config template, the core rustdoc, and the smoke scripts.
@aram356
aram356 requested a review from prk-Jr October 3, 2026 20:06

@dhruv8sh dhruv8sh left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PR Review

Summary

The descriptor removal and logical-alias binding are well executed. CI is green, and locally fmt, clippy, workspace tests, and both feature/wasm checks pass on the pinned 1.95.0 toolchain. This review covers blocking items only.

😃 Praise

  • The immutable-release verification in release.rs / prepare-release.sh is rigorous: it rejects path traversal and symlinks, denies unknown fields, verifies per-member digests on a private copy, and has adversarial tests.
  • Resource links are checked for exact map equality at the clone, reconciled-draft, and final stages. Every Fastly CLI call passes argv as an array, with no shell interpolation.

Findings

Blocking

  • 🔧 Broken anchor: the adoption guide links a heading slug that does not exist (docs/guide/deploy-action-adoption.md:139)
  • ❓ 14-day artifact retention vs. rollback window: the hard-coded retention can expire the archive that rollback needs (.github/actions/package-application-release-fastly/action.yml:115, .github/actions/build-app-cli/action.yml:282)
  • ❓ Cross-kind resource-link name collisions: reconciliation keys links by (kind, alias) (crates/edgezero-adapter-fastly/src/cli.rs:670)

CI Status

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

Comment thread docs/guide/deploy-action-adoption.md Outdated
Comment thread .github/actions/package-application-release-fastly/action.yml
Comment thread crates/edgezero-adapter-fastly/src/cli.rs
@aram356
aram356 requested a review from dhruv8sh October 8, 2026 05:27
@aram356
aram356 merged commit f717674 into main Oct 8, 2026
29 checks passed
@aram356
aram356 deleted the fix/fastly-environment-store-selectors branch October 8, 2026 16:29
dhruv8sh added a commit that referenced this pull request Oct 10, 2026
Bring in #381 (Fastly stores bound per deployment environment), which
moved deploy into the Adapter::preflight_deploy / deploy / finalize_deploy
hooks and made Fastly staging an adapter-managed release deploy.

Resolution keeps the --format json deploy result on the new hooks:
- Adapter::deploy and Adapter::finalize_deploy return ActionOutcome, like
  execute. Fastly reports DeployOutcome from the managed deploy plan and
  from finalize_deploy's resolved production version.
- The CLI's adapter::deploy returns DeployFailure, separating a failed
  deploy from a live deploy whose finalization failed; the latter keeps its
  JSON result with version: null.
- Fastly provision takes main's logical/physical alias handling with the
  typed ProvisionReport; the removed runtime-env provisioning and the old
  CLI-side staging and version-resolution code are dropped.
- emit_active_version_for returns ActiveVersionOutcome; finalize_deploy
  logs version=<N>, so text output is unchanged.
- Tests follow main's version-list schema and the --application-release
  requirement for staged deploys. Spec revision 5 records the merge.
aram356 added a commit that referenced this pull request Oct 14, 2026
The merge of main was textually clean in these files, but the code it
brought in was written against 1.95 and does not build here.

`release.rs` is new on main and hashes with `format!("{:x}", ..)`.
sha2 0.11's `Array` dropped `LowerHex`, so all three sites — one in the
digest helper, two in test helpers — now use `base16ct`, matching the
two sites this branch already converted. `base16ct` joins
`edgezero-adapter`'s `cli` feature beside `sha2`, which gates the
module that uses it. The test helper at `release.rs:504` recomputes the
digest independently and compares it against the production value, so
it stands as the check that the hex output is unchanged.

Four signatures tripped `inline_trait_bounds`: `serve_app`,
`assert_send_sync`, and `dispatch_app` in both the Spin and Cloudflare
adapters. The last two are behind `target_arch = "wasm32"`, so a clean
host clippy run does not see them — the per-target matrix is what
caught them, for the second time on this branch.

`check_shared_dep_pins.sh` still named the smoke fixture at its old
`deploy-core/tests/` path, which #381 moved to `deploy-fastly/tests/`.
The gate reported a missing validator pin rather than passing silently,
which is the behaviour it was written for; the path is updated and the
relocated fixture carries the `edition = "2024"` and `validator 0.21`
edits that the modify/delete conflict would otherwise have dropped.
@aram356 aram356 mentioned this pull request Oct 15, 2026
15 of 16 tasks
aram356 added a commit that referenced this pull request Oct 15, 2026
* Upgrade to Rust 1.98.1 and refresh dependencies

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.

* Fix wasm-target clippy and update stale toolchain references

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.

* Update pinned CLI tooling

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.

* Upgrade sha2 to 0.11 and render digests with base16ct

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.

* Take the remaining dependency majors

`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.

* Align the deploy fixture's validator pin and gate the 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.

* Tighten the redb expiry path and its surrounding comments

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.

* Drop comments that record history rather than constraints

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`.

* Consolidate brotli, refresh the demo lockfile, anchor the pin gate

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.

* Take spin-sdk 7, raise the redb floor, reuse the pin parser

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.

* Refresh the docs npm lockfile

`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.

* Update the Spin adapter guide for spin-sdk 7

`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.

* Propagate the Spin 4.1 floor and document the coordinated majors

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.

* Share the Spin floor check and gate the spin-sdk pins

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.

* Fail closed on an unreadable Spin version, refresh stale references

Review follow-ups. Nothing here blocked the approval.

`spin_meets_floor` returned 0 when `spin --version` produced nothing
parseable: `encoded` was empty, the `[ -n "$encoded" ]` test
short-circuited, and the caller was told a CLI of unknown version was
new enough -- the one outcome the helper's own contract ruled out, and
the one that hands the smoke test straight to the linker error it
exists to pre-empt. It now refuses anything that is not a run of
digits. Verified: empty output, a bare `spin`, and a reformatted
version string are all refused, while 3.7.1 / 4.0.2 / 4.0.9 stay
refused and 4.1.0 / 4.10.0 / 10.1.0 stay allowed.

`push_sqlite.rs` documents the three-layer guard around Spin's SQLite
schema, and two of its lines still named `spin-sdk = "~6.0"` after the
pin moved to `~7.0`.

`VERIFIED_SPIN_MAJOR_RANGE` stays `[2, 3, 4]` rather than narrowing to
`[4]`: it records which runtimes' on-disk KV schema ours was compared
against, not which can boot a spin-sdk 7 component, and `config push
--local` only writes that file. Narrowing it would warn operators on
Spin 3.x whose push is correct. The doc comment now says so, since the
two numbers otherwise read as contradicting each other.

Also: the `prepare_cached` -> `prepare` reason moves to the call site;
`simple_logger` joins the seed drift test, being the seed this PR
actually had to move; `source-path=SCRIPTDIR` makes the four
`# shellcheck source=` directives resolve; the `where` clause inside
`config_store_contract_tests!` is wrapped like its two siblings, which
rustfmt cannot reach; the Spin prerequisites bullet lifts its error
text into a fenced block instead of relying on lazy continuation; and
the migration guide gains the CLI pin table -- `install-fastly.sh`
fails closed when `.tool-versions` and `versions.json` disagree, so an
app that moves the Rust pin alone gets a red deploy.

* Carry main's new code onto the 1.98 toolchain

The merge of main was textually clean in these files, but the code it
brought in was written against 1.95 and does not build here.

`release.rs` is new on main and hashes with `format!("{:x}", ..)`.
sha2 0.11's `Array` dropped `LowerHex`, so all three sites — one in the
digest helper, two in test helpers — now use `base16ct`, matching the
two sites this branch already converted. `base16ct` joins
`edgezero-adapter`'s `cli` feature beside `sha2`, which gates the
module that uses it. The test helper at `release.rs:504` recomputes the
digest independently and compares it against the production value, so
it stands as the check that the hex output is unchanged.

Four signatures tripped `inline_trait_bounds`: `serve_app`,
`assert_send_sync`, and `dispatch_app` in both the Spin and Cloudflare
adapters. The last two are behind `target_arch = "wasm32"`, so a clean
host clippy run does not see them — the per-target matrix is what
caught them, for the second time on this branch.

`check_shared_dep_pins.sh` still named the smoke fixture at its old
`deploy-core/tests/` path, which #381 moved to `deploy-fastly/tests/`.
The gate reported a missing validator pin rather than passing silently,
which is the behaviour it was written for; the path is updated and the
relocated fixture carries the `edition = "2024"` and `validator 0.21`
edits that the modify/delete conflict would otherwise have dropped.

* Bring the lifecycle fixture onto the upgraded dependencies

CI failed ten jobs on one cause. `tests/fixtures/reusable-app` is a
separate workspace added on main with its own lockfile, and the merge
left it pinning the pre-upgrade versions. Every job that touches it
builds with `--locked`, so the first one to run stopped at

  error: cannot update the lock file ... because --locked was passed

and the rest followed. Its `fastly` pin moves 0.12 -> 0.13 and the
lockfile is refreshed, which brings it to the same `fastly` 0.13.1,
`validator` 0.21.0 and `sha2` 0.11.1 the root workspace resolves.

`fixture-spin` also pinned `spin-sdk = "~6.0"` while the adapter it
links against is on `~7.0`. Both copies landed in the lockfile and the
build failed with `expected IncomingBody<Request>, found a different
IncomingBody<Request>` -- an error that names neither the crate nor
either version. Aligned to `~7.0`.

`check_shared_dep_pins.sh` now compares `spin-sdk` majors across the
root, the demo, this fixture and the scaffold seed, the way it already
compares `validator`. Majors rather than exact strings, since the root
spells it `~7.0` and the seeds `7`. Confirmed it reports the fixture by
name and version when reverted to `~6.0`.

These fixture checks run through `--manifest-path`, so a
`cargo clippy --workspace` at the repo root never reaches them. That is
why the local gates passed while CI did not.
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.

Bind Fastly stores selected by each deployment environment

4 participants