From 8af454e50ef8a401646bb6ec921a30ea746a3caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Sat, 11 Apr 2026 11:07:01 +0200 Subject: [PATCH 1/2] feat: cross-compile for Scala 2.12 + 2.13 with Java 17 target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add crossScalaVersions (2.12.20, 2.13.16) with conditional scalacOptions - Replace akron with cron-utils (pure Java, cross-compile friendly) - Migrate Configs → ConfigReader for Scala 2.13 compatibility - Bump generic-persistence-api 0.7.3 → 0.8.1, scalapb-extensions 0.1.7 → 0.2.0 - Upgrade sbt 1.7.1 → 1.8.3, Docker base image to eclipse-temurin:17-jdk - Update GitHub Actions to v4, JDK 17, add cross-compile CI step - Fix Scala 2.13 postfix notation and exhaustive match warnings - Version bump 0.7.3 → 0.8-SNAPSHOT Closed Issue #7 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/build.yml | 48 +++++++----- .github/workflows/release.yml | 74 ++++++++++--------- api/build.sbt | 2 +- build.sbt | 71 +++++++++++++++--- common/build.sbt | 4 +- .../scheduler/config/SchedulerSettings.scala | 6 +- .../softnetwork/scheduler/model/package.scala | 21 ++++-- .../scheduler/api/SchedulerServer.scala | 1 + project/Versions.scala | 2 +- project/build.properties | 2 +- .../scheduler/api/SchedulerClientSpec.scala | 10 +-- 11 files changed, 160 insertions(+), 81 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f79041..c7fd65d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,12 +8,12 @@ name: Build on: workflow_dispatch: push: - branches: + branches: - '!main' # - '*' # matches every branch that doesn't contain a '/' # - '*/*' # matches every branch containing a single '/' # - '**' # matches every branch -# - '!main' # excludes main +# - '!main' # excludes main pull_request: branches: - '**' @@ -23,27 +23,33 @@ permissions: jobs: test: - runs-on: self-hosted -# runs-on: ubuntu-latest - env: - # define Java options for both official sbt and sbt-extras - JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 - JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 +# runs-on: self-hosted + runs-on: ubuntu-latest steps: + - name: Env + run: | + echo "JFROG_USER=${{ secrets.JFROG_USER }}" >> $GITHUB_ENV + echo "JFROG_PASSWORD=${{ secrets.JFROG_PASSWORD }}" >> $GITHUB_ENV + echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV + echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV - name: Checkout - uses: actions/checkout@v3 - - name: Set up JDK 8 - uses: actions/setup-java@v3 + uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - java-version: '8' + java-version: '17' distribution: 'temurin' # cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 + - name: Cross Compile + run: SBT_OPTS="-Xss4M -Xms1g -Xmx4g -Dfile.encoding=UTF-8" sbt '+ Test/compile' - name: Run tests & Coverage Report - run: sbt coverage test coverageReport + run: SBT_OPTS="-Xss4M -Xms1g -Xmx4g -Dfile.encoding=UTF-8" sbt coverage test coverageReport - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: - files: common/target/scala-2.12/coverage-report/cobertura.xml,core/target/scala-2.12/coverage-report/cobertura.xml,teskit/target/scala-2.12/coverage-report/cobertura.xml + files: common/target/scala-*/coverage-report/cobertura.xml,core/target/scala-*/coverage-report/cobertura.xml,testkit/target/scala-*/coverage-report/cobertura.xml flags: unittests fail_ci_if_error: true verbose: true @@ -52,12 +58,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 - - name: Set up JDK 8 - uses: actions/setup-java@v3 + uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - java-version: '8' + java-version: '17' distribution: 'temurin' # cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 - name: Formatting - run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck \ No newline at end of file + run: sbt scalafmtSbtCheck scalafmtCheck Test/scalafmtCheck diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31ca456..c06224e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,55 +8,63 @@ name: Release on: workflow_dispatch: push: - branches: + branches: - 'main' # - '*' # matches every branch that doesn't contain a '/' # - '*/*' # matches every branch containing a single '/' # - '**' # matches every branch -# - '!main' # excludes main +# - '!main' # excludes main permissions: contents: read jobs: release: - runs-on: self-hosted -# runs-on: ubuntu-latest - env: - # define Java options for both official sbt and sbt-extras - JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 - JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 +# runs-on: self-hosted + runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set up JDK 8 - uses: actions/setup-java@v3 - with: - java-version: '8' - distribution: 'temurin' - # cache: 'sbt' - - name: Run tests & Coverage Report - run: sbt coverage test coverageReport coverageAggregate - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - files: common/target/scala-2.12/coverage-report/cobertura.xml,core/target/scala-2.12/coverage-report/cobertura.xml,teskit/target/scala-2.12/coverage-report/cobertura.xml - flags: unittests - fail_ci_if_error: true - verbose: true - - name: Publish - run: sbt publish + - name: Env + run: | + echo "JFROG_USER=${{ secrets.JFROG_USER }}" >> $GITHUB_ENV + echo "JFROG_PASSWORD=${{ secrets.JFROG_PASSWORD }}" >> $GITHUB_ENV + echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV + echo "CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }}" >> $GITHUB_ENV + - name: Checkout + uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' +# cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 + - name: Cross Compile + run: SBT_OPTS="-Xss4M -Xms1g -Xmx4g -Dfile.encoding=UTF-8" sbt '+ Test/compile' + - name: Run tests & Coverage Report + run: SBT_OPTS="-Xss4M -Xms1g -Xmx4g -Dfile.encoding=UTF-8" sbt coverage test coverageReport coverageAggregate + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: common/target/scala-*/coverage-report/cobertura.xml,core/target/scala-*/coverage-report/cobertura.xml,testkit/target/scala-*/coverage-report/cobertura.xml + flags: unittests + fail_ci_if_error: false + verbose: true + - name: Publish + run: SBT_OPTS="-Xss4M -Xms1g -Xmx4g" sbt '+ publish' lint: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 - - name: Set up JDK 8 - uses: actions/setup-java@v3 + uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - java-version: '8' + java-version: '17' distribution: 'temurin' - # cache: 'sbt' +# cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 - name: Formatting - run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck \ No newline at end of file + run: sbt scalafmtSbtCheck scalafmtCheck Test/scalafmtCheck diff --git a/api/build.sbt b/api/build.sbt index 60c98ed..ae3f07a 100644 --- a/api/build.sbt +++ b/api/build.sbt @@ -2,7 +2,7 @@ import com.typesafe.sbt.packager.docker._ Compile / mainClass := Some("app.softnetwork.scheduler.api.SchedulerEndpointsPostgresLauncher") -dockerBaseImage := "openjdk:8" +dockerBaseImage := "eclipse-temurin:17-jdk" dockerEntrypoint := Seq(s"${(Docker / defaultLinuxInstallLocation).value}/bin/entrypoint.sh") diff --git a/build.sbt b/build.sbt index d3bc52d..0df34e3 100644 --- a/build.sbt +++ b/build.sbt @@ -1,14 +1,28 @@ +lazy val scala212 = "2.12.20" +lazy val scala213 = "2.13.16" +lazy val javacCompilerVersion = "17" +lazy val scalacCompilerOptions = Seq("-deprecation", "-feature") + +lazy val moduleSettings = Seq( + crossScalaVersions := Seq(scala212, scala213), + scalacOptions ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 12)) => scalacCompilerOptions :+ "-Ypartial-unification" + case Some((2, 13)) => scalacCompilerOptions :+ s"-release:$javacCompilerVersion" + case _ => Seq.empty + } + } +) + ThisBuild / organization := "app.softnetwork" name := "scheduler" -ThisBuild / version := "0.7.3" - -ThisBuild / scalaVersion := "2.12.15" +ThisBuild / version := "0.8-SNAPSHOT" -ThisBuild / scalacOptions ++= Seq("-deprecation", "-feature", "-target:jvm-1.8", "-Ypartial-unification") +ThisBuild / scalaVersion := scala212 -ThisBuild / javacOptions ++= Seq("-source", "1.8", "-target", "1.8", "-Xlint") +ThisBuild / javacOptions ++= Seq("-source", javacCompilerVersion, "-target", javacCompilerVersion) ThisBuild / resolvers ++= Seq( "Softnetwork Server" at "https://softnetwork.jfrog.io/artifactory/releases/", @@ -25,21 +39,47 @@ val scalatest = Seq( ThisBuild / libraryDependencies ++= Seq( "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf", - "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.1" + "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2", + "org.scala-lang.modules" %% "scala-collection-compat" % "2.11.0" ) ++ scalatest ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always //FIXME - remove when scalatest is updated +ThisBuild / javaOptions ++= Seq( + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED", + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED", + "--add-opens=java.base/java.math=ALL-UNNAMED", + "--add-opens=java.base/java.io=ALL-UNNAMED", + "--add-opens=java.base/java.net=ALL-UNNAMED", + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/java.text=ALL-UNNAMED", + "--add-opens=java.base/java.time=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED" +) + +ThisBuild / Test / fork := true + +ThisBuild / Test / javaOptions ++= (ThisBuild / javaOptions).value + Test / parallelExecution := false lazy val common = project.in(file("common")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .enablePlugins(AkkaGrpcPlugin) lazy val core = project.in(file("core")) .configs(IntegrationTest) - .settings(Defaults.itSettings, app.softnetwork.Info.infoSettings) + .settings( + Defaults.itSettings, + app.softnetwork.Info.infoSettings, + moduleSettings + ) .enablePlugins(BuildInfoPlugin) .dependsOn( common % "compile->compile;test->test;it->it" @@ -47,7 +87,10 @@ lazy val core = project.in(file("core")) lazy val testkit = project.in(file("testkit")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .enablePlugins(BuildInfoPlugin) .dependsOn( core % "compile->compile;test->test;it->it" @@ -55,7 +98,10 @@ lazy val testkit = project.in(file("testkit")) lazy val api = project.in(file("api")) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + moduleSettings + ) .enablePlugins(DockerPlugin, JavaAppPackaging) .dependsOn( core % "compile->compile;test->test;it->it" @@ -64,5 +110,8 @@ lazy val api = project.in(file("api")) lazy val root = project.in(file(".")) .aggregate(common, core, testkit, api) .configs(IntegrationTest) - .settings(Defaults.itSettings) + .settings( + Defaults.itSettings, + crossScalaVersions := Nil + ) diff --git a/common/build.sbt b/common/build.sbt index 51af1a5..e407a92 100644 --- a/common/build.sbt +++ b/common/build.sbt @@ -5,11 +5,11 @@ organization := "app.softnetwork.scheduler" name := "scheduler-common" libraryDependencies ++= Seq( - "com.markatta" %% "akron" % "1.2" excludeAll(ExclusionRule(organization = "com.typesafe.akka"), ExclusionRule(organization = "org.scala-lang.modules")), + "com.cronutils" % "cron-utils" % "9.2.1", // session "app.softnetwork.session" %% "session-core" % Versions.genericPersistence, "app.softnetwork.api" %% "generic-server-api" % Versions.genericPersistence, - "app.softnetwork.protobuf" %% "scalapb-extensions" % "0.1.7" + "app.softnetwork.protobuf" %% "scalapb-extensions" % "0.2.0" ) Compile / unmanagedResourceDirectories += baseDirectory.value / "src/main/protobuf" diff --git a/common/src/main/scala/app/softnetwork/scheduler/config/SchedulerSettings.scala b/common/src/main/scala/app/softnetwork/scheduler/config/SchedulerSettings.scala index 37b03f1..8df2b1d 100644 --- a/common/src/main/scala/app/softnetwork/scheduler/config/SchedulerSettings.scala +++ b/common/src/main/scala/app/softnetwork/scheduler/config/SchedulerSettings.scala @@ -2,7 +2,7 @@ package app.softnetwork.scheduler.config import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.scalalogging.StrictLogging -import configs.Configs +import configs.ConfigReader /** Created by smanciot on 16/12/2020. */ @@ -12,7 +12,9 @@ object SchedulerSettings extends StrictLogging { ConfigFactory.load().withFallback(ConfigFactory.load("softnetwork-scheduler.conf")) lazy val SchedulerConfig: SchedulerConfig = - Configs[SchedulerConfig].get(config, "softnetwork.scheduler").toEither match { + ConfigReader[SchedulerConfig] + .read(config, "softnetwork.scheduler") + .toEither match { case Left(configError) => logger.error(s"Something went wrong with the provided arguments $configError") throw configError.configException diff --git a/common/src/main/scala/app/softnetwork/scheduler/model/package.scala b/common/src/main/scala/app/softnetwork/scheduler/model/package.scala index a3b434e..dabd9ad 100644 --- a/common/src/main/scala/app/softnetwork/scheduler/model/package.scala +++ b/common/src/main/scala/app/softnetwork/scheduler/model/package.scala @@ -6,9 +6,14 @@ import java.sql.Timestamp import java.time.LocalDateTime import java.time.temporal.ChronoUnit import java.util.Date -import com.markatta.akron.CronExpression +import com.cronutils.model.CronType +import com.cronutils.model.definition.CronDefinitionBuilder +import com.cronutils.model.time.ExecutionTime +import com.cronutils.parser.CronParser import com.typesafe.scalalogging.StrictLogging +import java.time.ZoneId +import java.time.ZonedDateTime import scala.concurrent.duration._ import scala.language.implicitConversions import scala.util.{Failure, Success, Try} @@ -60,20 +65,26 @@ package object model { def view: ScheduleView = ScheduleView(this) } + private val cronDefinition = + CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX) + + private val cronParser = new CronParser(cronDefinition) + trait CronTabItem extends StrictLogging { def cron: String - lazy val cronExpression: CronExpression = Try { - CronExpression(cron) + lazy val executionTime: ExecutionTime = Try { + ExecutionTime.forCron(cronParser.parse(cron)) } match { case Success(s) => s case Failure(f) => logger.error(f.getMessage + s" -> [$cron]") - CronExpression("*/5 * * * *") // By default every 5 minutes + ExecutionTime.forCron(cronParser.parse("*/5 * * * *")) // By default every 5 minutes } def nextLocalDateTime(): Option[LocalDateTime] = { - cronExpression.nextTriggerTime(LocalDateTime.now()) + val next = executionTime.nextExecution(ZonedDateTime.now(ZoneId.systemDefault())) + if (next.isPresent) Some(next.get().toLocalDateTime) else None } def next(from: Option[Date] = None): Option[FiniteDuration] = { diff --git a/core/src/main/scala/app/softnetwork/scheduler/api/SchedulerServer.scala b/core/src/main/scala/app/softnetwork/scheduler/api/SchedulerServer.scala index 1693f6d..91abdcc 100644 --- a/core/src/main/scala/app/softnetwork/scheduler/api/SchedulerServer.scala +++ b/core/src/main/scala/app/softnetwork/scheduler/api/SchedulerServer.scala @@ -49,6 +49,7 @@ trait SchedulerServer extends SchedulerServiceApi with SchedulerHandler { case _: CronTabAdded => AddCronTabResponse(true) case _ => AddCronTabResponse() } + case _ => Future.successful(AddCronTabResponse()) } } diff --git a/project/Versions.scala b/project/Versions.scala index 15f15e1..1f75f83 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,6 +1,6 @@ object Versions { - val genericPersistence = "0.7.3" + val genericPersistence = "0.8.1" val scalatest = "3.2.16" } diff --git a/project/build.properties b/project/build.properties index 22af262..72413de 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.7.1 +sbt.version=1.8.3 diff --git a/testkit/src/test/scala/app/softnetwork/scheduler/api/SchedulerClientSpec.scala b/testkit/src/test/scala/app/softnetwork/scheduler/api/SchedulerClientSpec.scala index d62c14e..62d268b 100644 --- a/testkit/src/test/scala/app/softnetwork/scheduler/api/SchedulerClientSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/scheduler/api/SchedulerClientSpec.scala @@ -38,12 +38,12 @@ class SchedulerClientSpec "Scheduler client" must { "add schedule" in { - assert(client.addSchedule(schedule) complete ()) + assert(client.addSchedule(schedule).complete()) } "load scheduler" in { val scheduler: Option[Scheduler] = - client.loadScheduler(Some("my-scheduler")) complete () match { + client.loadScheduler(Some("my-scheduler")).complete() match { case Success(value) => value case Failure(_) => None } @@ -55,17 +55,17 @@ class SchedulerClientSpec "remove schedule" in { assert( - client.removeSchedule(schedule.persistenceId, schedule.entityId, schedule.key) complete () + client.removeSchedule(schedule.persistenceId, schedule.entityId, schedule.key).complete() ) } "add cron tab" in { - assert(client.addCronTab(cronTab) complete ()) + assert(client.addCronTab(cronTab).complete()) } "remove cron tab" in { assert( - client.removeCronTab(cronTab.persistenceId, cronTab.entityId, cronTab.key) complete () + client.removeCronTab(cronTab.persistenceId, cronTab.entityId, cronTab.key).complete() ) } } From 9d4fefb115c9f67564779afc11e7cc799be83d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Sat, 11 Apr 2026 11:26:45 +0200 Subject: [PATCH 2/2] chore: update sbt-softnetwork plugins to version 0.2.0 and sbt-scoverage to 2.3.0 --- project/plugins.sbt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 18ae239..70279cf 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,11 +7,11 @@ resolvers ++= Seq( "Softnetwork releases" at "https://softnetwork.jfrog.io/artifactory/releases/" ) -addSbtPlugin("app.softnetwork.sbt-softnetwork" % "sbt-softnetwork-git" % "0.1.7") +addSbtPlugin("app.softnetwork.sbt-softnetwork" % "sbt-softnetwork-git" % "0.2.0") -addSbtPlugin("app.softnetwork.sbt-softnetwork" % "sbt-softnetwork-info" % "0.1.7") +addSbtPlugin("app.softnetwork.sbt-softnetwork" % "sbt-softnetwork-info" % "0.2.0") -addSbtPlugin("app.softnetwork.sbt-softnetwork" % "sbt-softnetwork-publish" % "0.1.7") +addSbtPlugin("app.softnetwork.sbt-softnetwork" % "sbt-softnetwork-publish" % "0.2.0") addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.10") @@ -19,4 +19,4 @@ addDependencyTreePlugin //addSbtPlugin("com.typesafe.sbt" % "sbt-multi-jvm" % "0.4.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.8") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0")