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
122 lines
4.8 KiB
YAML
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
|