diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f74128f..7dcb353 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,9 +13,26 @@ permissions: contents: read jobs: + # A real release may only be dispatched from `main`: `gh workflow run --ref` + # accepts any branch or tag, and publishing plus tagging from anywhere else + # would permanently release the wrong commit. Dry runs are allowed from any + # ref so a branch can rehearse the packaging matrix. Failing here (instead + # of silently skipping the publish steps) makes the mistake visible. + preflight: + name: Preflight + if: github.repository == 'rstackjs/rstack-editor' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Require main for a real release + if: ${{ !inputs.dry_run && github.ref != 'refs/heads/main' }} + run: | + echo "::error::A real release must be dispatched from main (got ${GITHUB_REF}). Re-run with dry run enabled, or dispatch from main." + exit 1 + release_vscode_extension: name: Release VS Code Extension (${{ matrix.vsce-target }}) - if: github.repository == 'rstackjs/rstack-editor' && github.event_name == 'workflow_dispatch' + needs: preflight runs-on: ${{ matrix.runner }} environment: vscode-marketplace timeout-minutes: 30 @@ -100,3 +117,68 @@ jobs: env: OVSX_PAT: ${{ secrets.OVSX_PAT }} run: pnpm exec ovsx publish --target ${{ matrix.vsce-target }} --skip-duplicate + + # Runs only for a real release: a dry run must leave no tag behind. The tag + # points at the commit that was actually published (github.sha), the release + # notes come from the conventional commits since the previous tag, and the + # VSIX of every matrix target is attached as an asset so a release can be + # installed by hand (`code --install-extension`) without either marketplace. + github_release: + name: Tag and GitHub Release + needs: release_vscode_extension + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + # changelogithub needs the tags and history to find the previous + # release and list the commits since it; the tag step compares an + # existing tag's target against this commit. + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 22 + + - name: Download VSIX Artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: rstack-* + path: vsix + merge-multiple: true + + - name: Read Version + id: version + run: echo "tag=v$(node -p "require('./packages/vscode/package.json').version")" >> "$GITHUB_OUTPUT" + + # Idempotent for a re-run of the same commit (e.g. after one marketplace + # publish flaked and `--skip-duplicate` covered the rest), but a tag that + # already points at a *different* commit means the version was never + # bumped after the last release: fail instead of attaching VSIX files + # built from this commit to a release tagged at another one. + - name: Push Tag + env: + TAG: ${{ steps.version.outputs.tag }} + run: | + existing=$(git rev-parse --verify --quiet "refs/tags/$TAG^{commit}" || true) + if [ -z "$existing" ]; then + git tag "$TAG" "$GITHUB_SHA" + git push origin "refs/tags/$TAG" + elif [ "$existing" = "$GITHUB_SHA" ]; then + echo "tag $TAG already points at $GITHUB_SHA, leaving it as is" + else + echo "::error::tag $TAG already exists at $existing but this run is at $GITHUB_SHA — bump the version (pnpm bump) before releasing again." + exit 1 + fi + + - name: GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + run: | + npx changelogithub@15.0.4 --to "$TAG" --assets 'vsix/*.vsix' diff --git a/AGENTS.md b/AGENTS.md index 3debf2b..1169f8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ Unified editor support for the [Rstack](https://rstack.rs) toolchain. pnpm works ## Conventions - All code, comments, commit messages, PRs and docs are in English, regardless of the conversation language. -- Root scripts are thin `pnpm -r run` fan-outs. Never make a root script reach into a package's internals — add the script to the package instead. +- Root scripts either fan out (`pnpm -r run`) or act on the whole workspace at once (`pnpm bump`). Never make a root script reach into one named package's internals — add the script to that package instead. - READMEs are user-facing only. Contributor/agent material goes in AGENTS.md files, not READMEs. - Sibling checkouts of rslint / rstest / rstack-cli (`../rslint` etc.) are read-only references. Their working trees may be stale: `git fetch origin` and read via `git show origin/main:`. - Verify claims about upstream behavior or published packages against the actual source or registry — do not answer from memory. @@ -16,6 +16,7 @@ Unified editor support for the [Rstack](https://rstack.rs) toolchain. pnpm works - If extension source changed, also run the E2E slice covering the change (see `packages/vscode/AGENTS.md`). E2E launches a real VS Code and is the ground truth for editor behavior — unit tests are not a substitute. - Never delete `packages/vscode/.vscode-test/` — it caches the VS Code download the E2E suites reuse. - Report real command results only; never claim green without running. +- Releases: never tag or publish by hand — the **Release** workflow does both. See CONTRIBUTING.md → Releasing. ## Agent skills diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 601a0ab..2172752 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,3 +28,13 @@ To try the extension: press F5 in VS Code at the repo root — the playground la - The pre-commit hook formats and lints staged files (`rs staged`). - CI runs build, lint, format check, unit tests and the E2E suites on Linux and Windows — please run `pnpm lint && pnpm test:unit` locally before pushing. - Keep PRs focused; fill in the PR template. + +## Releasing + +All packages share one version and are bumped in lockstep. Today the VS Code extension (`packages/vscode`) is the only published artifact. + +1. On a branch, run `pnpm bump` (interactive; or `pnpm bump --release minor`) — [bumpp](https://github.com/antfu-collective/bumpp) bumps every `packages/*/package.json` and commits `vX.Y.Z`. It does not tag or push. +2. Open a PR with that commit and merge it into `main`. +3. Run the **Release** workflow (`Actions → Release → Run workflow`, branch `main`; `gh workflow run release.yml`). Tick **dry run** to only build the VSIX artifacts (one per platform target). A real run publishes every platform target to the VS Code Marketplace and Open VSX, then tags `main` as `vX.Y.Z` and creates the GitHub Release with notes generated from the conventional commits since the previous tag and the VSIX files attached. + +Re-running a release for the same commit is safe: both marketplaces skip an already-published version and the tag step leaves a tag that already points at that commit alone. A real (non-dry) run is rejected unless dispatched from `main`, and fails if the version's tag already exists on a different commit — bump first. diff --git a/bump.config.ts b/bump.config.ts new file mode 100644 index 0000000..781ff97 --- /dev/null +++ b/bump.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'bumpp'; + +// All packages share one version; see CONTRIBUTING.md → Releasing. +export default defineConfig({ + files: ['packages/*/package.json'], + commit: 'v%s', + // Tagging and pushing belong to the release workflow, which tags the commit + // it actually published from. `pnpm bump` only prepares the release commit + // for a PR. + tag: false, + push: false, +}); diff --git a/package.json b/package.json index 0ad79dc..3db0a89 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "type": "module", "scripts": { "build": "pnpm -r run build", + "bump": "bumpp", "fmt": "rs fmt", "fmt:check": "rs fmt --check", "lint": "rs lint --type-check", @@ -14,6 +15,7 @@ "test:unit": "pnpm -r run test:unit" }, "devDependencies": { + "bumpp": "^11.1.0", "rstack": "^0.3.2" }, "packageManager": "pnpm@11.20.0", diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 66b5a5c..283ce3a 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -1,7 +1,7 @@ { "name": "rstack", "displayName": "Rstack", - "version": "0.1.0", + "version": "0.0.1", "private": true, "description": "All Rstack tool integrations for VS Code.", "categories": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bfa8d0..c54b45c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + bumpp: + specifier: ^11.1.0 + version: 11.1.0 rstack: specifier: ^0.3.2 version: 0.3.2(jiti@2.7.0)(typescript@5.9.3) @@ -571,6 +574,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@rsbuild/core@2.1.9': resolution: {integrity: sha512-yqf1hFZ3wbMYI431LqsxLH3r0VZkfyarVKTf7kMeIiGe0YLwsrgsfp+sKpIyVkQkq60J0qyp6l/CoqqsQZqEwQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1065,6 +1071,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + args-tokenizer@0.3.0: + resolution: {integrity: sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1128,10 +1137,19 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bumpp@11.1.0: + resolution: {integrity: sha512-jdwOGMyX8JIqpQ0N2RMRR87DHZaoJnUtui5lU9LqFfFK5JC0H8qY9uWqXoa+dEWt/K7rOmmsoyiZB8RBM7RPBQ==} + engines: {node: '>=20.19.0'} + hasBin: true + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1279,6 +1297,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1876,6 +1897,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -1965,6 +1989,9 @@ packages: resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} engines: {node: '>=0.6'} + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2213,6 +2240,10 @@ packages: resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==} engines: {node: '>=4'} + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -2258,6 +2289,12 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unconfig@7.5.0: + resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + underscore@1.13.8: resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} @@ -2366,6 +2403,11 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -2848,6 +2890,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + '@rsbuild/core@2.1.9': dependencies: '@rspack/core': 2.1.7(@swc/helpers@0.5.23) @@ -3312,6 +3358,8 @@ snapshots: argparse@2.0.1: {} + args-tokenizer@0.3.0: {} + assertion-error@2.0.1: {} astral-regex@2.0.0: {} @@ -3371,10 +3419,24 @@ snapshots: ieee754: 1.2.1 optional: true + bumpp@11.1.0: + dependencies: + args-tokenizer: 0.3.0 + cac: 7.0.0 + jsonc-parser: 3.3.1 + package-manager-detector: 1.8.0 + semver: 7.8.5 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + unconfig: 7.5.0 + yaml: 2.9.0 + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 + cac@7.0.0: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -3528,6 +3590,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.7: {} + delayed-stream@1.0.0: {} detect-libc@2.1.2: @@ -3861,8 +3925,7 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jiti@2.7.0: - optional: true + jiti@2.7.0: {} js-tokens@4.0.0: {} @@ -4141,6 +4204,8 @@ snapshots: package-json-from-dist@1.0.1: {} + package-manager-detector@1.8.0: {} + pako@1.0.11: {} parse-json@8.3.0: @@ -4229,6 +4294,8 @@ snapshots: es-define-property: 1.0.1 side-channel: 1.1.1 + quansync@1.0.0: {} + queue-microtask@1.2.3: {} randombytes@2.1.0: @@ -4524,6 +4591,8 @@ snapshots: dependencies: editions: 6.22.0 + tinyexec@1.3.0: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) @@ -4560,6 +4629,19 @@ snapshots: uc.micro@2.1.0: {} + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.7 + jiti: 2.7.0 + quansync: 1.0.0 + unconfig-core: 7.5.0 + underscore@1.13.8: {} undici-types@6.21.0: {} @@ -4650,6 +4732,8 @@ snapshots: yallist@4.0.0: {} + yaml@2.9.0: {} + yargs-parser@21.1.1: {} yargs-unparser@2.0.0: