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*). That is enough for most libraries. A monorepo uses the same loop; you still do
not list modules in YAML.
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.8.4"))))
DocsRender.jobs("test", "fmt", "workflow-check", "advisories", "publish")(
Capability.test,
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"
local-cache: "true"
cache-epoch: "0.1.0-ci"
- name: test
run: sbt 'test'
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"
local-cache: "true"
cache-epoch: "0.1.0-ci"
- 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"
local-cache: "true"
cache-epoch: "0.1.0-ci"
- 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"
local-cache: "true"
cache-epoch: "0.1.0-ci"
- 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"
local-cache: "true"
cache-epoch: "0.1.0-ci"
- 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. Merge to
main is the release signal; image and deploy still wait on a human v* tag. That loop, the graphs, and the fail-closed
gate are Independent versions.
// project/ZipxVersions.scala
import zipx.*
object MyVersions extends ZipxVersions:
val sbt: SbtVersion = SbtVersion("2.1.0-M1")
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 (sbt 2.0)
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 every suite actually runs. 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.