Why zipx

If your team has lived through hand-maintained CI matrices, a second BUILD graph, or a cache product that made tasks faster while humans still juggled two sources of truth: you are in the right place.

zipx is not “almost Bazel,” and it is not another acceleration appliance bolted onto an opaque build. It is a gentler path home: one sbt graph becomes generated GitHub Actions, with sbt 2’s content-addressed cache along for the ride. Speed follows. The headline is ergonomics: fewer things to read, sync, and apologize for in review.

When you are ready for the migration stories, see From Bazel, Caching, and Remote cache for teams. Live Put/Get is proven by RemoteCacheItSpec (same RemoteCacheProof pins as the YAML examples here).

Three paths, one that heals

Most teams we meet are somewhere on this triangle. None of the first two are foolish; they were reasonable responses to real pain. zipx is the recovery path that keeps your Scala mental model intact.

oror
ApproachSource of truthWhat drifts (the bruise)
Disconnected CIbuild.sbt and hand YAMLModule lists, needs, publish order
Bazel second graphBUILD (+ often CI)Edges restated outside sbt
zipxbuild.sbt / .dependsOnCI is derived; zipxWorkflowCheck catches drift early

You do not need a second graph to feel safe. You need one honest graph, and a check that fails when CI lies.

Faster tasks are not the same as kinder CI

Acceleration layers (Develocity-class remote build cache, build scans, predictive test selection) can be wonderful at making an existing build feel snappier. They earn their keep. They are also a different category of tool: they rarely remove the second maintenance surface of hand YAML or restated edges.

Acceleration layerzipx
Primary artifactAgent/plugin + server + scan UIPlanner + generated workflow from the sbt graph
What you maintainBuild + CI lists + cache config (often independent)Modules + typed zipxCapabilities; CI is derived
How you know you’re safeFaster greens / scan insightszipxWorkflowCheck + docs-as-tests + live cache IT
ScopeSpeed / observability of tasksCI topology + cache wiring + packs as one system

If mornings still start with “did we update the workflow?”, caching alone will not heal that. zipx retires disconnected CI (and skips restating the graph in BUILD), then leans on sbt 2’s cache so Aggregate stays light to live with.

What you open on a good day

The hard years: a hand ci.yml module matrix, or BUILD files plus CI glue, plus cache product config. Every “add a module” meant a scavenger hunt.

The recovery: build.sbt and a small typed capability list. Generated .github/workflows/ci.yml is an output you commit and drift-gate. Reviewers (and future you) see intent, not archaeology.

Default Aggregate Verify is one calm job (test), not one job per module. Reach for Graph when the workflow needs isolation, not when you are only trying to make caching feel less lonely.

{
  val aggregate = DocsRender.body(Capability.test, Capability.publish)
  val graph     = DocsRender.body(Capability.testGraph)
  aggregate + "\n---\n" + graph
}
name: CI
"on":
  push:
    branches:
      - main
    tags:
      - v[0-9]+.[0-9]+.[0-9]+
  pull_request: null
concurrency:
  group: CI-${{ github.ref }}
  cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
jobs:
  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: Setup JDK 21
        uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
        with:
          distribution: temurin
          java-version: "21"
      - uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
        with:
          disk-cache: "false"
      - name: Cache sbt
        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
        with:
          path: |
            ~/.sbt
            ~/.cache/sbt
            ~/.cache/coursier
            target
          key: ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-test
          restore-keys: |
            ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-
            ubuntu-latest-jdk21-sbt-0.1.0-ci-
            ubuntu-latest-jdk21-sbt-0.1.0-
            ubuntu-latest-jdk21-sbt-
      - name: test
        run: sbt 'test'
  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: Setup JDK 21
        uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
        with:
          distribution: temurin
          java-version: "21"
      - uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
        with:
          disk-cache: "false"
      - name: Cache sbt
        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
        with:
          path: |
            ~/.sbt
            ~/.cache/sbt
            ~/.cache/coursier
            target
          key: ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-publish
          restore-keys: |
            ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-
            ubuntu-latest-jdk21-sbt-0.1.0-ci-
            ubuntu-latest-jdk21-sbt-0.1.0-
            ubuntu-latest-jdk21-sbt-
      - name: publish
        run: sbt 'schema/publish; api/publish'
---
name: CI
"on":
  push:
    branches:
      - main
  pull_request: null
concurrency:
  group: CI-${{ github.ref }}
  cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
jobs:
  affected:
    name: affected
    runs-on: ubuntu-latest
    if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
    outputs:
      modules: ${{ steps.compute.outputs.modules }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
        with:
          fetch-depth: "0"
          fetch-tags: "true"
      - name: Setup JDK 21
        uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
        with:
          distribution: temurin
          java-version: "21"
      - uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
        with:
          disk-cache: "false"
      - name: Compute affected modules
        id: compute
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            BASE="${{ github.event.pull_request.base.sha }}"
            sbt -batch --error "zipxAffectedModules $BASE"
            modules=$(cat target/zipx-affected.json)
          else
            modules='["all"]'
          fi
          echo "modules=$modules" >> "$GITHUB_OUTPUT"
  test-schema:
    name: test schema
    runs-on: ubuntu-latest
    needs:
      - affected
    if: (!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch') && (!cancelled() && (contains(fromJson(needs.affected.outputs.modules), 'schema') || contains(fromJson(needs.affected.outputs.modules), 'all')))
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
        with:
          fetch-depth: "0"
          fetch-tags: "true"
      - name: Setup JDK 21
        uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
        with:
          distribution: temurin
          java-version: "21"
      - uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
        with:
          disk-cache: "false"
      - name: Cache sbt
        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
        with:
          path: |
            ~/.sbt
            ~/.cache/sbt
            ~/.cache/coursier
            target
          key: ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-test-schema
          restore-keys: |
            ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-
            ubuntu-latest-jdk21-sbt-0.1.0-ci-
            ubuntu-latest-jdk21-sbt-0.1.0-
            ubuntu-latest-jdk21-sbt-
      - name: test
        run: sbt 'schema/test'
  test-api:
    name: test api
    runs-on: ubuntu-latest
    needs:
      - affected
      - test-schema
    if: (!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch') && (!cancelled() && (contains(fromJson(needs.affected.outputs.modules), 'api') || contains(fromJson(needs.affected.outputs.modules), 'all')) && needs.test-schema.result != 'failure')
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
        with:
          fetch-depth: "0"
          fetch-tags: "true"
      - name: Setup JDK 21
        uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
        with:
          distribution: temurin
          java-version: "21"
      - uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
        with:
          disk-cache: "false"
      - name: Cache sbt
        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
        with:
          path: |
            ~/.sbt
            ~/.cache/sbt
            ~/.cache/coursier
            target
          key: ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-test-api
          restore-keys: |
            ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-
            ubuntu-latest-jdk21-sbt-0.1.0-ci-
            ubuntu-latest-jdk21-sbt-0.1.0-
            ubuntu-latest-jdk21-sbt-
      - name: test
        run: sbt 'api/test'
  test-service:
    name: test service
    runs-on: ubuntu-latest
    needs:
      - affected
      - test-api
    if: (!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch') && (!cancelled() && (contains(fromJson(needs.affected.outputs.modules), 'service') || contains(fromJson(needs.affected.outputs.modules), 'all')) && needs.test-api.result != 'failure')
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
        with:
          fetch-depth: "0"
          fetch-tags: "true"
      - name: Setup JDK 21
        uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
        with:
          distribution: temurin
          java-version: "21"
      - uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
        with:
          disk-cache: "false"
      - name: Cache sbt
        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
        with:
          path: |
            ~/.sbt
            ~/.cache/sbt
            ~/.cache/coursier
            target
          key: ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-test-service
          restore-keys: |
            ubuntu-latest-jdk21-sbt-0.1.0-ci-${{ github.run_id }}-
            ubuntu-latest-jdk21-sbt-0.1.0-ci-
            ubuntu-latest-jdk21-sbt-0.1.0-
            ubuntu-latest-jdk21-sbt-
      - name: test
        run: sbt 'service/test'

Kind to humans and AI teammates

A self-documenting, single-graph build is easier to nurture, whether the reviewer is a person or an agent:

  • One place to edit when adding a module (no “also update workflow / BUILD”).

  • A contract that cares: zipxWorkflowCheck + Specular DocSpecs fail when examples drift from planner output.

  • Narrow diffs: capability and graph changes are typed Scala; generated YAML is regeneratable when you want a clean re-diff.

  • Named paved paths: packs like ZipxCentral and ZipxDocs say what you meant, instead of a paste of secret/step soup.

The everyday loop: edit build.sbtzipxWorkflowGenerate → the PR shows the graph and a regeneratable workflow. That is the experience we are trying to give you back.

Cache that travels with the topology

Remote backends are not a side confessional. The same planner that emits jobs also emits services and env. Shared proof pins:

DocsRender.job("test")(Capability.test)(using
  libGraph,
  config.copy(cache = RemoteCacheProof.sidecar),
)
test:
  name: test
  runs-on: ubuntu-latest
  if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
  services:
    bazel-remote:
      image: buchgr/bazel-remote-cache:v2.6.1
      ports:
        - "9092:9092"
      options: --max_size=1
  env:
    ZIPX_REMOTE_CACHE: grpc://localhost:9092
  steps:
    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
      with:
        fetch-depth: "0"
        fetch-tags: "true"
    - name: Setup JDK 21
      uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
      with:
        distribution: temurin
        java-version: "21"
    - uses: sbt/setup-sbt@d059c39de700f4cc5cb64f9f56577315e44a984e
    - name: test
      run: sbt 'test'