Quick start
Three steps. You do not write GitHub Actions YAML.
Add the plugin.
Run
sbt zipxWorkflowGenerateand commit the files it writes.Open a pull request. GitHub runs the workflow.
Defaults: test, fmt, workflow-check, and advisories in parallel on every PR and push, and a publish
job when you push a version tag (v*). On a PR, test runs only the suites of modules the PR can have broken. That is
enough for most libraries. A monorepo uses the same loop; you still do not list modules in YAML, and CI for a busy
monorepo covers what to turn on as it grows.
Install
// project/plugins.sbt
addSbtPlugin("rocks.earlyeffect" % "sbt-zipx" % "<version>")
Generate and commit:
sbt zipxWorkflowGenerate
git add .github/workflows/ci.yml .github/actions/
git commit -m "ci: generate with zipx"
You do not need a pin file or a github-actions Dependabot ecosystem.
sbt zipxGraph and sbt zipxPublishOrder print what zipx saw, if you want to inspect.
Defaults
Defaults are Aggregate: parallel Verify jobs (test, fmt, workflow-check, advisories) and one publish job
(plus docker when any module enables DockerPlugin). You write no module lists and no job-order YAML. For a typical
library that is the whole CI.
// project/plugins.sbt
addSbtPlugin("rocks.earlyeffect" % "sbt-zipx" % "<version>")
// build.sbt
lazy val lib = project.settings(/* publish settings */)
lazy val root = (project in file("."))
.aggregate(lib)
.settings(
// nothing required for Aggregate test + fmt + workflow-check + advisories + publish
// optional paved Central path:
zipxCapabilities += ZipxCentral.release,
zipxJavaVersion := JdkVersion("25"),
)
{
val g = GraphFixture(List(ModuleNode(ModuleId("lib"), publishes = true, crossScalaVersions = List("3.9.0"))))
DocsRender.jobs("test", "fmt", "workflow-check", "advisories", "publish")(
Capability.testAffected(onPush = false),
Capability.once(Capability.FmtName, SbtCommand.unsafeCommand("scalafmtCheckAll")),
Capability.once(Capability.WorkflowCheckName, SbtCommand.unsafeTask("zipxWorkflowCheck")),
Capability.once(Capability.AdvisoriesName, SbtCommand.unsafeTask("zipxAdvisoryCheck")),
Capability.publish,
)(using g)
}test:
name: test
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'zipxTestAffected ${{ github.event.pull_request.base.sha }}'
fmt:
name: fmt
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: fmt
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: fmt
run: sbt 'scalafmtCheckAll'
workflow-check:
name: workflow-check
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: workflow-check
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: workflow-check
run: sbt 'zipxWorkflowCheck'
advisories:
name: advisories
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: advisories
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: advisories
run: sbt 'zipxAdvisoryCheck'
publish:
name: publish
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: publish
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: publish
run: sbt 'lib/publish'Versions catalog
Library, plugin, pin, and Action versions live in one object you write under project/ and extend from
ZipxVersions. Drop MyVersions.settings at the top of build.sbt. Every Lib / Plugin / Pin / Action val is
a catalog row; you do not list them again. Each module picks a group. Full guide: Versions.
A multi-artifact repo that publishes libraries on different cadences adds outbound Ship / ShipGroup rows. Every
build compiles <row>-ci; a GitHub Release or a dispatch of zipx-release.yml releases rows; image and deploy still
wait on a human v* tag. That loop, the graphs, and the fail-closed gate are Independent versions.
sbt zipxSnapshotPublish local proves a change in a sibling repo before any of that; see Snapshots and releases.
// project/ZipxVersions.scala
import zipx.*
object MyVersions extends ZipxVersions:
val sbt: SbtVersion = SbtVersion("2.1.0-RC1")
val scala: ScalaVersion = ScalaVersion("3.9.0")
val zio = Lib("dev.zio", "zio", "2.1.26")
val slf4j = Lib("org.slf4j", "slf4j-simple", "2.0.18").java
def libraries = library(zio)
def service = library(zio, slf4j)
// build.sbt
MyVersions.settings
lazy val lib = project.settings(MyVersions.libraries)
lazy val service = project.settings(MyVersions.service)
The trait is the extension point: another plugin that sits on zipx adds members. Extra settings belong in build.sbt
(MyVersions.settings ++ …). Plugin authors: Extending Versions. When a row is stale, the scheduled companion opens
zipx/version-updates-$GITHUB_RUN_ID, or you
run zipxDepUpdate locally. See Dependency updates.
Bare settings
zipx reads these settings from the root project, so write them without a ThisBuild / prefix. That is an sbt 2
habit, not a zipx quirk.
A bare zipxTestTask := zipxTasks.of(testFull) is the plugin default. On sbt 2, plain sbt test can skip suites; CI
uses testFull so the suites it chooses really run. On a PR it chooses by the diff: test runs each changed module's
test task and those of its dependents (see Affected). Any module can override the task in its own
.settings(...).
zipxJavaVersion := JdkVersion("25")
zipxTestTask := zipxTasks.of(testFull) // plugin default
zipxWorkflowDispatch := true
Self-checking
zipxWorkflowGenerate writes .github/workflows/ci.yml and .github/actions/zipx-*/action.yml. Commit them.
zipxWorkflowCheck regenerates and diffs against those committed files. Run the check in CI (zipx already puts it in
the workflow) so a forgotten regenerate fails the PR. Generation is deterministic: same build, same YAML.
Action pins (optional, skip at first)
zipx already pins GitHub Actions to exact commits in the generated workflow. Jar defaults are fine. Add Action vals
to ZipxVersions and run zipxActionUpdate when you want to bump those Actions without waiting for a zipx release.
See Action pins.
CDN / checksum pins are Pin feeds. Library and plugin versions are the catalog; bump locally with zipxDepUpdate
and open a PR. See Dependency updates.