Step 2 of #187, and the one that actually retires setup-php. Closes the issue.
Every job that needs PHP now runs inside the images published by ci-images.yml. Nothing installs PHP at job time, so the apt path behind #178 — a ~145s floor on 8.3 against ~35s on 8.1/8.2, with a tail that twice ran past the step timeout and failed the run — is gone, along with all three copies of the Keep downloaded .debs / Cache apt packages workaround.
Changes
Job
Before
After
quality
setup-php 8.3 + apt cache
container: …/ci-php:8.3
test
setup-php per matrix leg
container: …/ci-php:${{ matrix.php }}
build
setup-php 8.3 + apt cache
container: …/ci-php:8.3
release (release.yml)
setup-php 8.3
container: …/ci-php:8.3, plus a Composer cache it never had
no-debug
unchanged
unchanged — no PHP, and it wants the runner's GNU grep --include
bump-version
unchanged
unchanged — no PHP
The Composer cache path moves to /composer/cache, which is COMPOSER_HOME in the image.
Net −76 lines.
Also in here
docs/ci.md gets the image size corrected to 67MB, measured from the published manifest. #190's description said it carried that fix; it did not — I had made the edit on this branch before cutting the fix branch off main, so it never landed there. The tar and zstd content of #190 was accurate, only that one docs line was misattributed.
Why the registry path is written out five times
jobs.<id>.container.image cannot read the env context, so a workflow-level env: CI_IMAGE: does not work here. A repo variable would, but hardcoding keeps the path visible at the point of use; docs/ci.md is the single reference.
Verification
Already confirmed independently of this PR:
All four tags published, linux/arm64, 66.6MB compressed, anonymous pull works — no credentials: block needed.
GNU tar present and asserted in the image smoke test (#190), which is what actions/cache needs.
This PR's own run is the first time anything executes inside these images — actions/cache, composer install, PHPUnit, PHPCS and PHPStan across all four PHP versions.
Not covered by this run:release.yml only fires on a v* tag, so its awk / tac / jq / curl path stays unexercised until v1.5.5 ships. That is why the image carries GNU coreutils, gawk and jq rather than the busybox applets.
composer.lock is now tracked
composer.lock was gitignored, so composer install re-resolved the dependency graph on every job. Two consequences, both in scope for a PR about making CI trustworthy:
CI could quietly start testing different dependency versions than it did yesterday, with no commit to explain the change.
bin/build-zip.sh runs its own composer install --no-dev in the staging directory, so the run that produces the release zip is also the run that picks the versions — two builds of the same tag could ship different vendor trees.
Changes:
composer.lock removed from .gitignore and committed (1 prod / 38 dev packages).
Composer cache keys hash composer.lock instead of composer.json — the lock is what actually fingerprints the dependency set now.
bin/build-zip.sh stages the lock alongside composer.json so the production install resolves to the locked versions. It already deleted both from the staging directory before zipping, so the shipped plugin is byte-for-byte the same shape as before.
Verified the lock installs on the oldest supported version: nothing in it requires a PHP newer than the >=8.1 in composer.json.
Worth a follow-up, not done here: there is no config.platform.php pin, so a composer update run on a newer local PHP can produce a lock that fails to install on the 8.1 matrix leg. Setting platform.php to 8.1 would make resolution always target the declared minimum. Left out because it changes local dev resolution too, which felt like your call rather than mine.
Image definitions moved to Unsupervised/ci-php
The Dockerfile and its publishing workflow were only here because this was the first project to need them. They are project-agnostic, so they now live in Unsupervised/ci-php and any PHP project can consume the same tags. That repo's first publish (run 569) is green across 8.1–8.5.
The registry path is unchanged: Gitea names container images owner/image, not after the repository that pushes them, so git.unsupervised.ca/unsupervised/ci-php:<v> still resolves and nothing here needed repointing. The org-level REGISTRY_TOKEN reached the new repo with no extra setup.
Removed here: .gitea/ci/Dockerfile, .gitea/workflows/ci-images.yml. docs/ci.md is cut down to what is specific to this project — which job runs in which tag, the lock/cache arrangement, and how to add a matrix version. Image contents and publishing are documented in ci-php's README.
PHPCS and PHPStan split into separate jobs
They were folded into one job to halve the number of toolchain installs per run. With the toolchain in the image that saves nothing, so they are now phpcs and phpstan, running in parallel, and build needs both.
The point is that a coding-standards failure no longer stops the job before static analysis reports — previously one style violation hid every type error until you fixed it and pushed again.
Measured, run 566 (combined) vs run 570 (split):
combined
split
Total runner time
160s
164s
Critical path
42s
37s
4s more runner time, and the critical path got slightly shorter because the two now overlap rather than running back to back.
Workflow comments rewritten
No setup-php references left anywhere in .gitea/ — the comments describe what the jobs do rather than the step they replaced.
Tests
composer test — 966 tests, 2795 assertions, OK (locally, on PHP 8.5.9)
All three workflow files parse as YAML
bin/build-zip.sh traced against the image: cp, rm, mkdir, composer, zip all present
No CHANGELOG.md entry: CI tooling only, nothing user-visible.
Step 2 of #187, and the one that actually retires `setup-php`. Closes the issue.
Every job that needs PHP now runs inside the images published by `ci-images.yml`. Nothing installs PHP at job time, so the apt path behind #178 — a ~145s floor on 8.3 against ~35s on 8.1/8.2, with a tail that twice ran past the step timeout and failed the run — is gone, along with all three copies of the `Keep downloaded .debs` / `Cache apt packages` workaround.
## Changes
| Job | Before | After |
|---|---|---|
| `quality` | setup-php 8.3 + apt cache | `container: …/ci-php:8.3` |
| `test` | setup-php per matrix leg | `container: …/ci-php:${{ matrix.php }}` |
| `build` | setup-php 8.3 + apt cache | `container: …/ci-php:8.3` |
| `release` (release.yml) | setup-php 8.3 | `container: …/ci-php:8.3`, plus a Composer cache it never had |
| `no-debug` | unchanged | unchanged — no PHP, and it wants the runner's GNU `grep --include` |
| `bump-version` | unchanged | unchanged — no PHP |
The Composer cache path moves to `/composer/cache`, which is `COMPOSER_HOME` in the image.
Net −76 lines.
## Also in here
`docs/ci.md` gets the image size corrected to 67MB, measured from the published manifest. #190's description said it carried that fix; it did not — I had made the edit on this branch before cutting the fix branch off `main`, so it never landed there. The tar and zstd content of #190 was accurate, only that one docs line was misattributed.
## Why the registry path is written out five times
`jobs.<id>.container.image` cannot read the `env` context, so a workflow-level `env: CI_IMAGE:` does not work here. A repo variable would, but hardcoding keeps the path visible at the point of use; `docs/ci.md` is the single reference.
## Verification
Already confirmed independently of this PR:
- All four tags published, `linux/arm64`, 66.6MB compressed, anonymous pull works — no `credentials:` block needed.
- GNU tar present and asserted in the image smoke test (#190), which is what `actions/cache` needs.
This PR's own run is the first time anything executes inside these images — `actions/cache`, `composer install`, PHPUnit, PHPCS and PHPStan across all four PHP versions.
**Not covered by this run:** `release.yml` only fires on a `v*` tag, so its `awk` / `tac` / `jq` / `curl` path stays unexercised until v1.5.5 ships. That is why the image carries GNU coreutils, gawk and jq rather than the busybox applets.
## composer.lock is now tracked
`composer.lock` was gitignored, so `composer install` re-resolved the dependency graph on every job. Two consequences, both in scope for a PR about making CI trustworthy:
- CI could quietly start testing different dependency versions than it did yesterday, with no commit to explain the change.
- `bin/build-zip.sh` runs its own `composer install --no-dev` in the staging directory, so the run that produces the release zip is also the run that picks the versions — two builds of the same tag could ship different vendor trees.
Changes:
- `composer.lock` removed from `.gitignore` and committed (1 prod / 38 dev packages).
- Composer cache keys hash `composer.lock` instead of `composer.json` — the lock is what actually fingerprints the dependency set now.
- `bin/build-zip.sh` stages the lock alongside `composer.json` so the production install resolves to the locked versions. It already deleted both from the staging directory before zipping, so the shipped plugin is byte-for-byte the same shape as before.
Verified the lock installs on the oldest supported version: nothing in it requires a PHP newer than the `>=8.1` in `composer.json`.
**Worth a follow-up, not done here:** there is no `config.platform.php` pin, so a `composer update` run on a newer local PHP can produce a lock that fails to install on the 8.1 matrix leg. Setting `platform.php` to `8.1` would make resolution always target the declared minimum. Left out because it changes local dev resolution too, which felt like your call rather than mine.
## Image definitions moved to Unsupervised/ci-php
The `Dockerfile` and its publishing workflow were only here because this was the first project to need them. They are project-agnostic, so they now live in [Unsupervised/ci-php](https://git.unsupervised.ca/Unsupervised/ci-php) and any PHP project can consume the same tags. That repo's first publish (run 569) is green across 8.1–8.5.
The registry path is unchanged: Gitea names container images `owner/image`, not after the repository that pushes them, so `git.unsupervised.ca/unsupervised/ci-php:<v>` still resolves and nothing here needed repointing. The org-level `REGISTRY_TOKEN` reached the new repo with no extra setup.
Removed here: `.gitea/ci/Dockerfile`, `.gitea/workflows/ci-images.yml`. `docs/ci.md` is cut down to what is specific to this project — which job runs in which tag, the lock/cache arrangement, and how to add a matrix version. Image contents and publishing are documented in ci-php's README.
## PHPCS and PHPStan split into separate jobs
They were folded into one job to halve the number of toolchain installs per run. With the toolchain in the image that saves nothing, so they are now `phpcs` and `phpstan`, running in parallel, and `build` needs both.
The point is that a coding-standards failure no longer stops the job before static analysis reports — previously one style violation hid every type error until you fixed it and pushed again.
Measured, run 566 (combined) vs run 570 (split):
| | combined | split |
|---|---|---|
| Total runner time | 160s | 164s |
| Critical path | 42s | 37s |
4s more runner time, and the critical path got slightly shorter because the two now overlap rather than running back to back.
## Workflow comments rewritten
No `setup-php` references left anywhere in `.gitea/` — the comments describe what the jobs do rather than the step they replaced.
## Tests
- `composer test` — 966 tests, 2795 assertions, OK (locally, on PHP 8.5.9)
- All three workflow files parse as YAML
- `bin/build-zip.sh` traced against the image: `cp`, `rm`, `mkdir`, `composer`, `zip` all present
No `CHANGELOG.md` entry: CI tooling only, nothing user-visible.
Replaces setup-php with `container:` in every job that needs PHP. The images
published by ci-images.yml already carry PHP, Composer, the intl and zip
extensions and the CLI tools the workflow scripts shell out to, so nothing
installs PHP at job time any more.
That removes the apt path #178 was about: a ~145s floor on 8.3 against ~35s
on 8.1/8.2, with a tail that twice ran past the step timeout and failed the
run. All three copies of the `Keep downloaded .debs` / `Cache apt packages`
workaround go with it.
no-debug and bump-version stay on the runner image — neither needs PHP, and
no-debug wants the runner's GNU `grep --include`.
The Composer cache moves to /composer/cache, which is COMPOSER_HOME in the
image, and release.yml gains the cache step it never had.
Also corrects the image size in docs/ci.md to the 67MB measured from the
published manifest; the ~120MB in the original was an estimate.
Closes#187
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01D9acV1mHktGAb1uyvNmrR2
composer.lock was gitignored, so `composer install` resolved the graph afresh
on every job. CI could quietly start testing different dependency versions
than it did the day before, and two builds of the same tag could ship
different vendor trees — the run that produces the zip is the same run that
picks the versions.
Track the lock, and make the two places that consume it use it:
- the Composer cache keys hash composer.lock rather than composer.json, since
the lock is now what actually fingerprints the dependency set;
- bin/build-zip.sh stages the lock alongside composer.json, so the --no-dev
production install resolves to the locked versions. It already deleted both
from the staging directory before zipping, so the shipped plugin is
unchanged.
The current lock installs cleanly on 8.1: nothing in it requires a PHP newer
than the >=8.1 composer.json declares.
Closes#187
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01D9acV1mHktGAb1uyvNmrR2
docs/ci.md and the workflow comment both described secrets.GITHUB_TOKEN as
the working credential with REGISTRY_TOKEN as a fallback. That is backwards:
the task token is rejected by Gitea's container registry (go-gitea/gitea#23642)
and the first publish attempt failed on exactly that. REGISTRY_TOKEN is
required.
Part of #187
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01D9acV1mHktGAb1uyvNmrR2
The image definition and its publishing workflow were only here because this
was the first project to need them. They are project-agnostic, so they now
live in Unsupervised/ci-php and any PHP project can consume the same tags.
The registry path is unchanged — Gitea container images are named
owner/image, not after the repository that pushes them — so nothing here
needs repointing.
Split the combined quality job into separate phpcs and phpstan jobs. They
were folded together to halve the number of toolchain installs per run; with
the toolchain in the image that saves nothing, and separating them means a
coding-standards failure no longer hides the static analysis result.
Rewrite the workflow comments to describe what the jobs do rather than the
setup step they replaced, and cut docs/ci.md down to what is specific to this
project — the image contents and publishing are documented in ci-php.
Closes#187
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01D9acV1mHktGAb1uyvNmrR2
thatguygriff
changed title from CI: run CI and release jobs inside the prebuilt CI images to CI: run jobs in shared ci-php images, commit composer.lock2026-08-25 01:40:45 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Step 2 of #187, and the one that actually retires
setup-php. Closes the issue.Every job that needs PHP now runs inside the images published by
ci-images.yml. Nothing installs PHP at job time, so the apt path behind #178 — a ~145s floor on 8.3 against ~35s on 8.1/8.2, with a tail that twice ran past the step timeout and failed the run — is gone, along with all three copies of theKeep downloaded .debs/Cache apt packagesworkaround.Changes
qualitycontainer: …/ci-php:8.3testcontainer: …/ci-php:${{ matrix.php }}buildcontainer: …/ci-php:8.3release(release.yml)container: …/ci-php:8.3, plus a Composer cache it never hadno-debuggrep --includebump-versionThe Composer cache path moves to
/composer/cache, which isCOMPOSER_HOMEin the image.Net −76 lines.
Also in here
docs/ci.mdgets the image size corrected to 67MB, measured from the published manifest. #190's description said it carried that fix; it did not — I had made the edit on this branch before cutting the fix branch offmain, so it never landed there. The tar and zstd content of #190 was accurate, only that one docs line was misattributed.Why the registry path is written out five times
jobs.<id>.container.imagecannot read theenvcontext, so a workflow-levelenv: CI_IMAGE:does not work here. A repo variable would, but hardcoding keeps the path visible at the point of use;docs/ci.mdis the single reference.Verification
Already confirmed independently of this PR:
linux/arm64, 66.6MB compressed, anonymous pull works — nocredentials:block needed.actions/cacheneeds.This PR's own run is the first time anything executes inside these images —
actions/cache,composer install, PHPUnit, PHPCS and PHPStan across all four PHP versions.Not covered by this run:
release.ymlonly fires on av*tag, so itsawk/tac/jq/curlpath stays unexercised until v1.5.5 ships. That is why the image carries GNU coreutils, gawk and jq rather than the busybox applets.composer.lock is now tracked
composer.lockwas gitignored, socomposer installre-resolved the dependency graph on every job. Two consequences, both in scope for a PR about making CI trustworthy:bin/build-zip.shruns its owncomposer install --no-devin the staging directory, so the run that produces the release zip is also the run that picks the versions — two builds of the same tag could ship different vendor trees.Changes:
composer.lockremoved from.gitignoreand committed (1 prod / 38 dev packages).composer.lockinstead ofcomposer.json— the lock is what actually fingerprints the dependency set now.bin/build-zip.shstages the lock alongsidecomposer.jsonso the production install resolves to the locked versions. It already deleted both from the staging directory before zipping, so the shipped plugin is byte-for-byte the same shape as before.Verified the lock installs on the oldest supported version: nothing in it requires a PHP newer than the
>=8.1incomposer.json.Worth a follow-up, not done here: there is no
config.platform.phppin, so acomposer updaterun on a newer local PHP can produce a lock that fails to install on the 8.1 matrix leg. Settingplatform.phpto8.1would make resolution always target the declared minimum. Left out because it changes local dev resolution too, which felt like your call rather than mine.Image definitions moved to Unsupervised/ci-php
The
Dockerfileand its publishing workflow were only here because this was the first project to need them. They are project-agnostic, so they now live in Unsupervised/ci-php and any PHP project can consume the same tags. That repo's first publish (run 569) is green across 8.1–8.5.The registry path is unchanged: Gitea names container images
owner/image, not after the repository that pushes them, sogit.unsupervised.ca/unsupervised/ci-php:<v>still resolves and nothing here needed repointing. The org-levelREGISTRY_TOKENreached the new repo with no extra setup.Removed here:
.gitea/ci/Dockerfile,.gitea/workflows/ci-images.yml.docs/ci.mdis cut down to what is specific to this project — which job runs in which tag, the lock/cache arrangement, and how to add a matrix version. Image contents and publishing are documented in ci-php's README.PHPCS and PHPStan split into separate jobs
They were folded into one job to halve the number of toolchain installs per run. With the toolchain in the image that saves nothing, so they are now
phpcsandphpstan, running in parallel, andbuildneeds both.The point is that a coding-standards failure no longer stops the job before static analysis reports — previously one style violation hid every type error until you fixed it and pushed again.
Measured, run 566 (combined) vs run 570 (split):
4s more runner time, and the critical path got slightly shorter because the two now overlap rather than running back to back.
Workflow comments rewritten
No
setup-phpreferences left anywhere in.gitea/— the comments describe what the jobs do rather than the step they replaced.Tests
composer test— 966 tests, 2795 assertions, OK (locally, on PHP 8.5.9)bin/build-zip.shtraced against the image:cp,rm,mkdir,composer,zipall presentNo
CHANGELOG.mdentry: CI tooling only, nothing user-visible.CI: run CI and release jobs inside the prebuilt CI imagesto CI: run jobs in shared ci-php images, commit composer.lock