generated from mathias/template-go-web
fix(ci): gate build and deploy behind a committed .deploy-enabled marker
Every push to main since the first substituted commit was red at "Update image tag in infra repo": sed: can't read k3s/apps/spatial-agent-app/ deployment.yaml. mathias/infra has no such manifest and the repo is a hello-world MVP that should not deploy yet. Port of doodle 32acff8/006c108/3a95cc9: guard outputs deploy_enabled from scripts/deploy-enabled.sh (regular file, not symlink); build and deploy require it to equal 'true', so a missing output fails closed. check and the template guard are unchanged. No marker is committed. Wiring test seen red on the old cd.yml (no gate), green after; removing the clause from build.if or deploy.if makes it fail. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZkhi49hfeSqBp5aZJNXsM
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for the deploy_enabled wiring in .gitea/workflows/cd.yml (#3, #15).
|
||||
#
|
||||
# scripts/tests/deploy-enabled_test.sh proves the script answers correctly. It
|
||||
# cannot see the workflow, so deleting the clause that makes build and deploy
|
||||
# depend on that answer left every test green and brought back the original
|
||||
# failure (pr-gatekeeper, PR #14, round 1).
|
||||
#
|
||||
# Two parts:
|
||||
# 1. check_wiring: assertions on the TEXT of cd.yml. They prove the clauses
|
||||
# are present, not that Gitea evaluates them; a run read by job proves that.
|
||||
# 2. mutation cases: copies of cd.yml with one clause broken, each of which
|
||||
# check_wiring must reject. A check that has never been seen to fail on a
|
||||
# broken file is not evidence (#15 lists the breakages an earlier version
|
||||
# let through).
|
||||
#
|
||||
# Plain awk/grep/sed: the CI runner is not known to have a YAML library.
|
||||
#
|
||||
# Usage: ./scripts/tests/cd-wiring_test.sh [path/to/cd.yml]
|
||||
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
WF="${1:-$ROOT/.gitea/workflows/cd.yml}"
|
||||
|
||||
pass=0; fail=0
|
||||
ok() { printf ' ok %s\n' "$1"; pass=$((pass+1)); }
|
||||
bad() { printf ' FAIL %s\n %s\n' "$1" "${2:-}"; fail=$((fail+1)); }
|
||||
|
||||
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
# job_block <file> <job>: the lines from " <job>:" up to the next top-level job key.
|
||||
job_block() {
|
||||
awk -v job="$2" '
|
||||
$0 ~ "^ "job":" { on=1; next }
|
||||
on && /^ [A-Za-z0-9_-]+:/ { exit }
|
||||
on { print }
|
||||
' "$1"
|
||||
}
|
||||
|
||||
# check_wiring <file>: prints " ok ..." / " FAIL ..." lines; the caller counts.
|
||||
check_wiring() {
|
||||
local f="$1" job
|
||||
for job in guard check build deploy; do
|
||||
[ -n "$(job_block "$f" "$job")" ] && printf ' ok job %s is found\n' "$job" \
|
||||
|| printf ' FAIL job %s is found\n' "$job"
|
||||
done
|
||||
|
||||
local guard out_line step_id
|
||||
guard="$(job_block "$f" guard)"
|
||||
out_line="$(printf '%s\n' "$guard" | grep -E '^ deploy_enabled: ' || true)"
|
||||
printf '%s\n' "$out_line" | grep -qE '^ deploy_enabled: \$\{\{ steps\.[a-z_]+\.outputs\.deploy_enabled \}\}' \
|
||||
&& echo " ok guard declares a deploy_enabled output" \
|
||||
|| echo " FAIL guard declares a deploy_enabled output"
|
||||
|
||||
# The output must point at a step that exists in the guard job. A typo in the
|
||||
# id leaves the output empty, which fails closed but silently.
|
||||
step_id="$(printf '%s\n' "$out_line" | sed -nE 's/.*steps\.([a-z_]+)\.outputs\..*/\1/p')"
|
||||
if [ -n "$step_id" ] && printf '%s\n' "$guard" | grep -qE "^ - id: $step_id\$"; then
|
||||
echo " ok the output's step id '$step_id' exists in the guard job"
|
||||
else
|
||||
echo " FAIL the output's step id '${step_id:-?}' exists in the guard job"
|
||||
fi
|
||||
|
||||
# The clause is read from the if: line with any trailing comment removed, and
|
||||
# must be AND-ed in: a clause that lives only in a comment, or behind ||, does
|
||||
# not gate anything.
|
||||
local cond
|
||||
for job in build deploy; do
|
||||
cond="$(job_block "$f" "$job" | grep -E '^ if:' | sed -E 's/[[:space:]]+#.*$//' || true)"
|
||||
if printf '%s\n' "$cond" | grep -qF "&& needs.guard.outputs.deploy_enabled == 'true' &&" \
|
||||
&& ! printf '%s\n' "$cond" | grep -qF '||'; then
|
||||
echo " ok $job.if requires deploy_enabled == 'true'"
|
||||
else
|
||||
echo " FAIL $job.if requires deploy_enabled == 'true'"
|
||||
fi
|
||||
done
|
||||
|
||||
printf '%s\n' "$guard" | grep -qE '^ +set -euo pipefail$' \
|
||||
&& echo " ok the guard step runs under set -euo pipefail" \
|
||||
|| echo " FAIL the guard step runs under set -euo pipefail"
|
||||
|
||||
job_block "$f" check | grep -q 'deploy_enabled' \
|
||||
&& echo " FAIL check.if does not mention deploy_enabled" \
|
||||
|| echo " ok check.if does not mention deploy_enabled"
|
||||
|
||||
if job_block "$f" guard | grep -qE 'echo .*\$\(.*deploy-enabled\.sh'; then
|
||||
echo " FAIL the guard step does not call deploy-enabled.sh inside echo"
|
||||
else
|
||||
echo " ok the guard step does not call deploy-enabled.sh inside echo"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "cd.yml wiring tests"
|
||||
if [ ! -f "$WF" ]; then bad "workflow exists" "$WF missing"; echo; echo "$pass passed, $fail failed"; exit 1; fi
|
||||
|
||||
real="$(check_wiring "$WF")"
|
||||
printf '%s\n' "$real"
|
||||
pass=$((pass + $(printf '%s\n' "$real" | grep -c '^ ok')))
|
||||
fail=$((fail + $(printf '%s\n' "$real" | grep -c '^ FAIL')))
|
||||
|
||||
# mutant <name> <substring check_wiring must report as FAIL> <sed expression>
|
||||
mutant() {
|
||||
local name="$1" want="$2" expr="$3" out
|
||||
sed -E "$expr" "$WF" > "$tmp/mutant.yml"
|
||||
if cmp -s "$WF" "$tmp/mutant.yml"; then
|
||||
bad "mutation: $name" "the sed expression changed nothing; the mutation no longer applies to cd.yml"; return
|
||||
fi
|
||||
out="$(check_wiring "$tmp/mutant.yml")"
|
||||
if printf '%s\n' "$out" | grep -F ' FAIL' | grep -qF "$want"; then ok "rejects: $name"
|
||||
else bad "rejects: $name" "expected a FAIL naming '$want'"; fi
|
||||
}
|
||||
|
||||
BUILD_CLAUSE="needs.guard.outputs.deploy_enabled == 'true' && github.event_name != 'pull_request'"
|
||||
mutant "clause only in a trailing comment on build.if" "build.if" \
|
||||
"s/ && $BUILD_CLAUSE\$/ \&\& github.event_name != 'pull_request' # needs.guard.outputs.deploy_enabled == 'true'/"
|
||||
mutant "|| in place of && before the clause on build.if" "build.if" \
|
||||
"s/is_template != 'true' && $BUILD_CLAUSE/is_template != 'true' || $BUILD_CLAUSE/"
|
||||
mutant "step id mistyped in the guard output" "step id" \
|
||||
"s/steps\.deploy\.outputs\.deploy_enabled/steps.deplyo.outputs.deploy_enabled/"
|
||||
mutant "set -euo pipefail removed from the guard step" "set -euo pipefail" \
|
||||
"/^ set -euo pipefail\$/d"
|
||||
mutant "a job named deploy2 leaks into build's block" "build.if" \
|
||||
"s/ && $BUILD_CLAUSE\$/ \&\& github.event_name != 'pull_request'/; s/^ deploy:\$/ deploy2:/"
|
||||
|
||||
echo
|
||||
echo "$pass passed, $fail failed"
|
||||
[ "$fail" -eq 0 ]
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for scripts/deploy-enabled.sh
|
||||
#
|
||||
# cd.yml gates build and deploy on this script's answer (#3). It exists because
|
||||
# main was red on every push: deploy patches k3s/apps/spatial-agent-app/deployment.yaml in
|
||||
# mathias/infra, which does not exist, and build kept pushing images for a
|
||||
# service that is not deployed.
|
||||
#
|
||||
# The failure that matters is answering "true" when deployment is not meant to
|
||||
# be on, so the cases below are about what must NOT enable it: a directory, a
|
||||
# symlink, an absent marker.
|
||||
#
|
||||
# Usage: ./scripts/tests/deploy-enabled_test.sh
|
||||
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
SCRIPT="$ROOT/scripts/deploy-enabled.sh"
|
||||
|
||||
pass=0; fail=0
|
||||
ok() { printf ' ok %s\n' "$1"; pass=$((pass+1)); }
|
||||
bad() { printf ' FAIL %s\n %s\n' "$1" "${2:-}"; fail=$((fail+1)); }
|
||||
|
||||
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
|
||||
echo "deploy-enabled tests"
|
||||
|
||||
if [ -f "$SCRIPT" ]; then ok "script exists"
|
||||
else bad "script exists" "$SCRIPT missing"; echo; echo "$pass passed, $fail failed"; exit 1; fi
|
||||
|
||||
# want_answer <name> <dir> <want>: stdout must equal <want> and exit must be 0.
|
||||
want_answer() {
|
||||
local name="$1" dir="$2" want="$3" got rc
|
||||
got="$(bash "$SCRIPT" "$dir" 2>/dev/null)"; rc=$?
|
||||
if [ "$got" = "$want" ] && [ "$rc" -eq 0 ]; then ok "$name"
|
||||
else bad "$name" "got '$got' (exit $rc), want '$want' (exit 0)"; fi
|
||||
}
|
||||
|
||||
mkdir "$tmp/regular"; echo "enabled" > "$tmp/regular/.deploy-enabled"
|
||||
mkdir "$tmp/absent"
|
||||
mkdir "$tmp/empty"; : > "$tmp/empty/.deploy-enabled"
|
||||
mkdir "$tmp/isdir"; mkdir "$tmp/isdir/.deploy-enabled"
|
||||
mkdir "$tmp/symlink"; echo "enabled" > "$tmp/symlink/target"; ln -s target "$tmp/symlink/.deploy-enabled"
|
||||
mkdir "$tmp/dangling"; ln -s nowhere "$tmp/dangling/.deploy-enabled"
|
||||
|
||||
want_answer "a regular marker file enables deployment" "$tmp/regular" true
|
||||
want_answer "no marker means deployment stays off" "$tmp/absent" false
|
||||
want_answer "an empty marker file still enables (content is ignored)" "$tmp/empty" true
|
||||
want_answer "a marker that is a directory does not enable" "$tmp/isdir" false
|
||||
want_answer "a symlink to a regular file does not enable" "$tmp/symlink" false
|
||||
want_answer "a dangling symlink does not enable" "$tmp/dangling" false
|
||||
|
||||
# A missing directory is not "off": answering false would let a typo in the
|
||||
# workflow look like a deliberate opt-out. Exit 2 and print nothing on stdout.
|
||||
got="$(bash "$SCRIPT" "$tmp/nope" 2>/dev/null)"; rc=$?
|
||||
if [ -z "$got" ] && [ "$rc" -eq 2 ]; then ok "a missing directory exits 2 with empty stdout"
|
||||
else bad "a missing directory exits 2 with empty stdout" "got '$got' (exit $rc)"; fi
|
||||
|
||||
# Default directory is the current one.
|
||||
got="$(cd "$tmp/regular" && bash "$SCRIPT" 2>/dev/null)"
|
||||
[ "$got" = "true" ] && ok "with no argument it inspects the current directory" \
|
||||
|| bad "with no argument it inspects the current directory" "got '$got'"
|
||||
|
||||
echo
|
||||
echo "$pass passed, $fail failed"
|
||||
[ "$fail" -eq 0 ]
|
||||
Reference in New Issue
Block a user