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 7ff0ffab7777695cf589423c1683eff648ffc937 Author: Wu Sheng <[email protected]> AuthorDate: Thu May 21 22:15:21 2026 +0800 release: post-vote finalize script + single from-source Dockerfile Add scripts/release-finalize.sh for the post-[VOTE] half of the release: SVN move dev->release (auto-creating the release/ path, pruning the prior release), GitHub release attaching the voted artifacts, and a multi-arch Docker Hub push to apache/skywalking-ui under horizon-<ver>/horizon-latest (+ optional latest). Harden release.sh to create the dev/ SVN folder too. Collapse the two Dockerfiles into one from-source multi-stage build (the copy-in image cannot go multi-arch from a single host: dist/node_modules carries an arch-specific argon2 binding). CI builds each arch natively, so the runner-side pnpm package step is dropped. Flip .dockerignore from a copy-in allowlist to a from-source denylist. Final shipped image is unchanged (~201MB; the build stage is discarded). Bump Node base + CI to 24, engines floor to >=22. Add a Docker Hub README documenting the shared-repo tag split (booster = 9.x/10.x/latest, Horizon = horizon-*). --- .dockerignore | 27 ++- .github/workflows/ci.yaml | 10 +- .github/workflows/publish-image.yaml | 48 ++--- Dockerfile | 73 ++++---- README.md | 3 +- dist-material/docker-hub/README.md | 76 ++++++++ docs/setup/container-image.md | 33 ++-- package.json | 2 +- scripts/package.mjs | 4 +- scripts/release-finalize.sh | 328 +++++++++++++++++++++++++++++++++++ scripts/release.sh | 26 ++- 11 files changed, 521 insertions(+), 109 deletions(-) diff --git a/.dockerignore b/.dockerignore index b37888f..d3ea2be 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,13 +13,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Allowlist style: the image is a pure copy-in of `./dist/`, so the -# Docker context never needs anything else. Excluding everything by -# default keeps the context tiny, the build deterministic, and -# eliminates any temptation to reach back into source from a future -# Dockerfile change. +# Denylist style: the image builds FROM SOURCE (see Dockerfile), so the +# context is the source tree. Exclude only what must NOT enter the build — +# host-built artifacts (arch-specific node_modules + dist), VCS, CI config, +# local secrets, and scratch dirs. Everything else (source, lockfile, +# horizon.example.yaml) is needed by `pnpm install` + `pnpm package`. -* -!dist -!dist/** -!Dockerfile +**/node_modules +**/dist +.git +.github +.pnpm-store +**/.DS_Store +scripts/.release-work +scripts/.finalize-work +.claude + +# Local runtime config may carry secrets — never ship it. The committed +# horizon.example.yaml stays (package.mjs reads it into dist/). +horizon.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d7f22a4..d9781fe 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,7 +58,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' cache: 'pnpm' - run: pnpm install --frozen-lockfile - name: Verify binary LICENSE/NOTICE (drift + ASF allow/deny) @@ -102,7 +102,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' cache: 'pnpm' - run: pnpm install --frozen-lockfile - run: pnpm -r run type-check @@ -118,7 +118,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' cache: 'pnpm' - run: pnpm install --frozen-lockfile - run: pnpm --filter @skywalking-horizon-ui/ui build @@ -134,7 +134,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' cache: 'pnpm' - run: pnpm install --frozen-lockfile - run: pnpm --filter @skywalking-horizon-ui/bff build @@ -150,7 +150,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' cache: 'pnpm' - run: pnpm install --frozen-lockfile # `pnpm -r` walks every workspace package and runs `test:unit` — diff --git a/.github/workflows/publish-image.yaml b/.github/workflows/publish-image.yaml index 4387d5c..baaeae2 100644 --- a/.github/workflows/publish-image.yaml +++ b/.github/workflows/publish-image.yaml @@ -19,17 +19,16 @@ # - any `v*` tag (tagged with the version + the full commit SHA) # # 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. +# 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), 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. +# (`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) @@ -42,7 +41,7 @@ # 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 +# 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. @@ -104,10 +103,11 @@ jobs: 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. + # ── 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 @@ -135,24 +135,6 @@ jobs: with: persist-credentials: false - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Activate pnpm - run: corepack enable && corepack prepare [email protected] --activate - - - name: Install workspace deps - run: pnpm install --frozen-lockfile - - - 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 Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f diff --git a/Dockerfile b/Dockerfile index 336b28a..b54ffb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,48 +13,55 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copy-in image. The image does NOT compile anything and does NOT run -# `pnpm install` — it consumes the pre-built `./dist/` produced by -# `pnpm package` at the repo root and lays it out under `/app/`. Build -# the artifact first: +# From-source, multi-arch image. Single source of truth for every image +# build: CI (GHCR), local dev, and the manual Docker Hub release push. # -# pnpm install # one-time / on lockfile changes -# pnpm package # produces ./dist/ (server.js + node_modules -# # + bundled_templates + static + example yaml) -# docker build -t horizon-ui:local . +# Why from-source (not copy-in): `dist/node_modules` carries argon2's +# native binding, which is architecture-specific. Building `dist/` INSIDE +# the image — once per target platform — lets one `buildx --platform +# linux/amd64,linux/arm64` invocation compile the correct binding for each +# arch (native on a matching runner, QEMU-emulated otherwise). A copy-in +# image cannot do this from a single host. # -# Net effect: tiny image (no Node toolchain, no devDeps, no pnpm store, -# no source), reproducible (the dist/ tarball is the contract), and -# air-gap-friendly (image build needs zero network). +# docker build -t horizon-ui:local . # single-arch dev build +# docker buildx build --platform linux/amd64,linux/arm64 -t … --push . +# +# Trade-off: the build needs the network (`pnpm install`) — but only in the +# throwaway build stage. The final shipped stage is network-free. + +# ---- build stage: compile the self-contained dist for THIS platform ---- +# Throwaway BUILD ENVIRONMENT — uses the network (`pnpm install`) and +# carries the compiler toolchain. Discarded; only the final stage ships. +FROM node:24-alpine AS build +WORKDIR /src + +# Toolchain for argon2's native build (node-gyp needs python3 + make + g++). +RUN apk add --no-cache python3 make g++ + +# corepack ships with node:24; pin pnpm to the workspace version. +RUN corepack enable && corepack prepare [email protected] --activate -FROM node:20-alpine +# Copy the whole source tree. `.dockerignore` keeps host-built artifacts +# (node_modules, dist) out of the context so nothing arch-specific leaks in. +COPY . . + +RUN pnpm install --frozen-lockfile +RUN pnpm package + +# ---- runtime stage: the shipped image — no toolchain, no network ---- +FROM node:24-alpine WORKDIR /app -# Run as a non-root user — the BFF doesn't need any privileged access. RUN addgroup -S horizon && adduser -S -G horizon horizon -# Pre-built artifact. Layout matches what `node server.js` expects: -# server.js + bundled_templates + node_modules + static all siblings -# under /app/. The bundled-template loader probes `__dirname/bundled_ -# templates` first (see apps/bff/src/logic/layers/loader.ts). -# -# Read-only artifacts owned by root: -COPY dist/server.js ./server.js -COPY dist/package.json ./package.json -COPY dist/node_modules ./node_modules -COPY dist/static ./static -COPY dist/horizon.example.yaml ./horizon.example.yaml +COPY --from=build /src/dist/server.js ./server.js +COPY --from=build /src/dist/package.json ./package.json +COPY --from=build /src/dist/node_modules ./node_modules +COPY --from=build /src/dist/static ./static +COPY --from=build /src/dist/horizon.example.yaml ./horizon.example.yaml -# `bundled_templates/` is writable: the admin Layer-Templates and -# Overview-Templates editors `writeFileSync` into per-key / per-id JSON -# files. Owned by the `horizon` user so saves don't EACCES. -COPY --chown=horizon:horizon dist/bundled_templates ./bundled_templates +COPY --from=build --chown=horizon:horizon /src/dist/bundled_templates ./bundled_templates -# `/data` is the writable state directory the BFF writes its runtime -# files into (audit log, setup state, alarm state, wire debug log). -# Operators mount a PVC / named volume / host bind here for durable -# storage. Without a mount the writes go to the container's writable -# layer (ephemeral). RUN mkdir -p /data && chown horizon:horizon /data VOLUME ["/data"] diff --git a/README.md b/README.md index cffc23b..0bfa0a7 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,9 @@ pnpm -r test:unit pnpm license:check # CI gate via skywalking-eyes ``` -Container build (zero-compile-in-image — Dockerfile just copies in the pre-built `dist/`): +Container build (multi-stage — the Dockerfile builds `dist/` from source inside the image, no host pre-step): ```bash -pnpm package # produces ./dist/ docker build -t horizon-ui:local . docker run --rm -p 8081:8081 -v "$PWD/horizon.yaml:/app/horizon.yaml:ro" horizon-ui:local ``` diff --git a/dist-material/docker-hub/README.md b/dist-material/docker-hub/README.md new file mode 100644 index 0000000..579341e --- /dev/null +++ b/dist-material/docker-hub/README.md @@ -0,0 +1,76 @@ +# Apache SkyWalking Horizon UI + +> This repository (`apache/skywalking-ui`) is **shared** between two UIs. +> Booster UI (the current production UI) owns the `9.x` / `10.x` tags. +> **Horizon UI** (the next-generation UI) is published under **`horizon-` +> prefixed tags**. Pick your tags accordingly. + +**Horizon UI** is the next-generation web UI for [Apache SkyWalking](https://skywalking.apache.org/) — a modernized, dense, dark-first dashboard built on the same OAP GraphQL query-protocol and MQE. It bundles both the backend-for-frontend (BFF) and the built UI in a single image; there is no separate frontend container. + +## Tags + +| Tag | Points at | Use | +|---|---|---| +| `horizon-X.Y.Z` | A specific Horizon release (e.g. `horizon-0.5.0`). Immutable. | **Production.** Pin to an exact version. | +| `horizon-latest` | The newest Horizon release. Moves over time. | Track the latest Horizon line. | +| `latest` | Newest image published to this repo. | Demos / dev only — do not pin production to `latest`. | + +```sh +docker pull apache/skywalking-ui:horizon-0.5.0 +``` + +A SHA-pinned, GHCR-hosted variant is also published at `ghcr.io/apache/skywalking-horizon-ui` for fully reproducible deploys. + +## Architectures + +Multi-arch manifest: `linux/amd64` and `linux/arm64`. Pulling the tag from any host selects the right architecture automatically. + +## Quick start + +The image expects a `horizon.yaml` config at `/app/horizon.yaml`. It is **not** baked into the image — provide it via bind-mount or your own layer. A reference config ships at `/app/horizon.example.yaml`. + +```sh +# 1. Get the example config to start from +docker run --rm apache/skywalking-ui:horizon-0.5.0 \ + cat /app/horizon.example.yaml > horizon.yaml + +# 2. Edit horizon.yaml — point it at your OAP and set auth. + +# 3. Run, mounting your config + a durable data volume +docker run -d --name horizon-ui \ + -p 8081:8081 \ + -v "$(pwd)/horizon.yaml:/app/horizon.yaml:ro" \ + -v horizon-data:/data \ + apache/skywalking-ui:horizon-0.5.0 +``` + +Open <http://localhost:8081>. + +## Configuration + +| Path / Variable | Purpose | +|---|---| +| `/app/horizon.yaml` | Active config. Mount it here (`HORIZON_CONFIG` to relocate). | +| `/app/horizon.example.yaml` | Read-only reference config. Copy from it. | +| `/data/` | Declared `VOLUME`. Audit log, setup state, alarm state, wire debug log land here. Mount for durable storage. | +| `/app/bundled_templates/` | Layer + overview dashboard templates. Writable by the admin template editors. | + +Key environment variables (all have sensible defaults in the image): + +| Variable | Default | Purpose | +|---|---|---| +| `HORIZON_CONFIG` | `/app/horizon.yaml` | Where the BFF reads its config. | +| `HORIZON_SERVER_HOST` / `HORIZON_SERVER_PORT` | `0.0.0.0` / `8081` | Bind address. The image `EXPOSE`s `8081`. | +| `LOG_LEVEL` | `error` (production) | `trace`/`debug`/`info`/`warn`/`error`/`fatal`. | + +The container runs as the non-root user `horizon`. + +## Documentation + +* Project: <https://github.com/apache/skywalking-horizon-ui> +* SkyWalking: <https://skywalking.apache.org/> +* Issues: <https://github.com/apache/skywalking/issues> + +## License + +[Apache License 2.0](https://github.com/apache/skywalking-horizon-ui/blob/main/LICENSE). diff --git a/docs/setup/container-image.md b/docs/setup/container-image.md index 9035650..a98e293 100644 --- a/docs/setup/container-image.md +++ b/docs/setup/container-image.md @@ -15,7 +15,7 @@ Registry: **GitHub Container Registry (GHCR)** at `ghcr.io/apache/skywalking-hor | `main` | Head of `main`. Moves on every merge. | Smoke-test the development branch. | ```sh -docker pull ghcr.io/apache/skywalking-horizon-ui:0.4.0 +docker pull ghcr.io/apache/skywalking-horizon-ui:0.5.0 docker pull ghcr.io/apache/skywalking-horizon-ui:<sha> ``` @@ -65,7 +65,7 @@ docker run -d \ --name horizon \ -p 8081:8081 \ -v "$PWD/horizon.yaml:/app/horizon.yaml:ro" \ - ghcr.io/apache/skywalking-horizon-ui:0.4.0 + ghcr.io/apache/skywalking-horizon-ui:0.5.0 ``` Notes: @@ -78,7 +78,7 @@ Notes: For immutable single-tenant deployments, build a child image that includes your config: ```dockerfile -FROM ghcr.io/apache/skywalking-horizon-ui:0.4.0 +FROM ghcr.io/apache/skywalking-horizon-ui:0.5.0 COPY horizon.yaml /app/horizon.yaml ``` @@ -146,7 +146,7 @@ spec: fsGroup: 101 containers: - name: horizon - image: ghcr.io/apache/skywalking-horizon-ui:0.4.0 + image: ghcr.io/apache/skywalking-horizon-ui:0.5.0 ports: - containerPort: 8081 envFrom: @@ -188,7 +188,7 @@ docker run -d --name horizon \ -p 8081:8081 \ -v "$PWD/horizon.yaml:/app/horizon.yaml:ro" \ -v horizon-state:/data \ - ghcr.io/apache/skywalking-horizon-ui:0.4.0 + ghcr.io/apache/skywalking-horizon-ui:0.5.0 ``` Without a mounted volume the writes still land in the container's writable layer at `/data/` (ephemeral, but at least non-failing). Mounting a volume is what makes them durable. @@ -239,7 +239,7 @@ NODE_ENV=production LOG_LEVEL=info node dist/server.js ### Per-request logging -Fastify's request logger is on by default and emits one `incoming request` line + one `request completed` line per HTTP request, both tagged with a stable `reqId`. These are level-`info` (30) events — **suppressed under the production default `error`**. Bump to `LOG_LEVEL=info` to surface them; example pair under that level: +The server request logger is on by default and emits one `incoming request` line + one `request completed` line per HTTP request, both tagged with a stable `reqId`. These are level-`info` (30) events — **suppressed under the production default `error`**. Bump to `LOG_LEVEL=info` to surface them; example pair under that level: ```json {"level":30,"time":1779109372598,"pid":1,"hostname":"...","reqId":"req-1","req":{"method":"GET","url":"/api/auth/health","host":"127.0.0.1:8081","remoteAddress":"192.168.65.1","remotePort":60655},"msg":"incoming request"} @@ -299,14 +299,18 @@ so session cookies are flagged `Secure` and the browser refuses to send them ove ## Building locally -The image is built from the pre-packaged `./dist/` directory in the repo root. Build that artifact first: +The image is a multi-stage build: it compiles the app from source inside the build stage, so there is no host pre-step — `docker build` is self-contained (it only needs network for `pnpm install` during the build). + +Single-arch dev build: ```sh -pnpm install -pnpm package +docker build -t horizon:local . +docker run --rm -it -p 8081:8081 \ + -v "$PWD/horizon.yaml:/app/horizon.yaml:ro" \ + horizon:local ``` -Then use the same `docker buildx` invocation shape as CI: +Multi-arch build (same shape as CI — needs a `docker-container` buildx builder and QEMU for the non-native arch): ```sh docker buildx build \ @@ -315,15 +319,6 @@ docker buildx build \ . ``` -For a single-arch dev build (faster): - -```sh -docker build -t horizon:local . -docker run --rm -it -p 8081:8081 \ - -v "$PWD/horizon.yaml:/app/horizon.yaml:ro" \ - horizon:local -``` - ## Health probes Wire your platform's readiness probe to one of: diff --git a/package.json b/package.json index 608b36e..c91f1c2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "type": "module", "engines": { - "node": ">=20" + "node": ">=22" }, "packageManager": "[email protected]", "scripts": { diff --git a/scripts/package.mjs b/scripts/package.mjs index 8ec251b..7355445 100644 --- a/scripts/package.mjs +++ b/scripts/package.mjs @@ -78,8 +78,8 @@ step('Building UI (vite production build)'); run('pnpm --filter @skywalking-horizon-ui/ui build'); step('Materializing production install tree (pnpm deploy)'); -// `--legacy` is required under pnpm 10+ for non-injected workspaces; see -// the matching note in the Dockerfile. We deploy directly into ./dist +// `--legacy` is required under pnpm 10+ for non-injected workspaces. +// We deploy directly into ./dist // (renaming through an intermediate) rather than copying out, because // the produced node_modules contains pnpm-style symlinks into an // in-tree `.pnpm/` store — copying would either preserve broken diff --git a/scripts/release-finalize.sh b/scripts/release-finalize.sh new file mode 100755 index 0000000..d282dbe --- /dev/null +++ b/scripts/release-finalize.sh @@ -0,0 +1,328 @@ +#!/usr/bin/env bash + +# +# 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. +# + +# Apache SkyWalking Horizon UI — POST-VOTE release finalization. +# +# Run this AFTER the [VOTE] on [email protected] passes. It is the +# second half of the release flow; `scripts/release.sh` is the first half +# (build, sign, upload RC to SVN dev, vote email, next-dev PR). +# +# What it does, in order: +# +# 1. Promote the voted artifacts on SVN: server-side move from +# dist/dev/skywalking/horizon-ui/<v>/ (release candidate) +# to +# dist/release/skywalking/horizon-ui/<v>/ (official release) +# and remove the PREVIOUS release from release/ (ASF keeps only the +# current release live; older ones are auto-archived). +# +# 2. Cut a GitHub release on tag v<v>, attaching the SAME voted bytes +# (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 +# :horizon-latest moving Horizon pointer +# :latest moving repo pointer (overrides booster-ui's — +# confirmed interactively before pushing) +# +# Usage: bash scripts/release-finalize.sh +# +# The script is idempotent-ish and confirms before every irreversible step +# (SVN move, SVN delete, gh release, each image push). Nothing destructive +# happens without a y/N. + +set -e -o pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROJECT_DIR=$(cd "${SCRIPT_DIR}/.." && pwd) +PRODUCT_NAME="apache-skywalking-horizon-ui" + +SVN_DEV_URL="https://dist.apache.org/repos/dist/dev/skywalking/horizon-ui" +SVN_RELEASE_URL="https://dist.apache.org/repos/dist/release/skywalking/horizon-ui" + +DOCKERHUB_REPO="apache/skywalking-ui" +WORK_DIR="${SCRIPT_DIR}/.finalize-work" +BUILDER_NAME="horizon-release-builder" + +# ========================== Helpers ========================== + +err() { echo "ERROR: $*" >&2; } +note() { echo ""; echo "=== $* ==="; } + +confirm() { + local prompt="$1" + read -r -p "${prompt} [y/N] " ans + [[ "$ans" == "y" || "$ans" == "Y" ]] +} + +svn_exists() { + svn ls "$1" >/dev/null 2>&1 +} + +# ========================== Step 1: Tool + auth preflight ========================== +note "Step 1 — Tool + auth preflight" + +MISSING=() +for t in svn gh git docker shasum curl node; do + command -v "$t" >/dev/null || MISSING+=("$t") +done +if [ ${#MISSING[@]} -gt 0 ]; then + err "Missing required tools: ${MISSING[*]}" + exit 1 +fi + +if ! docker buildx version >/dev/null 2>&1; then + err "docker buildx is required for the multi-arch image build." + exit 1 +fi + +# gh must be logged in with repo scope to cut a release. +if ! gh auth status >/dev/null 2>&1; then + err "gh is not authenticated. Run: gh auth login" + exit 1 +fi +echo "gh: $(gh auth status 2>&1 | grep -m1 'Logged in' | sed 's/^[[:space:]]*//')" + +# Docker Hub: confirm a stored login. The push itself will 403 if the +# logged-in account lacks push rights to the apache org — surface the +# identity now so a wrong account is caught before the long build. +DOCKER_USER=$(printf 'https://index.docker.io/v1/' | docker-credential-desktop get 2>/dev/null \ + | node -e "let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{try{process.stdout.write(JSON.parse(s).Username||'')}catch(e){}})" 2>/dev/null || true) +if [ -z "${DOCKER_USER}" ]; then + echo "Could not read a Docker Hub login from the credential store." + echo "If you are not logged in, run: docker login" + confirm "Continue anyway (the push will fail if not authorized)?" || { echo "Aborted."; exit 1; } +else + echo "Docker Hub login: ${DOCKER_USER}" + echo " NOTE: pushing to ${DOCKERHUB_REPO} needs this account to have push rights" + echo " in the 'apache' Docker Hub org. The push 403s otherwise." +fi + +# ========================== Step 2: Detect version ========================== +note "Step 2 — Detect release version" + +DETECTED=$(cd "${PROJECT_DIR}" && git tag --list 'v*' --sort=-version:refname | head -1 | sed 's/^v//') +echo "Most recent git tag: v${DETECTED:-<none>}" +read -r -p "Release version to finalize [${DETECTED}]: " RELEASE_VERSION +RELEASE_VERSION="${RELEASE_VERSION:-${DETECTED}}" +if [ -z "${RELEASE_VERSION}" ]; then + err "No release version provided." + exit 1 +fi +TAG="v${RELEASE_VERSION}" + +if ! (cd "${PROJECT_DIR}" && git rev-parse "${TAG}" >/dev/null 2>&1); then + err "Git tag ${TAG} does not exist locally. Fetch tags first: git fetch --tags" + exit 1 +fi +echo "Finalizing ${RELEASE_VERSION} (tag ${TAG})." +confirm "Proceed?" || { echo "Aborted."; exit 1; } + +rm -rf "${WORK_DIR}" +mkdir -p "${WORK_DIR}" + +# ========================== Step 3: SVN move dev -> release ========================== +note "Step 3 — Promote on SVN: dev (RC) -> release (official)" + +echo " FROM (release candidate): ${SVN_DEV_URL}/${RELEASE_VERSION}/" +echo " TO (official release): ${SVN_RELEASE_URL}/${RELEASE_VERSION}/" + +read -r -p "Apache SVN username: " SVN_USER +read -r -s -p "Apache SVN password: " SVN_PASS +echo "" +SVN_AUTH=(--username "${SVN_USER}" --password "${SVN_PASS}" --non-interactive --no-auth-cache) + +if ! svn_exists "${SVN_DEV_URL}/${RELEASE_VERSION}"; then + err "Release candidate not found at ${SVN_DEV_URL}/${RELEASE_VERSION}/." + err "Did scripts/release.sh upload it? (Step 13)" + exit 1 +fi + +if svn_exists "${SVN_RELEASE_URL}/${RELEASE_VERSION}"; then + echo "Already present at release/${RELEASE_VERSION} — skipping the move (idempotent)." +else + # The parent dir release/skywalking/horizon-ui may not exist yet (first + # SVN-published Horizon release). svn mv into a missing parent fails, so + # create the parent chain first. + if ! svn_exists "${SVN_RELEASE_URL}"; then + echo "Creating ${SVN_RELEASE_URL}/ (first Horizon release here)…" + svn mkdir --parents "${SVN_AUTH[@]}" \ + -m "Create Horizon UI release directory" \ + "${SVN_RELEASE_URL}" + fi + if confirm "Run the server-side svn mv now?"; then + svn mv "${SVN_AUTH[@]}" \ + -m "Release Apache SkyWalking Horizon UI ${RELEASE_VERSION}" \ + "${SVN_DEV_URL}/${RELEASE_VERSION}" \ + "${SVN_RELEASE_URL}/${RELEASE_VERSION}" + echo "Moved to ${SVN_RELEASE_URL}/${RELEASE_VERSION}/" + else + err "SVN move skipped — cannot continue without the official artifacts." + exit 1 + fi +fi + +# Remove the PREVIOUS release from release/ (ASF policy: only the current +# release stays live; older versions are auto-archived to archive.apache.org). +PREV_RELEASE=$(svn ls "${SVN_RELEASE_URL}/" 2>/dev/null \ + | sed 's,/$,,' \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ + | grep -vx "${RELEASE_VERSION}" \ + | sort -t. -k1,1n -k2,2n -k3,3n \ + | tail -1 || true) +if [ -n "${PREV_RELEASE}" ]; then + echo "Previous release in release/: ${PREV_RELEASE}" + if confirm "Remove release/${PREV_RELEASE}/ (auto-archived, still downloadable from archive.apache.org)?"; then + svn rm "${SVN_AUTH[@]}" \ + -m "Remove superseded release ${PREV_RELEASE} (archived)" \ + "${SVN_RELEASE_URL}/${PREV_RELEASE}" + echo "Removed release/${PREV_RELEASE}/." + else + echo "Left release/${PREV_RELEASE}/ in place." + fi +fi +unset SVN_PASS + +# ========================== Step 4: GitHub release ========================== +note "Step 4 — GitHub release ${TAG}" + +# Pull the VOTED artifacts back from release/ so the GitHub release attaches +# byte-identical files to what the PMC voted on (not a fresh rebuild). +ART_DIR="${WORK_DIR}/artifacts" +mkdir -p "${ART_DIR}" +SRC_BASE="${PRODUCT_NAME}-${RELEASE_VERSION}-src.tar.gz" +BIN_BASE="${PRODUCT_NAME}-${RELEASE_VERSION}-bin.tar.gz" +for f in \ + "${SRC_BASE}" "${SRC_BASE}.asc" "${SRC_BASE}.sha512" \ + "${BIN_BASE}" "${BIN_BASE}.asc" "${BIN_BASE}.sha512"; do + echo "Fetching ${f}…" + curl -fSL -o "${ART_DIR}/${f}" "${SVN_RELEASE_URL}/${RELEASE_VERSION}/${f}" +done + +# Re-verify the checksums locally before attaching. +(cd "${ART_DIR}" && shasum -a 512 -c "${SRC_BASE}.sha512" && shasum -a 512 -c "${BIN_BASE}.sha512") +echo "Checksums verified." + +# Extract the CHANGELOG section for this version as the release body. +NOTES_FILE="${WORK_DIR}/release-notes.md" +awk -v v="${RELEASE_VERSION}" ' + $0 == "## " v { in_sec=1; next } + in_sec && /^## / { in_sec=0 } + in_sec { print } +' "${PROJECT_DIR}/CHANGELOG.md" > "${NOTES_FILE}" +{ + echo "" + echo "---" + echo "" + echo "Source & binary releases (with signatures and checksums):" + echo "* ${SVN_RELEASE_URL}/${RELEASE_VERSION}/" + echo "* KEYS: https://dist.apache.org/repos/dist/release/skywalking/KEYS" + echo "" + echo "Container image: \`docker pull ${DOCKERHUB_REPO}:horizon-${RELEASE_VERSION}\`" +} >> "${NOTES_FILE}" + +if gh release view "${TAG}" --repo apache/skywalking-horizon-ui >/dev/null 2>&1; then + echo "GitHub release ${TAG} already exists — skipping create." +else + echo "Release notes preview:" + echo "------------------------------------------------------------" + cat "${NOTES_FILE}" + echo "------------------------------------------------------------" + 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}" \ + --notes-file "${NOTES_FILE}" \ + "${ART_DIR}/${SRC_BASE}" \ + "${ART_DIR}/${SRC_BASE}.asc" \ + "${ART_DIR}/${SRC_BASE}.sha512" \ + "${ART_DIR}/${BIN_BASE}" \ + "${ART_DIR}/${BIN_BASE}.asc" \ + "${ART_DIR}/${BIN_BASE}.sha512" + echo "GitHub release created." + else + echo "Skipped GitHub release." + fi +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 + +IMG_TAGS=(-t "${DOCKERHUB_REPO}:horizon-${RELEASE_VERSION}" -t "${DOCKERHUB_REPO}:horizon-latest") +echo "Image tags to push:" +echo " ${DOCKERHUB_REPO}:horizon-${RELEASE_VERSION} (immutable)" +echo " ${DOCKERHUB_REPO}:horizon-latest (moving Horizon pointer)" +echo "" +echo "Bare ':latest' on a SHARED repo overrides booster-ui's :latest —" +echo "anyone pulling ${DOCKERHUB_REPO}:latest would then get Horizon." +if confirm "Also push the bare ':latest' tag?"; then + IMG_TAGS+=(-t "${DOCKERHUB_REPO}:latest") + echo "Will also push :latest." +fi + +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}" +else + echo "Skipped Docker Hub push." +fi + +# ========================== Done ========================== +note "Done — ${RELEASE_VERSION} finalized" +echo " SVN release: ${SVN_RELEASE_URL}/${RELEASE_VERSION}/" +echo " GitHub: https://github.com/apache/skywalking-horizon-ui/releases/tag/${TAG}" +echo " Docker Hub: ${DOCKERHUB_REPO}:horizon-${RELEASE_VERSION}" +echo "" +echo "Remaining manual steps:" +echo " 1. Update the Docker Hub repo README from dist-material/docker-hub/README.md" +echo " (Docker Hub → ${DOCKERHUB_REPO} → 'Repository overview' → edit)." +echo " 2. Send the [ANNOUNCE] email to dev@ + [email protected]." +echo " 3. Update the download page on the SkyWalking website." +echo "" +echo "Working files left in ${WORK_DIR}/ (safe to delete)." diff --git a/scripts/release.sh b/scripts/release.sh index 51b3b57..a1468bb 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -165,21 +165,25 @@ for pj in package.json packages/api-client/package.json packages/design-tokens/p done check_file_has_version "apps/bff/src/server.ts" "'${CURRENT_VERSION}'" -# Docs reference the LAST RELEASED image tag — derive it from the most -# recent git tag (`vX.Y.Z`). We don't bump docs on main between releases; -# the next-version PR (run after the vote passes) is what advances them. +# Docs normally reference the last released image tag on main, then the +# release commit advances them to the new tag. If the docs were already +# prepared for the release version, accept that too. PRIOR_RELEASE=$(cd "${PROJECT_DIR}" && git tag --list 'v*' --sort=-version:refname | head -1 | sed 's/^v//') if [ -z "${PRIOR_RELEASE}" ]; then err "No prior release tag (vX.Y.Z) found. Tag the first release manually before using this script." exit 1 fi -check_file_has_version "docs/setup/container-image.md" "ghcr.io/apache/skywalking-horizon-ui:${PRIOR_RELEASE}" +if ! file_has "${PROJECT_DIR}/docs/setup/container-image.md" "ghcr.io/apache/skywalking-horizon-ui:${PRIOR_RELEASE}" && + ! file_has "${PROJECT_DIR}/docs/setup/container-image.md" "ghcr.io/apache/skywalking-horizon-ui:${RELEASE_VERSION}"; then + err "docs/setup/container-image.md must reference either prior image tag ${PRIOR_RELEASE} or release tag ${RELEASE_VERSION}." + CONSISTENT=false +fi if ! $CONSISTENT; then err "Version drift across files. Fix before continuing." exit 1 fi -echo "Code markers all at ${CURRENT_VERSION}; docs at last-released ${PRIOR_RELEASE}." +echo "Code markers all at ${CURRENT_VERSION}; container docs are release-check compatible." # ========================== Step 5: Doc + Changelog check ========================== note "Step 5 — Docs + CHANGELOG check" @@ -392,6 +396,18 @@ read -r -p "Apache SVN username: " SVN_USER read -r -s -p "Apache SVN password: " SVN_PASS echo "" +# The dev parent dir (dist/dev/skywalking/horizon-ui) may not exist yet on +# the first SVN-published release. `svn co` of a missing URL fails, so +# check-and-create the parent chain before checking it out. +if ! svn ls --username "${SVN_USER}" --password "${SVN_PASS}" \ + --non-interactive --no-auth-cache "${SVN_DEV_URL}" >/dev/null 2>&1; then + echo "Dev staging dir does not exist — creating ${SVN_DEV_URL}/" + svn mkdir --parents --username "${SVN_USER}" --password "${SVN_PASS}" \ + --non-interactive --no-auth-cache \ + -m "Create Horizon UI dev staging directory" \ + "${SVN_DEV_URL}" +fi + SVN_STAGE="${WORK_DIR}/svn-staging" rm -rf "${SVN_STAGE}" svn co --depth empty --username "${SVN_USER}" --password "${SVN_PASS}" \
