diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 987a4f6..c5fbab1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,4 +13,4 @@ jobs: java-version: 8 cache: sbt - uses: sbt/setup-sbt@v1 - - run: sbt +compile + - run: sbt +test diff --git a/build.sbt b/build.sbt index f3745f3..04e0678 100644 --- a/build.sbt +++ b/build.sbt @@ -40,6 +40,7 @@ lazy val plugin = project case _ => "2.0.0-M2" } }, + libraryDependencies += "org.scalameta" %% "munit" % "0.7.29" % Test, addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.0"), addSbtPlugin("com.github.sbt" % "sbt-git" % "2.1.0"), addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.0"), diff --git a/plugin/src/main/scala/com/geirsson/CiReleasePlugin.scala b/plugin/src/main/scala/com/geirsson/CiReleasePlugin.scala index ad99c50..3c46d4f 100644 --- a/plugin/src/main/scala/com/geirsson/CiReleasePlugin.scala +++ b/plugin/src/main/scala/com/geirsson/CiReleasePlugin.scala @@ -106,6 +106,8 @@ object CiReleasePlugin extends AutoPlugin { Some(s"scm:git:git@github.com:$user/$repo.git") ) + lazy val cireleasePublishCommand = settingKey[String]("") + override lazy val buildSettings: Seq[Def.Setting[_]] = List( dynverSonatypeSnapshots := true, scmInfo ~= { @@ -128,7 +130,20 @@ object CiReleasePlugin extends AutoPlugin { } catch { case NonFatal(_) => None } - } + }, + cireleasePublishCommand := { + val gitDescribe = dynverGitDescribeOutput.value + val v = gitDescribe.getVersion( + dynverCurrentDate.value, + dynverSeparator.value, + dynverSonatypeSnapshots.value + ) + sys.env.get("CI_RELEASE") match { + case Some(cmd) => cmd + case None => backPubVersionToCommand(v) + } + }, + version ~= dropBackPubCommand, ) override lazy val globalSettings: Seq[Def.Setting[_]] = List( @@ -136,7 +151,8 @@ object CiReleasePlugin extends AutoPlugin { publishMavenStyle := true, commands += Command.command("ci-release") { currentState => val shouldDeployToSonatypeCentral = isDeploySetToSonatypeCentral(currentState) - val isSnapshot = isSnapshotVersion(currentState) + val version = getVersion(currentState) + val isSnapshot = isSnapshotVersion(version) if (!isSecure) { println("No access to secret variables, doing nothing") currentState @@ -150,6 +166,8 @@ object CiReleasePlugin extends AutoPlugin { val reloadKeyFiles = "; set pgpSecretRing := pgpSecretRing.value; set pgpPublicRing := pgpPublicRing.value" + val publishCommand = getPublishCommand(currentState) + if (shouldDeployToSonatypeCentral) { if (isSnapshot) { println(s"Sonatype Central does not accept snapshots, only official releases. Aborting release.") @@ -161,7 +179,7 @@ object CiReleasePlugin extends AutoPlugin { println("Tag push detected, publishing a stable release") reloadKeyFiles :: sys.env.getOrElse("CI_CLEAN", "; clean ; sonatypeBundleClean") :: - sys.env.getOrElse("CI_RELEASE", "+publishSigned") :: + publishCommand :: sys.env.getOrElse("CI_SONATYPE_RELEASE", "sonatypeCentralRelease") :: currentState } @@ -184,7 +202,7 @@ object CiReleasePlugin extends AutoPlugin { println("Tag push detected, publishing a stable release") reloadKeyFiles :: sys.env.getOrElse("CI_CLEAN", "; clean ; sonatypeBundleClean") :: - sys.env.getOrElse("CI_RELEASE", "+publishSigned") :: + publishCommand :: sys.env.getOrElse("CI_SONATYPE_RELEASE", "sonatypeBundleRelease") :: currentState } @@ -210,10 +228,49 @@ object CiReleasePlugin extends AutoPlugin { } } - def isSnapshotVersion(state: State): Boolean = { + def getVersion(state: State): String = (ThisBuild / version).get(Project.extract(state).structure.data) match { - case Some(v) => v.endsWith("-SNAPSHOT") + case Some(v) => v case None => throw new NoSuchFieldError("version") } + + def getPublishCommand(state: State): String = + (ThisBuild / cireleasePublishCommand).get(Project.extract(state).structure.data) match { + case Some(v) => v + case None => throw new NoSuchFieldError("cireleasePublishCommand") + } + + def isSnapshotVersion(v: String): Boolean = v.endsWith("-SNAPSHOT") + + def backPubVersionToCommand(ver: String): String = + if (ver.contains("@")) { + val nonComment = + if (ver.contains("#")) ver.split("#").head + else ver + val commands0 = nonComment.split("@").toList.drop(1) + var nonDigit = false + val commands = (commands0.map { cmd => + if (cmd.isEmpty) sys.error(s"Invalid back-publish version: $ver") + else { + if (!cmd.head.isDigit) { + nonDigit = true + cmd + } + else if (cmd.contains(".x")) s"++${cmd}" + else s"++${cmd}!" + } + }) ::: (if (nonDigit) Nil else List("publishSigned")) + commands match { + case x :: Nil => x + case xs => xs.mkString(";", ";", "") + } + } else "+publishSigned" + + def dropBackPubCommand(ver: String): String = { + val nonComment = + if (ver.contains("#")) ver.split("#").head + else ver + if (nonComment.contains("@")) nonComment.split("@").head + else nonComment } } diff --git a/plugin/src/test/scala/com/geirsson/CiReleaseTest.scala b/plugin/src/test/scala/com/geirsson/CiReleaseTest.scala new file mode 100644 index 0000000..9de7c71 --- /dev/null +++ b/plugin/src/test/scala/com/geirsson/CiReleaseTest.scala @@ -0,0 +1,47 @@ +package com.geirsson + +import CiReleasePlugin.{ backPubVersionToCommand, dropBackPubCommand } + +class CiReleaseTest extends munit.FunSuite { + val expectedVer = "1.1.0" + + test("Normal version default") { + assertEquals(backPubVersionToCommand("1.1.0"), "+publishSigned") + assertEquals(dropBackPubCommand("1.1.0"), expectedVer) + } + + test("Command starting with number is assumed to be a cross version") { + assertEquals(backPubVersionToCommand("1.1.0@2.12.20"), ";++2.12.20!;publishSigned") + assertEquals(dropBackPubCommand("1.1.0@2.12.20"), expectedVer) + + assertEquals(backPubVersionToCommand("1.1.0@3.x"), ";++3.x;publishSigned") + assertEquals(dropBackPubCommand("1.1.0@3.x"), expectedVer) + } + + test("Non-number is treated as an alternative publish command") { + assertEquals(backPubVersionToCommand("1.1.0@foo/publishSigned"), "foo/publishSigned") + assertEquals(dropBackPubCommand("1.1.0@foo/publishSigned"), expectedVer) + + assertEquals(backPubVersionToCommand("1.1.0@+foo/publishSigned"), "+foo/publishSigned") + assertEquals(dropBackPubCommand("1.1.0@+foo/publishSigned"), expectedVer) + } + + test("Commands can be chained") { + assertEquals(backPubVersionToCommand("1.1.0@2.12.20@foo/publishSigned"), ";++2.12.20!;foo/publishSigned") + assertEquals(dropBackPubCommand("1.1.0@2.12.20@foo/publishSigned"), expectedVer) + + assertEquals(backPubVersionToCommand("1.1.0@foo/something@bar/publishSigned"), ";foo/something;bar/publishSigned") + assertEquals(dropBackPubCommand("1.1.0@foo/something@bar/publishSigned"), expectedVer) + } + + test("Treat # as comments") { + assertEquals(backPubVersionToCommand("1.1.0#comment"), "+publishSigned") + assertEquals(dropBackPubCommand("1.1.0#comment"), expectedVer) + + assertEquals(backPubVersionToCommand("1.1.0@2.12.20#comment"), ";++2.12.20!;publishSigned") + assertEquals(dropBackPubCommand("1.1.0@2.12.20#comment"), expectedVer) + + assertEquals(backPubVersionToCommand("1.1.0@3.x#comment"), ";++3.x;publishSigned") + assertEquals(dropBackPubCommand("1.1.0@3.x#comment"), expectedVer) + } +} diff --git a/readme.md b/readme.md index 7f2ed1c..a104ca2 100644 --- a/readme.md +++ b/readme.md @@ -356,6 +356,56 @@ page will look like this: Enjoy 👌 +### Back-publishing support + +sbt-ci-release implements a mini-DSL for the Git tag for back publishing purpose, which is useful if you maintain a compiler plugin, library for Scala Native, or an sbt plugin during 2.x migration etc. + +``` +v1.2.3[@3.x|@2.n.x|@a.b.c][@command][#comment] +``` + +- `#` is used for comments, which is useful if you need to use the same command multiple times +- `@3.x` expands to `++3.x`, and if no other commands follow, `;++3.x;publishSigned` +- `@2.13.x` expands to `++2.13.x`, and if no other commands follow, `;++2.13.x;publishSigned` +- Other commands such as `@foo/publishSigned` expands to `foo/publishSigned` + +#### Case 1: Publish all subprojects for Scala 2.13.15 + +`v1.2.3@2.13.15` + +#### Case 2: Publish all subprojects for Scala 3.x + +`v1.2.3@3.x`. Optionally we can add a comment: `v1.2.3@3.x#comment`. + +We can use this to back publish sbt 2.x plugins. + +1. Branch off of `v1.2.3` to create `release/1.2.3` branch, and send a PR to update sbt version +2. Tag the brach to `v1.2.3@3.x#sbt2.0.0-Mn` + +#### Case 3: Publish some subprojects for Scala 2.13.15 + +`v1.2.3@2.13.15@foo/publishSigned` + +You can create a subproject to aggregate 2 or more subprojects. + +#### Case 4: Publish some subprojects for supported Scala versions + +`v1.2.3@+foo_native/publishSigned#comment` + +1. Branch off of `v1.2.3` to create `release/1.2.3` branch, and send a PR to update the Scala Native version. +2. Tag the branch to `v1.2.3@+foo_native/publishSigned#native0.5` + +#### Case 5: Minimize the use of command + +`v1.2.3#unique_comment`, for example `v1.2.3#native0.5_3` + +If you prefer to keep most of the information in a git branch instead, you can just use the comment functionality. + +1. Branch off of `v1.2.3` to create `release/1.2.3` branch, and send a PR to: + a. Update appropriate dependency (sbt, Scala Native etc) + b. Modify the `CI_RELEASE` environment variable to encode the actions you want to take, like `;++3.x;foo_native/publishSigned`. For GitHub Actions, it would be in `.github/workflows/release.yml` +2. Tag the branch to `v1.2.3#unique_comment`. For record keeping, encode the version you're trying to back publishing for e.g. `v1.2.3#native0.5_3` + ## FAQ ### How do I publish to Sonatype Central?