evolution-gaming / sbt-kagi-plugin   0.1.0

MIT License GitHub

Simple SBT dependency lock plugin

Scala versions: 3.x
sbt plugins: 2.x

sbt-kagi-plugin

Simple SBT dependency lock plugin.

Build Status Maven Central Version

SBT version support:

Supported major SBT version Built against concrete SBT Version Build JDK version
1 1.4.5 17
2 2.0.0-RC12 17

If your SBT version is earlier than the build SBT version, the plugin might still work, but the support is not guaranteed.

  1. Why
  2. Getting Started
  3. How it Works
  4. Recipes
  5. For Contributors
  6. Reference

Why

The primary use case for the plugin:

  • In a multi-module project for some modules, which produce important artifacts (e.g. Docker images for services), we want to track a full set of dependencies, including transitive ones.
  • For those modules, we want to make dependency changes visible and easily reviewable in PRs / MRs.

What's different compared to stringbean/sbt-dependency-lock:

  • sbt-kagi-plugin is enabled explicitly per-module, not by default for all.
  • For simplicity only a single classpath (e.g. one cross Scala version + Test, Compile, Runtime) per module supported.
  • Concise lock file format, geared towards readability in change reviews. For instance, it uses one line per dependency, full coordinates are repeated for each dependency line.
  • Simplified file format also means JAR file checksums are not supported.
  • By default, sbt-kagi-plugin stores all the lock files in a single directory named dependency-lock - for easier bulk inspection and comparison between modules.

Dependency lock file format example:

# Dependency lock file generated by sbt-kagi-plugin
# Run "sbt kagiDependencyLockWrite" to update
# DO NOT EDIT MANUALLY!

# ch.qos.logback
ch.qos.logback:logback-classic:1.5.21
ch.qos.logback:logback-core:1.5.21

# co.fs2
co.fs2:fs2-core_2.13:3.12.2
co.fs2:fs2-io_2.13:3.12.2

# com.chuusai
com.chuusai:shapeless_2.13:2.3.13

# com.fasterxml.jackson.core
com.fasterxml.jackson.core:jackson-annotations:2.14.3
com.fasterxml.jackson.core:jackson-core:2.14.3
com.fasterxml.jackson.core:jackson-databind:2.14.3

# com.fasterxml.jackson.datatype
com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.3
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.3

Getting Started

1. Installation

To get started, add the following line to your project/plugins.sbt:

addSbtPlugin("com.evolution" % "sbt-kagi-plugin" % "LATEST_VERSION")

Replace LATEST_VERSION with the latest version of the plugin, which you can find on Maven Central.

2. Enable the Plugin

Enable KagiPlugin on the modules you want to lock dependencies for:

lazy val myProject = project
  .in(file("."))
  .enablePlugins(KagiPlugin)
  .settings(
    libraryDependencies += "com.google.guava" % "guava" % "33.4.0-jre",
  )

3. Write a Lock File

Generate lock files for your project by running:

sbt kagiDependencyLockWrite

This creates a dependency-lock/<module-name>.lock.txt file for each module with the plugin enabled. The lock file records all resolved external dependencies and their versions.

4. Check for Lock File Violations

To verify that current dependencies match the lock file, run:

sbt kagiDependencyLockCheck

If the dependencies have drifted from the lock file, the build will fail with a detailed diff:

[error] dependency lock file not in sync - 2 changes: /path/to/dependency-lock/root.lock.txt
[error] 	+ com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
[error] 	* com.google.guava:guava:33.3.0-jre -> 33.4.0-jre

5. Commit Lock Files

Lock files are plain text and VCS-friendly. Commit them to your repository so that dependency changes are visible in pull requests and code reviews.

How it Works

The sbt-kagi-plugin works by resolving the external dependency classpath for each module (by default, the Runtime configuration) and recording it in a human-readable lock file.

The lock file format groups dependencies by their group ID:

# Dependency lock file generated by sbt-kagi-plugin
# Run "sbt kagiDependencyLockWrite" to update
# DO NOT EDIT MANUALLY!

# com.google.guava
com.google.guava:failureaccess:1.0.2
com.google.guava:guava:33.4.0-jre
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava

When kagiDependencyLockCheck runs, it resolves the current classpath and compares it to the lock file. Any additions, removals, or version changes are reported as errors.

Recipes

How to Lock Against Compile Classpath Instead of Runtime

By default, the plugin locks the Runtime classpath. To lock only the Compile classpath (excluding runtime-only dependencies), override the kagiDependencyLockClasspath setting:

lazy val myProject = project
  .in(file("."))
  .enablePlugins(KagiPlugin)
  .settings(
    kagiDependencyLockClasspath := Compile,
  )

How to Customize the Lock File Directory

By default, lock files are stored in dependency-lock/ at the build root. To change this:

ThisBuild / kagiDependencyLockDir := (ThisBuild / baseDirectory).value / "locks"

How to Integrate with CI

Add kagiDependencyLockCheck to your CI pipeline to catch unintended dependency changes:

# GitHub Actions example
- name: Check dependency lock
  run: sbt kagiDependencyLockCheck

How to Use in a Multi-Module Project

Enable KagiPlugin on each module you want to lock. Each module gets its own lock file named after the module:

lazy val core = project
  .in(file("core"))
  .enablePlugins(KagiPlugin)
  .settings(/* ... */)

lazy val app = project
  .in(file("app"))
  .enablePlugins(KagiPlugin)
  .dependsOn(core)
  .settings(/* ... */)

This produces dependency-lock/core.lock.txt and dependency-lock/app.lock.txt.

For Contributors

Build Requirements

JDK 17 is required to build the project, due to sbt 2 having 17 as the minimal JDK version.

Useful SBT Commands

To reformat code using scalafmt, run:

sbt fmt

Code formatting is verified in build commands, and PRs with malformed code will not be accepted.

A fast build without scripted tests can be run with:

sbt buildFast

A full build with scripted tests can be run with:

sbt buildFull

Compatibility

DO NOT BREAK BINARY AND SOURCE COMPATIBILITY!

Binary compatibility is checked on every build automatically, but for the source compatibility you need to watch for it yourself.

How to Make a Release

  • Being on the up-to-date main branch, create a release tag:
git tag v1.2.3
  • Push the tag:
git push origin v1.2.3
  • This will trigger the release GitHub Action. If it succeeds, the release ends up on Maven Central with GitHub release notes generated automatically from PRs info.
  • If the release GitHub Action fails, the tag will be deleted on remote. After deleting the tag locally, fix the main branch and do the process again:
git tag -d v1.2.3

Reference

Settings

  • kagiDependencyLockDir: SettingKey[File]

    Directory where lock files are stored. Defaults to dependency-lock/ at the build root.

  • kagiDependencyLockFile: SettingKey[File]

    Path to the lock file for the current module. Defaults to <kagiDependencyLockDir>/<module-name>.lock.txt.

  • kagiDependencyLockClasspath: SettingKey[Configuration]

    Configuration used to resolve the dependency classpath for lock file generation. Defaults to Runtime.

Tasks

  • kagiDependencyLockWrite: TaskKey[Unit]

    Resolve the dependency classpath and write (or overwrite) the lock file for the current module.

  • kagiDependencyLockCheck: TaskKey[Unit]

    Resolve the dependency classpath and compare it to the existing lock file. Fails the build if they differ.