Skip to content
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
27 changes: 27 additions & 0 deletions modules/cli/src/test/scala/zipx/cli/CatalogOpsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,32 @@ object CatalogOpsSpec extends ZIOSpecDefault:
plan.toOption.exists(_.nextSource.contains("""Plugin("org.scalameta", "sbt-scalafmt", "2.6.2")""")),
)
},
test("planUpdate does not rewrite Ship or ShipGroup constructors") {
val dir = Files.createTempDirectory("zipx-cli-ships")
val cat = dir.resolve("ZipxVersions.scala")
Files.writeString(
cat,
"""object MyVersions:
| val zio = Lib("dev.zio", "zio", "2.1.26")
| val core = Ship("core", "1.4.2")
| val foo = ShipGroup("foo", "1.4.2")("foo-api", "foo-cli")
|""".stripMargin,
StandardCharsets.UTF_8,
)
val plan = CatalogOps.planUpdate(
cat,
lookupCoord = {
case c if c.artifact == "zio" => Right(Some("2.1.27"))
case _ => Right(None)
},
lookupAction = _ => Right(None),
)
assertTrue(
plan.isRight,
plan.toOption.exists(_.nextSource.contains("""Lib("dev.zio", "zio", "2.1.27")""")),
plan.toOption.exists(_.nextSource.contains("""Ship("core", "1.4.2")""")),
plan.toOption.exists(_.nextSource.contains("""ShipGroup("foo", "1.4.2")("foo-api", "foo-cli")""")),
)
},
)
end CatalogOpsSpec
40 changes: 40 additions & 0 deletions modules/core/src/main/scala/zipx/core/Modver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ final case class MovedRows(
object MovedRows:
val empty: MovedRows = MovedRows(Set.empty, Set.empty, Set.empty)

/** One catalog rewrite: identity is a Ship project id or a ShipGroup name. `to` is the release number, never `-ci`. */
final case class ShipBump(identity: String, from: String, to: String)

/** Fail-closed bump and publish sets. Verify's [[Affected]] stays a sibling; do not call it from here. */
object Modver:

Expand All @@ -75,6 +78,43 @@ object Modver:
def rowFor(projectId: ModuleId, ships: Seq[PublishedRow]): Option[PublishedRow] =
ships.find(_.memberRoots.contains(projectId))

/** Exact member root, then a JS/Native platform suffix of a root that has a row. */
def rowForProject(projectId: String, ships: Seq[PublishedRow]): Option[PublishedRow] =
ModuleId.make(projectId).toOption.flatMap { id =>
rowFor(id, ships).orElse {
val parent =
if projectId.endsWith("JS") && projectId.length > 2 then Some(projectId.dropRight(2))
else if projectId.endsWith("Native") && projectId.length > 6 then Some(projectId.dropRight(6))
else None
parent.flatMap(ModuleId.make(_).toOption).flatMap(rowFor(_, ships))
}
}

def bumpVersion(from: String, kind: BumpKind): Either[String, String] =
if from.endsWith("-ci") then Left(s"version '$from' must be the release number, not a -ci suffix")
else
parseSemver(from) match
case None => Left(s"not a major.minor.patch version: '$from'")
case Some((maj, min, pat)) =>
kind match
case BumpKind.None | BumpKind.PreRelease =>
Left(s"$kind is not a min-bump")
case BumpKind.Patch => Right(s"$maj.$min.${pat + 1}")
case BumpKind.Minor => Right(s"$maj.${min + 1}.0")
case BumpKind.Major => Right(s"${maj + 1}.0.0")

private def parseSemver(raw: String): Option[(Int, Int, Int)] =
val core = raw.stripPrefix("v").takeWhile(_ != '-')
val parts = core.split("\\.", -1)
if parts.length != 3 then None
else
for
major <- parts(0).toIntOption
minor <- parts(1).toIntOption
patch <- parts(2).toIntOption
yield (major, minor, patch)
end parseSemver

/** PR 1 stub: later rounds run MiMa then propagate. Identity so MatchBump cannot see Patch placeholders. */
def expand(bumps: BumpSet, graph: ModuleGraph, ships: ShipIndex): BumpSet =
val _ = (graph, ships)
Expand Down
31 changes: 31 additions & 0 deletions modules/core/src/main/scala/zipx/core/ZipxSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,34 @@ object ZipxSettings:
Build,
)

val ships: SettingDef[Seq[PublishedRow]] =
SettingDef.setting(
SettingName("zipxShips"),
Seq.empty,
SettingPurpose(
"Ship / ShipGroup rows collected from the ZipxVersions object. Presence is the independent-versioning flag."
),
Build,
)

val matrixRoot: SettingDef[Option[ModuleId]] =
SettingDef.setting(
SettingName("zipxMatrixRoot"),
None,
SettingPurpose(
"Override the inferred matrix root for this project (Scala-version axes, unusual layouts). Default None."
),
Project,
)

val modverBump: SettingDef[Unit] =
SettingDef.input(
SettingName("zipxModverBump"),
SettingPurpose(
"Rewrite a Ship / ShipGroup version in zipxVersionsFile. Default patch. Identity is a project id or group name. Never writes -ci."
),
)

val sbtVersionCoord: SettingDef[Option[SbtVersion]] =
SettingDef.setting(
SettingName("zipxSbt"),
Expand Down Expand Up @@ -608,6 +636,7 @@ object ZipxSettings:
preRelease,
versions,
pins,
ships,
sbtVersionCoord,
scalaVersionCoord,
checkDeps,
Expand Down Expand Up @@ -642,6 +671,7 @@ object ZipxSettings:
docker,
testTask,
publishTask,
matrixRoot,
)

val tasks: List[SettingDef[?]] = List(
Expand All @@ -659,6 +689,7 @@ object ZipxSettings:
pinUpdate,
depUpdate,
actionUpdate,
modverBump,
)

/** Every public catalog entry, in docs-friendly order. */
Expand Down
19 changes: 19 additions & 0 deletions modules/core/src/test/scala/zipx/core/ModverSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ object ModverSpec extends ZIOSpecDefault:
yield assertTrue(good.isRight, badId.isLeft, badName.isLeft)
},
),
suite("bumpVersion")(
test("patch, minor, and major increment and never write -ci") {
assertTrue(
Modver.bumpVersion("1.4.2", BumpKind.Patch) == Right("1.4.3"),
Modver.bumpVersion("1.4.2", BumpKind.Minor) == Right("1.5.0"),
Modver.bumpVersion("1.4.2", BumpKind.Major) == Right("2.0.0"),
Modver.bumpVersion("1.4.2-ci", BumpKind.Patch).isLeft,
)
},
test("rowForProject prefers the exact id then a JS suffix of a Ship root") {
val rows = List[PublishedRow](Ship("core", "1.4.2"), Ship("cli", "0.3.0"))
assertTrue(
Modver.rowForProject("core", rows).exists(_.identity == "core"),
Modver.rowForProject("coreJS", rows).exists(_.identity == "core"),
Modver.rowForProject("cli", rows).exists(_.identity == "cli"),
Modver.rowForProject("service", rows).isEmpty,
)
},
),
suite("membership")(
test("a covering catalog is Right and every publishing root is in exactly one row") {
check(gCovered) { (g, rows) =>
Expand Down
3 changes: 3 additions & 0 deletions modules/core/src/test/scala/zipx/core/ZipxSettingsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ object ZipxSettingsSpec extends ZIOSpecDefault:
ZipxSettings.names.contains("zipxCheckDeps"),
ZipxSettings.names.contains("zipxEmitSelf"),
ZipxSettings.names.contains("zipxSelfPlugins"),
ZipxSettings.names.contains("zipxShips"),
ZipxSettings.names.contains("zipxMatrixRoot"),
ZipxSettings.names.contains("zipxModverBump"),
)
},
test("build / project / task partitions cover every entry exactly once") {
Expand Down
61 changes: 47 additions & 14 deletions modules/sbt-plugin/src/main/scala/zipx/ZipxVersions.scala
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package zipx

import sbt.{Def, ModuleID, Setting}
import sbt.Keys.{crossScalaVersions, libraryDependencies, scalaVersion}
import sbt.Keys.{crossScalaVersions, libraryDependencies, pomPostProcess, scalaVersion, thisProject, version}
import zipx.plugin.ZipxDeps
import zipx.plugin.ZipxPlugin.autoImport.{zipxActionRows, zipxCheckDeps, zipxPins, zipxSbt, zipxScala, zipxVersions}
import zipx.plugin.ZipxPlugin.autoImport.{
zipxActionRows,
zipxCheckDeps,
zipxPins,
zipxSbt,
zipxScala,
zipxShips,
zipxVersions,
}

val ZipxSelf = zipx.plugin.ZipxSelf

/** Catalog a build writes under `project/` and extends. `.sbt` files get plugin autoImport; this package is what those
* Scala sources import.
*
* Row collection (`coords` / `pins` / `actions`) lives on [[Catalog]] in core so a process that is not the target
* session can compile this file. `settings` / `deps` / `library` stay here because they return sbt types.
* Row collection (`coords` / `pins` / `actions` / `ships`) lives on [[Catalog]] in core so a process that is not the
* target session can compile this file. `settings` / `deps` / `library` stay here because they return sbt types.
*
* Drop `MyVersions.settings` at the top of `build.sbt`. Extra settings belong next to that call (`MyVersions.settings
* ++ …`).
Expand All @@ -21,7 +29,7 @@ trait ZipxVersions extends Catalog:
* keys generate and `zipxCheckDeps` read. Inline so [[coords]] / [[pins]] / [[actions]] expand against the concrete
* object.
*/
inline def settings: Seq[Setting[?]] = ZipxVersions.applySettings(sbt, scala, coords, pins, actions)
inline def settings: Seq[Setting[?]] = ZipxVersions.applySettings(sbt, scala, coords, pins, actions, ships)

/** Per-module `crossScalaVersions` from [[crossScala]]. Scala-3-only modules inherit [[settings]] and skip this. */
def cross: Seq[Setting[?]] = Seq(
Expand All @@ -45,13 +53,38 @@ object ZipxVersions:
rows: Seq[ZipxCoord],
pinRows: Seq[Pin] = Nil,
actionRows: Seq[Action] = Nil,
): Seq[Setting[?]] = Seq(
scalaVersion := (scalaVer: String),
zipxVersions := rows,
zipxPins := pinRows,
zipxActionRows := actionRows,
zipxSbt := Some(sbtVer),
zipxScala := Some(scalaVer),
zipxCheckDeps := true,
)
shipRows: Seq[PublishedRow] = Nil,
): Seq[Setting[?]] =
val catalog = Seq(
scalaVersion := (scalaVer: String),
zipxVersions := rows,
zipxPins := pinRows,
zipxActionRows := actionRows,
zipxShips := shipRows,
zipxSbt := Some(sbtVer),
zipxScala := Some(scalaVer),
zipxCheckDeps := true,
)
val versions =
if shipRows.isEmpty then Nil
else
Seq(
version := Def.uncached {
val id = thisProject.value.id
zipx.core.Modver.rowForProject(id, shipRows) match
case Some(pub) => s"${pub.version}-ci"
case None => "0.1.0-SNAPSHOT"
},
pomPostProcess := { (node: scala.xml.Node) => stripCiPomVersions(node) },
)
catalog ++ versions
end applySettings

/** Never emit `-ci` into a POM. Sibling `dependsOn` revisions become catalog release numbers. */
private def stripCiPomVersions(node: scala.xml.Node): scala.xml.Node =
node match
case e: scala.xml.Elem if e.label == "version" && e.text.endsWith("-ci") =>
e.copy(child = Seq(scala.xml.Text(e.text.dropRight(3))))
case e: scala.xml.Elem => e.copy(child = e.child.map(stripCiPomVersions))
case other => other
end ZipxVersions
Loading