Changeset-based versioning for Scala multi-module builds (sbt plugin + GitHub Action)
sbt-changesets provides a better (and easier) way to manage libraries. Specially useful for company-internal monorepos, bringing changeset-based versioning to Scala multi-module builds. Developers describe their changes in small markdown files, and the plugin takes care of version bumps, dependency cascade, and changelog generation.
sbt-changesetsis an SBT-opinionated take on changesets.
Looking for the GitHub Action? Jump to GitHub Actions.
Add the plugin to your project/plugins.sbt:
addSbtPlugin("com.alejandrohdezma" % "sbt-changesets" % "0.1.0")This plugin depends on sbt-modules, which is pulled in automatically. It expects modules to be defined using module instead of project in your build.sbt, with source code living under modules/<module-name>/. See the sbt-modules documentation for details.
Each module in your build has a VERSION file and a CHANGELOG.md. Instead of bumping versions manually, developers create small .changeset/*.md files describing their changes. When it's time to release, the plugin reads those files, bumps versions (cascading through the dependency graph), updates changelogs, and cleans up.
All available commands
| Command | Description |
|---|---|
changesetAdd <bump> <description> |
Create a changeset for changed modules |
changesetStatus |
Validate all changed modules have changeset entries |
changesetAffected |
Validate + output affected modules as JSON |
changesetVersion |
Apply version bumps with cascade through dependency graph |
changesetRelease |
Output release info for modules with .publish markers |
generatePublishMarkers |
Create .publish markers for changed VERSION files |
publishSnapshot |
Publish snapshots for changed modules |
changesetConfig |
Output module dependency graph as JSON |
After making changes, run:
sbt> changesetAdd minor add-retry-logic
This detects which modules you changed (via git diff) and creates a file like .changeset/add-retry-logic.md:
---
"my-module": minor
"other-module": minor
---
TODO: Describe your changes hereThe first argument is the bump type (patch, minor, or major) and the rest becomes the filename. Edit the file to replace the placeholder with a meaningful description — this will end up in the changelog.
On pull requests, run changesetStatus to ensure every modified module has at least one changeset entry. It fails if any module is missing coverage or if a description still contains the placeholder text.
For CI matrix builds, changesetAffected does the same validation and writes target/changeset/affected.json — a JSON array of affected module names (including transitive dependents) that you can feed into a CI like GitHub Actions matrix.
On feature branches, publishSnapshot detects changed modules and their transitive dependents, creates .publish markers, and publishes snapshot artifacts. It writes target/changeset/snapshot-coordinates.json with the published Maven coordinates.
When changesets are merged to main branch, run changesetVersion. This:
- Parses all
.changeset/*.mdfiles. - Cascades bumps through the dependency graph following early-semver rules.
- Updates each module's
VERSIONfile. - Prepends new entries to each module's
CHANGELOG.md. - Removes processed changeset files.
- Writes
target/changeset/version-summary.jsonwith old/new versions.
Modules that are only bumped through cascade get auto-generated descriptions listing which dependencies changed.
After version bumps are committed:
generatePublishMarkersdetects which modules had theirVERSIONfile changed in the last commit and creates.publishmarker files for them.+publishpublishes only the modules with.publishmarkers.changesetReleasewritestarget/changeset/release.jsonwith tag names and changelog bodies for creating releases.
This repository also provides a composite GitHub Action that orchestrates the full CI workflow. Reference it as alejandrohdezma/[email protected] and choose a mode depending on the context.
Validates that all changed modules have changeset entries and outputs the list of affected modules (including transitive dependents). Use this on pull requests to gate CI and build a dynamic test matrix.
# .github/workflows/ci.yaml
on:
pull_request:
jobs:
detect:
runs-on: ubuntu-latest
outputs:
affected: ${{ steps.changesets.outputs.affected }}
steps:
- uses: actions/checkout@@v4
- id: changesets
uses: alejandrohdezma/[email protected]
with:
mode: detect
error-help-url: https://your-repo/docs/versioning # shown on validation failure
test:
needs: detect
if: needs.detect.outputs.affected != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
module: ${{ fromJson(needs.detect.outputs.affected) }}
steps:
- uses: actions/checkout@@v4
- run: sbt "+${{ matrix.module }}/test"Publishes snapshot artifacts for changed modules and posts a PR comment with the Maven coordinates. Use this on pull requests after tests pass so reviewers can try the changes.
snapshot:
needs: [detect, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@@v4
- uses: alejandrohdezma/[email protected]
with:
mode: snapshotNote: This is intended for private monorepos only. Exposing publishing credentials on pull requests in public repositories is a security risk.
Handles the main branch workflow. It checks for pending changeset files and takes one of two paths:
- Changesets present: runs
changesetVersion, commits the version bumps, and creates (or updates) a "Version Packages" PR on thechangeset-release/mainbranch. Outputsresult=version-prandbranch=changeset-release/main. - No changesets (i.e. a version PR was just merged): publishes artifacts, creates GitHub releases with changelog bodies. Outputs
result=published.
# .github/workflows/release.yaml
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@@v4
- id: changesets
uses: alejandrohdezma/[email protected]
with:
mode: release
# Optional: run repo-specific steps when a version PR is created
- if: steps.changesets.outputs.result == 'version-pr'
run: sbt mdoc| Input | Required | Default | Description |
|---|---|---|---|
mode |
yes | — | detect, snapshot, or release |
pr-number |
no | github.event.pull_request.number |
PR number for snapshot comments |
github-token |
no | github.token |
GitHub token for API operations |
error-help-url |
no | — | URL shown on changeset validation failure |
| Output | Modes | Description |
|---|---|---|
affected |
detect |
JSON array of affected module names |
result |
release |
version-pr, published, or no-changes |
branch |
release |
Release branch name (when result is version-pr) |