-
Notifications
You must be signed in to change notification settings - Fork 175
/
create-release.js
259 lines (233 loc) · 9.16 KB
/
create-release.js
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
'use strict'
// Creates a release asset in github for a tag with autogenerated changelogs following
// conventional-commit specifications.
//
// Usage: node create-release.js <token> <tag> [--deploy]
// token should be a github token capable of creating a release
// tag should be the tag for which the release should be created
// --deploy should be specified to actually create the release; if not specified, the script
// will print what it would do but not do it
// --allow-old allows the use of a tag that does not point to one of the last 100 commits. We
// only check the last 100 commits for the recent tag to not bog everything down,
// so normally if you try to make a release for a really old commit the check that
// prevents you from getting a conventional-changelog for the wrong thing will
// false-positive. Set this flag to not do that check.
// The release will be created as a prerelease if this is a prerelease tag.
// It will always be created as a draft so that humans can review it before it goes live.
//
// The changelog content will be in the body of the release.
const parseArgs = require('./lib/parseArgs')
const conventionalChangelog = require('conventional-changelog')
const semver = require('semver')
const { Octokit } = require('@octokit/rest')
const USAGE =
'\nUsage:\n node ./scripts/deploy/create-release <token> <tag> [--deploy] [--allow-old]'
// The allowed types of release. The order matters here - order in this array determines the
// "release kind greater than or equal to" logic for generating changelogs. A version matching
// one of the entries in this array will have its changelogs generated from the closest version
// matching the same type or a type to its right.
const ALLOWED_VERSION_TYPES = ['alpha', 'beta', 'candidate', 'production']
const REPO_DETAILS = {
owner: 'Opentrons',
repo: 'opentrons',
}
// The release kind is normally just the semver preproduction stage, but we need to account
// for PD using candidate-a, candidate-b etc - semver preproduction stage is separated from
// sequence by a . normally (i.e. alpha.0, beta.32), so semver.prerelease('candidate-a') gives
// you 'candidate-a' and we want 'candidate'
const releaseKind = version =>
(semver.prerelease(version)?.at(0) ?? 'production').split('-')[0]
const releasePriorityGreaterThanOrEqual = (kindA, kindB) =>
ALLOWED_VERSION_TYPES.indexOf(kindA) >= ALLOWED_VERSION_TYPES.indexOf(kindB)
const titleForProject = project => project.replaceAll('-', ' ')
const titleForRelease = (project, version) =>
`${titleForProject(project)} version ${version}`
// Return the version to build a changelog from, which is the most recent version whose prerelease
// level is equal to or greater than the current tag. So
// - if currentVersion is a production version (no prerelease data), use the last production version
// - if currentVersion is a beta version, use the most recent version that is either beta or production
// - if currentVersion is an alpha version, use the most recent version of any kind
// currentVersion should be the version-part of the tag (i.e. not including project@, not including v)
// previousVersions should be an array of version-parts (see above) in descending semver order
function versionPrevious(currentVersion, previousVersions) {
const currentReleaseKind = releaseKind(currentVersion)
if (!ALLOWED_VERSION_TYPES.includes(currentReleaseKind)) {
throw new Error(
`Prerelease tag ${currentReleaseKind} is not one of ${ALLOWED_VERSION_TYPES.join(
', '
)}`
)
}
const from = previousVersions.indexOf(currentVersion)
const notIncluding = previousVersions.slice(from + 1)
const releasesOfGEQKind = notIncluding.filter(version =>
releasePriorityGreaterThanOrEqual(releaseKind(version), currentReleaseKind)
)
return releasesOfGEQKind.length === 0 ? null : releasesOfGEQKind[0]
}
async function gitVersion() {
let imported
if (imported === undefined) {
imported = await import('../git-version.mjs')
}
return imported
}
async function monorepoGit() {
return await (await gitVersion()).monorepoGit()
}
async function detailsFromTag(tag) {
return await (await gitVersion()).detailsFromTag(tag)
}
async function tagFromDetails(project, version) {
return await (await gitVersion()).tagFromDetails(project, version)
}
async function prefixForProject(project) {
return (await gitVersion()).prefixForProject(project)
}
async function versionDetailsFromGit(tag, allowOld) {
if (!allowOld) {
const git = await monorepoGit()
const last100 = await git.log({ from: 'HEAD~100', to: 'HEAD' })
if (!last100.all.some(commit => commit.refs.includes('tag: ' + tag))) {
throw new Error(
`Cannot find tag ${tag} in last 100 commits. You must run this script from a ref with ` +
`the tag in its history to correctly generate a changelog. If your tag is very old but ` +
`is definitely in whatever branch is checked out, use --allow-old.`
)
}
}
const [project, currentVersion] = await detailsFromTag(tag)
const prefix = await prefixForProject(project)
const allTags = (await (await monorepoGit()).tags([prefix + '*'])).all
if (!allTags.includes(tag)) {
throw new Error(
`Tag ${tag} does not exist - create it before running this script`
)
}
const allVersions = await Promise.all(allTags.map(tag => detailsFromTag(tag)))
const sortedVersions = allVersions
.map(details => details[1])
.sort(semver.compare)
.reverse()
const previousVersion = versionPrevious(currentVersion, sortedVersions)
return [project, currentVersion, previousVersion]
}
async function buildChangelog(project, currentVersion, previousVersion) {
if (previousVersion === null) {
console.warn(
`Cannot find an appropriate previous version of ${project} for ` +
`version ${currentVersion}. ` +
`On the first run for a given project this script will emit an ` +
`empty changelog.`
)
return (
`## ${currentVersion}` + `\nFirst release for ${titleForProject(project)}`
)
}
const previousTag = await tagFromDetails(project, previousVersion)
const currentTag = await tagFromDetails(project, currentVersion)
const prefix = await prefixForProject(project)
const changelogStream = conventionalChangelog(
{ preset: 'angular', tagPrefix: prefix },
{
version: currentVersion,
currentTag,
previousTag,
host: 'https://github.com',
owner: REPO_DETAILS.owner,
repository: REPO_DETAILS.repo,
linkReferences: true,
},
{ from: previousTag }
)
const chunks = []
for await (const chunk of changelogStream) {
chunks.push(chunk.toString())
}
// For some reason, later chunks include the contents of earlier chunks so we need to
// accumulate chunks in reverse and drop earlier chunks that are included in later ones
const changelog = chunks
.reverse()
.reduce(
(accum, chunk) => (accum.includes(chunk.trim()) ? accum : chunk + accum),
''
)
return changelog
}
async function createRelease(token, tag, project, version, changelog, deploy) {
const title = titleForRelease(project, version)
const isPre = !!semver.prerelease(version)
if (deploy) {
const octokit = new Octokit({
auth: token,
userAgent: 'Opentrons Release Creator',
})
const { data } = await octokit.rest.repos.createRelease({
owner: REPO_DETAILS.owner,
repo: REPO_DETAILS.repo,
tag_name: tag,
name: title,
body: changelog,
draft: true,
prerelease: isPre,
})
return data.html_url
} else {
console.log(`${tag} ${title}\n${changelog}\n${isPre ? '\nprerelease' : ''}`)
return `http:https://github.com/${REPO_DETAILS.owner}/${REPO_DETAILS.repo}/releases/${tag}`
}
}
function truncateAndAnnotate(changelog, limit, prevtag, thistag) {
const linkmessage = `\n...and more! Log link: https://github.com/${REPO_DETAILS.owner}/${REPO_DETAILS.repo}/compare/${prevtag}...${thistag}`
const limitWithMessage = limit - linkmessage.length
if (changelog.length < limitWithMessage) {
return changelog
}
const truncated = changelog.substring(0, limitWithMessage)
return truncated + linkmessage
}
async function main() {
const { args, flags } = parseArgs(process.argv.slice(2))
const [token, tag] = args
if (!token || !tag) {
throw new Error(USAGE)
}
const deploy = flags.includes('--deploy')
const allowOld = flags.includes('--allow-old')
const [
project,
currentVersion,
previousVersion,
] = await versionDetailsFromGit(tag, allowOld)
const prefix = await prefixForProject(project)
const changelog = await buildChangelog(
project,
currentVersion,
previousVersion
)
const truncatedChangelog = truncateAndAnnotate(
changelog,
10000,
prefix + previousVersion,
prefix + currentVersion
)
return await createRelease(
token,
tag,
project,
currentVersion,
truncatedChangelog,
deploy
)
}
module.exports = { versionPrevious: versionPrevious }
if (require.main === module) {
main()
.then(url => {
console.log('Release created:', url)
})
.catch(error => {
console.error('Release failed:', error)
process.exitCode = -1
})
}