mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
665d3458b6
Change detection (`git diff HEAD~1`) only covers the whole change set when the checkout is the PR merge commit. `build-and-test` runs on `pull_request` while `build-and-deploy` ran on `push` for upstream PR branches, so a multi-commit push whose last commit was docs-only made `build-and-deploy` skip the preview build while deploy tests in `build-and-test` waited up to 30 minutes for a preview tarball that never got published. `build-and-deploy` now triggers on the same events as `build-and-test` (`push` to the default branch and `pull_request`), while still accepting release tags and workflow dispatch. The `deploy-target` fork guard is removed since upstream PRs are no longer covered by a push run, and `create-release-branch.js` now rewrites the workflow's branch list alongside the existing `refs/heads/canary` replacement so release branches keep their staging deploys. Preview tarball creation now uses the PR head sha instead of falling back to the merge-commit sha on `pull_request` `opened` events (`github.event.after` only exists on `synchronize`), matching the sha that `upload_preview_tarballs.yml` publishes under and that tests poll for. This fallback was a pre-existing bug that only affected fork PRs; it would have started affecting every upstream PR's first run once previews moved to the `pull_request` event.
170 lines
4.7 KiB
JavaScript
170 lines
4.7 KiB
JavaScript
// @ts-check
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const execa = require('execa')
|
|
const {
|
|
configureGitHubAuth,
|
|
getGitHubToken,
|
|
getGitHubTokenMissingMessage,
|
|
} = require('./release-github-auth')
|
|
const {
|
|
githubRequest,
|
|
createSignedCommit,
|
|
upsertBranchRef,
|
|
} = require('./github-utils/signed-commit')
|
|
|
|
const REPO_OWNER = 'vercel'
|
|
const REPO_NAME = 'next.js'
|
|
|
|
/**
|
|
* Fail fast, before any file mutation, if the branch already exists.
|
|
*
|
|
* This doubles as the API preflight: it exercises the same git-data path the
|
|
* script later writes to (blobs/trees/commits/refs), so a token missing those
|
|
* grants fails here rather than midway through creating objects. A plain
|
|
* repository GET would succeed with any valid installation token and so could
|
|
* never catch that.
|
|
*/
|
|
async function verifyBranchIsAvailable(token, branch) {
|
|
let existing
|
|
|
|
try {
|
|
existing = await githubRequest(
|
|
token,
|
|
'GET',
|
|
`/repos/${REPO_OWNER}/${REPO_NAME}/git/ref/heads/${branch}`
|
|
)
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
|
|
// A 404 is the expected, successful outcome: the branch is available.
|
|
if (message.includes('failed (404)')) {
|
|
console.log(`Verified GitHub API access; branch ${branch} is available`)
|
|
return
|
|
}
|
|
|
|
throw error
|
|
}
|
|
|
|
throw new Error(
|
|
`Branch ${branch} already exists (at ${existing.object?.sha}). ` +
|
|
`Delete it or choose a different branch name before re-running.`
|
|
)
|
|
}
|
|
|
|
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 = getGitHubToken()
|
|
|
|
if (!githubToken) {
|
|
console.log(getGitHubTokenMissingMessage())
|
|
return
|
|
}
|
|
|
|
await configureGitHubAuth(githubToken)
|
|
await verifyBranchIsAvailable(githubToken, branchName)
|
|
|
|
await execa('git', ['checkout', '-b', branchName], {
|
|
stdio: 'inherit',
|
|
})
|
|
await execa('git', ['fetch', 'origin', tagName, '--tags'], {
|
|
stdio: 'inherit',
|
|
})
|
|
await execa('git', ['reset', '--hard', tagName], {
|
|
stdio: 'inherit',
|
|
})
|
|
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
|
|
// The push trigger is limited to the default branch, same as
|
|
// build_and_test.yml below, so point it at the release branch as well.
|
|
.replace(`branches: ['canary']`, `branches: ['${branchName}']`)
|
|
.replace(/refs\/heads\/canary/g, `refs/heads/${branchName}`)
|
|
)
|
|
|
|
const buildAndTestPath = path.join(
|
|
__dirname,
|
|
'..',
|
|
'.github',
|
|
'workflows',
|
|
'build_and_test.yml'
|
|
)
|
|
let buildAndTest = await fs.promises.readFile(buildAndTestPath, 'utf8')
|
|
buildAndTest = buildAndTest
|
|
.replace(`['canary']`, `['${branchName}']`)
|
|
.replace(/[\s]{1,}('test-new-tests-.+',)/g, '')
|
|
|
|
await fs.promises.writeFile(buildAndTestPath, buildAndTest)
|
|
|
|
const commitMessage = 'setup release branch'
|
|
|
|
await execa('git', ['add', '.'], {
|
|
stdio: 'inherit',
|
|
})
|
|
await execa('git', ['commit', '-m', commitMessage], {
|
|
stdio: 'inherit',
|
|
})
|
|
|
|
// Branch protection requires signed commits, so create the commit on the
|
|
// remote as a GitHub-signed commit via the REST API instead of running
|
|
// `git push` (which would push the unsigned local commit).
|
|
//
|
|
// Release tags are annotated tag objects, so dereference to the underlying
|
|
// commit -- the tag object's SHA is not valid as a commit parent.
|
|
const { stdout: baseSha } = await execa('git', [
|
|
'rev-parse',
|
|
`${tagName}^{commit}`,
|
|
])
|
|
const { stdout: localCommitSha } = await execa('git', ['rev-parse', 'HEAD'])
|
|
|
|
const signedCommit = await createSignedCommit({
|
|
token: githubToken,
|
|
owner: REPO_OWNER,
|
|
repo: REPO_NAME,
|
|
baseSha: baseSha.trim(),
|
|
localCommitSha: localCommitSha.trim(),
|
|
message: commitMessage,
|
|
})
|
|
|
|
await upsertBranchRef({
|
|
token: githubToken,
|
|
owner: REPO_OWNER,
|
|
repo: REPO_NAME,
|
|
branch: branchName,
|
|
sha: signedCommit.sha,
|
|
})
|
|
|
|
console.log(
|
|
`Created branch ${branchName} at signed commit ${signedCommit.sha}`
|
|
)
|
|
}
|
|
|
|
main()
|