5 minute read

We’re gonna use the following sample project tree:

foo
- src
 - main
  - scala
core
- src
 - main
  - scala
build.sbt

You’ll first need to create a Sonatype JIRA account here and then create an issue https://issues.sonatype.org/secure/CreateIssue.jspa?issuetype=21&pid=10134

NOTE: The issue type will be New Project but this doesn’t necessarily mean you’ll need to do this for every project you want to publish. It all depends on the Group Id you set. For example for (circe)[https://circe.github.io/] the group id is io.circe under which lives circe-parser, circe-core, circe-generic, etc. So to push additional circe related that will live under this group id, the maintainer need not create additional tickets, but would if they started a project foo that wil live under the seperate group id io.foo

Once you see a comment on the ticket appear that reads something like this:

Configuration has been prepared, now you can:

Deploy snapshot artifacts into repository https://oss.sonatype.org/content/repositories/snapshots
Deploy release artifacts into the staging repository https://oss.sonatype.org/service/local/staging/deploy/maven2
Promote staged artifacts into repository 'Releases'
Download snapshot and release artifacts from group https://oss.sonatype.org/content/groups/public
Download snapshot, release and staged artifacts from staging group https://oss.sonatype.org/content/groups/staging
please comment on this ticket when you promoted your first release, thanks

You’re ready to deploy your project!

In-project config

You’ll need the following plugins in plugins.sbt:

addSbtPlugin("com.dwijnand" % "sbt-travisci" % "<version>")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "<version>")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "<version>")
addSbtPlugin("com.github.gseitz" % "sbt-release" % "<version>")

Here’s a minimal build.sbt:

import ReleaseTransformations._

organization in ThisBuild := "group.id.from.ticket"

lazy val root = project.in(file("."))
  .settings(noPublishSettings: _*)
  .aggregate(foo, core)

lazy val core = project.in(file("core"))
  .settings(releasePublishSettings: _*)
  .settings(name := "core")

lazy val foo = project.in(file("foo"))
  .settings(releasePublishSettings: _*)
  .settings(name := "foo")
  .dependsOn(core % "compile->compile;test->test")

/** We dont want to publish the `root` module */
lazy val noPublishSettings = Seq(
  publish := {},
  publishLocal := {},
  publishArtifact := false
)

lazy val releasePublishSettings = Seq(
  releaseCrossBuild := true,
  releasePublishArtifactsAction := PgpKeys.publishSigned.value,
  releaseProcess := Seq[ReleaseStep](
    /** This is my specific list of tasks to tell the `sbt-release` plugin to run.
    You can obviously configure this differently however if you don't care particularly,
    this particular configuration works well
    */
    checkSnapshotDependencies,
    inquireVersions,
    runClean,
    runTest,
    setReleaseVersion,
    commitReleaseVersion,
    tagRelease,
    publishArtifacts,
    setNextVersion,
    commitNextVersion,
    ReleaseStep(action = Command.process("sonatypeReleaseAll", _)),
    pushChanges
  ),
  homepage := Some(url("https://github.com/yourUsername/thisProject")),
  licenses := Seq("Apache 2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")),
  // or GitLabHosting
  sonatypeProjectHosting := Some(GitHubHosting("yourUsername", "thisProject", "yourEmail"))
  publishMavenStyle := true,
  publishArtifact in Test := false,
  pomIncludeRepository := { _ => false },
  // if not set, will be the same as `organization` above
  sonatypeProfileName := "group.id.from.ticket",
  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")
  },
  scmInfo := Some(
    ScmInfo(
      url("https://github.com/yourUsername/thisProject"),
      "scm:git@github.com:yourUsername/thisProject.git"
    )
  ),
  developers := List(
    Developer("yourSonatypeUsername",  "FirstName LastName", "yourEmail", url("yourWebsiteUrl"))
  )
)

TIP: dependsOn(core % "compile->compile;test->test") allows you to use common test code utilities defined in the core module in the foo module

sbt-release by default will read the version to release from a version.sbt file in the root of your project:

version in ThisBuild := "0.1.0"

If you add -SNAPSHOT to the end of your version sbt-release will put the packaged artifacts in the Snapshots repo in maven central. More on how the repos in maven central are set up in a second

Familiarizing yourself with the sonatype online explorer

Here, log in. Click on Repositories in left hand pane and search in the upper right corner for repo with Repository name just Snapshots. This is maven’s snapshot repo, where versions ending with -SNAPSHOT will go

The repo named just Releases is where versions of the form <major>.<minor>.<patch> will go. When you enter +publishSigned in the sbt shell your artifacts will first be placed in a staging repository. You can find it towards the bottom of the list when you click on Staging Repositories in the left hand pane. When you are doing a release to the Releases repo, maven promotes those artifacts you’ve placed in the staging repo to the Releases repo. It takes around 10 minutes for you to be able to pull in said artifact(s) in a project and up to 2 hrs for it to be searchable on http://mvnrepository.com

sbt-travisci and multiple Scala major-minor artifacts

In a .travis.yml file in your project we can declare various Scala version to build/release our project against. By default when you do say a sbt compile the last version in the list is chosen:

language: scala

scala:
 - 2.11.12
 - 2.12.4

jdk:
 - oraclejdk8

Out-of-project config

Generate PGP private/public key

You’ll need this to sign your artifacts when publishing them to the staging repo. In your project dir, enter an sbt shell

> set pgpReadOnly := false
> pgp-cmd gen-key
Please enter the name associated with the key: FirstName LastName
Please enter the email associated with the key: yourEmail
Please enter the passphrase for the key: ********
Please re-enter the passphrase for the key: ********
[info] Creating a new PGP key, this could take a long time.
[info] Public key := ~/.sbt/gpg/pubring.asc
[info] Secret key := ~/.sbt/gpg/secring.asc
[info] Please do not share your secret key.   Your public key is free to share.

You’ll need to upload this created key to a key server:

> pgp-cmd send-key yourEmail hkp://pool.sks-keyservers.net

Add sonatype credentials

Create a file ~/.sbt/<your-project-sbt-major-minor-version>/sonatype.sbt with the following:

credentials += Credentials(
  "Sonatype Nexus Repository Manager",
  "oss.sonatype.org",
  "<sonatype-username>",
  "<sonatype-password>"
)

And now you’re ready to release

In your project sbt prompt:

> +publishSigned

This will publish foo and core artifacts to the staging repo. The + indicates that you’d like to package and deploy your module(s) against all of the different major-minor Scala versions declared in your .travis.yml file

Mar 2019 Update: Getting the following exception with +publishLocal java.net.ProtocolException: Too many follow-up requests sbt which is some issue with gigahorse not being able to parse your sonatype.sbt remote host. Work around is to add updateOptions := updateOptions.value.withGigahorse(false) to your project’s settings (don’t just dump it in build.sbt). See this issue for more details/status

To promote these artifacts to the Releases repo:

> sonatypeReleaseAll

TIP: If +publishSigned fails for some reason it is best to drop, not close, the staging repo opened as a result via sonatypeDrop (if you have mutliple open this operation will fail withe something like:

[error] Multiple repositories are found:
[error] [yourorg-XXXX] status:open, profile:yourorg(somehash)
[error] [yourorg-XXXY] status:open, profile:yourorg(somehash)
[error] Specify one of the repository ids in the command line

so you’ll need to specify which one you want to drop via sonatypeDrop XXXX (the repo id from above)) if you only close a staging repo, push a new one up and attempt to promote, Nexus will oddly try to promote the earliest repo found, i.e. the closed one

And you’re done!

Updated:

Comments