From ffa4aac9740b7a3ca7a5cba8f4a20701c5e39ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 9 May 2026 20:48:37 +0200 Subject: [PATCH] feat(ci): post-publish verify workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Informational reachability gate that runs after `Build & Publish Docker Image` finishes a `main` (or tag) build. Triggered via `workflow_run` so it always observes the actual completion state of the build, then: 1. pulls `ghcr.io/${repo}:${tag}` (proves the manifest finished publishing — the v1.4.14 main run "succeeded" from a wall-clock POV but the image had to be retagged on the deploy host because main's manifest never finished pushing; this catches that) 2. boots the container against an ephemeral Postgres service 3. probes `/api/version` (smoke for the build SHA + version slug) 4. probes `/api/health` (smoke for the runtime DB/worker probes) Every probe runs `continue-on-error: true` — this workflow is informational, not a deploy gate. A transient registry blip should surface as a yellow run, not block subsequent builds. Also wired up `workflow_dispatch` with an `image_tag` input so we can spot-verify any tag (`v1.4.15`, `v1.4.14`, etc.) on demand without re-publishing. Marc-status: filed as proof-of-concept for v1.4.15 phase-C2's auto-deploy work — Coolify webhook listener can build on top of the same boot harness. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/post-publish-verify.yml | 144 ++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 .github/workflows/post-publish-verify.yml diff --git a/.github/workflows/post-publish-verify.yml b/.github/workflows/post-publish-verify.yml new file mode 100644 index 000000000..30ccee8c1 --- /dev/null +++ b/.github/workflows/post-publish-verify.yml @@ -0,0 +1,144 @@ +name: Post-publish verify + +# Informational reachability gate that runs AFTER `Build & Publish Docker +# Image` finishes a `main` (or tag) build. Pulls the just-published image +# from GHCR and probes: +# - the image manifest is fetchable (i.e. the push actually completed) +# - the container starts and `/api/version` returns the expected version +# - the container's `/api/health` endpoint reports OK +# +# Does NOT block deploys — `continue-on-error` is set on every probe step +# so a transient registry blip doesn't go red. This is a smoke gate the +# v1.4.14 release would have caught: the main push appeared to "succeed" +# from the deploy host's POV, but the host had to pull the tag image +# because main's manifest never finished pushing. +# +# Triggered by `workflow_run` so it only runs after the build workflow +# actually completes — chaining via `needs:` would force the build into +# the same workflow file. + +on: + workflow_run: + workflows: ["Build & Publish Docker Image"] + types: [completed] + branches: [main] + workflow_dispatch: + inputs: + image_tag: + description: "GHCR image tag to verify (defaults to `main`)" + required: false + default: "main" + +permissions: + contents: read + packages: read + +jobs: + verify: + name: Pull + smoke-test published image + runs-on: ubuntu-latest + timeout-minutes: 10 + # Skip if the upstream build itself failed — there's nothing to verify. + # Manual dispatch always runs. + if: >- + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: healthlog + POSTGRES_PASSWORD: healthlog + POSTGRES_DB: healthlog + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U healthlog" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + steps: + - name: Resolve image tag + id: tag + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "tag=${{ github.event.inputs.image_tag }}" >> "$GITHUB_OUTPUT" + else + echo "tag=main" >> "$GITHUB_OUTPUT" + fi + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pull image + id: pull + continue-on-error: true + run: | + IMAGE="ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.tag }}" + echo "Pulling $IMAGE" + docker pull "$IMAGE" + echo "image=$IMAGE" >> "$GITHUB_OUTPUT" + + - name: Start container + if: steps.pull.outcome == 'success' + id: run + continue-on-error: true + run: | + # Minimal env to satisfy the strict-env validators on boot. + docker run -d --name healthlog-verify \ + --network host \ + -e DATABASE_URL='postgresql://healthlog:healthlog@localhost:5432/healthlog?schema=public' \ + -e ENCRYPTION_KEYS='{"v1":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' \ + -e ENCRYPTION_ACTIVE_KEY_ID=v1 \ + -e API_TOKEN_HMAC_KEY=verify-hmac-key-32-bytes-padding-x \ + -e AUTH_RP_NAME=HealthLog \ + -e AUTH_RP_ID=localhost \ + -e AUTH_RP_ORIGIN=http://localhost:3000 \ + -e NODE_ENV=production \ + -e HEALTHLOG_PROCESS_TYPE=web \ + -e PORT=3000 \ + "${{ steps.pull.outputs.image }}" + + # Wait up to 60 s for the server to start answering. + for i in $(seq 1 30); do + if curl -sf -o /dev/null http://localhost:3000/api/version; then + echo "ready after ${i}*2s" + exit 0 + fi + sleep 2 + done + echo "::warning::Container did not become ready within 60 s" + docker logs healthlog-verify || true + exit 1 + + - name: Probe /api/version + if: steps.run.outcome == 'success' + continue-on-error: true + run: | + set -e + BODY=$(curl -sf http://localhost:3000/api/version) + echo "$BODY" | jq . + VER=$(echo "$BODY" | jq -r '.data.version // .version') + echo "::notice::published image ${{ steps.tag.outputs.tag }} reports version=$VER" + + - name: Probe /api/health + if: steps.run.outcome == 'success' + continue-on-error: true + run: | + curl -sf http://localhost:3000/api/health | jq . + + - name: Container logs (always) + if: always() && steps.run.outcome == 'success' + continue-on-error: true + run: docker logs healthlog-verify | tail -100 + + - name: Tear down + if: always() + continue-on-error: true + run: docker rm -f healthlog-verify || true