Verify

Verify is the test/build phase. Aggregate defaults to a root Once job; Layer and Graph use per-module (or per-wave) commands. Two settings configure the command for all modes. Path-based job skipping lives on the Affected page (Graph only, fail-open handoff, concurrency).

Test task and optional clean

zipxTestTask    := "testFull"            // Aggregate root; Graph/Layer per-module task
zipxVerifyClean := VerifyClean.CleanFull // None (default) | Clean | CleanFull

For a one-off LocalDir / action-cache bust without making clean permanent, leave zipxVerifyClean at None (default) and add the GitHub PR label clean. Verify then runs cleanFull; <task> only on that PR. Override the label name with zipxVerifyCleanLabel, or set it to None to disable.

zipxVerifyCleanLabel := Some("clean")  // default
{
  given PlanConfig = config.copy(verifyClean = VerifyClean.CleanFull)
  DocsRender.jobs("test")(Capability.test) + "\n---\n" +
    DocsRender.job("test-schema")(Capability.testGraph)
}
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 'cleanFull; 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 'cleanFull; schema/test'
{
  given PlanConfig = config.copy(verifyCleanLabel = Some("clean"))
  DocsRender.jobs("test")(Capability.test)
}
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: |
        if [ "$ZIPX_VERIFY_CLEAN_FULL" = "true" ]; then
          sbt 'cleanFull; test'
        else
          sbt 'test'
        fi
      env:
        ZIPX_VERIFY_CLEAN_FULL: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'clean') }}

Affected-only PRs (Graph only)

zipxAffectedOnPR (default true) emits an affected setup job only when a Graph Verify capability is present. Aggregate and Layer always invoke their full stage command (they do not skip GitHub jobs). That is not the same as "always recompile and retest everything": sbt 2's incremental test and cross-run task cache (restored by zipx at the epoch, or via remote cache) still skip unaffected work, even on a cold JVM. See Execution modes ("Two kinds of affected") and the Affected page for the fail-open handoff and who is gated today.

zipxAffectedOnPR := true   // default with Graph Verify
or Graph
{
  given PlanConfig = config.copy(affected = AffectedMode.AffectedOnPR)
  DocsRender.body(Capability.test) + "\n---\n" + 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:
  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'
---
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'

Changed files → owning module (longest base-dir prefix) → reverse-dependency closure. A .sbt change or anything under project/ forces a full build. On push/tag everything builds unless zipxAffectedOnPush is enabled. If the diff cannot run, zipx emits ["all"] (fail open) so a bad base ref never reports a green, untested PR.

Skip Verify after merge / on tags

By default (zipxSkipMergedPrPush := true), a push to main that lands a merged PR does not re-run Verify. Direct pushes still Verify. Tag pushes never run Verify (release tags only need Publish / Deploy).

With LocalDir, that skip would otherwise leave main without an actions/cache save (PR caches are branch-scoped; later PRs only warm from the default branch). So by default zipx also emits a minimal cache-rehydrate job that runs only when verify-gate skips Verify: same checkout / JDK / LocalDir cache path, then compile (override with zipxCacheRehydrateTask). No full test, no verifyClean. Set zipxCacheRehydrateOnMerge := false to opt out; remote backends never emit it.

To also warm non-sbt blobs that live under target/ (e.g. Playwright browsers), opt into rehydrate-only extraSteps. Prefer build-wide zipxEnv for vars needed on Verify and rehydrate; use zipxCacheRehydrateEnv only for merge-only overlays. Neither is copied from Verify capability extraSteps / env; assign the same setup function when you want step parity:

zipxSkipMergedPrPush := true  // default
zipxCacheRehydrateOnMerge := true  // default; LocalDir only
zipxCacheRehydrateTask := "compile"  // default
zipxEnv := Map(
  "PLAYWRIGHT_BROWSERS_PATH" -> EnvValue.expr("${{ github.workspace }}/target/ms-playwright"),
)
zipxCacheRehydrateExtraSteps := browserSetup  // after cache restore, before compile
{
  given PlanConfig = config.copy(skipMergedPrPush = true)
  DocsRender.jobs("verify-gate", "cache-rehydrate", "test")(Capability.test)
}
verify-gate:
  name: verify-gate
  runs-on: ubuntu-latest
  if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')
  permissions:
    contents: read
    pull-requests: read
  outputs:
    run: ${{ steps.check.outputs.run }}
  steps:
    - name: Skip Verify after merged PR push
      id: check
      run: |
        # Commits landed by merging/squashing a PR are associated with that PR via the API.
        prs=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" \
          --jq "[.[] | select(.merged_at != null and .base.ref == \"${{ github.ref_name }}\")] | length")
        if [ "$prs" -gt 0 ]; then
          echo "Merged PR push — skipping redundant Verify (already ran on the PR)"
          echo "run=false" >> "$GITHUB_OUTPUT"
        else
          echo "run=true" >> "$GITHUB_OUTPUT"
        fi
      env:
        GH_TOKEN: ${{ github.token }}
cache-rehydrate:
  name: cache-rehydrate
  runs-on: ubuntu-latest
  needs:
    - verify-gate
  if: needs.verify-gate.result == 'success' && needs.verify-gate.outputs.run == 'false'
  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 }}-cache-rehydrate
        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: cache-rehydrate
      run: sbt 'compile'
test:
  name: test
  runs-on: ubuntu-latest
  needs:
    - verify-gate
  if: "!cancelled() && !startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch' && ((needs.verify-gate.result != 'success') || (needs.verify-gate.outputs.run == 'true'))"
  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(
    skipMergedPrPush = true,
    env = Map(
      "PLAYWRIGHT_BROWSERS_PATH" -> EnvValue.expr("${{ github.workspace }}/target/ms-playwright")
    ),
    cacheRehydrateExtraSteps = _ => List(Step(name = Some("Install browsers"), run = Some("npm ci"))),
  )
  DocsRender.jobs("cache-rehydrate", "test")(Capability.test)
}
cache-rehydrate:
  name: cache-rehydrate
  runs-on: ubuntu-latest
  needs:
    - verify-gate
  if: needs.verify-gate.result == 'success' && needs.verify-gate.outputs.run == 'false'
  env:
    PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/target/ms-playwright
  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 }}-cache-rehydrate
        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: Install browsers
      run: npm ci
    - name: cache-rehydrate
      run: sbt 'compile'
test:
  name: test
  runs-on: ubuntu-latest
  needs:
    - verify-gate
  if: "!cancelled() && !startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch' && ((needs.verify-gate.result != 'success') || (needs.verify-gate.outputs.run == 'true'))"
  env:
    PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/target/ms-playwright
  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'