# 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 `.deb`s 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:-cli-alpine` and adds: - **`bash` and `nodejs`** — act_runner runs JavaScript actions (`actions/checkout`, `actions/cache`, `actions/upload-artifact`) *inside* the job container and shells `run:` steps through bash. Without these, the first step of every job fails. - **`coreutils`, `gawk`, `grep`, `sed`, `tar`** — GNU versions, because the workflow scripts use `tac` and `grep --include`, which busybox does not provide. GNU `tar` matters most: `actions/cache` shells out to `tar --posix -P`, and busybox rejects those flags, so every cache step fails without it. `zstd` is what `actions/cache` reaches for over gzip when it is installed. - **`curl`, `jq`, `git`, `zip`, `unzip`** — used by `release.yml` and `bin/build-zip.sh`. - **`intl` and `zip` PHP extensions**, plus Composer 2. `mbstring` is 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 1. Add the version to the `php` matrix in `.gitea/workflows/ci-images.yml`. 2. Merge to `main`, or dispatch the workflow, and wait for the tag to appear. 3. Add the version to the `test` matrix 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.