Job conditions
[[JobCondition]] is a typed AST for optional job if: filters (fork repo, PR label, branch, repo var, …).
[[Gate]] is still the timeline (Always vs OnReleaseTag). The planner ANDs Gate clauses with capability and
target conditions.
Default on every capability and target: condition = None (no extra filter). Prefer withCondition(...) to set a
filter, or andCondition(...) to layer onto a pack that already ships one (e.g. ZipxDocs.pages).
Compose with && and ||
val deployDocs =
JobCondition.onReleaseTag || JobCondition.onWorkflowDispatch
val upstreamOnly =
JobCondition.repositoryIs("acme/libs") && deployDocs
// Negation:
val notFork = !JobCondition.repositoryIs("acme/other")
JobCondition.and / or / not remain available; infix && / || / ! are the usual style. Precedence matches
Boolean ops: && binds tighter than || (a || b && c ≡ a || (b && c)); both are left-associative. Parenthesize
when you mean (a || b) && c. Typed leaves also include eventIs, onWorkflowDispatch, and onReleaseTag.
{
val c = (JobCondition.onReleaseTag || JobCondition.onWorkflowDispatch) &&
JobCondition.repositoryIs("early-effect/zipx")
Render.renderMapping(ListMap("if" -> c.render))
}if: ((startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch')) && (github.repository == 'early-effect/zipx')Defaults and Gate vs condition
| Capability | Default Gate | Default JobCondition |
|---|---|---|
| test / testJoined / Layers / Graph | Always | None |
| publish / docker / deploy | OnReleaseTag | None |
| ZipxCentral / ZipxGitHubPackages | OnReleaseTag | None (unless you pass one) |
| ZipxDocs.pages | Always | onReleaseTag or onWorkflowDispatch |
Important: Gate and JobCondition are ANDed. A capability with Gate.OnReleaseTag will not run on a PR even if
a Target has HasPrLabel. For stage-on-PR + prod-on-tag, use Gate.Always with per-Target conditions, or two
capabilities.
// Footgun: OnReleaseTag ∧ HasPrLabel still requires a v* tag
Capability.dockerGraph.copy(
gate = Gate.OnReleaseTag,
targets = _ => List(Target("stg", condition = Some(JobCondition.hasPrLabel("deploy-stg")))),
)
DocsRender.job("docker-service-stg")(
Capability.dockerGraph.copy(
gate = Gate.OnReleaseTag,
targets = _ => List(Target("stg", condition = Some(JobCondition.hasPrLabel("deploy-stg")))),
)
)(using dockerLibGraph)docker-service-stg:
name: docker service (stg)
runs-on: ubuntu-latest
if: (startsWith(github.ref, 'refs/tags/v')) && (contains(github.event.pull_request.labels.*.name, 'deploy-stg'))
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 }}-docker-service-stg
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: docker
run: sbt 'service/Docker/publish'Fork / upstream publish gate
zipxCapabilities += Capability.publish.withCondition(
JobCondition.repositoryIs("acme/my-fork"),
)
DocsRender.job("publish")(
Capability.publish.withCondition(JobCondition.repositoryIs("acme/my-fork"))
)publish:
name: publish
runs-on: ubuntu-latest
if: (startsWith(github.ref, 'refs/tags/v')) && (github.repository == 'acme/my-fork')
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'Repo-variable opt-in
Mechanoid-style: only publish when a repo variable is set.
zipxCapabilities += ZipxGitHubPackages.sameRepo(
condition = Some(JobCondition.varNonEmpty("PUBLISH_PACKAGES_REPO")),
)
DocsRender.job("github-packages")(
ZipxGitHubPackages.sameRepo(condition = Some(JobCondition.varNonEmpty("PUBLISH_PACKAGES_REPO")))
)github-packages:
name: github-packages
runs-on: ubuntu-latest
if: (startsWith(github.ref, 'refs/tags/v')) && (vars.PUBLISH_PACKAGES_REPO != '')
permissions:
contents: read
packages: write
env:
GITHUB_TOKEN: ${{ github.token }}
PUBLISH_GITHUB_PACKAGES: "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 }}-github-packages
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: github-packages
run: sbt 'schema/publish; api/publish'Multi-publish: Central + GitHub Packages
Distinct capability names coexist. zipx wires permissions + token env; sbt owns publishTo / Credentials when
PUBLISH_GITHUB_PACKAGES=true.
zipxCapabilities ++= Seq(
ZipxCentral.release,
ZipxGitHubPackages.sameRepo(repository = Some("acme/my-fork")),
)
DocsRender.jobs("publish", "github-packages")(
ZipxCentral.release,
ZipxGitHubPackages.sameRepo(repository = Some("acme/fork")),
)publish:
name: publish
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
env:
PGP_KEY_HEX: ${{ secrets.PGP_KEY_HEX }}
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
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: Import signing key
run: |
mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf
echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf
gpgconf --kill gpg-agent || true
echo "$PGP_SECRET" | base64 --decode | gpg --batch --import
env:
PGP_SECRET: ${{ secrets.PGP_SECRET }}
- name: publish
run: sbt 'publishSigned; sonaRelease'
github-packages:
name: github-packages
runs-on: ubuntu-latest
if: (startsWith(github.ref, 'refs/tags/v')) && (github.repository == 'acme/fork')
permissions:
contents: read
packages: write
env:
GITHUB_TOKEN: ${{ github.token }}
PUBLISH_GITHUB_PACKAGES: "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 }}-github-packages
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: github-packages
run: sbt 'schema/publish; api/publish'PR → stage/dev ECR before merge
Publish container images to stg/dev ECR from a labeled PR without waiting for merge or a release tag.
Ensure
pull_requesttriggers fire (zipx default). If labels are added after open, also allowtypes: [opened, synchronize, reopened, labeled](zipx does not auto-emit that yet — set triggers in a companion workflow or extend PlanConfig later).Use a custom docker capability with
Gate.Alwaysand per-Target conditions.Point Target env at the ECR registry + OIDC role; keep
Docker/publishas the command (native-packager /REGISTRYstill choose the repository URL).
zipxCapabilities += Capability
.custom(
name = "docker",
command = cmd"${Docker / publish}",
participates = _.docker,
phase = Phase.Publish,
gate = Gate.Always,
targets = _ => List(
Target(
name = "stg",
env = Map(
"REGISTRY" -> EnvValue.plain("111.dkr.ecr.us-east-1.amazonaws.com/stg"),
"DEPLOY_ROLE" -> secret"STG_REGISTRY_ROLE",
),
condition = Some(JobCondition.hasPrLabel("deploy-stg")),
),
Target(
name = "prod",
env = Map(
"REGISTRY" -> EnvValue.plain("111.dkr.ecr.us-east-1.amazonaws.com/prod"),
"DEPLOY_ROLE" -> secret"PROD_REGISTRY_ROLE",
),
condition = Some(JobCondition.refStartsWith("refs/tags/v")),
),
),
permissions = Map("id-token" -> "write", "contents" -> "read"),
)
.copy(
extraSteps = _ => List(
Step(
name = Some("Login to registry"),
uses = Some("aws-actions/configure-aws-credentials@v6"),
`with` = Map("role-to-assume" -> "${{ env.DEPLOY_ROLE }}"),
)
)
)
Add label deploy-stg on the PR → only the stg job's if is true; prod still waits for a v* tag.
{
val cap = Capability
.custom(
name = "docker",
command = n => s"${n.id}/Docker/publish",
participates = _.docker,
gate = Gate.Always,
targets = _ =>
List(
Target("stg", condition = Some(JobCondition.hasPrLabel("deploy-stg"))),
Target("prod", condition = Some(JobCondition.refStartsWith("refs/tags/v"))),
),
permissions = Map("id-token" -> "write"),
)
DocsRender.jobs("docker-service-stg", "docker-service-prod")(cap)(using dockerLibGraph)
}docker-service-stg:
name: docker service (stg)
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'deploy-stg')
permissions:
id-token: write
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 }}-docker-service-stg
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: docker
run: sbt 'service/Docker/publish'
docker-service-prod:
name: docker service (prod)
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
id-token: write
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 }}-docker-service-prod
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: docker
run: sbt 'service/Docker/publish'Capability-level docker-stg
Alternate to per-Target conditions: a separate capability name so it does not replace builtin docker.
zipxCapabilities += Capability.dockerGraph
.copy(name = "docker-stg", gate = Gate.Always)
.withCondition(JobCondition.hasPrLabel("deploy-stg"))
DocsRender.job("docker-stg-service")(
Capability.dockerGraph
.copy(name = "docker-stg", gate = Gate.Always)
.withCondition(JobCondition.hasPrLabel("deploy-stg"))
)(using dockerLibGraph)docker-stg-service:
name: docker-stg service
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'deploy-stg')
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 }}-docker-stg-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: docker-stg
run: sbt 'service/Docker/publish'Main-only target
Target(
"prod",
environment = Some("production"),
condition = Some(JobCondition.refIs("refs/heads/main")),
)
DocsRender.job("deploy-prod")(
Capability.deploy(
participates = _.id == "service",
command = n => s"${n.id}/promote",
targets = _ =>
List(
Target(
"prod",
environment = Some("production"),
condition = Some(JobCondition.refIs("refs/heads/main")),
)
),
needsCapabilities = Nil,
gate = Gate.Always,
)
)deploy-prod:
name: deploy (prod)
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
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 }}-deploy-prod
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: deploy
run: sbt 'service/promote'Raw escape hatch
JobCondition.raw("always()")
Prefer typed leaves and && / || when possible; Raw is for expressions the AST does not cover yet.
Render.renderMapping(ListMap("if" -> JobCondition.raw("always()").render))if: always()