diff --git a/.gitea/ci/Dockerfile b/.gitea/ci/Dockerfile deleted file mode 100644 index 42503b3..0000000 --- a/.gitea/ci/Dockerfile +++ /dev/null @@ -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:. 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"] diff --git a/.gitea/workflows/ci-images.yml b/.gitea/workflows/ci-images.yml deleted file mode 100644 index 2af1028..0000000 --- a/.gitea/workflows/ci-images.yml +++ /dev/null @@ -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 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 143dcd4..927c226 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -7,29 +7,26 @@ on: - develop pull_request: -# Every job that needs PHP runs inside a prebuilt image from the Gitea -# container registry (see docs/ci.md). Nothing installs PHP at job time: -# setup-php's apt path for 8.3+ was 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, #187). The images are published by ci-images.yml; the org is public, -# so they pull without credentials. +# Jobs that need PHP run inside the shared CI images maintained in the +# Unsupervised/ci-php repository. PHP, Composer, the intl and zip extensions +# and the GNU CLI tools are already in the image, so there is no toolchain +# setup step in any job here. # -# The registry path is spelled out in full at each use because +# The registry path is written out at each use because # jobs..container.image cannot read the `env` context. jobs: - # PHPCS and PHPStan share a job so the two of them install dependencies - # once rather than twice. They run as separate steps, and PHPCS failing - # stops the job before PHPStan reports. - quality: - name: Coding Standards & Static Analysis + phpcs: + name: Coding Standards runs-on: ubuntu-latest container: image: git.unsupervised.ca/unsupervised/ci-php:8.3 steps: - 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 uses: actions/cache@v4 with: @@ -42,6 +39,23 @@ jobs: - name: Run PHPCS 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 run: composer lint @@ -53,8 +67,8 @@ jobs: strategy: fail-fast: false matrix: - # Adding a version here needs the matching image published first — - # see docs/ci.md. + # A version can only be added here once ci-php publishes the matching + # tag. php: - '8.1' - '8.2' @@ -75,8 +89,8 @@ jobs: - name: Run PHPUnit run: composer test - # No PHP needed, so this one stays on the runner image — and it wants GNU - # grep's --include, which the runner has. + # Runs on the runner image rather than a container: it needs no PHP, and it + # uses GNU grep's --include. no-debug: name: No Debug Code runs-on: ubuntu-latest @@ -97,7 +111,7 @@ jobs: image: git.unsupervised.ca/unsupervised/ci-php:8.3 # Only build a shippable artifact once changes land on main, and only # 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' steps: - uses: actions/checkout@v4 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 0abe8e3..f467431 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -15,9 +15,8 @@ jobs: release: name: Build and Publish Release runs-on: ubuntu-latest - # Runs in the prebuilt CI image rather than installing PHP — see - # docs/ci.md and #187. The image carries composer, jq, curl and GNU - # coreutils, which is everything the steps below shell out to. + # The shared CI image carries composer, curl, jq and the GNU coreutils + # the steps below shell out to. See docs/ci.md. container: image: git.unsupervised.ca/unsupervised/ci-php:8.3 steps: @@ -36,7 +35,8 @@ jobs: fi 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 uses: actions/cache@v4 with: diff --git a/docs/ci.md b/docs/ci.md index 8beadc7..aa45132 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -1,7 +1,7 @@ -# CI images +# CI -CI and release jobs do not install PHP. They run inside prebuilt images -published to the Gitea container registry: +CI and release jobs do not install PHP. They run inside the shared images +maintained in [Unsupervised/ci-php](https://git.unsupervised.ca/Unsupervised/ci-php): ``` 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 ``` -The `Unsupervised` org is public, so the packages pull anonymously — jobs need -no registry credentials to use them. +The `Unsupervised` org is public, so they pull anonymously — no registry +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 -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 67MB image from a registry -inside the cluster replaces the whole thing (#187). +| Job | Runs in | +|---|---| +| Coding Standards (PHPCS) | `ci-php:8.3` | +| Static Analysis (PHPStan) | `ci-php:8.3` | +| Tests | `ci-php:${{ matrix.php }}` | +| 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:-cli-alpine` and adds: +## Composer -- **`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. +`composer.lock` is committed, so every job installs the same dependency set +and two builds of the same tag ship the same vendor tree. `bin/build-zip.sh` +stages the lock into its build directory for the same reason, then removes it +before writing the zip. -## 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 -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 a PHP version to the test matrix -## 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. - -## 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 +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` +matrix in `.gitea/workflows/ci.yml` here.