Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
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
60 changes: 60 additions & 0 deletions .github/actions/build-app-cli/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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

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: Build application CLI package
id: build
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'] }}
run: $GITHUB_ACTION_PATH/scripts/build-app-cli.sh

- name: Upload CLI artifact
uses: actions/upload-artifact@v4
with:
name: ${{ steps.build.outputs['app-cli-artifact'] }}
path: ${{ steps.build.outputs['tarball-path'] }}
if-no-files-found: error
retention-days: 1
217 changes: 217 additions & 0 deletions .github/actions/build-app-cli/scripts/build-app-cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
#!/usr/bin/env bash
set -euo pipefail

# Compiles the CLI package the APPLICATION provides — a crate in the app's own
# workspace — into an action-owned CARGO_TARGET_DIR, then packages the binary
# plus a self-describing app-cli-meta.json into a tar so the executable bit
# survives actions/upload-artifact. Never builds the EdgeZero monorepo CLI.
#
# Reads (env):
# EDGEZERO__APP__CLI__PACKAGE required Cargo package name to build
# EDGEZERO__ACTION__ROOT required EdgeZero action repo (toolchain fallback)
# GITHUB_WORKSPACE required checkout root; the search ceiling
# EDGEZERO__APP__CLI__BIN optional binary name (default: the package name)
# EDGEZERO__PROJECT__WORKING_DIRECTORY optional app dir under the workspace (default: ".")
# EDGEZERO__PROJECT__RUST_TOOLCHAIN optional explicit toolchain or "auto" (default: "auto")
# EDGEZERO__APP__CLI__ARTIFACT optional uploaded artifact name (default: "edgezero-cli")
# RUNNER_TEMP optional action-owned scratch root (default: /tmp)
# Writes (outputs):
# app-cli-package the package that was built
# app-cli-bin the binary name inside the artifact
# app-cli-version version from cargo metadata
# app-cli-artifact uploaded artifact name for downstream download
# tarball-path absolute path of the staged tar

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"

# --- Rust toolchain resolution helpers ---------------------------------------
parse_toolchain_from_channel_file() {
local file="$1"
local value
value=$(sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' "$file" | awk 'NF { print; exit }')
[[ -n "$value" ]] || fail "malformed Rust toolchain file: $file"
printf '%s\n' "$value"
}

parse_toolchain_from_toml() {
local file="$1"
local value
value=$(sed -nE 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*["'\''`]([^"'\''`]+)["'\''`][[:space:]]*$/\1/p' "$file" | head -n 1)
[[ -n "$value" ]] || fail "malformed Rust toolchain TOML file: $file"
printf '%s\n' "$value"
}

# The upward search for a toolchain file must not leave the APPLICATION's
# repository.
#
# The adoption guide's recommended layout checks the app out into a subdirectory
# of a separate deployer repo. Walking to github.workspace would then cross the
# app's Git boundary and let the *deployer's* `.tool-versions` silently decide
# which Rust compiles the app. The app's Git root is the honest boundary; fall
# back to github.workspace when the app dir is not a Git checkout at all.
#
# Every path here is canonicalized before comparison. The walk below tests the
# boundary with a string equality, so a symlinked TMPDIR or checkout would
# otherwise never match it — and the search would climb straight past the Git
# root it was meant to stop at.
resolve_search_boundary() {
local app_dir="$1" workspace_root="$2"
workspace_root=$(canonical_path "$workspace_root")

local git_root
git_root=$(git -C "$app_dir" rev-parse --show-toplevel 2>/dev/null) || {
printf '%s\n' "$workspace_root"
return
}
git_root=$(canonical_path "$git_root")

# Never search above github.workspace, even if the app's Git root is higher
# (a checkout mounted from outside the workspace).
if is_under "$workspace_root" "$git_root"; then
printf '%s\n' "$git_root"
else
printf '%s\n' "$workspace_root"
fi
}

# Resolve the application toolchain: explicit input > rustup files (walking up to
# the app's Git root) > .tool-versions > the EdgeZero action repo fallback.
resolve_rust_toolchain() {
local input="$1" app_dir="$2" workspace_root="$3" action_root="$4"
if [[ "$input" != "auto" ]]; then
[[ -n "$input" ]] || fail "input 'rust-toolchain' cannot be empty"
printf '%s\n' "$input"
return
fi

# Stop at the application repository boundary, not github.workspace.
local boundary
boundary=$(resolve_search_boundary "$app_dir" "$workspace_root")

local directory value
directory=$(canonical_path "$app_dir")
while true; do
if [[ -f "$directory/rust-toolchain.toml" ]]; then
parse_toolchain_from_toml "$directory/rust-toolchain.toml"
return
fi
if [[ -f "$directory/rust-toolchain" ]]; then
parse_toolchain_from_channel_file "$directory/rust-toolchain"
return
fi
if [[ -f "$directory/.tool-versions" ]] && value=$(read_tool_version "$directory/.tool-versions" rust) && [[ -n "$value" ]]; then
printf '%s\n' "$value"
return
fi
if [[ "$directory" == "$boundary" ]]; then
break
fi
local parent
parent=$(dirname "$directory")
if [[ "$parent" == "$directory" ]]; then
break
fi
directory="$parent"
done

if [[ -f "$action_root/.tool-versions" ]] && value=$(read_tool_version "$action_root/.tool-versions" rust) && [[ -n "$value" ]]; then
printf '%s\n' "$value"
return
fi
fail "could not resolve Rust toolchain; checked rust-toolchain.toml, rust-toolchain, .tool-versions; set input 'rust-toolchain' explicitly"
}

require_linux_x86_64() {
case "$(uname -s)-$(uname -m)" in
Linux-x86_64 | Linux-amd64) ;;
*) fail "build-app-cli supports only Linux x86-64 runners" ;;
esac
}

main() {
local workspace="${GITHUB_WORKSPACE:?GITHUB_WORKSPACE is required}"
local action_root="${EDGEZERO__ACTION__ROOT:?EDGEZERO__ACTION__ROOT is required}"
local cli_package="${EDGEZERO__APP__CLI__PACKAGE:?input 'app-cli-package' is required}"
local cli_bin="${EDGEZERO__APP__CLI__BIN:-}"
local working_directory="${EDGEZERO__PROJECT__WORKING_DIRECTORY:-.}"
local rust_toolchain_input="${EDGEZERO__PROJECT__RUST_TOOLCHAIN:-auto}"
local artifact_name="${EDGEZERO__APP__CLI__ARTIFACT:-edgezero-cli}"

# These directories are `rm -rf`d below, so they must NEVER come from the
# inherited environment — a colliding job-level variable could otherwise point
# them at the checkout. Always derive them from the action-owned temp root.
local runner_temp="${RUNNER_TEMP:-/tmp}"
local stage_root="$runner_temp/edgezero-cli-artifact"
local build_target_dir="$runner_temp/edgezero-cli-build"

require_linux_x86_64
require_cmd cargo
require_cmd rustup
require_cmd jq
require_cmd tar
validate_artifact_name "$artifact_name"

# Resolve the application directory beneath github.workspace.
local workspace_real app_dir
workspace_real=$(canonical_path "$workspace")
[[ -d "$workspace/$working_directory" ]] || fail "working-directory '$working_directory' does not exist or is not a directory"
app_dir=$(canonical_path "$workspace/$working_directory")
is_under "$workspace_real" "$app_dir" || fail "input 'working-directory' must resolve inside github.workspace"

# Install the host toolchain only. The CLI is a native tool; the WASM target
# the *application* needs is installed later by the deploy engine.
local rust_toolchain
rust_toolchain=$(resolve_rust_toolchain "$rust_toolchain_input" "$app_dir" "$workspace_real" "$action_root")
rustup toolchain install "$rust_toolchain" --profile minimal

# Require a committed lockfile and validate the package + binary target.
cd "$app_dir"
local metadata package_json
metadata=$(cargo +"$rust_toolchain" metadata --locked --no-deps --format-version 1) ||
fail "cargo metadata --locked failed; ensure Cargo.lock is present and up to date"
package_json=$(jq -c --arg p "$cli_package" '.packages[] | select(.name == $p)' <<<"$metadata")
[[ -n "$package_json" ]] || fail "app-cli-package '$cli_package' was not found in the application workspace"

[[ -n "$cli_bin" ]] || cli_bin="$cli_package"
local has_bin cli_version
has_bin=$(jq -r --arg b "$cli_bin" '[.targets[] | select(.kind | index("bin")) | .name] | index($b) != null' <<<"$package_json")
[[ "$has_bin" == "true" ]] || fail "app-cli-package '$cli_package' declares no binary target named '$cli_bin'"
cli_version=$(jq -r '.version' <<<"$package_json")

# Build into an action-owned target dir so the checkout stays clean.
reset_owned_dir "$build_target_dir" "$runner_temp"
CARGO_TARGET_DIR="$build_target_dir" cargo +"$rust_toolchain" build \
--locked --release -p "$cli_package" --bin "$cli_bin"

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

# Package the binary and self-describing metadata into a tar.
reset_owned_dir "$stage_root" "$runner_temp"
cp "$bin_path" "$stage_root/$cli_bin"
chmod +x "$stage_root/$cli_bin"
jq -n --arg bin "$cli_bin" --arg version "$cli_version" --arg package "$cli_package" \
'{"app-cli-bin": $bin, "app-cli-version": $version, "app-cli-package": $package}' \
>"$stage_root/app-cli-meta.json"

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

notice "built app CLI '$cli_bin' v$cli_version from package '$cli_package'"
append_output app-cli-version "$cli_version"
append_output app-cli-package "$cli_package"
append_output app-cli-bin "$cli_bin"
append_output app-cli-artifact "$artifact_name"
append_output tarball-path "$tarball"
}

# Sourcing this file exposes its functions without running a build, so the
# contract tests can exercise toolchain resolution directly.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
Loading
Loading