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:-} registry-mirror=${mirror:-}" [ -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:-} registry-mirror=${mirror:-}" [ -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 # 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: [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: 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:-} registry-mirror=${mirror:-}" [ -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 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=${{ 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: 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: [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: 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:-} registry-mirror=${mirror:-}" [ -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 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=${{ 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: 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:-} registry-mirror=${mirror:-}" [ -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:-} registry-mirror=${mirror:-}" [ -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