From Bazel
If you adopted Bazel because sbt CI felt unsafe or slow, that impulse was understandable. Many teams found peace in
hermeticity talk and remote cache, then discovered a quieter cost: a second graph in BUILD files while Scala
engineers still thought in modules and dependsOn.
zipx is not Bazel-parity, and it does not ask you to pretend the second graph never happened. It offers a different strategy for teams that already have the truth in sbt: one graph, generated CI, content-addressed reuse. Keep the vocabulary you learned; leave the duplicate edges behind.
For the broader recovery story (disconnected CI and acceleration layers), see Why zipx. For shared digests across laptops, see Remote cache for teams.
Shared vocabulary, kinder boundaries
We speak Bazel-fluent on purpose. The mapping helps you translate without re-litigating the past:
| Bazel | sbt / zipx |
|---|---|
| Action | Task (executable step with digests) |
| Target | Project / module |
| Remote cache | sbt 2 action cache over Bazel-compat gRPC |
| Remote execution | Out of scope; use Graph for more runners |
In Bazel, many small packages often improve hit rates because the target is the cache boundary. In sbt 2, compile and test already invalidate at class and suite digests inside a module. You do not need to shatter the repo into packages just to feel cacheable. Reach for zipx Graph when you need job isolation or path-affected PRs, not when you are only chasing hits.
What you maintain (before / after)
What hurt: BUILD files restating edges, plus CI glue, plus optional remote cache or RBE config. Adding a library meant updating more than one world.
What heals: modules in build.sbt, typed zipxCapabilities, regenerate the workflow. One graph again.
| Maintenance surface | Disconnected / Bazel-shaped | zipx Aggregate |
|---|---|---|
| Add a library module | Edit BUILD (and usually CI) | Edit build.sbt; regenerate |
| Verify shape | Often N jobs or a matrix you list | One test job |
| Cache | Separate product or RBE | Same planner (RemoteCacheProof / ManagedRemote) |
{
val agg = DocsRender.body(Capability.test)(using libGraph, config)
val pub = DocsRender.job("publish")(Capability.publish)(using libGraph, config)
agg + "\n---\n" + pub
}name: CI
"on":
push:
branches:
- main
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'A gentle migration path
You do not have to boil the ocean on day one:
Add
sbt-zipx; keep Aggregate defaults (one calm Verify job).zipxWorkflowGenerate/zipxWorkflowCheckso drift cannot sneak back in.Opt into
ManagedRemoteorRemoteCacheProof.sidecarwhen the team is ready; measure hits (RemoteCacheItSpecis the in-repo proof).Use Graph only when wall clock or isolation truly needs per-module jobs, matrices, or multi-env deploy.
That is the escape hatch as a ladder: safety first, fan-out when earned.
{
val sidecar = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cache = RemoteCacheProof.sidecar),
)
val graph = DocsRender.jobs("test-schema", "test-api", "test-service")(Capability.testGraph)
sidecar + "\n---\n" + graph
}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'
---
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'Easier to review, easier to help
One edit locus (build.sbt), checkable contracts (zipxWorkflowCheck, Specular docs-as-tests), regeneratable YAML,
and named packs give humans and AI assistants the same gift: they can change the build without keeping a second
graph honest. When the graph is the CI, nobody has to babysit a matrix that forgot a module.