Verify
Verify is several Once jobs with no needs between them, so GitHub runs them in parallel: test, fmt,
workflow-check, and advisories. Publish does not wait on them in-workflow (tag runs already do not need test);
PR merge is the required-checks story.
On sbt 2, plain sbt test can skip suites it thinks are unaffected. zipx defaults to testFull, so the suites it runs
really run. On a PR it chooses them from the diff: the builtin test runs zipxTestAffected, which tests each module
the PR can have broken and nothing else (see Affected-only PRs below, and Affected). A push, a dispatch, or a diff
that cannot run tests everything.
zipxVerify := ZipxVerify.Strict // all On; the default
zipxVerify := ZipxVerify.Strict.copy(
fmt = VerifyOpt.Skip("hotfix: scalafmt 3.x busts this branch, revert Monday"),
advisories = VerifyOpt.Skip("hotfix: GHSA-xxxx in checkout, Action bump in follow-up"),
)
Skip(reason) still emits the job: it prints zipx: skipping <gate>: <reason> and exits 0. The check name stays
on the PR. Empty or whitespace reason is a zipx: generate error. Replace-by-name still works for a totally custom
fmt command; Skip is for turning the gate off out loud.
fmt:
scalafmtCheckAll. No sbt-scalafmt means generate fails until you add the plugin or Skip fmt.workflow-check:
zipxWorkflowCheck. Drift fails the PR as its own job.advisories:
zipxAdvisoryCheck. OSV on catalogLibrows, resolved Action pins, and Pin vals when feeds exist. Any finding at or above default min-severity fails the job.
Test task and optional clean
zipxTestTask := zipxTasks.of(testFull) // Aggregate root; Graph/Layer per-module (plugin default)
zipxVerifyClean := VerifyClean.CleanFull // None (default) | Clean | CleanFull
For a one-off cleanFull without making it permanent, leave zipxVerifyClean at None (default) and add the
GitHub PR label clean. Verify then runs cleanFull; <task> only on that PR. cleanFull deletes declared
outputs, target/out, and the disk action cache (~/.cache/sbt/v2 on the runner). It does not delete
~/.cache/coursier or ~/.sbt, and it does not skip the LocalDir restore of those directories. target/ is not
part of that restore.
The version-updates and pin-updates companions always add that label. Override the label name with
zipxVerifyCleanLabel, or set it to None to disable. To skip that restore and save a fresh snapshot, use the
purge label (see Caching).
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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
- 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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test-schema
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'cleanFull; schema/test'{
given PlanConfig = config.copy(verifyCleanLabel = PlanConfig.verifyCleanLabel("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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
- 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') }}Test topology stays in the build
zipx does not assign testTopology, workerMaxInstances, Test / fork, testForkedParallel, or
parallelExecution. testFull reads the values in build.sbt.
sbt's four topologies are TestTopology.Default (the default), ClassParallel, SubprojectExclusive, and
SubprojectSplit(n). Default and ClassParallel ask for a split of 2, Exclusive asks for 1, and SubprojectSplit(n)
asks for n. The split runs only when fork, parallelExecution, and testForkedParallel are all on. Fork left off
is one group, which is what a zipx build does until the build itself turns fork on.
Coverage (and why `test` is the wrong task here)
On sbt 2 plain test is testQuick. It skips tests it deems unaffected and prints No tests to run, which reads
like success. zipx's Verify default is testFull so CI cannot skip suites. Under coverage that skip is a silent
wrong answer: the hand-rolled alias everyone writes,
addCommandAlias("testWithCoverage", "cleanFull; coverage; compile; test; coverageAggregate; coverageReport; coverageOff")
measures whichever tests sbt happened to run, satisfies coverageMinimum on near-zero data, and goes green. So zipx
builds the command instead of taking one. Prefer the companion workflow, which keeps coverage off every PR's
required checks:
zipxCoverageWorkflow := Some(
Coverage.workflow(
CoverageTrigger.Scheduled(Cron.daily(hour = 3)),
CoverageTrigger.Dispatch,
CoverageTrigger.prLabel("coverage"),
)
)
That writes .github/workflows/zipx-coverage.yml, one job running coverage; testFull; coverageAggregate:
Scheduledruns on the default branch, the only branch GitHub runs schedules on.Dispatchis Actions → Run workflow, on any branch.prLabel("coverage")runs on a PR once the label goes on, and on every push while it stays. A PR without the label skips the job, and so does adding some other label to a PR that has it.The job restores the build cache and never saves it, and nothing else runs in that workflow. An instrumented class therefore never reaches
test, an image, or a publish.
The builtin test stays the build cache's owner. The check to require on the PR is verify: it needs every Verify
job, so the ruleset does not grow when one is added. A skipped required check counts as passing, so verify runs
unless the workflow was cancelled and fails itself when a Verify job failed or was cancelled. Generate refuses a
coverage capability named test, which would make every PR wait on an instrumented build, and a coverage capability
that saves the cache. Setting zipxCoverageWorkflow := None (the default) deletes the file on the next generate.
To measure in ci.yml instead, add a capability:
zipxCapabilities += Coverage.once() // coverage; testFull; coverageAggregate (one session)
zipxCapabilities += Coverage.graph() // one job per module, each measuring that module's own zipxTestTask
Coverage.graph() reads each module's zipxTestTask and substitutes testFull where it is still the default test.
A module that set the task itself is left alone, because an explicit choice outranks zipx's. Pass _.testTask for
literal inheritance, default and all:
zipxCapabilities += Coverage.graph(task = _.testTask)
Two things the alias has that the capability deliberately does not. No trailing coverageOff: the sbt session ends
with the job, and that command exists because a developer's shell outlives the command. And no cleanFull: use
zipxVerifyClean or the clean PR label above if you want one, so the choice is in one place.
coverage is a session-wide toggle, which is why enable / test / report are one sbt '…; …; …' invocation rather than
three steps.
Prefer once: coverageAggregate is a root task over every module's measurement data, so splitting it across jobs
means downloading and merging artifacts to get one number back. graph buys affected-gating (it is an ordinary Graph
Verify capability, so everything on the Affected page applies) at the cost of per-module minimums instead of a
build-wide one.
The report is uploaded with the already-pinned actions/upload-artifact, per module under graph so N jobs do not
collide on one artifact name. if-no-files-found: error, on purpose: a run that measured nothing produces no report,
and that should be a red job rather than an empty upload. Turn it off with uploadReport = false on the capabilities.
{
val coverage = Coverage.workflow(
CoverageTrigger.Scheduled(Cron.daily(hour = 3)),
CoverageTrigger.Dispatch,
CoverageTrigger.prLabel("coverage"),
)
CoverageWorkflow.render(coverage, config).yaml
}# Generated by zipx. Do not edit. Run 'sbt zipxWorkflowGenerate' to regenerate.
name: zipx coverage
"on":
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
schedule:
- cron: "0 3 * * *"
workflow_dispatch: null
permissions:
contents: read
concurrency:
group: zipx-coverage-${{ github.ref }}
cancel-in-progress: true
jobs:
coverage:
name: coverage
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'coverage') && (github.event.action != 'labeled' || github.event.label.name == 'coverage'))
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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: coverage
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: coverage
run: sbt 'coverage; testFull; coverageAggregate'
- name: Upload coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-report
path: "**/scoverage-report/**"
if-no-files-found: error
DocsRender.jobs("coverage")(Coverage.once())coverage:
name: coverage
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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: coverage
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: coverage
run: sbt 'coverage; testFull; coverageAggregate'
- name: Upload coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: coverage-report
path: "**/scoverage-report/**"
if-no-files-found: errorDocsRender.job("coverage-schema")(Coverage.graph())coverage-schema:
name: coverage 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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: coverage-schema
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: coverage
run: sbt 'coverage; schema/testFull; schema/coverageReport'
- name: Upload coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: coverage-report-schema
path: "**/scoverage-report/**"
if-no-files-found: errorAffected-only PRs
zipxAffectedOnPR (default true) narrows Verify to what a PR can have broken, in three ways:
The builtin
testrunssbt 'zipxTestAffected <PR base>'. The command diffs against the same base as theaffectedjob, then runs each affected module's ownzipxTestTaskin the same sbt session, as oneall a/testFull b/testFullcommand: a single parallel task graph, as a roottestFullis. It runs only CI-relevant modules the root aggregate reaches, since a roottestFullnever reaches the others and an aggregator's own test task would rerun what it aggregates. No base, an all-zero one, or a diff that fails runs the full command as before, and an empty affected set tests nothing.testtakes noneeds: affected, so it neither waits on that job nor pays a second sbt load. Locally,sbt "zipxTestAffected origin/main"runs the same selection.A Graph Verify capability gets an
affectedsetup job, and each per-module job skips when its module is not affected.A Once job whose inputs the classpath cannot see, such as an integration test over images
Docker/publishLocalbuilds, opts in withwithAffectedBy, which gates it on theaffectedjob:
zipxCapabilities += zipxTasks
.once(name = CapabilityName("image-it"), command = imageIt / testFull, phase = Phase.Verify, gate = Gate.Always)
.withAffectedBy(n => imageModules.contains(n.id))
Layer and other Aggregate capabilities still run their full stage command. That is not the same as "always recompile
everything": Zinc and the cross-run task cache still skip unaffected compile work, even on a cold JVM. See Execution
modes ("Two kinds of affected") and the Affected page for the fail-open handoff and zipxAffectedPublish, which
extends the narrowing to Graph Publish jobs as a separate opt-in.
{
given PlanConfig = config.copy(affected = AffectedMode.AffectedOnPR)
val imageIt = Capability
.once(
name = CapabilityName("image-it"),
command = SbtCommand.unsafeTask("imageIt/testFull"),
phase = Phase.Verify,
gate = Gate.Always,
)
.withAffectedBy(_.id == "service")
DocsRender.jobs("test")(Capability.testAffected(onPush = false)) + "\n---\n" +
DocsRender.body(Capability.testGraph) + "\n---\n" + DocsRender.body(imageIt)
}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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'zipxTestAffected ${{ github.event.pull_request.base.sha }}'
---
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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: affected
node-version: ""
sbt-disk-cache: "false"
cache-mode: "off"
cache-epoch: "0.1.0-SNAPSHOT"
- 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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test-schema
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- 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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test-api
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- 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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test-service
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'service/test'
verify:
name: verify
runs-on: ubuntu-latest
needs:
- test-api
- test-schema
- test-service
if: "!cancelled()"
steps:
- name: Fail when a Verify job failed or was cancelled
if: (needs.test-api.result == 'failure' || needs.test-api.result == 'cancelled') || (needs.test-schema.result == 'failure' || needs.test-schema.result == 'cancelled') || (needs.test-service.result == 'failure' || needs.test-service.result == 'cancelled')
run: exit 1
---
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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: affected
node-version: ""
sbt-disk-cache: "false"
cache-mode: "off"
cache-epoch: "0.1.0-SNAPSHOT"
- 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"
image-it:
name: image-it
runs-on: ubuntu-latest
needs:
- affected
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')))
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: image-it
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: image-it
run: sbt 'imageIt/testFull'
verify:
name: verify
runs-on: ubuntu-latest
needs:
- image-it
if: "!cancelled()"
steps:
- name: Fail when a Verify job failed or was cancelled
if: (needs.image-it.result == 'failure' || needs.image-it.result == 'cancelled')
run: exit 1Changed 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, which diffs a
push against the head of this workflow's last successful push run on the branch. A cancelled or unapproved run ships
nothing, so diffing against the previous push would drop what it changed. That lookup calls gh. The runner
image has the client. The sbt image bakes it from Pin("sbt-image", "gh", "<x.y.z>") (see Caching, "Sbt image").
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). Graph
Publish and Deploy still run after a merge; the affected job they read stays up and emits ["all"] on that
non-PR push, so they do not fromJson an empty output.
verify-gate looks up the landed SHA on GET /commits/{sha}/pulls. That index can lag a few seconds after a
squash (new SHA at merge). The gate retries with 1/2/4/8/16s sleeps rather than treating an empty first answer as a
direct push and running Verify again.
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 Test/compile (override with zipxCacheRehydrateTask), so the next PR restores the action cache from
that compile, including test outputs. It is the merge push's only LocalDir save (see Caching, "Who saves"). No
full test, no verifyClean. Set zipxCacheRehydrateOnMerge := false to opt out; remote backends never emit it.
The snapshot does not keep files under target/. A browser install has to run in the job until the env image bakes
it (see Caching, "Sbt image"). 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:
val browserSetup = Steps.built("browsers")(
Step.run(Script(Exec("npm", Word.lit("ci")))).named("Install browsers")
)
zipxSkipMergedPrPush := true // default
zipxCacheRehydrateOnMerge := true // default; LocalDir only
zipxCacheRehydrateTask := zipxTasks.of(Test / compile) // default
zipxEnv := Map(
"PLAYWRIGHT_BROWSERS_PATH" -> EnvValue.typed(Expr.github("workspace") ++ Expr.lit("/target/ms-playwright")),
)
zipxCacheRehydrateExtraSteps := browserSetup // after cache restore, before Test/compile
EnvValue.typed takes any Expr, so ++ concatenates a context reference with literal text instead of spelling the
${{ … }} out. EnvValue.expr still accepts a raw string, and zipxWorkflowGenerate warns when one is used.
browserSetup is a Steps bundle, not a lambda: it carries a name, composes with ++, gates with .when(...),
and reports escape-hatch use so zipxWorkflowGenerate can warn and name it. The step body is a typed Script, so
nothing here is a hand-written shell string. See Shell and steps for the whole DSL.
{
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.
# The commit-to-PR index can lag after squash; retry rather than fail-open into a second Verify.
prs=0
for attempt in 1 2 3 4 5 6; do
if [ "$prs" -eq 0 ]; then
if [ "$attempt" -eq 2 ]; then
sleep 1
elif [ "$attempt" -eq 3 ]; then
sleep 2
elif [ "$attempt" -eq 4 ]; then
sleep 4
elif [ "$attempt" -eq 5 ]; then
sleep 8
elif [ "$attempt" -eq 6 ]; then
sleep 16
fi
prs=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" \
--jq "[.[] | select(.merged_at != null and .base.ref == \"${{ github.ref_name }}\")] | length")
fi
done
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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: cache-rehydrate
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
- name: cache-rehydrate
run: sbt 'Test/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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'test'{
given PlanConfig = config.copy(
skipMergedPrPush = true,
env = Map(
"PLAYWRIGHT_BROWSERS_PATH" ->
EnvValue.typed(Expr.github("workspace") ++ Expr.lit("/target/ms-playwright"))
),
cacheRehydrateExtraSteps = Steps.built("browsers")(
Step.run(Script(Exec("npm", Word.lit("ci")))).named("Install browsers")
),
)
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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: cache-rehydrate
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
- name: Install browsers
run: npm ci
- name: cache-rehydrate
run: sbt 'Test/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: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'test'