From 55908185605c328aa352424023c1c74243d1450b Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:17:30 -0700 Subject: [PATCH 1/2] Revise deploy-action spec: layered, adapter-independent, app-provided CLI Restructure the GitHub Actions deploy design into three composable layers (build-cli, deploy-core engine scripts, minimal deploy-fastly wrapper) and generalize the docs beyond Fastly-only / Trusted-Server-only. Key design changes: - build-cli compiles the CLI package the *application* provides (a crate in the app's own workspace), built from the app checkout with an isolated CARGO_TARGET_DIR and --locked so it never dirties the tree; publishes a self-describing tar (cli-meta.json) so the executable bit survives. - deploy-core is adapter-independent shared scripts sourced by wrappers via $GITHUB_ACTION_PATH; provider names/creds/flags arrive only via provider-env, provider-env-clear, deploy-flags, deploy-args. - Distinguish Git root (revision + dirty check) from Cargo workspace root (target/ + Cargo.lock + cache) for correct monorepo caching. - No Python in CI; actionlint + zizmor from pinned release binaries; third-party actions pinned to readable released tags. - Make the Fastly build-in-deploy credential caveat explicit (trusted refs). - Rename the Trusted Server migration plan to a general adoption guide for any EdgeZero app repo, with same-repo/separate-repo/monorepo examples and a private-repo token note. --- ...ezero-deploy-action-implementation-plan.md | 187 ++- docs/specs/edgezero-deploy-adoption-guide.md | 236 +++ docs/specs/edgezero-deploy-github-action.md | 1276 ++++++++--------- .../trusted-server-deployer-migration-plan.md | 70 - 4 files changed, 955 insertions(+), 814 deletions(-) create mode 100644 docs/specs/edgezero-deploy-adoption-guide.md delete mode 100644 docs/specs/trusted-server-deployer-migration-plan.md diff --git a/docs/specs/edgezero-deploy-action-implementation-plan.md b/docs/specs/edgezero-deploy-action-implementation-plan.md index fe108ad7..75f0db32 100644 --- a/docs/specs/edgezero-deploy-action-implementation-plan.md +++ b/docs/specs/edgezero-deploy-action-implementation-plan.md @@ -1,64 +1,151 @@ -# EdgeZero Deploy GitHub Action Implementation Plan +# EdgeZero Deploy GitHub Actions Implementation Plan -**Status:** Implemented in this worktree +**Status:** Revised plan (layered, adapter-independent) **Spec:** `docs/specs/edgezero-deploy-github-action.md` ## Scope -Implement the Fastly-only v0 deploy action in the EdgeZero monorepo at `.github/actions/deploy/action.yml`. +Implement the layered deploy actions in the EdgeZero monorepo: -The action deploys already checked-out application source. It does not perform checkout, Fastly staging, runtime config mutation, health checks, rollback, or provider-specific deployment metadata parsing. +```text +.github/actions/build-cli +.github/actions/deploy-core +.github/actions/deploy-fastly +``` + +`build-cli` compiles the caller-selected EdgeZero CLI once and publishes it as an +artifact. `deploy-core` is the adapter-independent deploy engine that consumes +the prebuilt CLI. `deploy-fastly` is a minimal wrapper that types Fastly +credentials and calls the engine. Provider orchestration (build, deploy, +config push, provision) stays in the CLI. + +The actions deploy already checked-out application source. They do not perform +checkout, staging, runtime config mutation as a deploy side effect, health +checks, rollback, or provider deployment-metadata parsing. ## Implementation phases -1. **Action metadata** - - Define required `adapter` input and v0 Fastly-only contract. - - Add `working-directory`, `manifest`, `rust-toolchain`, `build-mode`, JSON `build-args`/`deploy-args`, `cache`, and typed Fastly inputs. - - Expose `adapter`, `source-revision`, `edgezero-revision`, `provider-cli-version`, and `effective-build-mode` outputs. - -2. **Input validation** - - Reject missing/unsupported adapters before setup. - - Reject Cloudflare, Spin, Axum, and unknown adapters in v0. - - Validate exact booleans, build mode, JSON array arguments, string-only entries, and NUL bytes. - - Allow only Fastly deploy comment args and reject service selection, auth, endpoint, profile, debug, interactive behavior, short override flags, and unknown future flags. - -3. **Project resolution** - - Canonicalize `working-directory` and optional `manifest` inside `github.workspace`. - - Resolve source Git root and committed source revision. - - Fail on dirty source. - - Resolve Rust toolchain using explicit input, Rustup files, `.tool-versions`, then EdgeZero repo fallback. - - Build exact cache key from OS, arch, Rust toolchain, target, EdgeZero revision, source revision, and lockfile hash. - -4. **Tool installation** - - Install application Rust toolchain and `wasm32-wasip1` target. - - Set `RUSTUP_TOOLCHAIN` for subsequent application build/deploy commands. - - Build the action-owned `edgezero` binary from the selected EdgeZero action commit with only Fastly CLI support. - - Install pinned Fastly CLI from official release artifact and verify checksum. - -5. **Execution** - - Resolve Fastly `build-mode: auto` to `never`. - - Run optional build without Fastly credentials. - - Run deploy with Bash arrays, typed Fastly token, action-owned `--service-id`, action-owned `--non-interactive`, then safe caller deploy args. - - Clear inherited Fastly auth aliases from every non-provider step and clear/re-export typed values in deploy. - -6. **Caching and cleanup** - - Restore cache only when `cache: true`. - - Save cache only after successful execution and non-empty resolved key/path. - - Cache only application Git root `target/`. - - Clean action-owned state/tool directories with `if: always()`. - -7. **Logging and docs** - - Write non-sensitive summary fields. - - Document full-SHA invocation, examples, inputs/outputs, credential scope, build behavior, cache behavior, and non-goals. - -8. **Validation** - - Script contract tests for validation, path confinement, toolchain parsing, cache keys, credential scoping, and build/deploy argv. - - Actionlint, ShellCheck, zizmor, checksum metadata validation, docs validation, and composite smoke test workflow. +1. **`build-cli` action** + - Required `cli-package` input: the Cargo package name of the CLI defined in + the application's own workspace. Fail if missing or not found in the app + workspace under `working-directory`. + - `working-directory`, `rust-toolchain`, `artifact-name` inputs (no `adapters` + input — the app's `Cargo.toml` pins adapters). + - Optional `cli-bin` input (default = `cli-package`; the generated CLI names + its bin after the package). + - Require a `Cargo.lock` at the app's Cargo workspace root; run all Cargo + commands with `--locked` (never mutate the lockfile). Validate via + `cargo metadata --locked` that `cli-package` exists and declares a + `` binary target. + - Install the host toolchain (no WASM target — the CLI is a native tool); + build into an **action-owned `CARGO_TARGET_DIR` under `RUNNER_TEMP`** (never + the checkout) so the CLI build leaves the app tree clean for the later + dirty-source guard, via + `cargo build --locked --release -p --bin `. No + `--features` injection: the app's own `Cargo.toml` pins its adapters. + - Read `cli-version` from `cargo metadata`; smoke-check with ` --help` + (today's CLI has no `--version`); write `cli-meta.json` (`cli-bin`, + `cli-version`, `cli-package`) next to the binary and upload both as one + **tar** so the executable bit survives `actions/upload-artifact` and the + artifact is self-describing. + - Outputs: `cli-version`, `cli-package`, `cli-bin`, `artifact-name`. + - No provider credentials in scope. Never builds the EdgeZero monorepo CLI. + +2. **`deploy-core` shared engine scripts (provider-neutral)** + - A directory of scripts under `.github/actions/deploy-core/`, **not** a + standalone composite action. Wrappers source them via + `$GITHUB_ACTION_PATH/../deploy-core/…`. + - Parameters (via env from the wrapper): `adapter`, `cli-artifact`, `cli-bin`, + `working-directory`, `manifest`, `rust-toolchain`, `target`, `build-mode`, + `build-args`, `deploy-args`, `deploy-arg-allow`, `provider-env`, + `provider-env-clear`, `deploy-flags`, `cache`. + - Download the CLI artifact (tar) under `RUNNER_TEMP`, extract preserving the + executable bit (or `chmod +x `), read `cli-meta.json` for + `cli-bin`/`cli-version` (wrapper `cli-bin` overrides), and PATH-scope it. + - Validate `adapter` well-formedness (no compiled-adapter enumeration — the + CLI rejects unknown adapters itself), booleans, JSON arrays/object, NUL + bytes. + - Confine `working-directory` and `manifest` inside `github.workspace` + (canonical paths, symlink resolution). + - Resolve **Git root** for `source-revision` and the dirty-source guard + (`build-cli`'s isolated target dir keeps this clean). + - Resolve the **Cargo workspace root** (`cargo locate-project --workspace`) + for lockfile hashing and `target/` caching — in a monorepo this may be under + `working-directory`, not the Git root (spec §11.1). + - Resolve Rust toolchain (explicit → Rustup files → `.tool-versions` → repo + fallback) and application `target` (from `adapter` when `auto`). + - Optional exact-key cache of the **Cargo workspace root** `target/` + restore/save. + - Resolve `build-mode`; optional credential-free build. + - Non-deploy steps: unset the `provider-env-clear` names. + - Deploy step: clear the `provider-env-clear` aliases, export only + `provider-env`, then run + ` deploy --adapter -- ` via + Bash arrays. Note the build-in-deploy caveat: Fastly's default `never` + compiles the app during deploy with the token in scope, so require trusted + immutable refs (spec §10.1). + - Surface results to the wrapper: `adapter`, `source-revision`, `cli-version`, + `effective-build-mode`. + - Contains no provider-specific credential names, service concepts, endpoints, + or CLI flags; invokes ``, never a hard-coded `edgezero`. + +3. **`deploy-fastly` wrapper (minimal composite action)** + - Typed inputs: `cli-artifact`, `cli-bin`, `fastly-api-token`, + `fastly-service-id`, plus forwarded `working-directory`, `manifest`, + `build-mode`, `build-args`, `deploy-args`, `cache`. + - Map `fastly-api-token` → `provider-env: {FASTLY_API_TOKEN: …}` and + `fastly-service-id` → action-owned + `deploy-flags: ["--service-id", …, "--non-interactive"]`. + - Set `adapter: fastly`, `target: wasm32-wasip1`, `deploy-arg-allow` = + `--comment` only, `provider-env-clear` = Fastly auth/endpoint aliases + (`FASTLY_API_TOKEN`, `FASTLY_SERVICE_ID`, `FASTLY_ENDPOINT`, …), Fastly + `auto` build-mode → `never`. + - Source the shared `deploy-core` scripts; no build, toolchain, or path logic + of its own. + +4. **Scripts layout** + - Provider-neutral scripts under `deploy-core/`; the Fastly install + checksum + step lives with `deploy-fastly/` (or a shared script keyed by adapter). + - No CLI-build script here — CLI build lives entirely in `build-cli`. + +5. **CI workflow (`.github/workflows/deploy-action.yml`) — no Python** + - Pin third-party actions to readable released tags (`actions/checkout@v4`, + `actions/cache@v4`, artifact upload/download at released tags). + - Run `actionlint` from a pinned release binary (no `go run @`). + - Run `zizmor` from a pinned release binary or `cargo install zizmor --locked` + (no `pip`). + - Port the metadata-validation heredocs into `tests/run.sh`. + - Composite smoke test: `build-cli` → `deploy-fastly` with fake provider + binaries; assert CLI-artifact reuse and credential scoping. + +6. **Bash contract tests (`tests/run.sh`)** + - Cover engine + wrapper: adapter/boolean/JSON validation, path confinement, + symlink escape, dirty source, toolchain precedence, cache keys, credential + scoping, deploy-arg allowlist, build/deploy argv, cleanup, log redaction, + and metadata contract checks. + - No Python; no live provider credentials. + +7. **Companion CLI change** + - Add `#[command(version)]` to the downstream CLI template + (`crates/edgezero-cli/src/templates/cli/src/main.rs.hbs`) so generated app + CLIs expose `--version`. Until adopted, `build-cli` reads the version from + `cargo metadata` and smoke-checks with `--help`. + +8. **Docs** + - Rewrite `docs/guide/deploy-github-actions.md` around the three-layer model + and general EdgeZero-app-repo adoption (not Trusted-Server-specific). + - Document the app-provided CLI package build, artifact reuse, credential + scoping, adapter layering, and non-goals. + +9. **Validation** + - Bash contract tests, `actionlint`, `shellcheck`, `zizmor`, checksum + metadata, docs validation, composite smoke test. - Workspace Rust tests, format, clippy, and feature checks. ## Known follow-up candidates -- Add provider-specific Fastly staging/rollback actions separately if needed. -- Add Cloudflare and Spin through new specs. -- Replace source-built CLI with prebuilt attested EdgeZero CLI binaries when available. +- Add `deploy-cloudflare` / `deploy-spin` wrappers via the same engine. +- Add provider-specific staging/health-check/rollback as separate actions. +- Optionally consume a prebuilt/attested CLI binary matching the app's pinned + version instead of compiling from source. diff --git a/docs/specs/edgezero-deploy-adoption-guide.md b/docs/specs/edgezero-deploy-adoption-guide.md new file mode 100644 index 00000000..d64d57ef --- /dev/null +++ b/docs/specs/edgezero-deploy-adoption-guide.md @@ -0,0 +1,236 @@ +# EdgeZero Deploy Actions — Adoption Guide + +**Status:** Adoption guide for any EdgeZero application repository + +**Spec:** `docs/specs/edgezero-deploy-github-action.md` + +The layered deploy actions are for **any** EdgeZero application repository, not a +single deployer. This guide describes the general adoption shape and then walks +through the Trusted Server deployer as one concrete migration example. + +## 1. What a consumer gets + +Three composable actions: + +- `stackpop/edgezero/.github/actions/build-cli@` — compile the CLI package + the application provides (a crate in the app's own workspace) once, publish it + as an artifact; +- `stackpop/edgezero/.github/actions/deploy-fastly@` — deploy a checked-out + Fastly application using the prebuilt CLI artifact; +- future `deploy-cloudflare` / `deploy-spin` wrappers over the same engine. + +The actions own repeatable deploy setup. The consumer owns checkout, ref +selection, permissions, environments, concurrency, timeouts, and any health +check or rollback. + +## 2. Checkout layouts + +The adapters your CLI supports come from your app's own `Cargo.toml`, so +`build-cli` takes no `adapters` input — it builds your CLI package as declared. + +### 2.1 Same-repository application + +The app and its deploy workflow live in one repo. + +```yaml +jobs: + deploy: + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - id: cli + uses: stackpop/edgezero/.github/actions/build-cli@ + with: + cli-package: my-app-cli # the CLI crate in your app's workspace + + - uses: stackpop/edgezero/.github/actions/deploy-fastly@ + with: + cli-artifact: ${{ steps.cli.outputs.artifact-name }} + fastly-api-token: ${{ secrets.FASTLY_API_TOKEN }} + fastly-service-id: ${{ vars.FASTLY_SERVICE_ID }} +``` + +### 2.2 Separate deployer and application repositories + +A deployment repo drives a separate application repo. Check the app into a path +and point both actions at it. + +> **Private app repos need their own token.** The deployer job's default +> `GITHUB_TOKEN` cannot read a _different_ private repository. Mint an app-scoped +> token first — e.g. with `actions/create-github-app-token` for a GitHub App +> installed on the app repo, or a fine-grained PAT with `contents: read` — and +> pass it to the application checkout's `token:`. The step below assumes an +> earlier `id: app-token` step produced `steps.app-token.outputs.token`. + +```yaml +steps: + - name: Checkout deployer + uses: actions/checkout@v4 + with: + path: deployer + persist-credentials: false + + - name: Checkout application + uses: actions/checkout@v4 + with: + repository: stackpop/my-edgezero-app + ref: ${{ inputs.ref }} + path: app + persist-credentials: false + # A private app repo is NOT readable with the deployer's default + # GITHUB_TOKEN. Supply a token scoped to the app repo — a GitHub App + # installation token (preferred) or a fine-grained PAT with + # `contents: read` on the app repo: + token: ${{ steps.app-token.outputs.token }} + + - id: cli + uses: stackpop/edgezero/.github/actions/build-cli@ + with: + cli-package: my-app-cli + working-directory: app + + - uses: stackpop/edgezero/.github/actions/deploy-fastly@ + with: + cli-artifact: ${{ steps.cli.outputs.artifact-name }} + working-directory: app + fastly-api-token: ${{ secrets.FASTLY_API_TOKEN }} + fastly-service-id: ${{ vars.FASTLY_SERVICE_ID }} +``` + +### 2.3 Monorepo application + +Select the app subdirectory and, when needed, an explicit manifest. Caching +resolves `target/` and `Cargo.lock` at the **Cargo workspace root** for that +subdirectory (which in a nested workspace may be the subdirectory itself, not the +repo root), so a monorepo caches the right artifacts. + +```yaml +steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - id: cli + uses: stackpop/edgezero/.github/actions/build-cli@ + with: + cli-package: api-cli + working-directory: apps/api + + - uses: stackpop/edgezero/.github/actions/deploy-fastly@ + with: + cli-artifact: ${{ steps.cli.outputs.artifact-name }} + working-directory: apps/api + manifest: edgezero.toml + cache: true + fastly-api-token: ${{ secrets.FASTLY_API_TOKEN }} + fastly-service-id: ${{ vars.FASTLY_SERVICE_ID }} +``` + +## 3. Consumer requirements + +- Check out application source yourself; the actions never call + `actions/checkout`. +- Provide a CLI package in your own workspace and name it via `cli-package`; + `build-cli` compiles that package from your checkout, so the CLI and your app + never disagree on schema. `build-cli` does not use the EdgeZero monorepo CLI. +- Provide typed provider credentials through wrapper inputs, not caller `env:`. +- Ensure the deployed ref has committed source (no dirty working tree) and, for + caching, a root `Cargo.lock`. +- Pin action references to readable released tags, or full SHAs for production + reproducibility. +- Use least-privilege permissions (`contents: read`), protected environments, + `timeout-minutes`, and appropriate concurrency. + +## 4. What the actions intentionally do not do + +The deploy actions do not perform: internal application checkout; config +expansion or JSON→provider config conversion; provider staging; provider +service-version output; health checks; or rollback. Config push and provisioning +are explicit CLI subcommands (`edgezero config push`, `edgezero provision`) a +consumer may run as separate steps, not deploy side effects. + +Consumers needing those behaviors keep them in their own workflow steps or in +provider-specific actions pinned separately. + +## 5. Worked example — Trusted Server deployer migration + +### 5.1 Current behavior + +The Trusted Server deployer orchestrates Fastly deploys with: + +- `.github/workflows/deploy.yml` (manual) and `daily-deploy.yml` (scheduled); +- `stackpop/trusted-server-actions/fastly/deploy@v2`; +- `stackpop/trusted-server-actions/fastly/healthcheck@v2`; and +- `stackpop/trusted-server-actions/fastly/rollback@v2`. + +The old deploy action checks out Trusted Server internally, accepts +`trusted-server-ref`, expands `trusted-server-config`, supports Fastly staging, +and returns `fastly-version`. + +### 5.2 Compatibility gaps vs the generic actions + +The generic actions do not provide: internal checkout, `trusted-server-ref`, +`trusted-server-config` expansion, legacy JSON→Config Store TOML conversion, +Fastly staging, service-version output, health checks, or rollback. So the +deployer cannot switch all behavior in one step. + +### 5.3 Recommended migration + +Start with a production-only workflow and leave existing staging on the old +action path until a Fastly-specific staging design exists. + +Production workflow shape: + +1. check out `trusted-server-deployer` with `persist-credentials: false`; +2. check out Trusted Server source separately at the selected ref into + `trusted-server`; +3. write a pre-deploy summary; +4. capture the currently active Fastly version before deploy (for rollback + baseline); +5. run `build-cli` with `cli-package: ` and + `working-directory: trusted-server` (compiling Trusted Server's own CLI + package, whose `Cargo.toml` already pins the Fastly adapter); +6. run `deploy-fastly` with: + - `cli-artifact` from the `build-cli` output; + - `working-directory: trusted-server`; + - `manifest: edgezero.toml` when present on all deployed refs; + - `cache: true` if trusted and safe; + - typed Fastly credentials; and + - optional safe `deploy-args` such as `--comment`; +7. run the deployer-owned production health check; +8. on health-check failure, reactivate the captured previous Fastly version; and +9. write a post-deploy summary from action outputs and the rollback baseline. + +### 5.4 Required deployer changes + +- Add explicit Trusted Server checkout; the generic actions do not call + `actions/checkout`. +- Split CLI build (`build-cli`) from deploy (`deploy-fastly`) and pass the CLI + artifact between them. +- Pin action references to readable released tags, or full SHAs for production. +- Update the post-deploy summary to stop requiring + `steps.deploy.outputs.fastly-version`. +- Move health check and rollback into deployer-local scripts/actions, or keep the + old provider-specific actions pinned. +- Audit `TRUSTED_SERVER_CONFIG`; if still needed, keep config expansion in the + deployer workflow or run `edgezero config push` as an explicit step before or + after deploy. +- Confirm the canonical Trusted Server repository/ref has root `Cargo.lock`, + `Cargo.toml`, `fastly.toml`, and preferably `edgezero.toml`. + +### 5.5 Gotchas + +- `daily-deploy.yml` appears to stage but health-check/rollback production by + default. Decide whether the scheduled workflow is production or staging and fix + the mismatch before migration. +- The generic actions have no Fastly staging contract. Keep staging legacy or + design a separate Fastly staging action. +- The generic actions expose no Fastly version. Capture the active version before + deploy for production rollback. +- The old action targets `IABTechLab/trusted-server`; verify the actual + deployment refs before switching. diff --git a/docs/specs/edgezero-deploy-github-action.md b/docs/specs/edgezero-deploy-github-action.md index cf2e2440..fa5ee060 100644 --- a/docs/specs/edgezero-deploy-github-action.md +++ b/docs/specs/edgezero-deploy-github-action.md @@ -1,319 +1,344 @@ -# EdgeZero Deploy GitHub Action — Fastly v0 Spec +# EdgeZero Deploy GitHub Actions — Layered, Adapter-Independent Spec -**Status:** Implemented Fastly v0 contract +**Status:** Revised design (supersedes the Fastly-only v0 spec) -**Date:** 2026-07-02 +**Date:** 2026-07-08 **Delivery target:** implementation in the `stackpop/edgezero` monorepo -**Action path:** `.github/actions/deploy` +**Action paths:** -**Pre-release identity:** - -```yaml -uses: stackpop/edgezero/.github/actions/deploy@ +```text +.github/actions/build-cli +.github/actions/deploy-core +.github/actions/deploy-fastly ``` ## 1. Executive summary -Add a reusable GitHub composite action to the EdgeZero monorepo that deploys a -checked-out EdgeZero application to Fastly Compute. - -The first version intentionally supports only one deployable adapter: - -| Adapter | v0 status | Target | -| ------------ | ------------------------ | -------------------------------------- | -| `fastly` | Supported | `wasm32-wasip1` | -| `cloudflare` | Future | Not implemented in v0 | -| `spin` | Future preview candidate | Not implemented in v0 | -| `axum` | Excluded | No EdgeZero remote deployment contract | - -The action does not check out application source. The caller owns checkout, +Ship reusable GitHub composite actions that deploy a checked-out EdgeZero +application to a supported provider by driving the EdgeZero CLI. + +The design is **layered** so a new adapter is added without rewriting the deploy +engine, and the **CLI is the boundary** — the actions never reproduce provider +build, deploy, config-push, or provision logic in YAML or shell. Everything an +adapter needs (`edgezero build`, `edgezero deploy`, `edgezero config push`, +`edgezero provision`) already lives in the CLI; the actions only compile the +CLI, scope credentials, and invoke the right subcommand. + +The CLI is **the app's own CLI package.** The application tells `build-cli` which +CLI package to compile (a crate in the application's own workspace), and +`build-cli` builds it from the application checkout. It is **not** the EdgeZero +monorepo CLI and **not** the action's own repository revision. Because the CLI is +the app's own package, built from the app's source and lockfile, the CLI and the +application always agree on the manifest, adapter, and config schema — and an app +may ship a CLI extended with its own commands. + +Three layers: + +| Layer | Action | Responsibility | +| --------------- | ------------------- | ------------------------------------------------------------------------------------- | +| Build | `build-cli` | Compile the caller-selected EdgeZero CLI **once** and publish it. | +| Engine (shared) | `deploy-core` | Adapter-independent engine **scripts** sourced by wrappers; consume the prebuilt CLI. | +| Adapter wrapper | `deploy-fastly` (…) | Minimal per-adapter shim: type provider credentials, call the engine. | + +The actions do not check out application source. The caller owns checkout, repository permissions, ref selection, GitHub Environment policy, concurrency, timeouts, and any health check or rollback process. -The action owns repeatable deployment setup: - -1. validate public inputs; -2. resolve the checked-out application directory and optional `edgezero.toml`; -3. resolve Rust from the application or this repository's `.tool-versions`; -4. install `wasm32-wasip1`; -5. install an action-owned EdgeZero CLI from the EdgeZero repository revision - selected by the `uses:` ref; -6. install the pinned Fastly CLI version defined by this repository's tool - version policy; -7. optionally restore/save an exact-key application build cache; -8. apply the Fastly build-mode policy; -9. run EdgeZero build when required; and -10. run EdgeZero deploy with typed Fastly credentials scoped to provider - mutation steps. - The core boundary is EdgeZero itself: ```text -edgezero build --adapter fastly -edgezero deploy --adapter fastly + build --adapter + deploy --adapter ``` +where `` is the application's own CLI binary built by `build-cli`. + Provider-specific staging, deployment IDs, health checks, and rollback are out -of scope for this generic deploy action. +of scope for these deploy actions. ## 2. Design principles -1. **EdgeZero is the deployment boundary.** The action invokes the EdgeZero CLI - instead of reproducing provider build/deploy logic in YAML or shell. -2. **The caller owns source.** The action never calls `actions/checkout`. -3. **Fastly-only v0, future-compatible API.** The `adapter` input is required - even though only `fastly` is accepted in v0. -4. **Pre-release action ref.** Consumers may select the action with a branch, - tag, or commit SHA. A full commit SHA is recommended when reproducible - production deploys matter. No `v1` alias, Marketplace publication, or - release tag is defined by this spec. -5. **Use repository tool versions.** The action's EdgeZero CLI build toolchain - and default application Rust toolchain come from the EdgeZero monorepo's - `.tool-versions` file. -6. **Typed provider credentials.** Fastly credentials are passed through action - inputs, not caller `env:`, so setup and separate build steps do not inherit - the API token. Deploy is the only provider mutation step that receives the token. -7. **No shell string APIs.** Passthrough arguments are JSON arrays and are - invoked without `eval`. -8. **Safe by default.** Caching is opt-in, deploys require committed source, - and provider credentials are not written to outputs, summaries, caches, or - action-global environment files. +1. **EdgeZero CLI is the deployment boundary.** The actions invoke CLI + subcommands instead of reproducing provider logic. Provider orchestration + belongs in the CLI, not in action shell scripts. +2. **Layered and adapter-independent.** A generic `deploy-core` engine holds all + provider-neutral behavior. Adapter wrappers are minimal and only supply typed + credentials and the adapter name. Adding an adapter adds a wrapper; it does + not fork the engine. +3. **The caller owns source.** The actions never call `actions/checkout`. +4. **The application provides the CLI package.** The app tells `build-cli` which + CLI package to compile via a required `cli-package` input, and `build-cli` + builds that package from the application's own checkout. It never builds the + EdgeZero monorepo CLI or the action's own repository revision. The + application owns which CLI deploys it. +5. **Compile once, reuse everywhere.** `build-cli` compiles the CLI a single + time per workflow and publishes it as an artifact. Deploy actions consume the + prebuilt binary and never recompile it. +6. **Typed provider credentials.** Credentials are passed through wrapper action + inputs, not caller `env:`, so setup and build steps never inherit provider + tokens. Only the provider mutation step receives them. +7. **No shell string APIs.** Passthrough arguments are JSON arrays invoked + through Bash arrays without `eval`. +8. **No Python in tooling or CI.** Validation, metadata checks, and security + scans run through Bash, `jq`, and pinned release binaries (`actionlint`, + `zizmor`). No `python3` heredocs and no `pip install`. +9. **Pin third-party actions to readable released tags.** Reusable third-party + actions are referenced by their published major/minor tag (for example + `actions/checkout@v4`), not opaque commit SHAs chosen ad hoc. +10. **Safe by default.** Caching is opt-in, deploys require committed source, and + provider credentials never reach outputs, summaries, caches, or + action-global environment files. ## 3. Goals -1. Deploy any checked-out EdgeZero Fastly application from GitHub Actions. -2. Support same-repository, separate-repository, private-repository, and +1. Deploy any checked-out EdgeZero application from GitHub Actions through the + EdgeZero CLI. +2. Keep the deploy engine adapter-independent so Cloudflare, Spin, and future + adapters reuse it. +3. Compile the CLI package the application provides, from the application's own + source, so the CLI and the deployed application never disagree on schema. +4. Compile the CLI once and share the artifact across deploy steps and jobs. +5. Support same-repository, separate-repository, private-repository, and monorepo checkout layouts. -3. Require explicit `adapter: fastly` for v0. -4. Respect the application's `edgezero.toml` when present. -5. Support explicit `working-directory` and explicit `manifest` selection. -6. Build and install the EdgeZero CLI from the action commit selected by - `uses:`. -7. Install the Fastly CLI reproducibly from a pinned version. -8. Accept typed Fastly credentials and expose them only to provider mutation steps. -9. Support JSON-array build and deploy passthrough arguments. -10. Support opt-in exact-key application `target/` caching. -11. Produce actionable validation failures before deployment begins. -12. Avoid logging provider credentials or action-managed secret values. +6. Respect the application's `edgezero.toml` when present and support explicit + `working-directory` and `manifest` selection. +7. Accept typed provider credentials and expose them only to the provider + mutation step. +8. Support JSON-array build and deploy passthrough arguments. +9. Support opt-in, exact-key application `target/` caching. +10. Produce actionable validation failures before deployment begins. +11. Keep all tooling and CI free of Python; use pinned release binaries. ## 4. Non-goals -The v0 action will not: +The deploy actions will not: 1. check out application source; 2. choose an application ref; -3. deploy Cloudflare Workers, Fermyon Spin, or Axum applications; -4. deploy more than one adapter per invocation; -5. provision Fastly services, domains, dictionaries, config stores, secrets, or - other provider resources; -6. push runtime config or secrets to Fastly; -7. implement Fastly staging; -8. parse or expose a Fastly service version; -9. perform health checks; -10. perform automatic rollback; -11. configure GitHub job permissions, environments, approvals, concurrency, or - timeouts; -12. support Windows or macOS runners; -13. publish a stable version alias; or -14. provide a general `setup` action for running arbitrary EdgeZero commands. - -## 5. Public action contract - -### 5.1 Invocation path - -Use a trusted action ref. A full commit SHA is recommended for reproducible -production deploys, but branches and tags are accepted: - -```yaml -- uses: stackpop/edgezero/.github/actions/deploy@ - with: - adapter: fastly - fastly-api-token: ${{ secrets.FASTLY_API_TOKEN }} - fastly-service-id: ${{ vars.FASTLY_SERVICE_ID }} -``` - -The action lives at: - -```text -.github/actions/deploy/action.yml -``` - -The root of the EdgeZero repository remains the Rust workspace root and does -not become a GitHub Action entry point. - -### 5.2 Inputs - -| Input | Required | Default | Contract | -| ------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `adapter` | Yes | none | Must be exactly `fastly` in v0. Unknown adapters, `cloudflare`, `spin`, and `axum` fail before setup. | -| `working-directory` | No | `.` | Application directory relative to `github.workspace`. Must resolve inside `github.workspace`. | -| `manifest` | No | empty | Optional `edgezero.toml` path relative to `working-directory`. If set, the file must exist and is exported as `EDGEZERO_MANIFEST`. | -| `rust-toolchain` | No | `auto` | Explicit application Rust toolchain or automatic discovery. | -| `build-mode` | No | `auto` | One of `auto`, `always`, or `never`. For Fastly, `auto` resolves to `never`. | -| `build-args` | No | `[]` | JSON array of strings passed after `edgezero build --adapter fastly --`. Must not contain secrets. | -| `deploy-args` | No | `[]` | JSON array of caller-supplied Fastly comment arguments appended after the action-owned deploy flags. Must not contain secrets. Other Fastly deploy args are rejected in v0. | -| `cache` | No | `false` | Enable exact-key application `target/` caching. Accepts only `true` or `false`. | -| `fastly-api-token` | Yes | none | Fastly API token. Injected only into the EdgeZero deploy step as `FASTLY_API_TOKEN`. | -| `fastly-service-id` | Yes | none | Fastly service ID used by the action-owned deploy flag to prevent accidental service creation. | - -Boolean inputs accept only the literal strings `true` and `false`. - -`build-args` and `deploy-args` must parse as JSON arrays containing only string -values. Strings containing NUL bytes are rejected because operating-system -arguments cannot represent them. - -### 5.3 Outputs - -| Output | Meaning | -| ---------------------- | --------------------------------------------------------- | -| `adapter` | Normalized adapter, always `fastly` in v0. | -| `source-revision` | Git commit deployed from `working-directory`. | -| `edgezero-revision` | EdgeZero action/CLI revision selected by the `uses:` ref. | -| `provider-cli-version` | Installed Fastly CLI version. | -| `effective-build-mode` | Resolved build behavior, `always` or `never`. | - -The action intentionally does not expose a Fastly service version or deployment -ID. Provider-specific deployment metadata requires a separate design. - -## 6. Checkout and source contract - -The caller must check out application source before invoking the action. - -### 6.1 Same-repository application - -```yaml -jobs: - deploy: - runs-on: ubuntu-24.04 - permissions: - contents: read - steps: - - uses: actions/checkout@ - with: - persist-credentials: false - - - uses: stackpop/edgezero/.github/actions/deploy@ - with: - adapter: fastly - fastly-api-token: ${{ secrets.FASTLY_API_TOKEN }} - fastly-service-id: ${{ vars.FASTLY_SERVICE_ID }} -``` - -### 6.2 Separate orchestration and application repositories - -```yaml -jobs: - deploy: - runs-on: ubuntu-24.04 - permissions: - contents: read - steps: - - name: Checkout deployment repository - uses: actions/checkout@ - with: - path: deployer - persist-credentials: false - - - name: Checkout application - uses: actions/checkout@ - with: - repository: stackpop/my-edgezero-app - ref: ${{ inputs.ref }} - path: app - persist-credentials: false - - - name: Deploy application - uses: stackpop/edgezero/.github/actions/deploy@ - with: - adapter: fastly - working-directory: app - fastly-api-token: ${{ secrets.FASTLY_API_TOKEN }} - fastly-service-id: ${{ vars.FASTLY_SERVICE_ID }} -``` - -### 6.3 Monorepo application - -```yaml -- uses: stackpop/edgezero/.github/actions/deploy@ - with: - adapter: fastly - working-directory: apps/api - manifest: edgezero.toml - cache: true - fastly-api-token: ${{ secrets.FASTLY_API_TOKEN }} - fastly-service-id: ${{ vars.FASTLY_SERVICE_ID }} -``` - -## 7. Execution flow - -The action executes these steps in order: - -1. Verify the runner is Linux x86-64. GitHub-hosted `ubuntu-24.04` is the - tested environment. -2. Validate `adapter`; only `fastly` is accepted. +3. deploy more than one adapter per `deploy-*` invocation; +4. provision provider resources or push runtime config as a side effect of + deploy (these remain explicit CLI subcommands the caller may run separately); +5. implement provider staging; +6. parse or expose a provider deployment/service version; +7. perform health checks; +8. perform automatic rollback; +9. configure GitHub job permissions, environments, approvals, concurrency, or + timeouts; +10. support Windows or macOS runners; +11. publish a stable version alias; or +12. provide a general `setup` action for running arbitrary EdgeZero commands + (the CLI is available via the `build-cli` artifact for callers who need it). + +## 5. Architecture + +### 5.1 Layer 1 — `build-cli` + +Compiles the **CLI package the application provides** — a crate in the +application's own workspace, named by the required `cli-package` input — once, +and publishes it as a workflow artifact so every downstream deploy step consumes +the same binary. The CLI is built from the application checkout and its lockfile, +so it matches the application and may include app-specific commands. `build-cli` +never builds the EdgeZero monorepo CLI. + +**Inputs** + +| Input | Required | Default | Contract | +| ------------------- | -------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `cli-package` | Yes | none | Cargo package name of the CLI to build, defined in the application's own workspace. `build-cli` builds this package from the application checkout. | +| `cli-bin` | No | `` | Binary name produced by `cli-package`. Defaults to the package name (the generated downstream CLI names its bin after the package). | +| `working-directory` | No | `.` | Application directory (relative to `github.workspace`) containing the workspace/lockfile that defines `cli-package`. Must resolve inside `github.workspace`. | +| `rust-toolchain` | No | `auto` | Explicit toolchain, or `auto` to follow the application toolchain resolution precedence (§7). | +| `artifact-name` | No | `edgezero-cli` | Name of the uploaded artifact. | + +There is intentionally no `adapters` / features input. The application's own +`Cargo.toml` already pins which adapters compile into its CLI (through the +`edgezero-cli` dependency it declares); `build-cli` builds the package exactly as +the application declares it, so the app owns adapter selection. + +**Outputs** + +| Output | Meaning | +| --------------- | -------------------------------------------------------------- | +| `cli-version` | CLI package version, read from `cargo metadata` at build time. | +| `cli-package` | The application CLI package that was built. | +| `cli-bin` | The binary name inside the artifact. | +| `artifact-name` | Name of the uploaded CLI artifact for downstream `download`. | + +**Behavior** + +1. Require a `Cargo.lock` at the app's Cargo workspace root (see §11.1); fail + with a remediation message if it is missing. All Cargo commands run with + `--locked` so the build never creates or updates the lockfile. +2. Confirm via `cargo metadata --locked` that `cli-package` exists in the + application workspace under `working-directory` and that it declares a binary + target named `` (default ``). Fail if either is absent. +3. Install the resolved host Rust toolchain (§7). The CLI is a native host tool; + the WASM target needed to build the _application_ is installed later by the + deploy engine, not here. +4. Build **into an action-owned `CARGO_TARGET_DIR` below `RUNNER_TEMP`**, never + the app checkout, so the CLI build does not write `target/` into the + application working tree (which the deploy engine later dirty-checks): + + ```text + CARGO_TARGET_DIR=/edgezero-cli-build \ + cargo build --locked --release -p --bin + ``` + +5. Read `cli-version` from `cargo metadata` for `cli-package`, and smoke-check + the binary with ` --help` (today's CLI has no `--version`; see the + note below). +6. Write a small metadata file (`cli-meta.json`) next to the binary containing + `cli-bin`, `cli-version`, and `cli-package`. +7. Upload the binary **and `cli-meta.json`** as a single **tar archive** so the + executable bit survives the round trip (`actions/upload-artifact` zips and + drops POSIX permissions). + +The artifact is self-describing: the engine reads `cli-meta.json` to learn the +binary name and version, so callers do not have to re-pass `cli-bin`/`cli-version` +(a wrapper `cli-bin` input, if given, overrides the metadata). + +`build-cli` never receives provider credentials and leaves the app checkout +clean (no `target/`, no lockfile mutation), so a later dirty-source guard passes. + +> **Companion CLI improvement (tracked separately):** the generated downstream +> CLI template currently sets no clap `version`, so ` --version` fails. Add +> `#[command(version)]` to the downstream CLI template so future apps expose a +> version surface. Until then, `cli-version` comes from `cargo metadata` and the +> runnability check uses `--help`. + +### 5.2 Layer 2 — `deploy-core` (shared engine scripts) + +Adapter-independent. Holds every provider-neutral concern so wrappers stay +minimal. `deploy-core` is **not a standalone composite action**; it is a +directory of shared scripts under `.github/actions/deploy-core/`. Each adapter +wrapper is the real composite action and sources these scripts through +`$GITHUB_ACTION_PATH/../deploy-core/…`, which resolves inside the same fetched +EdgeZero action repository. This avoids referencing a "private" sibling action by +ref and keeps one engine for every adapter. + +The engine is parameterized by the values the wrapper passes to those scripts +(as environment variables), conceptually: + +| Parameter | Meaning | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `adapter` | Passed to ` deploy --adapter `. The engine does **not** enumerate compiled adapters; the CLI rejects an unknown adapter with its own error. | +| `cli-artifact` | Name of the `build-cli` artifact to download. The engine reads `cli-bin` and `cli-version` from the artifact's `cli-meta.json`. | +| `cli-bin` | Optional override for the binary name; if empty, taken from `cli-meta.json`. | +| `working-directory` | Application directory relative to `github.workspace`. Must resolve inside `github.workspace`. | +| `manifest` | Optional `edgezero.toml` path relative to `working-directory`. If set, must exist; exported as `EDGEZERO_MANIFEST`. | +| `rust-toolchain` | Application Rust toolchain for the deploy build. `auto` follows §7. | +| `target` | Application build target. `auto` derives it from `adapter` (for example `fastly` → `wasm32-wasip1`). | +| `build-mode` | One of `auto`, `always`, `never` (§8). | +| `build-args` | JSON array of strings passed after ` build --adapter --`. Must not contain secrets. | +| `deploy-args` | JSON array of caller-supplied deploy args appended after action-owned deploy flags. Must not contain secrets. | +| `deploy-arg-allow` | Adapter allowlist pattern for caller `deploy-args` (wrapper-provided; §9). | +| `provider-env` | JSON object of provider credential names → values, injected **only** into the deploy step (§10). | +| `provider-env-clear` | JSON array of env var names (wrapper-provided) the engine unsets in non-deploy steps and clears + re-exports from `provider-env` in the deploy step (§10). Keeps alias-clearing provider-agnostic. | +| `deploy-flags` | JSON array of action-owned deploy flags the wrapper injects before caller `deploy-args` (`--service-id …`, `--non-interactive`). | +| `cache` | Enable exact-key application `target/` caching (`true`/`false`). | + +The wrapper surfaces engine results as its own outputs: `adapter`, +`source-revision`, `cli-version`, `effective-build-mode`. + +The engine contains no provider-specific credential names, service concepts, +endpoints, or CLI flags — those, and the list of aliases to clear +(`provider-env-clear`), all arrive from the wrapper. It invokes the application's +CLI binary (``), not a hard-coded `edgezero`. + +### 5.3 Layer 3 — adapter wrappers (`deploy-fastly`, …) + +Minimal composite actions. A wrapper only: + +1. declares the provider's typed credential inputs; +2. maps them into `provider-env` and action-owned `deploy-flags`; +3. sets `adapter`, `target`, the adapter `deploy-arg` allowlist, and the + `provider-env-clear` alias list; and +4. sources the shared `deploy-core` scripts via `$GITHUB_ACTION_PATH/../deploy-core`. + +A wrapper contains no build logic, no toolchain resolution, no path +confinement — those are engine concerns. + +**`deploy-fastly` inputs** + +| Input | Required | Default | Contract | +| ------------------- | -------- | ------------- | ---------------------------------------------------------------------------------------------- | +| `cli-artifact` | Yes | none | `build-cli` artifact name. Forwarded to the engine. | +| `cli-bin` | No | from artifact | Binary name inside the artifact. Forwarded to the engine. | +| `fastly-api-token` | Yes | none | Mapped into `provider-env` as `FASTLY_API_TOKEN`, deploy step only. | +| `fastly-service-id` | Yes | none | Mapped into action-owned `deploy-flags` as `--service-id ` to prevent accidental creation. | +| `working-directory` | No | `.` | Forwarded to the engine. | +| `manifest` | No | empty | Forwarded to the engine. | +| `build-mode` | No | `auto` | Forwarded. Fastly `auto` resolves to `never`. | +| `build-args` | No | `[]` | Forwarded to the engine. | +| `deploy-args` | No | `[]` | Forwarded. Allowlisted to `--comment` for Fastly (§9). | +| `cache` | No | `false` | Forwarded to the engine. | + +The wrapper sets `adapter: fastly`, `target: wasm32-wasip1`, the action-owned +`deploy-flags` (`--service-id …`, `--non-interactive`) so deployments cannot +prompt in CI or select an unintended service, and +`provider-env-clear: ["FASTLY_API_TOKEN", "FASTLY_SERVICE_ID", "FASTLY_ENDPOINT", +"FASTLY_CARGO_PROFILE", …]` so the engine clears Fastly auth/endpoint aliases +without the engine itself knowing Fastly's names. + +## 6. Execution flow (engine) + +1. Verify the runner is Linux x86-64 (`ubuntu-24.04` is the tested environment). +2. Validate that `adapter` is a well-formed, non-empty token. The engine does + **not** enumerate the CLI's compiled adapters (there is no introspection + command); an unsupported adapter surfaces as the CLI's own error at build or + deploy time. 3. Validate exact boolean inputs. -4. Parse `build-args` and `deploy-args` as JSON string arrays. -5. Reject NUL-containing argument values. -6. Reject Fastly deploy passthrough flags that override typed authentication, - service selection, endpoint selection, or debug/authentication behavior. -7. Resolve `working-directory` beneath `github.workspace` using canonical paths - and symlink resolution. -8. Fail if the working directory does not exist or is not a directory. -9. If `manifest` is non-empty: - -- resolve it relative to `working-directory`; -- fail if it resolves outside `github.workspace`; -- fail if it does not exist or is not a regular file; and -- export it as `EDGEZERO_MANIFEST` for EdgeZero CLI invocations. - -10. Resolve the application Git root from `working-directory`. -11. Record `source-revision` and fail if the application working tree has - uncommitted or staged changes. -12. Resolve the application Rust toolchain. -13. Install the Rust toolchain and `wasm32-wasip1` target. -14. Build/install the action-owned EdgeZero CLI from the action repository root - at the selected action commit. -15. Install the pinned Fastly CLI. -16. If `cache: true`, restore the exact-key application `target/` cache. -17. Print non-sensitive diagnostics. -18. Resolve `build-mode`: - - `always` runs a separate EdgeZero build; - - `never` skips the separate build; and - - `auto` resolves to `never` for Fastly. -19. If effective build mode is `always`, run: - - ```text - edgezero build --adapter fastly -- - ``` - - The build step receives no Fastly credential values from typed inputs. - -20. In a separate deploy step, set only the typed Fastly API token and run - EdgeZero deploy with action-owned Fastly CI flags followed by caller - `deploy-args`: +4. Download the `cli-artifact` (a tar) into an action-owned directory below + `RUNNER_TEMP`, extract it preserving permissions (or `chmod +x `), + read `cli-meta.json` for `cli-bin`/`cli-version` (a wrapper `cli-bin` input + overrides), and prepend the directory to `PATH` for action steps only. +5. Parse `build-args`, `deploy-args`, `deploy-flags`, `provider-env-clear` as + JSON string arrays and `provider-env` as a JSON string→string object. +6. In every non-deploy step (setup, build), unset each name in + `provider-env-clear` so no provider credential leaks outside the deploy step. +7. Reject NUL-containing argument or value entries. +8. Apply the adapter's deploy-arg allowlist (wrapper-provided) to `deploy-args`. +9. Resolve `working-directory` beneath `github.workspace` using canonical paths + and symlink resolution; fail if missing or not a directory. +10. If `manifest` is non-empty: resolve it under `working-directory`, fail if it + escapes `github.workspace` or is not a regular file, and export + `EDGEZERO_MANIFEST`. +11. Resolve the application **Git root**; record `source-revision`; fail on a + dirty working tree (`build-cli` used an isolated `CARGO_TARGET_DIR`, so its + CLI build did not dirty this tree). +12. Resolve the **Cargo workspace root** for `working-directory` (§11.1) for all + Cargo-scoped operations that follow. +13. Resolve the application Rust toolchain (§7) and install it plus the resolved + application `target` (for example `wasm32-wasip1` for Fastly). +14. If `cache: true`, restore the exact-key **Cargo workspace root** `target/` + cache. +15. Print non-sensitive diagnostics. +16. Resolve `build-mode` (§8). If `always`, run + ` build --adapter -- ` with **no** provider + credentials in scope (the `provider-env-clear` names stay unset here). +17. In a separate deploy step, clear the `provider-env-clear` aliases, then + export only the `provider-env` values, and run: ```text - edgezero deploy --adapter fastly -- --service-id --non-interactive + deploy --adapter -- ``` - The action owns `--service-id` and `--non-interactive` so deployments cannot - prompt in CI or silently create/select an unintended service. + For adapters whose deploy also compiles the application (Fastly's default), + this step builds application code with credentials in scope — see §10.1. -21. Clean action-owned temporary tool, auth, log, and cache state with - `if: always()` where GitHub permits cleanup to run. -22. Save the application cache when enabled and safe to save. -23. Set outputs and write a non-sensitive GitHub step summary with +18. Clean action-owned temporary tool, auth, log, and cache state with + `if: always()`. +19. Save the application cache when enabled and safe. +20. Set outputs and write a non-sensitive GitHub step summary with `if: always()`. When an argument array is empty, the trailing `--` may be omitted. -## 8. Toolchain and tool installation - -### 8.1 Rust toolchain resolution +## 7. Toolchain resolution -Application Rust toolchain resolution uses this precedence: +Application Rust toolchain resolution precedence: 1. explicit `rust-toolchain` input when not `auto`; 2. nearest `rust-toolchain.toml` or `rust-toolchain`, walking from @@ -323,455 +348,318 @@ Application Rust toolchain resolution uses this precedence: At each directory, Rustup-native files take precedence over `.tool-versions`. Malformed toolchain files fail instead of silently selecting a different -compiler. - -The action-owned EdgeZero CLI build also uses the EdgeZero repository root -`.tool-versions` `rust` entry. - -### 8.2 EdgeZero CLI installation - -The action installs the EdgeZero CLI from the same repository revision selected -by the `uses:` ref. It does not install the CLI from the caller application's -Cargo dependencies, branch, tag, or path. - -For v0, the installed executable is the current repository binary name, -`edgezero`. The crate/package remains `edgezero-cli`, but the action invokes the -installed binary. If the project later renames the installed binary, the action -implementation and documentation must change together. The public action -contract should not require callers to know the internal executable path. - -The CLI is installed into an action-owned directory below `RUNNER_TEMP` and -prepended to `PATH` for action steps only. - -### 8.3 Fastly CLI installation - -The Fastly CLI version is pinned by the EdgeZero repository tool-version policy. -The initial implementation should follow the repository `.tool-versions` Fastly -CLI version and record the exact installer artifact and SHA-256 checksum in the -action implementation metadata. - -The installer must: - -- use an official Fastly distribution; -- verify the downloaded artifact checksum; -- place the binary in an action-owned directory below `RUNNER_TEMP`; -- print `fastly --version`; and -- avoid printing authentication state. - -## 9. Build behavior - -The public `build-mode` input controls whether the action runs a separate -EdgeZero build before deploy. - -| Value | Behavior | -| -------- | ------------------------------------------------------------------------------------- | -| `auto` | Apply the v0 Fastly policy. | -| `always` | Run `edgezero build --adapter fastly` before deploy. | -| `never` | Skip the separate build and rely on deploy to build or consume the required artifact. | - -The v0 Fastly `auto` policy is: - -| Adapter | Effective mode | Reason | -| -------- | -------------- | --------------------------------------------------------------------- | -| `fastly` | `never` | Fastly `compute deploy` builds unless a prebuilt package is provided. | - -`always` is useful for a separate validation build, but Fastly deploy may still -compile again. `never` is the default Fastly behavior and assumes the deploy -command is self-contained. +compiler. `build-cli` uses the same precedence for the CLI build. -## 10. Manifest behavior +## 8. Build behavior -When `manifest` is empty, the action leaves `EDGEZERO_MANIFEST` unset and runs -from `working-directory`. EdgeZero then applies its normal behavior: +| Value | Behavior | +| -------- | ---------------------------------------------------------------- | +| `auto` | Apply the adapter's default policy. | +| `always` | Run ` build --adapter ` before deploy. | +| `never` | Skip the separate build; deploy builds or consumes the artifact. | -- load `edgezero.toml` from the current working directory when present; or -- use the built-in Fastly adapter fallback when no default manifest exists. +Fastly `auto` resolves to `never` because ` deploy --adapter fastly` +builds unless a prebuilt package is provided. Other adapters define their own +default policy in their wrapper. -When `manifest` is provided, the action sets `EDGEZERO_MANIFEST` to the -canonical absolute path of that file. Missing explicit manifests are hard -errors. +## 9. Passthrough arguments -Provider manifest discovery remains EdgeZero's responsibility. The action must -not guess between multiple `fastly.toml` files. Monorepos with multiple Fastly -manifests should select a deterministic `working-directory` or define explicit -Fastly commands in `edgezero.toml`. - -Because v0 injects action-owned Fastly deploy flags after `--`, a manifest-defined -Fastly deploy command used with this action must forward or accept Fastly -Compute deploy/publish flags such as `--service-id`, `--non-interactive`, and -caller-supplied safe flags like `--comment`. Wrapper scripts are allowed, but -they must preserve this contract. - -## 11. Fastly credential contract - -Fastly authentication uses typed action inputs only: +`build-args`, `deploy-args`, and `deploy-flags` are JSON arrays so argument +boundaries are explicit: ```yaml with: - fastly-api-token: ${{ secrets.FASTLY_API_TOKEN }} - fastly-service-id: ${{ vars.FASTLY_SERVICE_ID }} + build-args: '["--verbose"]' + deploy-args: '["--comment", "deployed by GitHub Actions"]' ``` -The deploy step maps these inputs as follows: - -| Input | Deploy-step use | -| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `fastly-api-token` | Exported only for deploy as `FASTLY_API_TOKEN`. | -| `fastly-service-id` | Passed as the action-owned `--service-id` deploy flag and may also be exported as `FASTLY_SERVICE_ID` for Fastly CLI fallback compatibility. | +`deploy-core` must: -Setup and separate build steps must clear Fastly authentication aliases from -their environments, including values accidentally provided through caller -`env:`. - -The deploy step must clear known Fastly authentication and endpoint aliases -before exporting only the typed values needed for v0. This prevents caller `env:` -from silently overriding the typed credential contract. - -Application configuration may still be passed through normal workflow `env:`: - -```yaml -- uses: stackpop/edgezero/.github/actions/deploy@ - with: - adapter: fastly - fastly-api-token: ${{ secrets.FASTLY_API_TOKEN }} - fastly-service-id: ${{ vars.FASTLY_SERVICE_ID }} - env: - MY_APP_SETTING: ${{ vars.MY_APP_SETTING }} -``` +- parse arrays with `jq` (no Python); +- reject non-arrays, non-string entries, and NUL bytes; +- construct commands as Bash arrays; +- never use `eval`; and +- avoid printing raw JSON inputs during validation. -Callers should not duplicate provider authentication credentials in `env:`. -Runtime secrets should prefer provider-managed runtime secret stores rather -than deploy-time environment variables. +Each adapter wrapper supplies an allowlist for caller `deploy-args`. For Fastly, +`deploy-args` are allowlisted to `--comment VALUE` / `--comment=VALUE`; all other +deploy args are rejected so caller input cannot override typed service selection, +authentication, non-interactive mode, endpoint, profile, or debug behavior. The +allowlist ships with accept/reject tests. -## 12. Passthrough arguments +## 10. Provider credential contract -`build-args` and `deploy-args` are JSON arrays so argument boundaries are -explicit: +Credentials flow through the `provider-env` JSON object, which `deploy-core` +injects **only** into the deploy step: -```yaml -with: - build-args: '["--features", "fastly"]' - deploy-args: '["--comment", "deployed by GitHub Actions"]' +```text +wrapper inputs (typed) → provider-env {NAME: value} → deploy step env → CLI ``` -The action must: - -- parse arrays with `jq` or equivalent safe JSON parsing; -- reject non-arrays; -- reject non-string entries; -- reject strings containing NUL bytes; -- construct commands as Bash arrays; -- never use `eval`; and -- avoid printing the raw JSON input arrays during validation. - -Arguments must not contain secrets. EdgeZero, provider CLIs, or manifest-defined -wrapper commands may print command arguments as part of normal diagnostics; -GitHub secret masking is a final defense, not the primary security boundary. - -For v0, `deploy-args` are intentionally allowlisted to Fastly deploy comments: -`--comment VALUE` or `--comment=VALUE`. All other caller-supplied deploy args -are rejected so future Fastly flags cannot bypass the typed credential/service -contract, non-interactive mode, or endpoint/debug behavior. The -implementation must maintain accept/reject tests for allowed comments and -blocked service ID, service name, API token, endpoint, profile, interactive, -short-flag, and debug-mode overrides. - -## 13. Caching - -The `cache` input enables opt-in application build caching. - -Caching defaults to `false` because deployment builds run trusted application -code and build output may contain sensitive generated data. - -When enabled, the action caches only the canonical application Git root -`target/` directory. It must not cache: - -- provider authentication files; -- action-owned tool installations; -- Fastly logs; -- temporary deploy state; -- arbitrary workspace paths; or -- files outside the application Git root. - -The cache key must be exact and include at least: - -- runner OS; -- runner architecture; -- resolved Rust toolchain; -- Rust target, `wasm32-wasip1`; -- EdgeZero action/CLI revision; -- application source revision; and -- application `Cargo.lock` hash. - -The action must not use broad restore prefixes in v0. If `cache: true` and the -application lockfile cannot be found, the action fails before deployment with a -remediation message. - -Callers may enable caching only for trusted immutable refs and applications -whose builds do not write secret-derived data into `target/`. - -## 14. Logging and summary - -The action should log and summarize non-sensitive facts: - -- selected adapter; -- resolved application directory relative to `github.workspace`; -- source revision; -- explicit manifest path or default discovery; -- Rust toolchain and target; -- EdgeZero action/CLI revision; -- Fastly CLI version; -- requested and effective build mode; -- cache enabled/disabled and cache key fingerprint, not full secrets or args; -- final result. - -The action must not log: - -- `fastly-api-token`; -- full process environments; -- application secret values; -- provider authentication state; or -- values written to provider auth files. - -## 15. Error handling - -All validation and setup failures must stop before invoking Fastly deployment. - -Expected failures and diagnostics: - -| Failure | Required diagnostic | -| --------------------------------------- | ------------------------------------------------------------------------------ | -| Missing `adapter` | State that `adapter` is required and v0 supports `fastly`. | -| Unsupported adapter | State that v0 supports only `fastly`. | -| `axum` selected | State that Axum has no EdgeZero remote deployment contract. | -| `cloudflare` or `spin` selected | State that the adapter is planned for future work but not implemented in v0. | -| Invalid boolean | Name the input and allowed values. | -| Missing working directory | Print the workspace-relative requested path. | -| Path escapes workspace | Name the input and state that paths must stay under `github.workspace`. | -| Missing explicit manifest | Print the workspace-relative requested path. | -| Invalid JSON arguments | Name the invalid input without printing its value. | -| Non-string argument entry | State that every array element must be a string. | -| Unsupported Fastly deploy arg | State the allowlist and rejected argument position without printing the array. | -| Rust toolchain cannot be resolved | List files checked and suggest explicit `rust-toolchain`. | -| Dirty working tree | State that deployments require committed source. | -| Missing `Cargo.lock` when cache enabled | Explain the exact-key cache requirement. | -| EdgeZero CLI installation fails | Print the action revision and Rust toolchain, not secrets. | -| Fastly CLI installation fails | Print the pinned Fastly version and installer source. | -| Missing Fastly credential input | Name the missing input, never its value. | -| Build command fails | Preserve exit status and state that deploy was not attempted. | -| Deploy command fails | Preserve exit status and state that rollback is caller-owned. | -| Cleanup fails | Mark the action failed and identify the cleanup area without printing secrets. | - -Provider CLI stderr may pass through so Fastly API errors remain actionable. -The action must not construct its own error messages containing credentials. - -## 16. Security requirements - -1. Recommend full commit SHAs for production examples that need reproducible - action and third-party action behavior. -2. Do not accept a separate caller-selected EdgeZero CLI source outside the - selected action repository revision. -3. Install the EdgeZero CLI from the selected action repository revision. -4. Use the EdgeZero repository `.tool-versions` Rust version for the action CLI - build and application fallback. -5. Download provider tools only from official release locations and verify - SHA-256 checksums. -6. Install action-owned binaries below `RUNNER_TEMP`. -7. Use Bash arrays; never use `eval`. -8. Allow-list `adapter` before using it in file selection or command arguments. -9. Treat the checked-out application and `edgezero.toml` as executable code. -10. Require trusted immutable source refs for deployment workflows. -11. Inject Fastly credentials only into the EdgeZero deploy step. -12. Do not write Fastly credentials to `GITHUB_ENV`, `GITHUB_OUTPUT`, caches, or summaries. -13. Clear provider auth aliases from non-provider steps. -14. Reject caller paths outside `github.workspace`, including symlink escapes. -15. Escape percent, carriage return, and newline characters before emitting - user-influenced GitHub annotations or masking commands. -16. Reject carriage returns and newlines in single-line output values. -17. Disable caching by default and use exact keys only when enabled. -18. Do not automatically retry Fastly deployment. Retries are limited to - idempotent downloads. -19. Do not use `github.token` for provider authentication. -20. Document least-privilege workflow permissions: `contents: read` unless the - caller has additional needs. -21. Document caller-owned environment protection, concurrency, and timeouts. -22. Allowlist Fastly passthrough deploy args to comments so caller input cannot - override typed service selection, authentication, non-interactive mode, - endpoint, profile, debug controls, or future Fastly flags. - -## 17. Testing strategy - -### 17.1 Static validation - -CI for the action must run: - -- `actionlint` over workflow files; +Rules: + +- Setup, `build-cli`, and separate build steps never receive `provider-env`. +- Alias clearing is **wrapper-driven and provider-agnostic**: the engine unsets + the names in the wrapper-supplied `provider-env-clear` list in non-deploy + steps, and in the deploy step clears those same names before exporting only + the typed `provider-env` values. The engine hard-codes no provider names, so + caller `env:` cannot override the typed contract. +- `provider-env` values never reach `GITHUB_ENV`, `GITHUB_OUTPUT`, caches, or + summaries. + +Application (non-credential) configuration may still pass through normal +workflow `env:`. + +### 10.1 Build-in-deploy caveat (trusted source requirement) + +Some adapters compile the application **inside** the deploy step. Fastly's +default `build-mode: never` relies on ` deploy`, which runs +`fastly compute deploy` — and that command builds the application unless a +prebuilt package already exists. Consequently, with the Fastly default, the +application is compiled while `FASTLY_API_TOKEN` is in scope. + +This is an explicit, accepted boundary, not an oversight: + +- The action still guarantees credentials are absent from setup, `build-cli`, + and any separate `build-mode: always` build step. +- Because deploy may still recompile, a credential-free `always` prebuild does + not remove the exposure; it only front-loads a validation build. +- Therefore callers **must** deploy only trusted, immutable source refs (full + SHAs or protected tags) and use GitHub Environment approvals, so untrusted + code never runs with the token in scope. + +Adapters that support a genuinely credential-free prebuild followed by a +credential-only publish may set a different default in their wrapper; Fastly does +not today. + +## 11. Caching + +`cache` enables opt-in application build caching, `false` by default. + +### 11.1 Git root vs Cargo workspace root + +Two distinct roots are resolved from `working-directory`, and they are **not** +interchangeable: + +- **Git root** — the enclosing repository. Used only for `source-revision` and + the dirty-source guard. +- **Cargo workspace root** — resolved with `cargo locate-project --workspace` + (or `cargo metadata`) from `working-directory`. Owns the real `Cargo.lock` and + the real `target/` directory. In a monorepo or nested workspace this is often + under `working-directory` (for example `apps/api/`), not the Git root. + +Cargo-scoped operations — lockfile hashing, lockfile presence checks, and +`target/` caching — use the **Cargo workspace root**. Git-scoped operations use +the **Git root**. + +### 11.2 Cache contents and key + +When enabled, cache only the resolved **Cargo workspace root** `target/`. Never +cache provider auth files, action-owned tool installs, logs, temporary deploy +state, or paths outside that `target/`. + +The cache key is exact and includes at least: runner OS, runner architecture, +resolved Rust toolchain, resolved `target`, CLI version, application source +revision, and the **Cargo workspace root** `Cargo.lock` hash. No broad restore +prefixes. If `cache: true` and that lockfile is missing, fail before deployment +with a remediation message. + +## 12. Logging and summary + +Log and summarize non-sensitive facts only: adapter, workspace-relative +application directory, source revision, manifest path or default discovery, Rust +toolchain and target, CLI version, requested/effective build mode, cache +enabled/disabled and key fingerprint, and final result. + +Never log provider credentials, full process environments, application secret +values, or provider auth state. + +## 13. Error handling + +All validation and setup failures stop before invoking provider deployment. + +| Failure | Required diagnostic | +| --------------------------------------- | ----------------------------------------------------------------------------- | +| Missing/unknown `cli-package` | State that the app must name a CLI package present in its own workspace. | +| Missing `cli-artifact` | State that a compiled CLI artifact from `build-cli` is required. | +| Malformed `adapter` token | Name the input and its allowed shape (the CLI validates support at run time). | +| Invalid boolean | Name the input and allowed values. | +| Missing working directory | Print the workspace-relative requested path. | +| Path escapes workspace | Name the input; require paths under `github.workspace`. | +| Missing explicit manifest | Print the workspace-relative requested path. | +| Invalid JSON arguments/env | Name the invalid input without printing its value. | +| Non-string entry | State that every array/object value must be a string. | +| Disallowed deploy arg | State the allowlist and rejected position without printing the array. | +| Rust toolchain cannot be resolved | List files checked and suggest explicit `rust-toolchain`. | +| Dirty working tree | State that deployments require committed source. | +| Missing `Cargo.lock` when cache enabled | Explain the exact-key cache requirement. | +| Missing provider credential input | Name the missing input, never its value. | +| Build command fails | Preserve exit status; state that deploy was not attempted. | +| Deploy command fails | Preserve exit status; state that rollback is caller-owned. | +| Cleanup fails | Mark the action failed; identify the area without printing secrets. | + +Provider CLI stderr passes through so provider API errors stay actionable. The +actions never construct error messages containing credentials. + +## 14. Security requirements + +1. Recommend readable released tags for third-party actions and, for production, + full commit SHAs of the EdgeZero action ref where reproducibility matters. +2. Compile the CLI package the application provides, from the application + checkout and its lockfile; do not build the EdgeZero monorepo CLI or the + action's own revision. +3. Compile the CLI once in `build-cli`; deploy steps consume the artifact and + never recompile. +4. Download provider tools and validation binaries only from official release + locations and verify SHA-256 checksums. +5. Install action-owned binaries below `RUNNER_TEMP`. +6. Use Bash arrays; never use `eval`; never use Python. +7. Allow-list `adapter` before using it in file selection or command arguments. +8. Treat the checked-out application and `edgezero.toml` as executable code. +9. Inject provider credentials only into the deploy step via `provider-env`. +10. Never write provider credentials to `GITHUB_ENV`, `GITHUB_OUTPUT`, caches, or + summaries. +11. Clear the wrapper-supplied `provider-env-clear` aliases from non-provider + steps; the engine hard-codes no provider names. +12. Reject caller paths outside `github.workspace`, including symlink escapes. +13. Escape percent, carriage return, and newline characters before emitting + user-influenced GitHub annotations or masking commands; reject CR/LF in + single-line output values. +14. Disable caching by default; use exact keys only when enabled. +15. Do not auto-retry provider deployment; retries are limited to idempotent + downloads. +16. Do not use `github.token` for provider authentication. +17. Document least-privilege workflow permissions (`contents: read` unless the + caller needs more) and caller-owned environment protection, concurrency, and + timeouts. + +## 15. Testing strategy + +### 15.1 Static validation (no Python) + +CI for the actions runs: + +- `actionlint` from a **pinned release binary** over workflow and action files; - `shellcheck` over shell scripts; -- YAML parsing for `.github/actions/deploy/action.yml`; -- metadata contract tests for public inputs and outputs; -- a check that no unsupported provider credential inputs exist in v0; -- a workflow security scanner such as `zizmor`; -- checksum verification for provider installer metadata; -- a check that action tool versions agree with `.tool-versions`; and +- YAML parsing for each `action.yml`; +- metadata contract tests for public inputs/outputs, ported into the Bash + `tests/run.sh` harness (replacing the previous `python3` heredocs); +- a check that action tool versions agree with `.tool-versions`; +- `zizmor` from a **pinned release binary** (Rust; installed as a release + artifact or via `cargo install zizmor --locked`, never `pip`); and - Markdown/example validation. -### 17.2 Script contract tests +Third-party actions used by CI (`actions/checkout`, `actions/cache`, artifact +upload/download) are pinned to readable released tags. + +### 15.2 Script contract tests (Bash) -Use temporary directories and fake binaries to test: +Use temporary directories and fake binaries to test, across the engine and the +Fastly wrapper: -- required `adapter` validation; -- `fastly` acceptance; -- `cloudflare`, `spin`, `axum`, and unknown adapter rejection; +- `adapter` well-formedness validation (unknown adapters surface as the CLI's + own error, not an engine allowlist); +- app-provided `cli-package` build (fail on missing/unknown package), tar + round-trip preserving the executable bit, and artifact consumption; - exact boolean parsing; -- toolchain precedence; -- malformed toolchain files; -- working-directory confinement; -- symlink escape rejection; -- dirty source rejection; -- source revision output; +- toolchain precedence and malformed-file failure; +- working-directory confinement and symlink-escape rejection; +- dirty-source rejection and source-revision output; - explicit and default manifest behavior; -- JSON argument parsing; -- argument boundary preservation; -- rejected non-string and NUL-containing arguments; -- unsupported Fastly deploy-arg rejection, including short override flags; -- build-mode resolution; -- build failure preventing deploy; +- JSON argument/env parsing and boundary preservation; +- rejected non-string and NUL-containing entries; +- adapter deploy-arg allowlist (accept `--comment`, reject service/auth/endpoint/ + profile/interactive/short-flag/debug overrides); +- build-mode resolution and build-failure-prevents-deploy; - deploy exit-code propagation; -- credential presence validation; -- credentials absent from setup and separate build processes; -- credentials present only in deploy; -- cache key construction; -- missing lockfile failure when cache is enabled; +- credential presence validation and scoping (absent from build-cli/setup/build, + present only in deploy); +- cache key construction and missing-lockfile failure; - cleanup on success and failure; and - redaction of credentials from action-owned logs. -These tests must not need live Fastly credentials. - -### 17.3 Composite-action smoke tests - -A GitHub Actions workflow should exercise the local composite action with a -minimal fixture EdgeZero Fastly app. - -The smoke test should: - -1. check out this repository; -2. create or use a fixture application; -3. install the real pinned Rust and Fastly tools where practical; -4. invoke `./.github/actions/deploy` locally; -5. use manifest build/deploy commands or fake Fastly binaries that write marker - files instead of contacting Fastly; -6. assert invocation order, working directory, argument boundaries, cache behavior, - and credential scope; and -7. verify public outputs. - -### 17.4 Installer tests - -Scheduled or manually triggered CI should verify that the pinned Fastly CLI -installer still produces a runnable binary matching the expected version. - -This test verifies installation only and must not deploy. - -### 17.5 Live deployment gate - -A protected manual workflow should eventually deploy a disposable Fastly fixture -before any stable version alias is created. - -The live gate must: - -- run only from protected release branches or explicitly approved manual - dispatch; -- use isolated Fastly resources; -- never run for pull requests from forks; -- verify the deployed endpoint or provider deployment record; -- clean up through provider-specific steps; and -- treat rollback/cleanup as caller-owned provider logic, not generic action - behavior. - -This live gate is not required to publish the initial pre-release action, but -it is required before advertising a stable version alias. - -## 18. Documentation requirements - -Before implementation is considered complete, user-facing docs must include: - -1. action location and trusted-ref guidance; -2. supported adapter table showing Fastly-only v0; -3. runner support; -4. same-repository checkout example; -5. separate-repository checkout example; -6. monorepo `working-directory` and `manifest` example; -7. complete input and output tables; -8. typed Fastly credential guidance; -9. explanation of why provider credentials should not be passed through - caller `env:`; -10. build-mode behavior; -11. cache behavior and security caveats; -12. trusted-ref requirement; -13. least-privilege permissions example; -14. protected environment, timeout, and concurrency recommendations; -15. explicit non-goals; and -16. future adapter notes for Cloudflare and Spin. - -## 19. Acceptance criteria - -The v0 design is implemented when all of the following are true: - -1. A caller can check out an EdgeZero Fastly application and invoke - `stackpop/edgezero/.github/actions/deploy@`. -2. The action requires `adapter: fastly`. -3. Unknown adapters, `cloudflare`, `spin`, and `axum` fail before tool - installation. -4. The action contains no hard-coded application repository, application path, - Fastly domain, deployment environment, or service ID. -5. The action invokes EdgeZero CLI for build and deploy operations. -6. The EdgeZero CLI is built from the selected action commit. -7. Rust versions come from application discovery or the EdgeZero repo - `.tool-versions` fallback. -8. Fastly selects `wasm32-wasip1` and installs the pinned Fastly CLI. -9. The caller can select a non-root working directory and explicit manifest. -10. Typed Fastly credentials reach only deploy. -11. Fastly credentials never appear in outputs, caches, action-owned logs, or summaries. -12. Passthrough argument boundaries are preserved. -13. `build-mode: auto` resolves to `never` for Fastly. -14. A failed required build prevents deployment. -15. A failed deployment returns a failing action status and does not trigger - rollback. -16. `cache: true` uses exact keys and caches only the application Git root - `target/` directory. -17. Static checks, script contract tests, composite smoke tests, and installer - tests pass. -18. README or docs examples include same-repository, separate-repository, - and monorepo checkout models. - -## 20. Risks and mitigations - -| Risk | Mitigation | -| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| EdgeZero CLI and application manifest schema are incompatible | Pin the action to a full EdgeZero commit SHA and publish compatibility notes before stable aliases. | -| Fastly deploy builds while credentials are in scope | Require trusted immutable refs; keep separate build credential-free; document caching caveats. | -| Mutable refs execute unexpected manifest commands | Caller owns checkout; document full SHA/tag protection and GitHub Environment approvals. | -| Caching stores sensitive generated output | Disable by default; exact keys only; cache only `target/`; document when not to enable. | -| Provider CLI installer changes or disappears | Pin versions and checksums; run scheduled installer tests. | -| Monorepo has multiple `fastly.toml` files | Require deterministic `working-directory` or explicit `edgezero.toml`; action does not guess. | -| Generic action grows provider-specific behavior | Keep staging, rollback, health checks, and deployment metadata out of v0. | - -## 21. Future work - -Future designs may add: - -1. Cloudflare Workers deployment; -2. Spin/Fermyon Cloud preview deployment; -3. provider-specific deployment metadata outputs; -4. Fastly staging as a separate provider-specific action; -5. generic or provider-specific health checks; -6. provider-specific rollback actions; -7. reusable setup action for multiple EdgeZero commands; -8. release artifact reuse between build and deploy jobs; -9. prebuilt and attested EdgeZero CLI binaries; -10. stable version aliases such as `v1`; and -11. Linux arm64, macOS, or other runner support. - -## 22. References +Tests must not need live provider credentials. + +### 15.3 Composite smoke test + +A workflow exercises the layered actions end to end with a minimal fixture +EdgeZero app: run `build-cli`, then `deploy-fastly`, using fake provider binaries +that write marker files instead of contacting Fastly; assert CLI-artifact reuse, +invocation order, working directory, argument boundaries, cache behavior, +credential scope, and public outputs. + +### 15.4 Installer / live gates + +- Scheduled CI verifies the pinned Fastly CLI installer still produces a runnable + binary matching the expected version, without deploying. +- A protected manual workflow may eventually deploy a disposable Fastly fixture + before any stable version alias is created; it runs only from protected + branches or approved dispatch, never from fork PRs, uses isolated resources, + and treats rollback/cleanup as caller-owned. + +## 16. Documentation requirements + +User-facing docs must cover: the three-layer model and when to use each action; +how `build-cli` compiles the app-provided CLI package; supported adapters and how new adapters +layer on; runner support; same-repo, separate-repo, and monorepo checkout +examples; complete input/output tables per action; typed provider credential +guidance and why credentials must not pass through caller `env:`; build-mode and +cache behavior with security caveats; least-privilege permissions and +environment/concurrency/timeout recommendations; explicit non-goals; and future +adapter notes. + +## 17. Acceptance criteria + +The design is implemented when: + +1. A caller can compile the CLI once with `build-cli` and deploy a checked-out + EdgeZero application with `deploy-fastly`, reusing the same CLI artifact. +2. `build-cli` compiles the app-provided `cli-package` from the application + checkout and never builds the EdgeZero monorepo CLI or the action's own + revision. +3. `deploy-core` contains no provider-specific credential names, service + concepts, endpoints, or CLI flags — only `provider-env`, `provider-env-clear`, + `deploy-flags`, and `deploy-args` carry them. +4. Adding a second adapter is a new minimal wrapper plus target/allowlist data, + with no engine fork. +5. Deploy steps consume the prebuilt CLI artifact and never recompile it. +6. Typed provider credentials reach only the deploy step and never appear in + outputs, caches, action-owned logs, or summaries. +7. Passthrough argument boundaries are preserved; no `eval`. +8. `cache: true` uses exact keys and caches only the application Git root + `target/`. +9. All CI, tooling, and tests run without Python; `actionlint` and `zizmor` run + from pinned release binaries. +10. Third-party actions are pinned to readable released tags. +11. Static checks, Bash contract tests, and the composite smoke test pass. +12. Docs include same-repo, separate-repo, and monorepo examples across the + three-layer model. + +## 18. Risks and mitigations + +| Risk | Mitigation | +| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| CLI and application manifest schema incompatible | CLI is the app's own package, built from the app checkout, so they cannot diverge. | +| Provider deploy builds while credentials are in scope | Keep the separate build credential-free; document caching caveats; require trusted immutable refs. | +| Mutable refs execute unexpected manifest commands | Caller owns checkout; document tag/SHA protection and GitHub Environment approvals. | +| Caching stores sensitive generated output | Disable by default; exact keys only; cache only `target/`. | +| Provider CLI installer changes or disappears | Pin versions and checksums; run scheduled installer tests. | +| Monorepo has multiple provider manifests | Require deterministic `working-directory` or explicit `edgezero.toml`; the actions do not guess. | +| Engine grows provider-specific behavior | Keep provider concepts in wrappers and the CLI; keep `deploy-core` provider-neutral. | + +## 19. Future work + +1. Cloudflare Workers deployment (`deploy-cloudflare` wrapper). +2. Spin/Fermyon Cloud preview deployment (`deploy-spin` wrapper). +3. Provider-specific deployment metadata outputs. +4. Provider-specific staging, health-check, and rollback actions. +5. Optionally consume a prebuilt/attested CLI binary matching the application's + pinned version instead of compiling from source. +6. Release artifact reuse between build and deploy jobs beyond the CLI. +7. Stable version aliases such as `v1`. +8. Linux arm64, macOS, or other runner support. + +## 20. References - EdgeZero CLI reference: `docs/guide/cli-reference.md` - EdgeZero Fastly adapter: `crates/edgezero-adapter-fastly/src/cli.rs` diff --git a/docs/specs/trusted-server-deployer-migration-plan.md b/docs/specs/trusted-server-deployer-migration-plan.md deleted file mode 100644 index d308f695..00000000 --- a/docs/specs/trusted-server-deployer-migration-plan.md +++ /dev/null @@ -1,70 +0,0 @@ -# Trusted Server Deployer Migration Plan - -**Status:** Draft migration plan - -**Repository reviewed:** the Trusted Server deployer repository - -## Current deployer behavior - -The deployer repository currently orchestrates Trusted Server Fastly deployments with: - -- `.github/workflows/deploy.yml` for manual deploys; -- `.github/workflows/daily-deploy.yml` for scheduled deploys; -- `stackpop/trusted-server-actions/fastly/deploy@v2`; -- `stackpop/trusted-server-actions/fastly/healthcheck@v2`; and -- `stackpop/trusted-server-actions/fastly/rollback@v2`. - -The old deploy action checks out Trusted Server internally, accepts `trusted-server-ref`, expands `trusted-server-config`, supports Fastly staging, and returns `fastly-version`. - -## Compatibility gap - -The EdgeZero generic deploy action intentionally does not support: - -- internal Trusted Server checkout; -- `trusted-server-ref`; -- `trusted-server-config` JSON expansion; -- automatic conversion of legacy JSON config to Fastly Config Store TOML; -- Fastly staging; -- Fastly service-version output; -- health checks; or -- rollback. - -Therefore the deployer cannot switch all existing behavior to the generic action in one step. - -## Recommended migration - -Start with a new production-only workflow and leave existing staging on the old action path until a Fastly-specific staging design exists. - -Production workflow shape: - -1. checkout `trusted-server-deployer` with `persist-credentials: false`; -2. checkout Trusted Server source separately at the selected ref into `trusted-server`; -3. write pre-deploy summary; -4. capture the currently active Fastly version before deploy; -5. invoke `stackpop/edgezero/.github/actions/deploy@` with: - - `adapter: fastly`; - - `working-directory: trusted-server`; - - `manifest: edgezero.toml` when present on all deployed refs; - - `cache: true` if trusted and safe; - - typed Fastly credentials; - - optional safe `deploy-args` such as `--comment`; -6. run deployer-owned production health check; -7. on health-check failure, reactivate the captured previous Fastly version; -8. write post-deploy summary from generic action outputs and rollback baseline. - -## Required deployer changes - -- Add explicit Trusted Server checkout; the generic action will not call `actions/checkout`. -- Pin action references to full SHAs for production examples. -- Use `ubuntu-24.04`, least-privilege `contents: read`, `timeout-minutes`, protected environments, and per-domain concurrency with `cancel-in-progress: false`. -- Update post-deploy summary to stop requiring `steps.deploy.outputs.fastly-version`. -- Move health check and rollback logic into deployer-local scripts/actions or keep old provider-specific actions pinned to full SHAs. -- Audit `TRUSTED_SERVER_CONFIG`; if still needed, keep config expansion and provider mutation in the deployer workflow or a Trusted Server-specific helper before invoking the generic deploy action. -- Confirm the canonical Trusted Server repository/ref has root `Cargo.lock`, `Cargo.toml`, `fastly.toml`, and preferably `edgezero.toml`. - -## Gotchas - -- The current `daily-deploy.yml` appears to stage but health-check/rollback production by default. Decide whether the scheduled workflow is production or staging and fix the mismatch before migration. -- Generic deploy has no Fastly staging contract. Keep staging legacy or design a separate Fastly staging action. -- Generic deploy has no Fastly version output. Capture the active Fastly version before deploy for production rollback. -- The reviewed local Trusted Server checkout has root `edgezero.toml` and `fastly.toml`, but the existing old action targets `IABTechLab/trusted-server`; verify the actual deployment refs before switching. From a11ceaacda7bff7cd9e9a1d0d70075ac6e0b593f Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Thu, 9 Jul 2026 00:14:57 -0700 Subject: [PATCH 2/2] Spec review round: scope provider-env to deploy step; fix cache-root wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - provider-env is now bound only to the deploy step's own env:, never a job/engine-global variable, so setup/build never hold the secret-bearing blob (unsetting inner names did not remove the outer JSON var). provider-env-clear is reframed as defense-in-depth against inherited caller env:. - Acceptance criteria and adoption guide now reference the Cargo workspace root (not the Git root) for target/ caching and Cargo.lock, matching §11. - Align stale 'caller-selected CLI' wording with the app-provided model. --- ...ezero-deploy-action-implementation-plan.md | 2 +- docs/specs/edgezero-deploy-adoption-guide.md | 7 +- docs/specs/edgezero-deploy-github-action.md | 68 +++++++++++-------- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/docs/specs/edgezero-deploy-action-implementation-plan.md b/docs/specs/edgezero-deploy-action-implementation-plan.md index 75f0db32..5dd118c6 100644 --- a/docs/specs/edgezero-deploy-action-implementation-plan.md +++ b/docs/specs/edgezero-deploy-action-implementation-plan.md @@ -14,7 +14,7 @@ Implement the layered deploy actions in the EdgeZero monorepo: .github/actions/deploy-fastly ``` -`build-cli` compiles the caller-selected EdgeZero CLI once and publishes it as an +`build-cli` compiles the app-provided CLI package once and publishes it as an artifact. `deploy-core` is the adapter-independent deploy engine that consumes the prebuilt CLI. `deploy-fastly` is a minimal wrapper that types Fastly credentials and calls the engine. Provider orchestration (build, deploy, diff --git a/docs/specs/edgezero-deploy-adoption-guide.md b/docs/specs/edgezero-deploy-adoption-guide.md index d64d57ef..55dc3307 100644 --- a/docs/specs/edgezero-deploy-adoption-guide.md +++ b/docs/specs/edgezero-deploy-adoption-guide.md @@ -139,8 +139,11 @@ steps: `build-cli` compiles that package from your checkout, so the CLI and your app never disagree on schema. `build-cli` does not use the EdgeZero monorepo CLI. - Provide typed provider credentials through wrapper inputs, not caller `env:`. -- Ensure the deployed ref has committed source (no dirty working tree) and, for - caching, a root `Cargo.lock`. +- Ensure the deployed ref has committed source (no dirty working tree) and a + `Cargo.lock` at your app's **Cargo workspace root** (the workspace that owns + `cli-package` — in a nested-workspace monorepo this may be your app + subdirectory, not the repo root). `build-cli` requires it, and caching keys on + it. - Pin action references to readable released tags, or full SHAs for production reproducibility. - Use least-privilege permissions (`contents: read`), protected environments, diff --git a/docs/specs/edgezero-deploy-github-action.md b/docs/specs/edgezero-deploy-github-action.md index fa5ee060..7f64f33a 100644 --- a/docs/specs/edgezero-deploy-github-action.md +++ b/docs/specs/edgezero-deploy-github-action.md @@ -38,7 +38,7 @@ Three layers: | Layer | Action | Responsibility | | --------------- | ------------------- | ------------------------------------------------------------------------------------- | -| Build | `build-cli` | Compile the caller-selected EdgeZero CLI **once** and publish it. | +| Build | `build-cli` | Compile the app-provided CLI package **once** and publish it. | | Engine (shared) | `deploy-core` | Adapter-independent engine **scripts** sourced by wrappers; consume the prebuilt CLI. | | Adapter wrapper | `deploy-fastly` (…) | Minimal per-adapter shim: type provider credentials, call the engine. | @@ -221,23 +221,23 @@ ref and keeps one engine for every adapter. The engine is parameterized by the values the wrapper passes to those scripts (as environment variables), conceptually: -| Parameter | Meaning | -| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `adapter` | Passed to ` deploy --adapter `. The engine does **not** enumerate compiled adapters; the CLI rejects an unknown adapter with its own error. | -| `cli-artifact` | Name of the `build-cli` artifact to download. The engine reads `cli-bin` and `cli-version` from the artifact's `cli-meta.json`. | -| `cli-bin` | Optional override for the binary name; if empty, taken from `cli-meta.json`. | -| `working-directory` | Application directory relative to `github.workspace`. Must resolve inside `github.workspace`. | -| `manifest` | Optional `edgezero.toml` path relative to `working-directory`. If set, must exist; exported as `EDGEZERO_MANIFEST`. | -| `rust-toolchain` | Application Rust toolchain for the deploy build. `auto` follows §7. | -| `target` | Application build target. `auto` derives it from `adapter` (for example `fastly` → `wasm32-wasip1`). | -| `build-mode` | One of `auto`, `always`, `never` (§8). | -| `build-args` | JSON array of strings passed after ` build --adapter --`. Must not contain secrets. | -| `deploy-args` | JSON array of caller-supplied deploy args appended after action-owned deploy flags. Must not contain secrets. | -| `deploy-arg-allow` | Adapter allowlist pattern for caller `deploy-args` (wrapper-provided; §9). | -| `provider-env` | JSON object of provider credential names → values, injected **only** into the deploy step (§10). | -| `provider-env-clear` | JSON array of env var names (wrapper-provided) the engine unsets in non-deploy steps and clears + re-exports from `provider-env` in the deploy step (§10). Keeps alias-clearing provider-agnostic. | -| `deploy-flags` | JSON array of action-owned deploy flags the wrapper injects before caller `deploy-args` (`--service-id …`, `--non-interactive`). | -| `cache` | Enable exact-key application `target/` caching (`true`/`false`). | +| Parameter | Meaning | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `adapter` | Passed to ` deploy --adapter `. The engine does **not** enumerate compiled adapters; the CLI rejects an unknown adapter with its own error. | +| `cli-artifact` | Name of the `build-cli` artifact to download. The engine reads `cli-bin` and `cli-version` from the artifact's `cli-meta.json`. | +| `cli-bin` | Optional override for the binary name; if empty, taken from `cli-meta.json`. | +| `working-directory` | Application directory relative to `github.workspace`. Must resolve inside `github.workspace`. | +| `manifest` | Optional `edgezero.toml` path relative to `working-directory`. If set, must exist; exported as `EDGEZERO_MANIFEST`. | +| `rust-toolchain` | Application Rust toolchain for the deploy build. `auto` follows §7. | +| `target` | Application build target. `auto` derives it from `adapter` (for example `fastly` → `wasm32-wasip1`). | +| `build-mode` | One of `auto`, `always`, `never` (§8). | +| `build-args` | JSON array of strings passed after ` build --adapter --`. Must not contain secrets. | +| `deploy-args` | JSON array of caller-supplied deploy args appended after action-owned deploy flags. Must not contain secrets. | +| `deploy-arg-allow` | Adapter allowlist pattern for caller `deploy-args` (wrapper-provided; §9). | +| `provider-env` | JSON object of provider credential names → values. Present **only in the deploy step's own environment** (step-scoped `env:`); the variable never reaches setup/build steps, and the engine parses it only inside the deploy step (§10). | +| `provider-env-clear` | JSON array of env var names (wrapper-provided) the engine unsets in non-deploy steps and clears + re-exports from `provider-env` inside the deploy step (§10). Defense-in-depth against inherited caller `env:`; keeps clearing provider-agnostic. | +| `deploy-flags` | JSON array of action-owned deploy flags the wrapper injects before caller `deploy-args` (`--service-id …`, `--non-interactive`). | +| `cache` | Enable exact-key application `target/` caching (`true`/`false`). | The wrapper surfaces engine results as its own outputs: `adapter`, `source-revision`, `cli-version`, `effective-build-mode`. @@ -295,9 +295,11 @@ without the engine itself knowing Fastly's names. read `cli-meta.json` for `cli-bin`/`cli-version` (a wrapper `cli-bin` input overrides), and prepend the directory to `PATH` for action steps only. 5. Parse `build-args`, `deploy-args`, `deploy-flags`, `provider-env-clear` as - JSON string arrays and `provider-env` as a JSON string→string object. + JSON string arrays. **`provider-env` is not present in these steps** — it is + scoped to the deploy step's own `env:` and parsed only there (step 17), so + setup/build never hold the secret-bearing blob. 6. In every non-deploy step (setup, build), unset each name in - `provider-env-clear` so no provider credential leaks outside the deploy step. + `provider-env-clear` (defense-in-depth against inherited caller `env:`). 7. Reject NUL-containing argument or value entries. 8. Apply the adapter's deploy-arg allowlist (wrapper-provided) to `deploy-args`. 9. Resolve `working-directory` beneath `github.workspace` using canonical paths @@ -318,8 +320,9 @@ without the engine itself knowing Fastly's names. 16. Resolve `build-mode` (§8). If `always`, run ` build --adapter -- ` with **no** provider credentials in scope (the `provider-env-clear` names stay unset here). -17. In a separate deploy step, clear the `provider-env-clear` aliases, then - export only the `provider-env` values, and run: +17. In a separate deploy step whose step-scoped `env:` is the **only** place + `provider-env` is exposed: clear the `provider-env-clear` aliases, parse + `provider-env` and export only its values, and run: ```text deploy --adapter -- @@ -398,12 +401,17 @@ wrapper inputs (typed) → provider-env {NAME: value} → deploy step env → CL Rules: -- Setup, `build-cli`, and separate build steps never receive `provider-env`. -- Alias clearing is **wrapper-driven and provider-agnostic**: the engine unsets - the names in the wrapper-supplied `provider-env-clear` list in non-deploy - steps, and in the deploy step clears those same names before exporting only - the typed `provider-env` values. The engine hard-codes no provider names, so - caller `env:` cannot override the typed contract. +- **`provider-env` is bound only to the deploy step's own `env:`** — a + step-scoped environment, not a job/engine-global variable. Setup, `build-cli`, + and separate build steps never receive the `provider-env` variable at all, so + the secret-bearing blob is absent from their environments (not merely unset + after the fact). The engine parses `provider-env` only inside the deploy step. +- Alias clearing is **wrapper-driven and provider-agnostic**, and is + defense-in-depth for a _different_ threat — a caller who sets provider aliases + through their own workflow `env:`. The engine unsets the wrapper-supplied + `provider-env-clear` names in non-deploy steps, and clears them in the deploy + step before exporting only the `provider-env` values. The engine hard-codes no + provider names, so caller `env:` cannot override the typed contract. - `provider-env` values never reach `GITHUB_ENV`, `GITHUB_OUTPUT`, caches, or summaries. @@ -626,8 +634,8 @@ The design is implemented when: 6. Typed provider credentials reach only the deploy step and never appear in outputs, caches, action-owned logs, or summaries. 7. Passthrough argument boundaries are preserved; no `eval`. -8. `cache: true` uses exact keys and caches only the application Git root - `target/`. +8. `cache: true` uses exact keys and caches only the **Cargo workspace root** + `target/` (§11.1), so nested-workspace monorepos cache the right artifacts. 9. All CI, tooling, and tests run without Python; `actionlint` and `zizmor` run from pinned release binaries. 10. Third-party actions are pinned to readable released tags.