This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch chore/release-automation in repository https://gitbox.apache.org/repos/asf/skywalking-nodejs.git
commit aefcab4cbc0d3fdaf57b7162d6a155fa013afa14 Author: Wu Sheng <[email protected]> AuthorDate: Tue Jun 23 16:20:17 2026 +0800 chore: add release automation scripts (release.sh + release-finalize.sh) Adapted from apache/skywalking-horizon-ui for skywalking-nodejs' single-package npm layout. They automate the manual steps in docs/How-to-release.md. - scripts/release.sh (npm run release): GPG/preflight checks (signer must have an @apache.org uid), fresh recursive clone, build + sign the source release via release-src, verify the tarball contents + signature, push the tag ONLY after artifacts are verified, upload the RC to svn dev, and print the [VOTE] email. - scripts/release-finalize.sh (npm run release:finalize): svn move dev->release (retiring only the strictly-older previous release), publish the GitHub release with auto-generated notes (re-verifying the voted bytes), and an optional, triple-gated npm publish (skipped if the version is already on npm). - package.json: wire release / release:finalize; pin the signer end-to-end via SW_GPG_KEY (gpg -u) in release-src; exclude .claude from the source tarball. - .gitignore: release working dirs + generated source artifacts. Co-Authored-By: Claude Opus 4.8 <[email protected]> --- .gitignore | 7 + package.json | 6 +- scripts/release-finalize.sh | 244 ++++++++++++++++++++++++++++++ scripts/release.sh | 358 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 613 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d6ef811..e9bf187 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ lib src/proto/ *.log tsconfig.tsbuildinfo + +# Release tooling working dirs + generated source artifacts (scripts/release*.sh) +scripts/.release-work/ +scripts/.finalize-work/ +skywalking-nodejs-src-*.tgz +skywalking-nodejs-src-*.tgz.asc +skywalking-nodejs-src-*.tgz.sha512 diff --git a/package.json b/package.json index 36f0289..2ee556e 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ "test": "DEBUG=testcontainers* jest", "format": "prettier --write \"src/**/*.ts\"", "clean": "(rm -rf src/proto || true) && (rm -rf src/proto || true) && (rm -rf lib || true)", - "package-src": "touch skywalking-nodejs-src-$npm_package_version.tgz && tar -zcvf skywalking-nodejs-src-$npm_package_version.tgz --exclude bin --exclude .git --exclude .idea --exclude .DS_Store --exclude .github --exclude node_modules --exclude skywalking-nodejs-src-$npm_package_version.tgz .", - "release-src": "npm run prepare && npm run package-src && gpg --batch --yes --armor --detach-sig skywalking-nodejs-src-$npm_package_version.tgz && shasum -a 512 skywalking-nodejs-src-$npm_package_version.tgz > skywalking-nodejs-src-$npm_package_version.tgz.sha512" + "package-src": "touch skywalking-nodejs-src-$npm_package_version.tgz && tar -zcvf skywalking-nodejs-src-$npm_package_version.tgz --exclude bin --exclude .git --exclude .idea --exclude .DS_Store --exclude .github --exclude .claude --exclude node_modules --exclude skywalking-nodejs-src-$npm_package_version.tgz .", + "release-src": "npm run prepare && npm run package-src && gpg --batch --yes ${SW_GPG_KEY:+-u $SW_GPG_KEY} --armor --detach-sig skywalking-nodejs-src-$npm_package_version.tgz && shasum -a 512 skywalking-nodejs-src-$npm_package_version.tgz > skywalking-nodejs-src-$npm_package_version.tgz.sha512", + "release": "bash scripts/release.sh", + "release:finalize": "bash scripts/release-finalize.sh" }, "files": [ "lib/**/*" diff --git a/scripts/release-finalize.sh b/scripts/release-finalize.sh new file mode 100755 index 0000000..69f8ea4 --- /dev/null +++ b/scripts/release-finalize.sh @@ -0,0 +1,244 @@ +#!/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 NodeJS — POST-VOTE release finalization. +# +# Run AFTER the [VOTE] on [email protected] passes (>=72h, >=3 +# binding +1, more +1 than -1). Second half of the flow; scripts/release.sh +# is the first half. +# +# In order: +# 1. Promote on svn: server-side move dev/<v>/ -> release/<v>/, then remove +# the PREVIOUS (strictly-older) release (auto-archived to archive.apache.org). +# 2. Cut/publish the GitHub release on tag v<v> with auto-generated notes, +# attaching the SAME voted bytes fetched back from svn release (checksum +# AND signature verified before attaching). +# 3. (optional, IRREVERSIBLE) publish skywalking-backend-js@<v> to npm, +# built from a fresh clone of the tag. +# +# Every irreversible step confirms first. Run on a single-user trusted host. +# +# Usage: bash scripts/release-finalize.sh + +set -e -o pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROJECT_DIR=$(cd "${SCRIPT_DIR}/.." && pwd) +GH_REPO="apache/skywalking-nodejs" +NPM_PACKAGE="skywalking-backend-js" +REPO_URL="${SW_RELEASE_REPO_URL:-https://github.com/apache/skywalking-nodejs.git}" +SVN_DEV_URL="https://dist.apache.org/repos/dist/dev/skywalking/node-js" +SVN_RELEASE_URL="https://dist.apache.org/repos/dist/release/skywalking/node-js" +KEYS_URL="https://dist.apache.org/repos/dist/release/skywalking/KEYS" +WORK_DIR="${SCRIPT_DIR}/.finalize-work" + +# ========================== Helpers ========================== + +err() { echo "ERROR: $*" >&2; } +note() { echo ""; echo "=== $* ==="; } + +confirm() { + local prompt="$1" ans + read -r -p "${prompt} [y/N] " ans || { err "No input (no TTY?)."; exit 1; } + [[ "$ans" == "y" || "$ans" == "Y" ]] +} + +ask() { + local prompt="$1" val + read -r -p "${prompt}: " val || { err "No input for '${prompt}' (no TTY?)."; exit 1; } + [ -n "$val" ] || { err "'${prompt}' must not be empty."; exit 1; } + printf '%s' "$val" +} + +svn_exists() { svn ls "$1" >/dev/null 2>&1; } + +# ========================== Step 1: Preflight ========================== +note "Step 1 — Tool + auth preflight" + +MISSING=() +for t in svn gh git node npm gpg shasum curl; do + command -v "$t" >/dev/null || MISSING+=("$t") +done +if [ ${#MISSING[@]} -gt 0 ]; then err "Missing required tools: ${MISSING[*]}"; exit 1; fi +gh auth status >/dev/null 2>&1 || { err "gh is not authenticated. Run: gh auth login"; exit 1; } +# Fetch tags up front so version auto-detect (Step 2) sees the RC tag even on +# a checkout that never pulled it. +(cd "${PROJECT_DIR}" && git fetch --tags --quiet origin) || err "git fetch --tags failed (continuing with local tags)." +echo "gh authenticated; tools present." + +# ========================== 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 || { err "No input (no TTY?)."; exit 1; } +RELEASE_VERSION="${RELEASE_VERSION:-${DETECTED}}" +[ -n "${RELEASE_VERSION}" ] || { err "No release version provided."; exit 1; } +TAG="v${RELEASE_VERSION}" +SRC_TGZ="skywalking-nodejs-src-${RELEASE_VERSION}.tgz" + +# The tag MUST exist locally (we fetched in Step 1) — fail loudly, not later +# inside an opaque gh error. +(cd "${PROJECT_DIR}" && git rev-parse "${TAG}" >/dev/null 2>&1) || { + err "Tag ${TAG} not found locally or on origin. Did scripts/release.sh push it?"; exit 1; } +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}/" + +# NOTE: svn takes the password on argv; run only on a trusted host, never with set -x. +SVN_USER=$(ask "Apache SVN username") +read -r -s -p "Apache SVN password: " SVN_PASS || { err "No SVN password (no TTY?)."; exit 1; } +echo "" +[ -n "$SVN_PASS" ] || { err "SVN password must not be empty."; exit 1; } +SVN_AUTH=(--username "${SVN_USER}" --password "${SVN_PASS}" --non-interactive --no-auth-cache) + +if ! svn ls "${SVN_AUTH[@]}" "${SVN_DEV_URL}/${RELEASE_VERSION}" >/dev/null 2>&1; then + err "Release candidate not found at ${SVN_DEV_URL}/${RELEASE_VERSION}/. Did scripts/release.sh upload it?" + exit 1 +fi + +if svn ls "${SVN_AUTH[@]}" "${SVN_RELEASE_URL}/${RELEASE_VERSION}" >/dev/null 2>&1; then + echo "Already present at release/${RELEASE_VERSION} — skipping the move (idempotent)." +else + if ! svn ls "${SVN_AUTH[@]}" "${SVN_RELEASE_URL}" >/dev/null 2>&1; then + echo "Creating ${SVN_RELEASE_URL}/ …" + svn mkdir --parents "${SVN_AUTH[@]}" -m "Create SkyWalking NodeJS release directory" "${SVN_RELEASE_URL}" + fi + if confirm "Run the server-side svn mv now? (PMC-only action)"; then + svn mv "${SVN_AUTH[@]}" -m "Release Apache SkyWalking-NodeJS ${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 ONLY a strictly-older previous release (never the one being released, +# never a newer one). ASF keeps only the current release live; older versions +# stay downloadable from archive.apache.org. +PREV_RELEASE=$(svn ls "${SVN_AUTH[@]}" "${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 + if printf '%s\n%s\n' "${PREV_RELEASE}" "${RELEASE_VERSION}" \ + | sort -t. -k1,1n -k2,2n -k3,3n -C 2>/dev/null && [ "${PREV_RELEASE}" != "${RELEASE_VERSION}" ]; then + echo "Previous release to retire: ${SVN_RELEASE_URL}/${PREV_RELEASE}/" + read -r -p "To remove it, type the version '${PREV_RELEASE}' exactly (blank = keep): " TYPED || TYPED="" + if [ "${TYPED}" = "${PREV_RELEASE}" ]; then + svn rm "${SVN_AUTH[@]}" -m "Remove superseded release ${PREV_RELEASE} (archived)" \ + "${SVN_RELEASE_URL}/${PREV_RELEASE}" + echo "Removed release/${PREV_RELEASE}/." + else + echo "Kept release/${PREV_RELEASE}/." + fi + else + echo "WARN: latest other release ${PREV_RELEASE} is not strictly older than ${RELEASE_VERSION}; not removing anything." + fi +fi +unset SVN_PASS; unset SVN_AUTH +echo "Allow ~a few minutes for mirror propagation to downloads.apache.org before updating website links." + +# ========================== Step 4: GitHub release ========================== +note "Step 4 — GitHub release ${TAG}" + +# Fetch the VOTED bytes back from svn 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}" +for f in "${SRC_TGZ}" "${SRC_TGZ}.asc" "${SRC_TGZ}.sha512"; do + echo "Fetching ${f}…" + curl -fSL -o "${ART_DIR}/${f}" "${SVN_RELEASE_URL}/${RELEASE_VERSION}/${f}" +done +(cd "${ART_DIR}" && shasum -a 512 -c "${SRC_TGZ}.sha512") +# Re-verify the binding signature too (not just the checksum). +(cd "${ART_DIR}" && gpg --verify "${SRC_TGZ}.asc" "${SRC_TGZ}") \ + || { err "GPG signature verify failed on the fetched release artifact. Is the signing key in ${KEYS_URL}?"; exit 1; } +echo "Checksum + signature verified." + +if gh release view "${TAG}" --repo "${GH_REPO}" >/dev/null 2>&1; then + echo "GitHub release ${TAG} already exists." + if confirm "Publish it (clear draft, mark latest) and (re)upload the voted artifacts?"; then + gh release edit "${TAG}" --repo "${GH_REPO}" --draft=false --latest + gh release upload "${TAG}" --repo "${GH_REPO}" --clobber \ + "${ART_DIR}/${SRC_TGZ}" "${ART_DIR}/${SRC_TGZ}.asc" "${ART_DIR}/${SRC_TGZ}.sha512" + echo "GitHub release published." + fi +else + PREV_TAG=$(cd "${PROJECT_DIR}" && git tag --list 'v*' --sort=-version:refname | grep -vx "${TAG}" | head -1) + if confirm "Create GitHub release ${TAG} (auto-notes since ${PREV_TAG:-<none>}) and attach the artifacts?"; then + gh release create "${TAG}" --repo "${GH_REPO}" \ + --title "Apache SkyWalking NodeJS ${RELEASE_VERSION}" \ + --generate-notes ${PREV_TAG:+--notes-start-tag "${PREV_TAG}"} --latest \ + "${ART_DIR}/${SRC_TGZ}" "${ART_DIR}/${SRC_TGZ}.asc" "${ART_DIR}/${SRC_TGZ}.sha512" + echo "GitHub release created." + fi +fi + +# ========================== Step 5: npm publish (optional, IRREVERSIBLE) ========================== +note "Step 5 — npm publish ${NPM_PACKAGE}@${RELEASE_VERSION} (optional, IRREVERSIBLE)" + +if npm view "${NPM_PACKAGE}@${RELEASE_VERSION}" version >/dev/null 2>&1; then + echo "${NPM_PACKAGE}@${RELEASE_VERSION} is already on npm — skipping publish (immutable)." +elif confirm "Publish ${NPM_PACKAGE}@${RELEASE_VERSION} to npm now? (binding artifact is the svn tarball; npm is a convenience)"; then + NPM_USER=$(npm whoami 2>/dev/null || true) + [ -n "${NPM_USER}" ] || { err "Not logged in to npm. Run: npm login"; exit 1; } + echo "npm user: ${NPM_USER}" + + # Build + publish from a FRESH clone of the tag so the published bytes + # match the released tag exactly (not your working tree). + PUB_DIR="${WORK_DIR}/publish-clone" + git clone --recurse-submodules --branch "${TAG}" "${REPO_URL}" "${PUB_DIR}" + cd "${PUB_DIR}" + npm install + npm run build + [ -f lib/index.js ] || { err "npm run build did not produce lib/index.js — aborting publish."; exit 1; } + PUB_VERSION=$(node -e "process.stdout.write(require('./package.json').version)") + [ "${PUB_VERSION}" = "${RELEASE_VERSION}" ] || { err "Clone version ${PUB_VERSION} != ${RELEASE_VERSION}."; exit 1; } + npm publish --dry-run + if confirm "Dry-run looks correct — run the REAL npm publish?"; then + if [ -n "${NPM_OTP:-}" ]; then npm publish --otp="${NPM_OTP}"; else npm publish; fi + echo "Published ${NPM_PACKAGE}@${RELEASE_VERSION}." + else + echo "Skipped real npm publish." + fi + cd "${PROJECT_DIR}" +else + echo "Skipped npm publish." +fi + +# ========================== Done ========================== +note "Done — ${RELEASE_VERSION} finalized" +echo " svn release: ${SVN_RELEASE_URL}/${RELEASE_VERSION}/" +echo " GitHub: https://github.com/${GH_REPO}/releases/tag/${TAG}" +echo " npm: https://www.npmjs.com/package/${NPM_PACKAGE}" +echo "" +echo "Remaining MANUAL steps:" +echo " 1. Website PR (apache/skywalking-website): add the release event, bump the" +echo " NodeJS Agent block in data/releases.yml and the docs pointer in data/docs.yml." +echo " 2. Send the [ANNOUNCE] email from your @apache.org address to" +echo " [email protected] and [email protected]." +echo "" +echo "Working files left in ${WORK_DIR}/ (safe to delete)." diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..4b99df4 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,358 @@ +#!/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 NodeJS — release-candidate automation. +# +# Adapted from apache/skywalking-horizon-ui scripts/release.sh for the +# single-package npm layout of skywalking-nodejs. Produces the Apache source +# release, stages it for the vote, and prints the [VOTE] email: +# +# skywalking-nodejs-src-<v>.tgz {.asc,.sha512} +# +# The post-vote half lives in scripts/release-finalize.sh. +# +# PRECONDITION: the version bump to <v> is ALREADY merged on master +# (package.json "version" == <v>). This script does NOT bump the version. +# It builds the tarball from a FRESH recursive clone of master so the voted +# bytes always match the tag, and — importantly — pushes the git tag ONLY +# AFTER the artifacts are built, signed, and self-verified, so a build +# failure never leaves a public, immutable release tag behind. +# +# Run interactively on a single-user trusted host (it reads your SVN +# password). Requires bash, but is written to work on macOS bash 3.2. +# +# Usage: bash scripts/release.sh + +set -e -o pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROJECT_DIR=$(cd "${SCRIPT_DIR}/.." && pwd) +REPO_URL="${SW_RELEASE_REPO_URL:-https://github.com/apache/skywalking-nodejs.git}" +REPO_BRANCH="${SW_RELEASE_BRANCH:-master}" +SVN_DEV_URL="https://dist.apache.org/repos/dist/dev/skywalking/node-js" +KEYS_URL="https://dist.apache.org/repos/dist/release/skywalking/KEYS" +WORK_DIR="${SCRIPT_DIR}/.release-work" +CLONE_DIR="${WORK_DIR}/skywalking-nodejs" + +TEST_FILE="" +# Always clean up the throwaway GPG test file, even on Ctrl-C / early exit. +trap 'rm -f "${TEST_FILE:-}" "${TEST_FILE:-}.asc"' EXIT + +# ========================== Helpers ========================== + +err() { echo "ERROR: $*" >&2; } +note() { echo ""; echo "=== $* ==="; } + +confirm() { + local prompt="$1" ans + read -r -p "${prompt} [y/N] " ans || { err "No input (no TTY?)."; exit 1; } + [[ "$ans" == "y" || "$ans" == "Y" ]] +} + +# Prompt for a required, non-empty value. $1=prompt var-name is echoed back via stdout. +ask() { + local prompt="$1" val + read -r -p "${prompt}: " val || { err "No input for '${prompt}' (no TTY?)."; exit 1; } + [ -n "$val" ] || { err "'${prompt}' must not be empty."; exit 1; } + printf '%s' "$val" +} + +# Read the package.json "version" without jq (runs on stock macOS / Alpine). +read_version() { + node -e "process.stdout.write(JSON.parse(require('fs').readFileSync('$1','utf8')).version)" +} + +# ========================== 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 + +# Match @apache.org against ANY uid of the SELECTED key (not just the first +# uid of a blind dump), and tolerate an empty result without aborting. +GPG_EMAILS=$(gpg --list-keys --with-colons "${GPG_KEY_ID}" 2>/dev/null | awk -F: '/^uid:/{print $10}' || true) +if ! printf '%s\n' "${GPG_EMAILS}" | grep -q '@apache\.org'; then + err "Key ${GPG_KEY_ID} has no @apache.org uid — Apache releases must be signed with an @apache.org key." + err "uids found: ${GPG_EMAILS:-<none>}" + exit 1 +fi +echo "GPG Key: ${GPG_KEY_ID}" +gpg --list-keys --keyid-format LONG "${GPG_KEY_ID}" | grep -E '^(pub|uid)' || true +echo "Reminder: this key MUST already be in ${KEYS_URL} — every voter verifies against it." +confirm "Is this the correct signer?" || { echo "Aborted."; exit 1; } + +# Pin the signer end-to-end: package.json's release-src honors SW_GPG_KEY +# (gpg -u), so the tarball is signed by THIS key, not gpg's default key. +export SW_GPG_KEY="${GPG_KEY_ID}" + +export GPG_TTY=$(tty || true) +echo "Verifying GPG signing works (you may be prompted for the passphrase)…" +TEST_FILE=$(mktemp); echo "test" > "${TEST_FILE}" +if ! gpg --batch --yes -u "${GPG_KEY_ID}" --armor --detach-sig "${TEST_FILE}" 2>/dev/null; then + err "GPG signing with ${GPG_KEY_ID} failed. Try: export GPG_TTY=\$(tty) / gpgconf --launch gpg-agent" + exit 1 +fi +rm -f "${TEST_FILE}" "${TEST_FILE}.asc"; TEST_FILE="" +echo "GPG signing OK (signer pinned to ${GPG_KEY_ID})." + +# ========================== Step 2: Required tools ========================== +note "Step 2 — Tool check" + +MISSING=() +for t in gpg svn shasum git gh node npm tar; do + command -v "$t" >/dev/null || MISSING+=("$t") +done +if [ ${#MISSING[@]} -gt 0 ]; then + err "Missing required tools: ${MISSING[*]}" + exit 1 +fi +HAVE_LICENSE_EYE=true +command -v license-eye >/dev/null || HAVE_LICENSE_EYE=false +echo "All required tools present. node: $(node --version) npm: $(npm --version)" +$HAVE_LICENSE_EYE || echo "NOTE: license-eye not installed — header check will be skipped (CI still enforces it)." + +# Node baseline: engines.node >=20, and grpc-tools' node-pre-gyp needs >=18.17. +NODE_MAJOR=$(node -e "process.stdout.write(String(process.versions.node.split('.')[0]))") +[[ "${NODE_MAJOR}" =~ ^[0-9]+$ ]] || { err "Could not parse Node major version ('${NODE_MAJOR}')."; exit 1; } +if [ "${NODE_MAJOR}" -lt 20 ]; then + err "Node ${NODE_MAJOR}.x is below the >=20 baseline; grpc-tools install may also fail. Use Node 20/22/24." + exit 1 +fi + +# ========================== Step 3: Detect version ========================== +note "Step 3 — Detect version" + +# skywalking-nodejs does NOT use a -dev suffix; master carries the release +# number, bumped in a prior PR. +RELEASE_VERSION=$(read_version "${PROJECT_DIR}/package.json") +[ -n "$RELEASE_VERSION" ] || { err "Could not read version from package.json."; exit 1; } +echo "Release version (from package.json): ${RELEASE_VERSION}" +confirm "Release this version?" || RELEASE_VERSION=$(ask "Enter the release version to cut") +TAG="v${RELEASE_VERSION}" + +# ========================== Step 4: Consistency check ========================== +note "Step 4 — Consistency check" + +for f in LICENSE NOTICE package.json; do + [ -f "${PROJECT_DIR}/${f}" ] || { err "${f} missing at repo root."; exit 1; } +done +echo "LICENSE / NOTICE / package.json present." +# Soft check: README's advertised baseline should not contradict engines.node. +if grep -qiE 'NodeJS *>= *([0-9]|1[0-9])\b' "${PROJECT_DIR}/README.md" 2>/dev/null && + ! grep -qiE 'NodeJS *>= *2[0-9]' "${PROJECT_DIR}/README.md" 2>/dev/null; then + echo "WARN: README.md advertises a Node baseline below 20 while engines.node is >=20. Fix before tagging." +fi + +# ========================== Step 5: License-header check ========================== +if $HAVE_LICENSE_EYE; then + note "Step 5 — License-header check (license-eye)" + (cd "${PROJECT_DIR}" && license-eye -c .licenserc.yaml header check) + echo "License headers OK." +else + note "Step 5 — License-header check SKIPPED (license-eye absent; CI enforces it)" +fi + +# ========================== Step 6: Clone fresh + tag LOCALLY ========================== +note "Step 6 — Fresh recursive clone + local tag ${TAG}" + +rm -rf "${WORK_DIR}"; mkdir -p "${WORK_DIR}" +# --recurse-submodules is MANDATORY: protocol/ holds the .proto sources; +# without it prepare->protoc.sh codegens nothing (Step 8 guards against this). +echo "Cloning ${REPO_URL} (branch ${REPO_BRANCH}) with submodules…" +git clone --recurse-submodules --branch "${REPO_BRANCH}" "${REPO_URL}" "${CLONE_DIR}" + +CLONE_VERSION=$(read_version "${CLONE_DIR}/package.json") +if [ "${CLONE_VERSION}" != "${RELEASE_VERSION}" ]; then + err "Fresh clone of ${REPO_BRANCH} has version ${CLONE_VERSION}, but you are releasing ${RELEASE_VERSION}." + err "Merge the version-bump PR (package.json -> ${RELEASE_VERSION}) into ${REPO_BRANCH} first." + exit 1 +fi + +cd "${CLONE_DIR}" +# Capture ls-remote success explicitly: a FAILED ls-remote must not be read +# as "tag absent" (which would let us re-create an existing release tag). +REMOTE_TAGS=$(git ls-remote --tags origin) || { err "git ls-remote origin failed; cannot verify ${TAG} is unused."; exit 1; } +if printf '%s\n' "${REMOTE_TAGS}" | grep -q "refs/tags/${TAG}$"; then + err "Tag ${TAG} already exists on origin. Delete it first to re-cut, or pick a new version." + exit 1 +fi +# Create the annotated tag LOCALLY only. It is pushed in Step 9, AFTER the +# artifacts are built + verified — never before. +git tag -a "${TAG}" -m "Release Apache SkyWalking-NodeJS ${RELEASE_VERSION}" +RELEASE_COMMIT=$(git rev-parse "${TAG}") +echo "Local tag ${TAG} created at ${RELEASE_COMMIT} (NOT pushed yet)." + +# ========================== Step 7: Build + sign source release ========================== +note "Step 7 — Build + sign source tarball (npm run release-src)" + +# `npm install` runs prepare->scripts/protoc.sh (grpc-tools protoc; on Apple +# Silicon under Rosetta). release-src then runs prepare again + package-src +# (tar) + gpg detached sig (signer pinned via SW_GPG_KEY) + sha512. +npm install +npm run release-src + +SRC_TGZ="skywalking-nodejs-src-${RELEASE_VERSION}.tgz" +for f in "${SRC_TGZ}" "${SRC_TGZ}.asc" "${SRC_TGZ}.sha512"; do + [ -f "${CLONE_DIR}/${f}" ] || { err "Expected artifact ${f} not produced by release-src."; exit 1; } + cp "${CLONE_DIR}/${f}" "${WORK_DIR}/" +done + +# ========================== Step 8: Verify the tarball ========================== +note "Step 8 — Verify artifact contents + signature" + +cd "${WORK_DIR}" +PROTO_COUNT=$(tar -tzf "${SRC_TGZ}" | grep -cE 'protocol/.*\.proto' || true) +[ "${PROTO_COUNT}" -gt 0 ] || { err "Tarball contains 0 protocol/*.proto — submodule was empty. Aborting."; exit 1; } +if ! tar -tzf "${SRC_TGZ}" | grep -qE '(^|/)LICENSE$'; then err "Tarball missing LICENSE."; exit 1; fi +if ! tar -tzf "${SRC_TGZ}" | grep -qE '(^|/)NOTICE$'; then err "Tarball missing NOTICE."; exit 1; fi +if tar -tzf "${SRC_TGZ}" | grep -q 'node_modules/'; then err "Tarball unexpectedly contains node_modules/."; exit 1; fi +echo "Contents OK: ${PROTO_COUNT} .proto files, LICENSE + NOTICE present, no node_modules." + +shasum -a 512 -c "${SRC_TGZ}.sha512" +gpg --verify "${SRC_TGZ}.asc" "${SRC_TGZ}" +echo "Checksum + signature self-verify OK." +echo "Artifacts:"; ls -lh "${WORK_DIR}/${SRC_TGZ}" "${WORK_DIR}/${SRC_TGZ}.asc" "${WORK_DIR}/${SRC_TGZ}.sha512" + +# ========================== Step 9: Push the tag (artifacts are good now) ========================== +note "Step 9 — Push tag ${TAG}" + +TAG_PUSHED=false +if confirm "Artifacts built + verified. Push tag ${TAG} to origin now? (needed before the vote)"; then + (cd "${CLONE_DIR}" && git push origin "${TAG}") + TAG_PUSHED=true + echo "Pushed ${TAG}." +else + echo "Tag ${TAG} NOT pushed. Push it (from ${CLONE_DIR}) before sending the vote email:" + echo " git -C ${CLONE_DIR} push origin ${TAG}" +fi + +# ========================== Step 10: Upload RC to svn dev ========================== +note "Step 10 — Upload RC to ${SVN_DEV_URL}/${RELEASE_VERSION}" + +UPLOADED=false +if confirm "Upload the release candidate to svn dev now?"; then + # NOTE: svn takes the password on argv (--password), so it is briefly + # visible in `ps`. Run this only on a single-user trusted host and never + # with `set -x`. The password is cleared from the environment below. + SVN_USER=$(ask "Apache SVN username") + read -r -s -p "Apache SVN password: " SVN_PASS || { err "No SVN password (no TTY?)."; exit 1; } + echo "" + [ -n "$SVN_PASS" ] || { err "SVN password must not be empty."; exit 1; } + SVN_AUTH=(--username "${SVN_USER}" --password "${SVN_PASS}" --non-interactive --no-auth-cache) + + SVN_STAGE="${WORK_DIR}/svn-staging" + rm -rf "${SVN_STAGE}" + svn co --depth empty "${SVN_AUTH[@]}" "${SVN_DEV_URL}" "${SVN_STAGE}" + SVN_VERSION_DIR="${SVN_STAGE}/${RELEASE_VERSION}" + if svn ls "${SVN_AUTH[@]}" "${SVN_DEV_URL}/${RELEASE_VERSION}" >/dev/null 2>&1; then + echo "Version folder exists on svn — updating in place." + svn update "${SVN_AUTH[@]}" --set-depth infinity "${SVN_VERSION_DIR}" + else + mkdir -p "${SVN_VERSION_DIR}" + fi + cp "${WORK_DIR}/${SRC_TGZ}" "${WORK_DIR}/${SRC_TGZ}.asc" "${WORK_DIR}/${SRC_TGZ}.sha512" "${SVN_VERSION_DIR}/" + (cd "${SVN_STAGE}" && svn add --force "${RELEASE_VERSION}" || true) + (cd "${SVN_STAGE}" && svn commit "${SVN_AUTH[@]}" -m "Draft Apache SkyWalking-NodeJS release ${RELEASE_VERSION}") + UPLOADED=true + echo "Uploaded: ${SVN_DEV_URL}/${RELEASE_VERSION}" + unset SVN_PASS; unset SVN_AUTH +else + echo "Skipped svn upload. Artifacts are in ${WORK_DIR}/." +fi + +# ========================== Step 11: Vote email ========================== +note "Step 11 — Vote email" + +if ! $TAG_PUSHED || ! $UPLOADED; then + echo "WARNING: tag pushed=${TAG_PUSHED}, RC uploaded=${UPLOADED}." + echo " Some links in the email below are DEAD until you push the tag and/or upload the RC." + echo "" +fi + +SRC_SHA512=$(cat "${WORK_DIR}/${SRC_TGZ}.sha512") +VOTE_DATE=$(LC_ALL=C date +"%B %d, %Y") + +cat <<EOF + +======================================================================== +Vote Email — copy and send to [email protected] +======================================================================== + +Subject: [VOTE] Release Apache SkyWalking NodeJS version ${RELEASE_VERSION} + +Hi the SkyWalking Community, + +This is a call for vote to release Apache SkyWalking NodeJS version ${RELEASE_VERSION}. + +Release notes: + + * https://github.com/apache/skywalking-nodejs/releases/tag/${TAG} + +Release Candidate: + + * ${SVN_DEV_URL}/${RELEASE_VERSION} + * sha512 checksums + - ${SRC_SHA512} + +Release Tag : + + * (Git Tag) ${TAG} + +Release Commit Hash : + + * https://github.com/apache/skywalking-nodejs/tree/${RELEASE_COMMIT} + +Keys to verify the Release Candidate : + + * ${KEYS_URL} + +Guide to build the release from source : + + * https://github.com/apache/skywalking-nodejs/blob/${TAG}/CONTRIBUTING.md#compiling-and-building + +Voting will start now (${VOTE_DATE}) and will remain open for at least 72 hours. +A release passes with at least 3 binding +1 (PMC) votes and more +1 than -1. + +[ ] +1 Release this package. +[ ] +0 No opinion. +[ ] -1 Do not release this package because.... + +Thanks. + +[1] https://github.com/apache/skywalking-nodejs/blob/master/docs/How-to-release.md#vote-check +======================================================================== +EOF + +note "Done — release candidate ${RELEASE_VERSION} staged" +echo " Tag: ${TAG} ($($TAG_PUSHED && echo pushed || echo 'NOT pushed — push it before voting'))" +echo " Artifacts: ${WORK_DIR}/${SRC_TGZ}{,.asc,.sha512}" +echo " svn dev staging: $($UPLOADED && echo "${SVN_DEV_URL}/${RELEASE_VERSION}" || echo 'NOT uploaded')" +echo "" +echo "Next steps:" +echo " 1. Draft the GitHub release notes (auto-generated; CHANGELOG.md is a stub):" +echo " gh release create ${TAG} --draft --generate-notes --verify-tag \\" +echo " --notes-start-tag <previous tag> --title \"Apache SkyWalking NodeJS ${RELEASE_VERSION}\"" +echo " 2. Send the [VOTE] email above to [email protected] (>=72h)." +echo " 3. After the vote passes, run: bash scripts/release-finalize.sh"
