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