diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml index 4230bc9..fecb4bf 100644 --- a/.gitea/workflows/build-push.yaml +++ b/.gitea/workflows/build-push.yaml @@ -4,10 +4,26 @@ on: push: branches: - main + # Manual publish (e.g. after a [skip ci] merge, or to re-publish after a + # failed arm64 leg). + workflow_dispatch: +# Multi-arch (linux/amd64 + linux/arm64) in ONE job on the trusted x86 runner. +# Each platform is pushed BY DIGEST (untagged) to BOTH registries, then +# scripts/assemble-multiarch-index.sh creates :latest, : and : +# from those digests (same template as build-push-coraza.yaml; see +# docs/arm64/IMAGE-PIPELINE-PLAN.md sections 4.1 / 4.4 in the WHP repo). +# * amd64 is required and built exactly as before; the Dockerfile build gate +# (unit suites + `haproxy -c`) runs in it. Failure = nothing published. +# * arm64 is continue-on-error and runs the SAME Dockerfile gate under QEMU +# (the gate is RUN steps, so it executes on the arm64 binary). If arm64 +# fails or times out the tags are published amd64-only, exactly as before +# multi-arch, and a previous run's arm64 is never re-attached (the digest is +# read only from THIS run's step output). jobs: Build-and-Push: runs-on: ubuntu-latest + timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v4 @@ -30,6 +46,8 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 - name: Set up Docker Buildx uses: https://github.com/docker/setup-buildx-action@v3 @@ -59,20 +77,45 @@ jobs: id: ver run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" - - name: Build Image + # amd64: required. Pushed untagged by digest to both registries (same + # digest in each); tags are only created by the assemble step below. + - name: Build amd64 (push by digest) + id: amd64 uses: docker/build-push-action@v6 with: platforms: linux/amd64 - push: true build-args: | VERSION=${{ steps.ver.outputs.version }} PKG_CACHE=${{ steps.pkgcache.outputs.url }} - # Three tags per registry: :latest (moving), : (human-readable - # release), : (immutable, guaranteed-unique rollback target). - tags: | - repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest - repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:${{ steps.ver.outputs.version }} - repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:${{ gitea.sha }} - ghcr.io/shadowdao/haproxy-manager-base:latest - ghcr.io/shadowdao/haproxy-manager-base:${{ steps.ver.outputs.version }} - ghcr.io/shadowdao/haproxy-manager-base:${{ gitea.sha }} + outputs: type=image,"name=repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base,ghcr.io/shadowdao/haproxy-manager-base",push-by-digest=true,name-canonical=true,push=true + + # arm64: best effort (QEMU, apt-based image). Must never block or alter + # the amd64 tags. + - name: Build arm64 (push by digest) + id: arm64 + continue-on-error: true + timeout-minutes: 30 + uses: docker/build-push-action@v6 + with: + platforms: linux/arm64 + build-args: | + VERSION=${{ steps.ver.outputs.version }} + PKG_CACHE=${{ steps.pkgcache.outputs.url }} + outputs: type=image,"name=repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base,ghcr.io/shadowdao/haproxy-manager-base",push-by-digest=true,name-canonical=true,push=true + + # Three tags per registry: :latest (moving), : (human-readable + # release), : (immutable, guaranteed-unique rollback target). + - name: Assemble tag indexes + env: + AMD64_DIGEST: ${{ steps.amd64.outputs.digest }} + ARM64_DIGEST: ${{ steps.arm64.outputs.digest }} + ARM64_OUTCOME: ${{ steps.arm64.outcome }} + VERSION: ${{ steps.ver.outputs.version }} + SHA: ${{ gitea.sha }} + run: | + scripts/assemble-multiarch-index.sh \ + --amd64 "$AMD64_DIGEST" \ + --arm64 "$ARM64_DIGEST" --arm64-outcome "$ARM64_OUTCOME" \ + --repo repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base \ + --repo ghcr.io/shadowdao/haproxy-manager-base \ + --tag latest --tag "$VERSION" --tag "$SHA" diff --git a/Dockerfile b/Dockerfile index f176241..7b03dbd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,22 +23,30 @@ LABEL org.opencontainers.image.title="haproxy-manager-base" \ org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.licenses="MIT" -# haproxy is PINNED. It was previously unpinned, so the binary could move under -# us at Debian's timing — an upstream release that rejected our config would have -# broken an unrelated commit's build, or worse, shipped an edge that refuses to -# start (see the `haproxy -c` gate below for why that matters: a config HAProxy +# haproxy is PINNED to a reviewed UPSTREAM version (HAPROXY_UPSTREAM), not to one +# Debian package revision. It was previously unpinned, so the binary could move +# under us at Debian's timing - an upstream release that rejected our config would +# have broken an unrelated commit's build, or worse, shipped an edge that refuses +# to start (see the `haproxy -c` gate below for why that matters: a config HAProxy # rejects leaves the container Up with 80/443 unbound and /health still 200). # -# Pinning does NOT make the gate redundant, and the gate does NOT make pinning -# unnecessary — they compose. Pinned means the version moves deliberately; the -# gate then answers immediately whether the new binary still accepts fleet config. -# It also makes the image reproducible, which it previously was not. +# History: this used to pin the exact Debian version (3.0.11-1+deb13u3). Debian +# security then shipped 3.0.11-1+deb13u4 (same upstream 3.0.11, same arch set) and +# the exact pin silently froze the image on the OLDER, unpatched revision, and +# would hard-fail the build as soon as u3 left the archive. Debian security +# revisions of the SAME upstream version are exactly what we want to take, so: +# * an apt preference (Pin: version 3.0.11-*) makes apt install the NEWEST +# Debian revision of that upstream version on any architecture; +# * the build then FAILS unless the installed package AND `haproxy -v` both +# report that upstream version, so a major/minor bump (3.0 -> 3.1, 3.0.12) +# can never arrive by accident: that still needs this ARG changed and the +# `haproxy -c` gate below to pass. +# The pin and the gate compose: pinned = the version moves deliberately; the gate +# answers immediately whether the new binary still accepts fleet config. # -# To move it: bump the version here, rebuild, and let the gate verify. If Debian -# security-updates the package (e.g. -1+deb13u4) the build FAILS until this pin is -# updated — that failure is the point, not a bug. Check availability with: -# apt-cache policy haproxy -ARG HAPROXY_VERSION=3.0.11-1+deb13u3 +# To move to another upstream version: change HAPROXY_UPSTREAM, rebuild, let the +# gate verify. Check what Debian offers with: apt-cache madison haproxy +ARG HAPROXY_UPSTREAM=3.0.11 # OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40 # (CT130 pkg-cache, https only; CI sets it only after probing the cache with the # pinned CA). Unset = the upstream Debian repositories, exactly as before. The @@ -50,7 +58,14 @@ ARG PKG_CACHE= COPY ./scripts/pkg-cache-repos.sh /usr/local/sbin/pkg-cache-repos.sh COPY ./keys/pkg-cache/ /usr/local/share/pkg-cache/ RUN /usr/local/sbin/pkg-cache-repos.sh apt && \ - apt update -y && apt dist-upgrade -y && apt install socat "haproxy=${HAPROXY_VERSION}" cron certbot curl jq net-tools -y && apt-mark hold haproxy && \ + apt update -y && apt dist-upgrade -y && \ + printf 'Package: haproxy\nPin: version %s-*\nPin-Priority: 1001\n' "${HAPROXY_UPSTREAM}" > /etc/apt/preferences.d/haproxy-upstream-pin && \ + apt install socat haproxy cron certbot curl jq net-tools -y && apt-mark hold haproxy && \ + rm -f /etc/apt/preferences.d/haproxy-upstream-pin && \ + deb_ver="$(dpkg-query -W -f='${Version}' haproxy)" && \ + case "${deb_ver}" in "${HAPROXY_UPSTREAM}"-*) ;; *) echo "FATAL: installed haproxy ${deb_ver} is not upstream ${HAPROXY_UPSTREAM}" >&2; exit 1;; esac && \ + haproxy -v | head -n1 | grep -F "version ${HAPROXY_UPSTREAM}-" && \ + echo "haproxy pin OK: upstream ${HAPROXY_UPSTREAM}, Debian package ${deb_ver}" && \ /usr/local/sbin/pkg-cache-repos.sh restore && \ apt clean && rm -rf /var/lib/apt/lists/* WORKDIR /haproxy @@ -95,8 +110,8 @@ RUN pip install -r requirements.txt # * it cannot be skipped, and it protects local `docker build` too; # * no workflow restructuring (build-push-action builds and pushes in one # step, so gating in CI would mean splitting build from push); -# * it validates against the EXACT haproxy binary in this image. Line 26 -# installs haproxy unpinned, so that binary moves between builds - this +# * it validates against the EXACT haproxy binary in this image. The +# installed haproxy can move between builds (Debian revisions) - this # turns "the new haproxy rejects our config" from a silent production # risk into a build failure. # diff --git a/VERSION b/VERSION index b2cbf7e..99b89ef 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2026.11.2 +2026.11.3