Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

inline publishing plugin and increase timeouts #178

Merged
merged 2 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
- name: Publish ${{ github.ref }}
if: startsWith(github.ref, 'refs/tags/v') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.publishSnapshot == 'true')
run: ./mill -i io.kipp.mill.ci.release.ReleaseModule/publishAll
run: ./mill -i InternalReleaseModule.publishAll
env:
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
PGP_SECRET: ${{ secrets.PGP_SECRET }}
Expand Down
27 changes: 23 additions & 4 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import de.tobiasroeser.mill.vcs.version.VcsVersion
import $ivy.`io.github.davidgregory084::mill-tpolecat::0.3.5`
import $ivy.`com.lewisjkl::header-mill-plugin::0.0.3`
import header._
import $ivy.`io.chris-kipp::mill-ci-release::0.1.10`
import io.kipp.mill.ci.release.CiReleaseModule
import io.kipp.mill.ci.release.SonatypeHost
import $file.plugins.ci.CiReleaseModules
import CiReleaseModules.{CiReleaseModule, SonatypeHost, ReleaseModule, Discover}
import io.github.davidgregory084.TpolecatModule
import $ivy.`com.github.lolgab::mill-mima::0.1.1`
import com.github.lolgab.mill.mima._
Expand All @@ -15,6 +14,24 @@ import mill._
import mill.modules.Jvm
import mill.scalalib._
import mill.scalalib.publish._
import mill.define.ExternalModule
import mill.eval.Evaluator

object InternalReleaseModule extends Module {

/** 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] = {
ReleaseModule.publishAll(ev)
}

import Discover._
lazy val millDiscover: mill.define.Discover[this.type] =
mill.define.Discover[this.type]
}

trait BaseModule extends Module with HeaderModule {
def millSourcePath: os.Path = {
Expand Down Expand Up @@ -169,7 +186,9 @@ trait OpenapiModule extends BaseCrossScalaModule {
object test extends ScalaTests with BaseMunitTests

override def mimaBinaryIssueFilters = super.mimaBinaryIssueFilters() ++ Seq(
ProblemFilter.exclude[MissingClassProblem]("alloy.openapi.DiscriminatedUnions")
ProblemFilter.exclude[MissingClassProblem](
"alloy.openapi.DiscriminatedUnions"
)
)
}

Expand Down
287 changes: 287 additions & 0 deletions plugins/ci/CiReleaseModules.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version::0.4.0`
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

// Adapted from https://github.com/ckipp01/mill-ci-release in order to customize timeouts

/** 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 Module {

/** 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 = 60000,
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]
}

object Discover {
implicit def millEvaluatorTokenReader: mainargs.TokensReader[Evaluator] =
mill.main.TokenReaders.millEvaluatorTokenReader
}

/** The env variables that are necessary to sign and publish
*
* @param pgpSecret
* base64 encoded secret
* @param pgpPassword
* password to unlock your secret
* @param isTag
* whether or not this is a stable release or not
* @param sonatypeUser
* your sonatype user
* @param sonatypePassword
* your sontatype password
*/
case class Env(
pgpSecret: String,
pgpPassword: String,
isTag: Boolean,
sonatypeUser: String,
sonatypePassword: String
) {

/** Sonatype creds in the format that Mill uses
*/
val sonatypeCreds: String = s"${sonatypeUser}:${sonatypePassword}"
}

object Env {
implicit def rw: upickle.default.ReadWriter[Env] =
upickle.default.macroRW
}

object Eval {

def evalOrThrow(ev: Evaluator): Evaluator.EvalOrThrow = ev.evalOrThrow()

}

sealed trait SonatypeHost
object SonatypeHost {
case object Legacy extends SonatypeHost
case object s01 extends SonatypeHost
}
Loading