Build Tools and Project Management: SBT, Mill, and Scala CLI
Effective build tools and project management are essential for successful Scala development. This comprehensive lesson covers SBT (Scala Build Tool), Mill, and Scala CLI, exploring dependency management, multi-module projects, build optimization, and modern development workflows for enterprise-scale applications.
SBT (Scala Build Tool) Mastery
Advanced SBT Configuration and Multi-Module Projects
// project/plugins.sbt - Essential SBT plugins
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.8")
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.4.2")
addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4")
addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.6")
addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.1")
// project/Dependencies.scala - Centralized dependency management
import sbt._
object Dependencies {
// Versions
object Versions {
val scala213 = "2.13.11"
val scala3 = "3.3.0"
val akka = "2.8.3"
val akkaHttp = "10.5.2"
val cats = "2.9.0"
val circe = "0.14.5"
val doobie = "1.0.0-RC4"
val http4s = "0.23.23"
val zio = "2.0.15"
val tapir = "1.6.4"
val scalaTest = "3.2.16"
val testContainers = "0.40.17"
}
// Libraries organized by domain
object Libraries {
// Core functional programming
val cats = Seq(
"org.typelevel" %% "cats-core" % Versions.cats,
"org.typelevel" %% "cats-effect" % "3.5.1",
"org.typelevel" %% "cats-mtl" % "1.3.0"
)
// JSON processing
val circe = Seq(
"io.circe" %% "circe-core" % Versions.circe,
"io.circe" %% "circe-generic" % Versions.circe,
"io.circe" %% "circe-parser" % Versions.circe,
"io.circe" %% "circe-refined" % Versions.circe
)
// HTTP and web services
val http4s = Seq(
"org.http4s" %% "http4s-ember-server" % Versions.http4s,
"org.http4s" %% "http4s-ember-client" % Versions.http4s,
"org.http4s" %% "http4s-circe" % Versions.http4s,
"org.http4s" %% "http4s-dsl" % Versions.http4s
)
val akka = Seq(
"com.typesafe.akka" %% "akka-actor-typed" % Versions.akka,
"com.typesafe.akka" %% "akka-stream" % Versions.akka,
"com.typesafe.akka" %% "akka-cluster-typed" % Versions.akka,
"com.typesafe.akka" %% "akka-persistence-typed" % Versions.akka,
"com.typesafe.akka" %% "akka-serialization-jackson" % Versions.akka
)
val akkaHttp = Seq(
"com.typesafe.akka" %% "akka-http" % Versions.akkaHttp,
"com.typesafe.akka" %% "akka-http-spray-json" % Versions.akkaHttp,
"com.typesafe.akka" %% "akka-http-cors" % "1.2.0"
)
// Database access
val doobie = Seq(
"org.tpolecat" %% "doobie-core" % Versions.doobie,
"org.tpolecat" %% "doobie-hikari" % Versions.doobie,
"org.tpolecat" %% "doobie-postgres" % Versions.doobie,
"org.tpolecat" %% "doobie-scalatest" % Versions.doobie % Test
)
// ZIO ecosystem
val zio = Seq(
"dev.zio" %% "zio" % Versions.zio,
"dev.zio" %% "zio-streams" % Versions.zio,
"dev.zio" %% "zio-json" % "0.6.0",
"dev.zio" %% "zio-config" % "4.0.0-RC16",
"dev.zio" %% "zio-logging" % "2.1.13"
)
// API documentation and validation
val tapir = Seq(
"com.softwaremill.sttp.tapir" %% "tapir-core" % Versions.tapir,
"com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % Versions.tapir,
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % Versions.tapir,
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui" % Versions.tapir,
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % Versions.tapir
)
// Testing
val testing = Seq(
"org.scalatest" %% "scalatest" % Versions.scalaTest % Test,
"org.scalatestplus" %% "scalacheck-1-17" % "3.2.16.0" % Test,
"org.typelevel" %% "cats-effect-testing-scalatest" % "1.5.0" % Test,
"com.dimafeng" %% "testcontainers-scala-scalatest" % Versions.testContainers % Test,
"com.dimafeng" %% "testcontainers-scala-postgresql" % Versions.testContainers % Test
)
// Logging and monitoring
val logging = Seq(
"ch.qos.logback" % "logback-classic" % "1.4.8",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.5",
"io.micrometer" % "micrometer-core" % "1.11.2",
"io.micrometer" % "micrometer-registry-prometheus" % "1.11.2"
)
// Configuration
val config = Seq(
"com.github.pureconfig" %% "pureconfig" % "0.17.4",
"com.github.pureconfig" %% "pureconfig-cats-effect" % "0.17.4"
)
// Utilities
val utilities = Seq(
"eu.timepit" %% "refined" % "0.11.0",
"com.beachape" %% "enumeratum" % "1.7.3",
"org.typelevel" %% "squants" % "1.8.3"
)
}
// Dependency sets for different modules
object DependencySets {
val core = Libraries.cats ++ Libraries.config ++ Libraries.utilities
val web = Libraries.http4s ++ Libraries.circe ++ Libraries.tapir
val persistence = Libraries.doobie ++ Libraries.logging
val streaming = Libraries.akka ++ Libraries.akkaHttp
val zioStack = Libraries.zio ++ Libraries.circe
val testing = Libraries.testing
val monitoring = Libraries.logging
}
}
// build.sbt - Advanced multi-module build configuration
ThisBuild / scalaVersion := Dependencies.Versions.scala213
ThisBuild / version := "1.0.0-SNAPSHOT"
ThisBuild / organization := "com.example"
ThisBuild / organizationName := "Example Corp"
// Global settings
ThisBuild / scalacOptions ++= Seq(
"-deprecation",
"-encoding", "UTF-8",
"-feature",
"-language:existentials",
"-language:higherKinds",
"-language:implicitConversions",
"-unchecked",
"-Xlint",
"-Ywarn-dead-code",
"-Ywarn-numeric-widen",
"-Ywarn-value-discard",
"-Xfatal-warnings"
)
// Compiler plugin settings
ThisBuild / addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full)
ThisBuild / addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1")
// Test settings
ThisBuild / Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-oD")
ThisBuild / Test / parallelExecution := false
ThisBuild / Test / fork := true
// Coverage settings
ThisBuild / coverageMinimumStmtTotal := 80
ThisBuild / coverageFailOnMinimum := true
ThisBuild / coverageExcludedPackages := ".*BuildInfo.*"
// Publishing settings
ThisBuild / publishTo := {
val nexus = "https://oss.sonatype.org/"
if (isSnapshot.value) Some("snapshots" at nexus + "content/repositories/snapshots")
else Some("releases" at nexus + "service/local/staging/deploy/maven2")
}
ThisBuild / publishMavenStyle := true
ThisBuild / Test / publishArtifact := false
// Module definitions
lazy val root = (project in file("."))
.settings(
name := "scala-enterprise-app",
publish / skip := true
)
.aggregate(core, domain, infrastructure, web, cli, integration)
lazy val core = (project in file("modules/core"))
.settings(
name := "core",
libraryDependencies ++= Dependencies.DependencySets.core,
Compile / resourceGenerators += Def.task {
val file = (Compile / resourceManaged).value / "version.properties"
val contents = s"version=${version.value}"
IO.write(file, contents)
Seq(file)
}.taskValue
)
lazy val domain = (project in file("modules/domain"))
.dependsOn(core)
.settings(
name := "domain",
libraryDependencies ++= Dependencies.DependencySets.core
)
lazy val infrastructure = (project in file("modules/infrastructure"))
.dependsOn(domain)
.settings(
name := "infrastructure",
libraryDependencies ++= Dependencies.DependencySets.persistence ++
Dependencies.DependencySets.monitoring
)
lazy val web = (project in file("modules/web"))
.dependsOn(domain, infrastructure)
.enablePlugins(JavaAppPackaging, DockerPlugin)
.settings(
name := "web",
libraryDependencies ++= Dependencies.DependencySets.web,
// Docker configuration
Docker / packageName := "scala-enterprise-web",
Docker / version := version.value,
dockerBaseImage := "openjdk:17-jre-slim",
dockerExposedPorts ++= Seq(8080, 8081),
dockerRepository := Some("your-registry.com"),
// JVM options for production
Universal / javaOptions ++= Seq(
"-J-Xmx2g",
"-J-Xms1g",
"-J-XX:+UseG1GC",
"-J-XX:+UseStringDeduplication",
"-J-Dpidfile.path=/dev/null"
)
)
lazy val cli = (project in file("modules/cli"))
.dependsOn(domain, infrastructure)
.enablePlugins(JavaAppPackaging, GraalVMNativeImagePlugin)
.settings(
name := "cli",
libraryDependencies ++= Dependencies.DependencySets.core,
// Native image configuration
GraalVMNativeImage / graalVMNativeImageOptions ++= Seq(
"--no-fallback",
"--initialize-at-build-time",
"--report-unsupported-elements-at-runtime",
"--allow-incomplete-classpath"
)
)
lazy val integration = (project in file("modules/integration"))
.dependsOn(web % "test->test", infrastructure % "test->test")
.settings(
name := "integration",
libraryDependencies ++= Dependencies.DependencySets.testing,
Test / fork := true,
publish / skip := true
)
// Custom commands and aliases
addCommandAlias("fullTest", "clean; coverage; test; coverageReport")
addCommandAlias("fullCheck", "clean; scalafmtCheckAll; scalafix --check; test")
addCommandAlias("prepare", "scalafmtAll; scalafix; test")
addCommandAlias("buildDocker", "web/docker:publishLocal")
addCommandAlias("buildNative", "cli/GraalVMNativeImage/packageBin")
// Development tools integration
Global / onChangedBuildSource := ReloadOnSourceChanges
Global / semanticdbEnabled := true
Global / semanticdbVersion := scalafixSemanticdb.revision
// Wartremover configuration for code quality
wartremoverErrors ++= Warts.unsafe.filterNot(_ == Wart.Any)
SBT Plugins and Automation
// project/ProjectPlugin.scala - Custom SBT plugin for project standards
import sbt._
import sbt.Keys._
import sbt.plugins.JvmPlugin
object ProjectPlugin extends AutoPlugin {
override def trigger = allRequirements
override def requires = JvmPlugin
object autoImport {
val projectStandards = settingKey[Boolean]("Enable project standards")
val generateBuildInfo = taskKey[Seq[File]]("Generate build info")
val checkDependencyUpdates = taskKey[Unit]("Check for dependency updates")
val validateProject = taskKey[Unit]("Validate project structure")
}
import autoImport._
override lazy val projectSettings = Seq(
projectStandards := true,
// Build info generation
generateBuildInfo := {
val file = (Compile / sourceManaged).value / "BuildInfo.scala"
val buildTime = java.time.Instant.now().toString
val gitCommit = sys.process.Process("git rev-parse HEAD").!!.trim
val contents =
s"""package buildinfo
|
|object BuildInfo {
| val version: String = "${version.value}"
| val scalaVersion: String = "${scalaVersion.value}"
| val buildTime: String = "$buildTime"
| val gitCommit: String = "$gitCommit"
| val name: String = "${name.value}"
|}
""".stripMargin
IO.write(file, contents)
Seq(file)
},
// Dependency update checking
checkDependencyUpdates := {
val log = streams.value.log
log.info("Checking for dependency updates...")
// This would integrate with dependency update tools
// For now, just a placeholder
log.info("Dependency check completed")
},
// Project validation
validateProject := {
val log = streams.value.log
val baseDir = (ThisBuild / baseDirectory).value
// Check for required files
val requiredFiles = Seq(
"README.md",
"LICENSE",
".gitignore",
"docker-compose.yml"
)
val missingFiles = requiredFiles.filterNot(f => (baseDir / f).exists())
if (missingFiles.nonEmpty) {
log.warn(s"Missing required files: ${missingFiles.mkString(", ")}")
} else {
log.info("Project structure validation passed")
}
},
// Add build info generation to compile
Compile / sourceGenerators += generateBuildInfo.taskValue
)
}
// project/DatabasePlugin.scala - Database management plugin
object DatabasePlugin extends AutoPlugin {
object autoImport {
val dbMigrate = taskKey[Unit]("Run database migrations")
val dbReset = taskKey[Unit]("Reset database")
val dbSeed = taskKey[Unit]("Seed database with test data")
val dbUrl = settingKey[String]("Database URL")
val dbUser = settingKey[String]("Database user")
val dbPassword = settingKey[String]("Database password")
}
import autoImport._
override lazy val projectSettings = Seq(
dbUrl := sys.env.getOrElse("DB_URL", "jdbc:postgresql://localhost:5432/myapp"),
dbUser := sys.env.getOrElse("DB_USER", "postgres"),
dbPassword := sys.env.getOrElse("DB_PASSWORD", "postgres"),
dbMigrate := {
val log = streams.value.log
log.info("Running database migrations...")
// Integration with Flyway or Liquibase
val cp = (Compile / fullClasspath).value
val migrationClass = "com.example.db.Migration"
val command = Seq(
"java", "-cp", cp.files.mkString(":"),
migrationClass, "migrate",
"--url", dbUrl.value,
"--user", dbUser.value,
"--password", dbPassword.value
)
sys.process.Process(command).!
log.info("Database migration completed")
},
dbReset := {
val log = streams.value.log
log.info("Resetting database...")
// Reset logic here
log.info("Database reset completed")
},
dbSeed := {
val log = streams.value.log
log.info("Seeding database...")
// Seed logic here
log.info("Database seeding completed")
}
)
}
// Advanced SBT scripting
// scripts/release.scala - Release automation script
import scala.sys.process._
object ReleaseScript {
def main(args: Array[String]): Unit = {
val currentVersion = getCurrentVersion()
val nextVersion = getNextVersion(currentVersion)
println(s"Current version: $currentVersion")
println(s"Next version: $nextVersion")
if (confirmRelease(nextVersion)) {
performRelease(nextVersion)
} else {
println("Release cancelled")
}
}
private def getCurrentVersion(): String = {
val result = "git describe --tags --abbrev=0".!!.trim
result.stripPrefix("v")
}
private def getNextVersion(current: String): String = {
val parts = current.split("\\.").map(_.toInt)
val (major, minor, patch) = (parts(0), parts(1), parts(2))
s"$major.$minor.${patch + 1}"
}
private def confirmRelease(version: String): Boolean = {
print(s"Release version $version? (y/N): ")
scala.io.StdIn.readLine().toLowerCase.startsWith("y")
}
private def performRelease(version: String): Unit = {
// Update version
updateVersionFile(version)
// Run tests
runCommand("sbt clean test")
// Create git tag
runCommand(s"git add .")
runCommand(s"git commit -m 'Release version $version'")
runCommand(s"git tag v$version")
// Build and publish
runCommand("sbt publishSigned")
// Push changes
runCommand("git push origin main")
runCommand("git push origin --tags")
println(s"Successfully released version $version")
}
private def updateVersionFile(version: String): Unit = {
val versionFile = new java.io.File("version.sbt")
val content = s'ThisBuild / version := "$version"'
java.nio.file.Files.write(versionFile.toPath, content.getBytes)
}
private def runCommand(command: String): Unit = {
println(s"Running: $command")
val exitCode = command.!
if (exitCode != 0) {
throw new RuntimeException(s"Command failed with exit code $exitCode: $command")
}
}
}
Mill Build Tool
Mill Configuration and Advanced Features
// build.sc - Mill build configuration
import mill._
import mill.scalalib._
import mill.scalalib.scalafmt._
import mill.scalalib.publish._
// Cross-build configuration
val scalaVersions = Seq("2.13.11", "3.3.0")
object Versions {
val cats = "2.9.0"
val circe = "0.14.5"
val http4s = "0.23.23"
val scalaTest = "3.2.16"
}
// Core module
object core extends Cross[CoreModule](scalaVersions: _*)
class CoreModule(val crossScalaVersion: String) extends CrossScalaModule with ScalafmtModule {
def ivyDeps = Agg(
ivy"org.typelevel::cats-core:${Versions.cats}",
ivy"org.typelevel::cats-effect:3.5.1",
ivy"eu.timepit::refined:0.11.0"
)
object test extends Tests with TestModule.ScalaTest {
def ivyDeps = Agg(
ivy"org.scalatest::scalatest:${Versions.scalaTest}",
ivy"org.typelevel::cats-effect-testing-scalatest:1.5.0"
)
}
// Custom tasks
def generateBuildInfo = T {
val buildInfoDir = T.dest / "buildinfo"
os.makeDir.all(buildInfoDir)
val buildInfoFile = buildInfoDir / "BuildInfo.scala"
val content =
s"""package buildinfo
|
|object BuildInfo {
| val version: String = "${publishVersion()}"
| val scalaVersion: String = "$crossScalaVersion"
| val buildTime: String = "${java.time.Instant.now()}"
| val millVersion: String = "${mill.BuildInfo.millVersion}"
|}
""".stripMargin
os.write(buildInfoFile, content)
PathRef(buildInfoDir)
}
override def generatedSources = super.generatedSources() ++ Seq(generateBuildInfo())
}
// Domain module
object domain extends Cross[DomainModule](scalaVersions: _*)
class DomainModule(val crossScalaVersion: String) extends CrossScalaModule with ScalafmtModule {
def moduleDeps = Seq(core(crossScalaVersion))
def ivyDeps = Agg(
ivy"io.circe::circe-core:${Versions.circe}",
ivy"io.circe::circe-generic:${Versions.circe}",
ivy"com.beachape::enumeratum:1.7.3"
)
object test extends Tests with TestModule.ScalaTest {
def ivyDeps = Agg(
ivy"org.scalatest::scalatest:${Versions.scalaTest}",
ivy"io.circe::circe-parser:${Versions.circe}"
)
}
}
// Web module
object web extends Cross[WebModule](scalaVersions: _*)
class WebModule(val crossScalaVersion: String) extends CrossScalaModule with ScalafmtModule {
def moduleDeps = Seq(core(crossScalaVersion), domain(crossScalaVersion))
def ivyDeps = Agg(
ivy"org.http4s::http4s-ember-server:${Versions.http4s}",
ivy"org.http4s::http4s-ember-client:${Versions.http4s}",
ivy"org.http4s::http4s-circe:${Versions.http4s}",
ivy"org.http4s::http4s-dsl:${Versions.http4s}",
ivy"ch.qos.logback:logback-classic:1.4.8"
)
object test extends Tests with TestModule.ScalaTest {
def ivyDeps = Agg(
ivy"org.scalatest::scalatest:${Versions.scalaTest}",
ivy"org.http4s::http4s-testing:${Versions.http4s}"
)
}
// Docker image building
def dockerImage = T {
val jarPath = assembly().path
val dockerfile = T.dest / "Dockerfile"
val dockerfileContent =
s"""FROM openjdk:17-jre-slim
|COPY ${jarPath.last} /app.jar
|EXPOSE 8080
|ENTRYPOINT ["java", "-jar", "/app.jar"]
""".stripMargin
os.write(dockerfile, dockerfileContent)
os.copy(jarPath, T.dest / "app.jar")
val imageName = s"scala-mill-app:${publishVersion()}"
os.proc("docker", "build", "-t", imageName, T.dest).call()
imageName
}
}
// Global settings and tasks
def publishVersion = "1.0.0-SNAPSHOT"
def fmt() = T.command {
mill.scalalib.scalafmt.ScalafmtModule.reformatAll()()
}
def checkFmt() = T.command {
mill.scalalib.scalafmt.ScalafmtModule.checkFormatAll()()
}
def fullTest() = T.command {
val modules = Seq(core, domain, web)
for {
scalaVersion <- scalaVersions
module <- modules
} yield module(scalaVersion).test.test()()
}
def coverage() = T.command {
// Mill doesn't have built-in coverage, but can integrate with external tools
os.proc("scoverage-mill", "coverage").call()
}
// Custom task for dependency analysis
def dependencyTree() = T.command {
val allDeps = for {
scalaVersion <- scalaVersions
module <- Seq(core(scalaVersion), domain(scalaVersion), web(scalaVersion))
} yield {
val deps = module.runClasspath().map(_.path.toString())
s"${module.toString} dependencies:\n${deps.mkString("\n ")}"
}
println(allDeps.mkString("\n\n"))
}
// Publishing configuration
trait PublishModule extends ScalaModule with PublishModule {
def publishVersion = "1.0.0"
def pomSettings = PomSettings(
description = "Scala Enterprise Application",
organization = "com.example",
url = "https://github.com/example/scala-enterprise",
licenses = Seq(License.MIT),
versionControl = VersionControl.github("example", "scala-enterprise"),
developers = Seq(
Developer("dev1", "Developer One", "dev1@example.com")
)
)
}
// Integration with external tools
def sonarqube() = T.command {
val reportPaths = for {
scalaVersion <- scalaVersions
module <- Seq(core(scalaVersion), domain(scalaVersion), web(scalaVersion))
} yield module.test.test()
// Generate SonarQube report
os.proc(
"sonar-scanner",
s"-Dsonar.projectKey=scala-enterprise",
s"-Dsonar.sources=.",
s"-Dsonar.scala.coverage.reportPaths=${reportPaths.mkString(",")}"
).call()
}
// Benchmark task
def benchmark() = T.command {
// JMH benchmark integration
os.proc("java", "-jar", "jmh-benchmarks.jar").call()
}
// Custom Mill extensions
import mill.api.Result
import mill.define.{Discover, ExternalModule}
object Utils extends ExternalModule {
def gitCommit = T.input {
os.proc("git", "rev-parse", "HEAD").call().out.trim()
}
def isDirty = T.input {
os.proc("git", "status", "--porcelain").call().out.nonEmpty
}
def ensureCleanWorkspace() = T.command {
if (isDirty()) {
Result.Failure("Workspace is dirty. Please commit changes first.")
} else {
Result.Success(())
}
}
lazy val millDiscover = Discover[this.type]
}
Scala CLI: Modern Development Workflow
Scala CLI Configuration and Usage
// scala-cli.conf - Project configuration
//> using scala "3.3.0"
//> using jvm "17"
//> using dep "org.typelevel::cats-core:2.9.0"
//> using dep "org.typelevel::cats-effect:3.5.1"
//> using dep "io.circe::circe-core:0.14.5"
//> using dep "io.circe::circe-generic:0.14.5"
//> using dep "io.circe::circe-parser:0.14.5"
//> using dep "org.http4s::http4s-ember-server:0.23.23"
//> using dep "org.http4s::http4s-ember-client:0.23.23"
//> using dep "org.http4s::http4s-circe:0.23.23"
//> using dep "org.http4s::http4s-dsl:0.23.23"
//> using dep "ch.qos.logback:logback-classic:1.4.8"
//> using test.dep "org.scalatest::scalatest:3.2.16"
//> using test.dep "org.typelevel::cats-effect-testing-scalatest:1.5.0"
//> using repository "https://oss.sonatype.org/content/repositories/snapshots"
//> using option "-Xfatal-warnings"
//> using option "-deprecation"
//> using option "-feature"
//> using option "-unchecked"
// Main.scala - Application entry point
//> using dep "com.github.pureconfig::pureconfig:0.17.4"
//> using dep "com.github.pureconfig::pureconfig-cats-effect:0.17.4"
package com.example.app
import cats.effect._
import cats.implicits._
import org.http4s._
import org.http4s.ember.server._
import org.http4s.server.Router
import org.http4s.dsl.io._
import com.comcast.ip4s._
import pureconfig._
import pureconfig.generic.derivation.default._
import pureconfig.module.catseffect.syntax._
case class ServerConfig(
host: String = "0.0.0.0",
port: Int = 8080,
shutdownTimeout: Int = 30
) derives ConfigReader
case class DatabaseConfig(
url: String,
user: String,
password: String,
maxConnections: Int = 10
) derives ConfigReader
case class AppConfig(
server: ServerConfig,
database: DatabaseConfig
) derives ConfigReader
object Main extends IOApp {
def run(args: List[String]): IO[ExitCode] = {
for {
config <- ConfigSource.default.loadF[IO, AppConfig]()
_ <- IO.println(s"Starting server on ${config.server.host}:${config.server.port}")
_ <- startServer(config.server)
} yield ExitCode.Success
}
private def startServer(config: ServerConfig): IO[Unit] = {
val httpApp = Router("/" -> routes).orNotFound
EmberServerBuilder
.default[IO]
.withHost(Host.fromString(config.host).get)
.withPort(Port.fromInt(config.port).get)
.withHttpApp(httpApp)
.build
.use(_ => IO.never)
}
private val routes: HttpRoutes[IO] = HttpRoutes.of[IO] {
case GET -> Root / "health" =>
Ok("Service is healthy")
case GET -> Root / "version" =>
Ok("1.0.0")
case GET -> Root / "users" / IntVar(userId) =>
Ok(s"User ID: $userId")
}
}
// scripts/dev.scala - Development utilities
//> using dep "os-lib::os-lib:0.9.1"
import scala.sys.process._
@main def dev(command: String): Unit = command match {
case "start" => startDevelopment()
case "test" => runTests()
case "format" => formatCode()
case "lint" => lintCode()
case "build" => buildApplication()
case "docker" => buildDocker()
case _ => showHelp()
}
def startDevelopment(): Unit = {
println("Starting development environment...")
// Start database
"docker-compose up -d postgres".!
// Start application in watch mode
"scala-cli run . --watch".!
}
def runTests(): Unit = {
println("Running tests...")
"scala-cli test .".!
}
def formatCode(): Unit = {
println("Formatting code...")
"scala-cli fmt .".!
}
def lintCode(): Unit = {
println("Linting code...")
// Scala CLI doesn't have built-in linting, but can use external tools
"scalafmt --test .".!
}
def buildApplication(): Unit = {
println("Building application...")
val result = "scala-cli package . --output target/app.jar".!
if (result == 0) {
println("Build successful: target/app.jar")
} else {
println("Build failed")
sys.exit(1)
}
}
def buildDocker(): Unit = {
println("Building Docker image...")
buildApplication()
val dockerfile = """
FROM openjdk:17-jre-slim
COPY target/app.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
"""
os.write(os.pwd / "Dockerfile", dockerfile)
"docker build -t scala-cli-app .".!
}
def showHelp(): Unit = {
println("""
|Development script commands:
| start - Start development environment
| test - Run tests
| format - Format code
| lint - Lint code
| build - Build application
| docker - Build Docker image
""".stripMargin)
}
// scripts/benchmark.scala - Performance benchmarking
//> using dep "org.openjdk.jmh:jmh-core:1.36"
//> using dep "org.openjdk.jmh:jmh-generator-annprocess:1.36"
//> using plugin "org.openjdk.jmh:jmh-generator-bytecode:1.36"
import org.openjdk.jmh.annotations._
import org.openjdk.jmh.infra.Blackhole
import java.util.concurrent.TimeUnit
@BenchmarkMode(Array(Mode.Throughput))
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
@Fork(1)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
class StringBenchmark {
@Benchmark
def stringConcatenation(bh: Blackhole): Unit = {
val result = "Hello" + " " + "World"
bh.consume(result)
}
@Benchmark
def stringInterpolation(bh: Blackhole): Unit = {
val world = "World"
val result = s"Hello $world"
bh.consume(result)
}
@Benchmark
def stringBuilder(bh: Blackhole): Unit = {
val sb = new StringBuilder()
sb.append("Hello")
sb.append(" ")
sb.append("World")
val result = sb.toString()
bh.consume(result)
}
}
@main def runBenchmarks(): Unit = {
org.openjdk.jmh.Main.main(Array.empty)
}
// scripts/release.scala - Release automation
//> using dep "com.github.lolgab::mill-crossplatform:0.2.4"
import scala.sys.process._
import scala.util.{Try, Success, Failure}
@main def release(releaseType: String = "patch"): Unit = {
val currentVersion = getCurrentVersion()
val nextVersion = calculateNextVersion(currentVersion, releaseType)
println(s"Current version: $currentVersion")
println(s"Next version: $nextVersion")
if (confirmRelease(nextVersion)) {
performRelease(nextVersion)
} else {
println("Release cancelled")
}
}
def getCurrentVersion(): String = {
Try("git describe --tags --abbrev=0".!!.trim.stripPrefix("v")) match {
case Success(version) => version
case Failure(_) => "0.1.0"
}
}
def calculateNextVersion(current: String, releaseType: String): String = {
val parts = current.split("\\.").map(_.toInt)
val (major, minor, patch) = (parts(0), parts(1), parts(2))
releaseType match {
case "major" => s"${major + 1}.0.0"
case "minor" => s"$major.${minor + 1}.0"
case "patch" => s"$major.$minor.${patch + 1}"
case _ => throw new IllegalArgumentException(s"Unknown release type: $releaseType")
}
}
def confirmRelease(version: String): Boolean = {
print(s"Release version $version? (y/N): ")
scala.io.StdIn.readLine().toLowerCase.startsWith("y")
}
def performRelease(version: String): Unit = {
// Ensure clean workspace
if (hasUncommittedChanges()) {
throw new RuntimeException("Workspace has uncommitted changes")
}
// Run tests
runCommand("scala-cli test .")
// Update version in configuration
updateVersion(version)
// Build and package
runCommand("scala-cli package . --output target/release.jar")
// Create git tag
runCommand(s"git add .")
runCommand(s"git commit -m 'Release version $version'")
runCommand(s"git tag v$version")
// Push changes
runCommand("git push origin main")
runCommand("git push origin --tags")
println(s"Successfully released version $version")
}
def hasUncommittedChanges(): Boolean = {
"git status --porcelain".!!.trim.nonEmpty
}
def updateVersion(version: String): Unit = {
// Update version in appropriate configuration files
println(s"Updated version to $version")
}
def runCommand(command: String): Unit = {
println(s"Running: $command")
val exitCode = command.!
if (exitCode != 0) {
throw new RuntimeException(s"Command failed: $command")
}
}
CI/CD Integration and Best Practices
GitHub Actions and Build Automation
# .github/workflows/ci.yml - Comprehensive CI pipeline
name: CI
Comments
Be the first to comment on this lesson!