| #!/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)." |