Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/agent-guides/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ required; this fixture never proves live token generation or project association

**Explicit manifests recovery:** Default-off `enable-signed-recovery` requests only the original version alias, with signed promotion and caller pinning required before any legacy write. Verify the original catalogue identity and all original source/version/run claims, restore only authenticated missing versions, treat matching versions as no-ops, refuse contradictory or ambiguous reads, and always preserve `latest`. Keep recovery boundary, interruption/readback controls and native Flux/registry/cryptographic tests in required CI. The native key fixture proves signed claims and registry bytes, not production OIDC. Cross-repository publisher identity migration remains #242; complete recovery rollout remains #425.

The configuration caller guard approves the publisher workflow and both copied helpers by Git
blob and regular-file mode, while retaining the exact workflow identity, full commit reference,
publication opt-ins and artifact target. Releases with unchanged approved contents need no test-pin
edit; changed contents require review before updating the recorded blobs. Missing commits are read
through a disposable object store. The promotion suite exercises unchanged and changed releases,
helper changes, modes and input refusals. This source check does not replace the deployed verifier's
separate approved-revision gate.

**Explicit application recovery:** The same default-off mode verifies both original signatures with all nine paired publication claims before reading both version aliases. Any conflict or unknown read prevents all writes; only missing aliases are restored, with digest readback and matching retries producing no writes. Keep `.github/tests/test-app-recovery.sh` and the native single-platform Buildx/Flux proof required. Never rebuild, re-sign or move `latest` during recovery. Registry writes have no compare-and-set or cross-repository transaction; the shared queue and final readback cannot fence unrelated writers.

**Prebuilt WebApp publication:** Default-off `enable-prebuilt-webapp` requires
Expand Down
54 changes: 54 additions & 0 deletions .github/scripts/check-manifests-caller.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Approve publisher contents, so an unrelated release does not require a pin edit here.
set -euo pipefail
fail() { echo "FAIL: configuration caller: $*" >&2; exit 1; }
[[ "$#" == 1 ]] || fail 'expected one caller workflow'
caller="$1"
encoded_ref="$(yq -o=json -I=0 '.jobs["publish-manifests"].uses // ""' "$caller")" ||
fail 'cannot read publisher reference'
ref_pattern='^"devantler-tech/\.github/\.github/workflows/publish-manifests\.yaml@([0-9a-f]{40})"$'
[[ "$encoded_ref" =~ $ref_pattern ]] || fail 'expected a full immutable publisher reference'
revision="${BASH_REMATCH[1]}"
[[ "$revision" != 0000000000000000000000000000000000000000 ]] ||
fail 'expected a full immutable publisher reference'
yq -e '
(.jobs["publish-manifests"].with["enable-caller-pin"] == true) and
(.jobs["publish-manifests"].with["enable-signed-promotion"] == true) and
(.jobs["publish-manifests"].with["oci-name"] == "devantler-tech/github-config")
' "$caller" >/dev/null || fail 'expected verified-promotion inputs'

# The workflow pins every external action and copies exactly these two source helpers.
# All three files are unchanged between the previously approved v10.0.3 and v12.0.1.
# A new release carrying these contents passes; a workflow/helper change requires review
# and an explicit blob update. File modes are part of the approval too.
reviewed_workflow_blob='ce9a9897b154486218478734a02c61e5ac3ea479'
reviewed_absence_blob='3566e4067e953a1d9cb34e6a50a0f9b72a5d8bc8'
reviewed_recovery_blob='48dc38f60ddebf90b0976758df9f1983bbb86bfd'
source_remote='https://github.com/devantler-tech/.github.git'
object_store=()
read_object() { git --no-replace-objects ${object_store[@]+"${object_store[@]}"} "$@"; }
if ! read_object cat-file -e "$revision" 2>/dev/null; then
scratch="$(mktemp -d)"
trap 'rm -rf "$scratch"' EXIT
git init --quiet --bare "$scratch/source.git"
object_store=(--git-dir "$scratch/source.git")
bounded=()
if command -v timeout >/dev/null 2>&1; then bounded=(timeout 2m); fi
${bounded[@]+"${bounded[@]}"} git --no-replace-objects "${object_store[@]}" \
-c http.lowSpeedLimit=1000 -c http.lowSpeedTime=60 \
fetch --quiet --no-tags --depth=1 --filter=blob:none "$source_remote" "$revision" 2>/dev/null ||
fail 'publisher reference stays unverified: cannot read source commit'
fi
[[ "$(read_object cat-file -t "$revision" 2>/dev/null)" == commit ]] ||
fail 'publisher reference stays unverified: not a commit'
while IFS=' ' read -r expected path; do
actual="$(read_object ls-tree "$revision" -- "$path")" ||
fail "publisher reference stays unverified: cannot read $path"
[[ "$actual" == "100644 blob $expected"$'\t'"$path" ]] ||
fail "unreviewed publisher content at $revision: $path; review its contents and mode before updating the approved blob"
done <<EOF
$reviewed_workflow_blob .github/workflows/publish-manifests.yaml
$reviewed_absence_blob .github/scripts/require-unpublished-version.sh
$reviewed_recovery_blob .github/scripts/recover-manifests-version.sh
EOF
echo 'ok configuration caller pins the reviewed publisher and helper contents with verified promotion'
113 changes: 113 additions & 0 deletions .github/tests/fixtures/manifests-caller-contract.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# Exercise the real caller guard with independent local Git histories.
set -euo pipefail
root="$(cd "$(dirname "$0")/../../.." && pwd)"
guard="$root/.github/scripts/check-manifests-caller.sh"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
fail() { echo "FAIL: manifests caller fixture: $*" >&2; exit 1; }
fixture_git() {
git -C "$work/source" -c user.name=fixture -c user.email=fixture@example.invalid \
-c commit.gpgsign=false -c gc.auto=0 -c maintenance.auto=false "$@"
}
git init --quiet "$work/source"
mkdir -p "$work/source/.github/workflows" "$work/source/.github/scripts"
workflow='.github/workflows/publish-manifests.yaml'
absence='.github/scripts/require-unpublished-version.sh'
recovery='.github/scripts/recover-manifests-version.sh'
for path in "$workflow" "$absence" "$recovery"; do
printf 'independently reviewed fixture: %s\n' "$path" >"$work/source/$path"
fixture_git add -- "$path"
done
fixture_git commit --quiet -m reviewed
reviewed="$(fixture_git rev-parse HEAD)"
workflow_blob="$(fixture_git rev-parse "HEAD:$workflow")"
absence_blob="$(fixture_git rev-parse "HEAD:$absence")"
recovery_blob="$(fixture_git rev-parse "HEAD:$recovery")"
printf 'unrelated release\n' >"$work/source/RELEASE"
fixture_git add -- RELEASE
fixture_git commit --quiet -m release
release="$(fixture_git rev-parse HEAD)"
for path in "$workflow" "$absence" "$recovery"; do
printf 'unreviewed change\n' >>"$work/source/$path"
fixture_git add -- "$path"
fixture_git commit --quiet -m changed
case "$path" in
"$workflow") changed_workflow="$(fixture_git rev-parse HEAD)" ;;
"$absence") changed_absence="$(fixture_git rev-parse HEAD)" ;;
"$recovery") changed_recovery="$(fixture_git rev-parse HEAD)" ;;
esac
fixture_git checkout --quiet "$release" -- "$path"
fixture_git commit --quiet -m restore
done
fixture_git update-index --chmod=+x "$workflow"
fixture_git commit --quiet -m mode
changed_mode="$(fixture_git rev-parse HEAD)"
fixture_git config uploadpack.allowFilter true
fixture_git config uploadpack.allowAnySHA1InWant true
make_guard() {
sed -e "s|^reviewed_workflow_blob='.*'$|reviewed_workflow_blob='$workflow_blob'|" \
-e "s|^reviewed_absence_blob='.*'$|reviewed_absence_blob='$absence_blob'|" \
-e "s|^reviewed_recovery_blob='.*'$|reviewed_recovery_blob='$recovery_blob'|" \
-e "s|^source_remote='.*'$|source_remote='$1'|" "$guard" >"$2"
}
make_guard "$work/unavailable" "$work/offline.sh"
make_guard "file://$work/source" "$work/fetching.sh"
yq -o=json '.' "$root/.github/workflows/cd.yaml" >"$work/baseline.json"
prefix='devantler-tech/.github/.github/workflows/publish-manifests.yaml'
mutate() {
jq --arg prefix "$prefix" --arg reviewed "$reviewed" --arg release "$release" \
--arg workflow "$changed_workflow" --arg absence "$changed_absence" \
--arg recovery "$changed_recovery" --arg mode "$changed_mode" \
'.jobs["publish-manifests"].uses = ($prefix + "@" + $reviewed) | '"$1" \
"$work/baseline.json" >"$work/caller.json"
}
run_guard() { (cd "$2"; bash "$1" "$work/caller.json"); }
for revision in reviewed release; do
mutate ".jobs[\"publish-manifests\"].uses = (\$prefix + \"@\" + \$$revision)"
run_guard "$work/offline.sh" "$work/source" >"$work/result" 2>&1 || {
cat "$work/result" >&2
fail "rejected $revision content"
}
done
git init --quiet "$work/consumer"
# shellcheck disable=SC2016 # jq variables, not shell expansion.
mutate '.jobs["publish-manifests"].uses = ($prefix + "@" + $release)'
run_guard "$work/fetching.sh" "$work/consumer" >"$work/result" 2>&1 || {
cat "$work/result" >&2
fail 'rejected fetched unchanged release'
}
[[ ! -e "$work/consumer/.git/shallow" && ! -s "$work/consumer/.git/FETCH_HEAD" &&
-z "$(git -C "$work/consumer" remote)" ]] || fail 'fetch changed consumer Git state'
while IFS=$'\t' read -r label mutation diagnostic; do
mutate "$mutation"
if run_guard "$work/offline.sh" "$work/source" >"$work/result" 2>&1; then
fail "accepted $label"
fi
grep -qF -- "$diagnostic" "$work/result" || {
cat "$work/result" >&2
fail "$label failed for the wrong reason"
}
done <<'CASES'
mutable ref .jobs["publish-manifests"].uses=$prefix+"@main" full immutable publisher reference
short ref .jobs["publish-manifests"].uses=$prefix+"@"+$reviewed[0:12] full immutable publisher reference
zero ref .jobs["publish-manifests"].uses=$prefix+"@0000000000000000000000000000000000000000" full immutable publisher reference
trailing newline .jobs["publish-manifests"].uses=$prefix+"@"+$reviewed+"\n" full immutable publisher reference
lookalike owner .jobs["publish-manifests"].uses="outside/.github/.github/workflows/publish-manifests.yaml@"+$reviewed full immutable publisher reference
lookalike path .jobs["publish-manifests"].uses="devantler-tech/.github/.github/workflows/publish-manifestsXyaml@"+$reviewed full immutable publisher reference
missing pin verification del(.jobs["publish-manifests"].with["enable-caller-pin"]) verified-promotion inputs
disabled promotion .jobs["publish-manifests"].with["enable-signed-promotion"]=false verified-promotion inputs
wrong artifact .jobs["publish-manifests"].with["oci-name"]="devantler-tech/other" verified-promotion inputs
changed workflow .jobs["publish-manifests"].uses=$prefix+"@"+$workflow unreviewed publisher content
changed absence helper .jobs["publish-manifests"].uses=$prefix+"@"+$absence unreviewed publisher content
changed recovery helper .jobs["publish-manifests"].uses=$prefix+"@"+$recovery unreviewed publisher content
changed file mode .jobs["publish-manifests"].uses=$prefix+"@"+$mode unreviewed publisher content
unreadable revision .jobs["publish-manifests"].uses=$prefix+"@0123456789abcdef0123456789abcdef01234567" reference stays unverified
CASES
# shellcheck disable=SC2016 # jq variables, not shell expansion.
mutate '.jobs["publish-manifests"].uses=$prefix+"@"+$absence'
if run_guard "$work/fetching.sh" "$work/consumer" >"$work/result" 2>&1; then
fail 'accepted fetched changed helper'
fi
grep -qF 'unreviewed publisher content' "$work/result" || fail 'fetched content refusal had the wrong cause'
echo 'ok caller accepts unchanged releases and refuses unreviewed workflow/helper bytes, modes and unsafe inputs'
9 changes: 2 additions & 7 deletions .github/tests/test-publish-manifests-promotion.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,13 @@
set -euo pipefail
# The live configuration caller must opt in only through a SHA-pinned publisher.
validate_caller() {
yq -e '
(.jobs["publish-manifests"].uses ==
"devantler-tech/.github/.github/workflows/publish-manifests.yaml@0f390329b590404c02565b27a263ee841fa02bce") and
(.jobs["publish-manifests"].with["enable-caller-pin"] == true) and
(.jobs["publish-manifests"].with["enable-signed-promotion"] == true) and
(.jobs["publish-manifests"].with["oci-name"] == "devantler-tech/github-config")
' "$1" >/dev/null
bash .github/scripts/check-manifests-caller.sh "$1"
}
validate_caller .github/workflows/cd.yaml || {
echo 'FAIL: configuration publication requires a SHA-pinned, verified-promotion caller' >&2
exit 1
}
bash .github/tests/fixtures/manifests-caller-contract.sh
scratch="$(mktemp -d)"
trap 'rm -rf "$scratch"' EXIT
mkdir -p "$scratch/bin"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
# ecosystem, no cooldown for devantler-tech/*).
# Keep the ref a 40-hex commit: it is part of the cosign subject the platform
# verifies, so its shape is load-bearing here.
uses: devantler-tech/.github/.github/workflows/publish-manifests.yaml@0f390329b590404c02565b27a263ee841fa02bce # v10.0.3
uses: devantler-tech/.github/.github/workflows/publish-manifests.yaml@6f1dee9557a939cc24ca2c562ea775d05e01564d # v12.0.1
with:
oci-name: devantler-tech/github-config
# Without this, a ref the platform verifier does not accept fails nothing
Expand Down
Loading