This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit 80565f563b6ea47c274141029d8ef6bd33b7eeac
Author: Wu Sheng <[email protected]>
AuthorDate: Mon May 18 21:49:27 2026 +0800

    ci: multi-arch publish-image — native amd64 + arm64 builds, OCI manifest 
list
    
    The previous single-arch (linux/amd64) image worked on amd64 hosts but
    forced Apple Silicon Macs and AWS Graviton boxes through Rosetta /
    QEMU translation — argon2's CPU-intensive hashing took the brunt of
    the slowdown. With this change the canonical SHA tag on GHCR resolves
    to the host's native arch automatically.
    
    structure
    - New `tags` job computes the canonical SHA + moving tag set (`:main`,
      `:vX.Y.Z`, `:latest`, `:major.minor`) once and outputs them so the
      matrix build + the manifest job share the same vocabulary without
      duplicating the shell math.
    - `build` matrix runs on native runners — `ubuntu-latest` for amd64,
      `ubuntu-24.04-arm` for arm64 (free for public ASF repos). Each
      produces its own `./dist/` with the correct argon2 native binding,
      pushes `<base>:<sha>-<arch>`, and uses an arch-scoped GHA cache.
    - `manifest` job stitches the two `:<sha>-<arch>` tags into a single
      OCI manifest list per moving tag via `docker buildx imagetools
      create`. Pulling the canonical `:<sha>` (or `:main`, `:vX.Y.Z`, etc.)
      from any host selects the matching arch.
    
    trade-offs / follow-ups
    - If the org doesn't have `ubuntu-24.04-arm` runners enabled, the
      arm64 build fails and the manifest step short-circuits. Fix is org-
      side (enable the runner) or drop the arm64 matrix entry and accept
      Rosetta-emulation on Macs.
    - Per-arch debugger tags (`<sha>-amd64`, `<sha>-arm64`) remain
      permanently pushed so investigators can pull a specific arch without
      going through the manifest.
---
 .github/workflows/publish-image.yaml | 200 ++++++++++++++++++++++++-----------
 1 file changed, 138 insertions(+), 62 deletions(-)

diff --git a/.github/workflows/publish-image.yaml 
b/.github/workflows/publish-image.yaml
index 0fe0a9a..4387d5c 100644
--- a/.github/workflows/publish-image.yaml
+++ b/.github/workflows/publish-image.yaml
@@ -14,19 +14,38 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-# Publish a Horizon UI container image to GHCR on:
+# Publish a multi-arch Horizon UI container image to GHCR on:
 #   - push to `main`  (tagged with `main` + the full commit SHA)
 #   - any `v*` tag    (tagged with the version + the full commit SHA)
 #
-# The full commit SHA is the canonical, immutable identifier — moving
-# tags like `main` and `vX.Y.Z` are conveniences that point at the same
-# SHA-built image. Operators should pull by SHA in production.
+# Architecture story
+#   The image is a pure copy-in of `./dist/` (no compile or network
+#   inside `docker build`). The `dist/node_modules/` carries `argon2`'s
+#   native binding — that binding is platform-specific, so a single
+#   amd64-built dist/ cannot be packaged into an arm64-correct image.
+#   We build each architecture on its own native GitHub runner
+#   (`ubuntu-latest` for amd64, `ubuntu-24.04-arm` for arm64), push a
+#   per-arch SHA-suffix tag, and a final `manifest` job stitches them
+#   into a single SHA-canonical OCI manifest list. Pulling the canonical
+#   tag from any host (Apple Silicon, AWS Graviton, plain x86_64)
+#   selects the right arch automatically — no Rosetta / QEMU emulation
+#   needed at run time.
 #
-# Only login / setup-qemu / setup-buildx are pulled from `docker/*` —
-# everything else (tag computation, buildx build+push) is shell-driven
-# to stay within ASF infra's third-party-action allow-list. SHAs are
-# the same ones `apache/skywalking`'s publish-docker.yaml uses, so
-# they're already vetted at the org level.
+# Tagging
+#   - `:<40-char-sha>`           always (canonical, immutable)
+#   - `:main`                    on push to main
+#   - `:<ver>` + `:latest`       on v* tags (ver = the tag with leading `v` 
stripped)
+#   - `:<major.minor>`           on v* tags (e.g. `1.2` from `v1.2.3`)
+#
+# Per-arch interim tags
+#   - `:<sha>-amd64` / `:<sha>-arm64` are also pushed so a debugger can
+#     pull a specific arch directly. The manifest job is the only thing
+#     that exposes the canonical tag without an arch suffix.
+#
+# Only login / setup-buildx / setup-node are pulled from third-party
+# actions — everything else (tag computation, buildx build+push,
+# manifest stitching) is shell-driven to stay within ASF infra's
+# third-party-action allow-list. SHAs are vetted at the org level.
 name: publish-image
 
 on:
@@ -45,22 +64,77 @@ concurrency:
   cancel-in-progress: false
 
 jobs:
-  build-and-push:
+  # ── Compute tags once so the per-arch matrix + the manifest job all
+  # ── work from the same set without duplicating the shell math.
+  tags:
     if: github.repository == 'apache/skywalking-horizon-ui'
-    name: Build + push
+    name: Compute image tags
     runs-on: ubuntu-latest
+    outputs:
+      base: ${{ steps.tags.outputs.base }}
+      moving: ${{ steps.tags.outputs.moving }}
+    steps:
+      - id: tags
+        env:
+          REGISTRY: ghcr.io
+          IMAGE_NAME: ${{ github.repository }}
+        run: |
+          set -eu
+          base="${REGISTRY}/${IMAGE_NAME}"
+          # lower-case the image path — GHCR rejects mixed-case names.
+          base="$(echo "$base" | tr '[:upper:]' '[:lower:]')"
+          sha="${{ github.sha }}"
+          # `moving` = the SET of canonical/moving tags the manifest
+          # job needs to stitch (one per `imagetools create -t ...`).
+          # Space-separated; consumers split on whitespace.
+          moving="${base}:${sha}"
+          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
+            moving="${moving} ${base}:main"
+          fi
+          if [ "${{ github.ref_type }}" = "tag" ]; then
+            ver="${GITHUB_REF_NAME#v}"
+            moving="${moving} ${base}:${ver} ${base}:latest"
+            mm="${ver%.*}"
+            if [ "${mm}" != "${ver}" ]; then
+              moving="${moving} ${base}:${mm}"
+            fi
+          fi
+          echo "base=${base}" >> "$GITHUB_OUTPUT"
+          echo "moving=${moving}" >> "$GITHUB_OUTPUT"
+          echo "::notice::Canonical SHA: ${sha}"
+          echo "::notice::Moving tags: ${moving}"
+
+  # ── Per-arch native build. Each runner produces its own `./dist/`
+  # ── (with the correct argon2 native binding) and pushes
+  # ── `<base>:<sha>-<arch>`. The manifest job below stitches the
+  # ── `:<sha>-amd64` + `:<sha>-arm64` pair into the canonical tag set.
+  build:
+    if: github.repository == 'apache/skywalking-horizon-ui'
+    needs: tags
+    name: Build ${{ matrix.arch }}
+    runs-on: ${{ matrix.runner }}
     timeout-minutes: 30
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - runner: ubuntu-latest
+            platform: linux/amd64
+            arch: amd64
+          # Free arm64 runners for public repos. If the org doesn't have
+          # them enabled, this matrix entry fails and the manifest job
+          # below short-circuits — fix is to either enable them on the
+          # org or drop arm64 here and accept Rosetta-emulation on Macs.
+          - runner: ubuntu-24.04-arm
+            platform: linux/arm64
+            arch: arm64
     env:
-      REGISTRY: ghcr.io
-      IMAGE_NAME: ${{ github.repository }}
+      BASE: ${{ needs.tags.outputs.base }}
     steps:
       - uses: actions/checkout@v4
         with:
           persist-credentials: false
 
-      # The Dockerfile is a pure copy-in of `./dist/` (no compile or
-      # network inside `docker build`). Build the artifact on the
-      # runner first; the image just lays it out under /app.
       - name: Set up Node
         uses: actions/setup-node@v4
         with:
@@ -72,71 +146,73 @@ jobs:
       - name: Install workspace deps
         run: pnpm install --frozen-lockfile
 
-      - name: Build self-contained ./dist/
+      - name: Build self-contained ./dist/ (native ${{ matrix.arch }})
+        # `pnpm package` runs `pnpm deploy` whose `node_modules` contains
+        # the argon2 binding for the current runner's architecture.
+        # Running this on `ubuntu-24.04-arm` produces an arm64-correct
+        # tree; on `ubuntu-latest` it's amd64.
         run: pnpm package
 
-      - name: Set up QEMU
-        uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
-
       - name: Set up Docker Buildx
         uses: 
docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
 
       - name: Log in to GHCR
         uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
         with:
-          registry: ${{ env.REGISTRY }}
+          registry: ghcr.io
           username: ${{ github.actor }}
           password: ${{ secrets.GITHUB_TOKEN }}
 
-      # Compute image tags in shell so we don't need docker/metadata-action.
-      # Canonical = full 40-char SHA; moving tags layered per trigger.
-      - name: Compute image tags
-        id: tags
+      - name: Build + push (${{ matrix.arch }})
         run: |
           set -eu
-          base="${REGISTRY}/${IMAGE_NAME}"
-          # lower-case the image path — GHCR rejects mixed-case names.
-          base="$(echo "$base" | tr '[:upper:]' '[:lower:]')"
-          sha="${{ github.sha }}"
-          tags="-t ${base}:${sha}"
-          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
-            tags="${tags} -t ${base}:main"
-          fi
-          if [ "${{ github.ref_type }}" = "tag" ]; then
-            # Strip the leading `v` from `vX.Y.Z` for the semver tag.
-            ver="${GITHUB_REF_NAME#v}"
-            tags="${tags} -t ${base}:${ver} -t ${base}:latest"
-            # Bonus: major.minor (e.g. 1.2 from 1.2.3) for the common
-            # "track the minor line" usage. POSIX shell substitution.
-            mm="${ver%.*}"
-            if [ "${mm}" != "${ver}" ]; then
-              tags="${tags} -t ${base}:${mm}"
-            fi
-          fi
-          echo "IMAGE_BASE=${base}" >> "$GITHUB_ENV"
-          echo "TAG_ARGS=${tags}" >> "$GITHUB_ENV"
-          echo "tags=${tags}" >> "$GITHUB_OUTPUT"
-
-      - name: Build + push
-        run: |
-          set -eu
-          # Single-platform (linux/amd64) for now. The `./dist/` is built
-          # on the GitHub-hosted runner (linux/amd64), and `argon2` ships
-          # native bindings — cross-arch packaging would need a per-arch
-          # matrix of build jobs that each produce their own dist/ +
-          # platform-tagged image, then a buildx imagetools merge into a
-          # manifest list. Easy follow-up when arm64 demand is real.
           docker buildx build \
-            --platform linux/amd64 \
+            --platform ${{ matrix.platform }} \
             --file Dockerfile \
             --label "org.opencontainers.image.source=https://github.com/${{ 
github.repository }}" \
             --label "org.opencontainers.image.revision=${{ github.sha }}" \
             --label "org.opencontainers.image.title=Apache SkyWalking Horizon 
UI" \
             --label "org.opencontainers.image.description=Next-generation web 
UI for Apache SkyWalking." \
             --label "org.opencontainers.image.licenses=Apache-2.0" \
-            --cache-from type=gha \
-            --cache-to type=gha,mode=max \
-            ${TAG_ARGS} \
+            --cache-from type=gha,scope=${{ matrix.arch }} \
+            --cache-to type=gha,mode=max,scope=${{ matrix.arch }} \
+            -t "${BASE}:${{ github.sha }}-${{ matrix.arch }}" \
             --push \
             .
-          echo "::notice::Published ${IMAGE_BASE}@sha=${{ github.sha }}"
+          echo "::notice::Pushed ${BASE}:${{ github.sha }}-${{ matrix.arch }}"
+
+  # ── Stitch the per-arch tags into a single OCI manifest list per
+  # ── canonical/moving tag. Pulling any of these tags from any host
+  # ── selects the matching arch automatically.
+  manifest:
+    if: github.repository == 'apache/skywalking-horizon-ui'
+    needs: [tags, build]
+    name: Stitch multi-arch manifest
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
+    env:
+      BASE: ${{ needs.tags.outputs.base }}
+      MOVING: ${{ needs.tags.outputs.moving }}
+    steps:
+      - name: Set up Docker Buildx
+        uses: 
docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
+
+      - name: Log in to GHCR
+        uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Create manifest list per moving tag
+        run: |
+          set -eu
+          amd="${BASE}:${{ github.sha }}-amd64"
+          arm="${BASE}:${{ github.sha }}-arm64"
+          for tag in ${MOVING}; do
+            echo "::group::Stitch ${tag}"
+            docker buildx imagetools create -t "${tag}" "${amd}" "${arm}"
+            docker buildx imagetools inspect "${tag}" | head -40
+            echo "::endgroup::"
+          done
+          echo "::notice::Published multi-arch manifest @sha=${{ github.sha }} 
(amd64+arm64)"

Reply via email to