name: "π΅ CI: Orchestrator"
on:
workflow_call:
inputs:
distros:
description: "Distro pool the deploy matrix spreads its rows over. Empty: every declared distro, which is what a sweep wants. The rows rotate over the pool, so narrowing it only makes sense to chase one distribution."
required: false
type: string
default: ""
whitelist:
description: "Optional space-separated whitelist of app ids"
required: false
type: string
default: ""
lifecycles:
description: "Lifecycle envelope for test-deploy discovery (comma/space-separated)."
required: false
type: string
default: ""
mode:
description: "Deploy modes rows may be drawn from: 'auto' rotates every row over the modes its role offers; a subset of 'host compose swarm' keeps only the rows those modes can take."
required: false
type: string
default: "auto"
priority:
description: "Optional space-separated app ids for the priority line: sorted to the head of the list and cut into chunks of their own, so every priority row is deployed before the first regular one starts. Priority rows skip the sweep rotation and run every combination they can take (each variant, each offered mode, Tor and clearnet)."
required: false
type: string
default: ""
sweep:
description: "Sweep number driving the mode and tor rotation. Empty: the run number."
required: false
type: string
default: ""
offset:
description: "Regular rows to skip before filling the chunks. 0 starts at the head of the ranking; raise it to reach rows the budget leaves behind."
required: false
type: string
default: "0"
chunk_size:
description: "π¦ Rows one chunk holds. Empty or 0 derives it from the runner slots a chunk may assume and the waves the queue window affords. Typed string for the same reason as resume_from_chunk."
required: false
type: string
default: "0"
resume_from_chunk:
description: "βοΈ Skip every chunk below this index. Use to continue a sweep after fixing what broke it, instead of re-running the green chunks. Typed string, not number: a workflow_dispatch number input reaches the caller as a string, and handing that to a number-typed workflow_call input makes GitHub reject the job before it is created."
required: false
type: string
default: "0"
chunk_gate:
description: "π Stop the chunk chain at its first failed chunk. Off: keep deploying the remaining chunks."
required: false
type: boolean
default: true
filesystem:
description: "ποΈ Filesystem pool the deploy matrix spreads its rows over (ext4, btrfs, zfs). Empty: all three."
required: false
type: string
default: ""
image_wait_attempts:
description: "Polling attempts for waiting on prebuilt or mirrored images"
required: false
type: string
default: "1980"
image_wait_sleep_seconds:
description: "Polling interval in seconds for waiting on prebuilt or mirrored images"
required: false
type: string
default: "10"
workspace:
description: "Run the workspace test (dev-runtime image matrix). 'auto' runs it on a global scope only, 'true'/'false' force it."
required: false
type: string
default: "auto"
workspace_track:
description: "Which workspace track each distro runs: 'rotate' splits them across compose and swarm and flips with the run number, 'compose'/'swarm' pin every distro."
required: false
type: string
default: "rotate"
tor:
description: "Tor axis of the deploy matrix: 'auto' rotates the onion over the tor-capable rows, 'enforced' onions all of them, 'exclusive' also drops the rows that cannot, 'disabled' onions none."
required: false
type: string
default: "auto"
vpn:
description: "WireGuard axis of the deploy matrix: 'auto' rotates the mesh over the swarm rows, 'enforced' meshes all of them, 'disabled' meshes none."
required: false
type: string
default: "auto"
meshes:
description: "WireGuard layout a meshed swarm row deploys, e.g. an inventories/examples/*/wireguard.yml. Empty: INFINITO_SWARM_MESHES from default.env."
required: false
type: string
default: ""
trusted_pr:
description: "True when the PR carries the maintainer 'π‘οΈ Trusted' label; skips the fork-prereq wait."
required: false
type: boolean
default: false
permissions:
actions: read
contents: read
packages: read
jobs:
validate-selection:
name: π― Selection
uses: ./.github/workflows/call-validation.yml
with:
whitelist: ${{ inputs.whitelist }}
priority: ${{ inputs.priority }}
mode: ${{ inputs.mode }}
tor: ${{ inputs.tor }}
distros: ${{ inputs.distros }}
filesystem: ${{ inputs.filesystem }}
lifecycles: ${{ inputs.lifecycles }}
fork_prereqs: true
trusted_pr: ${{ inputs.trusted_pr }}
image_wait_attempts: ${{ inputs.image_wait_attempts }}
image_wait_sleep_seconds: ${{ inputs.image_wait_sleep_seconds }}
secrets: inherit
security-codeql:
needs: validate-selection
permissions:
contents: read
packages: read
security-events: write
uses: ./.github/workflows/cron-security-codeql.yml
lint:
needs: validate-selection
uses: ./.github/workflows/call-lint.yml
test:
needs: validate-selection
uses: ./.github/workflows/call-test.yml
build-ci-images:
needs: validate-selection
permissions:
contents: read
packages: write
uses: ./.github/workflows/call-images-build-ci.yml
with:
distros: "arch debian ubuntu fedora centos"
concurrency_channel: ci-orchestrator-build-consumer
secrets: inherit
test-dns:
needs:
- build-ci-images
- images-mirror-missing
- validate-selection
uses: ./.github/workflows/call-test-dns.yml
with:
distros: "arch debian ubuntu fedora centos"
images-mirror-missing:
needs: validate-selection
permissions:
contents: read
packages: write
uses: ./.github/workflows/call-images-mirror-missing.yml
with:
concurrency_channel: ci-orchestrator-mirror-consumer
image_wait_attempts: ${{ inputs.image_wait_attempts }}
image_wait_sleep_seconds: ${{ inputs.image_wait_sleep_seconds }}
secrets: inherit
test-deploy-chunk-0:
name: π§± Chunk 0
if: >-
${{ !cancelled()
&& inputs.resume_from_chunk <= 0
&& needs.lint.result == 'success'
&& needs.test.result == 'success'
&& needs.build-ci-images.result == 'success'
&& needs.images-mirror-missing.result == 'success' }}
needs:
- lint
- test
- build-ci-images
- images-mirror-missing
- validate-selection
uses: ./.github/workflows/call-test-deploy.yml
with:
distros: ${{ inputs.distros }}
index: "0"
sweep: ${{ inputs.sweep != '' && inputs.sweep || github.run_number }}
offset: ${{ inputs.offset }}
chunk_size: ${{ inputs.chunk_size }}
whitelist: ${{ inputs.whitelist }}
priority: ${{ inputs.priority }}
lifecycles: ${{ inputs.lifecycles }}
modes: ${{ inputs.mode }}
filesystem: ${{ inputs.filesystem }}
tor: ${{ inputs.tor }}
vpn: ${{ inputs.vpn }}
meshes: ${{ inputs.meshes }}
test-deploy-chunk-1:
name: π§± Chunk 1
if: >-
${{ !cancelled()
&& inputs.resume_from_chunk <= 1
&& needs.lint.result == 'success'
&& needs.test.result == 'success'
&& needs.build-ci-images.result == 'success'
&& needs.images-mirror-missing.result == 'success'
&& (!inputs.chunk_gate
|| contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-0.result)) }}
needs:
- lint
- test
- build-ci-images
- images-mirror-missing
- validate-selection
- test-deploy-chunk-0
uses: ./.github/workflows/call-test-deploy.yml
with:
distros: ${{ inputs.distros }}
index: "1"
sweep: ${{ inputs.sweep != '' && inputs.sweep || github.run_number }}
offset: ${{ inputs.offset }}
chunk_size: ${{ inputs.chunk_size }}
whitelist: ${{ inputs.whitelist }}
priority: ${{ inputs.priority }}
lifecycles: ${{ inputs.lifecycles }}
modes: ${{ inputs.mode }}
filesystem: ${{ inputs.filesystem }}
tor: ${{ inputs.tor }}
vpn: ${{ inputs.vpn }}
meshes: ${{ inputs.meshes }}
test-deploy-chunk-2:
name: π§± Chunk 2
if: >-
${{ !cancelled()
&& inputs.resume_from_chunk <= 2
&& needs.lint.result == 'success'
&& needs.test.result == 'success'
&& needs.build-ci-images.result == 'success'
&& needs.images-mirror-missing.result == 'success'
&& (!inputs.chunk_gate
|| (contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-0.result)
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-1.result))) }}
needs:
- lint
- test
- build-ci-images
- images-mirror-missing
- validate-selection
- test-deploy-chunk-0
- test-deploy-chunk-1
uses: ./.github/workflows/call-test-deploy.yml
with:
distros: ${{ inputs.distros }}
index: "2"
sweep: ${{ inputs.sweep != '' && inputs.sweep || github.run_number }}
offset: ${{ inputs.offset }}
chunk_size: ${{ inputs.chunk_size }}
whitelist: ${{ inputs.whitelist }}
priority: ${{ inputs.priority }}
lifecycles: ${{ inputs.lifecycles }}
modes: ${{ inputs.mode }}
filesystem: ${{ inputs.filesystem }}
tor: ${{ inputs.tor }}
vpn: ${{ inputs.vpn }}
meshes: ${{ inputs.meshes }}
test-deploy-chunk-3:
name: π§± Chunk 3
if: >-
${{ !cancelled()
&& inputs.resume_from_chunk <= 3
&& needs.lint.result == 'success'
&& needs.test.result == 'success'
&& needs.build-ci-images.result == 'success'
&& needs.images-mirror-missing.result == 'success'
&& (!inputs.chunk_gate
|| (contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-0.result)
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-1.result)
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-2.result))) }}
needs:
- lint
- test
- build-ci-images
- images-mirror-missing
- validate-selection
- test-deploy-chunk-0
- test-deploy-chunk-1
- test-deploy-chunk-2
uses: ./.github/workflows/call-test-deploy.yml
with:
distros: ${{ inputs.distros }}
index: "3"
sweep: ${{ inputs.sweep != '' && inputs.sweep || github.run_number }}
offset: ${{ inputs.offset }}
chunk_size: ${{ inputs.chunk_size }}
whitelist: ${{ inputs.whitelist }}
priority: ${{ inputs.priority }}
lifecycles: ${{ inputs.lifecycles }}
modes: ${{ inputs.mode }}
filesystem: ${{ inputs.filesystem }}
tor: ${{ inputs.tor }}
vpn: ${{ inputs.vpn }}
meshes: ${{ inputs.meshes }}
test-runner-smoke:
needs:
- test-deploy-chunk-0
if: ${{ !cancelled() && vars.CI_SELF_HOSTED_RUNNER_COUNT != '0' && vars.CI_SELF_HOSTED_RUNNER_COUNT != '' && (needs.test-deploy-chunk-0.result == 'success' || needs.test-deploy-chunk-0.result == 'failure') }}
uses: ./.github/workflows/call-test-runner-smoke.yml
with:
ref: ${{ github.sha }}
secrets: inherit
test-install-make:
needs: validate-selection
uses: ./.github/workflows/call-test-install-make.yml
test-install-pkgmgr:
needs: validate-selection
uses: ./.github/workflows/call-test-install-pkgmgr.yml
test-workspace:
if: ${{ inputs.workspace == 'true' || (inputs.workspace == 'auto' && (inputs.whitelist == '' || inputs.whitelist == '__ALL__')) }}
uses: ./.github/workflows/call-test-workspace.yml
with:
workspace_track: ${{ inputs.workspace_track }}
needs:
- images-mirror-missing
- lint
- test
secrets: inherit
report-main-failures:
name: π Report main role failures
needs:
- test-deploy-chunk-0
- test-deploy-chunk-1
- test-deploy-chunk-2
- test-deploy-chunk-3
if: ${{ always() && github.ref == 'refs/heads/main' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
runs-on: ubuntu-26.04
permissions:
issues: write
actions: read
contents: read
steps:
- uses: actions/checkout@v7.0.0
- name: π Open or append issues for failed roles
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 -m cli.meta.ci.report_failures --run-id "${{ github.run_id }}" --repo "${{ github.repository }}"
done:
name: π Finish pipeline
needs:
- validate-selection
- build-ci-images
- images-mirror-missing
- test-dns
- test-install-make
- test-install-pkgmgr
- test-deploy-chunk-0
- test-deploy-chunk-1
- test-deploy-chunk-2
- test-deploy-chunk-3
- test-runner-smoke
- test-workspace
- test
- lint
- security-codeql
if: ${{ always() }}
runs-on: ubuntu-26.04
steps:
- name: π Fail if any upstream job failed or was cancelled
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: |
echo "Pipeline failed: at least one upstream job failed or was cancelled." >&2
exit 1
- name: β
Done
run: echo "All checks passed."