mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
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:
7221 files changed
+251391
-73442
No files matched your search
+6
-2
@@ -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
@@ -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
@@ -1,3 +1,3 @@
|
||||
launch.json
|
||||
routines
|
||||
workflows
|
||||
*
|
||||
!.gitignore
|
||||
!settings.json
|
||||
+12
-6
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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[@]}"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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[@]}"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 .
|
||||
@@ -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[@]}"
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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 }})"
|
||||
@@ -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
|
||||
@@ -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 }})"
|
||||
@@ -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
|
||||
@@ -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
|
||||
'
|
||||
@@ -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
|
||||
'
|
||||
@@ -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
|
||||
'
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -29,12 +29,15 @@
|
||||
],
|
||||
"ignores": [
|
||||
"node_modules/**",
|
||||
"vendor/**",
|
||||
".cache/**",
|
||||
".git/**",
|
||||
"venv/**",
|
||||
".venv/**",
|
||||
"**/__pycache__/**",
|
||||
".github/PULL_REQUEST_TEMPLATE/**",
|
||||
".agents/skills/**",
|
||||
".claude/skills/**"
|
||||
".agents/**",
|
||||
".claude/**",
|
||||
"skills/**"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rules": {}
|
||||
}
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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.
|
||||
|
||||
@@ -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 📚
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
File renamed without changes.
@@ -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)
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
File renamed without changes.
@@ -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())
|
||||
File renamed without changes.
@@ -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
Reference in new issue
Block a user