ci: multi-arch-safe php.net guard and emergency workflow (no platform change)
- 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
This commit is contained in:
1 parent
4b35869477
commit
8a6a6d7b44
8 files changed
+565
-24
No files matched your search
@@ -59,12 +59,23 @@ jobs:
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- name: lsphp probe regression test
|
||||
run: ./scripts/tests/lsphp-info-probe.test.sh
|
||||
|
||||
@@ -445,7 +456,10 @@ jobs:
|
||||
# :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).
|
||||
# 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 }}
|
||||
@@ -453,14 +467,7 @@ jobs:
|
||||
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
|
||||
./scripts/php-emergency-guard.sh "$ref" "$NEW_PHP"
|
||||
|
||||
- name: Build and Push LiteSpeed Image
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -605,7 +612,10 @@ jobs:
|
||||
# :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).
|
||||
# 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 }}
|
||||
@@ -613,14 +623,7 @@ jobs:
|
||||
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
|
||||
./scripts/php-emergency-guard.sh "$ref" "$NEW_PHP"
|
||||
|
||||
- name: Build and Push lsphp Image
|
||||
uses: docker/build-push-action@v6
|
||||
|
||||
@@ -15,6 +15,16 @@ run-name: EMERGENCY lsphp php.net build by ${{ gitea.actor }}
|
||||
##
|
||||
## 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:
|
||||
@@ -32,6 +42,16 @@ on:
|
||||
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:
|
||||
@@ -49,6 +69,8 @@ jobs:
|
||||
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: |
|
||||
@@ -60,7 +82,11 @@ jobs:
|
||||
[[ "$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
|
||||
@@ -111,6 +137,12 @@ jobs:
|
||||
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
|
||||
@@ -119,13 +151,15 @@ jobs:
|
||||
username: ${{ secrets.CI_USER }}
|
||||
password: ${{ secrets.CI_TOKEN }}
|
||||
|
||||
- name: Build and push (php.net lsphp)
|
||||
# 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
|
||||
push: true
|
||||
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
|
||||
@@ -135,7 +169,74 @@ jobs:
|
||||
PHP_SRC_VERSION=${{ steps.t.outputs.version }}
|
||||
PHP_SRC_SHA256=${{ steps.v.outputs.sha }}
|
||||
LSPHP_SOURCE=php.net
|
||||
tags: |
|
||||
repo.anhonesthost.net/cloud-hosting-platform/${{ steps.t.outputs.image_name }}:php${{ matrix.phpver }}-phpnet-${{ steps.t.outputs.version }}
|
||||
${{ steps.t.outputs.prod == 'true' && format('repo.anhonesthost.net/cloud-hosting-platform/{0}:php{1}', steps.t.outputs.image_name, matrix.phpver) || '' }}
|
||||
${{ steps.t.outputs.prod == 'true' && matrix.phpver == 85 && format('repo.anhonesthost.net/cloud-hosting-platform/{0}:latest', steps.t.outputs.image_name) || '' }}
|
||||
|
||||
# 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[@]}"
|
||||
Reference in new issue
Block a user