Skip to content

Commit f717674

Browse files
authored
Bind Fastly stores per deployment environment (#381)
* fix(fastly): apply canonical store selectors at deploy * test(actions): model Fastly resource link IDs * docs: move deploy specs under superpowers * docs: align deploy design artifact layout * docs: design version-scoped Fastly selectors * docs: plan version-scoped Fastly selectors * refactor(deploy): let adapters claim managed deployment * feat(fastly): define versioned runtime descriptors * feat(fastly): load runtime config by service version * docs: drop legacy Fastly selector migration * fix(fastly): validate managed deploy inputs before dispatch * docs: require immutable Fastly application releases * feat(fastly): plan descriptors and resource links before deploy * fix(fastly): prepare selectors before publishing versions * fix(actions): retain failed Fastly deploy versions * test(actions): model version-scoped Fastly descriptors * fix(fastly): compile host-only tests for host targets * docs(fastly): explain version-scoped runtime selectors * chore: generalize lifecycle examples * fix(actions): tighten Fastly lifecycle contracts * fix(config): align staging keys with runtime descriptors * fix(config): apply manifest store defaults consistently * test(fastly): redact sensitive assertion diagnostics * fix(fastly): close managed deployment review gaps * test(fastly): gate package hash fixture by target * test(actions): satisfy shellcheck for literal domains * docs: replace Fastly runtime selector design * fix(config): reject invalid store selectors before provider access * refactor(fastly): use logical version resource links * test(actions): align staging link mutation order * test(fastly): avoid logging selector errors * fix(fastly): keep runtime store keys target independent * fix(manifest): resolve adapter logging case insensitively * fix(fastly): harden managed deployment lifecycle * test(actions): align smokes with logical store keys * fix(fastly): harden immutable release publication * fix(actions): align immutable release contracts * fix(actions): scope Fastly releases to Fastly * docs(actions): design provider-neutral action cores * docs(actions): define lifecycle capability contract * docs(actions): preserve repeated lifecycle outputs * docs(actions): preserve lifecycle parsing semantics * docs(actions): tighten modular core contracts * fix(fastly): parse live resource link types * docs(fastly): document runtime selector migration * docs(actions): plan provider-neutral action cores * refactor(actions): modularize application release lifecycle * fix(actions): preserve caller script directory * fix(ci): refresh app demo lockfile * test(ci): run every adapter CLI suite * ci: split host test suites by surface * ci: retain uploaded artifacts for 14 days * fix(fastly): avoid unsupported compute version diff * fix(fastly): use pubsub logging API path * fix(fastly): use current service commands * ci: retain full workspace test baseline * ci: add reusable Rust build cache * fix(fastly): canonicalize configuration snapshots * ci: cache application Cargo target artifacts * test(ci): satisfy cache action shellcheck * fix: address managed Fastly deployment review * fix: address deployment review after main merge * test: cover mixed-case deploy selector precedence * fix: thread packaged digest into deploy smoke * test: cover stale cached CLI under a configured Cargo target * fix: build the app CLI for an explicit native target A configured Cargo target (CARGO_BUILD_TARGET or build.target) moves the fresh executable to <target-dir>/<triple>/release, but build-app-cli always packaged <target-dir>/release. With a restored cache, an older root-level executable still passed --help and was published as the current CLI. Pass the toolchain's host triple as --target, which overrides any configured target, and package the executable from that triple's directory. * docs: restore corrections the main merge dropped Merging main (9da604f) resolved five guide files to this branch's side and dropped the corrections #373 made against the code. Restore each one that still matches the code here: the Fastly manifest version, provision behavior, build/serve commands, proxy import path, and streaming note; Cloudflare local push by binding; the serve fallbacks, healthcheck outputs, push outputs, and manifest/service-id rows in the CLI reference; and the bare-handle constructors and `wrangler kv key put` spelling in the store migration guide. Also correct the local secret commands that never worked: `wrangler secret put` takes no `--binding`, Fastly CLI 15.1.0 reads secret values from `--stdin` or `--file`, and Spin reads `SPIN_VARIABLE_<NAME>` rather than the bare variable name. Note the runner 2.327.1 requirement for the Node 24 producer actions and drop stale overlay references from the Fastly request rustdoc. * fix(scaffold): make the commented upstream handler compile The example returned `text(...)`, which does not exist, so uncommenting it in a generated app failed with E0425. Return `Text<&'static str>` like the template's other handlers. * feat(axum): read secrets from EDGEZERO__SECRETS__<KEY> The Axum dev server looked a secret key up as an environment variable of the same name, so `api_token = "demo_api_token"` read `demo_api_token`. That breaks env-var conventions and lets any inherited variable whose name matches a key answer for it silently. Secret key `k` now resolves from `EDGEZERO__SECRETS__` plus `k` in ASCII uppercase, e.g. `EDGEZERO__SECRETS__DEMO_API_TOKEN`. One rule in `secret_env` serves both the runtime store and `config validate`/`push`: a key must be ASCII letters, digits, or `_`, and two different keys that differ only in case are rejected because they would share a variable. The bare variable is no longer read. Update the Axum guide, CLI reference, migration guide, the generated README and app config template, the core rustdoc, and the smoke scripts.
1 parent 683202c commit f717674

128 files changed

Lines changed: 16895 additions & 10549 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/actions/build-app-cli/action.yml‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ outputs:
4747
description: The binary name inside the artifact.
4848
value: ${{ steps.build.outputs['app-cli-bin'] }}
4949
app-cli-artifact:
50-
description: Artifact name the deploy, healthcheck, rollback, and config-push-fastly actions consume.
50+
description: Artifact name consumed when assembling an immutable application release.
5151
value: ${{ steps.build.outputs['app-cli-artifact'] }}
5252

5353
runs:
@@ -105,6 +105,9 @@ runs:
105105
EDGEZERO__PROJECT__WORKING_DIRECTORY: ${{ inputs['working-directory'] }}
106106
EDGEZERO__PROJECT__RUST_TOOLCHAIN: ${{ inputs['rust-toolchain'] }}
107107
EDGEZERO__APP__CLI__ARTIFACT: ${{ inputs['app-cli-artifact'] }}
108+
# setup-rust-build-cache publishes this through GITHUB_ENV only when its
109+
# opt-in target cache is enabled. build-app-cli confines it to RUNNER_TEMP.
110+
EDGEZERO__RUST_CACHE__TARGET_DIR: ${{ env.EDGEZERO__RUST_CACHE__TARGET_DIR }}
108111
# This step runs app-controlled code, so it emits NO outputs through the
109112
# runner's GITHUB_OUTPUT (blanked below). It collects them into this
110113
# action-owned file; the separate "Publish CLI outputs" step re-emits them.
@@ -276,9 +279,7 @@ runs:
276279
name: ${{ steps.build.outputs['app-cli-artifact'] }}
277280
path: ${{ steps.build.outputs['tarball-path'] }}
278281
if-no-files-found: error
279-
# No forced retention: the repository/org default applies, so the artifact
280-
# survives an environment-approval gate that delays the deploy job past a day
281-
# (a forced `retention-days: 1` would expire the CLI before it downloads).
282+
retention-days: 14
282283

283284
# Remove the per-invocation workspace (build/stage dirs, the outputs handoff)
284285
# now that the artifact is uploaded. Runs no application code.

‎.github/actions/build-app-cli/scripts/build-app-cli.sh‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
set -euo pipefail
33

44
# Compiles the CLI package the APPLICATION provides — a crate in the app's own
5-
# workspace — into an action-owned CARGO_TARGET_DIR, then packages the binary
5+
# workspace — into an application-scoped cached CARGO_TARGET_DIR when configured,
6+
# or an action-owned scratch directory otherwise. It then packages the binary
67
# plus a self-describing app-cli-meta.json into a tar so the executable bit
78
# survives actions/upload-artifact. Never builds the EdgeZero monorepo CLI.
89
#
@@ -17,6 +18,7 @@ set -euo pipefail
1718
# RUNNER_TEMP optional action-owned scratch root (default: /tmp)
1819
# EDGEZERO__PROVIDER__ENV_CLEAR optional JSON array of provider aliases to scrub before the build (default: [])
1920
# EDGEZERO__ACTION__WORKSPACE optional per-invocation scratch root (default: mktemp under RUNNER_TEMP)
21+
# EDGEZERO__RUST_CACHE__TARGET_DIR optional shared Cargo target directory beneath RUNNER_TEMP
2022
# EDGEZERO__ACTION__OUTPUT_FILE optional outputs handoff file, started fresh (default: GITHUB_OUTPUT)
2123
# Writes (outputs):
2224
# app-cli-package the package that was built
@@ -194,9 +196,23 @@ main() {
194196
[[ -n "$action_ws" ]] || action_ws=$(mktemp -d "$runner_temp/edgezero-cli.XXXXXX")
195197
local stage_root="$action_ws/artifact"
196198
local build_target_dir="$action_ws/build"
199+
local cached_target_dir="${EDGEZERO__RUST_CACHE__TARGET_DIR:-}"
200+
201+
if [[ -n "$cached_target_dir" ]]; then
202+
[[ -d "$runner_temp" ]] || fail "RUNNER_TEMP does not exist or is not a directory"
203+
[[ -d "$cached_target_dir" ]] ||
204+
fail "cached Cargo target directory does not exist or is not a directory"
205+
local runner_temp_real cached_target_real
206+
runner_temp_real=$(canonical_path "$runner_temp")
207+
cached_target_real=$(canonical_path "$cached_target_dir")
208+
is_under "$runner_temp_real" "$cached_target_real" ||
209+
fail "cached Cargo target directory must resolve inside RUNNER_TEMP"
210+
build_target_dir="$cached_target_real"
211+
fi
197212

198213
require_linux_x86_64
199214
require_cmd cargo
215+
require_cmd rustc
200216
require_cmd rustup
201217
require_cmd jq
202218
require_cmd tar
@@ -260,12 +276,25 @@ main() {
260276
[[ "$has_bin" == "true" ]] || fail "app-cli-package '$cli_package' declares no binary target named '$cli_bin'"
261277
cli_version=$(jq -r '.version' <<<"$package_json")
262278

263-
# Build into an action-owned target dir so the checkout stays clean.
264-
reset_owned_dir "$build_target_dir" "$runner_temp"
279+
# Keep restored artifacts when setup-rust-build-cache supplied the target.
280+
# The uncached fallback remains isolated to this invocation and starts clean.
281+
if [[ -z "$cached_target_dir" ]]; then
282+
reset_owned_dir "$build_target_dir" "$runner_temp"
283+
fi
284+
285+
# Name the native target explicitly. A command-line `--target` overrides any
286+
# `build.target` the application configures (adapter scaffolds set a WASM one),
287+
# and it fixes Cargo's output path. Without it, a configured target sends the
288+
# fresh executable to `<triple>/release` while a restored cache can still hold
289+
# an older root-level `release/$cli_bin`, which would then be packaged.
290+
local host_triple
291+
host_triple=$(rustc +"$rust_toolchain" -vV | sed -n 's/^host: //p') ||
292+
fail "could not query the Rust host triple"
293+
[[ -n "$host_triple" ]] || fail "could not determine the Rust host triple"
265294
run_untrusted CARGO_TARGET_DIR="$build_target_dir" cargo +"$rust_toolchain" build \
266-
--locked --release -p "$cli_package" --bin "$cli_bin"
295+
--locked --release --target "$host_triple" -p "$cli_package" --bin "$cli_bin"
267296

268-
local bin_path="$build_target_dir/release/$cli_bin"
297+
local bin_path="$build_target_dir/$host_triple/release/$cli_bin"
269298
[[ -x "$bin_path" ]] || fail "build did not produce an executable at $bin_path"
270299
run_untrusted "$bin_path" --help >/dev/null 2>&1 || fail "built CLI '$cli_bin' did not run '$cli_bin --help'"
271300

@@ -278,7 +307,7 @@ main() {
278307
>"$stage_root/app-cli-meta.json"
279308

280309
# Fixed tarball name — never derive a path component from caller input.
281-
local tarball="$stage_root/../edgezero-cli.tar"
310+
local tarball="$stage_root/../app-cli.tar"
282311
tar -C "$stage_root" -cf "$tarball" "$cli_bin" app-cli-meta.json
283312
tarball=$(canonical_path "$tarball")
284313

‎.github/actions/config-push-fastly/action.yml‎

Lines changed: 43 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,68 @@
11
name: EdgeZero config-push-fastly
2-
description: Push a checked-out EdgeZero application's typed config to a Fastly config store using a prebuilt app CLI artifact.
2+
description: Push publisher runtime config using the CLI and manifest from a verified immutable application release.
33

44
inputs:
5-
app-cli-artifact:
6-
description: Name of the build-app-cli artifact to download and run.
5+
app-release-archive:
6+
description: Path to the pinned application release archive.
7+
required: true
8+
app-release-sha256:
9+
description: Expected lowercase SHA-256 digest of the application release archive.
10+
required: true
11+
expected-source-revision:
12+
description: Full source revision that release.json must record.
713
required: true
8-
app-cli-bin:
9-
description: Binary name inside the artifact. Defaults to the artifact metadata.
10-
required: false
11-
default: ""
1214
fastly-api-token:
13-
description: Fastly API token. Injected only into the push step.
15+
description: Fastly API token, scoped only to provider operations.
1416
required: true
1517
working-directory:
16-
description: Application directory relative to github.workspace (holds the manifest + typed config).
18+
description: Directory relative to github.workspace containing a publisher-owned app-config file.
1719
required: false
1820
default: .
19-
manifest:
20-
description: Optional edgezero.toml path relative to working-directory.
21-
required: false
22-
default: ""
2321
app-config:
24-
description: "Optional typed config file path relative to working-directory (default: resolved from the manifest). Mutually exclusive with app-config-inline."
22+
description: Typed runtime config file relative to working-directory; exactly one config input is required.
2523
required: false
2624
default: ""
2725
app-config-inline:
28-
description: "Optional raw typed-config content (TOML) supplied inline instead of from a checked-out file — for config that lives in a GitHub variable with no file on disk. Written to an action-owned temp file and passed to the CLI. Mutually exclusive with app-config."
26+
description: Inline typed runtime config; exactly one config input is required.
2927
required: false
3028
default: ""
3129
no-env:
32-
description: "When 'true', pass --no-env so the CLI does NOT overlay <APP_NAME>__…__<KEY> environment variables onto the typed config before pushing. Defaults to 'false'."
30+
description: "When 'true', skip the typed runtime environment overlay."
3331
required: false
3432
default: "false"
3533
store:
36-
description: "Optional logical config-store id (default: the manifest's resolved id)."
34+
description: Optional logical Config Store ID declared by the bundled application manifest.
3735
required: false
3836
default: ""
3937
key:
40-
description: "Optional explicit base key for a PRODUCTION push (default: the logical store id). Not allowed with deploy-to: staging, whose key is derived."
38+
description: "Deprecated and rejected when nonempty; select the environment's physical store with EDGEZERO__STORES__CONFIG__<ID>__NAME."
4139
required: false
4240
default: ""
4341
deploy-to:
44-
description: "'production' writes the base key; 'staging' writes the <logical-store-id>_staging variant in the same store (the key the staging selector points at)."
42+
description: "Select the canonical environment key, with production/staging fallback when absent."
4543
required: false
4644
default: production
4745

4846
outputs:
4947
pushed-key:
50-
description: The key that was written (the base key, or the derived _staging variant).
48+
description: Key written by the application CLI.
5149
value: ${{ steps.push.outputs['pushed-key'] }}
5250
store:
53-
description: The logical config-store id the CLI resolved (always emitted, not only when the `store` input was supplied).
51+
description: Logical Config Store ID resolved by the bundled manifest.
5452
value: ${{ steps.push.outputs.store }}
5553
mutation-attempted:
56-
description: "'true', emitted immediately BEFORE the config-push CLI runs (so a cancel/timeout mid-mutation can preserve it; a hard runner loss can still drop it, so absence is not proof the store is unchanged, and a cancel in the tiny pre-run window is a conservative false positive). On failure, read this via `if: always()` and reconcile — do not assume the config store is unchanged."
54+
description: "'true' when the config-push CLI was invoked."
5755
value: ${{ steps.push.outputs['mutation-attempted'] }}
5856
provider-cli-version:
59-
description: The pinned Fastly CLI version this action installed and ran.
57+
description: Pinned Fastly CLI version installed by this action.
6058
value: ${{ steps.install-fastly.outputs['provider-cli-version'] }}
6159

6260
runs:
6361
using: composite
6462
steps:
65-
# A UNIQUE per-invocation workspace root under RUNNER_TEMP, so two concurrent
66-
# invocations in one job (e.g. `background: true`) never collide on fixed temp
67-
# paths (CLI download, extracted tools). The cleanup step removes it.
6863
- name: Prepare action workspace
6964
id: ws
7065
shell: bash
71-
# Runs before validation, so it scrubs like every other step: blank the
72-
# shipped aliases and BASH_ENV/ENV (a caller's job env could otherwise point
73-
# BASH_ENV at checkout code that runs at bash startup with a token in scope).
7466
env:
7567
BASH_ENV: ""
7668
ENV: ""
@@ -97,10 +89,13 @@ runs:
9789
env:
9890
BASH_ENV: ""
9991
ENV: ""
100-
EDGEZERO__APP__CLI__ARTIFACT_PRESENT: ${{ inputs['app-cli-artifact'] != '' && 'true' || 'false' }}
92+
EDGEZERO__APP__RELEASE__ARCHIVE_PRESENT: ${{ inputs['app-release-archive'] != '' && 'true' || 'false' }}
93+
EDGEZERO__APP__RELEASE__SHA256_PRESENT: ${{ inputs['app-release-sha256'] != '' && 'true' || 'false' }}
10194
EDGEZERO__FASTLY__API_TOKEN_PRESENT: ${{ inputs['fastly-api-token'] != '' && 'true' || 'false' }}
10295
EDGEZERO__DEPLOY__TO: ${{ inputs['deploy-to'] }}
103-
EDGEZERO__CONFIG_PUSH__KEY_PRESENT: ${{ inputs.key != '' && 'true' || 'false' }}
96+
EDGEZERO__CONFIG_PUSH__APP_CONFIG_PRESENT: ${{ inputs['app-config'] != '' && 'true' || 'false' }}
97+
EDGEZERO__CONFIG_PUSH__APP_CONFIG_INLINE_PRESENT: ${{ inputs['app-config-inline'] != '' && 'true' || 'false' }}
98+
EDGEZERO__CONFIG_PUSH__KEY: ${{ inputs.key }}
10499
FASTLY_API_TOKEN: ""
105100
FASTLY_SERVICE_ID: ""
106101
FASTLY_TOKEN: ""
@@ -119,12 +114,18 @@ runs:
119114
FASTLY_HOME: ""
120115
run: exec "$GITHUB_ACTION_PATH/scripts/validate.sh"
121116

122-
- name: Download CLI artifact
123-
uses: actions/download-artifact@v8
124-
with:
125-
name: ${{ inputs['app-cli-artifact'] }}
126-
path: ${{ steps.ws.outputs.root }}/cli-download
117+
- name: Verify application release
118+
id: release
119+
shell: bash
127120
env:
121+
BASH_ENV: ""
122+
ENV: ""
123+
EDGEZERO__APP__RELEASE__ARCHIVE: ${{ inputs['app-release-archive'] }}
124+
EDGEZERO__APP__RELEASE__SHA256: ${{ inputs['app-release-sha256'] }}
125+
EDGEZERO__APP__RELEASE__EXPECTED_SOURCE_REVISION: ${{ inputs['expected-source-revision'] }}
126+
EDGEZERO__APP__RELEASE__EXPECTED_ADAPTER: fastly
127+
EDGEZERO__APP__RELEASE__EXPECTED_LIFECYCLE_PROTOCOL: "1"
128+
EDGEZERO__APP__RELEASE__ROOT: ${{ steps.ws.outputs.root }}/release
128129
FASTLY_API_TOKEN: ""
129130
FASTLY_SERVICE_ID: ""
130131
FASTLY_TOKEN: ""
@@ -141,16 +142,16 @@ runs:
141142
FASTLY_CONFIG_FILE: ""
142143
FASTLY_CARGO_PROFILE: ""
143144
FASTLY_HOME: ""
145+
run: exec "$GITHUB_ACTION_PATH/../release-core/scripts/prepare-release.sh"
144146

145-
- name: Extract CLI
147+
- name: Extract application CLI
146148
id: cli
147149
shell: bash
148150
env:
149151
BASH_ENV: ""
150152
ENV: ""
151-
EDGEZERO__APP__CLI__ARTIFACT_DIR: ${{ steps.ws.outputs.root }}/cli-download
153+
EDGEZERO__APP__CLI__ARCHIVE: ${{ steps.release.outputs['app-cli-archive'] }}
152154
EDGEZERO__ACTION__TOOL_ROOT: ${{ steps.ws.outputs.root }}/tools
153-
EDGEZERO__APP__CLI__BIN: ${{ inputs['app-cli-bin'] }}
154155
FASTLY_API_TOKEN: ""
155156
FASTLY_SERVICE_ID: ""
156157
FASTLY_TOKEN: ""
@@ -199,27 +200,19 @@ runs:
199200
id: push
200201
shell: bash
201202
env:
202-
# BASH_ENV/ENV are sourced at bash startup, before this script can scrub —
203-
# blank them so a caller's job env cannot run code here with the token.
204203
BASH_ENV: ""
205204
ENV: ""
206-
# Mint the sensitive lifecycle log under the per-invocation workspace so the
207-
# Cleanup step removes it wholesale even if the in-process EXIT trap cannot fire.
208205
EDGEZERO__ACTION__WORKSPACE: ${{ steps.ws.outputs.root }}
209-
EDGEZERO__APP__CLI__BIN: ${{ steps.cli.outputs['app-cli-bin'] }}
210206
EDGEZERO__APP__CLI__PATH: ${{ steps.cli.outputs['app-cli-path'] }}
211207
EDGEZERO__PROJECT__WORKING_DIRECTORY: ${{ inputs['working-directory'] }}
212208
EDGEZERO__DEPLOY__TO: ${{ inputs['deploy-to'] }}
213209
EDGEZERO__CONFIG_PUSH__STORE: ${{ inputs.store }}
214-
EDGEZERO__CONFIG_PUSH__KEY: ${{ inputs.key }}
215-
EDGEZERO__CONFIG_PUSH__MANIFEST: ${{ inputs.manifest }}
210+
EDGEZERO__CONFIG_PUSH__MANIFEST: ${{ steps.release.outputs['application-manifest'] }}
216211
EDGEZERO__CONFIG_PUSH__APP_CONFIG: ${{ inputs['app-config'] }}
217212
EDGEZERO__CONFIG_PUSH__APP_CONFIG_INLINE: ${{ inputs['app-config-inline'] }}
218213
EDGEZERO__CONFIG_PUSH__NO_ENV: ${{ inputs['no-env'] }}
219-
# Only the typed token reaches the CLI under the adapter's own convention
220-
# (FASTLY_API_TOKEN, what `fastly config-store-entry update` reads); every
221-
# other inherited FASTLY_* alias is blanked so none can redirect the push.
222-
FASTLY_API_TOKEN: ${{ inputs['fastly-api-token'] }}
214+
EDGEZERO__FASTLY__API_TOKEN: ${{ inputs['fastly-api-token'] }}
215+
FASTLY_API_TOKEN: ""
223216
FASTLY_SERVICE_ID: ""
224217
FASTLY_TOKEN: ""
225218
FASTLY_KEY: ""

0 commit comments

Comments
 (0)