From 2fbe7fc30ae3bc02c7cd93bb12d476603bb194f2 Mon Sep 17 00:00:00 2001 From: linkrobins Date: Tue, 28 Jul 2026 23:03:56 -0400 Subject: [PATCH 1/2] Pin the build's own inputs, and let tags name the Flarum they ship MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things, all closing #2. Extension Manager is now tested. It is 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 instead of resolved from the network, and a wrong owner or mode on the copied tree would leave the forum serving perfectly while silently refusing every extension install. roundtrip now installs fof/doorman the same way the Extension Manager does — composer, as www-data, against the volume — enables it, and checks the forum still serves. Build inputs pinned. redis came from pecl.php.net unpinned, which is why v1.0.0 needed three attempts while PECL returned 504s; a pin does not make PECL reliable but it does mean a retry produces the same extension rather than whatever is newest by then. Composer was worse: the installer was piped into php unverified and unversioned, so two builds of one commit could ship different Composers. It is now pinned to 2.10.2 with the signature check the Composer project publishes for this. Tags now name the forum. flarum- is read from the committed lock, so the tag answers the question users actually have — which Flarum is this — which a wrapper version number never could. Also emitted as image labels. --- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++++++++++++++ Dockerfile | 33 +++++++++++++++++++++++------ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 598bb2b..d1e4b2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,27 @@ 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. fof/doorman is small and has a stable 2.0 release. + - 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 fof/doorman --no-interaction --no-progress + docker compose exec -T -u www-data flarum php flarum extension:enable fof-doorman + 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; } + - name: Plant a marker in the database run: | docker compose exec -T mariadb mariadb -uroot -p"${DB_ROOT_PASS}" flarum \ @@ -238,6 +259,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 +287,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 From 8b21830826d669521ed584cf9000c1049f96e540 Mon Sep 17 00:00:00 2001 From: linkrobins Date: Tue, 28 Jul 2026 23:09:03 -0400 Subject: [PATCH 2/2] Test the volume, not an extension's compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first version of this step used fof/doorman and failed — but not because the seeded volume was wrong. composer require, extension:enable and cache:clear all succeeded; the forum then 500'd because doorman's Flarum 2 release is a beta against an RC core. Note the stable 2.0.0 is the Flarum 1.3 build, so composer had picked correctly. That conflated two things: whether the Extension Manager can write into a volume seeded from the image, and whether some third-party extension works on 2.0.0-rc.5. Only the first has anything to do with baking Flarum in, and only the first should be able to fail here. linkrobins/clipboard declares flarum/core ^2.0, has no dependencies of its own, and runs on a live 2.0 forum. It is installed, enabled, checked, then disabled and removed — the restore steps rebuild the volume from the image, which would not carry a third-party extension the backup's database still had enabled. --- .github/workflows/ci.yml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1e4b2c..c816a7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,17 +76,39 @@ jobs: # attribute to this change months later. # # Runs the same way the Extension Manager does: composer, as www-data, - # against the volume. fof/doorman is small and has a stable 2.0 release. + # 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 fof/doorman --no-interaction --no-progress - docker compose exec -T -u www-data flarum php flarum extension:enable fof-doorman + 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: |