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*). 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.