setup-php installs PHP 8.3+ from apt on these arm64 runners: a ~145s floor against ~35s for 8.1/8.2, with a tail that twice ran past the step timeout and failed the run (#178). Caching the .debs softened it without removing the apt step, and 8.5 has the same problem. Add a per-version CI image built on php:<version>-cli-alpine and a workflow that publishes it to git.unsupervised.ca/unsupervised/ci-php:<version>. The org is public, so the packages pull anonymously. The image carries bash and nodejs because act_runner runs JavaScript actions inside the job container, GNU coreutils/grep/sed because the workflow scripts use `tac` and `grep --include`, and curl/jq/git/zip for release.yml and bin/build-zip.sh. Composer 2 and the intl and zip extensions round it out. Nothing consumes the images yet — ci.yml and release.yml switch over in a follow-up, because a job cannot run in an image that has not been published. Part of #187 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01D9acV1mHktGAb1uyvNmrR2
2.9 KiB
CI images
CI and release jobs do not install PHP. They run inside prebuilt images published to the Gitea container registry:
git.unsupervised.ca/unsupervised/ci-php:8.1
git.unsupervised.ca/unsupervised/ci-php:8.2
git.unsupervised.ca/unsupervised/ci-php:8.3
git.unsupervised.ca/unsupervised/ci-php:8.5
The Unsupervised org is public, so the packages pull anonymously — jobs need
no registry credentials to use them.
Why
shivammathur/setup-php installs PHP 8.3+ from apt/the ondrej PPA on these
arm64 runners. That was a ~145s floor against ~35s for 8.1 and 8.2, with a
tail that twice ran past the step timeout and failed the run outright
(#178). Caching the .debs helped, but the apt step itself remained, and PHP
8.5 has the same shape of problem. Pulling a ~120MB image from a registry
inside the cluster replaces the whole thing (#187).
What is in the image
.gitea/ci/Dockerfile builds on php:<version>-cli-alpine and adds:
bashandnodejs— act_runner runs JavaScript actions (actions/checkout,actions/cache,actions/upload-artifact) inside the job container and shellsrun:steps through bash. Without these, the first step of every job fails.coreutils,gawk,grep,sed— GNU versions, because the workflow scripts usetacandgrep --include, which busybox does not provide.curl,jq,git,zip,unzip— used byrelease.ymlandbin/build-zip.sh.intlandzipPHP extensions, plus Composer 2.mbstringis already compiled into the official images.
Publishing
.gitea/workflows/ci-images.yml builds and pushes them. It runs when the
Dockerfile changes on main, weekly (so PHP patch releases and Alpine
security updates land on their own), and on workflow_dispatch. On a pull
request it builds without pushing, so a broken Dockerfile is caught before it
reaches main.
Adding or dropping a PHP version
- Add the version to the
phpmatrix in.gitea/workflows/ci-images.yml. - Merge to
main, or dispatch the workflow, and wait for the tag to appear. - Add the version to the
testmatrix in.gitea/workflows/ci.yml.
Steps 2 and 3 cannot be one commit: a job cannot run in an image that has not been published yet.
Architecture
The images are built natively on whichever runner picks the job, so they carry
that runner's architecture only. Every runner in the pool is arm64 today. If
one of a different architecture ever joins, it will overwrite these tags with
its own arch and the rest will fail to pull — at which point the build needs
docker buildx and a multi-arch manifest.
If a push is refused
The build authenticates with secrets.GITHUB_TOKEN, the Actions task token.
If the registry ever refuses it, create a personal access token with
package:write, store it as the REGISTRY_TOKEN secret, and optionally set
the REGISTRY_USER variable — the workflow prefers both when present.