Capabilities

A capability is something CI should do: run tests, publish a library, build a docker image, deploy.

You already get test, fmt, workflow-check, and advisories (parallel Verify), plus publish. Add a pack when you want Maven Central, GitHub Pages, or AWS (see Packs). You can invent extra stages later (Custom capabilities).

Execution mode and Matrix collapse decide how many GitHub jobs appear for a stage. Stay on Aggregate and you get one job per stage. Graph and Layer are opt-in; ignore them until one job is not enough.

Built-ins

CapabilityDefault modeRunsParticipatesPhaseGate
testAggregate (Once)root zipxTestTaskwhole build (.aggregate)Verifyalways
fmtOncescalafmtCheckAllwhole buildVerifyalways (zipxVerify.fmt)
workflow-checkOncezipxWorkflowCheckwhole buildVerifyalways (zipxVerify.workflowCheck)
advisoriesOncezipxAdvisoryCheckwhole buildVerifyalways (zipxVerify.advisories)
publishAggregate+?<module>/<publishTask> (joined)modules that publishPublishrelease tag
dockerAggregate<module>/Docker/publish (joined)DockerPlugin modulesPublishrelease tag

Verify jobs have empty needs versus each other (GitHub runs them in parallel). Pin-feed OSV folds into advisories when feeds are present. Capability.pinCheck remains if you want a dedicated job; see Pin feeds. Skip a gate with VerifyOpt.Skip(reason) (the job still emits). See Verify.

Use testGraph / publishGraph / dockerGraph for one-job-per-module. Use *Layers for wave scheduling. Use testJoined if Aggregate must join <module>/<testTask> instead of a root task. Packs (ZipxCentral.release, ZipxModver.publish, ZipxGitHubPackages, ZipxDocs.pages, AWS helpers) replace or extend these by name; see Packs, Independent versions, and Docker and deploy.

Phases and replace-by-name

Capabilities run Verify → Publish → Deploy. A capability can depend on another via needsCapabilities.

Path gating reaches Graph capabilities only (zipxAffectedOnPR / zipxAffectedOnPush; fail open): Verify by default, Publish under zipxAffectedPublish, where the release gate and the affected clause compose. Deploy is destination-driven and never path-affected.

Gate today is Always | OnReleaseTag | OnDefaultPush | AffectedOnly. AffectedOnly is rejected at generate time: affected gating is derived from phase, scope and the two settings, not from Gate, so this would be a silent Always. See Affected. OnDefaultPush is the independent-versioning library publish gate (see Independent versions).

zipxCapabilities += ... merges with built-ins; the same name replaces a built-in (e.g. turn Aggregate docker into a multi-registry Graph capability). A custom extraSteps uses: should be a full commit SHA (or an Action catalog row a pack looks up), not a floating @v6 tag; zipx-emitted steps are already SHA-pinned.

zipxCapabilities += Capability.publish.copy(
  env = Map(
    "PGP_PASSPHRASE"    -> secret"PGP_PASSPHRASE",
    "SONATYPE_USERNAME" -> secret"SONATYPE_USERNAME",
  )
)
DocsRender.job("publish")(
  Capability.publish.copy(
    env = Map(
      "PGP_PASSPHRASE"    -> secret"PGP_PASSPHRASE",
      "SONATYPE_USERNAME" -> EnvValue.secret("SONATYPE_USERNAME"),
    )
  )
)
publish:
  name: publish
  runs-on: ubuntu-latest
  if: startsWith(github.ref, 'refs/tags/v')
  env:
    PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
    SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
  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 'schema/publish; api/publish'

Replace vs plus vs drop

withEnv / withExtraSteps / withPostSteps replace the field. Packs already fill extras (ZipxCentral.release ships GPG import). To add a step without restating the pack bundle, use the layer combinators, the same split as plusEnv / andCondition / thenOnce:

ReplaceLayer
withEnvplusEnv
withExtraSteps / withPostStepsplusExtraSteps / plusPostSteps
dropExtraSteps(name) / dropPostSteps(name) (leaf [[Steps]] name, not the composed a+b string)
zipxCapabilities += ZipxCentral.release.plusExtraSteps(publishCleanFull)

zipxCapabilities += ZipxCentral.release
  .dropExtraSteps("gpg-import")
  .plusExtraSteps(customGpg ++ publishCleanFull)
{
  val clean = Steps.of("clean-full")(
    Step.run(Script(Exec("true"))).named("cleanFull").build
  )
  val yaml  = DocsRender.job("publish")(ZipxCentral.release.plusExtraSteps(clean))
  val names =
    ZipxCentral.release.plusExtraSteps(clean).extraSteps match
      case s: Steps => s.leaves.map(_.name).mkString(",")
      case _        => ""
  s"leaves: $names\n---\n$yaml"
}
leaves: gpg-import,clean-full
---
publish:
  name: publish
  runs-on: ubuntu-latest
  if: startsWith(github.ref, 'refs/tags/v')
  env:
    PGP_KEY_HEX: ${{ secrets.PGP_KEY_HEX }}
    PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
    SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
    SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
  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: Import signing key
      run: |
        mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
        echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf
        echo "pinentry-mode loopback"   >> ~/.gnupg/gpg.conf
        gpgconf --kill gpg-agent || true
        echo "$PGP_SECRET" | base64 --decode | gpg --batch --import
      env:
        PGP_SECRET: ${{ secrets.PGP_SECRET }}
    - name: cleanFull
      run: "true"
    - name: publish
      run: sbt 'schema/publishSigned; api/publishSigned; sonaRelease'

Verify knobs

Shared across Aggregate, Layer, and Graph (details on the Verify page):

zipxTestTask    := zipxTasks.of(testFull)
zipxVerifyClean := VerifyClean.CleanFull
// Aggregate → sbt 'cleanFull; testFull'
// Graph     → sbt 'cleanFull; core/testFull' (per job)