From 61f1602ab8adaa63774f0fb1b93e88502084c53a Mon Sep 17 00:00:00 2001 From: Anso Date: Fri, 10 Apr 2026 14:28:57 -0400 Subject: [PATCH] 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. --- .github/workflows/docker-publish.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 765c538f..415b584c 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -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