pediatric-ai-scribe-v3/.forgejo/workflows/docker-build.yml
Daniel 993442f98a feat: the API describes itself, at /api/openapi.json
docs/api-reference.md was hand-written, and by the time anyone checked
it was documenting twenty-three endpoints that answer 404 while missing
others that exist. That is what hand-written reference material does: it
is correct on the day it is written and silently wrong afterwards. A
second hand-written document, in YAML this time, would rot the same way.

So paths, methods and mount points are read from the Express router
stack at request time. They cannot disagree with the app, because they
are the app: 186 paths, 215 operations, and — checked — no /learning
endpoints, which is what the prose version went on claiming for weeks
after that feature was deleted.

What introspection cannot know is what an endpoint is *for*. That half
lives in src/utils/openapiRoutes.js, keyed by "METHOD /path", and it is
the half that rots, so it is the half that is enforced: a Playwright
spec fetches the live document and fails when the number of operations
without a summary rises above 199 — the debt as measured today. A
ratchet, not a target. Adding an endpoint pushes the count over and
fails the build; describing one lowers the number. The failure lists the
operations by name, so it says what to write.

Whether an operation is public is stated per route rather than inferred
from middleware. Guessing wrong there is worse in both directions:
calling a public endpoint protected hides a hole, and the reverse
invites a bug report.

The contract spec lives in e2e rather than the unit suite because it
needs the whole app mounted, and requiring server.js from node:test
pulls in the database pool and hangs the run — that has happened here
before.

Also: e2e now runs in CI on dev, gated by a shell check inside the step
rather than a job-level "if", which this Forgejo dispatches anyway and
then kills with "Early termination".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-13 00:52:55 +02:00

122 lines
4.8 KiB
YAML

name: Forgejo Docker Build
# dev proves the tests pass and the image builds. main additionally publishes
# the image, because main is what production runs.
#
# Deploying is a separate workflow, not a job here behind an "if". A job whose
# "if" is false is still dispatched to a runner by this Forgejo, which then
# cannot resolve it and reports "Early termination" — that was the failure on
# every run of this workflow. Nothing here now depends on a job being skipped.
on:
push:
branches: [main, dev]
workflow_dispatch:
inputs:
push_image:
description: Push image to Forgejo container registry (main only)
required: false
default: 'true'
jobs:
root-test:
name: Root app tests
runs-on: forgejo-local
steps:
- uses: actions/checkout@v4
- uses: https://github.com/actions/setup-node@v4
with:
node-version: '24'
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm test
build:
needs: root-test
name: Build Docker image
runs-on: forgejo-local
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Prepare compose env files
run: |
touch .env
- name: Validate Compose config
run: docker compose -f docker-compose.yml config >/tmp/ped-ai-compose.yml
# The same script a person runs locally, so a CI image and a hand-built
# one cannot drift. It validates the revision and bakes it into the image,
# which is what makes /api/build able to say what is running.
- name: Build compose service
run: ./scripts/build-image.sh
- name: Tag image by revision
run: |
IMAGE="git.danvics.com/danvics/pediatric-ai-scribe-v3"
REVISION=$(git rev-parse HEAD)
# The full revision is the immutable name; :latest is only a pointer.
docker tag ped-ai-local:latest "$IMAGE:$REVISION"
docker tag ped-ai-local:latest "$IMAGE:latest"
# A step, not a job. Step conditions are evaluated by the runner once the
# job is already running, so a false one simply skips — it cannot produce
# the dispatch failure a job-level condition does here.
- name: Push image to Forgejo registry
if: ${{ github.ref == 'refs/heads/main' && github.event.inputs.push_image != 'false' }}
env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
run: |
IMAGE="git.danvics.com/danvics/pediatric-ai-scribe-v3"
REVISION=$(git rev-parse HEAD)
echo "$FORGEJO_TOKEN" | docker login git.danvics.com -u danvics --password-stdin
docker push "$IMAGE:$REVISION"
docker push "$IMAGE:latest"
# ── End-to-end ────────────────────────────────────────────────────────
# A real browser against a real copy of the app, on a database created
# empty for this run. It is the pass that catches what unit tests cannot:
# every bug that reached production this week — a popup severed by COOP, a
# preview that hid its own failure, a login step nobody re-checked — was
# invisible to 893 unit tests and visible to a browser.
#
# dev only, and not blocking the image build. It takes ~7 minutes against
# ~4 seconds for the unit suite, and the point of dev is to find this before
# main, not to slow main down.
e2e:
needs: root-test
name: End-to-end (browser)
runs-on: forgejo-local
steps:
- uses: actions/checkout@v4
# Brings its own Postgres and Redis up on tmpfs, seeds them, runs
# Playwright on desktop and mobile, then tears the stack down. Nothing
# it touches is shared with production.
# The branch check is inside the step, not a job-level "if". A job whose
# condition is false is still dispatched by this Forgejo and dies with
# "Early termination" — that was the red on every run of this workflow
# until recently. A shell guard skips honestly and says so in the log.
- name: Run the suite
run: |
if [ "${{ github.ref }}" != "refs/heads/dev" ]; then
echo "e2e runs on dev only — nothing to do on ${{ github.ref }}."
exit 0
fi
./scripts/e2e.sh
# always(), because a stack left up holds a port and a gigabyte of tmpfs.
- name: Stop the stack
if: always()
run: ./scripts/e2e.sh --down || true
# The report carries the trace and screenshot of every failure, which is
# the only part worth reading after a red run.
- name: Keep the report
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: e2e/playwright-report/
retention-days: 14