Quick start

Three steps. You do not write GitHub Actions YAML.

  1. Add the plugin.

  2. Run sbt zipxWorkflowGenerate and commit the files it writes.

  3. 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.