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) +})