Repository: vercel/next.js
Pull request: Add create release branch workflow

This creates a new workflow to create a release branch and apply the necessary configs to allow releasing from that branch as this is quite common for backports. 

Changed files:
filename: .github/workflows/create_release_branch.yml
patch: @@ -0,0 +1,77 @@
+on:
+  workflow_dispatch:
+    inputs:
+      branchName:
+        description: name of branch to create (next-15-4)
+        required: true
+        type: string
+
+      tagName:
+        description: Tag to start the branch from (v15.4.1)
+        type: string
+        required: true
+
+    secrets:
+      RELEASE_BOT_GITHUB_TOKEN:
+        required: true
+
+name: Create Release Branch
+
+env:
+  NAPI_CLI_VERSION: 2.18.4
+  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
+
+    steps:
+      - name: Setup node
+        uses: actions/setup-node@v4
+        with:
+          node-version: 18
+          check-latest: true
+
+      - 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 }}
+
+      # 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: node ./scripts/create-release-branch.js --branch-name ${{ github.event.inputs.branchName }} --tag-name ${{ github.event.inputs.tagName }}
+        env:
+          RELEASE_BOT_GITHUB_TOKEN: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}

filename: scripts/create-release-branch.js
patch: @@ -0,0 +1,129 @@
+// @ts-check
+const fs = require('fs')
+const path = require('path')
+const execa = require('execa')
+const resolveFrom = require('resolve-from')
+
+async function main() {
+  const args = process.argv
+  const branchName = args[args.indexOf('--branch-name') + 1]
+  const tagName = args[args.indexOf('--tag-name') + 1]
+
+  if (!branchName) {
+    throw new Error('branchName value is missing!')
+  }
+
+  if (!tagName || !tagName.startsWith('v')) {
+    throw new Error('tagName value is invalid "' + tagName + '"')
+  }
+
+  const githubToken = process.env.RELEASE_BOT_GITHUB_TOKEN
+
+  if (!githubToken) {
+    console.log(`Missing RELEASE_BOT_GITHUB_TOKEN`)
+    return
+  }
+
+  const configStorePath = resolveFrom(
+    path.join(process.cwd(), 'node_modules/release'),
+    'configstore'
+  )
+  const ConfigStore = require(configStorePath)
+
+  const config = new ConfigStore('release')
+  config.set('token', githubToken)
+
+  await execa(
+    `git remote set-url origin https://nextjs-bot:${githubToken}@github.com/vercel/next.js.git`,
+    { stdio: 'inherit', shell: true }
+  )
+  await execa(`git config user.name "nextjs-bot"`, {
+    stdio: 'inherit',
+    shell: true,
+  })
+  await execa(`git config user.email "it+nextjs-bot@vercel.com"`, {
+    stdio: 'inherit',
+    shell: true,
+  })
+  await execa(`git checkout -b "${branchName}"`, {
+    stdio: 'inherit',
+    shell: true,
+  })
+  await execa(`git reset --hard ${tagName}`, {
+    stdio: 'inherit',
+    shell: true,
+  })
+  const lernaPath = path.join(__dirname, '..', 'lerna.json')
+  const existingLerna = JSON.parse(
+    await fs.promises.readFile(lernaPath, 'utf8')
+  )
+  existingLerna.command.publish.allowBranch.push(branchName)
+
+  await fs.promises.writeFile(lernaPath, JSON.stringify(existingLerna, null, 2))
+
+  const buildAndDeployPath = path.join(
+    __dirname,
+    '..',
+    '.github',
+    'workflows',
+    'build_and_deploy.yml'
+  )
+  const buildAndDeploy = await fs.promises.readFile(buildAndDeployPath, 'utf8')
+  await fs.promises.writeFile(
+    buildAndDeployPath,
+    buildAndDeploy.replace(/refs\/heads\/canary/g, `refs/heads/${branchName}`)
+  )
+
+  const buildAndTestPath = path.join(
+    __dirname,
+    '..',
+    '.github',
+    'workflows',
+    'build_and_test.yml'
+  )
+  const buildAndTest = await fs.promises.readFile(buildAndTestPath, 'utf8')
+  await fs.promises.writeFile(
+    buildAndTestPath,
+    buildAndTest.replace(`['canary']`, `['${branchName}']`)
+  )
+
+  await execa(`git add .`, {
+    stdio: 'inherit',
+    shell: true,
+  })
+  await execa(`git commit -m "setup release branch"`, {
+    stdio: 'inherit',
+    shell: true,
+  })
+  await execa(`git push origin "${branchName}"`, {
+    stdio: 'inherit',
+    shell: true,
+  })
+
+  console.log(`Waiting 5s before updating branch rules`)
+  await new Promise((resolve) => setTimeout(resolve, 5_000))
+
+  const updateEnvironmentRes = await fetch(
+    'https://api.github.com/repos/vercel/next.js/environments/release-stable/deployment-branch-policies',
+    {
+      method: 'POST',
+      headers: {
+        Accept: 'application/vnd.github+json',
+        Authorization: `Bearer ${githubToken}`,
+        'X-GitHub-Api-Version': '2022-11-28',
+      },
+      body: JSON.stringify({ name: branchName }),
+    }
+  )
+
+  if (!updateEnvironmentRes.ok) {
+    console.error(
+      { status: updateEnvironmentRes.status },
+      await updateEnvironmentRes.text()
+    )
+    throw new Error(`Failed to update environment branch rules`)
+  }
+  console.log(`Successfully updated deployment environment branch rules`)
+}
+
+main()