CI: run jobs in shared ci-php images, commit composer.lock #191

Merged
thatguygriff merged 4 commits from feature/187-run-jobs-in-ci-images into main 2026-08-25 01:41:20 +00:00
Owner

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.
thatguygriff added 1 commit 2026-08-25 01:26:08 +00:00
Run CI and release jobs inside the prebuilt CI images
CI / Coding Standards & Static Analysis (pull_request) Successful in 37s
CI / Tests (PHP 8.1) (pull_request) Successful in 21s
CI / Tests (PHP 8.2) (pull_request) Successful in 20s
CI / Tests (PHP 8.3) (pull_request) Successful in 19s
CI / Tests (PHP 8.5) (pull_request) Successful in 19s
CI / No Debug Code (pull_request) Successful in 2s
CI / Build Plugin Zip (pull_request) Skipped
ab609898d6
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
thatguygriff added 1 commit 2026-08-25 01:33:08 +00:00
Commit composer.lock so CI and releases install a fixed dependency set
CI / Tests (PHP 8.2) (pull_request) Successful in 25s
CI / Tests (PHP 8.1) (pull_request) Successful in 26s
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.3) (pull_request) Successful in 32s
CI / Coding Standards & Static Analysis (pull_request) Successful in 42s
CI / Tests (PHP 8.5) (pull_request) Successful in 32s
CI / Build Plugin Zip (pull_request) Skipped
c9d18fec74
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
thatguygriff added 1 commit 2026-08-25 01:35:13 +00:00
Correct the registry auth docs: the Actions token does not work
CI Images / Build CI image (PHP 8.2) (pull_request) Successful in 3s
CI Images / Build CI image (PHP 8.5) (pull_request) Successful in 8s
CI / Tests (PHP 8.3) (pull_request) Successful in 30s
CI Images / Build CI image (PHP 8.3) (pull_request) Successful in 4s
CI / Tests (PHP 8.2) (pull_request) Successful in 23s
CI / Tests (PHP 8.1) (pull_request) Successful in 28s
CI / No Debug Code (pull_request) Successful in 4s
CI / Coding Standards & Static Analysis (pull_request) Successful in 43s
CI / Tests (PHP 8.5) (pull_request) Successful in 23s
CI / Build Plugin Zip (pull_request) Skipped
CI Images / Build CI image (PHP 8.1) (pull_request) Successful in 1m3s
f8762e1095
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
thatguygriff added 1 commit 2026-08-25 01:39:11 +00:00
Move the CI image definitions to the shared ci-php repository
CI / Coding Standards (pull_request) Successful in 14s
CI / Tests (PHP 8.1) (pull_request) Successful in 30s
CI / Tests (PHP 8.2) (pull_request) Successful in 30s
CI / Tests (PHP 8.3) (pull_request) Successful in 29s
CI / No Debug Code (pull_request) Successful in 3s
CI / Tests (PHP 8.5) (pull_request) Successful in 21s
CI / Static Analysis (pull_request) Successful in 37s
CI / Build Plugin Zip (pull_request) Skipped
1552bf4b5f
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.lock 2026-08-25 01:40:45 +00:00
thatguygriff merged commit a23490ec80 into main 2026-08-25 01:41:20 +00:00
thatguygriff deleted branch feature/187-run-jobs-in-ci-images 2026-08-25 01:41:20 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Unsupervised/unsupervised-scheduler#191