- scripts/image-label-amd64.sh: index-safe read of the amd64 image's labels (OCI index with attestations, Docker manifest list, single manifest). - scripts/php-emergency-guard.sh: used by both build-push.yaml guard sites; same decisions as the old inline step on single-platform images, but FAILS CLOSED when the labels cannot be read (only "tag not found" passes). - lsphp-source-emergency.yaml: push by digest + assemble (copy of HMB assemble-multiarch-index.sh); optional arm64; refuses to replace a multi-arch production tag with amd64-only unless allow_amd64_only. - Tests wired into Shell-Checks; main build matrix platforms unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185ukXjXyosSkunDsdWz3mb
243 lines
12 KiB
YAML
243 lines
12 KiB
YAML
name: lsphp from php.net source (EMERGENCY)
|
|
run-name: EMERGENCY lsphp php.net build by ${{ gitea.actor }}
|
|
|
|
## NEVER runs on push. Use only when a PHP security release is out and the
|
|
## LiteSpeed apt repo (rpms.litespeedtech.com/debian, noble) still ships the
|
|
## vulnerable lsphp. Runbook + exact trigger command: docs/lsphp-source-emergency.md
|
|
##
|
|
## What it builds: the normal Dockerfile.lsphp / Dockerfile.litespeed with
|
|
## PHP_SRC_VERSION / PHP_SRC_SHA256 / LSPHP_SOURCE=php.net
|
|
## so lsphp${PHPVER} is compiled from the php.net tarball (sha256 from the
|
|
## php.net releases JSON + OpenPGP signature against keys/php-src/), configured
|
|
## from the packaged lsphp's own Configure Command, and the image build FAILS
|
|
## unless the result has the same extensions / ini / features as the packaged
|
|
## lsphp (scripts/lsphp-src-overlay.sh).
|
|
##
|
|
## Tags: always :phpNN-phpnet-X.Y.Z (immutable, for staging / rollback)
|
|
## and :phpNN only when push_production_tags = true.
|
|
##
|
|
## Multi-arch safety (docs/arm64/IMAGE-PIPELINE-PLAN.md 2.2 #6, 4.4): each
|
|
## platform is pushed BY DIGEST and the tags are assembled afterwards with
|
|
## scripts/assemble-multiarch-index.sh (copy of haproxy-manager-base's), so a
|
|
## tag is never overwritten with a bare single-platform manifest by accident.
|
|
## arm64 is built from the SAME php.net source in THIS run (continue-on-error);
|
|
## a previous run's arm64 is never re-attached. If a production tag is
|
|
## currently a multi-arch index and arm64 was not built successfully, the
|
|
## production tag is REFUSED unless allow_amd64_only = true (an explicit
|
|
## security-over-parity decision); the immutable tag is still created.
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
php_versions:
|
|
description: "Space-separated PHPVER=X.Y.Z pairs, e.g. '82=8.2.35 83=8.3.36 84=8.4.27 85=8.5.12'"
|
|
required: true
|
|
type: string
|
|
images:
|
|
description: "Which images: both | lsphp | litespeed"
|
|
required: true
|
|
default: both
|
|
type: string
|
|
push_production_tags:
|
|
description: "Also push :phpNN (what WHP pulls). false = only :phpNN-phpnet-X.Y.Z for staging."
|
|
required: true
|
|
default: false
|
|
type: boolean
|
|
build_arm64:
|
|
description: "Also build linux/arm64 (QEMU, slow, best effort). Needed to keep arm64 in a multi-arch :phpNN."
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
allow_amd64_only:
|
|
description: "Allow replacing a multi-arch production tag with amd64-only when arm64 is not built/failed (ARM hosts then cannot pull it)."
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
|
|
jobs:
|
|
Build-lsphp-from-source:
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
phpver: [82, 83, 84, 85]
|
|
image: [lsphp, litespeed]
|
|
steps:
|
|
- name: Select this job's target (skip if not requested)
|
|
id: t
|
|
# inputs via env, never spliced into the script text
|
|
env:
|
|
IN_VERSIONS: ${{ inputs.php_versions }}
|
|
IN_IMAGES: ${{ inputs.images }}
|
|
IN_PROD: ${{ format('{0}', inputs.push_production_tags) }}
|
|
IN_ARM: ${{ format('{0}', inputs.build_arm64) }}
|
|
IN_AMD_ONLY: ${{ format('{0}', inputs.allow_amd64_only) }}
|
|
PHPVER: ${{ matrix.phpver }}
|
|
IMG: ${{ matrix.image }}
|
|
run: |
|
|
set -euo pipefail
|
|
case "$IN_IMAGES" in both|lsphp|litespeed) ;; *) echo "::error::images must be both|lsphp|litespeed, got '$IN_IMAGES'"; exit 1 ;; esac
|
|
case "$IN_PROD" in true|false) ;; *) echo "::error::push_production_tags must be true|false, got '$IN_PROD'"; exit 1 ;; esac
|
|
v=""
|
|
for pair in $IN_VERSIONS; do
|
|
[[ "$pair" =~ ^(8[2-5])=([0-9]+\.[0-9]+\.[0-9]+)$ ]] || { echo "::error::bad php_versions entry '$pair' (want NN=X.Y.Z, NN in 82..85)"; exit 1; }
|
|
[ "${BASH_REMATCH[1]}" = "$PHPVER" ] && v="${BASH_REMATCH[2]}"
|
|
done
|
|
case "$IN_ARM" in true|false) ;; *) echo "::error::build_arm64 must be true|false, got '$IN_ARM'"; exit 1 ;; esac
|
|
case "$IN_AMD_ONLY" in true|false) ;; *) echo "::error::allow_amd64_only must be true|false, got '$IN_AMD_ONLY'"; exit 1 ;; esac
|
|
echo "prod=$IN_PROD" >> "$GITHUB_OUTPUT"
|
|
echo "arm=$IN_ARM" >> "$GITHUB_OUTPUT"
|
|
echo "amd_only=$IN_AMD_ONLY" >> "$GITHUB_OUTPUT"
|
|
if [ -z "$v" ] || { [ "$IN_IMAGES" != both ] && [ "$IN_IMAGES" != "$IMG" ]; }; then
|
|
echo "not requested: php${PHPVER} / ${IMG}"; echo "skip=1" >> "$GITHUB_OUTPUT"; exit 0
|
|
fi
|
|
echo "skip=0" >> "$GITHUB_OUTPUT"
|
|
echo "version=$v" >> "$GITHUB_OUTPUT"
|
|
case "$IMG" in lsphp|litespeed) ;; *) echo "::error::unexpected matrix image '$IMG'"; exit 1 ;; esac
|
|
echo "image_name=cac-${IMG}" >> "$GITHUB_OUTPUT"
|
|
echo "dockerfile=./Dockerfile.${IMG}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Checkout
|
|
if: steps.t.outputs.skip == '0'
|
|
uses: actions/checkout@v4
|
|
|
|
# sha256 of php-X.Y.Z.tar.xz from php.net's own releases JSON. The build
|
|
# re-verifies it AND the OpenPGP signature; this step only refuses early
|
|
# if the release does not exist or is not newer than what LiteSpeed ships.
|
|
- name: Resolve php.net release + packaged lsphp
|
|
if: steps.t.outputs.skip == '0'
|
|
id: v
|
|
env:
|
|
V: ${{ steps.t.outputs.version }}
|
|
PHPVER: ${{ matrix.phpver }}
|
|
RUN_ID: ${{ gitea.run_id }}
|
|
run: |
|
|
set -euo pipefail
|
|
v="$V"
|
|
[[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed version '$v'"; exit 1; }
|
|
json=$(curl -fsS --retry 3 --max-time 60 "https://www.php.net/releases/index.php?json&version=$v")
|
|
sha=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); v=sys.argv[1]; print(next((s["sha256"] for s in d.get("source", []) if s.get("filename")=="php-%s.tar.xz"%v), ""))' "$v")
|
|
[ -n "$sha" ] || { echo "::error::php.net has no release php-$v.tar.xz (EOL branch? typo?): $json"; exit 1; }
|
|
[[ "$sha" =~ ^[0-9a-f]{64}$ ]] || { echo "::error::no sha256 for php-$v.tar.xz in the php.net releases JSON"; exit 1; }
|
|
pkg=$(./scripts/lsphp-repo-version.sh "${PHPVER}" "${LSPHP_ARCH:-amd64}")
|
|
[ -n "$pkg" ] || { echo "::error::lsphp${PHPVER} not in the LiteSpeed repo index"; exit 1; }
|
|
[[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; }
|
|
pphp=${pkg%%-*}
|
|
[[ "$pphp" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '$pphp' from lsphp${PHPVER} $pkg"; exit 1; }
|
|
if [ "$(printf '%s\n%s\n' "$pphp" "$v" | sort -V | tail -n1)" != "$v" ] || [ "$pphp" = "$v" ]; then
|
|
echo "::error::LiteSpeed already ships lsphp${PHPVER} $pkg (PHP $pphp) >= $v — use the normal build (push to trunk / dispatch build-push.yaml)."
|
|
exit 1
|
|
fi
|
|
echo "php.net $v sha256=$sha ; LiteSpeed lsphp${PHPVER}=$pkg"
|
|
echo "sha=$sha" >> "$GITHUB_OUTPUT"
|
|
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
|
|
echo "pkgphp=$pphp" >> "$GITHUB_OUTPUT"
|
|
echo "bust=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Set up Docker Buildx
|
|
if: steps.t.outputs.skip == '0'
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Set up QEMU (arm64)
|
|
if: steps.t.outputs.skip == '0' && steps.t.outputs.arm == 'true'
|
|
uses: docker/setup-qemu-action@v3
|
|
with:
|
|
platforms: arm64
|
|
|
|
- name: Login to Gitea
|
|
if: steps.t.outputs.skip == '0'
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: repo.anhonesthost.net
|
|
username: ${{ secrets.CI_USER }}
|
|
password: ${{ secrets.CI_TOKEN }}
|
|
|
|
# amd64: required. Pushed untagged by digest; tags are made below.
|
|
- name: Build amd64 (push by digest)
|
|
if: steps.t.outputs.skip == '0'
|
|
id: amd64
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
file: ${{ steps.t.outputs.dockerfile }}
|
|
platforms: linux/amd64
|
|
outputs: type=image,name=repo.anhonesthost.net/cloud-hosting-platform/${{ steps.t.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
|
build-args: |
|
|
PHPVER=${{ matrix.phpver }}
|
|
OLS_VERSION=1.8.4
|
|
CACHE_BUST=${{ steps.v.outputs.bust }}
|
|
EXPECT_LSPHP_PKG=${{ steps.v.outputs.pkg }}
|
|
EXPECT_PHP_VERSION=${{ steps.v.outputs.pkgphp }}
|
|
PHP_SRC_VERSION=${{ steps.t.outputs.version }}
|
|
PHP_SRC_SHA256=${{ steps.v.outputs.sha }}
|
|
LSPHP_SOURCE=php.net
|
|
|
|
# arm64: best effort, same php.net source and version, never blocks amd64.
|
|
- name: Build arm64 (push by digest)
|
|
if: steps.t.outputs.skip == '0' && steps.t.outputs.arm == 'true'
|
|
id: arm64
|
|
continue-on-error: true
|
|
timeout-minutes: 120
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
file: ${{ steps.t.outputs.dockerfile }}
|
|
platforms: linux/arm64
|
|
outputs: type=image,name=repo.anhonesthost.net/cloud-hosting-platform/${{ steps.t.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
|
build-args: |
|
|
PHPVER=${{ matrix.phpver }}
|
|
OLS_VERSION=1.8.4
|
|
CACHE_BUST=${{ steps.v.outputs.bust }}
|
|
EXPECT_LSPHP_PKG=${{ steps.v.outputs.pkg }}
|
|
EXPECT_PHP_VERSION=${{ steps.v.outputs.pkgphp }}
|
|
PHP_SRC_VERSION=${{ steps.t.outputs.version }}
|
|
PHP_SRC_SHA256=${{ steps.v.outputs.sha }}
|
|
LSPHP_SOURCE=php.net
|
|
|
|
# Immutable staging/rollback tag: always created (amd64, + arm64 if built).
|
|
- name: Assemble immutable tag
|
|
if: steps.t.outputs.skip == '0'
|
|
env:
|
|
AMD64_DIGEST: ${{ steps.amd64.outputs.digest }}
|
|
ARM64_DIGEST: ${{ steps.arm64.outputs.digest }}
|
|
ARM64_OUTCOME: ${{ steps.arm64.outcome }}
|
|
REPO: repo.anhonesthost.net/cloud-hosting-platform/${{ steps.t.outputs.image_name }}
|
|
TAG: php${{ matrix.phpver }}-phpnet-${{ steps.t.outputs.version }}
|
|
run: |
|
|
./scripts/assemble-multiarch-index.sh \
|
|
--amd64 "$AMD64_DIGEST" \
|
|
--arm64 "$ARM64_DIGEST" --arm64-outcome "${ARM64_OUTCOME:-skipped}" \
|
|
--repo "$REPO" --tag "$TAG"
|
|
|
|
# Production tags: never silently drop arm64 from a multi-arch tag.
|
|
- name: Assemble production tags
|
|
if: steps.t.outputs.skip == '0' && steps.t.outputs.prod == 'true'
|
|
env:
|
|
AMD64_DIGEST: ${{ steps.amd64.outputs.digest }}
|
|
ARM64_DIGEST: ${{ steps.arm64.outputs.digest }}
|
|
ARM64_OUTCOME: ${{ steps.arm64.outcome }}
|
|
ALLOW_AMD_ONLY: ${{ steps.t.outputs.amd_only }}
|
|
REPO: repo.anhonesthost.net/cloud-hosting-platform/${{ steps.t.outputs.image_name }}
|
|
PHPVER: ${{ matrix.phpver }}
|
|
run: |
|
|
set -euo pipefail
|
|
tags=(--tag "php${PHPVER}")
|
|
[ "$PHPVER" = 85 ] && tags+=(--tag latest)
|
|
if [ "${ARM64_OUTCOME:-skipped}" != success ] && [ "$ALLOW_AMD_ONLY" != true ]; then
|
|
for t in "php${PHPVER}" $([ "$PHPVER" = 85 ] && echo latest); do
|
|
rc=0; info=$(./scripts/index-platforms.sh "$REPO:$t") || rc=$?
|
|
case "$rc" in
|
|
0) ;;
|
|
3) info="not found" ;;
|
|
*) echo "::error::cannot tell whether $REPO:$t is multi-arch; refusing to overwrite it (fail closed)"; exit 1 ;;
|
|
esac
|
|
echo "$REPO:$t currently: $info"
|
|
case "$info" in
|
|
*linux/arm64*)
|
|
echo "::error::$REPO:$t is multi-arch (has arm64) but arm64 was not built successfully in this run (outcome=${ARM64_OUTCOME:-skipped}). Publishing amd64-only would drop arm64. Re-run with build_arm64=true, or set allow_amd64_only=true to accept that. The immutable tag was still created."
|
|
exit 1 ;;
|
|
esac
|
|
done
|
|
fi
|
|
./scripts/assemble-multiarch-index.sh \
|
|
--amd64 "$AMD64_DIGEST" \
|
|
--arm64 "$ARM64_DIGEST" --arm64-outcome "${ARM64_OUTCOME:-skipped}" \
|
|
--repo "$REPO" "${tags[@]}"
|