Merge feature/web-app-openbao into feature/svc-net-wireguard

Brings this pre-swarm branch up to the current swarm-era codebase: 3681 commits
across 7221 files. Our side contributed almost nothing by comparison, so the
conflict surface stayed at the 17 files both sides had touched.

Resolutions:
- The 15 legacy svc-net-wireguard-{core,firewalled,plain} files conflicted as
  modify/delete and rename/delete (upstream renamed 01_core.yml to 00_core.yml).
  All resolved as deletions: this branch is starting wireguard over. Five more
  files of those roles came back silently because upstream had not touched them
  since the merge base, and were removed after the merge.
- sys-svc-container/templates/daemon.json.j2 took upstream's structure, which
  adds default-address-pools and json-file log limits and makes storage-driver
  conditional on DOCKER_IN_CONTAINER. Our unconditional version would have set
  fuse-overlayfs on bare-metal hosts too, so theirs is the correct semantics;
  only the value now routes through SYS_SVC_CONTAINER_STORAGE_DRIVER.
- sys-svc-container/vars/main.yml keeps that variable, which upstream never had
  and nothing supersedes, and adopts upstream's removal of the MTU comment.

Follow-up the merge itself required: the root README role table was regenerated
and the privileged_control_plane list in requirement 035 dropped from 24 to 21,
both of which still enumerated the deleted roles.
This commit is contained in:
evangelostsak committed 2026-09-18 00:41:03 +02:00
commit d783d83560
7221 files changed
+251391 -73442

No files matched your search

+6 -2
View File
@@ -14,10 +14,14 @@ exclude_paths:
- .cache/
- .claude/
- .github/
- tests/integration/fixtures/
- tests/lint/fixtures/
- /cli/
- compose/
- /docs/
- /tests/
- /utils/
- venv/
- .venv/
- build/
# Files that are not Ansible content but live under the same tree as roles
# and would otherwise be misclassified.
+24 -2
View File
@@ -12,14 +12,36 @@
roles/svc-ai-ollama/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/svc-db-mariadb/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/svc-db-openldap/tasks/roles/sys-svc-compose/tasks/utils/network/create.yml load-failure[filenotfounderror]
roles/svc-db-openldap/tasks/roles/sys-svc-compose/tasks/utils/post_deploy.yml load-failure[filenotfounderror]
roles/svc-db-postgres/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/svc-prx-openresty/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/sys-front-tls-letsencrypt/tasks/roles/sys-ca-selfsigned/tasks/disable_env.yml load-failure[filenotfounderror]
roles/sys-svc-compose/tasks/utils/network/roles/sys-svc-compose/tasks/utils/network/create.yml load-failure[filenotfounderror]
roles/sys-svc-mail-msmtp/tasks/roles/sys-svc-mail-smtp/tasks/disable.yml load-failure[filenotfounderror]
roles/web-app-bigbluebutton/tasks/roles/svc-bkp-container-2-local/tasks/04_seed-database-to-backup.yml load-failure[filenotfounderror]
roles/web-app-bigbluebutton/tasks/roles/svc-bkp-volume-2-local/tasks/03_seed-database-to-backup.yml load-failure[filenotfounderror]
roles/web-app-mailu/tasks/roles/sys-svc-mail-smtp/tasks/disable.yml load-failure[filenotfounderror]
roles/web-app-matrix/tasks/flavor/compose/roles/svc-bkp-container-2-local/tasks/04_seed-database-to-backup.yml load-failure[filenotfounderror]
roles/web-app-matrix/tasks/flavor/compose/roles/svc-bkp-volume-2-local/tasks/03_seed-database-to-backup.yml load-failure[filenotfounderror]
roles/web-app-prometheus/tasks/roles/sys-svc-compose/tasks/utils/network/create.yml load-failure[filenotfounderror]
roles/web-app-minio/tasks/roles/sys-svc-compose/tasks/utils/network/create.yml load-failure[filenotfounderror]
roles/web-app-seaweedfs/tasks/roles/sys-svc-compose/tasks/utils/network/create.yml load-failure[filenotfounderror]
roles/svc-db-mariadb/tasks/roles/sys-svc-compose/tasks/utils/swarm/resolve_container.yml load-failure[filenotfounderror]
roles/web-app-mediawiki/tasks/roles/sys-svc-compose/tasks/utils/finalize.yml load-failure[filenotfounderror]
roles/svc-db-elasticsearch/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/svc-db-memcached/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/svc-db-qdrant/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/svc-db-rabbitmq/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/svc-db-redis/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/svc-db-typesense/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/svc-dns-unbound/tasks/roles/sys-svc-compose/tasks/utils/network/routine.yml load-failure[filenotfounderror]
roles/sys-svc-compose/tasks/utils/swarm/roles/sys-svc-compose/tasks/utils/swarm/wait_converged.yml load-failure[filenotfounderror]
roles/web-app-decidim/tasks/roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml load-failure[filenotfounderror]
roles/web-app-discourse/tasks/roles/sys-svc-compose/tasks/utils/network/create.yml load-failure[filenotfounderror]
roles/web-app-fider/tasks/roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml load-failure[filenotfounderror]
roles/web-app-keycloak/tasks/04_realm_import/roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml load-failure[filenotfounderror]
roles/web-app-keycloak/tasks/utils/roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml load-failure[filenotfounderror]
roles/web-app-listmonk/tasks/roles/sys-svc-compose/tasks/utils/swarm/wait_converged.yml load-failure[filenotfounderror]
roles/web-app-matomo/tasks/utils/roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml load-failure[filenotfounderror]
roles/web-app-pixelfed/tasks/roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml load-failure[filenotfounderror]
# syntax-check ----------------------------------------------------------
# `keycloak_kcadm_update` is a role-local module shipped at
+3 -3
View File
@@ -1,3 +1,3 @@
launch.json
routines
workflows
*
!.gitignore
!settings.json
+12 -6
View File
@@ -3,6 +3,9 @@
# NEVER TOUCH THE .dockerignore, BECAUSE IT ANYHOW WILL BE OVERWRITTEN
site.retry
.cache
.ansible/
.stack-torn-down
*__pycache__
compose.cache.override.yml
venv
@@ -16,17 +19,20 @@ roles/importer_result.json
build/*
.ruff_cache
.agents
.claude/skills/
.claude-*
collections/
.env
.onion-identity/
custom.env
logs/
job-logs.txt
.claude/skills
.claude/agents
.claude/commands
.claude/hooks
.claude/settings.local.json
*.lock
node_modules/
vendor/
.phpunit.result.cache
inventories/production/
/core.*
# No-op for Git (ignored implicitly), but Docker needs .git in its ignore-list — keeps make dockerignore as a pure cat of this file.
.git
@@ -34,6 +40,7 @@ compose/coredns/Corefile
compose/Corefile
# Host shell / editor / tooling state accidentally surfaced inside this working copy
.ansible
.bash_profile
.bashrc
.gitconfig
@@ -45,4 +52,3 @@ compose/Corefile
.vscode
.zprofile
.zshrc
+1 -1
View File
@@ -88,7 +88,7 @@ Check the relevant rows and explain intentional omissions in `Additional Notes`.
| [ ] | `meta/server.yml` | Usually | Server-level config (file root IS `applications.<app>.server`). CSP, `domains`, `status_codes`, plus the per-role `networks.local.{subnet,dns_resolver}`. |
| [ ] | `meta/rbac.yml` | When the role declares RBAC | RBAC declarations (file root IS `applications.<app>.rbac`). |
| [ ] | `meta/volumes.yml` | When the role declares Compose volumes | Volumes map (file root IS the volumes map; no `compose:`/`volumes:` wrapper). |
| [ ] | `meta/schema.yml` | When the role declares credentials | Credential schema with optional `default:` field. |
| [ ] | `meta/secrets.yml` | When the role declares credentials | Credential schema with optional `default:` field. |
| [ ] | `tasks/main.yml` | Usually | Acts as the role entry point and includes the main task flow. |
| [ ] | `templates/compose.yml.j2` | For containerized app roles | Defines the service, volume, environment, port, and network wiring. |
| [ ] | `templates/env.j2` | When the app uses environment files | Renders the app environment configuration. |
+14 -8
View File
@@ -1,3 +1,4 @@
# nocheck: comments-valid intentional ecosystem catalogue; the commented stubs document which ecosystems are deliberately not scanned
version: 2
updates:
@@ -7,6 +8,7 @@ updates:
- package-ecosystem: "pip"
directories:
- "**"
- "**/*"
schedule:
interval: "daily"
time: "00:00"
@@ -21,6 +23,7 @@ updates:
- package-ecosystem: "github-actions"
directories:
- "**"
- "**/*"
schedule:
interval: "daily"
time: "00:00"
@@ -34,6 +37,7 @@ updates:
- package-ecosystem: "docker"
directories:
- "**"
- "**/*"
schedule:
interval: "daily"
time: "00:00"
@@ -47,6 +51,7 @@ updates:
- package-ecosystem: "docker-compose"
directories:
- "**"
- "**/*"
schedule:
interval: "daily"
time: "00:00"
@@ -64,6 +69,7 @@ updates:
- package-ecosystem: "pre-commit"
directories:
- "**"
- "**/*"
schedule:
interval: "daily"
time: "00:00"
@@ -72,15 +78,13 @@ updates:
- "ci"
# ------------------------------------------------------------
# npm / Node.js (package.json) — Bluesky login-broker has zero
# runtime npm deps but ships a package.json metadata stub; the
# repo-root package.json carries the ESLint + eslint-plugin-playwright
# dev-dependency pin used by `make lint-javascript`.
# npm / Node.js (package.json), scanned recursively so every role
# that ships its own package.json is covered automatically.
# ------------------------------------------------------------
- package-ecosystem: "npm"
directories:
- "/"
- "/roles/web-app-bluesky/files/login-broker"
- "**"
- "**/*"
schedule:
interval: "daily"
time: "00:00"
@@ -93,7 +97,8 @@ updates:
# ------------------------------------------------------------
- package-ecosystem: "composer"
directories:
- "/roles/web-app-joomla/files/joomla-oidc-plugin/plg_system_keycloak"
- "**"
- "**/*"
schedule:
interval: "daily"
time: "00:00"
@@ -176,7 +181,8 @@ updates:
# ------------------------------------------------------------
- package-ecosystem: "bundler"
directories:
- "/roles/web-app-openproject/files"
- "**"
- "**/*"
schedule:
interval: "daily"
time: "00:00"
+312
View File
@@ -0,0 +1,312 @@
# Workflow Dependency Map
How the workflows under `.github/workflows/` trigger and call each other.
Solid arrows: `workflow_call` (`uses:`) or job `needs`. Dotted arrows:
indirect coupling (CLI dispatch, shared concurrency group). Per-workflow
inputs and descriptions: [workflows.md](../../docs/contributing/tools/github/actions/workflows.md).
## CI pipeline
```mermaid
flowchart TB
push["push: main, feature/**, hotfix/**, fix/**"] --> epl["entry-push-latest.yml"]
pr["pull_request: opened, synchronize, reopened, ready_for_review"] --> eprc["entry-pr-change-orchestrate.yml"]
dispatch["workflow_dispatch"] --> eman["entry-manual-steer.yml"]
epl --> orch["ci-orchestrator.yml"]
epl -->|"version tag on main"| relv["release-version.yml"]
eprc --> orch
eprc -->|"fork PRs: privileged prebuild"| imgbuild["images-build-ci.yml"]
eprc -->|"fork PRs: privileged prebuild"| imgmirror["images-mirror-missing.yml"]
eman --> orch
subgraph orchestrator["ci-orchestrator.yml jobs"]
waitfork["wait-fork-prereq-run"] --> forkready["fork-prereqs-ready"]
lintwf["lint.yml: make lint + hadolint"]
testwf["test.yml: make test"]
codeql["cron-security-codeql.yml"]
buildci["build-ci-images: images-build-ci.yml"] --> dns["test-dns.yml"]
mirror["images-mirror-missing.yml"]
subgraph chain["serial chunk chain"]
chunk0["test-deploy-chunk-0"]
chunk0 --> chunk1["test-deploy-chunk-1"]
chunk1 --> chunk2["test-deploy-chunk-2"]
chunk2 --> chunk3["test-deploy-chunk-3"]
end
lintwf --> chain
testwf --> chain
dns --> chain
mirror --> chain
buildci --> chain
chunk0 --> smoke["test-runner-smoke.yml"]
chain --> report["report-main-failures"]
instmake["test-install-make.yml"]
instpkgmgr["test-install-pkgmgr.yml"]
mirror --> devenv["test-workspace: test-workspace.yml"]
chain --> donegate["done"]
smoke --> donegate
instmake --> donegate
instpkgmgr --> donegate
devenv --> donegate
end
chunk0 --> deploy["call-test-deploy.yml"]
chunk1 --> deploy
chunk2 --> deploy
```
## Sweeps and chunks
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.
### 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 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.
The chunk size is what makes that safe, and `cli.meta.ci.slots` derives it
rather than pinning it:
```text
waves = floor(INFINITO_CI_QUEUE_HOURS / the deploy job's timeout-minutes)
chunk size = INFINITO_CI_CONCURRENCY * waves
```
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.
### How many chunks a sweep spends
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. Declare one block more than
`available` fills (`ceil(available / chunk size) + 1`): the split receives
every declared block, so after a short priority chunk the regular rows still
fill the rest of the budget. 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.
### Selection tokens
An entry in `whitelist` or `priority` is a role name that MAY pin the axes the
row would otherwise be assigned. Two spellings are accepted:
| Form | Example |
|---|---|
| ASCII | `web-app-nextcloud#0,2@swarm+tor%debian/zfs` |
| the job label, pasted back out of the failed run | `🐳🧅🌀🦓网络应用·Nextcloud#0` |
| Separator | Axis | Values |
|---|---|---|
| `#` | variants | comma-separated indices |
| `@` | deploy mode | `compose`, `swarm`, `host` |
| `+` | onion state | `tor`, `clearnet` |
| `%` | distro | `arch`, `debian`, `ubuntu`, `fedora`, `centos` |
| `/` | filesystem | `zfs`, `btrfs`, `ext4` |
The onion state is spelled `+clearnet` rather than `-tor` because a role id may
itself end in `-tor`.
What a token leaves open belongs to the line it stands in: `priority` covers
every remaining combination in one sweep, `whitelist` keeps the rotation and
the row's position in the matrix. A pin the row cannot take aborts the matrix
rather than dropping the row, because a dropped row would report a green run
for a combination that never ran. Such a pin is `@swarm` on a role without its
own stack, `+tor` on a variant that pins `services.tor.enabled` false, or any
axis that fights the run's own `mode`, `tor`, `distros` or `filesystem` input.
That is also what a retrigger replays: `--failed` reads the glyphs off the failed
job's title and writes them back as a token, so the row comes back in the mode,
onion state and distribution it died on rather than on whatever the rotation
would pick next. The filesystem is deliberately left out. A title states the
kind the matrix *assigned*, which a deploy is allowed to fall back from, so
pinning it would both misstate what the job ran on and turn the kind into a
demand, failing the retrigger on the very condition the fallback absorbs.
### Mode, tor, distro and filesystem
The `tor` input decides what the onion axis is allowed to do at all:
| Value | Effect |
|---|---|
| `auto` | rotates; a priority row covers both states in one sweep |
| `enforced` | every capable row runs behind the onion |
| `exclusive` | as `enforced`, and rows that cannot take an onion are dropped |
| `disabled` | no row takes the onion |
`distros` and `filesystem` narrow the pools the other two axes draw from. Both
default to empty, which means the whole declared set. That is what a sweep
wants, because the rows are spread over the pool rather than all sharing one
value. Narrowing is for chasing a single distribution or a single filesystem.
Whether the assigned filesystem is binding depends on who chose it:
| How the row got its kind | The kind is | A host that cannot serve it |
|---|---|---|
| the rotation, from a pool of two or more (every automatic run) | a preference | falls back inside the pool, and says so in the step summary |
| the rotation, from a pool of exactly one (`filesystem: zfs`) | a demand | fails the row |
| a selection token (`role#0/zfs`) | a demand | fails the row |
A pool of two or more stays a preference because the matrix already narrowed
each row to one kind out of it: falling back to the other kind the run allowed
is inside what the operator asked for, while failing the row is not. A run whose
whole pool is unservable fails rather than leaving the pool.
The narrow case is what a deploy job actually sees, because the matrix hands
every row exactly one kind. Reading that narrowness as a demand would turn every
condition the applying step tolerates into a red row on every deploy: a loop
device it could not claim, a pool another job still holds. The run's own input
decides instead.
A fallback is reported, never silent. The job title and artifact name carry the
kind the matrix *assigned*, while
[docker_dataroot.sh](../../scripts/tests/deploy/utils/filesystem/docker_dataroot.sh)
writes the *effective* filesystem and the reason into the step summary. When a
run has to prove a filesystem-dependent path, read the summary rather than the
title, or name the kind on the run to make it a demand. The snapshot mode of
`svc-bkp-volume-2-local` is the case that matters: it only engages on btrfs and
zfs.
The userland tools are baked into every distro image by
[filesystem.sh](../../scripts/install/filesystem.sh); the kernel side comes from
the host.
For **regular** rows every axis is 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 |
|---|---|
| mode | `(position + sweep) % len(modes the role offers)` |
| tor | `(position + sweep // 2) % 2` |
| distro | `(position + sweep) % len(distro pool)` |
| filesystem | `(position // len(distro pool) + sweep) % len(filesystem pool)` |
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. Distro and
filesystem read the position like an odometer, the distro as the low digit, so
consecutive rows walk every pairing of the two pools rather than a diagonal
through it. Turning both on the position directly would cover only as many
pairings as the pools are long whenever they happen to be the same length, and
no sweep would unlock that, because the sweep shifts both by the same amount.
Priority rows skip the
mode/tor rotation entirely and take the whole cross-product at once; their
distro and filesystem walk on across those combinations, so one priority role
proves several distributions in one sweep.
Because the same variant can run several times in one sweep, the onion state is
part of what identifies a job: it is in the job label (`🧅` vs `🌐`) and in every
artifact name. Two jobs uploading under one artifact name is a conflict, not an
overwrite. The distro and
filesystem glyphs follow it in the label (`🐳🧅🌀🦓`), so a title says which
combination died without opening the job.
### Stopping on failure
`chunk_gate` (default `true`) decides whether the chain stops at its first
failed chunk:
| Value | Behaviour |
|---|---|
| `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 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. A retrigger carries the source run's
`chunk_gate` over unless `infinito administration deploy ci trigger
--chunk-gate false` overrides it; the `i8ciallon` alias is `i8ciall` with that
override, for sweeps that should report every chunk before anyone looks.
## Cancellation
```mermaid
flowchart TB
prclose["pull_request_target: closed, converted_to_draft"] --> eprcancel["entry-pr-closed-cancel-workflows.yml"]
branchdelete["delete: branch"] --> delbranch["entry-delete-branch.yml"]
newcommit["push (not main) / pull_request_target: opened, synchronize, reopened, ready_for_review, labeled"] --> superseded["entry-cancel-superseded.yml"]
eprcancel -.->|"cancels concurrency group"| runningci["running entry + child workflow runs"]
delbranch -.->|"cancels concurrency group"| runningci
superseded -.->|"cancels via API what the group left behind"| runningci
```
The concurrency group stays the primary mechanism; it reaps within seconds of a
run being created and needs no runner. `entry-cancel-superseded.yml` is the
fallback for the case where it does not, and carries no group of its own,
because the run holding a group cannot be the run that frees it.
A workflow the entries call declares no `concurrency` of its own: its jobs then
belong to the entry's group, and the entry's `cancel-in-progress` reaches them.
`call-images-build-ci.yml` and `call-images-mirror-missing.yml` still declare
one, so a run whose only live job is theirs stays outside the entry's group and
waits for the fallback.
## Scheduled and standalone
```mermaid
flowchart TB
daily["schedule: daily 00:00 UTC"] --> mirrorall["cron-images-mirror-all.yml"]
daily --> stale["cron-cleanup-stale.yml"]
daily --> relhighest["cron-release-highest.yml"]
weekly["schedule: weekly Sat 00:00 UTC"] --> updatewf["cron-update.yml"]
weekly --> cleanupci["cron-images-cleanup-ci.yml"]
weeklymon["schedule: weekly Mon 00:00 UTC"] --> scorecard["cron-security-scorecard.yml"]
branchprot["branch_protection_rule"] --> scorecard
pushmain["push: main"] --> updatewf
prtarget["pull_request_target: opened, reopened"] --> depclose["entry-pr-open-dependabot-close.yml"]
relhighest -.->|"gh workflow run"| relver["release-version.yml"]
relver --> imgbuildci["images-build-ci.yml"]
manual["workflow_dispatch"] --> mirrorcleanup["images-mirror-cleanup.yml"]
```
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-dns.yml`,
`test-workspace.yml`, `test-runner-smoke.yml`.
+153
View File
@@ -0,0 +1,153 @@
name: "🖼️ Build: CI Images (all distros)"
on:
workflow_call:
inputs:
distros:
description: "Space-separated distro list"
required: true
type: string
checkout_ref:
description: "Optional git ref to checkout before build (default: event ref)"
required: false
type: string
default: ""
source_repository:
description: "Repository to checkout as the build source (default: github.repository). Set to a fork only for maintainer-trusted PRs."
required: false
type: string
default: ""
image_tag:
description: "Optional image tag override (default: ci-${github.sha})"
required: false
type: string
default: ""
concurrency_channel:
description: "Optional channel to separate concurrency groups between callers"
required: false
type: string
default: "default"
allow_unsafe_checkout:
description: "Allow actions/checkout to fetch PR head/merge code under pull_request_target. Only set for maintainer-trusted fork PRs."
required: false
type: boolean
default: false
permissions:
contents: read
concurrency:
group: infinito-build-${{ github.repository }}-${{ inputs.concurrency_channel }}-${{ inputs.checkout_ref || github.ref }}
cancel-in-progress: false
jobs:
prepare:
name: 📋 Build Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.out.outputs.matrix }}
image_tag: ${{ steps.out.outputs.image_tag }}
steps:
- uses: actions/checkout@v7.0.0
- name: 📦 Install jq
run: ./scripts/install/apt.sh jq
- name: 🧮 Compute matrix JSON
id: out
run: scripts/github/runner/images_build_outputs.sh "${{ inputs.distros }}" "${{ inputs.image_tag }}"
build:
name: 🏗️ Build (${{ matrix.distro }})
needs: prepare
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
distro: ${{ fromJson(needs.prepare.outputs.matrix) }}
steps:
- name: ⬇️ Checkout trusted workflow repo (helpers)
uses: actions/checkout@v7.0.0
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: ⬇️ Checkout source to build
uses: actions/checkout@v7.0.0
with:
repository: ${{ inputs.source_repository || github.repository }}
ref: ${{ inputs.checkout_ref || github.ref }}
path: _src
persist-credentials: false
allow-unsafe-pr-checkout: ${{ inputs.allow_unsafe_checkout }}
- name: 💽 Disk pressure (start)
shell: bash
run: bash scripts/github/runner/disk_pressure_report.sh START
- name: 🔍 Resolve buildkit image ref
id: buildkit
shell: bash
run: |
set -euo pipefail
ref="$(bash scripts/meta/resolve/image/service_ref.sh svc-runner buildkit mirror)"
echo "ref=${ref}" >> "$GITHUB_OUTPUT"
- name: ⬇️ Pre-pull buildkit image (retry 7x, 20s)
shell: bash
env:
MAX_ATTEMPTS: "7"
RETRY_DELAY_SECONDS: "20"
run: bash scripts/github/runner/pull_with_retry.sh "${{ steps.buildkit.outputs.ref }}"
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
driver-opts: image=${{ steps.buildkit.outputs.ref }}
- name: 🔑 Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: 💽 Disk pressure (pre-build)
shell: bash
run: bash scripts/github/runner/disk_pressure_report.sh PRE-BUILD
- name: 🔍 Resolve repository owner
id: owner
shell: bash
run: |
set -euo pipefail
echo "owner=$(scripts/meta/resolve/repository/owner.sh)" >> "$GITHUB_OUTPUT"
- name: 🔨 Build & push (retry 7x, 20s)
if: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
shell: bash
env:
GITHUB_REPOSITORY_OWNER: ${{ steps.owner.outputs.owner }}
BUILD_CONTEXT_DIR: _src
MATRIX_DISTRO: ${{ matrix.distro }}
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
USE_NIX_TOKEN: ${{ !(github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository) }}
NIX_GITHUB_TOKEN: ${{ github.token }}
MAX_ATTEMPTS: "7"
RETRY_DELAY_SECONDS: "20"
run: ./scripts/image/push.sh
- name: 💽 Disk pressure (post-build)
if: always()
shell: bash
run: bash scripts/github/runner/disk_pressure_report.sh POST-BUILD
- name: 🧹 Cleanup (always)
if: always()
shell: bash
run: bash scripts/github/runner/cleanup_docker.sh
@@ -0,0 +1,160 @@
name: "🪞 Mirror: Docker Hub → GHCR (only missing)"
on:
workflow_call:
inputs:
ghcr_namespace:
description: "GHCR namespace/org (default: repository owner)"
required: false
type: string
ghcr_prefix:
description: "GHCR repo prefix (default: mirror)"
required: false
type: string
default: "mirror"
repo_root:
description: "Repository root path (default: .)"
required: false
type: string
default: "."
source_repository:
description: "Optional source repository for image config scan (e.g. fork owner/repo)"
required: false
type: string
default: ""
source_ref:
description: "Optional source ref/SHA for image config scan"
required: false
type: string
default: ""
source_path:
description: "Optional checkout path for source_repository/source_ref"
required: false
type: string
default: "_pr_source"
allow_unsafe_checkout:
description: "Allow actions/checkout to fetch PR head/merge code under pull_request_target. Only set for maintainer-trusted fork PRs."
required: false
type: boolean
default: false
images_per_hour:
description: "Optional throttle: max images mirrored per hour"
required: false
type: string
default: ""
concurrency_channel:
description: "Optional channel to separate concurrency groups between callers"
required: false
type: string
default: "default"
concurrency_key:
description: "Optional key (e.g. pr-123) to scope concurrency within a channel"
required: false
type: string
default: ""
image_wait_attempts:
description: "Polling attempts for waiting on mirrored images"
required: false
type: string
default: "1440"
image_wait_sleep_seconds:
description: "Polling interval in seconds for waiting on mirrored images"
required: false
type: string
default: "10"
secrets:
DOCKERHUB_USERNAME:
required: false
DOCKERHUB_TOKEN:
required: false
permissions:
contents: read
concurrency:
group: mirror-images-${{ github.repository }}-${{ inputs.concurrency_channel }}-${{ inputs.concurrency_key || github.ref_name }}
cancel-in-progress: true
jobs:
sync:
name: 🪞 Mirror
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
IMAGES_PER_HOUR: ${{ inputs.images_per_hour || '' }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
- name: 🔍 Resolve GHCR namespace and repository
id: vars
shell: bash
env:
GHCR_NAMESPACE_INPUT: ${{ inputs.ghcr_namespace || '' }}
run: bash scripts/meta/resolve/ghcr/namespace_and_repository.sh
- name: ⬇️ Checkout source repo for scan (optional)
if: ${{ inputs.source_repository != '' && inputs.source_ref != '' }}
uses: actions/checkout@v7.0.0
with:
repository: ${{ inputs.source_repository }}
ref: ${{ inputs.source_ref }}
path: ${{ inputs.source_path }}
fetch-depth: 1
persist-credentials: false
allow-unsafe-pr-checkout: ${{ inputs.allow_unsafe_checkout }}
- name: 🔧 Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: 📦 Install Python deps
run: PYTHON=python bash scripts/install/python.sh
- name: 📦 Install skopeo
run: ./scripts/install/apt.sh skopeo
- name: 🔑 Login to Docker Hub (optional)
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' && !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: 🔑 Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: ⏳ Wait for mirrored images (fork PR)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
env:
GHCR_NAMESPACE: ${{ steps.vars.outputs.ghcr_namespace }}
GHCR_REPOSITORY: ${{ steps.vars.outputs.ghcr_repository }}
GHCR_PREFIX: ${{ inputs.ghcr_prefix || 'mirror' }}
REPO_ROOT: ${{ inputs.repo_root || '.' }}
IMAGE_WAIT_ATTEMPTS: ${{ inputs.image_wait_attempts }}
IMAGE_WAIT_SLEEP_SECONDS: ${{ inputs.image_wait_sleep_seconds }}
run: ./scripts/meta/wait/mirrors.sh
- name: 🪞 Mirror images to GHCR (only missing, best-effort)
if: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
shell: bash
env:
GHCR_NAMESPACE: ${{ steps.vars.outputs.ghcr_namespace }}
GHCR_REPOSITORY: ${{ steps.vars.outputs.ghcr_repository }}
GHCR_PREFIX: ${{ inputs.ghcr_prefix || 'mirror' }}
REPO_ROOT: ${{ inputs.repo_root || '.' }}
run: bash scripts/meta/mirror/sync/missing.sh
+38
View File
@@ -0,0 +1,38 @@
name: "🧹 Lint"
on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
jobs:
lint:
name: 🧹 Lint
runs-on: ubuntu-latest
timeout-minutes: 45
env:
INFINITO_LINT_RUNNER: host
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
- name: 🔧 Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: 🧹 Run all lint checks via make
shell: bash
run: |
set -euo pipefail
make lint 2>&1 | tee /tmp/lint-output.txt
- name: 🔨 Build step summary from annotations
if: always()
working-directory: ${{ github.workspace }}
run: |
python3 -m utils.annotations.summarize /tmp/lint-output.txt "Lint Annotation Summary"
+362
View File
@@ -0,0 +1,362 @@
name: "🎵 CI: Orchestrator"
on:
workflow_call:
inputs:
distros:
description: "Distro pool the deploy matrix spreads its rows over. Empty: every declared distro, which is what a sweep wants. The rows rotate over the pool, so narrowing it only makes sense to chase one distribution."
required: false
type: string
default: ""
whitelist:
description: "Optional space-separated whitelist of app ids"
required: false
type: string
default: ""
lifecycles:
description: "Lifecycle envelope for test-deploy discovery (comma/space-separated)."
required: false
type: string
default: ""
mode:
description: "Deploy modes rows may be drawn from: 'auto' rotates every row over the modes its role offers; a subset of 'host compose swarm' keeps only the rows those modes can take."
required: false
type: string
default: "auto"
priority:
description: "Optional space-separated app ids for the priority line: sorted to the head of the list and cut into chunks of their own, so every priority row is deployed before the first regular one starts. Priority rows skip the sweep rotation and run every combination they can take (each variant, each offered mode, Tor and clearnet)."
required: false
type: string
default: ""
sweep:
description: "Sweep number driving the mode and tor rotation. Empty: the run number."
required: false
type: string
default: ""
offset:
description: "Regular rows to skip before filling the chunks. 0 starts at the head of the ranking; raise it to reach rows the budget leaves behind."
required: false
type: string
default: "0"
resume_from_chunk:
description: "⏭️ Skip every chunk below this index. Use to continue a sweep after fixing what broke it, instead of re-running the green chunks. Typed string, not number: a workflow_dispatch number input reaches the caller as a string, and handing that to a number-typed workflow_call input makes GitHub reject the job before it is created."
required: false
type: string
default: "0"
chunk_gate:
description: "🛑 Stop the chunk chain at its first failed chunk. Off: keep deploying the remaining chunks."
required: false
type: boolean
default: true
filesystem:
description: "🗄️ Filesystem pool the deploy matrix spreads its rows over (ext4, btrfs, zfs). Empty: all three."
required: false
type: string
default: ""
image_wait_attempts:
description: "Polling attempts for waiting on prebuilt or mirrored images"
required: false
type: string
default: "1980"
image_wait_sleep_seconds:
description: "Polling interval in seconds for waiting on prebuilt or mirrored images"
required: false
type: string
default: "10"
workspace:
description: "Run the workspace test (dev-runtime image matrix). 'auto' runs it on a global scope only, 'true'/'false' force it."
required: false
type: string
default: "auto"
workspace_track:
description: "Which workspace track each distro runs: 'rotate' splits them across compose and swarm and flips with the run number, 'compose'/'swarm' pin every distro."
required: false
type: string
default: "rotate"
tor:
description: "Tor axis of the deploy matrix: 'auto' rotates the onion over the tor-capable rows, 'enforced' onions all of them, 'exclusive' also drops the rows that cannot, 'disabled' onions none."
required: false
type: string
default: "auto"
trusted_pr:
description: "True when the PR carries the maintainer '🛡️ Trusted' label; skips the fork-prereq wait."
required: false
type: boolean
default: false
permissions:
actions: read
contents: read
packages: read
jobs:
validate-selection:
name: 🎯 Selection
uses: ./.github/workflows/call-validation.yml
with:
whitelist: ${{ inputs.whitelist }}
priority: ${{ inputs.priority }}
mode: ${{ inputs.mode }}
tor: ${{ inputs.tor }}
distros: ${{ inputs.distros }}
filesystem: ${{ inputs.filesystem }}
lifecycles: ${{ inputs.lifecycles }}
fork_prereqs: true
trusted_pr: ${{ inputs.trusted_pr }}
image_wait_attempts: ${{ inputs.image_wait_attempts }}
image_wait_sleep_seconds: ${{ inputs.image_wait_sleep_seconds }}
secrets: inherit
security-codeql:
needs: validate-selection
permissions:
contents: read
packages: read
security-events: write
uses: ./.github/workflows/cron-security-codeql.yml
lint:
needs: validate-selection
uses: ./.github/workflows/call-lint.yml
test:
needs: validate-selection
uses: ./.github/workflows/call-test.yml
build-ci-images:
needs: validate-selection
permissions:
contents: read
packages: write
uses: ./.github/workflows/call-images-build-ci.yml
with:
distros: "arch debian ubuntu fedora centos"
concurrency_channel: ci-orchestrator-build-consumer
secrets: inherit
test-dns:
needs:
- build-ci-images
- validate-selection
uses: ./.github/workflows/call-test-dns.yml
with:
distros: "arch debian ubuntu fedora centos"
images-mirror-missing:
needs: validate-selection
permissions:
contents: read
packages: write
uses: ./.github/workflows/call-images-mirror-missing.yml
with:
concurrency_channel: ci-orchestrator-mirror-consumer
image_wait_attempts: ${{ inputs.image_wait_attempts }}
image_wait_sleep_seconds: ${{ inputs.image_wait_sleep_seconds }}
secrets: inherit
test-deploy-chunk-0:
name: 🧱 Chunk 0
if: >-
${{ always()
&& inputs.resume_from_chunk <= 0
&& needs.lint.result == 'success'
&& needs.test.result == 'success'
&& needs.build-ci-images.result == 'success'
&& needs.images-mirror-missing.result == 'success'
&& needs.test-dns.result == 'success' }}
needs:
- lint
- test
- build-ci-images
- images-mirror-missing
- test-dns
- validate-selection
uses: ./.github/workflows/call-test-deploy.yml
with:
distros: ${{ inputs.distros }}
index: "0"
sweep: ${{ inputs.sweep != '' && inputs.sweep || github.run_number }}
offset: ${{ inputs.offset }}
whitelist: ${{ inputs.whitelist }}
priority: ${{ inputs.priority }}
lifecycles: ${{ inputs.lifecycles }}
modes: ${{ inputs.mode }}
filesystem: ${{ inputs.filesystem }}
tor: ${{ inputs.tor }}
test-deploy-chunk-1:
name: 🧱 Chunk 1
if: >-
${{ always()
&& inputs.resume_from_chunk <= 1
&& needs.lint.result == 'success'
&& needs.test.result == 'success'
&& needs.build-ci-images.result == 'success'
&& needs.images-mirror-missing.result == 'success'
&& needs.test-dns.result == 'success'
&& (!inputs.chunk_gate
|| contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-0.result)) }}
needs:
- lint
- test
- build-ci-images
- images-mirror-missing
- 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 }}
offset: ${{ inputs.offset }}
whitelist: ${{ inputs.whitelist }}
priority: ${{ inputs.priority }}
lifecycles: ${{ inputs.lifecycles }}
modes: ${{ inputs.mode }}
filesystem: ${{ inputs.filesystem }}
tor: ${{ inputs.tor }}
test-deploy-chunk-2:
name: 🧱 Chunk 2
if: >-
${{ always()
&& inputs.resume_from_chunk <= 2
&& needs.lint.result == 'success'
&& needs.test.result == 'success'
&& needs.build-ci-images.result == 'success'
&& needs.images-mirror-missing.result == 'success'
&& needs.test-dns.result == 'success'
&& (!inputs.chunk_gate
|| (contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-0.result)
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-1.result))) }}
needs:
- lint
- test
- build-ci-images
- images-mirror-missing
- test-dns
- test-deploy-chunk-0
- test-deploy-chunk-1
uses: ./.github/workflows/call-test-deploy.yml
with:
distros: ${{ inputs.distros }}
index: "2"
sweep: ${{ inputs.sweep != '' && inputs.sweep || github.run_number }}
offset: ${{ inputs.offset }}
whitelist: ${{ inputs.whitelist }}
priority: ${{ inputs.priority }}
lifecycles: ${{ inputs.lifecycles }}
modes: ${{ inputs.mode }}
filesystem: ${{ inputs.filesystem }}
tor: ${{ inputs.tor }}
test-deploy-chunk-3:
name: 🧱 Chunk 3
if: >-
${{ always()
&& inputs.resume_from_chunk <= 3
&& needs.lint.result == 'success'
&& needs.test.result == 'success'
&& needs.build-ci-images.result == 'success'
&& needs.images-mirror-missing.result == 'success'
&& needs.test-dns.result == 'success'
&& (!inputs.chunk_gate
|| (contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-0.result)
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-1.result)
&& contains(fromJSON('["success","skipped"]'), needs.test-deploy-chunk-2.result))) }}
needs:
- lint
- test
- build-ci-images
- images-mirror-missing
- test-dns
- test-deploy-chunk-0
- test-deploy-chunk-1
- test-deploy-chunk-2
uses: ./.github/workflows/call-test-deploy.yml
with:
distros: ${{ inputs.distros }}
index: "3"
sweep: ${{ inputs.sweep != '' && inputs.sweep || github.run_number }}
offset: ${{ inputs.offset }}
whitelist: ${{ inputs.whitelist }}
priority: ${{ inputs.priority }}
lifecycles: ${{ inputs.lifecycles }}
modes: ${{ inputs.mode }}
filesystem: ${{ inputs.filesystem }}
tor: ${{ inputs.tor }}
test-runner-smoke:
needs:
- 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 }}
secrets: inherit
test-install-make:
needs: validate-selection
uses: ./.github/workflows/call-test-install-make.yml
test-install-pkgmgr:
needs: validate-selection
uses: ./.github/workflows/call-test-install-pkgmgr.yml
test-workspace:
if: ${{ inputs.workspace == 'true' || (inputs.workspace == 'auto' && (inputs.whitelist == '' || inputs.whitelist == '__ALL__')) }}
uses: ./.github/workflows/call-test-workspace.yml
with:
workspace_track: ${{ inputs.workspace_track }}
needs:
- images-mirror-missing
- lint
- test
secrets: inherit
report-main-failures:
name: 🐞 Report main role failures
needs:
- test-deploy-chunk-0
- test-deploy-chunk-1
- test-deploy-chunk-2
- test-deploy-chunk-3
if: ${{ always() && github.ref == 'refs/heads/main' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
runs-on: ubuntu-latest
permissions:
issues: write
actions: read
contents: read
steps:
- uses: actions/checkout@v7.0.0
- name: 🐞 Open or append issues for failed roles
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 -m cli.meta.ci.report_failures --run-id "${{ github.run_id }}" --repo "${{ github.repository }}"
done:
name: 🏁 Finish pipeline
needs:
- validate-selection
- build-ci-images
- images-mirror-missing
- test-dns
- test-install-make
- test-install-pkgmgr
- test-deploy-chunk-0
- test-deploy-chunk-1
- test-deploy-chunk-2
- test-deploy-chunk-3
- test-runner-smoke
- test-workspace
- test
- lint
- security-codeql
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: 🛑 Fail if any upstream job failed or was cancelled
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: |
echo "Pipeline failed: at least one upstream job failed or was cancelled." >&2
exit 1
- name: ✅ Done
run: echo "All checks passed."
+174
View File
@@ -0,0 +1,174 @@
name: "🚀 Release: Version Logic"
on:
workflow_call:
inputs:
tag:
description: "Version tag to release (e.g. v1.2.3)"
required: true
type: string
workflow_dispatch:
inputs:
tag:
description: "🏷️ Version tag to release (e.g. v1.2.3)"
required: true
type: string
concurrency:
group: infinito-release
cancel-in-progress: false
permissions:
contents: read
packages: read
env:
REGISTRY: ghcr.io
DEFAULT_DISTRO: "debian"
jobs:
check-ci-images:
name: 🔎 Check CI images
runs-on: ubuntu-latest
outputs:
missing: ${{ steps.check.outputs.missing }}
ci_tag: ${{ steps.citag.outputs.ci_tag }}
distros: ${{ steps.vars.outputs.distros }}
repo_prefix: ${{ steps.vars.outputs.repo_prefix }}
owner: ${{ steps.vars.outputs.owner }}
version_tag: ${{ steps.vars.outputs.version_tag }}
publish_latest: ${{ steps.highest.outputs.publish_latest }}
steps:
- name: ⬇️ Checkout trusted workflow repo (helpers)
uses: actions/checkout@v7.0.0
with:
ref: ${{ github.sha }}
path: _helpers
persist-credentials: false
- name: 🔍 Resolve release inputs
id: vars
shell: bash
run: bash _helpers/scripts/github/resolve/release/inputs.sh "${{ inputs.tag || github.event.inputs.tag }}"
- name: ⬇️ Checkout repository at version tag
uses: actions/checkout@v7.0.0
with:
ref: ${{ steps.vars.outputs.version_tag }}
path: _src
fetch-depth: 0
fetch-tags: true
- name: 🧮 Compute CI tag (ci-<sha> of version tag)
id: citag
shell: bash
run: |
set -euo pipefail
sha="$(git -C _src rev-parse HEAD)"
echo "ci_tag=ci-${sha}" >> "$GITHUB_OUTPUT"
- name: 🤔 Decide whether this version should update latest
id: highest
shell: bash
env:
REPOSITORY_DIR: _src
VERSION_TAG: ${{ steps.vars.outputs.version_tag }}
run: _helpers/scripts/meta/resolve/publish_latest.sh
- name: 🔑 Login to GHCR (manifest checks)
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🔎 Check whether CI images already exist
id: check
shell: bash
env:
OWNER: ${{ steps.vars.outputs.owner }}
REPO_PREFIX: ${{ steps.vars.outputs.repo_prefix }}
INFINITO_DISTROS: ${{ steps.vars.outputs.distros }}
CI_TAG: ${{ steps.citag.outputs.ci_tag }}
run: |
set -euo pipefail
missing="$(_helpers/scripts/meta/resolve/missing/ci_images.sh)"
echo "missing=${missing}" >> "$GITHUB_OUTPUT"
build-ci-images-if-missing:
name: 🏗️ Build missing CI images
needs: check-ci-images
if: needs.check-ci-images.outputs.missing == 'true'
permissions:
contents: read
packages: write
uses: ./.github/workflows/call-images-build-ci.yml
with:
distros: ${{ needs.check-ci-images.outputs.distros }}
checkout_ref: ${{ needs.check-ci-images.outputs.version_tag }}
image_tag: ${{ needs.check-ci-images.outputs.ci_tag }}
concurrency_channel: release-backfill
secrets: inherit
release:
name: 🚀 Release version
needs:
- check-ci-images
- build-ci-images-if-missing
if: |
always() &&
needs.check-ci-images.result == 'success' &&
(
needs.build-ci-images-if-missing.result == 'success' ||
needs.build-ci-images-if-missing.result == 'skipped'
)
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
VERSION_TAG: ${{ needs.check-ci-images.outputs.version_tag }}
OWNER: ${{ needs.check-ci-images.outputs.owner }}
REPO_PREFIX: ${{ needs.check-ci-images.outputs.repo_prefix }}
INFINITO_DISTROS: ${{ needs.check-ci-images.outputs.distros }}
CI_TAG: ${{ needs.check-ci-images.outputs.ci_tag }}
PUBLISH_LATEST: ${{ needs.check-ci-images.outputs.publish_latest }}
steps:
- name: ⬇️ Checkout trusted workflow repo (helpers)
uses: actions/checkout@v7.0.0
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: 🔍 Resolve buildkit image ref
id: buildkit
shell: bash
run: |
set -euo pipefail
ref="$(bash scripts/meta/resolve/image/service_ref.sh svc-runner buildkit mirror)"
echo "ref=${ref}" >> "$GITHUB_OUTPUT"
- name: ⬇️ Pre-pull buildkit image (retry 7x, 20s)
shell: bash
env:
MAX_ATTEMPTS: "7"
RETRY_DELAY_SECONDS: "20"
run: bash scripts/github/runner/pull_with_retry.sh "${{ steps.buildkit.outputs.ref }}"
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
driver-opts: image=${{ steps.buildkit.outputs.ref }}
- name: 🔑 Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🏷️ Retag CI images to release tags (and latest for the highest version)
shell: bash
run: scripts/image/release.sh
+434
View File
@@ -0,0 +1,434 @@
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"
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
disable:
description: "Comma-separated provider keys removed from the test inventory; used when the matrix row carries none of its own."
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]
disable:
description: "🚫 Provider keys removed from the test inventory (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_PLAYWRIGHT_KEEP: ${{ vars.PLAYWRIGHT_KEEP_ALL }}
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-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: 🔧 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_CI_SWEEP: ${{ inputs.sweep }}
INFINITO_CI_CHUNK: ${{ inputs.index }}
INFINITO_CI_OFFSET: ${{ inputs.offset }}
run: ./scripts/github/resolve/output_apps.sh
- name: 🗺️ Render the plan into the run summary
shell: bash
env:
INFINITO_CI_SWEEP: ${{ inputs.sweep }}
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_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}" \
--offset "${PLAN_OFFSET}" \
>> "$GITHUB_STEP_SUMMARY"
deploy:
name: "${{ matrix.label }}"
if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]'
runs-on: ubuntu-latest
timeout-minutes: 355
needs: discover
# TODO: blocks the queue and prevents job cancelling.
# concurrency:
# group: "infinito-deploy-${{ matrix.mode }}-${{ matrix.tor }}-${{ matrix.apps }}#${{ matrix.variant }}"
# cancel-in-progress: false
# queue: max
strategy:
fail-fast: false
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: 💾 Enlarge swap space
if: ${{ !env.ACT }}
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: "🔍 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: ${{ matrix.disable || inputs.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 || 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
+65
View File
@@ -0,0 +1,65 @@
name: "💬 Test: DNS"
on:
workflow_dispatch:
inputs:
distros:
description: "🐧 Space-separated distro list (e.g. 'arch debian ubuntu')"
required: false
default: "debian"
type: string
workflow_call:
inputs:
distros:
description: "Space-separated distro list"
required: true
type: string
permissions:
contents: read
packages: read
jobs:
dns:
name: 🌐 DNS
runs-on: ubuntu-latest
timeout-minutes: 30
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
uses: actions/checkout@v7.0.0
- name: 🔑 Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🔍 Resolve image coordinates
shell: bash
run: |
set -euo pipefail
echo "OWNER=$(scripts/meta/resolve/repository/owner.sh)" >> "$GITHUB_ENV"
repo_name="${{ github.repository }}"
repo_name="${repo_name#*/}"
echo "REPO_NAME=${repo_name,,}" >> "$GITHUB_ENV"
- name: 🌐 DNS test per distro (parallel DinD)
shell: bash
env:
GHCR_USER: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./scripts/tests/dns/parallel.sh ${{ inputs.distros }}
@@ -0,0 +1,32 @@
name: "📦 Test: Install Make"
on:
workflow_call:
permissions:
contents: read
packages: read
jobs:
test-install-make:
name: 🛠️ Makefile
runs-on: ubuntu-latest
timeout-minutes: 30
env:
NIX_CONFIG: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
- name: 🔍 Resolve paths from default.env
run: |
for key in INFINITO_SRC_DIR INFINITO_VENV_DIR; do
value="$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' default.env)"
test -n "${value}" || { echo "${key} not found in default.env" >&2; exit 1; }
echo "${key}=${value}" >> "$GITHUB_ENV"
done
- name: 📦 Install & test in every pkgmgr virgin container (parallel)
run: ./scripts/tests/install/make.sh
@@ -0,0 +1,30 @@
name: "📦 Test: Install Package Manager"
on:
workflow_call:
permissions:
contents: read
packages: read
jobs:
test-install-pkgmgr:
name: 📦 PKGMGR
runs-on: ubuntu-latest
timeout-minutes: 30
env:
NIX_CONFIG: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
- name: 🔍 Resolve INFINITO_VENV_DIR from default.env
run: |
venv_dir="$(awk -F= '/^INFINITO_VENV_DIR=/{print $2; exit}' default.env)"
test -n "${venv_dir}" || { echo "INFINITO_VENV_DIR not found in default.env" >&2; exit 1; }
echo "INFINITO_VENV_DIR=${venv_dir}" >> "$GITHUB_ENV"
- name: 📦 Install & test in every pkgmgr virgin container (parallel)
run: ./scripts/tests/install/pkgmgr.sh
@@ -0,0 +1,51 @@
name: "🏃 Test: Runner smoke"
run-name: "🏃 Test: Runner smoke (self-hosted)"
on:
workflow_dispatch:
inputs:
ref:
description: "🔀 Git ref to check out; empty takes the repo default branch."
required: false
default: ""
type: string
workflow_call:
inputs:
ref:
description: "Git ref to check out (branch, tag, or SHA). Defaults to the repo default branch."
required: false
default: ""
type: string
permissions:
contents: read
packages: read
jobs:
smoke:
name: 🏃 Runner pickup + DooD
runs-on: [self-hosted, linux]
timeout-minutes: 345
steps:
- name: 🔧 Fix workspace ownership
shell: bash
run: sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
- uses: actions/checkout@v7.0.0
with:
ref: ${{ inputs.ref || github.sha }}
- name: ✅ Verify runner pickup + Docker (DooD) access
shell: bash
run: |
set -euo pipefail
echo ">>> Job picked up on self-hosted runner: $(hostname)"
docker version
docker info >/dev/null
docker run --rm hello-world
- name: 🔍 Dump runner state (on failure)
if: failure() || cancelled()
shell: bash
run: ./scripts/github/runner/dump_state.sh "runner-smoke failure"
+160
View File
@@ -0,0 +1,160 @@
name: "💻 Test: Workspace"
on:
workflow_dispatch:
inputs:
workspace_track:
description: "🧑‍💻 Track per distro: rotate splits compose/swarm, or pin one"
type: choice
default: rotate
options: [rotate, compose, swarm]
workflow_call:
inputs:
workspace_track:
type: string
default: rotate
permissions:
contents: read
packages: read
jobs:
prepare:
name: 🧑‍💻 Workspace Matrix
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
- name: 🐍 Assign one workspace track per distro
id: matrix
shell: bash
env:
WORKSPACE_TRACK: ${{ inputs.workspace_track }}
RUN_NUMBER: ${{ github.run_number }}
run: |
set -euo pipefail
matrix="$(python3 -m cli.meta.ci.dev_runtime matrix \
--mode "${WORKSPACE_TRACK:-rotate}" --run "${RUN_NUMBER:-0}")"
echo "matrix=${matrix}" | tee -a "$GITHUB_OUTPUT"
test-workspace:
name: ${{ matrix.icon }} Workspace (${{ matrix.label }})
runs-on: ubuntu-latest
timeout-minutes: 345
needs: prepare
concurrency:
group: infinito-workspace-${{ matrix.image }}-${{ matrix.track }}
cancel-in-progress: false
queue: max
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.prepare.outputs.matrix) }}
env:
DEV_RUNTIME_IMAGE: ${{ matrix.image }}
INFINITO_WORKSPACE_TRACKS: base ${{ matrix.track }}
NIX_CONFIG: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
INFINITO_CACHE_REGISTRY_MAX_SIZE: 16g
INFINITO_CACHE_REGISTRY_HOST_PATH: /mnt/cache/registry/mirror
INFINITO_CACHE_REGISTRY_CA_HOST_PATH: /mnt/cache/registry/ca
INFINITO_CACHE_PACKAGE_BLOBSTORE_MAX: 8g
INFINITO_CACHE_PACKAGE_HEAP: 1g
INFINITO_CACHE_PACKAGE_DIRECT_MEM: 1g
INFINITO_CACHE_PACKAGE_HOST_PATH: /mnt/cache/package/data
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: false
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
- name: "📥 Load the rescue paths into job env"
shell: bash
run: |
set -euo pipefail
make dotenv
{
grep '^INFINITO_RESCUE_DIAGNOSTICS_BASE=' default.env
grep '^INFINITO_DNS53_SAMPLER_LOG=' .env
echo "INFINITO_WORKSPACE_SLUG=$(printf '%s' "${DEV_RUNTIME_IMAGE}" | tr '/:' '--')-${{ matrix.track }}"
} >> "$GITHUB_ENV"
- name: "📥 Resolve the workspace distro"
shell: bash
run: |
set -euo pipefail
distro="$(python3 -m cli.meta.ci.dev_runtime distro --image "${DEV_RUNTIME_IMAGE}")"
echo "INFINITO_DISTRO=${distro}" >> "$GITHUB_ENV"
- name: ▶️ Run workspace routine
shell: bash
run: |
set -euo pipefail
HOST_WORKDIR="$(pwd -P)"
docker run --rm \
-e GITHUB_ACTIONS=true \
-e GITHUB_REPOSITORY="${{ github.repository }}" \
-e GITHUB_REPOSITORY_OWNER="${{ github.repository_owner }}" \
-e GITHUB_SHA="${{ github.sha }}" \
-e DEV_RUNTIME_IMAGE \
-e INFINITO_WORKSPACE_TRACKS \
-e INFINITO_DISTRO \
-e INFINITO_IMAGE="infinito-${INFINITO_DISTRO}" \
-e INFINITO_PULL_POLICY=never \
-e INFINITO_BUILD=1 \
-e NIX_CONFIG \
-e INFINITO_CACHE_REGISTRY_MAX_SIZE \
-e INFINITO_CACHE_REGISTRY_HOST_PATH \
-e INFINITO_CACHE_REGISTRY_CA_HOST_PATH \
-e INFINITO_CACHE_PACKAGE_BLOBSTORE_MAX \
-e INFINITO_CACHE_PACKAGE_HEAP \
-e INFINITO_CACHE_PACKAGE_DIRECT_MEM \
-e INFINITO_CACHE_PACKAGE_HOST_PATH \
--network host \
-v "${HOST_WORKDIR}:${HOST_WORKDIR}" \
-v "/mnt/docker:/var/lib/docker" \
-v "/var/run/docker.sock:/var/run/docker.sock" \
-w "${HOST_WORKDIR}" \
"${DEV_RUNTIME_IMAGE}" \
bash -lc '
bash ./scripts/tests/workspace/00_orchestrator.sh
'
- name: 🩺 Capture rescue diagnostics
if: failure() || cancelled()
timeout-minutes: 15
shell: bash
run: |
set -euo pipefail
rescue_dir="${INFINITO_RESCUE_DIAGNOSTICS_BASE:?}/${INFINITO_WORKSPACE_SLUG:?}"
INFINITO_RESCUE_DIAGNOSTICS_DIR="${rescue_dir}" \
python3 utils/diagnostics/container.py \
"${INFINITO_WORKSPACE_SLUG}" "workspace suite failure" || true
bash ./scripts/tests/deploy/utils/rescue_index.sh "${rescue_dir}"
- name: ⬆️ Upload rescue diagnostics
if: always()
uses: actions/upload-artifact@v7
with:
name: rescue-diagnostics-workspace-${{ env.INFINITO_WORKSPACE_SLUG }}
path: ${{ env.INFINITO_RESCUE_DIAGNOSTICS_BASE }}/
if-no-files-found: ignore
+44
View File
@@ -0,0 +1,44 @@
name: "🧪 Test"
on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
name: 🧪 Test
runs-on: ubuntu-latest
timeout-minutes: 45
env:
INFINITO_TEST_RUNNER: host
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
- name: 🔧 Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: ▶️ Run the full test suite via make
shell: bash
run: |
set -euo pipefail
make test 2>&1 | tee /tmp/test-output.txt
- name: 📊 Per-test summary from junit reports
if: always()
shell: bash
run: |
python3 utils/github/pytest_summary.py build/test-reports >> "$GITHUB_STEP_SUMMARY"
- name: 🔨 Build step summary from annotations
if: always()
working-directory: ${{ github.workspace }}
run: |
python3 -m utils.annotations.summarize /tmp/test-output.txt "Test Annotation Summary"
+204
View File
@@ -0,0 +1,204 @@
# The gate every entry point passes before it spends a runner on anything else.
# What it checks depends on the event and on the inputs it is handed, not on
# which workflow called it: a pull request gets its branch prefix and its merge
# base asserted, a run that carries a selection gets that selection checked
# against the branch it would deploy from, and a fork PR waits here for the
# privileged run's images. Nothing downstream may start before this is green,
# so a refusal fails the whole run instead of surfacing as one red job beside a
# chain that keeps going.
#
# Under pull_request_target the token is privileged and the head belongs to the
# fork, so the code under test is not checked out at all there and the branch
# prefix is validated from the calling ref's own tooling. Only the merge
# assertion and the selection check read the head, and both are reachable on
# pull_request and workflow_dispatch alone.
name: "✅ CI: Validation"
on:
workflow_call:
inputs:
pr_scope:
description: "🔎 Detected PR scope. Set: the branch prefix is validated against it. Empty: not a PR entry, skipped."
required: false
type: string
default: ""
whitelist:
description: "🎯 Selection to validate against this branch. Empty: nothing to check."
required: false
type: string
default: ""
priority:
description: "⭐ Priority line to validate against this branch. Empty: nothing to check."
required: false
type: string
default: ""
mode:
description: "🚀 Deploy modes the run draws from; narrows what a pinned mode may be."
required: false
type: string
default: "auto"
tor:
description: "🧅 Tor axis of the run; decides whether a pinned onion state is reachable."
required: false
type: string
default: "auto"
distros:
description: "🐧 Distro pool of the run; narrows what a pinned distro may be."
required: false
type: string
default: ""
filesystem:
description: "🗄️ Filesystem pool of the run; narrows what a pinned filesystem may be."
required: false
type: string
default: ""
lifecycles:
description: "♻️ Lifecycle envelope; a role outside it has no row to match."
required: false
type: string
default: ""
fork_prereqs:
description: "🍴 Wait for the privileged fork run's images. Off for entries that build their own."
required: false
type: boolean
default: false
trusted_pr:
description: "🛡️ True when the PR carries the maintainer Trusted label; skips the fork-prereq wait."
required: false
type: boolean
default: false
image_wait_attempts:
description: "🔁 Polling attempts while waiting on prebuilt or mirrored images."
required: false
type: string
default: "1980"
image_wait_sleep_seconds:
description: "⏱️ Polling interval in seconds while waiting on images."
required: false
type: string
default: "10"
permissions:
contents: read
jobs:
validation:
name: ✅ Validation
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout the calling ref for its tooling
uses: actions/checkout@v7.0.0
with:
path: .base-tooling
persist-credentials: false
- name: ⬇️ Checkout the code under test
if: ${{ github.event_name != 'pull_request_target' }}
uses: actions/checkout@v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: ${{ inputs.pr_scope != '' && '0' || '1' }}
path: pr-head
persist-credentials: false
- name: 🧭 Validate the branch prefix against the detected scope
if: ${{ inputs.pr_scope != '' }}
working-directory: .base-tooling
env:
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_SCOPE: ${{ inputs.pr_scope }}
run: ./scripts/meta/resolve/pr/branch_prefix.sh
- name: 🔀 Assert the base branch is fully merged into the PR head
if: ${{ github.event_name == 'pull_request' && inputs.pr_scope != '' }}
working-directory: pr-head
env:
INFINITO_MAIN_REMOTE: origin
INFINITO_MAIN_BRANCH: ${{ github.event.pull_request.base.ref }}
run: bash "${GITHUB_WORKSPACE}/.base-tooling/scripts/git/assert/main_merged.sh"
- name: 🔧 Set up Python
if: ${{ inputs.whitelist != '' || inputs.priority != '' }}
uses: actions/setup-python@v6
with:
python-version: "3.12"
# Exception: no pip cache. The steps below run the PR head's own code,
# which would write a cache that a default-branch run later restores.
- name: 📦 Install project (host venv)
if: ${{ inputs.whitelist != '' || inputs.priority != '' }}
working-directory: pr-head
shell: bash
run: make install
- name: 🎯 Validate the selection against this branch
if: ${{ inputs.whitelist != '' || inputs.priority != '' }}
working-directory: pr-head
shell: bash
env:
INPUT_WHITELIST: ${{ inputs.whitelist }}
INPUT_PRIORITY: ${{ inputs.priority }}
INPUT_MODE: ${{ inputs.mode }}
INPUT_TOR: ${{ inputs.tor }}
INPUT_DISTROS: ${{ inputs.distros }}
INPUT_FILESYSTEM: ${{ inputs.filesystem }}
INPUT_LIFECYCLES: ${{ inputs.lifecycles }}
run: ./scripts/github/validate/selection.sh
wait-fork-prereq-run:
name: ⏳ Wait for fork run
if: ${{ inputs.fork_prereqs }}
runs-on: ubuntu-latest
env:
WAIT_REQUIRED: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && !inputs.trusted_pr }}
steps:
- name: 🏷️ Mark fork prerequisites as ready outside fork PRs or for trusted PRs
if: ${{ env.WAIT_REQUIRED != 'true' }}
shell: bash
run: |
set -euo pipefail
echo "Fork prerequisites are implicitly ready (non-fork PR or '🛡️ Trusted' label)."
- name: ⬇️ Checkout trusted workflow repo (helpers)
if: ${{ env.WAIT_REQUIRED == 'true' }}
uses: actions/checkout@v7.0.0
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: 🔑 Login to GHCR for CI image checks
if: ${{ env.WAIT_REQUIRED == 'true' }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: ⏳ Wait for privileged fork CI images
if: ${{ env.WAIT_REQUIRED == 'true' }}
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_FILE: entry-pr-change-orchestrate.yml
TARGET_EVENT: pull_request_target
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_UPDATED_AT: ${{ github.event.pull_request.updated_at }}
INFINITO_DISTROS: "arch debian ubuntu fedora centos"
IMAGE_TAG: ci-${{ github.sha }}
WAIT_ATTEMPTS: ${{ inputs.image_wait_attempts }}
WAIT_SLEEP_SECONDS: ${{ inputs.image_wait_sleep_seconds }}
run: ./scripts/meta/wait/images.sh
fork-prereqs-ready:
name: 🚦 Gate Dependencies
needs: wait-fork-prereq-run
if: ${{ always() && inputs.fork_prereqs }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- name: ✅ Assert privileged prerequisites are ready
shell: bash
env:
RESULT: ${{ needs.wait-fork-prereq-run.result }}
run: ./scripts/github/gate/fork_prereqs.sh
-290
View File
@@ -1,290 +0,0 @@
name: "🎵 CI: Orchestrator"
on:
workflow_call:
inputs:
distros:
description: "Space-separated distro list"
required: false
type: string
default: "arch debian ubuntu fedora centos"
whitelist:
description: "Optional space-separated whitelist of app ids"
required: false
type: string
default: ""
image_wait_attempts:
description: "Polling attempts for waiting on prebuilt or mirrored images"
required: false
type: string
default: "1980"
image_wait_sleep_seconds:
description: "Polling interval in seconds for waiting on prebuilt or mirrored images"
required: false
type: string
default: "10"
roles_only:
description: "When 'true', skip the env-matrix test-development job."
required: false
type: string
default: "false"
trusted_pr:
description: "True when the PR carries the maintainer '🛡️ Trusted' label; skips the fork-prereq wait."
required: false
type: boolean
default: false
concurrency:
group: infinito-test-${{ github.ref }}
cancel-in-progress: false
permissions:
actions: write
contents: read
packages: write
security-events: write
jobs:
wait-fork-prereq-run:
name: ⏳ Wait for fork run
runs-on: ubuntu-latest
env:
WAIT_REQUIRED: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && !inputs.trusted_pr }}
steps:
- name: Mark fork prerequisites as ready outside fork PRs or for trusted PRs
if: ${{ env.WAIT_REQUIRED != 'true' }}
shell: bash
run: |
set -euo pipefail
echo "Fork prerequisites are implicitly ready (non-fork PR or '🛡️ Trusted' label)."
- name: Checkout trusted workflow repo (helpers)
if: ${{ env.WAIT_REQUIRED == 'true' }}
uses: actions/checkout@v7.0.0
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Login to GHCR for CI image checks
if: ${{ env.WAIT_REQUIRED == 'true' }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Wait for privileged fork CI images
if: ${{ env.WAIT_REQUIRED == 'true' }}
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_FILE: entry-pull-request-change.yml
TARGET_EVENT: pull_request_target
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_UPDATED_AT: ${{ github.event.pull_request.updated_at }}
INFINITO_DISTROS: "arch debian ubuntu fedora centos"
IMAGE_TAG: ci-${{ github.sha }}
WAIT_ATTEMPTS: ${{ inputs.image_wait_attempts }}
WAIT_SLEEP_SECONDS: ${{ inputs.image_wait_sleep_seconds }}
run: ./scripts/meta/wait/images.sh
fork-prereqs-ready:
name: 🚦 Gate Dependencies
needs: wait-fork-prereq-run
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Assert privileged prerequisites are ready
shell: bash
run: |
set -euo pipefail
result="${{ needs.wait-fork-prereq-run.result }}"
if [[ "${result}" != "success" && "${result}" != "skipped" ]]; then
echo "Fork prerequisites are not ready. wait-fork-prereq-run=${result}" >&2
exit 1
fi
echo "Fork prerequisites are ready."
# Security Checks
security-codeql:
uses: ./.github/workflows/security-codeql.yml
# Linting
lint-ansible:
uses: ./.github/workflows/lint-ansible.yml
lint-docker:
uses: ./.github/workflows/lint-docker.yml
lint-python:
uses: ./.github/workflows/lint-python.yml
lint-shell:
uses: ./.github/workflows/lint-shell.yml
lint-markdown:
uses: ./.github/workflows/lint-markdown.yml
lint-makefile:
uses: ./.github/workflows/lint-makefile.yml
lint-packages:
uses: ./.github/workflows/lint-packages.yml
lint-javascript:
uses: ./.github/workflows/lint-javascript.yml
lint-playwright:
uses: ./.github/workflows/lint-playwright.yml
lint-gate:
name: 🚦 Gate lint
needs:
- lint-ansible
- lint-python
- lint-docker
- lint-shell
- lint-markdown
- lint-makefile
- lint-packages
- lint-javascript
- lint-playwright
runs-on: ubuntu-latest
steps:
- run: echo "lint gate passed"
# Code Testing
test-code-integration:
uses: ./.github/workflows/test-code-integration.yml
test-code-lint:
uses: ./.github/workflows/test-code-lint.yml
test-code-external:
uses: ./.github/workflows/test-code-external.yml
test-code-unit:
uses: ./.github/workflows/test-code-unit.yml
test-code-gate:
name: 🚦 Gate code
needs: [test-code-integration, test-code-lint, test-code-external, test-code-unit]
runs-on: ubuntu-latest
steps:
- run: echo "code gate passed"
# Gate for the code Quality
code-quality-gate:
name: 🚦 Gate Quality
needs:
- lint-gate
- test-code-gate
- security-codeql
runs-on: ubuntu-latest
steps:
- run: echo "Quality Checks passed"
# Build Docker Images
build-ci-images:
needs:
- code-quality-gate
uses: ./.github/workflows/images-build-ci.yml
with:
distros: "arch debian ubuntu fedora centos"
concurrency_channel: ci-orchestrator-build-consumer
secrets: inherit
# Test DNS configuration
test-dns:
needs:
- build-ci-images
uses: ./.github/workflows/test-dns.yml
with:
distros: "arch debian ubuntu fedora centos"
# Sync Missing Images
images-mirror-missing:
uses: ./.github/workflows/images-mirror-missing.yml
with:
concurrency_channel: ci-orchestrator-mirror-consumer
image_wait_attempts: ${{ inputs.image_wait_attempts }}
image_wait_sleep_seconds: ${{ inputs.image_wait_sleep_seconds }}
secrets: inherit
# Deploy tests
test-deploy-server:
needs:
- test-dns
- images-mirror-missing
uses: ./.github/workflows/test-deploy-server.yml
with:
distros: ${{ inputs.distros }}
whitelist: ${{ inputs.whitelist }}
test-deploy-universal:
needs:
- test-dns
- images-mirror-missing
uses: ./.github/workflows/test-deploy-universal.yml
with:
distros: ${{ inputs.distros }}
whitelist: ${{ inputs.whitelist }}
test-runner-smoke:
needs:
- test-deploy-universal
# Skip when no self-hosted runners are configured (CI_SELF_HOSTED_RUNNER_COUNT = 0
# or unset) — without this guard the job queues indefinitely and times out.
# Run whenever universal actually executed (success or failure), not when it was
# skipped/cancelled. A partial universal failure (e.g. svc-db-mariadb) must not
# block the smoke test — only a svc-runner deploy failure will, because in that
# case no self-hosted runner picks up the job and it times out / fails correctly.
if: ${{ always() && vars.CI_SELF_HOSTED_RUNNER_COUNT != '0' && vars.CI_SELF_HOSTED_RUNNER_COUNT != '' && (needs.test-deploy-universal.result == 'success' || needs.test-deploy-universal.result == 'failure') }}
uses: ./.github/workflows/test-runner-smoke.yml
with:
ref: ${{ github.sha }}
secrets: inherit
test-deploy-workstation:
needs:
- code-quality-gate
- build-ci-images
uses: ./.github/workflows/test-deploy-workstation.yml
with:
distros: ${{ inputs.distros }}
whitelist: ${{ inputs.whitelist }}
# Test Installation
test-install-make:
uses: ./.github/workflows/test-install-make.yml
needs:
- code-quality-gate
test-install-pkgmgr:
uses: ./.github/workflows/test-install-pkgmgr.yml
needs:
- code-quality-gate
test-install-gate:
name: 🚦 Gate installation
needs:
- test-install-make
- test-install-pkgmgr
runs-on: ubuntu-latest
steps:
- run: echo "Installation is possible via pkgmgr and make"
# Test Development Environment
test-development:
if: ${{ inputs.roles_only != 'true' }}
uses: ./.github/workflows/test-environment.yml
needs:
- images-mirror-missing
- code-quality-gate
secrets: inherit
done:
name: 🏁 Finish pipeline
needs:
- test-deploy-server
- test-deploy-universal
- test-runner-smoke
- test-deploy-workstation
- test-install-gate
- test-development
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Fail if any upstream job failed or was cancelled
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: |
echo "Pipeline failed: at least one upstream job failed or was cancelled." >&2
exit 1
- name: Done
run: echo "All checks passed."
-83
View File
@@ -1,83 +0,0 @@
name: "🧹 Cleanup: Stale Repository Data"
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: write
issues: write
pull-requests: write
packages: write
jobs:
stale:
name: 🏷️ Close inactive issues and pull requests
runs-on: ubuntu-latest
steps:
- name: 🏷️ Mark stale issues and pull requests
uses: actions/stale@v10
with:
# Issues
days-before-issue-stale: 90
days-before-issue-close: 7
stale-issue-label: "stale"
stale-issue-message: >
This issue has been marked as stale because it has had no activity
for the last 90 days. It will be automatically closed in 7 days
unless there is further activity.
close-issue-message: >
This issue has been automatically closed due to inactivity.
# Pull Requests
days-before-pr-stale: 180
days-before-pr-close: 30
stale-pr-label: "stale"
stale-pr-message: >
This pull request has been marked as stale because it has had no
activity for the last 180 days. It will be automatically closed in
30 days unless there is further activity.
close-pr-message: >
This pull request has been automatically closed due to inactivity.
remove-stale-when-updated: true
delete-stale-branches:
name: 🌿 Delete inactive branches
runs-on: ubuntu-latest
steps:
- name: 🌿 Delete branches inactive for 360 days
uses: crs-k/stale-branches@e876b957ab08f0bad43c8cc271a22cdd7604bd09 # v9.0.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 350
days-before-delete: 360
branches-filter-regex: '^(?!(?:main|master|develop|dev)$)(?!release/)(?!hotfix/).*'
compare-branches: info
dry-run: false
cleanup-ghcr-ci-images:
name: 📦 Delete old GHCR CI images
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v7.0.0
- name: 📦 Install project environment
run: make install
- name: 📦 Delete ci-* images older than 60 days
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG: infinito-nexus
PACKAGE_PREFIX: core
MAX_AGE_DAYS: 60
TAG_PREFIX: ci-
run: bash scripts/github/cleanup/ghcr_ci_images.sh
+88
View File
@@ -0,0 +1,88 @@
name: "🧹 Prune stale data"
run-name: "${{ github.event_name == 'schedule' && '⏰ ' || '' }}🧹 Prune stale data"
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
stale:
name: 🏷️ Close inactive issues and pull requests
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: 🏷️ Mark stale issues and pull requests
uses: actions/stale@v10
with:
days-before-issue-stale: 90
days-before-issue-close: 7
stale-issue-label: "stale"
stale-issue-message: >
This issue has been marked as stale because it has had no activity
for the last 90 days. It will be automatically closed in 7 days
unless there is further activity.
close-issue-message: >
This issue has been automatically closed due to inactivity.
days-before-pr-stale: 180
days-before-pr-close: 30
stale-pr-label: "stale"
stale-pr-message: >
This pull request has been marked as stale because it has had no
activity for the last 180 days. It will be automatically closed in
30 days unless there is further activity.
close-pr-message: >
This pull request has been automatically closed due to inactivity.
remove-stale-when-updated: true
delete-stale-branches:
name: 🌿 Delete inactive branches
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
steps:
- name: 🌿 Delete branches inactive for 360 days
uses: crs-k/stale-branches@e876b957ab08f0bad43c8cc271a22cdd7604bd09 # v9.0.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 350
days-before-delete: 360
branches-filter-regex: '^(?!main$|master$|develop$|dev$|release/|hotfix/)'
compare-branches: info
dry-run: false
cleanup-ghcr-ci-images:
name: 📦 Delete old GHCR CI images
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
- name: 📦 Install project environment
run: make install
- name: 📦 Delete ci-* images older than 60 days
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG: infinito-nexus
PACKAGE_PREFIX: core
MAX_AGE_DAYS: 60
TAG_PREFIX: ci-
run: bash scripts/github/cleanup/ghcr_ci_images.sh
@@ -0,0 +1,61 @@
name: "🧹 Prune CI images"
run-name: "${{ github.event_name == 'schedule' && '⏰ ' || '' }}🧹 Prune CI images"
on:
schedule:
- cron: "0 0 * * 6"
workflow_dispatch:
inputs:
days:
description: "🧹 Delete CI images older than N days (default: 7)"
required: false
default: "7"
type: string
permissions:
contents: read
concurrency:
group: images-cleanup-ci
cancel-in-progress: false
jobs:
cleanup:
name: 🧹 Delete old ci-* image versions
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
packages: write
pull-requests: read
steps:
- uses: actions/checkout@v7.0.0
- name: 🔍 Resolve image metadata
id: vars
shell: bash
run: |
set -euo pipefail
source scripts/meta/env/load.sh
{
echo "owner=$(scripts/meta/resolve/repository/owner.sh)"
echo "repo_prefix=$(scripts/meta/resolve/repository/name.sh)"
echo "distros=${INFINITO_DISTROS}"
} >> "$GITHUB_OUTPUT"
- name: 📦 Install jq
shell: bash
run: ./scripts/install/apt.sh jq
- name: 🧹 Cleanup old ci-* versions
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OWNER: ${{ steps.vars.outputs.owner }}
DAYS: ${{ github.event.inputs.days || '7' }}
REPO_PREFIX: ${{ steps.vars.outputs.repo_prefix }}
INFINITO_DISTROS: ${{ steps.vars.outputs.distros }}
run: |
set -euo pipefail
./scripts/image/cleanup.sh
@@ -0,0 +1,119 @@
name: "🪞 Mirror all images"
run-name: "${{ github.event_name == 'schedule' && '⏰ ' || '' }}🪞 Mirror all images"
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
inputs:
ghcr_namespace:
description: "🏛️ GHCR namespace/org (default: repository owner)"
required: false
type: string
ghcr_prefix:
description: "🏷️ GHCR repo prefix (default: mirror)"
required: false
default: "mirror"
type: string
repo_root:
description: "📁 Repository root path (default: .)"
required: false
default: "."
type: string
images_per_hour:
description: "⏱️ Optional throttle: max images mirrored per hour"
required: false
type: string
workflow_call:
inputs:
ghcr_namespace:
description: "GHCR namespace/org (default: repository owner)"
required: false
type: string
ghcr_prefix:
description: "GHCR repo prefix (default: mirror)"
required: false
type: string
default: "mirror"
repo_root:
description: "Repository root path (default: .)"
required: false
type: string
default: "."
images_per_hour:
description: "Optional throttle: max images mirrored per hour"
required: false
type: string
secrets:
DOCKERHUB_USERNAME:
required: false
DOCKERHUB_TOKEN:
required: false
permissions:
contents: read
concurrency:
group: mirror-images-${{ github.repository }}-${{ github.ref_name }}
cancel-in-progress: false
jobs:
sync:
name: 🪞 Mirror
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
IMAGES_PER_HOUR: ${{ github.event.inputs.images_per_hour || inputs.images_per_hour || '50' }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
- name: 🔍 Resolve GHCR namespace and repository
id: vars
shell: bash
env:
GHCR_NAMESPACE_INPUT: ${{ inputs.ghcr_namespace || '' }}
run: bash scripts/meta/resolve/ghcr/namespace_and_repository.sh
- name: 🔧 Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: 📦 Install Python deps
run: PYTHON=python bash scripts/install/python.sh
- name: 📦 Install skopeo
run: ./scripts/install/apt.sh skopeo
- name: 🔑 Login to Docker Hub (optional)
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: 🔑 Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🪞 Mirror images to GHCR (best-effort)
shell: bash
env:
GHCR_NAMESPACE: ${{ steps.vars.outputs.ghcr_namespace }}
GHCR_REPOSITORY: ${{ steps.vars.outputs.ghcr_repository }}
GHCR_PREFIX: ${{ inputs.ghcr_prefix || 'mirror' }}
REPO_ROOT: ${{ inputs.repo_root || '.' }}
run: bash scripts/meta/mirror/sync/all.sh
@@ -0,0 +1,82 @@
name: "🚀 Release missing tag"
run-name: "${{ github.event_name == 'schedule' && '⏰ ' || '' }}🚀 Release missing tag"
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
concurrency:
group: infinito-release
cancel-in-progress: false
permissions:
contents: read
env:
REGISTRY: ghcr.io
jobs:
check-and-trigger:
name: 🔎 Backfill highest missing release tag
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
actions: write
steps:
- name: ⬇️ Checkout repository (with tags)
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
fetch-tags: true
- name: 🔍 Resolve image metadata
id: vars
shell: bash
run: |
set -euo pipefail
source scripts/meta/env/load.sh
owner="$(scripts/meta/resolve/repository/owner.sh)"
repo_prefix="$(scripts/meta/resolve/repository/name.sh)"
{
echo "owner=${owner}"
echo "repo_prefix=${repo_prefix}"
echo "distros=${INFINITO_DISTROS}"
} >> "$GITHUB_OUTPUT"
- name: 🔑 Login to GHCR (for manifest checks)
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🤔 Determine highest version tag with missing release image
id: pick
shell: bash
env:
OWNER: ${{ steps.vars.outputs.owner }}
REPO_PREFIX: ${{ steps.vars.outputs.repo_prefix }}
INFINITO_DISTROS: ${{ steps.vars.outputs.distros }}
run: |
set -euo pipefail
tag="$(scripts/meta/resolve/missing/highest_release_tag.sh)"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
- name: ✅ Ensure GitHub CLI is available
shell: bash
run: |
set -euo pipefail
if command -v gh >/dev/null 2>&1; then
exit 0
fi
./scripts/install/apt.sh gh
- name: ⚡ Trigger release workflow for missing tag
if: ${{ steps.pick.outputs.tag != '' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.pick.outputs.tag }}
run: gh workflow run release-version.yml -f tag="${TAG}"
+129
View File
@@ -0,0 +1,129 @@
# nocheck: comments-valid vendored CodeQL starter workflow; upstream comments kept verbatim
#
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "🔐 Scan with CodeQL"
run-name: "${{ github.event_name == 'schedule' && '⏰ ' || '' }}🔐 Scan with CodeQL"
concurrency:
group: codeql-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_call:
schedule:
- cron: '0 0 * * 6'
permissions:
contents: read
jobs:
analyze:
name: 🔐 Security
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
security-events: write
packages: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
- language: python
build-mode: none
- language: ruby
build-mode: none
# Disabled — no matching C/C++ files found in the repository
# - language: c-cpp
# build-mode: none
# Disabled — no matching C# files found in the repository
# - language: csharp
# build-mode: none
# Disabled — no matching Go files found in the repository
# - language: go
# build-mode: none
# Disabled — the bluesky Keycloak event-listener SPI bridge
# scaffold (the only Java source in the repo) was removed; no
# matching Java/Kotlin files remain. Re-enable once a real
# Java component (e.g. a built Keycloak SPI) lands again.
# - language: java-kotlin
# build-mode: none
# Disabled — no matching Rust files found in the repository
# - language: rust
# build-mode: none
# Disabled — no matching Swift files found in the repository
# - language: swift
# build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: 🎬 Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: ▶️ Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: ▶️ Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
env:
CODEQL_ACTION_FILE_COVERAGE_ON_PRS: "true"
@@ -0,0 +1,84 @@
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
# nocheck: comments-valid vendored OSSF scorecard workflow; upstream comments kept verbatim
name: "🛡️ Score supply chain"
run-name: "${{ github.event_name == 'schedule' && '⏰ ' || '' }}🛡️ Score supply chain"
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '00 00 * * *'
push:
branches: [ "main" ]
concurrency:
group: scorecard-${{ github.repository }}-${{ github.ref_name }}
cancel-in-progress: true
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: 🔍 Scorecard analysis
runs-on: ubuntu-latest
# `publish_results: true` only works when run from the default branch. conditional can be removed if disabled.
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
# Uncomment the permissions below if installing in a private repository.
# contents: read
# actions: read
steps:
- name: "📥 Checkout code"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4.2.2
with:
persist-credentials: false
- name: "🔬 Run analysis"
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.1
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecard on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore
# file_mode: git
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "📤 Upload artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4.6.1
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "📊 Upload to code-scanning"
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif
+160
View File
@@ -0,0 +1,160 @@
name: "🔄 Update versions"
run-name: "${{ github.event_name == 'schedule' && '⏰ ' || '' }}🔄 Update versions"
on:
push:
branches:
- main
schedule:
- cron: "0 0 * * 6"
workflow_dispatch:
concurrency:
group: update-versions-${{ github.repository }}-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
jobs:
update-docker-image-versions:
name: 🐳 Update Docker image versions
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
with:
ref: main
- name: 🔧 Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: 📦 Install update dependencies
run: bash scripts/github/update/install_python.sh
- name: 🔄 Update Docker image versions
run: bash scripts/github/update/image_versions.sh
- name: 🔎 Check for changes
id: diff
run: bash scripts/github/resolve/check_git_changes.sh roles
- name: ⚙️ Generate app token
if: steps.diff.outputs.changed == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
- name: 📂 Open PR if Docker image versions changed
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
UPDATE_BRANCH_PREFIX: update/docker-image-versions
UPDATE_COMMIT_MESSAGE: "update(images): update Docker image versions"
UPDATE_PR_TITLE: "update(images): update Docker image versions"
UPDATE_PR_BODY: "Automated update of semver-based Docker image versions in `roles/*/meta/services.yml`."
UPDATE_BASE_BRANCH: main
run: bash scripts/github/update/open/role_prs.sh roles
update-pip-pins:
name: 🐍 Update pinned pip requirements
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
with:
ref: main
- name: 🔧 Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: 📦 Install update dependencies
run: bash scripts/github/update/install_python.sh
- name: 🔄 Update pinned pip requirements
run: bash scripts/github/update/pip_pins.sh
- name: 🔎 Check for changes
id: diff
run: bash scripts/github/resolve/check_git_changes.sh roles
- name: ⚙️ Generate app token
if: steps.diff.outputs.changed == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
- name: 📂 Open PR if pip pins changed
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
UPDATE_BRANCH_PREFIX: update/pip-pins
UPDATE_COMMIT_MESSAGE: "update(pip): update pinned pip requirements"
UPDATE_PR_TITLE: "update(pip): update pinned pip requirements"
UPDATE_PR_BODY: "Automated update of exact pip pins in `roles/**/*.yml`."
UPDATE_BASE_BRANCH: main
run: bash scripts/github/update/open/role_prs.sh roles
update-repository-refs:
name: 🌿 Update repository refs
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
with:
ref: main
- name: 🔧 Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: 📦 Install update dependencies
run: bash scripts/github/update/install_python.sh
- name: 🔄 Update repository refs
run: bash scripts/github/update/repository_refs.sh
- name: 🔎 Check for changes
id: diff
run: bash scripts/github/resolve/check_git_changes.sh roles
- name: ⚙️ Generate app token
if: steps.diff.outputs.changed == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
- name: 📂 Open PR if repository refs changed
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
UPDATE_BRANCH_PREFIX: update/repository-refs
UPDATE_COMMIT_MESSAGE: "update(repos): bump git refs to latest semver tags"
UPDATE_PR_TITLE: "update(repos): bump git refs to latest semver tags"
UPDATE_PR_BODY: "Automated update of semver-based git `ref:` values in `roles/*/meta/services.yml`. Upstream tags resolved via `git ls-remote --tags`."
UPDATE_BASE_BRANCH: main
run: bash scripts/github/update/open/role_prs.sh roles
-35
View File
@@ -1,35 +0,0 @@
name: "🚫 Cancel: Runs on Branch Delete"
on:
delete:
# Enters the same concurrency group as entry-push-latest.yml for the deleted
# branch, which cancels that run and all its child workflow_call runs.
# NOTE: on `delete` events, `github.ref_name` resolves to the default branch
# (the workflow runs from the default branch context), not the deleted ref.
# Use `github.event.ref` to get the actually deleted branch name — otherwise
# this group would collide with the push-to-main CI and cancel it.
concurrency:
group: global-ci-${{ github.repository }}-${{ github.event.ref }}
cancel-in-progress: true
permissions:
actions: write
contents: read
jobs:
# Fallback: cancel any remaining active runs via the GitHub API.
cancel-branch-runs:
if: ${{ github.event.ref_type == 'branch' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Cancel active workflow runs for deleted branch
shell: bash
env:
BRANCH: ${{ github.event.ref }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
run: bash scripts/github/cancel/deleted_branch_runs.sh
-28
View File
@@ -1,28 +0,0 @@
name: "🚫 Dependabot: Close while auto-updates disabled"
# Closes Dependabot PRs while update PRs are gated off via the
# CI_ENABLE_AUTO_UPDATES repository variable. Dependabot itself cannot read
# repository variables, so the gate is enforced here.
#
# pull_request_target runs in base-branch context and therefore has
# access to repository vars/secrets and a write-scoped GITHUB_TOKEN
# even for Dependabot's PRs. This workflow does NOT check out PR code.
on:
pull_request_target:
types: [opened, reopened]
permissions:
pull-requests: write
jobs:
close-when-disabled:
name: 🚫 Close Dependabot PR while gated
if: github.event.pull_request.user.login == 'dependabot[bot]' && vars.CI_ENABLE_AUTO_UPDATES != 'true'
runs-on: ubuntu-latest
steps:
- name: Close PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: gh pr close "$PR_URL" --comment "Auto-closed by \`dependabot-close.yml\`. The repository variable \`CI_ENABLE_AUTO_UPDATES\` is not set to \`true\`, so update PRs are disabled. Set it to \`true\` under **Settings → Secrets and variables → Actions → Variables** to allow Dependabot PRs."
@@ -0,0 +1,76 @@
# Deliberately without a `concurrency:` block: this workflow frees the very
# groups that block other runs, so a group of its own would deadlock it.
name: "🚫 Cancel: Superseded Runs"
on:
push:
branches:
- "feature/**"
- "hotfix/**"
- "fix/**"
- "update/**"
- "chore/**"
- "alert-autofix-*"
pull_request_target:
types:
- opened
- synchronize
- reopened
- ready_for_review
- labeled
permissions:
contents: read
jobs:
cancel-branch-runs:
name: 🚫 Cancel superseded branch runs
if: ${{ github.event_name == 'push' && github.ref_name != 'main' }}
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
- name: 🚫 Cancel branch runs superseded by a newer one
shell: bash
env:
BRANCH: ${{ github.ref_name }}
CURRENT_RUN_ID: ${{ github.run_id }}
KEEP_NEWEST_PER: all
FORCE_CANCEL_AFTER_SECONDS: "60"
INCLUDE_PATHS: |
.github/workflows/entry-push-latest.yml
.github/workflows/entry-manual-steer.yml
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
run: bash scripts/github/cancel/branch_runs.sh
cancel-pull-request-runs:
name: 🚫 Cancel superseded pull request runs
if: ${{ github.event_name == 'pull_request_target' }}
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
pull-requests: read
steps:
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
- name: 🚫 Cancel pull request runs superseded by a newer one
shell: bash
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
CURRENT_RUN_ID: ${{ github.run_id }}
KEEP_NEWEST_PER: event
FORCE_CANCEL_AFTER_SECONDS: "60"
INCLUDE_PATHS: .github/workflows/entry-pr-change-orchestrate.yml
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
run: bash scripts/github/cancel/pull_request_runs.sh
+37
View File
@@ -0,0 +1,37 @@
# Enters the same concurrency group as entry-push-latest.yml for the deleted
# branch, which cancels that run and all its child workflow_call runs.
#
# On `delete` events `github.ref_name` resolves to the default branch, not the
# deleted ref, so the group keys on `github.event.ref`. Keying on `ref_name`
# would collide with the push-to-main CI and cancel it.
name: "🚫 Cancel: Runs on Branch Delete"
on:
delete:
concurrency:
group: global-ci-${{ github.repository }}-${{ github.event.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
cancel-branch-runs:
if: ${{ github.event.ref_type == 'branch' }}
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
- name: 🚫 Cancel active workflow runs for deleted branch
shell: bash
env:
BRANCH: ${{ github.event.ref }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
run: bash scripts/github/cancel/branch_runs.sh
@@ -0,0 +1,88 @@
name: "🧹 Images: Cleanup GHCR"
run-name: "🧹 Images: Cleanup GHCR (Private)"
on:
workflow_dispatch:
inputs:
ghcr_namespace:
description: "🏛️ GHCR namespace/org (default: repository owner)"
required: false
type: string
default: ""
ghcr_prefix:
description: "🏷️ Package name prefix to delete (default: mirror)"
required: false
type: string
default: "mirror"
visibility:
description: "👁️ Visibility filter: only these packages are deleted"
required: false
type: choice
default: "private"
options:
- private
- public
- internal
dry_run:
description: "🧪 Dry-run: list what would be deleted without deleting it"
required: false
type: boolean
default: true
token:
description: "🔑 Classic PAT with delete:packages. Empty: the default GITHUB_TOKEN"
required: false
type: string
default: ""
permissions:
contents: read
jobs:
cleanup:
name: 🗑️ Delete private GHCR mirror packages
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
- name: 🔍 Resolve GHCR namespace and repository
id: vars
shell: bash
run: |
set -euo pipefail
ghcr_namespace="${{ inputs.ghcr_namespace }}"
if [[ -z "${ghcr_namespace}" ]]; then
ghcr_namespace="$(scripts/meta/resolve/repository/owner.sh)"
fi
ghcr_repository="$(scripts/meta/resolve/repository/name.sh)"
echo "ghcr_namespace=${ghcr_namespace,,}" >> "$GITHUB_OUTPUT"
echo "ghcr_repository=${ghcr_repository}" >> "$GITHUB_OUTPUT"
- name: 🔧 Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: ▶️ Run mirror cleanup
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token != '' && inputs.token || secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
EXTRA_ARGS=()
if [[ "${{ inputs.dry_run }}" == "true" ]]; then
EXTRA_ARGS+=( --dry-run )
fi
python -m cli.contributing.mirror.cleanup \
--ghcr-namespace "${{ steps.vars.outputs.ghcr_namespace }}" \
--ghcr-prefix "${{ steps.vars.outputs.ghcr_repository }}/${{ inputs.ghcr_prefix }}" \
--visibility "${{ inputs.visibility }}" \
"${EXTRA_ARGS[@]}"
+115
View File
@@ -0,0 +1,115 @@
name: "🕹️ Manual"
run-name: "🕹️ ${{ inputs.chunk_gate && '🛑 ' || '' }}${{ inputs.offset != '0' && inputs.offset != '' && format('⏩{0} ', inputs.offset) || '' }}${{ inputs.workspace != 'auto' && format('🧑‍💻{0} ', inputs.workspace == '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 }}
cancel-in-progress: true
on:
workflow_dispatch:
inputs:
distros:
description: "🐧 Distro pool the rows are spread over. Empty: all five. Example: 'arch debian'"
required: false
default: ""
type: string
priority:
description: "⭐ Roles to lead, in every combination left open (see README)"
required: false
default: ""
type: string
whitelist:
description: "🎯 Roles to deploy. Empty: diff vs origin/main. '__ALL__': every role"
required: false
default: ""
type: string
lifecycles:
description: "♻️ Lifecycle envelope for test-deploy discovery (comma/space-separated)."
required: false
default: ""
type: string
mode:
description: "🚀 Deploy mode. 'auto' rotates a row over the modes its role offers"
required: false
default: "auto"
type: choice
options:
- "auto"
- "host"
- "compose"
- "swarm"
filesystem:
description: "🗄️ Filesystem pool the rows are spread over. Empty: ext4, btrfs, zfs"
required: false
default: ""
type: string
tor:
description: "🧅 Tor axis for swarm and compose; the four values are in the README"
required: false
default: "auto"
type: choice
options:
- "auto"
- "enforced"
- "exclusive"
- "disabled"
offset:
description: "⏩ Where chunk 0 starts: a row count, or a token like web-app-x#2"
required: false
default: "0"
type: string
chunk_gate:
description: "🛑 Stop the chunk chain at its first failed chunk"
required: false
default: true
type: boolean
sweep:
description: "🔁 Sweep number driving the mode and tor rotation. Empty: the run number"
required: false
default: ""
type: string
workspace:
description: "🧑‍💻 Run the workspace test. 'auto': on a global scope only"
required: false
default: "auto"
type: choice
options:
- "auto"
- "true"
- "false"
workspace_track:
description: "🧑‍💻 Workspace track per distro. 'rotate' splits compose and swarm"
required: false
default: "rotate"
type: choice
options:
- "rotate"
- "compose"
- "swarm"
permissions:
contents: read
jobs:
ci-orchestrator:
name: 🎛️ Orchestrate CI (manual)
permissions:
actions: read
contents: read
packages: write
security-events: write
issues: write
uses: ./.github/workflows/call-orchestrator.yml
with:
distros: ${{ inputs.distros }}
whitelist: ${{ inputs.whitelist }}
lifecycles: ${{ inputs.lifecycles }}
mode: ${{ inputs.mode }}
priority: ${{ inputs.priority }}
sweep: ${{ inputs.sweep }}
offset: ${{ inputs.offset }}
chunk_gate: ${{ inputs.chunk_gate }}
filesystem: ${{ inputs.filesystem }}
workspace: ${{ inputs.workspace }}
workspace_track: ${{ inputs.workspace_track }}
tor: ${{ inputs.tor }}
secrets: inherit
-36
View File
@@ -1,36 +0,0 @@
name: "⚡ CI: Manual"
run-name: "Manual CI triggered: ${{ inputs.distros }}; ${{ inputs.whitelist != '' && inputs.whitelist || 'diff-derived (origin/main)' }}"
concurrency:
group: global-ci-${{ github.repository }}-${{ github.ref_name }}
cancel-in-progress: true
on:
workflow_dispatch:
inputs:
distros:
description: "Space-separated distro list (overrides default). Example: 'arch debian'"
required: false
default: "debian"
type: string
whitelist:
description: "Empty: diff vs origin/main. '__ALL__': force full deploy. Else: explicit space-separated app ids."
required: false
default: ""
type: string
permissions:
contents: read
packages: write
security-events: write
actions: write
jobs:
ci-orchestrator:
name: 🎛️ Orchestrate CI (manual)
uses: ./.github/workflows/ci-orchestrator.yml
with:
distros: ${{ inputs.distros }}
whitelist: ${{ inputs.whitelist }}
roles_only: ${{ (inputs.whitelist != '' && inputs.whitelist != '__ALL__') && 'true' || 'false' }}
secrets: inherit
@@ -0,0 +1,171 @@
name: "🔀 Pull Request"
run-name: >-
${{ github.event_name }} / PR #${{ github.event.pull_request.number }} / ${{ github.event.action }} / ${{ github.event.pull_request.head.label }}
on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
- labeled
pull_request_target:
types:
- opened
- synchronize
- reopened
- ready_for_review
- labeled
concurrency:
group: pr-ci-${{ github.repository }}-${{ github.event_name }}-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
jobs:
detect-pr-scope:
name: 🔎 Detect PR scope
if: ${{ github.event.pull_request.draft == false && (github.event_name == 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository) }}
runs-on: ubuntu-latest
outputs:
scope: ${{ steps.resolve.outputs.scope }}
run_ci_orchestrator: ${{ steps.resolve.outputs.run_ci_orchestrator }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
- name: 🔍 Resolve PR scope
id: resolve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: ./scripts/meta/resolve/pr/scope.sh
validation:
name: 🧭 PR scope
needs: detect-pr-scope
if: ${{ github.event_name == 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository }}
uses: ./.github/workflows/call-validation.yml
with:
pr_scope: ${{ needs.detect-pr-scope.outputs.scope }}
pr-scope-gate:
name: 🚦 Gate PR scope
needs:
- detect-pr-scope
- validation
if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'false' }}
runs-on: ubuntu-latest
steps:
- name: ✅ Confirm lightweight PR scope
shell: bash
run: |
set -euo pipefail
echo "Lightweight PR scope '${{ needs.detect-pr-scope.outputs.scope }}' passed."
detect-affected-roles:
name: 🧭 Detect affected roles
needs:
- detect-pr-scope
- validation
if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }}
runs-on: ubuntu-latest
outputs:
whitelist: ${{ steps.resolve.outputs.whitelist }}
steps:
- name: ⬇️ Checkout
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 affected roles
id: resolve
shell: bash
env:
SUBSET_LABELED: ${{ contains(github.event.pull_request.labels.*.name, '🧩 Subset') }}
PR_BODY: ${{ github.event.pull_request.body }}
run: bash scripts/github/resolve/pr_affected_roles.sh
ci-orchestrator:
needs:
- detect-pr-scope
- validation
- detect-affected-roles
if: ${{ github.event_name == 'pull_request' && needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }}
permissions:
actions: read
contents: read
packages: write
security-events: write
issues: write
uses: ./.github/workflows/call-orchestrator.yml
with:
distros: "arch debian ubuntu fedora centos"
whitelist: ${{ needs.detect-affected-roles.outputs.whitelist }}
trusted_pr: ${{ contains(github.event.pull_request.labels.*.name, '🛡️ Trusted') }}
prepare-fork-merge-ref:
needs:
- detect-pr-scope
- validation
if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }}
runs-on: ubuntu-latest
outputs:
checkout_ref: ${{ steps.out.outputs.checkout_ref }}
image_tag: ${{ steps.out.outputs.image_tag }}
trusted: ${{ contains(github.event.pull_request.labels.*.name, '🛡️ Trusted') }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
- name: 🔍 Resolve merge ref and image tag
id: out
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha || '' }}
run: ./scripts/meta/resolve/pr/merge_ref.sh
build-ci-images-fork:
needs: prepare-fork-merge-ref
permissions:
contents: read
packages: write
uses: ./.github/workflows/call-images-build-ci.yml
with:
distros: "arch debian ubuntu fedora centos"
checkout_ref: ${{ needs.prepare-fork-merge-ref.outputs.trusted == 'true' && github.event.pull_request.head.sha || needs.prepare-fork-merge-ref.outputs.checkout_ref }}
source_repository: ${{ needs.prepare-fork-merge-ref.outputs.trusted == 'true' && github.event.pull_request.head.repo.full_name || github.repository }}
image_tag: ${{ needs.prepare-fork-merge-ref.outputs.image_tag }}
concurrency_channel: pr-target-build-producer
allow_unsafe_checkout: true
images-mirror-missing-fork:
needs: prepare-fork-merge-ref
permissions:
contents: read
packages: write
uses: ./.github/workflows/call-images-mirror-missing.yml
with:
repo_root: _pr_source
source_repository: ${{ github.repository }}
source_ref: ${{ needs.prepare-fork-merge-ref.outputs.checkout_ref }}
source_path: _pr_source
concurrency_channel: pr-target-mirror-producer
concurrency_key: pr-${{ github.event.pull_request.number }}
allow_unsafe_checkout: true
secrets: inherit
@@ -0,0 +1,37 @@
name: "🚫 Cancel: PR Runs on Close"
on:
pull_request_target:
types:
- closed
- converted_to_draft
concurrency:
group: pr-ci-${{ github.repository }}-pull_request-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
jobs:
cancel-pr-runs:
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
pull-requests: read
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
- name: 🚫 Cancel active workflow runs for closed PR
shell: bash
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
CURRENT_RUN_ID: ${{ github.run_id }}
run: bash scripts/github/cancel/pull_request_runs.sh
@@ -0,0 +1,28 @@
# Closes Dependabot PRs while update PRs are gated off via the
# CI_ENABLE_AUTO_UPDATES repository variable. Dependabot itself cannot read
# repository variables, so the gate is enforced here.
#
# pull_request_target runs in base-branch context and therefore has
# access to repository vars/secrets and a write-scoped GITHUB_TOKEN
# even for Dependabot's PRs. This workflow does NOT check out PR code.
name: "🚫 Dependabot: Close while auto-updates disabled"
on:
pull_request_target:
types: [opened, reopened]
permissions:
pull-requests: write
jobs:
close-when-disabled:
name: 🚫 Close Dependabot PR while gated
if: github.event.pull_request.user.login == 'dependabot[bot]' && vars.CI_ENABLE_AUTO_UPDATES != 'true'
runs-on: ubuntu-latest
steps:
- name: 📪 Close PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: gh pr close "$PR_URL" --comment "Auto-closed by \`entry-pr-open-dependabot-close.yml\`. The repository variable \`CI_ENABLE_AUTO_UPDATES\` is not set to \`true\`, so update PRs are disabled. Set it to \`true\` under **Settings → Secrets and variables → Actions → Variables** to allow Dependabot PRs."
@@ -1,30 +0,0 @@
name: "🚫 Cancel: PR Runs on Close"
on:
pull_request_target:
types:
- closed
- converted_to_draft
permissions:
actions: write
contents: read
jobs:
cancel-pr-runs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Cancel active workflow runs for closed PR
shell: bash
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
CURRENT_RUN_ID: ${{ github.run_id }}
run: bash scripts/github/cancel/pull_request_runs.sh
@@ -1,171 +0,0 @@
name: "⚡ CI: Pull Request"
run-name: >-
${{ github.event_name }} / PR #${{ github.event.pull_request.number }} / ${{ github.event.action }} / ${{ github.event.pull_request.head.label }}
on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review
- labeled
pull_request_target:
types:
- opened
- synchronize
- reopened
- ready_for_review
- labeled
concurrency:
# Keep only the newest run per PR and event type.
# This avoids pull_request and pull_request_target cancelling each other.
group: pr-ci-${{ github.repository }}-${{ github.event_name }}-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
actions: write
contents: read
packages: write
security-events: write
jobs:
detect-pr-scope:
name: 🔎 Detect PR scope
if: ${{ github.event.pull_request.draft == false && (github.event_name == 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository) }}
runs-on: ubuntu-latest
outputs:
scope: ${{ steps.resolve.outputs.scope }}
run_ci_orchestrator: ${{ steps.resolve.outputs.run_ci_orchestrator }}
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Resolve PR scope
id: resolve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: ./scripts/meta/resolve/pr/scope.sh
validate-pr-branch-prefix:
name: 🧭 Validate PR branch prefix
needs: detect-pr-scope
if: ${{ github.event_name == 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Validate branch prefix for detected scope
env:
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_SCOPE: ${{ needs.detect-pr-scope.outputs.scope }}
run: ./scripts/meta/resolve/pr/branch_prefix.sh
pr-scope-gate:
name: 🚦 Gate PR scope
needs:
- detect-pr-scope
- validate-pr-branch-prefix
if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Confirm lightweight PR scope
shell: bash
run: |
set -euo pipefail
echo "Lightweight PR scope '${{ needs.detect-pr-scope.outputs.scope }}' passed."
detect-affected-roles:
name: 🧭 Detect affected roles
needs: detect-pr-scope
if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }}
runs-on: ubuntu-latest
outputs:
whitelist: ${{ steps.resolve.outputs.whitelist }}
roles_only: ${{ steps.resolve.outputs.roles_only }}
steps:
- name: Checkout
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 affected roles
id: resolve
shell: bash
env:
SUBSET_LABELED: ${{ contains(github.event.pull_request.labels.*.name, '🧩 Subset') }}
PR_BODY: ${{ github.event.pull_request.body }}
run: bash scripts/github/resolve/pr_affected_roles.sh
ci-orchestrator:
needs:
- detect-pr-scope
- validate-pr-branch-prefix
- detect-affected-roles
if: ${{ github.event_name == 'pull_request' && needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }}
uses: ./.github/workflows/ci-orchestrator.yml
with:
distros: "arch debian ubuntu fedora centos"
whitelist: ${{ needs.detect-affected-roles.outputs.whitelist }}
roles_only: ${{ needs.detect-affected-roles.outputs.roles_only }}
trusted_pr: ${{ contains(github.event.pull_request.labels.*.name, '🛡️ Trusted') }}
prepare-fork-merge-ref:
needs:
- detect-pr-scope
- validate-pr-branch-prefix
if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }}
runs-on: ubuntu-latest
outputs:
checkout_ref: ${{ steps.out.outputs.checkout_ref }}
image_tag: ${{ steps.out.outputs.image_tag }}
trusted: ${{ contains(github.event.pull_request.labels.*.name, '🛡️ Trusted') }}
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Resolve merge ref and image tag
id: out
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha || '' }}
run: ./scripts/meta/resolve/pr/merge_ref.sh
build-ci-images-fork:
needs: prepare-fork-merge-ref
uses: ./.github/workflows/images-build-ci.yml
with:
distros: "arch debian ubuntu fedora centos"
checkout_ref: ${{ needs.prepare-fork-merge-ref.outputs.trusted == 'true' && github.event.pull_request.head.sha || needs.prepare-fork-merge-ref.outputs.checkout_ref }}
source_repository: ${{ needs.prepare-fork-merge-ref.outputs.trusted == 'true' && github.event.pull_request.head.repo.full_name || github.repository }}
image_tag: ${{ needs.prepare-fork-merge-ref.outputs.image_tag }}
concurrency_channel: pr-target-build-producer
allow_unsafe_checkout: true
images-mirror-missing-fork:
needs: prepare-fork-merge-ref
uses: ./.github/workflows/images-mirror-missing.yml
with:
repo_root: _pr_source
source_repository: ${{ github.repository }}
source_ref: ${{ needs.prepare-fork-merge-ref.outputs.checkout_ref }}
source_path: _pr_source
concurrency_channel: pr-target-mirror-producer
concurrency_key: pr-${{ github.event.pull_request.number }}
allow_unsafe_checkout: true
secrets: inherit
+59 -34
View File
@@ -1,8 +1,8 @@
name: "⚡ CI: Push"
name: "📤 Push"
concurrency:
group: global-ci-${{ github.repository }}-${{ github.ref_name }}
cancel-in-progress: ${{ vars.CI_CANCEL_IN_PROGRESS != 'false' }}
cancel-in-progress: ${{ github.ref_name != 'main' }}
on:
push:
@@ -16,22 +16,21 @@ on:
- "alert-autofix-*"
permissions:
contents: write
packages: write
actions: write
security-events: write
contents: read
jobs:
sync-main:
name: 🔄 Sync main
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
- name: Sync main from source repository
- name: 🔄 Sync main from source repository
shell: bash
env:
CI_SYNC_MAIN_SOURCE_REPOSITORY_DEFAULT: infinito-nexus/core
@@ -39,31 +38,31 @@ jobs:
CI_SYNC_MAIN_SOURCE_REPOSITORY_IS_SET: ${{ contains(toJSON(vars), '"CI_SYNC_MAIN_SOURCE_REPOSITORY"') }}
run: ./scripts/github/sync/main_from_repository.sh
# Skip CI when the push is to main and CI_RUN_ON_MAIN is not 'true', or
# when the push is a branch creation whose commit is already in origin/main.
# All other pushes run CI.
run-policy:
name: 🧭 Decide CI policy
needs: sync-main
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
should_run: ${{ steps.decide.outputs.should_run }}
skip_reason: ${{ steps.decide.outputs.skip_reason }}
commit_in_main: ${{ steps.decide.outputs.commit_in_main }}
steps:
- name: Checkout repository
- name: ⬇️ Checkout repository
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
- name: Fetch main
- name: ⬇️ Fetch main
if: github.event.created == true
shell: bash
run: |
set -euo pipefail
git fetch origin main:refs/remotes/origin/main --force
- name: Decide whether CI should run for this push
- name: 🤔 Decide whether CI should run for this push
id: decide
env:
GITHUB_EVENT_CREATED: ${{ github.event.created }}
@@ -78,24 +77,23 @@ jobs:
if: needs.run-policy.outputs.should_run != 'true'
runs-on: ubuntu-latest
steps:
- name: Explain skip
- name: ⏭️ Explain skip
shell: bash
run: |
set -euo pipefail
echo "Skipping CI for commit ${GITHUB_SHA}."
echo "Reason: ${{ needs.run-policy.outputs.skip_reason }}"
pick-distro:
detect-release:
if: needs.run-policy.outputs.should_run == 'true'
needs: run-policy
name: 🎲 Pick distro(s)
name: 🏷️ Detect release tag
runs-on: ubuntu-latest
outputs:
distros: ${{ steps.pick.outputs.distros }}
is_version: ${{ steps.pick.outputs.is_version }}
version_tag: ${{ steps.pick.outputs.version_tag }}
steps:
- name: Checkout (with tags)
- name: ⬇️ Checkout (with tags)
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
@@ -103,59 +101,86 @@ jobs:
- id: pick
shell: bash
run: bash scripts/github/resolve/pick_distro.sh
run: bash scripts/github/resolve/release/tag.sh
resolve-priority:
if: needs.run-policy.outputs.should_run == 'true'
name: ⭐ Resolve new-role priority
needs: run-policy
runs-on: ubuntu-latest
outputs:
priority: ${{ steps.resolve.outputs.priority }}
steps:
- name: ⬇️ Checkout (full history)
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
- id: resolve
shell: bash
run: |
set -euo pipefail
echo "priority=$(bash scripts/meta/resolve/diff/new_roles.sh)" >> "$GITHUB_OUTPUT"
ci-orchestrator:
if: needs.run-policy.outputs.should_run == 'true'
name: 🎶 Orchestrate CI
needs:
- run-policy
- pick-distro
uses: ./.github/workflows/ci-orchestrator.yml
- detect-release
- resolve-priority
permissions:
actions: read
contents: read
packages: write
security-events: write
issues: write
uses: ./.github/workflows/call-orchestrator.yml
with:
# ci-orchestrator expects a space-separated list → here it's either one or many
distros: ${{ needs.pick-distro.outputs.distros }}
priority: ${{ needs.resolve-priority.outputs.priority }}
secrets: inherit
# Guard job: only allow version releases if the tag commit is contained in origin/main
release-guard:
name: "🛡️ Guard release"
if: needs.run-policy.outputs.should_run == 'true' && needs.pick-distro.outputs.is_version == 'true'
if: needs.run-policy.outputs.should_run == 'true' && needs.detect-release.outputs.is_version == 'true'
needs:
- run-policy
- pick-distro
- detect-release
- ci-orchestrator
runs-on: ubuntu-latest
outputs:
allowed: ${{ steps.guard.outputs.allowed }}
steps:
- name: Checkout (with full history)
- name: ⬇️ Checkout (with full history)
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
fetch-tags: true
- name: Fetch main
- name: ⬇️ Fetch main
shell: bash
run: |
set -euo pipefail
git fetch origin main:refs/remotes/origin/main --force
- name: "Guard: tag commit must be contained in origin/main"
- name: "🛡️ Guard: tag commit must be contained in origin/main"
id: guard
shell: bash
env:
VERSION_TAG: ${{ needs.pick-distro.outputs.version_tag }}
VERSION_TAG: ${{ needs.detect-release.outputs.version_tag }}
run: bash scripts/github/release/guard.sh
release-version:
name: 🚀 Release version
needs:
- run-policy
- pick-distro
- detect-release
- release-guard
if: needs.run-policy.outputs.should_run == 'true' && needs.release-guard.outputs.allowed == 'true'
uses: ./.github/workflows/release-version.yml
permissions:
contents: read
packages: write
uses: ./.github/workflows/call-release-version.yml
with:
tag: ${{ needs.pick-distro.outputs.version_tag }}
tag: ${{ needs.detect-release.outputs.version_tag }}
secrets: inherit
-143
View File
@@ -1,143 +0,0 @@
name: "🐳 Build: CI Images (all distros)"
on:
workflow_call:
inputs:
distros:
description: "Space-separated distro list"
required: true
type: string
checkout_ref:
description: "Optional git ref to checkout before build (default: event ref)"
required: false
type: string
default: ""
source_repository:
description: "Repository to checkout as the build source (default: github.repository). Set to a fork only for maintainer-trusted PRs."
required: false
type: string
default: ""
image_tag:
description: "Optional image tag override (default: ci-${github.sha})"
required: false
type: string
default: ""
concurrency_channel:
description: "Optional channel to separate concurrency groups between callers"
required: false
type: string
default: "default"
allow_unsafe_checkout:
description: "Allow actions/checkout to fetch PR head/merge code under pull_request_target. Only set for maintainer-trusted fork PRs."
required: false
type: boolean
default: false
permissions:
contents: read
packages: write
concurrency:
group: infinito-build-${{ github.repository }}-${{ inputs.concurrency_channel }}-${{ inputs.checkout_ref || github.ref }}
cancel-in-progress: false
jobs:
prepare:
name: 🏷️ Label images
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.out.outputs.matrix }}
image_tag: ${{ steps.out.outputs.image_tag }}
steps:
- uses: actions/checkout@v7.0.0
- name: 📦 Install jq
run: ./scripts/install/apt.sh jq
- name: Compute matrix JSON
id: out
run: scripts/github/runner/images_build_outputs.sh "${{ inputs.distros }}" "${{ inputs.image_tag }}"
build:
name: 🏗️ Build (${{ matrix.distro }})
needs: prepare
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
distro: ${{ fromJson(needs.prepare.outputs.matrix) }}
steps:
- name: Checkout trusted workflow repo (helpers)
uses: actions/checkout@v7.0.0
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Checkout source to build
uses: actions/checkout@v7.0.0
with:
repository: ${{ inputs.source_repository || github.repository }}
ref: ${{ inputs.checkout_ref || github.ref }}
path: _src
persist-credentials: false
allow-unsafe-pr-checkout: ${{ inputs.allow_unsafe_checkout }}
- name: Disk pressure (start)
shell: bash
run: bash scripts/github/runner/disk_pressure_report.sh START
- name: Pre-pull buildkit image (retry 7x, 20s)
shell: bash
env:
MAX_ATTEMPTS: "7"
RETRY_DELAY_SECONDS: "20"
run: bash scripts/github/runner/pull_with_retry.sh moby/buildkit:buildx-stable-1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
driver-opts: image=moby/buildkit:buildx-stable-1
- name: Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Disk pressure (pre-build)
shell: bash
run: bash scripts/github/runner/disk_pressure_report.sh PRE-BUILD
- name: 🔍 Resolve repository owner
id: owner
shell: bash
run: |
set -euo pipefail
echo "owner=$(scripts/meta/resolve/repository/owner.sh)" >> "$GITHUB_OUTPUT"
- name: Build & push (retry 7x, 20s)
if: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
shell: bash
env:
GITHUB_REPOSITORY_OWNER: ${{ steps.owner.outputs.owner }}
BUILD_CONTEXT_DIR: _src
MATRIX_DISTRO: ${{ matrix.distro }}
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
USE_NIX_TOKEN: ${{ !(github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository) }}
NIX_GITHUB_TOKEN: ${{ github.token }}
MAX_ATTEMPTS: "7"
RETRY_DELAY_SECONDS: "20"
run: ./scripts/image/push.sh
- name: Disk pressure (post-build)
if: always()
shell: bash
run: bash scripts/github/runner/disk_pressure_report.sh POST-BUILD
- name: Cleanup (always)
if: always()
shell: bash
run: bash scripts/github/runner/cleanup_docker.sh
-58
View File
@@ -1,58 +0,0 @@
name: "🧹 Images: Cleanup CI (GHCR)"
on:
schedule:
- cron: "0 0 * * 6"
workflow_dispatch:
inputs:
days:
description: "Delete CI images older than N days (default: 7)"
required: false
default: "7"
type: string
permissions:
actions: read
contents: read
packages: write
pull-requests: read
concurrency:
group: images-cleanup-ci
cancel-in-progress: false
jobs:
cleanup:
name: 🧹 Delete old ci-* image versions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- name: Resolve image metadata
id: vars
shell: bash
run: |
set -euo pipefail
source scripts/meta/env/load.sh
{
echo "owner=$(scripts/meta/resolve/repository/owner.sh)"
echo "repo_prefix=$(scripts/meta/resolve/repository/name.sh)"
echo "distros=${INFINITO_DISTROS}"
} >> "$GITHUB_OUTPUT"
- name: 📦 Install jq
shell: bash
run: ./scripts/install/apt.sh jq
- name: Cleanup old ci-* versions
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OWNER: ${{ steps.vars.outputs.owner }}
DAYS: ${{ github.event.inputs.days || '7' }}
REPO_PREFIX: ${{ steps.vars.outputs.repo_prefix }}
INFINITO_DISTROS: ${{ steps.vars.outputs.distros }}
run: |
set -euo pipefail
./scripts/image/cleanup.sh
-120
View File
@@ -1,120 +0,0 @@
name: "🪞 Mirror: Docker Hub → GHCR"
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
inputs:
ghcr_namespace:
description: "GHCR namespace/org (default: repository owner)"
required: false
type: string
ghcr_prefix:
description: "GHCR repo prefix (default: mirror)"
required: false
default: "mirror"
type: string
repo_root:
description: "Repository root path (default: .)"
required: false
default: "."
type: string
images_per_hour:
description: "Optional throttle: max images mirrored per hour"
required: false
type: string
workflow_call:
inputs:
ghcr_namespace:
description: "GHCR namespace/org (default: repository owner)"
required: false
type: string
ghcr_prefix:
description: "GHCR repo prefix (default: mirror)"
required: false
type: string
default: "mirror"
repo_root:
description: "Repository root path (default: .)"
required: false
type: string
default: "."
images_per_hour:
description: "Optional throttle: max images mirrored per hour"
required: false
type: string
secrets:
DOCKERHUB_USERNAME:
required: false
DOCKERHUB_TOKEN:
required: false
permissions:
contents: read
packages: write
concurrency:
group: mirror-images-${{ github.repository }}-${{ github.ref_name }}
cancel-in-progress: false
jobs:
sync:
name: 🪞 Mirror images
runs-on: ubuntu-latest
# Copy secrets into env so we can reference them in "if:" (env.* is allowed there)
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
IMAGES_PER_HOUR: ${{ github.event.inputs.images_per_hour || inputs.images_per_hour || '50' }}
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Resolve GHCR namespace and repository
id: vars
shell: bash
env:
GHCR_NAMESPACE_INPUT: ${{ inputs.ghcr_namespace || '' }}
run: bash scripts/meta/resolve/ghcr/namespace_and_repository.sh
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install Python deps
run: |
python -m pip install --upgrade pip
python -m pip install PyYAML
- name: Install skopeo
run: ./scripts/install/apt.sh skopeo
# Optional: Docker Hub login to avoid rate limits while mirroring
- name: Login to Docker Hub (optional)
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Mirror images to GHCR (best-effort)
shell: bash
env:
GHCR_NAMESPACE: ${{ steps.vars.outputs.ghcr_namespace }}
GHCR_REPOSITORY: ${{ steps.vars.outputs.ghcr_repository }}
GHCR_PREFIX: ${{ inputs.ghcr_prefix || 'mirror' }}
REPO_ROOT: ${{ inputs.repo_root || '.' }}
run: bash scripts/meta/mirror/sync_all.sh
@@ -1,85 +0,0 @@
name: "🧹 Images: Cleanup GHCR (Private)"
on:
workflow_dispatch:
inputs:
ghcr_namespace:
description: "GHCR namespace/org (default: repository owner)"
required: false
type: string
default: ""
ghcr_prefix:
description: "Package name prefix to delete (default: mirror)"
required: false
type: string
default: "mirror"
visibility:
description: "Visibility filter: only packages with this visibility are deleted"
required: false
type: choice
default: "private"
options:
- private
- public
- internal
dry_run:
description: "Dry-run: list packages that would be deleted without deleting them"
required: false
type: boolean
default: true
token:
description: "Classic PAT with delete:packages scope. Leave empty to use the default GITHUB_TOKEN The default token may lack the delete:packages scope."
required: false
type: string
default: ""
permissions:
contents: read
packages: write
jobs:
cleanup:
name: 🗑️ Delete private GHCR mirror packages
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Resolve GHCR namespace and repository
id: vars
shell: bash
run: |
set -euo pipefail
ghcr_namespace="${{ inputs.ghcr_namespace }}"
if [[ -z "${ghcr_namespace}" ]]; then
ghcr_namespace="$(scripts/meta/resolve/repository/owner.sh)"
fi
ghcr_repository="$(scripts/meta/resolve/repository/name.sh)"
echo "ghcr_namespace=${ghcr_namespace,,}" >> "$GITHUB_OUTPUT"
echo "ghcr_repository=${ghcr_repository}" >> "$GITHUB_OUTPUT"
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Run mirror cleanup
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token != '' && inputs.token || secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
EXTRA_ARGS=()
if [[ "${{ inputs.dry_run }}" == "true" ]]; then
EXTRA_ARGS+=( --dry-run )
fi
python -m cli.contributing.mirror.cleanup \
--ghcr-namespace "${{ steps.vars.outputs.ghcr_namespace }}" \
--ghcr-prefix "${{ steps.vars.outputs.ghcr_repository }}/${{ inputs.ghcr_prefix }}" \
--visibility "${{ inputs.visibility }}" \
"${EXTRA_ARGS[@]}"
-163
View File
@@ -1,163 +0,0 @@
name: "🪞 Mirror: Docker Hub → GHCR (only missing)"
on:
workflow_call:
inputs:
ghcr_namespace:
description: "GHCR namespace/org (default: repository owner)"
required: false
type: string
ghcr_prefix:
description: "GHCR repo prefix (default: mirror)"
required: false
type: string
default: "mirror"
repo_root:
description: "Repository root path (default: .)"
required: false
type: string
default: "."
source_repository:
description: "Optional source repository for image config scan (e.g. fork owner/repo)"
required: false
type: string
default: ""
source_ref:
description: "Optional source ref/SHA for image config scan"
required: false
type: string
default: ""
source_path:
description: "Optional checkout path for source_repository/source_ref"
required: false
type: string
default: "_pr_source"
allow_unsafe_checkout:
description: "Allow actions/checkout to fetch PR head/merge code under pull_request_target. Only set for maintainer-trusted fork PRs."
required: false
type: boolean
default: false
images_per_hour:
description: "Optional throttle: max images mirrored per hour"
required: false
type: string
default: ""
concurrency_channel:
description: "Optional channel to separate concurrency groups between callers"
required: false
type: string
default: "default"
concurrency_key:
description: "Optional key (e.g. pr-123) to scope concurrency within a channel"
required: false
type: string
default: ""
image_wait_attempts:
description: "Polling attempts for waiting on mirrored images"
required: false
type: string
default: "1440"
image_wait_sleep_seconds:
description: "Polling interval in seconds for waiting on mirrored images"
required: false
type: string
default: "10"
secrets:
DOCKERHUB_USERNAME:
required: false
DOCKERHUB_TOKEN:
required: false
permissions:
contents: read
packages: write
concurrency:
group: mirror-images-${{ github.repository }}-${{ inputs.concurrency_channel }}-${{ inputs.concurrency_key || github.ref_name }}
# Cancel scheduled Mirror sync, because may it works with outdatet mirror lists
cancel-in-progress: true
jobs:
sync:
name: 🪞 Mirror images
runs-on: ubuntu-latest
# Copy secrets/inputs into env so we can reference them in "if:" (env.* is allowed there)
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
IMAGES_PER_HOUR: ${{ inputs.images_per_hour || '' }}
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Resolve GHCR namespace and repository
id: vars
shell: bash
env:
GHCR_NAMESPACE_INPUT: ${{ inputs.ghcr_namespace || '' }}
run: bash scripts/meta/resolve/ghcr/namespace_and_repository.sh
- name: Checkout source repo for scan (optional)
if: ${{ inputs.source_repository != '' && inputs.source_ref != '' }}
uses: actions/checkout@v7.0.0
with:
repository: ${{ inputs.source_repository }}
ref: ${{ inputs.source_ref }}
path: ${{ inputs.source_path }}
fetch-depth: 1
persist-credentials: false
allow-unsafe-pr-checkout: ${{ inputs.allow_unsafe_checkout }}
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install Python deps
run: |
python -m pip install --upgrade pip
python -m pip install PyYAML
- name: Install skopeo
run: ./scripts/install/apt.sh skopeo
# Optional: Docker Hub login to avoid rate limits while mirroring
- name: Login to Docker Hub (optional)
if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' && !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ env.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Wait for mirrored images (fork PR)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
env:
GHCR_NAMESPACE: ${{ steps.vars.outputs.ghcr_namespace }}
GHCR_REPOSITORY: ${{ steps.vars.outputs.ghcr_repository }}
GHCR_PREFIX: ${{ inputs.ghcr_prefix || 'mirror' }}
REPO_ROOT: ${{ inputs.repo_root || '.' }}
IMAGE_WAIT_ATTEMPTS: ${{ inputs.image_wait_attempts }}
IMAGE_WAIT_SLEEP_SECONDS: ${{ inputs.image_wait_sleep_seconds }}
run: ./scripts/meta/wait/mirrors.sh
- name: Mirror images to GHCR (only missing, best-effort)
if: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
shell: bash
env:
GHCR_NAMESPACE: ${{ steps.vars.outputs.ghcr_namespace }}
GHCR_REPOSITORY: ${{ steps.vars.outputs.ghcr_repository }}
GHCR_PREFIX: ${{ inputs.ghcr_prefix || 'mirror' }}
REPO_ROOT: ${{ inputs.repo_root || '.' }}
run: bash scripts/meta/mirror/sync_missing.sh
-59
View File
@@ -1,59 +0,0 @@
name: "🔍 Lint: Ansible"
on:
workflow_call:
permissions:
contents: read
packages: read
jobs:
lint-ansible:
name: 🧹 Lint Ansible
runs-on: ubuntu-latest
timeout-minutes: 30
env:
INFINITO_LINT_RUNNER: host
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install ansible-core + ansible-lint
run: |
python -m pip install --upgrade pip
pip install ansible-core ansible-lint pyyaml
- name: Cache Ansible collections
uses: actions/cache@v6
with:
path: ~/.ansible/collections
key: ansible-collections-${{ runner.os }}-${{ hashFiles('requirements/requirements.galaxy.yml', 'requirements/requirements.git.yml') }}
- name: Install required Ansible collections
run: |
ansible-galaxy collection install \
-r requirements/requirements.galaxy.yml
- name: Generate role include files
# `tasks/groups/*-roles.yml` are generated artefacts (gitignored
# via tasks/groups/.gitignore) referenced by `tasks/stages/*.yml`.
# Without them ansible-lint reports a fatal load-failure on
# `tasks/groups/desk-roles.yml`. Generate via the same script the
# local `make setup` target uses; PyYAML is already on the runner
# because we install it above. PYTHONPATH=. is set explicitly so
# `cli/build/include/__main__.py` can import the repo-local
# `utils.*` modules — locally the Makefile env layer
# (scripts/meta/env/load.sh) does this, but here we invoke the
# script directly without going through `make`.
run: PYTHON=python3 PYTHONPATH=. bash scripts/setup.sh
- name: Run lint-ansible via make
run: make lint-ansible
-39
View File
@@ -1,39 +0,0 @@
name: "🔍 Lint: Dockerfiles"
on:
workflow_call:
permissions:
contents: read
jobs:
lint-docker:
name: 🧹 Lint Dockerfile
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v7.0.0
- name: Run hadolint (produce SARIF)
id: hadolint
continue-on-error: true
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5
with:
dockerfile: ./Dockerfile
format: sarif
output-file: hadolint-results.sarif
failure-threshold: warning
- name: Upload analysis results to GitHub
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: hadolint-results.sarif
wait-for-processing: true
category: hadolint
- name: Fail if SARIF contains warnings/errors
if: always()
run: python3 utils/docker/hadolint.py hadolint-results.sarif
-31
View File
@@ -1,31 +0,0 @@
name: "🔍 Lint: JavaScript"
on:
workflow_call:
permissions:
contents: read
jobs:
lint-javascript:
name: 🧹 Lint JavaScript
runs-on: ubuntu-latest
timeout-minutes: 5
env:
INFINITO_LINT_RUNNER: host
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
cache: "npm"
- name: Install ESLint dependencies
run: npm ci --no-fund --no-audit
- name: Run lint-javascript via make
run: make lint-javascript
-34
View File
@@ -1,34 +0,0 @@
name: "🔍 Lint: Makefile"
on:
workflow_call:
permissions:
contents: read
jobs:
lint-makefile:
name: 🧹 Lint Makefile
runs-on: ubuntu-latest
timeout-minutes: 5
env:
INFINITO_LINT_RUNNER: host
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install mbake
run: |
python -m pip install --upgrade pip
pip install mbake
- name: Run mbake format --check via make
run: make lint-makefile
-29
View File
@@ -1,29 +0,0 @@
name: "🔍 Lint: Markdown"
on:
workflow_call:
permissions:
contents: read
jobs:
lint-markdown:
name: 🧹 Lint Markdown
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
cache: "npm"
- name: Install markdownlint-cli2
run: npm install -g markdownlint-cli2
- name: Run markdownlint-cli2
run: bash scripts/lint/markdown.sh
-25
View File
@@ -1,25 +0,0 @@
name: "🔍 Lint: Packages"
on:
workflow_call:
permissions:
contents: read
jobs:
lint-packages:
name: 🧹 Lint Packages
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Install packaging validators
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends dpkg-dev rpm
- name: Validate distro packaging metadata
run: bash scripts/lint/packages.sh
-31
View File
@@ -1,31 +0,0 @@
name: "🔍 Lint: Playwright"
on:
workflow_call:
permissions:
contents: read
jobs:
lint-playwright:
name: 🧹 Lint Playwright
runs-on: ubuntu-latest
timeout-minutes: 10
env:
INFINITO_LINT_RUNNER: host
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
cache: "npm"
- name: Install npm dependencies (eslint + @playwright/test)
run: npm ci --no-fund --no-audit
- name: Run lint-playwright via make
run: make lint-playwright
-37
View File
@@ -1,37 +0,0 @@
name: "🔍 Lint: Python"
on:
workflow_call:
permissions:
contents: read
jobs:
lint-python:
name: 🧹 Lint Python
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install ruff
run: |
python -m pip install --upgrade pip
pip install ruff
- name: Ruff (lint)
run: |
ruff check .
- name: Ruff (format check)
run: |
ruff format --check .
-27
View File
@@ -1,27 +0,0 @@
name: "🔍 Lint: Shell Scripts"
on:
workflow_call:
permissions:
contents: read
jobs:
lint-shell:
name: 🧹 Lint shell
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- name: Run ShellCheck (container)
run: |
mapfile -d '' shell_files < <(find . -type f -name '*.sh' -print0)
if [ "${#shell_files[@]}" -eq 0 ]; then
echo "No shell scripts found."
exit 0
fi
docker run --rm \
-v "$PWD:/mnt" \
-w /mnt \
koalaman/shellcheck:latest \
-x "${shell_files[@]}"
-79
View File
@@ -1,79 +0,0 @@
name: "🚀 Release: Highest Missing Version"
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
concurrency:
group: infinito-release
cancel-in-progress: false
permissions:
contents: read
packages: read
actions: write
env:
REGISTRY: ghcr.io
jobs:
check-and-trigger:
name: 🔎 Backfill highest missing release tag
runs-on: ubuntu-latest
steps:
- name: Checkout repository (with tags)
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0
fetch-tags: true
- name: Resolve image metadata
id: vars
shell: bash
run: |
set -euo pipefail
source scripts/meta/env/load.sh
owner="$(scripts/meta/resolve/repository/owner.sh)"
repo_prefix="$(scripts/meta/resolve/repository/name.sh)"
{
echo "owner=${owner}"
echo "repo_prefix=${repo_prefix}"
echo "distros=${INFINITO_DISTROS}"
} >> "$GITHUB_OUTPUT"
- name: Login to GHCR (for manifest checks)
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Determine highest version tag with missing release image
id: pick
shell: bash
env:
OWNER: ${{ steps.vars.outputs.owner }}
REPO_PREFIX: ${{ steps.vars.outputs.repo_prefix }}
INFINITO_DISTROS: ${{ steps.vars.outputs.distros }}
run: |
set -euo pipefail
tag="$(scripts/meta/resolve/missing/highest_release_tag.sh)"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
- name: Ensure GitHub CLI is available
shell: bash
run: |
set -euo pipefail
if command -v gh >/dev/null 2>&1; then
exit 0
fi
./scripts/install/apt.sh gh
- name: Trigger release workflow for missing tag
if: ${{ steps.pick.outputs.tag != '' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.pick.outputs.tag }}
run: gh workflow run release-version.yml -f tag="${TAG}"
-162
View File
@@ -1,162 +0,0 @@
name: "🚀 Release: Version Logic"
on:
workflow_call:
inputs:
tag:
description: "Version tag to release (e.g. v1.2.3)"
required: true
type: string
workflow_dispatch:
inputs:
tag:
description: "Version tag to release (e.g. v1.2.3)"
required: true
type: string
concurrency:
group: infinito-release
cancel-in-progress: false
permissions:
contents: write
packages: write
security-events: write
actions: read
env:
REGISTRY: ghcr.io
DEFAULT_DISTRO: "debian"
jobs:
check-ci-images:
name: 🔎 Check CI images
runs-on: ubuntu-latest
outputs:
missing: ${{ steps.check.outputs.missing }}
ci_tag: ${{ steps.citag.outputs.ci_tag }}
distros: ${{ steps.vars.outputs.distros }}
repo_prefix: ${{ steps.vars.outputs.repo_prefix }}
owner: ${{ steps.vars.outputs.owner }}
version_tag: ${{ steps.vars.outputs.version_tag }}
publish_latest: ${{ steps.highest.outputs.publish_latest }}
steps:
- name: Checkout trusted workflow repo (helpers)
uses: actions/checkout@v7.0.0
with:
ref: ${{ github.sha }}
path: _helpers
persist-credentials: false
- name: Resolve release inputs
id: vars
shell: bash
run: bash _helpers/scripts/github/resolve/release_inputs.sh "${{ inputs.tag || github.event.inputs.tag }}"
- name: Checkout repository at version tag
uses: actions/checkout@v7.0.0
with:
ref: ${{ steps.vars.outputs.version_tag }}
path: _src
fetch-depth: 0
fetch-tags: true
- name: Compute CI tag (ci-<sha> of version tag)
id: citag
shell: bash
run: |
set -euo pipefail
sha="$(git -C _src rev-parse HEAD)"
echo "ci_tag=ci-${sha}" >> "$GITHUB_OUTPUT"
- name: Decide whether this version should update latest
id: highest
shell: bash
env:
REPOSITORY_DIR: _src
VERSION_TAG: ${{ steps.vars.outputs.version_tag }}
run: _helpers/scripts/meta/resolve/publish_latest.sh
- name: Login to GHCR (manifest checks)
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Check whether CI images already exist
id: check
shell: bash
env:
OWNER: ${{ steps.vars.outputs.owner }}
REPO_PREFIX: ${{ steps.vars.outputs.repo_prefix }}
INFINITO_DISTROS: ${{ steps.vars.outputs.distros }}
CI_TAG: ${{ steps.citag.outputs.ci_tag }}
run: |
set -euo pipefail
missing="$(_helpers/scripts/meta/resolve/missing/ci_images.sh)"
echo "missing=${missing}" >> "$GITHUB_OUTPUT"
build-ci-images-if-missing:
name: 🏗️ Build missing CI images
needs: check-ci-images
if: needs.check-ci-images.outputs.missing == 'true'
uses: ./.github/workflows/images-build-ci.yml
with:
distros: ${{ needs.check-ci-images.outputs.distros }}
checkout_ref: ${{ needs.check-ci-images.outputs.version_tag }}
image_tag: ${{ needs.check-ci-images.outputs.ci_tag }}
concurrency_channel: release-backfill
secrets: inherit
release:
name: 🚀 Release version
needs:
- check-ci-images
- build-ci-images-if-missing
if: |
always() &&
needs.check-ci-images.result == 'success' &&
(
needs.build-ci-images-if-missing.result == 'success' ||
needs.build-ci-images-if-missing.result == 'skipped'
)
runs-on: ubuntu-latest
env:
VERSION_TAG: ${{ needs.check-ci-images.outputs.version_tag }}
OWNER: ${{ needs.check-ci-images.outputs.owner }}
REPO_PREFIX: ${{ needs.check-ci-images.outputs.repo_prefix }}
INFINITO_DISTROS: ${{ needs.check-ci-images.outputs.distros }}
CI_TAG: ${{ needs.check-ci-images.outputs.ci_tag }}
PUBLISH_LATEST: ${{ needs.check-ci-images.outputs.publish_latest }}
steps:
- name: Checkout trusted workflow repo (helpers)
uses: actions/checkout@v7.0.0
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Pre-pull buildkit image (retry 7x, 20s)
shell: bash
env:
MAX_ATTEMPTS: "7"
RETRY_DELAY_SECONDS: "20"
run: bash scripts/github/runner/pull_with_retry.sh moby/buildkit:buildx-stable-1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
driver-opts: image=moby/buildkit:buildx-stable-1
- name: Login to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Retag CI images to release tags (and latest for the highest version)
shell: bash
run: scripts/image/release.sh
-124
View File
@@ -1,124 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "🔒 Scan: CodeQL (Advanced)"
concurrency:
group: codeql-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_call:
schedule:
- cron: '0 0 * * 6'
jobs:
analyze:
name: 🔐 Security
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
security-events: write
packages: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
- language: python
build-mode: none
- language: ruby
build-mode: none
# Disabled — no matching C/C++ files found in the repository
# - language: c-cpp
# build-mode: none
# Disabled — no matching C# files found in the repository
# - language: csharp
# build-mode: none
# Disabled — no matching Go files found in the repository
# - language: go
# build-mode: none
# Disabled — the bluesky Keycloak event-listener SPI bridge
# scaffold (the only Java source in the repo) was removed; no
# matching Java/Kotlin files remain. Re-enable once a real
# Java component (e.g. a built Keycloak SPI) lands again.
# - language: java-kotlin
# build-mode: none
# Disabled — no matching Rust files found in the repository
# - language: rust
# build-mode: none
# Disabled — no matching Swift files found in the repository
# - language: swift
# build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
env:
CODEQL_ACTION_FILE_COVERAGE_ON_PRS: "true"
-78
View File
@@ -1,78 +0,0 @@
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '00 00 * * *'
push:
branches: [ "main" ]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
# `publish_results: true` only works when run from the default branch. conditional can be removed if disabled.
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
# Uncomment the permissions below if installing in a private repository.
# contents: read
# actions: read
steps:
- name: "Checkout code"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecard on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore
# file_mode: git
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif
-39
View File
@@ -1,39 +0,0 @@
name: "🧪 Test: Code (External)"
on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
jobs:
test-code-external:
name: 🧪 Test external
runs-on: ubuntu-latest
timeout-minutes: 30
env:
INFINITO_TEST_RUNNER: host
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Run external tests via make
shell: bash
run: |
set -euo pipefail
make test-external 2>&1 | tee /tmp/external-output.txt
- name: Build step summary from annotations
if: always()
working-directory: ${{ github.workspace }}
run: |
python3 -m utils.annotations.summarize /tmp/external-output.txt "External Test Annotation Summary"
@@ -1,38 +0,0 @@
name: "🧪 Test: Code (Integration)"
on:
workflow_call:
permissions:
contents: read
jobs:
test-code-integration:
name: 🧪 Test integration
runs-on: ubuntu-latest
timeout-minutes: 15
env:
INFINITO_TEST_RUNNER: host
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Run integration tests via make
shell: bash
run: |
set -euo pipefail
make test-integration 2>&1 | tee /tmp/integration-output.txt
- name: Build step summary from annotations
if: always()
working-directory: ${{ github.workspace }}
run: |
python3 -m utils.annotations.summarize /tmp/integration-output.txt "Integration Test Annotation Summary"
-38
View File
@@ -1,38 +0,0 @@
name: "🧪 Test: Code (Lint)"
on:
workflow_call:
permissions:
contents: read
jobs:
test-code-lint:
name: 🧪 Test lint
runs-on: ubuntu-latest
timeout-minutes: 30
env:
INFINITO_TEST_RUNNER: host
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Run lint tests via make
shell: bash
run: |
set -euo pipefail
make test-lint 2>&1 | tee /tmp/lint-output.txt
- name: Build step summary from annotations
if: always()
working-directory: ${{ github.workspace }}
run: |
python3 -m utils.annotations.summarize /tmp/lint-output.txt "Lint Annotation Summary"
-38
View File
@@ -1,38 +0,0 @@
name: "🧪 Test: Code (Units)"
on:
workflow_call:
permissions:
contents: read
jobs:
test-code-unit:
name: 🧪 Test unit
runs-on: ubuntu-latest
timeout-minutes: 30
env:
INFINITO_TEST_RUNNER: host
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Run unit tests via make
shell: bash
run: |
set -euo pipefail
make test-unit 2>&1 | tee /tmp/unit-output.txt
- name: Build step summary from annotations
if: always()
working-directory: ${{ github.workspace }}
run: |
python3 -m utils.annotations.summarize /tmp/unit-output.txt "Unit Test Annotation Summary"
-143
View File
@@ -1,143 +0,0 @@
name: "📦 Test: Deploy (local)"
on:
workflow_dispatch:
inputs:
test_deploy_type:
description: "server | universal | workstation"
required: false
default: "server"
type: string
distros:
description: "Space-separated distro list"
required: false
default: "debian"
type: string
whitelist:
description: "Optional space-separated whitelist of app ids"
required: false
type: string
default: ""
permissions:
contents: read
packages: read
# IMPORTANT:
# - For local `act`, prefer overriding via `--env`.
# - For GitHub UI dispatch, inputs are used as fallback.
env:
INFINITO_DEPLOY_TYPE: "server"
INFINITO_DISTROS: "debian"
NIX_CONFIG: ""
INFINITO_IMAGE_TAG: "latest"
INFINITO_PLAYWRIGHT_KEEP: ${{ vars.INFINITO_PLAYWRIGHT_KEEP }}
INFINITO_APP_DISCOVERY_RUNNER: host
jobs:
discover:
name: 🔎 Discover apps (local)
runs-on: ubuntu-latest
timeout-minutes: 60
outputs:
apps: ${{ steps.discover.outputs.apps }}
test_deploy_type: ${{ steps.vars.outputs.test_deploy_type }}
distros: ${{ steps.vars.outputs.distros }}
whitelist: ${{ steps.vars.outputs.whitelist }}
steps:
- uses: actions/checkout@v7.0.0
- name: Resolve effective inputs/env
id: vars
shell: bash
env:
INPUT_TEST_DEPLOY_TYPE: ${{ inputs.test_deploy_type }}
INPUT_DISTROS: ${{ inputs.distros }}
INPUT_WHITELIST: ${{ inputs.whitelist }}
run: |
set -euo pipefail
scripts/meta/resolve/input.sh
- 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
- name: Discover apps
id: discover
shell: bash
env:
INFINITO_WHITELIST: ${{ inputs.whitelist }}
run: ./scripts/github/resolve/output_apps.sh
deploy:
name: 🚀 Deploy ${{ needs.discover.outputs.test_deploy_type }} app (${{ matrix.apps }}${{ matrix.variant && format(' [{0}]', matrix.variant) || '' }})
if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]'
runs-on: ubuntu-latest
timeout-minutes: 360
needs: discover
strategy:
fail-fast: false
max-parallel: 1 # immer linear
matrix:
include: ${{ fromJson(needs.discover.outputs.apps) }}
steps:
# Reclaim runner space before Docker pulls start. Deploy jobs otherwise
# compete with the default Ubuntu image payload for limited root space.
# Option rationale: docs/contributing/artefact/files/github/workflow.md
- 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
# Replace the default ~4 GB runner swap with a larger file so transient
# memory spikes (e.g. peertube plugin install, issue #162) don't trip
# the host OOM-killer. Script picks / or /mnt based on live free space.
- name: 💾 Enlarge swap space
shell: bash
run: ./scripts/github/runner/enlarge_swap.sh
- name: 📦 Install jq
shell: bash
run: ./scripts/install/apt.sh jq
- name: 🚀 Deploy app (same as CI logic)
shell: bash
env:
INFINITO_DEPLOY_TYPE: ${{ needs.discover.outputs.test_deploy_type }}
INFINITO_DISTROS: ${{ needs.discover.outputs.distros }}
apps: ${{ matrix.apps }}
variant: ${{ matrix.variant }}
ANSIBLE_LOG_PATH: /tmp/ansible-${{ matrix.apps }}.log
run: ./scripts/tests/deploy/ci/all.sh
- name: 📊 Role runtimes
if: always()
shell: bash
run: python3 utils/github/profile/role_summary.py /tmp/ansible-${{ matrix.apps }}.log
- name: 🩺 Dump runner state (on failure)
if: failure()
shell: bash
run: ./scripts/github/runner/dump_state.sh "post-deploy failure (${{ matrix.apps }})"
-158
View File
@@ -1,158 +0,0 @@
name: "📦 Test: Deploy (server)"
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: ""
permissions:
contents: read
packages: read
env:
INFINITO_DEPLOY_TYPE: server
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
jobs:
discover:
name: 🌐 Discover apps
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: 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 }}
run: ./scripts/github/resolve/output_apps.sh
server:
name: 🌐 Deploy ${{ matrix.apps }}${{ matrix.variant && format(' [{0}]', matrix.variant) || '' }}
if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]'
runs-on: ubuntu-latest
timeout-minutes: 360
needs: discover
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.discover.outputs.apps) }}
steps:
# Reclaim runner space before Docker pulls start. Server deploy tests pull
# large CI and application images and can otherwise exhaust / on
# GitHub-hosted runners.
# Option rationale: docs/contributing/artefact/files/github/workflow.md
- 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
# Replace the default ~4 GB runner swap with a larger file so transient
# memory spikes (e.g. peertube plugin install, issue #162) don't trip
# the host OOM-killer. Script picks / or /mnt based on live free space.
- name: 💾 Enlarge swap space
shell: bash
run: ./scripts/github/runner/enlarge_swap.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
shell: bash
env:
apps: ${{ matrix.apps }}
variant: ${{ matrix.variant }}
ANSIBLE_LOG_PATH: /tmp/ansible-${{ matrix.apps }}.log
run: ./scripts/tests/deploy/ci/all.sh
- name: 📊 Role runtimes
if: always()
shell: bash
run: python3 utils/github/profile/role_summary.py /tmp/ansible-${{ matrix.apps }}.log
- name: 🩺 Dump runner state (on failure)
if: failure()
shell: bash
run: ./scripts/github/runner/dump_state.sh "post-deploy failure (${{ matrix.apps }})"
- 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-server-${{ matrix.apps }}${{ matrix.variant_slug && format('-{0}', matrix.variant_slug) || '' }}
path: /tmp/playwright-artifacts/
if-no-files-found: ignore
-189
View File
@@ -1,189 +0,0 @@
name: "📦 Test: Deploy (universal)"
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: ""
permissions:
contents: read
packages: read
actions: write
env:
INFINITO_DEPLOY_TYPE: universal
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
jobs:
discover:
name: ✨ Discover apps
runs-on: ubuntu-latest
timeout-minutes: 60
outputs:
apps_github: ${{ steps.split.outputs.apps_github }}
apps_self_hosted: ${{ steps.split.outputs.apps_self_hosted }}
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
- 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 }}
run: ./scripts/github/resolve/output_apps.sh
- name: Split apps between GitHub-hosted and self-hosted runners
id: split
shell: bash
env:
APPS_JSON: ${{ steps.discover.outputs.apps }}
CI_SELF_HOSTED_RUNNER_COUNT: ${{ vars.CI_SELF_HOSTED_RUNNER_COUNT || '0' }}
run: bash scripts/github/split_runner_apps.sh
universal:
name: ✨ Deploy ${{ matrix.apps }}${{ matrix.variant && format(' [{0}]', matrix.variant) || '' }}
if: needs.discover.outputs.apps_github != '' && needs.discover.outputs.apps_github != '[]'
runs-on: ubuntu-latest
timeout-minutes: 360
needs: discover
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.discover.outputs.apps_github) }}
steps:
# Reclaim runner space before Docker pulls start. Deploy jobs otherwise
# compete with the default Ubuntu image payload for limited root space.
# Option rationale: docs/contributing/artefact/files/github/workflow.md
- 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
# Replace the default ~4 GB runner swap with a larger file so transient
# memory spikes (e.g. peertube plugin install, issue #162) don't trip
# the host OOM-killer. Script picks / or /mnt based on live free space.
- name: 💾 Enlarge swap space
shell: bash
run: ./scripts/github/runner/enlarge_swap.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
shell: bash
env:
apps: ${{ matrix.apps }}
variant: ${{ matrix.variant }}
ANSIBLE_LOG_PATH: /tmp/ansible-${{ matrix.apps }}.log
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./scripts/tests/deploy/ci/all.sh
- name: 📊 Role runtimes
if: always()
shell: bash
run: python3 utils/github/profile/role_summary.py /tmp/ansible-${{ matrix.apps }}.log
- name: 🩺 Dump runner state (on failure)
if: failure()
shell: bash
run: ./scripts/github/runner/dump_state.sh "post-deploy failure (${{ matrix.apps }})"
universal-self-hosted:
name: ✨ Deploy ${{ matrix.apps }}${{ matrix.variant && format(' [{0}]', matrix.variant) || '' }} (self-hosted)
if: needs.discover.outputs.apps_self_hosted != '' && needs.discover.outputs.apps_self_hosted != '[]'
runs-on: [self-hosted, linux]
timeout-minutes: 360
needs: discover
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.discover.outputs.apps_self_hosted) }}
steps:
- name: Fix workspace ownership
shell: bash
run: sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
- uses: actions/checkout@v7.0.0
- name: Resolve repository owner
shell: bash
run: |
set -euo pipefail
echo "GITHUB_REPOSITORY_OWNER=$(scripts/meta/resolve/repository/owner.sh)" >> "$GITHUB_ENV"
- name: Deploy app
shell: bash
env:
apps: ${{ matrix.apps }}
variant: ${{ matrix.variant }}
INFINITO_TIMEOUT_MULTIPLIER: ${{ vars.INFINITO_TIMEOUT_MULTIPLIER || '1' }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./scripts/tests/deploy/ci/all.sh
- name: Dump runner state (on failure)
if: failure()
shell: bash
run: ./scripts/github/runner/dump_state.sh "post-deploy failure (${{ matrix.apps }})"
@@ -1,186 +0,0 @@
name: "📦 Test: Deploy (workstation)"
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: ""
permissions:
contents: read
packages: read
env:
INFINITO_DEPLOY_TYPE: workstation
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
jobs:
discover:
name: 🖥️ Discover apps
runs-on: ubuntu-latest
timeout-minutes: 60
outputs:
apps_github: ${{ steps.split.outputs.apps_github }}
apps_self_hosted: ${{ steps.split.outputs.apps_self_hosted }}
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
- 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 }}
run: ./scripts/github/resolve/output_apps.sh
- name: Split apps between GitHub-hosted and self-hosted runners
id: split
shell: bash
env:
APPS_JSON: ${{ steps.discover.outputs.apps }}
CI_SELF_HOSTED_RUNNER_COUNT: ${{ vars.CI_SELF_HOSTED_RUNNER_COUNT || '0' }}
run: bash scripts/github/split_runner_apps.sh
workstation:
name: 🖥️ Deploy ${{ matrix.apps }}${{ matrix.variant && format(' [{0}]', matrix.variant) || '' }}
if: needs.discover.outputs.apps_github != '' && needs.discover.outputs.apps_github != '[]'
runs-on: ubuntu-latest
timeout-minutes: 360
needs: discover
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.discover.outputs.apps_github) }}
steps:
# Reclaim runner space before Docker pulls start. Deploy jobs otherwise
# compete with the default Ubuntu image payload for limited root space.
# Option rationale: docs/contributing/artefact/files/github/workflow.md
- 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
# Replace the default ~4 GB runner swap with a larger file so transient
# memory spikes (e.g. peertube plugin install, issue #162) don't trip
# the host OOM-killer. Script picks / or /mnt based on live free space.
- name: 💾 Enlarge swap space
shell: bash
run: ./scripts/github/runner/enlarge_swap.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
shell: bash
env:
apps: ${{ matrix.apps }}
variant: ${{ matrix.variant }}
ANSIBLE_LOG_PATH: /tmp/ansible-${{ matrix.apps }}.log
run: ./scripts/tests/deploy/ci/all.sh
- name: 📊 Role runtimes
if: always()
shell: bash
run: python3 utils/github/profile/role_summary.py /tmp/ansible-${{ matrix.apps }}.log
- name: 🩺 Dump runner state (on failure)
if: failure()
shell: bash
run: ./scripts/github/runner/dump_state.sh "post-deploy failure (${{ matrix.apps }})"
workstation-self-hosted:
name: 🖥️ Deploy ${{ matrix.apps }}${{ matrix.variant && format(' [{0}]', matrix.variant) || '' }} (self-hosted)
if: needs.discover.outputs.apps_self_hosted != '' && needs.discover.outputs.apps_self_hosted != '[]'
runs-on: [self-hosted, linux]
timeout-minutes: 360
needs: discover
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.discover.outputs.apps_self_hosted) }}
steps:
- name: Fix workspace ownership
shell: bash
run: sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
- uses: actions/checkout@v7.0.0
- name: Resolve repository owner
shell: bash
run: |
set -euo pipefail
echo "GITHUB_REPOSITORY_OWNER=$(scripts/meta/resolve/repository/owner.sh)" >> "$GITHUB_ENV"
- name: Deploy app
shell: bash
env:
apps: ${{ matrix.apps }}
variant: ${{ matrix.variant }}
INFINITO_TIMEOUT_MULTIPLIER: ${{ vars.INFINITO_TIMEOUT_MULTIPLIER || '1' }}
run: ./scripts/tests/deploy/ci/all.sh
- name: Dump runner state (on failure)
if: failure()
shell: bash
run: ./scripts/github/runner/dump_state.sh "post-deploy failure (${{ matrix.apps }})"
-121
View File
@@ -1,121 +0,0 @@
name: "💬 Test: DNS"
on:
workflow_dispatch:
inputs:
distros:
description: "Space-separated distro list (e.g. 'arch debian ubuntu')"
required: false
default: "debian"
type: string
workflow_call:
inputs:
distros:
description: "Space-separated distro list"
required: true
type: string
permissions:
contents: read
packages: read
env:
INFINITO_IMAGE_TAG: ci-${{ github.sha }}
jobs:
prepare:
name: 🧮 Build matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.out.outputs.matrix }}
owner: ${{ steps.vars.outputs.owner }}
repo_name: ${{ steps.vars.outputs.repo_name }}
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Resolve image coordinates
id: vars
shell: bash
run: |
set -euo pipefail
owner="$(scripts/meta/resolve/repository/owner.sh)"
repo_name="${{ github.repository }}"
repo_name="${repo_name#*/}"
repo_name="${repo_name,,}"
echo "owner=${owner}" >> "$GITHUB_OUTPUT"
echo "repo_name=${repo_name}" >> "$GITHUB_OUTPUT"
- name: 📦 Install jq
shell: bash
run: ./scripts/install/apt.sh jq
- name: Compute matrix JSON
id: out
shell: bash
run: |
set -euo pipefail
distros="${{ inputs.distros }}"
json="$(scripts/github/runner/distros_matrix.sh "${distros:-debian}")"
echo "matrix=${json}" >> "$GITHUB_OUTPUT"
echo "Using matrix: ${json}"
dns:
name: 🌐 DNS (${{ matrix.distro }})
runs-on: ubuntu-latest
timeout-minutes: 20
needs: prepare
env:
NIX_CONFIG: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
INFINITO_IMAGE_TAG: ci-${{ github.sha }}
# Use the prebuilt CI image directly instead of the :latest fallback from env/all.sh.
INFINITO_IMAGE: ghcr.io/${{ needs.prepare.outputs.owner }}/${{ needs.prepare.outputs.repo_name }}/${{ matrix.distro }}:ci-${{ github.sha }}
strategy:
fail-fast: false
matrix:
distro: ${{ fromJson(needs.prepare.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
- name: Install dnsutils (dig)
shell: bash
run: ./scripts/install/apt.sh dnsutils
- name: Start stack (coredns + infinito) for distro
shell: bash
env:
INFINITO_DISTRO: ${{ matrix.distro }}
run: |
set -euo pipefail
make compose-up
- name: DNS test suite
shell: bash
env:
INFINITO_DISTRO: ${{ matrix.distro }}
run: |
set -euo pipefail
scripts/tests/dns.sh
- name: Dump CoreDNS logs on failure
if: failure()
shell: bash
run: |
docker ps -a || true
docker logs infinito-coredns --tail=200 || true
- name: Dump infinito logs on failure
if: failure()
shell: bash
env:
INFINITO_DISTRO: ${{ matrix.distro }}
run: |
set -euo pipefail
# shellcheck source=scripts/meta/env/load.sh
source scripts/meta/env/load.sh
docker logs "${INFINITO_CONTAINER}" --tail=200 || true
-90
View File
@@ -1,90 +0,0 @@
name: "💻 Test: Development Environment"
on:
workflow_dispatch:
workflow_call:
permissions:
actions: write
contents: read
packages: read
jobs:
test-environment:
name: 🧑‍💻 Environment (${{ matrix.dev_runtime_image }})
runs-on: ubuntu-latest
timeout-minutes: 270
strategy:
fail-fast: false
matrix:
dev_runtime_image:
- manjarolinux/base
- debian:bookworm
- ubuntu:latest
- fedora:latest
- quay.io/centos/centos:latest
env:
DEV_RUNTIME_IMAGE: ${{ matrix.dev_runtime_image }}
NIX_CONFIG: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
# Cache caps sized for post-prune re-pulls; paths on /mnt to spare the small / partition.
INFINITO_CACHE_REGISTRY_MAX_SIZE: 16g
INFINITO_CACHE_REGISTRY_HOST_PATH: /mnt/cache/registry/mirror
INFINITO_CACHE_REGISTRY_CA_HOST_PATH: /mnt/cache/registry/ca
INFINITO_CACHE_PACKAGE_BLOBSTORE_MAX: 8g
INFINITO_CACHE_PACKAGE_HEAP: 1g
INFINITO_CACHE_PACKAGE_DIRECT_MEM: 1g
INFINITO_CACHE_PACKAGE_HOST_PATH: /mnt/cache/package/data
steps:
# Reclaim host space before Docker pulls (rationale: docs/contributing/artefact/files/github/workflow.md); swap-storage stays since no enlarge_swap.sh replacement exists here.
- 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: false
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Run dev environment routine
shell: bash
run: |
set -euo pipefail
HOST_WORKDIR="$(pwd -P)"
# Build the dev image locally and mirror the host workdir path inside the container so docker compose can reach the host daemon via /var/run/docker.sock.
docker run --rm \
-e GITHUB_ACTIONS=true \
-e GITHUB_REPOSITORY="${{ github.repository }}" \
-e GITHUB_REPOSITORY_OWNER="${{ github.repository_owner }}" \
-e GITHUB_SHA="${{ github.sha }}" \
-e DEV_RUNTIME_IMAGE \
-e INFINITO_DISTRO=debian \
-e INFINITO_IMAGE=infinito-debian \
-e INFINITO_PULL_POLICY=never \
-e INFINITO_BUILD=1 \
-e NIX_CONFIG \
-e INFINITO_CACHE_REGISTRY_MAX_SIZE \
-e INFINITO_CACHE_REGISTRY_HOST_PATH \
-e INFINITO_CACHE_REGISTRY_CA_HOST_PATH \
-e INFINITO_CACHE_PACKAGE_BLOBSTORE_MAX \
-e INFINITO_CACHE_PACKAGE_HEAP \
-e INFINITO_CACHE_PACKAGE_DIRECT_MEM \
-e INFINITO_CACHE_PACKAGE_HOST_PATH \
--network host \
-v "${HOST_WORKDIR}:${HOST_WORKDIR}" \
-v "/mnt/docker:/var/lib/docker" \
-v "/var/run/docker.sock:/var/run/docker.sock" \
-w "${HOST_WORKDIR}" \
"${DEV_RUNTIME_IMAGE}" \
bash -lc '
bash ./scripts/tests/environment/00_orchestrator.sh
'
-51
View File
@@ -1,51 +0,0 @@
name: "📥 Test: Install Make"
on:
workflow_call:
permissions:
actions: write
contents: read
packages: read
jobs:
test-install-make:
name: 🛠️ Makefile
runs-on: ubuntu-latest
timeout-minutes: 15
env:
NIX_CONFIG: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
strategy:
fail-fast: false
matrix:
distro: [arch, debian, ubuntu, fedora, centos]
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Resolve paths from default.env
run: |
for key in INFINITO_SRC_DIR INFINITO_VENV_DIR; do
value="$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' default.env)"
test -n "${value}" || { echo "${key} not found in default.env" >&2; exit 1; }
echo "${key}=${value}" >> "$GITHUB_ENV"
done
- name: Install & test in pkgmgr virgin container
run: |
docker run --rm \
-e NIX_CONFIG="${NIX_CONFIG}" \
-e INFINITO_SRC_DIR="${INFINITO_SRC_DIR}" \
-e INFINITO_VENV_DIR="${INFINITO_VENV_DIR}" \
-v "$(pwd):${INFINITO_SRC_DIR}" \
-w "${INFINITO_SRC_DIR}" \
"ghcr.io/kevinveenbirkenbach/pkgmgr-${{ matrix.distro }}-virgin:stable" \
bash -lc '
set -euo pipefail
make install
make setup
source "${INFINITO_VENV_DIR}/bin/activate"
infinito --help
'
-48
View File
@@ -1,48 +0,0 @@
name: "📥 Test: Install Package Manager"
on:
workflow_call:
permissions:
actions: write
contents: read
packages: read
jobs:
test-install-pkgmgr:
name: 📦 PKGMGR
runs-on: ubuntu-latest
timeout-minutes: 15
env:
NIX_CONFIG: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
strategy:
fail-fast: false
matrix:
distro: [arch, debian, ubuntu, fedora, centos]
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
- name: Resolve INFINITO_VENV_DIR from default.env
run: |
venv_dir="$(awk -F= '/^INFINITO_VENV_DIR=/{print $2; exit}' default.env)"
test -n "${venv_dir}" || { echo "INFINITO_VENV_DIR not found in default.env" >&2; exit 1; }
echo "INFINITO_VENV_DIR=${venv_dir}" >> "$GITHUB_ENV"
- name: Install & test in pkgmgr virgin container
run: |
docker run --rm \
-w "/root/" \
-e NIX_CONFIG="${NIX_CONFIG}" \
-e INFINITO_VENV_DIR="${INFINITO_VENV_DIR}" \
-v "$(pwd):/root/Repositories/github.com/kevinveenbirkenbach/infinito-nexus" \
"ghcr.io/kevinveenbirkenbach/pkgmgr-${{ matrix.distro }}:stable" \
bash -lc '
set -euo pipefail
make -C "/root/Repositories/github.com/kevinveenbirkenbach/infinito-nexus" install-system-python
pkgmgr install infinito --clone-mode shallow --no-verification
source "${INFINITO_VENV_DIR}/bin/activate"
infinito --help
'
-58
View File
@@ -1,58 +0,0 @@
name: "🏃 Test: Runner smoke (self-hosted)"
# Dispatched by roles/svc-runner/files/test.sh after a runner deploy: verifies a
# self-hosted runner picks up a CI job and has Docker (DooD) access. Full deploy
# capability is covered by the svc-runner DinD test (test-e2e-cli).
on:
workflow_dispatch:
inputs:
ref:
description: "Git ref to check out (branch, tag, or SHA). Defaults to the repo default branch."
required: false
default: ""
type: string
workflow_call:
inputs:
ref:
description: "Git ref to check out (branch, tag, or SHA). Defaults to the repo default branch."
required: false
default: ""
type: string
concurrency:
group: runner-smoke-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: read
jobs:
smoke:
name: 🏃 Runner pickup + DooD
runs-on: [self-hosted, linux]
timeout-minutes: 360
steps:
- name: Fix workspace ownership
shell: bash
run: sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
- uses: actions/checkout@v7.0.0
with:
ref: ${{ inputs.ref || github.sha }}
- name: Verify runner pickup + Docker (DooD) access
shell: bash
run: |
set -euo pipefail
echo ">>> Job picked up on self-hosted runner: $(hostname)"
docker version
docker info >/dev/null
docker run --rm hello-world
- name: Dump runner state (on failure)
if: failure()
shell: bash
run: ./scripts/github/runner/dump_state.sh "runner-smoke failure"
-151
View File
@@ -1,151 +0,0 @@
name: "🔄 Update: Versions"
on:
push:
branches:
- main
schedule:
- cron: "0 0 * * 6"
workflow_dispatch:
permissions:
contents: read
pull-requests: read
jobs:
update-docker-image-versions:
name: 🐳 Update Docker image versions
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
with:
ref: main
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install update dependencies
run: bash scripts/github/update/install_python.sh
- name: Update Docker image versions
run: bash scripts/github/update/image_versions.sh
- name: Check for changes
id: diff
run: bash scripts/github/resolve/check_git_changes.sh roles
- name: Generate app token
if: steps.diff.outputs.changed == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
- name: Open PR if Docker image versions changed
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
UPDATE_BRANCH_PREFIX: update/docker-image-versions
UPDATE_COMMIT_MESSAGE: "update(images): update Docker image versions"
UPDATE_PR_TITLE: "update(images): update Docker image versions"
UPDATE_PR_BODY: "Automated update of semver-based Docker image versions in `roles/*/meta/services.yml`."
UPDATE_BASE_BRANCH: main
run: bash scripts/github/update/open_role_prs.sh roles
update-repository-refs:
name: 🌿 Update repository refs
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
with:
ref: main
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install update dependencies
run: bash scripts/github/update/install_python.sh
- name: Update repository refs
run: bash scripts/github/update/repository_refs.sh
- name: Check for changes
id: diff
run: bash scripts/github/resolve/check_git_changes.sh roles
- name: Generate app token
if: steps.diff.outputs.changed == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
- name: Open PR if repository refs changed
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
UPDATE_BRANCH_PREFIX: update/repository-refs
UPDATE_COMMIT_MESSAGE: "update(repos): bump git refs to latest semver tags"
UPDATE_PR_TITLE: "update(repos): bump git refs to latest semver tags"
UPDATE_PR_BODY: "Automated update of semver-based git `ref:` values in `roles/*/meta/services.yml`. Upstream tags resolved via `git ls-remote --tags`."
UPDATE_BASE_BRANCH: main
run: bash scripts/github/update/open_role_prs.sh roles
update-skills:
name: 🧠 Update skills-lock.json
if: vars.CI_ENABLE_AUTO_UPDATES == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.0
with:
ref: main
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: "npm"
- name: Update skills
run: bash scripts/install/skills/update.sh
- name: Check for changes
id: diff
run: bash scripts/github/resolve/check_git_changes.sh skills-lock.json
- name: Generate app token
if: steps.diff.outputs.changed == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.BOT_APP_CLIENT_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
- name: Open PR if skills-lock.json changed
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
UPDATE_BRANCH_PREFIX: update/skills
UPDATE_COMMIT_MESSAGE: "update(skills): update skills-lock.json to latest versions"
UPDATE_PR_TITLE: "update(skills): update agent skills to latest versions"
UPDATE_PR_BODY: "Automated daily update of `skills-lock.json` via `npx skills update`."
UPDATE_BASE_BRANCH: main
run: bash scripts/github/update/open_pr.sh skills-lock.json
+12 -6
View File
@@ -3,6 +3,9 @@
# NEVER TOUCH THE .dockerignore, BECAUSE IT ANYHOW WILL BE OVERWRITTEN
site.retry
.cache
.ansible/
.stack-torn-down
*__pycache__
compose.cache.override.yml
venv
@@ -16,17 +19,20 @@ roles/importer_result.json
build/*
.ruff_cache
.agents
.claude/skills/
.claude-*
collections/
.env
.onion-identity/
custom.env
logs/
job-logs.txt
.claude/skills
.claude/agents
.claude/commands
.claude/hooks
.claude/settings.local.json
*.lock
node_modules/
vendor/
.phpunit.result.cache
inventories/production/
/core.*
# No-op for Git (ignored implicitly), but Docker needs .git in its ignore-list — keeps make dockerignore as a pure cat of this file.
.git
@@ -34,6 +40,7 @@ compose/coredns/Corefile
compose/Corefile
# Host shell / editor / tooling state accidentally surfaced inside this working copy
.ansible
.bash_profile
.bashrc
.gitconfig
@@ -45,4 +52,3 @@ compose/Corefile
.vscode
.zprofile
.zshrc
+5 -2
View File
@@ -29,12 +29,15 @@
],
"ignores": [
"node_modules/**",
"vendor/**",
".cache/**",
".git/**",
"venv/**",
".venv/**",
"**/__pycache__/**",
".github/PULL_REQUEST_TEMPLATE/**",
".agents/skills/**",
".claude/skills/**"
".agents/**",
".claude/**",
"skills/**"
]
}
+34
View File
@@ -1,7 +1,9 @@
---
default_install_hook_types:
- pre-commit
- commit-msg
- pre-push
- post-checkout
repos:
- repo: local
@@ -13,6 +15,30 @@ repos:
pass_filenames: false
stages:
- pre-commit
- id: make-test-merge-signed
name: Run make test-merge-signed (merge gate)
entry: make test-merge-signed
language: system
pass_filenames: false
always_run: true
stages:
- commit-msg
- id: make-lint
name: Run make lint (pre-push gate)
entry: make lint
language: system
pass_filenames: false
always_run: true
stages:
- pre-push
- id: make-test-main-merged
name: Run make test-main-merged (pre-push gate)
entry: make test-main-merged
language: system
pass_filenames: false
always_run: true
stages:
- pre-push
- id: make-test-signed
name: Run make test-signed (pre-push gate)
entry: make test-signed
@@ -21,3 +47,11 @@ repos:
always_run: true
stages:
- pre-push
- id: clean-pycache-dirs
name: Prune orphan __pycache__-only dirs (post-checkout)
entry: make clean-pycache-dirs
language: system
pass_filenames: false
always_run: true
stages:
- post-checkout
+11
View File
@@ -0,0 +1,11 @@
[sqlfluff]
# Postgres is the majority engine. A role whose meta/services.yml declares
# `mariadb:` instead overrides this with its own .sqlfluff beside the files.
dialect = postgres
templater = placeholder
# The shipped SQL is executed through psycopg, so `%(name)s` parameter markers
# are the driver's, not SQL. Without this the parser rejects every file that
# takes an argument.
[sqlfluff:templater:placeholder]
param_style = pyformat
+3
View File
@@ -0,0 +1,3 @@
{
"rules": {}
}
+14
View File
@@ -41,6 +41,12 @@ Agents whose runtime does not consult `.claude/settings.json` MUST still enforce
Changes to the policy MUST edit [`.claude/settings.json`](.claude/settings.json). Per-entry rationale: [settings.md](docs/contributing/tools/agents/claude/settings.md); sandbox layer: [sandbox.md](docs/contributing/tools/agents/claude/sandbox.md).
## Shortcuts ⌨️
Operator messages MAY use the portable conversation shortcuts from the `shortcuts` skill (set up via `make install-skills`); agents MUST expand any matching shortcut before acting. The repository's own agent workflows live in the `i8-` skills catalogued in [cheatsheet.md](docs/contributing/tools/agents/cheatsheet.md).
Whenever an operator types out something an existing alias (listed by `make alias`) covers, the agent MUST append a hint right after that block so the operator learns the shorter form: ``Speed up by using prompt alias `<alias>` instead of `<what the operator wrote>`.`` for a prompt or request an agent shortcut covers, and ``Speed up by using cli alias `<alias>` instead of `<what the operator wrote>`.`` for a shell command a terminal alias covers.
## Role-Specific Instructions 📂
- Before modifying any file under `roles/<role>/`, check for `roles/<role>/AGENTS.md`. If present, read and follow it (including any file-scoped subsections) before any change.
@@ -49,6 +55,14 @@ Changes to the policy MUST edit [`.claude/settings.json`](.claude/settings.json)
Agents MUST write all transient files (downloaded logs, intermediate output, scratch artefacts) to `/tmp`. The set of writable paths is defined by `sandbox.filesystem.allowWrite` in [`.claude/settings.json`](.claude/settings.json); of those entries, `/tmp` is the designated path for agent scratch data. Other entries are reserved for their respective tooling and MUST NOT be repurposed for agent temp data. The repository working tree MUST NOT hold transient files.
## Credentials in Agent Output 🔐
Agents MUST read deploy logs by extracting the field they need (`msg`, `stderr`, a probe result). Printing a raw task block, `argv` list, or environment dump is **FORBIDDEN ⛔**, including via a line-range `sed`/`head` over one.
A secret denylist such as `grep -v 'password|token|key'` does **NOT** count as protection: it is case-sensitive, and secrets also travel inside URL paths. Local deploys run with `MASK_CREDENTIALS_IN_LOGS` disabled, so `no_log` does not mask these tasks.
When a credential must be compared, compute and compare digests inside the container; never emit the value. If a credential does reach the transcript, agents MUST say so in the same message and list it for rotation.
## Container-Owned Filesystem Entries 🐳
Files produced by the containerized runner (e.g. `__pycache__/*.pyc` under `tests/`, build artefacts) are often owned by `nobody` or another in-container UID and cannot be removed from the host. When a host-level `rm`/`chmod`/edit fails with `Permission denied` on such paths, agents MUST run the cleanup via `make compose-exec` (see [compose.yml](compose.yml); the repo is mounted at `/opt/src/infinito`) and MUST NOT ask the operator which path to take.
+260
View File
@@ -1,5 +1,265 @@
# Changelog
## [13.0.0] - 2026-08-26
* **Tor as a first-class deployment axis.** The new *svc-net-tor* role routes onion
names through Tor via DNS instead of per-app proxies, offers a chutney flavor for
private-net test deployments, and ties its dnsmasq listener to ownership rather than
the deploy mode. Applications follow: server-side OIDC discovery and token exchange
reach plaintext onion issuers through the SOCKS proxy (WordPress, Joomla, PeerTube,
Mobilizon, EspoCRM, Nextcloud), session cookies drop the *Secure* flag where an onion
cannot carry it (Taiga, Pixelfed, Joomla), and mail authenticates on the submission
port the onion relay actually serves. The deploy matrix gained a tor axis with an
onion marker plus enforced and exclusive modes, and lint now requires the onion in
every variant. See the [svc-net-tor](roles/svc-net-tor/README.md) README.
* **Single sign-on across protocols and domains.** Keycloak imports a SAML client for
every app that asks for one; SuiteCRM authenticates against it via SAML instead of a
proxy, and Shopware provisions its admin through AdminOpenAuth with the front and
back channel split. Sessions are shared across app domains, the issuer is pinned to
the canonical OIDC URL, each consumer's redirect scheme resolves on its own side, and
the logout sweep runs where the visitor is looking. Directory users are read through
one lookup so seeding cannot diverge.
* **One CI chain instead of four workflows.** The deploy workflows collapse into a
single chunk chain gated by one validation job, with every variant getting its own
row and rotating distro, filesystem and tor axes. Selection tokens pin deploy axes,
retriggers reproduce the exact selection of the source run in every mode, and the
regular line resumes where the source stopped. Fork code no longer runs with the base
repository token, workflow tokens are scoped to the jobs that need them, superseded
runs are force-cancelled, and a mid-run budget SIGKILL is counted as skipped rather
than green. Apt call sites are bounded so a stalled mirror cannot burn a job budget.
* **Restore verified against real dumps.** The *recover* CLI replays a generation's
database and cluster dumps, reads the generation's own layout instead of guessing it,
and places each dump by the engine the backup observed rather than by name alone. The
*svc-bkp-volume-2-local* drill now empties every volume it claims to test, fails on a
database that was never dumped, and stops the backup timers before it goes
destructive. Baudolo is pinned per host, its contract read from baudolo itself.
* **A probe for every container.** Health checks are composed from prefixes and are
mandatory: every declared image either carries a probe or states why it cannot have
one, enforced from both sides. Probes hit the port the container actually binds, no
longer derive an HTTP Host from a kernel hostname, and are defined once instead of per
proxy. Rescue diagnostics pin the dnsmasq listener loss to its moment, sample what the
resolver answers rather than just its socket, probe the daemon.json upstreams, and
record the whole boot instead of the last 10 000 lines.
* **Single points of truth, enforced by lint.** The new *sys-lint* role pins the
GitHub-release lint tools. Every mount target has one spot in *meta/volumes.yml* and
container paths are derived from it, *default.env* is the enforced source of the
primary domain, *container_backup* is rejected where no volume exists, and role
credentials live at *meta/secrets.yml*. Linters no longer read gitignored build
output, comments are checked in every file rather than only changed ones, and every
shipped script requires a mirrored unit test.
* **Tests in every language the project ships.** The quality gate parses each shipped
language and runs its unit suite; the PHP and Ruby interpreters are baked into the
image, and the JavaScript, PHP and Ruby suites are reported in the CI summary
alongside Python. Playwright stages the persona flows once per run and mounts them
read-only, and the workspace environment suite splits into base, compose and swarm
tracks.
* **Swarm and proxy operation.** Convergence gates read one budget sized for a real
stack, print every task row when a stack fails to converge, and give up on a terminal
task; the deployment mode a stack was deployed with is resolved instead of assumed.
OpenResty reloads a vhost, lua or redirect config instead of redeploying the task.
GitLab ships its config trees as swarm configs rather than host binds, and role
templates expand through a lookup instead of inline Jinja.
* **New and reshaped roles.** *sys-svc-nss* is split out of *sys-hostname*,
*web-svc-mirror* serves frontend assets as a privacy proxy, PeerTube becomes
clearnet-only, and roles that could not run were deleted rather than repaired. CLI
tools install via pip instead of pkgmgr.
* **Image and dependency version jumps (net since 12.0.0):**
*Applications*
* *web-app-erpnext*: v16.26.2 to v16.32.3
* *web-app-gitlab*: v19.1.2 to v19.3.0 (all six CNG component images)
* *web-app-semaphore*: v2.18.25 to v2.19.8
* *web-app-nextcloud*: PHP 33-fpm-alpine to 34-fpm-alpine, nginx 1.31.2 to 1.31.3
* *web-app-shopware*: 6.7.8.2 to 6.7.13.0, OpenSearch 3.7.0 to 3.8.0
* *web-app-matomo*: 5.11.2 to 5.13.0
* *web-app-matrix*: Synapse v1.156.0 to v1.159.0, Element v1.12.23 to v1.12.26
* *web-app-mattermost*: 11.9.0 to 11.10.1
* *web-app-keycloak*: 26.7.0 to 26.7.2, oauth2-proxy v7.15.3 to v7.15.4
* *web-app-gitea*: 1.26.4 to 1.27.2
* *web-app-funkwhale*: 2.0.6 to 2.0.9 (api and front)
* *web-app-penpot*: 2.16.2 to 2.17.1 (frontend, backend, exporter)
* *web-app-espocrm*: 10.0.2 to 10.0.6
* *web-app-opencloud*: 7.2.1 to 7.2.4
* *web-app-seaweedfs*: 4.39 to 4.44
* *web-app-snipe-it*: v8.6.3-alpine to v8.7.2-alpine
* *web-app-wordpress*: 7.0.1 to 7.1.0
* *web-app-peertube*: v8.2.2 to v8.2.4
* *web-app-joomla*: 6.1.2-php8.3-apache to 6.1.3-php8.3-apache
* *web-app-jira*: 11.3.8 to 11.3.10
* *web-app-confluence*: 10.2.14 to 10.2.15
* *web-app-baserow*: 2.3.1 to 2.3.3
* *web-app-flowise*: 3.1.3 to 3.1.4
* *web-app-mailu*: 1.5.3 to 1.5.4
* *web-app-pgadmin*: 9.16 to 9.17
* *web-app-prometheus*: Prometheus v3.13.1 to v3.14.0, Alertmanager v0.33.1 to
v0.34.0, node-exporter v1.12.0 to v1.12.1
* *web-app-opentalk*: LiveKit v1.13.3 to v1.13.5, RabbitMQ 4.3.2 to 4.3.4; the
frontend (v2.9.0), controller (v0.34.1) and recorder (v0.16.2) stay pinned until
the set is released as a bundle and *recorder.toml* is migrated
* *web-app-magento*: OpenSearch 3.7.0 to 3.8.0; PHP held at 8.4-fpm and MariaDB at
11.4, both deliberately fenced against the image bot
* *web-app-socialhome*: newly pinned to 2026.6.16
* *web-app-bluesky* and *web-app-bridgy-fed*: upstream git refs pinned to commits
*Services and infrastructure*
* *svc-ai-ollama*: 0.31.2 to 0.32.15
* *svc-db-elasticsearch*: 9.4.3 to 9.5.2
* *svc-db-qdrant*: v1.18.2 to v1.19.0
* *svc-runner*: Ubuntu 24.04 to 26.04
* *sys-ctl-hlth-csp*: 2.1.2 to 2.2.1
* *web-svc-coturn*: 4.14.0 to 4.17.2
* *web-svc-logout*: *latest* to universal-logout v1.3.1
* *web-svc-xmpp*: 26.04 to 26.07
* *test-e2e-playwright*: v1.61.1-noble to v1.62.1-noble
* *svc-net-tor*: newly on debian trixie-slim
* *svc-bkp-volume-2-local*: newly on alpine 3.24
*Toolchain and dependencies*
* *backup-docker-to-local* (*baudolo*): pinned to 7.0.1, via 6.0.0 and 7.0.0
* *ruff*: 0.16.0 to 0.16.4
* *@playwright/test*: ^1.61.1 to ^1.62.1
* *eslint*: ^10.6.0 to ^10.9.0, *globals*: ^17.7.0 to ^17.9.0
* *stylelint*: ^16.0.0 added
* *brace-expansion*: overridden to ^5.0.9, past three DoS advisories
* *keycloak-js*: 24.0.5 to 26.2.4, loaded as an ES module
* *bootstrap*: 5.3.3 to 5.3.8, *@fortawesome/fontawesome-free*: 6.5.1 to 7.3.1
* *roles/sys-lint* pins the GitHub-release tools: actionlint v1.7.12,
hadolint v2.15.1, shfmt v3.13.1
**Renamed**
* Role credential declarations move from *meta/credentials.yml* to *meta/secrets.yml*,
addressed at *secrets.credentials* like every other meta file.
* The NSS half of *sys-hostname* becomes its own role, *sys-svc-nss*.
**Contributors**
* [Kevin Veen-Birkenbach](https://veen.world): Tor deployment axis, SAML and
cross-domain SSO, CI chunk chain, generation-level recovery, health-check and
lint enforcement, multi-language test suites
## [12.0.0] - 2026-08-08
* **Docker Swarm as a second deployment mode.** Roles can now be deployed across
multiple hosts instead of being limited to a single Compose stack. New
*svc-swarm-node* initializes the cluster, while *svc-swarm-manager* marks the Raft
managers in the inventory. Shared state is provided through
*svc-storage-nfs-server* and *svc-storage-nfs-client*. *svc-registry-docker*
distributes locally built images, and *svc-registry-cache* provides a Docker Hub
pull-through cache so additional nodes do not multiply external pulls and
rate-limit usage. Deployment, dependency resolution, networking, health checks and
role templates now understand both Compose and Swarm semantics. See the
[svc-swarm-node](roles/svc-swarm-node/README.md) and
[svc-storage-nfs-server](roles/svc-storage-nfs-server/README.md) READMEs.
* **Central engines and object storage.** A new *engine* lookup extends the existing
shared-database pattern to non-RDBMS services. Consumers can use centrally
provisioned Redis, Memcached, RabbitMQ, Elasticsearch, Typesense, Qdrant and Unbound
instances with isolated credentials, or retain an embedded sidecar.
*sys-svc-objstore* and the new *objstore* lookup provide the same abstraction for
SeaweedFS and MinIO, with lazy provider inclusion and object-storage integration
across fourteen application roles.
* **Backup and disaster recovery.** *svc-bkp-container-2-local* is superseded by
*svc-bkp-volume-2-local*, making backup policy explicit per volume. The role prefers
filesystem snapshots when available and can prepare btrfs hosts for snapshot-based
backups itself. New *svc-bkp-nfs-2-local* protects the shared NFS export,
*svc-bkp-secrets-2-local* captures secret material, and
*svc-bkp-local-2-device* synchronizes backups to mounted external media. A unified
*recover* CLI drives restores, while Compose recovery checks and a full Swarm
disaster-recovery drill continuously verify the path. Rsync no longer retains
superseded files as *~* copies.
* **Deployment and packaging foundations.** A host deployment mode joins Compose and
Swarm in the deployment tooling. The former *sys-package*, *sys-aur* and
*sys-aur-install* roles were replaced by a distribution-independent package
registry. Volume metadata now uses one canonical schema, single-file mounts become
Swarm configs, and service placement, replica counts, dependencies, addresses and
health probes resolve through deployment-aware lookups.
* **Isolated development worktrees.** Parallel branches can use independent stacks
while sharing one cache layer, preventing containers and network state from
colliding. New *swarm-app-exec*, *swarm-diagnostic*, zombie-cluster and cross-mode
roundtrip targets improve live inspection and Compose/Swarm parity testing.
* **CI and diagnostics.** Deployment runs are planned separately per mode and divided
into starred priority and regular lines, with branch-new roles scheduled first.
Matrix entries serialize across branches, large lines serialize their deployment
modes, and failed roles can be retriggered on the original distribution set;
*--strict* limits this to hard failures. Cancelled runs can have their failed jobs
rerun, and role failures on *main* open or update dedicated issues. Deployment stages
are sourced from *categories.yml*.
Rescue collection now uses one indexed diagnostic pipeline covering journal and
kernel windows, resolver and network state, PostgreSQL activity, container-kill
attribution, storage devices and NFS-Ganesha thread state. The Swarm recovery drill
and README deployment guide run across every supported distribution. CI also varies
Docker's data-root filesystem so ext4, btrfs and ZFS-specific paths receive real
coverage.
* **Testing and documentation tooling.** New *test-e2e-cli* exercises the command-line
interface end to end. Lint and test jobs publish per-target and per-test summaries,
failed target logs are replayed after the summary, and the complexity matrix shows
whether a role already exists on *origin/main*. Generated role READMEs now provide
consistent status tables, quick-start instructions and dependency diagrams.
* **New lint contracts.** Port variables must resolve through lookups, scheduler
services must declare *replicas: 1*, and volume-owning stack roles must declare
their backup policy. *role_path* is forbidden in shell and command bodies.
Deployment-mode defaults, container-inspection resolvers, asynchronous-task
rationales and health-check declarations are validated centrally. Hadolint
directives and documented *nocheck* exceptions are recognized instead of producing
false positives.
* **Image delivery and frontend dependencies.** Image references now resolve from
*meta/services.yml* as their single source of truth, replacing unpinned *latest*
tags. *web-app-bigbluebutton* pins its component images separately. *web-svc-cdn*
builds and serves mirrored frontend packages through an internal one-shot service,
with the package-cache CA trusted by build containers.
* **Application improvements.** Penpot gained hardened OIDC, LDAP and native-login
variants, SeaweedFS-backed asset tests and Swarm-ready health checks. Pi-hole
received its v6-compatible login and password flow, Keycloak RBAC integration and
expanded persona coverage. Further deployment, replication and startup fixes landed
across Akaunting, BigBlueButton, ERPNext, Gitea, Keycloak, Magento, Mailu, Mastodon,
Matomo, MediaWiki, Nextcloud, OpenProject, Pixelfed, Semaphore, Taiga, WordPress,
XWiki and the internal CDN.
* **Lifecycle promotions:** *web-app-checkmk*, *web-app-jellyfin*, *web-app-n8n*,
*svc-registry-docker* and *svc-registry-cache* advance to *beta*.
* **Image and dependency version jumps (net since 11.6.0):**
* *web-app-gitlab*: omnibus *gitlab-ee* 19.1.1 to CNG component images v19.1.2
* *web-app-mattermost*: 11.8.3 to 11.9.0
* *web-app-opentalk*: v2.8.1 to v2.9.0
* *web-app-prometheus*: v3.13.0 to v3.13.1
* *web-app-joomla*: 6.1.1-php8.3-apache to 6.1.2-php8.3-apache
* *web-app-baserow*: 2.3.0 to 2.3.1
* *web-app-magento*: PHP 8.5-fpm to 8.4-fpm
* *backup-docker-to-local*: 3.1.1 to 3.1.3
* *baudolo*: pinned to 3.4.1
**Removed**
* *sys-package*, *sys-aur* and *sys-aur-install*, superseded by the
distribution-independent package registry.
* *svc-bkp-container-2-local*, superseded by the volume-oriented backup roles.
**Contributors**
* [Kevin Veen-Birkenbach](https://veen.world): Swarm and NFS deployment mode,
central engines and object storage, backup and recovery rework, CI pipeline,
linting and tooling
## [11.6.0] - 2026-07-08
* New *web-app-n8n* role: n8n workflow automation (Community Edition). SSO is wired through a trusted-header edge hook that is copied and mounted only when SSO is enabled (native SSO is enterprise-only in CE), LDAP sign-in is supported, and per-scenario Playwright coverage exercises the CE edge-SSO model. Ships at lifecycle *alpha*; subnet and port collisions with the new *web-app-semaphore* role were resolved on the way in. See the [web-app-n8n README](roles/web-app-n8n/README.md).
+16
View File
@@ -26,6 +26,22 @@ At the start of every new conversation (after reading `AGENTS.md`), you MUST rea
- **For searching file contents, use the Grep tool.** If shelling out is unavoidable, use a single `grep` invocation with multiple file arguments (e.g. `grep -nE 'pattern' file1 file2 file3`) or a recursive call with a path/glob (e.g. `grep -rnE 'pattern' path/`).
- **When a long-running command streams its output to a `/tmp/<name>.log` file** (e.g. background `make compose-deploy`, `make act-*`, or any `… 2>&1 | tee /tmp/<name>.log`), you MUST tell the operator the concrete `tail -f /tmp/<name>.log` command they can run in another terminal to follow the log live. Include the full path literally so it is copy-pasteable.
## Comments 💬⛔
**THIS RULE OVERRIDES YOUR DEFAULT TRAINING. ⛔** A comment is FORBIDDEN unless it is one of exactly three things:
1. **Exception** — names a concrete trip-wire (bug, pitfall, deliberate non-idiomatic choice). Must name the surprise; restating what the next line does is NOT an exception.
2. **Parameter doc** — enumerates inputs/outputs (Python docstring `Args:`, Make `# Param:`, Ansible defaults header, Jinja macro doc).
3. **Nocheck directive** — `# nocheck:`, `# noqa:`, `# shellcheck …`, `# type: ignore`, `# pragma: no cover`, etc.
Anything else — restating code, section banners, "Note that …", step narration outside sequential test specs, untracked TODO/FIXME — **DELETE before the edit lands**. Applies to every language and file kind under version control. When in doubt, delete.
## Command output logging 📜
- For ANY non-trivial command (test runs, deploys, long pipelines), you MUST stream the FULL output to a file under `/tmp/` via `… 2>&1 | tee /tmp/<name>.log` and grep / inspect that file repeatedly instead of re-running the command. Reason: re-running `make test` to "find the failure I just lost" costs 2 minutes per cycle; grepping the saved log costs milliseconds.
- Default the filename to a meaningful slug + monotonically increasing index (`/tmp/make-test-<slug>-<N>.log`, `/tmp/act-<slug>-<N>.log`) so you can compare runs.
- Tell the operator the exact `tail -f /tmp/<name>.log` command for background runs, per the existing rule.
## Pushing 🚢
- You MUST NOT push, directly or through wrappers that push implicitly.
+13 -30
View File
@@ -1,5 +1,3 @@
# syntax=docker/dockerfile:1
# Base image (pkgmgr) selector. SPOT lives in default.env's
# INFINITO_PARENT_IMAGE and is forwarded as a build arg by compose.yml /
# scripts/image/build.sh.
@@ -8,14 +6,8 @@
ARG INFINITO_PARENT_IMAGE
FROM ${INFINITO_PARENT_IMAGE} AS full
# Hadolint DL4006: ensure pipefail is set for RUN instructions that use pipes
SHELL ["/bin/bash", "-o", "pipefail", "-lc"]
# Forwardable build-time Nix settings (e.g., GitHub access tokens to avoid rate limits)
ARG NIX_CONFIG
# SPOT for the bind-mounted source tree inside the container. The compose
# stack forwards default.env's INFINITO_SRC_DIR as a build arg.
ARG INFINITO_SRC_DIR
ENV INFINITO_SRC_DIR=${INFINITO_SRC_DIR}
ARG INFINITO_VENV_DIR
@@ -24,7 +16,9 @@ ENV PYTHON="${INFINITO_VENV_DIR}/bin/python"
ENV PIP="${INFINITO_VENV_DIR}/bin/python -m pip"
ENV PATH="${INFINITO_VENV_DIR}/bin:${PATH}"
# Make Nix non-interactive for flake config (CI-friendly)
# hadolint DL4006: ensure pipefail is set for RUN instructions that use pipes
SHELL ["/bin/bash", "-o", "pipefail", "-lc"]
RUN set -euo pipefail; \
cat /etc/os-release || true; \
if [ -f /etc/nix/nix.conf ]; then \
@@ -32,48 +26,37 @@ RUN set -euo pipefail; \
echo 'accept-flake-config = true' >> /etc/nix/nix.conf; \
fi
# ------------------------------------------------------------
# Infinito.Nexus source in
# ------------------------------------------------------------
COPY . ${INFINITO_SRC_DIR}
# ------------------------------------------------------------
# Install Python 3.11+, Docker CLI, and distro package dependencies
# ------------------------------------------------------------
# hadolint ignore=DL3008,DL3033,DL3041
# hadolint ignore=DL3008,DL3033,DL3041,SC1090
RUN set -euo pipefail; \
/bin/bash ${INFINITO_SRC_DIR}/roles/dev-python/files/install.sh; \
/bin/bash ${INFINITO_SRC_DIR}/roles/sys-svc-container/files/install-cli.sh; \
/bin/bash ${INFINITO_SRC_DIR}/scripts/install/package.sh
source <(grep -hE '^INFINITO_((PYTHON|DOCKER_CLI|PACKAGE|FILESYSTEM|INTERPRETERS)_INSTALL_SCRIPT|APT_UBUNTU_MIRRORS)=' "${INFINITO_SRC_DIR}/default.env"); \
INFINITO_APT_UBUNTU_MIRRORS="${INFINITO_APT_UBUNTU_MIRRORS:?}" /bin/bash "${INFINITO_SRC_DIR}/scripts/install/apt-mirrors.sh"; \
/bin/bash "${INFINITO_SRC_DIR}/${INFINITO_PYTHON_INSTALL_SCRIPT:?}"; \
/bin/bash "${INFINITO_SRC_DIR}/${INFINITO_DOCKER_CLI_INSTALL_SCRIPT:?}"; \
/bin/bash "${INFINITO_SRC_DIR}/${INFINITO_PACKAGE_INSTALL_SCRIPT:?}"; \
/bin/bash "${INFINITO_SRC_DIR}/${INFINITO_FILESYSTEM_INSTALL_SCRIPT:?}"; \
/bin/bash "${INFINITO_SRC_DIR}/${INFINITO_INTERPRETERS_INSTALL_SCRIPT:?}"
# ------------------------------------------------------------
# Disable interactive first-boot units (CI / container safe)
# ------------------------------------------------------------
RUN set -euo pipefail; \
systemctl mask systemd-firstboot.service first-boot-complete.target || true; \
systemd-machine-id-setup || true
# systemd-in-container conventions
ENV container=docker
STOPSIGNAL SIGRTMIN+3
# hadolint ignore=SC3040
RUN set -euo pipefail; \
export NIX_CONFIG="${NIX_CONFIG:-}"; \
"${INFINITO_SRC_DIR}/scripts/docker/entry.sh" --compile -- true
# Set workdir to / to avoid ambiguous commands
WORKDIR /
COPY scripts/docker/healthcheck.sh /usr/local/bin/healthcheck.sh
RUN chmod +x /usr/local/bin/healthcheck.sh
# Allow 20s because `infinito --help` can exceed 5s on fresh CI runners.
HEALTHCHECK --interval=5s --timeout=20s --start-period=30s --retries=20 \
CMD /usr/local/bin/healthcheck.sh
CMD ["/usr/local/bin/healthcheck.sh"]
# JSON-form ENTRYPOINT does not expand ENV vars; wrap via bash -c so the
# INFINITO_SRC_DIR SPOT still controls the path. `exec` preserves systemd
# signal forwarding; `--` is the placeholder for $0.
ENTRYPOINT ["/bin/bash", "-c", "exec \"${INFINITO_SRC_DIR}/scripts/docker/entry.sh\" \"$@\"", "--"]
# IMPORTANT: default to systemd as PID 1
CMD ["/sbin/init"]
+396 -34
View File
@@ -15,26 +15,43 @@ endif
.DEFAULT_GOAL := help
.PHONY: act-all
# Run all act-based deploy checks.
act-all:
@bash scripts/tests/deploy/act/all.sh
.PHONY: act-debug
# Param: node=<container_name> cmd='<shell pipeline>'
act-debug:
@docker exec $(node) bash --noprofile --norc -c "$(cmd)"
.PHONY: act-app
# Run the act-based app deploy check.
act-app:
@bash scripts/tests/deploy/act/app.sh
.PHONY: act-runner-image
# Build local/act-runner-fixed: the stock act runner image with /var/run removed so a recent Docker engine accepts act's job-setup content copy.
# Usage: ACT_PLATFORM_IMAGE=local/act-runner-fixed:latest make swarm-zombie app=<app>
# Note: see docs/agents/action/iteration/workflow.md.
act-runner-image:
@bash scripts/tests/deploy/act/build_runner_image.sh
.PHONY: act-workflow
# Run the act-based workflow deploy check.
act-workflow:
act-workflow: install-act
@bash scripts/tests/deploy/act/workflow.sh
.PHONY: alias
# Print the portable agent shortcuts and the operator's terminal aliases.
alias:
@bash scripts/make/alias.sh
.PHONY: autoformat
# Auto-format all source files (skips tools that are not installed).
autoformat: install-lint
@bash scripts/lint/wrapper.sh autoformat
.PHONY: autoformat-restage
# Autoformat, then re-stage files it rewrote that were already staged -- only when no unstaged changes were present beforehand.
autoformat-restage:
@bash scripts/git/autoformat_restage.sh "$(MAKE)" autoformat
.PHONY: bond
# Serve the role bond matrix, where editing a cell rewrites the role's bond.
bond:
@"$${PYTHON}" -m cli.meta.roles.applications.bond $(args)
.PHONY: bootstrap
# Install dependencies and prepare the project.
bootstrap: install setup
@@ -53,7 +70,7 @@ build-cleanup:
.PHONY: build-dependency
# Pull the build dependency image.
build-dependency:
@docker pull ghcr.io/kevinveenbirkenbach/pkgmgr-$${INFINITO_DISTRO}:stable
@docker pull "$${INFINITO_PARENT_IMAGE:?Run 'make dotenv' to generate the .env single source of truth}"
.PHONY: build-missing
# Build the local image if it is missing.
@@ -76,6 +93,11 @@ build-no-cache-all:
INFINITO_DISTRO="$$d" "$(MAKE)" build-no-cache; \
done
.PHONY: cheat
# Print the operator prompt cheatsheet from docs/contributing/tools/agents/cheatsheet.md.
cheat:
@bash scripts/make/cheatsheet.sh
.PHONY: clean
# Remove ignored files from the working tree.
# Note: falls back to sudo for container-owned __pycache__/*.pyc; warns and continues if both fail.
@@ -101,7 +123,7 @@ clean-cache:
@bash scripts/system/cache/clean.sh
.PHONY: clean-container-owned
# Remove container-owned generated artefacts (build/, tasks/groups/*.yml).
# Remove container-owned generated artefacts (build/).
# Note: these files are created inside the compose container with the in-container UID (typically `nobody`); the host cannot rm them directly.
# Note: the helper auto-starts a stopped infinito container before deleting; safe no-op when the targets do not exist.
clean-container-owned:
@@ -113,12 +135,46 @@ clean-container-owned:
clean-pycache-dirs:
@"$${PYTHON}" -m utils.cleanup.pycache_only_dirs
.PHONY: clean-stale-nfs
# Recover stale in-namespace NFS mounts from wedged act-swarm nfs-server containers.
# Usage: make clean-stale-nfs [cid=<container-id-or-name>] [mount=/mnt/gtest]
clean-stale-nfs: swarm-clean-stale-nfs
.PHONY: clean-sudo
# Remove ignored files from the working tree with sudo.
clean-sudo:
@echo "Removing ignored git files with sudo"
sudo git clean -fdX;
.PHONY: compose-app-exec
# Run a one-off command inside a deployed app container of the local compose stack.
# Usage: make compose-app-exec app=<container> cmd="..."
# Example: make compose-app-exec app=flowise cmd="wget -qO- http://localhost:3000/api/v1/ping"
# Param app: container name of the deployed app.
# Param cmd: shell command to run inside it.
compose-app-exec:
@test -n '$(app)' || { echo 'usage: make compose-app-exec app=<container> cmd="..."'; exit 2; }
@app='$(app)' cmd='$(cmd)' bash scripts/tests/deploy/local/exec/app.sh
.PHONY: compose-app-logs
# Dump the logs of a deployed app container of the local compose stack.
# Usage: make compose-app-logs app=<container> [tail=<lines>]
# Example: make compose-app-logs app=litellm tail=80
# Param app: container name of the deployed app.
# Param tail: number of trailing lines (default: 200).
compose-app-logs:
@test -n '$(app)' || { echo 'usage: make compose-app-logs app=<container> [tail=<lines>]'; exit 2; }
@app='$(app)' tail='$(tail)' bash scripts/tests/deploy/local/exec/logs.sh
.PHONY: compose-app-restart
# Restart a single deployed app container of the local compose stack.
# Usage: make compose-app-restart app=<container>
# Example: make compose-app-restart app=nextcloud
# Param app: container name of the deployed app.
compose-app-restart:
@test -n '$(app)' || { echo 'usage: make compose-app-restart app=<container>'; exit 2; }
@app='$(app)' bash scripts/tests/deploy/local/exec/restart.sh
.PHONY: compose-deploy
# Run the local deploy router.
# Usage: make compose-deploy [mode=...] [apps=...] [purge=...] [type=...] [bundles=...] [disable=...] [full_cycle=...] [variant=...] [debug=...]
@@ -127,15 +183,13 @@ clean-sudo:
# Param mode: initialize | reinstall | update (default: initialize)
# Param apps: comma-separated app ids (e.g. web-app-matomo,web-app-keycloak)
# Param purge: true | false (default: false) — purge entities before deploy
# Param type: server | workstation | universal (default: from default.env)
# Param bundles: comma-separated bundle names; overrides apps when set
# Param disable: comma-separated service names to render as disabled
# Param full_cycle: true | false — when true, also run the async update pass
# Param variant: matrix round index to pin the redeploy to a specific variant
# Param debug: true | false (default: from default.env)
compose-deploy:
@$(if $(type),INFINITO_DEPLOY_TYPE="$(type)") \
$(if $(debug),INFINITO_DEBUG="$(debug)") \
@$(if $(debug),INFINITO_DEBUG="$(debug)") \
bash scripts/tests/deploy/local/deploy/main.sh
.PHONY: compose-down
@@ -209,6 +263,13 @@ compose-up: install
console:
@"$${PYTHON}" -m cli.console
.PHONY: cosmos
# Regenerate the '## Cosmos' mermaid diagram in every role README (or one role).
# Usage: make cosmos [role=<id>]
# Param role: single role id (default: all roles)
cosmos:
@"$${PYTHON}" -m cli.build.docs.readme $(role) --update-cosmos
.PHONY: diagnose-disk-usage
# Show disk and Docker resource usage to identify what to clean up.
diagnose-disk-usage:
@@ -220,6 +281,14 @@ diagnose-disk-usage:
diagnose-network:
@$(MAKE) compose-exec cmd="python3 -m cli.contributing.network.diagnose"
.PHONY: docs
# Regenerate generated documentation: role Cosmos diagrams, Quick Setup blocks, the root-README roles index, and the MCP audit report.
docs:
@"$(MAKE)" cosmos
@"$(MAKE)" readme-generate quick_setup=true
@"$(MAKE)" readme-index
@"$(MAKE)" mcp-audit
.PHONY: dotenv
# Regenerate .env (SPOT) from default.env + runtime context.
# Note: runtime context covers distro, cache sizes, secrets, and the like.
@@ -267,12 +336,27 @@ help:
install:
@bash scripts/install/all.sh
.PHONY: install-act
# Install act (nektos/act) if missing; provisions the act-based make targets.
install-act:
@bash scripts/install/act.sh
.PHONY: install-act-update
# Force act (nektos/act) to its latest release.
install-act-update:
@bash scripts/install/act.sh update
.PHONY: install-agent
# Install OS-level sandbox dependencies required by the Claude Code sandbox.
# Note: pulls in bubblewrap and socat.
install-agent:
@bash scripts/install/sandbox.sh
.PHONY: install-alias
# Install the terminal aliases from INFINITO_ALIAS_REPOSITORY into the user's shell config.
install-alias:
@bash scripts/install/alias.sh
.PHONY: install-ansible
# Install Ansible dependencies.
install-ansible:
@@ -297,10 +381,20 @@ install-lint:
install-lint-force:
@bash scripts/install/wrapper.sh --force
.PHONY: install-node
# Install the Node.js runtime the JavaScript unit suite runs on.
install-node:
@bash scripts/install/node.sh
.PHONY: install-php
# Install PHP, Composer and the vendor tree the PHP unit suite runs on.
install-php:
@bash scripts/install/php.sh
.PHONY: install-python
# Install Python tooling.
install-python: install-venv
@bash scripts/install/python.sh
@bash scripts/install/python.sh deploy
.PHONY: install-python-dev
# Install Python tooling including lint and dev dependencies.
@@ -308,34 +402,57 @@ install-python-dev: install-python
@bash scripts/install/python.sh dev
@bash scripts/install/pre-commit.sh
.PHONY: install-ruby
# Install the Ruby interpreter the Ruby unit suite runs on.
install-ruby:
@bash scripts/install/ruby.sh
.PHONY: install-skills
# Install agent skills from skills-lock.json.
# Install the agent skills from INFINITO_SKILLS_REPOSITORY into this project.
install-skills:
@bash scripts/install/skills/install.sh
@bash scripts/install/skills.sh
.PHONY: install-system-python
# Install the system Python prerequisites.
install-system-python:
@bash roles/dev-python/files/install.sh ensure
@bash "$${INFINITO_PYTHON_INSTALL_SCRIPT:?}" ensure
.PHONY: install-venv
# Install the virtual environment.
install-venv: install-system-python
@bash scripts/install/venv.sh
.PHONY: integration-matrix
# Regenerate the role-by-role integration matrix from the roles and the curated edge map.
integration-matrix:
@"$${PYTHON}" -m cli.build.docs.integration_matrix
.PHONY: kernel-loop-load
# Load the kernel loop driver the swarm backup DR drill needs.
# Note: run this on the host; container environments have no modprobe.
kernel-loop-load:
@sudo bash scripts/system/kernel/loop/load.sh
.PHONY: lint
# Run all lint checks in parallel.
# Note: each check runs on host or in docker per INFINITO_LINT_RUNNER.
lint: install-lint
@bash scripts/make/parallel.sh lint-action \
lint-ansible \
lint-css \
lint-dockerfile \
lint-javascript \
lint-makefile \
lint-markdown \
lint-mermaid \
lint-packages \
lint-php \
lint-playwright \
lint-php \
lint-python \
lint-shellcheck
lint-ruby \
lint-shellcheck \
lint-sql
.PHONY: lint-action
# Run the GitHub Actions lint checks.
@@ -348,6 +465,17 @@ lint-action: install-lint
lint-ansible: install-lint setup
@bash scripts/lint/wrapper.sh ansible
.PHONY: lint-css
# Check that every CSS file parses, via stylelint with an empty rule set.
lint-css: install-lint
@bash scripts/install/wrapper.sh css
@bash scripts/lint/wrapper.sh css
.PHONY: lint-dockerfile
# Run hadolint over the root Dockerfile.
lint-dockerfile: install-lint
@bash scripts/lint/wrapper.sh dockerfile
.PHONY: lint-javascript
# Run ESLint over the project's JavaScript files.
# Note: covers Playwright specs and persona helpers.
@@ -364,6 +492,11 @@ lint-makefile: install-lint
lint-markdown: install-lint
@bash scripts/lint/wrapper.sh markdown
.PHONY: lint-mermaid
# Render every Markdown mermaid diagram via mmdc; fails on any diagram GitHub cannot render.
lint-mermaid: install-lint
@bash scripts/lint/wrapper.sh mermaid
.PHONY: lint-packages
# Validate distro packaging metadata (debian changelog, fedora spec, arch PKGBUILD).
# Note: provisions the native parsers explicitly, then validates; absent tools are skipped.
@@ -371,6 +504,12 @@ lint-packages: install-lint
@bash scripts/install/wrapper.sh packages
@bash scripts/lint/wrapper.sh packages
.PHONY: lint-php
# Check that every PHP file parses, via `php -l`.
lint-php: install-lint
@bash scripts/install/wrapper.sh php
@bash scripts/lint/wrapper.sh php
.PHONY: lint-playwright
# Verify every role's Playwright spec parses + resolves its helpers.
# Note: stages the spec like test-e2e-playwright does and runs `npx playwright test --list`.
@@ -382,11 +521,29 @@ lint-playwright: install-lint
lint-python: install-lint
@bash scripts/lint/wrapper.sh python
.PHONY: lint-ruby
# Check that every Ruby file parses, via `ruby -c`.
lint-ruby: install-lint
@bash scripts/install/wrapper.sh ruby
@bash scripts/lint/wrapper.sh ruby
.PHONY: lint-shellcheck
# Run shellcheck lint checks.
lint-shellcheck: install-lint
@bash scripts/lint/wrapper.sh shellcheck
.PHONY: lint-sql
# Check that every SQL file parses, via `sqlfluff parse`.
# Note: dialect comes from the nearest .sqlfluff; absent tooling is skipped.
lint-sql: install-lint
@bash scripts/install/wrapper.sh sql
@bash scripts/lint/wrapper.sh sql
.PHONY: mcp-audit
# Regenerate the MCP audit report; test_mcp_audit_report fails when it drifts.
mcp-audit:
@"$${PYTHON}" -m cli.build.docs.mcp_audit
.PHONY: meta-list
# Print the repository role list.
meta-list:
@@ -437,19 +594,66 @@ network-trust-ca:
@bash scripts/system/tls/trust/linux.sh
@bash scripts/system/tls/trust/wsl2.sh
.PHONY: onboard
# Set up a developer workstation end to end: dependencies, project setup, agent skills, terminal aliases, host network/security prep, and the dev extras inside the running dev container.
onboard: bootstrap install-skills install-alias environment-bootstrap
@"$(MAKE)" compose-up
@"$(MAKE)" compose-exec cmd="bash scripts/install/dev-extras.sh"
.PHONY: quality
# Regenerate generated docs, autoformat, then run the full test suite (pre-commit gate).
quality:
@"$(MAKE)" docs
@"$(MAKE)" autoformat
@"$(MAKE)" test
.PHONY: quality-high
# Full gate: quality (autoformat + test) followed by every lint check.
quality-high: quality lint
.PHONY: readme-check
# Verify every role README matches the schema template (writes nothing; fails if any would change).
readme-check:
@"$${PYTHON}" -m cli.build.docs.readme --check
.PHONY: readme-generate
# Generate/complete role README.md files from templates/roles/README.md.j2.tmpl.
# Usage: make readme-generate [role=<id>] [override=true] [cosmos=true] [quick_setup=true]
# Param role: single role id (default: all roles)
# Param override: true regenerates managed sections even when present
# Param cosmos: true regenerates only the Cosmos diagram
# Param quick_setup: true regenerates only the Quick Setup section
readme-generate:
@"$${PYTHON}" -m cli.build.docs.readme $(role) $(if $(filter true,$(override)),--override) $(if $(filter true,$(cosmos)),--update-cosmos) $(if $(filter true,$(quick_setup)),--update-quick-setup)
.PHONY: readme-index
# Regenerate the invokable-role overview table in the root README.md.
# Param check: true verifies only and fails when the table is outdated
readme-index:
@"$${PYTHON}" -m cli.build.docs.readme.overview $(if $(filter true,$(check)),--check)
.PHONY: requirements-archive
# Archive fully-checked requirement files via pkgmgr (installs kpmx if missing).
requirements-archive:
@"$${PYTHON}" -m pip install --quiet --upgrade kpmx
@"$${PYTHON}" -m pkgmgr archive docs/requirements
.PHONY: roundtrip
# Validate one or more roles through every deploy mode in order (compose, then swarm), stopping at the first failure.
# Param apps: space-separated role ids; default = one role per base cluster, most-complex first (complexity --unique).
# Param modes: optional mode sequence (default "compose swarm"; append k8s once it exists).
# Param keep: true keeps each validated swarm cluster instead of releasing it.
# Param disable: optional comma-separated provider keys removed from the test inventory (e.g. matomo,dashboard,prometheus,email,css).
roundtrip:
@apps='$(apps)' modes='$(modes)' keep='$(keep)' disable='$(disable)' bash scripts/tests/deploy/roundtrip.sh
.PHONY: runner-ci-deploy
# Provision self-hosted CI runner instances on a remote host.
# Usage: make runner-ci-deploy HOST=runner.example.com DISTRO=debian [COUNT=15] [PORT=22] [OWNER=myuser] [REPO=infinito-nexus]
runner-ci-deploy:
@: "$${HOST:?HOST must be set (e.g. make runner-ci-deploy HOST=runner.example.com DISTRO=debian)}"
@: "$${DISTRO:?DISTRO must be set (e.g. debian, archlinux)}"
@"$${PYTHON}" -m cli.deploy.runner "$${HOST}" \
@"$${PYTHON}" -m cli.administration.deploy.runner "$${HOST}" \
--roles svc-runner \
--distribution "$${DISTRO}" \
--runner-count "$${COUNT:-15}" \
@@ -507,17 +711,115 @@ setup: fix-dockerignore dotenv
setup-clean: clean setup
@echo "Full build with cleanup before was executed."
.PHONY: swarm-app-exec
# Run a one-off command inside a swarm SERVICE container (one replica) on a DinD node.
# Param service: swarm service name (e.g. moodle_moodle | openldap_openldap).
# Param name: cluster id (the app id) used to default the node.
# Param node: full DinD node container (default <name>-swarm-mgr-01).
# Param cmd: shell pipeline executed inside the resolved service container.
swarm-app-exec:
@test -n '$(service)' || { echo 'usage: make swarm-app-exec service=<svc> [name=<cluster>] [node=<container>] cmd="..."'; exit 2; }
@node='$(or $(node),$(name)-swarm-mgr-01)' service='$(service)' cmd='$(cmd)' bash scripts/tests/deploy/act/exec/service.sh
.PHONY: swarm-clean
# Reclaim ALL leftover act-swarm state (DinD nodes, NFS sidecars, lab networks, act outer containers) from aborted/wedged roundtrip runs, across every cluster id.
# Note: removes what the docker CLI can; D-state remnants (wedged kernel NFS) need a host docker restart under sudo, or are reported in a no-priv sandbox.
# Note: run BETWEEN swarm runs; it would kill an in-flight one.
swarm-clean:
@bash scripts/tests/deploy/swarm/utils/clean/all.sh
.PHONY: swarm-clean-stale-nfs
# Recover stale in-namespace NFS mounts from wedged act-swarm nfs-server containers.
# Usage: make swarm-clean-stale-nfs [cid=<container-id-or-name>] [mount=/mnt/gtest]
# Note: needs sudo; may restart containerd/docker only if docker rm still cannot reap the container.
swarm-clean-stale-nfs:
@CID='$(cid)' NFS_MOUNT='$(mount)' bash scripts/tests/deploy/swarm/utils/clean/stale_nfs.sh
.PHONY: swarm-diagnostic
# Backup/NFS diagnostics for a live swarm-test cluster: backup unit state + journal, NFS mounts, D-state (wedged NFS) processes, rsync/dump processes, disk. Read-only.
# Param name: REQUIRED cluster id (the app id when no name= was passed to swarm-zombie).
# Param node: optional single node container name; default probes mgr-01, nfs-server, bkp-01.
# Param unit: optional systemd unit glob for the journal dump; default svc-bkp-*.
swarm-diagnostic:
@test -n '$(name)' || { echo 'usage: make swarm-diagnostic name=<cluster-id> [node=<container>] [unit=<glob>]'; exit 2; }
@SWARM_NAME='$(name)' node='$(node)' unit='$(unit)' bash scripts/tests/deploy/swarm/utils/diagnostic.sh
.PHONY: swarm-down
# Release a named swarm-test cluster (DinD nodes, lab network, act outer container).
# Param name: REQUIRED cluster id matching the one swarm-zombie used (the app id when no name= was passed).
# Note: Safe to run multiple times.
swarm-down:
@test -n '$(name)' || { echo 'usage: make swarm-down name=<cluster-id> (the app id if you did not pass name=)'; exit 2; }
@SWARM_NAME='$(name)' INFINITO_KEEP_SWARM_NODES=false bash scripts/tests/deploy/swarm/utils/clean/teardown.sh
@bash scripts/tests/deploy/act/down_act_outer.sh
.PHONY: swarm-exec
# Run a one-off command inside one of the swarm-test DinD nodes.
# Param node: full container name (e.g. <cluster>-swarm-mgr-01 | <cluster>-nfs-server; cluster = name= or the app id).
# Param cmd: shell pipeline executed inside that container.
swarm-exec:
@node='$(node)' cmd='$(cmd)' bash scripts/tests/deploy/act/exec/node.sh
.PHONY: swarm-playwright
# Rerun a role-local Playwright spec against the live swarm-test cluster (no redeploy).
# Note: nodes hold a frozen bootstrap copy (not a compose-style mount), so the working-tree's modified+untracked files are copied into the node before rerunning via the same rerun-spec.sh engine as compose-playwright; solve ALL of a role's tests (no pw= narrowing) before any redeploy.
# Usage: make swarm-playwright role=<role> name=<cluster-id> [pw="--grep <pattern>"] [keep=true] [node=<container>]
# Example: make swarm-playwright role=web-svc-logout name=web-app-baserow pw="--grep baserow" keep=true
swarm-playwright:
@: $${role:?role=<role> required, e.g. role=web-svc-logout}
@test -n '$(name)' || { echo 'name=<cluster-id> required (the app id when no name= was passed to swarm-zombie)'; exit 2; }
@node='$(or $(node),$(name)-swarm-mgr-01)' bash scripts/tests/deploy/act/copy_worktree_to_node.sh
@node='$(or $(node),$(name)-swarm-mgr-01)' cmd="cd $${INFINITO_NODE_SRC_DIR:?} && TEST_E2E_PLAYWRIGHT_NETWORK_HOST=true $(if $(keep),INFINITO_PLAYWRIGHT_KEEP=$(keep) )bash scripts/tests/e2e/rerun-spec.sh $(role) $(pw)" bash scripts/tests/deploy/act/exec/node.sh
.PHONY: swarm-shell
# Drop into an interactive shell on one of the swarm-test DinD nodes.
# Param name: REQUIRED cluster id (the app id when no name= was passed); node defaults to <name>-swarm-mgr-01.
# Param node: full container name to target (overrides the default).
swarm-shell:
@test -n '$(name)' || { echo 'usage: make swarm-shell name=<cluster-id> [node=<container>]'; exit 2; }
@SWARM_NAME='$(name)' node='$(node)' bash scripts/tests/deploy/act/shell_node.sh
SWARM_DISTROS = $(or $(distros),$${INFINITO_DISTRO:?})
.PHONY: swarm-zombie
# Run a swarm matrix-app test and leave the cluster alive afterwards for post-mortem inspection.
# Param app: matrix application id (e.g. web-app-baserow).
# Param distros: optional single distro the cluster runs on (default: INFINITO_DISTRO from .env).
# Param variant: optional matrix variant index to deploy (default 0); a multi-variant app runs one cluster per swarm-zombie, so pick the round to validate.
# Param disable: optional comma-separated provider keys removed from the test inventory (e.g. matomo,dashboard,prometheus,email,css).
# Param name: optional cluster-id prefix for the container + network names; release with the same name=.
# Param step_timeout: optional minute budget for the matrix-deploy step (default 690).
# Note: Use `make swarm-exec` / `make swarm-shell` to inspect, `make swarm-down` to release.
swarm-zombie: install-act
@test -n '$(app)' || { echo 'usage: make swarm-zombie app=<application_id> [distros=<distro>] [variant=<idx>] [name=<cluster-id>] [disable=<keys>]'; exit 2; }
@"$${PYTHON}" -m cli.meta.ci.validate --modes swarm --whitelist '$(app)#$(or $(variant),0)@swarm'
@SWARM_NAME='$(or $(name),$(app))' INFINITO_KEEP_SWARM_NODES=false bash scripts/tests/deploy/swarm/utils/clean/teardown.sh
@bash scripts/tests/deploy/swarm/utils/clean/lab_subnet.sh
@bash scripts/tests/deploy/act/down_act_outer.sh
@ACT_RM=false \
ACT_BIND=true \
ACT_ENV="INFINITO_KEEP_SWARM_NODES=true; \
INFINITO_APP_DISCOVERY_RUNNER=host; \
INFINITO_DEPLOY_MODE=swarm; \
disable=$(disable); \
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.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 modes=swarm disable=$(disable)" \
bash scripts/tests/deploy/act/workflow.sh
.PHONY: system-purge
# Run the broad low-hardware cleanup routine.
system-purge:
@bash scripts/system/purge/system.sh
.PHONY: test
# Run the full test pipeline (lint + tests).
# Note: parallel execution with fail-fast.
test: install install-lint
# Run the full test pipeline.
test: install
@bash scripts/make/parallel.sh \
lint \
test-external \
test-integration \
test-lint \
@@ -528,21 +830,38 @@ test: install install-lint
test-external: install
@INFINITO_TEST_TYPE="external" \
INFINITO_COMPILE=0 \
bash scripts/tests/code/wrapper.sh
bash scripts/tests/code/wrapper.sh scripts/tests/code/run.sh
.PHONY: test-integration
# Run the integration test suite.
test-integration: install
@INFINITO_TEST_TYPE="integration" \
INFINITO_COMPILE=0 \
bash scripts/tests/code/wrapper.sh
bash scripts/tests/code/wrapper.sh scripts/tests/code/run.sh
.PHONY: test-lint
# Run the lint test suite.
test-lint: install
@INFINITO_TEST_TYPE="lint" \
INFINITO_COMPILE=0 \
bash scripts/tests/code/wrapper.sh
bash scripts/tests/code/wrapper.sh scripts/tests/code/run.sh
.PHONY: test-main-merged
# Verify upstream main is fully merged into HEAD (pre-push gate); fetches and fails if the branch lags main.
test-main-merged:
@bash scripts/git/assert/main_merged.sh
.PHONY: test-merge-signed
# Verify every commit an in-progress merge brings in (HEAD..MERGE_HEAD) is signed (commit-msg gate).
test-merge-signed:
@bash scripts/git/assert/merge_signed.sh
.PHONY: test-performance
# Run the runtime-performance suite (not part of the `test` fan-out).
test-performance: install
@INFINITO_TEST_TYPE="performance" \
INFINITO_COMPILE=0 \
bash scripts/tests/code/wrapper.sh scripts/tests/code/run.sh
.PHONY: test-signed
# Verify HEAD is signed.
@@ -556,16 +875,59 @@ test-signed:
echo "✅ HEAD commit signature status: $$status"
.PHONY: test-unit
# Run the unit test suite.
test-unit: install
# Run the unit test suite (every language).
test-unit: test-unit-python test-unit-javascript test-unit-php test-unit-ruby
.PHONY: test-unit-javascript
# Run the JavaScript unit test suite (node:test, no npm dependency).
test-unit-javascript:
@INFINITO_TEST_TYPE="unit-javascript" \
bash scripts/tests/code/wrapper.sh scripts/tests/unit/javascript.sh
.PHONY: test-unit-php
# Run the PHP unit test suite (PHPUnit).
test-unit-php:
@INFINITO_TEST_TYPE="unit-php" \
bash scripts/tests/code/wrapper.sh scripts/tests/unit/php.sh
.PHONY: test-unit-python
# Run the Python unit test suite.
test-unit-python: install
@INFINITO_TEST_TYPE="unit" \
INFINITO_COMPILE=0 \
bash scripts/tests/code/wrapper.sh
bash scripts/tests/code/wrapper.sh scripts/tests/code/run.sh
.PHONY: update-skills
# Update all agent skills to latest versions and refresh skills-lock.json.
update-skills:
@bash scripts/install/skills/update.sh
.PHONY: test-unit-ruby
# Run the Ruby unit test suite (minitest, stdlib).
test-unit-ruby:
@INFINITO_TEST_TYPE="unit-ruby" \
bash scripts/tests/code/wrapper.sh scripts/tests/unit/ruby.sh
.PHONY: worktree-down
# Stop a branch worktree's stack, release the checkout and free its slot.
# Usage: make worktree-down branch=<name> [base=<dir>] [force=true]
# Param branch: branch the worktree was created for (required).
# Param base: parent directory the worktree lives in (default ~/.local/share/worktrees/<domain>/<account>/<repo>).
# Param force: true discards uncommitted changes in the worktree.
worktree-down:
@test -n '$(branch)' || { echo 'usage: make worktree-down branch=<name> [base=<dir>] [force=true]'; exit 2; }
@bash scripts/system/worktree/down.sh '$(branch)' '$(base)' '$(or $(force),false)'
.PHONY: worktree-prune
# Drop registrations of worktrees whose directory is gone, freeing the branches they claim.
# Usage: make worktree-prune
worktree-prune:
@bash scripts/system/worktree/prune.sh
.PHONY: worktree-up
# Check a branch out into an isolated worktree with its own subnet, ports and container names.
# Usage: make worktree-up branch=<name> [base=<dir>]
# Note: the worktree shares the primary checkout's cache stack instead of starting its own.
# Param branch: branch to check out (required).
# Param base: parent directory for the worktree (default ~/.local/share/worktrees/<domain>/<account>/<repo>).
worktree-up:
@test -n '$(branch)' || { echo 'usage: make worktree-up branch=<name> [base=<dir>]'; exit 2; }
@bash scripts/system/worktree/up.sh '$(branch)' '$(base)'
.PHONY: wsl2-dns-setup
# Set up DNS on WSL2.
+135 -3
View File
@@ -50,11 +50,143 @@
---
## Get Started 🚀
## Use it online 🚀
### Use it online 🌐
Try [Infinito.Nexus](https://infinito.nexus). Sign up in seconds, explore the platform, and discover what our solution can do for you!✨
Try [Infinito.Nexus](https://infinito.nexus) – sign up in seconds, explore the platform, and discover what our solution can do for you! 🚀🔧✨
## Roles Overview 🧩
Every invokable role in the tested lifecycle envelope, with its upstream homepage, an introduction video, the roles it integrates with, and a one-command local install.
| Name | Status | Description | More | Integrated with |
|---|---|---|---|---|
| [Akaunting](roles/web-app-akaunting/) | beta | Self-hosted platform for online accounting, bookkeeping, and business finance management | [🌐](https://akaunting.com/) [🎬](https://www.youtube.com/watch?v=UTVe_Zob_k0) [🛠️](roles/web-app-akaunting/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Asset Mirror (Privacy Proxy)](roles/web-svc-mirror/) | alpha | Nginx-based privacy proxy that mirrors whitelisted third-party assets under a first-party domain | [🛠️](roles/web-svc-mirror/README.md#quick-setup) | [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Assets Server](roles/web-svc-asset/) | beta | Nginx-based server for hosting static assets copied from a source directory to the web data path | [🛠️](roles/web-svc-asset/README.md#quick-setup) | [File Server](roles/web-svc-file/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Backup Docker Volumes](roles/svc-bkp-volume-2-local/) | beta | Scheduled deduplicating backup system for Docker container volumes to a local directory, with consistent SQL dumps | [🛠️](roles/svc-bkp-volume-2-local/README.md#quick-setup) | [Backup Host Secrets](roles/svc-bkp-secrets-2-local/) |
| [Backup Host Secrets](roles/svc-bkp-secrets-2-local/) | beta | Scheduled deduplicating backup of host secrets, CA, ACME credentials, and node identity to the local backup directory | [🛠️](roles/svc-bkp-secrets-2-local/README.md#quick-setup) | |
| [Backup NFS](roles/svc-bkp-nfs-2-local/) | beta | Scheduled backup job for hard-linked differential rsync snapshots of an NFS export into the local backup tree | [🛠️](roles/svc-bkp-nfs-2-local/README.md#quick-setup) | [NFS Server](roles/svc-storage-nfs-server/) |
| [Backup Remote Server](roles/svc-bkp-remote-2-local/) | beta | Scheduled pull-style backup replicating remote hosts onto this host over SSH and rsync as hard-linked snapshots | [🛠️](roles/svc-bkp-remote-2-local/README.md#quick-setup) | [Backup to External Device](roles/svc-bkp-local-2-device/) |
| [Backup to External Device](roles/svc-bkp-local-2-device/) | beta | Plug-triggered systemd backup for hard-link deduplicated snapshots of the local backup tree onto a swappable USB device | [🛠️](roles/svc-bkp-local-2-device/README.md#quick-setup) | |
| [Baserow](roles/web-app-baserow/) | beta | Self-hosted no-code database platform with a spreadsheet-like interface for building and collaborating on data | [🌐](https://baserow.io/) [🎬](https://www.youtube.com/watch?v=5yFMfP96_4M) [🛠️](roles/web-app-baserow/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [BigBlueButton](roles/web-app-bigbluebutton/) | beta | Self-hosted web conferencing platform for virtual classrooms, online meetings, and real-time collaboration | [🌐](https://bigbluebutton.org/) [🎬](https://www.youtube.com/watch?v=YRkQB6MJllI) [🛠️](roles/web-app-bigbluebutton/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Collabora](roles/web-svc-collabora/), [Corporate Design](roles/web-svc-css/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Universal Logout](roles/web-svc-logout/) |
| [Bluesky](roles/web-app-bluesky/) | beta | Self-hosted decentralized social network on the open AT Protocol, with its own Personal Data Server | [🌐](https://bsky.social/) [🛠️](roles/web-app-bluesky/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [BookWyrm](roles/web-app-bookwyrm/) | beta | Self-hosted federated reading platform for book reviews and reading tracking across the Fediverse | [🌐](https://joinbookwyrm.com/) [🛠️](roles/web-app-bookwyrm/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Bridgy Fed](roles/web-app-bridgy-fed/) | beta | Federation bridge connecting ActivityPub (Fediverse), the AT Protocol (Bluesky), and the IndieWeb | [🌐](https://fed.brid.gy/) [🛠️](roles/web-app-bridgy-fed/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Checkmk](roles/web-app-checkmk/) | beta | Open-source IT and application monitoring server behind SSO with native LDAP user management | [🌐](https://checkmk.com/) [🎬](https://www.youtube.com/watch?v=opO-SOgOJ1I) [🛠️](roles/web-app-checkmk/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Chess](roles/web-app-chess/) | beta | Federated chess server for playing and following rule-validated games over ActivityPub across the Fediverse | [🌐](https://castling.club/) [🛠️](roles/web-app-chess/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Collabora](roles/web-svc-collabora/) | beta | Self-hosted online office backend for real-time in-browser editing of documents, spreadsheets, and presentations | [🌐](https://www.collaboraonline.com/) [🎬](https://www.youtube.com/watch?v=8pqkclWG2BY) [🛠️](roles/web-svc-collabora/README.md#quick-setup) | [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Content Delivery Network](roles/web-svc-cdn/) | beta | Nginx-based content delivery network for serving shared static assets to internal web applications | [🛠️](roles/web-svc-cdn/README.md#quick-setup) | [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Corporate Design](roles/web-svc-css/) | beta | Service-registry role owning the canonical `css` flag and redirecting the CSS hostname to the CDN upstream | [🛠️](roles/web-svc-css/README.md#quick-setup) | [Content Delivery Network](roles/web-svc-cdn/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Coturn](roles/web-svc-coturn/) | beta | Free and open-source TURN/STUN relay server for NAT traversal in WebRTC and real-time communication | [🌐](https://github.com/coturn/coturn) [🛠️](roles/web-svc-coturn/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Prometheus](roles/web-app-prometheus/) |
| [Dashboard](roles/web-app-dashboard/) | beta | Self-hosted portal for unified access to all deployed applications via a customizable tile and menu interface | [🛠️](roles/web-app-dashboard/README.md#quick-setup) | [Assets Server](roles/web-svc-asset/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Simple Icons](roles/web-svc-simpleicons/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Decidim](roles/web-app-decidim/) | beta | Open-source participatory democracy platform for public consultations, civic processes, and community voting | [🌐](https://decidim.org/) [🎬](https://www.youtube.com/watch?v=luSdPZ-wUIY) [🛠️](roles/web-app-decidim/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Discourse](roles/web-app-discourse/) | beta | Open-source discussion and community forum platform with moderation, notifications, and plugin support | [🌐](https://www.discourse.org/) [🎬](https://www.youtube.com/watch?v=z3qp_uB46FA) [🛠️](roles/web-app-discourse/README.md#quick-setup) | [Assets Server](roles/web-svc-asset/), [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Docker Registry](roles/svc-registry-docker/) | beta | Cluster-local container image registry for serving custom-built images to Docker Swarm worker nodes | [🌐](https://distribution.github.io/distribution/) [🛠️](roles/svc-registry-docker/README.md#quick-setup) | [Docker Swarm](roles/svc-swarm-node/) |
| [Docker Registry Cache](roles/svc-registry-cache/) | beta | Cluster-local pull-through cache for shared Docker Hub base-image pulls, cutting duplicate transfers and rate limits | [🛠️](roles/svc-registry-cache/README.md#quick-setup) | [Docker Swarm](roles/svc-swarm-node/) |
| [Docker Swarm](roles/svc-swarm-node/) | beta | Container orchestration cluster for scheduling services across pooled manager and worker nodes | [🛠️](roles/svc-swarm-node/README.md#quick-setup) | |
| [Docker Swarm Manager](roles/svc-swarm-manager/) | beta | Marker role tagging a host as the Docker Swarm manager node, with the init logic delegated to svc-swarm-node | [🛠️](roles/svc-swarm-manager/README.md#quick-setup) | [Docker Swarm](roles/svc-swarm-node/) |
| [Elasticsearch](roles/svc-db-elasticsearch/) | beta | Central search and analytics engine with per-consumer index namespaces isolating tenants in a shared Docker deployment | [🌐](https://www.elastic.co/elasticsearch) [🛠️](roles/svc-db-elasticsearch/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [ERPNext](roles/web-app-erpnext/) | beta | Self-hosted open-source ERP suite for finance, CRM, inventory, manufacturing, HR, sales, and projects | [🌐](https://erpnext.com/) [🎬](https://www.youtube.com/watch?v=JlqE-BfDEUY) [🛠️](roles/web-app-erpnext/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [EspoCRM](roles/web-app-espocrm/) | beta | Open-source CRM platform for managing sales, customer interactions, and business processes across teams | [🌐](https://www.espocrm.com/) [🎬](https://www.youtube.com/watch?v=TH6yJPTPLO0) [🛠️](roles/web-app-espocrm/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Fediwall](roles/web-app-fediwall/) | beta | Self-hosted Fediverse wall for showing recent public posts by hashtag or account in a live masonry grid | [🌐](https://github.com/defnull/fediwall) [🛠️](roles/web-app-fediwall/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Friendica](roles/web-app-friendica/), [Mastodon](roles/web-app-mastodon/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Fider](roles/web-app-fider/) | beta | Open-source community feedback platform for collecting, voting on, and prioritizing feature requests and ideas | [🌐](https://www.fider.io/) [🛠️](roles/web-app-fider/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [File Server](roles/web-svc-file/) | beta | Static file server over HTTPS with directory listing, .well-known handling, and automatic TLS certificates | [🛠️](roles/web-svc-file/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Flowise](roles/web-app-flowise/) | beta | Low-code visual builder for creating, testing, and publishing LLM and RAG workflows as HTTP endpoints | [🌐](https://flowiseai.com/) [🎬](https://www.youtube.com/watch?v=h_0TJTY6Ulg) [🛠️](roles/web-app-flowise/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Ollama](roles/svc-ai-ollama/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Qdrant](roles/svc-db-qdrant/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Friendica](roles/web-app-friendica/) | beta | Decentralized, federated social-networking platform for peer-to-peer communication and community building | [🌐](https://friendi.ca/) [🛠️](roles/web-app-friendica/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Funkwhale](roles/web-app-funkwhale/) | beta | Self-hosted, federated platform for sharing, hosting, and streaming your own music and audio library | [🌐](https://www.funkwhale.audio/) [🛠️](roles/web-app-funkwhale/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Typesense](roles/svc-db-typesense/), [Universal Logout](roles/web-svc-logout/) |
| [FusionDirectory](roles/web-app-fusiondirectory/) | beta | Web-based LDAP administration interface for managing users, groups, and directory objects | [🌐](https://www.fusiondirectory.org/) [🎬](https://www.youtube.com/watch?v=dThpssnXGyk) [🛠️](roles/web-app-fusiondirectory/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Gitea](roles/web-app-gitea/) | beta | Lightweight self-hosted Git service for code collaboration, version control, and repository management | [🌐](https://about.gitea.com/) [🛠️](roles/web-app-gitea/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [GitLab](roles/web-app-gitlab/) | beta | Self-hosted DevOps platform for Git source-code management, CI/CD pipelines, and issue tracking | [🌐](https://about.gitlab.com/) [🎬](https://www.youtube.com/watch?v=7q9Y1Cv-ib0) [🛠️](roles/web-app-gitlab/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Hermes Agent](roles/web-app-hermes/) | beta | Kernel-isolated agent runtime serving an OpenAI-compatible API, routed to models through the shared LLM gateway | [🌐](https://hermes-agent.nousresearch.com/) [🛠️](roles/web-app-hermes/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Kata & gVisor](roles/svc-virt-kata/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Home Assistant](roles/web-app-homeassistant/) | beta | Smart-home automation hub with device integrations, dashboards and a built-in MCP server for AI agent access | [🌐](https://www.home-assistant.io/) [🛠️](roles/web-app-homeassistant/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Dashboard](roles/web-app-dashboard/), [Flowise](roles/web-app-flowise/), [Hermes Agent](roles/web-app-hermes/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [HTML Server](roles/web-svc-html/) | beta | NGINX vhost for serving static HTML websites over HTTPS with automatic Let's Encrypt certificates | [🛠️](roles/web-svc-html/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Hugo](roles/web-app-hugo/) | beta | Static site generator building HTML from an upstream Git repository at build time and serving it via nginx | [🌐](https://gohugo.io/) [🛠️](roles/web-app-hugo/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Imprint](roles/web-svc-legal/) | beta | NGINX-served static page rendering a legal imprint (Impressum) from a Jinja2 template | [🛠️](roles/web-svc-legal/README.md#quick-setup) | [HTML Server](roles/web-svc-html/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Intel Driver](roles/drv-intel/) | alpha | Intel VA-API media driver for hardware-accelerated video decode and encode across Linux distributions | [🌐](https://github.com/intel/media-driver) [🛠️](roles/drv-intel/README.md#quick-setup) | |
| [Jellyfin](roles/web-app-jellyfin/) | beta | Self-hosted media server for streaming and organizing movies, TV, music, and photos, with OIDC and LDAP login | [🌐](https://jellyfin.org/) [🎬](https://www.youtube.com/watch?v=0t3lSJL3B_Q) [🛠️](roles/web-app-jellyfin/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Jenkins](roles/web-app-jenkins/) | beta | Open-source automation server for CI/CD pipelines, builds, and plugin-driven deployment workflows | [🌐](https://www.jenkins.io/) [🎬](https://www.youtube.com/watch?v=rNy00_gOo8o) [🛠️](roles/web-app-jenkins/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Jitsi Meet](roles/web-app-jitsi/) | beta | Self-hosted WebRTC platform for end-to-end-encryptable video conferencing with federated authentication | [🌐](https://jitsi.org/) [🎬](https://www.youtube.com/watch?v=TB7LlM4erx8) [🛠️](roles/web-app-jitsi/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Universal Logout](roles/web-svc-logout/) |
| [Joomla](roles/web-app-joomla/) | beta | Open-source content management system for building websites, intranets, and online applications without writing code | [🌐](https://www.joomla.org/) [🎬](https://www.youtube.com/watch?v=mXFSIC_zM2o) [🛠️](roles/web-app-joomla/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Kata & gVisor](roles/svc-virt-kata/) | beta | Kernel-isolating container runtimes that sandbox untrusted workloads in a microVM or a userspace kernel | [🌐](https://katacontainers.io/) [🛠️](roles/svc-virt-kata/README.md#quick-setup) | |
| [Keycloak](roles/web-app-keycloak/) | beta | Self-hosted identity and access management platform for single sign-on, multi-factor authentication, and user federation | [🌐](https://www.keycloak.org/) [🛠️](roles/web-app-keycloak/README.md#quick-setup) | [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [KIX](roles/web-app-kix/) | beta | Open-source ITSM and helpdesk platform (OTRS fork) for ticketing, CMDB, knowledge base, and reporting | [🌐](https://www.kixdesk.com/en/) [🎬](https://www.youtube.com/watch?v=D62emos1FQ8) [🛠️](roles/web-app-kix/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [LAM](roles/web-app-lam/) | beta | Web interface for administering LDAP directories: managing users, groups, and other directory objects | [🌐](https://www.ldap-account-manager.org/) [🛠️](roles/web-app-lam/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [LibreTranslate](roles/web-svc-libretranslate/) | beta | Self-hosted open-source machine translation API for translating and detecting text across languages | [🌐](https://libretranslate.com/) [🛠️](roles/web-svc-libretranslate/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [LID Switch Driver](roles/drv-lid-switch/) | alpha | Power-aware systemd logind configuration for predictable laptop lid-close behavior across battery, AC, and docked states | [🛠️](roles/drv-lid-switch/README.md#quick-setup) | |
| [Listmonk](roles/web-app-listmonk/) | beta | Self-hosted platform for newsletters and mailing lists with subscriber segmentation and campaign analytics | [🌐](https://listmonk.app/) [🛠️](roles/web-app-listmonk/README.md#quick-setup) | [Assets Server](roles/web-svc-asset/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [LiteLLM Admin UI](roles/web-app-litellm/) | beta | Browser-facing admin interface for the shared LLM gateway, published on its own domain behind the front proxy | [🌐](https://docs.litellm.ai/) [🛠️](roles/web-app-litellm/README.md#quick-setup) | [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [LiteLLM Gateway](roles/svc-ai-litellm/) | beta | OpenAI-compatible LLM gateway routing a single /v1 endpoint to local and hosted model backends with per-consumer keys | [🌐](https://docs.litellm.ai/) [🛠️](roles/svc-ai-litellm/README.md#quick-setup) | [LM Studio](roles/svc-ai-lmstudio/), [Ollama](roles/svc-ai-ollama/), [PostgreSQL](roles/svc-db-postgres/) |
| [LittleJS](roles/web-app-littlejs/) | alpha | Self-hosted HTML5 game sandbox for browsing and running lightweight engine demos and games in the browser | [🌐](https://killedbyapixel.github.io/LittleJS/) [🎬](https://youtu.be/chuBzGjv7Ms) [🛠️](roles/web-app-littlejs/README.md#quick-setup) | [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [LM Studio](roles/svc-ai-lmstudio/) | beta | Headless local model server for open LLMs with an OpenAI-compatible API on the container network | [🌐](https://lmstudio.ai/) [🛠️](roles/svc-ai-lmstudio/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/) |
| [Magento](roles/web-app-magento/) | alpha | Self-hosted e-commerce platform for catalog management, checkout, promotions, and multi-store online retail | [🌐](https://business.adobe.com/products/commerce/magento/open-source.html) [🎬](https://www.youtube.com/watch?v=nFdz9DZrdFc) [🛠️](roles/web-app-magento/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Mailu](roles/web-app-mailu/) | beta | Self-hosted mail server suite with SMTP/IMAP/POP3, webmail, spam filtering, antivirus, DKIM signing, and TLS | [🌐](https://mailu.io/) [🛠️](roles/web-app-mailu/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [MariaDB](roles/svc-db-mariadb/) | beta | Shared relational database server for applications needing central SQL storage, run in Docker | [🌐](https://mariadb.org/) [🎬](https://www.youtube.com/watch?v=YsIJB15fq_c) [🛠️](roles/svc-db-mariadb/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [Mastodon](roles/web-app-mastodon/) | beta | Self-hosted federated social network for microblogging, real-time timelines, and community moderation | [🌐](https://joinmastodon.org/) [🛠️](roles/web-app-mastodon/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Matomo](roles/web-app-matomo/) | beta | Self-hosted, privacy-first web analytics platform for real-time visitor tracking and site metrics | [🌐](https://matomo.org/) [🎬](https://www.youtube.com/watch?v=Qc2kooLNDiU) [🛠️](roles/web-app-matomo/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [MariaDB](roles/svc-db-mariadb/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Matrix](roles/web-app-matrix/) | beta | Federated, end-to-end-encrypted messaging platform with a web client and protocol bridges to external chat networks | [🌐](https://matrix.org/) [🎬](https://www.youtube.com/watch?v=3AVsNqH_-9M) [🛠️](roles/web-app-matrix/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Mattermost](roles/web-app-mattermost/) | beta | Open-source, self-hosted team messaging platform for real-time collaboration via channels, threads, and integrations | [🌐](https://mattermost.com/) [🎬](https://www.youtube.com/watch?v=szfZfQFUSnU) [🛠️](roles/web-app-mattermost/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [MCP Adapter](roles/svc-ai-mcp-adapter/) | alpha | Reusable per-provider MCP adapter exposing a narrow allowlisted read surface | [🌐](https://modelcontextprotocol.io/) [🛠️](roles/svc-ai-mcp-adapter/README.md#quick-setup) | |
| [MediaWiki](roles/web-app-mediawiki/) | beta | Collaborative wiki platform for user-driven documentation with an extensible extension ecosystem and content management | [🌐](https://www.mediawiki.org/) [🎬](https://www.youtube.com/watch?v=-JnIjpRvgNY) [🛠️](roles/web-app-mediawiki/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Memcached](roles/svc-db-memcached/) | beta | Central in-memory caching engine with per-consumer key-prefix isolation, usable centrally or as an embedded sidecar | [🌐](https://memcached.org/) [🛠️](roles/svc-db-memcached/README.md#quick-setup) | [Tor Onion Service](roles/svc-net-tor/) |
| [MIG](roles/web-app-mig/) | alpha | Interactive graph visualization of all Infinito.Nexus roles and their dependencies for architecture transparency | [🛠️](roles/web-app-mig/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Mini-QR](roles/web-app-mini-qr/) | beta | Self-hosted browser tool for generating QR codes from text or URLs with privacy-friendly local rendering | [🌐](https://github.com/lyqht/mini-qr) [🛠️](roles/web-app-mini-qr/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Mobilizon](roles/web-app-mobilizon/) | beta | Federated, self-hosted platform for organizing events and building communities and groups | [🌐](https://joinmobilizon.org/) [🎬](https://framatube.org/w/2mtugeJXaihsQnTHVWHf2E) [🛠️](roles/web-app-mobilizon/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Moodle](roles/web-app-moodle/) | beta | Open-source learning management system for creating, managing, and delivering online courses and e-learning | [🌐](https://moodle.org/) [🎬](https://www.youtube.com/watch?v=3ORsUGVNxGs) [🛠️](roles/web-app-moodle/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [n8n](roles/web-app-n8n/) | beta | Self-hosted low-code automation platform for connecting services and building workflow integrations visually | [🌐](https://n8n.io/) [🎬](https://youtu.be/4cQWJViybAQ) [🛠️](roles/web-app-n8n/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Nextcloud](roles/web-app-nextcloud/) | beta | Self-hosted cloud platform for file sync, sharing, real-time collaboration, and team workspaces under your own domain | [🌐](https://nextcloud.com/) [🎬](https://youtu.be/3jcYJGQgenI?si=FDmoMSrAb9_WvviC) [🛠️](roles/web-app-nextcloud/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [BigBlueButton](roles/web-app-bigbluebutton/), [Collabora](roles/web-svc-collabora/), [Corporate Design](roles/web-svc-css/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Discourse](roles/web-app-discourse/), [GitLab](roles/web-app-gitlab/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Mastodon](roles/web-app-mastodon/), [Matomo](roles/web-app-matomo/), [Matrix](roles/web-app-matrix/), [Mattermost](roles/web-app-mattermost/), [OnlyOffice](roles/web-svc-onlyoffice/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [OpenProject](roles/web-app-openproject/), [PeerTube](roles/web-app-peertube/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/), [XWiki](roles/web-app-xwiki/), [Zammad](roles/web-app-zammad/) |
| [NFS Client](roles/svc-storage-nfs-client/) | beta | Client-side NFS setup for mounting a remote server export on every swarm node, with a deploy-time reachability probe | [🛠️](roles/svc-storage-nfs-client/README.md#quick-setup) | [NFS Server](roles/svc-storage-nfs-server/) |
| [NFS Server](roles/svc-storage-nfs-server/) | beta | Kernel-level NFS server for exporting a shared directory tree with IP-restricted access for swarm-node hosts | [🛠️](roles/svc-storage-nfs-server/README.md#quick-setup) | [Backup NFS](roles/svc-bkp-nfs-2-local/) |
| [Odoo ERP](roles/web-app-odoo/) | beta | Open-source ERP suite for CRM, sales, accounting, inventory, HR, and website management | [🌐](https://www.odoo.com/) [🎬](https://www.youtube.com/watch?v=FCmFgHVzd3E) [🛠️](roles/web-app-odoo/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Ollama](roles/svc-ai-ollama/) | beta | Self-hosted model server for running open LLMs locally behind a simple HTTP API | [🌐](https://ollama.com/) [🛠️](roles/svc-ai-ollama/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/) |
| [OnlyOffice](roles/web-svc-onlyoffice/) | beta | Document server backend for real-time in-browser editing of documents, spreadsheets, and presentations | [🌐](https://www.onlyoffice.com/) [🎬](https://www.youtube.com/watch?v=uiFxnRK_aWM) [🛠️](roles/web-svc-onlyoffice/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/) |
| [Open WebUI](roles/web-app-openwebui/) | beta | Self-hosted chat interface for interacting with local and private AI models via backends such as Ollama | [🌐](https://openwebui.com/) [🛠️](roles/web-app-openwebui/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenBao](roles/web-app-openbao/) | alpha | Identity-based secrets, PKI and encryption manager with OIDC login, group-mapped policies and static-seal auto-unseal | [🌐](https://openbao.org/) [🎬](https://www.youtube.com/watch?v=hPBT-kSS-68) [🛠️](roles/web-app-openbao/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenClaw](roles/web-app-openclaw/) | beta | Self-hosted personal AI agent gateway with a browser control UI, sandboxed in a Kata microVM or gVisor runtime | [🌐](https://openclaw.ai/) [🛠️](roles/web-app-openclaw/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Kata & gVisor](roles/svc-virt-kata/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenCloud](roles/web-app-opencloud/) | beta | Self-hosted platform for file sync, sharing, and collaboration with Keycloak SSO and central LDAP directory | [🌐](https://opencloud.eu/) [🎬](https://www.youtube.com/watch?v=Cg48rFNY0R4) [🛠️](roles/web-app-opencloud/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenLDAP](roles/svc-db-openldap/) | beta | Self-hosted LDAP directory service for centralized user, group, and schema management with TLS-secured access | [🌐](https://www.openldap.org/) [🛠️](roles/svc-db-openldap/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [OpenProject](roles/web-app-openproject/) | beta | Self-hosted platform for project management, work-package tracking, Gantt planning, and team collaboration | [🌐](https://www.openproject.org/) [🎬](https://www.youtube.com/watch?v=rvCd0xH7rCs) [🛠️](roles/web-app-openproject/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [OpenResty](roles/svc-prx-openresty/) | beta | NGINX-based web platform and reverse proxy with validated config reloads and Let's Encrypt integration | [🌐](https://openresty.org/) [🎬](https://www.youtube.com/watch?v=NR8U1qYHGqk) [🛠️](roles/svc-prx-openresty/README.md#quick-setup) | [Tor Onion Service](roles/svc-net-tor/) |
| [OpenTalk](roles/web-app-opentalk/) | beta | Sovereign open-source video conferencing platform for audio/video calls, screen sharing, breakout rooms, and SSO | [🌐](https://opentalk.eu/) [🛠️](roles/web-app-opentalk/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Universal Logout](roles/web-svc-logout/) |
| [Package update](roles/update/) | beta | System-maintenance role for refreshing the package index and upgrading installed packages via the OS package manager | [🛠️](roles/update/README.md#quick-setup) | |
| [PeerTube](roles/web-app-peertube/) | beta | Decentralized, federated platform for peer-to-peer video hosting, streaming, and live broadcasting | [🌐](https://joinpeertube.org/) [🎬](https://framatube.org/w/cX4KAPLTwha6hzLZ5xePGZ) [🛠️](roles/web-app-peertube/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Universal Logout](roles/web-svc-logout/) |
| [Penpot](roles/web-app-penpot/) | beta | Self-hosted design and prototyping platform for UI/UX work, real-time collaboration, and developer handoff | [🌐](https://penpot.app/) [🎬](https://www.youtube.com/watch?v=vAkb4QQmbfk) [🛠️](roles/web-app-penpot/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [pgAdmin](roles/web-app-pgadmin/) | beta | Web-based administration and development platform for managing multiple PostgreSQL servers, with optional OAuth2 login | [🌐](https://www.pgadmin.org/) [🎬](https://www.youtube.com/watch?v=WFT5MaZN6g4) [🛠️](roles/web-app-pgadmin/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [PhpMyAdmin](roles/web-app-phpmyadmin/) | beta | Web-based administration tool for MySQL and MariaDB with schema management, querying, and import/export | [🌐](https://www.phpmyadmin.net/) [🛠️](roles/web-app-phpmyadmin/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Pixelfed](roles/web-app-pixelfed/) | beta | Decentralized, federated platform for photo sharing and image feeds across the fediverse | [🌐](https://pixelfed.org/) [🛠️](roles/web-app-pixelfed/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [PostgreSQL](roles/svc-db-postgres/) | beta | Containerized relational database with automated database, user, and privilege provisioning | [🌐](https://www.postgresql.org/) [🛠️](roles/svc-db-postgres/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [Postmarks](roles/web-app-postmarks/) | beta | Single-user ActivityPub bookmarking site federating public bookmarks to Mastodon and the Fediverse | [🌐](https://github.com/ckolderup/postmarks) [🛠️](roles/web-app-postmarks/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Pretix](roles/web-app-pretix/) | beta | Self-hosted ticketing platform for events with online sales, registration management, and secure payments | [🌐](https://pretix.eu/) [🎬](https://www.youtube.com/watch?v=5F6QkZP1Zf0) [🛠️](roles/web-app-pretix/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Prometheus](roles/web-app-prometheus/) | beta | Open-source monitoring and alerting toolkit for storing metrics as time-series data and querying them with PromQL | [🌐](https://prometheus.io/) [🎬](https://www.youtube.com/watch?v=PzFUwBflXYc) [🛠️](roles/web-app-prometheus/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Flowise](roles/web-app-flowise/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Proprietary Drivers](roles/drv-non-free/) | alpha | Proprietary GPU driver installer for Arch-based systems via mhwd nonfree hardware detection | [🛠️](roles/drv-non-free/README.md#quick-setup) | |
| [Qdrant](roles/svc-db-qdrant/) | beta | Central vector database engine for storing and searching per-consumer embeddings shared across the stack | [🌐](https://qdrant.tech/) [🎬](https://www.youtube.com/watch?v=_opNdOa48Lc) [🛠️](roles/svc-db-qdrant/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Flowise](roles/web-app-flowise/), [Hermes Agent](roles/web-app-hermes/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [Tor Onion Service](roles/svc-net-tor/) |
| [RabbitMQ](roles/svc-db-rabbitmq/) | beta | Shared message broker for async messaging with per-consumer virtual host and user isolation | [🌐](https://www.rabbitmq.com/) [🛠️](roles/svc-db-rabbitmq/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [Redirector](roles/web-opt-rdr-domains/) | beta | NGINX glue role for 301 redirects from source to target domains with automatic SSL for the source hosts | [🛠️](roles/web-opt-rdr-domains/README.md#quick-setup) | |
| [Redis](roles/svc-db-redis/) | beta | In-memory key-value cache and store, shared centrally with per-consumer ACL isolation or embedded as a sidecar | [🌐](https://redis.io/) [🛠️](roles/svc-db-redis/README.md#quick-setup) | [Tor Onion Service](roles/svc-net-tor/) |
| [Roulette Wheel](roles/web-app-roulette-wheel/) | alpha | Browser-based spinning wheel app for decision making, random selection, and gamification | [🌐](https://github.com/p-wojt/roulette-wheel) [🛠️](roles/web-app-roulette-wheel/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [SeaweedFS](roles/web-app-seaweedfs/) | beta | Distributed S3-compatible object store for shared, per-consumer bucket provisioning across web-app roles | [🌐](https://seaweedfs.org/) [🛠️](roles/web-app-seaweedfs/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Prometheus](roles/web-app-prometheus/), [Universal Logout](roles/web-svc-logout/) |
| [Self-Hosted Runner](roles/svc-runner/) | beta | Self-hosted CI runner infrastructure for executing GitHub Actions workflow jobs in ephemeral Docker containers | [🌐](https://github.com/actions/runner) [🛠️](roles/svc-runner/README.md#quick-setup) | |
| [Semaphore UI](roles/web-app-semaphore/) | beta | Web UI and API for running Ansible, Terraform/OpenTofu, and shell automation on a schedule or on demand | [🌐](https://semaphoreui.com/) [🎬](https://www.youtube.com/watch?v=nzR4yDuZQMU) [🛠️](roles/web-app-semaphore/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Shopware](roles/web-app-shopware/) | beta | Open-source ecommerce platform built on Symfony and Vue.js for building and scaling online storefronts | [🌐](https://www.shopware.com/en/) [🎬](https://www.youtube.com/watch?v=bG57TZPYsyw) [🛠️](roles/web-app-shopware/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Simple Icons](roles/web-svc-simpleicons/) | beta | Containerized icon server for hosting the Simple Icons brand SVG and PNG collection for web and documentation use | [🌐](https://simpleicons.org/) [🛠️](roles/web-svc-simpleicons/README.md#quick-setup) | [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Snipe‑IT](roles/web-app-snipe-it/) | beta | Open-source IT asset management system for tracking hardware and software inventory | [🌐](https://snipeitapp.com/) [🎬](https://www.youtube.com/watch?v=q0V6FAPc1ls) [🛠️](roles/web-app-snipe-it/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Social Home](roles/web-app-socialhome/) | alpha | Privacy-first social platform for households, federating peer-to-peer over WebRTC on an embedded SQLite store | [🌐](https://github.com/social-home-io/socialhome) [🛠️](roles/web-app-socialhome/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Coturn](roles/web-svc-coturn/), [Dashboard](roles/web-app-dashboard/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Sphinx](roles/web-app-sphinx/) | beta | Documentation generator that builds HTML from a source repository and serves it through a lightweight HTTP server | [🌐](https://www.sphinx-doc.org/) [🛠️](roles/web-app-sphinx/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [Storage Optimizer](roles/svc-opt-ssd-hdd/) | beta | Tiering role placing Docker volumes on SSD or HDD by workload type, with symlinks keeping paths consistent | [🛠️](roles/svc-opt-ssd-hdd/README.md#quick-setup) | |
| [SuiteCRM](roles/web-app-suitecrm/) | beta | Open-source CRM platform for sales, service, cases, and campaign management with workflow automation | [🌐](https://suitecrm.com/) [🎬](https://www.youtube.com/watch?v=pOAs_C3Fp_E) [🛠️](roles/web-app-suitecrm/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Swapfile](roles/svc-opt-swapfile/) | beta | On-target automation for creating and activating swap space via a cloned swapfile script | [🛠️](roles/svc-opt-swapfile/README.md#quick-setup) | |
| [Taiga](roles/web-app-taiga/) | beta | Open-source agile project management platform for Scrum, Kanban, and hybrid team workflows | [🌐](https://taiga.io/) [🎬](https://www.youtube.com/watch?v=TYPX8nQEU5I) [🛠️](roles/web-app-taiga/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [SeaweedFS](roles/web-app-seaweedfs/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Tor Onion Service](roles/svc-net-tor/) | alpha | Tor daemon publishing the node as a v3 onion service | [🛠️](roles/svc-net-tor/README.md#quick-setup) | [Asset Mirror (Privacy Proxy)](roles/web-svc-mirror/) |
| [Typesense](roles/svc-db-typesense/) | beta | Central Dockerized search engine shared by multiple apps with per-consumer scoped API keys | [🌐](https://typesense.org/) [🛠️](roles/svc-db-typesense/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Tor Onion Service](roles/svc-net-tor/) |
| [Unbound](roles/svc-dns-unbound/) | beta | Central recursive/forwarding DNSSEC resolver on a static IP for consumer stacks to target as their DNS | [🌐](https://nlnetlabs.nl/projects/unbound/about/) [🛠️](roles/svc-dns-unbound/README.md#quick-setup) | |
| [Universal Logout](roles/web-svc-logout/) | beta | Central logout proxy for single-action sign-out across all OIDC-connected apps, with Nginx snippets and a live status UI | [🛠️](roles/web-svc-logout/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [web-app-pihole](roles/web-app-pihole/) | beta | Network-wide DNS sinkhole for blocking ads and trackers across all devices, with DHCP and a web admin dashboard | [🌐](https://pi-hole.net/) [🎬](https://youtu.be/KBXTnrD_Zs4) [🛠️](roles/web-app-pihole/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Content Delivery Network](roles/web-svc-cdn/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [WordPress](roles/web-app-wordpress/) | beta | Content management system for building and managing websites, blogs, and multisite networks with themes and plugins | [🌐](https://wordpress.org/) [🎬](https://www.youtube.com/watch?v=QU8GFqvXRxE) [🛠️](roles/web-app-wordpress/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Discourse](roles/web-app-discourse/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [OpenClaw](roles/web-app-openclaw/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [World Wide Web](roles/web-opt-rdr-www/) | beta | NGINX server blocks that permanently redirect www subdomains to their non-www apex domains | [🛠️](roles/web-opt-rdr-www/README.md#quick-setup) | [Tor Onion Service](roles/svc-net-tor/) |
| [XMPP](roles/web-svc-xmpp/) | beta | Placeholder stub role for a future XMPP server/container deployment, implementation pending | [🌐](https://ejabberd.im/) [🛠️](roles/web-svc-xmpp/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Keycloak](roles/web-app-keycloak/), [OpenLDAP](roles/svc-db-openldap/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/) |
| [XWiki](roles/web-app-xwiki/) | beta | Self-hosted enterprise wiki and knowledge-management platform with collaboration and app-based extensibility | [🌐](https://www.xwiki.org/) [🎬](https://www.youtube.com/watch?v=8DePx30dh2g) [🛠️](roles/web-app-xwiki/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [YOURLS](roles/web-app-yourls/) | beta | Self-hosted URL shortener for creating, tracking, and managing short links with built-in click analytics | [🌐](https://yourls.org/) [🛠️](roles/web-app-yourls/README.md#quick-setup) | [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Keycloak](roles/web-app-keycloak/), [MariaDB](roles/svc-db-mariadb/), [Matomo](roles/web-app-matomo/), [Prometheus](roles/web-app-prometheus/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
| [Zammad](roles/web-app-zammad/) | beta | Open-source helpdesk and ticketing system for handling customer requests via email, chat, phone, and web | [🌐](https://zammad.org/) [🎬](https://www.youtube.com/watch?v=K30V0Uimr-s) [🛠️](roles/web-app-zammad/README.md#quick-setup) | [Backup Docker Volumes](roles/svc-bkp-volume-2-local/), [Corporate Design](roles/web-svc-css/), [Dashboard](roles/web-app-dashboard/), [Hermes Agent](roles/web-app-hermes/), [Keycloak](roles/web-app-keycloak/), [LiteLLM Gateway](roles/svc-ai-litellm/), [Mailu](roles/web-app-mailu/), [Matomo](roles/web-app-matomo/), [Memcached](roles/svc-db-memcached/), [Open WebUI](roles/web-app-openwebui/), [OpenClaw](roles/web-app-openclaw/), [OpenLDAP](roles/svc-db-openldap/), [PostgreSQL](roles/svc-db-postgres/), [Prometheus](roles/web-app-prometheus/), [Redis](roles/svc-db-redis/), [Tor Onion Service](roles/svc-net-tor/), [Universal Logout](roles/web-svc-logout/) |
## Guides 📚
+95
View File
@@ -12,3 +12,98 @@
- run over all distros for each app
- msg: can not use content with a dir as dest for NGINX multi distro
- split service database to postgres and mariadb
## Onion e2e (feature/svc-net-tor) — remaining failures
Status after deploy-matrix run 29657147883 (commit 407b8a8a1). Every fixable
failure class from that run is committed (hlth-csp onion-sibling skip,
onion-aware Playwright request timeouts + lint guard, joomla server-side OIDC
reachability). The items below are the ones that are NOT fixed yet. Runs
31756400129 and 31756560361 have since moved several of them; entries corrected
against those runs say so inline.
- **https on the onion IdP — investigated and REJECTED, do not re-derive.** OIDC
Discovery / RFC 8414 want an https issuer, and an onion-exclusive Keycloak has
none, so strict client libraries refuse it. Serving TLS on the onion is blocked
on four independent things, in this order: (1) `roles/svc-net-tor/templates/torrc.j2`
publishes only `HiddenServicePort 80`, and no role declares 443 in
`ports.public`/`ports.local`, so `lookup('tor_ports')` can never emit it —
flipping TLS on first makes Keycloak unreachable over Tor entirely, because
`nginx_ssl_header.j2` is one if/else and port 80 disappears; (2)
`roles/sys-front-tls-selfsigned/tasks/00_core.yml` writes an unguarded CN, which
overflows X.509's 64-byte cap on a 62-char v3 onion — `group_vars/all/02_tls.yml`
already guards exactly this for the root CA and nowhere else; (3)
`nginx_ssl_header.j2` emits HSTS in the same branch, and browsers offer no
click-through on an HSTS host with an untrusted chain, so a self-signed interim
locks real users out while staying invisible to Playwright's
`ignoreHTTPSErrors`; (4) it fixes zero libcurl consumers, because RFC 7686
refusal happens at name resolution and is scheme-independent. Unblocking needs a
CA that issues for v3 `.onion` (HARICA, CA/B Forum ballot 144) — procurement,
not code. Until then the per-consumer fixes stand: SOCKS proxy for libcurl
clients. peertube is no longer among them - it is a clearnet-only role.
- **dual-family providers** — the one real remaining design gap. On a CI onion
node, tor-enabled providers (e.g. mastodon) are onion-exclusive: their domain
list contains only the `.onion` canonical, no clearnet sibling. The
family-alignment resolver (`utils/tls_common.py:align_domain_to_consumer`,
wired into `plugins/lookup/tls.py`) correctly bails in that case, so a
clearnet-pinned consumer still receives an onion URL it can neither resolve
nor reach. First hard evidence: `web-app-fediwall` variant 2 (tor=false)
renders `microblog.<onion>` into `wall-config.json.servers`; the clearnet
browser (no SOCKS) times out waiting for mastodon wall items
(`test-walls-surface-posts.js:24`, deterministic across retries). The same
class is expected to keep `web-app-bigbluebutton`, `web-app-nextcloud`,
`web-app-opentalk` and `web-app-jitsi` red (clearnet-pinned apps whose peer
refs — OIDC/cdn/matomo/logout — resolve onion-primary). Fix direction:
providers must publish BOTH families (clearnet sibling alongside the onion
canonical) so the resolver has something to align to; includes re-applying
the deferred `KC_HOSTNAME` onion-gate for Keycloak that was dropped from the
earlier bundle because it is only needed once dual-family exists.
- **joomla local-login fallback spec** — `test-oidc-fallback.js:52` (the
`?fallback=local` emergency hatch: local Joomla admin form login must reach
the control panel). Root cause UNCONFIRMED. It is independent of the fixed
OIDC-login spec: the fallback path short-circuits before the plugin's
server-side discovery, so the committed `extra_hosts` fix does not cover it.
The `secure => true` sticky-cookie hypothesis was rejected (`.onion` is a
potentially-trustworthy origin and the e2e Firefox sets
`dom.securecontext.allowlist_onions`, so Secure cookies are accepted over
plain-http onion). Next step when it fails again post-fix: isolate the
post-submit URL — Keycloak redirect (cookie lost) vs. form reject vs. slow
control-panel render.
- **matomo container health flake (checkmk batch)** — in the checkmk job the
4th deploy play failed at `Wait for Matomo container to be healthy` with
`container inspect: map has no entry for key "State"` for the whole retry
window (container started, then vanished — likely crash + prune). Plays 1–3
in the SAME job deployed matomo healthy, so classified transient
(resource/prune pressure after ~3h). No code fix derivable. If it recurs,
investigate as a real matomo crash/OOM (container logs, mem_limit).
- **server-side OIDC reachability candidates** — joomla failed because its app
container performs server-side OIDC discovery against `http://auth.<onion>`
without an `extra_hosts` mapping. Roles with the same mechanic and no
`container_extra_hosts` lookup yet (fix is the same one-line lookup,
apply on CI evidence): wordpress (daggerhart plugin) and mediawiki
(PluggableAuth) are structurally identical PHP in-app plugins (high
likelihood); discourse is now confirmed and pinned (run 33445831564, job
Nextcloud#7); jenkins, jira, confluence, odoo, openwebui,
pretix, zammad, semaphore, jellyfin, listmonk, xwiki, mastodon consume the
issuer server-side too (unverified network path). oauth2-proxy-based apps
are NOT affected (proven green: prometheus admin SSO). Two names have left
this list on evidence: mobilizon got the lookup in 0e39f3fad after its
code-to-token call raised OAuth2.Error Connection refused twenty-four times,
and gitlab was NEVER a member - run 31756560361 shows it resolving and
connecting (peeraddr=172.30.0.10:443); its failure was the openid_connect gem
rebuilding the discovery URL over https regardless of the issuer scheme,
fixed in 2389b6e38.
- **baseline exclusions** — `web-app-fider` and `web-app-bookwyrm` (also
keycloak-job, n8n) were red before the tor branch and stay out of scope for
it; bookwyrm fails its oauth2 trusted-header session spec, fider its
baseline. Fix separately from the onion work. espocrm has left this list: its
onion variant failed in the shared admin persona (fixed in b97cf8888) and its
clearnet variant returns HTTP 500 on GET /api/v1/App/user, cause still open.
- **test-development environment jobs** — all 5 distro jobs
(arch, debian, ubuntu, fedora, centos) hit the 4h30m cap under congestion
while still progressing (playwright image pull). Environmental, no code fix -
distinct from the distro sweep's own budget, which did have one: 508669809
books a budget SIGKILL as skipped, which ad9222630 had only done for SIGTERM;
note that onion e2e inherently lengthens the routine (×5 onion timeouts,
circuit waits), so under congestion these jobs are the first to hit the cap.
+168
View File
@@ -0,0 +1,168 @@
alias i8='infinito' # the infinito CLI
alias i8bo='m bootstrap'
alias i8bond='m bond'
alias i8bu='m build'
alias i8buce='m build-cleanup'
alias i8bude='m build-dependency'
alias i8bumi='m build-missing'
alias i8buno='m build-no-cache'
alias i8bunoca='m build-no-cache-all'
alias i8ca='m act-debug'
alias i8caru='m act-runner-image'
alias i8cawo='m act-workflow'
alias i8ce='m cheat'
alias i8ce2='m clean'
alias i8ceca='m clean-cache'
alias i8ceco='m clean-container-owned'
alias i8cepa='m clean-pycache-dirs'
alias i8cesa='m clean-stale-nfs'
alias i8cesu='m clean-sudo'
alias i8ciall='__i8ciall(){ [ -z "${1:-}" ] && { echo "usage: i8ciall <run-id|url>" >&2; return 2; }; ghaf "$1" || echo "i8ciall: force-cancel of run $1 reported nothing to cancel; continuing" >&2; infinito administration deploy ci trigger --failed --strict --roles-only --run "$1"; }; __i8ciall' # retrigger run $1 (id or url): only the roles that truly failed take the priority line, by name, letting the rotation pick the axes, plus the priority entries that run never deployed, axes and all; the aborted, the still-running and the never-reached stay on the regular line, which resumes behind the green stretch the run added to the offset it was given; the workspace suite comes back unless every one of its jobs went green; force-cancels the run first so it stops holding runner slots
alias i8ciallon='__i8ciallon(){ [ -z "${1:-}" ] && { echo "usage: i8ciallon <run-id|url>" >&2; return 2; }; ghaf "$1" || echo "i8ciallon: force-cancel of run $1 reported nothing to cancel; continuing" >&2; infinito administration deploy ci trigger --failed --strict --roles-only --chunk-gate false --run "$1"; }; __i8ciallon' # like i8ciall, but the retrigger keeps deploying the remaining chunks after a failed one (chunk_gate=false) instead of stopping the chain; the run still ends red
alias i8cifail='__i8cifail(){ [ -z "${1:-}" ] && { echo "usage: i8cifail <run-id|url>" >&2; return 2; }; ghaf "$1" || echo "i8cifail: force-cancel of run $1 reported nothing to cancel; continuing" >&2; infinito administration deploy ci trigger --failed --run "$1"; }; __i8cifail' # retrigger the exact failed selections (role#variant@mode+tor) of run $1 (id or url) across every mode, plus its never-deployed priority entries, reproducing every other input of that run; force-cancels the run first so it stops holding runner slots
alias i8cifo='__i8cifo(){ [ -z "${1:-}" ] && { echo "usage: i8cifo <run-id|url>" >&2; return 2; }; ghaf "$1" || echo "i8cifo: force-cancel of run $1 reported nothing to cancel; continuing" >&2; infinito administration deploy ci trigger --failed --strict --run "$1"; }; __i8cifo' # like i8cifail but only hard-failed selections, excluding cancelled/aborted; force-cancels the run first
alias i8cire='__i8cire(){ make quality && gips && i8cifail "$1"; }; __i8cire'
alias i8co='m compose-deploy'
alias i8co2='m console'
alias i8coape='m compose-app-exec'
alias i8codo='m compose-down'
alias i8cone='m compose-entity-purge'
alias i8coni='m compose-inner-run'
alias i8conire='m compose-inventory-refresh'
alias i8copa='m compose-playwright'
alias i8copal='m compose-app-logs'
alias i8copare='m compose-app-restart'
alias i8copu='m compose-up'
alias i8core='m compose-restart'
alias i8cos='m cosmos'
alias i8cose='m compose-system-purge'
alias i8coso='m compose-stop'
alias i8coxe='m compose-exec'
alias i8di='m diagnose-disk-usage'
alias i8dine='m diagnose-network'
alias i8do='m dotenv'
alias i8docu='m docs'
alias i8dofo='m dotenv-force'
alias i8fi='m fix-chmod'
alias i8fido='m fix-dockerignore'
alias i8he='m help'
alias i8inma='m integration-matrix'
alias i8kelo='m kernel-loop-load'
alias i8la='m alias'
alias i8li='m lint'
alias i8lica='m lint-action'
alias i8lics='m lint-css'
alias i8lido='m lint-dockerfile'
alias i8lija='m lint-javascript'
alias i8lima='m lint-makefile'
alias i8lima2='m lint-markdown'
alias i8lime='m lint-mermaid'
alias i8lina='m lint-ansible'
alias i8lipa='m lint-packages'
alias i8lipa2='m lint-playwright'
alias i8liph='m lint-php'
alias i8lipo='m lint-python'
alias i8liru='m lint-ruby'
alias i8lise='m lint-shellcheck'
alias i8lisq='m lint-sql'
alias i8maal='i8source && python3 -m cli.meta.ci.query --modes auto --matrix' # complexity matrix of every deployable row, in CI query order
alias i8maco='i8source && python3 -m cli.meta.ci.query --modes compose --matrix' # complexity matrix in compose query order
alias i8maho='i8source && python3 -m cli.meta.ci.query --modes host --matrix' # complexity matrix in host query order
alias i8masa='i8source && python3 -m cli.meta.ci.query --modes swarm --matrix' # complexity matrix in swarm query order
alias i8matis='i8matix -s' # variant with symbols
alias i8matiso='clear && i8maal --roles' # clear + deploy matrix of every deployable role, in CI query order
alias i8mativ='i8matix --variant' # deploy matrix / complexity overview for variants
alias i8mativas='i8mativ -s' # variant with symbols
alias i8mativaso='clear && i8maal' # clear + deploy matrix of every deployable variant, in CI query order
alias i8matiw='i8matis --sort "desc weight"' # complexity matrix, symbols, heaviest roles first
alias i8matix='infinito meta roles applications complexity' # deploy matrix / complexity overview
alias i8mcau='m mcp-audit'
alias i8me='m meta-list'
alias i8memi='m meta-mig'
alias i8mete='m meta-tree'
alias i8ne='m environment-bootstrap'
alias i8nedo='m network-dns-remove'
alias i8nedose='m network-dns-setup'
alias i8nepi='m network-ipv6-disable'
alias i8nepire='m network-ipv6-restore'
alias i8nere='m network-refresh'
alias i8nete='m environment-teardown'
alias i8netu='m network-trust-ca'
alias i8ni='m install'
alias i8nica='m install-act'
alias i8nicapu='m install-act-update'
alias i8nifo='m install-force'
alias i8niga='m install-agent'
alias i8nila='m install-alias'
alias i8nilas='i8nila && source ~/.bashrc' # Install aliases and source them
alias i8nili='m install-lint'
alias i8nilifo='m install-lint-force'
alias i8nina='m install-ansible'
alias i8nino='m install-node'
alias i8niph='m install-php'
alias i8nipo='m install-python'
alias i8nipode='m install-python-dev'
alias i8niru='m install-ruby'
alias i8nise='m install-system-python'
alias i8nisi='m install-skills'
alias i8nive='m install-venv'
alias i8on='m onboard'
alias i8paal='i8source && python3 -m cli.meta.ci.plan --cli --modes auto' # plan table for every deployable row, in CI query order
alias i8paco='i8source && python3 -m cli.meta.ci.plan --cli --modes compose' # plan table for the compose line
alias i8paho='i8source && python3 -m cli.meta.ci.plan --cli --modes host' # plan table for the host line
alias i8papr='i8source && python3 -m cli.meta.ci.plan --cli --modes auto --priority' # plan table for the priority line given as the next argument
alias i8pasa='i8source && python3 -m cli.meta.ci.plan --cli --modes swarm' # plan table for the swarm line
alias i8pi='pcd infinito' # cd to the infinito repo
alias i8puci='__i8puci(){ gips && i8cifail "$1"; }; __i8puci' # sign-push + retrigger the failed selections of run $1, skipping the quality gate
alias i8pufail='__i8pufail(){ gips && i8cifail "$1"; }; __i8pufail' # sign-push, then retrigger the failed selections of run $1 across every mode
alias i8pufo='__i8pufo(){ gips && i8cifo "$1"; }; __i8pufo' # sign-push, then retrigger only hard-failed roles of run $1, excluding cancelled/aborted
alias i8puma='__i8puma(){ local gates=() ok=1; case "${3:-}" in "") ;; true|false) gates=(-f workspace="$3" -f instructions="$3");; *) ok=0;; esac; { [ -z "${1:-}" ] || [ "$ok" = 0 ]; } && { echo "usage: i8puma <priority app ids> [distros] [true|false]" >&2; return 2; }; i8source && gips && gh workflow run entry-manual.yml --repo "$(git remote get-url "$(git config --get remote.pushDefault)")" --ref "$(git rev-parse --abbrev-ref HEAD)" -f priority="$(python3 -m utils.roles.display --encode "$1")" -f distros="${2:-${INFINITO_DISTROS}}" "${gates[@]}"; }; __i8puma' # sign-push, then dispatch a manual CI run on the push remote (remote.pushDefault): priority line $1, distros $2 (default INFINITO_DISTROS), whitelist diff-derived; $3 true forces the workspace and instructions tests, false suppresses them, omitted leaves them on the workflow auto scope
alias i8puwl='__i8puwl(){ [ -z "${1:-}" ] && { echo "usage: i8puwl <whitelist app ids> [distros]" >&2; return 2; }; i8source && gh workflow run entry-manual.yml --repo "$(git remote get-url "$(git config --get remote.pushDefault)")" --ref "$(git rev-parse --abbrev-ref HEAD)" -f whitelist="$(python3 -m utils.roles.display --encode "$1")" -f distros="${2:-${INFINITO_DISTROS}}" -f workspace=false -f instructions=false; }; __i8puwl' # dispatch a manual CI run on the push remote (remote.pushDefault) limited to whitelist $1, distros $2 (default INFINITO_DISTROS), with the workspace and instructions tests off; pushes nothing, so the run uses whatever the remote already holds for the current branch
alias i8qu='m quality'
alias i8quhi='m quality-high'
alias i8re='m requirements-archive'
alias i8rece='m readme-check'
alias i8rege='m readme-generate'
alias i8rein='m readme-index'
alias i8ro='m roundtrip'
alias i8ru='m runner-ci-deploy'
alias i8ruci='m runner-ci-disable'
alias i8rucine='m runner-ci-enable'
alias i8s='i8nilas' # shortcut for i8nilas
alias i8sa='m swarm-clean'
alias i8saae='m swarm-app-exec'
alias i8sace='m swarm-clean-stale-nfs'
alias i8sadi='m swarm-diagnostic'
alias i8sado='m swarm-down'
alias i8sapa='m swarm-playwright'
alias i8sase='m swarm-shell'
alias i8saxe='m swarm-exec'
alias i8sazo='m swarm-zombie'
alias i8se='m security-apparmor-restore'
alias i8se2='m setup'
alias i8sece='m setup-clean'
alias i8sepa='m security-apparmor-teardown'
alias i8sepu='m system-purge'
alias i8source='source scripts/meta/env/load.sh' # load the project .env + venv python into the current shell
alias i8ta='m autoformat'
alias i8tare='m autoformat-restage'
alias i8te='(i8pi && tunit && tint && lint)' # unit + integration + lint
alias i8te2='m test'
alias i8teli='m test-lint'
alias i8tema='m test-main-merged'
alias i8temesi='m test-merge-signed'
alias i8teni='m test-integration'
alias i8tenu='m test-unit'
alias i8tenuja='m test-unit-javascript'
alias i8tenuph='m test-unit-php'
alias i8tenupo='m test-unit-python'
alias i8tenuru='m test-unit-ruby'
alias i8tepe='m test-performance'
alias i8tesi='m test-signed'
alias i8texe='m test-external'
alias i8wo='m wsl2-dns-setup'
alias i8wodo='m worktree-down'
alias i8wopr='m worktree-prune'
alias i8wose='m wsl2-systemd-check'
alias i8wotu='m wsl2-trust-windows'
alias i8woup='m worktree-up'
+3
View File
@@ -25,6 +25,9 @@ lookup_plugins = ./plugins/lookup
library = ./library
module_utils = ./utils
[callback_profile_tasks]
task_output_limit = 300
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=20s -o ControlPath=~/.ssh/ansible-%h-%p-%r -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -o StrictHostKeyChecking=accept-new -o PreferredAuthentications=publickey,password,keyboard-interactive
pipelining = True
-8
View File
@@ -4,14 +4,6 @@ from __future__ import annotations
import sys
from pathlib import Path
# This file is invoked directly as a script (the ``cli`` console-script
# entry-point and the integration help-test), in which case
# ``from . import PROJECT_ROOT`` fails with ``attempted relative import
# with no known parent package``. Compute the repo root
# locally and prepend it to sys.path before any project-package import
# resolves. Without this, sys.path[0] points to ``<root>/cli`` and
# importing ``cli.*`` may accidentally resolve to an installed package
# from site-packages.
# nocheck: project-root-import sys.path bootstrap before package imports resolve
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
+3
View File
@@ -1,3 +1,6 @@
# Deploy 🚀
Run Ansible deploys against a dedicated inventory, a CI container image, or the development matrix that iterates per-role variants.
- [development/](development/README.md): matrix deploy, include resolution, `disable` and orphan pruning (with diagrams).
- [ci/](ci/README.md): CI container image deploy.
+6
View File
@@ -0,0 +1,6 @@
# CI Deploy Runs 🚦
Inspect and trigger the compose and swarm deploy-test runs for the branch you
are on. `status` summarises each role's per-mode result (docker, swarm, and an
aggregated all) from the last run; `trigger` dispatches a fresh manual run for
all roles, only the ones that failed last time, or an explicit list.
+106
View File
@@ -0,0 +1,106 @@
"""Git and ``gh`` plumbing for the CI deploy-run status/trigger commands."""
from __future__ import annotations
import json
import re
import subprocess
import sys
def run_id_from_url(url: str) -> str:
match = re.search(r"/runs/(\d+)", url)
if not match:
raise ValueError(f"no run id found in URL: {url}")
return match.group(1)
def slug_from_url(url: str) -> str:
"""Extract the ``owner/repo`` slug from a github.com URL (https or ssh)."""
match = re.search(r"github\.com[/:]([^/]+)/([^/]+?)(?:\.git)?(?:/|$)", url)
if not match:
raise ValueError(f"no owner/repo found in URL: {url}")
return f"{match.group(1)}/{match.group(2)}"
def _run(args: list[str]) -> str:
return subprocess.run(
args, check=True, capture_output=True, text=True
).stdout.strip()
def _branch_remote() -> str:
"""The remote the current branch tracks (e.g. a fork), falling back to
``remote.pushDefault``, then 'origin' or the only configured remote."""
try:
upstream = _run(
["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]
)
if "/" in upstream:
return upstream.split("/", 1)[0]
except subprocess.CalledProcessError:
pass
remotes = _run(["git", "remote"]).split()
try:
push_default = _run(["git", "config", "--get", "remote.pushDefault"])
if push_default in remotes:
return push_default
except subprocess.CalledProcessError:
pass
if "origin" in remotes:
return "origin"
if not remotes:
raise RuntimeError("no git remote configured")
return remotes[0]
def resolve_repo() -> str:
"""The ``owner/repo`` the current branch lives on, derived from its
tracking remote (not gh's default repo, which may be the upstream)."""
return slug_from_url(_run(["git", "remote", "get-url", _branch_remote()]))
def _gh(args: list[str], repo: str | None = None, check: bool = True) -> str:
"""Run ``gh`` and return its stdout.
Args:
args: the ``gh`` arguments, without the ``gh`` itself.
repo: appended as ``--repo`` when given.
check: abort the command on a non-zero exit. Pass ``False`` for calls
whose failure the caller handles, which then yields ``""``.
"""
cmd = ["gh", *args]
if repo:
cmd += ["--repo", repo]
proc = subprocess.run(cmd, check=False, capture_output=True, text=True)
if proc.returncode != 0:
if not check:
return ""
sys.stderr.write(
proc.stderr or f"gh exited {proc.returncode}: {' '.join(cmd)}\n"
)
raise SystemExit(proc.returncode)
return proc.stdout
def current_branch() -> str:
return _run(["git", "rev-parse", "--abbrev-ref", "HEAD"])
def fetch_jobs(run_id: str, repo: str | None = None) -> list[dict]:
return json.loads(_gh(["run", "view", run_id, "--json", "jobs"], repo=repo)).get(
"jobs", []
)
def fetch_run(run_id: str, repo: str | None = None) -> dict:
"""Jobs plus the run title, in one ``gh`` call.
The REST API answers ``inputs: null`` for a finished workflow_dispatch, so
the inputs are recovered from the run itself: short ones from the title
(``config_from_title``), the long ones from a called job's log
(``inputs_from_jobs``), which the title truncates.
"""
return json.loads(
_gh(["run", "view", run_id, "--json", "jobs,displayTitle"], repo=repo)
)
+498
View File
@@ -0,0 +1,498 @@
"""Shared helpers for the CI deploy-run status/trigger commands.
Reads GitHub Actions runs via the ``gh`` CLI and maps the per-app
compose ("docker") and swarm deploy jobs to pass/fail/abort states.
A mode job that ran but did not finish successfully (cancelled, timed out,
still running) counts as a failure; a mode the role has no job for is N/A
and never fails the aggregated ``total`` column.
Deploy jobs are matched by the compose/swarm/host glyph (from the symbol
glossary, the single source of truth) their test-deploy-{compose,swarm,host}.yml
matrix titles them with -- NOT the words "Compose"/"Swarm"/"Host", which no
real job carries (matching those made ``--failed swarm`` silently find
nothing). What follows the glyph is the role's display name
(``utils.roles.display``), decoded back to the role id; a bare role id from a
run that predates the display names decodes just as well. The orchestrator
prepends a "caller / " prefix and the shard suffix (e.g. " 0,1") rides along
in the display name; both are tolerated.
"""
from __future__ import annotations
import json
import re
from cli.administration.deploy.ci.gh import (
_gh,
fetch_jobs,
)
from utils.cache import PROJECT_ROOT
from utils.cache.yaml import load_yaml_any
from utils.github import run_name
from utils.github.variant import axes
from utils.roles.display import display_names, split_axes
from utils.symbol_glossary import to_emoji
PASS = "✅" # noqa: S105 emoji glyph, not a credential
FAIL = "❌"
ABORT = "🚫"
RUNNING = "⏳"
MISSING = "➖"
MODES = ("docker", "swarm", "host")
ENTRY_WORKFLOW = ".github/workflows/entry-manual-steer.yml"
SELECTION_INPUTS = ("priority", "offset")
"""The two inputs a retrigger decides itself rather than carrying over.
``priority`` IS the retrigger: it names what failed. ``offset`` is recomputed
rather than carried, but from the carried value up: the source run got a
stretch behind its own offset green before its budget ran out, so the regular
line resumes behind that stretch
(:func:`cli.administration.deploy.ci.selections.resume_offset`) instead of
repeating it, and never falls back behind where that run started. Everything
else is carried verbatim, including ``whitelist``, so a retrigger of a scoped
run stays inside that scope instead of quietly widening to the whole
repository."""
def dispatch_inputs() -> tuple[str, ...]:
"""Every ``workflow_dispatch`` input the manual entry declares.
Read from the workflow rather than listed here, so an input added to the
form is carried over without anyone remembering to teach this module about
it -- the failure mode otherwise is silent: the retrigger runs with that
input on its default and nothing says so.
"""
data = (
load_yaml_any(str(PROJECT_ROOT / ENTRY_WORKFLOW), default_if_missing={}) or {}
)
triggers = data.get("on") if isinstance(data.get("on"), dict) else data.get(True)
dispatch = triggers.get("workflow_dispatch") if isinstance(triggers, dict) else None
inputs = dispatch.get("inputs") if isinstance(dispatch, dict) else None
return tuple(inputs or ())
def carried_inputs() -> tuple[str, ...]:
"""The inputs a retrigger reproduces: every dispatch input except the
selection it computes itself."""
return tuple(name for name in dispatch_inputs() if name not in SELECTION_INPUTS)
CONFIG_INPUTS = (
"distros",
"mode",
"lifecycles",
"filesystem",
"chunk_gate",
"workspace",
)
LOG_INPUTS = ("tor",)
"""Inputs the retrigger reads from the job log rather than the run title.
The title renders these as glyphs, so recovering one means translating a
symbol back into a value; the log holds the word the operator picked. Reading
it there keeps the retrigger honest even if the title's marker changes shape.
"""
MODE_GLYPHS = {
"docker": to_emoji("compose"),
"swarm": to_emoji("swarm"),
"host": to_emoji("host"),
}
_STATUS_MODE = {"compose": "docker", "swarm": "swarm", "host": "host"}
"""The status table keeps calling compose 'docker'; the deploy axis calls it
'compose'. Translate at the boundary instead of renaming either vocabulary."""
def _effective(job: dict) -> str:
"""The job's outcome: its conclusion when completed, else 'running'."""
if job.get("status") != "completed":
return "running"
return job.get("conclusion") or "running"
def cell(state: str) -> str:
if state == "success":
return PASS
if state == "failure":
return FAIL
if state == "running":
return RUNNING
if state == "missing":
return MISSING
return ABORT
def _iter_deploy_jobs(jobs: list[dict]):
"""Yield ``(app, mode, job)`` for every deploy job."""
codec = display_names()
for job in jobs:
label = axes.parse_label(str(job.get("name", "")))
if label is None:
continue
app = codec.decode(label.name)
if app is not None:
yield app, _STATUS_MODE[label.mode], job
def app_of_job(name: str) -> str | None:
"""The role id a deploy job ``name`` encodes, or None if it is not one."""
label = axes.parse_label(name)
return display_names().decode(label.name) if label else None
_SEVERITY = {"success": 0, "running": 1, "failure": 3}
def _severity(state: str) -> int:
return _SEVERITY.get(state, 2)
def parse_role_statuses(jobs: list[dict]) -> dict[str, dict[str, str]]:
"""Map ``app id -> {"docker": state, "swarm": state}`` from gh job dicts.
``state`` is the raw effective outcome ('success' / 'failure' /
'running' / 'cancelled' / ...). Modes without a job are simply absent.
A role's variant shards run as separate jobs per mode; the mode state
aggregates them worst-first, so one green shard can never mask a failed
sibling (gitlab swarm variant 1 red, variant 0 green -> swarm failure).
"""
out: dict[str, dict[str, str]] = {}
for app, mode, job in _iter_deploy_jobs(jobs):
state = _effective(job)
modes = out.setdefault(app, {})
if mode not in modes or _severity(state) > _severity(modes[mode]):
modes[mode] = state
return out
def parse_role_urls(jobs: list[dict]) -> dict[str, dict[str, str]]:
"""Map ``app id -> {"docker": url, "swarm": url}`` of the job html URLs,
keeping the URL of the worst shard so links point at the failing job."""
out: dict[str, dict[str, str]] = {}
worst: dict[tuple[str, str], int] = {}
for app, mode, job in _iter_deploy_jobs(jobs):
url = job.get("url")
if not url:
continue
severity = _severity(_effective(job))
if (app, mode) not in worst or severity > worst[(app, mode)]:
worst[(app, mode)] = severity
out.setdefault(app, {})[mode] = url
return out
def total_state(modes: dict[str, str]) -> str:
"""Aggregate the per-mode states into the ``total`` column: green only when
every mode that actually ran is green. A mode the role has no job for is N/A
(a host driver never deploys in swarm) and does NOT fail the total;
``parse_role_statuses`` only lists roles with at least one deploy job, so
there is always a present mode to judge."""
present = [modes[m] for m in MODES if m in modes]
return "success" if present and all(s == "success" for s in present) else "failure"
def failed_roles(
statuses: dict[str, dict[str, str]], scope: str = "total", *, strict: bool = False
) -> list[str]:
"""Roles that are not green for the given scope: ``total`` (a mode that ran
is not green), ``swarm``, or ``docker`` (compose). A role with no job in the
requested mode is skipped, not failed: a host driver that never deploys in
swarm is not a swarm failure, only roles whose swarm job ran and did not pass
are.
With ``strict``, only a hard ``failure`` (❌) selects a role; cancelled,
timed-out, skipped (🚫) and still-running (⏳) modes do not.
"""
def fails(modes: dict[str, str]) -> bool:
if strict:
if scope == "total":
return any(state == "failure" for state in modes.values())
return modes.get(scope) == "failure"
if scope == "total":
return total_state(modes) != "success"
return scope in modes and modes[scope] != "success"
return sorted(app for app, modes in statuses.items() if fails(modes))
_INPUT_RE = re.compile(r"^\S+\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*): ?(?P<value>.*)$")
def _parse_inputs(log: str) -> dict[str, str]:
"""The ``##[group] Inputs`` block of a job log, as name -> value."""
inputs: dict[str, str] = {}
inside = False
for line in log.splitlines():
if line.endswith("##[group] Inputs"):
inside = True
continue
if inside:
if line.endswith("##[endgroup]"):
break
match = _INPUT_RE.match(line)
if match:
inputs[match["name"]] = match["value"].strip()
return inputs
def inputs_from_jobs(jobs: list[dict], repo: str) -> dict[str, str]:
"""The dispatch inputs a called job's log records verbatim.
GitHub cuts ``display_title`` at 512 UTF-8 bytes (measured on two runs,
both exactly 512) and closes it with a literal ``...``. The run-name
template renders ``priority`` second-to-last and ``whitelist`` last, and
on the ``--failed`` path the whitelist is empty, so the cut always lands
inside the priority list: measured 21 of 69 roles survived, the 21st a
half-token. A CJK display name costs 3 bytes per character, so the budget
is ~22 display names against ~28 bare role ids. Every job of the *called*
workflow opens its log with an ``##[group] Inputs`` block holding the full
values, so that is the SPOT for anything the title cannot hold.
Candidates are read shallowest first, because a depth-1 job records the
orchestrator's own inputs while a deeper one records what its caller
forwarded. Skipped jobs are passed over: they never upload a log blob, so
their log endpoint answers ``BlobNotFound`` as a bare HTTP 404 -- and a
force-cancelled run can leave every depth-1 job skipped, which is why the
deeper jobs are read at all rather than treated as inputs-free.
Args:
jobs: the run's jobs, as ``gh run view --json jobs`` returns them.
repo: ``owner/repo`` the run lives in.
Returns:
input name -> value, empty when no job of the run records an ``Inputs``
block -- including when the logs have expired.
"""
candidates = sorted(
(
job
for job in jobs
if str(job.get("name", "")).count(" / ") >= 1
and job.get("databaseId")
and job.get("conclusion") != "skipped"
),
key=lambda job: str(job["name"]).count(" / "),
)
for job in candidates:
inputs = _parse_inputs(
_gh(
[
"api",
"--allow-escape-sequences",
f"repos/{repo}/actions/jobs/{job['databaseId']}/logs",
],
check=False,
)
)
if inputs:
return inputs
return {}
def dispatched_priority(source: dict, repo: str) -> str:
"""The source run's ``priority`` input, whole.
The title's priority segment is the cross-check, not the source: it proves
a priority line existed even when truncation mangled it, so an empty log
read is a broken reader -- expired logs, a moved Inputs block, an inlined
orchestrator -- rather than a run dispatched without one.
Args:
source: the run as :func:`fetch_run` returns it.
repo: ``owner/repo`` the run lives in.
"""
priority = inputs_from_jobs(source["jobs"], repo).get("priority", "")
if not priority and run_name.value_from_title(source["displayTitle"], "priority"):
raise SystemExit(
"Source run has a priority line but its job log records no inputs; "
"the Inputs block moved or the log expired. Retriggering now would "
"silently drop every priority role."
)
return priority
def untriggered_priority(
priority: str, statuses: dict[str, dict[str, str]]
) -> list[str]:
"""Roles the source run's priority line named but never deployed at all.
A priority role can fall behind the discovery budget cut in every mode, so
the run holds no job for it and :func:`failed_roles` cannot see it — it is
neither green nor red, it simply never ran. Carrying it into the retrigger
is the only way it ever gets deployed.
A priority entry may pin axes (``web-app-zammad#1@compose+tor``), and the
pin rides along into the retrigger: the entry never ran, so the run holds
no evidence that the axes it named were the wrong ones. Only the name in
front of the axis suffix is resolved against the statuses.
Args:
priority: the raw ``priority`` input, as :func:`inputs_from_jobs` reads
it back. A truncated value would smuggle a half-token into the
retrigger's role set, so an undecodable name raises instead.
statuses: role -> mode -> state, from :func:`parse_role_statuses`.
Returns:
the entries whose role has no job at all in the source run, each with
its own axis suffix intact.
"""
codec = display_names()
untriggered = []
for token in priority.split():
name, axes = split_axes(token)
role = codec.decode(name)
if role is None:
raise SystemExit(
f"Cannot resolve priority entry {token!r} to a role. The source "
f"run's priority input is corrupt or truncated; retriggering "
f"with it would deploy a role set that is silently incomplete."
)
if role not in statuses:
untriggered.append(role + axes)
return sorted(untriggered)
def config_from_title(title: str) -> dict[str, str]:
"""Configuration inputs a manual run was dispatched with, keyed by input
name. An input left on its workflow default renders no title segment and
is absent here, so the retrigger leaves it on that same default.
Only the configuration is recovered, never the selection: ``whitelist``
and ``priority`` say which roles ran, and a retrigger computes those
itself. Runs from any other entry point carry an unrelated title and
yield nothing at all.
"""
recorded = run_name.values_from_title(title)
return {name: recorded[name] for name in CONFIG_INPUTS if name in recorded}
SUITE_JOB_IDS = {"workspace": "test-workspace"}
def suite_passed(jobs: list[dict], suite: str) -> bool:
"""Whether every job of ``suite`` ('workspace') completed successfully.
Only a clean sweep of successes retires the suite. A shard that failed,
was cancelled, timed out or skipped did not run through, and a run that
holds no job for the suite at all never started it -- none of those is
evidence the suite is good, so the retrigger keeps asking for it.
"""
job_id = SUITE_JOB_IDS[suite]
outcomes = [
_effective(job)
for job in jobs
if job_id in str(job.get("name", "")).split(" / ")[1:]
]
return bool(outcomes) and all(outcome == "success" for outcome in outcomes)
def config_from_run(
title: str,
logged: dict[str, str] | None = None,
jobs: list[dict] | None = None,
) -> dict[str, str]:
"""Every input the source run was dispatched with, except the selection.
The job log is the source: it records all inputs verbatim, including the
ones the title renders as a glyph and the ones it renders not at all. The
title fills the gaps when a log is unreadable.
An input the source left on its default resolves to empty and is dropped,
so the retrigger leaves it on that same default rather than pinning
today's default into a run that never asked for it.
A suite is turned off only when every job of it came back green; one that
failed, was cancelled or never ran at all keeps it on the retrigger, so a
retrigger spends its runners on what is not yet proven.
What the source asked for does not enter that decision: the run's own jobs
are the evidence. A suite reached on the ``auto`` default is the common
case -- ``call-orchestrator.yml`` reads ``auto`` as global scope off the
``whitelist`` alone, so a ``--failed`` retrigger, which puts its selection
on ``priority`` and leaves the whitelist empty, would run every green
suite again for as long as the retrigger chain lasts.
Args:
title: the source run's display title.
logged: inputs read verbatim from a called job's log
(:func:`inputs_from_jobs`).
jobs: the source run's jobs, which record the suites it already
passed (:func:`suite_passed`).
"""
recorded = run_name.values_from_title(title)
config = {
name: (logged or {}).get(name) or recorded.get(name, "")
for name in carried_inputs()
}
for suite in SUITE_JOB_IDS:
if suite_passed(jobs or [], suite):
config = {**config, suite: "false"}
return {name: value for name, value in config.items() if value}
def find_last_deploy_run(
branch: str, repo: str | None = None, limit: int = 15
) -> dict | None:
"""Newest run on ``branch`` (in ``repo``) that actually contains compose/
swarm deploy jobs. Returns the run dict (with a cached ``_jobs`` key) or
None.
Walks up to ``limit`` recent runs because the very latest run on a branch
is often a lint-only or skipped event with no deploy matrix.
"""
listed = _gh(
[
"run",
"list",
"--branch",
branch,
"-L",
str(limit),
"--json",
"databaseId,url,workflowName,createdAt,status,displayTitle",
],
repo=repo,
)
for run in json.loads(listed):
jobs = fetch_jobs(str(run["databaseId"]), repo=repo)
if parse_role_statuses(jobs):
run["_jobs"] = jobs
return run
return None
def dispatch_workflow(
workflow: str,
ref: str,
whitelist: str = "",
*,
priority: str = "",
config: dict[str, str] | None = None,
repo: str | None = None,
) -> None:
"""Dispatch *workflow*, carrying *config* (see :func:`config_from_title`)
over verbatim. An input left out keeps the workflow's own default.
The role selections go out as display names (``utils.roles.display``), so
the run title reads them back the way the deploy jobs are titled. The
workflow decodes them again before any consumer sees a role id.
"""
codec = display_names()
args = ["workflow", "run", workflow, "--ref", ref]
if whitelist:
args += ["-f", f"whitelist={codec.encode_list(whitelist)}"]
if priority:
args += ["-f", f"priority={codec.encode_list(priority)}"]
args += [
arg
for name, value in (config or {}).items()
if value
for arg in ("-f", f"{name}={value}")
]
_gh(args, repo=repo)
+255
View File
@@ -0,0 +1,255 @@
"""Turn a run's deploy jobs back into selection tokens.
A deploy job title carries the whole row it deployed -- role, variant, mode,
onion state, distro, filesystem -- and :mod:`utils.github.variant.selection`
is the grammar that writes it back down.
The filesystem is the one axis a token built here leaves out. The title states
the kind the matrix *assigned*, which a deploy is allowed to fall back from
when the host cannot serve it, so the title does not prove what the job ran on.
Writing it into a token would also state it as named by a human, which makes it
binding (:func:`utils.github.variant.axes.assign`): a retrigger would then fail
on the very condition the fallback exists to absorb. The retrigger lets the
rotation assign it again and reports what it got. Reading the tokens out of a finished run is what lets a
retrigger replay exactly what failed and start the regular line where that run
stopped, instead of aggregating to role ids and guessing the rest.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from utils.github.variant import axes, selection
if TYPE_CHECKING:
from collections.abc import Iterable, Mapping
from typing import Any
from .runs import _effective, _iter_deploy_jobs
def failed_selections(jobs: list[dict], *, strict: bool = False) -> list[str]:
"""The selection tokens that reproduce exactly what did not pass.
A role aggregated to its id loses what actually broke: the retrigger then
redeploys every variant of it, in whatever mode and onion state the sweep
rotation happens to pick, and the combination that failed may not be among
them. Each failed job therefore contributes its own
``role#variant@mode+tor`` token (:mod:`utils.github.variant.selection`), so
the priority line replays that job and nothing else.
Every mode is read; there is no scope to narrow to. A run that failed in
swarm and in compose comes back as two tokens for the same role.
Args:
jobs: the source run's jobs.
strict: only hard failures (❌) count; cancelled, timed out and still
running are left out.
Returns:
sorted, deduplicated tokens.
"""
tokens = set()
for app, _mode, job in _iter_deploy_jobs(jobs):
state = _effective(job)
if state == "success" or (strict and state != "failure"):
continue
label = axes.parse_label(str(job.get("name", "")))
tokens.add(
selection.describe(
selection.Pin(
app,
tuple(int(part) for part in label.variant.split(",") if part),
label.mode,
label.tor,
label.distro or None,
)
)
)
return sorted(tokens)
def collapse_to_roles(tokens: Iterable[str]) -> list[str]:
"""The same selections with every axis dropped, one entry per role.
:func:`failed_selections` reproduces the combination that broke, which is
what a handful of red rows deserves. Once most of the ranking is red the
question is no longer which combination broke but whether the role comes up
at all, and a token per combination spends the priority budget proving the
same role red twice. The role name lets the rotation pick the axes.
What this gives up is the guarantee the pinned form exists for: the
combination that failed may not be among the ones the retrigger draws.
Args:
tokens: selection tokens, as :func:`failed_selections` returns them.
Returns:
sorted, deduplicated role ids.
"""
return sorted({selection.parse(token).app for token in tokens})
def _variants(entry: Mapping[str, Any]) -> tuple[int, ...]:
"""The variant shards one matrix row covers."""
return tuple(int(part) for part in str(entry.get("variant", "")).split(",") if part)
def row_identity(entry: Mapping[str, Any]) -> str:
"""One matrix row as the ``role#variant`` the sweep rotation cannot move.
Args:
entry: a row of :func:`cli.meta.ci.matrix.entries_of`.
"""
return selection.describe(selection.Pin(entry["apps"], _variants(entry)))
def proven_rows(jobs: list[dict]) -> set[str]:
"""The rows the source run proved, as ``role#variant``, axes dropped.
Mode, onion state, distro and filesystem are assigned by the rotation over
the sweep number, and a retrigger is a new run with a new number
(``call-orchestrator.yml`` falls back to ``github.run_number``). Comparing
the full deploy tokens therefore compares two different sweeps: measured
against the current ranking, 2 of 277 rows matched, so every walk stopped
on its first row and the regular line never left the head. The row is the
coarsest identity the rotation cannot move.
Dropping the axes does not mean forgiving them. A row is proven only when
*every* job it had was green: one red or aborted combination un-proves it,
however many green siblings it has. Only a priority row can have several,
since it deploys its whole cross-product at once while a regular row gets
one combination per sweep -- and letting a green compose job carry a red
swarm one would walk the regular line past a combination nobody fixed.
Args:
jobs: the source run's jobs.
"""
green: set[str] = set()
broken: set[str] = set()
for app, _mode, job in _iter_deploy_jobs(jobs):
label = axes.parse_label(str(job.get("name", "")))
if label is None:
continue
row = selection.describe(
selection.Pin(
app, tuple(int(part) for part in label.variant.split(",") if part)
)
)
(green if _effective(job) == "success" else broken).add(row)
return green - broken
def carried_index(regular: list[dict[str, str]], carried: str, proven: set[str]) -> int:
"""Where in *regular* the source run began reading.
A retrigger inherits the window its source was given: the rows in front of
that window were proven by the runs before it, and re-walking them from the
head would redeploy a green stretch that nobody doubts. The source run's
own ``offset`` input is that window's start.
The token can name a row this ranking no longer has -- the role was
removed, a variant was dropped, the filters moved. The start is then the
first row the source run got green, which lies inside the same window and
so never falls behind it. A run with nothing green and an unresolvable
token leaves no anchor at all and starts at the head.
Args:
regular: the regular line of the retrigger's own discovery, in ranking
order.
carried: the source run's ``offset`` input, a token or a row count.
proven: the rows the source run got green (:func:`proven_rows`).
Returns:
the index to start reading at; ``0`` when the source run was given no
offset.
"""
if not carried:
return 0
if carried.lstrip("+-").isdigit():
return max(int(carried), 0)
pin = selection.parse(carried)
for index, entry in enumerate(regular):
if selection.covers(pin, entry):
return index
for index, entry in enumerate(regular):
if row_identity(entry) in proven:
return index
return 0
def resume_offset(
regular: list[dict[str, str]],
proven: set[str],
leading: Iterable[selection.Pin] = (),
carried: str = "",
) -> str:
"""Where a retrigger should pick the regular line up again.
The source run read a window of the ranking, starting where *carried* put
it (:func:`carried_index`) and stopping at its budget. Only the green part
of that window is newly settled, so the answer is the last row of the
leading run of *successful* rows behind the window's start, as a selection
token -- a token still names the same row after the ranking shifts, a row
count does not.
The window's start is the floor: with nothing green behind it the answer is
the start itself, never the head. Walking back to the head is what made
every retrigger redeploy the whole stretch its predecessors had already
proven.
Stops at the first row that is not proven, whether or not the priority line
claims it, and one red combination is enough to un-prove a row
(:func:`proven_rows`). Resuming past it would drop every other combination
of that row and everything the run never reached behind it.
A row the priority line claims is never the answer. ``regular`` is
discovered without the priority line (see the retrigger's ``_ranking``), so
it still holds rows the run will hand to that line instead
(:func:`cli.meta.ci.matrix.candidates`): a variant-less pin takes its whole
role, a pinned one takes those variants. Naming such a row would emit an
offset that resolves against nothing once the run applies the priority
line, and abort the discovery of every chunk.
Args:
regular: the regular line of the retrigger's own discovery, in ranking
order.
proven: the rows the source run got green in every combination it ran
them in (:func:`proven_rows`).
leading: the retrigger's priority pins, whose rows leave the regular
line.
carried: the source run's own ``offset`` input.
Returns:
the ``role#variant`` token to resume at, or ``''`` when the source run
started at the head and got nothing skippable of this line green --
then the retrigger starts at the head too, as it would without an
offset.
The returned token names a place in the ranking, so it carries no axes,
and neither does the membership test behind it (:func:`proven_rows`):
the source run recorded its combinations under its own sweep number and
the retrigger gets a fresh one, so comparing them row by row is the only
comparison that survives the rotation.
"""
pins = tuple(leading)
claimed_apps = {pin.app for pin in pins if not pin.variants}
claimed_rows = {(pin.app, variant) for pin in pins for variant in pin.variants}
def free(entry: Mapping[str, Any]) -> str:
app = entry["apps"]
variants = _variants(entry)
if app in claimed_apps or any((app, v) in claimed_rows for v in variants):
return ""
return row_identity(entry)
start = carried_index(regular, carried, proven)
window = regular[start:]
resume = next((token for entry in window if (token := free(entry))), "")
if not start:
resume = ""
for entry in window:
if row_identity(entry) not in proven:
break
resume = free(entry) or resume
return resume
@@ -0,0 +1,128 @@
"""Per-role compose ("docker") and swarm deploy status of a CI run."""
from __future__ import annotations
import argparse
import sys
from cli.administration.deploy.ci import gh, runs
def _build_rows(
statuses: dict[str, dict[str, str]], urls: dict[str, dict[str, str]]
) -> list[tuple[str, str, str, str, str]]:
rows = []
for app, modes in sorted(statuses.items()):
url = urls.get(app, {})
run = " ".join(u for u in (url.get("docker"), url.get("swarm")) if u)
rows.append(
(
app,
runs.cell(modes.get("docker", "missing")),
runs.cell(modes.get("swarm", "missing")),
runs.cell(runs.total_state(modes)),
run,
)
)
return rows
def _render_table(rows: list[tuple[str, str, str, str, str]]) -> str:
if not rows:
return ""
name_w = max(len("role"), max(len(r[0]) for r in rows))
lines = [
f"{'role':<{name_w}} docker swarm total run",
f"{'-' * name_w} ------ ----- ----- ---",
]
for app, docker, swarm, totalc, run in rows:
lines.append(f"{app:<{name_w}} {docker} {swarm} {totalc} {run}")
return "\n".join(lines)
def _render_string(rows: list[tuple[str, str, str, str, str]]) -> str:
return "\n".join(
f"{app} {docker} {swarm} {totalc} {run}"
for app, docker, swarm, totalc, run in rows
)
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(
prog="infinito administration deploy ci status",
description=(
"Show every role's compose (docker) and swarm deploy result from a "
"CI run, plus an aggregated 'total' column (green when every mode "
"that ran is green; a mode the role skips is N/A)."
),
)
p.add_argument(
"--url",
default=None,
help=(
"GitHub Actions run URL to read. Default: the most recent run on "
"the current branch that has deploy jobs."
),
)
p.add_argument(
"--failed",
nargs="?",
const="total",
default=None,
choices=("total", "swarm", "compose", "docker", "host"),
metavar="{total,swarm,compose,host}",
help=(
"Show only roles that were not green. Optional scope: 'total' "
"(default; failed in any mode), 'swarm', 'compose', or 'host'."
),
)
p.add_argument(
"--format",
choices=("table", "string"),
default="table",
help="Output format. 'table' (default) is aligned; 'string' is "
"whitespace-separated 'role docker swarm total' per line.",
)
args = p.parse_args(argv)
if args.url:
jobs = runs.fetch_jobs(
gh.run_id_from_url(args.url), repo=gh.slug_from_url(args.url)
)
source = args.url
else:
branch = gh.current_branch()
repo = gh.resolve_repo()
run = runs.find_last_deploy_run(branch, repo=repo)
if run is None:
print(
f"No CI run with deploy jobs found on {repo}@{branch}.",
file=sys.stderr,
)
return 1
jobs = run["_jobs"]
source = run["url"]
statuses = runs.parse_role_statuses(jobs)
if not statuses:
print("No compose/swarm deploy jobs in that run.", file=sys.stderr)
return 1
if args.failed is not None:
scope = "docker" if args.failed == "compose" else args.failed
keep = set(runs.failed_roles(statuses, scope))
statuses = {app: modes for app, modes in statuses.items() if app in keep}
if not statuses:
print(f"No roles failed ({args.failed}).", file=sys.stderr)
return 0
rows = _build_rows(statuses, runs.parse_role_urls(jobs))
rendered = _render_table(rows) if args.format == "table" else _render_string(rows)
print(rendered)
if args.format == "table":
print(f"\nsource: {source}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,235 @@
"""Trigger the manual CI run (entry-manual-steer.yml) for the current branch.
entry-manual-steer.yml reads the "__ALL__" whitelist sentinel as "force a full
deploy across all roles".
A retrigger differs from its source run in the selection and in nothing else:
every other dispatch input is carried over (:func:`runs.carried_inputs`, read
off the workflow itself), and the priority line names the exact selections that
failed -- role, variant, deploy mode, onion state and distro -- rather than the
roles they belong to. The filesystem is left to the rotation
(:mod:`cli.administration.deploy.ci.selections` says why). ``--chunk-gate`` is
the one deliberate exception: it overrides the carried value so a retrigger can
keep deploying past a failed chunk.
"""
from __future__ import annotations
import argparse
import sys
from cli.administration.deploy.ci import gh, runs, selections
from cli.meta.ci import matrix, query
from utils.github import run_name
from utils.github.variant import pools, selection, tor
_WORKFLOW = "entry-manual-steer.yml"
_ALL = "__ALL__"
def _fetch(run: str, repo: str) -> dict:
"""The jobs and title of *run*, given as a URL or a bare id (which
resolves against *repo*, the current branch's own)."""
if run.isdigit():
return gh.fetch_run(run, repo=repo)
return gh.fetch_run(gh.run_id_from_url(run), repo=gh.slug_from_url(run))
def _ranking(whitelist: str, config: dict[str, str]) -> list[dict[str, str]]:
"""The regular line the retrigger's own discovery would walk.
The priority line is deliberately left out of that computation: it only
blacklists rows from the regular query, and a token in it that no longer
resolves would abort here, in a helper whose job is to save runner time.
"""
return matrix.entries_of(
modes=query.resolve_modes(config.get("mode") or query.ALL_MODES),
whitelist="" if whitelist == _ALL else whitelist,
priority="",
lifecycles=config.get("lifecycles", ""),
sweep=0,
tor_mode=tor.resolve_tor_mode(config.get("tor")),
distros=pools.resolve_distros(config.get("distros")),
filesystems=pools.resolve_filesystems(config.get("filesystem")),
)
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(
prog="infinito administration deploy ci trigger",
description=(
"Dispatch the manual CI workflow for the branch you are on. "
"Default: trigger every role. With --failed: the roles that "
"failed in the last run form the priority line and the full "
"run follows once they are green. With --apps: an explicit "
"role list as whitelist. Whenever a source run is read, its "
"configuration is carried over so the retrigger reproduces it."
),
)
group = p.add_mutually_exclusive_group()
group.add_argument(
"--failed",
nargs="?",
const=True,
default=None,
metavar="(ignored)",
help=(
"Re-trigger what was not green in the source run as the priority "
"line, together with the priority entries that run never deployed "
"at all; the remaining roles follow once they succeed, starting "
"behind the green stretch the run added to the offset it was "
"given. Every mode is read, and each failed job comes back as the "
"exact selection that failed -- variant, deploy mode and onion "
"state included. A leftover scope argument is accepted and "
"ignored."
),
)
group.add_argument(
"--apps",
default=None,
metavar='"app1 app2 ..."',
help="Explicit space-separated role ids to trigger.",
)
p.add_argument(
"--run",
default=None,
metavar="URL|ID",
help=(
"Run URL or bare run id to reproduce: its configuration (distros, "
"mode, lifecycles, filesystem, chunk_gate, "
"workspace, instructions) is carried over, and with --failed its "
"results also pick the apps. A bare id resolves against the current "
"branch's repo. With --failed and no --run: the latest deploy run "
"on the current branch."
),
)
p.add_argument(
"--strict",
action="store_true",
help=(
"With --failed: re-trigger only roles with a hard failure (❌), "
"not cancelled/aborted (🚫) or still-running (⏳). Those two never "
"reached a verdict, so the regular line owes them their turn "
"rather than the priority line owing them a place."
),
)
p.add_argument(
"--roles-only",
action="store_true",
help=(
"With --failed: put the failed roles on the priority line by name, "
"letting the rotation assign the axes, instead of replaying the "
"exact combination each job failed in. Useful when so much is red "
"that covering the role matters more than reproducing the row. "
"Priority entries the source run never deployed keep their pins "
"either way -- that run holds no evidence against the axes they "
"named."
),
)
p.add_argument(
"--chunk-gate",
choices=("true", "false"),
default=None,
help=(
"Override the carried chunk_gate input. 'false' keeps deploying "
"the remaining chunks after a failed one instead of stopping the "
"chain; 'true' stops at the first failed chunk. Omitted: the "
"source run's value, else the workflow default."
),
)
args = p.parse_args(argv)
if args.strict and args.failed is None:
p.error("--strict only applies with --failed")
if args.roles_only and args.failed is None:
p.error("--roles-only only applies with --failed")
branch = gh.current_branch()
repo = gh.resolve_repo()
source = None
if args.run:
source = _fetch(args.run, repo)
elif args.failed is not None:
run = runs.find_last_deploy_run(branch, repo=repo)
if run is None:
print(
f"No CI run with deploy jobs found on {repo}@{branch}.",
file=sys.stderr,
)
return 1
source = {"jobs": run["_jobs"], "displayTitle": run.get("displayTitle", "")}
whitelist = ""
priority = ""
priority_entries: set[str] = set()
if args.apps is not None:
apps = " ".join(args.apps.split())
if not apps:
p.error("--apps was empty")
whitelist = apps
elif args.failed is not None:
statuses = runs.parse_role_statuses(source["jobs"])
failed = selections.failed_selections(source["jobs"], strict=args.strict)
if args.roles_only:
failed = selections.collapse_to_roles(failed)
untriggered = runs.untriggered_priority(
runs.dispatched_priority(source, repo), statuses
)
if not failed and not untriggered:
print("Nothing failed in that run; not triggering.")
return 0
if untriggered:
print(f"Priority roles that never deployed: {' '.join(untriggered)}")
priority_entries = set(failed) | set(untriggered)
else:
whitelist = _ALL
config: dict[str, str] = {}
carried_offset = ""
if source is not None:
logged = runs.inputs_from_jobs(source["jobs"], repo)
config = runs.config_from_run(
source["displayTitle"], logged, jobs=source["jobs"]
)
carried_offset = logged.get("offset") or run_name.value_from_title(
source["displayTitle"], "offset"
)
carried_whitelist = config.pop("whitelist", "")
if not whitelist and carried_whitelist:
whitelist = carried_whitelist
if args.chunk_gate is not None:
config["chunk_gate"] = args.chunk_gate
if args.failed is not None:
ranking = _ranking(whitelist, config)
priority = " ".join(sorted(priority_entries))
config["offset"] = selections.resume_offset(
ranking,
selections.proven_rows(source["jobs"]),
selection.parse_list(priority),
carried=carried_offset,
)
if config["offset"]:
print(f"Regular line resumes at: {config['offset']}")
else:
config.pop("offset")
if priority:
label = f"priority {priority}, then the remaining roles"
elif whitelist == _ALL:
label = "all roles"
else:
label = whitelist
carried = ", ".join(f"{name}={value}" for name, value in config.items())
print(f"Triggering {_WORKFLOW} on {repo}@{branch} for: {label}")
if carried:
print(f"Carrying over from the source run: {carried}")
runs.dispatch_workflow(
_WORKFLOW, branch, whitelist, priority=priority, config=config, repo=repo
)
print("Dispatched. Watch with: infinito administration deploy ci status")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -87,7 +87,6 @@ def wait_for_docker_socket(container: str, timeout: int = 60) -> None:
"""
print(">>> Waiting for Docker to be usable inside CI container...")
# 0) Verify docker CLI exists
which = _docker_exec_capture(container, ["sh", "-lc", "command -v docker || true"])
docker_path = (which.stdout or "").strip()
if not docker_path:
@@ -106,7 +105,6 @@ def wait_for_docker_socket(container: str, timeout: int = 60) -> None:
f"[diag stderr]\n{diag.stderr}\n"
)
# 2) Poll docker info
last_out = ""
last_err = ""
for _ in range(timeout):
@@ -118,7 +116,6 @@ def wait_for_docker_socket(container: str, timeout: int = 60) -> None:
print(">>> Docker is usable inside container.")
return
# capture a bit more detail for later
dv = _docker_exec_capture(
container, ["sh", "-lc", "docker version 2>&1 || true"]
)
@@ -209,7 +206,6 @@ def run_in_container(
name=name,
)
# 1) Create CI inventory
print(
">>> Creating CI inventory inside container "
"(infinito administration inventory provision)..."
@@ -231,7 +227,6 @@ def run_in_container(
check=True,
)
# 1b) Mock backup authorized_keys file for user-backup
print(">>> Mocking backup authorized_keys file inside CI inventory...")
mock_key = os.environ.get(
"AUTHORIZED_KEYS",
@@ -249,7 +244,6 @@ def run_in_container(
)
docker_exec(container_name, ["bash", "-lc", mock_cmd], check=True)
# 2) Ensure vault password file exists
print(">>> Ensuring CI vault password file exists...")
ensure_pw_cmd = (
f"mkdir -p {shlex.quote(inv_root)} && "
@@ -262,7 +256,6 @@ def run_in_container(
check=True,
)
# 3) Run dedicated deploy
cmd = [
"python3",
"-m",
Loaded 100 of 7221 files, more files were not shown because too many files have changed in this diff. Show more