Gitea cannot re-run one job and a full run takes ~1h. Add a plan job that turns the optional `only` input (e.g. cac:php85) into one filtered matrix per image family; image jobs take their matrix from it and are skipped when empty. Push events / empty input plan the full set as before. Unknown values fail the plan job. Tests added to multiarch-workflow.test.py. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185ukXjXyosSkunDsdWz3mb
1004 lines
48 KiB
YAML
1004 lines
48 KiB
YAML
name: Cloud Apache Container
|
||
run-name: ${{ gitea.actor }} pushed a change to trunk
|
||
# Manual run. `only` (optional) rebuilds just the named legs instead of every
|
||
# image: Gitea cannot re-run a single job and a full run takes ~1h.
|
||
# Syntax: comma list of cac:php85, cac-fpm:php84, cac-litespeed:php83,
|
||
# cac-lsphp:php82, shared-httpd, shared-ols; a bare family name (cac) = all its
|
||
# PHP versions. Empty = everything (same as a push). Unknown values fail the
|
||
# plan job. See .gitea/scripts/plan-legs.sh.
|
||
on:
|
||
push:
|
||
branches:
|
||
- trunk
|
||
workflow_dispatch:
|
||
inputs:
|
||
only:
|
||
description: 'Only build these legs (comma list, e.g. cac:php85 or cac-fpm:php84,shared-httpd). Empty = build everything.'
|
||
required: false
|
||
default: ''
|
||
type: string
|
||
|
||
jobs:
|
||
# Shell gate. Runs FIRST and costs seconds; the images below do not depend on
|
||
# it (a red job here does not block a push that is otherwise fine), but it is
|
||
# the only place the ENTRYPOINT logic is executed at all. The .phpt suite and
|
||
# the Dockerfile's `lsphp -i` probe both test the extension, and both were
|
||
# green for the release whose entrypoint declared that same extension
|
||
# missing — see scripts/tests/lsphp-info-probe.test.sh for what went wrong
|
||
# and why it needed a test outside the image build to catch it.
|
||
Shell-Checks:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
# Runner images differ on whether they are root and whether sudo exists.
|
||
- name: Install shellcheck
|
||
run: |
|
||
if ! command -v shellcheck >/dev/null 2>&1; then
|
||
(apt-get update && apt-get install -y shellcheck) ||
|
||
(sudo apt-get update && sudo apt-get install -y shellcheck)
|
||
fi
|
||
shellcheck --version
|
||
|
||
- name: Syntax check every shell script
|
||
run: |
|
||
set -euo pipefail
|
||
find scripts ext -name '*.sh' -print0 | xargs -0 -n1 bash -n
|
||
|
||
# Deliberately NOT repo-wide. The older scripts (entrypoint.sh,
|
||
# entrypoint-fpm.sh, create-vhost.sh, create-php-config.sh,
|
||
# detect-memory.sh) carry pre-existing SC2154/SC2027 findings that predate
|
||
# this job; listing them here would make the gate red on arrival and
|
||
# therefore ignored. This is the set that is clean today — the scripts
|
||
# that run `set -o pipefail` plus the tests. Add files as they are fixed;
|
||
# do not add one that is not yet clean.
|
||
- name: shellcheck (warnings and above, on the clean set)
|
||
run: |
|
||
shellcheck -S warning \
|
||
scripts/entrypoint-lsphp.sh \
|
||
scripts/entrypoint-litespeed.sh \
|
||
scripts/entrypoint-shared-ols.sh \
|
||
scripts/render-shared-ols-config.sh \
|
||
scripts/ols-htaccess-watcher.sh \
|
||
scripts/create-vhost-litespeed.sh \
|
||
scripts/install-lscache-wp.sh \
|
||
scripts/lsphp-apt-upgrade.sh \
|
||
scripts/lsphp-src-build.sh \
|
||
scripts/lsphp-src-overlay.sh \
|
||
scripts/lsphp-abi-gate.sh \
|
||
scripts/tune-mpm.sh \
|
||
scripts/httpd-h2-md-gate.sh \
|
||
scripts/tests/lsphp-info-probe.test.sh \
|
||
scripts/lsphp-repo-version.sh \
|
||
scripts/tests/lsphp-repo-version.test.sh \
|
||
scripts/image-label-amd64.sh \
|
||
scripts/php-emergency-guard.sh \
|
||
scripts/index-platforms.sh \
|
||
scripts/assemble-multiarch-index.sh \
|
||
scripts/tests/image-label-amd64.test.sh \
|
||
scripts/tests/index-platforms.test.sh \
|
||
scripts/tests/lsphp-abi-gate.test.sh \
|
||
.gitea/scripts/resolve-base-image.sh \
|
||
.gitea/scripts/plan-legs.sh
|
||
|
||
- name: lsphp repo-version lookup test
|
||
run: ./scripts/tests/lsphp-repo-version.test.sh
|
||
|
||
- name: multi-arch guard tests (index-safe label read, fail-closed, assemble rules)
|
||
run: |
|
||
./scripts/tests/image-label-amd64.test.sh
|
||
./scripts/tests/index-platforms.test.sh
|
||
|
||
# Static wiring of the multi-arch publish jobs (cac, cac-fpm, shared-httpd):
|
||
# by-digest pushes, non-fatal arm64 leg, timeouts, exact tag scheme.
|
||
- name: multi-arch workflow wiring test
|
||
run: python3 -I scripts/tests/multiarch-workflow.test.py
|
||
|
||
- name: lsphp probe regression test
|
||
run: ./scripts/tests/lsphp-info-probe.test.sh
|
||
|
||
# The emergency php.net lsphp path (lsphp-source-emergency.yaml) keeps
|
||
# LiteSpeed's PECL .so files only if this gate says the Zend ABI is
|
||
# unchanged; prove here, on every push, that the gate can still FAIL.
|
||
- name: lsphp ABI gate regression test
|
||
run: |
|
||
if ! command -v pahole >/dev/null 2>&1 || ! command -v gcc >/dev/null 2>&1; then
|
||
(apt-get update && apt-get install -y gcc dwarves) ||
|
||
(sudo apt-get update && sudo apt-get install -y gcc dwarves)
|
||
fi
|
||
./scripts/tests/lsphp-abi-gate.test.sh
|
||
|
||
# Decides which image legs run. A job-level `if:` cannot see the `matrix`
|
||
# context, so instead of skipping matrix cells this job emits one filtered
|
||
# matrix per image family (and a true/false per single-leg image); each image
|
||
# job below takes its matrix from here and is skipped when its list is empty.
|
||
# Push events and an empty `only` input plan the full set, as before.
|
||
plan:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
outputs:
|
||
cac: ${{ steps.plan.outputs.cac }}
|
||
cac_fpm: ${{ steps.plan.outputs.cac_fpm }}
|
||
cac_litespeed: ${{ steps.plan.outputs.cac_litespeed }}
|
||
cac_lsphp: ${{ steps.plan.outputs.cac_lsphp }}
|
||
shared_httpd: ${{ steps.plan.outputs.shared_httpd }}
|
||
shared_ols: ${{ steps.plan.outputs.shared_ols }}
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Plan legs
|
||
id: plan
|
||
env:
|
||
ONLY: ${{ github.event.inputs.only }}
|
||
run: .gitea/scripts/plan-legs.sh
|
||
|
||
Build-and-Push:
|
||
needs: plan
|
||
if: needs.plan.outputs.cac != '[]'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 75
|
||
strategy:
|
||
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
||
# security builds of the others.
|
||
fail-fast: false
|
||
matrix:
|
||
phpver: ${{ fromJSON(needs.plan.outputs.cac) }}
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
# Base image pinned by DIGEST, looked up directly at Docker Hub (never via
|
||
# the pkg-cache mirror), so the mirror can only serve content-addressed
|
||
# blobs that BuildKit verifies. If the lookup fails this job does NOT use
|
||
# the mirror and pulls the tag straight from Docker Hub. docs/pkg-cache.md.
|
||
- name: Resolve base image digest (Docker Hub, direct)
|
||
id: base
|
||
env:
|
||
BASE_TAG: almalinux/10-base:latest
|
||
run: |
|
||
set -uo pipefail
|
||
if ref=$(.gitea/scripts/resolve-base-image.sh "$BASE_TAG") && [[ "$ref" =~ ^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$ ]]; then
|
||
echo "base-image: ${BASE_TAG} -> ${ref} (digest from registry-1.docker.io)"
|
||
echo "image=${ref}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=true" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "::warning::base image ${BASE_TAG}: Docker Hub digest lookup failed - pulling the tag directly from Docker Hub, pkg-cache registry mirror NOT used"
|
||
echo "image=${BASE_TAG}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=false" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
|
||
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
|
||
# bad/expired cert) this job builds from the upstream repos and Docker Hub
|
||
# exactly as before. The repo cache and the registry mirror are probed
|
||
# separately so either can fail back on its own.
|
||
- name: Probe package cache
|
||
id: pkgcache
|
||
# The registry mirror is only used for a digest-pinned base image.
|
||
env:
|
||
BASE_PINNED: ${{ steps.base.outputs.pinned }}
|
||
run: |
|
||
url=""; mirror=""
|
||
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
|
||
if [ "$BASE_PINNED" = true ] && curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
|
||
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<not used: Docker Hub direct>}"
|
||
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
|
||
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
||
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Set up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
with:
|
||
platforms: arm64
|
||
|
||
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
|
||
# so the mirror is configured on the builder; BuildKit itself still falls
|
||
# back to Docker Hub if the mirror stops answering mid-run.
|
||
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
|
||
if: steps.pkgcache.outputs.mirror != ''
|
||
uses: docker/setup-buildx-action@v3
|
||
with:
|
||
buildkitd-config-inline: |
|
||
[registry."docker.io"]
|
||
mirrors = ["192.168.1.40:5000"]
|
||
[registry."192.168.1.40:5000"]
|
||
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
|
||
|
||
- name: Set up Docker Buildx
|
||
if: steps.pkgcache.outputs.mirror == ''
|
||
uses: docker/setup-buildx-action@v3
|
||
|
||
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
||
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
||
# cached between runs — this keeps it that way if a cache is ever added:
|
||
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
||
- name: Cache-bust value
|
||
id: bust
|
||
env:
|
||
RUN_ID: ${{ gitea.run_id }}
|
||
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
|
||
|
||
# The minimum PHP version this build must ship: php.net's current release for
|
||
# the branch (over HTTPS). The Dockerfile fails if Remi gave us anything older,
|
||
# so lagging mirrors or replayed repo metadata can't ship a vulnerable PHP.
|
||
- name: Resolve current PHP release from php.net
|
||
id: phpmin
|
||
# Values reach the script via env, never via ${{ }} inside run: (a
|
||
# crafted string would otherwise be shell); outputs are regex-checked.
|
||
env:
|
||
PHPVER: ${{ matrix.phpver }}
|
||
run: |
|
||
set -euo pipefail
|
||
v="$PHPVER"; branch="${v:0:1}.${v:1}"
|
||
json=$(curl -fsS --retry 5 --max-time 60 "https://www.php.net/releases/index.php?json&max=1&version=${branch}")
|
||
min=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(iter(d)) if isinstance(d,dict) and d else "")')
|
||
if [ -z "$min" ]; then echo "::error::no php.net release found for ${branch}"; exit 1; fi
|
||
[[ "$min" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::php.net returned a malformed version for ${branch}"; exit 1; }
|
||
echo "php.net current ${branch}: ${min}"
|
||
echo "min=${min}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Login to Gitea
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: repo.anhonesthost.net
|
||
username: ${{ secrets.CI_USER }}
|
||
password: ${{ secrets.CI_TOKEN }}
|
||
|
||
# amd64: required. Pushed untagged BY DIGEST (no tag); failure fails the
|
||
# job and publishes nothing, exactly as before multi-arch. The tags are only
|
||
# created by the assemble step below.
|
||
- name: Build amd64 (push by digest)
|
||
id: amd64
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
platforms: linux/amd64
|
||
build-args: |
|
||
PHPVER=${{ matrix.phpver }}
|
||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||
BASE_IMAGE=${{ steps.base.outputs.image }}
|
||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
|
||
outputs: type=image,name=repo.anhonesthost.net/cloud-hosting-platform/cac,push-by-digest=true,name-canonical=true,push=true
|
||
|
||
# arm64: best effort, built under QEMU on this same trusted x86 runner (no
|
||
# secret ever leaves it). Same Dockerfile, same build-args (incl. the php.net
|
||
# EXPECT_PHP_MIN gate, which runs `php -r` on the arm64 binary). Must never
|
||
# block or alter the amd64 tag: on failure/timeout the tag is published
|
||
# amd64-only, as before multi-arch.
|
||
- name: Build arm64 (push by digest)
|
||
id: arm64
|
||
continue-on-error: true
|
||
timeout-minutes: 45
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
platforms: linux/arm64
|
||
build-args: |
|
||
PHPVER=${{ matrix.phpver }}
|
||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||
BASE_IMAGE=${{ steps.base.outputs.image }}
|
||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
|
||
outputs: type=image,name=repo.anhonesthost.net/cloud-hosting-platform/cac,push-by-digest=true,name-canonical=true,push=true
|
||
|
||
# Creates the index tag(s) from THIS run's digests only (a previous run's
|
||
# arm64 is never re-attached). scripts/assemble-multiarch-index.sh.
|
||
- name: Assemble index tag(s)
|
||
env:
|
||
AMD64_DIGEST: ${{ steps.amd64.outputs.digest }}
|
||
ARM64_DIGEST: ${{ steps.arm64.outputs.digest }}
|
||
ARM64_OUTCOME: ${{ steps.arm64.outcome }}
|
||
PHPVER: ${{ matrix.phpver }}
|
||
run: |
|
||
set -euo pipefail
|
||
tags=(--tag "php${PHPVER}")
|
||
# :latest tracks php85 (unchanged tag scheme).
|
||
[ "$PHPVER" = 85 ] && tags+=(--tag latest)
|
||
scripts/assemble-multiarch-index.sh \
|
||
--amd64 "$AMD64_DIGEST" \
|
||
--arm64 "$ARM64_DIGEST" --arm64-outcome "$ARM64_OUTCOME" \
|
||
--repo repo.anhonesthost.net/cloud-hosting-platform/cac \
|
||
"${tags[@]}"
|
||
|
||
Build-FPM-Images:
|
||
needs: plan
|
||
if: needs.plan.outputs.cac_fpm != '[]'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 75
|
||
strategy:
|
||
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
||
# security builds of the others.
|
||
fail-fast: false
|
||
matrix:
|
||
phpver: ${{ fromJSON(needs.plan.outputs.cac_fpm) }}
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
# Base image pinned by DIGEST, looked up directly at Docker Hub (never via
|
||
# the pkg-cache mirror), so the mirror can only serve content-addressed
|
||
# blobs that BuildKit verifies. If the lookup fails this job does NOT use
|
||
# the mirror and pulls the tag straight from Docker Hub. docs/pkg-cache.md.
|
||
- name: Resolve base image digest (Docker Hub, direct)
|
||
id: base
|
||
env:
|
||
BASE_TAG: almalinux/10-base:latest
|
||
run: |
|
||
set -uo pipefail
|
||
if ref=$(.gitea/scripts/resolve-base-image.sh "$BASE_TAG") && [[ "$ref" =~ ^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$ ]]; then
|
||
echo "base-image: ${BASE_TAG} -> ${ref} (digest from registry-1.docker.io)"
|
||
echo "image=${ref}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=true" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "::warning::base image ${BASE_TAG}: Docker Hub digest lookup failed - pulling the tag directly from Docker Hub, pkg-cache registry mirror NOT used"
|
||
echo "image=${BASE_TAG}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=false" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
|
||
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
|
||
# bad/expired cert) this job builds from the upstream repos and Docker Hub
|
||
# exactly as before. The repo cache and the registry mirror are probed
|
||
# separately so either can fail back on its own.
|
||
- name: Probe package cache
|
||
id: pkgcache
|
||
# The registry mirror is only used for a digest-pinned base image.
|
||
env:
|
||
BASE_PINNED: ${{ steps.base.outputs.pinned }}
|
||
run: |
|
||
url=""; mirror=""
|
||
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
|
||
if [ "$BASE_PINNED" = true ] && curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
|
||
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<not used: Docker Hub direct>}"
|
||
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
|
||
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
||
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Set up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
with:
|
||
platforms: arm64
|
||
|
||
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
|
||
# so the mirror is configured on the builder; BuildKit itself still falls
|
||
# back to Docker Hub if the mirror stops answering mid-run.
|
||
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
|
||
if: steps.pkgcache.outputs.mirror != ''
|
||
uses: docker/setup-buildx-action@v3
|
||
with:
|
||
buildkitd-config-inline: |
|
||
[registry."docker.io"]
|
||
mirrors = ["192.168.1.40:5000"]
|
||
[registry."192.168.1.40:5000"]
|
||
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
|
||
|
||
- name: Set up Docker Buildx
|
||
if: steps.pkgcache.outputs.mirror == ''
|
||
uses: docker/setup-buildx-action@v3
|
||
|
||
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
||
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
||
# cached between runs — this keeps it that way if a cache is ever added:
|
||
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
||
- name: Cache-bust value
|
||
id: bust
|
||
env:
|
||
RUN_ID: ${{ gitea.run_id }}
|
||
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
|
||
|
||
# The minimum PHP version this build must ship: php.net's current release for
|
||
# the branch (over HTTPS). The Dockerfile fails if Remi gave us anything older,
|
||
# so lagging mirrors or replayed repo metadata can't ship a vulnerable PHP.
|
||
- name: Resolve current PHP release from php.net
|
||
id: phpmin
|
||
# Values reach the script via env, never via ${{ }} inside run: (a
|
||
# crafted string would otherwise be shell); outputs are regex-checked.
|
||
env:
|
||
PHPVER: ${{ matrix.phpver }}
|
||
run: |
|
||
set -euo pipefail
|
||
v="$PHPVER"; branch="${v:0:1}.${v:1}"
|
||
json=$(curl -fsS --retry 5 --max-time 60 "https://www.php.net/releases/index.php?json&max=1&version=${branch}")
|
||
min=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(iter(d)) if isinstance(d,dict) and d else "")')
|
||
if [ -z "$min" ]; then echo "::error::no php.net release found for ${branch}"; exit 1; fi
|
||
[[ "$min" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::php.net returned a malformed version for ${branch}"; exit 1; }
|
||
echo "php.net current ${branch}: ${min}"
|
||
echo "min=${min}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Login to Gitea
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: repo.anhonesthost.net
|
||
username: ${{ secrets.CI_USER }}
|
||
password: ${{ secrets.CI_TOKEN }}
|
||
|
||
# amd64: required. Pushed untagged BY DIGEST (no tag); failure fails the
|
||
# job and publishes nothing, exactly as before multi-arch. The tags are only
|
||
# created by the assemble step below.
|
||
- name: Build amd64 (push by digest)
|
||
id: amd64
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
file: ./Dockerfile.fpm
|
||
platforms: linux/amd64
|
||
build-args: |
|
||
PHPVER=${{ matrix.phpver }}
|
||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||
BASE_IMAGE=${{ steps.base.outputs.image }}
|
||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
|
||
outputs: type=image,name=repo.anhonesthost.net/cloud-hosting-platform/cac-fpm,push-by-digest=true,name-canonical=true,push=true
|
||
|
||
# arm64: best effort, built under QEMU on this same trusted x86 runner (no
|
||
# secret ever leaves it). Same Dockerfile, same build-args (incl. the php.net
|
||
# EXPECT_PHP_MIN gate, which runs `php -r` on the arm64 binary). Must never
|
||
# block or alter the amd64 tag: on failure/timeout the tag is published
|
||
# amd64-only, as before multi-arch.
|
||
- name: Build arm64 (push by digest)
|
||
id: arm64
|
||
continue-on-error: true
|
||
timeout-minutes: 45
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
file: ./Dockerfile.fpm
|
||
platforms: linux/arm64
|
||
build-args: |
|
||
PHPVER=${{ matrix.phpver }}
|
||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||
BASE_IMAGE=${{ steps.base.outputs.image }}
|
||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
|
||
outputs: type=image,name=repo.anhonesthost.net/cloud-hosting-platform/cac-fpm,push-by-digest=true,name-canonical=true,push=true
|
||
|
||
# Creates the index tag(s) from THIS run's digests only (a previous run's
|
||
# arm64 is never re-attached). scripts/assemble-multiarch-index.sh.
|
||
- name: Assemble index tag(s)
|
||
env:
|
||
AMD64_DIGEST: ${{ steps.amd64.outputs.digest }}
|
||
ARM64_DIGEST: ${{ steps.arm64.outputs.digest }}
|
||
ARM64_OUTCOME: ${{ steps.arm64.outcome }}
|
||
PHPVER: ${{ matrix.phpver }}
|
||
run: |
|
||
set -euo pipefail
|
||
tags=(--tag "php${PHPVER}")
|
||
# :latest tracks php85 (unchanged tag scheme).
|
||
[ "$PHPVER" = 85 ] && tags+=(--tag latest)
|
||
scripts/assemble-multiarch-index.sh \
|
||
--amd64 "$AMD64_DIGEST" \
|
||
--arm64 "$ARM64_DIGEST" --arm64-outcome "$ARM64_OUTCOME" \
|
||
--repo repo.anhonesthost.net/cloud-hosting-platform/cac-fpm \
|
||
"${tags[@]}"
|
||
|
||
Build-LiteSpeed-Images:
|
||
needs: plan
|
||
if: needs.plan.outputs.cac_litespeed != '[]'
|
||
runs-on: ubuntu-latest
|
||
# One place for the OLS base tag: digest lookup and build-args both read it.
|
||
# OLS_VERSION pinned to 1.8.4 (see the build step); Dockerfile default differs.
|
||
env:
|
||
OLS_VERSION: "1.8.4"
|
||
strategy:
|
||
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
||
# security builds of the others.
|
||
fail-fast: false
|
||
matrix:
|
||
# PHP 7.4/8.0 deliberately excluded — the LiteSpeed prebuilt base
|
||
# images stop at older OLS releases for those PHP versions, and the
|
||
# cac-litespeed tier is a paid premium offering: 8.1+ is the
|
||
# modernization story we're selling.
|
||
phpver: ${{ fromJSON(needs.plan.outputs.cac_litespeed) }}
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
# Base image pinned by DIGEST, looked up directly at Docker Hub (never via
|
||
# the pkg-cache mirror), so the mirror can only serve content-addressed
|
||
# blobs that BuildKit verifies. If the lookup fails this job does NOT use
|
||
# the mirror and pulls the tag straight from Docker Hub. docs/pkg-cache.md.
|
||
- name: Resolve base image digest (Docker Hub, direct)
|
||
id: base
|
||
env:
|
||
BASE_TAG: litespeedtech/openlitespeed:${{ env.OLS_VERSION }}-lsphp${{ matrix.phpver }}
|
||
run: |
|
||
set -uo pipefail
|
||
if ref=$(.gitea/scripts/resolve-base-image.sh "$BASE_TAG") && [[ "$ref" =~ ^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$ ]]; then
|
||
echo "base-image: ${BASE_TAG} -> ${ref} (digest from registry-1.docker.io)"
|
||
echo "image=${ref}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=true" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "::warning::base image ${BASE_TAG}: Docker Hub digest lookup failed - pulling the tag directly from Docker Hub, pkg-cache registry mirror NOT used"
|
||
echo "image=${BASE_TAG}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=false" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
|
||
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
|
||
# bad/expired cert) this job builds from the upstream repos and Docker Hub
|
||
# exactly as before. The repo cache and the registry mirror are probed
|
||
# separately so either can fail back on its own.
|
||
- name: Probe package cache
|
||
id: pkgcache
|
||
# The registry mirror is only used for a digest-pinned base image.
|
||
env:
|
||
BASE_PINNED: ${{ steps.base.outputs.pinned }}
|
||
run: |
|
||
url=""; mirror=""
|
||
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
|
||
if [ "$BASE_PINNED" = true ] && curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
|
||
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<not used: Docker Hub direct>}"
|
||
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
|
||
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
||
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Set up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
|
||
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
|
||
# so the mirror is configured on the builder; BuildKit itself still falls
|
||
# back to Docker Hub if the mirror stops answering mid-run.
|
||
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
|
||
if: steps.pkgcache.outputs.mirror != ''
|
||
uses: docker/setup-buildx-action@v3
|
||
with:
|
||
buildkitd-config-inline: |
|
||
[registry."docker.io"]
|
||
mirrors = ["192.168.1.40:5000"]
|
||
[registry."192.168.1.40:5000"]
|
||
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
|
||
|
||
- name: Set up Docker Buildx
|
||
if: steps.pkgcache.outputs.mirror == ''
|
||
uses: docker/setup-buildx-action@v3
|
||
|
||
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
||
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
||
# cached between runs — this keeps it that way if a cache is ever added:
|
||
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
||
- name: Cache-bust value
|
||
id: bust
|
||
env:
|
||
RUN_ID: ${{ gitea.run_id }}
|
||
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
|
||
|
||
# Ask the LiteSpeed apt repo (the same one the base image's apt uses) for the
|
||
# CURRENT lsphp version. The Dockerfile asserts the installed package equals
|
||
# this (build fails if the repo changed mid-build — just re-run) and writes
|
||
# it into the image labels org.whp.php-version / org.whp.lsphp-package-version.
|
||
- name: Resolve current lsphp${{ matrix.phpver }} from the LiteSpeed repo
|
||
id: lsphp
|
||
# HTTPS (rpms.litespeedtech.com serves it; measured 2026-10-21), values via
|
||
# env, and both outputs regex-checked before they reach GITHUB_OUTPUT.
|
||
env:
|
||
PHPVER: ${{ matrix.phpver }}
|
||
run: |
|
||
set -euo pipefail
|
||
pkg=$(./scripts/lsphp-repo-version.sh "${PHPVER}" "${LSPHP_ARCH:-amd64}")
|
||
if [ -z "$pkg" ]; then echo "::error::lsphp${PHPVER} not found in the LiteSpeed repo index"; exit 1; fi
|
||
php="${pkg%%-*}"
|
||
[[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; }
|
||
[[ "$php" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '${php}' derived from lsphp${PHPVER} ${pkg}"; exit 1; }
|
||
echo "lsphp${PHPVER} in repo: $pkg (PHP ${php})"
|
||
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
|
||
echo "php=${php}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Login to Gitea
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: repo.anhonesthost.net
|
||
username: ${{ secrets.CI_USER }}
|
||
password: ${{ secrets.CI_TOKEN }}
|
||
|
||
# A php.net emergency build (lsphp-source-emergency.yaml) may be what
|
||
# :phpNN points at right now. While LiteSpeed still lags, a normal build
|
||
# would silently put the VULNERABLE packaged lsphp back. Refuse instead;
|
||
# the job goes green again by itself once the LiteSpeed repo catches up.
|
||
# Reads the labels of the linux/amd64 image behind the tag (index-safe:
|
||
# works for single-platform and multi-arch tags) and FAILS CLOSED if they
|
||
# cannot be read; only a tag that does not exist yet (first build) passes
|
||
# unchecked. scripts/php-emergency-guard.sh, tests in scripts/tests/.
|
||
- name: Do not roll back a newer php.net emergency image
|
||
env:
|
||
PHPVER: ${{ matrix.phpver }}
|
||
NEW_PHP: ${{ steps.lsphp.outputs.php }}
|
||
run: |
|
||
set -euo pipefail
|
||
ref="repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${PHPVER}"
|
||
./scripts/php-emergency-guard.sh "$ref" "$NEW_PHP"
|
||
|
||
- name: Build and Push LiteSpeed Image
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
file: ./Dockerfile.litespeed
|
||
platforms: linux/amd64
|
||
push: true
|
||
build-args: |
|
||
PHPVER=${{ matrix.phpver }}
|
||
OLS_VERSION=${{ env.OLS_VERSION }}
|
||
BASE_IMAGE=${{ steps.base.outputs.image }}
|
||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||
EXPECT_LSPHP_PKG=${{ steps.lsphp.outputs.pkg }}
|
||
EXPECT_PHP_VERSION=${{ steps.lsphp.outputs.php }}
|
||
# OLS_VERSION pinned to 1.8.4. It was the only release with an lsphp81
|
||
# variant; 8.1 and older are no longer built (2026-10-21), so the pin
|
||
# can move to a newer OLS release after a local rebuild test.
|
||
# See spec: docs/superpowers/specs/2026-06-01-cac-litespeed-design.md
|
||
tags: |
|
||
repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }}
|
||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:latest' || '' }}
|
||
|
||
Build-LSPHP-Images:
|
||
needs: plan
|
||
if: needs.plan.outputs.cac_lsphp != '[]'
|
||
runs-on: ubuntu-latest
|
||
# One place for the OLS base tag: digest lookup and build-args both read it.
|
||
# OLS_VERSION pinned to 1.8.4 (see the build step); Dockerfile default differs.
|
||
env:
|
||
OLS_VERSION: "1.8.4"
|
||
strategy:
|
||
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
||
# security builds of the others.
|
||
fail-fast: false
|
||
matrix:
|
||
# Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached
|
||
# backend for the shared-ols tier and shares the litespeed prebuilt
|
||
# base, which only ships lsphp for 8.1+. Keep this matrix in lockstep
|
||
# with Build-LiteSpeed-Images.
|
||
phpver: ${{ fromJSON(needs.plan.outputs.cac_lsphp) }}
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
# Base image pinned by DIGEST, looked up directly at Docker Hub (never via
|
||
# the pkg-cache mirror), so the mirror can only serve content-addressed
|
||
# blobs that BuildKit verifies. If the lookup fails this job does NOT use
|
||
# the mirror and pulls the tag straight from Docker Hub. docs/pkg-cache.md.
|
||
- name: Resolve base image digest (Docker Hub, direct)
|
||
id: base
|
||
env:
|
||
BASE_TAG: litespeedtech/openlitespeed:${{ env.OLS_VERSION }}-lsphp${{ matrix.phpver }}
|
||
run: |
|
||
set -uo pipefail
|
||
if ref=$(.gitea/scripts/resolve-base-image.sh "$BASE_TAG") && [[ "$ref" =~ ^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$ ]]; then
|
||
echo "base-image: ${BASE_TAG} -> ${ref} (digest from registry-1.docker.io)"
|
||
echo "image=${ref}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=true" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "::warning::base image ${BASE_TAG}: Docker Hub digest lookup failed - pulling the tag directly from Docker Hub, pkg-cache registry mirror NOT used"
|
||
echo "image=${BASE_TAG}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=false" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
|
||
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
|
||
# bad/expired cert) this job builds from the upstream repos and Docker Hub
|
||
# exactly as before. The repo cache and the registry mirror are probed
|
||
# separately so either can fail back on its own.
|
||
- name: Probe package cache
|
||
id: pkgcache
|
||
# The registry mirror is only used for a digest-pinned base image.
|
||
env:
|
||
BASE_PINNED: ${{ steps.base.outputs.pinned }}
|
||
run: |
|
||
url=""; mirror=""
|
||
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
|
||
if [ "$BASE_PINNED" = true ] && curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
|
||
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<not used: Docker Hub direct>}"
|
||
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
|
||
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
||
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Set up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
|
||
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
|
||
# so the mirror is configured on the builder; BuildKit itself still falls
|
||
# back to Docker Hub if the mirror stops answering mid-run.
|
||
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
|
||
if: steps.pkgcache.outputs.mirror != ''
|
||
uses: docker/setup-buildx-action@v3
|
||
with:
|
||
buildkitd-config-inline: |
|
||
[registry."docker.io"]
|
||
mirrors = ["192.168.1.40:5000"]
|
||
[registry."192.168.1.40:5000"]
|
||
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
|
||
|
||
- name: Set up Docker Buildx
|
||
if: steps.pkgcache.outputs.mirror == ''
|
||
uses: docker/setup-buildx-action@v3
|
||
|
||
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
||
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
||
# cached between runs — this keeps it that way if a cache is ever added:
|
||
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
||
- name: Cache-bust value
|
||
id: bust
|
||
env:
|
||
RUN_ID: ${{ gitea.run_id }}
|
||
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
|
||
|
||
# Ask the LiteSpeed apt repo (the same one the base image's apt uses) for the
|
||
# CURRENT lsphp version. The Dockerfile asserts the installed package equals
|
||
# this (build fails if the repo changed mid-build — just re-run) and writes
|
||
# it into the image labels org.whp.php-version / org.whp.lsphp-package-version.
|
||
- name: Resolve current lsphp${{ matrix.phpver }} from the LiteSpeed repo
|
||
id: lsphp
|
||
# HTTPS (rpms.litespeedtech.com serves it; measured 2026-10-21), values via
|
||
# env, and both outputs regex-checked before they reach GITHUB_OUTPUT.
|
||
env:
|
||
PHPVER: ${{ matrix.phpver }}
|
||
run: |
|
||
set -euo pipefail
|
||
pkg=$(./scripts/lsphp-repo-version.sh "${PHPVER}" "${LSPHP_ARCH:-amd64}")
|
||
if [ -z "$pkg" ]; then echo "::error::lsphp${PHPVER} not found in the LiteSpeed repo index"; exit 1; fi
|
||
php="${pkg%%-*}"
|
||
[[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; }
|
||
[[ "$php" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '${php}' derived from lsphp${PHPVER} ${pkg}"; exit 1; }
|
||
echo "lsphp${PHPVER} in repo: $pkg (PHP ${php})"
|
||
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
|
||
echo "php=${php}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Login to Gitea
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: repo.anhonesthost.net
|
||
username: ${{ secrets.CI_USER }}
|
||
password: ${{ secrets.CI_TOKEN }}
|
||
|
||
# A php.net emergency build (lsphp-source-emergency.yaml) may be what
|
||
# :phpNN points at right now. While LiteSpeed still lags, a normal build
|
||
# would silently put the VULNERABLE packaged lsphp back. Refuse instead;
|
||
# the job goes green again by itself once the LiteSpeed repo catches up.
|
||
# Reads the labels of the linux/amd64 image behind the tag (index-safe:
|
||
# works for single-platform and multi-arch tags) and FAILS CLOSED if they
|
||
# cannot be read; only a tag that does not exist yet (first build) passes
|
||
# unchecked. scripts/php-emergency-guard.sh, tests in scripts/tests/.
|
||
- name: Do not roll back a newer php.net emergency image
|
||
env:
|
||
PHPVER: ${{ matrix.phpver }}
|
||
NEW_PHP: ${{ steps.lsphp.outputs.php }}
|
||
run: |
|
||
set -euo pipefail
|
||
ref="repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${PHPVER}"
|
||
./scripts/php-emergency-guard.sh "$ref" "$NEW_PHP"
|
||
|
||
- name: Build and Push lsphp Image
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
file: ./Dockerfile.lsphp
|
||
platforms: linux/amd64
|
||
push: true
|
||
build-args: |
|
||
PHPVER=${{ matrix.phpver }}
|
||
OLS_VERSION=${{ env.OLS_VERSION }}
|
||
BASE_IMAGE=${{ steps.base.outputs.image }}
|
||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||
EXPECT_LSPHP_PKG=${{ steps.lsphp.outputs.pkg }}
|
||
EXPECT_PHP_VERSION=${{ steps.lsphp.outputs.php }}
|
||
# OLS_VERSION pinned to 1.8.4 to match Build-LiteSpeed-Images — same
|
||
# prebuilt base, same lsphp binaries. Bump both together.
|
||
tags: |
|
||
repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${{ matrix.phpver }}
|
||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:latest' || '' }}
|
||
|
||
Build-Shared-httpd:
|
||
needs: plan
|
||
if: needs.plan.outputs.shared_httpd == 'true'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 50
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
# Base image pinned by DIGEST, looked up directly at Docker Hub (never via
|
||
# the pkg-cache mirror), so the mirror can only serve content-addressed
|
||
# blobs that BuildKit verifies. If the lookup fails this job does NOT use
|
||
# the mirror and pulls the tag straight from Docker Hub. docs/pkg-cache.md.
|
||
- name: Resolve base image digest (Docker Hub, direct)
|
||
id: base
|
||
env:
|
||
BASE_TAG: almalinux/10-base:latest
|
||
run: |
|
||
set -uo pipefail
|
||
if ref=$(.gitea/scripts/resolve-base-image.sh "$BASE_TAG") && [[ "$ref" =~ ^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$ ]]; then
|
||
echo "base-image: ${BASE_TAG} -> ${ref} (digest from registry-1.docker.io)"
|
||
echo "image=${ref}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=true" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "::warning::base image ${BASE_TAG}: Docker Hub digest lookup failed - pulling the tag directly from Docker Hub, pkg-cache registry mirror NOT used"
|
||
echo "image=${BASE_TAG}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=false" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
|
||
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
|
||
# bad/expired cert) this job builds from the upstream repos and Docker Hub
|
||
# exactly as before. The repo cache and the registry mirror are probed
|
||
# separately so either can fail back on its own.
|
||
- name: Probe package cache
|
||
id: pkgcache
|
||
# The registry mirror is only used for a digest-pinned base image.
|
||
env:
|
||
BASE_PINNED: ${{ steps.base.outputs.pinned }}
|
||
run: |
|
||
url=""; mirror=""
|
||
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
|
||
if [ "$BASE_PINNED" = true ] && curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
|
||
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<not used: Docker Hub direct>}"
|
||
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
|
||
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
||
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Set up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
with:
|
||
platforms: arm64
|
||
|
||
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
|
||
# so the mirror is configured on the builder; BuildKit itself still falls
|
||
# back to Docker Hub if the mirror stops answering mid-run.
|
||
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
|
||
if: steps.pkgcache.outputs.mirror != ''
|
||
uses: docker/setup-buildx-action@v3
|
||
with:
|
||
buildkitd-config-inline: |
|
||
[registry."docker.io"]
|
||
mirrors = ["192.168.1.40:5000"]
|
||
[registry."192.168.1.40:5000"]
|
||
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
|
||
|
||
- name: Set up Docker Buildx
|
||
if: steps.pkgcache.outputs.mirror == ''
|
||
uses: docker/setup-buildx-action@v3
|
||
|
||
- name: Login to Gitea
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: repo.anhonesthost.net
|
||
username: ${{ secrets.CI_USER }}
|
||
password: ${{ secrets.CI_TOKEN }}
|
||
|
||
# amd64: required. Pushed untagged BY DIGEST (no tag); failure fails the
|
||
# job and publishes nothing, exactly as before multi-arch. The tags are only
|
||
# created by the assemble step below.
|
||
- name: Build amd64 (push by digest)
|
||
id: amd64
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
file: ./Dockerfile.shared-httpd
|
||
platforms: linux/amd64
|
||
build-args: |
|
||
BASE_IMAGE=${{ steps.base.outputs.image }}
|
||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||
outputs: type=image,name=repo.anhonesthost.net/cloud-hosting-platform/shared-httpd,push-by-digest=true,name-canonical=true,push=true
|
||
|
||
# arm64: best effort, built under QEMU on this same trusted x86 runner (no
|
||
# secret ever leaves it). Same Dockerfile, same build-args (incl. the php.net
|
||
# EXPECT_PHP_MIN gate, which runs `php -r` on the arm64 binary). Must never
|
||
# block or alter the amd64 tag: on failure/timeout the tag is published
|
||
# amd64-only, as before multi-arch.
|
||
- name: Build arm64 (push by digest)
|
||
id: arm64
|
||
continue-on-error: true
|
||
timeout-minutes: 25
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
file: ./Dockerfile.shared-httpd
|
||
platforms: linux/arm64
|
||
build-args: |
|
||
BASE_IMAGE=${{ steps.base.outputs.image }}
|
||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||
outputs: type=image,name=repo.anhonesthost.net/cloud-hosting-platform/shared-httpd,push-by-digest=true,name-canonical=true,push=true
|
||
|
||
# Creates the index tag(s) from THIS run's digests only (a previous run's
|
||
# arm64 is never re-attached). scripts/assemble-multiarch-index.sh.
|
||
- name: Assemble index tag(s)
|
||
env:
|
||
AMD64_DIGEST: ${{ steps.amd64.outputs.digest }}
|
||
ARM64_DIGEST: ${{ steps.arm64.outputs.digest }}
|
||
ARM64_OUTCOME: ${{ steps.arm64.outcome }}
|
||
run: |
|
||
set -euo pipefail
|
||
scripts/assemble-multiarch-index.sh \
|
||
--amd64 "$AMD64_DIGEST" \
|
||
--arm64 "$ARM64_DIGEST" --arm64-outcome "$ARM64_OUTCOME" \
|
||
--repo repo.anhonesthost.net/cloud-hosting-platform/shared-httpd \
|
||
--tag latest
|
||
|
||
Build-Shared-OLS:
|
||
needs: plan
|
||
if: needs.plan.outputs.shared_ols == 'true'
|
||
runs-on: ubuntu-latest
|
||
# One place for the OLS base tag: digest lookup and build-args both read it.
|
||
env:
|
||
OLS_VERSION: "1.8.4"
|
||
PHPVER: "83"
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
# Base image pinned by DIGEST, looked up directly at Docker Hub (never via
|
||
# the pkg-cache mirror), so the mirror can only serve content-addressed
|
||
# blobs that BuildKit verifies. If the lookup fails this job does NOT use
|
||
# the mirror and pulls the tag straight from Docker Hub. docs/pkg-cache.md.
|
||
- name: Resolve base image digest (Docker Hub, direct)
|
||
id: base
|
||
env:
|
||
BASE_TAG: litespeedtech/openlitespeed:${{ env.OLS_VERSION }}-lsphp${{ env.PHPVER }}
|
||
run: |
|
||
set -uo pipefail
|
||
if ref=$(.gitea/scripts/resolve-base-image.sh "$BASE_TAG") && [[ "$ref" =~ ^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$ ]]; then
|
||
echo "base-image: ${BASE_TAG} -> ${ref} (digest from registry-1.docker.io)"
|
||
echo "image=${ref}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=true" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "::warning::base image ${BASE_TAG}: Docker Hub digest lookup failed - pulling the tag directly from Docker Hub, pkg-cache registry mirror NOT used"
|
||
echo "image=${BASE_TAG}" >> "$GITHUB_OUTPUT"
|
||
echo "pinned=false" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
|
||
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
|
||
# bad/expired cert) this job builds from the upstream repos and Docker Hub
|
||
# exactly as before. The repo cache and the registry mirror are probed
|
||
# separately so either can fail back on its own.
|
||
- name: Probe package cache
|
||
id: pkgcache
|
||
# The registry mirror is only used for a digest-pinned base image.
|
||
env:
|
||
BASE_PINNED: ${{ steps.base.outputs.pinned }}
|
||
run: |
|
||
url=""; mirror=""
|
||
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
|
||
if [ "$BASE_PINNED" = true ] && curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
|
||
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<not used: Docker Hub direct>}"
|
||
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
|
||
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
||
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Set up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
|
||
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
|
||
# so the mirror is configured on the builder; BuildKit itself still falls
|
||
# back to Docker Hub if the mirror stops answering mid-run.
|
||
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
|
||
if: steps.pkgcache.outputs.mirror != ''
|
||
uses: docker/setup-buildx-action@v3
|
||
with:
|
||
buildkitd-config-inline: |
|
||
[registry."docker.io"]
|
||
mirrors = ["192.168.1.40:5000"]
|
||
[registry."192.168.1.40:5000"]
|
||
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
|
||
|
||
- name: Set up Docker Buildx
|
||
if: steps.pkgcache.outputs.mirror == ''
|
||
uses: docker/setup-buildx-action@v3
|
||
|
||
- name: Login to Gitea
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: repo.anhonesthost.net
|
||
username: ${{ secrets.CI_USER }}
|
||
password: ${{ secrets.CI_TOKEN }}
|
||
|
||
- name: Build and Push Shared OLS Image
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
file: ./Dockerfile.shared-ols
|
||
platforms: linux/amd64
|
||
push: true
|
||
# Single image (runs no PHP). PHPVER just selects the OLS base tag;
|
||
# pinned to 83 / OLS 1.8.4 to match the rest of the litespeed family.
|
||
build-args: |
|
||
PHPVER=${{ env.PHPVER }}
|
||
OLS_VERSION=${{ env.OLS_VERSION }}
|
||
BASE_IMAGE=${{ steps.base.outputs.image }}
|
||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||
tags: |
|
||
repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest
|