Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b012273
build-app-cli cache: fail-closed image.json digest-pin validator
aram356 Aug 27, 2026
3b5436b
Merge branch 'main' into feature/build-app-cli-cache
aram356 Aug 27, 2026
55dcf07
Move build-caching spec to docs/superpowers/specs (superpowers conven…
aram356 Aug 28, 2026
aac4448
Timestamp the build-caching spec filename to match the superpowers co…
aram356 Aug 28, 2026
599ffef
check-image-pin: reject non-string repository/tag/digest (jq -r coerc…
aram356 Aug 28, 2026
9f7526b
build-caching spec v6.15 + plan: address the v6.14 review's findings
aram356 Aug 28, 2026
e040db4
check-image-pin: require the canonical EdgeZero GHCR repository
aram356 Aug 28, 2026
3e1ae38
build-caching spec v6.16 + plan: address the v6.15 review's contract …
aram356 Aug 28, 2026
3f81105
build-caching spec v6.17 + plan: address the v6.16 review's contract …
aram356 Aug 29, 2026
4cda752
docs: finalize build caching design and plan
aram356 Aug 31, 2026
2ea8c2d
docs: harden build caching release contracts
aram356 Aug 31, 2026
97b2eec
docs: finalize build cache design and plans
aram356 Sep 4, 2026
dbef380
docs: close build cache design review gaps
aram356 Sep 4, 2026
9a778ea
docs: fix cache runner identity contracts
aram356 Sep 5, 2026
6743379
docs: harden cache workflow guard contracts
aram356 Sep 6, 2026
b358974
ci: enforce exact action versions and documentation release gates
aram356 Sep 6, 2026
8f68476
feat: add canonical provenance JSON protocol and schema
aram356 Sep 6, 2026
30b87db
docs: reconcile caching contracts and consolidate deployment docs
aram356 Sep 7, 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
2 changes: 1 addition & 1 deletion .github/actions/build-app-cli/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ runs:
# 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@v7
uses: actions/upload-artifact@v7.0.1
env:
FASTLY_API_TOKEN: ""
FASTLY_SERVICE_ID: ""
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/config-push-fastly/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ runs:
run: exec "$GITHUB_ACTION_PATH/scripts/validate.sh"

- name: Download CLI artifact
uses: actions/download-artifact@v8
uses: actions/download-artifact@v8.0.1
with:
name: ${{ inputs['app-cli-artifact'] }}
path: ${{ steps.ws.outputs.root }}/cli-download
Expand Down
110 changes: 44 additions & 66 deletions .github/actions/deploy-core/tests/check-action-pins.sh
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail

# Verifies every `uses:` reference — across ALL repository workflows and composite
# action metadata — is pinned to a CONCRETE ref: a released VERSION TAG (a major tag
# `@v4`, or a fuller `@v4.3.0`) or a full commit SHA, never a mutable branch/floating
# ref like `@main`, `@develop`, or `@latest`, or an unpinned reference.
#
# This does NOT assert immutability. A version tag — a major tag such as `@v4`
# especially — is repointed by the action's publisher on every release, so it can
# move under you (the tj-actions/changed-files compromise is exactly this). The gate
# enforces the repo's version-tag policy (see .github/zizmor.yml) and a concrete,
# reviewable ref; it rejects refs that move on their own (branches) but not a
# publisher re-tag. Pin to a full commit SHA where cryptographic immutability
# matters. Local (`./...`) refs are exempt; a `docker://` ref must itself be pinned —
# by an `@sha256:` digest or a version tag, never a floating `:latest`/bare image.
# Public action/workflow refs use exact stable vMAJOR.MINOR.PATCH tags. Docker
# actions use lowercase sha256 digests. Tag movement is an accepted release risk.
#
# The `uses` values are extracted STRUCTURALLY with yq, so no YAML spelling — a space
# before the colon, a quoted or unicode-escaped key, a `!!str`-tagged or multiline
Expand All @@ -31,79 +20,68 @@ if ! command -v yq >/dev/null 2>&1 || ! yq --version 2>&1 | grep -qE 'mikefarah/
echo "::error::check-action-pins.sh requires mikefarah yq v4 for structural YAML parsing" >&2
exit 2
fi
if ! command -v jq >/dev/null 2>&1; then
echo "::error::check-action-pins.sh requires jq" >&2
exit 2
fi

# Extract ONLY genuine action references: workflow job-level `uses` (reusable
# workflow calls), workflow step `uses`, and composite-action `runs.steps[].uses`.
# A blanket "any map with a `uses` key" would also reject unrelated fields such as
# `jobs.<job>.env.uses`.
uses_query='[(.jobs[]? | .uses), (.jobs[]? | .steps[]? | .uses), (.runs.steps[]? | .uses)] | .[] | select(. != null)'
# has() preserves explicit nulls. JSON keeps multiline scalars in one record.
uses_query='[(.jobs[]? | select(has("uses")) | .uses), (.jobs[]? | .steps[]? | select(has("uses")) | .uses), (.runs.steps[]? | select(has("uses")) | .uses)]'

files=()
if [[ "$#" -gt 0 ]]; then
files=("$@")
else
while IFS= read -r found; do files+=("$found"); done < <(
# GitHub only reads workflows from .github/workflows (no nesting), so maxdepth 1
# is correct there. Composite/local actions, however, can live ANYWHERE in the
# repo (e.g. tools/deploy/action.yml), so scan action.yml repo-wide — pruning
# build/vendor/VCS trees — rather than only under .github/actions.
find "$REPO_ROOT/.github/workflows" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null
find "$REPO_ROOT" \
\( -path '*/.git' -o -name target -o -name node_modules \) -prune -o \
-type f \( -name 'action.yml' -o -name 'action.yaml' \) -print 2>/dev/null
)
inventory=$(mktemp)
trap 'rm -f "$inventory"' EXIT
# Workflows are direct children; local actions may live anywhere in the repo.
find "$REPO_ROOT/.github/workflows" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print0 >"$inventory"
find "$REPO_ROOT" \
\( -path '*/.git' -o -name target -o -name node_modules \) -prune -o \
-type f \( -name 'action.yml' -o -name 'action.yaml' \) -print0 >>"$inventory"
while IFS= read -r -d '' found; do files+=("$found"); done <"$inventory"
fi

# A full commit SHA, or a release version tag. The tag allows an optional semver
# prerelease AND build-metadata suffix together (v1.2.3-rc.1+build.5), not just one.
# Regexes live in variables (the bash-3.2-safe idiom for `=~`).
sha_re='^[0-9a-fA-F]{40}$'
tag_re='^v?[0-9]+(\.[0-9]+)*(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'
policy='
def valid:
if type != "string" then false
elif test("[\\s\\x00-\\x1f\\x7f]") then false
elif startswith("./") then length > 2
elif startswith("docker://") then
test("^docker://[a-z0-9][a-z0-9._:/-]*@sha256:[0-9a-f]{64}$")
else
test("^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*@v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$")
end;
[.[] | select(valid | not)]'

status=0
refs_seen=0
for file in "${files[@]}"; do
[[ -f "$file" ]] || continue
# Bash 3.2 treats an empty array as unset under nounset.
for file in ${files[@]+"${files[@]}"}; do
if [[ ! -f "$file" ]]; then
echo "::error::missing action metadata or workflow: $file" >&2
status=1
continue
fi
# FAIL CLOSED on a parse/tool failure: if yq cannot read the file, a `2>/dev/null`
# process substitution would yield no refs and the gate would silently pass a file
# it never checked. Capture the output and the exit status instead.
if ! uses_list=$(yq "$uses_query" "$file" 2>/dev/null); then
if ! uses_list=$(yq -o=json -I=0 "$uses_query" "$file" | jq -cse 'if length == 1 and (.[0] | type == "array") then .[0] else error("expected one YAML document") end'); then
echo "::error::could not parse '$file' as YAML — refusing to pass a file the pin gate cannot read" >&2
status=1
continue
fi
while IFS= read -r ref; do
[[ -z "$ref" || "$ref" == "null" ]] && continue
refs_seen=$((refs_seen + 1))
case "$ref" in
./*) continue ;;
docker://*)
# A docker ref is pinned by an `@<algo>:<digest>` (immutable) or a version
# tag; a bare image or a floating `:latest` is rejected like a branch ref.
docker_ref="${ref#docker://}"
if [[ "$docker_ref" == *@*:* ]]; then continue; fi
docker_tag="${docker_ref##*:}"
if [[ "$docker_ref" == *:* && "$docker_tag" != *"/"* && "$docker_tag" =~ $tag_re ]]; then
continue
fi
echo "::error::docker action ref must be pinned by an @<algo>:<digest> or a version tag (floating ':latest'/bare images are not allowed): '$ref' in $file" >&2
status=1
continue
;;
esac
if [[ "$ref" != *@* ]]; then
echo "::error::unpinned action reference (no @ref): '$ref' in $file" >&2
status=1
continue
fi
suffix="${ref##*@}"
if [[ "$suffix" =~ $sha_re || "$suffix" =~ $tag_re ]]; then
:
else
echo "::error::action ref '@$suffix' is neither a full commit SHA nor a release version tag (mutable branch/floating refs are not allowed): '$ref' in $file" >&2
status=1
fi
done <<<"$uses_list"
invalid=$(jq -c "$policy" <<<"$uses_list")
if [[ "$invalid" != '[]' ]]; then
echo "::error::expected an exact stable version tag, local action, or Docker sha256 digest in $file: $invalid" >&2
status=1
fi
count=$(jq '[.[] | select(type == "string") | select(startswith("./") | not)] | length' <<<"$uses_list")
refs_seen=$((refs_seen + count))
done

# A default (whole-repo) scan that finds ZERO action references is not a pass: the
Expand All @@ -112,11 +90,11 @@ done
# green while checking nothing. Fail closed. (An explicit FILE... run may legitimately
# target a file with no refs, so only guard the default scan.)
if [[ "$#" -eq 0 && "$refs_seen" -eq 0 ]]; then
echo "::error::pin gate parsed 0 action references across the repository — expected many; refusing to pass (is yq working?)" >&2
echo "::error::pin gate parsed 0 external action references; refusing a vacuous repository scan" >&2
status=1
fi

if [[ "$status" -eq 0 ]]; then
echo "all action references (repository-wide) are pinned to a concrete ref"
echo "action reference policy passed ($refs_seen external references)"
fi
exit "$status"
Loading