This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch chore/release-dev-versioning in repository https://gitbox.apache.org/repos/asf/skywalking-nodejs.git
commit 7cc9ecae2911106f80bf41263f4abc7b4f7851c1 Author: Wu Sheng <[email protected]> AuthorDate: Tue Jun 23 21:37:16 2026 +0800 feat(release): automate version dance (-dev) in release.sh, migrate to 0.9.0-dev release.sh now manages the version like apache/skywalking-horizon-ui: master carries a -dev version; the script cuts a release branch, strips -dev and commits + tags the release commit, builds + signs + verifies, pushes the tag ONLY after verify, adds a next-dev bump commit, and opens the release PR. - Step 3: derive release/next-dev from the -dev (or -SNAPSHOT) version; validate the manual-override path. - Step 6: install deps before committing so the tagged lockfile is the one the tarball ships; tag the commit and resolve RELEASE_COMMIT via ^{commit} (an annotated tag's object sha would 404 in the [VOTE] email). - Step 9: add the next-dev commit, push branch+tag with --atomic, open the PR. - New --dry-run / SW_RELEASE_DRY_RUN=1: full local build+sign+verify, zero remote mutations (no push, no svn, no PR) — for rehearsing against the apache repo. - Migrate package.json + lockfile 0.8.0 -> 0.9.0-dev (adopt the convention). - docs/How-to-release.md: describe the -dev flow + dry-run; fix the stale manual fallback and wording. Co-Authored-By: Claude Opus 4.8 <[email protected]> --- docs/How-to-release.md | 35 +++++++---- package-lock.json | 4 +- package.json | 2 +- scripts/release.sh | 156 +++++++++++++++++++++++++++++++++++++------------ 4 files changed, 146 insertions(+), 51 deletions(-) diff --git a/docs/How-to-release.md b/docs/How-to-release.md index fabe4be..c0d85ba 100644 --- a/docs/How-to-release.md +++ b/docs/How-to-release.md @@ -4,16 +4,22 @@ This documentation guides the release manager to release the SkyWalking NodeJS i ## Automated release (recommended) -Most of the steps below are automated by two scripts. Run them on a single-user trusted host, with your Apache GPG key (an `@apache.org` uid, already in the [KEYS](https://dist.apache.org/repos/dist/release/skywalking/KEYS) file) configured and Node >= 20: +`master` carries the in-flight dev version (e.g. `0.9.0-dev`), like SkyWalking's `-SNAPSHOT` convention. Two scripts automate the rest. Run them on a single-user trusted host, with your Apache GPG key (an `@apache.org` uid, already in the [KEYS](https://dist.apache.org/repos/dist/release/skywalking/KEYS) file) configured and Node >= 20: ```shell -# 1. Bump `version` in package.json to the release version and merge that PR first. -npm run release # GPG/preflight checks, fresh recursive clone, build + sign the source - # release, verify it, push the tag, upload the RC to svn dev, print the [VOTE] email -# 2. ... after the vote passes (open >= 72h, >= 3 binding +1, more +1 than -1) ... +npm run release # cut a release branch: strip -dev, commit + tag the release commit, + # build + sign the source release, verify it, push the tag, add a + # second commit bumping the branch to the next -dev, open the release PR, + # upload the RC to svn dev, and print the [VOTE] email +# ... after the vote passes (open >= 72h, >= 3 binding +1, more +1 than -1) ... npm run release:finalize # svn move dev -> release, publish the GitHub release, optionally publish to npm +# then merge the release PR opened by `npm run release` (master returns to -dev; the tag stays put). ``` +You do **not** bump the version by hand — `npm run release` strips the `-dev` suffix for the release commit and bumps the branch to the next dev version in the same PR (master returns to `-dev` when the PR merges). + +To rehearse the whole flow with **zero side effects** — a full local clone, strip, build, sign and verify, but **no** tag/branch push, **no** svn upload and **no** PR — run `npm run release -- --dry-run` (or `SW_RELEASE_DRY_RUN=1 npm run release`). + If you intend to publish to npm in `release:finalize`, run `npm login` first (you must be a maintainer of `skywalking-backend-js`). The script verifies npm auth **up front** — before the irreversible svn move — and auto-skips the npm step if that version is already published. The rest of this guide is the reference those scripts implement, and the fallback for running a step by hand. @@ -21,7 +27,7 @@ The rest of this guide is the reference those scripts implement, and the fallbac ## Prerequisites 1. Close (if finished, or move to next milestone otherwise) all issues in the current milestone from [skywalking-nodejs](https://github.com/apache/skywalking-nodejs/milestones) and [skywalking](https://github.com/apache/skywalking/milestones), create a new milestone for the next release. -1. Update `version` in [package.json](../package.json). (CHANGELOG.md is a stub — release notes are the auto-generated [GitHub Release](https://github.com/apache/skywalking-nodejs/releases) notes.) +1. The version is managed by `npm run release` (it strips `-dev` for the release commit and bumps `master` to the next `-dev`); you do not edit `package.json` by hand. CHANGELOG.md is a stub — release notes are the auto-generated [GitHub Release](https://github.com/apache/skywalking-nodejs/releases) notes. ## Add your GPG public key to Apache svn @@ -35,14 +41,21 @@ The rest of this guide is the reference those scripts implement, and the fallbac ## Build and sign the source code package +`npm run release` automates this. To do it by hand, mirror the script — strip `-dev`, build, and push the tag **only after** the build verifies (master carries `$VERSION-dev`): + ```shell -export VERSION=<the version to release> +export VERSION=<the release version, e.g. 0.9.0> # bare semver git clone --recurse-submodules [email protected]:apache/skywalking-nodejs && cd skywalking-nodejs -git tag -a "v$VERSION" -m "Release Apache SkyWalking-NodeJS $VERSION" -git push --tags - -npm install && npm run release-src +git checkout -b "prepare-release-$VERSION" +npm version "$VERSION" --no-git-tag-version # strip -dev: package.json + lockfile -> $VERSION +npm install +git commit -am "Prepare release $VERSION" +git tag -a "v$VERSION" -m "Release Apache SkyWalking-NodeJS $VERSION" # tag LOCALLY first + +npm run release-src # skywalking-nodejs-src-$VERSION.tgz{,.asc,.sha512} +# verify the tarball + signature, THEN push the tag (and branch, for the next-dev PR): +git push origin "v$VERSION" "prepare-release-$VERSION" ``` ## Upload to Apache svn diff --git a/package-lock.json b/package-lock.json index d8fdfa9..265683d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skywalking-backend-js", - "version": "0.8.0", + "version": "0.9.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skywalking-backend-js", - "version": "0.8.0", + "version": "0.9.0-dev", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 2ee556e..ca70182 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skywalking-backend-js", - "version": "0.8.0", + "version": "0.9.0-dev", "description": "The NodeJS agent for Apache SkyWalking", "homepage": "skywalking.apache.org", "main": "lib/index.js", diff --git a/scripts/release.sh b/scripts/release.sh index 4b99df4..56956dc 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -27,12 +27,14 @@ # # 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. +# VERSIONING: master carries the in-flight dev version (e.g. 0.9.0-dev), like +# SkyWalking's -SNAPSHOT / horizon-ui's -dev. In a FRESH recursive clone this +# script cuts a release branch, strips the -dev suffix and commits + tags that +# commit (the release candidate), then adds a second commit bumping back to the +# next dev version, and opens ONE PR carrying both. The tag is pushed ONLY AFTER +# the artifacts are built, signed and self-verified, so a build failure never +# leaves a public, immutable release tag behind. Merge the PR after the vote +# passes (master then returns to -dev with the release in its history). # # 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. @@ -45,11 +47,20 @@ 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}" +GH_REPO="${SW_RELEASE_GH_REPO:-apache/skywalking-nodejs}" 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" +# --dry-run / SW_RELEASE_DRY_RUN=1: run the full LOCAL pipeline (clone, strip -dev, +# build, sign, verify) but perform ZERO remote mutations — no tag/branch push, no +# svn upload, no PR. Lets a committer rehearse against the real repo with nothing to undo. +DRY_RUN=false +if [ "${1:-}" = "--dry-run" ] || [ -n "${SW_RELEASE_DRY_RUN:-}" ]; then + DRY_RUN=true +fi + 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 @@ -78,6 +89,8 @@ read_version() { node -e "process.stdout.write(JSON.parse(require('fs').readFileSync('$1','utf8')).version)" } +$DRY_RUN && note "DRY RUN — full local build + sign + verify, but NO tag/branch push, NO svn upload, NO PR" + # ========================== Step 1: GPG signer ========================== note "Step 1 — GPG signer check" @@ -144,13 +157,39 @@ 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") +# master carries the in-flight dev version (e.g. 0.9.0-dev). The release version +# is the bare semver; the next dev version bumps the minor (patch reset to 0). +CURRENT_VERSION=$(read_version "${PROJECT_DIR}/package.json") +[ -n "$CURRENT_VERSION" ] || { err "Could not read version from package.json."; exit 1; } +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 "master must carry the dev-suffixed version between releases; bump it first." + exit 1 +fi +if ! printf '%s' "${RELEASE_VERSION}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + err "Release version '${RELEASE_VERSION}' (from '${CURRENT_VERSION}') is not X.Y.Z."; exit 1 +fi +MAJOR=$(printf '%s' "${RELEASE_VERSION}" | cut -d. -f1) +MINOR=$(printf '%s' "${RELEASE_VERSION}" | cut -d. -f2) +NEXT_DEV_VERSION="${MAJOR}.$((MINOR + 1)).0-dev" + +echo "Current (master): ${CURRENT_VERSION}" +echo "Release: ${RELEASE_VERSION}" +echo "Next dev: ${NEXT_DEV_VERSION}" +if ! confirm "Are these correct?"; then + RELEASE_VERSION=$(ask "Enter the release version (X.Y.Z)") + NEXT_DEV_VERSION=$(ask "Enter the next dev version (e.g. 0.10.0-dev)") + printf '%s' "${RELEASE_VERSION}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' \ + || { err "Release version must be X.Y.Z (got '${RELEASE_VERSION}')."; exit 1; } + printf '%s' "${NEXT_DEV_VERSION}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+-dev$' \ + || { err "Next dev version must be X.Y.Z-dev (got '${NEXT_DEV_VERSION}')."; exit 1; } + [ "${NEXT_DEV_VERSION}" != "${RELEASE_VERSION}" ] \ + || { err "Next dev version must differ from the release version."; exit 1; } +fi TAG="v${RELEASE_VERSION}" +RELEASE_BRANCH="prepare-release-${RELEASE_VERSION}" # ========================== Step 4: Consistency check ========================== note "Step 4 — Consistency check" @@ -174,8 +213,8 @@ 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}" +# ========================== Step 6: Clone fresh, strip -dev, commit + tag LOCALLY ========================== +note "Step 6 — Fresh clone, strip -dev, commit + local tag ${TAG}" rm -rf "${WORK_DIR}"; mkdir -p "${WORK_DIR}" # --recurse-submodules is MANDATORY: protocol/ holds the .proto sources; @@ -184,33 +223,50 @@ 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." +if [ "${CLONE_VERSION}" != "${CURRENT_VERSION}" ]; then + err "Fresh clone of ${REPO_BRANCH} has version ${CLONE_VERSION}, expected ${CURRENT_VERSION}." + err "Re-run after ${REPO_BRANCH} settles, or fix its package.json version." 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). +# as "tag/branch absent" (which would let us re-create an existing one). 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. +REMOTE_HEADS=$(git ls-remote --heads origin "${RELEASE_BRANCH}") || { err "git ls-remote origin failed."; exit 1; } +if [ -n "${REMOTE_HEADS}" ]; then + err "Branch ${RELEASE_BRANCH} already exists on origin. Close/delete it first to re-cut." + exit 1 +fi + +# The release commit lands via PR (master is protected). Cut a dedicated branch; +# the tag points at the version-strip commit, and the next-dev bump is added as a +# second commit on the SAME branch in Step 9 — so one PR carries both. +git checkout -q -b "${RELEASE_BRANCH}" +npm version "${RELEASE_VERSION}" --no-git-tag-version --allow-same-version >/dev/null +# Install deps BEFORE committing so the committed (and tagged) lockfile is exactly +# the one the tarball ships — no post-build drift. This also runs prepare->protoc.sh +# (needs the protocol/ submodule + grpc-tools; on Apple Silicon protoc runs via Rosetta). +npm install +git add package.json package-lock.json +git commit -q --no-verify -m "Prepare release ${RELEASE_VERSION}" +# Tag the version-strip commit LOCALLY only. It is pushed in Step 9, AFTER the +# artifacts are built + verified — never before. Use ^{commit} so RELEASE_COMMIT is +# the COMMIT sha (an annotated tag's own object sha would 404 in the vote email). 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)." +RELEASE_COMMIT=$(git rev-parse "${TAG}^{commit}") +echo "Branch ${RELEASE_BRANCH} + local tag ${TAG} 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 +# Deps were installed in Step 6 (before the commit). release-src runs prepare +# (protoc) + package-src (tar) + gpg detached sig (signer pinned via SW_GPG_KEY) +# + sha512, from the tagged working tree. npm run release-src SRC_TGZ="skywalking-nodejs-src-${RELEASE_VERSION}.tgz" @@ -235,24 +291,47 @@ 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}" +# ========================== Step 9: Push tag + branch, next-dev bump, open PR ========================== +note "Step 9 — Push tag ${TAG} + branch ${RELEASE_BRANCH}, open the release PR" + +cd "${CLONE_DIR}" +# Add the next-dev bump as a SECOND commit on the branch so one PR carries both: +# version strip (tagged commit 1) -> back to -dev (commit 2). The tag stays on +# commit 1. (checkout guards against any stray working-tree drift first.) +git checkout -q -- package.json package-lock.json 2>/dev/null || true +npm version "${NEXT_DEV_VERSION}" --no-git-tag-version >/dev/null +git add package.json package-lock.json +git commit -q --no-verify -m "Prepare next release ${NEXT_DEV_VERSION}" 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}") +if $DRY_RUN; then + echo "DRY RUN: prepared branch ${RELEASE_BRANCH} (2 commits) + local tag ${TAG} in ${CLONE_DIR}." + echo "DRY RUN: skipping push of branch + tag and the release PR." +elif confirm "Artifacts built + verified. Push tag ${TAG} + branch ${RELEASE_BRANCH} (atomic) and open the release PR now?"; then + # --atomic: both refs land together or neither, avoiding a branch-without-tag + # half-state that the re-run guards would then trip on. + git push --atomic origin "${RELEASE_BRANCH}" "${TAG}" TAG_PUSHED=true - echo "Pushed ${TAG}." + echo "Pushed ${RELEASE_BRANCH} + ${TAG}." + if gh pr create --repo "${GH_REPO}" --base "${REPO_BRANCH}" --head "${RELEASE_BRANCH}" \ + --title "Release ${RELEASE_VERSION}, bump to ${NEXT_DEV_VERSION}" \ + --body "Release branch for ${RELEASE_VERSION} — two commits: strip -dev (tagged ${TAG}, the RC the vote runs against) and bump to ${NEXT_DEV_VERSION}. Merge AFTER the [VOTE] passes; the ${TAG} tag stays pinned to the release commit."; then + echo "Opened release PR: ${RELEASE_BRANCH} -> ${REPO_BRANCH}." + else + echo "Could not open the PR automatically — open it by hand (${RELEASE_BRANCH} -> ${REPO_BRANCH})." + fi else - echo "Tag ${TAG} NOT pushed. Push it (from ${CLONE_DIR}) before sending the vote email:" - echo " git -C ${CLONE_DIR} push origin ${TAG}" + echo "Tag ${TAG} + branch ${RELEASE_BRANCH} NOT pushed; the prepared clone is in ${CLONE_DIR}." + echo "To finish by hand: git -C ${CLONE_DIR} push --atomic origin ${RELEASE_BRANCH} ${TAG}, then open the PR." 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 +if $DRY_RUN; then + echo "DRY RUN: skipping svn upload to ${SVN_DEV_URL}/${RELEASE_VERSION}." +elif 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. @@ -345,8 +424,9 @@ Thanks. ======================================================================== EOF -note "Done — release candidate ${RELEASE_VERSION} staged" -echo " Tag: ${TAG} ($($TAG_PUSHED && echo pushed || echo 'NOT pushed — push it before voting'))" +note "Done — $($DRY_RUN && echo 'DRY RUN for' || echo 'release candidate') ${RELEASE_VERSION}$($DRY_RUN || echo ' staged')" +$DRY_RUN && echo " (DRY RUN: nothing was pushed/uploaded; the prepared clone is in ${CLONE_DIR}.)" +echo " Tag: ${TAG} ($($TAG_PUSHED && echo pushed || echo 'NOT pushed'))" echo " Artifacts: ${WORK_DIR}/${SRC_TGZ}{,.asc,.sha512}" echo " svn dev staging: $($UPLOADED && echo "${SVN_DEV_URL}/${RELEASE_VERSION}" || echo 'NOT uploaded')" echo "" @@ -355,4 +435,6 @@ echo " 1. Draft the GitHub release notes (auto-generated; CHANGELOG.md is a stu 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" +echo " 3. After the vote passes:" +echo " - run: bash scripts/release-finalize.sh" +echo " - merge the release PR (${RELEASE_BRANCH} -> ${REPO_BRANCH}); ${REPO_BRANCH} returns to ${NEXT_DEV_VERSION}, tag ${TAG} stays put."
