-
Notifications
You must be signed in to change notification settings - Fork 267
/
release.ts
executable file
·275 lines (238 loc) · 9.31 KB
/
release.ts
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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#!/usr/bin/env ts-node
import * as execa from "execa"
import * as semver from "semver"
import * as inquirer from "inquirer"
import chalk from "chalk"
import parseArgs = require("minimist")
import deline = require("deline")
import { resolve } from "path"
const replace = require("replace-in-file")
type ReleaseType = "minor" | "patch" | "preminor" | "prepatch" | "prerelease"
const RELEASE_TYPES = ["minor", "patch", "preminor", "prepatch", "prerelease"]
const gardenRoot = resolve(__dirname, "..")
/**
* Performs the following steps to prepare for a release:
* 1. Check out to a branch named release-${version}
* 2. Bump the version in garden-service/package.json and garden-service/package-lock.json.
* 5. Update the changelog.
* 6. Add and commit CHANGELOG.md, garden-service/package.json and garden-service/package-lock.json
* 7. Tag the commit.
* 8. Push the tag. This triggers a CircleCI job that creates the release artifacts and publishes them to Github.
* 9. If we're making a minor release, update links to examples and re-push the tag.
* 10. Pushes the release branch to Github.
*
* Usage: ./bin/release.ts <minor | patch | preminor | prepatch | prerelease> [--force] [--dry-run]
*/
async function release() {
// Parse arguments
const argv = parseArgs(process.argv.slice(2))
const releaseType = <ReleaseType>argv._[0]
const force = !!argv.force
const dryRun = !!argv["dry-run"]
// Check if branch is clean
try {
await execa("git", ["diff", "--exit-code"], { cwd: gardenRoot })
} catch (_) {
throw new Error("Current branch has unstaged changes, aborting.")
}
if (!RELEASE_TYPES.includes(releaseType)) {
throw new Error(`Invalid release type ${releaseType}, available types are: ${RELEASE_TYPES.join(", ")}`)
}
// Update package.json versions
await execa("node_modules/.bin/lerna", [
"version", "--no-git-tag-version", "--yes", releaseType,
], { cwd: gardenRoot })
// Read the version from garden-service/package.json after setting it (rather than parsing the lerna output)
const version = "v" + require("../garden-service/package.json").version
const branchName = `release-${version}`
// Check if branch already exists locally
let localBranch
try {
localBranch = await execa("git", ["rev-parse", "--verify", branchName], { cwd: gardenRoot })
} catch (_) {
// no op
} finally {
if (localBranch) {
await rollBack()
throw new Error(`Branch ${branchName} already exists locally. Aborting.`)
}
}
// Check if branch already exists remotely
let remoteBranch
try {
remoteBranch = await execa(
"git",
["ls-remote", "--exit-code", "--heads", "origin", branchName],
{ cwd: gardenRoot },
)
} catch (_) {
// no op
} finally {
if (remoteBranch) {
await rollBack()
throw new Error(`Branch ${branchName} already exists remotely. Aborting.`)
}
}
// Check if user wants to continue
const proceed = await prompt(version)
if (!proceed) {
await rollBack()
return
}
// Lerna doesn't update package-lock.json so we need the following workaround.
// See this issue for details: https://github.com/lerna/lerna/issues/1415
console.log("Updating package-lock.json for all packages")
await execa("node_modules/.bin/lerna", ["clean", "--yes"], { cwd: gardenRoot })
await execa("node_modules/.bin/lerna", [
"bootstrap",
"--ignore-scripts",
"--",
"--package-lock-only",
"--no-audit",
], { cwd: gardenRoot })
// Pull remote tags
console.log("Pulling remote tags...")
await execa("git", ["fetch", "origin", "--tags", "-f"], { cwd: gardenRoot })
// Verify tag doesn't exist
const tags = (await execa("git", ["tag"], { cwd: gardenRoot })).stdout.split("\n")
if (tags.includes(version) && !force) {
await rollBack()
throw new Error(`Tag ${version} already exists. Use "--force" to override.`)
}
// Checkout to a release branch
console.log(`Checking out to branch ${branchName}...`)
await execa("git", ["checkout", "-b", branchName], { cwd: gardenRoot })
// Remove pre-release tags so they don't get included in the changelog
await stripPrereleaseTags(tags, version)
// Update changelog
console.log("Updating changelog...")
await execa("git-chglog", [
"--next-tag", version,
"--output", "CHANGELOG.md",
`..${version}`,
], { cwd: gardenRoot })
// Add and commit changes
console.log("Committing changes...")
await execa("git", [
"add",
"CHANGELOG.md",
"garden-service/package.json", "garden-service/package-lock.json",
"dashboard/package.json", "dashboard/package-lock.json",
], { cwd: gardenRoot })
await execa("git", [
"commit",
"-m", `chore(release): bump version to ${version}`,
], { cwd: gardenRoot })
// Tag the commit and push the tag
if (!dryRun) {
console.log("Pushing tag...")
await createTag(version, force)
}
// Reset local tag state (after stripping release tags)
await execa("git", ["fetch", "origin", "--tags"], { cwd: gardenRoot })
// For non pre-releases, we update links to examples in the docs so that they point to the relevant tag.
// E.g.: "github.com/garden-io/tree/v0.8.0/example/..." becomes "github.com/garden-io/tree/v0.9.0/example/..."
// Note that we do this after pushing the tag originally. This is because we check that links are valid in CI
// and the check would fail if the tag hasn't been created in the first place.
if (releaseType === "minor" || releaseType === "patch") {
console.log("Updating links to examples and re-pushing tag...")
await updateExampleLinks(version)
// Add and commit changes to example links
await execa("git", [
"add",
"README.md", "docs",
], { cwd: gardenRoot })
await execa("git", ["commit", "--amend", "--no-edit"], { cwd: gardenRoot })
// Tag the commit and force push the tag after updating the links (this triggers another CI build)
if (!dryRun) {
await createTag(version, true)
}
}
if (!dryRun) {
console.log("Pushing release branch...")
const pushArgs = ["push", "origin", branchName, "--no-verify"]
if (force) {
pushArgs.push("-f")
}
await execa("git", pushArgs, { cwd: gardenRoot })
}
console.log(deline`
\nVersion ${chalk.bold.cyan(version)} has been ${chalk.bold("tagged")}, ${chalk.bold("committed")},
and ${chalk.bold("pushed")} to Github! 🎉\n
A CI job that creates the release artifacts is currently in process: https://circleci.com/gh/garden-io/garden\n
Create a pull request for ${branchName} on Github by visting:
https://github.com/garden-io/garden/pull/new/${branchName}\n
Please refer to our contributing docs for the next steps:
https://github.com/garden-io/garden/blob/master/CONTRIBUTING.md
`)
}
async function createTag(version: string, force: boolean) {
// Tag the commit
const createTagArgs = ["tag", "-a", version, "-m", `chore(release): release ${version}`]
if (force) {
createTagArgs.push("-f")
}
await execa("git", createTagArgs, { cwd: gardenRoot })
// Push the tag
const pushTagArgs = ["push", "origin", version, "--no-verify"]
if (force) {
pushTagArgs.push("-f")
}
await execa("git", pushTagArgs, { cwd: gardenRoot })
}
async function updateExampleLinks(version: string) {
const options = {
files: ["docs/**/*.md", "README.md"],
from: /github\.com\/garden-io\/garden\/tree\/[^\/]*\/examples/g,
to: `github.com/garden-io/garden/tree/${version}/examples`,
}
const results = await replace(options)
console.log("Modified files:", results.filter(r => r.hasChanged).map(r => r.file).join(", "))
}
async function rollBack() {
// Undo any file changes. This is safe since we know the branch is clean.
console.log("Undoing file changes")
await execa("git", ["checkout", "."], { cwd: gardenRoot })
}
async function prompt(version: string): Promise<boolean> {
const message = deline`
Running this script will create a branch and a tag for ${chalk.bold.cyan(version)} and push them to Github.
This triggers a CI process that creates the release artifacts.\n
Are you sure you want to continue?
`
const ans = await inquirer.prompt({
name: "continue",
message,
})
return ans.continue.startsWith("y")
}
/**
* We don't include pre-release tags in the changelog except for the current release cycle.
* So if we're releasing, say, v0.9.1-3, we include the v0.9.1-0, v0.9.1-1, and v0.9.1-2 tags.
*
* Once we release v0.9.1, we remove the pre-release tags, so the changelog will only show the changes
* between v0.9.0 and v0.9.1.
*/
async function stripPrereleaseTags(tags: string[], version: string) {
const prereleaseTags = tags.filter(t => !!semver.prerelease(t))
for (const tag of prereleaseTags) {
// If we're not releasing a pre-release, we remove the tag. Or,
// if we are releasing a pre-release and the tag is not from the same cycle, we remove it.
// E.g., if the current tag is v0.5.0-2 and we're releasing v0.9.0-2, we remove it.
// If the current tag is v0.9.0-0 and we're releasing v0.9.0-2, we keep it.
if (!semver.prerelease(version) || semver.diff(version, tag) !== "prerelease") {
await execa("git", ["tag", "-d", tag])
}
}
// We also need to remove the "edge" tag
await execa("git", ["tag", "-d", "edge"])
}
(async () => {
try {
await release()
process.exit(0)
} catch (err) {
console.log(err)
process.exit(1)
}
})().catch(() => { })