diff --git a/.github/workflows/prepare-publish.yml b/.github/workflows/prepare-publish.yml
new file mode 100644
index 000000000..615ccd89e
--- /dev/null
+++ b/.github/workflows/prepare-publish.yml
@@ -0,0 +1,99 @@
+name: Prepare Publish
+
+on:
+ workflow_dispatch:
+ inputs:
+ target_branch:
+ description: Release target branch.
+ required: true
+ default: main
+ type: string
+ release:
+ description: Bumpp release type.
+ required: true
+ default: next
+ type: choice
+ options:
+ - next
+ - patch
+ - minor
+ - major
+ - prepatch
+ - preminor
+ - premajor
+ version:
+ description: Specify exact version instead of bumpp release type
+ required: false
+ type: string
+
+concurrency:
+ group: ${{ github.workflow }}-${{ inputs.target_branch }}-${{ inputs.version || inputs.release }}
+ cancel-in-progress: false
+
+permissions: {}
+
+jobs:
+ prepare:
+ if: github.repository == 'vitest-dev/vitest'
+ name: Prepare release PR
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read # checkout target branch
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ inputs.target_branch }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
+
+ - name: Set node version to 24
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: 24
+ package-manager-cache: false
+
+ - name: Install
+ run: pnpm install --frozen-lockfile --prefer-offline
+ env:
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
+
+ - name: Create branch and update version
+ env:
+ TARGET_BRANCH: ${{ inputs.target_branch }}
+ RELEASE_TYPE: ${{ inputs.release }}
+ RELEASE_VERSION: ${{ inputs.version }}
+ RUN_ID: ${{ github.run_id }}
+ run: |
+ RELEASE_INPUT="${RELEASE_VERSION:-$RELEASE_TYPE}"
+ PREPARE_BRANCH="prepare-$TARGET_BRANCH-$RELEASE_INPUT-$RUN_ID"
+ git switch -c "$PREPARE_BRANCH"
+ # The numeric prefix comes from `gh api /users/vitest-release-bot%5Bbot%5D --jq .id`.
+ # This is just to make the avatar in the commit look pretty.
+ git config user.name "vitest-release-bot[bot]"
+ git config user.email "292707936+vitest-release-bot[bot]@users.noreply.github.com"
+ pnpm run release
+
+ - id: generate-token
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
+ with:
+ app-id: ${{ secrets.RELEASE_GITHUB_APP_ID }}
+ private-key: ${{ secrets.RELEASE_GITHUB_APP_PRIVATE_KEY }}
+ permission-contents: write
+ permission-pull-requests: write
+
+ - name: Open release PR
+ env:
+ TARGET_BRANCH: ${{ inputs.target_branch }}
+ GH_TOKEN: ${{ steps.generate-token.outputs.token }}
+ run: |
+ PREPARE_BRANCH="$(git branch --show-current)"
+ VERSION="$(jq -r .version package.json)"
+ git push -u "https://x-access-token:$GH_TOKEN@github.com/$GITHUB_REPOSITORY.git" HEAD
+ gh pr create \
+ --base "$TARGET_BRANCH" \
+ --head "$PREPARE_BRANCH" \
+ --title "chore: release v$VERSION" \
+ --body "Release PR generated by the Prepare Publish workflow."
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 20f536fec..4b797854b 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -2,8 +2,9 @@ name: Publish Package
on:
push:
- tags:
- - 'v*'
+ branches:
+ - main
+ - 'v[0-9]*'
permissions: {}
@@ -11,9 +12,40 @@ env:
VITE_TEST_WATCHER_DEBUG: 'false'
jobs:
- publish:
- # only run on main, don't trigger in forks
+ # Keep detection outside the Release environment. Environment approval is job-level,
+ # so the publish job is only created after a release commit is detected.
+ detect:
if: github.repository == 'vitest-dev/vitest'
+ name: Detect release commit
+ runs-on: ubuntu-slim
+ outputs:
+ release: ${{ steps.detect.outputs.release }}
+ version: ${{ steps.detect.outputs.version }}
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Detect release
+ id: detect
+ run: |
+ SUBJECT="$(git log -1 --format=%s)"
+ VERSION="$(jq -r .version package.json)"
+ EXPECTED="chore: release v$VERSION"
+ # use prefix match since commit has PR number trailer.
+ if [[ "$SUBJECT" == "$EXPECTED"* ]]; then
+ echo "release=true" >> "$GITHUB_OUTPUT"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ echo "Detected release v$VERSION"
+ else
+ echo "release=false" >> "$GITHUB_OUTPUT"
+ echo "No release commit found at HEAD"
+ fi
+
+ publish:
+ if: needs.detect.outputs.release == 'true'
+ needs: detect
name: Publish Vitest
runs-on: ubuntu-latest
permissions:
@@ -26,6 +58,17 @@ jobs:
fetch-depth: 0
persist-credentials: false
+ - name: Check release tag
+ env:
+ VERSION: ${{ needs.detect.outputs.version }}
+ run: |
+ TAG="v$VERSION"
+
+ if git rev-parse --verify --quiet "refs/tags/$TAG"; then
+ echo "Tag $TAG already exists"
+ exit 1
+ fi
+
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
@@ -45,10 +88,34 @@ jobs:
- name: Build
run: pnpm build
- - name: Publish to npm
- run: npm i -g npm@^11.5.2 && pnpm run publish-ci "${GITHUB_REF_NAME}"
+ - name: Stage publish to npm (dry run)
+ env:
+ VERSION: ${{ needs.detect.outputs.version }}
+ PUBLISH_BRANCH: ${{ github.ref_name }}
+ PUBLISH_DRY_RUN: 'true'
+ run: pnpm run publish-ci "$VERSION"
+
+ - name: Stage publish to npm
+ env:
+ VERSION: ${{ needs.detect.outputs.version }}
+ PUBLISH_BRANCH: ${{ github.ref_name }}
+ run: pnpm run publish-ci "$VERSION"
+
+ - name: Push release tag
+ env:
+ VERSION: ${{ needs.detect.outputs.version }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ TAG="v$VERSION"
+ git config user.name "vitest-release-bot"
+ git config user.email "actions@github.com"
+ git tag "$TAG" "$GITHUB_SHA"
+ git push "https://x-access-token:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git" "$TAG"
- name: Generate Changelog
- run: npx changelogithub
env:
+ VERSION: ${{ needs.detect.outputs.version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ TAG="v$VERSION"
+ npx changelogithub --to "$TAG" --name "$TAG"
diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml
index 069a97844..25f171ef6 100644
--- a/.github/workflows/zizmor.yml
+++ b/.github/workflows/zizmor.yml
@@ -26,6 +26,8 @@ jobs:
with:
persist-credentials: false
+ # test locally by
+ # zizmor .github/workflows --pedantic
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
with:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 512c2dfb4..16e9b72bd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -161,6 +161,23 @@ The release branches are also linked with the documentation site releases:
- `release` points to the latest stable release line used for . Release managers update it manually for non-beta releases from `main`; it is not moved for older-line backports.
- `vN` branches are used for old major documentation sites. For example, uses `v3`.
+### Release process
+
+Releases — publishing the npm packages, creating the git release tag, and generating the associated GitHub release — are driven by a pull request and carried out by GitHub Actions, not from a maintainer's machine. The release PR holds the version bump, and merging it kicks off the actual publish.
+
+1. **Prepare the release PR.** Run the `Prepare Publish` workflow with two inputs:
+
+ - `target_branch` — the branch matching the [release branch](#release-branches) convention for the release line. The Actions menu's separate "Use workflow from" selector should point at the same branch, to keep the workflow definition and release target aligned.
+ - `release` or `version` — the version bump. The default `release: next` bumps to the next patch for stable releases (`4.1.2 -> 4.1.3`), or the next prerelease when already on one (`4.2.0-beta.2 -> 4.2.0-beta.3`). Otherwise set `release` to a specific bump type, or pass an exact `version` for pre-releases.
+
+ The workflow pushes the bump to a branch and opens the release PR. To preview what a `release` input resolves to, run `pnpm release` locally first — it lets you browse release types and versions interactively (cancel before confirming so nothing is committed).
+
+2. **Review and merge the PR.** Check the version bump, then merge so the `chore: release v*` commit lands on the release branch — that commit is what triggers publishing.
+
+3. **Approve the publish workflow.** Merging triggers the `Publish Package` workflow, which builds the packages and then pauses for `Release` environment approval. Open the workflow run in GitHub Actions and approve the `Release` environment deployment; the workflow then stages the packages on npm, pushes the release tag, and generates the GitHub release.
+
+4. **Approve the npm staged publish.** Review the staged packages on npm, then approve them with 2FA so the release becomes installable. Afterwards, confirm npm, the tag, and the GitHub release all look right.
+
### Issue Triaging Workflow
```mermaid
diff --git a/package.json b/package.json
index c711d8948..04f932f4e 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"type": "module",
"version": "5.0.0-beta.4",
"private": true,
- "packageManager": "pnpm@11.1.2",
+ "packageManager": "pnpm@11.6.0",
"description": "Next generation testing framework powered by Vite",
"engines": {
"node": "^22.12.0 || ^24.0.0 || >=26.0.0"
@@ -47,6 +47,7 @@
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@types/node": "24.12.0",
+ "@types/semver": "catalog:",
"@types/ws": "catalog:",
"@vitest/browser": "workspace:*",
"@vitest/coverage-istanbul": "workspace:*",
@@ -62,6 +63,7 @@
"rollup": "^4.59.0",
"rollup-plugin-dts": "^6.3.0",
"rollup-plugin-license": "^3.7.0",
+ "semver": "catalog:",
"tinyglobby": "catalog:",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f2f27fe76..ad3948385 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -51,6 +51,9 @@ catalogs:
'@types/istanbul-reports':
specifier: ^3.0.4
version: 3.0.4
+ '@types/semver':
+ specifier: ^7.7.1
+ version: 7.7.1
'@types/ws':
specifier: ^8.18.1
version: 8.18.1
@@ -117,6 +120,9 @@ catalogs:
playwright:
specifier: ^1.60.0
version: 1.60.0
+ semver:
+ specifier: ^7.8.3
+ version: 7.8.3
sinon:
specifier: ^21.0.3
version: 21.0.3
@@ -215,6 +221,9 @@ importers:
'@types/node':
specifier: 24.12.0
version: 24.12.0
+ '@types/semver':
+ specifier: 'catalog:'
+ version: 7.7.1
'@types/ws':
specifier: 'catalog:'
version: 8.18.1
@@ -260,6 +269,9 @@ importers:
rollup-plugin-license:
specifier: ^3.7.0
version: 3.7.0(picomatch@4.0.4)(rollup@4.59.0)
+ semver:
+ specifier: 'catalog:'
+ version: 7.8.3
tinyglobby:
specifier: 'catalog:'
version: 0.2.15
@@ -1272,10 +1284,10 @@ importers:
version: link:../../packages/web-worker
'@vue/test-utils':
specifier: latest
- version: 2.4.10(@vue/compiler-dom@3.5.29)(@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
+ version: 2.4.11(@vue/compiler-dom@3.5.29)(@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
happy-dom:
specifier: latest
- version: 20.9.0
+ version: 20.10.2
istanbul-lib-coverage:
specifier: 'catalog:'
version: 3.2.2
@@ -1512,7 +1524,7 @@ importers:
version: link:../../packages/browser-preview
happy-dom:
specifier: latest
- version: 20.9.0
+ version: 20.10.2
vitest:
specifier: workspace:*
version: link:../../packages/vitest
@@ -5160,6 +5172,9 @@ packages:
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
+ '@types/semver@7.7.1':
+ resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
+
'@types/sinonjs__fake-timers@8.1.5':
resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==}
@@ -5524,6 +5539,16 @@ packages:
'@vue/server-renderer':
optional: true
+ '@vue/test-utils@2.4.11':
+ resolution: {integrity: sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==}
+ peerDependencies:
+ '@vue/compiler-dom': 3.x
+ '@vue/server-renderer': 3.x
+ vue: 3.x
+ peerDependenciesMeta:
+ '@vue/server-renderer':
+ optional: true
+
'@vueuse/core@12.8.2':
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
@@ -5963,6 +5988,10 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ buffer-image-size@0.6.4:
+ resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
+ engines: {node: '>=4.0'}
+
builtin-modules@5.0.0:
resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==}
engines: {node: '>=18.20'}
@@ -7251,12 +7280,12 @@ packages:
handle-thing@2.0.1:
resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==}
- happy-dom@20.8.3:
- resolution: {integrity: sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==}
+ happy-dom@20.10.2:
+ resolution: {integrity: sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ==}
engines: {node: '>=20.0.0'}
- happy-dom@20.9.0:
- resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==}
+ happy-dom@20.8.3:
+ resolution: {integrity: sha512-lMHQRRwIPyJ70HV0kkFT7jH/gXzSI7yDkQFe07E2flwmNDFoWUTRMKpW2sglsnpeA7b6S2TJPp98EbQxai8eaQ==}
engines: {node: '>=20.0.0'}
has-bigints@1.1.0:
@@ -9258,18 +9287,13 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
- semver@7.7.2:
- resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
- engines: {node: '>=10'}
- hasBin: true
-
- semver@7.7.3:
- resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
hasBin: true
- semver@7.7.4:
- resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ semver@7.8.3:
+ resolution: {integrity: sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==}
engines: {node: '>=10'}
hasBin: true
@@ -10452,6 +10476,18 @@ packages:
utf-8-validate:
optional: true
+ ws@8.21.0:
+ resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
xml-name-validator@4.0.0:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
@@ -12874,7 +12910,7 @@ snapshots:
extract-zip: 2.0.1
progress: 2.0.3
proxy-agent: 6.5.0
- semver: 7.7.3
+ semver: 7.7.4
tar-fs: 3.0.8
yargs: 17.7.2
transitivePeerDependencies:
@@ -13563,6 +13599,8 @@ snapshots:
'@types/resolve@1.20.2': {}
+ '@types/semver@7.7.1': {}
+
'@types/sinonjs__fake-timers@8.1.5': {}
'@types/statuses@2.0.6': {}
@@ -14113,6 +14151,15 @@ snapshots:
optionalDependencies:
'@vue/server-renderer': 3.5.29(vue@3.5.29(typescript@5.9.3))
+ '@vue/test-utils@2.4.11(@vue/compiler-dom@3.5.29)(@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))':
+ dependencies:
+ '@vue/compiler-dom': 3.5.29
+ js-beautify: 1.15.1
+ vue: 3.5.29(typescript@5.9.3)
+ vue-component-type-helpers: 3.2.8
+ optionalDependencies:
+ '@vue/server-renderer': 3.5.29(vue@3.5.29(typescript@5.9.3))
+
'@vueuse/core@12.8.2(typescript@5.9.3)':
dependencies:
'@types/web-bluetooth': 0.0.21
@@ -14536,6 +14583,10 @@ snapshots:
buffer-from@1.1.2: {}
+ buffer-image-size@0.6.4:
+ dependencies:
+ '@types/node': 24.12.0
+
builtin-modules@5.0.0: {}
bumpp@10.4.1:
@@ -14547,7 +14598,7 @@ snapshots:
escalade: 3.2.0
jsonc-parser: 3.3.1
package-manager-detector: 1.6.0
- semver: 7.7.3
+ semver: 7.7.4
tinyexec: 1.0.2
tinyglobby: 0.2.15
yaml: 2.8.2
@@ -14654,7 +14705,7 @@ snapshots:
pathe: 1.1.2
pkg-types: 1.3.1
scule: 1.3.0
- semver: 7.7.3
+ semver: 7.7.4
std-env: 3.10.0
yaml: 2.8.2
transitivePeerDependencies:
@@ -14669,7 +14720,7 @@ snapshots:
convert-gitmoji: 0.1.5
execa: 9.6.0
ofetch: 1.5.1
- semver: 7.7.3
+ semver: 7.7.4
tinyglobby: 0.2.15
transitivePeerDependencies:
- magicast
@@ -15445,7 +15496,7 @@ snapshots:
globals: 15.15.0
globrex: 0.1.2
ignore: 5.3.2
- semver: 7.7.3
+ semver: 7.7.4
ts-declaration-location: 1.0.7(typescript@5.9.3)
transitivePeerDependencies:
- typescript
@@ -15511,7 +15562,7 @@ snapshots:
pluralize: 8.0.0
regexp-tree: 0.1.27
regjsparser: 0.13.0
- semver: 7.7.3
+ semver: 7.7.4
strip-indent: 4.1.1
eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1)):
@@ -15527,7 +15578,7 @@ snapshots:
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.1
- semver: 7.7.3
+ semver: 7.7.4
vue-eslint-parser: 10.4.0(eslint@10.0.3(jiti@2.6.1))
xml-name-validator: 4.0.0
optionalDependencies:
@@ -15787,7 +15838,7 @@ snapshots:
process-warning: 5.0.0
rfdc: 1.4.1
secure-json-parse: 4.0.0
- semver: 7.7.3
+ semver: 7.7.4
toad-cache: 3.7.0
fastq@1.17.1:
@@ -16114,19 +16165,20 @@ snapshots:
handle-thing@2.0.1: {}
- happy-dom@20.8.3:
+ happy-dom@20.10.2:
dependencies:
'@types/node': 24.12.0
'@types/whatwg-mimetype': 3.0.2
'@types/ws': 8.18.1
+ buffer-image-size: 0.6.4
entities: 7.0.1
whatwg-mimetype: 3.0.0
- ws: 8.19.0
+ ws: 8.21.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
- happy-dom@20.9.0:
+ happy-dom@20.8.3:
dependencies:
'@types/node': 24.12.0
'@types/whatwg-mimetype': 3.0.2
@@ -16486,7 +16538,7 @@ snapshots:
'@babel/parser': 7.27.2
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
- semver: 7.7.2
+ semver: 7.7.4
transitivePeerDependencies:
- supports-color
@@ -16905,7 +16957,7 @@ snapshots:
make-dir@4.0.0:
dependencies:
- semver: 7.7.3
+ semver: 7.7.4
mark.js@8.11.1: {}
@@ -18478,12 +18530,10 @@ snapshots:
semver@6.3.1: {}
- semver@7.7.2: {}
-
- semver@7.7.3: {}
-
semver@7.7.4: {}
+ semver@7.8.3: {}
+
send@0.18.0:
dependencies:
debug: 2.6.9
@@ -18557,7 +18607,7 @@ snapshots:
dependencies:
color: 4.2.3
detect-libc: 2.0.4
- semver: 7.7.3
+ semver: 7.7.4
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5
@@ -19568,7 +19618,7 @@ snapshots:
eslint-visitor-keys: 5.0.0
espree: 11.1.0
esquery: 1.7.0
- semver: 7.7.3
+ semver: 7.7.4
transitivePeerDependencies:
- supports-color
@@ -19929,6 +19979,8 @@ snapshots:
ws@8.19.0: {}
+ ws@8.21.0: {}
+
xml-name-validator@4.0.0: {}
xml-name-validator@5.0.0: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 7964e4a16..617d7453b 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -62,6 +62,7 @@ catalog:
'@types/istanbul-lib-source-maps': ^4.0.4
'@types/istanbul-reports': ^3.0.4
'@types/node': 24.12.0
+ '@types/semver': ^7.7.1
'@types/ws': ^8.18.1
'@types/yauzl': ^2.10.3
'@unocss/reset': ^66.6.6
@@ -84,6 +85,7 @@ catalog:
obug: ^2.1.1
pathe: ^2.0.3
playwright: ^1.60.0
+ semver: ^7.8.3
sinon: ^21.0.3
sinon-chai: ^4.0.1
sirv: ^3.0.2
diff --git a/scripts/publish-ci.ts b/scripts/publish-ci.ts
index 7f53a7897..442377967 100644
--- a/scripts/publish-ci.ts
+++ b/scripts/publish-ci.ts
@@ -1,43 +1,73 @@
-#!/usr/bin/env zx
-
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
+import * as semver from 'semver'
import { $ } from 'zx'
-if (process.env.VITE_TEST_WATCHER_DEBUG !== 'false') {
- throw new Error(`Cannot release Vitest without VITE_TEST_WATCHER_DEBUG=${process.env.VITE_TEST_WATCHER_DEBUG} environment variable. `)
-}
+// How to test release script locally:
+// RELEASE_VERSION=5.0.0-beta.5 pnpm release
+// VITE_TEST_WATCHER_DEBUG=false PUBLISH_DRY_RUN=true PUBLISH_BRANCH=main pnpm publish-ci 5.0.0-beta.5
-let version = process.argv[2]
+const $$ = $({ stdio: 'inherit' })
-if (!version) {
- throw new Error('No tag specified')
-}
+async function main() {
+ if (process.env.VITE_TEST_WATCHER_DEBUG !== 'false') {
+ throw new Error(`Cannot release Vitest without VITE_TEST_WATCHER_DEBUG=${process.env.VITE_TEST_WATCHER_DEBUG} environment variable. `)
+ }
-if (version.startsWith('v')) {
- version = version.slice(1)
-}
+ const version = process.argv[2]
+ if (!version) {
+ throw new Error('Missing argument to specify version')
+ }
-const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url))
-const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
+ const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url))
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
+ if (pkg.version !== version) {
+ throw new Error(
+ `Input version "${version}" does not match package.json version "${pkg.version}"`,
+ )
+ }
-if (pkg.version !== version) {
- throw new Error(
- `Package version from tag "${version}" mismatches with the current version "${pkg.version}"`,
- )
+ const publishBranch = process.env.PUBLISH_BRANCH
+ if (!publishBranch) {
+ throw new Error('Missing PUBLISH_BRANCH environment variable')
+ }
+ const releaseTag = await getReleaseTag(version, publishBranch)
+
+ const dryRun = process.env.PUBLISH_DRY_RUN === 'true'
+ if (dryRun) {
+ console.log('== DRY RUN ==')
+ }
+ console.log(`Staging version '${version}' with tag '${releaseTag}'`)
+ await $$`pnpm -r stage publish --access public --no-git-checks --tag ${releaseTag} ${dryRun ? ['--dry-run'] : []}`
}
-const releaseTag = version.includes('beta')
- ? 'beta'
- : version.includes('alpha')
- ? 'alpha'
- : undefined
+async function getReleaseTag(version: string, publishBranch: string) {
+ // Always specify the dist-tag explicitly since otherwise `latest` would be overwritten.
+ // Note that `main` branch doesn't always mean `latest` tag because of pre-release phase.
-console.log('Publishing version', version, 'with tag', releaseTag || 'latest')
+ // check prerelease e.g. beta, alpha, rc
+ const parsed = semver.parse(version, {}, true)
+ if (parsed.prerelease.length > 0) {
+ return parsed.prerelease[0]
+ }
-if (releaseTag) {
- await $`pnpm -r publish --access public --no-git-checks --tag ${releaseTag}`
-}
-else {
- await $`pnpm -r publish --access public --no-git-checks`
+ // If the version is not a pre-release and is greater than the latest version on npm,
+ // then that should become the new latest version.
+ const npmView = await $`npm view vitest dist-tags --json`
+ const latestVersion = JSON.parse(npmView.stdout).latest
+ if (semver.gt(version, latestVersion)) {
+ return 'latest'
+ }
+
+ // Otherwise this is a backport release.
+ // Use the uppercase of the branch name to avoid npm dist-tag caveats
+ // https://docs.npmjs.com/cli/v11/commands/npm-dist-tag#caveats
+ // - v4 branch -> V4 dist tag
+ // - v4.1 branch -> V4.1 dist tag
+ return publishBranch.toUpperCase()
}
+
+main().catch((error) => {
+ console.error('Error during publishing:', error)
+ process.exit(1)
+})
diff --git a/scripts/release.ts b/scripts/release.ts
index 3f85d4560..d837215d5 100644
--- a/scripts/release.ts
+++ b/scripts/release.ts
@@ -1,22 +1,27 @@
-#!/usr/bin/env zx
-
import { versionBump } from 'bumpp'
import { glob } from 'tinyglobby'
-try {
- const packages = await glob(['package.json', './packages/*/package.json'], { expandDirectories: false })
+async function main() {
+ const packages = await glob(['package.json', './packages/*/package.json'], {
+ expandDirectories: false,
+ })
console.log('Bumping versions in packages:', packages.join(', '), '\n')
+ const release = process.env.RELEASE_VERSION || process.env.RELEASE_TYPE
+
await versionBump({
files: packages,
+ release,
commit: true,
- push: true,
- tag: true,
+ tag: false,
+ push: false,
+ printCommits: false,
+ confirm: !release,
})
-
- console.log('New release is ready, waiting for conformation at https://github.com/vitest-dev/vitest/actions')
-}
-catch (err) {
- console.error(err)
}
+
+main().catch((error) => {
+ console.error('Error during version bump:', error)
+ process.exit(1)
+})