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 649d23c6ee06d9c857bf408268c5d209d636fdf9 Author: Wu Sheng <[email protected]> AuthorDate: Tue May 19 23:37:44 2026 +0800 release: 0.5.0-dev — release.sh + dist LICENSE/NOTICE tooling + CI dep-license job - Version bump to 0.5.0-dev across every workspace package + server.ts HORIZON_VERSION default. -dev suffix marks main as in-flight; the release script strips it to derive the tagged version (mirrors the -SNAPSHOT pattern in the upstream skywalking repo). - scripts/release.sh: 15-step Apache release orchestrator — GPG signer preflight (@apache.org), required tooling check, cross-file version consistency, CHANGELOG section validation (rejects the "In development" placeholder), license-header gate, fresh clone, strip-dev + advance docs + tag, source tarball, self-contained binary tarball (`pnpm package` output + dep LICENSE/NOTICE), tarball LICENSE/NOTICE shape cross-check, GPG sign + sha512 + self-verify, prompt-for-Apache-SVN upload to dist/dev/skywalking/horizon-ui/<v>/, vote-email generation, next-version PR (0.5.0 → 0.6.0-dev). - scripts/collect-dist-licenses.mjs: walks the production dep graph via `pnpm list --prod`, copies each package's LICENSE-like file into dist/licenses/<name>-<ver>/, generates dist/LICENSE (Apache 2.0 + license family summary) and dist/NOTICE (ASF + verbatim third-party NOTICE pass-throughs) from dist-material/release-docs templates, and emits dist/.dependency-report.json for the checker. - scripts/check-dist-licenses.mjs: ASF Category-A allow-list, Category-X deny-list (AGPL / SSPL / BUSL / Commons-Clause / MPL-1.x / Ms-RL / RPL), warns on weak-copyleft (EPL / CDDL / MPL-2 / LGPL) and on packages shipped without a LICENSE-like file. - .github/workflows/ci.yaml: split license-header and the new `dependency-license` job (separate from header check on purpose — dep collection requires a full `pnpm package` and the dist walk). The job also asserts that the source-flavored LICENSE (repo root) lacks the bundled-dep summary section and that the binary-flavored dist/LICENSE contains it. Gated by the `Required` rollup status. - .licenserc.yaml: ignore dist-material/release-docs/**/*.tpl — those templates RENDER the LICENSE/NOTICE text, so a header comment would bleed into the output. - CHANGELOG.md: 0.5.0 placeholder section — to be filled before tagging. --- .github/workflows/ci.yaml | 61 +++- .licenserc.yaml | 5 + CHANGELOG.md | 11 + apps/bff/package.json | 2 +- apps/bff/src/server.ts | 2 +- apps/ui/package.json | 2 +- dist-material/release-docs/LICENSE.tpl | 217 +++++++++++++ dist-material/release-docs/NOTICE.tpl | 12 + package.json | 2 +- packages/api-client/package.json | 2 +- packages/design-tokens/package.json | 2 +- packages/templates/package.json | 2 +- scripts/check-dist-licenses.mjs | 163 ++++++++++ scripts/collect-dist-licenses.mjs | 263 ++++++++++++++++ scripts/release.sh | 540 +++++++++++++++++++++++++++++++++ 15 files changed, 1273 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9823ddb..78a7659 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,11 +38,59 @@ jobs: - name: Check license header uses: apache/skywalking-eyes@5b7ee1731d036b5aac68f8bd3fc9e6f98ada082e - # NOTE: `license-eye dependency resolve` is deferred to a release-only - # workflow. The tool invokes `npm ci` internally, which fails on pnpm - # workspaces (no package-lock.json). When we cut an Apache release we'll - # add a separate job that runs the resolver against a one-shot npm install - # of the production dep graph. For day-to-day CI, header-check is enough. + dependency-license: + # Separate from `license-header` on purpose: header check is a quick + # static scan; dep-license collection requires a full production build + # (pnpm package → dist/node_modules) and then a walk of that tree to + # produce dist/LICENSE + dist/NOTICE + dist/licenses/. We also verify + # source vs. binary LICENSE/NOTICE differ in the expected way + # (only the binary carries the bundled-dep summary). + name: Dependency license + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + - run: pnpm package + - name: Collect bundled-dep LICENSE/NOTICE + run: node scripts/collect-dist-licenses.mjs + - name: Check bundled-dep licenses against ASF allow/deny + run: node scripts/check-dist-licenses.mjs + - 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 dist/LICENSE (binary tarball)." + exit 1 + fi + # Binary-flavored LICENSE = generated; must contain the summary. + if ! grep -q 'Horizon UI Subcomponents' dist/LICENSE; then + echo "ERROR: dist/LICENSE missing 'Horizon UI Subcomponents' section." + echo " collect-dist-licenses.mjs did not run or template drift." + exit 1 + fi + # NOTICE: both must exist and start with the same ASF copyright line. + test -f NOTICE && test -f dist/NOTICE + head -1 NOTICE | grep -q 'Apache SkyWalking Horizon UI' + head -1 dist/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/LICENSE + dist/NOTICE + dist/.dependency-report.json type-check: name: Type-check (workspaces) @@ -120,13 +168,14 @@ jobs: # [Required]`) can gate on a single rolled-up signal. if: always() && !cancelled() name: Required - needs: [license-header, type-check, build-ui, build-bff, test] + needs: [license-header, dependency-license, type-check, build-ui, build-bff, test] runs-on: ubuntu-latest timeout-minutes: 5 steps: - name: Check upstream jobs run: | [[ ${{ needs.license-header.result }} == 'success' ]] || exit 1 + [[ ${{ needs.dependency-license.result }} == 'success' ]] || exit 1 [[ ${{ needs.type-check.result }} == 'success' ]] || exit 1 [[ ${{ needs.build-ui.result }} == 'success' ]] || exit 1 [[ ${{ needs.build-bff.result }} == 'success' ]] || exit 1 diff --git a/.licenserc.yaml b/.licenserc.yaml index 074d08f..ab40acf 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -50,6 +50,11 @@ header: - 'src/types/auto-imports.d.ts' - 'src/types/components.d.ts' - 'public/**' + # LICENSE.tpl / NOTICE.tpl in dist-material/release-docs/ render the + # binary tarball's LICENSE / NOTICE (scripts/collect-dist-licenses.mjs). + # Their contents ARE the Apache 2.0 license text and the ASF notice + # text — an extra license-header comment would bleed into the output. + - 'dist-material/release-docs/**/*.tpl' comment: on-failure diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a7da78..8c9f8af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ file-by-file implementation. For per-commit detail, see the git log. The version line is shared by every package in the monorepo (apps + shared packages) plus the BFF's `HORIZON_VERSION` default. +## 0.5.0 + +First Apache-style release cut from this repo: source + binary tarballs, +GPG-signed and SHA-512 checksummed, with a self-contained binary that +boots via `node server.js` and no `pnpm install` step. Binary distribution +ships a regenerated `LICENSE` + `NOTICE` that enumerate every bundled +third-party package — produced by `scripts/collect-dist-licenses.mjs` +during packaging and validated against a deny-list before signing. + +Fill in screen-facing highlights here before tagging. + ## 0.4.0 OAP becomes the runtime source of truth for UI templates, the 5-theme system diff --git a/apps/bff/package.json b/apps/bff/package.json index 2ab2d2f..6918656 100644 --- a/apps/bff/package.json +++ b/apps/bff/package.json @@ -1,6 +1,6 @@ { "name": "@skywalking-horizon-ui/bff", - "version": "0.4.0", + "version": "0.5.0-dev", "private": true, "type": "module", "main": "dist/server.js", diff --git a/apps/bff/src/server.ts b/apps/bff/src/server.ts index 82960e8..86547ec 100644 --- a/apps/bff/src/server.ts +++ b/apps/bff/src/server.ts @@ -221,7 +221,7 @@ if (staticDir && existsSync(staticDir)) { app.get('/api/health', async () => ({ status: 'ok', - version: process.env.HORIZON_VERSION ?? '0.4.0', + version: process.env.HORIZON_VERSION ?? '0.5.0-dev', sessions: sessions.size(), })); diff --git a/apps/ui/package.json b/apps/ui/package.json index 2c3878c..1747484 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -1,6 +1,6 @@ { "name": "@skywalking-horizon-ui/ui", - "version": "0.4.0", + "version": "0.5.0-dev", "private": true, "type": "module", "scripts": { diff --git a/dist-material/release-docs/LICENSE.tpl b/dist-material/release-docs/LICENSE.tpl new file mode 100644 index 0000000..7b18cfc --- /dev/null +++ b/dist-material/release-docs/LICENSE.tpl @@ -0,0 +1,217 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for describing the origin of the Work and + reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Support. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or support. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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 Subcomponents: + + The Apache SkyWalking Horizon UI binary distribution includes a number + of subcomponents with separate copyright notices and license terms. + Your use of the source code for these subcomponents is subject to the + terms and conditions of the licenses listed below. The full text of + each license is reproduced in the licenses/ directory of this + distribution. + + By license family, the bundled third-party software is: +======================================================================== + +{{ .Groups }} diff --git a/dist-material/release-docs/NOTICE.tpl b/dist-material/release-docs/NOTICE.tpl new file mode 100644 index 0000000..a774f22 --- /dev/null +++ b/dist-material/release-docs/NOTICE.tpl @@ -0,0 +1,12 @@ +Apache SkyWalking Horizon UI +Copyright {{ .Year }} The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +======================================================================== +This binary distribution bundles third-party software, whose own NOTICE +files (where present) are reproduced below verbatim. +======================================================================== + +{{ .Notices }} diff --git a/package.json b/package.json index ce18b32..1a62408 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skywalking-horizon-ui", - "version": "0.4.0", + "version": "0.5.0-dev", "private": true, "description": "Apache SkyWalking Horizon UI - next-generation web UI", "license": "Apache-2.0", diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 85f64ff..a3f5301 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@skywalking-horizon-ui/api-client", - "version": "0.4.0", + "version": "0.5.0-dev", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/design-tokens/package.json b/packages/design-tokens/package.json index 7dec6e7..612d42a 100644 --- a/packages/design-tokens/package.json +++ b/packages/design-tokens/package.json @@ -1,6 +1,6 @@ { "name": "@skywalking-horizon-ui/design-tokens", - "version": "0.4.0", + "version": "0.5.0-dev", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/templates/package.json b/packages/templates/package.json index 29ae86e..d8d322c 100644 --- a/packages/templates/package.json +++ b/packages/templates/package.json @@ -1,6 +1,6 @@ { "name": "@skywalking-horizon-ui/templates", - "version": "0.4.0", + "version": "0.5.0-dev", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/scripts/check-dist-licenses.mjs b/scripts/check-dist-licenses.mjs new file mode 100644 index 0000000..4a595d7 --- /dev/null +++ b/scripts/check-dist-licenses.mjs @@ -0,0 +1,163 @@ +/* + * 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. + */ + +/** + * Read the dependency report produced by `collect-dist-licenses.mjs` + * and verify: + * + * 1. Every package has a recognized license string. + * 2. No package falls into the ASF "Category-X" (forbidden) bucket + * — strong copyleft, source-available-only, or commercial-restrictive. + * 3. Every package's LICENSE file is reproduced under dist/licenses/ + * (a missing file fails the build — operators must commit a + * hand-supplied copy under `dist-material/release-docs/licenses-extra/` + * and the collector picks it up next run). + * + * Exits non-zero with a per-package diagnostic on any violation. + * Designed to run in CI (separate job from license-header) and as the + * last step before the release script signs the binary tarball. + */ + +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, '..'); +const distDir = resolve(repoRoot, 'dist'); +const reportPath = resolve(distDir, '.dependency-report.json'); + +if (!existsSync(reportPath)) { + console.error( + `FATAL: ${reportPath} not found. Run \`node scripts/collect-dist-licenses.mjs\` first.`, + ); + process.exit(1); +} + +const report = JSON.parse(readFileSync(reportPath, 'utf8')); + +// ASF "Category A" — permissive, freely redistributable. SPDX-ish keys. +// See https://www.apache.org/legal/resolved.html +const ALLOWED = new Set( + [ + 'Apache-2.0', + 'Apache 2.0', + 'Apache License 2.0', + 'Apache-2', + 'MIT', + 'MIT*', + 'MIT-0', + 'ISC', + 'BSD', + 'BSD-2-Clause', + 'BSD-3-Clause', + 'BSD-3-Clause-Clear', + '0BSD', + 'CC0-1.0', + 'CC-BY-3.0', + 'CC-BY-4.0', + 'Unlicense', + 'WTFPL', + 'Python-2.0', + 'PSF-2.0', + 'BlueOak-1.0.0', + 'Zlib', + 'Artistic-2.0', + ].map((s) => s.toLowerCase()), +); + +// Category X — must not appear in a binary release. +const FORBIDDEN_PATTERNS = [ + /\bAGPL/i, + /\bSSPL/i, + /\bBUSL/i, + /commons[\s-]?clause/i, + /\bRPL/i, // Reciprocal Public License + /\bMs[-\s]?RL/i, // Microsoft Reciprocal + /\bMPL[-\s]?1\./i, // MPL 1.x (only 2.0 is Cat-B / allowed-with-care) +]; + +// Category B — allowed but each must be reproduced verbatim. EPL, CDDL, +// MPL-2.0, LGPL-2.1, LGPL-3.0. Flagged but not failing — the LICENSE +// summary already lists them; vote reviewers can audit. +const WEAK_COPYLEFT_PATTERNS = [ + /\bEPL\b/i, + /\bCDDL/i, + /\bMPL[-\s]?2/i, + /\bLGPL/i, +]; + +const errors = []; +const warnings = []; + +for (const pkg of report.packages) { + const id = `${pkg.name}@${pkg.version}`; + const lic = (pkg.license ?? '').trim(); + const licLower = lic.toLowerCase(); + + if (!lic || licLower === 'unknown') { + errors.push(`${id}: missing or unknown license (declared: ${JSON.stringify(pkg.license)})`); + continue; + } + + // SPDX expressions: split on OR / AND. If any sub-expression is fully + // allowed, we accept the package under that one. AND requires all parts allowed. + const parts = lic.split(/\s+OR\s+|\s+AND\s+/i).map((s) => s.replace(/[()]/g, '').trim()); + const operator = / AND /i.test(lic) ? 'AND' : 'OR'; + + const partOk = parts.map((p) => ALLOWED.has(p.toLowerCase())); + const accepted = operator === 'AND' ? partOk.every(Boolean) : partOk.some(Boolean); + + for (const re of FORBIDDEN_PATTERNS) { + if (re.test(lic)) { + errors.push(`${id}: forbidden license: ${lic}`); + } + } + + if (!accepted && !errors.some((e) => e.startsWith(`${id}:`))) { + // Allow weak-copyleft with a warning. Block everything else. + if (WEAK_COPYLEFT_PATTERNS.some((re) => re.test(lic))) { + warnings.push(`${id}: weak-copyleft license '${lic}' — included verbatim under licenses/`); + } else { + errors.push(`${id}: unrecognized license '${lic}' — add to ALLOWED if compatible, or remove the dep`); + } + } + + if (!pkg.licenseFile) { + // A missing license file is a vote-blocker for non-trivial deps. + warnings.push(`${id}: no LICENSE-like file shipped under licenses/ (license declared: ${lic})`); + } +} + +if (warnings.length > 0) { + console.warn('Warnings:'); + for (const w of warnings) console.warn(` - ${w}`); +} + +if (errors.length > 0) { + console.error(''); + console.error(`License check FAILED with ${errors.length} error(s):`); + for (const e of errors) console.error(` - ${e}`); + console.error(''); + process.exit(1); +} + +console.log( + `License check OK: ${report.packageCount} packages, ` + + `${Object.keys(report.byLicense).length} license families, ` + + `${warnings.length} warning(s).`, +); diff --git a/scripts/collect-dist-licenses.mjs b/scripts/collect-dist-licenses.mjs new file mode 100644 index 0000000..ed8700b --- /dev/null +++ b/scripts/collect-dist-licenses.mjs @@ -0,0 +1,263 @@ +/* + * 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. + */ + +/** + * Walk the production `dist/node_modules/` tree built by + * `scripts/package.mjs` and emit the binary-tar's LICENSE / NOTICE / + * licenses/ subtree. + * + * Apache distribution rule: every bundled third-party module's license + * is reproduced in the binary tarball. The source tarball ships only + * first-party source (top-level LICENSE + NOTICE are enough), but the + * binary bundles npm dependencies and therefore needs an expanded LICENSE + * (license-family summary) and NOTICE (third-party NOTICE pass-throughs) + * plus per-package license texts under licenses/<name>-<version>/. + * + * Output (relative to repo `dist/`): + * LICENSE — Apache-2.0 + grouped third-party summary + * NOTICE — ASF + concatenated third-party NOTICEs + * licenses/<pkg>-<ver>/ — verbatim LICENSE-ish files from each dep + * .dependency-report.json — { packages: [...] } for check-dist-licenses + * + * Run after `pnpm package`. Re-runs are idempotent: the script clears + * dist/licenses/ first. + */ + +import { execSync } from 'node:child_process'; +import { + cpSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, '..'); +const distDir = resolve(repoRoot, 'dist'); +const nmDir = resolve(distDir, 'node_modules'); +const licensesOutDir = resolve(distDir, 'licenses'); +const templatesDir = resolve(repoRoot, 'dist-material/release-docs'); + +if (!existsSync(nmDir)) { + console.error( + `FATAL: ${nmDir} does not exist. Run \`pnpm package\` first.`, + ); + process.exit(1); +} + +const LICENSE_FILE_PATTERNS = [ + /^LICENSE$/i, + /^LICENCE$/i, + /^LICENSE\.(md|txt|rst)$/i, + /^LICENCE\.(md|txt|rst)$/i, + /^COPYING$/i, + /^COPYING\.(md|txt|rst)$/i, + /^COPYRIGHT$/i, + /^COPYRIGHT\.(md|txt|rst)$/i, +]; +const NOTICE_FILE_PATTERNS = [/^NOTICE$/i, /^NOTICE\.(md|txt|rst)$/i]; + +function pickFile(dir, patterns) { + if (!existsSync(dir)) return null; + for (const entry of readdirSync(dir)) { + if (patterns.some((p) => p.test(entry))) return join(dir, entry); + } + return null; +} + +// Find every realpath-distinct package directory under dist/node_modules. +// pnpm's layout puts true packages under `.pnpm/<pkg>@<ver>(_<peers>)/node_modules/<pkg>/`, +// with top-level entries being symlinks into that store. We use `pnpm list` +// to get the canonical production dep graph and resolve each entry's +// realpath to get the package.json we should be reading. +function collectPackages() { + // `pnpm list --prod --depth Infinity --json` returns the realized + // production dep tree. We flatten it ourselves so first-party workspace + // packages can be filtered out by name prefix. + const raw = execSync( + 'pnpm list --prod --depth Infinity --json', + { + cwd: distDir, + maxBuffer: 64 * 1024 * 1024, + stdio: ['ignore', 'pipe', 'inherit'], + }, + ).toString(); + const json = JSON.parse(raw); + // pnpm list returns an array of root packages. dist/ has exactly one. + const root = Array.isArray(json) ? json[0] : json; + + const seen = new Map(); // key: name@version → { path, name, version } + function walk(deps) { + if (!deps) return; + for (const [name, info] of Object.entries(deps)) { + // Skip first-party workspace packages — they're our own code. + if (name.startsWith('@skywalking-horizon-ui/')) { + walk(info.dependencies); + continue; + } + const version = info.version; + const key = `${name}@${version}`; + if (seen.has(key)) continue; + const pkgPath = info.path; + if (!pkgPath || !existsSync(pkgPath)) { + console.warn(`WARN: package path missing for ${key}: ${pkgPath}`); + continue; + } + seen.set(key, { name, version, path: pkgPath }); + walk(info.dependencies); + } + } + walk(root.dependencies); + return Array.from(seen.values()).sort((a, b) => + a.name === b.name ? a.version.localeCompare(b.version) : a.name.localeCompare(b.name), + ); +} + +function normalizeLicense(pkgJson) { + const lic = pkgJson.license; + if (typeof lic === 'string') return lic; + if (lic && typeof lic === 'object' && typeof lic.type === 'string') { + return lic.type; + } + if (Array.isArray(pkgJson.licenses)) { + // Deprecated form. Join SPDX-style. + return pkgJson.licenses.map((l) => l.type || l).filter(Boolean).join(' OR '); + } + return 'UNKNOWN'; +} + +function readPkgJson(pkgPath) { + const p = join(pkgPath, 'package.json'); + if (!existsSync(p)) return null; + try { + return JSON.parse(readFileSync(p, 'utf8')); + } catch (e) { + console.warn(`WARN: cannot parse ${p}: ${e.message}`); + return null; + } +} + +const packages = collectPackages(); + +// Reset output directory +rmSync(licensesOutDir, { recursive: true, force: true }); +mkdirSync(licensesOutDir, { recursive: true }); + +const report = []; +const byLicense = new Map(); +const noticePieces = []; + +for (const pkg of packages) { + const pj = readPkgJson(pkg.path); + if (!pj) continue; + const license = normalizeLicense(pj); + const homepage = pj.homepage || pj.repository?.url || pj.repository || ''; + const entry = { + name: pkg.name, + version: pkg.version, + license, + homepage: typeof homepage === 'string' ? homepage : '', + licenseFile: null, + noticeFile: null, + }; + + const slug = `${pkg.name.replace(/\//g, '__')}-${pkg.version}`; + const outDir = join(licensesOutDir, slug); + + const licFile = pickFile(pkg.path, LICENSE_FILE_PATTERNS); + if (licFile) { + if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true }); + const dest = join(outDir, relative(pkg.path, licFile)); + mkdirSync(dirname(dest), { recursive: true }); + cpSync(licFile, dest); + entry.licenseFile = relative(distDir, dest); + } + const noticeFile = pickFile(pkg.path, NOTICE_FILE_PATTERNS); + if (noticeFile) { + if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true }); + const dest = join(outDir, relative(pkg.path, noticeFile)); + mkdirSync(dirname(dest), { recursive: true }); + cpSync(noticeFile, dest); + entry.noticeFile = relative(distDir, dest); + noticePieces.push( + `------ ${pkg.name}@${pkg.version} ------\n${readFileSync( + noticeFile, + 'utf8', + ).trim()}\n`, + ); + } + + report.push(entry); + const bucket = byLicense.get(license) ?? []; + bucket.push(entry); + byLicense.set(license, bucket); +} + +// Render LICENSE.tpl → dist/LICENSE +const groupLines = []; +const sortedLicenses = Array.from(byLicense.keys()).sort(); +for (const lic of sortedLicenses) { + groupLines.push(`\n--- ${lic} ---\n`); + for (const e of byLicense.get(lic)) { + const ref = e.licenseFile ? ` (${e.licenseFile})` : ''; + groupLines.push(` * ${e.name}@${e.version}${ref}`); + } +} +const licenseTpl = readFileSync(join(templatesDir, 'LICENSE.tpl'), 'utf8'); +const licenseOut = licenseTpl.replace('{{ .Groups }}', groupLines.join('\n')); +writeFileSync(join(distDir, 'LICENSE'), licenseOut); + +// Render NOTICE.tpl → dist/NOTICE +const noticeTpl = readFileSync(join(templatesDir, 'NOTICE.tpl'), 'utf8'); +const year = new Date().getUTCFullYear(); +const noticeOut = noticeTpl + .replace('{{ .Year }}', String(year)) + .replace( + '{{ .Notices }}', + noticePieces.length > 0 + ? noticePieces.join('\n') + : '(No third-party NOTICE files present in bundled dependencies.)\n', + ); +writeFileSync(join(distDir, 'NOTICE'), noticeOut); + +// Machine-readable report for the check step. +writeFileSync( + join(distDir, '.dependency-report.json'), + JSON.stringify( + { + generatedAt: new Date().toISOString(), + packageCount: report.length, + packages: report, + byLicense: Object.fromEntries( + sortedLicenses.map((l) => [l, byLicense.get(l).length]), + ), + }, + null, + 2, + ), +); + +console.log( + `Wrote dist/LICENSE, dist/NOTICE, dist/licenses/ (${report.length} packages, ` + + `${sortedLicenses.length} license families).`, +); diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..7936b4c --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,540 @@ +#!/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 release automation. +# +# Mirrors the apache/skywalking release.sh flow (see +# docs/en/guides/How-to-release.md upstream) but adapted for the +# pnpm-workspace Node project layout. Produces: +# +# apache-skywalking-horizon-ui-<v>-src.tar.gz {.asc,.sha512} +# apache-skywalking-horizon-ui-<v>-bin.tar.gz {.asc,.sha512} +# +# Uploads them to https://dist.apache.org/repos/dist/dev/skywalking/horizon-ui/<v>/ +# then prepares a next-version PR. +# +# Usage: bash scripts/release.sh + +set -e -o pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROJECT_DIR=$(cd "${SCRIPT_DIR}/.." && pwd) +PRODUCT_NAME="apache-skywalking-horizon-ui" +REPO_URL="${HORIZON_RELEASE_REPO_URL:-https://github.com/apache/skywalking-horizon-ui.git}" +REPO_BRANCH="${HORIZON_RELEASE_BRANCH:-main}" +SVN_DEV_URL="https://dist.apache.org/repos/dist/dev/skywalking/horizon-ui" +WORK_DIR="${SCRIPT_DIR}/.release-work" +CLONE_DIR="${WORK_DIR}/skywalking-horizon-ui" + +# ========================== Helpers ========================== + +err() { echo "ERROR: $*" >&2; } +note() { echo ""; echo "=== $* ==="; } + +confirm() { + local prompt="$1" + read -r -p "${prompt} [y/N] " ans + [[ "$ans" == "y" || "$ans" == "Y" ]] +} + +# Extract the root package.json "version" without depending on jq — +# we want this script to be runnable on stock macOS / Alpine. +read_version() { + node -e "process.stdout.write(JSON.parse(require('fs').readFileSync('${PROJECT_DIR}/package.json','utf8')).version)" +} + +# Quietly check that the named file contains a literal needle. +file_has() { + grep -F -q -- "$2" "$1" +} + +# ========================== Step 1: GPG signer ========================== +note "Step 1 — GPG signer check" + +GPG_KEY_ID=$(git config user.signingkey 2>/dev/null || true) +if [ -z "$GPG_KEY_ID" ]; then + GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG 2>/dev/null | grep -A1 '^sec' | tail -1 | awk '{print $1}' || true) +fi +if [ -z "$GPG_KEY_ID" ]; then + err "No GPG secret key found. Configure your Apache GPG key first." + exit 1 +fi + +GPG_UIDS=$(gpg --list-secret-keys --keyid-format LONG 2>/dev/null | grep 'uid' | sed 's/.*] //') +GPG_EMAIL=$(echo "$GPG_UIDS" | grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' | head -1) + +if [[ "$GPG_EMAIL" != *"@apache.org" ]]; then + err "GPG key email '${GPG_EMAIL}' is not @apache.org — Apache releases must be signed with an @apache.org key." + exit 1 +fi +echo "GPG Signer: ${GPG_UIDS}" +echo "GPG Key: ${GPG_KEY_ID}" +confirm "Is this the correct signer?" || { echo "Aborted."; exit 1; } + +export GPG_TTY=$(tty) +echo "Verifying GPG signing works (you may be prompted for the passphrase)…" +TEST_FILE=$(mktemp); echo "test" > "${TEST_FILE}" +if ! gpg --armor --detach-sig "${TEST_FILE}" 2>/dev/null; then + rm -f "${TEST_FILE}" "${TEST_FILE}.asc" + err "GPG signing failed. Try: export GPG_TTY=\$(tty) / gpgconf --launch gpg-agent" + exit 1 +fi +rm -f "${TEST_FILE}" "${TEST_FILE}.asc" +echo "GPG signing OK." + +# ========================== Step 2: Required tools ========================== +note "Step 2 — Tool check" + +MISSING=() +for t in gpg svn shasum git gh node pnpm tar license-eye; do + command -v "$t" >/dev/null || MISSING+=("$t") +done +if [ ${#MISSING[@]} -gt 0 ]; then + err "Missing required tools: ${MISSING[*]}" + exit 1 +fi +echo "All tools present." +echo "node: $(node --version)" +echo "pnpm: $(pnpm --version)" + +# ========================== Step 3: Detect version ========================== +note "Step 3 — Detect version" + +CURRENT_VERSION=$(read_version) +if [ -z "$CURRENT_VERSION" ]; then + err "Could not read version from package.json." + exit 1 +fi +# main carries `<release>-dev` while the next release is in flight. The +# release version is the bare semver — strip the `-dev` (also `-SNAPSHOT` +# for parity with the upstream skywalking convention). +RELEASE_VERSION="${CURRENT_VERSION%-dev}" +RELEASE_VERSION="${RELEASE_VERSION%-SNAPSHOT}" +if [ "${CURRENT_VERSION}" = "${RELEASE_VERSION}" ]; then + err "package.json version '${CURRENT_VERSION}' has no '-dev' / '-SNAPSHOT' suffix." + err "main should carry the dev-suffixed version between releases — bump it before running this script." + exit 1 +fi +MAJOR=$(echo "$RELEASE_VERSION" | cut -d. -f1) +MINOR=$(echo "$RELEASE_VERSION" | cut -d. -f2) +NEXT_MINOR=$((MINOR + 1)) +NEXT_RELEASE_VERSION="${MAJOR}.${NEXT_MINOR}.0" + +echo "Current (in package.json): ${CURRENT_VERSION}" +echo "Release: ${RELEASE_VERSION}" +echo "Next dev: ${NEXT_RELEASE_VERSION}-dev" +if ! confirm "Are these correct?"; then + read -r -p "Enter release version: " RELEASE_VERSION + read -r -p "Enter next version (without -dev suffix): " NEXT_RELEASE_VERSION +fi +TAG="v${RELEASE_VERSION}" + +# ========================== Step 4: Version-consistency check ========================== +note "Step 4 — Version consistency check" + +CONSISTENT=true +check_file_has_version() { + local file="$1"; local needle="$2" + if ! file_has "${PROJECT_DIR}/${file}" "$needle"; then + err "${file} is missing expected token: ${needle}" + CONSISTENT=false + fi +} +# Every code-side marker carries the dev-suffixed version (`-dev`) on +# main between releases. Strip-and-tag happens later in the clone, NOT +# in the local working tree. +for pj in package.json packages/api-client/package.json packages/design-tokens/package.json \ + packages/templates/package.json apps/bff/package.json apps/ui/package.json; do + check_file_has_version "$pj" "\"version\": \"${CURRENT_VERSION}\"" +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. +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 ! $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}." + +# ========================== Step 5: Doc + Changelog check ========================== +note "Step 5 — Docs + CHANGELOG check" + +if ! grep -q "^## ${RELEASE_VERSION}$" "${PROJECT_DIR}/CHANGELOG.md"; then + err "CHANGELOG.md has no '## ${RELEASE_VERSION}' section heading." + exit 1 +fi +# Reject the placeholder body. Operators MUST fill the section in before +# casting a vote — a stub CHANGELOG line in a release tarball is a +# review smell. +if awk -v v="${RELEASE_VERSION}" ' + $0 == "## " v { in_sec=1; next } + in_sec && /^## / { in_sec=0 } + in_sec { print } +' "${PROJECT_DIR}/CHANGELOG.md" | grep -q "In development"; then + err "CHANGELOG.md ${RELEASE_VERSION} section still contains the '(In development …)' placeholder. Fill it in." + exit 1 +fi +echo "CHANGELOG.md has a non-placeholder section for ${RELEASE_VERSION}." + +# Make sure LICENSE / NOTICE exist at the repo root (they ship in src+bin tarballs). +for f in LICENSE NOTICE HEADER; do + [ -f "${PROJECT_DIR}/${f}" ] || { err "${f} missing at repo root."; exit 1; } +done +echo "LICENSE / NOTICE / HEADER present." + +# ========================== Step 6: License-header check ========================== +note "Step 6 — License-header check (license-eye)" + +(cd "${PROJECT_DIR}" && license-eye -c .licenserc.yaml header check) +echo "License headers OK." + +# ========================== Step 7: Clone fresh ========================== +note "Step 7 — Clone fresh repo" + +rm -rf "${WORK_DIR}" +mkdir -p "${WORK_DIR}" +echo "Cloning ${REPO_URL} (branch: ${REPO_BRANCH}) into ${CLONE_DIR}…" +git clone --depth 1 --branch "${REPO_BRANCH}" "${REPO_URL}" "${CLONE_DIR}" + +CLONE_VERSION=$(node -e "process.stdout.write(JSON.parse(require('fs').readFileSync('${CLONE_DIR}/package.json','utf8')).version)") +if [ "${CLONE_VERSION}" != "${CURRENT_VERSION}" ]; then + err "Fresh clone has version ${CLONE_VERSION}, expected ${CURRENT_VERSION} (the dev-suffixed version on ${REPO_BRANCH})." + exit 1 +fi + +# ========================== Step 8: Strip -dev, advance docs, commit, tag ========================== +note "Step 8 — Prepare release commit + tag ${TAG}" + +cd "${CLONE_DIR}" + +# Strip the -dev suffix on every code marker in the clone. The committed +# release-tagged commit must carry the bare semver. +node -e " +const fs = require('fs'); +const files = [ + 'package.json', + 'packages/api-client/package.json', + 'packages/design-tokens/package.json', + 'packages/templates/package.json', + 'apps/bff/package.json', + 'apps/ui/package.json', +]; +for (const f of files) { + const j = JSON.parse(fs.readFileSync(f, 'utf8')); + j.version = '${RELEASE_VERSION}'; + fs.writeFileSync(f, JSON.stringify(j, null, 2) + '\n'); +} +" +sed -i.bak "s/'${CURRENT_VERSION}'/'${RELEASE_VERSION}'/g" apps/bff/src/server.ts +rm apps/bff/src/server.ts.bak + +# Advance docs from the prior release tag to the new one so the image +# tag references in the release tarball match the release being cut. +sed -i.bak "s|ghcr.io/apache/skywalking-horizon-ui:${PRIOR_RELEASE}|ghcr.io/apache/skywalking-horizon-ui:${RELEASE_VERSION}|g" docs/setup/container-image.md +rm docs/setup/container-image.md.bak + +git add package.json packages/*/package.json apps/*/package.json apps/bff/src/server.ts docs/setup/container-image.md +git commit -m "Prepare release ${RELEASE_VERSION}" + +if git ls-remote --tags origin | grep -q "refs/tags/${TAG}$"; then + err "Tag ${TAG} already exists on origin. Delete it first if you need to re-cut, or pick a new version." + exit 1 +fi +git tag "${TAG}" +git push origin HEAD:"${REPO_BRANCH}" "${TAG}" +echo "Pushed release commit + tag ${TAG}." + +# ========================== Step 9: Build source tarball ========================== +note "Step 9 — Build source tarball" + +SRC_TAR="${WORK_DIR}/${PRODUCT_NAME}-${RELEASE_VERSION}-src.tar.gz" +# A "source release" is the canonical Apache artifact. It must be buildable +# from scratch — no node_modules, no dist, no editor leftovers. +tar -C "${WORK_DIR}" \ + --exclude='skywalking-horizon-ui/.git' \ + --exclude='skywalking-horizon-ui/.github/workflows/publish-image.yaml' \ + --exclude='skywalking-horizon-ui/node_modules' \ + --exclude='skywalking-horizon-ui/**/node_modules' \ + --exclude='skywalking-horizon-ui/dist' \ + --exclude='skywalking-horizon-ui/**/dist' \ + --exclude='skywalking-horizon-ui/_deploy_tmp' \ + --exclude='skywalking-horizon-ui/.DS_Store' \ + --exclude='skywalking-horizon-ui/**/.DS_Store' \ + --exclude='skywalking-horizon-ui/.release-work' \ + --transform "s,^skywalking-horizon-ui,${PRODUCT_NAME}-${RELEASE_VERSION}-src," \ + -czf "${SRC_TAR}" \ + skywalking-horizon-ui + +echo "Source tarball: ${SRC_TAR}" + +# ========================== Step 10: Build binary (self-contained) ========================== +note "Step 10 — Build binary tarball (self-contained, no install/network at boot)" + +cd "${CLONE_DIR}" +pnpm install --frozen-lockfile +pnpm package +# The packager left dist/server.js + dist/node_modules + dist/static + … +# Now layer in LICENSE/NOTICE + per-dep license texts. +node "${CLONE_DIR}/scripts/collect-dist-licenses.mjs" +node "${CLONE_DIR}/scripts/check-dist-licenses.mjs" + +# Stage the binary contents under a clean folder name so the tar root +# entry matches the artifact name. Copy in the operator-facing docs too. +BIN_STAGE="${WORK_DIR}/${PRODUCT_NAME}-${RELEASE_VERSION}-bin" +rm -rf "${BIN_STAGE}" +cp -R "${CLONE_DIR}/dist" "${BIN_STAGE}" +cp "${CLONE_DIR}/CHANGELOG.md" "${BIN_STAGE}/CHANGELOG.md" +cp "${CLONE_DIR}/README.md" "${BIN_STAGE}/README.md" +# dist/LICENSE and dist/NOTICE were just generated by the collector and +# are the BINARY-flavored versions (Apache-2.0 + bundled-dep summary + +# pass-through NOTICEs). The repo-root LICENSE/NOTICE — source-flavored — +# stay in the source tarball only. Do NOT overwrite. + +BIN_TAR="${WORK_DIR}/${PRODUCT_NAME}-${RELEASE_VERSION}-bin.tar.gz" +tar -C "${WORK_DIR}" -czf "${BIN_TAR}" "${PRODUCT_NAME}-${RELEASE_VERSION}-bin" + +echo "Binary tarball: ${BIN_TAR}" + +# ========================== Step 11: Compare LICENSE/NOTICE in tarballs ========================== +note "Step 11 — Verify LICENSE/NOTICE in src + bin tarballs" + +# Both tarballs must carry a LICENSE and NOTICE at their root, and the +# binary version must be the expanded one (contains the "Subcomponents" +# section the collector appends). The source version must NOT contain +# that section — a bundled-dep summary on a source-only tarball would +# be a wire-shape lie. +src_license=$(tar -tzf "${SRC_TAR}" | grep -E "^${PRODUCT_NAME}-${RELEASE_VERSION}-src/LICENSE$" || true) +src_notice=$( tar -tzf "${SRC_TAR}" | grep -E "^${PRODUCT_NAME}-${RELEASE_VERSION}-src/NOTICE$" || true) +bin_license=$(tar -tzf "${BIN_TAR}" | grep -E "^${PRODUCT_NAME}-${RELEASE_VERSION}-bin/LICENSE$" || true) +bin_notice=$( tar -tzf "${BIN_TAR}" | grep -E "^${PRODUCT_NAME}-${RELEASE_VERSION}-bin/NOTICE$" || true) +[ -n "$src_license" ] || { err "Source tarball missing LICENSE"; exit 1; } +[ -n "$src_notice" ] || { err "Source tarball missing NOTICE"; exit 1; } +[ -n "$bin_license" ] || { err "Binary tarball missing LICENSE"; exit 1; } +[ -n "$bin_notice" ] || { err "Binary tarball missing NOTICE"; exit 1; } + +src_lic_text=$(tar -xzf "${SRC_TAR}" -O "${PRODUCT_NAME}-${RELEASE_VERSION}-src/LICENSE") +bin_lic_text=$(tar -xzf "${BIN_TAR}" -O "${PRODUCT_NAME}-${RELEASE_VERSION}-bin/LICENSE") +if echo "$src_lic_text" | grep -qE 'Horizon UI Subcomponents'; then + err "Source LICENSE unexpectedly contains 'Horizon UI Subcomponents' — that section belongs in the binary tarball only." + exit 1 +fi +if ! echo "$bin_lic_text" | grep -qE 'Horizon UI Subcomponents'; then + err "Binary LICENSE missing 'Horizon UI Subcomponents' — collector did not run." + exit 1 +fi + +echo "src LICENSE sha512: $(echo "$src_lic_text" | shasum -a 512 | cut -d' ' -f1)" +echo "bin LICENSE sha512: $(echo "$bin_lic_text" | shasum -a 512 | cut -d' ' -f1)" + +# ========================== Step 12: GPG sign + sha512 ========================== +note "Step 12 — GPG sign + sha512" + +cd "${WORK_DIR}" +for t in "${SRC_TAR}" "${BIN_TAR}"; do + gpg --armor --detach-sig "${t}" + shasum -a 512 "$(basename "${t}")" > "${t}.sha512" +done + +echo "Artifacts:" +ls -lh "${SRC_TAR}" "${SRC_TAR}.asc" "${SRC_TAR}.sha512" \ + "${BIN_TAR}" "${BIN_TAR}.asc" "${BIN_TAR}.sha512" + +# Verify signatures locally before publishing. +gpg --verify "${SRC_TAR}.asc" "${SRC_TAR}" +gpg --verify "${BIN_TAR}.asc" "${BIN_TAR}" +shasum -a 512 -c "${SRC_TAR}.sha512" +shasum -a 512 -c "${BIN_TAR}.sha512" +echo "Self-verify OK." + +# ========================== Step 13: SVN upload ========================== +note "Step 13 — Upload to ${SVN_DEV_URL}/${RELEASE_VERSION}" + +read -r -p "Apache SVN username: " SVN_USER +read -r -s -p "Apache SVN password: " SVN_PASS +echo "" + +SVN_STAGE="${WORK_DIR}/svn-staging" +rm -rf "${SVN_STAGE}" +svn co --depth empty --username "${SVN_USER}" --password "${SVN_PASS}" \ + --non-interactive --no-auth-cache \ + "${SVN_DEV_URL}" "${SVN_STAGE}" + +SVN_VERSION_DIR="${SVN_STAGE}/${RELEASE_VERSION}" +if svn ls --username "${SVN_USER}" --password "${SVN_PASS}" --non-interactive --no-auth-cache \ + "${SVN_DEV_URL}/${RELEASE_VERSION}" >/dev/null 2>&1; then + echo "Version folder exists on SVN. Updating in place." + svn update --username "${SVN_USER}" --password "${SVN_PASS}" --non-interactive --no-auth-cache \ + --set-depth infinity "${SVN_VERSION_DIR}" +else + mkdir -p "${SVN_VERSION_DIR}" + (cd "${SVN_STAGE}" && svn add "${RELEASE_VERSION}") +fi + +cp "${SRC_TAR}" "${SRC_TAR}.asc" "${SRC_TAR}.sha512" "${SVN_VERSION_DIR}/" +cp "${BIN_TAR}" "${BIN_TAR}.asc" "${BIN_TAR}.sha512" "${SVN_VERSION_DIR}/" + +(cd "${SVN_STAGE}" && svn add --force "${RELEASE_VERSION}") +(cd "${SVN_STAGE}" && svn commit \ + --username "${SVN_USER}" --password "${SVN_PASS}" \ + --non-interactive --no-auth-cache \ + -m "Upload Apache SkyWalking Horizon UI ${RELEASE_VERSION} release candidate") + +echo "Uploaded: ${SVN_DEV_URL}/${RELEASE_VERSION}" +unset SVN_PASS + +# ========================== Step 14: Vote email ========================== +note "Step 14 — Vote email" + +SRC_SHA512=$(cat "${SRC_TAR}.sha512") +BIN_SHA512=$(cat "${BIN_TAR}.sha512") +VOTE_DATE=$(date +"%B %d, %Y") +RELEASE_COMMIT=$(git -C "${CLONE_DIR}" rev-parse "${TAG}") + +cat <<EOF + +======================================================================== +Vote Email — copy and send to [email protected] +======================================================================== + +Subject: [VOTE] Release Apache SkyWalking Horizon UI version ${RELEASE_VERSION} + +Hi All, + +This is a call for vote to release Apache SkyWalking Horizon UI +version ${RELEASE_VERSION}. + +Release notes: + + * https://github.com/apache/skywalking-horizon-ui/blob/${TAG}/CHANGELOG.md + +Release Candidate: + + * ${SVN_DEV_URL}/${RELEASE_VERSION} + * sha512 checksums + - ${SRC_SHA512} + - ${BIN_SHA512} + +Release Tag: + + * (Git Tag) ${TAG} + +Release CommitID: + + * https://github.com/apache/skywalking-horizon-ui/tree/${TAG} + * SHA: ${RELEASE_COMMIT} + +Keys to verify the Release Candidate: + + * https://dist.apache.org/repos/dist/release/skywalking/KEYS + +Guide to build the release from source: + + * Extract apache-skywalking-horizon-ui-${RELEASE_VERSION}-src.tar.gz + * cd into the extracted directory + * pnpm install --frozen-lockfile + * pnpm package + * node dist/server.js (after copying horizon.example.yaml → horizon.yaml) + +Voting will start now (${VOTE_DATE}) and will remain open for at least +72 hours. PMC members, please cast your vote. + +[ ] +1 Release this package. +[ ] +0 No opinion. +[ ] -1 Do not release this package because … + +======================================================================== +EOF + +# ========================== Step 15: Prepare next version ========================== +note "Step 15 — Prepare next-version (${NEXT_RELEASE_VERSION}) PR" + +if ! confirm "Push next-version PR (${NEXT_RELEASE_VERSION}) now?"; then + echo "Skipping next-version PR. Release artifacts are in ${WORK_DIR}/." + exit 0 +fi + +cd "${CLONE_DIR}" +git checkout -b "prepare-next-${NEXT_RELEASE_VERSION}" + +# Bump every code marker to the next dev-suffixed version. +NEXT_DEV_VERSION="${NEXT_RELEASE_VERSION}-dev" +node -e " +const fs = require('fs'); +const files = [ + 'package.json', + 'packages/api-client/package.json', + 'packages/design-tokens/package.json', + 'packages/templates/package.json', + 'apps/bff/package.json', + 'apps/ui/package.json', +]; +for (const f of files) { + const j = JSON.parse(fs.readFileSync(f, 'utf8')); + j.version = '${NEXT_DEV_VERSION}'; + fs.writeFileSync(f, JSON.stringify(j, null, 2) + '\n'); +} +" + +# server.ts default — keep in lock-step with HORIZON_VERSION. +sed -i.bak "s/'${RELEASE_VERSION}'/'${NEXT_DEV_VERSION}'/g" apps/bff/src/server.ts +rm apps/bff/src/server.ts.bak + +# Container-image docs already point at ${RELEASE_VERSION} (the release +# commit just bumped them). They stay there — docs always reference the +# last released tag, not the in-flight dev version. + +# Rotate CHANGELOG: insert a fresh placeholder at the top. +node -e " +const fs = require('fs'); +const path = 'CHANGELOG.md'; +const txt = fs.readFileSync(path, 'utf8'); +const insertion = '## ${NEXT_RELEASE_VERSION}\n\n(In development — fill in highlights here before cutting the release.)\n\n'; +// Insert above the first '## <prev>' heading. +const out = txt.replace(/^## /m, insertion + '## '); +fs.writeFileSync(path, out); +" + +git add package.json packages/*/package.json apps/*/package.json apps/bff/src/server.ts CHANGELOG.md +git commit -m "Prepare next release ${NEXT_DEV_VERSION}" +git push --set-upstream origin "prepare-next-${NEXT_RELEASE_VERSION}" + +gh pr create --title "Prepare next release ${NEXT_DEV_VERSION}" \ + --body "Bump every package version to ${NEXT_DEV_VERSION} and rotate CHANGELOG for the next development cycle after ${RELEASE_VERSION}." \ + --base "${REPO_BRANCH}" + +note "Done." +echo " Release version: ${RELEASE_VERSION}" +echo " Next dev version: ${NEXT_DEV_VERSION}" +echo " SVN dev staging: ${SVN_DEV_URL}/${RELEASE_VERSION}" +echo " Release tag: ${TAG}" +echo "" +echo "Next steps:" +echo " 1. Send the vote email above to [email protected]." +echo " 2. After the vote passes, run: svn mv ${SVN_DEV_URL}/${RELEASE_VERSION} \\" +echo " https://dist.apache.org/repos/dist/release/skywalking/horizon-ui/${RELEASE_VERSION}" +echo " 3. Merge the next-version PR."
