Skip to content

feat(profiling): Hide frames, Find frame and Pop out on eBPF, Async and pprof profiling #1118

feat(profiling): Hide frames, Find frame and Pop out on eBPF, Async and pprof profiling

feat(profiling): Hide frames, Find frame and Pop out on eBPF, Async and pprof profiling #1118

Workflow file for this run

#
# 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.
#
name: CI
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
license-header:
# Kept separate because it needs neither Node nor `pnpm install` — it is a
# container action that would otherwise sit behind the toolchain setup the
# build job spends its first half-minute on.
name: License header
runs-on: ubuntu-26.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Check license header
uses: apache/skywalking-eyes@61275cc80d0798a405cb070f7d3a8aaf7cf2c2c1 # v0.8.0
build-and-check:
# Every Node-side gate in one job. They all need the same checkout + pnpm
# install, and each gate itself finishes in seconds, so fanning them across
# runners bought a few tens of seconds of wall clock at the price of paying
# the setup cost eight times over. Steps run cheapest-first so a lint or
# type error reports before the package build starts.
name: Build and check
runs-on: ubuntu-26.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 — the commit on the ASF allow-list; a version tag is refused
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# ESLint (rules + 2000-line cap, comments excluded) across ui + bff, the
# source-budget gate (≤2000 code / ≤500 comment lines per file), both
# i18n gates (UI catalog coverage/parity and the BFF template-overlay
# validator), and the bundled layer / overview templates. Those templates
# are configuration that ships inside the release, so a broken one is a
# shipping defect rather than operator error — and the loader only
# JSON.parses them, which is how an orderBy naming a nonexistent column
# and an unsupported aggregation both shipped.
- name: Lint, source budget, i18n, security, docs links, bundled templates
run: pnpm lint
# The vendored BanyanDB wire contract, in one regenerate-then-diff gate —
# the same shape as `licenses:bin:check`. Re-fetches the .proto from the
# PINNED upstream commits, regenerates proto.pb.d.ts from what it fetched,
# then diffs both: so it proves in one pass that nobody hand-edited a
# vendored .proto, that nobody hand-edited the generated declarations, and
# that the two are in step. `git diff --exit-code` names the paths that
# moved, which is what says whether the contract or the generator drifted.
#
# Pinning commits rather than branches is what stops an upstream release
# turning an unrelated PR red. `pnpm proto:check:types` is the same check
# minus the fetch, kept for working offline — redundant here, since this
# step regenerates from the freshly fetched tree anyway.
- name: Vendored proto tree matches its pinned upstream
run: pnpm proto:check
# Advisories against what actually SHIPS. `--prod` matches the scope the
# binary LICENSE is generated from, so the two gates describe the same
# artifact. A finding here is always something an operator would run, so
# it blocks. Only high+ does: a gate that fails on things nobody can act
# on gets ignored.
# remove, will aduit periodically, not every PR
# - name: Audit shipped dependencies
# run: pnpm audit --prod --audit-level=high
# Dev tooling is a different threat model, not a lesser one — it runs in
# CI with repository credentials and on contributors' machines, so a
# compromised build tool can reach the artifact without ever being IN it.
# Reported rather than enforced: an advisory in a linter's transitive
# tree with no patch available would otherwise block every unrelated PR.
- name: Audit build tooling (report only)
continue-on-error: true
run: pnpm audit --audit-level=high
- name: Type-check
run: pnpm -r run type-check
# `pnpm -r` walks every workspace package and runs `test:unit` —
# currently @skywalking-horizon-ui/ui (vitest + jsdom),
# @skywalking-horizon-ui/bff and @skywalking-horizon-ui/api-client
# (vitest, node env). Packages without the script (design-tokens,
# templates) are skipped automatically.
# The scripts already include `vitest run`, so no `--run` forwarding
# needed — keeps the CI line robust against pnpm-flag parsing quirks.
# The runner pool is pinned in those same scripts, not on this line, so a
# contributor's `pnpm test:unit` and this job share one isolation model.
# Why `threads`, and what enforces it: `src/runner-pool.test.ts`.
- name: Unit tests
run: pnpm -r run test:unit
# ASSEMBLES the distributable: the three packages/* builds, the BFF
# esbuild bundle, the Vite UI build, dist/server.js, the built UI at
# dist/static/, bundled templates, i18n catalogs, AI resources. Because it
# compiles the UI and the BFF on the way, it subsumes standalone build
# steps. Until this ran in CI the assembly happened for the first time
# inside the release image build — i.e. after merge — so a broken tarball
# merged green.
- name: Package the distributable
run: pnpm package
# The binary tarball bundles the whole production dependency tree — the
# BFF runtime deps and the UI deps Vite compiles into the static bundle —
# so the binary LICENSE has to name every one of them. license-eye
# regenerates it from LICENSE.tpl and `git diff` is the drift gate: a
# dependency added or removed without running `pnpm licenses:bin:update`
# fails here.
#
# Built from a commit rather than v0.8.0 because pnpm support landed after
# that release; revert to a tag once one ships with it.
#
# A license the tool cannot identify makes `dependency resolve` itself
# exit non-zero, so an unknown license fails this step too. The licenses/
# directory beside the LICENSE is maintained by hand — see CLAUDE.md.
- name: Set up Go for license-eye
uses: actions/setup-go@v6
with:
go-version: '1.25'
cache: false
- name: Verify binary LICENSE is current
run: |
go install "github.com/apache/skywalking-eyes/cmd/license-eye@$(tr -d '[:space:]' < .license-eye-version)"
pnpm licenses:bin:check
- name: Compare source vs. binary LICENSE/NOTICE shape
run: |
# Source-flavored LICENSE = repo root; must NOT contain the
# bundled-dep summary section.
if grep -q 'Horizon UI Subcomponents' LICENSE; then
echo "ERROR: repo-root LICENSE contains the bundled-dep summary section."
echo " That section belongs only in the binary LICENSE."
exit 1
fi
# Committed binary-flavored LICENSE must contain the summary.
if ! grep -q 'Horizon UI Subcomponents' dist-material/release-docs/LICENSE; then
echo "ERROR: committed binary LICENSE missing 'Horizon UI Subcomponents'."
exit 1
fi
# NOTICE: both must exist and start with the same ASF copyright line.
test -f NOTICE && test -f dist-material/release-docs/NOTICE
head -1 NOTICE | grep -q 'Apache SkyWalking Horizon UI'
head -1 dist-material/release-docs/NOTICE | grep -q 'Apache SkyWalking Horizon UI'
echo "src/bin LICENSE+NOTICE shape OK."
- name: Upload dependency report
uses: actions/upload-artifact@v4
with:
name: dist-license-report
path: |
dist-material/release-docs/LICENSE
dist-material/release-docs/NOTICE
# A dist/ that assembles but 404s at `/` is not a working release.
- name: Boot the packaged server and smoke-test it
run: |
set -eu
# No HORIZON_STATIC_DIR on purpose: the packaged layout must serve
# the UI from the static/ dir beside server.js without being told.
HORIZON_SERVER_PORT=8099 \
node dist/server.js > /tmp/ci-boot.log 2>&1 &
for _ in $(seq 1 60); do
curl -sf -o /dev/null "http://127.0.0.1:8099/api/health" && break
sleep 1
done
curl -sf -o /dev/null "http://127.0.0.1:8099/api/health" || { echo "server never became healthy"; cat /tmp/ci-boot.log; exit 1; }
fail() { echo "::error::$1"; cat /tmp/ci-boot.log; exit 1; }
# `/` must return the SPA shell, not a 404.
code=$(curl -s -o /tmp/index.html -w '%{http_code}' "http://127.0.0.1:8099/")
[ "$code" = "200" ] || fail "GET / returned ${code}, expected 200 (is the UI in the package?)"
grep -qi '<div id="app"' /tmp/index.html || fail "GET / did not return the SPA shell"
# A hashed asset referenced by that shell must resolve.
asset=$(grep -o '/assets/[^"]*\.js' /tmp/index.html | head -1)
[ -n "$asset" ] || fail "no hashed asset referenced by index.html"
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8099${asset}")
[ "$code" = "200" ] || fail "asset ${asset} returned ${code}"
# Client-side route falls through to the shell; /api/* still 404s as JSON.
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8099/layer/general/traces")
[ "$code" = "200" ] || fail "SPA deep link returned ${code}"
curl -s "http://127.0.0.1:8099/api/nope" | grep -q '"code":"not_found"' || fail "/api/* 404 is not the JSON envelope"
# Security headers, asserted against the SHIPPED artifact — a unit
# test on the header map cannot catch a hook that never runs, a
# plugin registered ahead of it, or a route that writes its own head.
hdr() { curl -s -D - -o /dev/null "$1" | tr -d '\r' | grep -i "^$2:" | cut -d' ' -f2-; }
csp=$(hdr "http://127.0.0.1:8099/" "content-security-policy")
[ -n "$csp" ] || fail "no Content-Security-Policy on the SPA shell"
# Pull script-src out and judge it alone. Matching the whole policy
# would be both wrong ways round: style-src legitimately carries
# 'unsafe-inline', and a substring test on the joined string depends
# on token order. This is the directive that makes an injected
# handler or a stored javascript: URL inert.
scriptsrc=$(printf '%s' "$csp" | tr ';' '\n' | grep -i 'script-src' | head -1 || true)
[ -n "$scriptsrc" ] || fail "CSP has no script-src: ${csp}"
case "$scriptsrc" in *"'self'"*) ;; *) fail "script-src lacks 'self': ${scriptsrc}" ;; esac
case "$scriptsrc" in *unsafe-inline*|*unsafe-eval*|*unsafe-hashes*)
fail "script-src grants an unsafe keyword: ${scriptsrc}" ;; esac
for h in x-content-type-options x-frame-options referrer-policy; do
[ -n "$(hdr "http://127.0.0.1:8099/" "$h")" ] || fail "missing ${h} on the SPA shell"
done
# Every /api/ response, including a 404 and an error, is unstorable.
for p in /api/health /api/nope; do
cc=$(hdr "http://127.0.0.1:8099${p}" "cache-control")
[ "$cc" = "no-store" ] || fail "${p} Cache-Control is '${cc}', expected no-store"
[ -n "$(hdr "http://127.0.0.1:8099${p}" "content-security-policy")" ] || fail "no CSP on ${p}"
done
# ...while content-hashed assets stay cacheable.
cc=$(hdr "http://127.0.0.1:8099${asset}" "cache-control")
case "$cc" in *no-store*) fail "hashed asset ${asset} is no-store — static files must stay cacheable" ;; esac
echo "packaged artifact boots, serves the UI, and carries its security headers"
banyandb-client-it:
# The BanyanDB client's only real coverage. Its unit tests exercise
# encoding in isolation; everything that matters — schema reconciliation,
# the write path's series construction, query criteria — is only true or
# false against a running server, and each of those has already shipped a
# bug that type-check, lint and unit tests all passed.
#
# Self-contained: testcontainers starts the BanyanDB that
# `test/e2e/script/env` pins, on an ephemeral port, and stops it after. No
# service container to declare and no port to collide with.
name: BanyanDB client IT
runs-on: ubuntu-26.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 — the commit on the ASF allow-list; a version tag is refused
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Integration tests against a real BanyanDB
run: pnpm test:it
required:
# Status-check name pinned to "Required" so Apache branch protection
# (`.asf.yaml: protected_branches.main.required_status_checks.contexts:
# [Required]`) can gate on a single rolled-up signal.
#
# E2E is a separate workflow with its own rolled-up "E2E Required"
# status, listed alongside this one in .asf.yaml — it cannot be a `needs`
# here because `needs` does not cross workflow boundaries.
if: always() && !cancelled()
name: Required
needs: [license-header, build-and-check, banyandb-client-it]
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- name: Check upstream jobs
run: |
[[ ${{ needs.license-header.result }} == 'success' ]] || exit 1
[[ ${{ needs.build-and-check.result }} == 'success' ]] || exit 1
[[ ${{ needs.banyandb-client-it.result }} == 'success' ]] || exit 1