sbt plugin to automate Sonatype releases from GitHub Actions
Scala
308
712 commits
updated Sep 5, 2026
sbt-ci-release is an sbt plugin to help automate releases to Sonatype and the Central Repository (aka Maven Central) from CI environments such as GitHub Actions.
Beware that publishing from GitHub Actions requires you to expose Sonatype credentials as secret environment variables in GitHub Actions jobs. However, secret environment variables are not accessible during pull requests.
Note: Sonatype has sunset the Legacy OSSRH endpoint to publish on 2025-06-30. To publish to the Central Repository, please migrate to sbt 1.11.x or later and sbt-ci-release 1.11.0 or later after you migrate to publish to the Central Portal publishing.
Let's get started!
First, follow the instructions in https://central.sonatype.org/publish/generate-portal-token/ to create a Sonatype account and make sure you have publishing rights for a domain name. This is a one-time setup per domain name.
If you don't have a domain name, you can use io.github.<@your_username>. Here
is a template you can use to write the Sonatype issue:
Title:
Publish rights for io.github.sbt
Description:
Hi, I would like to publish under the groupId: io.github.sbt.
It's my GitHub account https://github.com/sbt/
Sonatype no longer allows using your actual username and password to authenticate during publishing. Instead, you must use the name and password from your "user token".
Next, install this plugin in project/plugins.sbt
// sbt 1 only, see FAQ for 0.13 support
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "<version>")
Note:
<version>must be replaced by a full SemVer version number (like1.11.2).
By installing sbt-ci-release the following sbt plugins are also brought in:
Make sure build.sbt does not define any of the following settings
version: handled by sbt-dynverpublishTo: handled by sbt-ci-releasepublishMavenStyle: handled by sbt-ci-releasecredentials: handled by sbt-sonatypeNext, define publishing settings at the top of build.sbt
inThisBuild(List(
organization := "com.github.sbt",
homepage := Some(url("https://github.com/sbt/sbt-ci-release")),
// Alternatively License.Apache2 see https://github.com/sbt/librarymanagement/blob/develop/core/src/main/scala/sbt/librarymanagement/License.scala
licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")),
developers := List(
Developer(
"olafurpg",
"Ólafur Páll Geirsson",
"olafurpg@gmail.com",
url("https://geirsson.com")
)
)
))
Next, create a fresh gpg key that you will share with GitHub Actions and only use for this project.
gpg --gen-key
PGP_PASSPHRASE in your CI config. Take note of PGP_PASSPHRASE.At the end you'll see output like this
pub rsa2048 2018-06-10 [SC] [expires: 2020-06-09]
$LONG_ID
uid $PROJECT_NAME bot <$EMAIL>
Take note of $LONG_ID, make sure to replace this ID from the code examples
below. The ID will look something like (a)
6E8ED79B03AD527F1B281169D28FC818985732D9 or something like (b)
A4C9 75D9 9C05 E4C7 2163 4BBD ACA8 EB32 0BFE FE2C (in which case delete the
spaces to make it look like (a)). A command like this one should do:
# On UNIX
LONG_ID=6E8ED79B03AD527F1B281169D28FC818985732D9
# On Windows
set LONG_ID=6E8ED79B03AD527F1B281169D28FC818985732D9
Next, copy the public gpg signature
# macOS
gpg --armor --export $LONG_ID | pbcopy
# linux
gpg --armor --export $LONG_ID | xclip -selection clipboard
# Windows
gpg --armor --export %LONG_ID%
and post the signature to a keyserver: https://keyserver.ubuntu.com/

or run:
# macOS
gpg --keyserver hkp://keyserver.ubuntu.com --send-key $LONG_ID && \
gpg --keyserver hkp://keys.openpgp.org --send-key $LONG_ID
# linux
gpg --keyserver hkp://keyserver.ubuntu.com --send-key $LONG_ID && \
gpg --keyserver hkp://keys.openpgp.org --send-key $LONG_ID
# Windows
gpg --keyserver hkp://keyserver.ubuntu.com --send-key %LONG_ID% && \
gpg --keyserver hkp://keys.openpgp.org --send-key %LONG_ID%
Next, you'll need to declare four environment variables in your CI.
Select Settings -> Secrets and variables -> Actions -> New repository secret to add each of the
required variables as shown in the next figure:

When complete, your secrets settings should look like the following:

Add the following secrets:
PGP_PASSPHRASE: The randomly generated password you used to create a fresh
gpg key.PGP_SECRET: The base64 encoded secret of your private key that you can
export from the command line like here below.# macOS
gpg --armor --export-secret-keys $LONG_ID | base64 | pbcopy
# Ubuntu (assuming GNU base64)
gpg --armor --export-secret-keys $LONG_ID | base64 -w0 | xclip -selection clipboard
# Arch
gpg --armor --export-secret-keys $LONG_ID | base64 | sed -z 's;\n;;g' | xclip -selection clipboard -i
# FreeBSD (assuming BSD base64)
gpg --armor --export-secret-keys $LONG_ID | base64 | xclip -selection clipboard
# Windows
gpg --armor --export-secret-keys %LONG_ID% | openssl base64
If you try to display the base64 encoded string in the terminal, some shells (like zsh or fish) may include an additional % character at the end, to mark the end of content which was not terminated by a newline character. This does not indicate a problem. Note for Windows - delete any linebreaks or spaces when copying the encoded string from terminal.
SONATYPE_PASSWORD: The password part of your Sonatype
user token, generated on the Central Portal (not the account password!).SONATYPE_USERNAME: The username part of your Sonatype
user token (not the account username!).CI_RELEASE: the command to publish all artifacts for stable
releases. Defaults to +publishSigned if not provided.CI_SNAPSHOT_RELEASE: the command to publish all artifacts for a
SNAPSHOT releases. Defaults to +publish if not provided.CI_SONATYPE_RELEASE: the command called to close and promote the
staged repository. Useful when, for example, also dealing with non-sbt
projects to change to sonatypeReleaseAll. Defaults to
sonatypeBundleRelease if not provided.Run the following command to install the same
release.yml
script that is used to release this repository.
mkdir -p .github/workflows && \
curl -L https://raw.githubusercontent.com/sbt/sbt-ci-release/main/.github/workflows/release.yml > .github/workflows/release.yml
Commit the file and merge into main.
We're all set! Time to manually try out the new setup
git tag -a v0.1.0 -m "v0.1.0"
git push origin v0.1.0
Note that the tag version MUST start with v.
It is normal that something fails on the first attempt to publish from CI. Even if it takes 10 attempts to get it right, it's still worth it because it's so nice to have automatic CI releases.
Enjoy 👌
sbt-ci-release implements a mini-DSL for the Git tag for back publishing purpose, which is useful if you maintain a compiler plugin, library for Scala Native, or an sbt plugin during 2.x migration etc.
v1.2.3[@3.x|@2.n.x|@a.b.c][@command][#comment]
# is used for comments, which is useful if you need to use the same command multiple times@3.x expands to ++3.x, and if no other commands follow, ;++3.x;publishSigned@2.13.x expands to ++2.13.x, and if no other commands follow, ;++2.13.x;publishSigned@foo/publishSigned expands to foo/publishSignedv1.2.3@2.13.15
v1.2.3@3.x. Optionally we can add a comment: v1.2.3@3.x#comment.
We can use this to back publish sbt 2.x plugins.
v1.2.3 to create release/1.2.3 branch, and send a PR to update sbt versionv1.2.3@3.x#sbt2.0.0-Mnv1.2.3@2.13.15@foo/publishSigned
You can create a subproject to aggregate 2 or more subprojects.
v1.2.3@+foo_native/publishSigned#comment
v1.2.3 to create release/1.2.3 branch, and send a PR to update the Scala Native version.v1.2.3@+foo_native/publishSigned#native0.5v1.2.3#unique_comment, for example v1.2.3#native0.5_3
If you prefer to keep most of the information in a git branch instead, you can just use the comment functionality.
v1.2.3 to create release/1.2.3 branch, and send a PR to:
a. Update appropriate dependency (sbt, Scala Native etc)
b. Modify the CI_RELEASE environment variable to encode the actions you want to take, like ;++3.x;foo_native/publishSigned. For GitHub Actions, it would be in .github/workflows/release.ymlv1.2.3#unique_comment. For record keeping, encode the version you're trying to back publishing for e.g. v1.2.3#native0.5_3Add the following to the project settings (works only in sbt 1)
publish / skip := true
Make sure that projects that compile against multiple Scala versions declare the
crossScalaVersions setting in build.sbt, for example
lazy val core = project.settings(
...
crossScalaVersions := List("2.13.1", "2.12.10", "2.11.12")
)
The command +publishSigned (default value for CI_RELEASE) will then publish
that project for 2.11, 2.12 and 2.13.
If you publish for multiple Scala.js versions, start by disabling publishing of
the non-JS projects when the SCALAJS_VERSION environment variable is defined.
// build.sbt
+ val customScalaJSVersion = Option(System.getenv("SCALAJS_VERSION"))
lazy val myLibrary = crossProject(JSPlatform, JVMPlatform)
.settings(
// ...
)
+ .jvmSettings(
+ skip.in(publish) := customScalaJSVersion.isDefined
+ )
Next, add an additional ci-release step in your CI config to publish the
custom Scala.js version
+ SCALAJS_VERSION=0.6.31 sbt ci-release
Add the following setting
resolvers += Resolver.sonatypeCentralSnapshots
If you are still using Travis-CI, see old revisions of this readme for instructions.
CircleCI should work as well.
You could also try sbt-release-early.
Alternatively, the source code for sbt-ci-release is only ~50 loc, see
CiReleasePlugin.scala.
You can copy-paste it to project/ of your build and tweak the settings for
your environment.
Yes, but the plugin is not released for sbt 0.13. The plugin source code is a
single file which you can copy-paste into project/CiReleasePlugin.scala of
your 0.13 build. Make sure you also
addSbtPlugin(sbt-dynver + sbt-sonatype + sbt-gpg).
You can publish sbt plugins to Maven Central like a normal library, no custom setup required. It is not necessary to publish sbt plugins to Bintray.
LONG_ID for the gpg key.base64 (default on Ubuntu), pass in
the -w0 flag to disable line wrapping.This error happens when you publish a non-SNAPSHOT version to the snapshot
repository. If you pushed a tag, make sure the tag version number starts with
v. This error can happen if you tag with the version 0.1.0 instead of
v0.1.0.
We think that the creation of release notes should not be fully automated because commit messages don't often communicate the end user impact well. You can use Release Drafter github app (or the Github Action) to help you craft release notes.
Make sure your pgp key did not expire. If it expired you have to change the expiry date and reupload it. See: https://github.com/sbt/sbt-ci-release#gpg.
Below is a non-exhaustive list of projects using sbt-ci-release. Don't see your project? Add it in a PR!
There exist great alternatives to sbt-ci-release that may work better for your setup.
The biggest difference between these and sbt-ci-release wrt to publishing is the
base64 encoded PGP_SECRET variable. I never managed to get the encrypted files
and openssl working.
(top 30 of 60)
Scala
100.0%
sbt plugin to automate Sonatype releases from GitHub Actions
Scala
308
712 commits
updated Sep 5, 2026
sbt-ci-release is an sbt plugin to help automate releases to Sonatype and the Central Repository (aka Maven Central) from CI environments such as GitHub Actions.
Beware that publishing from GitHub Actions requires you to expose Sonatype credentials as secret environment variables in GitHub Actions jobs. However, secret environment variables are not accessible during pull requests.
Note: Sonatype has sunset the Legacy OSSRH endpoint to publish on 2025-06-30. To publish to the Central Repository, please migrate to sbt 1.11.x or later and sbt-ci-release 1.11.0 or later after you migrate to publish to the Central Portal publishing.
Let's get started!
First, follow the instructions in https://central.sonatype.org/publish/generate-portal-token/ to create a Sonatype account and make sure you have publishing rights for a domain name. This is a one-time setup per domain name.
If you don't have a domain name, you can use io.github.<@your_username>. Here
is a template you can use to write the Sonatype issue:
Title:
Publish rights for io.github.sbt
Description:
Hi, I would like to publish under the groupId: io.github.sbt.
It's my GitHub account https://github.com/sbt/
Sonatype no longer allows using your actual username and password to authenticate during publishing. Instead, you must use the name and password from your "user token".
Next, install this plugin in project/plugins.sbt
// sbt 1 only, see FAQ for 0.13 support
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "<version>")
Note:
<version>must be replaced by a full SemVer version number (like1.11.2).
By installing sbt-ci-release the following sbt plugins are also brought in:
Make sure build.sbt does not define any of the following settings
version: handled by sbt-dynverpublishTo: handled by sbt-ci-releasepublishMavenStyle: handled by sbt-ci-releasecredentials: handled by sbt-sonatypeNext, define publishing settings at the top of build.sbt
inThisBuild(List(
organization := "com.github.sbt",
homepage := Some(url("https://github.com/sbt/sbt-ci-release")),
// Alternatively License.Apache2 see https://github.com/sbt/librarymanagement/blob/develop/core/src/main/scala/sbt/librarymanagement/License.scala
licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")),
developers := List(
Developer(
"olafurpg",
"Ólafur Páll Geirsson",
"olafurpg@gmail.com",
url("https://geirsson.com")
)
)
))
Next, create a fresh gpg key that you will share with GitHub Actions and only use for this project.
gpg --gen-key
PGP_PASSPHRASE in your CI config. Take note of PGP_PASSPHRASE.At the end you'll see output like this
pub rsa2048 2018-06-10 [SC] [expires: 2020-06-09]
$LONG_ID
uid $PROJECT_NAME bot <$EMAIL>
Take note of $LONG_ID, make sure to replace this ID from the code examples
below. The ID will look something like (a)
6E8ED79B03AD527F1B281169D28FC818985732D9 or something like (b)
A4C9 75D9 9C05 E4C7 2163 4BBD ACA8 EB32 0BFE FE2C (in which case delete the
spaces to make it look like (a)). A command like this one should do:
# On UNIX
LONG_ID=6E8ED79B03AD527F1B281169D28FC818985732D9
# On Windows
set LONG_ID=6E8ED79B03AD527F1B281169D28FC818985732D9
Next, copy the public gpg signature
# macOS
gpg --armor --export $LONG_ID | pbcopy
# linux
gpg --armor --export $LONG_ID | xclip -selection clipboard
# Windows
gpg --armor --export %LONG_ID%
and post the signature to a keyserver: https://keyserver.ubuntu.com/

or run:
# macOS
gpg --keyserver hkp://keyserver.ubuntu.com --send-key $LONG_ID && \
gpg --keyserver hkp://keys.openpgp.org --send-key $LONG_ID
# linux
gpg --keyserver hkp://keyserver.ubuntu.com --send-key $LONG_ID && \
gpg --keyserver hkp://keys.openpgp.org --send-key $LONG_ID
# Windows
gpg --keyserver hkp://keyserver.ubuntu.com --send-key %LONG_ID% && \
gpg --keyserver hkp://keys.openpgp.org --send-key %LONG_ID%
Next, you'll need to declare four environment variables in your CI.
Select Settings -> Secrets and variables -> Actions -> New repository secret to add each of the
required variables as shown in the next figure:

When complete, your secrets settings should look like the following:

Add the following secrets:
PGP_PASSPHRASE: The randomly generated password you used to create a fresh
gpg key.PGP_SECRET: The base64 encoded secret of your private key that you can
export from the command line like here below.# macOS
gpg --armor --export-secret-keys $LONG_ID | base64 | pbcopy
# Ubuntu (assuming GNU base64)
gpg --armor --export-secret-keys $LONG_ID | base64 -w0 | xclip -selection clipboard
# Arch
gpg --armor --export-secret-keys $LONG_ID | base64 | sed -z 's;\n;;g' | xclip -selection clipboard -i
# FreeBSD (assuming BSD base64)
gpg --armor --export-secret-keys $LONG_ID | base64 | xclip -selection clipboard
# Windows
gpg --armor --export-secret-keys %LONG_ID% | openssl base64
If you try to display the base64 encoded string in the terminal, some shells (like zsh or fish) may include an additional % character at the end, to mark the end of content which was not terminated by a newline character. This does not indicate a problem. Note for Windows - delete any linebreaks or spaces when copying the encoded string from terminal.
SONATYPE_PASSWORD: The password part of your Sonatype
user token, generated on the Central Portal (not the account password!).SONATYPE_USERNAME: The username part of your Sonatype
user token (not the account username!).CI_RELEASE: the command to publish all artifacts for stable
releases. Defaults to +publishSigned if not provided.CI_SNAPSHOT_RELEASE: the command to publish all artifacts for a
SNAPSHOT releases. Defaults to +publish if not provided.CI_SONATYPE_RELEASE: the command called to close and promote the
staged repository. Useful when, for example, also dealing with non-sbt
projects to change to sonatypeReleaseAll. Defaults to
sonatypeBundleRelease if not provided.Run the following command to install the same
release.yml
script that is used to release this repository.
mkdir -p .github/workflows && \
curl -L https://raw.githubusercontent.com/sbt/sbt-ci-release/main/.github/workflows/release.yml > .github/workflows/release.yml
Commit the file and merge into main.
We're all set! Time to manually try out the new setup
git tag -a v0.1.0 -m "v0.1.0"
git push origin v0.1.0
Note that the tag version MUST start with v.
It is normal that something fails on the first attempt to publish from CI. Even if it takes 10 attempts to get it right, it's still worth it because it's so nice to have automatic CI releases.
Enjoy 👌
sbt-ci-release implements a mini-DSL for the Git tag for back publishing purpose, which is useful if you maintain a compiler plugin, library for Scala Native, or an sbt plugin during 2.x migration etc.
v1.2.3[@3.x|@2.n.x|@a.b.c][@command][#comment]
# is used for comments, which is useful if you need to use the same command multiple times@3.x expands to ++3.x, and if no other commands follow, ;++3.x;publishSigned@2.13.x expands to ++2.13.x, and if no other commands follow, ;++2.13.x;publishSigned@foo/publishSigned expands to foo/publishSignedv1.2.3@2.13.15
v1.2.3@3.x. Optionally we can add a comment: v1.2.3@3.x#comment.
We can use this to back publish sbt 2.x plugins.
v1.2.3 to create release/1.2.3 branch, and send a PR to update sbt versionv1.2.3@3.x#sbt2.0.0-Mnv1.2.3@2.13.15@foo/publishSigned
You can create a subproject to aggregate 2 or more subprojects.
v1.2.3@+foo_native/publishSigned#comment
v1.2.3 to create release/1.2.3 branch, and send a PR to update the Scala Native version.v1.2.3@+foo_native/publishSigned#native0.5v1.2.3#unique_comment, for example v1.2.3#native0.5_3
If you prefer to keep most of the information in a git branch instead, you can just use the comment functionality.
v1.2.3 to create release/1.2.3 branch, and send a PR to:
a. Update appropriate dependency (sbt, Scala Native etc)
b. Modify the CI_RELEASE environment variable to encode the actions you want to take, like ;++3.x;foo_native/publishSigned. For GitHub Actions, it would be in .github/workflows/release.ymlv1.2.3#unique_comment. For record keeping, encode the version you're trying to back publishing for e.g. v1.2.3#native0.5_3Add the following to the project settings (works only in sbt 1)
publish / skip := true
Make sure that projects that compile against multiple Scala versions declare the
crossScalaVersions setting in build.sbt, for example
lazy val core = project.settings(
...
crossScalaVersions := List("2.13.1", "2.12.10", "2.11.12")
)
The command +publishSigned (default value for CI_RELEASE) will then publish
that project for 2.11, 2.12 and 2.13.
If you publish for multiple Scala.js versions, start by disabling publishing of
the non-JS projects when the SCALAJS_VERSION environment variable is defined.
// build.sbt
+ val customScalaJSVersion = Option(System.getenv("SCALAJS_VERSION"))
lazy val myLibrary = crossProject(JSPlatform, JVMPlatform)
.settings(
// ...
)
+ .jvmSettings(
+ skip.in(publish) := customScalaJSVersion.isDefined
+ )
Next, add an additional ci-release step in your CI config to publish the
custom Scala.js version
+ SCALAJS_VERSION=0.6.31 sbt ci-release
Add the following setting
resolvers += Resolver.sonatypeCentralSnapshots
If you are still using Travis-CI, see old revisions of this readme for instructions.
CircleCI should work as well.
You could also try sbt-release-early.
Alternatively, the source code for sbt-ci-release is only ~50 loc, see
CiReleasePlugin.scala.
You can copy-paste it to project/ of your build and tweak the settings for
your environment.
Yes, but the plugin is not released for sbt 0.13. The plugin source code is a
single file which you can copy-paste into project/CiReleasePlugin.scala of
your 0.13 build. Make sure you also
addSbtPlugin(sbt-dynver + sbt-sonatype + sbt-gpg).
You can publish sbt plugins to Maven Central like a normal library, no custom setup required. It is not necessary to publish sbt plugins to Bintray.
LONG_ID for the gpg key.base64 (default on Ubuntu), pass in
the -w0 flag to disable line wrapping.This error happens when you publish a non-SNAPSHOT version to the snapshot
repository. If you pushed a tag, make sure the tag version number starts with
v. This error can happen if you tag with the version 0.1.0 instead of
v0.1.0.
We think that the creation of release notes should not be fully automated because commit messages don't often communicate the end user impact well. You can use Release Drafter github app (or the Github Action) to help you craft release notes.
Make sure your pgp key did not expire. If it expired you have to change the expiry date and reupload it. See: https://github.com/sbt/sbt-ci-release#gpg.
Below is a non-exhaustive list of projects using sbt-ci-release. Don't see your project? Add it in a PR!
There exist great alternatives to sbt-ci-release that may work better for your setup.
The biggest difference between these and sbt-ci-release wrt to publishing is the
base64 encoded PGP_SECRET variable. I never managed to get the encrypted files
and openssl working.
(top 30 of 60)
Scala
100.0%