diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 598bb2b..c816a7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,49 @@ jobs: fi echo "$logs" | grep 'from the baked skeleton' + # The one behaviour baking Flarum into the image could plausibly disturb. + # The volume is still meant to be a writable Composer project — merely + # SEEDED from the image rather than resolved from the network — and the + # Extension Manager depends on exactly that. A wrong owner or mode on the + # copied tree would leave the forum serving perfectly while silently + # refusing every extension install, which is not a failure anyone would + # attribute to this change months later. + # + # Runs the same way the Extension Manager does: composer, as www-data, + # against the volume. + # + # The extension is chosen so a failure here can only mean the volume is + # wrong. linkrobins/clipboard declares flarum/core ^2.0, has zero + # dependencies of its own, and runs on a live 2.0 forum. An earlier + # version of this step used fof/doorman and failed — not because the + # seeded volume was broken (composer require, extension:enable and + # cache:clear all succeeded) but because doorman's Flarum 2 release is a + # beta that 500s against an RC core. That conflated "can the Extension + # Manager write here" with "is this extension compatible", and only the + # first has anything to do with baking Flarum into the image. + # + # Removed again afterwards: the restore steps below rebuild the volume + # from the image, which would not contain a third-party extension the + # backup's database still had enabled. + - name: Extension Manager can still install into the seeded volume + run: | + docker compose exec -T -u www-data \ + -e COMPOSER_HOME=/var/www/html/storage/composer-cache \ + -e COMPOSER_MEMORY_LIMIT=-1 \ + flarum composer require linkrobins/clipboard --no-interaction --no-progress + docker compose exec -T -u www-data flarum php flarum extension:enable linkrobins-clipboard + docker compose exec -T -u www-data flarum php flarum cache:clear + curl -fsS http://localhost/ | grep -qi 'flarum' \ + || { echo "::error::forum stopped serving after an extension install"; exit 1; } + docker compose exec -T -u www-data flarum php flarum extension:disable linkrobins-clipboard + docker compose exec -T -u www-data \ + -e COMPOSER_HOME=/var/www/html/storage/composer-cache \ + -e COMPOSER_MEMORY_LIMIT=-1 \ + flarum composer remove linkrobins/clipboard --no-interaction --no-progress + docker compose exec -T -u www-data flarum php flarum cache:clear + curl -fsS http://localhost/ | grep -qi 'flarum' \ + || { echo "::error::forum stopped serving after the extension was removed"; exit 1; } + - name: Plant a marker in the database run: | docker compose exec -T mariadb mariadb -uroot -p"${DB_ROOT_PASS}" flarum \ @@ -238,6 +281,24 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + # Read the Flarum version out of the committed lock so image tags can name + # the forum they ship. An image tag that only versions the wrapper cannot + # answer the question users actually have — "which Flarum is this?" — and + # nothing else in the tag list ever will. + - uses: actions/checkout@v4 + - name: Read the pinned Flarum version + id: flarum + run: | + v="$(php -r ' + $l = json_decode(file_get_contents("skeleton/composer.lock"), true); + foreach ($l["packages"] as $p) { + if ($p["name"] === "flarum/core") { echo ltrim($p["version"], "v"); exit; } + } + exit(1); + ')" + echo "version=$v" >> "$GITHUB_OUTPUT" + echo "Flarum $v" + - name: Image tags + labels id: meta uses: docker/metadata-action@v5 @@ -248,6 +309,12 @@ jobs: type=sha,format=short type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} + # flarum-: what forum this image contains, independent of + # the wrapper's own release number. Moves only when the lock does. + type=raw,value=flarum-${{ steps.flarum.outputs.version }},enable=${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + labels: | + org.opencontainers.image.version=${{ steps.flarum.outputs.version }} + com.linkrobins.flarum.version=${{ steps.flarum.outputs.version }} - name: Create the multi-arch manifest working-directory: /tmp/digests run: | diff --git a/Dockerfile b/Dockerfile index be01c57..738cfa0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,13 @@ RUN apt-get update \ # install-php-extensions pinned to a release tag (not /latest) so a compromised # release can't slip into a build without a git diff. +# +# redis is pinned too. Everything else here is compiled from the PHP source +# already in the base image, but redis is fetched from pecl.php.net at build +# time — which is why v1.0.0 needed three attempts while PECL returned 504s. +# A pin does not make PECL more reliable, but it does mean a retry produces the +# SAME extension rather than whatever is newest by then. +ARG PECL_REDIS_VERSION=6.3.0 RUN curl -sSLf --retry 5 --retry-delay 2 --retry-connrefused \ -o /usr/local/bin/install-php-extensions \ https://github.com/mlocati/docker-php-extension-installer/releases/download/2.11.1/install-php-extensions \ @@ -30,13 +37,27 @@ RUN curl -sSLf --retry 5 --retry-delay 2 --retry-connrefused \ && install-php-extensions \ bcmath ctype curl dom exif fileinfo filter gd hash intl json \ mbstring openssl pcre pdo session sodium tokenizer xml \ - pdo_mysql opcache redis pcntl sockets zip + pdo_mysql opcache "redis-${PECL_REDIS_VERSION}" pcntl sockets zip -# Composer baked in (Flarum create-project / extension requires run at runtime -# against the data volume, but the binary is present in the image). -RUN curl -sS --retry 5 --retry-delay 2 --retry-connrefused \ - https://getcomposer.org/installer \ - | php -- --install-dir=/usr/local/bin --filename=composer +# Composer pinned, and the installer's signature checked before it is run. +# +# This used to pipe whatever getcomposer.org served straight into php, +# unverified and unversioned: a bad day upstream became a bad image, and two +# builds of the same commit could ship different Composer versions. The +# signature check is the one the Composer project publishes for exactly this. +ARG COMPOSER_VERSION=2.10.2 +RUN set -eux; \ + curl -sSLf --retry 5 --retry-delay 2 --retry-connrefused \ + -o /tmp/composer-setup.php https://getcomposer.org/installer; \ + expected="$(curl -sSLf --retry 5 https://composer.github.io/installer.sig)"; \ + actual="$(php -r 'echo hash_file("sha384", "/tmp/composer-setup.php");')"; \ + if [ "$expected" != "$actual" ]; then \ + echo "composer installer signature mismatch" >&2; exit 1; \ + fi; \ + php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer \ + --version="${COMPOSER_VERSION}"; \ + rm -f /tmp/composer-setup.php; \ + composer --version # ── The pinned Flarum skeleton ─────────────────────────────────────────────── # Flarum itself, resolved at BUILD time from the committed lock rather than