sbt/project/Release.scala

417 lines
12 KiB
Scala

import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Paths}
import java.util.regex.Pattern
import com.typesafe.sbt.pgp.PgpKeys
import sbt._
import sbt.Keys._
import sbt.Package.ManifestAttributes
import sbtrelease.ReleasePlugin.autoImport._
import sbtrelease.ReleasePlugin.autoImport.ReleaseTransformations._
import scala.io._
import scala.sys.process.ProcessLogger
object Release {
// adapted from https://github.com/sbt/sbt-release/blob/eccd4cb7b9818b2a731380fe31c399dc9cb7375b/src/main/scala/ReleaseExtra.scala#L239-L243
private def toProcessLogger(st: State): ProcessLogger =
new ProcessLogger {
def err(s: => String) = st.log.info(s)
def out(s: => String) = st.log.info(s)
def buffer[T](f: => T) = st.log.buffer(f)
}
implicit final class StateOps(val state: State) extends AnyVal {
def vcs: sbtrelease.Vcs =
Project.extract(state).get(releaseVcs).getOrElse {
sys.error("VCS not set")
}
}
val checkTravisStatus = ReleaseStep { state =>
val currentHash = state.vcs.currentHash
val build = Travis.builds("coursier/coursier", state.log)
.find { build =>
build.job_ids.headOption.exists { id =>
Travis.job(id, state.log).commit.sha == currentHash
}
}
.getOrElse {
sys.error(s"Status for commit $currentHash not found on Travis")
}
state.log.info(s"Found build ${build.id.value} for commit $currentHash, state: ${build.state}")
build.state match {
case "passed" =>
case _ =>
sys.error(s"Build for $currentHash in state ${build.state}")
}
state
}
val checkAppveyorStatus = ReleaseStep { state =>
val currentHash = state.vcs.currentHash
val build = Appveyor.branchLastBuild("alexarchambault/coursier-a7n6k", "master", state.log)
state.log.info(s"Found last build ${build.buildId} for branch master, status: ${build.status}")
if (build.commitId != currentHash)
sys.error(s"Last master Appveyor build corresponds to commit ${build.commitId}, expected $currentHash")
if (build.status != "success")
sys.error(s"Last master Appveyor build status: ${build.status}")
state
}
val previousReleaseVersion = AttributeKey[String]("previousReleaseVersion")
val initialVersion = AttributeKey[String]("initialVersion")
val saveInitialVersion = ReleaseStep { state =>
val currentVer = Project.extract(state).get(version)
state.put(initialVersion, currentVer)
}
def versionChanges(state: State): Boolean = {
val initialVer = state.get(initialVersion).getOrElse {
sys.error(s"${initialVersion.label} key not set")
}
val (_, nextVer) = state.get(ReleaseKeys.versions).getOrElse {
sys.error(s"${ReleaseKeys.versions.label} key not set")
}
initialVer == nextVer
}
val updateVersionPattern = "(?m)^VERSION=.*$".r
def updateVersionInScript(file: File, newVersion: String): Unit = {
val content = Source.fromFile(file)(Codec.UTF8).mkString
updateVersionPattern.findAllIn(content).toVector match {
case Seq() => sys.error(s"Found no matches in $file")
case Seq(_) =>
case _ => sys.error(s"Found too many matches in $file")
}
val newContent = updateVersionPattern.replaceAllIn(content, "VERSION=" + newVersion)
Files.write(file.toPath, newContent.getBytes(StandardCharsets.UTF_8))
}
val updateScripts = ReleaseStep { state =>
val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse {
sys.error(s"${ReleaseKeys.versions.label} key not set")
}
val scriptsDir = Project.extract(state).get(baseDirectory.in(ThisBuild)) / "scripts"
val scriptFiles = Seq(
scriptsDir / "generate-launcher.sh"
)
val vcs = state.vcs
val log = toProcessLogger(state)
for (f <- scriptFiles) {
updateVersionInScript(f, releaseVer)
vcs.add(f.getAbsolutePath).!!(log)
}
state
}
val updateLaunchers = ReleaseStep { state =>
val baseDir = Project.extract(state).get(baseDirectory.in(ThisBuild))
val scriptsDir = baseDir / "scripts"
val scriptFiles = Seq(
(scriptsDir / "generate-launcher.sh") -> (baseDir / "coursier")
)
val vcs = state.vcs
val log = toProcessLogger(state)
for ((f, output) <- scriptFiles) {
sys.process.Process(Seq(f.getAbsolutePath, "-f")).!!(log)
vcs.add(output.getAbsolutePath).!!(log)
}
state
}
val savePreviousReleaseVersion = ReleaseStep { state =>
val cmd = Seq(state.vcs.commandName, "tag", "--sort", "version:refname")
val tag = scala.sys.process.Process(cmd)
.!!
.lines
.toVector
.lastOption
.getOrElse {
sys.error(s"Found no tags when running ${cmd.mkString(" ")}")
}
val ver =
if (tag.startsWith("v"))
tag.stripPrefix("v")
else
sys.error(s"Last tag '$tag' doesn't start with 'v'")
state.put(previousReleaseVersion, ver)
}
val updateTutReadme = ReleaseStep { state =>
val log = toProcessLogger(state)
val previousVer = state.get(previousReleaseVersion).getOrElse {
sys.error(s"${previousReleaseVersion.label} key not set")
}
val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse {
sys.error(s"${ReleaseKeys.versions.label} key not set")
}
val readmeFile = Project.extract(state).get(baseDirectory.in(ThisBuild)) / "doc" / "readme" / "README.md"
val pattern = Pattern.quote(previousVer).r
val content = Source.fromFile(readmeFile)(Codec.UTF8).mkString
val newContent = pattern.replaceAllIn(content, releaseVer)
Files.write(readmeFile.toPath, newContent.getBytes(StandardCharsets.UTF_8))
state.vcs.add(readmeFile.getAbsolutePath).!!(log)
state
}
val stageReadme = ReleaseStep { state =>
val log = toProcessLogger(state)
val baseDir = Project.extract(state).get(baseDirectory.in(ThisBuild))
val processedReadmeFile = baseDir / "README.md"
state.vcs.add(processedReadmeFile.getAbsolutePath).!!(log)
state
}
val coursierVersionPattern = s"(?m)^${Pattern.quote("def coursierVersion0 = \"")}[^${'"'}]*${Pattern.quote("\"")}$$".r
val updatePluginsSbt = ReleaseStep { state =>
val vcs = state.vcs
val log = toProcessLogger(state)
val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse {
sys.error(s"${ReleaseKeys.versions.label} key not set")
}
val baseDir = Project.extract(state).get(baseDirectory.in(ThisBuild))
val projectProjectPluginsSbtFile = baseDir / "project" / "project" / "project" / "plugins.sbt"
val files = Seq(
projectProjectPluginsSbtFile
)
for (f <- files) {
val content = Source.fromFile(f)(Codec.UTF8).mkString
coursierVersionPattern.findAllIn(content).toVector match {
case Seq() => sys.error(s"Found no matches in $f")
case Seq(_) =>
case _ => sys.error(s"Found too many matches in $f")
}
val newContent = coursierVersionPattern.replaceAllIn(content, "def coursierVersion0 = \"" + releaseVer + "\"")
Files.write(f.toPath, newContent.getBytes(StandardCharsets.UTF_8))
vcs.add(f.getAbsolutePath).!!(log)
}
state
}
val mimaVersionsPattern = s"(?m)^(\\s+)${Pattern.quote("\"\" // binary compatibility versions")}$$".r
val milestonePattern = ("[^-]+" + Pattern.quote("-M") + "[0-9]+(-[0-9]+)*").r
val updateMimaVersions = ReleaseStep { state =>
val vcs = state.vcs
val log = toProcessLogger(state)
val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse {
sys.error(s"${ReleaseKeys.versions.label} key not set")
}
if (milestonePattern.unapplySeq(releaseVer).isEmpty) {
val baseDir = Project.extract(state).get(baseDirectory.in(ThisBuild))
val mimaScalaFile = baseDir / "project" / "Mima.scala"
val content = Source.fromFile(mimaScalaFile)(Codec.UTF8).mkString
mimaVersionsPattern.findAllIn(content).toVector match {
case Seq() => sys.error(s"Found no matches in $mimaScalaFile")
case Seq(_) =>
case _ => sys.error(s"Found too many matches in $mimaScalaFile")
}
val newContent = mimaVersionsPattern.replaceAllIn(
content,
m => {
val indent = m.group(1)
indent + "\"" + releaseVer + "\",\n" +
indent + "\"\" // binary compatibility versions"
}
)
Files.write(mimaScalaFile.toPath, newContent.getBytes(StandardCharsets.UTF_8))
vcs.add(mimaScalaFile.getAbsolutePath).!!(log)
} else
state.log.info(s"$releaseVer is a milestone release, not adding it to the MIMA checks")
state
}
val updateTestFixture = ReleaseStep(
action = { state =>
val initialVer = state.get(initialVersion).getOrElse {
sys.error(s"${initialVersion.label} key not set")
}
val (_, nextVer) = state.get(ReleaseKeys.versions).getOrElse {
sys.error(s"${ReleaseKeys.versions.label} key not set")
}
if (initialVer == nextVer)
state
else {
val vcs = state.vcs
val log = toProcessLogger(state)
val originalFile = Paths.get(s"tests/shared/src/test/resources/resolutions/io.get-coursier/coursier_2.11/$initialVer")
val originalContent = new String(Files.readAllBytes(originalFile), StandardCharsets.UTF_8)
val destFile = Paths.get(s"tests/shared/src/test/resources/resolutions/io.get-coursier/coursier_2.11/$nextVer")
val destContent = originalContent.replace(initialVer, nextVer)
log.out(s"Writing $destFile")
Files.write(destFile, destContent.getBytes(StandardCharsets.UTF_8))
vcs.add(destFile.toAbsolutePath.toString).!!(log)
vcs.cmd("rm", originalFile.toAbsolutePath.toString).!!(log)
state
}
}
)
val commitUpdates = ReleaseStep(
action = { state =>
val log = toProcessLogger(state)
val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse {
sys.error(s"${ReleaseKeys.versions.label} key not set")
}
state.vcs.commit(s"Updates for $releaseVer", sign = true).!(log)
state
},
check = { state =>
val vcs = state.vcs
if (vcs.hasModifiedFiles)
sys.error("Aborting release: unstaged modified files")
if (vcs.hasUntrackedFiles && !Project.extract(state).get(releaseIgnoreUntrackedFiles))
sys.error(
"Aborting release: untracked files. Remove them or specify 'releaseIgnoreUntrackedFiles := true' in settings"
)
state
}
)
val addReleaseToManifest = ReleaseStep { state =>
val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse {
sys.error(s"${ReleaseKeys.versions.label} key not set")
}
val tag = "v" + releaseVer
reapply(
Seq(
// Tag will be one commit after the one with which the publish was really made, because of the commit
// updating scripts / plugins.
packageOptions += ManifestAttributes("Vcs-Release-Tag" -> tag)
),
state
)
}
// tagRelease from sbt-release seem to use the next version (snapshot one typically) rather than the released one :/
val reallyTagRelease = ReleaseStep { state =>
val log = toProcessLogger(state)
val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse {
sys.error(s"${ReleaseKeys.versions.label} key not set")
}
val sign = Project.extract(state).get(releaseVcsSign)
val tag = "v" + releaseVer
state.vcs.tag(tag, s"Releasing $tag", sign).!(log)
state
}
val settings = Seq(
releaseProcess := Seq[ReleaseStep](
checkTravisStatus,
checkAppveyorStatus,
savePreviousReleaseVersion,
checkSnapshotDependencies,
inquireVersions,
saveInitialVersion,
setReleaseVersion,
commitReleaseVersion,
addReleaseToManifest,
publishArtifacts,
releaseStepCommand("sonatypeRelease"),
updateScripts,
updateLaunchers,
updateTutReadme,
releaseStepCommand(s"++${ScalaVersion.scala211}"),
releaseStepCommand("tut"),
stageReadme,
updatePluginsSbt,
updateMimaVersions,
updateTestFixture,
commitUpdates,
reallyTagRelease,
setNextVersion,
commitNextVersion,
ReleaseStep(_.reload),
pushChanges
),
releasePublishArtifactsAction := PgpKeys.publishSigned.value
)
}