From 96ad68c619f0924d0f66cb11dd4667b690568e21 Mon Sep 17 00:00:00 2001 From: vreshch Date: Mon, 10 Aug 2026 23:49:23 +0200 Subject: [PATCH] feat(showcase): serve a real /health instead of the SPA fallback /health returned index.html. An SPA fallback answers 200 on every path, so the estate console could only record it as 'reachability' - proof that nginx is up and nothing more, however dead the bundle behind it. Now an exact-match location, declared before the fallback so try_files can never swallow it, serving a payload baked at image build. That also makes the showcase report WHICH commit it serves; it previously reported no version at all, so drift was undetectable. No uptime/instance/checkedAt: a static file cannot measure them, and a frozen checkedAt is precisely the 'something is serving a cached copy' signal the estate health contract relies on. Omitted beats fabricated. The container HEALTHCHECK now probes /health rather than /, so it fails when the bundle is missing instead of passing on index.html. Verified against the built image: envelope + application/json + no-store on /health, SPA and client-side routes unaffected. --- .github/workflows/deploy.yml | 1 + Dockerfile | 14 +++++++++++++- docker/nginx.conf | 15 +++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b47361e..751656d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -45,6 +45,7 @@ jobs: ${{ env.IMAGE }}:latest build-args: | COMMIT_SHA=${{ github.sha }} + BUILD_TIME=${{ github.event.head_commit.timestamp || github.event.repository.updated_at }} cache-from: type=gha,scope=ds-showcase cache-to: type=gha,mode=max,scope=ds-showcase diff --git a/Dockerfile b/Dockerfile index 81a3ef6..384b668 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,19 @@ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 8080 # Build provenance, baked late so a changing SHA only busts this layer. ARG COMMIT_SHA="" +ARG BUILD_TIME="" ENV COMMIT_SHA=$COMMIT_SHA +# Static health payload, baked with the bundle it describes. `service` matches the +# estate registry name so the console's probe and its telemetry row name one thing. +# USER root for this layer only: the base image runs as uid 101, which cannot write +# into the root-owned web root. Dropped back immediately so nothing else runs as root. +USER root +RUN printf '{"success":true,"data":{"status":"ok","service":"agentage-ds","commit":"%s","buildTime":"%s"}}' \ + "$COMMIT_SHA" "$BUILD_TIME" > /usr/share/nginx/html/health.json \ + && chown 101:101 /usr/share/nginx/html/health.json +USER 101 # 127.0.0.1, not localhost: nginx binds IPv4 only; busybox wget picks ::1 and gets refused. +# Probes /health, not /: with the SPA fallback removed from that path, this now +# fails if the bundle is missing rather than passing on index.html. HEALTHCHECK --interval=15s --timeout=5s --retries=3 \ - CMD wget -q --spider http://127.0.0.1:8080/ || exit 1 + CMD wget -q --spider http://127.0.0.1:8080/health || exit 1 diff --git a/docker/nginx.conf b/docker/nginx.conf index e57b0ca..de0b045 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -17,6 +17,21 @@ server { try_files $uri =404; } + # Health, declared BEFORE the SPA fallback and as an exact match, so it can never + # be swallowed by try_files. Without it /health returned index.html: a 200 on every + # path, which reads as healthy however dead the bundle is. + # + # The payload is baked at image build (see Dockerfile), so it also proves WHICH + # commit nginx is serving - the showcase previously reported no version at all. + # No uptime/instance/checkedAt: a static file cannot measure them, and a frozen + # checkedAt is exactly the "something is serving a cached copy" signal the estate + # health contract uses. Omitted beats fabricated. + location = /health { + default_type application/json; + add_header Cache-Control "no-store"; + try_files /health.json =404; + } + # SPA: fall back to index.html for client-side routes. location / { try_files $uri $uri/ /index.html;