CI: run jobs in shared ci-php images, commit composer.lock #191
@@ -1,66 +0,0 @@
|
|||||||
# 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, sed and tar replace the busybox applets with the GNU
|
|
||||||
# ones the workflow scripts are written against (`tac`, `grep --include`).
|
|
||||||
# tar is not optional: actions/cache shells out to `tar --posix -P`, which
|
|
||||||
# busybox rejects outright, so every cache step fails without it. zstd is what
|
|
||||||
# actions/cache prefers over gzip when it is present.
|
|
||||||
#
|
|
||||||
# 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 \
|
|
||||||
tar \
|
|
||||||
unzip \
|
|
||||||
zip \
|
|
||||||
zstd \
|
|
||||||
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" \
|
|
||||||
&& tar --version | head -1 | grep -q 'GNU tar' \
|
|
||||||
&& php -v \
|
|
||||||
&& php -m | grep -qx intl \
|
|
||||||
&& php -m | grep -qx mbstring \
|
|
||||||
&& composer --version
|
|
||||||
|
|
||||||
CMD ["/bin/bash"]
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
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}}'
|
|
||||||
|
|
||||||
# REGISTRY_TOKEN (org secret, a PAT with the package scope) is what
|
|
||||||
# actually works. Gitea's Actions task token is rejected by the
|
|
||||||
# container registry — go-gitea/gitea#23642 — so the GITHUB_TOKEN
|
|
||||||
# fallback below only exists to keep this readable on an instance where
|
|
||||||
# that is ever fixed. See docs/ci.md.
|
|
||||||
- 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
|
|
||||||
+32
-18
@@ -7,29 +7,26 @@ on:
|
|||||||
- develop
|
- develop
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
# Every job that needs PHP runs inside a prebuilt image from the Gitea
|
# Jobs that need PHP run inside the shared CI images maintained in the
|
||||||
# container registry (see docs/ci.md). Nothing installs PHP at job time:
|
# Unsupervised/ci-php repository. PHP, Composer, the intl and zip extensions
|
||||||
# setup-php's apt path for 8.3+ was a ~145s floor against ~35s for 8.1/8.2,
|
# and the GNU CLI tools are already in the image, so there is no toolchain
|
||||||
# with a tail that twice ran past the step timeout and failed the run
|
# setup step in any job here.
|
||||||
# (#178, #187). The images are published by ci-images.yml; the org is public,
|
|
||||||
# so they pull without credentials.
|
|
||||||
#
|
#
|
||||||
# The registry path is spelled out in full at each use because
|
# The registry path is written out at each use because
|
||||||
# jobs.<id>.container.image cannot read the `env` context.
|
# jobs.<id>.container.image cannot read the `env` context.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# PHPCS and PHPStan share a job so the two of them install dependencies
|
phpcs:
|
||||||
# once rather than twice. They run as separate steps, and PHPCS failing
|
name: Coding Standards
|
||||||
# stops the job before PHPStan reports.
|
|
||||||
quality:
|
|
||||||
name: Coding Standards & Static Analysis
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: git.unsupervised.ca/unsupervised/ci-php:8.3
|
image: git.unsupervised.ca/unsupervised/ci-php:8.3
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# COMPOSER_HOME is /composer in the CI image.
|
# COMPOSER_HOME is /composer in the image, so that is where the
|
||||||
|
# download cache lives. composer.lock is what fingerprints the
|
||||||
|
# dependency set.
|
||||||
- name: Cache Composer packages
|
- name: Cache Composer packages
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -42,6 +39,23 @@ jobs:
|
|||||||
- name: Run PHPCS
|
- name: Run PHPCS
|
||||||
run: composer cs
|
run: composer cs
|
||||||
|
|
||||||
|
phpstan:
|
||||||
|
name: Static Analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: git.unsupervised.ca/unsupervised/ci-php:8.3
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache Composer packages
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: /composer/cache
|
||||||
|
key: composer-${{ hashFiles('composer.lock') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --prefer-dist --no-progress --no-interaction
|
||||||
|
|
||||||
- name: Run PHPStan
|
- name: Run PHPStan
|
||||||
run: composer lint
|
run: composer lint
|
||||||
|
|
||||||
@@ -53,8 +67,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# Adding a version here needs the matching image published first —
|
# A version can only be added here once ci-php publishes the matching
|
||||||
# see docs/ci.md.
|
# tag.
|
||||||
php:
|
php:
|
||||||
- '8.1'
|
- '8.1'
|
||||||
- '8.2'
|
- '8.2'
|
||||||
@@ -75,8 +89,8 @@ jobs:
|
|||||||
- name: Run PHPUnit
|
- name: Run PHPUnit
|
||||||
run: composer test
|
run: composer test
|
||||||
|
|
||||||
# No PHP needed, so this one stays on the runner image — and it wants GNU
|
# Runs on the runner image rather than a container: it needs no PHP, and it
|
||||||
# grep's --include, which the runner has.
|
# uses GNU grep's --include.
|
||||||
no-debug:
|
no-debug:
|
||||||
name: No Debug Code
|
name: No Debug Code
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -97,7 +111,7 @@ jobs:
|
|||||||
image: git.unsupervised.ca/unsupervised/ci-php:8.3
|
image: git.unsupervised.ca/unsupervised/ci-php:8.3
|
||||||
# Only build a shippable artifact once changes land on main, and only
|
# Only build a shippable artifact once changes land on main, and only
|
||||||
# after the quality gates pass.
|
# after the quality gates pass.
|
||||||
needs: [quality, test, no-debug]
|
needs: [phpcs, phpstan, test, no-debug]
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
name: Build and Publish Release
|
name: Build and Publish Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Runs in the prebuilt CI image rather than installing PHP — see
|
# The shared CI image carries composer, curl, jq and the GNU coreutils
|
||||||
# docs/ci.md and #187. The image carries composer, jq, curl and GNU
|
# the steps below shell out to. See docs/ci.md.
|
||||||
# coreutils, which is everything the steps below shell out to.
|
|
||||||
container:
|
container:
|
||||||
image: git.unsupervised.ca/unsupervised/ci-php:8.3
|
image: git.unsupervised.ca/unsupervised/ci-php:8.3
|
||||||
steps:
|
steps:
|
||||||
@@ -36,7 +35,8 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "version=${header_version}" >> "$GITHUB_OUTPUT"
|
echo "version=${header_version}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# COMPOSER_HOME is /composer in the CI image.
|
# COMPOSER_HOME is /composer in the image, so that is where the
|
||||||
|
# download cache lives.
|
||||||
- name: Cache Composer packages
|
- name: Cache Composer packages
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
+28
-65
@@ -1,7 +1,7 @@
|
|||||||
# CI images
|
# CI
|
||||||
|
|
||||||
CI and release jobs do not install PHP. They run inside prebuilt images
|
CI and release jobs do not install PHP. They run inside the shared images
|
||||||
published to the Gitea container registry:
|
maintained in [Unsupervised/ci-php](https://git.unsupervised.ca/Unsupervised/ci-php):
|
||||||
|
|
||||||
```
|
```
|
||||||
git.unsupervised.ca/unsupervised/ci-php:8.1
|
git.unsupervised.ca/unsupervised/ci-php:8.1
|
||||||
@@ -10,73 +10,36 @@ git.unsupervised.ca/unsupervised/ci-php:8.3
|
|||||||
git.unsupervised.ca/unsupervised/ci-php:8.5
|
git.unsupervised.ca/unsupervised/ci-php:8.5
|
||||||
```
|
```
|
||||||
|
|
||||||
The `Unsupervised` org is public, so the packages pull anonymously — jobs need
|
The `Unsupervised` org is public, so they pull anonymously — no registry
|
||||||
no registry credentials to use them.
|
credentials in any job here. What the images contain, how they are published,
|
||||||
|
and how to add a PHP version are documented in that repository's README.
|
||||||
|
|
||||||
## Why
|
## Which job runs where
|
||||||
|
|
||||||
`shivammathur/setup-php` installs PHP 8.3+ from apt/the ondrej PPA on these
|
| Job | Runs in |
|
||||||
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
|
| Coding Standards (PHPCS) | `ci-php:8.3` |
|
||||||
(#178). Caching the `.deb`s helped, but the apt step itself remained, and PHP
|
| Static Analysis (PHPStan) | `ci-php:8.3` |
|
||||||
8.5 has the same shape of problem. Pulling a 67MB image from a registry
|
| Tests | `ci-php:${{ matrix.php }}` |
|
||||||
inside the cluster replaces the whole thing (#187).
|
| Build Plugin Zip | `ci-php:8.3` |
|
||||||
|
| No Debug Code | runner image — no PHP, and it uses GNU `grep --include` |
|
||||||
|
| Open next-version bump PR (release.yml) | runner image — no PHP |
|
||||||
|
|
||||||
## What is in the image
|
PHPCS and PHPStan are separate jobs so a coding-standards failure still lets
|
||||||
|
the static analysis result through. They run in parallel.
|
||||||
|
|
||||||
`.gitea/ci/Dockerfile` builds on `php:<version>-cli-alpine` and adds:
|
## Composer
|
||||||
|
|
||||||
- **`bash` and `nodejs`** — act_runner runs JavaScript actions
|
`composer.lock` is committed, so every job installs the same dependency set
|
||||||
(`actions/checkout`, `actions/cache`, `actions/upload-artifact`) *inside*
|
and two builds of the same tag ship the same vendor tree. `bin/build-zip.sh`
|
||||||
the job container and shells `run:` steps through bash. Without these, the
|
stages the lock into its build directory for the same reason, then removes it
|
||||||
first step of every job fails.
|
before writing the zip.
|
||||||
- **`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
|
The Composer download cache lives at `/composer/cache` — `COMPOSER_HOME` is
|
||||||
|
`/composer` in the image — and is keyed on `composer.lock`.
|
||||||
|
|
||||||
`.gitea/workflows/ci-images.yml` builds and pushes them. It runs when the
|
## Adding a PHP version to the test matrix
|
||||||
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
|
The image has to exist first. Add the version to the `php` matrix in
|
||||||
|
`ci-php`'s `.gitea/workflows/publish.yml` and merge, then add it to the `test`
|
||||||
1. Add the version to the `php` matrix in `.gitea/workflows/ci-images.yml`.
|
matrix in `.gitea/workflows/ci.yml` here.
|
||||||
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.
|
|
||||||
|
|
||||||
## Registry authentication
|
|
||||||
|
|
||||||
The build pushes with the `REGISTRY_TOKEN` secret, set at the organisation
|
|
||||||
level. **This is required, not optional.** Gitea's Actions task token
|
|
||||||
(`secrets.GITHUB_TOKEN`) is rejected by the container registry —
|
|
||||||
`docker login` fails with `Get "https://git.unsupervised.ca/v2/":
|
|
||||||
unauthorized`. That is [go-gitea/gitea#23642][], open since 2023.
|
|
||||||
|
|
||||||
`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; if it ever needs to differ, set a `REGISTRY_USER`
|
|
||||||
variable and the workflow will prefer it.
|
|
||||||
|
|
||||||
[go-gitea/gitea#23642]: https://github.com/go-gitea/gitea/issues/23642
|
|
||||||
|
|||||||
Reference in New Issue
Block a user