name: "π Test: Deploy"
on:
workflow_call:
inputs:
distros:
description: "π§ Space-separated distro pool the matrix spreads its rows over. Empty: every declared distro."
required: false
type: string
default: ""
index:
description: "Chunk index this call deploys; chunks run serially, one block per index."
required: true
type: string
sweep:
description: "Sweep number driving the mode and tor rotation and the regular-line offset."
required: true
type: string
offset:
description: "Regular rows to skip before filling the chunks; 0 starts at the head of the ranking."
required: false
type: string
default: "0"
chunk_size:
description: "Rows one chunk holds; 0 derives it from the runner slots and the queue window."
required: false
type: string
default: "0"
whitelist:
description: "Optional space-separated whitelist of app ids"
required: false
type: string
default: ""
priority:
description: "Optional space-separated app ids for the priority line: sorted to the head of the list, cut into chunks of their own, and deployed in every combination they can take."
required: false
type: string
default: ""
lifecycles:
description: "Lifecycle envelope for discovery (comma/space-separated)."
required: false
type: string
default: ""
modes:
description: "Deploy modes rows may be drawn from: 'auto' for every mode, or a subset of 'host compose swarm'."
required: false
type: string
default: "auto"
filesystem:
description: "ποΈ Space-separated filesystem pool the matrix spreads its rows over (ext4, btrfs, zfs). Empty: all three."
required: false
type: string
default: ""
marker:
description: "Optional suffix appended to the discover job name (e.g. ' β' for a priority chunk)"
required: false
type: string
default: ""
tor:
required: false
type: string
default: auto
vpn:
required: false
type: string
default: auto
meshes:
description: "WireGuard layout a meshed swarm row deploys. Empty: INFINITO_SWARM_MESHES from default.env."
required: false
type: string
default: ""
disable:
description: "Comma-separated provider keys removed from the test inventory, on top of the matrix row's own tokens."
required: false
type: string
default: ""
workflow_dispatch:
inputs:
distros:
description: "π§ Distro pool the matrix spreads its rows over. Empty: every declared distro"
required: false
type: string
default: ""
index:
description: "π¦ Chunk index to deploy."
required: false
type: string
default: "0"
sweep:
description: "π Sweep number driving the mode/tor rotation and the regular-line offset."
required: false
type: string
default: "0"
whitelist:
description: "π― Roles to deploy. Empty: diff vs origin/main. '__ALL__': every role."
required: false
type: string
default: ""
priority:
description: "β Roles to deploy first, in every combination they can take."
required: false
type: string
default: ""
lifecycles:
description: "β»οΈ Lifecycle envelope for discovery (comma/space-separated)."
required: false
type: string
default: ""
modes:
description: "π Deploy modes to draw from: 'auto' or a subset of 'host compose swarm'"
required: false
type: string
default: "auto"
filesystem:
description: "ποΈ Filesystem pool the matrix spreads its rows over. Empty: ext4, btrfs, zfs"
required: false
type: string
default: ""
tor:
description: "π§
Tor axis; see .github/workflows/README.md for what each value does"
required: false
type: choice
default: auto
options: [auto, enforced, exclusive, disabled]
vpn:
description: "π WireGuard axis; see .github/workflows/README.md for what each value does"
required: false
type: choice
default: auto
options: [auto, enforced, disabled]
meshes:
description: "πΈοΈ WireGuard layout meshed swarm rows deploy. Empty: the development lab's"
required: false
type: string
default: ""
disable:
description: "π« Provider keys removed on top of the row's own (comma-separated)"
required: false
type: string
default: ""
permissions:
contents: read
packages: read
env:
INFINITO_DISTROS: ${{ inputs.distros }}
NIX_CONFIG: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
INFINITO_IMAGE_TAG: ci-${{ github.sha }}
INFINITO_GHCR_MIRROR_PREFIX: mirror
INFINITO_MAIL_PRODUCTION_HOST: ${{ vars.INFINITO_MAIL_PRODUCTION_HOST }}
INFINITO_MAIL_PRODUCTION_RESOLVER: ${{ vars.INFINITO_MAIL_PRODUCTION_RESOLVER }}
INFINITO_PLAYWRIGHT_KEEP: ${{ vars.PLAYWRIGHT_KEEP_ALL }}
INFINITO_API_OPENAI_KEY: ${{ secrets.API_OPENAI_KEY || vars.API_OPENAI_KEY }}
INFINITO_API_ANTHROPIC_KEY: ${{ secrets.API_ANTHROPIC_KEY || vars.API_ANTHROPIC_KEY }}
INFINITO_API_OPENROUTER_KEY: ${{ secrets.API_OPENROUTER_KEY || vars.API_OPENROUTER_KEY }}
INFINITO_APP_DISCOVERY_RUNNER: host
INFINITO_DOCKER_FILESYSTEM_ALLOWED: ${{ inputs.filesystem }}
INFINITO_LIFECYCLES: ${{ inputs.lifecycles }}
INFINITO_DISCOVERY_SEED: ${{ github.run_id }}
jobs:
discover:
name: π Chunk ${{ inputs.index }}${{ inputs.marker }}
runs-on: ubuntu-26.04
timeout-minutes: 60
outputs:
apps: ${{ steps.discover.outputs.apps }}
steps:
- uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
- name: π Resolve repository owner
shell: bash
run: |
set -euo pipefail
echo "GITHUB_REPOSITORY_OWNER=$(scripts/meta/resolve/repository/owner.sh)" >> "$GITHUB_ENV"
- name: π¦ Install jq
shell: bash
run: ./scripts/install/apt.sh jq
- name: π§ Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: π¦ Install project (host venv)
shell: bash
run: |
make install-force
# shellcheck source=/dev/null
source scripts/meta/env/load.sh
echo "${VENV}/bin" >> "${GITHUB_PATH}"
- name: π Resolve effective whitelist (diff vs origin/main)
id: resolve_wl
shell: bash
env:
INPUT_WHITELIST: ${{ inputs.whitelist }}
run: ./scripts/github/resolve/effective_whitelist.sh
- name: π Discover the chunk's rows
id: discover
shell: bash
env:
INFINITO_WHITELIST: ${{ steps.resolve_wl.outputs.whitelist }}
INFINITO_PRIORITY: ${{ inputs.priority }}
INFINITO_MODES: ${{ inputs.modes }}
INFINITO_TOR: ${{ inputs.tor }}
INFINITO_VPN: ${{ inputs.vpn }}
INFINITO_CI_SWEEP: ${{ inputs.sweep }}
INFINITO_CI_CHUNK: ${{ inputs.index }}
INFINITO_CI_OFFSET: ${{ inputs.offset }}
INFINITO_CI_CHUNK_SIZE: ${{ inputs.chunk_size }}
run: ./scripts/github/resolve/output_apps.sh
- name: πΊοΈ Render the plan into the run summary
shell: bash
env:
INFINITO_CI_SWEEP: ${{ inputs.sweep }}
INFINITO_CI_CHUNK_SIZE: ${{ inputs.chunk_size }}
PLAN_DISTROS: ${{ inputs.distros }}
PLAN_FILESYSTEM: ${{ inputs.filesystem }}
PLAN_WHITELIST: ${{ steps.resolve_wl.outputs.whitelist }}
PLAN_PRIORITY: ${{ inputs.priority }}
PLAN_MODES: ${{ inputs.modes }}
PLAN_LIFECYCLES: ${{ inputs.lifecycles }}
PLAN_TOR: ${{ inputs.tor }}
PLAN_VPN: ${{ inputs.vpn }}
PLAN_OFFSET: ${{ inputs.offset }}
run: |
source scripts/meta/env/load.sh
"${PYTHON}" -m cli.meta.ci.plan \
--distros "${PLAN_DISTROS}" \
--filesystem "${PLAN_FILESYSTEM}" \
--whitelist "${PLAN_WHITELIST}" \
--priority "${PLAN_PRIORITY}" \
--modes "${PLAN_MODES}" \
--lifecycles "${PLAN_LIFECYCLES}" \
--tor "${PLAN_TOR}" \
--vpn "${PLAN_VPN}" \
--offset "${PLAN_OFFSET}" \
>> "$GITHUB_STEP_SUMMARY"
deploy:
name: "${{ matrix.label }}"
if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]'
runs-on: ubuntu-26.04
timeout-minutes: 355
needs: discover
# TODO: verify in a CI deploy that a cancel drops queued jobs from this
# group and that an identical row of a second run queues behind it.
concurrency:
group: "infinito-deploy-${{ matrix.mode }}-${{ matrix.tor }}-${{ matrix.distro }}-${{ matrix.filesystem }}-${{ matrix.apps }}#${{ matrix.variant }}"
cancel-in-progress: false
queue: max
strategy:
fail-fast: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}
matrix:
include: ${{ fromJson(needs.discover.outputs.apps) }}
env:
APP_ID: ${{ matrix.apps }}
INFINITO_DEPLOY_MODE: ${{ matrix.mode }}
INFINITO_DISTROS: ${{ matrix.distro }}
INFINITO_DOCKER_FILESYSTEM_ALLOWED: ${{ inputs.filesystem }}
INFINITO_DOCKER_FILESYSTEM_PICK: ${{ matrix.filesystem }}
INFINITO_DOCKER_FILESYSTEM_ENFORCED: ${{ matrix.enforce_filesystem }}
RESCHED_TIMEOUT: "120"
SCRIPTS: scripts/tests/deploy/swarm/routine
steps:
- name: π§Ή Free disk space
if: ${{ !env.ACT }}
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: true
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: "β¬οΈ Checkout"
if: ${{ !env.ACT }}
uses: actions/checkout@v7.0.0
- name: "π₯ Load rescue-diagnostics SPOT into job env"
shell: bash
run: grep '^INFINITO_RESCUE_DIAGNOSTICS_BASE=' default.env >> "$GITHUB_ENV"
- name: π½ Mount the empty spare disk at /mnt
if: ${{ !env.ACT }}
shell: bash
run: ./scripts/github/runner/mount_spare_disk.sh /mnt 75
- name: π¦ Relocate containerd and the Docker data-root onto /mnt when roomier
if: ${{ !env.ACT }}
shell: bash
run: ./scripts/github/runner/relocate_docker_dataroot.sh 75
- name: πΎ Enlarge swap space
if: ${{ !env.ACT }}
shell: bash
run: ./scripts/github/runner/enlarge_swap.sh
- name: π‘οΈ Tear down host AppArmor profiles (dev-host parity)
if: ${{ !env.ACT }}
shell: bash
run: make security-apparmor-teardown
- name: π Resolve repository owner
shell: bash
run: |
set -euo pipefail
echo "GITHUB_REPOSITORY_OWNER=$(scripts/meta/resolve/repository/owner.sh)" >> "$GITHUB_ENV"
- name: π¦ Install jq and iproute2
shell: bash
run: ./scripts/install/apt.sh jq iproute2
- name: "π Resolve SWARM_NAME (mandatory; default to the matrix app)"
if: ${{ matrix.mode == 'swarm' }}
shell: bash
run: echo "SWARM_NAME=${SWARM_NAME:-${{ matrix.apps }}}" >> "$GITHUB_ENV"
- name: "π₯ Load swarm topology SPOT into job env"
if: ${{ matrix.mode == 'swarm' }}
shell: bash
run: scripts/tests/deploy/swarm/utils/topology/export.sh
- name: π§ Set up Python
if: ${{ matrix.mode == 'swarm' }}
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: π¦ Install project (host venv)
if: ${{ matrix.mode == 'swarm' }}
shell: bash
run: |
make install-force
# shellcheck source=/dev/null
source scripts/meta/env/load.sh
echo "${VENV}/bin" >> "${GITHUB_PATH}"
- name: "π Drill ${{ matrix.apps }} on ${{ matrix.distro }}/${{ matrix.filesystem }}"
if: ${{ matrix.mode == 'swarm' }}
shell: bash
env:
variant: ${{ matrix.variant }}
disable: ${{ format('{0} {1}', matrix.disable, inputs.disable) }}
INFINITO_SWARM_VPN: ${{ matrix.vpn }}
INFINITO_SWARM_MESHES: ${{ inputs.meshes }}
run: ./scripts/tests/deploy/distros.sh "${{ env.SCRIPTS }}/00_one.sh" 2>&1 | tee "/tmp/ansible-${{ matrix.apps }}.log"
- name: π Deploy app
if: ${{ matrix.mode != 'swarm' }}
timeout-minutes: 350
shell: bash
env:
apps: ${{ matrix.apps }}
variant: ${{ matrix.variant }}
disable: ${{ format('{0} {1}', matrix.disable, inputs.disable) }}
ANSIBLE_LOG_PATH: /tmp/ansible-${{ matrix.apps }}.log
run: ./scripts/tests/deploy/ci/all.sh 2>&1 | tee "/tmp/ansible-${{ matrix.apps }}.log"
- name: π« Fail on deprecation warnings
if: ${{ always() && matrix.mode != 'swarm' }}
shell: bash
run: ./scripts/tests/deploy/ci/assert_no_deprecation_warnings.sh "/tmp/ansible-${{ matrix.apps }}.log"
- name: "ποΈ Effective node filesystems"
if: ${{ always() && matrix.mode == 'swarm' }}
shell: bash
run: ./scripts/tests/deploy/utils/filesystem/report.sh "/tmp/ansible-${{ matrix.apps }}.log"
- name: "π Role runtimes"
if: always()
shell: bash
run: |
set -euo pipefail
log="/tmp/ansible-${{ matrix.apps }}.log"
if [ ! -f "${log}" ]; then
echo "No ansible log at ${log}; deploy never started."
exit 0
fi
python3 -m cli.meta.ci.roles.runtime "${log}" \
--format csv --output "/tmp/role-runtimes-${{ matrix.apps }}.csv"
python3 -m cli.meta.ci.roles.runtime "/tmp/role-runtimes-${{ matrix.apps }}.csv" \
--format markdown >> "$GITHUB_STEP_SUMMARY"
- name: "β¬οΈ Upload role runtimes CSV"
if: ${{ always() && !env.ACT }}
uses: actions/upload-artifact@v7
with:
name: role-runtimes-${{ matrix.artifact }}
path: /tmp/role-runtimes-${{ matrix.apps }}.csv
if-no-files-found: ignore
- name: π©Ί Dump runner state (on failure)
if: ${{ failure() && matrix.mode != 'swarm' }}
timeout-minutes: 5
shell: bash
run: ./scripts/github/runner/dump_state.sh "post-deploy failure (${{ matrix.apps }})"
- name: "β¬οΈ Upload rescue diagnostics"
if: ${{ always() && !env.ACT }}
uses: actions/upload-artifact@v7
with:
name: rescue-diagnostics-${{ matrix.artifact }}
path: ${{ env.INFINITO_RESCUE_DIAGNOSTICS_BASE }}/
if-no-files-found: ignore
- name: "ποΈ Zip generated inventory for debugging"
if: always()
shell: bash
env:
APP_ID: ${{ matrix.apps }}
run: |
set -euo pipefail
kind=swarm
[ "${{ matrix.mode }}" = "swarm" ] || kind=compose
bash "scripts/github/inventory/zip/${kind}_for_upload.sh"
echo "INVENTORY_ZIP=/tmp/inventory-${kind}-${APP_ID}.zip" >> "$GITHUB_ENV"
- name: "β¬οΈ Upload inventory artifact"
if: ${{ always() && !env.ACT }}
uses: actions/upload-artifact@v7
with:
name: inventory-${{ matrix.artifact }}
path: ${{ env.INVENTORY_ZIP }}
if-no-files-found: ignore
- name: "π Playwright step summary"
if: always()
shell: bash
run: |
set -euo pipefail
python3 utils/github/playwright_summary.py \
/tmp/playwright-artifacts \
"${{ matrix.apps }}" \
>> "$GITHUB_STEP_SUMMARY"
- name: "β¬οΈ Upload Playwright artifacts"
if: ${{ always() && !env.ACT }}
uses: actions/upload-artifact@v7.0.1
with:
name: playwright-${{ matrix.artifact }}
path: /tmp/playwright-artifacts/
if-no-files-found: ignore
- name: "π§Ή Cleanup"
if: ${{ always() && matrix.mode == 'swarm' }}
timeout-minutes: 15
run: scripts/tests/deploy/swarm/utils/clean/teardown.sh
- name: "π Replay the ${{ matrix.apps }} README instructions"
if: ${{ matrix.instructions != '' }}
env:
GUIDE_ROLE: ${{ matrix.apps }}
GUIDE_MODE: ${{ matrix.instructions }}
run: ./scripts/tests/deploy/distros.sh scripts/github/guide/one.sh
- name: "β¬οΈ Upload instructions rescue diagnostics"
if: ${{ always() && matrix.instructions != '' && !env.ACT }}
uses: actions/upload-artifact@v7
with:
name: rescue-diagnostics-instructions-${{ matrix.artifact }}
path: ${{ env.INFINITO_RESCUE_DIAGNOSTICS_BASE }}/
if-no-files-found: ignore