From 64f0e9c1d65ce45b4f1aa43e693e72b7ea1a3fd4 Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Wed, 27 May 2026 09:32:10 -0400 Subject: [PATCH 1/8] Add smoke test instructions --- .../generate-publishing-instructions.sh | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/rangelink-vscode-extension/scripts/generate-publishing-instructions.sh b/packages/rangelink-vscode-extension/scripts/generate-publishing-instructions.sh index 12e770943..27931a243 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-publishing-instructions.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-publishing-instructions.sh @@ -434,7 +434,45 @@ The following were validated when generating this file: --- -## Phase 1: Create Git Tag +## Phase 1: Smoke Test + +Install the extension locally and verify it works before publishing. This catches packaging +errors (missing files, broken builds) before they reach the marketplace. + +### Install Locally + +Run from **monorepo root**: + +\`\`\`bash +pnpm install-local:vscode-extension:both +\`\`\` + +This installs the VSIX in both VS Code and Cursor. Restart any running instances first. + +### Verify Version Number + +1. Open VS Code (\`code\`), then the Command Palette (\`Cmd+Shift+P\` / \`Ctrl+Shift+P\`) +2. Run: **RangeLink: Show Extension Info** +3. Confirm the version shown is **${VERSION}** +4. Confirm \`isDirty\` is **false** — a dirty build should never be published + +### Spot-Check Core Commands and Content + +Verify these commands work and the content is OK: + +- [ ] **R-L** +- [ ] **R-F** +- [ ] **Status bar menu** +- [ ] **No README artifacts** — open the extension's README in the Extensions panel. No \`Unreleased\` markers, no \`> [!IMPORTANT]\` banner. + +### If Anything Fails + +- Fix the issue, rebuild (\`pnpm package:vscode-extension\`), and re-run the smoke test. +- Regenerate these instructions afterward: \`pnpm generate:publish-instructions:vscode-extension\` + +--- + +## Phase 2: Create Git Tag ### Tag the Release @@ -459,7 +497,7 @@ git show ${GIT_TAG} --stat --- -## Phase 2: Create GitHub Release +## Phase 3: Create GitHub Release ### Navigate to GitHub Releases @@ -479,7 +517,7 @@ Click "Publish release" --- -## Phase 3: Publish to VS Code Marketplace +## Phase 4: Publish to VS Code Marketplace ### Ensure Logged In @@ -506,7 +544,7 @@ Wait 5-10 minutes, then check: --- -## Phase 4: Publish to Open-VSX Registry +## Phase 5: Publish to Open-VSX Registry ### Login (First Time Setup) @@ -541,7 +579,7 @@ Check: --- -## Phase 5: Post-Publishing Verification +## Phase 6: Post-Publishing Verification ### Check All Locations From e7d0dd363a104f689ab38435266e722892ee975b Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Wed, 27 May 2026 10:49:20 -0400 Subject: [PATCH 2/8] [issues/249] Regroup release scripts under `release:` prefix, chain QA issue generation into lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the three release pnpm scripts for logical grouping and clarity: - `lock-version` → `release:lock` (freeze version for QA) - `finalize-release` → `release:prepare` (prepare docs for publication) - `start-release` → `release:start` (open next dev cycle) `release:lock` now chains `generate-qa-issue.sh` automatically — running one command both locks the version and creates the GitHub QA issue tracker. The issue generator skips its interactive prompt when stdin is not a terminal. Also refactors `generate-qa-issue.sh` to eliminate duplicated content between GitHub and local output modes, and adds a final "Release Ready" checkbox that points to the next step (`release:prepare`). Benefits: - `release:` prefix groups related scripts in pnpm's flat namespace - "prepare" replaces the misleading "finalize" (which isn't the terminal step) - One command to lock + create QA issue instead of two - Local mode reuses GitHub body instead of duplicating it - QA issue includes explicit next-step instruction --- CLAUDE.md | 2 +- docs/RELEASE-STRATEGY.md | 46 ++++++++++++------- package.json | 6 +-- .../rangelink-vscode-extension/TESTING.md | 27 +++-------- .../rangelink-vscode-extension/package.json | 6 +-- .../scripts/generate-qa-issue.sh | 31 ++++++------- 6 files changed, 57 insertions(+), 61 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d76c12ca7..35b323000 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -605,7 +605,7 @@ QA YAML is a single file per release cycle - During trunk-based development, edit `qa-test-cases-unreleased.yaml` — add TCs, update automated status, adjust preconditions. The version is deferred until `finalize-release` locks it in. + During trunk-based development, edit `qa-test-cases-unreleased.yaml` — add TCs, update automated status, adjust preconditions. The version is deferred until `release:lock` locks it in. When starting a new release cycle, use `pnpm generate:qa-test-plan:vscode-extension` to create a fresh `qa-test-cases-unreleased.yaml` carrying forward all TCs from the previous version's YAML Edit a YAML file from a past release (e.g., don't touch v1.0.0.yaml after it has shipped) Fixing typos or updating `automated` status (`true`/`assisted`/`false`) in the current file is always allowed diff --git a/docs/RELEASE-STRATEGY.md b/docs/RELEASE-STRATEGY.md index bc32cd1d2..484178f95 100644 --- a/docs/RELEASE-STRATEGY.md +++ b/docs/RELEASE-STRATEGY.md @@ -129,19 +129,31 @@ Skip marking cosmetic renames or rewording of existing features — only mark ge ## Release Workflow +### Lifecycle + +```mermaid +flowchart TD + A[Unreleased (deferred)] -->|release:lock| B[Version locked] + B --> C[QA pass] + C -->|release:prepare| D[Docs ready for publish] + D --> E[publish] + E -->|release:start| A +``` + +Three verbs, three phases: **lock** (freeze the version for QA), **prepare** (make documentation publication-ready), **start** (open the next development cycle). + ### High-Level Process Releasing a package involves these phases: -1. **Prepare** - 1. `pnpm lock-version:vscode-extension X.Y.Z` — soft-lock the version for QA - 2. Run QA pass (manual + automated TCs) - 3. `pnpm finalize-release:vscode-extension` — hard-finalize: updates CHANGELOG, strips README markers, generates publishing instructions -2. **Build & Test** - Package and validate locally -3. **Publish** - Deploy to marketplace(s) and create GitHub release -4. **Tag** - Create annotated git tag following [tagging convention](#tagging-convention) -5. **Verify** - Confirm publication and test installation -6. **Next cycle** — `pnpm start-release:vscode-extension` to begin the next development cycle +1. **Lock** — `pnpm release:lock:vscode-extension X.Y.Z` — soft-lock the version for QA +2. **QA Pass** — manual + automated TCs against the versioned YAML +3. **Prepare** — `pnpm release:prepare:vscode-extension` — date-stamp CHANGELOG, strip `Unreleased` markers, remove `[!IMPORTANT]` banner, generate publishing instructions +4. **Build & Test** - Package and validate locally +5. **Publish** - Deploy to marketplace(s) and create GitHub release +6. **Tag** - Create annotated git tag following [tagging convention](#tagging-convention) +7. **Verify** - Confirm publication and test installation +8. **Next cycle** — `pnpm release:start:vscode-extension` to begin the next development cycle ### Package-Specific Instructions @@ -321,9 +333,9 @@ git push origin vscode-extension-v0.1.0 ```bash # 1. Prepare: lock version, run QA, then finalize -pnpm lock-version:vscode-extension 0.1.1 +pnpm release:lock:vscode-extension 0.1.1 # ... QA pass ... -pnpm finalize-release:vscode-extension +pnpm release:prepare:vscode-extension # 2. Build, test, package locally pnpm package:vscode-extension @@ -337,7 +349,7 @@ git tag -a vscode-extension-v0.1.1 -m "Release vscode-extension v0.1.1" git push origin vscode-extension-v0.1.1 # 5. Start next development cycle -pnpm start-release:vscode-extension +pnpm release:start:vscode-extension # 6. Create GitHub release with CHANGELOG content ``` @@ -346,15 +358,15 @@ pnpm start-release:vscode-extension ```bash # Same process as Example 2, with the new version number. -pnpm lock-version:vscode-extension 1.0.0 +pnpm release:lock:vscode-extension 1.0.0 # ... QA pass ... -pnpm finalize-release:vscode-extension +pnpm release:prepare:vscode-extension pnpm package:vscode-extension pnpm install-local:vscode-extension:both pnpm publish:vscode-extension:vsix git tag -a vscode-extension-v1.0.0 -m "Release vscode-extension v1.0.0" git push origin vscode-extension-v1.0.0 -pnpm start-release:vscode-extension +pnpm release:start:vscode-extension ``` ### Example 4: Multiple Package Release @@ -419,7 +431,7 @@ These items are planned but not yet implemented: - Better monorepo version coordination - [x] **Pre-commit hooks** — working tree cleanliness enforced by the release scripts; remaining hook work: prevent commits that introduce version/CHANGELOG mismatches. -- [x] **Release verification scripts** — `pnpm lock-version:vscode-extension`, `pnpm finalize-release:vscode-extension`, and `pnpm generate:publish-instructions:vscode-extension` validate the working tree, version numbers, CHANGELOG, and unreleased markers before allowing each phase to proceed. +- [x] **Release verification scripts** — `pnpm release:lock:vscode-extension`, `pnpm release:prepare:vscode-extension`, and `pnpm generate:publish-instructions:vscode-extension` validate the working tree, version numbers, CHANGELOG, and unreleased markers before allowing each phase to proceed. --- @@ -444,4 +456,4 @@ git push origin vscode-extension-v0.1.0 --- **Last Updated:** 2026-05-26 -**Status:** Active — automated via `pnpm lock-version:vscode-extension` → `pnpm finalize-release:vscode-extension` → `pnpm start-release:vscode-extension` +**Status:** Active — automated via `pnpm release:lock:vscode-extension` → `pnpm release:prepare:vscode-extension` → `pnpm release:start:vscode-extension` diff --git a/package.json b/package.json index a95688e03..057d958f5 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "clean:all": "pnpm -r clean:all", "compile": "pnpm clean && find . -name '*.tsbuildinfo' -delete && pnpm -r --workspace-concurrency=1 compile", "enable-pnpm": "corepack enable", - "finalize-release:vscode-extension": "pnpm --filter rangelink-vscode-extension run finalize-release", "fix": "pnpm lint:fix && pnpm format:fix", "format": "prettier --check .", "format:fix": "prettier --write .", @@ -26,12 +25,13 @@ "install-local:vscode-extension:vscode": "pnpm --filter rangelink-vscode-extension install-local:vscode", "lint": "eslint .", "lint:fix": "eslint . --fix", - "lock-version:vscode-extension": "pnpm --filter rangelink-vscode-extension run lock-version", "package:prepare": "pnpm clean:all && pnpm install", "package:vscode-extension": "pnpm --filter rangelink-vscode-extension clean && pnpm --filter rangelink-vscode-extension compile && pnpm --filter rangelink-vscode-extension test && pnpm --filter rangelink-vscode-extension package", "package:vscode-extension:withInstall:both": "pnpm package:vscode-extension && pnpm install-local:vscode-extension:both", "publish:vscode-extension:vsix": "pnpm --filter rangelink-vscode-extension publish:vsix", - "start-release:vscode-extension": "pnpm --filter rangelink-vscode-extension run start-release", + "release:lock:vscode-extension": "pnpm --filter rangelink-vscode-extension run release:lock", + "release:prepare:vscode-extension": "pnpm --filter rangelink-vscode-extension run release:prepare", + "release:start:vscode-extension": "pnpm --filter rangelink-vscode-extension run release:start", "test": "pnpm -r test", "test:bats": "bats tests/shell/", "test:release": "pnpm --filter rangelink-vscode-extension test:release", diff --git a/packages/rangelink-vscode-extension/TESTING.md b/packages/rangelink-vscode-extension/TESTING.md index 4cf8a15ad..63622f608 100644 --- a/packages/rangelink-vscode-extension/TESTING.md +++ b/packages/rangelink-vscode-extension/TESTING.md @@ -34,24 +34,13 @@ All `test:release*` commands accept `--label ` (include TCs with QA YAML la ### Release QA Cycle (once per release) -| Script | When | Re-runnable? | What it does | -| ------------------------------------------ | ---------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------- | -| `pnpm lock-version:vscode-extension X.Y.Z` | Ready to start QA | Yes (idempotent) | Renames QA YAML → versioned, bumps `.version`, regenerates instructions | -| `pnpm finalize-release:vscode-extension` | QA passed, ready to ship | No (one-way door) | Finalizes CHANGELOG, strips README markers/banner, generates publishing instructions | -| `pnpm start-release:vscode-extension` | After publish, starting next cycle | Yes (idempotent) | Copies versioned YAML → unreleased, adds `[Unreleased]` CHANGELOG header, re-adds README banner | +| Script | When | Re-runnable? | What it does | +| ------------------------------------------- | ---------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------- | +| `pnpm release:lock:vscode-extension X.Y.Z` | Ready to start QA | Yes (idempotent) | Renames QA YAML → versioned, bumps `.version`, regenerates instructions, generates QA issue | +| `pnpm release:prepare:vscode-extension` | QA passed, ready to ship | No (one-way door) | Date-stamps CHANGELOG, strips README markers/banner, generates publishing instructions | +| `pnpm release:start:vscode-extension` | After publish, starting next cycle | Yes (idempotent) | Copies versioned YAML → unreleased, adds `[Unreleased]` CHANGELOG header, re-adds README banner | -```mermaid -flowchart TD - A[Version: Unreleased (deferred)] --> B[lock-version.sh X.Y.Z] - B --> C[QA pass — manual + automated TCs] - C --> D{All TCs pass?} - D -- No --> E[Fix bugs] - E --> C - D -- Yes --> F[finalize-release.sh] - F --> G[build VSIX + publish] - G --> H[start-release.sh] - H --> A -``` +The release lifecycle is documented in [RELEASE-STRATEGY.md](../../docs/RELEASE-STRATEGY.md#release-workflow). --- @@ -277,9 +266,7 @@ The QA test plan is a version-scoped YAML file that tracks both automated and ma qa/qa-test-cases-unreleased.yaml ``` -During trunk-based development the file is `qa/qa-test-cases-unreleased.yaml`. At release time `pnpm lock-version:vscode-extension` renames it to `qa/qa-test-cases-v.yaml`. - -The filename is always `qa-test-cases-unreleased.yaml` during trunk-based development — the version is deferred until `pnpm lock-version:vscode-extension` locks it in at QA time. It is parsed automatically by the `generate-qa-issue` script — no extra flags needed. One file per release — Git tracks history across versions. +During trunk-based development the file is `qa/qa-test-cases-unreleased.yaml`. At release time `pnpm release:lock:vscode-extension` renames it to `qa/qa-test-cases-v.yaml` and generates the QA issue automatically. One file per release — Git tracks history across versions. New QA YAML files are created by `pnpm generate:qa-test-plan`. The script carries forward all TCs from the most recent YAML, resets `status:` fields to `pending`, and preserves `automated:` flags. diff --git a/packages/rangelink-vscode-extension/package.json b/packages/rangelink-vscode-extension/package.json index 6ef4d01aa..fca712dc0 100644 --- a/packages/rangelink-vscode-extension/package.json +++ b/packages/rangelink-vscode-extension/package.json @@ -36,7 +36,6 @@ "clean:all": "pnpm clean && rm -rf node_modules .eslintcache *.log .vscode-test", "compile": "pnpm compile:deps && pnpm generate-version:all && node esbuild.config.js", "compile:deps": "cd ../barebone-logger && pnpm clean && pnpm compile && cd ../barebone-logger-testing && pnpm clean && pnpm compile && cd ../rangelink-core-ts && pnpm clean && pnpm compile", - "finalize-release": "./scripts/finalize-release.sh", "generate-version": "node scripts/generate-version.js --copy-to out", "generate-version:all": "node scripts/generate-version.js --copy-to out,dist", "generate:publish-instructions": "./scripts/generate-publishing-instructions.sh", @@ -46,10 +45,11 @@ "install-local": "./scripts/install-local.sh", "install-local:cursor": "./scripts/install-local.sh cursor", "install-local:vscode": "./scripts/install-local.sh vscode", - "lock-version": "./scripts/lock-version.sh", "package": "rm -rf *.vsix && ../../scripts/sync-assets.sh && rm -f rangelink-vscode-extension-*.vsix && vsce package --no-dependencies", "publish:vsix": "./scripts/publish-from-vsix.sh", - "start-release": "./scripts/start-release.sh", + "release:lock": "./scripts/lock-version.sh && ./scripts/generate-qa-issue.sh", + "release:prepare": "./scripts/finalize-release.sh", + "release:start": "./scripts/start-release.sh", "test": "jest --coverage", "test:coverage": "jest --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=html", "test:fast": "jest --coverage --testPathIgnorePatterns '/src/__integration-tests__/' '\\.integration\\.test\\.ts$'", diff --git a/packages/rangelink-vscode-extension/scripts/generate-qa-issue.sh b/packages/rangelink-vscode-extension/scripts/generate-qa-issue.sh index be85d9f0a..254cc1461 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-qa-issue.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-qa-issue.sh @@ -62,11 +62,15 @@ if [[ -z "$YAML_FILE" ]]; then fi YAML_FILE="$QA_DIR/$LATEST" - printf 'Use %s? [Y/n] ' "qa/$LATEST" - read -r REPLY - if [[ -n "$REPLY" && ! "$REPLY" =~ ^[Yy]$ ]]; then - echo "Aborted." >&2 - exit 0 + if [[ -t 0 ]]; then + printf 'Use %s? [Y/n] ' "qa/$LATEST" + read -r REPLY + if [[ -n "$REPLY" && ! "$REPLY" =~ ^[Yy]$ ]]; then + echo "Aborted." >&2 + exit 0 + fi + else + echo "Auto-selected latest QA YAML: qa/$LATEST" fi fi @@ -169,11 +173,12 @@ if [[ "$UBUNTU_COUNT" -gt 0 ]]; then GROUP_CHECKBOXES+="${UBUNTU_SECTION}"$'\n' fi -ISSUE_BODY="${ISSUE_BODY}${GROUP_CHECKBOXES}" - TOTAL_ASSISTED=$(echo "$GROUPS_JSON" | node -e "process.stdout.write(String(JSON.parse(require('fs').readFileSync(0,'utf8')).total_assisted))") TOTAL_MANUAL=$(echo "$GROUPS_JSON" | node -e "process.stdout.write(String(JSON.parse(require('fs').readFileSync(0,'utf8')).total_manual))") -ISSUE_BODY="${ISSUE_BODY}"$'\n'"**Total: ${TOTAL_ASSISTED} assisted, ${TOTAL_MANUAL} manual**" + +BODY_FOOTER="${GROUP_CHECKBOXES}"$'\n\n'"**Total: ${TOTAL_ASSISTED} assisted, ${TOTAL_MANUAL} manual**"$'\n\n'"---"$'\n\n'"Once all checkboxes above are checked, the release is ready to prepare for publication:"$'\n\n'"- [ ] **Release Ready** — Run \`pnpm release:prepare:vscode-extension\` to date-stamp the CHANGELOG, strip unreleased markers, and generate publishing instructions." + +ISSUE_BODY="${ISSUE_BODY}${BODY_FOOTER}" # --- Local mode: write to a markdown file --- if [[ "$LOCAL_MODE" == true ]]; then @@ -182,20 +187,12 @@ if [[ "$LOCAL_MODE" == true ]]; then TIMESTAMP=$(date -u +"%Y%m%d-%H%M%S") LOCAL_FILE="$OUTPUT_DIR/qa-checklist-${VERSION}-${TIMESTAMP}.md" - TOTAL_ASSISTED=$(echo "$GROUPS_JSON" | node -e "process.stdout.write(String(JSON.parse(require('fs').readFileSync(0,'utf8')).total_assisted))") - TOTAL_MANUAL=$(echo "$GROUPS_JSON" | node -e "process.stdout.write(String(JSON.parse(require('fs').readFileSync(0,'utf8')).total_manual))") - { echo "# QA Checklist — ${VERSION}" echo "" echo "Generated from \`$(basename "$YAML_FILE")\` on $(date -u +"%Y-%m-%d %H:%M:%S UTC")" echo "" - echo "CI runs fully automated tests (\`test:release:automated\`). The checkboxes below cover assisted and manual tests only." - echo "" - echo "${GROUP_CHECKBOXES}" - echo "" - echo "**Total: ${TOTAL_ASSISTED} assisted, ${TOTAL_MANUAL} manual**" - + echo "${ISSUE_BODY}" } > "$LOCAL_FILE" REPO_ROOT="$(git rev-parse --show-toplevel)" From fcd2086d22b07254417216f7ade1518eb07bbed9 Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Wed, 27 May 2026 10:57:23 -0400 Subject: [PATCH 3/8] Ran `pnpm fix` --- packages/rangelink-vscode-extension/TESTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/rangelink-vscode-extension/TESTING.md b/packages/rangelink-vscode-extension/TESTING.md index 63622f608..803ddd3d7 100644 --- a/packages/rangelink-vscode-extension/TESTING.md +++ b/packages/rangelink-vscode-extension/TESTING.md @@ -34,11 +34,11 @@ All `test:release*` commands accept `--label ` (include TCs with QA YAML la ### Release QA Cycle (once per release) -| Script | When | Re-runnable? | What it does | -| ------------------------------------------- | ---------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------- | -| `pnpm release:lock:vscode-extension X.Y.Z` | Ready to start QA | Yes (idempotent) | Renames QA YAML → versioned, bumps `.version`, regenerates instructions, generates QA issue | -| `pnpm release:prepare:vscode-extension` | QA passed, ready to ship | No (one-way door) | Date-stamps CHANGELOG, strips README markers/banner, generates publishing instructions | -| `pnpm release:start:vscode-extension` | After publish, starting next cycle | Yes (idempotent) | Copies versioned YAML → unreleased, adds `[Unreleased]` CHANGELOG header, re-adds README banner | +| Script | When | Re-runnable? | What it does | +| ------------------------------------------ | ---------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------- | +| `pnpm release:lock:vscode-extension X.Y.Z` | Ready to start QA | Yes (idempotent) | Renames QA YAML → versioned, bumps `.version`, regenerates instructions, generates QA issue | +| `pnpm release:prepare:vscode-extension` | QA passed, ready to ship | No (one-way door) | Date-stamps CHANGELOG, strips README markers/banner, generates publishing instructions | +| `pnpm release:start:vscode-extension` | After publish, starting next cycle | Yes (idempotent) | Copies versioned YAML → unreleased, adds `[Unreleased]` CHANGELOG header, re-adds README banner | The release lifecycle is documented in [RELEASE-STRATEGY.md](../../docs/RELEASE-STRATEGY.md#release-workflow). From 45e32344ec1b1ae378fc6af53c81d8aad1b72912 Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Wed, 27 May 2026 11:15:10 -0400 Subject: [PATCH 4/8] =?UTF-8?q?[issues/249]=20Add=20orchestrate-release-lo?= =?UTF-8?q?ck.sh=20=E2=80=94=20single=20command=20for=20the=20full=20lock?= =?UTF-8?q?=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps branch creation, version locking, QA issue generation, and commit message generation into one script. `pnpm release:lock:vscode-extension X.Y.Z` now: 1. Creates a `release/vX.Y.Z` branch from main 2. Runs lock-version.sh (rename YAML, bump .version, regenerate instructions) 3. Runs generate-qa-issue.sh (creates GitHub issue with correct versioned title) 4. Writes a commit message file with the QA issue URL 5. Prints the remaining manual steps (review, commit, push, QA) Benefits: - Predictable branch naming (`release/vX.Y.Z`) - QA issue URL is embedded in the commit message and summary - lock-version.sh stays focused on locking — orchestration lives separately - No more chained `&&` in package.json --- .../rangelink-vscode-extension/package.json | 2 +- .../scripts/orchestrate-release-lock.sh | 112 ++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100755 packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh diff --git a/packages/rangelink-vscode-extension/package.json b/packages/rangelink-vscode-extension/package.json index fca712dc0..b7b0e47b6 100644 --- a/packages/rangelink-vscode-extension/package.json +++ b/packages/rangelink-vscode-extension/package.json @@ -47,7 +47,7 @@ "install-local:vscode": "./scripts/install-local.sh vscode", "package": "rm -rf *.vsix && ../../scripts/sync-assets.sh && rm -f rangelink-vscode-extension-*.vsix && vsce package --no-dependencies", "publish:vsix": "./scripts/publish-from-vsix.sh", - "release:lock": "./scripts/lock-version.sh && ./scripts/generate-qa-issue.sh", + "release:lock": "./scripts/orchestrate-release-lock.sh", "release:prepare": "./scripts/finalize-release.sh", "release:start": "./scripts/start-release.sh", "test": "jest --coverage", diff --git a/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh b/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh new file mode 100755 index 000000000..c96b413e2 --- /dev/null +++ b/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./scripts/orchestrate-release-lock.sh X.Y.Z +# +# Orchestrates the release lock workflow: +# 1. Create a release/vX.Y.Z branch from main +# 2. Run lock-version.sh (rename YAML, bump .version, regenerate instructions) +# 3. Run generate-qa-issue.sh (create GitHub QA issue tracker) +# 4. Generate a commit message file +# +# Delegates the actual work to the individual scripts; this script handles +# branch setup and sequencing only. + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +VERSION="${1:-}" +if [[ -z "$VERSION" ]]; then + echo -e "${RED}Usage: $0 ${NC}" >&2 + echo "Example: $0 2.0.0" >&2 + exit 1 +fi + +if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo -e "${RED}Error: version must be SemVer (X.Y.Z), got '$VERSION'${NC}" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_DIR="$(dirname "$SCRIPT_DIR")" +REPO_ROOT="$(git -C "$PACKAGE_DIR" rev-parse --show-toplevel)" + +# --- Working tree must be clean --- + +if ! git -C "$REPO_ROOT" diff-index --quiet HEAD --; then + echo -e "${RED}Error: working tree is dirty. Commit or stash changes first.${NC}" >&2 + exit 1 +fi + +# --- Create release branch --- + +RELEASE_BRANCH="release/v${VERSION}" +if git -C "$REPO_ROOT" rev-parse --verify "$RELEASE_BRANCH" >/dev/null 2>&1; then + echo -e "${RED}Error: branch $RELEASE_BRANCH already exists.${NC}" >&2 + exit 1 +fi + +CURRENT_BRANCH=$(git -C "$REPO_ROOT" branch --show-current) +if [[ "$CURRENT_BRANCH" != "main" ]]; then + echo -e "${YELLOW}Not on main (current: $CURRENT_BRANCH). Creating $RELEASE_BRANCH from main.${NC}" +fi + +git -C "$REPO_ROOT" checkout -b "$RELEASE_BRANCH" main +echo -e "${GREEN}Created branch $RELEASE_BRANCH from main${NC}" + +# --- Step 1: Lock the version --- + +"$SCRIPT_DIR/lock-version.sh" "$VERSION" +echo "" + +# --- Step 2: Generate QA issue --- + +QA_OUTPUT=$("$SCRIPT_DIR/generate-qa-issue.sh") || { + echo -e "${RED}Error: generate-qa-issue.sh failed${NC}" >&2 + exit 1 +} +echo "$QA_OUTPUT" +echo "" + +QA_ISSUE_URL=$(echo "$QA_OUTPUT" | grep -o 'https://github\.com/[^ ]*issues/[0-9]*') +if [[ -z "$QA_ISSUE_URL" ]]; then + echo -e "${RED}Error: could not extract QA issue URL from generate-qa-issue.sh output${NC}" >&2 + exit 1 +fi + +# --- Step 3: Generate commit message --- + +COMMIT_MSGS_DIR="$REPO_ROOT/.commit-msgs" +mkdir -p "$COMMIT_MSGS_DIR" + +LAST=$(ls "$COMMIT_MSGS_DIR" | grep -o '^[0-9]\{4\}' | sort -n | tail -1) +NEXT_NUM=$(printf "%04d" $((10#${LAST:-0} + 1))) +COMMIT_MSG_FILE="$COMMIT_MSGS_DIR/${NEXT_NUM}-lock-version-v${VERSION}.txt" + +cat > "$COMMIT_MSG_FILE" < Date: Wed, 27 May 2026 11:48:51 -0400 Subject: [PATCH 5/8] [PR feedback] Fix clean-tree guard and first-run grep error in release scripts Replace `git diff-index --quiet HEAD --` with `git status --porcelain` in all three release scripts so untracked files also block execution. Add `|| true` to the commit-message numbering pipeline so the script doesn't abort on first run when `.commit-msgs/` is empty. Benefits: - Untracked files no longer slip past the clean-tree guard - First-run commit numbering works without aborting - All three scripts (orchestrate-release-lock, lock-version, finalize-release) now use the same clean-tree check Ref: https://github.com/couimet/rangeLink/pull/608#pullrequestreview-4373699285 --- .../rangelink-vscode-extension/scripts/finalize-release.sh | 2 +- packages/rangelink-vscode-extension/scripts/lock-version.sh | 2 +- .../scripts/orchestrate-release-lock.sh | 4 ++-- tests/shell/finalize-release.bats | 4 ++-- tests/shell/lock-version.bats | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/rangelink-vscode-extension/scripts/finalize-release.sh b/packages/rangelink-vscode-extension/scripts/finalize-release.sh index 60bc47016..61a295c64 100755 --- a/packages/rangelink-vscode-extension/scripts/finalize-release.sh +++ b/packages/rangelink-vscode-extension/scripts/finalize-release.sh @@ -41,7 +41,7 @@ fi # --- Working tree must be clean --- -if ! git -C "$REPO_ROOT" diff-index --quiet HEAD --; then +if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then echo -e "${RED}Error: working tree is dirty. Commit or stash changes first.${NC}" >&2 exit 1 fi diff --git a/packages/rangelink-vscode-extension/scripts/lock-version.sh b/packages/rangelink-vscode-extension/scripts/lock-version.sh index 0ab710c0c..7ab5ca9eb 100755 --- a/packages/rangelink-vscode-extension/scripts/lock-version.sh +++ b/packages/rangelink-vscode-extension/scripts/lock-version.sh @@ -41,7 +41,7 @@ QA_DIR="$PACKAGE_DIR/qa" # --- Validation --- -if ! git -C "$REPO_ROOT" diff-index --quiet HEAD --; then +if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then echo -e "${RED}Error: working tree is dirty. Commit or stash changes first.${NC}" >&2 exit 1 fi diff --git a/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh b/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh index c96b413e2..0e974cd5f 100755 --- a/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh +++ b/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh @@ -35,7 +35,7 @@ REPO_ROOT="$(git -C "$PACKAGE_DIR" rev-parse --show-toplevel)" # --- Working tree must be clean --- -if ! git -C "$REPO_ROOT" diff-index --quiet HEAD --; then +if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then echo -e "${RED}Error: working tree is dirty. Commit or stash changes first.${NC}" >&2 exit 1 fi @@ -81,7 +81,7 @@ fi COMMIT_MSGS_DIR="$REPO_ROOT/.commit-msgs" mkdir -p "$COMMIT_MSGS_DIR" -LAST=$(ls "$COMMIT_MSGS_DIR" | grep -o '^[0-9]\{4\}' | sort -n | tail -1) +LAST=$(ls "$COMMIT_MSGS_DIR" | grep -o '^[0-9]\{4\}' || true | sort -n | tail -1) NEXT_NUM=$(printf "%04d" $((10#${LAST:-0} + 1))) COMMIT_MSG_FILE="$COMMIT_MSGS_DIR/${NEXT_NUM}-lock-version-v${VERSION}.txt" diff --git a/tests/shell/finalize-release.bats b/tests/shell/finalize-release.bats index dc244882e..c7355578a 100644 --- a/tests/shell/finalize-release.bats +++ b/tests/shell/finalize-release.bats @@ -16,7 +16,7 @@ setup_fixture() { make_stub "git" <<'ENDOFSTUB' case "$*" in *--show-toplevel*) echo "${FIXTURE_ROOT_FOR_GIT:-$TEST_TEMP_DIR}" ;; - *diff-index*) exit 0 ;; + *status*) exit 0 ;; *) exit 0 ;; esac ENDOFSTUB @@ -176,7 +176,7 @@ EOF #!/usr/bin/env bash case "$*" in *--show-toplevel*) echo "${FIXTURE_ROOT_FOR_GIT:-$TEST_TEMP_DIR}" ;; - *diff-index*) exit 1 ;; + *status*) echo "?? untracked-file.txt"; exit 0 ;; *) exit 0 ;; esac ENDOFSTUB diff --git a/tests/shell/lock-version.bats b/tests/shell/lock-version.bats index 5e070e320..e5a71b8d4 100644 --- a/tests/shell/lock-version.bats +++ b/tests/shell/lock-version.bats @@ -16,7 +16,7 @@ setup_fixture() { make_stub "git" <<'ENDOFSTUB' case "$*" in *--show-toplevel*) echo "${FIXTURE_ROOT_FOR_GIT:-$TEST_TEMP_DIR}" ;; - *diff-index*) exit 0 ;; + *status*) exit 0 ;; *"mv"*) shift 3; mv "$@" ;; *) exit 0 ;; esac From 39a955154a8fb298151693ffbf70db53576ff49f Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Wed, 27 May 2026 12:58:02 -0400 Subject: [PATCH 6/8] [PR feedback] Fix || true placement and wrong command title in release scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move `|| true` to end of full pipeline in orchestrate-release-lock.sh so sort/tail always execute when commit-msg files exist. Fix "Show Extension Info" → "Show Version Info" in generate-publishing-instructions.sh smoke-test step. Benefits: - Commit message numbering works correctly when multiple files exist - Smoke-test instructions reference the actual registered command title Ref: https://github.com/couimet/rangeLink/pull/608#pullrequestreview-4374224021 --- .../generate-publishing-instructions.sh | 2 +- .../scripts/orchestrate-release-lock.sh | 2 +- .../generate-publishing-instructions.bats | 48 +++++++++++++++ tests/shell/orchestrate-release-lock.bats | 61 +++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 tests/shell/generate-publishing-instructions.bats create mode 100644 tests/shell/orchestrate-release-lock.bats diff --git a/packages/rangelink-vscode-extension/scripts/generate-publishing-instructions.sh b/packages/rangelink-vscode-extension/scripts/generate-publishing-instructions.sh index 27931a243..b48ff6a39 100755 --- a/packages/rangelink-vscode-extension/scripts/generate-publishing-instructions.sh +++ b/packages/rangelink-vscode-extension/scripts/generate-publishing-instructions.sh @@ -452,7 +452,7 @@ This installs the VSIX in both VS Code and Cursor. Restart any running instances ### Verify Version Number 1. Open VS Code (\`code\`), then the Command Palette (\`Cmd+Shift+P\` / \`Ctrl+Shift+P\`) -2. Run: **RangeLink: Show Extension Info** +2. Run: **RangeLink: Show Version Info** 3. Confirm the version shown is **${VERSION}** 4. Confirm \`isDirty\` is **false** — a dirty build should never be published diff --git a/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh b/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh index 0e974cd5f..16ab4255e 100755 --- a/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh +++ b/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh @@ -81,7 +81,7 @@ fi COMMIT_MSGS_DIR="$REPO_ROOT/.commit-msgs" mkdir -p "$COMMIT_MSGS_DIR" -LAST=$(ls "$COMMIT_MSGS_DIR" | grep -o '^[0-9]\{4\}' || true | sort -n | tail -1) +LAST=$(ls "$COMMIT_MSGS_DIR" | grep -o '^[0-9]\{4\}' | sort -n | tail -1 || true) NEXT_NUM=$(printf "%04d" $((10#${LAST:-0} + 1))) COMMIT_MSG_FILE="$COMMIT_MSGS_DIR/${NEXT_NUM}-lock-version-v${VERSION}.txt" diff --git a/tests/shell/generate-publishing-instructions.bats b/tests/shell/generate-publishing-instructions.bats new file mode 100644 index 000000000..9ce0789a6 --- /dev/null +++ b/tests/shell/generate-publishing-instructions.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats + +load test_helper + +REAL_SCRIPT="$PROJECT_ROOT/packages/rangelink-vscode-extension/scripts/generate-publishing-instructions.sh" + +setup_fixture() { + FIXTURE_ROOT="$TEST_TEMP_DIR" + mkdir -p "$FIXTURE_ROOT/scripts" + mkdir -p "$FIXTURE_ROOT/src" + + cp "$REAL_SCRIPT" "$FIXTURE_ROOT/scripts/generate-publishing-instructions.sh" + SCRIPT="$FIXTURE_ROOT/scripts/generate-publishing-instructions.sh" + + cat > "$FIXTURE_ROOT/package.json" <<'EOF' +{ + "version": "2.0.0", + "publisher": "test-publisher" +} +EOF + + cat > "$FIXTURE_ROOT/src/version.json" <<'EOF' +{"isDirty": false} +EOF + + stub_dir + make_stub "git" <<'ENDOFSTUB' +case "$*" in + *rev-parse*) exit 1 ;; + *) exit 0 ;; +esac +ENDOFSTUB +} + +# ── Smoke-test command title ───────────────────────────────────────────────────── + +@test "publishing instructions smoke-test step uses Show Version Info" { + setup_fixture + run "$SCRIPT" --allow-dirty + [[ "$status" -eq 0 ]] + + output_file="$FIXTURE_ROOT/publishing-instructions/publish-vscode-extension-v2.0.0.md" + [[ -f "$output_file" ]] + + run cat "$output_file" + [[ "$output" =~ "Show Version Info" ]] + ! [[ "$output" =~ "Show Extension Info" ]] +} diff --git a/tests/shell/orchestrate-release-lock.bats b/tests/shell/orchestrate-release-lock.bats new file mode 100644 index 000000000..147fc39ca --- /dev/null +++ b/tests/shell/orchestrate-release-lock.bats @@ -0,0 +1,61 @@ +#!/usr/bin/env bats + +load test_helper + +REAL_SCRIPT="$PROJECT_ROOT/packages/rangelink-vscode-extension/scripts/orchestrate-release-lock.sh" + +setup_fixture() { + FIXTURE_ROOT="$TEST_TEMP_DIR" + mkdir -p "$FIXTURE_ROOT/scripts" + mkdir -p "$FIXTURE_ROOT/.commit-msgs" + + cp "$REAL_SCRIPT" "$FIXTURE_ROOT/scripts/orchestrate-release-lock.sh" + SCRIPT="$FIXTURE_ROOT/scripts/orchestrate-release-lock.sh" + + stub_dir + make_stub "git" <<'ENDOFSTUB' +case "$*" in + *--show-toplevel*) echo "$FIXTURE_ROOT_FOR_GIT" ;; + *status*) exit 0 ;; + *"checkout -b"*) exit 0 ;; + *"rev-parse --verify"*) exit 1 ;; + *"branch --show-current"*) echo "main" ;; + *) exit 0 ;; +esac +ENDOFSTUB + export FIXTURE_ROOT_FOR_GIT="$FIXTURE_ROOT" + + # Stub lock-version.sh to do nothing. + cat > "$FIXTURE_ROOT/scripts/lock-version.sh" <<'STUBEOF' +#!/usr/bin/env bash +echo "Locked version $1" +STUBEOF + chmod +x "$FIXTURE_ROOT/scripts/lock-version.sh" + + # Stub generate-qa-issue.sh to output a fake issue URL. + cat > "$FIXTURE_ROOT/scripts/generate-qa-issue.sh" <<'STUBEOF' +#!/usr/bin/env bash +echo "Created QA issue: https://github.com/couimet/rangeLink/issues/999" +STUBEOF + chmod +x "$FIXTURE_ROOT/scripts/generate-qa-issue.sh" +} + +# ── Commit message numbering ───────────────────────────────────────────────────── + +@test "first run with empty .commit-msgs creates 0001 file" { + setup_fixture + run "$SCRIPT" "1.0.0" + [[ "$status" -eq 0 ]] + [[ -f "$FIXTURE_ROOT/.commit-msgs/0001-lock-version-v1.0.0.txt" ]] +} + +@test "picks next number after existing files" { + setup_fixture + touch "$FIXTURE_ROOT/.commit-msgs/0001-old.txt" + touch "$FIXTURE_ROOT/.commit-msgs/0005-old.txt" + touch "$FIXTURE_ROOT/.commit-msgs/0010-old.txt" + + run "$SCRIPT" "2.0.0" + [[ "$status" -eq 0 ]] + [[ -f "$FIXTURE_ROOT/.commit-msgs/0011-lock-version-v2.0.0.txt" ]] +} From 8c7cc10ff697d51aae4001b3cbf6f3246f1b3d4d Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Wed, 27 May 2026 13:16:06 -0400 Subject: [PATCH 7/8] Stub node in BATS so CI is happy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI workflow only installs bats — no Node.js. Added a node stub so we can support `generate-publishing-instructions.sh` calling node -p "require(...)" to read package.json and version.json. --- tests/shell/generate-publishing-instructions.bats | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/shell/generate-publishing-instructions.bats b/tests/shell/generate-publishing-instructions.bats index 9ce0789a6..e937bdf24 100644 --- a/tests/shell/generate-publishing-instructions.bats +++ b/tests/shell/generate-publishing-instructions.bats @@ -24,6 +24,16 @@ EOF EOF stub_dir + make_stub "node" <<'ENDOFSTUB' +#!/usr/bin/env bash +if [[ "$*" == *"isDirty"* ]]; then + echo "false" +elif [[ "$*" == *".version"* ]]; then + echo "2.0.0" +else + exit 1 +fi +ENDOFSTUB make_stub "git" <<'ENDOFSTUB' case "$*" in *rev-parse*) exit 1 ;; From 58183d2950f13de085af003672fb9dee58c4767e Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Wed, 27 May 2026 13:57:35 -0400 Subject: [PATCH 8/8] [PR feedback] Complete BATS fixture so generate-publishing-instructions test passes on Ubuntu CI The script validates VSIX existence, CHANGELOG.md, and README.md before generating instructions. CI runners have no Node.js, npx, or project files. Add the missing fixture files and stubs so the test runs without a real toolchain. Ref: https://github.com/couimet/rangeLink/pull/608#pullrequestreview-4374224021 --- tests/shell/generate-publishing-instructions.bats | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/shell/generate-publishing-instructions.bats b/tests/shell/generate-publishing-instructions.bats index e937bdf24..5eea0fb90 100644 --- a/tests/shell/generate-publishing-instructions.bats +++ b/tests/shell/generate-publishing-instructions.bats @@ -23,17 +23,31 @@ EOF {"isDirty": false} EOF + # Files the script validates before generating instructions. + touch "$FIXTURE_ROOT/rangelink-vscode-extension-2.0.0.vsix" + cat > "$FIXTURE_ROOT/CHANGELOG.md" <<'EOF' +## [2.0.0] + +### Added +- Test entry +[2.0.0]: https://example.com +EOF + touch "$FIXTURE_ROOT/README.md" + stub_dir make_stub "node" <<'ENDOFSTUB' #!/usr/bin/env bash if [[ "$*" == *"isDirty"* ]]; then echo "false" +elif [[ "$*" == *"publisher"* ]]; then + echo "test-publisher" elif [[ "$*" == *".version"* ]]; then echo "2.0.0" else exit 1 fi ENDOFSTUB + make_passive_stub "npx" make_stub "git" <<'ENDOFSTUB' case "$*" in *rev-parse*) exit 1 ;;