Repository navigation
Conversation
Empty tracking commit for the implementation work tracked in: docs/superpowers/plans/2026-06-27-provision-local.md Issues: - Epic: <epic-issue-url> - Per-section sub-issues linked from the epic. This PR opens as a DRAFT and stays draft until Section 1 lands its first real commit. Each section opens as its own follow-up PR that lands here before the umbrella merges to main.
This was referenced Jun 29, 2026
run_shared_checks iterates every declared adapter and dispatches validate_adapter_manifest, which for Spin does fs::read_to_string(manifest_root.join(rel)). With the containment guard sitting after run_shared_checks, a manifest declaring [adapters.spin.adapter].manifest = "../outside/spin.toml" could trigger a filesystem read outside the project root before the guard rejected it — a spec violation of §"Path containment (MUST)" which requires the helper run BEFORE any manifest-path use. Fix by relocating the check to fire immediately after load_push_context, and looping over every declared adapter (not just ctx.adapter) since run_shared_checks reads all of them. Also close Task 7's Minor about the duplicate adapter_entry call by removing the now-redundant per-adapter guard block. Regression test: config_push_local_rejects_parent_traversal_in_ sibling_spin_adapter declares a poisoned Spin adapter alongside the pushed axum adapter, and asserts the error names the containment violation (not Spin's "failed to read spin manifest" message that would surface under the old ordering). Also tighten copy_tree's else-branch to explicitly gate on is_regular_file() rather than "everything non-dir non-symlink", add a Unix symlink-skip test, and drop a stale #[expect(dead_code)] on ValidationContext::manifest_path that now has real callers.
…to feature/provision-local-impl
The bare-cwd variant of the accept-test (--manifest edgezero.toml)
previously wrote the manifest to a tempdir and set EDGEZERO_MANIFEST,
but run_provision reads args.manifest directly (no env fallback).
The test therefore failed on manifest load ("failed to load
edgezero.toml") and its negative !contains(path-safety-markers)
assertion vacuously passed — no actual coverage of the
`args.manifest.parent() == ""` fallback.
Fix by adding a CwdGuard RAII helper that chdirs into the tempdir
under the manifest_guard() serialisation lock and restores the
previous cwd on drop. Both accept-tests now also assert positively
that the error is the (true, true) dispatch stub ("local dry-run
staging lands in Task 10/11"), proving the manifest loaded AND
path-safety passed AND we reached the dispatch matrix. Drop the
now-unnecessary EnvOverride from both tests.
Reviewer: reviewer of Task 9 pushed this as a Low ahead of Task 10
because run_with_staging depends on manifest-root/cwd correctness.
…closed
Build / artifact discovery:
- Cloudflare `config push` / `diff` pass wrangler an ABSOLUTE `--config`
path; the previous manifest-root-relative path doubled under the
command's changed cwd (`crates/cf/wrangler.toml` -> `crates/cf/crates/cf/
wrangler.toml`), targeting the wrong project.
- The cargo target-dir resolver also honours `CARGO_BUILD_TARGET_DIR` and a
`--config build.target-dir=…` build arg, so those redirects no longer
fall back to a stale conventional artifact.
- Spin's native build refreshes the conventional `target/` path that the
synthesised `spin.toml` `source` references, so `spin up` / `spin deploy`
don't read a stale module after a custom-target build.
Provision / push correctness:
- The provision lock's inherited advertisement is now VERIFIED: a matching
`EDGEZERO_PROVISION_LOCK` only lets a nested provision borrow after a
non-blocking lock attempt proves a real lock is held; a forged / stale
advertisement takes the real lock instead of skipping serialization.
- Provider not-found classification excludes auth/config failures ("API
key/token not found", "profile not found", 403, unauthorized) across
Fastly + Cloudflare read + chunk paths, so a credentials error is
surfaced instead of masked as an absent store/key.
- Fastly local diff distinguishes a malformed INTERMEDIATE node
(`local_server` / `config_stores` / the store scalar) from an absent one
-- the former is now an error, not MissingStore.
- Fastly cloud provision re-emits the resource-link remediation on the
skip path (already-declared store on a deployed service), so an operator
who missed the first run can still finish linking.
- The typed local dry-run stops before typed provision when the base
partially fails, mirroring real execution instead of previewing writes
the real command won't perform.
Read-side symlink boundaries:
- Axum diff/read rejects a symlink at ANY component down to the JSON
(a symlinked `.edgezero`), not just the final file.
- Spin's read-path symlink guard walks from the CRATE ROOT, covering a
nested manifest whose db resolves above the manifest's own dir.
Smoke scripts:
- `restore_backups` stages into a sibling temp and atomically swaps, so a
failed restore never deletes the live path, and it returns non-zero on
failure (the earlier version deleted first and returned success).
- The config and KV smokes back up emulator state (`.edgezero` /
`.wrangler` / `.spin`) that pushes overwrite.
Docs / gates:
- Reconcile a remaining spec paragraph with the no-template hard cutoff and
extend the no-legacy-read gate to scan generated `*.rs.hbs` templates.
Provision lock: - Borrowers of a deploy's advertised lock now hold a SEPARATE sibling lock (`provision.borrow.lock`) so two provisions the same deploy spawns still serialise against each other instead of both running lock-free and concurrently rewriting the same manifests / env files. The parent's main lock still gives tree-level exclusion; a forged advertisement still takes the real lock (proven via a non-blocking attempt). Build / artifact discovery: - The cargo target-dir resolver matches cargo more closely: the LAST `--target-dir` / `--config build.target-dir=` wins, a `--config <file>` is read for its `[build] target-dir`, and the `.cargo/config.toml` walk covers every ancestor to the filesystem root plus `$CARGO_HOME`. - The generated Spin `commands.build` pins `--target-dir target` so a `CARGO_TARGET_DIR=custom` build can't leave `spin.toml`'s `source` pointing at a stale conventional artifact (the shell build bypasses the native source-refresh). Smoke scripts: - Backup capture REFUSES a symlink (matching the provision-side policy) rather than following it and losing link identity / mutating the target. - Cleanup kills the server AND its descendants (workerd/spin) and waits for the port to free BEFORE restoring, so a survivor can't flush state over the restore; INT/TERM now run cleanup then EXIT so an interrupt can't resume the smoke and re-mutate after restoration. The config and KV smokes back up emulator state too. Migration / cutoff enforcement: - The migration matcher, runbook, and CI gate now also untrack the provision-written secret-bearing env files (Spin's `<crate>/.env`, Axum's `.edgezero/.env`), not just `.dev.vars`. - A generator test forbids any scaffold template from reclaiming a provision-owned adapter manifest (`axum.toml` / `spin.toml` / ...). Dry-run: - A partial-failure error surfaced from the staged provision is sanitised (staged tempdir path -> project path), matching the status-line rewrite, so no raw staging path leaks to the operator.
Provision lock (crates/edgezero-cli): - Authenticate the deploy lock advertisement with a per-holder token written into the lock file. A nested provision borrows only when the advertised token matches the file token, so a stale/leaked/forged advertisement can no longer bypass an unrelated holder -- it serialises on the real lock instead. - Make the lock advertisement (path + token) OVERRIDE inherited env in the child overlay so a nested deploy advertises its own lock rather than an ancestor's, closing a nested-deploy self-deadlock. Cargo target resolution (edgezero-adapter cli_support): - Parse inline `--config` payloads as TOML so a spaced `build.target-dir = "x"` is honoured, not dropped. - Follow config `include` directives when reading `[build] target-dir`. - Resolve a config-file-relative `target-dir` against cargo's real base (the parent of the directory containing the file), matching where the artifact actually lands. Smoke cleanup (scripts): - Only kill port holders when the smoke actually launched a server, and refuse to launch when the port is already held, so a pre-launch failure never SIGKILLs an unrelated service. Generated commands: - Omit the Axum shell `commands` block so build/serve route through the axum.toml-aware registry adapter instead of a raw `cargo -p` override. - Point Spin serve/deploy at the declared manifest (and its parent for the runtime config) so a nested spin.toml resolves. Cloudflare: - Check auth/config failures before the missing-store/missing-key mapping so an auth error mentioning "binding" isn't misread as a missing store. - Reject typed secret keys in the reserved `EDGEZERO__STORES__` namespace, which would otherwise dedup against generated store overlays. Dry-run: - Rewrite Fastly "pinned" and Cloudflare "kv binding" status lines to the conditional so a preview no longer reads as a completed write. Docs: - Correct the CLI walkthrough: provision declares typed Spin `#[secret]` variables; only code-local store_ref keys need manual declaration.
Provision lock:
- Fail the acquire if the per-holder token can't be persisted to the lock
file. Previously a failed write was ignored while the token was still
advertised, so a nested provision could not authenticate the borrow and
dead-locked on the parent's lock.
Cargo target resolution (edgezero-adapter cli_support):
- Follow `include` from inline `--config` payloads too, and let a LATER
include override an earlier one (cargo's merge order).
- Detect inline `--config` TOML by parsing (accepts quoted dotted keys),
not a hand-rolled key scan.
- Honour a ctx-provided CARGO_HOME when locating the global config.
Axum:
- Anchor the dev server's KV `.edgezero` at the project root (matching the
config store and provision), and refuse a symlinked `.edgezero`, so a
registry `serve` (cwd = crate) no longer strands KV state in the crate
dir or follows a link out of the tree.
- Reject typed secret keys in the reserved `EDGEZERO__STORES__` namespace,
which would dedup against generated store overlays in `.edgezero/.env`.
Spin:
- Omit the generated shell `commands` block so build/serve/deploy route
through the registry adapter, which reads the declared spin.toml
dynamically and refreshes the conventional artifact -- a static override
baked the manifest path and diverged from provision on a nested move.
Fastly:
- Reserve the internal `edgezero_runtime_env` store name: a user store
resolving to it is refused before any write / account mutation.
- On a broken stdin write to `fastly`, reap the child and surface its
stderr instead of returning a bare pipe error.
Cloudflare:
- Refuse a malformed tracked namespace id instead of restoring garbage and
reporting success.
- Map wrangler's "No KV Namespaces configured!" to MissingStore.
Dry-run:
- Rewrite Axum "ensured" and the Fastly local store status lines to the
conditional so a preview never reads as a completed write.
Smoke scripts:
- Route the config-key-override smoke's teardown through the ownership-
guarded shared helper and detect port occupancy via lsof, so a
pre-launch failure never SIGKILLs an unrelated service.
Docs:
- Fix the VitePress production build (the `{{ }}` example moved back into a
fenced block) and correct the Spin secret workflow: provision --local
declares typed `#[secret]` variables; the migration guide uses
SPIN_VARIABLE_.
CI:
- Gate the smoke scripts with ShellCheck and run the smoke backup helper
unit tests.
Cargo target resolution (edgezero-adapter):
- Accept array-of-tables `include = [{ path = "..." }]` entries.
- Prefer the extensionless `.cargo/config` over `.cargo/config.toml` when
both exist, matching cargo (verified against cargo 1.95).
Cloudflare:
- Validate tracked namespace ids in LOCAL provision too (cloud already
did) -- a malformed id no longer seeds wrangler.toml.
- Require an absence qualifier alongside "binding" before mapping a
wrangler error to MissingStore, so an invalid-binding / malformed-
manifest error surfaces instead of reading as an absent store.
- Reserve the whole `EDGEZERO__` namespace for typed secrets.
Axum:
- Reserve the whole `EDGEZERO__` namespace for typed secrets (not just
`EDGEZERO__STORES__`), so a secret can't shadow adapter/logging config.
Fastly:
- Reject the reserved `edgezero_runtime_env` store in the config
read/write dispatch, not only during provision, so a runtime env
overlay can't route app config into the internal override store.
- Correct the Config Store key limit to 255 (was 256).
Spin:
- Pass `--from <declared-manifest>` on deploy and serve so a non-standard
manifest filename is honoured.
- Refresh the component's DECLARED `source` (matched by this crate's wasm
filename) in addition to the conventional target, so a custom source
path never serves a stale module.
Smoke scripts:
- Tie the port teardown to processes proven to be descendants of the
wrapper (captured before teardown); an unrelated process that grabs the
freed port is never killed.
- Replace the fragile `[^[]*` runtime-env block regex with a line-based
section removal that survives brackets in comments/values.
Docs:
- Correct the manifest-store-migration host claim, the direct `spin up`
command (runtime-config + .env), and the Spin secret .env path.
Spin cloud detection: - Add an explicit `[adapters.spin.adapter].cloud = true` flag threaded through the push context. The hard-cutoff manifest omits the `[adapters.spin.commands]` block, so the old "sniff commands.deploy for 'spin cloud deploy'" heuristic could never fire on generated projects -- cloud `config push`/`diff` silently fell through to local SQLite. Cloud now routes on the flag (or the legacy deploy-command sniff for hand-written manifests). Provision lock (composed-deploy deadlock): - A deploy only SPAWNS provisions; it writes no provision files itself, so a borrowing deploy now takes NO sibling lock. Holding the single shared sibling lock across the deploy subprocess dead-locked a nested (grand)child borrower that then contended on the same file -- a three-level composed deploy hung. Co-sibling provisions still serialise, because each of them takes the sibling lock via `acquire`.
A wasip2 Spin component reads its own sandboxed WASI env (`std::env`); it does NOT inherit the `spin` host process environment. Provision writes the `EDGEZERO__*` store overlays (`__NAME`, per-environment `__KEY`) into the spin crate's `.env`, and `edgezero serve` loaded them into the host process -- but Spin never forwarded them into the guest, so every store silently fell back to its logical id and a `__KEY` override had no effect. - `run::serve` now forwards every `EDGEZERO__*` var to the guest via `spin up --env KEY=VALUE` (secrets still travel as Spin variables; other host env stays out of the sandbox). Extracted `guest_env_forwards` with a unit test. - The config-key-override smoke's Spin row passes the `__KEY` override via `spin up --env` instead of a host env var; verified live against Spin 4.0.2 (staging blob with the override, default without). - Corrected the spec's Spin environment model and the adapter guide to document that store overrides must reach the guest through `--env`, not the host environment.
…to feature/provision-local-impl
The dependabot syn 2->3 update refreshed the ROOT Cargo.lock but not the excluded `examples/app-demo` workspace's lock, so its CI step (`cargo test --workspace --all-targets --locked`) failed with 'cannot update the lock file ... --locked was passed'. Regenerate app-demo's lock (adds syn 3.0.3) so it matches its manifests; all app-demo tests pass under --locked.
…ules Reconciles the branch with origin/main (Fastly chunked-config GC feature #314 plus dependency bumps). The main feature — operator-invoked `config gc` that reclaims orphaned Fastly config-store chunk entries — was added to the monolithic `cli.rs` on main, while this branch had split that file into a `cli/` module directory (matching every other adapter). The feature is ported into the split structure: - cli/gc.rs (new): the reclamation core (list/classify/plan/execute, generation proofs, RFC3339 age gating, recovery-command reporting). - cli/mod.rs: the `gc_config_entries` + `preflight_config_write` trait methods and the shared manifest-lock / atomic-replace / key-hygiene guards. - push_cloud / push_local / provision_local / provision_cloud: main's GC write-path changes (gc_roots tracking, orphan prune, listing-based fail-closed reads, manifest locking) hand-merged onto the branch's versions, PRESERVING the branch fixes (reserved runtime-env store guard, child-reaping stdin writes, "wrote local ..." status verbs, the not-yet-provisioned refusal, the 255-char key limit). - chunked_config.rs taken from main (GC exports + 255 limit). - The other adapters (axum/cloudflare/spin) get explicit no-op `gc_config_entries`/`preflight_config_write` impls for the `missing_trait_methods` lint. - Two main GC test fixtures updated to the branch's "manifest resolves inside the adapter crate" layout; the malformed-TOML dry-run case (which the branch deliberately refuses, consistent with the real push) dropped from the "reaches the writer" set. - app-demo Cargo.lock synced for the syn 3 bump. All gates green: fmt, clippy --all-features -D warnings, workspace tests, feature check, spin wasm32, app-demo --locked, docs build/format/lint, shellcheck, smoke backup tests. Fastly cli 252 tests, edgezero-cli 302.
The merge auto-merged `examples/app-demo/Cargo.lock` into a state cargo
could not parse ("failed to parse lock file"), because origin/main's GC
feature adds `rusqlite` (and siblings) to that excluded workspace and the
three-way lock merge left it inconsistent. Regenerate it so
`cargo test --locked` parses and resolves cleanly.
The ported fastly `cli` test modules import/use `#[cfg(unix)]` fixtures
(fake-`fastly` PATH shims, `path_mutation_guard`) whose imports were left
ungated. On the host (unix) everything compiles, but the CI `fastly wasm
clippy` matrix (`--features "fastly cli"` and `--features fastly` on
wasm32-wasip1, which is NOT unix, with --all-targets) saw the cfg(unix)
helpers vanish and the imports/one test dangle -- 9 unresolved/unused-import
errors. Align the `#[cfg(unix)]` gating on those imports (and the one test
that calls `path_mutation_guard`) so the import set matches what compiles on
a non-unix target. No imports deleted; each stays available on unix.
Verified clean: fastly clippy on {fastly cli, fastly} x wasm32-wasip1, cli
+ no-default on host; fastly tests (252 cli / 53 default); plus cloudflare
+ spin wasm clippy and app-demo fmt/clippy.
…ects Two P0 blockers (both live-verified): - Spin build no longer TRUNCATES its own artifact. `refresh_declared_source` compared lexical paths, so the default `source` (`../../target/.../x.wasm`, a relative path with `..`) never matched the normalised artifact and `fs::copy` copied the file onto itself -> 0 bytes. Now guarded by a canonicalize-based same-file check (also for the conventional refresh), plus containment (refuse a `source` resolving outside the workspace or through a symlink) and single-component matching (#1, #5). - Fresh Cloudflare scaffolds can build/serve/deploy. The synthesised `wrangler.toml` now declares `[build] command = "worker-build --release"` (main points at the worker-build-generated shim), and the adapter drops its `[commands]` shell block so build/serve/deploy route through the registry dispatch, which passes `--config <manifest>` (also fixes custom manifest paths being ignored) (#2, #7). Spin: - `[adapters.spin.adapter].cloud = true` is the SOLE Fermyon Cloud selector; the legacy deploy-command heuristic and its `manifest_adapter_deploy_cmd` field are removed, and `config diff` threads the flag like `config push` (#3). - `edgezero serve` forwards parent-shell `EDGEZERO__*` overrides to the guest via `spin up --env` (highest precedence) so a shell override reaches the sandboxed component (#4). - deploy uses `spin cloud deploy` (bare `spin deploy` needs a plugin on Spin 3+/4) (#6). Cloudflare / Axum (via subagents): - typed secret keys are validated for `.dev.vars` round-trip (empty/=/#/ newline/whitespace) (#16); local push refuses a `.wrangler` symlink escape (#11); axum runtime honours `EDGEZERO_MANIFEST` when locating `.edgezero` (#8). Fastly: - cloud provision now writes the per-store `__NAME` runtime mappings into `edgezero_runtime_env` so a renamed cloud store is reachable (#10); the dry-run preflight redacts malformed-manifest parse errors and models the real writer's inline-toml rejection (#13). Convergence (via subagent): reprovisioning converges managed `__NAME` overlays across env_file + all four adapters via a new `upsert_lines_with_header` (#9). CLI: - dry-run staging refuses a TMPDIR inside the project (self-recursive copy) and no longer follows a symlinked live path into the diff output (#12); - cloud provision preflights manifest writability before mutating the account, so an unwritable manifest can't orphan a created resource (#15).
R23 fixed a three-level composed-deploy DEADLOCK by making a borrowing deploy hold NO lock -- but a borrowing deploy DOES write `fastly.toml` (`fastly compute deploy` records the service_id), so a lock-free deploy borrower races a co-sibling provision and loses an edit. Replace the single shared `provision.borrow.lock` with a DEPTH-KEYED sibling lock: a borrower serialises its file writes against its co-siblings on `provision.borrow.<depth>.lock`, and a deploy advertises `depth + 1` to its subprocess (`EDGEZERO_PROVISION_SIBLING_DEPTH`). A more-nested borrower therefore serialises on the NEXT depth's file and never blocks on an ancestor's sibling lock -- holding serialisation at EVERY level with no deadlock, and making borrowing deploys serialise like provisions. (#14)
…tainment refinements - cloudflare: run deploy/serve from the crate dir with an absolute --config and a depth-aware `main` relpath so a nested wrangler config resolves the worker-build shim correctly - spin: honour the manifest's adapter component selector when refreshing the declared source, reject intermediate symlinks + self-copy, and forward the guest --env overlay without panicking on non-Unicode env keys; deploy via `spin cloud deploy` - axum: absolute()-normalise before .parent() in manifest_root_from_env - fastly: converge every store's runtime name mapping on cloud provision; local push/provision dry-run degrades over unprovisioned/malformed prior state instead of hard-failing (matches the chunked-config GC contract) - provision lock: compare advertised vs calculated lock targets by canonical path, and re-authenticate after a sibling borrow so a stale token can't bypass an unrelated holder - provision dry-run: diff the "before" side against a T1 staging snapshot instead of re-reading the live worktree at render time, so a concurrent external rewrite can't smear two worktree instants into one report; redaction now tracks multiline quoted env values - docs: cloudflare required [build], cloud=true selector, `spin cloud deploy`, fastly secrets via env var - smoke scripts: share a single backup/provision-local helper
Integrates upstream's layered deploy actions + Fastly staging lifecycle (#316), optional typed secret paths + Fastly store mappings (#344), runtime_env_config for custom Fastly entry points (#351), and dependency bumps, reconciled against this branch's provision-local work. Conflict resolutions: - adapters (cloudflare/spin/axum): added the 4 Fastly-only staging-action reject arms (DeployStaged/EmitVersion/Healthcheck/Rollback) to each `cli/mod.rs`; kept our `cli/` module restructure over upstream's monolithic `cli.rs`. - fastly: re-homed upstream's ~4300-line staging lifecycle into a new `cli/staging.rs` wired to the 4 execute arms, reusing our existing module helpers (RUNTIME_ENV_STORE_NAME, ConfigStoreLookup, populate_runtime_name_mappings, find_fastly_manifest, ...) instead of duplicating them. - adapter.rs: merged our `build_child_env` (provision-lock advertisement + required-secrets assertion + env overlay) into upstream's `build_shell_command`/`run_shell`/`run_shell_tee` split; `execute_capture` is now overlay-aware. - lib.rs: `run_deploy` now acquires the deploy provision lock and threads its advertisement overlay through upstream's staged/capture/production dispatch; kept upstream's new run_healthcheck/run_rollback/ run_active_version. Extracted `acquire_deploy_lock`. - config.rs: kept our read-phase lock release before the consent prompt and upstream's emit-outputs-on-skip; unioned ConfigPushArgs bools (added `staging`). Extracted `assert_push_adapter_paths`. - args/main/template: merged the `--staging`/`Rollback` additions with our `--local`/typed-provision changes. Verified: fmt, clippy --workspace --all-targets --all-features, workspace tests, feature check, spin wasm32-wasip2, app-demo --locked, docs lint, shellcheck.
`run_with_staging_refuses_a_tempdir_inside_the_project` overrode the process-global `TMPDIR` while holding `env_lock()` -- a DIFFERENT mutex from the `manifest_guard()` that the other ~93 env-mutating tests in the `edgezero-cli` binary take. Two disjoint mutexes over one global environment serialise nothing. `tempfile::TempDir::new()` reads `TMPDIR`, so concurrent tests had their tempdirs created inside this test's `inside-tmp` directory, which it then deleted on drop. The victim failed with a confusing `No such file or directory (os error 2)` on a path like `.../<outer>/inside-tmp/<inner>/edgezero.toml`, and because it panicked while holding `manifest_guard`, it POISONED that mutex -- cascading into 30-47 collateral `manifest guard: PoisonError` failures that disguised the single root cause. Take `manifest_guard()` instead, per that guard's documented contract. Also correct `env_lock`'s doc: "one lock per test binary is sufficient" is only true when a binary uses ONE lock. `edgezero-cli` defines three, so the guidance as written invited exactly this bug. Verified: reproduced at ~1/10 stress runs (16 threads) before the change, including a captured leaked-tempdir path; 0/10 after. Full workspace tests 4x (20/20 binaries, no panics), fmt, clippy --workspace --all-targets --all-features, feature check, spin wasm32-wasip2, shellcheck.
…ing the smoke fixture Three independent CI failures on the merge commit, none caught locally because the gate set I had been running was incomplete. 1. `fastly wasm clippy` (format.yml matrix): `unused import: tempfile::tempdir` in cli/staging.rs. Every `tempdir()` call site is inside a `#[cfg(unix)]` test that PATH-shims a fake `fastly`, so the import is dead on wasm32-wasip1. Gate it with `#[cfg(unix)]` like its sibling imports. A host-target `clippy --all-features` never compiles this target, which is why it passed locally. 2. `lifecycle-smoke` (deploy-action.yml): a staged deploy against a COMMANDS-ONLY adapter was rejected with "`[adapters.fastly.adapter] .manifest` is not declared". `assert_adapter_declaration_consistent` deliberately permits an adapter that declares NEITHER `.manifest` nor `.crate` (it owns its build/deploy and never uses declared-path discovery), but `resolve_declared_adapter_paths` hard-errored on the missing `.manifest`. A `deploy --staging` bypasses `commands.<action>` and drives the vendor CLI, so it reaches that resolver. Return `(None, None)` for the neither-declared case -- exactly as for an undeclared adapter -- so the adapter falls back to its own discovery. Half-declared adapters are still rejected. 3. `cache-smoke` / `config-push-smoke` (deploy-action.yml): "committed source is required; the working tree for 'fixture-app' is dirty" on the SECOND deploy. `run_deploy` now holds the cross-process provision lock, which creates `<project>/.edgezero/provision.lock`. Every real project gitignores the whole `.edgezero/` directory (this repo's .gitignore and the scaffolder's gitignore.hbs both do), but the smoke fixture predates the lock and hand-rolls a minimal ignore list, so the lock file showed up as untracked. Add `.edgezero/` to the fixture's .gitignore. The `cargo test` job's failure was the TMPDIR guard leak already fixed in 022b933 (its log shows the same `inside-tmp` leaked path); no further change needed here. Verified: deploy-core contract tests 260/260, smoke_backup_test 37/37, workspace tests 20/20 binaries, full wasm matrix (fastly wasip1 with `fastly` and `fastly cli`, cloudflare wasm32-unknown-unknown, spin wasip2), fastly cli-only and no-default-features clippy, check_no_nested_app_config, app-demo fmt + clippy --locked, workspace fmt + clippy --all-features, shellcheck.
8 tasks
Let adapters classify sensitive TOML artifacts for fail-closed preview redaction, and omit source excerpts from native provision parse errors. Use the existing Fastly manifest lock and atomic replacement for both base and typed local provision so deploy writeback cannot be erased. Normalize child application manifest selectors before changing cwd, accept root adapter crates, and guard Spin artifact and SQLite paths. Cover the reported failures with secret-sentinel, subprocess locking, child execution, and public adapter symlink regressions.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
edgezero provision --local, folds all five adapter manifests into the gitignored-generated model, and hardens the surrounding CLI (path safety, env redaction, adapter-scoped env-file load) acrossprovision,config push,config diff, andserve.Source artifacts
docs/superpowers/specs/2026-06-23-provision-local.mddocs/superpowers/plans/2026-06-27-provision-local.md— 43 tasks across 9 sectionsThe 9 plan sections all landed on this branch (rather than as separate sub-PRs merging in); each closes on merge below.
What ships
provision --local. NewProvisionMode::Localarm threads throughAdapter::provision. Local mode:toml_edit::DocumentMut(CLI-owned bootstrap runs before validation).EDGEZERO__STORES__<KIND>__<ID>__NAME=…) so the runtime resolves stores at startup..edgezero/.env,.dev.vars,<spin_crate>/.env).Dry-run stages a real
fs::copyinto atempfile::TempDirand diffs the result back. The tree stays byte-identical either way.Typed provision (
run_provision_typed::<C>). Generated<app>-clibinaries dispatch the typed variant, which runs the base flow then additionally emits per-secret placeholder lines derived fromAppConfig's#[secret]/#[secret(store_ref)]fields (Axum:<key>=; Cloudflare:<key>=""in.dev.vars; Fastly:[[local_server.secret_stores.<store_id>]]; Spin: lowercased[variables]+SPIN_VARIABLE_<NAME>=in.env). The bundlededgezerobinary intentionally does not emit placeholders — it has no downstreamAppConfigtype.All five adapter manifests are provision-generated and gitignored.
axum.tomljoinedwrangler.toml/fastly.toml/spin.toml/runtime-config.tomlin the 2026-07 amendment; the scaffold.hbstemplate foraxum.tomlwas removed so scaffold-time provision is the single writer. A CI grep gate enforces the whole set.Path safety. New
path_safety::{assert_provision_paths_safe, assert_provision_paths_contained}helpers guard every CLI entry point that joinsmanifest_rootwith an operator-declared adapter path. Cloud dispatch runs the "safe" variant (absolute-path rejection +..traversal rejection);--localruns the stricter "contained" variant that additionally requires BOTH[adapters.<name>.adapter].manifestAND[adapters.<name>.adapter].crateto be declared, with the manifest resolving inside the crate dir. Wired intorun_provision,run_provision_typed,run_config_push_typed,run_config_diff_typed, andrun_serve.Renamed / nested adapter crate support.
cli_support::read_adapter_crate_namewalks upward from the manifest's parent to the first Cargo.toml insidemanifest_rootand reads[package].name. All four bundled adapter synthesisers use this to name generated manifests correctly under operator renames ([adapters.axum.adapter].crate = "crates/server"→crate = "server"inaxum.toml; Cargo packagespin-server→ wasm pathspin_server.wasm). Nested manifests likecrates/server/config/spin.tomlresolve correctly too.Spin component / crate decoupling.
synthesise_spin_tomlnow takes two distinct identities —crate_name(from Cargo.toml, drives[application].nameand the wasm source basename) andcomponent(from[adapters.spin.adapter].component, drives[[trigger.http]].componentand the[component.<id>]table key). Fixes the pre-2026-07 conflation where setting a component selector would silently mispoint the wasm artifact.Env-value redaction in dry-run.
.envand.dev.varsbodies are rewritten asKEY=<redacted>before diffing so operator secrets never surface in dry-run output. CommentedKEY=valuelines (adapter provisioners emit these as# EDGEZERO__STORES__…__KEY=placeholders, and operators stash real values behind#for later use) get the same treatment; pure comments and blank lines pass through so structural drift stays readable.Adapter-scoped env-file load in
run_serve. Axum reads<manifest_root>/.edgezero/.env; Spin reads.envnext to the resolvedspin.toml(matches where provision writes it — a nestedmanifest = "crates/spin/config/spin.toml"correctly resolves tocrates/spin/config/.env). Both run through the path-safety guard before the read. Contract test spawns a real serve child, inspects its inherited environment via a file the child writes, and asserts the marker propagates.Section breakdown
Each section closes on merge:
ManifestAdapterDeployedschema + writeback (Tasks 14–16, 16b–16c)Post-plan additions (from external review iterations)
Multiple review passes drove refinements past the original plan:
.gitignore+ CI gate + spec-plan-guide docs updated.read_adapter_crate_nameupward walk added to support nested manifests (v5 review).[application].nameand wasm basename decoupled from the component selector (v5 review)..manifestAND.crate;run_config_diff_typedgets the guard (previously read-only justified skipping);run_servegets the guard on the Spin env-file load.# KEY=valuelines (v6/v7 reviews).run_manifest_shape_gates(addsvalidate_deployed_field_ownershipalongside capability + handler-path checks).run_servederives.envpath from[adapters.spin.adapter].manifest.parent()(was.crate— broke on nested manifests). Contract test observes the spawned child's env (v8 review).#[expect(...)]restructure sweep acrossedgezero-cli,edgezero-core/manifest.rs(Deserialize impls), and adaptercli/mod.rsfiles (removed via explicit trait method overrides,ConfigPushSuppressionssub-struct,streammodule, structural code fixes rather than workspace-levelallows).CI gates (all pass locally)
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test --workspace --all-targetscargo check --workspace --all-targets --features "fastly cloudflare spin"cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spinexamples/app-demoworkspace testscargo test -p edgezero-cli --test generated_project_builds -- --ignored)Test plan
provision_local_*cases + Spin's env-label alignment quartet (Section 9)run_serve,run_config_diff_typed,run_config_push_typed,run_provisionMigration notes
<app>-cli provision --adapter <name> --localafter cloning to regenerate the five adapter manifests (.gitignorecovers all of them plus.dev.vars).[adapters.<name>.adapter].manifestand[adapters.<name>.adapter].crateare now both required forprovision --localandconfig push/diff --local. Every scaffolded project already sets both. Cloud dispatch remains permissive.run_provision_typed::<AppConfig>(not the untypedrun_provision) so#[secret]fields reach the adapters'provision_typedimpls.