Skip to content

Commit 5965654

Browse files
committed
inline publishing plugin and increase timeouts
1 parent 93378b8 commit 5965654

File tree

3 files changed

+307
-4
lines changed

3 files changed

+307
-4
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
- name: Publish ${{ github.ref }}
7272
if: startsWith(github.ref, 'refs/tags/v') ||
7373
(github.event_name == 'workflow_dispatch' && github.event.inputs.publishSnapshot == 'true')
74-
run: mill -i io.kipp.mill.ci.release.ReleaseModule/publishAll
74+
run: mill -i InternalReleaseModule.publishAll
7575
env:
7676
PGP_PASSPHRASE: ""
7777
PGP_SECRET: ${{ secrets.PGP_SECRET }}

buildSetup.sc

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import $ivy.`com.lihaoyi::mill-contrib-bloop:`
22
import $ivy.`com.lihaoyi::mill-contrib-scalapblib:`
33
import $ivy.`com.lihaoyi::mill-contrib-buildinfo:`
4-
import $ivy.`io.chris-kipp::mill-ci-release::0.1.9`
54
import $ivy.`com.lewisjkl::header-mill-plugin::0.0.3`
65
import $file.buildDeps
76

87
import header._
9-
import io.kipp.mill.ci.release.CiReleaseModule
10-
import io.kipp.mill.ci.release.SonatypeHost
8+
import $file.plugins.ci.CiReleaseModules
9+
import CiReleaseModules.{CiReleaseModule, SonatypeHost, ReleaseModule, Discover}
1110
import mill._
1211
import mill.scalajslib.api.ModuleKind
1312
import mill.scalajslib.ScalaJSModule
@@ -18,6 +17,23 @@ import mill.contrib.buildinfo.BuildInfo
1817
import mill.scalalib.api.ZincWorkerUtil
1918

2019
import scala.Ordering.Implicits._
20+
import mill.eval.Evaluator
21+
22+
object InternalReleaseModule extends Module {
23+
24+
/** This is a replacement for the mill.scalalib.PublishModule/publishAll task
25+
* that should basically work identically _but_ without requiring the user to
26+
* pass in anything. It also sets up your gpg stuff and grabs the necessary
27+
* env variables to publish to sonatype for you.
28+
*/
29+
def publishAll(ev: Evaluator): Command[Unit] = {
30+
ReleaseModule.publishAll(ev)
31+
}
32+
33+
import Discover._
34+
lazy val millDiscover: mill.define.Discover[this.type] =
35+
mill.define.Discover[this.type]
36+
}
2137

2238
object ScalaVersions {
2339
val scala212 = "2.12.18"

plugins/ci/CiReleaseModules.sc

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version::0.4.0`
2+
import de.tobiasroeser.mill.vcs.version.VcsVersion
3+
import mill._
4+
import mill.api.Result
5+
import mill.define.Command
6+
import mill.define.ExternalModule
7+
import mill.define.Task
8+
import mill.eval.Evaluator
9+
import mill.main.Tasks
10+
import mill.scalalib.PublishModule
11+
import mill.scalalib.publish.Artifact
12+
import mill.scalalib.publish.SonatypePublisher
13+
14+
import java.nio.charset.StandardCharsets
15+
import java.util.Base64
16+
import scala.annotation.nowarn
17+
import scala.util.control.NonFatal
18+
19+
// Adapted from https://github.com/ckipp01/mill-ci-release in order to customize timeouts
20+
21+
/** Helper module extending PublishModule. We use our own Trait to have a bit
22+
* more control over things and so that we can set the version for example for
23+
* the user. This should hopefully just be one less thing they need to worry
24+
* about. The entire goal of this is to make it frictionless for a user to
25+
* release their project.
26+
*/
27+
trait CiReleaseModule extends PublishModule {
28+
override def publishVersion: T[String] = T {
29+
VcsVersion.vcsState().format(untaggedSuffix = "-SNAPSHOT")
30+
}
31+
32+
/** Helper available to users be able to more easily use the new s01 and
33+
* future hosts for sonatype by just setting this.
34+
*/
35+
def sonatypeHost: Option[SonatypeHost] = None
36+
37+
override def sonatypeUri: String = sonatypeHost match {
38+
case Some(SonatypeHost.Legacy) => "https://oss.sonatype.org/service/local"
39+
case Some(SonatypeHost.s01) => "https://s01.oss.sonatype.org/service/local"
40+
case None => super.sonatypeUri
41+
}
42+
43+
override def sonatypeSnapshotUri: String = sonatypeHost match {
44+
case Some(SonatypeHost.Legacy) =>
45+
"https://oss.sonatype.org/content/repositories/snapshots"
46+
case Some(SonatypeHost.s01) =>
47+
"https://s01.oss.sonatype.org/content/repositories/snapshots"
48+
case None => super.sonatypeSnapshotUri
49+
}
50+
51+
def stagingRelease: Boolean = true
52+
}
53+
54+
// In here for the Discover import
55+
@nowarn("msg=Unused import")
56+
object ReleaseModule extends Module {
57+
58+
/** This is a replacement for the mill.scalalib.PublishModule/publishAll task
59+
* that should basically work identically _but_ without requiring the user to
60+
* pass in anything. It also sets up your gpg stuff and grabs the necessary
61+
* env variables to publish to sonatype for you.
62+
*/
63+
def publishAll(ev: Evaluator): Command[Unit] = T.command {
64+
val log = T.log
65+
setupGpg()()
66+
val env = envTask()
67+
68+
val modules = releaseModules(ev)
69+
70+
val uris = modules.map { m =>
71+
(m.sonatypeUri, m.sonatypeSnapshotUri, m.stagingRelease)
72+
}
73+
74+
val sonatypeUris = uris.map(_._1).toSet
75+
val sonatypeSnapshotUris = uris.map(_._2).toSet
76+
val stagingReleases = uris.map(_._3).toSet
77+
78+
val allPomSettings = modules.map { m =>
79+
Eval.evalOrThrow(ev)(m.pomSettings)
80+
}
81+
82+
def mustBeUniqueMsg[T](value: String, values: Set[T]): String = {
83+
s"""It looks like you have multiple different values set for ${value}
84+
|
85+
|${values.mkString(" - ", " - \n", "")}
86+
|
87+
|In order to use publishAll these should all be the same.""".stripMargin
88+
}
89+
90+
val result: Result[Unit] = if (sonatypeUris.size != 1) {
91+
Result.Failure[Unit](mustBeUniqueMsg("sonatypeUri", sonatypeUris))
92+
} else if (sonatypeSnapshotUris.size != 1) {
93+
Result.Failure[Unit](
94+
mustBeUniqueMsg("sonatypeSnapshotUri", sonatypeSnapshotUris)
95+
)
96+
} else if (stagingReleases.size != 1) {
97+
Result.Failure[Unit](
98+
mustBeUniqueMsg("stagingRelease", stagingReleases)
99+
)
100+
} else if (allPomSettings.flatMap(_.licenses).isEmpty) {
101+
Result.Failure[Unit](
102+
"You must have a license set in your PomSettings or Sonatype will silently fail."
103+
)
104+
} else if (allPomSettings.flatMap(_.developers).isEmpty) {
105+
Result.Failure[Unit](
106+
"You must have a at least one developer set in your PomSettings or Sonatype will silently fail."
107+
)
108+
} else {
109+
// Not ideal here to call head but we just checked up above and already failed
110+
// if they aren't size 1.
111+
val sonatypeUri = sonatypeUris.head
112+
val sonatypeSnapshotUri = sonatypeSnapshotUris.head
113+
val stagingRelease = stagingReleases.head
114+
if (env.isTag) {
115+
log.info("Tag push detected, publishing a new stable release")
116+
log.info(s"Publishing to ${sonatypeUri}")
117+
} else {
118+
log.info("No new tag detected, publishing a SNAPSHOT")
119+
log.info(s"Publishing to ${sonatypeSnapshotUri}")
120+
}
121+
122+
// At this point since we pretty much have everything we need we mimic publishAll from here:
123+
// https://github.com/com-lihaoyi/mill/blob/d944b3cf2aa9a286262e7963a7fea63e1986c627/scalalib/src/PublishModule.scala#L214-L245
124+
val artifactPaths: Seq[(Seq[(os.Path, String)], Artifact)] =
125+
T.sequence(artifacts(ev).value)().map {
126+
case PublishModule.PublishData(a, s) =>
127+
(s.map { case (p, f) => (p.path, f) }, a)
128+
}
129+
130+
new SonatypePublisher(
131+
sonatypeUri,
132+
sonatypeSnapshotUri,
133+
env.sonatypeCreds,
134+
signed = true,
135+
Seq(
136+
s"--passphrase=${env.pgpPassword}",
137+
"--no-tty",
138+
"--pinentry-mode",
139+
"loopback",
140+
"--batch",
141+
"--yes",
142+
"--armor",
143+
"--detach-sign"
144+
),
145+
readTimeout = 60000,
146+
connectTimeout = 60000,
147+
log,
148+
workspace = os.pwd,
149+
env = sys.env,
150+
awaitTimeout = 600000,
151+
stagingRelease = stagingRelease
152+
).publishAll(
153+
release = true,
154+
artifactPaths: _*
155+
)
156+
Result.Success(())
157+
}
158+
result
159+
}
160+
161+
/** All the publish artifacts for the release modules.
162+
*/
163+
private def artifacts(ev: Evaluator) = {
164+
val modules = releaseModules(ev).map { m => m.publishArtifacts }
165+
Tasks(modules)
166+
}
167+
168+
private val envTask: Task[Env] = setupEnv()
169+
170+
/** Ensures that your key is imported prio to signing and publishing.
171+
*/
172+
def setupGpg(): Task[Unit] = T.task {
173+
T.log.info("Attempting to setup gpg")
174+
val pgpSecret = envTask().pgpSecret.replaceAll("\\s", "")
175+
try {
176+
val decoded = new String(
177+
Base64.getDecoder.decode(pgpSecret.getBytes(StandardCharsets.UTF_8))
178+
)
179+
180+
// https://dev.gnupg.org/T2313
181+
val imported = os
182+
.proc("gpg", "--batch", "--import", "--no-tty")
183+
.call(stdin = decoded)
184+
185+
if (imported.exitCode != 0)
186+
Result.Failure(
187+
"Unable to import your pgp key. Make sure your secret is correct."
188+
)
189+
} catch {
190+
case e: IllegalArgumentException =>
191+
Result.Failure(
192+
s"Invalid secret, unable to decode it: ${e.getMessage()}"
193+
)
194+
case NonFatal(e) => Result.Failure(e.getMessage())
195+
}
196+
}
197+
198+
/** Ensures that the user has all the ENV variable set up that are necessary
199+
* to both take care of pgp related stuff and also publish to sonatype.
200+
* @return
201+
* a Env Task
202+
*/
203+
private def setupEnv(): Task[Env] = T.input {
204+
val env = T.ctx().env
205+
val pgpSecret = env.get("PGP_SECRET")
206+
val pgpPassword = env.get("PGP_PASSPHRASE")
207+
val isTag = env.get("GITHUB_REF").exists(_.startsWith("refs/tags"))
208+
val sonatypeUser = env.get("SONATYPE_USERNAME")
209+
val sonatypePassword = env.get("SONATYPE_PASSWORD")
210+
211+
if (pgpSecret.isEmpty) {
212+
Result.Failure("Missing PGP_SECRET. Make sure you have it set.")
213+
} else if (pgpPassword.isEmpty) {
214+
Result.Failure("Missing PGP_PASSPHRASE. Make sure you have it set.")
215+
} else if (sonatypeUser.isEmpty) {
216+
Result.Failure("Missing SONATYPE_USERNAME. Make sure you have it set.")
217+
} else if (sonatypePassword.isEmpty) {
218+
Result.Failure("Missing SONATYPE_PASSWORD. Make sure you have it set.")
219+
} else {
220+
Env(
221+
pgpSecret.get,
222+
pgpPassword.get,
223+
isTag,
224+
sonatypeUser.get,
225+
sonatypePassword.get
226+
)
227+
}
228+
}
229+
230+
/** Gathers all the CiReleaseModules, which is used to determine what should
231+
* be released
232+
*/
233+
private def releaseModules(ev: Evaluator) =
234+
ev.rootModule.millInternal.modules.collect { case m: CiReleaseModule => m }
235+
236+
import Discover._
237+
lazy val millDiscover: mill.define.Discover[this.type] =
238+
mill.define.Discover[this.type]
239+
}
240+
241+
object Discover {
242+
implicit def millEvaluatorTokenReader: mainargs.TokensReader[Evaluator] =
243+
mill.main.TokenReaders.millEvaluatorTokenReader
244+
}
245+
246+
/** The env variables that are necessary to sign and publish
247+
*
248+
* @param pgpSecret
249+
* base64 encoded secret
250+
* @param pgpPassword
251+
* password to unlock your secret
252+
* @param isTag
253+
* whether or not this is a stable release or not
254+
* @param sonatypeUser
255+
* your sonatype user
256+
* @param sonatypePassword
257+
* your sontatype password
258+
*/
259+
case class Env(
260+
pgpSecret: String,
261+
pgpPassword: String,
262+
isTag: Boolean,
263+
sonatypeUser: String,
264+
sonatypePassword: String
265+
) {
266+
267+
/** Sonatype creds in the format that Mill uses
268+
*/
269+
val sonatypeCreds: String = s"${sonatypeUser}:${sonatypePassword}"
270+
}
271+
272+
object Env {
273+
implicit def rw: upickle.default.ReadWriter[Env] =
274+
upickle.default.macroRW
275+
}
276+
277+
object Eval {
278+
279+
def evalOrThrow(ev: Evaluator): Evaluator.EvalOrThrow = ev.evalOrThrow()
280+
281+
}
282+
283+
sealed trait SonatypeHost
284+
object SonatypeHost {
285+
case object Legacy extends SonatypeHost
286+
case object s01 extends SonatypeHost
287+
}

0 commit comments

Comments
 (0)