call-test-deploy.ymlΒΆ

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