Repository: vercel/next.js
Pull request: Fix preview version in `window.next.version` for PR builds

Preview builds served via `vercel-packages.vercel.app` were showing the published canary version instead of the PR-specific preview version. This happened because `create-preview-tarballs.js` updated `package.json` after the build, but the version was already baked into the compiled `dist/` files by `setNextVersion` in `taskfile-swc.js`.

This extracts the version computation from `create-preview-tarballs.js` into a new `set-preview-version.js` script that runs before `pnpm run build`, so the compiled output naturally includes the correct version. `create-preview-tarballs.js` now reads the version from `packages/next/package.json` instead of computing it independently.

## Test Plan

1. Create a new app: `pnpm create next-app@canary`
2. Install the preview build from this PR: `pnpm i https://vercel-packages.vercel.app/next/commits/06681c21e886a97e39d277808780884151c00318/next`
3. Run `pnpm dev` and inspect `next.version` in DevTools — should show `16.2.0-preview-06681c21-20260306` instead of the canary version


Changed files:
filename: .github/workflows/build_and_deploy.yml
patch: @@ -113,6 +113,12 @@ jobs:
 
       - run: pnpm install
 
+      - name: Set preview version
+        if: ${{ contains(fromJSON('["automated-preview","force-preview"]'), needs.deploy-target.outputs.value) }}
+        run: |
+          node scripts/set-preview-version.js "${{ github.sha }}"
+          pnpm install --no-frozen-lockfile
+
       - run: pnpm run build
 
       - uses: actions/cache@v4
@@ -542,7 +548,7 @@ jobs:
       - name: Create tarballs
         # github.event.after is available on push and pull_request#synchronize events.
         # For workflow_dispatch events, github.sha is the head commit.
-        run: node scripts/create-preview-tarballs.js "${{ github.sha }}" "${{ github.event.after || github.sha }}" "${{ runner.temp }}/preview-tarballs"
+        run: node scripts/create-preview-tarballs.js "${{ github.event.after || github.sha }}" "${{ runner.temp }}/preview-tarballs"
 
       - name: Upload tarballs
         uses: actions/upload-artifact@v4

filename: scripts/create-preview-tarballs.js
patch: @@ -6,35 +6,19 @@ const path = require('node:path')
 
 async function main() {
   const [
-    githubSha,
     githubHeadSha,
     tarballDirectory = path.join(os.tmpdir(), 'vercel-nextjs-preview-tarballs'),
   ] = process.argv.slice(2)
   const repoRoot = path.resolve(__dirname, '..')
 
   await fs.mkdir(tarballDirectory, { recursive: true })
 
-  const [{ stdout: shortSha }, { stdout: dateString }] = await Promise.all([
-    execa('git', ['rev-parse', '--short', githubSha]),
-    // Source: https://github.com/facebook/react/blob/767f52237cf7892ad07726f21e3e8bacfc8af839/scripts/release/utils.js#L114
-    execa(`git`, [
-      'show',
-      '-s',
-      '--no-show-signature',
-      '--format=%cd',
-      '--date=format:%Y%m%d',
-      githubSha,
-    ]),
-  ])
-
-  const lernaConfig = JSON.parse(
-    await fs.readFile(path.join(repoRoot, 'lerna.json'), 'utf8')
+  // The preview version is set in packages/next/package.json by
+  // scripts/set-preview-version.js before the build step.
+  const nextPackageJson = JSON.parse(
+    await fs.readFile(path.join(repoRoot, 'packages/next/package.json'), 'utf8')
   )
-
-  // 15.0.0-canary.17 -> 15.0.0
-  // 15.0.0 -> 15.0.0
-  const [semverStableVersion] = lernaConfig.version.split('-')
-  const version = `${semverStableVersion}-preview-${shortSha}-${dateString}`
+  const version = nextPackageJson.version
   console.info(`Designated version: ${version}`)
 
   const nativePackagesDir = path.join(repoRoot, 'crates/next-napi-bindings/npm')

filename: scripts/set-preview-version.js
patch: @@ -0,0 +1,56 @@
+// @ts-check
+const execa = require('execa')
+const fs = require('node:fs/promises')
+const path = require('node:path')
+
+async function main() {
+  const [githubSha] = process.argv.slice(2)
+  if (!githubSha) {
+    throw new Error('Usage: set-preview-version.js <githubSha>')
+  }
+
+  const repoRoot = path.resolve(__dirname, '..')
+
+  const [{ stdout: shortSha }, { stdout: dateString }] = await Promise.all([
+    execa('git', ['rev-parse', '--short', githubSha]),
+    // Source: https://github.com/facebook/react/blob/767f52237cf7892ad07726f21e3e8bacfc8af839/scripts/release/utils.js#L114
+    execa('git', [
+      'show',
+      '-s',
+      '--no-show-signature',
+      '--format=%cd',
+      '--date=format:%Y%m%d',
+      githubSha,
+    ]),
+  ])
+
+  const lernaConfigPath = path.join(repoRoot, 'lerna.json')
+  const lernaConfig = JSON.parse(await fs.readFile(lernaConfigPath, 'utf8'))
+
+  // 15.0.0-canary.17 -> 15.0.0
+  // 15.0.0 -> 15.0.0
+  const [semverStableVersion] = lernaConfig.version.split('-')
+  const version = `${semverStableVersion}-preview-${shortSha}-${dateString}`
+
+  await execa(
+    'pnpm',
+    [
+      'lerna',
+      'version',
+      version,
+      '--no-git-tag-version',
+      '--no-push',
+      '--allow-branch',
+      '**',
+      '--yes',
+    ],
+    { cwd: repoRoot, stdio: 'inherit' }
+  )
+
+  console.info(`Set preview version: ${version}`)
+}
+
+main().catch((err) => {
+  console.error(err)
+  process.exit(1)
+})