Skip to content

feat(traces): one trace link form, ?traceId=&traceType=<trace source>… #339

feat(traces): one trace link form, ?traceId=&traceType=<trace source>…

feat(traces): one trace link form, ?traceId=&traceType=<trace source>… #339

#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Publish a multi-arch Horizon UI container image:
# - GHCR on every push to `main` and every `v*` tag (a tag push publishes
# the immutable `:<sha>` only — see Tagging).
# - Docker Hub (`apache/skywalking-ui`) only when a tag is PROMOTED after
# the release vote — 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 (and is
# only ever moved forward — see Tagging below).
#
# Manual trigger
# Promotion is normally automatic: publishing the GitHub release attaches
# the stable tags and mirrors to Docker Hub. `release: types: [released]`
# fires for a real publish only — not a draft, not a prerelease — and the
# release is created after the vote passes, so it is a faithful signal that
# a candidate became a release.
#
# The `workflow_dispatch` accepts an optional `tag` input (e.g. `v0.5.0`;
# defaults to the most recent v* tag) and does the same thing by hand. Use
# it to back-fill a release whose tag-push pre-dated this workflow, or to
# retry one whose publish failed. `latest` is attached only if that tag is
# the highest v* in the repo, whichever path promoted it.
#
# Architecture story
# The Dockerfile builds `dist/` from source inside the image. The
# resulting `node_modules/` carries `argon2`'s native binding — that
# binding is platform-specific, so the build must run per-architecture.
# We build each architecture on its own native GitHub runner
# (`ubuntu-latest` for amd64, `ubuntu-24.04-arm` for arm64) so the
# compile is native (no QEMU), 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.
#
# Tagging
# - `:<40-char-sha>` always (canonical, immutable)
# - `:main` on push to main
# - `:<ver>` / `:<major.minor>` / `:latest` (ver = tag minus leading `v`)
# NOT on the tag push — a tag is only a release
# CANDIDATE until the Apache vote passes. They
# are attached by re-running this workflow via
# workflow_dispatch for that tag AFTER the vote
# (the promotion step). `:latest` additionally
# requires the promoted tag to be the highest
# v* in the repo, so promoting a patch on a
# superseded line never moves it backwards.
#
# 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 / checkout 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:
push:
branches:
- main
tags:
- 'v*'
# Publishing a GitHub release IS the promotion. `released` fires only for a
# real publish — never a draft, never a prerelease — and the release is
# created after the vote passes, so it is the honest signal that a candidate
# became a release. The tag it names already exists by then.
release:
types:
- released
workflow_dispatch:
inputs:
tag:
description: 'v-prefixed tag to PROMOTE, e.g. v0.5.0. Leave empty for the most recent v* tag. This run attaches the stable GHCR tags and mirrors to Docker Hub; the tag push itself published :<sha> only.'
required: false
default: ''
permissions:
contents: read
packages: write
concurrency:
# 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:
# ── 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
runs-on: ubuntu-latest
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 }}
is_highest: ${{ steps.tags.outputs.is_highest }}
is_promote: ${{ steps.tags.outputs.is_promote }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- id: resolve
env:
INPUT_TAG: ${{ github.event.inputs.tag }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -eu
# A release names its tag; a dispatch is given one or defaults to the
# newest. Both then resolve the COMMIT from the tag rather than
# trusting the event's sha, so the image is built from the ref that
# was voted on.
if [ "${{ github.event_name }}" = "release" ]; then
tag="${RELEASE_TAG}"
if [ -z "$tag" ]; then
echo "::error::release event carried no tag_name"; exit 1
fi
elif [ "${{ 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
fi
if [ "${{ github.event_name }}" = "release" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
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:]')"
# `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.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
moving="${moving} ${base}:main"
fi
# STABLE TAGS ARE VOTE-GATED. Pushing `v1.2.3` only cuts the release
# CANDIDATE — the Apache vote happens afterwards and can fail, so a
# tag push publishes the immutable `:<sha>` and nothing else. The
# human-facing `:<ver>`, `:<major.minor>` and `:latest` tags are
# attached by the PROMOTION run, which rebuilds from the same tag.
#
# Promotion happens when the GitHub RELEASE is published, since that
# is done after the vote passes and cannot be done for a candidate.
# A workflow_dispatch does the same thing by hand, for back-filling a
# release whose tag pre-dates this workflow and for retrying a
# publish that failed.
#
# `latest` additionally must always name the NEWEST release, so it
# is attached only when the promoted tag is the highest v* in the
# repo — otherwise promoting a patch on a superseded line (v0.7.1
# after v1.0.0) would drag `latest` backwards. `--sort=version:refname`
# is git's numeric version sort, so v1.0.10 ranks above v1.0.2.
is_highest=false
is_promote=false
if [ "${IS_TAG}" = "true" ]; then
case "${{ github.event_name }}" in
release|workflow_dispatch) is_promote=true ;;
esac
fi
if [ "${IS_TAG}" = "true" ] && [ "${is_promote}" != "true" ]; then
echo "::notice::${TAG_NAME} tagged — publishing the immutable :<sha> only. Stable tags (:${TAG_NAME#v}, :latest) wait for the release vote; re-run this workflow via workflow_dispatch for ${TAG_NAME} to promote."
fi
if [ "${is_promote}" = "true" ]; then
ver="${TAG_NAME#v}"
moving="${moving} ${base}:${ver}"
highest="$(git tag --list 'v*' --sort=-version:refname | head -1)"
if [ "${TAG_NAME}" = "${highest}" ]; then
is_highest=true
moving="${moving} ${base}:latest"
else
echo "::notice::${TAG_NAME} is not the highest tag (${highest}) — leaving :latest where it is"
fi
mm="${ver%.*}"
if [ "${mm}" != "${ver}" ]; then
moving="${moving} ${base}:${mm}"
fi
fi
echo "is_highest=${is_highest}" >> "$GITHUB_OUTPUT"
echo "is_promote=${is_promote}" >> "$GITHUB_OUTPUT"
echo "base=${base}" >> "$GITHUB_OUTPUT"
echo "moving=${moving}" >> "$GITHUB_OUTPUT"
echo "::notice::Canonical SHA: ${SHA}"
echo "::notice::Moving tags: ${moving}"
# ── Per-arch native build. The Dockerfile builds `dist/` from source
# ── inside the image, so each native runner compiles the correct
# ── argon2 binding for its arch 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:
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
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build + push (${{ matrix.arch }})
run: |
set -eu
docker buildx build \
--platform ${{ matrix.platform }} \
--file Dockerfile \
--label "org.opencontainers.image.source=https://github.com/${{ github.repository }}" \
--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}:${SHA}-${{ matrix.arch }}" \
--push \
.
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
# ── 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 }}
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@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create manifest list per moving tag
run: |
set -eu
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=${SHA} (amd64+arm64)"
# ── Mirror to Docker Hub on a PROMOTION run only — a workflow_dispatch
# ── for a `v*` tag. A tag PUSH sets is_tag=true but not is_promote, so
# ── it publishes `:<sha>` to GHCR and mirrors nothing.
#
# `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 (promoted release only)
if: needs.tags.outputs.is_promote == 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Mirror multi-arch manifest to Docker Hub (promoted release only)
if: needs.tags.outputs.is_promote == 'true'
env:
DOCKERHUB_IMAGE: apache/skywalking-ui
IS_HIGHEST: ${{ needs.tags.outputs.is_highest }}
run: |
set -eu
ver="${TAG_NAME#v}"
src="${BASE}:${SHA}"
# Mirror `latest` only for the highest release — same rule as GHCR,
# so re-publishing an older line never demotes the Docker Hub tag.
latest_arg=""
if [ "${IS_HIGHEST}" = "true" ]; then
latest_arg="-t ${DOCKERHUB_IMAGE}:latest"
else
echo "::notice::not the highest release — mirroring horizon-${ver} only, :latest untouched"
fi
echo "::group::Mirror ${src} → ${DOCKERHUB_IMAGE}:horizon-${ver}${latest_arg:+ + :latest}"
# shellcheck disable=SC2086 # latest_arg is an intentional word-split flag pair
docker buildx imagetools create \
-t "${DOCKERHUB_IMAGE}:horizon-${ver}" \
${latest_arg} \
"${src}"
docker buildx imagetools inspect "${DOCKERHUB_IMAGE}:horizon-${ver}" | head -40
echo "::endgroup::"
echo "::notice::Mirrored to ${DOCKERHUB_IMAGE}:horizon-${ver}${latest_arg:+ + :latest}"