Files
unsupervised-scheduler/docs/ci.md
T
thatguygriffandClaude Opus 5 572aaf5b49
CI Images / Build CI image (PHP 8.2) (pull_request) Successful in 1m14s
CI Images / Build CI image (PHP 8.1) (pull_request) Successful in 1m26s
CI Images / Build CI image (PHP 8.3) (pull_request) Successful in 2m3s
CI Images / Build CI image (PHP 8.5) (pull_request) Successful in 2m9s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m7s
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.3) (pull_request) Successful in 3m8s
CI / Tests (PHP 8.1) (pull_request) Successful in 5m55s
CI / Tests (PHP 8.5) (pull_request) Successful in 6m21s
CI / Coding Standards & Static Analysis (pull_request) Successful in 18m26s
CI / Build Plugin Zip (pull_request) Skipped
Publish prebuilt CI images to the Gitea container registry
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
2026-08-24 21:55:28 -03:00

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:

  • 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 — GNU versions, because the workflow scripts use tac and grep --include, which busybox does not provide.
  • 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.