Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
b946250
Design: layered EdgeZero deploy actions with Fastly staging lifecycle
aram356 Jul 9, 2026
49f5668
Plan: align deploy-core credential scoping with the spec
aram356 Jul 9, 2026
7446fbb
Spec/plan review: lifecycle service-id, provider-tool install, concre…
aram356 Jul 9, 2026
6f18573
Self-review: wire staging lifecycle through error/testing/acceptance;…
aram356 Jul 9, 2026
f3e09ef
Merge branch 'main' into feature/edgezero-deploy-actions
aram356 Jul 9, 2026
193c045
impl(actions): build-cli + deploy-core engine foundation
aram356 Jul 10, 2026
a668a8f
impl(actions): deploy-fastly Fastly CLI installer + versions.json
aram356 Jul 10, 2026
658e6da
impl(actions): run-cli engine script + deploy-fastly/healthcheck/roll…
aram356 Jul 10, 2026
1007c37
impl(actions): readability pass on run-cli, build-cli, tests
aram356 Jul 10, 2026
344d244
impl(actions): consistent main() structure, CI workflow, use setup-ru…
aram356 Jul 10, 2026
06ac765
impl(cli): Fastly staging lifecycle scaffolding (deploy --stage, heal…
aram356 Jul 10, 2026
747cf70
Merge remote-tracking branch 'origin/feature/edgezero-deploy-actions'…
aram356 Jul 10, 2026
9a1d242
docs(plan): note install-rust replaced by setup-rust-toolchain@v1
aram356 Jul 10, 2026
2b18e73
fix(ci): clippy restriction lints in tests, scope actionlint, extract…
aram356 Jul 10, 2026
80293d3
fix(ci): executable bits, zizmor tag ignore, smoke fixture fastly.toml
aram356 Jul 11, 2026
7515286
fix(ci): shellcheck -e SC1091, edgezero.toml deploy override for smoke
aram356 Jul 11, 2026
7492be3
fix(ci): cleanup.sh set -e footgun (absent dirs) + two more [[..]] &&…
aram356 Jul 11, 2026
3370d2b
docs(guide): add 'Deploying from GitHub Actions' user guide (plan pha…
aram356 Jul 11, 2026
fda39db
Merge branch 'main' into feature/edgezero-deploy-actions
aram356 Jul 12, 2026
a7bc4d2
fix: address security + production-safety review (9 findings)
aram356 Jul 12, 2026
1358c02
Merge remote-tracking branch 'origin/feature/edgezero-deploy-actions'…
aram356 Jul 12, 2026
de476c6
Add Fastly lifecycle acceptance test; close the --features cli test gap
aram356 Jul 13, 2026
730b27c
lifecycle-smoke: exec bit on fake-env script; bind cli-bin via env (z…
aram356 Jul 13, 2026
348e680
deploy-action: move every smoke assertion into a shellcheck'd script
aram356 Jul 13, 2026
5db686b
Fix review findings; unify action env vars under EDGEZERO__<SECTION>_…
aram356 Jul 14, 2026
688649e
Say "app CLI" everywhere: EDGEZERO__APP__CLI__*, app-cli-* inputs, *-…
aram356 Jul 14, 2026
e640640
Drop the __INPUT pseudo-section; standardize every script header
aram356 Jul 14, 2026
2b9163c
Merge origin/main (Rust edition 2024) and resolve for the deploy-acti…
aram356 Jul 15, 2026
1897411
lifecycle-smoke: keep fake-binary control vars out of the scrubbed na…
aram356 Jul 15, 2026
10f3207
spec+plan: add config-push-fastly (§5.5) — same store, different key
aram356 Jul 15, 2026
5a423fe
Add config-push-fastly action + CLI `config push --staging`
aram356 Jul 16, 2026
4755691
Close the contract matrix: exit propagation, dirty source, cache key,…
aram356 Jul 16, 2026
db53283
Fix review: config-push confinement, exit status, resolved store, CI …
aram356 Jul 16, 2026
fc04401
config-push-smoke: assert each push against its own log; re-seed the …
aram356 Jul 16, 2026
1874ae7
Fix review: idempotent push, artifact validation, tool collision, NUL…
aram356 Jul 17, 2026
9f8d3d0
Bound Cargo + manifest resolution to the application repository
aram356 Jul 17, 2026
c97ba53
Make staged config real: point the staged draft at a staging selector…
aram356 Jul 17, 2026
299ff9d
lifecycle-smoke: teach the fake the staging relink, and assert it
aram356 Jul 17, 2026
ef58805
Archive link check: capture the listing instead of piping into grep -q
aram356 Jul 17, 2026
ec95681
Fix review round: rollback target, staging edge cases, path confineme…
aram356 Jul 17, 2026
e4edc0f
Move wrapper input validation out of inline YAML into tested scripts
aram356 Jul 17, 2026
445557e
lifecycle-smoke: fake an app WITH config selection so the relink fires
aram356 Jul 17, 2026
a6a606c
lifecycle-smoke: prove rollback RESOLVES the target, not staged-1
aram356 Jul 17, 2026
67fcfc5
Standardize the assert-*.sh Reads(env) headers to the block format
aram356 Jul 17, 2026
b650b85
Fix production rollback: activate an explicit target, not an inferred…
aram356 Jul 17, 2026
912ea9a
app-demo: order the Cmd variants alphabetically for clippy
aram356 Jul 18, 2026
d0c95fc
Mirror production overrides into the staging selector twin at deploy …
aram356 Jul 18, 2026
0990e68
Reject key + staging in config-push-fastly and align the staging-key …
aram356 Jul 18, 2026
284e972
Document the explicit production-rollback target contract
aram356 Jul 18, 2026
9ac68e6
Add a path-filtered CI job that installs the real Fastly CLI
aram356 Jul 18, 2026
833379d
Fix SIGPIPE in the Fastly installer-check verify step
aram356 Jul 18, 2026
a426ee5
Move the installer-check verification into a shellcheck'd script
aram356 Jul 18, 2026
5a5601e
Scrub process references from deploy-lifecycle code comments
aram356 Jul 18, 2026
015466f
Make verify-installed-version.sh executable
aram356 Jul 18, 2026
89925cf
Expose active-version in the generated CLI template
aram356 Jul 18, 2026
7a34e58
Harden staging isolation and rollback-target capture
aram356 Jul 18, 2026
65996d2
Parse a TOML-form extensionless rust-toolchain file
aram356 Jul 18, 2026
6cd558f
Update rollback and installer-gate docs to the implemented contract
aram356 Jul 18, 2026
daf03a8
Make enter_app_dir accept absolute paths; format spec doc
aram356 Jul 18, 2026
a31ff46
Give the production smoke a fake Fastly API for rollback-target capture
aram356 Jul 18, 2026
b8ee910
Close the remaining rollback/staging fail-open and lifecycle gaps
aram356 Jul 19, 2026
5eb03ec
Harden installer provenance and active-version validation
aram356 Jul 19, 2026
f51c156
Close remaining active-version, config-store, and installer-gate gaps
aram356 Jul 19, 2026
2ef447b
Close rollback-capture fail-open and add a rollback race guard
aram356 Jul 19, 2026
145f169
Reject malformed rollback targets; be honest about the rollback race
aram356 Jul 20, 2026
f9af7a8
Scope provider credentials to provider-calling steps; harden capture …
aram356 Jul 20, 2026
838e78b
Make build-app-cli credential scrubbing provider-neutral; cover the n…
aram356 Jul 22, 2026
77493ef
Restore the official Fastly pin and guard against committing a local …
aram356 Jul 22, 2026
338ba10
Establish a real credential-clear boundary in build-app-cli (re-exec,…
aram356 Jul 23, 2026
def385a
Close the build-app-cli credential-boundary bypasses (ancestor env, s…
aram356 Jul 24, 2026
8a874dd
Close the pre-scrub startup-sourcing and cross-step injection bypasses
aram356 Jul 24, 2026
6be9419
Close deployer adoption gaps: healthcheck path, inline/no-env config …
aram356 Jul 26, 2026
f364f94
Close the /proc GITHUB_ENV recovery, inline-file leak, and curl glob …
aram356 Jul 26, 2026
d6d907a
Isolate the app build from runner command storage; fix signal timing,…
aram356 Jul 26, 2026
2b91e3f
Publish mutation-attempted before the mutation; fix recovery, cleanup…
aram356 Jul 28, 2026
31de799
Fix VitePress build: don't let prettier wrap a long inline-code angle…
aram356 Jul 28, 2026
c6e0798
Fix rollback contracts, cancellation-aware recovery, and the trust-bo…
aram356 Jul 29, 2026
1a2a609
Reject zero healthcheck timeout; make recovery examples correct and h…
aram356 Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions .github/actions/build-app-cli/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
name: EdgeZero build-app-cli
description: Compile the CLI package the application provides and publish it as a self-describing artifact.

inputs:
app-cli-package:
description: Cargo package name of the CLI to build, defined in the application's own workspace.
required: true
app-cli-bin:
description: Binary name produced by app-cli-package. Defaults to the package name.
required: false
default: ""
working-directory:
description: Application directory (relative to github.workspace) whose workspace defines app-cli-package.
required: false
default: .
rust-toolchain:
description: Explicit host Rust toolchain, or 'auto' to follow application discovery.
required: false
default: auto
app-cli-artifact:
description: Name for the uploaded app-CLI artifact.
required: false
default: edgezero-cli
provider-env-clear:
description: >-
The DYNAMIC half of a two-layer credential scrub. A JSON array of
environment variable names re-exec'd out of the process before any
app-controlled command runs (cargo metadata, cargo build, the built CLI's
--help). The STATIC half lives in this action's step env, which blanks the
shipped adapters' aliases outright (and is the only layer that also protects
the upload step). This input exists so a consumer can add a provider this
action does not know about; its default repeats the shipped aliases so the
dynamic layer is self-contained too. Must be a JSON array of non-empty
identifier strings or the build fails closed.
required: false
default: >-
["FASTLY_API_TOKEN","FASTLY_SERVICE_ID","FASTLY_TOKEN","FASTLY_KEY","FASTLY_API_KEY","FASTLY_AUTH_TOKEN","FASTLY_API_ENDPOINT","FASTLY_ENDPOINT","FASTLY_API_URL","FASTLY_PROFILE","FASTLY_SERVICE_NAME","FASTLY_DEBUG","FASTLY_DEBUG_MODE","FASTLY_CONFIG_FILE","FASTLY_CARGO_PROFILE","FASTLY_HOME","CLOUDFLARE_API_TOKEN","CLOUDFLARE_API_KEY","CLOUDFLARE_ACCOUNT_ID","CLOUDFLARE_EMAIL","CF_API_TOKEN","CF_API_KEY","CF_ACCOUNT_ID","SPIN_AUTH_TOKEN","FERMYON_TOKEN"]

outputs:
app-cli-version:
description: CLI package version read from cargo metadata.
value: ${{ steps.build.outputs['app-cli-version'] }}
app-cli-package:
description: The application CLI package that was built.
value: ${{ steps.build.outputs['app-cli-package'] }}
app-cli-bin:
description: The binary name inside the artifact.
value: ${{ steps.build.outputs['app-cli-bin'] }}
app-cli-artifact:
description: Artifact name the deploy, healthcheck, and rollback actions consume.
value: ${{ steps.build.outputs['app-cli-artifact'] }}

runs:
using: composite
steps:
- name: Compile application CLI package
id: compile
shell: bash
env:
EDGEZERO__ACTION__ROOT: ${{ github.action_path }}/../../..
EDGEZERO__APP__CLI__PACKAGE: ${{ inputs['app-cli-package'] }}
EDGEZERO__APP__CLI__BIN: ${{ inputs['app-cli-bin'] }}
EDGEZERO__PROJECT__WORKING_DIRECTORY: ${{ inputs['working-directory'] }}
EDGEZERO__PROJECT__RUST_TOOLCHAIN: ${{ inputs['rust-toolchain'] }}
EDGEZERO__APP__CLI__ARTIFACT: ${{ inputs['app-cli-artifact'] }}
# This step runs app-controlled code, so it emits NO outputs through the
# runner's GITHUB_OUTPUT (blanked below). It collects them into this
# action-owned file; the separate "Publish CLI outputs" step re-emits them.
EDGEZERO__ACTION__OUTPUT_FILE: ${{ runner.temp }}/edgezero-build-app-cli-outputs.env
# This step runs APP-CONTROLLED code (cargo metadata/build, and the built
# CLI's `--help`), which must never see a provider token.
#
# Two layers, because neither alone is sufficient:
# 1. STATIC (below): blank the shipped adapters' aliases in the step's
# own `env:`. A `uses:`/`run:` step's environment can only be set
# statically, so these must be spelled out — and because they are set
# here, even this step's own `/proc/<pid>/environ` never contained
# them. This is the only layer that also protects the upload step.
# 2. DYNAMIC (`provider-env-clear`): covers aliases this action cannot
# know about (a caller's OWN provider). The `run:` body `exec`s the
# script, replacing the generated wrapper shell, and the script then
# re-execs itself with those names stripped — so no ancestor process
# that app code could walk up to still holds them.
EDGEZERO__PROVIDER__ENV_CLEAR: ${{ inputs['provider-env-clear'] }}
# BASH_ENV (bash) and ENV (POSIX sh) name a file the shell SOURCES AT
# STARTUP — before the first line of this step's script, and so before the
# credential scrub re-exec runs. If a caller's job env pointed either at
# checkout-controlled code, that code would run with the provider token
# still present AND could forge the re-exec sentinel argument (`set --
# <sentinel>`) to skip the scrub entirely. Blank both so no startup file
# is sourced.
BASH_ENV: ""
ENV: ""
# Isolate the app-controlled build from the runner's command storage. The
# runner keeps GITHUB_ENV/OUTPUT/PATH/STATE/step-summary as files in ONE
# directory, so holding ANY of their real paths lets a build script derive
# the siblings and append (e.g.) LD_PRELOAD to the real GITHUB_ENV, which
# the runner then applies to a LATER, secret-bearing step. The authoritative
# defense is the script's re-exec: it does `env -u` on all five channels, so
# the process that runs cargo (and every child) has none of their real
# paths in its environment or /proc. We do NOT blank them in this step's
# `env:` — the runner reinjects reserved GITHUB_* values after step-env
# evaluation, so that would be an ineffective no-op. Outputs are emitted by
# the separate publish step instead. NOTE: this closes the env/proc vectors,
# but a fully malicious build can still enumerate the runner's command
# directory at the same uid — so a provider secret must never share a step
# or job with the build (see the guide's security notes).
FASTLY_API_TOKEN: ""
FASTLY_SERVICE_ID: ""
FASTLY_TOKEN: ""
FASTLY_KEY: ""
FASTLY_API_KEY: ""
FASTLY_AUTH_TOKEN: ""
FASTLY_API_ENDPOINT: ""
FASTLY_ENDPOINT: ""
FASTLY_API_URL: ""
FASTLY_PROFILE: ""
FASTLY_SERVICE_NAME: ""
FASTLY_DEBUG: ""
FASTLY_DEBUG_MODE: ""
FASTLY_CONFIG_FILE: ""
FASTLY_CARGO_PROFILE: ""
FASTLY_HOME: ""
CLOUDFLARE_API_TOKEN: ""
CLOUDFLARE_API_KEY: ""
CLOUDFLARE_ACCOUNT_ID: ""
CLOUDFLARE_EMAIL: ""
CF_API_TOKEN: ""
CF_API_KEY: ""
CF_ACCOUNT_ID: ""
SPIN_AUTH_TOKEN: ""
FERMYON_TOKEN: ""
# `exec` REPLACES the generated wrapper shell with the script, so there is
# no wrapper-shell ancestor still holding the caller's custom provider
# secret (from its own execve environment) for app-controlled Cargo code to
# read via `/proc/<ppid>/environ`. The script then re-execs itself with the
# `provider-env-clear` names stripped.
run: exec "$GITHUB_ACTION_PATH/scripts/build-app-cli.sh"

# The trusted output boundary: a step that runs NO app-controlled code reads
# the compile step's handoff file and re-emits the outputs to the real
# GITHUB_OUTPUT, re-validating each (first-occurrence-wins; tarball-path
# confined to the action-owned temp area). Its `id` is `build` so the action's
# `outputs:` mapping (steps.build.outputs.*) is unchanged for consumers.
- name: Publish CLI outputs
id: build
shell: bash
env:
EDGEZERO__BUILD__OUTPUTS_FILE: ${{ runner.temp }}/edgezero-build-app-cli-outputs.env
FASTLY_API_TOKEN: ""
FASTLY_SERVICE_ID: ""
FASTLY_TOKEN: ""
FASTLY_KEY: ""
FASTLY_API_KEY: ""
FASTLY_AUTH_TOKEN: ""
FASTLY_API_ENDPOINT: ""
FASTLY_ENDPOINT: ""
FASTLY_API_URL: ""
FASTLY_PROFILE: ""
FASTLY_SERVICE_NAME: ""
FASTLY_DEBUG: ""
FASTLY_DEBUG_MODE: ""
FASTLY_CONFIG_FILE: ""
FASTLY_CARGO_PROFILE: ""
FASTLY_HOME: ""
CLOUDFLARE_API_TOKEN: ""
CLOUDFLARE_API_KEY: ""
CLOUDFLARE_ACCOUNT_ID: ""
CLOUDFLARE_EMAIL: ""
CF_API_TOKEN: ""
CF_API_KEY: ""
CF_ACCOUNT_ID: ""
SPIN_AUTH_TOKEN: ""
FERMYON_TOKEN: ""
run: exec "$GITHUB_ACTION_PATH/scripts/publish-outputs.sh"

# A script-level scrub cannot reach a LATER step: each step gets its own
# process with the job environment re-applied. This non-provider step blanks
# the SHIPPED aliases itself rather than inheriting them.
#
# A `uses:` step's `env:` is static, so it CANNOT blank a caller's custom
# `provider-env-clear` alias (only the compile step, which runs a script, can
# do that dynamically). That is an accepted boundary: this step runs no
# application code — it hands the publish step's confined tarball path to a
# pinned first-party action. The compile step runs every app-controlled command
# with all GitHub file-command channels blanked, so a build script cannot
# inject an environment variable (e.g. `LD_PRELOAD`) or PATH entry into this
# later step through them. That closes the demonstrated vectors, but is not a
# hard boundary against a fully malicious same-uid build (see the compile
# step's note): a custom provider secret should be scoped to the one step that
# needs it, never to job-level `env:` shared with the build.
- name: Upload CLI artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
env:
FASTLY_API_TOKEN: ""
FASTLY_SERVICE_ID: ""
FASTLY_TOKEN: ""
FASTLY_KEY: ""
FASTLY_API_KEY: ""
FASTLY_AUTH_TOKEN: ""
FASTLY_API_ENDPOINT: ""
FASTLY_ENDPOINT: ""
FASTLY_API_URL: ""
FASTLY_PROFILE: ""
FASTLY_SERVICE_NAME: ""
FASTLY_DEBUG: ""
FASTLY_DEBUG_MODE: ""
FASTLY_CONFIG_FILE: ""
FASTLY_CARGO_PROFILE: ""
FASTLY_HOME: ""
CLOUDFLARE_API_TOKEN: ""
CLOUDFLARE_API_KEY: ""
CLOUDFLARE_ACCOUNT_ID: ""
CLOUDFLARE_EMAIL: ""
CF_API_TOKEN: ""
CF_API_KEY: ""
CF_ACCOUNT_ID: ""
SPIN_AUTH_TOKEN: ""
FERMYON_TOKEN: ""
with:
name: ${{ steps.build.outputs['app-cli-artifact'] }}
path: ${{ steps.build.outputs['tarball-path'] }}
if-no-files-found: error
retention-days: 1
Loading
Loading