This is an automated email from the ASF dual-hosted git repository.
wankai123 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
The following commit(s) were added to refs/heads/main by this push:
new b8650c9 ci: mirror multi-arch image to Docker Hub on tag-push +
manual dispatch (#12)
b8650c9 is described below
commit b8650c9c3902c59da094c1bc1f474a430eeff368
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Sun May 24 09:39:25 2026 +0800
ci: mirror multi-arch image to Docker Hub on tag-push + manual dispatch
(#12)
---
.github/workflows/publish-image.yaml | 146 ++++++++++++++++++++++++++++++-----
.gitignore | 3 +-
scripts/release-finalize.sh | 104 +++++++++++++------------
3 files changed, 185 insertions(+), 68 deletions(-)
diff --git a/.github/workflows/publish-image.yaml
b/.github/workflows/publish-image.yaml
index baaeae2..7d69466 100644
--- a/.github/workflows/publish-image.yaml
+++ b/.github/workflows/publish-image.yaml
@@ -14,9 +14,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-# 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)
+# Publish a multi-arch Horizon UI container image:
+# - GHCR on every push to `main` and every `v*` tag.
+# - Docker Hub (`apache/skywalking-ui`) on `v*` tags only — release
+# mirror, not per-commit. The Docker Hub repo is shared with
+# booster-ui, so Horizon's tag prefix is `horizon-<v>` and the
+# `latest` tag on this repo points at the newest Horizon release.
+#
+# Manual trigger
+# The `workflow_dispatch` accepts an optional `tag` input (e.g.
+# `v0.5.0`; defaults to the most recent v* tag). Use it to back-fill
+# a release whose original tag-push pre-dated this workflow's Docker
+# Hub mirror, or to retry a release whose initial publish failed.
+# A manual run with a tag is treated identically to a tag-push of
+# that tag — it (re)publishes GHCR + mirrors to Docker Hub.
#
# Architecture story
# The Dockerfile builds `dist/` from source inside the image. The
@@ -53,18 +64,28 @@ on:
- main
tags:
- 'v*'
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'v-prefixed tag to (re-)publish, e.g. v0.5.0. Leave empty
for the most recent v* tag. Treated identically to a tag-push (GHCR + Docker
Hub mirror).'
+ required: false
+ default: ''
permissions:
contents: read
packages: write
concurrency:
- group: publish-image-${{ github.ref }}
+ # Distinct group per resolved tag (manual + push converge on the same
+ # ref). Don't cancel in flight — release publishes shouldn't race.
+ group: publish-image-${{ github.event.inputs.tag || github.ref }}
cancel-in-progress: false
jobs:
- # ── Compute tags once so the per-arch matrix + the manifest job all
- # ── work from the same set without duplicating the shell math.
+ # ── Resolve sha / tag_name / is_tag ONCE, here, so every downstream
+ # ── job reads from a single source of truth — works the same way
+ # ── whether the trigger is a push (tag or branch) or a manual
+ # ── workflow_dispatch.
tags:
if: github.repository == 'apache/skywalking-horizon-ui'
name: Compute image tags
@@ -72,26 +93,71 @@ jobs:
outputs:
base: ${{ steps.tags.outputs.base }}
moving: ${{ steps.tags.outputs.moving }}
+ sha: ${{ steps.resolve.outputs.sha }}
+ ref: ${{ steps.resolve.outputs.ref }}
+ tag_name: ${{ steps.resolve.outputs.tag_name }}
+ is_tag: ${{ steps.resolve.outputs.is_tag }}
steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - id: resolve
+ env:
+ INPUT_TAG: ${{ github.event.inputs.tag }}
+ run: |
+ set -eu
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ tag="${INPUT_TAG}"
+ if [ -z "$tag" ]; then
+ tag=$(git tag --list 'v*' --sort=-version:refname | head -1)
+ if [ -z "$tag" ]; then
+ echo "::error::no v* tag exists in the repo"; exit 1
+ fi
+ echo "::notice::No tag provided — defaulting to latest: ${tag}"
+ fi
+ if ! git rev-parse "${tag}^{commit}" >/dev/null 2>&1; then
+ echo "::error::tag ${tag} does not exist (must be already pushed
to the repo)"; exit 1
+ fi
+ sha=$(git rev-parse "${tag}^{commit}")
+ echo "ref=refs/tags/${tag}" >> "$GITHUB_OUTPUT"
+ echo "sha=${sha}" >> "$GITHUB_OUTPUT"
+ echo "tag_name=${tag}" >> "$GITHUB_OUTPUT"
+ echo "is_tag=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "ref=${{ github.ref }}" >> "$GITHUB_OUTPUT"
+ echo "sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
+ if [ "${{ github.ref_type }}" = "tag" ]; then
+ echo "tag_name=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
+ echo "is_tag=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "tag_name=" >> "$GITHUB_OUTPUT"
+ echo "is_tag=false" >> "$GITHUB_OUTPUT"
+ fi
+ fi
+
- id: tags
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
+ SHA: ${{ steps.resolve.outputs.sha }}
+ IS_TAG: ${{ steps.resolve.outputs.is_tag }}
+ TAG_NAME: ${{ steps.resolve.outputs.tag_name }}
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="${base}:${SHA}"
+ if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}"
= "refs/heads/main" ]; then
moving="${moving} ${base}:main"
fi
- if [ "${{ github.ref_type }}" = "tag" ]; then
- ver="${GITHUB_REF_NAME#v}"
+ if [ "${IS_TAG}" = "true" ]; then
+ ver="${TAG_NAME#v}"
moving="${moving} ${base}:${ver} ${base}:latest"
mm="${ver%.*}"
if [ "${mm}" != "${ver}" ]; then
@@ -100,7 +166,7 @@ jobs:
fi
echo "base=${base}" >> "$GITHUB_OUTPUT"
echo "moving=${moving}" >> "$GITHUB_OUTPUT"
- echo "::notice::Canonical SHA: ${sha}"
+ echo "::notice::Canonical SHA: ${SHA}"
echo "::notice::Moving tags: ${moving}"
# ── Per-arch native build. The Dockerfile builds `dist/` from source
@@ -130,9 +196,14 @@ jobs:
arch: arm64
env:
BASE: ${{ needs.tags.outputs.base }}
+ SHA: ${{ needs.tags.outputs.sha }}
steps:
- uses: actions/checkout@v4
with:
+ # Build from the resolved ref — a manual dispatch with a tag
+ # checks out THAT tag, not main. For pushes, this is the
+ # event's own ref (no behaviour change).
+ ref: ${{ needs.tags.outputs.ref }}
persist-credentials: false
- name: Set up Docker Buildx
@@ -152,16 +223,16 @@ jobs:
--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.revision=${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,scope=${{ matrix.arch }} \
--cache-to type=gha,mode=max,scope=${{ matrix.arch }} \
- -t "${BASE}:${{ github.sha }}-${{ matrix.arch }}" \
+ -t "${BASE}:${SHA}-${{ matrix.arch }}" \
--push \
.
- echo "::notice::Pushed ${BASE}:${{ github.sha }}-${{ matrix.arch }}"
+ echo "::notice::Pushed ${BASE}:${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
@@ -175,6 +246,9 @@ jobs:
env:
BASE: ${{ needs.tags.outputs.base }}
MOVING: ${{ needs.tags.outputs.moving }}
+ SHA: ${{ needs.tags.outputs.sha }}
+ IS_TAG: ${{ needs.tags.outputs.is_tag }}
+ TAG_NAME: ${{ needs.tags.outputs.tag_name }}
steps:
- name: Set up Docker Buildx
uses:
docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
@@ -189,12 +263,48 @@ jobs:
- name: Create manifest list per moving tag
run: |
set -eu
- amd="${BASE}:${{ github.sha }}-amd64"
- arm="${BASE}:${{ github.sha }}-arm64"
+ amd="${BASE}:${SHA}-amd64"
+ arm="${BASE}:${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)"
+ echo "::notice::Published multi-arch manifest @sha=${SHA}
(amd64+arm64)"
+
+ # ── Mirror to Docker Hub on `v*` tags only (push-tag OR a manual
+ # ── workflow_dispatch with a tag input — both set is_tag=true).
+ #
+ # `apache/skywalking-ui` is shared with booster-ui; Horizon's
+ # release tags are `horizon-<v>` and the `latest` tag (Horizon
+ # owns `latest` on this repo as the newest Horizon release —
+ # booster-ui uses its own dated tags).
+ #
+ # `docker buildx imagetools create` does a manifest-level copy of
+ # the GHCR canonical sha tag (already multi-arch from the loop
+ # above) into the two Docker Hub tags. No rebuild, copies blobs
+ # across registries via the registry-to-registry mount API.
+ - name: Log in to Docker Hub (release only)
+ if: needs.tags.outputs.is_tag == 'true'
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
+ with:
+ username: ${{ secrets.DOCKERHUB_USER }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Mirror multi-arch manifest to Docker Hub (release only)
+ if: needs.tags.outputs.is_tag == 'true'
+ env:
+ DOCKERHUB_IMAGE: apache/skywalking-ui
+ run: |
+ set -eu
+ ver="${TAG_NAME#v}"
+ src="${BASE}:${SHA}"
+ echo "::group::Mirror ${src} → ${DOCKERHUB_IMAGE}:horizon-${ver} +
:latest"
+ docker buildx imagetools create \
+ -t "${DOCKERHUB_IMAGE}:horizon-${ver}" \
+ -t "${DOCKERHUB_IMAGE}:latest" \
+ "${src}"
+ docker buildx imagetools inspect "${DOCKERHUB_IMAGE}:horizon-${ver}"
| head -40
+ echo "::endgroup::"
+ echo "::notice::Mirrored to
${DOCKERHUB_IMAGE}:{horizon-${ver},latest}"
diff --git a/.gitignore b/.gitignore
index 694f426..d068795 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,4 +43,5 @@ horizon-wire.jsonl
horizon-setup.json
# Release
-scripts/.release-work
\ No newline at end of file
+scripts/.release-work
+scripts/.finalize-work
\ No newline at end of file
diff --git a/scripts/release-finalize.sh b/scripts/release-finalize.sh
index 671c738..c160c00 100755
--- a/scripts/release-finalize.sh
+++ b/scripts/release-finalize.sh
@@ -36,11 +36,13 @@
# (src + bin tarballs + .asc + .sha512) fetched back from SVN release,
# with the CHANGELOG section for <v> as the body.
#
-# 3. Build + push the multi-arch (amd64 + arm64) container image to
-# Docker Hub apache/skywalking-ui, tagged:
-# :horizon-<v> immutable, this release
-# :latest moving — newest Horizon release. On this shared
-# repo, `latest` serves Horizon (not booster-ui).
+# 3. Verify the Docker Hub multi-arch image — CI's publish-image
+# workflow mirrors the GHCR image to Docker Hub automatically on
+# every `v*` tag push (apache/skywalking-ui:horizon-<v> and
+# apache/skywalking-ui:latest). This step just confirms the two
+# expected tags are present. Falls back to a manual local mirror
+# (same `docker buildx imagetools create` CI runs) if CI didn't
+# publish — needs Docker Hub push rights on apache/skywalking-ui.
#
# Usage: bash scripts/release-finalize.sh
#
@@ -59,8 +61,6 @@
SVN_RELEASE_URL="https://dist.apache.org/repos/dist/release/skywalking/horizon-u
DOCKERHUB_REPO="apache/skywalking-ui"
WORK_DIR="${SCRIPT_DIR}/.finalize-work"
-BUILDER_NAME="horizon-release-builder"
-
# ========================== Helpers ==========================
err() { echo "ERROR: $*" >&2; }
@@ -89,7 +89,8 @@ if [ ${#MISSING[@]} -gt 0 ]; then
fi
if ! docker buildx version >/dev/null 2>&1; then
- err "docker buildx is required for the multi-arch image build."
+ err "docker buildx is required (Step 5 uses 'imagetools create' to copy"
+ err "the CI-built multi-arch manifest from GHCR to Docker Hub)."
exit 1
fi
@@ -248,7 +249,7 @@ else
if confirm "Create the GitHub release ${TAG} and attach the 6 artifacts?";
then
gh release create "${TAG}" \
--repo apache/skywalking-horizon-ui \
- --title "Apache SkyWalking Horizon UI ${RELEASE_VERSION}" \
+ --title "${RELEASE_VERSION}" \
--notes-file "${NOTES_FILE}" \
"${ART_DIR}/${SRC_BASE}" \
"${ART_DIR}/${SRC_BASE}.asc" \
@@ -265,47 +266,52 @@ fi
# ========================== Step 5: Docker Hub multi-arch image
==========================
note "Step 5 — Docker Hub image: ${DOCKERHUB_REPO}"
-# Build from a CLEAN checkout of the tag so the image matches the released
-# source exactly (no local uncommitted edits leak in).
-BUILD_SRC="${WORK_DIR}/src"
-echo "Checking out ${TAG} into ${BUILD_SRC}…"
-git -C "${PROJECT_DIR}" archive --format=tar --prefix=src/ "${TAG}" | (cd
"${WORK_DIR}" && tar -x)
-
-# A docker-container builder is required: the default 'docker' driver cannot
-# emit a multi-platform manifest. Create one if absent + ensure QEMU is set
-# up for the foreign-arch emulation.
-if ! docker buildx inspect "${BUILDER_NAME}" >/dev/null 2>&1; then
- echo "Creating buildx builder '${BUILDER_NAME}' (docker-container driver)…"
- docker buildx create --name "${BUILDER_NAME}" --driver docker-container
--bootstrap
-fi
-docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
>/dev/null 2>&1 || true
-
-# Horizon publishes the immutable per-release tag plus the moving `latest`
-# on the shared repo. `latest` here points at the newest Horizon release —
-# pulling `${DOCKERHUB_REPO}:latest` gets Horizon, not booster-ui.
-IMG_TAGS=(-t "${DOCKERHUB_REPO}:horizon-${RELEASE_VERSION}" -t
"${DOCKERHUB_REPO}:latest")
-echo "Image tags to push:"
-echo " ${DOCKERHUB_REPO}:horizon-${RELEASE_VERSION} (immutable, this
release)"
-echo " ${DOCKERHUB_REPO}:latest (moving — newest Horizon
release)"
-
-if confirm "Build linux/amd64+arm64 and push to Docker Hub now? (emulated arch
is slow)"; then
- docker buildx build \
- --builder "${BUILDER_NAME}" \
- --platform linux/amd64,linux/arm64 \
- --file "${PROJECT_DIR}/Dockerfile" \
- --label
"org.opencontainers.image.source=https://github.com/apache/skywalking-horizon-ui"
\
- --label "org.opencontainers.image.revision=$(git -C "${PROJECT_DIR}"
rev-parse "${TAG}")" \
- --label "org.opencontainers.image.version=${RELEASE_VERSION}" \
- --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" \
- "${IMG_TAGS[@]}" \
- --push \
- "${BUILD_SRC}/src"
- echo "Pushed multi-arch image to ${DOCKERHUB_REPO}."
- echo "Verify: docker buildx imagetools inspect
${DOCKERHUB_REPO}:horizon-${RELEASE_VERSION}"
+# CI (.github/workflows/publish-image.yaml) mirrors the multi-arch image
+# to Docker Hub automatically on every `v*` tag push, so by the time
+# you're finalizing a passed vote this should already be live. We just
+# verify the two expected tags are present.
+#
+# Fallback: if CI didn't publish (workflow failed / secrets missing /
+# tag pushed before this workflow shipped), we fall back to the manual
+# `docker buildx imagetools create` mirror from the GHCR canonical tag
+# — same operation CI does, run locally. That needs Docker Hub push
+# rights on `apache/skywalking-ui`.
+DH_VERSION_TAG="${DOCKERHUB_REPO}:horizon-${RELEASE_VERSION}"
+DH_LATEST_TAG="${DOCKERHUB_REPO}:latest"
+GHCR_SRC="ghcr.io/apache/skywalking-horizon-ui:${RELEASE_VERSION}"
+
+echo "Expected on Docker Hub:"
+echo " ${DH_VERSION_TAG} (immutable, this release)"
+echo " ${DH_LATEST_TAG} (moving — newest Horizon
release)"
+
+if docker buildx imagetools inspect "${DH_VERSION_TAG}" >/dev/null 2>&1; then
+ echo "✓ ${DH_VERSION_TAG} already on Docker Hub — CI's publish-image
mirror succeeded."
+ echo " Verify: docker buildx imagetools inspect ${DH_VERSION_TAG}"
else
- echo "Skipped Docker Hub push."
+ echo "✗ ${DH_VERSION_TAG} NOT on Docker Hub yet."
+ echo " This is the expected outcome only if the publish-image workflow"
+ echo " failed or didn't run on tag ${TAG}. Check:"
+ echo "
https://github.com/apache/skywalking-horizon-ui/actions/workflows/publish-image.yaml"
+ if ! docker buildx imagetools inspect "${GHCR_SRC}" >/dev/null 2>&1; then
+ err "Source ${GHCR_SRC} not on GHCR either — CI didn't produce a
multi-arch"
+ err "image to mirror. Re-run publish-image on ${TAG} from the Actions
UI"
+ err "and then re-run this script."
+ exit 1
+ fi
+ if confirm "Fall back to a manual local mirror from ${GHCR_SRC}?"; then
+ docker buildx imagetools create \
+ -t "${DH_VERSION_TAG}" \
+ -t "${DH_LATEST_TAG}" \
+ "${GHCR_SRC}"
+ echo "Pushed multi-arch manifest to ${DOCKERHUB_REPO}."
+ else
+ echo "Skipped Docker Hub push — fix CI and re-run, OR run the
imagetools"
+ echo "create manually:"
+ echo " docker buildx imagetools create \\"
+ echo " -t ${DH_VERSION_TAG} \\"
+ echo " -t ${DH_LATEST_TAG} \\"
+ echo " ${GHCR_SRC}"
+ fi
fi
# ========================== Done ==========================