Affected
Affected answers a GitHub Actions question Graph mode can ask: which jobs should run for this PR's diff? That is different from sbt 2 incrementality inside Aggregate (see Execution modes). Today only Graph Verify jobs are path-gated. Publish and Deploy stay release-gated; Deploy is never affected-gated.
Closure flow
From git diff to owning module
The affected job takes the PR's changed paths (repo-root-relative, from git diff against the base
ref) and maps each file to at most one module:
Build files force everything. If any path ends in
.sbtor sits under aproject/directory (root or nested), the whole module set is affected. Plugins and the graph may have changed, so nothing is safe to skip.Otherwise: longest
baseDirprefix. Each module'sbaseDiris a path prefix. A file is owned by the matching module whosebaseDiris longest (most specific). Matching is directory-aware:core/ownscore/src/X.scala, but notcore-lib/…orcore-extra/…. Nested bases win:mods/inner/X.scalabelongs tomods/inner, notmods.Unowned paths seed nothing.
README.md,.github/…, and other files outside every modulebaseDirare ignored (unless step 1 applies). Aggregators with emptybaseDirnever own files.
Those owning modules are the seeds. Step 3 of the chart expands them to the reverse-dependency
closure; step 5 gates each Graph Verify job on whether its id (or all) appears in the published JSON.
| Changed path | Owning module | After closure (example) |
|---|---|---|
client/src/… | client (leaf) | just client |
models/src/… | models | models + every transitive dependent |
mods/inner/X.scala | inner (longer than mods) | inner + dependents |
README.md | none | empty (no Graph Verify) |
build.sbt / project/plugins.sbt | (build file) | all modules |
zipxAffectedOnPR := true // default; emits `affected` only when Graph Verify is present
zipxAffectedOnPush := false // opt-in: also scope branch pushes via before-sha
{
given PlanConfig = config.copy(affected = AffectedMode.AffectedOnPR)
DocsRender.body(Capability.testGraph)
}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'Fail open, not closed
The affected handoff must never turn a broken git diff into a green, untested PR. Two outcomes used to look the same
([]): “diff succeeded and found nothing” versus “diff could not run.” Empty JSON makes every
contains(..., '<id>') false, so every Graph Verify job skips.
| Diff outcome | Value | Emitted | CI result |
|---|---|---|---|
| Succeeded, no changes | Some(Nil) | [] | Skip Graph Verify (deliberate) |
| Could not run (bad ref, no git, …) | None | ["all"] | Run everything |
| Succeeded with files | Some(files) | affected closure | Gate per module |
A broken base ref costs runner minutes, not coverage. The affected job logs a warning when it disables gating for
that run.
Who is gated today
| Capability shape | Path-affected? | Why |
|---|---|---|
Capability.testGraph (and other Graph + Verify) | Yes | Per-module jobs can skip |
| Aggregate / Layer Verify | No | One (or few) jobs; sbt cache skips work inside |
| Publish / docker | No (yet) | Gate.OnReleaseTag only; Publish affected is an open seam |
| Deploy | Never | Environments and approvals are destination-driven |
Gate.AffectedOnly is a design seam, not a shipped gate. Affected-gating is derived from phase + scope +
zipxAffectedOnPR, not from Gate. The planner rejects Gate.AffectedOnly at generate time so it cannot
silently mean Always.
Proving more affected value
Existing machinery already path-gates any phase = Verify + scope = Graph capability. The next proof is not a
new Gate: put expensive Verify stages on Graph (scripted, MiMa, PR-local docker builds) and measure leaf-PR skips.
Partial monorepo publish on a tag is the headline Wave 2 win; until Gate can compose with release, keep Publish on
OnReleaseTag alone.
Concurrency (cancel superseded runs)
Superseded PR pushes should not burn runners. zipx emits workflow-level concurrency by default:
concurrency:
group: CI-${{ github.ref }}
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}
A half-cancelled Central publish can leave a staged-but-unreleased bundle; that is worse than a wasted runner. Opt
out with zipxCancelSupersededRuns := false.
DocsRender.body(Capability.test)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'{
given PlanConfig = config.copy(cancelSupersededRuns = false)
DocsRender.body(Capability.test)
}name: CI
"on":
push:
branches:
- main
pull_request: null
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'