blob: 82da6dad4865b44a0f22dac4b4303758e50f7bab [file]
#!/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 dev@skywalking.apache.org 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}"
# ---- npm-publish intent + auth, decided NOW (before any irreversible step) ----
# The npm publish is the last step (Step 5), but we decide whether you'll do it
# and verify `npm login` HERE, so a missing login fails fast — BEFORE the svn
# move (Step 3) and the GitHub release (Step 4), not after them.
WILL_PUBLISH_NPM=false
if npm view "${NPM_PACKAGE}@${RELEASE_VERSION}" version >/dev/null 2>&1; then
echo "npm: ${NPM_PACKAGE}@${RELEASE_VERSION} is already published — the npm step will be skipped."
elif confirm "Will you publish ${NPM_PACKAGE}@${RELEASE_VERSION} to npm in this run? (the binding artifact is the svn tarball; npm is a convenience)"; then
NPM_USER=$(npm whoami 2>/dev/null || true)
[ -n "${NPM_USER}" ] || { err "You chose to publish to npm but are not logged in. Run 'npm login' first, then re-run."; exit 1; }
echo "npm user: ${NPM_USER} — will publish after the GitHub release."
WILL_PUBLISH_NPM=true
else
echo "npm publish will be skipped this run."
fi
# ========================== 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 "${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)"
# Intent + npm auth were already decided/verified in the preflight above.
if ! $WILL_PUBLISH_NPM; then
echo "Skipping npm publish (decided in preflight)."
else
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}"
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 " dev@skywalking.apache.org and announce@apache.org."
echo ""
echo "Working files left in ${WORK_DIR}/ (safe to delete)."