One image per PHP version, published to git.unsupervised.ca/unsupervised/ci-php:<php-version>, so PHP projects can run their jobs with `container:` instead of installing PHP per job. Built on php:<version>-cli-alpine with Composer 2, the intl and zip extensions, and the GNU CLI tools workflow scripts expect. bash and nodejs are present because act_runner runs JavaScript actions inside the job container; GNU tar because actions/cache shells out to `tar --posix -P`, which busybox rejects. Covers 8.1 through 8.5. Pushing needs the REGISTRY_TOKEN organisation secret — Gitea's Actions task token cannot write packages (go-gitea/gitea#23642). Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01D9acV1mHktGAb1uyvNmrR2
3.6 KiB
ci-php
Shared PHP CI images for Gitea Actions, one tag per PHP version.
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.4
git.unsupervised.ca/unsupervised/ci-php:8.5
The Unsupervised org is public, so these pull anonymously — consuming jobs
need no registry credentials.
Using them
jobs:
test:
runs-on: ubuntu-latest
container:
image: git.unsupervised.ca/unsupervised/ci-php:${{ matrix.php }}
strategy:
matrix:
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: /composer/cache
key: ${{ matrix.php }}-composer-${{ hashFiles('composer.lock') }}
- run: composer install --prefer-dist --no-progress --no-interaction
- run: composer test
COMPOSER_HOME is /composer, so /composer/cache is the path to cache.
Note that jobs.<id>.container.image cannot read the env context, so the
registry path has to be written out at each use or come from a repo variable.
What is in them
Built on php:<version>-cli-alpine:
bash,nodejs— required by the runner, not by your build. act_runner executes JavaScript actions (actions/checkout,actions/cache,actions/upload-artifact) inside the job container and shellsrun:steps through bash. A job in an image without these fails on its first step.coreutils,gawk,grep,sed,tar— GNU rather than busybox.taris the one that matters:actions/cacheshells out totar --posix -P, which busybox rejects outright, so every cache step fails without it.zstdis whatactions/cacheprefers over gzip when present.curl,git,jq,zip,unzip— what release and packaging scripts usually reach for.- PHP extensions:
intlandzipon top of the official image, which already compiles inmbstring. Enough for phpunit, phpstan, phpcs and Composer. - Composer 2.
Roughly 67MB compressed.
If a project needs an extension that is not here, add it to the Dockerfile
rather than installing it at job time — that is the whole point of the image.
Publishing
.gitea/workflows/publish.yml builds and pushes. It runs when Dockerfile
changes on main, weekly so PHP patch releases and Alpine security updates
land unattended, and on workflow_dispatch. Pull requests build every
version without pushing.
Registry authentication
Pushing requires the REGISTRY_TOKEN secret, set at the organisation
level. This is not optional: Gitea's Actions task token
(secrets.GITHUB_TOKEN) is rejected by the container registry, failing with
Get "https://git.unsupervised.ca/v2/": unauthorized. See
go-gitea/gitea#23642.
REGISTRY_TOKEN is a personal access token with the package scope, Read
and Write. The workflow logs in as github.actor, which must be the account
that owns the token; set a REGISTRY_USER variable if it needs to differ.
Architecture
Images are built natively, so each carries the architecture of the runner
that built it. Every runner in the pool is arm64. If a runner of another
architecture joins, it will overwrite these tags with its own arch and the
rest will fail to pull — that is the point to switch to docker buildx and a
multi-arch manifest.
Adding a PHP version
Add it to the php matrix in .gitea/workflows/publish.yml and merge.
Consuming projects can only reference a tag once it has been published.