Publish prebuilt CI images to the Gitea container registry
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
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
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
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
# CI image for unsupervised-scheduler, one tag per PHP version.
|
||||||
|
#
|
||||||
|
# Built and published by .gitea/workflows/ci-images.yml to
|
||||||
|
# git.unsupervised.ca/unsupervised/ci-php:<php-version>. CI and release jobs
|
||||||
|
# run inside it via `container:`, so nothing installs PHP at job time.
|
||||||
|
#
|
||||||
|
# Why: setup-php installs 8.3+ through apt/the ondrej PPA on these arm64
|
||||||
|
# runners — a ~145s floor against ~35s for 8.1/8.2, with a tail that has
|
||||||
|
# twice crossed into hard failure (#178). Pulling a ~120MB image from the
|
||||||
|
# registry in our own cluster replaces that entirely (#187).
|
||||||
|
|
||||||
|
ARG PHP_VERSION=8.3
|
||||||
|
FROM php:${PHP_VERSION}-cli-alpine
|
||||||
|
|
||||||
|
# bash and nodejs are not optional: act_runner executes JavaScript actions
|
||||||
|
# (actions/checkout, actions/cache, actions/upload-artifact) *inside* the job
|
||||||
|
# container, and shells `run:` steps through bash.
|
||||||
|
#
|
||||||
|
# coreutils, gawk, grep and sed replace the busybox applets with the GNU ones
|
||||||
|
# the workflow scripts are written against (`tac`, `grep --include`).
|
||||||
|
#
|
||||||
|
# jq, curl, git and zip/unzip are used by release.yml and bin/build-zip.sh.
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
coreutils \
|
||||||
|
curl \
|
||||||
|
gawk \
|
||||||
|
git \
|
||||||
|
grep \
|
||||||
|
jq \
|
||||||
|
nodejs \
|
||||||
|
sed \
|
||||||
|
unzip \
|
||||||
|
zip \
|
||||||
|
icu-libs \
|
||||||
|
libzip \
|
||||||
|
&& apk add --no-cache --virtual .build-deps \
|
||||||
|
$PHPIZE_DEPS \
|
||||||
|
icu-dev \
|
||||||
|
libzip-dev \
|
||||||
|
&& docker-php-ext-install -j"$(nproc)" intl zip \
|
||||||
|
&& apk del --no-network .build-deps
|
||||||
|
|
||||||
|
# mbstring is compiled into the official php images; intl and zip are added
|
||||||
|
# above. That covers what phpunit, phpstan, phpcs and Composer need.
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# Jobs run as root inside the container, and never answer prompts.
|
||||||
|
ENV COMPOSER_ALLOW_SUPERUSER=1 \
|
||||||
|
COMPOSER_NO_INTERACTION=1 \
|
||||||
|
COMPOSER_HOME=/composer
|
||||||
|
|
||||||
|
RUN mkdir -p "$COMPOSER_HOME" \
|
||||||
|
&& php -v \
|
||||||
|
&& php -m | grep -qx intl \
|
||||||
|
&& php -m | grep -qx mbstring \
|
||||||
|
&& composer --version
|
||||||
|
|
||||||
|
CMD ["/bin/bash"]
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
name: CI Images
|
||||||
|
|
||||||
|
# Publishes the per-PHP-version images that ci.yml and release.yml run inside
|
||||||
|
# (#187). Nothing else consumes them, so this workflow is the only place the
|
||||||
|
# registry path is written down.
|
||||||
|
#
|
||||||
|
# Triggers:
|
||||||
|
# - the Dockerfile or this workflow changing on main, so an edit ships;
|
||||||
|
# - the same paths on a pull request, which builds but does not push, so a
|
||||||
|
# broken Dockerfile is caught before it reaches main;
|
||||||
|
# - workflow_dispatch, to rebuild on demand;
|
||||||
|
# - weekly, so PHP patch releases and Alpine security updates land without
|
||||||
|
# anyone remembering to ask.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '.gitea/ci/Dockerfile'
|
||||||
|
- '.gitea/workflows/ci-images.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.gitea/ci/Dockerfile'
|
||||||
|
- '.gitea/workflows/ci-images.yml'
|
||||||
|
schedule:
|
||||||
|
- cron: '17 4 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# The instance ROOT_URL host — the container registry lives on the same host.
|
||||||
|
REGISTRY: git.unsupervised.ca
|
||||||
|
IMAGE: unsupervised/ci-php
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build CI image (PHP ${{ matrix.php }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
# One version failing should not hide whether the others built.
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Keep in step with the test matrix in ci.yml.
|
||||||
|
php:
|
||||||
|
- '8.1'
|
||||||
|
- '8.2'
|
||||||
|
- '8.3'
|
||||||
|
- '8.5'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# The images are built natively, so they carry the runner's
|
||||||
|
# architecture only. Fine while every runner is arm64; if a runner of a
|
||||||
|
# different architecture ever joins the pool it will overwrite these
|
||||||
|
# tags with its own arch and the others will fail to pull.
|
||||||
|
- name: Check Docker is available
|
||||||
|
run: |
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "No usable Docker daemon in the job container." >&2
|
||||||
|
echo "act_runner needs container.docker_host set (or left empty to autodetect)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker version --format 'client {{.Client.Version}} / server {{.Server.Version}} / arch {{.Server.Arch}}'
|
||||||
|
|
||||||
|
# secrets.GITHUB_TOKEN is the Actions task token and can write packages
|
||||||
|
# for the repository owner. REGISTRY_TOKEN is an escape hatch: set it to
|
||||||
|
# a PAT with package:write if the task token is ever refused.
|
||||||
|
- name: Log in to the container registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN || secrets.GITHUB_TOKEN }}" \
|
||||||
|
| docker login "${REGISTRY}" -u "${{ vars.REGISTRY_USER || github.actor }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
--pull \
|
||||||
|
--build-arg "PHP_VERSION=${{ matrix.php }}" \
|
||||||
|
--tag "${REGISTRY}/${IMAGE}:${{ matrix.php }}" \
|
||||||
|
--file .gitea/ci/Dockerfile \
|
||||||
|
.gitea/ci
|
||||||
|
|
||||||
|
# Pull requests build only — the tags on the registry are what the other
|
||||||
|
# workflows run inside, so only main and a manual dispatch move them.
|
||||||
|
- name: Push
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
run: |
|
||||||
|
image="${REGISTRY}/${IMAGE}:${{ matrix.php }}"
|
||||||
|
docker push "${image}"
|
||||||
|
echo "Published ${image}"
|
||||||
|
|
||||||
|
- name: Log out
|
||||||
|
if: always() && github.event_name != 'pull_request'
|
||||||
|
run: docker logout "${REGISTRY}" || true
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
# 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:<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.
|
||||||
Reference in New Issue
Block a user