Repository: vercel/next.js
Pull request: [release] add new ci to use changesets/action for stable release

This PR added `.github/workflows/trigger_release_new.yml`, which is a port of `.github/workflows/trigger_release.yml` and modified [publishRelease](https://github.com/vercel/next.js/blob/8223eb86925e575cd641b55626b4bbc4a4d788c6/.github/workflows/build_and_deploy.yml#L529) to use the `changesets/action` to publish to NPM and GitHub Release.

This workflow uses [changesets/action](https://github.com/changesets/action) to publish NPM and GitHub Release. The main difference is that the GitHub Release will be published per package.

### Testing Plan

Try running `ci:version` and `ci:publish` locally.

Changed files:
filename: .github/workflows/build_and_deploy.yml
patch: @@ -19,6 +19,9 @@ env:
   #
   # See https://doc.rust-lang.org/rustc/platform-support/apple-darwin.html#os-version for more details
   MACOSX_DEPLOYMENT_TARGET: 11.0
+  # This will become "true" if the latest commit is
+  # "Version Packages", set from check-is-release.js
+  __NEW_RELEASE: 'false'
 
 jobs:
   deploy-target:
@@ -44,7 +47,12 @@ jobs:
         # 'staging' for canary branch since that will eventually be published i.e. become the production build.
         id: deploy-target
         run: |
-          if [[ $(node ./scripts/check-is-release.js 2> /dev/null || :) = v* ]];
+          # TODO: Remove the new release check once the new release workflow is fully replaced.
+          RELEASE_CHECK=$(node ./scripts/check-is-release.js 2> /dev/null || :)
+          if [[ $RELEASE_CHECK =~ ^Version\ Packages$ ]];
+          then
+            echo "__NEW_RELEASE=true" >> $GITHUB_ENV
+          elif [[ $RELEASE_CHECK = v* ]];
           then
             echo "value=production" >> $GITHUB_OUTPUT
           elif [ '${{ github.ref }}' == 'refs/heads/canary' ]
@@ -586,10 +594,23 @@ jobs:
       - run: npm i -g npm@10.4.0 # need latest version for provenance (pinning to avoid bugs)
       - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
       - run: ./scripts/publish-native.js
+      # Legacy release process
       - run: ./scripts/publish-release.js
+        if: ${{ env.__NEW_RELEASE == 'false' }}
         env:
           RELEASE_BOT_GITHUB_TOKEN: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
 
+      # New release process
+      - name: Publish to NPM
+        id: publish-packages
+        if: ${{ env.__NEW_RELEASE == 'true' }}
+        uses: changesets/action@v1
+        with:
+          publish: pnpm ci:publish
+        env:
+          GITHUB_TOKEN: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
+          NPM_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }}
+
       - name: Upload npm log artifact
         uses: actions/upload-artifact@v4
         if: always()

filename: .github/workflows/trigger_release_new.yml
patch: @@ -0,0 +1,103 @@
+name: Trigger Release (New)
+
+on:
+  workflow_dispatch:
+    inputs:
+      releaseType:
+        description: Release Type
+        required: true
+        type: choice
+        options:
+          # TODO: Follow up enable the case.
+          # - canary
+          - stable
+          # TODO: Follow up enable the case.
+          # - release-candidate
+
+      force:
+        description: Forced Release
+        default: false
+        type: boolean
+
+concurrency: ${{ github.workflow }}-${{ github.ref }}
+
+env:
+  NAPI_CLI_VERSION: 2.14.7
+  TURBO_VERSION: 2.3.3
+  NODE_LTS_VERSION: 20
+
+jobs:
+  start:
+    if: github.repository_owner == 'vercel'
+    runs-on: ubuntu-latest
+    env:
+      NEXT_TELEMETRY_DISABLED: 1
+      # we build a dev binary for use in CI so skip downloading
+      # canary next-swc binaries in the monorepo
+      NEXT_SKIP_NATIVE_POSTINSTALL: 1
+
+    environment: release-${{ github.event.inputs.releaseType }}
+    steps:
+      - name: Setup node
+        uses: actions/setup-node@v4
+        with:
+          node-version: ${{ env.NODE_LTS_VERSION }}
+          check-latest: true
+
+      # Since actions/checkout won't include the latest tag information,
+      # use the old clone workflow while still preserving branch specific
+      # checkout behavior to support backports.
+      # x-ref: https://github.com/vercel/next.js/pull/63167
+      - name: Clone Next.js repository
+        run: git clone https://github.com/vercel/next.js.git --depth=25 --single-branch --branch ${GITHUB_REF_NAME:-canary} .
+
+      - name: Check token
+        run: gh auth status
+        env:
+          GITHUB_TOKEN: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
+
+      - name: Get commit of the latest tag
+        run: echo "LATEST_TAG_COMMIT=$(git rev-list -n 1 $(git describe --tags --abbrev=0))" >> $GITHUB_ENV
+
+      - name: Get latest commit
+        run: echo "LATEST_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
+
+      - name: Check if new commits since last tag
+        if: ${{ github.event.inputs.releaseType != 'stable' && github.event.inputs.force != true }}
+        run: |
+          if [ "$LATEST_TAG_COMMIT" = "$LATEST_COMMIT" ]; then
+            echo "No new commits. Exiting..."
+            exit 1
+          fi
+
+      # https://github.com/actions/virtual-environments/issues/1187
+      - name: tune linux network
+        run: sudo ethtool -K eth0 tx off rx off
+
+      - name: Setup corepack
+        run: |
+          npm i -g corepack@0.31
+          corepack enable
+          pnpm --version
+
+      - id: get-store-path
+        run: echo STORE_PATH=$(pnpm store path) >> $GITHUB_OUTPUT
+
+      - uses: actions/cache@v4
+        timeout-minutes: 5
+        id: cache-pnpm-store
+        with:
+          path: ${{ steps.get-store-path.outputs.STORE_PATH }}
+          key: pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
+          restore-keys: |
+            pnpm-store-
+            pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
+
+      - run: pnpm install
+      - run: pnpm run build
+
+      - name: Create Release Pull Request
+        id: version-packages
+        uses: changesets/action@v1
+        with:
+          version: pnpm ci:version

filename: package.json
patch: @@ -13,6 +13,8 @@
     "lerna": "lerna",
     "dev": "turbo run dev --parallel",
     "pack-next": "tsx scripts/pack-next.ts",
+    "ci:version": "changeset version && pnpm install --no-frozen-lockfile",
+    "ci:publish": "changeset publish",
     "test-types": "tsc",
     "test-unit": "jest test/unit/ packages/next/ packages/font",
     "test-dev": "cross-env NEXT_TEST_MODE=dev pnpm testheadless",

filename: scripts/check-is-release.js
patch: @@ -13,10 +13,14 @@ const checkIsRelease = async () => {
 
   const versionString = commitMsg.split(' ').pop().trim()
   const publishMsgRegex = /^v\d{1,}\.\d{1,}\.\d{1,}(-\w{1,}\.\d{1,})?$/
+  const newPublishMsgRegex = /^Version Packages$/
 
   if (publishMsgRegex.test(versionString)) {
     console.log(versionString)
     process.exit(0)
+  } else if (newPublishMsgRegex.test(commitMsg)) {
+    console.log(commitMsg)
+    process.exit(0)
   } else {
     console.log('not publish commit', { commitId, commitMsg, versionString })
     process.exit(1)