ci: smoke-test the release image before publishing to Docker Hub (#487)

Starts the already-loaded localhost/sencho:release-scan image headless
on the runner and polls /api/health for up to 30 seconds. Catches
entrypoint regressions, native module ABI breakage, and first-boot
crashes on the exact artifact that the multi-arch push step below
will republish moments later.

Placed BEFORE the build-push step so a smoke failure does not move
the `latest`, semver, or minor tags on Docker Hub. The image reused
here is the same amd64 artifact Trivy scanned a step earlier, so
there is no extra build or pull on the critical path.

The container is started without --rm so that docker logs in the
trap can surface the stack trace when the container crashes during
boot; the trap force-removes it after capturing logs whether it
exited or is still running.

/api/health is public and returns before any DB, JWT, or Docker
socket work, so no env vars, volume mounts, or bind mounts are
required for the smoke test to be a valid signal that the process
booted and the HTTP server is accepting connections.
This commit is contained in:
Anso
2026-04-10 14:28:57 -04:00
committed by GitHub
parent 3f9aeb229b
commit 61f1602ab8
+28
View File
@@ -100,6 +100,34 @@ jobs:
severity: 'CRITICAL,HIGH'
format: 'table'
# Start the scanned image headless on the runner and poll /api/health
# until it returns 200. Catches entrypoint regressions, native module
# ABI breakage, and first-boot crashes on the exact artifact that the
# multi-arch push below will republish moments later. Intentionally
# placed BEFORE the publish step so a smoke failure does not move
# `latest` or the semver tags on Docker Hub. The health endpoint is
# public and returns before any DB or Docker-socket work, so the
# container only needs a free port, no env vars, and no volume mounts.
- name: Smoke test release image (pre-publish)
run: |
set -euo pipefail
# Not using --rm so that a crashed container sticks around long
# enough for `docker logs` in the trap to surface the stack trace.
# The trap force-removes it explicitly whether it is still running
# or already exited.
docker run -d --name sencho-smoke -p 3000:3000 localhost/sencho:release-scan
trap 'docker logs sencho-smoke 2>&1 || true; docker rm -f sencho-smoke >/dev/null 2>&1 || true' EXIT
for i in $(seq 1 30); do
if curl -fsS http://localhost:3000/api/health >/dev/null 2>&1; then
echo "Container healthy after ${i}s"
curl -s http://localhost:3000/api/health
exit 0
fi
sleep 1
done
echo "FAILED: /api/health did not become ready within 30s"
exit 1
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v7