-
Notifications
You must be signed in to change notification settings - Fork 31.9k
Expand file tree
/
Copy pathcreate-release-branch.js
More file actions
169 lines (145 loc) · 4.71 KB
/
Copy pathcreate-release-branch.js
File metadata and controls
169 lines (145 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// @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()