With the pkg-cache BuildKit mirror on, `FROM almalinux/10-base` was resolved by tag through the cache, so a compromised cache could substitute the image. Each job using the mirror now looks up the base tag's digest DIRECTLY at registry-1.docker.io (.gitea/scripts/resolve-base-image.sh, HTTPS, HEAD on the manifest, regex-validated) and builds FROM image@sha256:... via the BASE_IMAGE build-arg, so the mirror can only serve content-addressed blobs. If the lookup fails, the mirror is NOT used for that job and the tag is pulled straight from Docker Hub. The digest is resolved every run (no manual bumps); local builds default to the tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
699 lines
34 KiB
YAML
699 lines
34 KiB
YAML
name: Cloud Apache Container
|
||
run-name: ${{ gitea.actor }} pushed a change to trunk
|
||
on:
|
||
push:
|
||
branches:
|
||
- trunk
|
||
workflow_dispatch:
|
||
|
||
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/tests/lsphp-info-probe.test.sh \
|
||
scripts/tests/lsphp-abi-gate.test.sh \
|
||
.gitea/scripts/resolve-base-image.sh
|
||
|
||
- 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
|
||
|
||
Build-and-Push:
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
||
# security builds of the others.
|
||
fail-fast: false
|
||
matrix:
|
||
phpver: [82, 83, 84, 85]
|
||
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
|
||
|
||
# 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 }}
|
||
|
||
- name: Build and Push Image
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
platforms: linux/amd64
|
||
push: true
|
||
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 }}
|
||
tags: |
|
||
repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }}
|
||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac:latest' || '' }}
|
||
|
||
Build-FPM-Images:
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
||
# security builds of the others.
|
||
fail-fast: false
|
||
matrix:
|
||
phpver: [82, 83, 84, 85]
|
||
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
|
||
|
||
# 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 }}
|
||
|
||
- name: Build and Push FPM Image
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
file: ./Dockerfile.fpm
|
||
platforms: linux/amd64
|
||
push: true
|
||
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 }}
|
||
tags: |
|
||
repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }}
|
||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:latest' || '' }}
|
||
|
||
Build-LiteSpeed-Images:
|
||
runs-on: ubuntu-latest
|
||
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: [82, 83, 84, 85]
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Set up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
|
||
- name: Set up Docker Buildx
|
||
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
|
||
idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
|
||
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
|
||
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.
|
||
# Fail-open if the tag cannot be inspected (first build, registry blip).
|
||
- 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}"
|
||
src=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.lsphp-source" }}' 2>/dev/null || true)
|
||
cur=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.php-version" }}' 2>/dev/null || true)
|
||
new="$NEW_PHP"
|
||
echo "published $ref: source=${src:-?} php=${cur:-?}; this build: packaged $new"
|
||
if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then
|
||
echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed."
|
||
exit 1
|
||
fi
|
||
|
||
- 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=1.8.4
|
||
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:
|
||
runs-on: ubuntu-latest
|
||
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: [82, 83, 84, 85]
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Set up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
|
||
- name: Set up Docker Buildx
|
||
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
|
||
idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
|
||
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
|
||
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.
|
||
# Fail-open if the tag cannot be inspected (first build, registry blip).
|
||
- 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}"
|
||
src=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.lsphp-source" }}' 2>/dev/null || true)
|
||
cur=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.php-version" }}' 2>/dev/null || true)
|
||
new="$NEW_PHP"
|
||
echo "published $ref: source=${src:-?} php=${cur:-?}; this build: packaged $new"
|
||
if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then
|
||
echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed."
|
||
exit 1
|
||
fi
|
||
|
||
- 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=1.8.4
|
||
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:
|
||
runs-on: ubuntu-latest
|
||
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
|
||
|
||
# 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 httpd Image
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
file: ./Dockerfile.shared-httpd
|
||
platforms: linux/amd64
|
||
push: true
|
||
build-args: |
|
||
BASE_IMAGE=${{ steps.base.outputs.image }}
|
||
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||
tags: |
|
||
repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest
|
||
|
||
Build-Shared-OLS:
|
||
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
|