ci: pin Docker Deploy to a single app container IP
CI / Lint (push) Successful in 3m27s
CI / Architecture Guardrails (push) Successful in 4m31s
CI / Assistant Split Regression (push) Successful in 5m32s
CI / Typecheck (push) Successful in 6m24s
CI / Unit Tests (push) Successful in 8m31s
CI / Build (push) Successful in 7m35s
CI / E2E Tests (push) Successful in 7m48s
Nightly Security / Dependency Audit (push) Successful in 1m42s
CI / Fresh-Linux Docker Deploy (push) Failing after 9m57s
CI / Release Images (push) Has been skipped

Smoke test #2 kept hitting ERR_CONNECTION_REFUSED on the root path
even though curl warm-ups of the same path succeeded. Root cause is
the same split-brain bug we just fixed for e2epg: the 'app' hostname
on the shared gitea_gitea network resolves to multiple IPs (leftover
containers from concurrent runs), and curl vs Chromium picked
different ones.

Probe each resolved IP for /api/health, pin the winner as APP_BASE_URL
via GITHUB_ENV, and route health check, warm-up, and the Playwright
smoke run through that explicit IP.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 03:54:19 +02:00
parent 5da90af432
commit 97b77c29f9
+28 -14
View File
@@ -484,23 +484,39 @@ jobs:
- name: Build and start app (full profile)
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile full up -d --build app
- name: Wait for /api/health (up to 3 minutes)
# docker-compose.ci.yml attaches app/postgres/redis to gitea_gitea so the
# act_runner job container can reach them by compose service name.
- name: Resolve and pin app IP
# 'app' hostname collides on shared gitea_gitea network — same split
# -brain as e2epg. Smoke test #2 hit ERR_CONNECTION_REFUSED even
# after both warm-ups returned the expected codes, because curl and
# Playwright-launched Chromium picked different IPs from the DNS set.
# Probe each resolved IP for /api/health and pin the winner as
# APP_BASE_URL so every subsequent step (wait, warm-up, smoke run)
# hits the same container.
run: |
for i in $(seq 1 36); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://app:3100/api/health || echo "000")
echo "Attempt $i: HTTP $STATUS"
if [ "$STATUS" = "200" ]; then exit 0; fi
IPS=$(getent hosts app | awk '{print $1}')
if [ -n "$IPS" ]; then
for ip in $IPS; do
CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://$ip:3100/api/health" || echo "000")
echo "Attempt $i: $ip -> HTTP $CODE"
if [ "$CODE" = "200" ]; then
echo "APP_IP=$ip" >> "$GITHUB_ENV"
echo "APP_BASE_URL=http://$ip:3100" >> "$GITHUB_ENV"
exit 0
fi
done
else
echo "Attempt $i: 'app' not yet in DNS"
fi
sleep 5
done
echo "Health check timed out"
echo "No reachable app:3100 found"
docker compose -f docker-compose.yml -f docker-compose.ci.yml logs app --tail=50
exit 1
- name: Verify health response contains status ok
run: |
BODY=$(curl -sf http://app:3100/api/health)
BODY=$(curl -sf "$APP_BASE_URL/api/health")
echo "$BODY"
echo "$BODY" | grep '"status":"ok"'
@@ -519,7 +535,7 @@ jobs:
local path="$1"
local expect="$2"
for i in $(seq 1 24); do
CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "http://app:3100${path}" || echo "000")
CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "${APP_BASE_URL}${path}" || echo "000")
echo "Warm-up ${path} $i: HTTP $CODE"
if [ "$CODE" = "$expect" ]; then return 0; fi
sleep 5
@@ -572,11 +588,9 @@ jobs:
/tmp/pw-install/node_modules/.bin/playwright install chromium --with-deps
- name: Run smoke tests
env:
# App runs on the compose network; act_runner job is on gitea_gitea
# (docker-compose.ci.yml attaches services to both). Override baseURL.
PLAYWRIGHT_BASE_URL: http://app:3100
run: /tmp/pw-install/node_modules/.bin/playwright test --config apps/web/playwright.ci.config.ts
# Use the pinned APP_BASE_URL (explicit IP) so Chromium hits the same
# container as the warm-up probes.
run: PLAYWRIGHT_BASE_URL="$APP_BASE_URL" /tmp/pw-install/node_modules/.bin/playwright test --config apps/web/playwright.ci.config.ts
- name: Upload Playwright report
if: failure()