ci: multi-arch (amd64+arm64) publish for cac, cac-fpm, shared-httpd #40

Merged
jknapp merged 1 commits from feat/multiarch-matrix into trunk 2026-11-12 14:50:27 +00:00
Owner

What

Multi-arch (linux/amd64 + linux/arm64) publishing for cac:php82..85, cac-fpm:php82..85 and shared-httpd:latest (WHP image pipeline plan T9, docs/arm64/IMAGE-PIPELINE-PLAN.md 4.1/4.4). Workflow + tests only; no Dockerfile change (so no overlap with fix/disable-http2-md-modules).

Design

Per matrix leg, ONE job on the trusted x86 runner (same template as haproxy-manager-base): build amd64 (native) and arm64 (QEMU), each pushed BY DIGEST (untagged) to Gitea, then scripts/assemble-multiarch-index.sh creates the existing tags (:phpNN, :latest for php85; shared-httpd:latest) from this run's digests. arm64 is continue-on-error with its own timeout: if it fails/times out the tag is published amd64-only exactly as today, and an old arm64 is never re-attached (digest read only from the step output). amd64 failure = nothing published. Unchanged: triggers, matrix, fail-fast: false, base-image digest pin, pkg-cache probe/mirror, cache-bust, php.net EXPECT_PHP_MIN gate (runs on the arm64 binary too), build args, secrets (Gitea only; CAC has never pushed to GHCR). cac-litespeed / cac-lsphp / shared-ols jobs untouched (still amd64-only).

Timeouts (T3: cac-fpm:php85 amd64 1m41s, arm64 under QEMU 7m47s, Pi native 5m33s): cac / cac-fpm: arm64 step 45 min (~5.8x T3), job 75 min; shared-httpd: arm64 25, job 50. Expected wall time per php leg ~10-12 min (2 amd64 + ~8 arm64 + setup); 9 jobs, so ~15 min if the runner runs them in parallel, ~100 min if capacity is 1.

Arch audit (Dockerfile, Dockerfile.fpm, Dockerfile.shared-httpd, scripts/, configs/)

No new assumptions: mariadb.repo sed already arch-aware (PR #37); wp-cli/composer phars arch-neutral; no downloaded binaries. dnf repoquery in almalinux:10 aarch64 under QEMU for the exact install-php82/83/84 lists: every package resolves on aarch64 (php 8.2.35 / 8.3.36 / 8.4.27; php-pecl-redis5 resolves via Provides of php-pecl-redis6, identical on x86_64). php85 per T3. shared-httpd builds and runs on arm64.

Tests

scripts/tests/multiarch-workflow.test.py (added to Shell-Checks; no PyYAML needed): triggers, matrix/fail-fast, step order, arm64 continue-on-error and amd64 fatal, timeouts bound, by-digest outputs and no tags:/push: in build steps, identical build-args for both platforms, assemble uses this run's outputs, exact tag scheme (executes the assemble step's shell for php82..85 and shared-httpd), no arm64 outside the slice. 12 tests pass; against trunk's workflow 8 of 12 fail. Also fixes index-platforms.test.sh:17 (| head -1 tripped the early-exit-reader scan in lsphp-info-probe.test.sh, red on trunk).

Local proof (throwaway registry:2, buildx docker-container builder, nothing published)

cac-fpm:php85 and shared-httpd built for both platforms through the same outputs: form + assemble-multiarch-index.sh; imagetools inspect shows linux/amd64, linux/arm64 and one attestation manifest per platform. arm64 image under QEMU: php 8.5.12 ... aarch64, php-fpm -t OK, php-pecl-redis6/imagick installed; shared-httpd httpd 2.4.63 aarch64, httpd -t Syntax OK. Forced arm64 failure (bad base) -> outcome failure -> index is amd64-only. amd64 content vs a plain build of the same Dockerfile: 486 rpms identical, image config identical, file list/hashes identical except runtime /etc/hosts|hostname, fontconfig/ldconfig/dnf caches and rpmdb.sqlite (build-time nondeterminism).

NOT verified

Real runner (buildx version there, QEMU speed under load, runner capacity), Gitea registry accepting these indexes (HMB proves the pattern), pkg-cache path for arm64 (CT130 serves $basearch paths; untested for aarch64), cac: (Apache) image not built locally (same Dockerfile pattern as fpm + httpd/mod_ssl, proven by shared-httpd).

SAFE MERGE

A merge to trunk triggers the whole matrix. Merge with [skip ci] in the title, record rollback digests first (see PR comment), then dispatch manually.

🤖 Generated with Claude Code

https://claude.ai/code/session_0185ukXjXyosSkunDsdWz3mb

## What Multi-arch (linux/amd64 + linux/arm64) publishing for `cac:php82..85`, `cac-fpm:php82..85` and `shared-httpd:latest` (WHP image pipeline plan T9, `docs/arm64/IMAGE-PIPELINE-PLAN.md` 4.1/4.4). Workflow + tests only; **no Dockerfile change** (so no overlap with `fix/disable-http2-md-modules`). ## Design Per matrix leg, ONE job on the trusted x86 runner (same template as haproxy-manager-base): build amd64 (native) and arm64 (QEMU), each pushed BY DIGEST (untagged) to Gitea, then `scripts/assemble-multiarch-index.sh` creates the existing tags (`:phpNN`, `:latest` for php85; `shared-httpd:latest`) from this run's digests. arm64 is `continue-on-error` with its own timeout: if it fails/times out the tag is published amd64-only exactly as today, and an old arm64 is never re-attached (digest read only from the step output). amd64 failure = nothing published. Unchanged: triggers, matrix, `fail-fast: false`, base-image digest pin, pkg-cache probe/mirror, cache-bust, php.net `EXPECT_PHP_MIN` gate (runs on the arm64 binary too), build args, secrets (Gitea only; CAC has never pushed to GHCR). cac-litespeed / cac-lsphp / shared-ols jobs untouched (still amd64-only). Timeouts (T3: cac-fpm:php85 amd64 1m41s, arm64 under QEMU 7m47s, Pi native 5m33s): cac / cac-fpm: arm64 step 45 min (~5.8x T3), job 75 min; shared-httpd: arm64 25, job 50. Expected wall time per php leg ~10-12 min (2 amd64 + ~8 arm64 + setup); 9 jobs, so ~15 min if the runner runs them in parallel, ~100 min if capacity is 1. ## Arch audit (Dockerfile, Dockerfile.fpm, Dockerfile.shared-httpd, scripts/, configs/) No new assumptions: mariadb.repo sed already arch-aware (PR #37); wp-cli/composer phars arch-neutral; no downloaded binaries. `dnf repoquery` in `almalinux:10` aarch64 under QEMU for the exact install-php82/83/84 lists: every package resolves on aarch64 (php 8.2.35 / 8.3.36 / 8.4.27; `php-pecl-redis5` resolves via Provides of `php-pecl-redis6`, identical on x86_64). php85 per T3. shared-httpd builds and runs on arm64. ## Tests `scripts/tests/multiarch-workflow.test.py` (added to Shell-Checks; no PyYAML needed): triggers, matrix/fail-fast, step order, arm64 `continue-on-error` and amd64 fatal, timeouts bound, by-digest outputs and no `tags:`/`push:` in build steps, identical build-args for both platforms, assemble uses this run's outputs, exact tag scheme (executes the assemble step's shell for php82..85 and shared-httpd), no arm64 outside the slice. 12 tests pass; against trunk's workflow 8 of 12 fail. Also fixes `index-platforms.test.sh:17` (`| head -1` tripped the early-exit-reader scan in `lsphp-info-probe.test.sh`, red on trunk). ## Local proof (throwaway registry:2, buildx docker-container builder, nothing published) cac-fpm:php85 and shared-httpd built for both platforms through the same `outputs:` form + `assemble-multiarch-index.sh`; `imagetools inspect` shows linux/amd64, linux/arm64 and one attestation manifest per platform. arm64 image under QEMU: `php 8.5.12 ... aarch64`, `php-fpm -t` OK, php-pecl-redis6/imagick installed; shared-httpd `httpd 2.4.63 aarch64`, `httpd -t` Syntax OK. Forced arm64 failure (bad base) -> outcome failure -> index is amd64-only. amd64 content vs a plain build of the same Dockerfile: 486 rpms identical, image config identical, file list/hashes identical except runtime `/etc/hosts|hostname`, fontconfig/ldconfig/dnf caches and rpmdb.sqlite (build-time nondeterminism). ## NOT verified Real runner (buildx version there, QEMU speed under load, runner capacity), Gitea registry accepting these indexes (HMB proves the pattern), pkg-cache path for arm64 (CT130 serves `$basearch` paths; untested for aarch64), `cac:` (Apache) image not built locally (same Dockerfile pattern as fpm + httpd/mod_ssl, proven by shared-httpd). ## SAFE MERGE A merge to `trunk` triggers the whole matrix. Merge with `[skip ci]` in the title, record rollback digests first (see PR comment), then dispatch manually. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0185ukXjXyosSkunDsdWz3mb
jknapp added 1 commit 2026-11-12 14:13:36 +00:00
One job per matrix leg on the trusted x86 runner: amd64 built natively, arm64
under QEMU, each pushed BY DIGEST (untagged), then
scripts/assemble-multiarch-index.sh creates the existing tags (:phpNN, :latest
for php85 / shared-httpd :latest) from this run's digests. The arm64 step is
continue-on-error with its own timeout, so an arm64 failure publishes the usual
amd64-only tag and a stale arm64 is never re-attached. Build args, base-image
pin, pkg-cache probe, php.net EXPECT_PHP_MIN gate, secrets and the
cac-litespeed/cac-lsphp/shared-ols jobs are untouched. No Dockerfile change.

Adds scripts/tests/multiarch-workflow.test.py (static wiring + tag-scheme
execution) to Shell-Checks, and fixes index-platforms.test.sh line 17 which
tripped lsphp-info-probe.test.sh's early-exit-reader scan on trunk.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185ukXjXyosSkunDsdWz3mb
Author
Owner

Rollback digests: record BEFORE merging/dispatching (read-only; needs docker login repo.anhonesthost.net only if the packages are private):

R=repo.anhonesthost.net/cloud-hosting-platform
for ref in cac:php82 cac:php83 cac:php84 cac:php85 cac-fpm:php82 cac-fpm:php83 cac-fpm:php84 cac-fpm:php85 shared-httpd:latest; do
  idx=$(docker buildx imagetools inspect "$R/$ref" --format '{{.Manifest.Digest}}')
  amd=$(docker buildx imagetools inspect --raw "$R/$ref" | python3 -I -c 'import json,sys; d=json.load(sys.stdin); print(next(m["digest"] for m in d["manifests"] if m.get("platform",{}).get("architecture")=="amd64" and m["platform"].get("os")=="linux"))')
  echo "$ref index=$idx amd64=$amd"
done | tee rollback-digests-$(date -u +%Y%m%dT%H%M).txt

Rollback one tag (re-points the tag at the recorded pre-change index; same-registry copy by digest, works with the runner's buildx):

docker buildx imagetools create -t $R/cac-fpm:php85 $R/cac-fpm@sha256:<recorded index digest>
# or amd64-only: ... create -t $R/cac-fpm:php85 $R/cac-fpm@sha256:<recorded amd64 digest>

First run: merge with [skip ci], record digests, dispatch Cloud Apache Container manually, then verify each of the 9 tags with docker buildx imagetools inspect (linux/amd64 + linux/arm64) and that the amd64 digest/RPM set matches expectations.

Rollback digests: record BEFORE merging/dispatching (read-only; needs `docker login repo.anhonesthost.net` only if the packages are private): ```bash R=repo.anhonesthost.net/cloud-hosting-platform for ref in cac:php82 cac:php83 cac:php84 cac:php85 cac-fpm:php82 cac-fpm:php83 cac-fpm:php84 cac-fpm:php85 shared-httpd:latest; do idx=$(docker buildx imagetools inspect "$R/$ref" --format '{{.Manifest.Digest}}') amd=$(docker buildx imagetools inspect --raw "$R/$ref" | python3 -I -c 'import json,sys; d=json.load(sys.stdin); print(next(m["digest"] for m in d["manifests"] if m.get("platform",{}).get("architecture")=="amd64" and m["platform"].get("os")=="linux"))') echo "$ref index=$idx amd64=$amd" done | tee rollback-digests-$(date -u +%Y%m%dT%H%M).txt ``` Rollback one tag (re-points the tag at the recorded pre-change index; same-registry copy by digest, works with the runner's buildx): ```bash docker buildx imagetools create -t $R/cac-fpm:php85 $R/cac-fpm@sha256:<recorded index digest> # or amd64-only: ... create -t $R/cac-fpm:php85 $R/cac-fpm@sha256:<recorded amd64 digest> ``` First run: merge with `[skip ci]`, record digests, dispatch `Cloud Apache Container` manually, then verify each of the 9 tags with `docker buildx imagetools inspect` (linux/amd64 + linux/arm64) and that the amd64 digest/RPM set matches expectations.
jknapp merged commit 4e657304c5 into trunk 2026-11-12 14:50:27 +00:00
jknapp deleted branch feat/multiarch-matrix 2026-11-12 14:50:27 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: cloud-hosting-platform/cloud-apache-container#40