From 80f738ddcb9985f7d101c213eb41a2c3a98a0b4f Mon Sep 17 00:00:00 2001 From: RaymondVerhoef Date: Sat, 11 Jul 2026 14:20:15 +0200 Subject: [PATCH] fix: valid Caddyfile handle_errors + frontend health-gate in deploys (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The learning-platform (Caddy) container crash-looped since this morning's 89d3395: `Content-Type application/json` is not a valid subdirective of `respond` — a Caddyfile parse error aborts Caddy at startup, taking the whole app offline for authenticated users while every CI run stayed green. Verified with caddy v2.10.0: "unrecognized subdirective 'Content-Type', at Caddyfile:53"; the fixed file validates clean. CI was blind to this twice over: test.yml swaps the real Caddyfile for Caddyfile.test in the test image, and the deploy health-gate from #18 only covers PocketBase. - Caddyfile: set Content-Type via a `header @api` directive before the `respond`; behaviour of the JSON error response is unchanged - infra/*/site/deploy-playbook.yml: frontend health-gate (docker exec wget --spider on the container) with log dump + abort on failure, and an always-on post-deploy smoke report (compose ps, proxy health, team_members auth-methods, PocketBase log tail) for visibility behind the auth perimeter Closes #20 Co-Authored-By: Claude Fable 5 --- Caddyfile | 9 ++-- infra/development/site/deploy-playbook.yml | 50 ++++++++++++++++++++++ infra/production/site/deploy-playbook.yml | 50 ++++++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/Caddyfile b/Caddyfile index 29b6001..115c88f 100644 --- a/Caddyfile +++ b/Caddyfile @@ -48,10 +48,13 @@ handle_errors { # Don't mask API errors with the SPA shell — return a proper # JSON error so the frontend can display a meaningful message. + # NOTE: `respond` only accepts `body`/`close` subdirectives; the + # Content-Type must be set via a separate `header` directive. An + # invalid subdirective is a Caddyfile parse error and crash-loops + # the container at startup (issue #20). @api path /api/* - respond @api `{"code":{err.status_code},"message":"Backend unavailable"}` {err.status_code} { - Content-Type application/json - } + header @api Content-Type application/json + respond @api `{"code":{err.status_code},"message":"Backend unavailable"}` {err.status_code} rewrite * /index.html file_server diff --git a/infra/development/site/deploy-playbook.yml b/infra/development/site/deploy-playbook.yml index 442b8b4..537e1db 100644 --- a/infra/development/site/deploy-playbook.yml +++ b/infra/development/site/deploy-playbook.yml @@ -100,3 +100,53 @@ {{ pb_logs.stdout | default('') }} {{ pb_logs.stderr | default('') }} when: pb_health is failed + + # The frontend container carries the SPA + Caddy. An invalid Caddyfile + # crash-loops it at startup while the PocketBase gate stays green — that + # outage (issue #20) was invisible to CI because the test job swaps in + # Caddyfile.test. Gate on the real container actually serving. + - name: Wait for the frontend (Caddy) to become healthy + ansible.builtin.command: docker exec learning-platform wget -q --spider http://127.0.0.1:80/ + register: fe_health + until: fe_health.rc == 0 + retries: 18 + delay: 5 + changed_when: false + ignore_errors: yes + + - name: Collect frontend logs for diagnosis + ansible.builtin.command: docker logs --tail 80 learning-platform + register: fe_logs + when: fe_health is failed + changed_when: false + failed_when: false + + - name: Abort deploy — frontend is not healthy + ansible.builtin.fail: + msg: | + The frontend (Caddy) container did not become healthy after the deploy. + Recent container logs: + {{ fe_logs.stdout | default('') }} + {{ fe_logs.stderr | default('') }} + when: fe_health is failed + + # Observability behind the auth perimeter: surface the end-to-end state + # in the CI log on every deploy. + - name: Post-deploy smoke report + ansible.builtin.shell: | + cd /opt/learning-platform + echo '--- docker compose ps ---' + docker compose ps + echo '--- frontend -> pocketbase proxy health ---' + docker exec learning-platform wget -q -O - http://127.0.0.1:80/api/health || echo 'PROXY HEALTH FAILED' + echo '--- team_members auth methods (OIDC provider present?) ---' + docker exec pocketbase-learning wget -q -O - http://127.0.0.1:8090/api/collections/team_members/auth-methods || echo 'AUTH-METHODS FAILED' + echo '--- pocketbase logs (tail 30) ---' + docker compose logs --tail=30 pocketbase-learning + register: smoke_report + changed_when: false + failed_when: false + + - name: Show smoke report + ansible.builtin.debug: + msg: "{{ smoke_report.stdout_lines }}" diff --git a/infra/production/site/deploy-playbook.yml b/infra/production/site/deploy-playbook.yml index 442b8b4..537e1db 100644 --- a/infra/production/site/deploy-playbook.yml +++ b/infra/production/site/deploy-playbook.yml @@ -100,3 +100,53 @@ {{ pb_logs.stdout | default('') }} {{ pb_logs.stderr | default('') }} when: pb_health is failed + + # The frontend container carries the SPA + Caddy. An invalid Caddyfile + # crash-loops it at startup while the PocketBase gate stays green — that + # outage (issue #20) was invisible to CI because the test job swaps in + # Caddyfile.test. Gate on the real container actually serving. + - name: Wait for the frontend (Caddy) to become healthy + ansible.builtin.command: docker exec learning-platform wget -q --spider http://127.0.0.1:80/ + register: fe_health + until: fe_health.rc == 0 + retries: 18 + delay: 5 + changed_when: false + ignore_errors: yes + + - name: Collect frontend logs for diagnosis + ansible.builtin.command: docker logs --tail 80 learning-platform + register: fe_logs + when: fe_health is failed + changed_when: false + failed_when: false + + - name: Abort deploy — frontend is not healthy + ansible.builtin.fail: + msg: | + The frontend (Caddy) container did not become healthy after the deploy. + Recent container logs: + {{ fe_logs.stdout | default('') }} + {{ fe_logs.stderr | default('') }} + when: fe_health is failed + + # Observability behind the auth perimeter: surface the end-to-end state + # in the CI log on every deploy. + - name: Post-deploy smoke report + ansible.builtin.shell: | + cd /opt/learning-platform + echo '--- docker compose ps ---' + docker compose ps + echo '--- frontend -> pocketbase proxy health ---' + docker exec learning-platform wget -q -O - http://127.0.0.1:80/api/health || echo 'PROXY HEALTH FAILED' + echo '--- team_members auth methods (OIDC provider present?) ---' + docker exec pocketbase-learning wget -q -O - http://127.0.0.1:8090/api/collections/team_members/auth-methods || echo 'AUTH-METHODS FAILED' + echo '--- pocketbase logs (tail 30) ---' + docker compose logs --tail=30 pocketbase-learning + register: smoke_report + changed_when: false + failed_when: false + + - name: Show smoke report + ansible.builtin.debug: + msg: "{{ smoke_report.stdout_lines }}"