sbt 2.x:
sbt-ossuminc2.0.0+ requires sbt 2.0.2+ and Scala 3. For sbt 1.x builds, use the 1.x line (last release v1.4.0).
An sbt plugin that can be used for a wide range of projects. This plugin is the only requirement for every project at Ossum Inc. and is maintained by and for that company. However, it is likely quite useful for other companies because of its modularity and ability to override the Ossum Inc. defaults.
sbt-ossuminc is likely most helpful if you:
- Develop software in Scala (why else would you use
sbt? :) ) - Believe in using mono-repos containing many subprojects
- Need to support JVM, JS, and Native targets for your Scala code
- Work at the sbt command line and want lots of utilities there
sbt-ossuminc embraces a functional, minimalist, Don't-Repeat-Yourself (DRY) approach to build configuration:
Instead of writing imperative sbt settings, you declare what you want using composable configuration helpers. The plugin handles the details.
// ❌ Imperative (verbose, repetitive)
lazy val myModule = project
.settings(scalaVersion := "3.8.4")
.settings(scalacOptions ++= Seq("-deprecation", "-feature"))
.settings(libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % Test)
.enablePlugins(GitPlugin, DynVerPlugin)
// ... 20 more lines of boilerplate
// ✅ Declarative (concise, clear intent)
lazy val myModule = Module("my-module")
.configure(With.typical)Configuration helpers are pure functions (Project => Project) that compose naturally:
Module("my-lib")
.configure(With.typical) // Scala 3 + testing + versioning + git
.configure(With.coverage(80)) // Add code coverage with 80% threshold
.configure(With.GithubPublishing) // Publish to GitHub Packages
.dependsOn(otherModule)Defaults are chosen for modern Scala development but can always be overridden:
- Scala 3.8.4 (Scala Next) by default (override with
With.Scala3(version = Some("3.3.8"))) - Cross-platform ready (JVM, JS, Native with one declaration)
- Dynamic versioning from git tags (no manual version management)
- Automatic header management (keep license headers current)
Define once, use everywhere. No copy-paste configuration across subprojects:
// Define your standard configuration once
val standardModule = (p: Project) => p
.configure(With.typical, With.coverage(70), With.GithubPublishing)
// Apply it to multiple modules
lazy val moduleA = Module("module-a").configure(standardModule)
lazy val moduleB = Module("module-b").configure(standardModule)
lazy val moduleC = Module("module-c").configure(standardModule)Requirements: sbt 2.0.0+, JDK 17+ (Temurin 25 recommended), Scala 3.
Pin sbt 2.0.2 (or newer) in your project/build.properties:
sbt.version=2.0.2In your project/plugins.sbt file, add the GitHub Packages resolver and the plugin:
// GitHub Packages resolver for sbt-ossuminc
resolvers += "GitHub Packages" at "https://maven.pkg.github.com/ossuminc/sbt-ossuminc"
addSbtPlugin("com.ossuminc" % "sbt-ossuminc" % "2.0.0")You must also set up your credentials file as a global .sbt file. This file permits you to read public repositories from GitHub Package Repository (such as those published by Ossum Inc.) and managing private repositories in your organization(s).
We recommend placing the credentials in your private home directory at ~/.sbt/2/github.sbt
(sbt 2 reads global configuration from ~/.sbt/2/, not ~/.sbt/1.0/).
It should have content like:
credentials += Credentials(
"GitHub Package Registry",
"maven.pkg.github.com",
"your-github-user-name-here",
"your-github-token-here"
)
where:
your-github-user-name-hereis replaced with your Github user nameyour-github-token-hereis replaced with the GitHub Personal Access token (classic) that you have generated with therepoandread:packagesprivilege enabled.
In your build.sbt, place this line near the top to enable everything sbt-ossuminc supports:
enablePlugins(OssumIncPlugin)The above three things are required to activate the plugin for your build. There's more you can do, as described below.
Using that single plugin causes several other plugins to be adopted.
While you can use other addSbtPlugin declarations in project/plugins.sbt,
chances are you don't need to. The one line above also brings in all
the plugins listed in the sections below. These dependencies of
sbt-ossuminc are regularly updated with help from
Scala Steward so all you have
to keep up to date is your version of sbt-ossuminc which Scala Steward
can also help you within your project.
// Generic plugins from the github.sbt project
addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1")
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.7")
addSbtPlugin("com.github.sbt" % "sbt-git" % "2.1.0")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")
addSbtPlugin("com.github.sbt" % "sbt-release" % "1.5.0")
addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.6.1")
// dependency-tree is built into sbt 2 — no addDependencyTreePlugin needed- sbt-dynver - dynamic versioning based on git tags, commits, and date stamp
- sbt-native-packager - packaging your compilation results into a package for various platforms
- sbt-git - git commands from the sbt prompt
- sbt-pgp - artifact signing (
publishSigned) for Maven Central via the Central Portal - sbt-release - full control of the release process for your project
- sbt-unidoc - unifying the documentation output from your programming language from several sub-projects
- dependency-tree commands (
dependencyTree, etc.) are built into sbt 2 — the oldaddDependencyTreePluginis no longer required
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1")
addSbtPlugin("com.github.sbt" % "sbt-header" % "5.11.0")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.7.0")- sbt-buildinfo - Your program can know all kinds of things about your build
- sbt-header - Keep those file headers up to date with your project license (now published under
com.github.sbt) - sbt-updates - Check for dependency updates
Dropped on sbt 2:
sbt-sonatype(deprecated — Maven Central publishing is built into sbt 2 via the Central Portal),sbt-github-packages(abandoned — GitHub Packages is configured directly viapublishTo/Credentials), andsbt-paradox(no stable sbt 2 release yet). See Publishing helpers and the Migration Notes below.
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.7")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.22.0")
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.6")- sbt-scalafix - Code refactoring and linting
- sbt-scalafmt - Code formatting
- sbt-scoverage - Code coverage measurement
- sbt-scala-native - Compile Scala to native code
- sbt-scalajs - Compile Scala to JavaScript
- sbt-mima-plugin - Binary compatibility checking
Obsolete / unavailable on sbt 2: the
org.portable-scalacross-project plugins andsbt-platform-depsare replaced by sbt 2's built-inprojectMatrixand the%%operator (the%%%operator is gone).sbt-coveralls,sbt-tasty-mima, andsbt-idea-pluginhave no sbt 2 release yet:With.coverage/With.MiMawork without them, butWith.IdeaPluginis unavailable until JetBrains ships sbt 2 support.
Without any further definitions in your build.sbt, this plugin provides
various features that we like at Ossum Inc.:
- Git commands at the sbt prompt
- Dynamic versioning based on your git tag and updated on each sbt reload
- Automatic placement and update of your source file header comments
- Standardized Scala code formatting with scalameta from a single configuration file
- Automatic updates of dependencies with sbt-updates
- sbt-unidoc for collation of sub-project documentation into a single site
- sbt-native-packager for output packaging
- Native sbt 2 Maven Central publishing via the Sonatype Central Portal
- sbt-scoverage for code coverage measurement
Tag a release candidate X.Y.Z-rc.N (SemVer prerelease, no v prefix).
sbt-dynver already does the right thing with it:
tag 1.32.0-rc.1 -> version 1.32.0-rc.1, isSnapshot = false
That isSnapshot = false is correct — an RC is a real, immutable artifact,
not a snapshot — which is precisely why isSnapshot cannot be used to answer
"is this a prerelease?".
Two settings, available unqualified in build.sbt, so nothing has to
string-match the version:
Compile / packageBin / publishArtifact := !isPrerelease.value
someTask := {
if (isReleaseCandidate.value) log.info("this is an RC build")
}| Version | isPrerelease |
isReleaseCandidate |
isSnapshot |
|---|---|---|---|
1.32.0 |
false | false | false |
1.32.0-rc.1 |
true | true | false |
1.32.0-4-abc12345 |
true | false | true |
npm publish moves the latest dist-tag by default, whatever the version
string says. Publishing 1.32.0-rc.1 without --tag therefore makes a
release candidate what a plain npm install <pkg> resolves to.
With.Publishing.npm(...) derives the tag from the version:
| Version | dist-tag | npm install behavior |
|---|---|---|
1.32.0 |
latest |
the default |
1.32.0-rc.1 |
rc |
only via npm install <pkg>@rc |
1.32.0-4-abc12345 |
dev |
only via npm install <pkg>@dev |
Override with the npmDistTag setting when the heuristic is wrong; because its
default comes from the plugin's projectSettings, your override wins wherever
you write it:
com.ossuminc.sbt.helpers.NpmPublishing.Keys.npmDistTag := "next"Two traps live in your CI, not in the build, so you must handle them yourself:
-
gh release create --prereleasestill firesrelease: [created]. A Homebrew-tap dispatch job wired to that event will happily overwrite your stable formula with a release candidate. Branch on the payload:if: github.event.release.prerelease == false
For an RC formula, Homebrew's
develblock is deprecated; the supported pattern is a separate versioned formula (pkg@rc) declaringconflicts_withagainst the stable one. -
Announcement / notification jobs. Same event fires for a prerelease. Gate them the same way. (This repo's own
release.ymlgatesnotify-blogonendsWith(tag, '.0'), which incidentally excludes-rc.Ntags, but that is a side effect of the patch-release rule, not an RC guard — do not rely on that shape in your own workflows.)
The sbt-ossuminc plugin automatically defines some top level objects you can
use to define your subprojects. The sub-sections below cover each of these
lightly. For more details see the scaladoc for the plugin.
Use this when you want to have a root project that aggregates all the other
sub-projects. When you've selected the root project (sbt command: project root)
then your commands get passed down to the sub-projects.
For example, this:
lazy val riddl: Project = Root(
ghRepoName = "my-project",
ghOrgName = "my-organization",
orgPackage = "com.my_org.my_proj",
orgName = "My Organization",
orgPage = url("https://my_org.com/"),
startYr = 2024,
projectId = "root" // Optional: customize sbt project ID (default: "root")
)
.configure(With.noPublishing, With.Git, With.DynVer)
.aggregate(
module0, // a sub-component of your project
module1,
module2
)defines a top-level Root project in the top level directory that aggregates the
three modules listed in the .aggregate call. The parameters to Root define the
basic identifiers about the project so you don't have to set them as sbt settings
elsewhere. You must define a Root in your build.sbt as it provides basic information
about your project that are used by other features of sbt-ossuminc.
So how do module0, module1, and module2 get specified? With the Module
object of course! Like this:
lazy val module0: Project = Module(dirName = "module0", modName = "proj-mod-0")
.configure(With.typical, With.coverage(30))
.configure(With.publishing)
.settings(
coverageExcludedPackages := "<empty>;$anon",
description := "An example of a module sub-project",
libraryDependencies ++= Seq(
"org.scala-js" %% "scalajs-stubs" % "1.1.0" % "provided"
)
)
.dependsOn(module1)The above defines a module named proj-mod-0 in the directory named module0.
The module name will be used as the artifact name that the module compilation
produces. We've also asked for With.typical scala configuration and for code
coverage to be supported with at least 30% coverage, via With.coverage(30).
This module is also configured to be published by using With.publishing.
All the With configuration options are described in a section below.
As you can see, you can still override the configured settings for your
subprojects. In the .settings(...), it excludes empty packages from coverage, sets the module description
for publishing, and allows the scalajs annotations to be used, but just as stubs
If you want to build your module for more than just the JVM, you can use the CrossModule object in a pattern like this:
lazy val foo_cp: CrossModule =
CrossModule(dirName = "foo", modName = "foo", scalaVersion = "3.8.4")(JVM, JS, Native)
.configure(With.typical, With.publishing)
.settings(
scalacOptions ++= Seq("-explain", "-explain-cyclic"),
description := "The fooness of existence"
)
.jvmConfigure(With.coverage(30))
.jvmSettings(
coverageExcludedPackages := "<empty>;$anon"
)
.jsConfigure(With.ScalaJS("foo: js", withCommonJSModule = true))
.jsSettings(
libraryDependencies += "com.foo" %% "fooness" % "0.1.0"
)
val foo = foo_cp.jvm
val fooJS = foo_cp.js
val fooNative = foo_cp.nativeOn sbt 2, CrossModule is implemented on sbt's built-in projectMatrix (which
replaces the sbt 1.x org.portable-scala cross-project plugins). From top to bottom:
- a lazy val named
foo_cpis defined as aCrossModule— an immutable builder wrapping aprojectMatrix. It is named with the_cpsuffix to distinguish it from the per-platform projects extracted below. - The
CrossModuleobject is invoked. The first arguments are like aModule(dirName,modName), plus ascalaVersion(default"3.8.4"):projectMatrixneeds the Scala version when each platform is declared, and it must match the version configured byWith.typical/With.Scala3on this module. The trailing argument list selects the targets to build — here all three:JVM,JS, andNative. .configureand.settingsapply to all platforms. Doing something specific to one (like a JS-onlylibraryDependency) here would break the JVM build..jvmConfigure/.jvmSettingsapply only to the JVM build;.jsConfigure/.jsSettingsonly to the Scala.js build;.nativeConfigure/.nativeSettingsonly to the Scala Native build.- Cross-platform dependencies use the ordinary
%%operator (sbt 2 cross-builds it across platforms; the sbt 1.x%%%operator is gone). - The terse vals
foo,fooJS, andfooNativeextract the per-platform sub-projects (so you can, e.g., runfooJS/test)..jvm/.js/.nativeresolvematrix.{jvm,js,native}.apply(scalaVersion).
CrossModule accumulates the two separately and applies all .settings
first, then all .configure transforms. Where both touch the same key, the
.configure wins — even if you wrote .settings afterwards:
CrossModule("m", "m", scalaVersion = "3.8.4")(JS)
.settings(description := "FROM_SETTINGS")
.configure(_.settings(description := "FROM_CONFIGURE"))
// show mJS/description --> FROM_CONFIGUREThis bites hardest with With.BuildInfo, which assigns buildInfoObject
itself. A .settings(buildInfoObject := "MyBuildInfo") is silently reset to the
default by a later .configure(With.BuildInfo…), and the symptom is a missing
generated object at compile time with nothing pointing at ordering. It has now
cost debugging sessions in both riddl-generator and Synapify.
Workaround: put the override in a trailing .configure.
CrossModule("facades", "facades", scalaVersion = "3.8.4")(JS)
.jsConfigure(With.BuildInfo.withKeys("k" -> v))
.configure(_.settings(buildInfoObject := "SynapifyBuildInfo")) // last wordPlain Module/Root projects do not behave this way — that is ordinary
sbt, where the last assignment wins. This is specific to CrossModule's
builder.
CrossModule takes targets: Target*, so one platform is a supported shape —
useful for an all-Scala.js repo:
lazy val renderer_cp = CrossModule("renderer", "renderer", scalaVersion = "3.8.4")(JS)
.configure(With.basic)
.jsConfigure(With.ScalaJS("my-renderer"))
lazy val renderer = renderer_cp.jsThe sbt project ID keeps its platform suffix: rendererJS, not renderer.
CrossModule pins defaultAxes(VirtualAxis.jvm, …), so with no JVM row in the
matrix the JS row is not the default and stays suffixed. Use rendererJS in
aggregate, addCommandAlias, and on the command line (sbt rendererJS/test);
the Scala-side .js accessor and the published artifact name are unaffected
(artifact is com.ossuminc:renderer).
Module(...) + With.ScalaJS(...) also still works on sbt 2 — verified
2026-08-10 by resolving "org.scala-js" %% "scalajs-dom" from a plain Module
with ScalaJSPlugin enabled, which correctly fetched scalajs-dom_sjs1_3. %%
is platform-aware there because ScalaJSPlugin sets the platform cross-version,
so the loss of %%% does not break the plain-project shape. Prefer
CrossModule(…)(JS) for new code — it is the shape the rest of this plugin is
built around — but an existing sbt 1.x build using Module + With.ScalaJS
does not have to be converted to migrate.
Use this to define an SBT plugin in a sub-project, like this:
lazy val plugin = Plugin(dirName = "sbt-plugin")
.configure(With.BuildInfo)
.settings(
description := "An sbt plugin to help the build world along",
buildInfoObject := "SbtRiddlPluginBuildInfo",
buildInfoPackage := "com.ossuminc.riddl.sbt",
buildInfoUsePackageAsPath := true
)In this example we define that the sbt-plugin is in the eponymous directory and add BuildInfo
generation. On sbt 2 a plugin compiles with Scala 3 (managed by sbt — do not set
With.Scala2/scalaVersion := "2.12.x"), and Plugin(...) automatically configures GitHub
Packages publishing plus the scripted testing framework (built into sbt 2) for you.
Use this to define an executable Program with a mainClass like this:
lazy val program = Program(dirName = "my-program", programName = "myprog", mainClass = Option("com.myprog.Main"))
.configure(With.typical, With.publishing)
.dependsOn(
module0,
module1,
module2,
)
.settings(
description := "The main program",
maintainer := "reid@ossuminc.com",
)By now you should be able to figure out the above settings. It will yield a program executable named
myprog from the contents of directory my-program that must define a class named com.myprog.Main
and will also include module0, module1, and module2 in its classpath.
Use this top level definition to gather your api documentation into a web site. Here's an example from
the ossuminc/riddl repository:
lazy val docsite = DocSite(
dirName = "doc",
apiOutput = file("src") / "main" / "hugo" / "static" / "apidoc",
baseURL = Some("https://riddl.tech/apidoc"),
inclusions = Seq(utils, language, passes, diagrams, commands),
logoPath = Some("doc/src/main/hugo/static/images/RIDDL-Logo-128x128.png")
)
.settings(
name := "riddl-doc",
description := "Generation of the documentation web site",
libraryDependencies ++= Dep.testing
)
.configure(With.noMiMa)
.dependsOn(utils, language, passes, diagrams, commands)Since the goal of sbt-ossuminc is to be declarative, we want to specify what we want to
include in the build. For this, we have the With.* configuration helpers. These are
pure functions (Project => Project) that transform projects by adding settings and plugins.
You can pass these directly into a .configure() call chain.
These helpers take no parameters (or use all defaults):
With.Aliases- Add useful command line aliases to the sbt shellWith.BuildInfo- Enablesbt-buildinfoplugin (default configuration)With.DynVer- Enablesbt-dynverfor git-based dynamic versioningWith.Git- Enablesbt-gitto issue git commands from sbt promptWith.Header- Enablesbt-headerfor automatic license header managementWith.Java- Enable javac compiler for Java/Scala projectsWith.ScalaJS- Enable Scala.js compilation (default configuration)With.noMiMa- Disable binary compatibility checkingWith.noPublishing- Disable artifact publishing (useful for root aggregator projects)With.NoDocs- Suppress Scaladoc/Javadoc generation (clears thedoctask's input sources) for modules where doc generation is slow or unnecessary; the emptypackageDocartifact is still produced so Sonatype publishing remains validWith.ScalaJavaTime()- Addscala-java-timedependency for cross-platformjava.timeAPIWith.ClassPathJar- Use classpath JAR for packaging (reduces command line length)With.UnmanagedJars- Use unmanaged JAR files fromlibs/directoryWith.ShellPrompt- Custom shell prompt showing project name, git branch, and versionWith.Release- Enablesbt-releasepluginWith.Resolvers- Add standard resolvers (Maven Local, JCenter, Typesafe)With.Scala2- Configure for Scala 2.13 (latest)With.Scala3- Configure for Scala 3.8.4 (Scala Next, default)With.ScalaCoverage(a.k.a.With.coverage(...)) - Enable code coverage with sbt-scoverage
With.Publishing- Configure publishing (defaults to GitHub Packages)With.Publishing.github- Explicitly configure GitHub Packages publishingWith.Publishing.sonatype- Configure publishing to Sonatype/Maven CentralWith.GithubPublishing- Alias forWith.Publishing.githubWith.SonatypePublishing- Alias forWith.Publishing.sonatypeWith.Publishing.npm(...)- Publish npm packages to registries (see npm Publishing below)
Note: Do not combine GitHub and Sonatype publishing in the same project.
Maven Central (Central Portal):
With.Publishing.sonatypeconfigures sbt 2's native Central Portal publishing — snapshots go to the Central snapshots repo, releases to the built-inlocalStaging. Release withpublishSigned ; sonaUpload ; sonaRelease, providingSONATYPE_USERNAME/SONATYPE_PASSWORD(env) or~/.sbt/sonatype_central_credentials, plus PGP signing keys. The legacysbt-sonatypeplugin is not used (and has no sbt 2 build).
Shortcuts that combine multiple helpers:
With.basic- Combines:aliases,dynver,git,header,resolversWith.typical- Combines:basic,scala3,Scalatest()With.everything- Combines:typical,java,release
These helpers accept parameters for customization:
Add Akka dependencies to the project. Akka requires a commercial license and repository token since 2024.
Parameters:
release: Akka version ("24.10"or"25.10"(latest), default:""= latest)withHTTP: Include Akka HTTP modules (default:false)withGrpc: Include Akka gRPC runtime (default:false)withPersistence: Include Akka Persistence R2DBC (default:false)withProjections: Include Akka Projections (default:false)withManagement: Include Akka Management core (health checks, cluster HTTP) (default:false)withManagementKubernetes: Include Kubernetes modules (discovery, lease, rolling updates) (default:false)withKafka: Include Alpakka Kafka connector (default:false)withInsights: Include Akka Insights/Cinnamon telemetry (default:false)withInsightsPrometheus: Include Prometheus export (default:true, only applies whenwithInsights = true)withInsightsOpenTelemetry: Include OpenTelemetry tracing (default:true, only applies whenwithInsights = true)
Basic usage (core modules only):
Module("my-actor-system")
.configure(With.Akka.forRelease("25.10"))Full-featured server example:
Module("my-server")
.configure(With.Akka.forRelease(
"25.10",
withHTTP = true,
withPersistence = true,
withProjections = true,
withManagement = true,
withManagementKubernetes = true,
withInsights = true
))Modules included by default (core):
- akka-actor, akka-actor-typed
- akka-cluster, akka-cluster-typed, akka-cluster-sharding, akka-cluster-sharding-typed, akka-cluster-tools
- akka-coordination, akka-discovery, akka-distributed-data
- akka-persistence, akka-persistence-typed, akka-persistence-query
- akka-remote, akka-serialization-jackson, akka-slf4j
- akka-stream, akka-stream-typed
- Test: akka-testkit, akka-actor-testkit-typed, akka-stream-testkit
Note: Akka repository access requires
AKKA_REPO_TOKENenvironment variable. Get your token at https://account.akka.io
Note for riddl-server-infrastructure dependents: If your project depends on
riddl-server-infrastructure, Akka core and HTTP modules are already provided transitively. Only useWith.Akka.forRelease()if you need additional modules beyond what the server infrastructure provides (e.g., Kafka, Insights, Management, Projections, or specific persistence backends).
Configure AsciiDoc document generation for static websites and PDFs.
sourceDir: Directory containing AsciiDoc source files (default:"src/asciidoc")enablePdf: Enable PDF generation (default:true)enableDiagrams: Enable diagram support for PlantUML, Graphviz, etc. (default:false)attributes: Custom AsciiDoc attributes for document processing
Features:
- HTML5 website generation with customizable attributes
- PDF generation with asciidoctorj-pdf
- Diagram support via asciidoctorj-diagram (PlantUML, Graphviz, etc.)
- Customizable source directories and output locations
DocSite("docs", "project-docs")
.configure(With.AsciiDoc(
sourceDir = "src/docs/asciidoc",
enablePdf = true,
enableDiagrams = true,
attributes = Map("toc" -> "left", "icons" -> "font")
))Note: For full HTML generation via sbt-site, add to
project/plugins.sbt:addSbtPlugin("com.github.sbt" % "sbt-site-asciidoctor" % "1.7.0")Then enable in
build.sbt:enablePlugins(AsciidoctorPlugin)
Customize BuildInfo generation.
buildInfoObject: Name of generated objectbuildInfoPackage: Package for generated codebuildInfoUsePackageAsPath: Use package as directory structure
Module("my-app")
.configure(With.BuildInfo(
buildInfoObject = "AppBuildInfo",
buildInfoPackage = "com.myapp.build"
))On a
CrossModule, do not setbuildInfoObjectin.settings. The builder applies every.settingsbefore every.configure, soWith.BuildInforesets your override no matter which you wrote first, and the only symptom is a generated object that isn't found at compile time. Put the override in a trailing.configureinstead — see ".settings is applied BEFORE .configure".
Enable code coverage with minimum threshold.
Module("my-lib")
.configure(With.coverage(80.0)) // Require 80% coverageUnavailable on sbt 2 until JetBrains'
sbt-idea-pluginships an sbt 2.0 build (SCL-23480). On sbt 2, callingWith.IdeaPluginfails fast with a clear message — use the sbt-ossuminc 1.x line for IntelliJ plugin projects until then.
Configure IntelliJ IDEA plugin development.
name: Plugin namedescription: Plugin descriptionbuild: IntelliJ build version (e.g.,"243.x")platform:"Community"or"Ultimate"
Plugin("my-idea-plugin")
.configure(With.IdeaPlugin(
name = "My Cool Plugin",
build = "243.x",
platform = "Community"
))Configure Scala.js compilation.
header: JS file header commenthasMain: Enable main module initializerforProd: Enable optimizer (production mode)withCommonJSModule: Use CommonJS modules instead of ES modulesscalaJavaTimeVersion: Override scala-java-time version (default:"2.6.0")scalatestVersion: Override scalatest version (default:"3.2.19")
CrossModule("my-ui", "ui")(JVM, JS)
.jsConfigure(With.ScalaJS(
header = "My App UI v1.0",
hasMain = true,
forProd = true,
scalaJavaTimeVersion = "2.6.0"
))Removed in 3.1.0:
With.Javascript, along with the other lowercase aliases deprecated back in 1.1.0 (With.akka,With.aliases,With.build_info,With.dynver,With.git,With.header,With.java,With.release,With.resolvers,With.riddl,With.scala2,With.scala3,With.scalajs,With.scoverage). Use the capitalized names:With.ScalaJS,With.Akka,With.Aliases,With.BuildInfo,With.DynVer,With.Git,With.Header,With.Java,With.Release,With.Resolvers,With.Riddl,With.Scala2,With.Scala3,With.ScalaCoverage. (With.coverage(percent)is not affected — it is a current helper, not an alias.)
Add Laminar reactive UI dependencies (Scala.js).
version: Laminar versiondomVersion: Scala.js DOM versionwaypointVersion: Waypoint router version (optional)laminextVersion: Laminext utilities version (optional)laminextModules: Specific Laminext modules to include
CrossModule("frontend", "app-frontend")(JS)
.jsConfigure(With.Laminar(
version = "17.1.0",
domVersion = "2.8.0"
))Enable binary compatibility checking.
previousVersion: Version to check compatibility against (required)excludedClasses: Classes to exclude from checksreportSignatureIssues: Include generic type parameter checks
Module("my-stable-api")
.configure(With.MiMa(
previousVersion = "1.0.0",
excludedClasses = Seq("com.myapp.internal.*")
))Configure Scala Native compilation.
mode: Compilation mode ("debug","fast","full","size","release")buildTarget: Build type ("application","dynamic","static")gc: Garbage collector to uselto: Link-time optimization ("none","thin","full")debugLog: Enable debug loggingverbose: Verbose compilation outputtargetTriple: Target platform triple (optional)linkOptions: Additional linker optionsscalatestVersion: Override scalatest version (default:"3.2.19")
CrossModule("cli-tool", "tool")(JVM, Native)
.nativeConfigure(With.Native(
mode = "release",
buildTarget = "application",
scalatestVersion = "3.2.19"
))Create universal (zip/tgz) packages.
maintainerEmail: Package maintainer emailpkgName: Package namepkgSummary: One-line summarypkgDescription: Full description
Program("my-app", "app", Some("com.myapp.Main"))
.configure(With.Packaging.universal(
maintainerEmail = "dev@example.com",
pkgName = "my-app",
pkgSummary = "My Application",
pkgDescription = "A useful application"
))Create Docker images.
maintainerEmail: Package maintainer emailpkgName: Docker image namepkgSummary: Image summarypkgDescription: Image description
Program("my-service", "service", Some("com.myapp.Service"))
.configure(With.Packaging.docker(
maintainerEmail = "dev@example.com",
pkgName = "my-service"
))Create separate Docker images for development (local) and production (GKE/cloud).
Parameters:
mainClass: Fully qualified main class name (e.g.,"com.myapp.Main")pkgName: Docker image name (e.g.,"my-service")exposedPorts: Ports to expose in the containerpkgDescription: Optional image description
Dev image (default, built with docker:publishLocal):
- Base:
eclipse-temurin:25-jdk-noble(Ubuntu 24.04 with JDK tools) - Architecture: Host platform (arm64 on Apple Silicon, amd64 on Intel/Linux)
- Tags:
:dev-latest,:dev-<version> - Includes JDK diagnostic tools (jcmd, jstack, jmap) for debugging
Prod image (built with dockerPublishProd):
- Base:
gcr.io/distroless/java25-debian13:nonroot(minimal, secure) - Architecture:
linux/amd64(for GKE/cloud deployment) - Tags:
:latest,:<version> - Minimal attack surface, no shell, runs as non-root
Module("my-service", "service")
.configure(With.typical)
.configure(
With.Packaging.dockerDual(
mainClass = "com.myapp.Main",
pkgName = "my-service",
exposedPorts = Seq(8080, 9001)
)
)Building images:
# Build dev image for local testing
sbt docker:publishLocal
# Build and push prod image to registry (requires docker buildx)
sbt dockerPublishProdDefault registry: Google Artifact Registry
(us-central1-docker.pkg.dev / ossuminc-production/ossum-images); override with
the dockerRepository and dockerUsername settings.
Assemble Scala.js output into an npm-publishable package. Requires the project
to be configured with Scala.js (With.ScalaJS(...) or a CrossModule with JS
target).
Parameters:
scope: npm scope (e.g.,"@ossuminc"), empty string for unscopedpkgName: npm package name (without scope)pkgDescription: Description for package.jsonkeywords: npm keywords for package discoveryesModule: Whether to set"type": "module"in package.json (default:true)templateFile: Optionalpackage.jsontemplate withVERSION_PLACEHOLDERtypesDir: Optional explicit TypeScript definitions directory, overriding the convention below (default:None)outputDir: Optional staging directory (default:target/npm-package)
Overriding a key directly (3.1.0+): npmTypesDir, npmTemplateFile and
npmOutputDir get their convention-derived values from the plugin's
projectSettings, which sbt orders below everything in build.sbt. A project
override therefore takes effect wherever you write it — including in a
.jsSettings(...) block above the .jsConfigure(With.Packaging.npm(...)) call.
Before 3.1.0 npm(...) assigned those keys unconditionally, so an override in
that position was silently discarded and the ordering was load-bearing.
Values you pass as arguments to npm(...) remain hard assignments — an explicit
argument is an explicit instruction.
Tasks provided:
npmPrepare: Assembles the npm package directory (pure sbt, no npm binary required). RunsfullOptJS, copies JS output, generatespackage.json, and includes TypeScript definitions if found.npmPack: Shells out tonpm packto create a.tgzarchive, and returns the tarball it just produced. It emptiestarget/npm-packagesfirst (3.2.0+) so exactly one tarball is present afterwards; if there is not exactly one, it fails rather than guess. Before 3.2.0 that directory accumulated one tarball per version ever built andnpmPackreturned an arbitrary member of that history — sonpmPublishLocalcould file a months-old package under the current version number, and succeed. If you were collecting build artifacts out of that directory, copy them elsewhere fromnpmPack's return value instead.
TypeScript definitions are discovered by convention (3.0.4+): the first of
<moduleDir>/js/types/ or <moduleDir>/types/ that exists is used, and an
index.d.ts found there is copied into the package and referenced by types
and exports in package.json.
<moduleDir> is the module's real source tree — for a CrossModule that is the
directory named in CrossModule("my-lib", ...), not the JS row's
baseDirectory. A projectMatrix row is based at a synthetic
.sbt/matrix/<rowId>/ containing no sources, so it is deliberately not used
here.
npmPrepare reports which directory it used, or warns listing every location it
searched when none was found:
[info] TypeScript definitions: /path/to/my-lib/js/types/index.d.ts
[warn] no TypeScript definitions found (looked in /path/to/my-lib/js/types,
/path/to/my-lib/types); package will ship without them
Pass typesDir = Some(file(...)) to state the location outright instead of
relying on the convention.
CrossModule("my-lib", "my-lib")(JVM, JS)
.jsConfigure(
With.ScalaJS("My Lib", hasMain = false, forProd = true,
withCommonJSModule = true),
With.Packaging.npm(
scope = "@myorg",
pkgName = "my-lib",
pkgDescription = "My library for JavaScript",
keywords = Seq("scala", "library"),
esModule = true
)
)Generated package.json (3.1.0+) includes files, homepage, bugs,
author, and — for a scoped package — publishConfig.access = "public".
When TypeScript definitions are present, exports["."] declares types,
import and default: bundlers resolve import for an ESM consumer,
while default remains the universal fallback.
Template mode: Instead of generating package.json from settings, you
can provide a template file containing VERSION_PLACEHOLDER which will be
replaced with the project version at build time.
A template is auto-discovered (3.1.0+) at <moduleDir>/js/package.json.template
or <moduleDir>/package.json.template; when one is found it governs packaging
and the settings-generated document is not used. npmPrepare logs which
template it used. Pass templateFile explicitly to use a different path:
With.Packaging.npm(
scope = "@myorg",
pkgName = "my-lib",
templateFile = Some(file("npm/package.json.template"))
)Publish assembled npm packages to registries. Must be used together with
With.Packaging.npm(...) which provides the npmPrepare task.
Parameters:
registries: Target registries —Seq("npmjs"),Seq("github"), orSeq("npmjs", "github")to publish to both.
Tasks provided:
npmPublish: Publish to all configured registries.npmPublishNpmjs: Publish to npmjs.com only.npmPublishGithub: Publish to GitHub Packages only.
Authentication via environment variables:
NPM_TOKEN: Access token for npmjs.com (required for"npmjs"registry)GITHUB_TOKEN: Token withwrite:packagesscope (required for"github"registry)
Note: GitHub Packages requires the npm scope to be set (e.g.,
@ossuminc). The scope is read fromWith.Packaging.npm(scope = ...).
CrossModule("my-lib", "my-lib")(JVM, JS)
.jsConfigure(
With.Packaging.npm(
scope = "@myorg",
pkgName = "my-lib",
pkgDescription = "My library"
),
With.Publishing.npm(
registries = Seq("npmjs", "github")
)
)Typical workflow:
# Build and assemble npm package (pure sbt, no npm needed)
sbt my-libJS/npmPrepare
# Publish to configured registries
NPM_TOKEN=<token> GITHUB_TOKEN=<token> sbt my-libJS/npmPublishCreate a tar.gz archive of a Scala Native binary for distribution. The archive includes the binary and optionally README and LICENSE files.
Parameters:
pkgName: Base name for the archive and binarypkgDescription: Package description (included in generated README)arch: Architecture label override (empty = auto-detect from host)os: OS label override (empty = auto-detect from host)includeReadme: Whether to include a README.md (default:true)includeLicense: Whether to include LICENSE from project root (default:true)
Task provided:
linuxPackage: Compiles vianativeLink, stages binary + docs, creates<pkgName>-<version>-<os>-<arch>.tar.gz.
OS and architecture are auto-detected from the build host since Scala Native compiles for the host platform only. For multi-platform distribution, use CI matrix runners for each target.
Program("my-tool", "my-tool", Some("com.myapp.Main"))
.nativeConfigure(With.Native(mode = "release"))
.configure(
With.Packaging.linux(
pkgName = "my-tool",
pkgDescription = "A useful CLI tool"
)
)Generate a Homebrew formula .rb file for inclusion in a tap repository.
Parameters:
formulaName: Formula name (used as Ruby class name)binaryName: Binary executable namepkgDescription: Description shown inbrew infohomepage: Project homepage URLjavaVersion: Required JDK version, universal variant only (default:"25")tapRepo: Tap repo path (documentation only, publishing is not automated)variant:"universal"(JVM, default) or"native"(Scala Native)
Task provided:
homebrewGenerate: Produces a.rbformula file attarget/homebrew/Formula/<formulaName>.rbwith SHA256 hash computed from the build artifact.
Variants:
"universal": Depends onUniversal/packageBin(.zip). Formula includesdepends_on "openjdk@<version>"."native": Depends onlinuxPackage(.tar.gzfromWith.Packaging.linux(...)). No JDK dependency.
// JVM universal variant
Program("my-tool", "my-tool", Some("com.myapp.Main"))
.configure(
With.Packaging.universal(
maintainerEmail = "dev@example.com",
pkgName = "my-tool",
pkgSummary = "My Tool",
pkgDescription = "A useful tool"
),
With.Packaging.homebrew(
formulaName = "my-tool",
binaryName = "my-tool",
pkgDescription = "A useful tool",
homepage = "https://example.com"
)
)Publishing the generated formula to a tap is a separate operation — copy the
.rb file to your tap repository (e.g., ossuminc/homebrew-tap).
Reserved for future Windows MSI installer packaging. Currently logs a warning and returns the project unchanged. Not yet implemented.
Create GraalVM native images.
pkgName: Executable namepkgSummary: Summarynative_image_path: Path to native-image executable
Add RIDDL library dependencies.
version: RIDDL version to usenonJVM: marks the dependency as cross-platform (JS/Native as well as JVM). On sbt 2 the%%operator cross-builds across platforms (the%%%operator is gone).
Module("my-riddl-app")
.configure(With.Riddl(version = "0.50.0"))Configure Scala 3 with custom version, compiler options, and documentation.
version: Scala version (defaults to"3.8.4")scala3Options: Additional compiler optionsprojectName: Project name for scaladoc output (optional)docSiteRoot: Root directory for documentation site (optional)docBaseURL: Base URL for API documentation (optional)
Module("my-experimental")
.configure(With.Scala3(
version = Some("3.4.0"),
scala3Options = Seq("-experimental")
))
// With documentation settings
Module("my-lib")
.configure(With.Scala3(
projectName = Some("MyLib"),
docSiteRoot = Some("docs/api"),
docBaseURL = Some("https://myproject.org/api")
))Add ScalaTest dependencies with custom version.
version: ScalaTest version
Module("my-tests")
.configure(With.Scalatest(version = "3.2.19"))Generate unified API documentation.
apiOutput: Output directorybaseURL: Base URL for documentationinclusions: Projects to includeexclusions: Projects to excludelogoPath: Path to logo imageexternalMappings: External API mappings
DocSite(
dirName = "docs",
apiOutput = file("docs/api"),
baseURL = Some("https://myproject.org/api"),
inclusions = Seq(moduleA, moduleB, moduleC)
)sbt-ossuminc 2.0.0 requires sbt 2.0.2+ and Scala 3; the 1.x line (v1.4.0) remains for sbt 1.x builds. To upgrade a consuming project:
- Bump sbt: set
sbt.version=2.0.2(the minimum; newer is fine) inproject/build.properties, and the plugin to2.0.0inproject/plugins.sbt. - Credentials: move
~/.sbt/1.0/github.sbtto~/.sbt/2/github.sbt. - Cross-platform modules:
CrossModule(...)now takes ascalaVersionargument and is built on the built-inprojectMatrix. Replace%%%with%%in cross-platform dependencies. The.jvm/.js/.native,.configure,.settings, and.{jvm,js,native}{Configure,Settings}API is otherwise unchanged. - sbt plugins (
Plugin(...)): dropWith.Scala2and anyscalaVersion := "2.12.x"— sbt 2 plugins compile with Scala 3 (managed by sbt). - Publishing: GitHub Packages is unchanged. Maven Central now uses sbt 2's
native Central Portal (see Publishing helpers) — no
sbt-sonatype. - Unavailable until upstream ships sbt 2 builds:
With.IdeaPlugin(JetBrains sbt-idea-plugin), Coveralls upload, TASTy-MiMa, and stable sbt-paradox docs. The corresponding helpers degrade gracefully or fail fast with a clear message.
No special configuration is needed for the transitive scala-xml / scala-collection-compat cross-version clash: sbt-ossuminc's published POM excludes the Scala 2.13 variants so your meta-build resolves cleanly.
In 1.1.0, CrossModule automatically included testing and time dependencies. In 1.2.0, these are now opt-in for cleaner, more explicit builds.
// Old (1.1.0) - dependencies were automatic
CrossModule("foo", "bar")(JVM, JS)
// New (1.2.0) - explicitly add what you need
CrossModule("foo", "bar")(JVM, JS)
.configure(With.Scalatest()) // Add if you need testing
.configure(With.ScalaJavaTime()) // Add if you need java.time API-
With.Publishing- Generic publishing helper (defaults to GitHub Packages)- Use
With.Publishingfor default (GitHub) publishing - Use
With.Publishing.githubto explicitly use GitHub Packages - Use
With.Publishing.sonatypefor Sonatype/Maven Central
- Use
-
With.ScalaJavaTime()- Addscala-java-timedependency for cross-platform date/time support -
With.ClassPathJar- Use classpath JAR to reduce command line length on Windows -
With.UnmanagedJars- Configure unmanaged JAR files fromlibs/directory -
With.ShellPrompt- Custom sbt shell prompt with project, branch, and version info
Root()project ID is now configurable - UseprojectIdparameter to customize (default:"root")- Parameterized versions -
With.ScalaJS()andWith.Native()now accept version parameters to override defaults
-
With.Packaging.npm(...)- Assemble Scala.js output into npm packages. Tasks:npmPrepare(pure sbt) andnpmPack(shells out to npm). Supports template mode and TypeScript definition auto-discovery. -
With.Packaging.linux(...)- Create tar.gz archives of Scala Native binaries. Auto-detects host OS and architecture. Task:linuxPackage. -
With.Packaging.homebrew(...)- Generate Homebrew formula.rbfiles. Supports"universal"(JVM) and"native"(Scala Native) variants. Task:homebrewGenerate. -
With.Packaging.windowsMsi(...)- Placeholder for future Windows MSI support (not yet implemented).
With.Publishing.npm(...)- Publish npm packages to npmjs.com and/or GitHub Packages. Auth viaNPM_TOKENandGITHUB_TOKENenv vars. Tasks:npmPublish,npmPublishNpmjs,npmPublishGithub.