-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathrelease.main.kts
181 lines (156 loc) · 6.83 KB
/
release.main.kts
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
#!/usr/bin/env kotlin
@file:DependsOn("com.github.ajalt.clikt:clikt-jvm:4.3.0")
@file:DependsOn("com.lordcodes.turtle:turtle:0.9.0")
@file:DependsOn("io.arrow-kt:arrow-core:1.2.4")
@file:DependsOn("io.github.z4kn4fein:semver-jvm:2.0.0")
@file:Suppress("MemberVisibilityCanBePrivate")
import arrow.core.Either.Companion.catch
import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.EitherNel
import arrow.core.NonEmptyList
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.zipOrAccumulate
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.terminal
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.convert
import com.github.ajalt.clikt.parameters.arguments.help
import com.github.ajalt.clikt.parameters.arguments.validate
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.help
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.mordant.rendering.TextColors.*
import com.github.ajalt.mordant.terminal.YesNoPrompt
import com.lordcodes.turtle.shellRun
import io.github.z4kn4fein.semver.Version
import io.github.z4kn4fein.semver.toVersionOrNull
import Release_main.ReleaseError.*
import java.lang.Thread.sleep
import kotlin.random.Random
import kotlin.system.exitProcess
// ------------------------------------------------------ error & success types
sealed class ReleaseError(val message: String) {
data object UncommittedChanges : ReleaseError("You have uncommitted changes.")
data class TagExists(val tag: String) : ReleaseError("The tag '$tag' already exists.")
}
data class Release(val releaseVersion: Version, val nextVersion: Version) {
val tag = "v$releaseVersion"
val snapshot = "$nextVersion-SNAPSHOT"
}
// ------------------------------------------------------ release command
class ReleaseCommand : CliktCommand(name = "release") {
val dryRun: Boolean by option()
.flag(default = false)
.help("Does not release, but only prints the steps")
val releaseVersion: Version by argument("release-version")
.help("The release version (as semver)")
.convert {
it.toVersionOrNull(strict = false) ?: fail("$it is not a valid semantic version")
}
val nextVersion: Version by argument("next-version")
.help("The next snapshot version (as semver)")
.convert {
it.toVersionOrNull(strict = false) ?: fail("$it is not a valid semantic version")
}.validate {
require(nextVersion > releaseVersion) {
"Next version must be greater than release version"
}
}
override fun run() {
when (val validate = validate(Release(releaseVersion, nextVersion))) {
is Left -> die(validate.value)
is Right -> if (really(validate.value)) {
release(validate.value)
} else {
terminal.warning("\nAborted")
}
}
}
// ------------------------------------------------------ validation
fun validate(release: Release): EitherNel<ReleaseError, Release> = either {
zipOrAccumulate(
{
// If there are no uncommitted changes, 'git diff-index' will return 0 and 'isRight()' is true.
// Otherwise, the command will return 1 and shellRun() will throw an exception
ensure(catch {
shellRun("git", listOf("diff-index", "--quiet", "HEAD"))
}.isRight()) { UncommittedChanges }
},
{
// if the output of 'git -l <tag>' is not empty, the tag already exists
ensure(shellRun("git", listOf("tag", "-l", release.tag)).isEmpty()) { TagExists(release.tag) }
}
) { _, _ -> release }
}
fun die(errors: NonEmptyList<ReleaseError>) {
errors.forEach { terminal.println("${terminal.theme.danger("Error:")} ${it.message}", stderr = true) }
exitProcess(1)
}
// ------------------------------------------------------ release
fun really(release: Release) = YesNoPrompt("""
|Codebase is ready to be released.
|
|If you decide to continue, this script will
|
| 1. Bump the version to ${cyan(release.releaseVersion.toString())}
| 2. Update the ${cyan("changelog")} (there should already be entries in the ${cyan("Unreleased")} section!)
| 3. Create a tag for ${cyan(release.tag)}
| 4. ${cyan("Commit")} and ${cyan("push")} to origin (which will trigger the ${cyan("release workflow")} at GitHub)
| 5. Bump the version to ${cyan(release.snapshot)}
| 6. ${cyan("Commit")} and ${cyan("push")} to origin
|
|Do you wish to continue
""".trimMargin(), terminal).ask() ?: false
fun step(message: String, code: () -> Unit) {
echo("${yellow("…")} $message", trailingNewline = false)
if (dryRun) {
sleep(Random.nextLong(1111, 4444))
} else {
code.invoke()
}
echo("\r${green("✓")}")
}
fun release(release: Release) {
echo()
step("Update to version ${release.releaseVersion}") {
shellRun("mvn", listOf("-DnewVersion=${release.releaseVersion}", "versions:set"))
}
step("Update README & changelog") {
shellRun {
command("sed", listOf(
"-i",
"''",
"-E",
"""s/<version>[0-9]+\.[0-9]+\.[0-9]+(.*)<\/version>/<version>${release.releaseVersion}\1<\/version>/""",
"README.md"
))
command("mvn", listOf("-DskipModules", "keepachangelog:release"))
}
}
step("Push changes") {
shellRun {
git.commitAllChanges("Release ${release.releaseVersion}")
git.push(remote = "origin", branch = "main")
}
}
step("Push tag") {
shellRun {
command("git", listOf("tag", release.tag))
command("push", listOf("--tags", "origin", "main"))
}
}
step("Update to version ${release.snapshot}") {
shellRun("mvn", listOf("-DnewVersion=${release.snapshot}", "versions:set"))
}
step("Push changes") {
shellRun {
git.commitAllChanges("Next is ${release.snapshot}")
git.push(remote = "origin", branch = "main")
}
}
echo("\n${green("All done")}. Watch the release workflow at https://github.com/hal/foundation/actions/workflows/release.yml")
}
}
ReleaseCommand().main(args)