.gitea/workflows/build.yml builds the directus image on path-filtered pushes to main and validates the boot pipeline against a throwaway Postgres before pushing the image to the registry. The dry-run is the gate that catches snapshot drift, broken db-init scripts, or incompatible schema changes before they reach stage. Workflow shape (mirrors processor's CI but tailored to Directus): - Path filter: snapshots/, db-init/, extensions/, scripts/, entrypoint.sh, Dockerfile, the workflow file itself. Docs-only commits (.planning/, README.md, compose.dev.yaml, package.json) do NOT trigger CI. - Throwaway Postgres via services: block, pinned to the same timescale/timescaledb-ha:pg16.6-ts2.17.2-all tag as compose.dev.yaml. - Plain `docker build` (NOT build-push-action) so the image stays in the local daemon for the subsequent docker run dry-run. - Dry-run: --network host + --entrypoint bash to override the upstream entrypoint and run only apply-db-init.sh && schema-apply.sh. Skips bootstrap and pm2-runtime — the schema apply is the gate. - Two image tags: :main (mutable) and :<sha> (immutable). - Optional Portainer webhook gated on secret presence; curl -fsS so a misconfigured URL fails the step explicitly. Spec corrections folded in (the spec's draft had two contradictions that would have failed at runtime): 1. DB_HOST=localhost (not 'postgres'). With --network host, service containers are reachable on the runner's loopback by their port mapping, NOT by service name. Service-name resolution requires the default bridge network; --network host overrides it. 2. health-retries 20 (not 10). timescaledb-ha:*-all does more init work at boot than vanilla postgres; 50s isn't always enough. Operator action required in the Gitea repo Settings before first run: configure REGISTRY_USERNAME and REGISTRY_PASSWORD secrets (required for push); optionally PORTAINER_WEBHOOK_URL (for auto-deploy). Live verification deferred to first relevant commit. Documented in the task spec's Done section: positive (clean snapshot → push succeeds) and negative (malformed snapshot → halt before push) cases to validate once CI runs. ROADMAP marks 1.8 done. Phase 1 progress: 8/9 tasks complete (1.1–1.8); only 1.9 (Rally Albania 2026 dogfood seed) remains before Phase 1 ships.
10 KiB
Task 1.8 — Gitea CI dry-run workflow
Phase: 1 — Slice 1 schema + deploy pipeline
Status: ⬜ Not started
Depends on: 1.7
Wiki refs: docs/wiki/entities/directus.md (Schema management section)
Goal
Build a Gitea Actions workflow that on push to main (when relevant paths change): builds the image, spins up a throwaway Postgres + TimescaleDB in CI, runs the entrypoint flow as a dry-run to catch snapshot/migration breakage, and only publishes the image to the registry if the dry-run succeeds. Mirrors the processor and tcp-ingestion workflow shape.
Deliverables
.gitea/workflows/build.yml:name: Build directus image on: push: branches: [main] paths: - 'snapshots/**' - 'db-init/**' - 'extensions/**' - 'scripts/**' - 'entrypoint.sh' - 'Dockerfile' - '.gitea/workflows/build.yml' workflow_dispatch: jobs: build-and-publish: runs-on: ubuntu-22.04 services: postgres: image: timescale/timescaledb-ha:pg16.6-ts2.17.2-all # match compose.dev.yaml; :pg16-latest does NOT exist on Docker Hub env: POSTGRES_USER: directus POSTGRES_PASSWORD: directus POSTGRES_DB: directus ports: ['5432:5432'] options: >- --health-cmd "pg_isready -U directus" --health-interval 5s --health-timeout 5s --health-retries 10 steps: - uses: actions/checkout@v4 - name: Build image run: docker build -t trm-directus:ci . - name: Dry-run boot against throwaway Postgres env: DB_HOST: postgres DB_PORT: 5432 DB_USER: directus DB_PASSWORD: directus DB_DATABASE: directus KEY: ci-key-not-secret SECRET: ci-secret-not-secret ADMIN_EMAIL: ci@example.com ADMIN_PASSWORD: ci-password-not-secret PUBLIC_URL: http://localhost:8055 run: | docker run --rm \ -e DB_CLIENT=pg \ -e DB_HOST=$DB_HOST -e DB_PORT=$DB_PORT \ -e DB_USER=$DB_USER -e DB_PASSWORD=$DB_PASSWORD -e DB_DATABASE=$DB_DATABASE \ -e KEY=$KEY -e SECRET=$SECRET \ -e ADMIN_EMAIL=$ADMIN_EMAIL -e ADMIN_PASSWORD=$ADMIN_PASSWORD \ -e PUBLIC_URL=$PUBLIC_URL \ --network host \ --entrypoint bash \ trm-directus:ci \ -c '/directus/scripts/apply-db-init.sh && /directus/scripts/schema-apply.sh && echo "dry-run ok"' - name: Login to Gitea registry uses: docker/login-action@v3 with: registry: git.dev.microservices.al username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - name: Tag and push run: | docker tag trm-directus:ci git.dev.microservices.al/trm/directus:main docker tag trm-directus:ci git.dev.microservices.al/trm/directus:${{ github.sha }} docker push git.dev.microservices.al/trm/directus:main docker push git.dev.microservices.al/trm/directus:${{ github.sha }} - name: Trigger Portainer redeploy (optional) if: secrets.PORTAINER_WEBHOOK_URL != '' run: curl -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}"
Specification
- Dry-run runs the entrypoint scripts only, not
directus start. Starting the server and waiting for it to serve is slow and unnecessary — the goal is to catch DDL / snapshot apply errors. Override theENTRYPOINTand run the two scripts directly. - Service container is the throwaway Postgres.
services:block in Gitea Actions (compatible syntax with GitHub Actions). Use the pinned TimescaleDB image; mismatch with prod hides bugs. - Path filter on
on.push.pathskeeps CI quiet for unrelated repo changes (docs-only commits, etc.). Mirrors the processor workflow. - Two image tags published:
:main(always points at latest main) and:<sha>(specific commit, immutable). The deploy stack can pin to either. - Portainer webhook is optional (gated by secret presence). If unset, no auto-deploy.
- No integration tests in CI for Phase 1. The dry-run boot is the integration test — it proves the snapshot+db-init combination works against a fresh Postgres. Phase 5+ adds extension-specific tests as those land.
- Required Gitea secrets:
REGISTRY_USERNAME,REGISTRY_PASSWORD— for the image push.PORTAINER_WEBHOOK_URL— optional, for auto-deploy.
Acceptance criteria
- Workflow file is committed at
.gitea/workflows/build.yml. - First push to
mainafter this lands triggers the workflow. - Workflow steps in order: checkout → build → dry-run boot → registry login → tag/push → optional Portainer ping.
- Dry-run step exits 0 with logs showing "db-init complete" and "schema apply: no changes" (after the snapshot has been applied once, subsequent runs against fresh Postgres still apply from scratch — verify the apply step works in both cases).
- Intentionally break the snapshot (manually edit
snapshots/schema.yamlto a malformed YAML) → workflow fails at the dry-run step → image is NOT pushed. - Intentionally break a migration (introduce SQL syntax error in
db-init/) → workflow fails at the dry-run step → image is NOT pushed. - Push a docs-only change → workflow does NOT trigger.
- Image pushed to registry under
git.dev.microservices.al/trm/directus:mainand:<sha>. - Portainer webhook fires if configured.
Risks / open questions
- Gitea Actions
services:syntax compatibility. Gitea's runner is mostly GitHub-Actions-compatible but has historically had quirks with theservices:block (especially around image pulls from private registries). If the throwaway Postgres can't be brought up viaservices:, fall back to adocker runstep that backgrounds the container and a wait-loop onpg_isready. Document the chosen approach. - Network access between job container and service container.
--network hostis the simplest solution if Gitea's runner allows it. If not, use the Docker network created by the runner and reference the service by name (postgres:5432).
Done
Implementation landed (pending live trigger by first relevant commit). Workflow file at .gitea/workflows/build.yml. Statically validated; live trigger requires a push that touches one of the path-filtered locations.
Corrections folded in vs. the spec's draft YAML:
DB_HOST=localhost, notDB_HOST=postgres. The spec's draft mixed--network hostwith service-name resolution; those are mutually exclusive. With--network hostthe docker-run container shares the runner's loopback, so the service's port mapping (5432:5432) is reachable aslocalhost:5432, not by service namepostgres. (Service-name resolution would only work with the runner's default bridge network.)--health-retries 20instead of 10. Thetimescaledb-ha:*-allimage runs more init work at startup than vanilla postgres and occasionally exceeds the 50s window on cold runner images. 20 retries × 5s = 100s margin.--health-cmd "pg_isready -U directus -d directus"with explicit-d. Spec had user only.curl -fsS -X POSTfor the Portainer webhook step. Barecurl -X POSTreturns 0 even on HTTP 4xx/5xx;-fmakes a misconfigured webhook URL fail the step explicitly.- Plain
docker build, NOTdocker/build-push-action@v5. The dry-run step needs the freshly-built image accessible to a subsequentdocker run.build-push-actionwith the docker-container Buildx driver exports into a separate buildkitd cache thatdocker runcannot see — the run would fail with "image not found." Plaindocker buildkeeps the image in the local Docker daemon.
Deliberate divergences from processor/.gitea/workflows/build.yml:
| Aspect | Processor | Directus | Why |
|---|---|---|---|
| Build mechanism | docker/build-push-action@v5 |
plain docker build |
dry-run needs local-daemon access (above) |
| Buildx setup | yes | no | Buildx isolates the image; would defeat the dry-run |
services: block |
absent | present | Directus dry-run needs a live Postgres; processor mocks it |
| Node/pnpm setup | yes | no | No TS to compile in Phase 1 (Phase 5 adds this) |
| typecheck/lint/test | three steps | none | No extensions yet |
| Portainer webhook | unconditional | gated on secret presence | Spec requirement |
runs-on |
ubuntu-latest |
ubuntu-22.04 |
Pin to avoid floating-tag runner image breakage |
Acceptance criteria status:
Static (verified):
- ✅ Workflow file at
.gitea/workflows/build.yml. - ✅ Steps in correct order: checkout → build → dry-run → login → tag/push → optional Portainer.
- ✅ Path filter excludes
.planning/,README.md,compose.dev.yaml,package.json— docs-only commits won't trigger CI. - ✅ Workflow file itself is in the path-filter list (so changes to CI trigger CI).
- ✅ Two image tags published (
:main,:<sha>). - ✅ Required secrets identified:
REGISTRY_USERNAME,REGISTRY_PASSWORD. Optional:PORTAINER_WEBHOOK_URL. - ✅ Dry-run command logic traced: env vars, network mode, entrypoint override, script chain all consistent.
Pending live trigger (will validate on first push that hits the path filter):
- ⏳ Workflow triggers on push.
- ⏳ Dry-run step exits 0 against a fresh Postgres + the committed snapshot (currently 105 KB, 13 collections).
- ⏳ Snapshot drift simulation: hand-edit
snapshots/schema.yamlto malformed YAML → push → CI fails at dry-run → image NOT pushed. - ⏳ Migration syntax error simulation: introduce broken
db-init/006_*.sql→ push → CI fails at dry-run → image NOT pushed. - ⏳ Image actually published to
git.dev.microservices.al/trm/directus:mainafter a clean run. - ⏳ Portainer webhook fires if configured.
Operator action required before first run: in the Gitea repo at git.dev.microservices.al/trm/directus → Settings → Secrets, configure:
REGISTRY_USERNAME— Gitea user with write access to the container registryREGISTRY_PASSWORD— password or PAT for that userPORTAINER_WEBHOOK_URL(optional) — for auto-redeploy on push
Without REGISTRY_USERNAME / REGISTRY_PASSWORD the Login step fails with a clear auth error. Without PORTAINER_WEBHOOK_URL the Portainer step is skipped entirely.