Repository navigation
Prepare release 1.1.0 #347
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # | |
| # 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@v7 | |
| 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@v7 | |
| 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}" |