# sbt-release turns publishing into a sequence of build tasks

> sbt-release is an sbt plugin that runs a release as a scripted sequence: it checks the working copy, asks for the release and next development versions, writes them into version.sbt, tests, tags, publishes and bumps, with a non-interactive mode and cross building support.

**sbt/sbt-release** — A release plugin for sbt

- Repository: https://github.com/sbt/sbt-release
- Stars: 648 · Forks: 160
- Language: Scala
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/sbt-sbt-release

## What the plugin actually does for you

sbt-release is described as providing a customizable release process that you can add to your project. That phrase carries two separate promises, and the first one is the smaller one.

The default process is a fixed sequence of tasks that the plugin runs for you, so the version bump, the commit, the tag, the publish and the next development version happen in a known order with known failure behaviour. The second promise, customization, is what lets a team replace or insert steps, and the README points to a section on customizing the release process and to a ReleaseStep concept for taking part in a cross build.

The audience is a Scala project that publishes artifacts. The requirements section is explicit that a publish repository must be configured, and that this is required only for the default release process, with the pointer to the customization section for teams who want something else.

Two other requirements shape the work. sbt 1.x is required, and git is listed as optional, which is worth pausing on. The plugin is not tied to a specific version control system in principle, and the repository topics name both git and mercurial. The default release process does, however, begin by checking that the working directory is a git repository with no outstanding changes, so in practice the default path assumes git.

Adding the plugin is a single line in the plugin definition file:

```scala
addSbtPlugin("com.github.sbt" % "sbt-release" % "1.5.0")
```

The README also notes that it contains information for the latest release, and that documents for a specific version should be read from the corresponding tag, which is the sensible thing to do when a build pins an older plugin.

## The default process, one task at a time

The default release process is listed as a sequence of tasks, and the order is the design. The first entries look like this:

```text
 1. Check that the working directory is a git repository and the repository has no outstanding changes. Also prints the hash of the last commit to the console.
 1. If there are any snapshot dependencies, ask the user whether to continue or not (default: no).
 1. Ask the user for the `release version` and the `next development version`. Sensible defaults are provided.
 1. Run `clean`.
 1. Run `test`, if any test fails, the release process is aborted.
```

Then the write, publish and bump half. The release version is written as ThisBuild / version into version.sbt and also applied to the current build state, so the running build and the file agree. The change is committed, and the previous commit is tagged with v plus the version, for example v1.2 or v1.2.3. Then publish runs. Finally the next development version is written to version.sbt and applied, and that change is committed too.

The ordering is what makes the process safe. The version is set before the tests run and before publishing, so the artifact that reaches the repository carries the release version rather than a development version, and the development version is only written after publish succeeds, so a failed release leaves the tree on the release version rather than half way between two states.

One rule is stated without exception: in case of a failure of a task, the release process is aborted. There is no rollback step described. That makes the preflight checks worth taking seriously, since a failure after the first commit leaves the version file changed and the tag not yet created.

## version.sbt, and the file the plugin refuses to touch

The version handling is the design decision that most affects how you adopt the plugin, and the README explains it at length.

Because the build definition is actual Scala code, it is not as straightforward to change something in the middle of it as it is with an XML definition. Rewriting build.sbt to inject a version would mean parsing and rewriting a source file, which is exactly the kind of operation a release tool should not perform silently.

So sbt-release never touches your build definition files. Instead it writes the new release or development version to a file defined by the setting releaseVersionFile, which is set to file("version.sbt") by default and points at $PROJECT_ROOT/version.sbt. The file is a plain assignment that your build reads, and the plugin only ever writes to it.

There is a second choice about where the version lives. By default the version is set at the build level using ThisBuild / version, and the file written contains that global form. Setting releaseUseGlobalVersion to false changes the behaviour so that a version like version := "1.2.3" is written instead, which puts the setting on the project rather than the build.

The practical effect is that adding this plugin to an existing project is a decision about your build's shape. If your build already sets the version in build.sbt, you have to move it to version.sbt, or accept the global form, or skip the plugin. That migration is the main friction point, and it is the first thing to plan for rather than discover during a release.

## Non-interactive releases and the defaults they pick

The with-defaults argument, with tab completion, runs a release without asking anything. The README then lists exactly which default is chosen for each interaction, and the list is worth reading in full because it is the specification of what an unattended release does.

On the dependency question, the answer is no, so a release that still depends on a -SNAPSHOT artifact is aborted rather than published. The release version defaults to the current version with the qualifier removed, so 1.2-SNAPSHOT becomes 1.2. The next version defaults to an increase of the minor segment with the -SNAPSHOT qualifier, so 1.2.1-SNAPSHOT becomes 1.3.0-SNAPSHOT.

Version control behaviour is the more interesting part. For the tag, the default is to abort if the tag already exists, and the answer can be overridden with a setting that takes one of several values: o to override, k to keep the existing tag without overwriting, a to abort as the default, or an explicit custom tag name such as 1.2-M3.

For the push, the defaults are all conservative. The process aborts if no remote tracking branch is set up, if the remote tracking branch cannot be checked, for instance through a git fetch that fails, or if the remote tracking branch has unmerged commits. Each of those is a case where pushing would either lose work or publish something unexpected.

Two other command-line arguments exist for the emergency case. The release-version and next-version arguments set the two versions directly, with the example release release-version 1.0.99 next-version 1.2.0-SNAPSHOT, and the skip-tests argument exists for the release at two in the morning on a Sunday, which is exactly the scenario the README describes. A team that uses skip-tests should have a clear view of what it trades away, since the default process aborts on a failing test and this removes that check.

## Cross building and the version bump strategies

Since version 0.7 the plugin has had built-in support for cross building and cross publishing, which matters for a library published for several Scala versions.

A cross release is triggered in one of two ways. The setting releaseCrossBuild enables it, and it is false by default, or the cross option can be given to the release command, as in release cross with-defaults. The README notes that combining them, setting releaseCrossBuild to false while running release cross, disables the automatic detection of cross release.

Of the predefined release steps, clean, test and publish are the ones set up for cross building, which is a sensible division. A cross release then behaves like the plus command. If crossScalaVersions is not set, running release or release cross will not trigger a cross release at all, and the release runs with the version in the scalaVersion setting. If crossScalaVersions is set, only those versions are used, and the default scalaVersion should be included in the list, since running release cross on a root project whose crossScalaVersions is Nil releases nothing.

Version bumping is configurable too. Since version 0.8 the plugin has several strategies for computing the next development version, selected through the releaseVersionBump setting, and they are defined in sbtrelease.Version.Bump. The list given is Major, which always bumps the major part, Minor, Bugfix, Nano, and Next, which is the default and bumps the last version segment. The README text for that last entry is cut off in the copy available here, so the exact rule for Next beyond its name has to be read in the documentation for the version you use.

That choice interacts with the non-interactive defaults, since the default next version is described in terms of a minor version increase, so a team that switches the bump strategy should check that the with-defaults behaviour still matches what it expects.

## The version scheme is looser than semver, on purpose

The requirements section says the project version should follow the semantic versioning scheme on semver.org, with additions, and the additions are the interesting part.

The minor and bugfix part of the version, and anything beyond it, are optional. There is no limit to the number of subversions. The appendix after the bugfix part must be alphanumeric, matching the pattern of letters and digits, but may also contain dash characters. And the README then lists what that allows: 1.2.3, 1.2.3-SNAPSHOT, 1.2beta1, 1.2-beta.1, 1.2, 1, 1-BETA17, 1.2.3.4.5 and 1.2.3.4.5-SNAPSHOT.

So the scheme is semver with the requirement that the parts be present relaxed, and with a milestone-style appendix permitted. That is a deliberate choice, and it explains why the plugin offers a Nano bump strategy and why the tag override accepts a custom name such as 1.2-M3. A project that needs release candidates or milestone tags has somewhere to put them in the version string itself.

On the plugin itself, the licence is Apache 2.0 with the LICENSE file at the root, and the releases are v1.5.0 on 2026-07-18, v1.4.0 on 2024-01-23 and v1.1.0 on 2021-06-24. The last push was on 2026-09-29. The gap between 1.4.0 and 1.5.0 is long, which fits a plugin whose behaviour people depend on at release time, where a new major version is the thing that forces a decision.

## Conclusion

sbt-release fits a Scala team that publishes to Maven Central from sbt and wants the version bump, the tag, the publish and the next development version to be one repeatable command rather than a checklist someone follows under pressure, and who are prepared to keep the version in a version.sbt file rather than in the build definition. It is the wrong tool for a project that does not publish from sbt, and it is the wrong tool if your release needs approvals, changelog generation or signing steps the plugin does not model, since then you are writing custom ReleaseStep code. Before adopting it, read the requirements list carefully, because the version scheme is looser than plain semver and the optional parts are exactly what the plugin will propose defaults for, and pin the plugin version in plugins.sbt rather than tracking a moving target.

## FAQ

### How do I add sbt-release to my build?

Add the plugin to your project plugins.sbt with addSbtPlugin("com.github.sbt" % "sbt-release" % "1.5.0"). The plugin requires sbt 1.x, a configured publish repository for the default release process, and git is optional.

### What does the default sbt-release process do?

It checks that the working directory is a git repository with no outstanding changes, asks about dependencies on -SNAPSHOT versions, asks for the release and next development versions, runs clean and test, writes the release version to version.sbt and commits it, tags the previous commit with v plus the version, runs publish, then writes and commits the next development version. A failed task aborts the process.

### How do I run a release without being prompted?

Pass the with-defaults argument to the release command. The plugin then uses its documented defaults, aborting on dependencies on -SNAPSHOT versions, using the current version without its qualifier as the release version, and increasing the minor segment with a -SNAPSHOT qualifier as the next version.

### Where does sbt-release write the version?

It writes to the file named by the releaseVersionFile setting, which is file("version.sbt") by default, pointing at $PROJECT_ROOT/version.sbt. The plugin never modifies your build definition files, because the build definition is Scala code.

### How do I trigger a cross release with sbt-release?

Either set releaseCrossBuild, which is false by default, or run release cross. If crossScalaVersions is not set, no cross release happens; if it is set, only those versions are released and the default scalaVersion should be included in the list.

## Sources

- [Issues](https://github.com/sbt/sbt-release/issues)
- [License: Apache-2.0](https://github.com/sbt/sbt-release/blob/main/LICENSE)
- [README](https://github.com/sbt/sbt-release/blob/main/README.md)
- [Releases](https://github.com/sbt/sbt-release/releases)
- [sbt/sbt-release on GitHub](https://github.com/sbt/sbt-release)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sbt-sbt-release
