Repository navigation
392 lines (377 loc) · 17.9 KB
/
Copy pathpublish-image.yaml
File metadata and controls
392 lines (377 loc) · 17.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#
# 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}"