5757 with :
5858 fetch-depth : 0
5959
60+ # The runners are amd64. Every other image platform is built and gated
61+ # under emulation, so binfmt has to be registered before buildx starts.
62+ # Rendered only while linux/amd64,linux/arm64 asks for a non-native
63+ # platform -- an amd64-only image installs nothing.
64+ - name : Set up QEMU
65+ if : ${{ github.event_name != 'pull_request' }}
66+ uses : docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
67+
6068 - name : Set up Docker Buildx
6169 uses : docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
6270 with :
@@ -227,6 +235,20 @@ jobs:
227235 platform_digest="$digest"
228236 fi
229237 case "$platform_digest" in sha256:*) ;; *) echo "unexpected source platform digest: $platform_digest" >&2; exit 1 ;; esac
238+ # BASE_IMAGE is passed as the index digest, so buildx picks the right
239+ # manifest per platform on its own. Check up front that every platform
240+ # we are about to build is actually in there: a missing one otherwise
241+ # surfaces much later as a bare "no match for platform" from a build
242+ # step that has no idea which of its inputs was at fault.
243+ for want in linux/amd64 linux/arm64; do
244+ jq -e --arg want "$want" '
245+ [.manifest.manifests[]? | select("\(.platform.os)/\(.platform.architecture)" == $want)]
246+ | length == 1
247+ ' <<<"$inspect" >/dev/null || {
248+ echo "::error title=Base image is missing a platform::${source} does not publish ${want}; the DHI customization has to be built for it first" >&2
249+ exit 1
250+ }
251+ done
230252 if [[ "$source" == *@sha256:* ]]; then
231253 test "${source##*@}" = "$digest"
232254 ref="$source"
@@ -245,6 +267,9 @@ jobs:
245267 digest="$(jq -er '.manifest.digest' <<<"$inspect")"
246268 test -n "$digest"
247269 case "$digest" in sha256:*) ;; *) echo "unexpected Go toolchain digest: $digest" >&2; exit 1 ;; esac
270+ # amd64 on purpose, and not a copy of the base check above: golang-cross
271+ # is published for amd64 only. Other platforms take the Go distribution
272+ # from go.dev instead -- see the sha256 resolution further down.
248273 if jq -e '(.manifest.manifests? | type) == "array"' <<<"$inspect" >/dev/null; then
249274 platform_digest="$(jq -er '
250275 [.manifest.manifests[] | select(
@@ -264,13 +289,50 @@ jobs:
264289 echo "platform_digest=$platform_digest" >> "$GITHUB_OUTPUT"
265290 echo "ref=$ref" >> "$GITHUB_OUTPUT"
266291
292+ # golang-cross is the authority for WHICH Go version the gateway uses,
293+ # because the gateway release runs this same image. Read the version
294+ # out of it so platforms that cannot COPY from it can still be pinned
295+ # to exactly that version.
296+ #
297+ # This costs a full pull of golang-cross into the daemon, which is why
298+ # it is not done unconditionally: an amd64 image COPYs its toolchain
299+ # from that image anyway and reads the version out of it for free.
300+ go_version="$(docker run --rm --platform linux/amd64 "$ref" go version | awk '{print $3}')"
301+ case "$go_version" in
302+ go1.*) ;;
303+ *) echo "unexpected Go version from ${ref}: $go_version" >&2; exit 1 ;;
304+ esac
305+ echo "version=$go_version" >> "$GITHUB_OUTPUT"
306+
307+ # golang-cross is published for amd64 only, so every other platform
308+ # takes the stock go.dev tarball at the version resolved above. Pin the
309+ # checksum here, before anything is downloaded, so the image build
310+ # verifies against a value settled outside it.
311+ #
312+ # Deliberately fatal: this only renders when a platform that needs the
313+ # tarball is being built, and guessing a checksum is not an option.
314+ sha_arm64="$(curl -fsSL --retry 3 --proto '=https' --tlsv1.2 \
315+ 'https://go.dev/dl/?mode=json&include=all' | jq -er --arg v "$go_version" '
316+ [.[] | select(.version == $v) | .files[]
317+ | select(.os == "linux" and .arch == "arm64" and .kind == "archive")]
318+ | if length == 1 then .[0].sha256
319+ else error("no unique linux-arm64 archive for \($v)")
320+ end')"
321+ case "$sha_arm64" in
322+ ????????????????????????????????????????????????????????????????) ;;
323+ *) echo "unexpected sha256 for ${go_version} linux-arm64: $sha_arm64" >&2; exit 1 ;;
324+ esac
325+ echo "sha256_arm64=$sha_arm64" >> "$GITHUB_OUTPUT"
326+
267327 - name : Build workflow-local NG base
268328 id : build-ng-base
269329 uses : docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4
270330 with :
271331 context : ci/images/plugin-compiler-ng
272332 file : ci/images/plugin-compiler-ng/Dockerfile.base
273- platforms : linux/amd64
333+ # Pull requests use the `docker` driver, which builds one platform
334+ # only, and they never push -- so they stay on the gate platform.
335+ platforms : ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
274336 push : ${{ github.event_name != 'pull_request' }}
275337 load : ${{ github.event_name == 'pull_request' }}
276338 sbom : ${{ github.event_name != 'pull_request' }}
@@ -319,13 +381,16 @@ jobs:
319381 with :
320382 context : .
321383 file : ci/images/plugin-compiler-ng/Dockerfile.release
384+ # Single platform deliberately: `load: true` accepts exactly one.
322385 platforms : linux/amd64
323386 push : false
324387 load : true
325388 tags : tyk-plugin-compiler-ng-gate:std
326389 build-args : |
327390 BASE_IMAGE=${{ env.NG_BASE_IMAGE }}
328391 GOLANG_IMAGE=${{ steps.source-go.outputs.ref }}
392+ GO_VERSION=${{ steps.source-go.outputs.version }}
393+ GO_TARBALL_SHA256_ARM64=${{ steps.source-go.outputs.sha256_arm64 }}
329394 GITHUB_SHA=${{ github.sha }}
330395 GITHUB_TAG=${{ github.ref_name }}
331396 WITH_GATEWAY_SELFTEST=1
@@ -348,7 +413,7 @@ jobs:
348413 with :
349414 context : .
350415 file : ci/images/plugin-compiler-ng/Dockerfile.release
351- platforms : linux/amd64
416+ platforms : linux/amd64,linux/arm64
352417 push : true
353418 sbom : true
354419 provenance : mode=max
@@ -357,6 +422,8 @@ jobs:
357422 build-args : |
358423 BASE_IMAGE=${{ env.NG_BASE_IMAGE }}
359424 GOLANG_IMAGE=${{ steps.source-go.outputs.ref }}
425+ GO_VERSION=${{ steps.source-go.outputs.version }}
426+ GO_TARBALL_SHA256_ARM64=${{ steps.source-go.outputs.sha256_arm64 }}
360427 GITHUB_SHA=${{ github.sha }}
361428 GITHUB_TAG=${{ github.ref_name }}
362429 WITH_GATEWAY_SELFTEST=0
@@ -415,6 +482,28 @@ jobs:
415482 docker buildx imagetools inspect "$exact_ref" --format '{{json .Provenance}}' > "$provenance_file"
416483 jq -e '[.. | objects | select(has("SLSA"))] | length > 0' "$provenance_file" >/dev/null
417484
485+ # The index must offer exactly the platforms that were asked for --
486+ # no more, no fewer. buildx reports success for a partially built
487+ # index, and an attestation manifest carries `unknown/unknown`, so
488+ # counting manifests or trusting the exit code would both pass while
489+ # a platform silently went missing. `.manifests` being absent
490+ # entirely means the push produced a bare manifest instead of an
491+ # index, which is the same failure in its most extreme form, so name
492+ # it rather than letting jq fall over on a missing key.
493+ platforms_file="$evidence_dir/${safe_repository}.platforms.txt"
494+ docker buildx imagetools inspect "$exact_ref" --raw |
495+ jq -er 'if has("manifests") then
496+ .manifests[]
497+ | select(.platform.os != "unknown") # attestations carry unknown/unknown
498+ | "\(.platform.os)/\(.platform.architecture)"
499+ else error("published ref is not an image index") end' |
500+ sort -u > "$platforms_file"
501+ printf '%s\n' linux/amd64 linux/arm64 > "$evidence_dir/platforms.expected.txt"
502+ if ! diff -u "$evidence_dir/platforms.expected.txt" "$platforms_file"; then
503+ echo "::error title=Published platforms do not match::${exact_ref} does not publish exactly linux/amd64,linux/arm64" >&2
504+ exit 1
505+ fi
506+
418507 printf 'verified %s\n' "$exact_ref" | tee -a "$evidence_dir/verified-refs.txt" "$GITHUB_STEP_SUMMARY"
419508 done < "$repositories"
420509
@@ -451,13 +540,16 @@ jobs:
451540 with :
452541 context : .
453542 file : ci/images/plugin-compiler-ng/Dockerfile.release
543+ # Single platform deliberately: `load: true` accepts exactly one.
454544 platforms : linux/amd64
455545 push : false
456546 load : true
457547 tags : tyk-plugin-compiler-ng-gate:ee
458548 build-args : |
459549 BASE_IMAGE=${{ env.NG_BASE_IMAGE }}
460550 GOLANG_IMAGE=${{ steps.source-go.outputs.ref }}
551+ GO_VERSION=${{ steps.source-go.outputs.version }}
552+ GO_TARBALL_SHA256_ARM64=${{ steps.source-go.outputs.sha256_arm64 }}
461553 GITHUB_SHA=${{ github.sha }}
462554 GITHUB_TAG=${{ github.ref_name }}
463555 WITH_GATEWAY_SELFTEST=1
@@ -473,14 +565,56 @@ jobs:
473565 run : VALIDATE_ONLY=1 ci/images/plugin-compiler-ng/scripts/loadtest-gate.sh tyk-plugin-compiler-ng-gate:ee ee arm64
474566
475567
568+ # Tier B gate. The push below emits a single OCI index covering every
569+ # platform, so a broken linux/arm64 image blocks the whole push --
570+ # including platforms that are perfectly fine. Build and exercise this one
571+ # on its own first, where a failure is attributable and nothing has been
572+ # published yet.
573+ #
574+ # WITH_GATEWAY_SELFTEST=0 on purpose: VALIDATE_ONLY skips the load/HTTP
575+ # gate, so the gateway binary would be built under emulation and never run.
576+ - name : Build linux/arm64 NG gate image EE
577+ if : ${{ github.event_name != 'pull_request' }}
578+ uses : docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4
579+ with :
580+ context : .
581+ file : ci/images/plugin-compiler-ng/Dockerfile.release
582+ platforms : linux/arm64
583+ push : false
584+ load : true
585+ tags : tyk-plugin-compiler-ng-gate-linux-arm64:ee
586+ build-args : |
587+ BASE_IMAGE=${{ env.NG_BASE_IMAGE }}
588+ GOLANG_IMAGE=${{ steps.source-go.outputs.ref }}
589+ GO_VERSION=${{ steps.source-go.outputs.version }}
590+ GO_TARBALL_SHA256_ARM64=${{ steps.source-go.outputs.sha256_arm64 }}
591+ GITHUB_SHA=${{ github.sha }}
592+ GITHUB_TAG=${{ github.ref_name }}
593+ WITH_GATEWAY_SELFTEST=0
594+ DEFAULT_EDITION=ee
595+ EE_AVAILABLE=true
596+ EE_BUILD_TAG=ee
597+ SELFTEST_BUILD_TAG=ee
598+
599+ # Proves the linux/arm64 toolchain executes and emits a loadable
600+ # object for its own architecture -- strictly more than the cross-build
601+ # checks above, which only ever exercise the amd64 image.
602+ #
603+ # It stops short of plugin.Open in a linux/arm64 gateway: that needs a
604+ # runner of this architecture. See the PR for the gap and the follow-up.
605+ - name : Validate linux/arm64 NG toolchain EE
606+ if : ${{ github.event_name != 'pull_request' }}
607+ run : VALIDATE_ONLY=1 COMPILER_PLATFORM=linux/arm64 ci/images/plugin-compiler-ng/scripts/loadtest-gate.sh tyk-plugin-compiler-ng-gate-linux-arm64:ee ee
608+
609+
476610 - name : Build and push NG image EE
477611 if : ${{ github.event_name != 'pull_request' }}
478612 id : build-push-ee-ng
479613 uses : docker/build-push-action@0a97817b6ade9f46837855d676c4cca3a2471fc9 # v4
480614 with :
481615 context : .
482616 file : ci/images/plugin-compiler-ng/Dockerfile.release
483- platforms : linux/amd64
617+ platforms : linux/amd64,linux/arm64
484618 push : true
485619 sbom : true
486620 provenance : mode=max
@@ -489,6 +623,8 @@ jobs:
489623 build-args : |
490624 BASE_IMAGE=${{ env.NG_BASE_IMAGE }}
491625 GOLANG_IMAGE=${{ steps.source-go.outputs.ref }}
626+ GO_VERSION=${{ steps.source-go.outputs.version }}
627+ GO_TARBALL_SHA256_ARM64=${{ steps.source-go.outputs.sha256_arm64 }}
492628 GITHUB_SHA=${{ github.sha }}
493629 GITHUB_TAG=${{ github.ref_name }}
494630 WITH_GATEWAY_SELFTEST=0
@@ -550,6 +686,28 @@ jobs:
550686 docker buildx imagetools inspect "$exact_ref" --format '{{json .Provenance}}' > "$provenance_file"
551687 jq -e '[.. | objects | select(has("SLSA"))] | length > 0' "$provenance_file" >/dev/null
552688
689+ # The index must offer exactly the platforms that were asked for --
690+ # no more, no fewer. buildx reports success for a partially built
691+ # index, and an attestation manifest carries `unknown/unknown`, so
692+ # counting manifests or trusting the exit code would both pass while
693+ # a platform silently went missing. `.manifests` being absent
694+ # entirely means the push produced a bare manifest instead of an
695+ # index, which is the same failure in its most extreme form, so name
696+ # it rather than letting jq fall over on a missing key.
697+ platforms_file="$evidence_dir/${safe_repository}.platforms.txt"
698+ docker buildx imagetools inspect "$exact_ref" --raw |
699+ jq -er 'if has("manifests") then
700+ .manifests[]
701+ | select(.platform.os != "unknown") # attestations carry unknown/unknown
702+ | "\(.platform.os)/\(.platform.architecture)"
703+ else error("published ref is not an image index") end' |
704+ sort -u > "$platforms_file"
705+ printf '%s\n' linux/amd64 linux/arm64 > "$evidence_dir/platforms.expected.txt"
706+ if ! diff -u "$evidence_dir/platforms.expected.txt" "$platforms_file"; then
707+ echo "::error title=Published platforms do not match::${exact_ref} does not publish exactly linux/amd64,linux/arm64" >&2
708+ exit 1
709+ fi
710+
553711 printf 'verified %s\n' "$exact_ref" | tee -a "$evidence_dir/verified-refs.txt" "$GITHUB_STEP_SUMMARY"
554712 done < "$repositories"
555713
@@ -586,13 +744,16 @@ jobs:
586744 with :
587745 context : .
588746 file : ci/images/plugin-compiler-ng/Dockerfile.release
747+ # Single platform deliberately: `load: true` accepts exactly one.
589748 platforms : linux/amd64
590749 push : false
591750 load : true
592751 tags : tyk-plugin-compiler-ng-gate:fips
593752 build-args : |
594753 BASE_IMAGE=${{ env.NG_BASE_IMAGE }}
595754 GOLANG_IMAGE=${{ steps.source-go.outputs.ref }}
755+ GO_VERSION=${{ steps.source-go.outputs.version }}
756+ GO_TARBALL_SHA256_ARM64=${{ steps.source-go.outputs.sha256_arm64 }}
596757 GITHUB_SHA=${{ github.sha }}
597758 GITHUB_TAG=${{ github.ref_name }}
598759 WITH_GATEWAY_SELFTEST=1
@@ -617,7 +778,7 @@ jobs:
617778 with :
618779 context : .
619780 file : ci/images/plugin-compiler-ng/Dockerfile.release
620- platforms : linux/amd64
781+ platforms : linux/amd64,linux/arm64
621782 push : true
622783 sbom : true
623784 provenance : mode=max
@@ -626,6 +787,8 @@ jobs:
626787 build-args : |
627788 BASE_IMAGE=${{ env.NG_BASE_IMAGE }}
628789 GOLANG_IMAGE=${{ steps.source-go.outputs.ref }}
790+ GO_VERSION=${{ steps.source-go.outputs.version }}
791+ GO_TARBALL_SHA256_ARM64=${{ steps.source-go.outputs.sha256_arm64 }}
629792 GITHUB_SHA=${{ github.sha }}
630793 GITHUB_TAG=${{ github.ref_name }}
631794 WITH_GATEWAY_SELFTEST=0
@@ -689,6 +852,28 @@ jobs:
689852 docker buildx imagetools inspect "$exact_ref" --format '{{json .Provenance}}' > "$provenance_file"
690853 jq -e '[.. | objects | select(has("SLSA"))] | length > 0' "$provenance_file" >/dev/null
691854
855+ # The index must offer exactly the platforms that were asked for --
856+ # no more, no fewer. buildx reports success for a partially built
857+ # index, and an attestation manifest carries `unknown/unknown`, so
858+ # counting manifests or trusting the exit code would both pass while
859+ # a platform silently went missing. `.manifests` being absent
860+ # entirely means the push produced a bare manifest instead of an
861+ # index, which is the same failure in its most extreme form, so name
862+ # it rather than letting jq fall over on a missing key.
863+ platforms_file="$evidence_dir/${safe_repository}.platforms.txt"
864+ docker buildx imagetools inspect "$exact_ref" --raw |
865+ jq -er 'if has("manifests") then
866+ .manifests[]
867+ | select(.platform.os != "unknown") # attestations carry unknown/unknown
868+ | "\(.platform.os)/\(.platform.architecture)"
869+ else error("published ref is not an image index") end' |
870+ sort -u > "$platforms_file"
871+ printf '%s\n' linux/amd64 linux/arm64 > "$evidence_dir/platforms.expected.txt"
872+ if ! diff -u "$evidence_dir/platforms.expected.txt" "$platforms_file"; then
873+ echo "::error title=Published platforms do not match::${exact_ref} does not publish exactly linux/amd64,linux/arm64" >&2
874+ exit 1
875+ fi
876+
692877 printf 'verified %s\n' "$exact_ref" | tee -a "$evidence_dir/verified-refs.txt" "$GITHUB_STEP_SUMMARY"
693878 done < "$repositories"
694879
0 commit comments