-
-
Notifications
You must be signed in to change notification settings - Fork 6
/
CiReleaseModule.scala
238 lines (212 loc) · 8.01 KB
/
CiReleaseModule.scala
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
package io.kipp.mill.ci.release
import de.tobiasroeser.mill.vcs.version.VcsVersion
import mill._
import mill.api.Result
import mill.define.Command
import mill.define.ExternalModule
import mill.define.Task
import mill.eval.Evaluator
import mill.main.Tasks
import mill.scalalib.PublishModule
import mill.scalalib.publish.Artifact
import mill.scalalib.publish.SonatypePublisher
import java.nio.charset.StandardCharsets
import java.util.Base64
import scala.annotation.nowarn
import scala.util.control.NonFatal
/** Helper module extending PublishModule. We use our own Trait to have a bit
* more control over things and so that we can set the version for example for
* the user. This should hopefully just be one less thing they need to worry
* about. The entire goal of this is to make it frictionless for a user to
* release their project.
*/
trait CiReleaseModule extends PublishModule {
override def publishVersion: T[String] = T {
VcsVersion.vcsState().format(untaggedSuffix = "-SNAPSHOT")
}
/** Helper available to users be able to more easily use the new s01 and
* future hosts for sonatype by just setting this.
*/
def sonatypeHost: Option[SonatypeHost] = None
override def sonatypeUri: String = sonatypeHost match {
case Some(SonatypeHost.Legacy) => "https://oss.sonatype.org/service/local"
case Some(SonatypeHost.s01) => "https://s01.oss.sonatype.org/service/local"
case None => super.sonatypeUri
}
override def sonatypeSnapshotUri: String = sonatypeHost match {
case Some(SonatypeHost.Legacy) =>
"https://oss.sonatype.org/content/repositories/snapshots"
case Some(SonatypeHost.s01) =>
"https://s01.oss.sonatype.org/content/repositories/snapshots"
case None => super.sonatypeSnapshotUri
}
def stagingRelease: Boolean = true
}
// In here for the Discover import
@nowarn("msg=Unused import")
object ReleaseModule extends ExternalModule {
/** This is a replacement for the mill.scalalib.PublishModule/publishAll task
* that should basically work identically _but_ without requiring the user to
* pass in anything. It also sets up your gpg stuff and grabs the necessary
* env variables to publish to sonatype for you.
*/
def publishAll(ev: Evaluator): Command[Unit] = T.command {
val log = T.log
setupGpg()()
val env = envTask()
val modules = releaseModules(ev)
val uris = modules.map { m =>
(m.sonatypeUri, m.sonatypeSnapshotUri, m.stagingRelease)
}
val sonatypeUris = uris.map(_._1).toSet
val sonatypeSnapshotUris = uris.map(_._2).toSet
val stagingReleases = uris.map(_._3).toSet
val allPomSettings = modules.map { m =>
Eval.evalOrThrow(ev)(m.pomSettings)
}
def mustBeUniqueMsg[T](value: String, values: Set[T]): String = {
s"""It looks like you have multiple different values set for ${value}
|
|${values.mkString(" - ", " - \n", "")}
|
|In order to use publishAll these should all be the same.""".stripMargin
}
val result: Result[Unit] = if (sonatypeUris.size != 1) {
Result.Failure[Unit](mustBeUniqueMsg("sonatypeUri", sonatypeUris))
} else if (sonatypeSnapshotUris.size != 1) {
Result.Failure[Unit](
mustBeUniqueMsg("sonatypeSnapshotUri", sonatypeSnapshotUris)
)
} else if (stagingReleases.size != 1) {
Result.Failure[Unit](
mustBeUniqueMsg("stagingRelease", stagingReleases)
)
} else if (allPomSettings.flatMap(_.licenses).isEmpty) {
Result.Failure[Unit](
"You must have a license set in your PomSettings or Sonatype will silently fail."
)
} else if (allPomSettings.flatMap(_.developers).isEmpty) {
Result.Failure[Unit](
"You must have a at least one developer set in your PomSettings or Sonatype will silently fail."
)
} else {
// Not ideal here to call head but we just checked up above and already failed
// if they aren't size 1.
val sonatypeUri = sonatypeUris.head
val sonatypeSnapshotUri = sonatypeSnapshotUris.head
val stagingRelease = stagingReleases.head
if (env.isTag) {
log.info("Tag push detected, publishing a new stable release")
log.info(s"Publishing to ${sonatypeUri}")
} else {
log.info("No new tag detected, publishing a SNAPSHOT")
log.info(s"Publishing to ${sonatypeSnapshotUri}")
}
// At this point since we pretty much have everything we need we mimic publishAll from here:
// https://github.com/com-lihaoyi/mill/blob/d944b3cf2aa9a286262e7963a7fea63e1986c627/scalalib/src/PublishModule.scala#L214-L245
val artifactPaths: Seq[(Seq[(os.Path, String)], Artifact)] =
T.sequence(artifacts(ev).value)().map {
case PublishModule.PublishData(a, s) =>
(s.map { case (p, f) => (p.path, f) }, a)
}
new SonatypePublisher(
sonatypeUri,
sonatypeSnapshotUri,
env.sonatypeCreds,
signed = true,
Seq(
s"--passphrase=${env.pgpPassword}",
"--no-tty",
"--pinentry-mode",
"loopback",
"--batch",
"--yes",
"--armor",
"--detach-sign"
),
readTimeout = 60000,
connectTimeout = 5000,
log,
workspace = os.pwd,
env = sys.env,
awaitTimeout = 600000,
stagingRelease = stagingRelease
).publishAll(
release = true,
artifactPaths: _*
)
Result.Success(())
}
result
}
/** All the publish artifacts for the release modules.
*/
private def artifacts(ev: Evaluator) = {
val modules = releaseModules(ev).map { m => m.publishArtifacts }
Tasks(modules)
}
private val envTask: Task[Env] = setupEnv()
/** Ensures that your key is imported prio to signing and publishing.
*/
def setupGpg(): Task[Unit] = T.task {
T.log.info("Attempting to setup gpg")
val pgpSecret = envTask().pgpSecret.replaceAll("\\s", "")
try {
val decoded = new String(
Base64.getDecoder.decode(pgpSecret.getBytes(StandardCharsets.UTF_8))
)
// https://dev.gnupg.org/T2313
val imported = os
.proc("gpg", "--batch", "--import", "--no-tty")
.call(stdin = decoded)
if (imported.exitCode != 0)
Result.Failure(
"Unable to import your pgp key. Make sure your secret is correct."
)
} catch {
case e: IllegalArgumentException =>
Result.Failure(
s"Invalid secret, unable to decode it: ${e.getMessage()}"
)
case NonFatal(e) => Result.Failure(e.getMessage())
}
}
/** Ensures that the user has all the ENV variable set up that are necessary
* to both take care of pgp related stuff and also publish to sonatype.
* @return
* a Env Task
*/
private def setupEnv(): Task[Env] = T.input {
val env = T.ctx().env
val pgpSecret = env.get("PGP_SECRET")
val pgpPassword = env.get("PGP_PASSPHRASE")
val isTag = env.get("GITHUB_REF").exists(_.startsWith("refs/tags"))
val sonatypeUser = env.get("SONATYPE_USERNAME")
val sonatypePassword = env.get("SONATYPE_PASSWORD")
if (pgpSecret.isEmpty) {
Result.Failure("Missing PGP_SECRET. Make sure you have it set.")
} else if (pgpPassword.isEmpty) {
Result.Failure("Missing PGP_PASSPHRASE. Make sure you have it set.")
} else if (sonatypeUser.isEmpty) {
Result.Failure("Missing SONATYPE_USERNAME. Make sure you have it set.")
} else if (sonatypePassword.isEmpty) {
Result.Failure("Missing SONATYPE_PASSWORD. Make sure you have it set.")
} else {
Env(
pgpSecret.get,
pgpPassword.get,
isTag,
sonatypeUser.get,
sonatypePassword.get
)
}
}
/** Gathers all the CiReleaseModules, which is used to determine what should
* be released
*/
private def releaseModules(ev: Evaluator) =
ev.rootModule.millInternal.modules.collect { case m: CiReleaseModule => m }
import Discover._
lazy val millDiscover: mill.define.Discover[this.type] =
mill.define.Discover[this.type]
}