Add prebuilt CI image and images.yml workflow #57

Closed
opened 2026-08-15 13:43:05 +02:00 by myrmidex · 2 comments
Owner

Adopt ffr's CI-image approach. This is the concrete implementation of #46 — that ticket states the goal, this one states the method.

Current cost per CI run

.forgejo/workflows/ci.yml runs on catthehacker/ubuntu:act-latest and, every single run:

  1. Installs PHP 8.3 from scratch via setup-php with 8 extensions (pdo_mysql, mbstring, xml, dom, bcmath, gd, exif, pcntl) plus pcov
  2. Spins up a mysql:8.0 service container
  3. Polls it with a 30×2s wait loop before migrating

ffr does none of this — it runs directly in forge.lvl0.xyz/lvl0/fedi-feed-router-ci:php8.3-3, where PHP and extensions are already baked in, and the whole job is checkout → composer install → pint → phpstan → phpunit.

Scope

  • docker/build/Dockerfile.ci — PHP 8.3 + the extension set + composer
  • .forgejo/workflows/images.yml — mirror ffr's: matrix build, triggered on pushes to main touching docker/build/** or the workflow itself, plus workflow_dispatch. Tag :php8.3-N, :latest, :${{ github.sha }}
  • Rewrite ci.yml to run in that image and drop the setup-php step and the MySQL wait loop

Decisions to make during planning

Does the DB stay? ffr's CI has no service container. Whether incr still needs mysql:8.0 depends on how #54's tests are written — if they use SQLite in-memory or pure factories, the service container and the wait loop both disappear and the run gets dramatically faster. Worth deciding deliberately rather than inheriting.

Skip Dockerfile.base? ffr builds both a base and a CI image. incr already has docker/production/Dockerfile doing that job, so probably only the CI image is needed here.

Do not regress

incr's build.yml is ahead of ffr's — it builds linux/amd64,linux/arm64 via QEMU where ffr is amd64-only. Keep multi-arch.

Likewise ci.yml's coverage-parsing and PR-comment steps have no ffr equivalent and should be preserved. Note the comment step already has continue-on-error: true from 7c56df3 (DNS unreachable from the runner) — that workaround should be re-evaluated once CI runs in a controlled image.

Sequencing

Cheapest after #52. Once React is gone, the separate build job (npm ci + npm run build) shrinks to almost nothing or disappears, which changes what the image needs to contain.

Acceptance criteria

  • CI image builds and pushes via images.yml
  • ci.yml runs in it with no setup-php step
  • Wall-clock CI time measurably lower than the current baseline — record before/after in a comment
  • Multi-arch production build still works
  • Coverage PR comment still works
Adopt ffr's CI-image approach. This is the concrete implementation of #46 — that ticket states the goal, this one states the method. ## Current cost per CI run `.forgejo/workflows/ci.yml` runs on `catthehacker/ubuntu:act-latest` and, every single run: 1. Installs PHP 8.3 from scratch via `setup-php` with 8 extensions (`pdo_mysql, mbstring, xml, dom, bcmath, gd, exif, pcntl`) plus pcov 2. Spins up a `mysql:8.0` service container 3. Polls it with a 30×2s wait loop before migrating ffr does none of this — it runs directly in `forge.lvl0.xyz/lvl0/fedi-feed-router-ci:php8.3-3`, where PHP and extensions are already baked in, and the whole job is checkout → composer install → pint → phpstan → phpunit. ## Scope - `docker/build/Dockerfile.ci` — PHP 8.3 + the extension set + composer - `.forgejo/workflows/images.yml` — mirror ffr's: matrix build, triggered on pushes to `main` touching `docker/build/**` or the workflow itself, plus `workflow_dispatch`. Tag `:php8.3-N`, `:latest`, `:${{ github.sha }}` - Rewrite `ci.yml` to run in that image and drop the `setup-php` step and the MySQL wait loop ## Decisions to make during planning **Does the DB stay?** ffr's CI has no service container. Whether incr still needs `mysql:8.0` depends on how #54's tests are written — if they use SQLite in-memory or pure factories, the service container and the wait loop both disappear and the run gets dramatically faster. Worth deciding deliberately rather than inheriting. **Skip `Dockerfile.base`?** ffr builds both a base and a CI image. incr already has `docker/production/Dockerfile` doing that job, so probably only the CI image is needed here. ## Do not regress incr's `build.yml` is **ahead** of ffr's — it builds `linux/amd64,linux/arm64` via QEMU where ffr is amd64-only. Keep multi-arch. Likewise `ci.yml`'s coverage-parsing and PR-comment steps have no ffr equivalent and should be preserved. Note the comment step already has `continue-on-error: true` from 7c56df3 (DNS unreachable from the runner) — that workaround should be re-evaluated once CI runs in a controlled image. ## Sequencing Cheapest **after** #52. Once React is gone, the separate `build` job (`npm ci` + `npm run build`) shrinks to almost nothing or disappears, which changes what the image needs to contain. ## Acceptance criteria - [ ] CI image builds and pushes via `images.yml` - [ ] `ci.yml` runs in it with no `setup-php` step - [ ] Wall-clock CI time measurably lower than the current baseline — record before/after in a comment - [ ] Multi-arch production build still works - [ ] Coverage PR comment still works
myrmidex added this to the v0.4.0 milestone 2026-08-15 13:43:05 +02:00
myrmidex added the
enhancement
label 2026-08-15 13:43:05 +02:00
myrmidex self-assigned this 2026-08-15 13:43:05 +02:00
Author
Owner

⚠️ Bootstrap gap — CI will be red on release/0.4.0 until merge

ci.yml now runs in forge.lvl0.xyz/lvl0/incr-ci:php8.3-1, which does not exist yet. images.yml builds it only on pushes to main touching docker/build/**, and workflow_dispatch only appears in the Forgejo UI once the workflow exists on the default branch.

So the sequence is:

  1. Now — CI on this branch fails with an image pull error. Expected, not a regression.
  2. At merge to mainimages.yml fires, builds and pushes incr-ci:php8.3-1.
  3. From then on — CI runs green, and fast.

Accepted deliberately rather than diverging from ffr's main-only image convention or hand-seeding the registry. If the red run becomes a nuisance before merge, adding release/* to images.yml's branch filter closes the gap in one line.

Do not interpret the failing run as a broken pipeline — check the pull error names incr-ci before investigating anything else.

## ⚠️ Bootstrap gap — CI will be red on `release/0.4.0` until merge `ci.yml` now runs in `forge.lvl0.xyz/lvl0/incr-ci:php8.3-1`, **which does not exist yet**. `images.yml` builds it only on pushes to `main` touching `docker/build/**`, and `workflow_dispatch` only appears in the Forgejo UI once the workflow exists on the default branch. So the sequence is: 1. **Now** — CI on this branch fails with an image pull error. Expected, not a regression. 2. **At merge to `main`** — `images.yml` fires, builds and pushes `incr-ci:php8.3-1`. 3. **From then on** — CI runs green, and fast. Accepted deliberately rather than diverging from ffr's main-only image convention or hand-seeding the registry. If the red run becomes a nuisance before merge, adding `release/*` to `images.yml`'s branch filter closes the gap in one line. **Do not interpret the failing run as a broken pipeline** — check the pull error names `incr-ci` before investigating anything else.
Author
Owner

Done — 1fecc2b

docker/build/Dockerfile.ciphp:8.3-cli with pdo_mysql, mbstring, dom, xml, bcmath, fileinfo, pcntl, gd, pcov baked in, plus git/unzip/nodejs/netcat-openbsd and Composer. Debian rather than alpine, following ffr's note about DNS timeouts against codeload during composer install.

.forgejo/workflows/images.yml — matrix build on main pushes touching docker/build/**, plus workflow_dispatch. Tags :php8.3-1, :latest, :${{ github.sha }}.

ci.yml — now runs in the prebuilt image; the setup-php step (PHP 8.3 + 8 extensions + pcov, installed on every single run) is gone.

Only a CI image was added, no Dockerfile.basedocker/production/Dockerfile already covers the runtime, as the ticket anticipated.

Open decision resolved: the database stays

The ticket flagged this as a planning call. Settled with evidence rather than inherited from ffr:

  • tests/Feature/DropUsersMigrationTest.php:53 queries information_schema.columns
  • The migration tests exercise MySQL-specific DDL — dropForeign, the unsignedInteger ceiling, and the SQLSTATE[22003] overflow that caught a real bug during #50

Switching to SQLite in-memory would silently weaken exactly the tests that have been catching defects all milestone. The mysql:8.0 service and the readiness loop stay — hence netcat-openbsd in the image.

The win is therefore from baking PHP in, not from dropping the DB.

Two bugs caught while writing this

Composer cache path was wrong. ci.yml cached ~/.composer/cache, which is what catthehacker/ubuntu produced. On php:8.3-cli Composer uses ~/.cache/composer (ffr's value). Left unchanged, the cache key would have matched but the path would have been empty — a silent permanent cache miss, and the obvious "why is CI still slow" trap.

The build job would have broken. It runs npm ci, but the new CI image is PHP-only. Moved to node:22-bookworm-slim.

Not regressed

  • build.yml still builds linux/amd64,linux/arm64 — ahead of ffr, preserved as the ticket required
  • All 8 coverage lines preserved, including the continue-on-error PR comment from 7c56df3

The continue-on-error workaround is worth re-evaluating once runs happen in the controlled image, as the ticket suggests — but that needs a real run to assess, so it stays for now.

Outstanding: the measurement

"Wall-clock CI time measurably lower than the current baseline" cannot be checked yet — the image does not exist until v0.4.0 merges to main (see the bootstrap note above). That measurement belongs to #46, which stays open for it.

Gates

PHPUnit 33 tests, 81 assertions PASS. No app code touched.

## Done — `1fecc2b` **`docker/build/Dockerfile.ci`** — `php:8.3-cli` with `pdo_mysql`, `mbstring`, `dom`, `xml`, `bcmath`, `fileinfo`, `pcntl`, `gd`, `pcov` baked in, plus git/unzip/nodejs/netcat-openbsd and Composer. Debian rather than alpine, following ffr's note about DNS timeouts against codeload during composer install. **`.forgejo/workflows/images.yml`** — matrix build on `main` pushes touching `docker/build/**`, plus `workflow_dispatch`. Tags `:php8.3-1`, `:latest`, `:${{ github.sha }}`. **`ci.yml`** — now runs in the prebuilt image; the `setup-php` step (PHP 8.3 + 8 extensions + pcov, installed on every single run) is gone. Only a CI image was added, no `Dockerfile.base` — `docker/production/Dockerfile` already covers the runtime, as the ticket anticipated. ## Open decision resolved: the database stays The ticket flagged this as a planning call. Settled with evidence rather than inherited from ffr: - `tests/Feature/DropUsersMigrationTest.php:53` queries `information_schema.columns` - The migration tests exercise MySQL-specific DDL — `dropForeign`, the `unsignedInteger` ceiling, and the `SQLSTATE[22003]` overflow that caught a real bug during #50 Switching to SQLite in-memory would silently weaken exactly the tests that have been catching defects all milestone. **The `mysql:8.0` service and the readiness loop stay** — hence `netcat-openbsd` in the image. The win is therefore from baking PHP in, not from dropping the DB. ## Two bugs caught while writing this **Composer cache path was wrong.** `ci.yml` cached `~/.composer/cache`, which is what `catthehacker/ubuntu` produced. On `php:8.3-cli` Composer uses `~/.cache/composer` (ffr's value). Left unchanged, the cache key would have matched but the path would have been empty — a silent permanent cache miss, and the obvious "why is CI still slow" trap. **The `build` job would have broken.** It runs `npm ci`, but the new CI image is PHP-only. Moved to `node:22-bookworm-slim`. ## Not regressed - `build.yml` still builds `linux/amd64,linux/arm64` — ahead of ffr, preserved as the ticket required - All 8 coverage lines preserved, including the `continue-on-error` PR comment from `7c56df3` The `continue-on-error` workaround is worth re-evaluating once runs happen in the controlled image, as the ticket suggests — but that needs a real run to assess, so it stays for now. ## Outstanding: the measurement "Wall-clock CI time measurably lower than the current baseline" **cannot be checked yet** — the image does not exist until v0.4.0 merges to `main` (see the bootstrap note above). That measurement belongs to #46, which stays open for it. ## Gates PHPUnit 33 tests, 81 assertions PASS. No app code touched.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/incr#57
No description provided.