Files
Epicnabbo-Catalogus-Updated…/src/test/ci-deploy-harness.sh
T
openhands 8ee144745a fix(deploy): stub ss in the deploy harness and cover the port-conflict path
The port-conflict guard added in the previous commit made the six existing
blue/green deployment simulation tests fail. assert_port_free() shells out to
ss, and the simulation harness stubs git, curl, docker, nginx, pnpm and node —
but not ss. Because the runner is self-hosted and the containers use
--net=host, the simulation saw the production CMS containers holding 3002 and
3003 and refused to start its own candidate.

The harness now stubs ss. It reports no listener for every scenario except
'port-taken', which reserves whichever port the script asks about, so the
simulation stays independent of the host it runs on.

Also switched the ss probe from `command -v ss` to `type ss`. The stub is a
shell function delivered through BASH_ENV; `command -v` happens to find it,
but `type` is the reliable test for "is this resolvable", and the two differ
across shells.

Added a regression test for the guard itself: with the candidate port already
occupied, the deploy must fail, must not have run `docker run`, and must leave
the nginx upstream untouched on the old port — no half-finished cutover.
Verified it fails when the assert_port_free call is removed.

Deploy simulation: 26 passed. Full unit suite: 3316 passed, 12 skipped.
2026-10-03 18:30:00 +02:00

107 lines
4.2 KiB
Bash

# Sourced only by the deployment simulation tests; no external services are used.
git() {
if [ "$1" = rev-parse ]; then printf '%s\n' "$TEST_SHA"; return; fi
local n=0
if [ -f "$TEST_DIR/remote-count" ]; then read -r n < "$TEST_DIR/remote-count"; fi
n=$((n+1)); printf '%s\n' "$n" > "$TEST_DIR/remote-count"
if [ "$SCENARIO" = remote-failure ]; then return 1; fi
if [ "$SCENARIO" = stale ] || { [ "$SCENARIO" = superseded ] && [ "$n" -gt 1 ]; }; then
printf '%s\trefs/heads/main\n' bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
else printf '%s\trefs/heads/main\n' "$TEST_SHA"; fi
}
flock() { echo "lock" >> "$TEST_DIR/calls"; [ "$SCENARIO" != lock-failure ]; }
pnpm() {
# De env-prefix (bv. PLAYWRIGHT_BASE_URL) hoort bij het deploypad dat getest
# wordt, dus die loggen we mee: zo blijft zichtbaar dat de e2e-test tegen de
# juiste poort draait.
echo "${PLAYWRIGHT_BASE_URL:+PLAYWRIGHT_BASE_URL=$PLAYWRIGHT_BASE_URL }pnpm $*" >> "$TEST_DIR/calls"
if [ "$1" = db:migrate ] && [ "$SCENARIO" = migration-failure ]; then return 1; fi
if [ "$1" = test:e2e ] && [ "$SCENARIO" = smoke-failure ] && candidate_is_new; then return 1; fi
return 0
}
# De kandidaat is het container dat het script zojuist heeft gestart. Bij
# blue/green is dat niet per se `epicnext-cms-app`.
candidate_is_new() {
for f in epicnext-cms-app epicnext-cms-green; do
if [ "$(cat "$TEST_DIR/$f" 2>/dev/null)" = new ]; then return 0; fi
done
return 1
}
nginx() {
echo "nginx $*" >> "$TEST_DIR/calls"
if [ "$SCENARIO" = nginx-reject ] && [ "$1" = "-t" ]; then return 1; fi
if [ "$SCENARIO" = nginx-reload-fail ] && [ "$1" = "-s" ]; then return 1; fi
return 0
}
curl() {
if [ "$SCENARIO" = health-failure ] && candidate_is_new; then return 1; fi
echo '{"database":true}'
}
sleep() { :; }
# De poortconflict-check (assert_port_free in scripts/ci-deploy.sh) leest de
# echte luisterende sockets. Zonder deze stub zou de simulatie de containers van
# de productiehost zien — de test draait immers op dezelfde machine — en elke
# blauwe/groene scenario laten falen op een poort die de simulatie zelf net
# virtueel heeft toegewezen. Standaard is geen enkele poort bezet; het scenario
# 'port-taken' reserveert expliciet de kandidaatpoort.
ss() {
local requested="" arg
for arg in "$@"; do
case "$arg" in
sport=*) requested="${arg#sport=}" ;;
'sport'|'='|':') ;;
*:*) [ -z "$requested" ] && requested="${arg##*:}" ;;
esac
done
if [ "$SCENARIO" = port-taken ] && [ -n "$requested" ]; then
printf 'State Recv-Q Send-Q Local Address:Port Peer Address:Port\n'
printf 'LISTEN 0 511 0.0.0.0:%s 0.0.0.0:*\n' "$requested"
return 0
fi
printf 'State Recv-Q Send-Q Local Address:Port Peer Address:Port\n'
}
docker() {
echo "docker $*" >> "$TEST_DIR/calls"
local name="${@: -1}"
case "$1" in
inspect)
[ -f "$TEST_DIR/$name" ] || return 1
if [ "${3:-}" = '{{.State.Running}}' ]; then echo true
elif [ "${3:-}" = '{{.Image}}' ]; then echo "sha256:$(cat "$TEST_DIR/$name")"
fi ;;
image)
if [ "$2" = ls ]; then
printf '%s\n' "epicnext-cms:verified-$TEST_SHA" "epicnext-cms:verified-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" "epicnext-cms:verified-cccccccccccccccccccccccccccccccccccccccc"
elif [ "$2" = inspect ]; then
case "$name" in *bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb) echo sha256:old ;; *) echo sha256:unused ;; esac
fi ;;
build) [ "$SCENARIO" != build-failure ] ;;
stop) return 0 ;;
rename) mv "$TEST_DIR/$2" "$TEST_DIR/$3" ;;
run)
run_name=epicnext-cms-app
prev=""
for arg in "$@"; do
if [ "$prev" = "--name" ]; then run_name="$arg"; fi
prev="$arg"
done
echo new > "$TEST_DIR/$run_name"
[ "$SCENARIO" != run-failure ] ;;
start) [ -f "$TEST_DIR/$2" ] ;;
rm) rm -f "$TEST_DIR/$name" ;;
*) return 0 ;;
esac
}
export -f git flock pnpm curl sleep ss docker nginx candidate_is_new
node() {
echo "node $*" >> "$TEST_DIR/calls"
case "$SCENARIO" in
release-failure) [[ "$*" != *verify-deployed-release.mjs* ]] || return 1 ;;
news-e2e-failure) [[ "$*" != *e2e/news-real/run.ts* ]] || return 1 ;;
esac
return 0
}
export -f node