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