Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/actions/build-app-cli/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ runs:
EDGEZERO__PROJECT__WORKING_DIRECTORY: ${{ inputs['working-directory'] }}
EDGEZERO__PROJECT__RUST_TOOLCHAIN: ${{ inputs['rust-toolchain'] }}
EDGEZERO__APP__CLI__ARTIFACT: ${{ inputs['app-cli-artifact'] }}
# setup-rust-build-cache publishes this through GITHUB_ENV only when its
# opt-in target cache is enabled. build-app-cli confines it to RUNNER_TEMP.
# setup-rust-build-cache always publishes this stable build directory,
# independently of target restore/save. Confine it to RUNNER_TEMP.
EDGEZERO__RUST_CACHE__TARGET_DIR: ${{ env.EDGEZERO__RUST_CACHE__TARGET_DIR }}
# This step runs app-controlled code, so it emits NO outputs through the
# runner's GITHUB_OUTPUT (blanked below). It collects them into this
Expand Down
7 changes: 4 additions & 3 deletions .github/actions/build-app-cli/scripts/build-app-cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
set -euo pipefail

# Compiles the CLI package the APPLICATION provides — a crate in the app's own
# workspace — into an application-scoped cached CARGO_TARGET_DIR when configured,
# or an action-owned scratch directory otherwise. It then packages the binary
# workspace — into the shared job-local target provided by cache setup (even
# without remote target caching), or fresh action-owned scratch when setup is
# absent. It 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.
#
Expand All @@ -18,7 +19,7 @@ set -euo pipefail
# RUNNER_TEMP optional action-owned scratch root (default: /tmp)
# EDGEZERO__PROVIDER__ENV_CLEAR optional JSON array of provider aliases to scrub before the build (default: [])
# EDGEZERO__ACTION__WORKSPACE optional per-invocation scratch root (default: mktemp under RUNNER_TEMP)
# EDGEZERO__RUST_CACHE__TARGET_DIR optional shared Cargo target directory beneath RUNNER_TEMP
# EDGEZERO__RUST_CACHE__TARGET_DIR optional shared job-local target beneath RUNNER_TEMP, independent of remote cache mode
# EDGEZERO__ACTION__OUTPUT_FILE optional outputs handoff file, started fresh (default: GITHUB_OUTPUT)
# Writes (outputs):
# app-cli-package the package that was built
Expand Down
18 changes: 18 additions & 0 deletions .github/actions/deploy-core/tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1991,6 +1991,13 @@ test_action_output_contracts() {

script=$(awk -v want="$step_id" '$1 == want { $1 = ""; sub(/^ /, ""); print; exit }' <<<"$scripts")
if [[ -z "$script" ]]; then
# actions/cache declares cache-hit itself, not through a local script.
# Ref pinning is enforced separately; accept only this documented output.
if [[ "$out_name" == cache-hit ]] && command -v yq >/dev/null 2>&1; then
local output_action
output_action=$(STEP_ID="$step_id" yq -r '.runs.steps[] | select(.id == strenv(STEP_ID)) | .uses' "$action")
case "${output_action%@*}" in actions/cache | actions/cache/restore) continue ;; esac
fi
fail "$name_of output '$out_name' names step '$step_id', which runs no script"
missing=$((missing + 1))
continue
Expand Down Expand Up @@ -2292,6 +2299,17 @@ test_fastly_smoke_release_contract() {
grep -Fq './.github/actions/setup-rust-build-cache' <<<"$cache_restore"
assert_succeeds "cache restore verifies a marker created by the seed job" \
grep -Fq 'restore-marker' <<<"$cache_restore"
local cache_call missing_revisions revision_smoke
cache_call='.jobs[] | .steps[]? | select(.uses == "./.github/actions/setup-rust-build-cache")'
missing_revisions=$(yq eval -r "$cache_call | select(.with.\"cache-target\" == true and .with.\"target-cache-revision\" == null) | .name" "$workflow")
assert_equals "all target cache callers supply their source revision" "" "$missing_revisions"
revision_smoke=$(yq eval -r '.jobs."cache-new-revision-smoke"' "$workflow")
assert_succeeds "new-revision smoke checks target artifact isolation" \
grep -Fq 'target-cache-hit' <<<"$revision_smoke"
assert_succeeds "new-revision smoke checks source cache reuse" \
grep -Fq 'source-cache-hit' <<<"$revision_smoke"
assert_succeeds "new-revision smoke checks compiler reuse for both targets" \
grep -Fq 'assert-compiler-reuse.sh' <<<"$revision_smoke"
for obsolete in cache-smoke-capture.sh cache-smoke-assert.sh cache-smoke-assert-no-restore.sh; do
assert_fails "obsolete deploy-time cache helper is removed ($obsolete)" \
test -e "$ACTIONS_DIR/deploy-core/tests/$obsolete"
Expand Down
5 changes: 4 additions & 1 deletion .github/actions/deploy-fastly/tests/make-smoke-fixture.sh
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,11 @@ source = "fixture.wasm"
TOML

cargo generate-lockfile
# Path dependencies live in the enclosing EdgeZero checkout, not this Git repo.
git -C "$workspace" rev-parse HEAD >.edgezero-source-revision
git add -A
git commit -q -m fixture
GIT_AUTHOR_DATE=2000-01-01T00:00:00Z GIT_COMMITTER_DATE=2000-01-01T00:00:00Z \
git -c core.hooksPath=/dev/null -c commit.gpgsign=false commit -q -m fixture
}

main() {
Expand Down
52 changes: 44 additions & 8 deletions .github/actions/setup-rust-build-cache/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,33 @@ inputs:
required: false
default: .
cache-target:
description: Restore and save the application's Cargo target directory in addition to compiler outputs.
description: Restore the application's revision-specific Cargo target directory in addition to compiler outputs.
required: false
default: "false"
save-target:
description: Opt in to saving a missed target cache after a successful job; requires cache-target true. Prefer writes from trusted main workflows.
required: false
default: "false"
target-cache-revision:
description: Full lowercase application source Git SHA; required when cache-target is true. Never use a deployment repository SHA for different application source.
required: false

outputs:
source-cache-hit:
description: Whether the dependency source cache matched its exact primary key (false also includes prefix reuse).
value: ${{ steps.cargo-sources.outputs.cache-hit == 'true' }}
target-cache-hit:
description: Whether target artifacts matched the revision-specific key; false when target caching is disabled or misses.
value: ${{ steps.cargo-target.outputs.cache-hit == 'true' || steps.cargo-target-save.outputs.cache-hit == 'true' }}
source-cache-key:
description: Dependency source cache primary key.
value: ${{ steps.prepare.outputs['source-cache-key'] }}
target-cache-key:
description: Revision-specific target cache key; empty if no revision was supplied.
value: ${{ steps.prepare.outputs['target-cache-key'] }}
cargo-target-dir:
description: Stable confined directory used by later native and WASM builds, even when target caching is disabled.
value: ${{ steps.prepare.outputs['cargo-target-dir'] }}

runs:
using: composite
Expand All @@ -23,33 +47,45 @@ runs:
env:
EDGEZERO__RUST_CACHE__APP_NAME: ${{ inputs['app-name'] }}
EDGEZERO__RUST_CACHE__CACHE_TARGET: ${{ inputs['cache-target'] }}
EDGEZERO__RUST_CACHE__SAVE_TARGET: ${{ inputs['save-target'] }}
EDGEZERO__RUST_CACHE__TARGET_CACHE_REVISION: ${{ inputs['target-cache-revision'] }}
EDGEZERO__RUST_CACHE__WORKING_DIRECTORY: ${{ inputs['working-directory'] }}
BASH_ENV: ""
ENV: ""
run: exec "$GITHUB_ACTION_PATH/scripts/prepare.sh"

- name: Restore Cargo dependency sources
id: cargo-sources
if: steps.prepare.outputs.already-initialized != 'true'
uses: actions/cache@v5
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: edgezero-app-${{ steps.prepare.outputs['cache-namespace'] }}-rust-sources-${{ runner.os }}-${{ runner.arch }}-${{ steps.prepare.outputs['cargo-lock-sha256'] }}
restore-keys: |
edgezero-app-${{ steps.prepare.outputs['cache-namespace'] }}-rust-sources-${{ runner.os }}-${{ runner.arch }}-
key: ${{ steps.prepare.outputs['source-cache-key'] }}
restore-keys: ${{ steps.prepare.outputs['source-cache-restore-prefix'] }}

- name: Restore Cargo target artifacts
if: inputs['cache-target'] == 'true'
id: cargo-target
if: inputs['cache-target'] == 'true' && inputs['save-target'] == 'false' && steps.prepare.outputs.already-initialized != 'true'
uses: actions/cache/restore@v5
with:
path: ${{ steps.prepare.outputs['cargo-target-dir'] }}
key: ${{ steps.prepare.outputs['target-cache-key'] }}

# The combined action saves in its success-only post-job hook, after builds.
- name: Restore Cargo targets and enable post-job save
id: cargo-target-save
if: inputs['cache-target'] == 'true' && inputs['save-target'] == 'true' && steps.prepare.outputs.already-initialized != 'true'
uses: actions/cache@v5
with:
path: ${{ steps.prepare.outputs['cargo-target-dir'] }}
key: edgezero-app-${{ steps.prepare.outputs['cache-namespace'] }}-rust-target-${{ runner.os }}-${{ runner.arch }}-${{ steps.prepare.outputs['cargo-lock-sha256'] }}
restore-keys: |
edgezero-app-${{ steps.prepare.outputs['cache-namespace'] }}-rust-target-${{ runner.os }}-${{ runner.arch }}-${{ steps.prepare.outputs['cargo-lock-sha256'] }}-
key: ${{ steps.prepare.outputs['target-cache-key'] }}

- name: Set up Rust compiler cache
id: sccache
if: steps.prepare.outputs.already-initialized != 'true'
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.16.0
110 changes: 83 additions & 27 deletions .github/actions/setup-rust-build-cache/scripts/prepare.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ set -euo pipefail
# Validates the application and Rust workspace selected by
# setup-rust-build-cache, binds its cache keys to the application and Cargo.lock
# bytes, and enables sccache for every later Rust build in the current GitHub
# Actions job. When target caching is enabled, it also publishes one confined,
# application-scoped CARGO_TARGET_DIR beneath RUNNER_TEMP.
# Actions job. Always publishes a stable, confined, application-scoped
# CARGO_TARGET_DIR. Remote target artifacts are isolated by source revision.
#
# This script reads no application manifest and runs no application code. The
# action therefore remains independent of the package, adapter, and deployment
Expand All @@ -18,7 +18,18 @@ fail() {

canonical_path() {
local path="$1"
realpath "$path" 2>/dev/null || fail "could not resolve path '$path'"
[[ ! "$path" =~ [[:cntrl:]] ]] || fail 'Rust cache paths must not contain control characters'
local resolved
# Preserve trailing newlines until validation, including in symlink targets.
resolved=$(realpath "$path" 2>/dev/null && printf '.') || fail "could not resolve path '$path'"
resolved=${resolved%.}
resolved=${resolved%$'\n'}
[[ ! "$resolved" =~ [[:cntrl:]] ]] || fail 'Resolved Rust cache paths must not contain control characters'
printf '%s' "$resolved"
}

append_output() {
printf '%s=%s\n' "$1" "$2" >>"${GITHUB_OUTPUT:?}"
}

is_under() {
Expand All @@ -40,10 +51,12 @@ sha256_file() {

main() {
local workspace="${GITHUB_WORKSPACE:?GITHUB_WORKSPACE is required}"
local output_file="${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}"
: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}"
local env_file="${GITHUB_ENV:?GITHUB_ENV is required}"
local app_name="${EDGEZERO__RUST_CACHE__APP_NAME-}"
local cache_target="${EDGEZERO__RUST_CACHE__CACHE_TARGET-false}"
local save_target="${EDGEZERO__RUST_CACHE__SAVE_TARGET-false}"
local revision="${EDGEZERO__RUST_CACHE__TARGET_CACHE_REVISION-}"
local working_directory="${EDGEZERO__RUST_CACHE__WORKING_DIRECTORY-}"

[[ "$app_name" =~ ^[a-z0-9][a-z0-9._-]{0,63}$ ]] ||
Expand All @@ -52,6 +65,16 @@ main() {
true | false) ;;
*) fail "input 'cache-target' must be 'true' or 'false'" ;;
esac
case "$save_target" in
true | false) ;;
*) fail "input 'save-target' must be 'true' or 'false'" ;;
esac
[[ "$save_target" != true || "$cache_target" == true ]] ||
fail "input 'save-target' requires cache-target true"
if [[ "$cache_target" == true || -n "$revision" ]]; then
[[ "$revision" =~ ^[0-9a-f]{40}$ || "$revision" =~ ^[0-9a-f]{64}$ ]] ||
fail "input 'target-cache-revision' must be a full lowercase Git SHA (required when cache-target is true)"
fi
[[ -n "$working_directory" ]] ||
fail "input 'working-directory' cannot be empty"
[[ "$working_directory" != /* ]] ||
Expand All @@ -76,34 +99,67 @@ main() {
[[ "$lock_sha" =~ ^[0-9a-f]{64}$ ]] ||
fail "could not compute a lowercase SHA-256 for Cargo.lock"

{
printf 'cache-namespace=%s\n' "$app_name"
printf 'cargo-lock-sha256=%s\n' "$lock_sha"
} >>"$output_file"
local runner_temp="${RUNNER_TEMP:?RUNNER_TEMP is required}"
[[ -d "$runner_temp" ]] || fail "RUNNER_TEMP does not exist or is not a directory"
local runner_temp_real cache_root app_dir target_dir selection_file path
runner_temp_real=$(canonical_path "$runner_temp")
cache_root="$runner_temp_real/edgezero-rust-cache"
app_dir="$cache_root/$app_name"
target_dir="$app_dir/target"
selection_file="$app_dir/selection"
for path in "$cache_root" "$app_dir" "$target_dir" "$selection_file"; do
[[ ! -L "$path" ]] || fail "Rust cache path must not be a symlink: '$path'"
done
for path in "$cache_root" "$app_dir" "$target_dir"; do
[[ ! -e "$path" || -d "$path" ]] || fail "Rust cache path is not a directory: '$path'"
done
[[ ! -e "$selection_file" || -f "$selection_file" ]] || fail "Rust cache selection is not a regular file"

local ownership_revision="$revision" selection already_initialized=false
if [[ -z "$ownership_revision" ]]; then
ownership_revision=$(git -C "$project_real" rev-parse --verify HEAD 2>/dev/null || true)
[[ "$ownership_revision" =~ ^[0-9a-f]{40}$ || "$ownership_revision" =~ ^[0-9a-f]{64}$ ]] || ownership_revision=''
fi
selection=$(printf '%s\n' "$project_real" "${ownership_revision:-uncached}" "$lock_sha" "$cache_target" "$save_target")
if [[ -e "$selection_file" ]]; then
[[ -n "$ownership_revision" ]] || fail "repeated non-Git cache setup requires target-cache-revision"
[[ "$(<"$selection_file")" == "$selection" ]] ||
fail "Rust target directory already belongs to a different workspace, revision, lockfile or cache settings"
already_initialized=true
elif [[ -d "$target_dir" ]]; then
local target_contents
# Quote control characters so newline-only names survive command substitution.
target_contents=$(ls -Aq -- "$target_dir") || fail "could not inspect Rust target directory"
[[ -z "$target_contents" ]] ||
fail "Rust target directory contains unowned artifacts without a selection marker"
fi

local runner_os="${RUNNER_OS:?RUNNER_OS is required}" runner_arch="${RUNNER_ARCH:?RUNNER_ARCH is required}"
[[ "$runner_os" =~ ^[a-zA-Z0-9_-]+$ && "$runner_arch" =~ ^[a-zA-Z0-9_-]+$ ]] ||
fail "runner OS and architecture must be safe cache-key components"
local source_prefix source_key target_key=''
source_prefix="edgezero-app-$app_name-rust-sources-$runner_os-$runner_arch-"
source_key="$source_prefix$lock_sha"
[[ -z "$revision" ]] || target_key="edgezero-app-$app_name-rust-target-v2-$runner_os-$runner_arch-$lock_sha-$revision-exact"

mkdir -p "$target_dir"
target_dir=$(canonical_path "$target_dir")
is_under "$runner_temp_real" "$target_dir" ||
fail "Cargo target cache directory must resolve inside RUNNER_TEMP"
printf '%s\n' "$selection" >"$selection_file"

append_output already-initialized "$already_initialized"
append_output source-cache-key "$source_key"
append_output source-cache-restore-prefix "$source_prefix"
append_output target-cache-key "$target_key"
append_output cargo-target-dir "$target_dir"
{
printf 'RUSTC_WRAPPER=sccache\n'
printf 'SCCACHE_GHA_ENABLED=true\n'
printf 'CARGO_INCREMENTAL=0\n'
printf 'CARGO_TARGET_DIR=%s\n' "$target_dir"
printf 'EDGEZERO__RUST_CACHE__TARGET_DIR=%s\n' "$target_dir"
} >>"$env_file"

if [[ "$cache_target" == "true" ]]; then
local runner_temp="${RUNNER_TEMP:?RUNNER_TEMP is required when cache-target is true}"
[[ -d "$runner_temp" ]] || fail "RUNNER_TEMP does not exist or is not a directory"

local runner_temp_real target_dir
runner_temp_real=$(canonical_path "$runner_temp")
target_dir="$runner_temp_real/edgezero-rust-cache/$app_name/target"
mkdir -p "$target_dir"
target_dir=$(canonical_path "$target_dir")
is_under "$runner_temp_real" "$target_dir" ||
fail "Cargo target cache directory must resolve inside RUNNER_TEMP"

printf 'cargo-target-dir=%s\n' "$target_dir" >>"$output_file"
{
printf 'CARGO_TARGET_DIR=%s\n' "$target_dir"
printf 'EDGEZERO__RUST_CACHE__TARGET_DIR=%s\n' "$target_dir"
} >>"$env_file"
fi
}

main "$@"
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail

# Require native/WASM hits on the first builds of a fresh revision-specific
# target directory. Only a previous job may have populated compiler outputs.
fixture="${GITHUB_WORKSPACE:?}/cache-restore-fixture"
target_dir="${CARGO_TARGET_DIR:?}"
expected="$(realpath "${RUNNER_TEMP:?}")/edgezero-rust-cache/cache-restore-smoke-v2/target"
[[ "$(realpath "$target_dir")" == "$expected" ]] || {
printf '::error::compiler probe must use its confined fixture target directory\n' >&2
exit 1
}
cd "$fixture"
host=$(rustc -vV | sed -n 's/^host: //p')
[[ -n "$host" ]] || exit 1
for target in "$host" wasm32-wasip1; do
[[ ! -e "$target_dir/$target/debug" ]] || {
printf '::error::compiler probe target must be fresh before its first build\n' >&2
exit 1
}
sccache --zero-stats
cargo build --locked --lib --target "$target"
stats=$(sccache --show-stats --stats-format json)
printf 'Compiler reuse for %s: %s\n' "$target" "$stats"
jq -e '(.stats.cache_hits.counts.Rust // 0) > 0 and (.stats.cache_errors.counts.Rust // 0) == 0 and (.stats.cache_read_errors // 0) == 0 and (.stats.cache_write_errors // 0) == 0' <<<"$stats" >/dev/null || {
printf '::error::expected error-free Rust compiler-cache reuse on the first build for %s\n' "$target" >&2
exit 1
}
done
4 changes: 4 additions & 0 deletions .github/actions/setup-rust-build-cache/tests/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# Shared assertions for local cache contracts; callers own fixtures and cleanup.
fail() { printf 'Rust cache test failed: %s\n' "$*" >&2; exit 1; }
assert_line() { grep -Fqx -- "$2" "$1" || fail "expected '$2' in $1"; }
Loading
Loading