Commit Graph
2780 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.8 f4172c547a fix(prometheus): exclude swarm-unreachable force_bridge apps at the native_metrics_apps SPOT
A native-metrics app on a node-local force_bridge network cannot be joined or resolved by a swarm-wide prometheus: the stack deploy fails with the network scope error and the scrape job would flap as permanently down. native_metrics_apps now drops those apps at the source (DEPLOYMENT_MODE swarm + networks.local.force_bridge), covering all four call sites (network precreate, compose service networks, external-networks block, scrape include) without per-template guards. DEPLOYMENT_MODE is templated without a default so a missing value fails loudly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:44:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 10a218ed6e fix(nfs): correct volume ownership for non-root NFS-backed containers
Per-volume nfs uid/gid/mode (erpnext/frappe 1000, jenkins 1000, moodle/www-data 33, ejabberd 9000) so NFS-backed named volumes get the container runtime owner. nfs_prep falls back to default(omit) instead of re-forcing root:0755, which reverted the docker copy-up ownership for non-root images on every redeploy. compose_volumes test asserts the x-infinito-nfs marker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:44:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4dbaa60707 feat(stages): drive deploy stages from a categories.yml SPOT
roles/categories.yml now declares the ordered 'stages' and a per-category 'stage' (plus run_after and bootstrap). utils/roles/stage.py resolves a role to its stage and exposes stage_groups(); the new stage_groups lookup feeds the 01_constructor / 02_server / 02_workstation / 03_destructor group loops in place of their hardcoded lists. A static invariant test proves the lookup reproduces the exact previous loops, so the rendered plays (and deploy behavior) are unchanged.

cli.meta.roles.order.run orders its main phase by stage as a run_after tiebreak. plugins/filter/invokable_paths tolerates the new sibling 'stages' key, and the invokable-roles wiring test recognizes stage_groups-driven wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:44:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 90b3845998 feat(readme): role README generator with Cosmos diagrams, Quick Setup, and lint
Add cli/build/readme: derives each role README's managed sections from
templates/roles/README.md.j2.tmpl (the Cosmos mermaid diagram from
meta/services.yml, an invokable-gated Quick Setup, and Credits).

The Cosmos diagram shows dependencies, every declared service, and
dependents (roles that consume this one, matched by role name and by
provides provision-name in both directions). Role nodes carry deploy-mode
emoji markers (compose/swarm/host); disabled and shared services get a
stop mark; dependents are capped at 12.

test_readme enforces the Quick Setup body is byte-exact with the template
render. `make readme-generate` and `make cosmos` regenerate; `make cosmos`
also runs inside `make quality`.

Refactor: utils/symbol_glossary.py is the single source of truth for the
word/emoji mapping (unique project-wide), consumed by the complexity
matrix and the Cosmos builder. utils/roles/deploy.py holds role_has_stack
and role_deploy_modes, shared by both so the logic never drifts.

Add `make lint-mermaid` and its CI workflow, rendering every diagram via
mermaid-cli; the render step provisions chrome-headless-shell and unzip
on demand so it works in the minimal container.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:44:33 +02:00
Alejandro Roman ab25684714 adrees all commens and fix CI usses on prev run 2026-07-16 19:19:23 +02:00
Alejandro Roman 889d3a370f fix SPot and issues found 2026-07-16 14:26:11 +02:00
Alejandro Roman f79037b7ac add mermaid and refacotr some code 2026-07-16 11:28:58 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8e685e751f feat(complexity): test_* columns driven by meta/tests.yml skip, wide-safe symbols
meta/services.yml modes states where a role RUNS; a new meta/tests.yml
skip list deactivates TESTING a mode without touching that capability.
The table gains test_compose/test_swarm/test_host columns (base column
minus skip), --filter/--sort and the JSON/YAML payloads know the new
fields, and CI discovery (apps.sh) now filters on test_<mode> == true.
svc-storage-nfs-client is the first consumer and skips all three modes.

Also replaces the nine VS16-based header/lifecycle emojis (like the
old host/jobs/weight symbols) with East-Asian-Wide ones: terminals
render VS16 sequences 2 cells wide while the width model said 1,
shifting every header right of them off its column. A guard test pins
all table symbols to wide, VS16-free codepoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 05:20:54 +02:00
kevinveenbirkenbachandClaude Opus 4.8 cd4d0e3e28 refactor(cli): move cli/deploy/runner under cli/administration/deploy
The runner command joins its siblings (dedicated, development) under
cli/administration/deploy; cli/deploy is gone. Makefile, docs and the
requirement links now point at the new path, PROJECT_ROOT climbs one
level further, and the unit tests moved along.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 05:20:54 +02:00
kevinveenbirkenbachandClaude Opus 4.8 16aa00d905 refactor(deploy-cli): split deploy.py into a per-responsibility package
One module per concern, mirroring the inventory/ subpackage layout:
run.py (single deploy pass + env forwarding SPOT), drill.py (recover
drill, host mode exempt, paths from CONTAINER_REPO_ROOT instead of a
hardcoded /opt/src/infinito), purge.py (inter-round cleanup) and
cli.py (arg parsing + round/pass loop, PASS 1/2 deduplicated).

Also extend the comment linter to flag bare string-expression
statements outside docstring position in Python: a triple-quoted
literal floating in a body is a comment in disguise and now runs
through the same validity rules; docstrings and PEP 224 attribute
docstrings stay allowed. Test narration comments moved into method
docstrings accordingly, and the design docs now link deploy/cli.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 05:20:54 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b5e5554883 fix(complexity): gate the host column on the lifecycle envelope
compose and swarm columns go through _tested_apps (invokable +
lifecycle envelope + per-mode skip) but host only checked non-stack +
modes.host.enabled, so host CI discovery reacted to every lifecycle:
61 roles instead of the 24 inside the alpha/beta/rc/stable envelope.
Route host through _tested_apps("host") in both the role-level and
per-variant row builders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 05:20:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5a52ab2024 fix(ci): cap workflow timeouts at 345m and lint the headroom
GitHub hard-kills hosted-runner jobs at 360 minutes, so a 360m budget
leaves the post-failure diagnostics and artifact-upload steps zero
runtime and the failure logs are lost. Cap every deploy workflow at
345m and add a lint that fails any .github/workflows/*.yml
timeout-minutes above 345.

nextcloud swarm shards 2 and 5 in run 29448092737 died at the previous
240m step budget with zero task failures, 10-15 minutes short of a
green update pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 03:44:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 672e93246b feat(deploy): add host deploy mode + matrix symbol view
Host lane for invokable non-stack roles (drivers, backup, wireguard, swapfile, ...). modes.host.enabled declared on 56 such roles; new lint test_role_host_mode enforces it. The complexity matrix gains a host column (not stack and modes.host.enabled), --deploy-mode host, and host bundles (one per variant); apps.sh accepts INFINITO_DEPLOY_MODE=host and filters host == true. New test-deploy-host.yml workflow (INFINITO_MAX_JOBS=10) deploys discovered host roles via the mode-agnostic ci/all.sh path, wired into ci-orchestrator (modes default 'swarm compose host').

Also: complexity -s/--symbol compact view (emoji-only headers, true/false as check/cross, lifecycle stage symbols, grouped legend with per-entry explanations, display-width aligned); stack column moved after host. scripts/meta/env/load.sh made zsh-compatible by delegating to bash and importing INFINITO_/PYTHON/VENV/PATH back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 03:44:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7a6268fa96 refactor(deploy): replace skip with services modes
Per-role deploy-mode opt-out moves from meta/tests.yml 'skip:' to meta/services.yml <entity>.modes.{compose,swarm}.enabled on the primary entity. get_role_skip now derives from it; get_role_mode_enabled added. A new lint requires every role shipping templates/*compose*.yml.j2 to declare both mode flags. Migrates 107 services.yml and drops 11 skip-only tests.yml. Also removes the variant_status deployability gate so swarm runs one job per variant unconditionally (variant_status.py and deployable_variant_indices deleted, complexity model simplified). Adds the i8matix alias.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 03:44:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 15e7647d2f refactor(env): central TZ+LANG base included by every role env.j2
New roles/sys-svc-container/templates/env.j2 sets TZ={{ HOST_TIMEZONE }} and LANG=C.UTF-8. Every role env.j2 includes it at the top; it renders via stack_host_template, the same engine that resolves the includes in compose.yml.j2, so it works in both compose and swarm. TZ/LANG are removed from the individual role env.j2 so each value stays single-source.

New lint tests/lint/ansible/test_env_base_include.py verifies every role env.j2 includes the base and never re-sets a base-owned key (keys derived from the base file).

mattermost and gitea meta/volumes.yml drop the /etc/timezone host-path bind: swarm hard-rejects a task whose bind source is missing on the node (Debian-trixie nodes have no /etc/timezone). TZ now comes from the base env. /etc/localtime bind also dropped; the container timezone follows TZ.

Non-directive comments were removed from the touched env.j2 (banners, restatements, reference URLs, narration) so the changed files pass the comment-lint; only tool directives remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:13:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ee07cd96b2 fix(ci): cap variants per compose job per role instead of globally
The nextcloud compose job bundled variants 2,3 into one runner and
needed ~332min of deploy time against the 300min step budget inside
GitHub's hard 360min job cap; raising the timeout leaves no headroom
for the measured runtime variance and starves the post-steps that
upload artifacts. Add a variant_bundle_size override in meta/tests.yml,
consumed by the variant bundle packer and the complexity report alike,
and set it to 1 for web-app-nextcloud only: eight ~190min jobs with
over 100min of headroom while every other app keeps the global
bundling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:46:23 +02:00
kevinveenbirkenbachandClaude Opus 4.8 768e210c27 fix(compose): keep compose warnings out of pull.py's parsed output
run_cmd merged stderr into stdout, so compose warnings ('variable is
not set', ...) leaked into everything pull.py parses: the yaml config
for pull targets, the config --images fallback list (turning warning
lines into phantom image names), and the pull --help probe. A project
with an unset optional variable then failed the pull step even though
every image was present locally. Capture the streams separately, parse
stdout only, and forward stderr to the log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:46:23 +02:00
Alejandro Roman cb3382f0d5 add test, fix SPOT 2026-07-15 12:53:58 +02:00
kevinveenbirkenbachandClaude Fable 5 15e707e9b2 fix(svc-runner): resolve empty entity name breaking swarm stack naming
get_entity_name('svc-runner') returned '' because the exact-match branch short-circuited on the category path 'svc-runner' before the shorter 'svc-' prefix could strip. The empty entity produced an empty swarm stack name (docker stack deploy "") and SERVICE_NAME '_' in the swarm convergence probe ('no such service: _' -> timeout). Record the exact match as a fallback and keep scanning so a shorter category prefix wins; simulated across all 254 roles, only svc-runner changes ('' -> 'runner').

Re-key roles/svc-runner/meta/services.yml from 'svc-runner' to the entity name 'runner' (services.yml is keyed by primary entity) and its 12 vars lookups, and add proxy.service_key: runner-1 so the swarm probe targets the real service (runner_runner-1) instead of the non-existent runner_runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 04:25:51 +02:00
kevinveenbirkenbachandClaude Fable 5 a786c8fcd7 fix(backup): make the DR-drill pull survive rsync version drift and stalls
The backup pull ssh forced-command wrapper exact-matched a fixed rsync --server flag string. rsync 3.4.x renegotiates that flag block (and our own --numeric-ids was added), so the match fell through to the reject echo, which polluted the rsync protocol stream and failed every pull with 'protocol incompatibility (code 2)'. Glob the version-dependent flags while pinning mode and destination, and reject shell metacharacters so the relaxation cannot be abused.

The pull script carried no ssh/rsync timeouts and the oneshot unit inherits the 24h default start timeout, so a stalled provider hung the whole job budget. Add ssh BatchMode/ConnectTimeout/ServerAlive and rsync --timeout so a dead provider fails in seconds and the retry loop reports it.

Add an integration test that routes a real rsync client through the rendered wrapper (the --numeric-ids path) and asserts the pull works and injection is rejected, so future rsync version drift fails in the suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 04:25:50 +02:00
kevinveenbirkenbachandClaude Fable 5 40e553069d refactor(structure): nest prefix-grouped sibling files into subfolders
Move files that share a delimiter-bounded name prefix within a folder (threshold 2) into a subfolder named after the prefix, dropping the prefix from the filename. Covers plugins/filter (docker, generate, has, merge, get/all), env handler subpackages, utils/roles/entity, utils/tests/swarm/write, role SQL/scripts/templates, keycloak/mailu task-includes, shared tasks/utils/load, and CI scripts. Update every import, query_file/script/include_tasks/src/lookup target, compose bind mount, and python -m path to the new locations.

Extend the prefix-grouped-files lint to threshold 2 with dir-level .nocheck and file-head nocheck opt-outs, and skip groups whose shortest member's stem equals the shared prefix. Keep flat only where a loader forbids nesting: WordPress mu-plugins and the docs/requirements namespace. Ansible filter discovery is recursive, so nested filters stay Jinja-resolvable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:00:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fc0799883f refactor(structure): move prefix-grouped files into named subfolders
Resolve every 3+-file same-prefix group flagged by the new prefix lint
by moving the files into a subfolder named after the shared prefix and
dropping the prefix from the file names, updating all references:

- utils/env/handlers/infinito_*.py -> infinito/ (dir/, package_cache/
  nested a second level); registry imports and the four handler-dir
  globs switched to iter_project_files
- utils/install/lint/ansible_*.py -> ansible/ (aliased imports)
- roles/sys-svc-compose/files/swarm_*.{py,sh} -> files/swarm/
- web-app-erpnext/web-app-zammad files/scripts/apply_*.{py,rb} -> apply/
- web-app-odoo files/sql/{select,update}-* and sql/oidc/{grant,insert,
  select,update}-* -> matching subfolders
- web-app-keycloak tasks/update/05_ldap*.yml -> 05_ldap/ (internal
  includes made role_path-absolute)
- plugins/filter/get_*.py -> plugins/filter/get/ (filter names are the
  filters() dict keys, so Jinja usage is unaffected; ansible discovers
  the one-level-deep subdir)

Ansible lookup dirs, addon dirs and .github/workflows keep their flat
layout (the filename is the public id / GitHub only discovers flat).
Unit tests mirror the new source layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:00:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 eaf53257cf test(lint): fail on sibling files sharing a name prefix
Scan git-tracked files per directory and fail when three or more
siblings share a delimiter-bounded name prefix (the longest common
token run); a shared prefix is a folder in disguise, so the group
belongs in a subfolder named after the prefix with the prefix dropped
from the file names. Test-prefixed files, dotfiles/dunders and purely
numeric ordering prefixes are exempt, and directories whose flat layout
an external contract enforces (.github/workflows, ansible lookup dirs,
addon dirs) are skipped structurally rather than via a grandfather list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:00:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ab3accba3a refactor(paths): derive var-lib subpaths everywhere from the paths SPOT
Add DIR_BACKUPS to utils.paths and INFINITO_DIR_BACKUPS /
INFINITO_DIR_SECRETS env handlers, derive the recover-console constants
(including NFS_EXPORT_STATE) from utils.paths and the nfs export SPOT,
and switch every remaining self-built /var/lib/infinito subpath - the
secrets recover targets, the swarm extras backup source, the users
token-store candidate chain, the CI recover drill, the swarm NFS
diagnostics and the playwright reports dir - to those constants. Tests
consume the env constants directly; narration comments in the touched
files are dropped per the comment lint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:15:27 +02:00
kevinveenbirkenbachandClaude Opus 4.8 07c6ad8e7c test(e2e): scale every nav click + timeout literal for onion targets
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.

Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).

Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:35:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 453ddb9170 fix(web-app-bigbluebutton): do not load BigBlueButton on an onion node
BigBlueButton is WebRTC (SRTP/UDP with ICE/STUN) which Tor cannot carry. Guard the role's core include on the resolved Keycloak domain not being an onion (alongside the existing run_once guard), disable the co-installed cloud_bbb Nextcloud app on onion at runtime, and skip the BBB Playwright specs on onion targets.

Relax the run_once schema test (test_service_core_first_task_run_once) to accept a when-list that contains the run_once guard, so a role can add extra include conditions while staying protected against duplicate execution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:50:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e77aae2afe perf(service-loader): single applications-load preload
Flatten a single list term in the service lookup so query(service, [ids]) does one applications load instead of one per id. Precompute the required service ids in sys-service-loader and gate the preload by set membership, collapsing about 30 applications loads into one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 02:47:40 +02:00
kevinveenbirkenbachandClaude Opus 4.8 75d84af636 feat(recover): unified recover CLI + swarm DR drill via CLI
Add cli.administration.recover: one uniform type/source/target command wrapping each svc-bkp-* role files/recover.py (nfs, volume, secrets, device) plus full for whole-tree recovery. Colon-encoded sources are classified by shape; device supports an on-device subpath, a generation, and remote pull over ssh.

Replace the per-host swarm DR-drill scripts (03/06/07/08_recover_*.sh) with the recover CLI in backup/base.sh. Pass the device on-device subpath relative (the CLI reads absolute segments as the restore-root) and load the applications-free .deploy.yml extras twin on the step-10 redeploy so host_vars credentials survive the -e override.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 02:47:39 +02:00
Alejandro Roman bdeab36bd4 next cloud issues with storage vs mailu 2026-07-14 01:16:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 76cc5db491 fix(deploy): tolerate first-install state in matrix reconfigure + cached read
- matrix 02_render_inventory: the mdad-state volume does not exist yet on a
  fresh install, so `container volume inspect` errored the reconfigure task
  (`no such volume`) on the row-1 baseline. Guard the marker drop behind the
  inspect succeeding; the runner bounce still runs if the container is up.
- test_coverage: read services.yml via utils.cache.files.read_text so the
  no-uncached-file-reads lint passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:34:00 +02:00
kevinveenbirkenbachandClaude Opus 4.8 21ae788a92 fix(hlth-webserver): skip canonicals of disabled services
A service entry may declare `domains: [<canonical key>, ...]`; when its
`enabled` resolves falsy those canonicals get no vhost (e.g. the seaweedfs
filer/master frontend on an onion node), so probing them can only fail
(TLSV1_UNRECOGNIZED_NAME / ERR_NAME_NOT_RESOLVED). Drop them from the
web-health expectation map. Mirrors the same suppression already added to
the CSP health probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:34:00 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c8a4320478 feat(lookup): container_hostname — kernel-safe container hostname
`lookup('container_hostname', app)` returns the app's canonical primary
domain when it fits sethostname(2)'s 63-byte cap, else the short entity
name. Onion subdomains (sub.<56-char>.onion) overflow that limit and make
`docker run` fail with "sethostname: invalid argument", killing container
init on onion nodes. Replaces the seven roles that hostname'd their
container on the raw domain (bookwyrm, bridgy-fed, confluence, jira,
postmarks, pretix, xwiki) with the encapsulated lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:34:00 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4e6ef25423 fix(test): repo-lint compliance + seaweedfs api-primary readiness
- _shared.js: wrap the boot-poll delay in resolveTimeout (raw-timeout lint)
- 02_render_inventory: docker -> container wrapper (engine-abstraction lint)
- seaweedfs: api becomes the primary canonical so the deploy readiness
  probe hits a served vhost when the frontend (filer/master) is disabled
- seaweedfs frontend: exempt from variant pinning — the onion gate is a
  hard invariant, no variant may force the frontend on a tor node
- test_coverage: add the `# nocheck: variants-coverage` exempt (same
  mechanism as bond-baseline) for such invariant flags

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:34:00 +02:00
kevinveenbirkenbachandClaude Fable 5 feccc1d916 refactor(registry): make run_after ordering bucket-agnostic
The per-bucket topological sort forbade run_after edges across loader
buckets (later bucket = hard error, earlier = silently ignored), a
leftover from before the service loader owned the preload order. The
sort is now global: an edge orders across any bucket, the name-prefix
bucket only tie-breaks among simultaneously ready roles, and cycle
detection covers the whole graph. The sort lives in the shared
run_after_topological_order helper, and the never-declared preload
flag is gone (run_after is the single ordering source).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 01:47:52 +02:00
kevinveenbirkenbachandClaude Fable 5 c3d6657311 fix(suitecrm): seed the app tree from the image instead of volume copy-up
Baking the multi-GB tree into /var/www/html made dockerd's volume
copy-on-first-use flood the shared NFS export from every swarm node in
parallel, so the stack never converged. The Dockerfile becomes a
multi-stage build (shared base with PHP/Apache setup, builder producing
the tree without node_modules, final stage with one COPY --chown layer
under /usr/src/suitecrm) and the entrypoint seeds the empty webroot
volume once, under the existing cross-replica boot lock; installed
volumes are only stamped, never overwritten. Ownership and the
writable-dir modes are baked at build, dropping the runtime chown pass
that crashed on the read-only ldap.yaml config mount. start_period
grows to 20m with the lock ceilings recoupled (live lab NFS measures
the seed at roughly a minute). App dir, seed dir and web user/group are
single-point role vars threaded through build args, entrypoint and
php.ini; the FROM lint now accepts file-internal stage references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:44:17 +02:00
kevinveenbirkenbachandClaude Fable 5 cc4945aaa5 fix(svc-bkp-nfs-2-local): stop snapshotting the shared backup root
The nfs backup rsyncs the whole export, and the shared backup root
lives inside it (every node's DIR_BACKUPS is the mounted state dir), so
each generation embedded a copy of all previous backups and grew
quadratically; the giant listings also flushed real errors out of
captured journals. The unit now passes the backup root relative to the
export as an anchored rsync exclude, and recover.py protects the live
root from --delete while never replaying embedded stale copies from old
generations (DirectoryRecovery gains rsync_extra_args for that).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:44:17 +02:00
kevinveenbirkenbachandClaude Fable 5 f7102b40d8 fix(user-backup): whitelist backup repos from the BACKUP_REPOSITORIES SPOT
The provider ssh-wrapper enumerated only the docker and nfs repo types,
so the first secrets generation made every remote pull fail: the puller
discovers types dynamically, hit backup-secrets-to-local, and the ls
fell through to 'This command is not supported' (exit 1), killing the
DR drill at step 4/9. BACKUP_REPOSITORIES in group_vars now drives both
the wrapper whitelist and its type discovery, so only whitelisted repos
are returned and only whitelisted repos are pullable. A lint pins the
SPOT against the svc-bkp-* repo names in both directions, so a fourth
repo type cannot bypass the wrapper again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:42:13 +02:00
kevinveenbirkenbachandClaude Fable 5 329897f335 refactor(tests): move the skip list into meta/tests.yml
meta/tests.yml becomes the SPOT for per-role test metadata: the
test-deploy discovery skip list moves there (top-level skip:) from the
primary service entity in meta/services.yml, next to the existing
cli.timeout. get_role_skip reads the new location; the drv lint,
--skip-mode help and unit fixtures follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:13:40 +02:00
kevinveenbirkenbachandClaude Fable 5 808efad765 fix(test-e2e-cli): per-role CLI timeout via meta/tests.yml
svc-runner's CLI test runs a full nested reinstall deploy and needs
75+ minutes, so the uniform 3600s timeout killed it deterministically
in both modes. A new role_tests lookup reads roles/<id>/meta/tests.yml
(cli.timeout, scaled through the timeout lookup) with the existing
default as fallback, keeping the short hang-leash for every other CLI
test. svc-runner gets 4h and pins the nested deploy to variant 0; the
second matrix round only redeployed cdn+dashboard and added no
assertion value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:13:40 +02:00
kevinveenbirkenbachandClaude Fable 5 3a2ef3da01 fix(nfs): soft client mounts for the github CI runtime
The swarm labs run NFS server and clients on one shared kernel; with
hard mounts a writeback stall wedges containers in D-state and takes
the whole DinD node with it (the bbb/matomo and nextcloud wedges).
dev/act already mount soft for exactly this reason; github gets the
same lab profile. Production (host) keeps hard mounts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:13:39 +02:00
kevinveenbirkenbachandClaude Fable 5 e2704ffd71 refactor(svc-bkp-volume-2-local)!: resolve baudolo args via backup_* lookups
Replace the find_dock_val_by_bkp_entr filter with three role-local
lookups that read the live merged applications config:
backup_database_containers (backup.database_routine -> service names),
backup_image (backup.no_stop_required/disabled -> exact deployed image
refs via the image lookup, swarm registry prefix and custom builds
included), and backup_hard_restart_projects
(backup.project_hard_restart -> compose project names, declared on
mailu instead of hardcoded). Pin baudolo to 3.0.0, whose --images-*
flags match .Config.Image exactly and whose hard-restart flag is
--hard-restart-projects.

The restore drill now starts all compose projects in parallel per
retry round; the previous serial alphabetical loop deadlocked when a
project's migrate service waited on a datastore project that sorted
later (mastodon -> postgres), killing the CLI test at the 3600s
timeout. Volume restore rsyncs into the mountpoint directly, dropping
the rsync helper image remnants from the test env.

BREAKING CHANGE: deploys require backup-docker-to-local 3.0.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Fable 5 4a23e0cb09 feat(services): declare locally-built images via services.<key>.custom
The custom flag moves from 33 hardcoded template kwargs into the merged
service config: 'custom: true' names the shared <entity>_custom build,
a string names a second distinct '<string>_custom' image inside one app
(nextcloud whiteboard). The image lookup reads the declaration as a
fallback, so config consumers (e.g. backup image matching) resolve the
exact deployed reference from one declaration point. nextcloud cron now
runs the nextcloud_custom build like the app service instead of the
upstream image.

A new services lint couples the declaration to reality in both
directions: custom requires a role Dockerfile, and a role Dockerfile
requires a custom declaration. The eight legacy roles deploying local
builds under upstream tags or ad-hoc image= overrides are ratcheted via
'# nocheck: dockerfile-custom'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Fable 5 e917f0860d refactor(lookups): split bare-ref image resolver out of container_image
New 'image' lookup is the SPOT for resolving <registry>/<image>:<version>
(swarm registry prefix in swarm mode, bare in compose; custom= accepts a
string base for a second distinct custom image inside one app).
container_image becomes a thin wrapper that forwards terms+kwargs and adds
the compose image: "<ref>" line, so all existing template call sites keep
their output byte-identical. The lookup-usage integration test now counts
lookup_loader.get() delegation as production usage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Fable 5 781e787550 fix(opentalk): grant the embedded seaweedfs sidecar its consumer identity
The OpenTalk controller v0.34.1 verifies its bucket via S3 ListBuckets
membership, but SeaweedFS hides every bucket from an unconfigured
identity (confirmed identical on 4.38 and 4.39), and the embedded sidecar
started with no s3.config, so the controller's signed request got an
empty bucket list and crash-looped on 'bucket missing' although the
bucket existed. The sidecar now runs s3.configure to register the
consumer's access key with Read/Write/List/Tagging on its bucket, the
same grant the central provider gets from its mounted s3.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 be1a22661a feat(lookup): central onion-aware canonical_url resolver + enforcement
lookup('canonical_url', app_id[, key][, consumer=app]) resolves the web base
URL for an app or one of its named canonicals (element/synapse/filer),
following the onion-injected merged-domain map: .onion targets resolve to
http (Tor provides transport security), everything else follows TLS_ENABLED.
The consumer= kwarg returns '' unless the consuming app has the target's
service binding enabled, replacing the recurring
"(lookup(...) if lookup('config', app, 'services.X.enabled') else '')"
template branching.

- utils/cache/domains.py: inject the node onion into dict-form
  (named-canonical) domain declarations, not just list/str forms
- keycloak redirect_uris: onion redirect URIs are http
- 13 playwright.env.j2: manual scheme branching -> canonical_url(consumer=)
- lint: forbid manual '://' concatenation and to_onion_url in role templates
- unit tests: 10 cases (primary/named x clearnet/onion, consumer gating)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:56:13 +02:00
kevinveenbirkenbachandClaude Fable 5 5af4b081df refactor(swarm-tests): move teardown and topology under utils, drop shell rescue collector
routine/08_cleanup.sh becomes utils/clean/teardown.sh and topology/
becomes utils/topology/ so routine/ holds only the sequenced flow; all
consumers (workflow steps, compose/swarm.yml env guards, relative
sources, README) follow. The shell rescue collector and its lint are
gone: utils/diagnostics/container.py covers compose and swarm
identically. extend_inventory gains the backup-host placements the DR
drill deploys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:53 +02:00
kevinveenbirkenbachandClaude Fable 5 cf32840586 feat(lint): require *_PORT vars to resolve via lookup, migrate literals
New lint rejects literal port numbers in roles/*/vars/main.yml: the port
belongs in meta/services.yml (services.<svc>.ports.<scope>.<name>) where
band governance and stack rendering read it; vars resolve it via
lookup('config', ...). Migrated the 16 remaining literals across
elasticsearch, registry-docker, erpnext, flowise, funkwhale, magento,
moodle, nextcloud, penpot and shopware.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:53 +02:00
kevinveenbirkenbachandClaude Fable 5 c51c0de9ba refactor(diagnostics): recursive workflow-level rescue snapshot, no in-play rescue blocks
Replace scripts/system/diagnostics/rescue.sh with utils/diagnostics/
container.py: one workflow call on failure captures the current level
(containers with logs+inspect, swarm services, journal, host resources,
in-play role dumps) and recurses itself through every DiD boundary via
self-copy, from the runner down to the innermost runtime (RESCUE_MAX_DEPTH).
compose and swarm now share an identical failure step; the swarm per-node
collector wrapper is gone. All 17 script-only Ansible rescue: blocks are
deleted and their bare block: wrappers inlined (plus a wordpress leftover);
the three justified blocks (postgres pg_hba, xwiki extensions, keycloak
cross-node recovery) keep their evidence tasks and now end in a fail task.
Two lints enforce the state: rescue: is forbidden without a
'# nocheck: rescue-<reason>' first-task marker, and a block without
rescue/always/when or another scoping key must be inlined.
sys-service-terminator hoists become to the include apply. Also move
routine/08_cleanup.sh to utils/clean/teardown.sh and topology/ to
utils/topology/ so routine/ holds only the sequenced flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:53 +02:00
kevinveenbirkenbachandClaude Fable 5 65c2e2721d feat(svc-bkp): service dependencies wire the backup chain together
svc-bkp-volume-2-local now declares secrets_backup (enabled+shared true):
host secrets are captured unconditionally wherever container backups run.
svc-bkp-remote-2-local declares local-2-device gated on group membership
(operator flag, no variants) so the pull host syncs to the encrypted
device when the role group is assigned; local-2-device registers as a
shared provider and gains the 01_core run-once schema. local-2-device
recover.py learns --device-target: snapshots live under <mount>/<target>,
so recovering without the configured target subpath found nothing.
secrets-2-local memory shrinks to 128m/256m (a oneshot rsync) to keep
variant stacks under the host memory budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:53 +02:00
kevinveenbirkenbachandClaude Fable 5 a3c9db91bb test(swarm): backup+restore DR drill exercising the full svc-bkp-* chain
Add routine/backup/: base.sh drives a marker seeded into the live NFS
volume and the manager secrets through the whole chain (volume+secrets+nfs
backup units -> remote-2-local pull -> local-2-device LUKS 'USB') and back
via every recover.py (device -> local root -> nfs export, docker volume and
host secrets), asserting each marker survives. Each per-host step is one
in-node routine run over docker exec. extend_inventory maps the volume/
secrets/nfs backup roles onto their hosts so the units install; write_extras
generates the pull keypair; the INFINITO_SWARM_BACKUP_* keys are added to
default.env + the passthrough handler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:53 +02:00