#!/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 : the lines from " :" 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 : 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 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 ]