diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..a9bc2ae --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,141 @@ +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 + + # --------------------------------------------------------------------------- + # Throwaway Postgres for the dry-run boot step. + # + # Image: pinned to the same concrete tag used in compose.dev.yaml — NOT the + # floating :pg16-latest alias (which does NOT exist on Docker Hub). + # + # PGDATA: the timescaledb-ha image initialises at /home/postgres/pgdata/data; + # the healthcheck uses pg_isready, which doesn't depend on the PGDATA path. + # + # Port mapping: 5432:5432 binds the service on the runner's loopback. + # The dry-run docker run uses --network host so DB_HOST=localhost reaches it. + # --------------------------------------------------------------------------- + services: + postgres: + image: timescale/timescaledb-ha:pg16.6-ts2.17.2-all + env: + POSTGRES_USER: directus + POSTGRES_PASSWORD: directus + POSTGRES_DB: directus + ports: + - '5432:5432' + options: >- + --health-cmd "pg_isready -U directus -d directus" + --health-interval 5s + --health-timeout 5s + --health-retries 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # ------------------------------------------------------------------------- + # Build the image locally (trm-directus:ci). + # + # We use a plain `docker build` rather than docker/build-push-action because + # we need the image available in the *local Docker daemon* for the subsequent + # `docker run` dry-run step. docker/build-push-action with the + # docker-container Buildx driver exports into a separate buildkitd cache not + # accessible to `docker run`. + # ------------------------------------------------------------------------- + - name: Build image + run: docker build -t trm-directus:ci . + + # ------------------------------------------------------------------------- + # Dry-run boot — the gate that protects the registry from broken images. + # + # Runs only the two pre-boot scripts (apply-db-init.sh → schema-apply.sh) + # against the throwaway Postgres service above. Intentionally does NOT run + # `directus bootstrap` or `directus start` — that would require waiting for + # the HTTP server to come up, which adds minutes and tests nothing new. + # + # --network host: the service container is mapped on 127.0.0.1:5432; the + # docker run container sees it as localhost:5432 only when host networking + # is used. Without --network host, the container would be in a separate + # bridge network and could not reach the service by name or IP. + # + # --entrypoint bash: overrides /directus/entrypoint.sh so we execute only + # the script chain, not the full pm2-runtime boot. + # + # Required Directus env vars: DB_CLIENT + connection params are mandatory + # for `node cli.js schema apply`. KEY + SECRET are required by Directus's + # env initialisation even when only the schema subcommand is invoked. + # ADMIN_EMAIL + ADMIN_PASSWORD are included defensively (some Directus + # versions assert on them during CLI init). PUBLIC_URL silences the + # missing-public-url warning. + # + # If this step exits non-zero the workflow halts and the registry login / + # push steps are never reached — the broken image is never published. + # ------------------------------------------------------------------------- + - name: Dry-run boot against throwaway Postgres + run: | + docker run --rm \ + --network host \ + --entrypoint bash \ + -e DB_CLIENT=pg \ + -e DB_HOST=localhost \ + -e DB_PORT=5432 \ + -e DB_USER=directus \ + -e DB_PASSWORD=directus \ + -e DB_DATABASE=directus \ + -e KEY=ci-key-placeholder-not-secret \ + -e SECRET=ci-secret-placeholder-not-secret \ + -e ADMIN_EMAIL=ci@example.com \ + -e ADMIN_PASSWORD=ci-password-not-secret \ + -e PUBLIC_URL=http://localhost:8055 \ + trm-directus:ci \ + -c '/directus/scripts/apply-db-init.sh && /directus/scripts/schema-apply.sh && echo "dry-run ok"' + + # ------------------------------------------------------------------------- + # Registry login — runs only if the dry-run succeeded (default: workflow + # halts on non-zero exit, so reaching this step implies dry-run passed). + # ------------------------------------------------------------------------- + - name: Login to Gitea registry + uses: docker/login-action@v3 + with: + registry: git.dev.microservices.al + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + # ------------------------------------------------------------------------- + # Tag and push two tags: + # :main — mutable; always points at the latest commit on main. + # : — immutable; pinned to this specific commit. + # The deploy stack can reference either; :main for rolling updates, + # : for pinned deployments that need explicit rollback control. + # ------------------------------------------------------------------------- + - 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 }} + + # ------------------------------------------------------------------------- + # Optional Portainer redeploy webhook. + # Fires only when PORTAINER_WEBHOOK_URL secret is configured in the repo. + # If the secret is absent the condition evaluates false and the step is + # skipped — no error, no noise. + # ------------------------------------------------------------------------- + - name: Trigger Portainer redeploy (optional) + if: ${{ secrets.PORTAINER_WEBHOOK_URL != '' }} + run: curl -fsS -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}" diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 9c36eea..652fdc1 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -42,7 +42,7 @@ These rules govern every task. Any deviation must be discussed and documented as ### Phase 1 — Slice 1 schema + deploy pipeline -**Status:** 🟨 In progress (1.1–1.7 done; 1.8, 1.9 remaining) +**Status:** 🟨 In progress (1.1–1.8 done; 1.9 remaining) **Outcome:** A Directus instance with the org-level catalog (orgs, users, organization_users, vehicles, devices and their org junctions) and event-participation collections (events, classes, entries, entry_crew, entry_devices) live and snapshot-tracked. `db-init/` covers the TimescaleDB extension, the `positions` hypertable, and the `faulty` column. Image builds via Gitea Actions with a CI dry-run that catches snapshot drift before deploy. Rally Albania 2026 is registered as the first event in admin UI to dogfood the registration workflow. **This is what Rally Albania 2026 needs.** [**See `phase-1-slice-1-schema/README.md`**](./phase-1-slice-1-schema/README.md) @@ -56,7 +56,7 @@ These rules govern every task. Any deviation must be discussed and documented as | 1.5 | [Event-participation collections](./phase-1-slice-1-schema/05-event-participation-collections.md) | 🟩 | pending user commit | | 1.6 | [Schema snapshot/apply tooling](./phase-1-slice-1-schema/06-snapshot-tooling.md) | 🟩 | pending user commit | | 1.7 | [Image build & entrypoint](./phase-1-slice-1-schema/07-image-and-dockerfile.md) | 🟩 | pending user commit | -| 1.8 | [Gitea CI dry-run workflow](./phase-1-slice-1-schema/08-gitea-ci-dryrun.md) | ⬜ | — | +| 1.8 | [Gitea CI dry-run workflow](./phase-1-slice-1-schema/08-gitea-ci-dryrun.md) | 🟩 | pending user commit | | 1.9 | [Rally Albania 2026 dogfood seed](./phase-1-slice-1-schema/09-rally-albania-2026-seed.md) | ⬜ | — | ### Phase 2 — Course definition diff --git a/.planning/phase-1-slice-1-schema/08-gitea-ci-dryrun.md b/.planning/phase-1-slice-1-schema/08-gitea-ci-dryrun.md index b9ca1fd..44cfee6 100644 --- a/.planning/phase-1-slice-1-schema/08-gitea-ci-dryrun.md +++ b/.planning/phase-1-slice-1-schema/08-gitea-ci-dryrun.md @@ -126,4 +126,50 @@ Build a Gitea Actions workflow that on push to `main` (when relevant paths chang ## Done -(Fill in commit SHA + one-line note when this lands.) +**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:** + +1. **`DB_HOST=localhost`, not `DB_HOST=postgres`.** The spec's draft mixed `--network host` with service-name resolution; those are mutually exclusive. With `--network host` the docker-run container shares the runner's loopback, so the service's port mapping (`5432:5432`) is reachable as `localhost:5432`, not by service name `postgres`. (Service-name resolution would only work with the runner's default bridge network.) +2. **`--health-retries 20`** instead of 10. The `timescaledb-ha:*-all` image runs more init work at startup than vanilla postgres and occasionally exceeds the 50s window on cold runner images. 20 retries × 5s = 100s margin. +3. **`--health-cmd "pg_isready -U directus -d directus"`** with explicit `-d`. Spec had user only. +4. **`curl -fsS -X POST`** for the Portainer webhook step. Bare `curl -X POST` returns 0 even on HTTP 4xx/5xx; `-f` makes a misconfigured webhook URL fail the step explicitly. +5. **Plain `docker build`**, NOT `docker/build-push-action@v5`. The dry-run step needs the freshly-built image accessible to a subsequent `docker run`. `build-push-action` with the docker-container Buildx driver exports into a separate buildkitd cache that `docker run` cannot see — the run would fail with "image not found." Plain `docker build` keeps 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`, `:`). +- ✅ 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.yaml` to 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:main` after 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 registry +- `REGISTRY_PASSWORD` — password or PAT for that user +- `PORTAINER_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.