mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
feat(ci): collapse the four deploy workflows into one chunk chain
call-test-deploy-{compose,host,swarm,single-node}.yml differed only in
which mode they pinned and which artifact prefix they wrote; compose and
host already ran the identical drill. They become one call-test-deploy.yml
whose matrix rows carry the mode, so the swarm-specific steps are guarded
on it and everything else is shared.
The orchestrator loses its eight deploy jobs and the sequencing job for
three serial chunk blocks. GitHub Actions cannot generate a variable
number of jobs, so the blocks are written out and INFINITO_CI_MAX_CHUNKS
tells the budget how many exist; blocks whose slice is empty skip
themselves. chunk_gate stops the chain at the first red chunk,
resume_from_chunk re-enters after a fix instead of re-running the green
chunks, and modes collapses to a single mode choice.
apps.sh now emits matrix entries for one chunk rather than a token list
for one mode, and no longer sorts them: the discovery order is what the
chunker slices on. split_runner_apps.sh reads its runner quota from
INFINITO_CI_CONCURRENCY instead of carrying a second copy of the 20.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5009ec81c9
commit
db66e01020
16 files changed
+432
-1211
No files matched your search
+97
-109
@@ -28,32 +28,21 @@ flowchart TB
|
||||
codeql["cron-security-codeql.yml"]
|
||||
buildci["build-ci-images: images-build-ci.yml"] --> dns["test-dns.yml"]
|
||||
mirror["images-mirror-missing.yml"]
|
||||
seq["sequencing: serial or parallel, per line"]
|
||||
|
||||
subgraph prio["priority line, skipped without a priority input"]
|
||||
swarmprio["test-deploy-swarm-priority"]
|
||||
swarmprio -->|"serial"| composeprio["test-deploy-compose-priority"]
|
||||
composeprio -->|"serial"| hostprio["test-deploy-host-priority"]
|
||||
snprio["test-deploy-single-node-priority: parallel"]
|
||||
subgraph chain["serial chunk chain"]
|
||||
chunk0["test-deploy-chunk-0"]
|
||||
chunk0 --> chunk1["test-deploy-chunk-1"]
|
||||
chunk1 --> chunk2["test-deploy-chunk-2"]
|
||||
end
|
||||
|
||||
subgraph reg["regular line"]
|
||||
swarmreg["test-deploy-swarm"]
|
||||
swarmreg -->|"serial"| composereg["test-deploy-compose"]
|
||||
composereg -->|"serial"| hostreg["test-deploy-host"]
|
||||
snreg["test-deploy-single-node: parallel"]
|
||||
end
|
||||
lintwf --> chain
|
||||
testwf --> chain
|
||||
dns --> chain
|
||||
mirror --> chain
|
||||
buildci --> chain
|
||||
|
||||
lintwf --> prio
|
||||
testwf --> prio
|
||||
dns --> prio
|
||||
mirror --> prio
|
||||
seq --> prio
|
||||
prio -->|"all priority jobs green"| reg
|
||||
|
||||
swarmreg --> smoke["test-runner-smoke.yml"]
|
||||
prio --> report["report-main-failures"]
|
||||
reg --> report
|
||||
chunk0 --> smoke["test-runner-smoke.yml"]
|
||||
chain --> report["report-main-failures"]
|
||||
|
||||
instmake["test-install-make.yml"]
|
||||
instpkgmgr["test-install-pkgmgr.yml"]
|
||||
@@ -61,9 +50,7 @@ flowchart TB
|
||||
buildci --> testguide["test-instructions.yml"]
|
||||
mirror --> testguide
|
||||
|
||||
prio --> donegate["done"]
|
||||
reg --> donegate
|
||||
seq --> donegate
|
||||
chain --> donegate["done"]
|
||||
smoke --> donegate
|
||||
instmake --> donegate
|
||||
instpkgmgr --> donegate
|
||||
@@ -71,111 +58,112 @@ flowchart TB
|
||||
testguide --> donegate
|
||||
end
|
||||
|
||||
snprio --> singlenode["test-deploy-single-node.yml"]
|
||||
snreg --> singlenode
|
||||
singlenode --> deploycompose["test-deploy-compose.yml"]
|
||||
singlenode --> deployhost["test-deploy-host.yml"]
|
||||
composeprio --> deploycompose
|
||||
composereg --> deploycompose
|
||||
hostprio --> deployhost
|
||||
hostreg --> deployhost
|
||||
swarmprio --> deployswarm["test-deploy-swarm.yml"]
|
||||
swarmreg --> deployswarm
|
||||
chunk0 --> deploy["call-test-deploy.yml"]
|
||||
chunk1 --> deploy
|
||||
chunk2 --> deploy
|
||||
```
|
||||
|
||||
The priority line runs only when the orchestrator's `priority` input is set;
|
||||
with it empty every priority job is skipped and the regular line starts
|
||||
directly. The regular jobs receive the priority ids as `blacklist`, so each
|
||||
role deploys in exactly one line.
|
||||
## Sweeps and chunks
|
||||
|
||||
## Deploy-line sequencing
|
||||
A **sweep** is one orchestrator run. It builds a single ordered list of
|
||||
`role#variant` rows, assigns each row a deploy mode and a tor state, and
|
||||
deploys the list in serial **chunk** blocks.
|
||||
|
||||
A **line** is one half of a run: the priority line (`⭐`, the roles named in
|
||||
`priority`) or the regular line (`🔁`, everything else). Each line deploys up
|
||||
to three **modes**: swarm, compose, host. The lines are always sequential
|
||||
relative to each other; this section is about the order *inside* one line.
|
||||
|
||||
### Why the order exists
|
||||
### Why chunks exist
|
||||
|
||||
GitHub cancels a job that has sat queued for 24 hours. That clock starts when
|
||||
the job is queued, not when the run starts, and a `needs:` edge delays queueing
|
||||
until the dependency completes. A line that starts all three modes at once
|
||||
queues its whole matrix in one go, so with more roles than runner slots the
|
||||
tail of that matrix waits past the cut and dies unrun. Deploying the modes one
|
||||
after the other restarts the clock per mode.
|
||||
until the dependency completes. A run that queues its whole matrix at once
|
||||
leaves the tail waiting past the cut, where it dies unrun. Deploying in chunks
|
||||
chained on `needs:` restarts the clock per chunk.
|
||||
|
||||
Serialising costs wall-clock, so it is only worth it above a certain size. The
|
||||
run does not pay it on a small diff.
|
||||
The chunk size is what makes that safe, and `cli.meta.ci.slots` derives it
|
||||
rather than pinning it:
|
||||
|
||||
### The decision
|
||||
```text
|
||||
waves = floor(INFINITO_CI_QUEUE_HOURS / the deploy job's timeout-minutes)
|
||||
chunk size = INFINITO_CI_CONCURRENCY * waves
|
||||
```
|
||||
|
||||
The `sequencing` job resolves the effective whitelist (the same
|
||||
`scripts/github/resolve/effective_whitelist.sh` the discover steps use) and
|
||||
calls `cli.meta.ci.sequencing` once per line, emitting `serial` or `parallel`
|
||||
as a job output.
|
||||
Assuming every job burns its full timeout is deliberately pessimistic — no job
|
||||
can outlast it — so the estimate can only overshoot the real drain time. With
|
||||
the current constants that is 4 waves of 20 runners, so 80 rows per chunk, and
|
||||
the last job of a chunk starts around t=18h, well inside the 24h window.
|
||||
|
||||
Job counts are taken on the row basis each mode actually runs on: swarm
|
||||
selections are `role#variant` tokens that map 1:1 onto jobs, compose and host
|
||||
selections are whole roles whose variants pack into bundles
|
||||
(`utils.github.variant.bundles`). The count deliberately omits the
|
||||
runner-storage filter that `scripts/meta/resolve/apps.sh` applies inside
|
||||
GitHub Actions, so it can only overestimate — erring towards the sequential
|
||||
layout, which cannot be cancelled.
|
||||
### How many chunks a sweep spends
|
||||
|
||||
| `sequencing` input | Behaviour |
|
||||
The run's 256-job cap is the second ceiling. `slots` counts every non-deploy
|
||||
job the orchestrator chain spawns, subtracts the worst `entry-*.yml` overhead,
|
||||
and what remains is `available` — the rows one sweep may deploy across all its
|
||||
chunks. Rows beyond that roll into the next sweep, which starts reading the
|
||||
regular line at an offset (`cli.meta.ci.chunks`), so consecutive sweeps walk
|
||||
the whole list instead of re-testing the same head forever.
|
||||
|
||||
GitHub Actions cannot generate a variable number of jobs, so the chunk blocks
|
||||
are written out in the orchestrator. `INFINITO_CI_MAX_CHUNKS` must equal how
|
||||
many exist there — `slots` plans a sweep against that key, and a sweep planned
|
||||
larger than the chain silently drops its tail. Blocks whose slice comes back
|
||||
empty skip themselves and cost nothing.
|
||||
|
||||
Run `python -m cli.meta.ci.slots --matrix` to see the whole budget.
|
||||
|
||||
### Priority
|
||||
|
||||
`priority` names the roles that lead. They sort to the head of the list and the
|
||||
split forces a chunk boundary at the priority/regular seam, so a chunk is
|
||||
either all priority or all regular — the seam chunk stays short rather than
|
||||
being topped up. That is what guarantees every priority row is deployed before
|
||||
the first regular one starts. Priority rows wear a trailing `⭐` in their job
|
||||
name and never move with the sweep offset.
|
||||
|
||||
They are also the rows a run must not sample. A priority row is deployed in
|
||||
**every combination it can take** — each variant, in each mode its role offers,
|
||||
and on the modes that carry the onion axis once behind Tor and once on
|
||||
clearnet — all within the same sweep. A 3-variant role offering compose and
|
||||
swarm therefore becomes 3 × 2 × 2 = **12 jobs**, not 3. That is the point of
|
||||
naming a role in `priority`: it is proven everywhere at once instead of over
|
||||
four sweeps.
|
||||
|
||||
An explicit `tor` input still wins over the full coverage: `enforced`,
|
||||
`exclusive` and `disabled` are operator narrowings, and a variant that pins
|
||||
`services.tor.enabled` to false never gets an onion run regardless.
|
||||
|
||||
### Mode and tor
|
||||
|
||||
For **regular** rows both axes are a deterministic rotation over the row's
|
||||
position in the global list and the sweep number, never random, so a red job
|
||||
reproduces by re-running the same sweep:
|
||||
|
||||
| Axis | Rotation |
|
||||
|---|---|
|
||||
| `auto` (default) | `serial` above `INFINITO_CI_SEQUENTIAL_THRESHOLD` jobs in that line, `parallel` at or below it |
|
||||
| `serial` | forced; skips the count entirely |
|
||||
| `parallel` | forced; skips the count entirely |
|
||||
| mode | `(position + sweep) % len(modes the role offers)` |
|
||||
| tor | `(position + sweep // 2) % 2` |
|
||||
|
||||
The threshold lives in [`default.env`](../../default.env) as
|
||||
`INFINITO_CI_SEQUENTIAL_THRESHOLD`. Both lines are decided independently: a
|
||||
small priority line can run parallel while the regular line behind it runs
|
||||
serial.
|
||||
A role offers at most two modes in practice — swarm needs its own stack, host
|
||||
needs the absence of one — so a row flips between its two modes on consecutive
|
||||
sweeps. Tor turns on `sweep // 2` so it does not flip in lockstep: a row walks
|
||||
all four mode/tor combinations over four sweeps instead of only two. Priority
|
||||
rows skip the rotation entirely and take the whole cross-product at once.
|
||||
|
||||
### The two layouts
|
||||
|
||||
Serial order is **swarm, then compose, then host** — heaviest mode first, so
|
||||
the longest queue drains while the run is youngest.
|
||||
|
||||
- **parallel** — `test-deploy-swarm-priority` plus
|
||||
`test-deploy-single-node-priority`, which fans compose and host out
|
||||
together. This is the layout the pipeline had before sequencing existed.
|
||||
- **serial** — `test-deploy-swarm-priority` →
|
||||
`test-deploy-compose-priority` → `test-deploy-host-priority`, each chained
|
||||
on the previous through `needs:`.
|
||||
|
||||
The regular line mirrors this with the unsuffixed job names.
|
||||
|
||||
A `needs:` edge is static, so a workflow cannot make one conditional. Both
|
||||
layouts therefore exist as separate jobs and the `sequencing` output skips
|
||||
one of them. Because a skipped dependency otherwise skips its dependents, the
|
||||
serial jobs carry `always()` plus explicit `needs.<job>.result` checks on the
|
||||
real gates. The swarm job depends on no sequencing decision at all: it leads
|
||||
in either layout.
|
||||
Because the same variant can now run twice in one sweep, the onion state is
|
||||
part of what identifies a job: it is in the job label (`🧅` vs `🌐`), in the
|
||||
deploy concurrency group, and in every artifact name. Two jobs uploading under
|
||||
one artifact name is a conflict, not an overwrite.
|
||||
|
||||
### Stopping on failure
|
||||
|
||||
`mode_fail_fast` (default `true`) decides whether a serial line stops at its
|
||||
first failed mode:
|
||||
`chunk_gate` (default `true`) decides whether the chain stops at its first
|
||||
failed chunk:
|
||||
|
||||
| Value | Behaviour |
|
||||
|---|---|
|
||||
| `true` | swarm fails → compose and host of that line are skipped |
|
||||
| `false` | every mode deploys and reports; the run still ends red |
|
||||
| `true` | chunk 0 fails → the remaining chunks are skipped |
|
||||
| `false` | every chunk deploys and reports; the run still ends red |
|
||||
|
||||
`skipped` counts as passed, so a mode absent from `modes` never blocks the
|
||||
chain. The parallel layout ignores the switch — with no order there is nothing
|
||||
to stop. The setting is a checkbox on `entry-manual-steer.yml`; the other entry
|
||||
points take the default.
|
||||
|
||||
### Job budget
|
||||
|
||||
`cli.meta.ci.slots` divides the run's 256-job cap between the deploy
|
||||
matrices and reads the orchestrator to do it, so every deploy caller —
|
||||
including the serial twins — must be listed in its `_DEPLOY_CALLERS`.
|
||||
A caller missing there is charged a guessed dynamic-matrix estimate instead
|
||||
of its single discover job.
|
||||
`skipped` counts as passed, so an empty chunk never blocks the chain. After
|
||||
fixing what broke a sweep, `resume_from_chunk` re-enters at that index instead
|
||||
of re-running the green chunks. Both are inputs on `entry-manual-steer.yml`;
|
||||
the other entry points take the defaults.
|
||||
|
||||
## Cancellation
|
||||
|
||||
@@ -209,5 +197,5 @@ flowchart TB
|
||||
|
||||
Also manually dispatchable: `cron-images-mirror-all.yml`, `cron-images-cleanup-ci.yml`,
|
||||
`cron-cleanup-stale.yml`, `cron-update.yml`, `cron-release-highest.yml`, `call-release-version.yml`,
|
||||
`call-lint.yml`, `call-test.yml`, `call-test-deploy-swarm.yml`, `call-test-dns.yml`,
|
||||
`call-lint.yml`, `call-test.yml`, `call-test-dns.yml`,
|
||||
`test-workspace.yml`, `test-runner-smoke.yml`.
|
||||
@@ -16,23 +16,28 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
modes:
|
||||
description: "Deploy modes to run (space-separated: 'swarm compose host'). Only listed modes run their deploy job."
|
||||
required: false
|
||||
type: string
|
||||
default: "swarm compose host"
|
||||
priority:
|
||||
description: "Optional space-separated app ids for the priority line: deployed first in every mode; the regular line runs the remaining roles only after every priority job succeeded."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
sequencing:
|
||||
description: "🐛 Deploy-mode order per line. 'serial': swarm, then compose, then host. 'parallel': all at once. 'auto': serial above the job threshold."
|
||||
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"
|
||||
mode_fail_fast:
|
||||
description: "🛑 Stop a serial line at its first failed mode. Off: keep deploying the remaining modes."
|
||||
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/tor rotation and the regular-line offset. Empty: the run number."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
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."
|
||||
required: false
|
||||
type: number
|
||||
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
|
||||
@@ -67,7 +72,7 @@ on:
|
||||
type: string
|
||||
default: "auto"
|
||||
tor:
|
||||
description: "Tor axis of the swarm and compose matrices: 'auto' onions every second tor-capable entry and flips with the run number, 'enforced' onions all of them, 'exclusive' also drops the entries that cannot, 'disabled' onions none."
|
||||
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"
|
||||
@@ -171,7 +176,7 @@ jobs:
|
||||
distros: "arch debian ubuntu fedora centos"
|
||||
concurrency_channel: ci-orchestrator-build-consumer
|
||||
secrets: inherit
|
||||
|
||||
|
||||
test-dns:
|
||||
needs:
|
||||
- build-ci-images
|
||||
@@ -179,7 +184,7 @@ jobs:
|
||||
uses: ./.github/workflows/call-test-dns.yml
|
||||
with:
|
||||
distros: "arch debian ubuntu fedora centos"
|
||||
|
||||
|
||||
images-mirror-missing:
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -191,305 +196,101 @@ jobs:
|
||||
image_wait_sleep_seconds: ${{ inputs.image_wait_sleep_seconds }}
|
||||
secrets: inherit
|
||||
|
||||
sequencing:
|
||||
name: 🐛 Sequencing
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
outputs:
|
||||
priority: ${{ steps.priority.outputs.sequencing }}
|
||||
regular: ${{ steps.regular.outputs.sequencing }}
|
||||
env:
|
||||
INFINITO_APP_DISCOVERY_RUNNER: host
|
||||
INFINITO_LIFECYCLES: ${{ inputs.lifecycles }}
|
||||
INFINITO_HOST_EXCLUDE_COMPOSE: ${{ contains(inputs.modes, 'compose') && 'true' || 'false' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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
|
||||
|
||||
- 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: ⭐ Decide the priority line
|
||||
id: priority
|
||||
if: ${{ inputs.priority != '' }}
|
||||
shell: bash
|
||||
run: |
|
||||
source scripts/meta/env/load.sh
|
||||
"${PYTHON}" -m cli.meta.ci.sequencing \
|
||||
--modes "${{ inputs.modes }}" \
|
||||
--whitelist "${{ inputs.priority }}" \
|
||||
--lifecycles "${{ inputs.lifecycles }}" \
|
||||
--choice "${{ inputs.sequencing }}" \
|
||||
| tee -a "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 🔁 Decide the regular line
|
||||
id: regular
|
||||
shell: bash
|
||||
run: |
|
||||
source scripts/meta/env/load.sh
|
||||
"${PYTHON}" -m cli.meta.ci.sequencing \
|
||||
--modes "${{ inputs.modes }}" \
|
||||
--whitelist "${{ steps.resolve_wl.outputs.whitelist }}" \
|
||||
--blacklist "${{ inputs.priority }}" \
|
||||
--lifecycles "${{ inputs.lifecycles }}" \
|
||||
--choice "${{ inputs.sequencing }}" \
|
||||
| tee -a "$GITHUB_OUTPUT"
|
||||
|
||||
test-deploy-swarm-priority:
|
||||
name: ⭐ Priority · swarm
|
||||
if: ${{ inputs.priority != '' && contains(inputs.modes, 'swarm') }}
|
||||
needs:
|
||||
- lint
|
||||
- test
|
||||
- build-ci-images
|
||||
- images-mirror-missing
|
||||
- fork-prereqs-ready
|
||||
uses: ./.github/workflows/call-test-deploy-swarm.yml
|
||||
with:
|
||||
distros: ${{ inputs.distros }}
|
||||
whitelist: ${{ inputs.priority }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
tor: ${{ inputs.tor }}
|
||||
marker: " ⭐"
|
||||
|
||||
test-deploy-single-node-priority:
|
||||
name: ⭐ Priority · single-node
|
||||
if: >-
|
||||
${{ inputs.priority != ''
|
||||
&& needs.sequencing.outputs.priority == 'parallel'
|
||||
&& (contains(inputs.modes, 'compose') || contains(inputs.modes, 'host')) }}
|
||||
needs:
|
||||
- test-dns
|
||||
- images-mirror-missing
|
||||
- lint
|
||||
- test
|
||||
- sequencing
|
||||
uses: ./.github/workflows/call-test-deploy-single-node.yml
|
||||
with:
|
||||
distros: ${{ inputs.distros }}
|
||||
whitelist: ${{ inputs.priority }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
modes: ${{ inputs.modes }}
|
||||
tor: ${{ inputs.tor }}
|
||||
marker: " ⭐"
|
||||
|
||||
test-deploy-compose-priority:
|
||||
name: ⭐ Priority · compose
|
||||
test-deploy-chunk-0:
|
||||
name: 🧱 Chunk 0
|
||||
if: >-
|
||||
${{ always()
|
||||
&& inputs.priority != ''
|
||||
&& needs.sequencing.outputs.priority == 'serial'
|
||||
&& contains(inputs.modes, 'compose')
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs.test.result == 'success'
|
||||
&& needs.test-dns.result == 'success'
|
||||
&& needs.images-mirror-missing.result == 'success'
|
||||
&& (!inputs.mode_fail_fast
|
||||
|| contains(fromJSON('["success","skipped"]'), needs.test-deploy-swarm-priority.result)) }}
|
||||
needs:
|
||||
- test-dns
|
||||
- images-mirror-missing
|
||||
- lint
|
||||
- test
|
||||
- sequencing
|
||||
- test-deploy-swarm-priority
|
||||
uses: ./.github/workflows/call-test-deploy-compose.yml
|
||||
with:
|
||||
distros: ${{ inputs.distros }}
|
||||
whitelist: ${{ inputs.priority }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
tor: ${{ inputs.tor }}
|
||||
marker: " ⭐"
|
||||
|
||||
test-deploy-host-priority:
|
||||
name: ⭐ Priority · host
|
||||
if: >-
|
||||
${{ always()
|
||||
&& inputs.priority != ''
|
||||
&& needs.sequencing.outputs.priority == 'serial'
|
||||
&& contains(inputs.modes, 'host')
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs.test.result == 'success'
|
||||
&& needs.test-dns.result == 'success'
|
||||
&& needs.images-mirror-missing.result == 'success'
|
||||
&& (!inputs.mode_fail_fast
|
||||
|| contains(fromJSON('["success","skipped"]'), needs.test-deploy-compose-priority.result)) }}
|
||||
needs:
|
||||
- test-dns
|
||||
- images-mirror-missing
|
||||
- lint
|
||||
- test
|
||||
- sequencing
|
||||
- test-deploy-compose-priority
|
||||
uses: ./.github/workflows/call-test-deploy-host.yml
|
||||
with:
|
||||
distros: ${{ inputs.distros }}
|
||||
whitelist: ${{ inputs.priority }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
marker: " ⭐"
|
||||
exclude_compose_covered: ${{ contains(inputs.modes, 'compose') && 'true' || 'false' }}
|
||||
|
||||
test-deploy-swarm:
|
||||
name: 🔁 Regular · swarm
|
||||
if: >-
|
||||
${{ always()
|
||||
&& contains(inputs.modes, 'swarm')
|
||||
&& 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'
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-single-node-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-compose-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-host-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-swarm-priority.result) }}
|
||||
&& needs.test-dns.result == 'success' }}
|
||||
needs:
|
||||
- lint
|
||||
- test
|
||||
- build-ci-images
|
||||
- images-mirror-missing
|
||||
- test-deploy-single-node-priority
|
||||
- test-deploy-compose-priority
|
||||
- test-deploy-host-priority
|
||||
- test-deploy-swarm-priority
|
||||
- lint
|
||||
- test
|
||||
uses: ./.github/workflows/call-test-deploy-swarm.yml
|
||||
- test-dns
|
||||
- fork-prereqs-ready
|
||||
uses: ./.github/workflows/call-test-deploy.yml
|
||||
with:
|
||||
distros: ${{ inputs.distros }}
|
||||
index: "0"
|
||||
sweep: ${{ inputs.sweep != '' && inputs.sweep || github.run_number }}
|
||||
whitelist: ${{ inputs.whitelist }}
|
||||
priority: ${{ inputs.priority }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
modes: ${{ inputs.mode }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
tor: ${{ inputs.tor }}
|
||||
blacklist: ${{ inputs.priority }}
|
||||
|
||||
test-deploy-single-node:
|
||||
name: 🔁 Regular · single-node
|
||||
test-deploy-chunk-1:
|
||||
name: 🧱 Chunk 1
|
||||
if: >-
|
||||
${{ always()
|
||||
&& needs.sequencing.outputs.regular == 'parallel'
|
||||
&& (contains(inputs.modes, 'compose') || contains(inputs.modes, 'host'))
|
||||
&& inputs.resume_from_chunk <= 1
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs.test.result == 'success'
|
||||
&& needs.test-dns.result == 'success'
|
||||
&& needs.build-ci-images.result == 'success'
|
||||
&& needs.images-mirror-missing.result == 'success'
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-single-node-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-compose-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-host-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-swarm-priority.result) }}
|
||||
&& needs.test-dns.result == 'success'
|
||||
&& (!inputs.chunk_gate
|
||||
|| contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-0.result)) }}
|
||||
needs:
|
||||
- test-dns
|
||||
- images-mirror-missing
|
||||
- sequencing
|
||||
- test-deploy-single-node-priority
|
||||
- test-deploy-compose-priority
|
||||
- test-deploy-host-priority
|
||||
- test-deploy-swarm-priority
|
||||
- lint
|
||||
- test
|
||||
uses: ./.github/workflows/call-test-deploy-single-node.yml
|
||||
with:
|
||||
distros: ${{ inputs.distros }}
|
||||
whitelist: ${{ inputs.whitelist }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
modes: ${{ inputs.modes }}
|
||||
tor: ${{ inputs.tor }}
|
||||
blacklist: ${{ inputs.priority }}
|
||||
|
||||
test-deploy-compose:
|
||||
name: 🔁 Regular · compose
|
||||
if: >-
|
||||
${{ always()
|
||||
&& needs.sequencing.outputs.regular == 'serial'
|
||||
&& contains(inputs.modes, 'compose')
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs.test.result == 'success'
|
||||
&& needs.test-dns.result == 'success'
|
||||
&& needs.images-mirror-missing.result == 'success'
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-single-node-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-compose-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-host-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-swarm-priority.result)
|
||||
&& (!inputs.mode_fail_fast
|
||||
|| contains(fromJSON('["success","skipped"]'), needs.test-deploy-swarm.result)) }}
|
||||
needs:
|
||||
- test-dns
|
||||
- build-ci-images
|
||||
- images-mirror-missing
|
||||
- sequencing
|
||||
- test-deploy-single-node-priority
|
||||
- test-deploy-compose-priority
|
||||
- test-deploy-host-priority
|
||||
- test-deploy-swarm-priority
|
||||
- test-deploy-swarm
|
||||
- lint
|
||||
- test
|
||||
uses: ./.github/workflows/call-test-deploy-compose.yml
|
||||
- test-dns
|
||||
- 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 }}
|
||||
whitelist: ${{ inputs.whitelist }}
|
||||
priority: ${{ inputs.priority }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
modes: ${{ inputs.mode }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
tor: ${{ inputs.tor }}
|
||||
blacklist: ${{ inputs.priority }}
|
||||
|
||||
test-deploy-host:
|
||||
name: 🔁 Regular · host
|
||||
test-deploy-chunk-2:
|
||||
name: 🧱 Chunk 2
|
||||
if: >-
|
||||
${{ always()
|
||||
&& needs.sequencing.outputs.regular == 'serial'
|
||||
&& contains(inputs.modes, 'host')
|
||||
&& inputs.resume_from_chunk <= 2
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs.test.result == 'success'
|
||||
&& needs.test-dns.result == 'success'
|
||||
&& needs.build-ci-images.result == 'success'
|
||||
&& needs.images-mirror-missing.result == 'success'
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-single-node-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-compose-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-host-priority.result)
|
||||
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-swarm-priority.result)
|
||||
&& (!inputs.mode_fail_fast
|
||||
|| contains(fromJSON('["success","skipped"]'), needs.test-deploy-compose.result)) }}
|
||||
&& needs.test-dns.result == 'success'
|
||||
&& (!inputs.chunk_gate
|
||||
|| contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-1.result)) }}
|
||||
needs:
|
||||
- test-dns
|
||||
- images-mirror-missing
|
||||
- sequencing
|
||||
- test-deploy-single-node-priority
|
||||
- test-deploy-compose-priority
|
||||
- test-deploy-host-priority
|
||||
- test-deploy-swarm-priority
|
||||
- test-deploy-compose
|
||||
- lint
|
||||
- test
|
||||
uses: ./.github/workflows/call-test-deploy-host.yml
|
||||
- build-ci-images
|
||||
- images-mirror-missing
|
||||
- test-dns
|
||||
- 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 }}
|
||||
whitelist: ${{ inputs.whitelist }}
|
||||
priority: ${{ inputs.priority }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
modes: ${{ inputs.mode }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
blacklist: ${{ inputs.priority }}
|
||||
exclude_compose_covered: ${{ contains(inputs.modes, 'compose') && 'true' || 'false' }}
|
||||
tor: ${{ inputs.tor }}
|
||||
|
||||
test-runner-smoke:
|
||||
needs:
|
||||
- test-deploy-swarm
|
||||
if: ${{ always() && vars.CI_SELF_HOSTED_RUNNER_COUNT != '0' && vars.CI_SELF_HOSTED_RUNNER_COUNT != '' && (needs.test-deploy-swarm.result == 'success' || needs.test-deploy-swarm.result == 'failure') }}
|
||||
- test-deploy-chunk-0
|
||||
if: ${{ always() && 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 }}
|
||||
@@ -527,14 +328,9 @@ jobs:
|
||||
report-main-failures:
|
||||
name: 🐞 Report main role failures
|
||||
needs:
|
||||
- test-deploy-single-node-priority
|
||||
- test-deploy-compose-priority
|
||||
- test-deploy-host-priority
|
||||
- test-deploy-swarm-priority
|
||||
- test-deploy-single-node
|
||||
- test-deploy-compose
|
||||
- test-deploy-host
|
||||
- test-deploy-swarm
|
||||
- test-deploy-chunk-0
|
||||
- test-deploy-chunk-1
|
||||
- test-deploy-chunk-2
|
||||
if: ${{ always() && github.ref == 'refs/heads/main' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -558,15 +354,9 @@ jobs:
|
||||
- test-dns
|
||||
- test-install-make
|
||||
- test-install-pkgmgr
|
||||
- sequencing
|
||||
- test-deploy-single-node-priority
|
||||
- test-deploy-compose-priority
|
||||
- test-deploy-host-priority
|
||||
- test-deploy-swarm-priority
|
||||
- test-deploy-single-node
|
||||
- test-deploy-compose
|
||||
- test-deploy-host
|
||||
- test-deploy-swarm
|
||||
- test-deploy-chunk-0
|
||||
- test-deploy-chunk-1
|
||||
- test-deploy-chunk-2
|
||||
- test-runner-smoke
|
||||
- test-workspace
|
||||
- test-instructions
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
name: "📦 Test: Deploy (compose)"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
distros:
|
||||
description: "Space-separated distro list"
|
||||
required: true
|
||||
type: string
|
||||
whitelist:
|
||||
description: "Optional space-separated whitelist of app ids"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
lifecycles:
|
||||
description: "Lifecycle envelope for discovery (comma/space-separated)."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
blacklist:
|
||||
description: "Optional space-separated app ids to drop from discovery"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
marker:
|
||||
description: "Optional suffix appended to the discover job name (e.g. ' ⭐' for the priority line)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
filesystem:
|
||||
description: "🗄️ Space-separated filesystems the per-distro pick draws from (ext4, btrfs, zfs). Empty: all three."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
tor:
|
||||
description: "🧅 Tor axis: auto | enforced | exclusive | disabled."
|
||||
required: false
|
||||
type: string
|
||||
default: auto
|
||||
|
||||
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_PLAYWRIGHT_KEEP: ${{ vars.INFINITO_PLAYWRIGHT_KEEP }}
|
||||
INFINITO_APP_DISCOVERY_RUNNER: host
|
||||
INFINITO_DEPLOY_MODE: compose
|
||||
INFINITO_DOCKER_FILESYSTEM_ALLOWED: ${{ inputs.filesystem }}
|
||||
INFINITO_LIFECYCLES: ${{ inputs.lifecycles }}
|
||||
INFINITO_BLACKLIST: ${{ inputs.blacklist }}
|
||||
jobs:
|
||||
discover:
|
||||
name: 🪼 Compose (${{ inputs.distros }})${{ inputs.marker }}
|
||||
runs-on: ubuntu-latest
|
||||
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: 📦 Install deps (jq + envsubst)
|
||||
shell: bash
|
||||
run: ./scripts/install/apt.sh jq gettext-base
|
||||
|
||||
- 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
|
||||
|
||||
- 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 apps
|
||||
id: discover
|
||||
shell: bash
|
||||
env:
|
||||
INFINITO_WHITELIST: ${{ steps.resolve_wl.outputs.whitelist }}
|
||||
INFINITO_TOR: ${{ inputs.tor }}
|
||||
RUN_NUMBER: ${{ github.run_number }}
|
||||
run: ./scripts/github/resolve/output_apps.sh
|
||||
|
||||
- name: 🗺️ Render the plan into the run summary
|
||||
shell: bash
|
||||
run: |
|
||||
source scripts/meta/env/load.sh
|
||||
"${PYTHON}" -m cli.meta.ci.plan \
|
||||
--distros "${{ inputs.distros }}" \
|
||||
--whitelist "${{ steps.resolve_wl.outputs.whitelist }}" \
|
||||
--priority "${{ inputs.blacklist }}" \
|
||||
--modes "compose" \
|
||||
--lifecycles "${{ inputs.lifecycles }}" \
|
||||
>> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
compose:
|
||||
name: 🐳${{ matrix.label }}
|
||||
if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 355
|
||||
needs: discover
|
||||
|
||||
concurrency:
|
||||
group: "infinito-deploy-compose-${{ matrix.apps }}#${{ matrix.variant_slug }}"
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.discover.outputs.apps) }}
|
||||
|
||||
steps:
|
||||
- name: 🧹 Free disk space
|
||||
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
|
||||
|
||||
- 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: 💾 Enlarge swap space
|
||||
shell: bash
|
||||
run: ./scripts/github/runner/enlarge_swap.sh
|
||||
|
||||
- name: 📦 Relocate Docker data-root onto /mnt when roomier
|
||||
if: ${{ !env.ACT }}
|
||||
shell: bash
|
||||
run: ./scripts/github/runner/relocate_docker_dataroot.sh
|
||||
|
||||
- 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: 🚀 Deploy app
|
||||
timeout-minutes: 350
|
||||
shell: bash
|
||||
env:
|
||||
apps: ${{ matrix.apps }}
|
||||
variant: ${{ matrix.variant }}
|
||||
disable: ${{ matrix.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()
|
||||
shell: bash
|
||||
run: ./scripts/tests/deploy/ci/assert_no_deprecation_warnings.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()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: role-runtimes-${{ matrix.apps }}
|
||||
path: /tmp/role-runtimes-${{ matrix.apps }}.csv
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: 🩺 Dump runner state (on failure)
|
||||
if: failure() || cancelled()
|
||||
timeout-minutes: 5
|
||||
shell: bash
|
||||
run: ./scripts/github/runner/dump_state.sh "post-deploy failure (${{ matrix.apps }})"
|
||||
|
||||
- name: ⬆️ Upload rescue diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: rescue-diagnostics-compose-${{ matrix.apps }}${{ matrix.variant_slug && format('-{0}', matrix.variant_slug) || '' }}
|
||||
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: bash scripts/github/inventory/zip/compose_for_upload.sh
|
||||
|
||||
- name: ⬆️ Upload inventory artifact
|
||||
if: ${{ always() && !env.ACT }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: inventory-compose-${{ matrix.apps }}
|
||||
path: /tmp/inventory-compose-${{ matrix.apps }}.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()
|
||||
uses: actions/upload-artifact@v7.0.1
|
||||
with:
|
||||
name: playwright-compose-${{ matrix.apps }}${{ matrix.variant_slug && format('-{0}', matrix.variant_slug) || '' }}
|
||||
path: /tmp/playwright-artifacts/
|
||||
if-no-files-found: ignore
|
||||
@@ -1,240 +0,0 @@
|
||||
name: "💻 Test: Deploy (host)"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
distros:
|
||||
description: "Space-separated distro list"
|
||||
required: true
|
||||
type: string
|
||||
whitelist:
|
||||
description: "Optional space-separated whitelist of app ids"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
lifecycles:
|
||||
description: "Lifecycle envelope for discovery (comma/space-separated)."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
blacklist:
|
||||
description: "Optional space-separated app ids to drop from discovery"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
marker:
|
||||
description: "Optional suffix appended to the discover job name (e.g. ' ⭐' for the priority line)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
filesystem:
|
||||
description: "🗄️ Space-separated filesystems the per-distro pick draws from (ext4, btrfs, zfs). Empty: all three."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
exclude_compose_covered:
|
||||
description: "Drop roles the same run's compose line already deploys"
|
||||
required: false
|
||||
type: string
|
||||
default: "false"
|
||||
|
||||
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_PLAYWRIGHT_KEEP: ${{ vars.INFINITO_PLAYWRIGHT_KEEP }}
|
||||
INFINITO_APP_DISCOVERY_RUNNER: host
|
||||
INFINITO_DEPLOY_MODE: host
|
||||
INFINITO_DOCKER_FILESYSTEM_ALLOWED: ${{ inputs.filesystem }}
|
||||
INFINITO_LIFECYCLES: ${{ inputs.lifecycles }}
|
||||
INFINITO_BLACKLIST: ${{ inputs.blacklist }}
|
||||
INFINITO_HOST_EXCLUDE_COMPOSE: ${{ inputs.exclude_compose_covered }}
|
||||
jobs:
|
||||
discover:
|
||||
name: 💻 Host (${{ inputs.distros }})${{ inputs.marker }}
|
||||
runs-on: ubuntu-latest
|
||||
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: 📦 Install deps (jq + envsubst)
|
||||
shell: bash
|
||||
run: ./scripts/install/apt.sh jq gettext-base
|
||||
|
||||
- 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
|
||||
|
||||
- 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 host roles
|
||||
id: discover
|
||||
shell: bash
|
||||
env:
|
||||
INFINITO_WHITELIST: ${{ steps.resolve_wl.outputs.whitelist }}
|
||||
run: ./scripts/github/resolve/output_apps.sh
|
||||
|
||||
- name: 🗺️ Render the plan into the run summary
|
||||
shell: bash
|
||||
run: |
|
||||
source scripts/meta/env/load.sh
|
||||
"${PYTHON}" -m cli.meta.ci.plan \
|
||||
--distros "${{ inputs.distros }}" \
|
||||
--whitelist "${{ steps.resolve_wl.outputs.whitelist }}" \
|
||||
--priority "${{ inputs.blacklist }}" \
|
||||
--modes "host" \
|
||||
--lifecycles "${{ inputs.lifecycles }}" \
|
||||
>> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
host:
|
||||
name: 💻${{ matrix.label }}
|
||||
if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 355
|
||||
needs: discover
|
||||
|
||||
concurrency:
|
||||
group: "infinito-deploy-host-${{ matrix.apps }}#${{ matrix.variant_slug }}"
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.discover.outputs.apps) }}
|
||||
|
||||
steps:
|
||||
- name: 🧹 Free disk space
|
||||
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
|
||||
|
||||
- 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: 💾 Enlarge swap space
|
||||
shell: bash
|
||||
run: ./scripts/github/runner/enlarge_swap.sh
|
||||
|
||||
- name: 📦 Relocate Docker data-root onto /mnt when roomier
|
||||
if: ${{ !env.ACT }}
|
||||
shell: bash
|
||||
run: ./scripts/github/runner/relocate_docker_dataroot.sh
|
||||
|
||||
- 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: 🚀 Deploy host role
|
||||
timeout-minutes: 350
|
||||
shell: bash
|
||||
env:
|
||||
apps: ${{ matrix.apps }}
|
||||
variant: ${{ matrix.variant }}
|
||||
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()
|
||||
shell: bash
|
||||
run: ./scripts/tests/deploy/ci/assert_no_deprecation_warnings.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()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: role-runtimes-host-${{ matrix.apps }}
|
||||
path: /tmp/role-runtimes-${{ matrix.apps }}.csv
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: 🩺 Dump runner state (on failure)
|
||||
if: failure() || cancelled()
|
||||
timeout-minutes: 5
|
||||
shell: bash
|
||||
run: ./scripts/github/runner/dump_state.sh "post-deploy failure (${{ matrix.apps }})"
|
||||
|
||||
- name: ⬆️ Upload rescue diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: rescue-diagnostics-host-${{ matrix.apps }}${{ matrix.variant_slug && format('-{0}', matrix.variant_slug) || '' }}
|
||||
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: bash scripts/github/inventory/zip/compose_for_upload.sh
|
||||
|
||||
- name: ⬆️ Upload inventory artifact
|
||||
if: ${{ always() && !env.ACT }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: inventory-host-${{ matrix.apps }}
|
||||
path: /tmp/inventory-compose-${{ matrix.apps }}.zip
|
||||
if-no-files-found: ignore
|
||||
@@ -1,75 +0,0 @@
|
||||
name: "🏝️ Test: Deploy (single-node)"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
distros:
|
||||
description: "Space-separated distro list"
|
||||
required: true
|
||||
type: string
|
||||
whitelist:
|
||||
description: "Optional space-separated whitelist of app ids"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
lifecycles:
|
||||
description: "Lifecycle envelope for discovery (comma/space-separated)."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
modes:
|
||||
description: "Single-node deploy modes to run (space-separated subset of 'compose host')."
|
||||
required: false
|
||||
type: string
|
||||
default: "compose host"
|
||||
blacklist:
|
||||
description: "Optional space-separated app ids to drop from discovery"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
marker:
|
||||
description: "Optional suffix appended to the discover job names (e.g. ' ⭐' for the priority line)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
filesystem:
|
||||
description: "🗄️ Space-separated filesystems the per-distro pick draws from (ext4, btrfs, zfs). Empty: all three."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
tor:
|
||||
description: "🧅 Tor axis for the compose leg: auto | enforced | exclusive | disabled. The host leg has no onion."
|
||||
required: false
|
||||
type: string
|
||||
default: auto
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
compose:
|
||||
name: 📦 Compose
|
||||
if: ${{ contains(inputs.modes, 'compose') }}
|
||||
uses: ./.github/workflows/call-test-deploy-compose.yml
|
||||
with:
|
||||
distros: ${{ inputs.distros }}
|
||||
whitelist: ${{ inputs.whitelist }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
blacklist: ${{ inputs.blacklist }}
|
||||
marker: ${{ inputs.marker }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
tor: ${{ inputs.tor }}
|
||||
|
||||
host:
|
||||
name: 💻 Host
|
||||
if: ${{ contains(inputs.modes, 'host') }}
|
||||
uses: ./.github/workflows/call-test-deploy-host.yml
|
||||
with:
|
||||
distros: ${{ inputs.distros }}
|
||||
whitelist: ${{ inputs.whitelist }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
blacklist: ${{ inputs.blacklist }}
|
||||
marker: ${{ inputs.marker }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
exclude_compose_covered: ${{ contains(inputs.modes, 'compose') && 'true' || 'false' }}
|
||||
+111
-37
@@ -1,4 +1,4 @@
|
||||
name: "🐝 Test: Deploy (swarm)"
|
||||
name: "🚀 Test: Deploy"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -7,28 +7,41 @@ on:
|
||||
description: "Space-separated distro list (shown in the discover job name)"
|
||||
required: true
|
||||
type: string
|
||||
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
|
||||
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: ""
|
||||
blacklist:
|
||||
description: "Optional space-separated app ids to drop from discovery"
|
||||
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: ""
|
||||
default: "auto"
|
||||
filesystem:
|
||||
description: "🗄️ Space-separated filesystems the per-distro pick draws from (ext4, btrfs, zfs). Empty: all three."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
marker:
|
||||
description: "Optional suffix appended to the discover job name (e.g. ' ⭐' for the priority line)"
|
||||
description: "Optional suffix appended to the discover job name (e.g. ' ⭐' for a priority chunk)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
@@ -39,31 +52,48 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
distros:
|
||||
description: "Space-separated distro list (shown in the discover job name)"
|
||||
description: "🐧 Space-separated distro list"
|
||||
required: true
|
||||
type: string
|
||||
default: "debian"
|
||||
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: "Optional space-separated whitelist of app ids"
|
||||
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)."
|
||||
description: "♻️ Lifecycle envelope for discovery (comma/space-separated)."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
blacklist:
|
||||
description: "Optional space-separated app ids to drop from discovery"
|
||||
modes:
|
||||
description: "🚀 Deploy modes rows may be drawn from: 'auto', or a subset of 'host compose swarm'."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
default: "auto"
|
||||
filesystem:
|
||||
description: "🗄️ Space-separated filesystems the per-distro pick draws from (ext4, btrfs, zfs). Empty: all three."
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
tor:
|
||||
description: "🧅 auto onions every second tor-capable variant and flips with the run number; enforced onions every capable one; exclusive additionally drops the variants that cannot take an onion; disabled onions none."
|
||||
description: "🧅 Tor axis: 'auto' rotates the onion over the capable rows, 'enforced' onions all of them, 'exclusive' drops the rows that cannot, 'disabled' onions none."
|
||||
required: false
|
||||
type: choice
|
||||
default: auto
|
||||
options: [auto, enforced, exclusive, disabled]
|
||||
@@ -78,22 +108,21 @@ env:
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
INFINITO_IMAGE_TAG: ci-${{ github.sha }}
|
||||
INFINITO_GHCR_MIRROR_PREFIX: mirror
|
||||
INFINITO_PLAYWRIGHT_KEEP: ${{ vars.INFINITO_PLAYWRIGHT_KEEP }}
|
||||
INFINITO_APP_DISCOVERY_RUNNER: host
|
||||
INFINITO_DEPLOY_MODE: swarm
|
||||
INFINITO_DOCKER_FILESYSTEM_ALLOWED: ${{ inputs.filesystem }}
|
||||
INFINITO_LIFECYCLES: ${{ inputs.lifecycles }}
|
||||
INFINITO_BLACKLIST: ${{ inputs.blacklist }}
|
||||
|
||||
jobs:
|
||||
discover:
|
||||
name: 🚢 Swarm (${{ inputs.distros }})${{ inputs.marker }}
|
||||
name: 🔍 Chunk ${{ inputs.index }} (${{ inputs.distros }})${{ inputs.marker }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
outputs:
|
||||
apps: ${{ steps.discover.outputs.apps }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -103,10 +132,6 @@ jobs:
|
||||
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: 📦 Install deps (jq + envsubst)
|
||||
shell: bash
|
||||
run: ./scripts/install/apt.sh jq gettext-base
|
||||
@@ -133,36 +158,42 @@ jobs:
|
||||
INPUT_WHITELIST: ${{ inputs.whitelist }}
|
||||
run: ./scripts/github/resolve/effective_whitelist.sh
|
||||
|
||||
- name: 🔍 Discover apps
|
||||
- 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 }}
|
||||
RUN_NUMBER: ${{ github.run_number }}
|
||||
INFINITO_CI_SWEEP: ${{ inputs.sweep }}
|
||||
INFINITO_CI_CHUNK: ${{ inputs.index }}
|
||||
run: ./scripts/github/resolve/output_apps.sh
|
||||
|
||||
- name: 🗺️ Render the plan into the run summary
|
||||
shell: bash
|
||||
env:
|
||||
INFINITO_CI_SWEEP: ${{ inputs.sweep }}
|
||||
run: |
|
||||
source scripts/meta/env/load.sh
|
||||
"${PYTHON}" -m cli.meta.ci.plan \
|
||||
--distros "${{ inputs.distros }}" \
|
||||
--whitelist "${{ steps.resolve_wl.outputs.whitelist }}" \
|
||||
--priority "${{ inputs.blacklist }}" \
|
||||
--modes "swarm" \
|
||||
--priority "${{ inputs.priority }}" \
|
||||
--modes "${{ inputs.modes }}" \
|
||||
--lifecycles "${{ inputs.lifecycles }}" \
|
||||
--chunk "${{ inputs.index }}" \
|
||||
>> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
swarm:
|
||||
name: "🐝${{ matrix.label }}"
|
||||
deploy:
|
||||
name: "${{ matrix.label }}"
|
||||
if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 355
|
||||
needs: discover
|
||||
|
||||
concurrency:
|
||||
group: "infinito-deploy-swarm-${{ matrix.apps }}#${{ matrix.variant_slug }}"
|
||||
group: "infinito-deploy-${{ matrix.mode }}-${{ matrix.tor }}-${{ matrix.apps }}#${{ matrix.variant }}"
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
@@ -173,6 +204,7 @@ jobs:
|
||||
|
||||
env:
|
||||
APP_ID: ${{ matrix.apps }}
|
||||
INFINITO_DEPLOY_MODE: ${{ matrix.mode }}
|
||||
RESCHED_TIMEOUT: "120"
|
||||
SCRIPTS: scripts/tests/deploy/swarm/routine
|
||||
|
||||
@@ -191,7 +223,7 @@ jobs:
|
||||
|
||||
- name: "⬇️ Checkout"
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: "📥 Load rescue-diagnostics SPOT into job env"
|
||||
shell: bash
|
||||
@@ -207,15 +239,28 @@ jobs:
|
||||
shell: bash
|
||||
run: ./scripts/github/runner/relocate_docker_dataroot.sh
|
||||
|
||||
- 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: "🔍 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"
|
||||
@@ -223,6 +268,7 @@ jobs:
|
||||
cache-dependency-path: pyproject.toml
|
||||
|
||||
- name: 📦 Install project (host venv)
|
||||
if: ${{ matrix.mode == 'swarm' }}
|
||||
shell: bash
|
||||
run: |
|
||||
make install-force
|
||||
@@ -231,14 +277,31 @@ jobs:
|
||||
echo "${VENV}/bin" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: "🚀 Drill ${{ matrix.apps }} per distro: ${{ inputs.distros }}"
|
||||
if: ${{ matrix.mode == 'swarm' }}
|
||||
shell: bash
|
||||
env:
|
||||
variant: ${{ matrix.variant }}
|
||||
disable: ${{ matrix.disable }}
|
||||
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: ${{ matrix.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()
|
||||
if: ${{ always() && matrix.mode == 'swarm' }}
|
||||
shell: bash
|
||||
run: ./scripts/tests/deploy/utils/filesystem/report.sh "/tmp/ansible-${{ matrix.apps }}.log"
|
||||
|
||||
@@ -261,15 +324,21 @@ jobs:
|
||||
if: ${{ always() && !env.ACT }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: role-runtimes-swarm-${{ matrix.apps }}${{ matrix.variant_slug && format('-{0}', matrix.variant_slug) || '' }}
|
||||
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-swarm-${{ matrix.apps }}${{ matrix.variant_slug && format('-{0}', matrix.variant_slug) || '' }}
|
||||
name: rescue-diagnostics-${{ matrix.artifact }}
|
||||
path: ${{ env.INFINITO_RESCUE_DIAGNOSTICS_BASE }}/
|
||||
if-no-files-found: ignore
|
||||
|
||||
@@ -278,14 +347,19 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
APP_ID: ${{ matrix.apps }}
|
||||
run: bash scripts/github/inventory/zip/swarm_for_upload.sh
|
||||
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-swarm-${{ matrix.apps }}
|
||||
path: /tmp/inventory-swarm-${{ matrix.apps }}.zip
|
||||
name: inventory-${{ matrix.artifact }}
|
||||
path: ${{ env.INVENTORY_ZIP }}
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: "📋 Playwright step summary"
|
||||
@@ -300,13 +374,13 @@ jobs:
|
||||
|
||||
- name: "⬆️ Upload Playwright artifacts"
|
||||
if: ${{ always() && !env.ACT }}
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v7.0.1
|
||||
with:
|
||||
name: playwright-swarm-${{ matrix.apps }}${{ matrix.variant_slug && format('-{0}', matrix.variant_slug) || '' }}
|
||||
name: playwright-${{ matrix.artifact }}
|
||||
path: /tmp/playwright-artifacts/
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: "🧹 Cleanup"
|
||||
if: always()
|
||||
if: ${{ always() && matrix.mode == 'swarm' }}
|
||||
timeout-minutes: 15
|
||||
run: scripts/tests/deploy/swarm/utils/clean/teardown.sh
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "🕹️ Manual"
|
||||
run-name: "🕹️ ${{ inputs.mode_fail_fast && '🛑 ' || '' }}${{ inputs.sequencing != 'auto' && format('🐛{0} ', inputs.sequencing == 'serial' && '序' || '并') || '' }}${{ inputs.workspace != 'auto' && format('🧑💻{0} ', inputs.workspace == 'true' && '是' || '否') || '' }}${{ inputs.instructions != 'auto' && format('📖{0} ', inputs.instructions == 'true' && '是' || '否') || '' }}${{ inputs.tor != 'auto' && format('{0} ', inputs.tor == 'disabled' && '🌐' || inputs.tor == 'enforced' && '🧅强制' || '🧅独占') || '' }}${{ inputs.filesystem != '' && format('🗄️{0} ', inputs.filesystem) || '' }}${{ inputs.distros != '' && format('🐧{0} ', inputs.distros) || '' }}${{ inputs.lifecycles != '' && format('♻️{0} ', inputs.lifecycles) || '' }}${{ inputs.modes != 'swarm compose host' && format('🚀{0} ', inputs.modes) || '' }}${{ inputs.priority != '' && format('⭐{0} ', inputs.priority) || '' }}${{ inputs.whitelist != '' && format('🎯{0}', inputs.whitelist) || '🔀 diff (origin/main)' }}"
|
||||
run-name: "🕹️ ${{ inputs.chunk_gate && '🛑 ' || '' }}${{ inputs.resume_from_chunk != 0 && format('⏭️{0} ', inputs.resume_from_chunk) || '' }}${{ inputs.workspace != 'auto' && format('🧑💻{0} ', inputs.workspace == 'true' && '是' || '否') || '' }}${{ inputs.instructions != 'auto' && format('📖{0} ', inputs.instructions == 'true' && '是' || '否') || '' }}${{ inputs.tor != 'auto' && format('{0} ', inputs.tor == 'disabled' && '🌐' || inputs.tor == 'enforced' && '🧅强制' || '🧅独占') || '' }}${{ inputs.filesystem != '' && format('🗄️{0} ', inputs.filesystem) || '' }}${{ inputs.distros != '' && format('🐧{0} ', inputs.distros) || '' }}${{ inputs.lifecycles != '' && format('♻️{0} ', inputs.lifecycles) || '' }}${{ inputs.mode != 'auto' && format('🚀{0} ', inputs.mode) || '' }}${{ inputs.priority != '' && format('⭐{0} ', inputs.priority) || '' }}${{ inputs.whitelist != '' && format('🎯{0}', inputs.whitelist) || '🔀 diff (origin/main)' }}"
|
||||
|
||||
concurrency:
|
||||
group: global-ci-${{ github.repository }}-${{ github.ref_name }}
|
||||
@@ -14,7 +14,7 @@ on:
|
||||
default: ""
|
||||
type: string
|
||||
priority:
|
||||
description: "⭐ Roles to deploy first. The rest waits until they are green."
|
||||
description: "⭐ Roles to deploy first, in every combination they can take (each variant × each offered mode × Tor and clearnet). They get chunks of their own; the regular rows wait until those are green."
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
@@ -28,11 +28,16 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
modes:
|
||||
description: "🚀 Deploy modes to test. Only the listed ones run their deploy job."
|
||||
mode:
|
||||
description: "🚀 Deploy mode. 'auto' rotates every row over the modes its role offers; a single mode keeps only the rows that mode can take."
|
||||
required: false
|
||||
default: "swarm compose host"
|
||||
type: string
|
||||
default: "auto"
|
||||
type: choice
|
||||
options:
|
||||
- "auto"
|
||||
- "host"
|
||||
- "compose"
|
||||
- "swarm"
|
||||
filesystem:
|
||||
description: "🗄️ Space-separated filesystems the per-distro pick draws from (ext4, btrfs, zfs). Empty: all three."
|
||||
required: false
|
||||
@@ -48,20 +53,21 @@ on:
|
||||
- "enforced"
|
||||
- "exclusive"
|
||||
- "disabled"
|
||||
sequencing:
|
||||
description: "🐛 Deploy-mode order per line. 'serial': swarm, then compose, then host. 'parallel': all at once. 'auto': serial above 65 jobs per line."
|
||||
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."
|
||||
required: false
|
||||
default: "auto"
|
||||
type: choice
|
||||
options:
|
||||
- "auto"
|
||||
- "serial"
|
||||
- "parallel"
|
||||
mode_fail_fast:
|
||||
description: "🛑 Stop a serial line at its first failed mode. Off: keep deploying the remaining modes."
|
||||
default: 0
|
||||
type: number
|
||||
chunk_gate:
|
||||
description: "🛑 Stop the chunk chain at its first failed chunk. Off: keep deploying the remaining chunks."
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
sweep:
|
||||
description: "🔁 Sweep number driving the mode/tor rotation and the regular-line offset. Empty: the run number."
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
workspace:
|
||||
description: "🧑💻 Run the workspace test (dev-runtime image matrix). 'auto' runs it on a global scope only."
|
||||
required: false
|
||||
@@ -122,10 +128,11 @@ jobs:
|
||||
distros: ${{ needs.pick-distro.outputs.distros }}
|
||||
whitelist: ${{ inputs.whitelist }}
|
||||
lifecycles: ${{ inputs.lifecycles }}
|
||||
modes: ${{ inputs.modes }}
|
||||
mode: ${{ inputs.mode }}
|
||||
priority: ${{ inputs.priority }}
|
||||
sequencing: ${{ inputs.sequencing }}
|
||||
mode_fail_fast: ${{ inputs.mode_fail_fast }}
|
||||
sweep: ${{ inputs.sweep }}
|
||||
resume_from_chunk: ${{ inputs.resume_from_chunk }}
|
||||
chunk_gate: ${{ inputs.chunk_gate }}
|
||||
filesystem: ${{ inputs.filesystem }}
|
||||
workspace: ${{ inputs.workspace }}
|
||||
workspace_track: ${{ inputs.workspace_track }}
|
||||
|
||||
@@ -713,10 +713,10 @@ swarm-zombie: install-act
|
||||
SWARM_NAME=$(or $(name),$(app)); \
|
||||
INFINITO_SWARM_STEP_TIMEOUT_MINUTES=$(or $(step_timeout),690); \
|
||||
INFINITO_DISTROS=$(SWARM_DISTROS)" \
|
||||
ACT_WORKFLOW=.github/workflows/call-test-deploy-swarm.yml \
|
||||
ACT_JOB=swarm \
|
||||
ACT_MATRIX='apps:$(app);variant:$(or $(variant),0)' \
|
||||
ACT_INPUTS="whitelist=$(app) distros=$(SWARM_DISTROS)" \
|
||||
ACT_WORKFLOW=.github/workflows/call-test-deploy.yml \
|
||||
ACT_JOB=deploy \
|
||||
ACT_MATRIX='apps:$(app);variant:$(or $(variant),0);mode:swarm' \
|
||||
ACT_INPUTS="whitelist=$(app) distros=$(SWARM_DISTROS) index=0 sweep=0" \
|
||||
bash scripts/tests/deploy/act/workflow.sh
|
||||
|
||||
.PHONY: system-purge
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Resolve the app list and write it to GITHUB_OUTPUT.
|
||||
# Resolve the chunk's deploy matrix and write it to GITHUB_OUTPUT.
|
||||
# Inputs via env (forwarded to scripts/meta/resolve/apps.sh):
|
||||
# INFINITO_WHITELIST — optional space-separated allowlist
|
||||
# INFINITO_CI_CHUNK — chunk index to emit
|
||||
# INFINITO_CI_SWEEP — sweep number
|
||||
# INFINITO_WHITELIST / INFINITO_PRIORITY / INFINITO_MODES / INFINITO_TOR
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
# shellcheck source=scripts/meta/env/python.sh
|
||||
source "${REPO_ROOT}/scripts/meta/env/python.sh"
|
||||
|
||||
apps="$(./scripts/meta/resolve/apps.sh)"
|
||||
[[ -n "$apps" ]] || apps='[]'
|
||||
|
||||
matrix="$(printf '%s' "$apps" | "${PYTHON}" -m utils.github.variant.bundles)"
|
||||
matrix="$(./scripts/meta/resolve/apps.sh)"
|
||||
[[ -n "$matrix" ]] || matrix='[]'
|
||||
|
||||
echo "apps=$matrix" >>"$GITHUB_OUTPUT"
|
||||
|
||||
@@ -12,15 +12,23 @@
|
||||
# Inputs (environment variables):
|
||||
# APPS_JSON JSON array of app IDs from the discover step
|
||||
# CI_SELF_HOSTED_RUNNER_COUNT self-hosted runner instance count (default: 0)
|
||||
# INFINITO_CI_CONCURRENCY GitHub-hosted runner slots (SPOT: default.env)
|
||||
#
|
||||
# Outputs (appended to $GITHUB_OUTPUT):
|
||||
# apps_github JSON array routed to GitHub-hosted runners
|
||||
# apps_self_hosted JSON array routed to self-hosted runners ([] when disabled)
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
if [[ -f "${REPO_ROOT}/scripts/meta/env/load.sh" ]]; then
|
||||
# shellcheck source=scripts/meta/env/load.sh
|
||||
source "${REPO_ROOT}/scripts/meta/env/load.sh"
|
||||
fi
|
||||
|
||||
APPS_JSON="${APPS_JSON:-[]}"
|
||||
SELF_HOSTED_COUNT="${CI_SELF_HOSTED_RUNNER_COUNT:-0}"
|
||||
GITHUB_QUOTA=20
|
||||
GITHUB_QUOTA="${INFINITO_CI_CONCURRENCY:?INFINITO_CI_CONCURRENCY must be set (SPOT: default.env)}"
|
||||
|
||||
if [[ "${SELF_HOSTED_COUNT}" -le 0 ]]; then
|
||||
echo "apps_github=${APPS_JSON}" >>"${GITHUB_OUTPUT}"
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve the JSON list the CI matrix deploys for the current
|
||||
# INFINITO_DEPLOY_MODE. The query itself (filter, coverage-first sort,
|
||||
# lifecycle envelope, INFINITO_MAX_JOBS cap with 'auto') lives in
|
||||
# cli.meta.ci.query, shared with the deploy-plan table. Compose and host
|
||||
# emit whole role names; swarm emits per-variant "role#variant" tokens.
|
||||
# variant.bundles maps the list onto matrix entries.
|
||||
# Resolve the CI deploy matrix for one chunk of one sweep. The pipeline itself
|
||||
# (two discovery queries, the mode/tor rotation, the chunk split) lives in
|
||||
# cli.meta.ci.matrix, shared with the deploy-plan table, so the matrix this
|
||||
# emits and the plan the summary renders can never disagree. Every entry is one
|
||||
# role#variant row carrying the deploy mode it was assigned.
|
||||
#
|
||||
# Inputs via env (defaults live in default.env, the single source of truth):
|
||||
# INFINITO_DEPLOY_MODE compose|swarm|host (required; workflows set it)
|
||||
# INFINITO_CI_CHUNK chunk index to emit (required)
|
||||
# INFINITO_CI_SWEEP sweep number driving rotation and offset
|
||||
# INFINITO_MODES 'auto' or a subset of 'host compose swarm'
|
||||
# INFINITO_WHITELIST optional space-separated app ids to keep
|
||||
# INFINITO_BLACKLIST optional space-separated app ids to drop
|
||||
# INFINITO_MAX_JOBS cumulative job cap; 'auto' derives it per
|
||||
# mode from the CI chain via cli.meta.ci.slots
|
||||
# INFINITO_PRIORITY optional space-separated app ids to lead
|
||||
# INFINITO_TOR auto|enforced|exclusive|disabled
|
||||
# INFINITO_LIFECYCLES lifecycle envelope for discovery
|
||||
# INFINITO_DISCOVERY_SORT complexity --sort spec (coverage-first)
|
||||
# INFINITO_REQUIRED_STORAGE per-runner CI storage budget
|
||||
# INFINITO_APP_DISCOVERY_RUNNER host|docker
|
||||
#
|
||||
# Output: JSON array to stdout (single line, always valid).
|
||||
# Output: JSON array of matrix entries to stdout (single line, always valid).
|
||||
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
|
||||
@@ -52,23 +53,21 @@ run_meta_cli() {
|
||||
esac
|
||||
}
|
||||
|
||||
mode="${INFINITO_DEPLOY_MODE:?INFINITO_DEPLOY_MODE must be set to compose, swarm or host}"
|
||||
case "$mode" in
|
||||
compose | swarm | host) ;;
|
||||
*)
|
||||
echo "apps.sh: INFINITO_DEPLOY_MODE must be compose, swarm or host, got '$mode'" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
apps_json="$(run_meta_cli -m cli.meta.ci.query --mode "$mode" --format json)"
|
||||
|
||||
apps_json="$(printf '%s' "${apps_json}" | jq -c 'sort')"
|
||||
matrix_json="$(
|
||||
run_meta_cli -m cli.meta.ci.matrix \
|
||||
--index "${INFINITO_CI_CHUNK:?INFINITO_CI_CHUNK must be set to the chunk index to emit}" \
|
||||
--sweep "${INFINITO_CI_SWEEP}" \
|
||||
--modes "${INFINITO_MODES}" \
|
||||
--whitelist "${INFINITO_WHITELIST}" \
|
||||
--priority "${INFINITO_PRIORITY}" \
|
||||
--lifecycles "${INFINITO_LIFECYCLES}" \
|
||||
--tor "${INFINITO_TOR}"
|
||||
)"
|
||||
|
||||
if [[ -n "${GITHUB_ACTIONS:-}" && -z "${ACT:-}" ]]; then
|
||||
required_storage="${INFINITO_REQUIRED_STORAGE}"
|
||||
|
||||
mapfile -t roles < <(printf '%s\n' "${apps_json}" | jq -r '.[] | split("#")[0]' | sort -u)
|
||||
mapfile -t roles < <(printf '%s\n' "${matrix_json}" | jq -r '.[].apps' | sort -u)
|
||||
if [[ "${#roles[@]}" -gt 0 ]]; then
|
||||
run_meta_cli \
|
||||
-m cli.meta.roles.applications.sufficient_storage \
|
||||
@@ -87,12 +86,12 @@ if [[ -n "${GITHUB_ACTIONS:-}" && -z "${ACT:-}" ]]; then
|
||||
json_compact_array
|
||||
)"
|
||||
|
||||
apps_json="$(
|
||||
printf '%s' "${apps_json}" |
|
||||
matrix_json="$(
|
||||
printf '%s' "${matrix_json}" |
|
||||
jq -c --argjson keep "${kept_roles}" \
|
||||
'map(select(. as $t | $keep | index($t | split("#")[0]) != null))'
|
||||
'map(select(.apps as $a | $keep | index($a) != null))'
|
||||
)"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s\n' "${apps_json}" | json_compact_array
|
||||
printf '%s\n' "${matrix_json}" | json_compact_array
|
||||
@@ -173,7 +173,7 @@ the cluster declaration (image, containers, network, DNS play) in
|
||||
The matrix orchestrator
|
||||
(`utils/tests/swarm/matrix.py`) drives the deploy stage
|
||||
per variant round; `routine/00_one.sh` drives the surrounding stages for one
|
||||
distro, and the workflow `.github/workflows/call-test-deploy-swarm.yml` drives the
|
||||
distro, and the workflow `.github/workflows/call-test-deploy.yml` drives the
|
||||
distro loop. Run one app locally with
|
||||
`make swarm-zombie app=<id>` (keeps the cluster for inspection) or the
|
||||
whole matrix via `make roundtrip`.
|
||||
@@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
import unittest
|
||||
|
||||
from cli.administration.deploy.ci import runs
|
||||
from tests.utils.ci.job_names import ORCHESTRATOR_PREFIX, deploy_job_name
|
||||
from tests.utils.ci.job_names import deploy_job_name, orchestrator_prefix
|
||||
|
||||
MODES = ("docker", "swarm")
|
||||
MODES = ("docker", "swarm", "host")
|
||||
SAMPLE_APP = "web-app-matomo"
|
||||
SAMPLE_VARIANTS = ("", "0", "0,1", "0,1,2")
|
||||
SAMPLE_VARIANTS = ("", "0", "2")
|
||||
|
||||
|
||||
class TestCiDeployJobNamesParse(unittest.TestCase):
|
||||
@@ -31,7 +31,7 @@ class TestCiDeployJobNamesParse(unittest.TestCase):
|
||||
def test_failed_roles_detect_each_scope(self) -> None:
|
||||
jobs = [
|
||||
{
|
||||
"name": deploy_job_name(mode, SAMPLE_APP, "0,1"),
|
||||
"name": deploy_job_name(mode, SAMPLE_APP, "1"),
|
||||
"status": "completed",
|
||||
"conclusion": "failure",
|
||||
}
|
||||
@@ -44,8 +44,8 @@ class TestCiDeployJobNamesParse(unittest.TestCase):
|
||||
|
||||
def test_non_deploy_jobs_are_ignored(self) -> None:
|
||||
noise = [
|
||||
{"name": ORCHESTRATOR_PREFIX["docker"] + "⛵ Navigate composition"},
|
||||
{"name": ORCHESTRATOR_PREFIX["swarm"] + "🍯 Lure swarm"},
|
||||
{"name": orchestrator_prefix(0) + "⛵ Navigate composition"},
|
||||
{"name": orchestrator_prefix(1) + "🍯 Lure swarm"},
|
||||
{"name": "🐳 Update Docker image versions"},
|
||||
]
|
||||
self.assertEqual(runs.parse_role_statuses(noise), {})
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
"""The orchestrator's two deploy layouts must stay mutually exclusive.
|
||||
|
||||
Each line renders swarm plus either the parallel single-node job or the
|
||||
serial compose -> host chain; a `needs:` edge cannot be conditional, so both
|
||||
layouts exist as jobs and the sequencing decision skips one. Nothing at
|
||||
runtime proves the ten hand-written `if` expressions still agree with each
|
||||
other, and the failure is silent in both directions: two layouts deploying
|
||||
the same roles twice, or neither deploying anything.
|
||||
|
||||
These are structural assertions, not an expression evaluator. They catch the
|
||||
mistakes this shape actually invites -- a copied sentinel, a missing chain
|
||||
edge, a fail-fast guard pointing at the wrong predecessor.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from itertools import pairwise
|
||||
|
||||
from tests.utils import PROJECT_ROOT
|
||||
from utils.cache.yaml import load_yaml
|
||||
|
||||
ORCHESTRATOR = PROJECT_ROOT / ".github" / "workflows" / "call-orchestrator.yml"
|
||||
|
||||
LINES = {"priority": "-priority", "regular": ""}
|
||||
CHAIN = ("test-deploy-swarm", "test-deploy-compose", "test-deploy-host")
|
||||
PARALLEL = "test-deploy-single-node"
|
||||
|
||||
|
||||
def _jobs() -> dict:
|
||||
return load_yaml(str(ORCHESTRATOR))["jobs"]
|
||||
|
||||
|
||||
def _job(name: str, suffix: str) -> dict:
|
||||
return _jobs()[f"{name}{suffix}"]
|
||||
|
||||
|
||||
def _condition(name: str, suffix: str) -> str:
|
||||
return " ".join(str(_job(name, suffix).get("if", "")).split())
|
||||
|
||||
|
||||
def _needs(name: str, suffix: str) -> list[str]:
|
||||
declared = _job(name, suffix).get("needs", [])
|
||||
return [declared] if isinstance(declared, str) else list(declared)
|
||||
|
||||
|
||||
class TestLayoutsAreMutuallyExclusive(unittest.TestCase):
|
||||
def test_the_serial_chain_selects_on_the_serial_sentinel(self) -> None:
|
||||
for line, suffix in LINES.items():
|
||||
for name in CHAIN[1:]:
|
||||
with self.subTest(line=line, job=name):
|
||||
self.assertIn(
|
||||
f"needs.sequencing.outputs.{line} == 'serial'",
|
||||
_condition(name, suffix),
|
||||
)
|
||||
|
||||
def test_the_parallel_job_selects_on_the_parallel_sentinel(self) -> None:
|
||||
for line, suffix in LINES.items():
|
||||
with self.subTest(line=line):
|
||||
self.assertIn(
|
||||
f"needs.sequencing.outputs.{line} == 'parallel'",
|
||||
_condition(PARALLEL, suffix),
|
||||
)
|
||||
|
||||
def test_a_skipped_dependency_cannot_skip_a_serial_job(self) -> None:
|
||||
for line, suffix in LINES.items():
|
||||
for name in CHAIN[1:]:
|
||||
with self.subTest(line=line, job=name):
|
||||
self.assertIn("always()", _condition(name, suffix))
|
||||
|
||||
|
||||
class TestSerialChain(unittest.TestCase):
|
||||
def test_each_mode_waits_for_its_predecessor(self) -> None:
|
||||
for line, suffix in LINES.items():
|
||||
for predecessor, successor in pairwise(CHAIN):
|
||||
with self.subTest(line=line, job=successor):
|
||||
self.assertIn(f"{predecessor}{suffix}", _needs(successor, suffix))
|
||||
|
||||
def test_swarm_leads_and_waits_for_no_decision(self) -> None:
|
||||
"""It runs first in either layout, so making it wait for the
|
||||
sequencing job would delay every run for nothing."""
|
||||
for line, suffix in LINES.items():
|
||||
with self.subTest(line=line):
|
||||
self.assertNotIn("sequencing", _needs(CHAIN[0], suffix))
|
||||
|
||||
def test_fail_fast_guards_the_immediate_predecessor(self) -> None:
|
||||
for line, suffix in LINES.items():
|
||||
for predecessor, successor in pairwise(CHAIN):
|
||||
with self.subTest(line=line, job=successor):
|
||||
condition = _condition(successor, suffix)
|
||||
self.assertIn("inputs.mode_fail_fast", condition)
|
||||
self.assertIn(f"needs.{predecessor}{suffix}.result", condition)
|
||||
|
||||
|
||||
class TestRegularLineWaitsForPriority(unittest.TestCase):
|
||||
def test_every_priority_job_gates_every_regular_job(self) -> None:
|
||||
priority = [f"{name}-priority" for name in (PARALLEL, *CHAIN)]
|
||||
for name in (PARALLEL, *CHAIN):
|
||||
with self.subTest(job=name):
|
||||
self.assertLessEqual(set(priority), set(_needs(name, "")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -25,9 +25,7 @@ from . import PROJECT_ROOT
|
||||
HANDLERS = PROJECT_ROOT / "roles" / "sys-svc-compose" / "handlers"
|
||||
SWARM = HANDLERS / "swarm.yml"
|
||||
COMPOSE = HANDLERS / "compose.yml"
|
||||
COMPOSE_WORKFLOW = (
|
||||
PROJECT_ROOT / ".github" / "workflows" / "call-test-deploy-compose.yml"
|
||||
)
|
||||
DEPLOY_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "call-test-deploy.yml"
|
||||
|
||||
BUILDS = {
|
||||
"swarm: pre-deploy build of local images": (SWARM, "_swarm_pre_deploy_build"),
|
||||
@@ -57,13 +55,25 @@ def _fallbacks(path, var):
|
||||
|
||||
|
||||
def _longest_step_seconds(path):
|
||||
"""Longest per-step timeout the deploy workflow declares, in seconds.
|
||||
|
||||
Raises instead of falling back: an empty result means the workflow was
|
||||
renamed or its step timeouts vanished, and a silent 0 would let any build
|
||||
ladder pass the budget assertion below.
|
||||
"""
|
||||
document = load_yaml_any(str(path), default_if_missing={}) or {}
|
||||
return 60 * max(
|
||||
timeouts = [
|
||||
step["timeout-minutes"]
|
||||
for job in (document.get("jobs") or {}).values()
|
||||
for step in (job.get("steps") or [])
|
||||
if "timeout-minutes" in step
|
||||
)
|
||||
]
|
||||
if not timeouts:
|
||||
raise AssertionError(
|
||||
f"{path} declares no step-level 'timeout-minutes'; the compose "
|
||||
f"build-retry budget is derived from it."
|
||||
)
|
||||
return 60 * max(timeouts)
|
||||
|
||||
|
||||
def _by_name(path, name):
|
||||
@@ -87,7 +97,7 @@ class TestComposeBuildRetryBudget(unittest.TestCase):
|
||||
"INFINITO_CI_DISTRO_BUDGET_SECONDS"
|
||||
]
|
||||
)
|
||||
cap = _longest_step_seconds(COMPOSE_WORKFLOW)
|
||||
cap = _longest_step_seconds(DEPLOY_WORKFLOW)
|
||||
for name, (path, var) in BUILDS.items():
|
||||
task = _by_name(path, name)
|
||||
worst = (
|
||||
|
||||
+55
-27
@@ -4,6 +4,10 @@ Single point of truth for test fixtures so they cannot drift back to job
|
||||
names GitHub never emits -- the bug that silently made ``--failed swarm`` a
|
||||
no-op was masked by fixtures hand-typed as ``🐳 Compose web-app-x``, which no
|
||||
real job is ever called.
|
||||
|
||||
Since the deploy modes collapsed into one workflow, a job's mode is no longer
|
||||
in the workflow file name but in the row's own label, so the glyph prefix is
|
||||
built here the same way ``utils.github.variant.axes`` builds it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -12,61 +16,85 @@ import re
|
||||
|
||||
from tests.utils import PROJECT_ROOT
|
||||
from utils.cache.files import read_text
|
||||
from utils.github.variant.axes import TOR_DEPLOY_MODES
|
||||
from utils.roles.display import display_names
|
||||
from utils.symbol_glossary import to_emoji
|
||||
|
||||
WORKFLOWS = PROJECT_ROOT / ".github" / "workflows"
|
||||
|
||||
_DEPLOY = {
|
||||
"docker": ("call-test-deploy-compose.yml", "compose"),
|
||||
"swarm": ("call-test-deploy-swarm.yml", "swarm"),
|
||||
"host": ("call-test-deploy-host.yml", "host"),
|
||||
}
|
||||
ORCHESTRATOR_PREFIX = {
|
||||
"docker": "🎶 Orchestrate CI / test-deploy-compose / ",
|
||||
"swarm": "🎶 Orchestrate CI / test-deploy-swarm / ",
|
||||
"host": "🎶 Orchestrate CI / test-deploy-host / ",
|
||||
}
|
||||
DEPLOY_WORKFLOW = "call-test-deploy.yml"
|
||||
|
||||
_MODE_NAMES = {"docker": "compose", "swarm": "swarm", "host": "host"}
|
||||
|
||||
_NAME_RE = re.compile(r"name: (.+)$", re.MULTILINE)
|
||||
|
||||
|
||||
def _template(mode: str, job_id: str | None = None) -> str:
|
||||
workflow_file, deploy_job = _DEPLOY[mode]
|
||||
job_id = job_id or deploy_job
|
||||
def orchestrator_prefix(chunk: int = 0) -> str:
|
||||
"""The caller path GitHub prepends to a chunk's nested deploy jobs."""
|
||||
return f"🎶 Orchestrate CI / test-deploy-chunk-{chunk} / "
|
||||
|
||||
|
||||
def _template(job_id: str) -> str:
|
||||
block = re.search(
|
||||
rf"^ {job_id}:\n((?: .*\n)+)",
|
||||
read_text(str(WORKFLOWS / workflow_file)),
|
||||
read_text(str(WORKFLOWS / DEPLOY_WORKFLOW)),
|
||||
re.MULTILINE,
|
||||
)
|
||||
assert block, f"job '{job_id}' not found in {workflow_file}"
|
||||
assert block, f"job '{job_id}' not found in {DEPLOY_WORKFLOW}"
|
||||
name = _NAME_RE.search(block.group(1))
|
||||
assert name, f"no name: in job '{job_id}' of {workflow_file}"
|
||||
assert name, f"no name: in job '{job_id}' of {DEPLOY_WORKFLOW}"
|
||||
return name.group(1).strip().strip('"').strip("'")
|
||||
|
||||
|
||||
def discover_job_name(mode: str, distros: str, marker: str = "") -> str:
|
||||
"""The job display name GitHub emits for *mode*'s discover step.
|
||||
def discover_job_name(chunk: int, distros: str, marker: str = "") -> str:
|
||||
"""The job display name GitHub emits for a chunk's discover step.
|
||||
|
||||
Args:
|
||||
mode: ``'docker'`` (compose), ``'swarm'`` or ``'host'``.
|
||||
chunk: chunk index the block deploys.
|
||||
distros: the distro list the run swept.
|
||||
marker: line suffix, e.g. ``' ⭐'`` for the priority line.
|
||||
marker: suffix, e.g. ``' ⭐'`` for a priority chunk.
|
||||
"""
|
||||
rendered = _template(mode, "discover").replace("${{ inputs.distros }}", distros)
|
||||
rendered = _template("discover").replace("${{ inputs.index }}", str(chunk))
|
||||
rendered = rendered.replace("${{ inputs.distros }}", distros)
|
||||
return rendered.replace("${{ inputs.marker }}", marker)
|
||||
|
||||
|
||||
def row_label(
|
||||
mode: str, app: str, variant: str = "", *, tor: bool = False, priority: bool = False
|
||||
) -> str:
|
||||
"""The ``matrix.label`` axes assigns to one row: mode glyph, tor glyph on
|
||||
the modes that carry the onion axis, display name, and the priority star."""
|
||||
deploy_mode = _MODE_NAMES[mode]
|
||||
glyphs = to_emoji(deploy_mode)
|
||||
if deploy_mode in TOR_DEPLOY_MODES:
|
||||
glyphs += to_emoji("tor" if tor else "clearnet")
|
||||
label = f"{glyphs}{display_names().encode(app, variant)}"
|
||||
return f"{label} {to_emoji('priority')}" if priority else label
|
||||
|
||||
|
||||
def deploy_job_name(
|
||||
mode: str, app: str, variant: str = "", *, orchestrated: bool = True
|
||||
mode: str,
|
||||
app: str,
|
||||
variant: str = "",
|
||||
*,
|
||||
tor: bool = False,
|
||||
priority: bool = False,
|
||||
chunk: int = 0,
|
||||
orchestrated: bool = True,
|
||||
) -> str:
|
||||
"""The job display name GitHub emits for an ``app`` deploy in ``mode``.
|
||||
|
||||
Args:
|
||||
mode: ``'docker'`` (compose) or ``'swarm'``.
|
||||
mode: ``'docker'`` (compose), ``'swarm'`` or ``'host'``.
|
||||
app: role id, e.g. ``'web-app-matomo'``.
|
||||
variant: shard token GitHub appends, e.g. ``'0,1'`` (``''`` = single).
|
||||
variant: the row's variant index, e.g. ``'0'`` (``''`` = none).
|
||||
tor: whether the row deploys behind the node onion.
|
||||
priority: whether the row belongs to the priority line.
|
||||
chunk: chunk index, for the orchestrator prefix.
|
||||
orchestrated: include the ci-orchestrator caller prefix (real runs do).
|
||||
"""
|
||||
rendered = _template(mode).replace(
|
||||
"${{ matrix.label }}", display_names().encode(app, variant)
|
||||
rendered = _template("deploy").replace(
|
||||
"${{ matrix.label }}",
|
||||
row_label(mode, app, variant, tor=tor, priority=priority),
|
||||
)
|
||||
return (ORCHESTRATOR_PREFIX[mode] if orchestrated else "") + rendered
|
||||
return (orchestrator_prefix(chunk) if orchestrated else "") + rendered
|
||||
Reference in new issue
Block a user