diff --git a/project/Publish.scala b/project/Publish.scala index a18005312..50b8a03e4 100644 --- a/project/Publish.scala +++ b/project/Publish.scala @@ -62,6 +62,6 @@ object Publish { } ) - lazy val released = pomStuff ++ pushToSonatypeStuff + lazy val released = pomStuff ++ pushToSonatypeStuff ++ Release.settings } diff --git a/project/Release.scala b/project/Release.scala new file mode 100644 index 000000000..3a700c131 --- /dev/null +++ b/project/Release.scala @@ -0,0 +1,244 @@ +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.util.regex.Pattern + +import com.typesafe.sbt.pgp.PgpKeys +import sbt._ +import sbt.Keys._ +import sbtrelease.ReleasePlugin.autoImport._ +import sbtrelease.ReleasePlugin.autoImport.ReleaseTransformations._ + +import scala.io._ + +object Release { + + 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 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", + scriptsDir / "generate-sbt-launcher.sh" + ) + + val vcs = state.vcs + + for (f <- scriptFiles) { + updateVersionInScript(f, releaseVer) + vcs.add(f.getAbsolutePath).!!(state.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"), + (scriptsDir / "generate-sbt-launcher.sh") -> (baseDir / "csbt") + ) + + val vcs = state.vcs + + for ((f, output) <- scriptFiles) { + sbt.Process(Seq(f.getAbsolutePath, "-f")).!!(state.log) + vcs.add(output.getAbsolutePath).!!(state.log) + } + + state + } + + val savePreviousReleaseVersion = ReleaseStep { state => + + val cmd = Seq(state.vcs.commandName, "tag", "-l") + + val tag = scala.sys.process.Process(cmd) + .!! + .linesIterator + .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 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.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).!!(state.log) + + state + } + + val stageReadme = ReleaseStep { state => + + val baseDir = Project.extract(state).get(baseDirectory.in(ThisBuild)) + val processedReadmeFile = baseDir / "README.md" + + state.vcs.add(processedReadmeFile.getAbsolutePath).!!(state.log) + + state + } + + + val coursierVersionPattern = s"(?m)^${Pattern.quote("def coursierVersion = \"")}[^${'"'}]*${Pattern.quote("\"")}$$".r + + val updatePluginsSbt = ReleaseStep { state => + + val vcs = state.vcs + + 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 pluginsSbtFile = baseDir / "project" / "plugins.sbt" + val projectPluginsSbtFile = baseDir / "project" / "project" / "plugins.sbt" + + val files = Seq( + pluginsSbtFile, + projectPluginsSbtFile + ) + + 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 coursierVersion = \"" + releaseVer + "\"") + Files.write(f.toPath, newContent.getBytes(StandardCharsets.UTF_8)) + vcs.add(f.getAbsolutePath).!!(state.log) + } + + state + } + + val commitUpdates = ReleaseStep( + action = { 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).!(state.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 settings = Seq( + releaseProcess := Seq[ReleaseStep]( + savePreviousReleaseVersion, + checkSnapshotDependencies, + inquireVersions, + saveInitialVersion, + runTest, + setReleaseVersion, + commitReleaseVersion, + publishArtifacts, + releaseStepCommand("sonatypeRelease"), + updateScripts, + updateLaunchers, + updateTutReadme, + releaseStepCommand("tut"), + stageReadme, + updatePluginsSbt, + commitUpdates, + tagRelease, + setNextVersion, + commitNextVersion, + ReleaseStep(_.reload), + pushChanges + ), + releasePublishArtifactsAction := PgpKeys.publishSigned.value + ) + +} diff --git a/project/plugins.sbt b/project/plugins.sbt index aea457064..cbb164012 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,18 +1,23 @@ plugins_( - "io.get-coursier" % "sbt-coursier" % "1.0.0-RC1", - "com.typesafe" % "sbt-mima-plugin" % "0.1.13", - "org.xerial.sbt" % "sbt-pack" % "0.8.2", - "com.jsuereth" % "sbt-pgp" % "1.0.0", - "com.typesafe.sbt" % "sbt-proguard" % "0.2.2", - "org.scala-js" % "sbt-scalajs" % "0.6.15", - "org.scoverage" % "sbt-scoverage" % "1.4.0", - "io.get-coursier" % "sbt-shading" % "1.0.0-RC1", - "org.tpolecat" % "tut-plugin" % "0.4.8" + "io.get-coursier" % "sbt-coursier" % coursierVersion, + "com.typesafe" % "sbt-mima-plugin" % "0.1.13", + "org.xerial.sbt" % "sbt-pack" % "0.8.2", + "com.jsuereth" % "sbt-pgp" % "1.0.0", + "com.typesafe.sbt" % "sbt-proguard" % "0.2.2", + "com.github.gseitz" % "sbt-release" % "1.0.4", + "org.scala-js" % "sbt-scalajs" % "0.6.15", + "org.scoverage" % "sbt-scoverage" % "1.4.0", + "io.get-coursier" % "sbt-shading" % coursierVersion, + "org.xerial.sbt" % "sbt-sonatype" % "1.1", + "org.tpolecat" % "tut-plugin" % "0.4.8" ) libs += "org.scala-sbt" % "scripted-plugin" % sbtVersion.value +// important: this line is matched / substituted during releases (via sbt-release) +def coursierVersion = "1.0.0-RC1" + def plugins_(modules: ModuleID*) = modules.map(addSbtPlugin) def libs = libraryDependencies diff --git a/project/project/plugins.sbt b/project/project/plugins.sbt index dfe274c01..f9b32135b 100644 --- a/project/project/plugins.sbt +++ b/project/project/plugins.sbt @@ -1 +1,4 @@ -addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC1") +addSbtPlugin("io.get-coursier" % "sbt-coursier" % coursierVersion) + +// important: this line is matched / substituted during releases (via sbt-release) +def coursierVersion = "1.0.0-RC1" diff --git a/scripts/generate-sbt-launcher.sh b/scripts/generate-sbt-launcher.sh index 164171291..364980aa5 100755 --- a/scripts/generate-sbt-launcher.sh +++ b/scripts/generate-sbt-launcher.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash set -e -COURSIER_VERSION=1.0.0-M15-2 +VERSION=1.0.0-M15-2 "$(dirname "$0")/../coursier" bootstrap \ - "io.get-coursier:sbt-launcher_2.12:$COURSIER_VERSION" \ + "io.get-coursier:sbt-launcher_2.12:$VERSION" \ -r sonatype:releases \ --no-default \ -i launcher \