Commit Graph
76 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.7 191bc1229a chore(ruff): enable S (bandit) rule family — 269 → 0
Adds ``S`` (flake8-bandit) to the strict ruleset. ``PTH`` was scoped
out: it surfaced 1000+ violations with no auto-fix support (most PTH
sub-rules are warning-only because each ``str`` ↔ ``Path`` conversion
is context-sensitive). Reserved for its own dedicated commit.

Config tuning
=============
Global ``ignore`` additions with rationale:
- ``S603`` (subprocess call without ``shell=False``) and ``S607`` (start
  process with partial path) are over-eager for a project whose CLI
  invokes ``make`` / ``git`` / ``docker`` / ``ansible-playbook`` from
  PATH using list-form args. The list-form already prevents shell
  injection (Python's ``subprocess`` does not parse argv[0] as a shell
  command), and resolving every binary's absolute path in the source
  would couple the code to the host filesystem layout. The genuinely
  dangerous shell-injection patterns (``shell=True``) are still caught
  by S602 / S605.

New ``[tool.ruff.lint.per-file-ignores]`` for ``tests/**``:
- ``S101``         — ``assert`` is the unittest idiom.
- ``S102``         — ``exec`` is used to load script-under-test by file path.
- ``S105`` / ``S106`` — hardcoded credentials in fixtures / dummy creds.
- ``S108``         — ``/tmp`` paths in tempdir fixtures.
- ``S110`` / ``S112`` — ``try``/``except``/``pass``/``continue`` in cleanup helpers.

Hand-fixes (38 across 23 files)
================================
S101 × 3
  Type-narrowing asserts (``cli/deploy/development/proc.py`` —
  ``p.stdout is not None`` after ``stdout=PIPE``) and an upstream-Odoo
  invariant assert in ``roles/web-app-odoo/.../res_users.py``.

S105 × 1
  ``cli/core/app.py`` argv parser comparing ``token == "--log"`` —
  ``token`` is a CLI argv element, not a credential. Inline noqa.

S106 × 1
  ``utils/cache/users.py`` Jinja template string ``"{{ 42 |
  strong_password }}"`` rendered to a real password at runtime; not a
  literal credential. Inline noqa.

S108 × 6
  Container-internal paths bind-mounted from the host:
  - ``/tmp/infinito/ca/root-ca.crt`` and ``/tmp/infinito/bin/with-ca-trust.sh``
    in ``roles/sys-svc-compose-ca/files/compose_ca.py`` and
    ``roles/sys-svc-container/files/container.py`` (CA-injection
    contract between role and wrapper).
  - ``/tmp/cache_nginx_general/`` and ``/tmp/cache_nginx_image/`` in
    ``plugins/lookup/nginx.py`` (well-known nginx cache dirs).

S110 × 1
  ``roles/web-app-bigbluebutton/filter_plugins/compose_mods.py``: filter
  intentionally swallows YAML parse errors and falls back to the
  unmodified text. Inline noqa.

S112 × 5
  Filter plugins / inventory builders iterating over role config files
  with skip-on-malformed semantics. The pattern is intentional
  throughout (CLI tools / Ansible plugins shouldn't crash on a single
  bad role file). Per-line noqa with rationale.

S324 × 2
  ``utils/cache/base.py`` uses md5 as a fast non-cryptographic
  fingerprint for cache keying; ``utils/manager/value_generator.py``
  uses sha1 when the caller selects ``algorithm == "sha1"`` for legacy
  app compatibility. Both gained ``usedforsecurity=False`` (Python 3.9+
  hashlib API) which silences the rule structurally; comments document
  the non-security purpose.

S310 × 8
  ``urlopen`` calls to fixed HTTPS registries (``api.github.com``,
  ``hub.docker.com``, ``ghcr.io``). URL prefix is a literal; only the
  ``namespace`` / ``image`` segment varies and comes from the project's
  own ``meta/services.yml``, never user input. No ``file://`` scheme
  exposure. Inline noqa per call site.

S602 / S605 / S606 × 8
  Role-deployed scripts that legitimately use ``shell=True`` /
  ``os.system`` for pipes/``&&``, or ``os.execvp`` in list-form (safe by
  construction — no shell parses argv). Each call site gained an inline
  noqa with a per-call rationale identifying where the command string
  is sourced (Ansible-templated config, host-trusted, never untrusted
  external input).

Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files formatted
- 1621 unit tests → all pass
- Integration suite: 44 baseline failures (unchanged)

23 files changed, +97 / −38. Combined with the prior commits the
branch satisfies 21 ruff rule families end-to-end. ``PTH``
(modernise ``os.path`` to ``pathlib``) is the remaining big-impact
candidate, deferred to its own session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:57:29 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0127b2aeb9 chore(ruff): enable TID/LOG/G rule families — 11 → 0
Adds three more rule families to the strict ruleset:
- ``TID``  — flake8-tidy-imports (banned modules, banned relative imports)
- ``LOG``  — flake8-logging      (proper module-scoped loggers)
- ``G``    — flake8-logging-format (lazy ``%``-format, not f-strings, in
             ``logger.<level>(...)`` calls)

Fixes
=====
TID252 (1):  ``from ...meta.runtime import detect_runtime`` →
  ``from cli.meta.runtime import detect_runtime`` in
  ``cli/deploy/development/init.py``. Absolute imports survive package
  reorgs that would silently break a parent-relative dotted import.

LOG015 (7) + G004 (3): replaced root-logger calls
  (``logging.<level>(...)``) with module-scoped
  ``logger = logging.getLogger(__name__)`` so log records carry the
  origin module, and switched f-strings inside those calls to
  ``%``-format placeholders so the formatting cost is paid only when
  the level is enabled. Touched:
  - ``cli/fix/move_unnecessary_dependencies/yaml_io.py``
  - ``tests/integration/jinja/test_variable_definitions.py``
  - ``tests/integration/jinja/test_variable_usage.py``
  - ``utils/roles/dependency_resolver.py``

Verification
============
- ``ruff check .`` → All checks passed
- 1621 unit tests → all pass
- Suppression-related lints (project-root-import, no-direct-yaml-calls,
  noqa-only-ruff-codes) → green

5 files changed. The branch now satisfies 20 ruff rule families
end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:39:33 +02:00
kevinveenbirkenbachandClaude Opus 4.7 16e292e805 chore(ruff): enable RUF/SIM/C4/PERF/RET/PIE rule families — 364 → 0
Extends ``[tool.ruff.lint].select`` with six additional rule families:
``RUF`` (ruff-specific), ``SIM`` (flake8-simplify), ``C4``
(flake8-comprehensions), ``PERF`` (perflint), ``RET`` (flake8-return),
``PIE`` (flake8-pie). Adds ``RUF001/002/003`` to ``ignore`` because
those exist for homoglyph-attack detection, not English typography —
the project uses em/en-dashes and ``ℹ`` as a UI-hint glyph in CLI help
output deliberately.

Auto-fixed (~225, mechanical & behavior-preserving)
====================================================
- RUF005:  ``a + [b]``     → ``[*a, b]``
- C408:    ``dict(k=v)``   → ``{"k": v}``
- C420:    ``dict.fromkeys(seq)`` shape simplifications
- SIM103/SIM110/SIM118:    truthy-return / dict-key / dict-method
                           idiom simplifications
- RET504/RET505:           drop unneeded final assigns / ``else`` after
                           ``return``
- PIE810:                  combine ``str.startswith(a) or .startswith(b)``
                           into one call with a tuple
- RUF010:                  ``f"{str(x)}"`` → ``f"{x\!s}"``
- RUF100:                  removed unused ``# noqa`` directives that
                           became stale (mostly old ``# noqa: E402``
                           markers that ruff's smart E402 implementation
                           no longer flags after ``sys.path`` shims)
- RUF015 / RUF013 / RUF022 / RUF046 / RUF059: misc small wins
- I001:                    re-sorted import blocks where the new
                           imports above shifted them

Hand-fixed (~140, scripted where mechanical)
============================================
PERF401 (42): scripted AST rewrite of
  ``for x in iter: target.append(expr)`` (with optional ``if`` guard)
  →  ``target.extend(expr for x in iter [if cond])``.
  ``list.extend(genexp)`` consumes the generator immediately, so the
  rewrite is semantically equivalent to the original eager loop.

SIM117 (41): scripted AST combiner for nested ``with`` blocks where
  the inner is the only statement of the outer (38 sites). The remaining
  3 multi-line ``patch.object(...)`` chains were combined by hand using
  the parenthesised ``with (\\n A,\\n B,\\n):`` form.

SIM102 (14): scripted AST combiner for nested ``if`` (10 sites). The
  remaining 4 carry an inline ``# noqa: SIM102`` with rationale —
  combining would either change semantics (the outer body has siblings
  beyond the inner ``if``, e.g. ``cli/encrypt/inventory/__main__.py``
  where ``encrypted_value = …`` runs regardless of ``ask_confirmation``)
  or hurt readability (long literal whitelists).

RUF012 (9): annotated mutable class attributes with ``ClassVar[…]``
  and added ``from typing import ClassVar``.

SIM105 (6): ``try/except: pass`` → ``with contextlib.suppress(…):``
  with the necessary ``import contextlib`` added. Verified on
  ``utils/templating.py`` (3 occurrences in a ``finally`` block) —
  cleanup behaviour is identical.

SIM115 (4): wrapped ``tempfile.NamedTemporaryFile(..., delete=False)``
  call patterns in a ``with`` block; ``delete=False`` keeps the file
  on disk after the block, so callers' ``return f.name`` still works.

PERF402, PERF403, B007, SIM108, SIM103: 5 misc fixes (loop→list-copy,
  loop→dict-comprehension, unused-loop-var rename, ternary, return
  expression).

Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files already formatted
- 1621 unit tests → all pass
- Integration suite: 44 baseline failures, 1 skipped (unchanged)
- Sample-audited PERF401 rewrites in ``resolver.py``, ``inventory.py``,
  ``cache/files.py``, ``ressources/__main__.py``; SIM117 in
  ``test_main.py``; SIM105 in ``utils/templating.py``; SIM102 noqa
  rationales in ``cli/encrypt/inventory/__main__.py``.

182 files changed, +993 / −960. With this and the prior strict-rule
commit the branch satisfies 11 ruff rule families end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:23:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 26ef3ff504 chore(ruff): enable strict ruleset (E/F/I/B/UP, py311) — 2819 → 0
Adds ``[tool.ruff.lint]`` with ``select = ["E", "F", "I", "B", "UP"]``
and ``target-version = "py311"``. ``E501`` is ignored because
``ruff format`` already reflows what it can; the remaining long lines
are unsplittable string literals / URLs.

Auto-fixed (2753 issues, behavior-preserving):
- UP006/UP035: ``List[X]`` → ``list[X]``, ``Dict[X]`` → ``dict[X]``,
  ``from typing import …`` redirected where superseded by builtins.
- UP045: ``Optional[X]`` → ``X | None``.
- UP010/UP009/UP015: dropped Python-2-era pragmas (``from __future__
  import absolute_import, division, print_function``,
  ``# -*- coding: utf-8 -*-``), redundant ``open(..., "r")`` mode.
- UP004: dropped implicit ``class X(object):``.
- UP022/UP008/UP032: stdlib idiom modernization (``capture_output``,
  super-call simplification, etc.).
- B905: ``zip(...)`` → ``zip(..., strict=False)`` (preserves prior
  behavior; switching to ``strict=True`` is a separate semantic
  choice not made here).
- I001: import-block ordering.

Hand-fixed (66 issues):
- B904 × 54: appended ``from <bound>`` (when the enclosing ``except``
  binds a name) or ``from None`` (when it doesn't) so chained
  exceptions stay distinguishable from errors-during-exception-
  handling. Applied via a small AST-aware script that walked back to
  the enclosing ``except`` clause and matched the binding.
- B017 × 4: narrowed broad ``assertRaises(Exception)`` to the actual
  ``AnsibleError`` / ``AnsibleFilterError`` raised by the code under
  test.
- B023 × 2: bound captured loop variables via default args
  (``def _record(name, *, _path_real=path_real, ...)``) so each
  closure gets its own snapshot.
- B007 × 2: unused loop control variables renamed to ``_`` /
  rewritten to ``.values()``.
- UP007 × 2: ``Union[X, Y]`` → ``X | Y`` (plus dropping the now-unused
  ``from typing import Union``).
- B018 × 1: Odoo addon manifests are bare dict expressions at module
  level by design — suppressed with an inline ``# noqa: B018`` and
  rationale comment.
- UP031 × 1: printf-style format → f-string.

The 65 ``# noqa: …`` markers introduced by the auto-fixer all carry
real ruff codes (``B018``, ``E402``, ``F401``, etc.), so
``test_noqa_only_ruff_codes`` stays green.

Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files already formatted
- ``tests.lint.repository.{test_project_root_import,
  test_no_direct_yaml_calls, test_noqa_only_ruff_codes}`` and
  ``tests.unit.utils.annotations.test_suppress`` → 20 tests OK
- 1621 unit tests → all pass
- Integration suite: 44 failures (pre-existing baseline, none from
  this change), 1 skipped.

Going forward, the new rules block regressions on E (style), F
(pyflakes), I (import order), B (likely-bug patterns), and UP
(modernization to py311) for any new code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:57:20 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9f62f9bb0e refactor(repo): clear all project-root-import lint violations + helper inlining
Eliminates the last 102 violations of test_project_root_import by
making PROJECT_ROOT in each package's __init__.py the single source
and inlining every wrapper helper.

Migrations
==========
- ~70 files: ``Path(__file__).resolve().parents[N]`` → ``from <pkg>
  import PROJECT_ROOT`` (single-expression shape, scripted).
- ~12 files: two-statement helpers (``T = Path(__file__).resolve();
  R = T.parents[N]``) and multi-line ``os.path.abspath(os.path.join(...
  os.pardir...))`` chains rewritten by a follow-up scripted pass.
- ~20 files migrated by hand for shapes the scripts couldn't safely
  rewrite (helper functions like ``_repo_root_from_test_file``,
  embedded ``os.pardir`` chains, etc.).

Helpers removed (inlined to PROJECT_ROOT)
=========================================
- All ``def repo_root_from_here() / _project_root_from_here()`` and
  same-shape variants in cli/ (``cli/deploy/development/{down,common,
  build,deploy}.py``, ``cli/meta/categories/invokable/command.py``,
  ``cli/meta/applications/resolution/{combined/repo_paths,
  dependencies/__main__,run_after/__main__}.py``).
- All zero-arg ``def <name>() -> Path: return PROJECT_ROOT`` helpers
  (utils/invokable.py, utils/roles/resource_validation.py, cli/meta/
  runtime/__init__.py, cli/meta/callorder/__main__.py, plus a dozen
  test files).
- 4 ``PROJECT_ROOT = PROJECT_ROOT`` self-assignments removed from
  test files where the class attribute became dead after migration.

Test-mock updates
=================
``patch.object(repo_paths, 'repo_root_from_here', return_value=ROOT)``
→ ``patch.object(repo_paths, 'PROJECT_ROOT', ROOT)`` (27 patches across
8 unit-test files); same shape for ``patch('utils.invokable._repo_root',
return_value=…)``.

Suppressions documented
=======================
5 files marked ``# nocheck: project-root-import`` for the catalog's
documented exemptions:
- 2 lints walking from a discovered glob match (<role>/templates/...)
  up to its role dir, NOT the repo root.
- ``cli/validate/inventory/__main__.py`` invoked as standalone
  subprocess (relative imports unavailable).
- ``plugins/filter/value_generator.py`` loaded by Ansible outside the
  project package (relative imports broken).
- ``roles/test-e2e-playwright/filter_plugins/discover_playwright_roles.py``.

Bugs surfaced and fixed in the same commit
==========================================
The migration uncovered two latent role-content violations that were
silently masked by buggy ``_repo_root()`` helpers in two integration
tests (the helpers walked upward looking for marker dirs and returned
``tests/integration/`` instead of the repo root, so the tests scanned
nothing under ``roles/`` for years):
- roles/web-app-minio: extracted 6-task OIDC block into 02_oidc.yml +
  ``include_tasks`` (the documented "Good" pattern from
  test_block_size; parsing cost is paid only when OIDC is enabled).
- roles/web-app-bluesky/tasks/01_core.yml: collapsed 3 sequential
  ``when: BLUESKY_WEB_ENABLED | bool`` tasks into one ``block:`` with
  the guard at block level (4 → 2 occurrences, under threshold).

Two semantic-bug fixes carried alongside
========================================
- cli/core/app.py: ``cli_dir`` was previously ``parents[1]`` (= cli/),
  not the repo root; rewritten to ``PROJECT_ROOT / "cli"`` to match
  the variable's intent.
- cli/make/__main__.py: pre-existing bug — ``repo_root`` was actually
  the ``cli/`` directory (so ``cli/Makefile`` was the search target,
  which never existed). Now correctly uses PROJECT_ROOT.
- scripts/lint/sort_claude_settings.sh: invokes utils/claude/
  settings_sort.py as a module (``python -m``) so its
  ``from . import PROJECT_ROOT`` resolves.

Verification
============
- ``ruff check .`` → clean
- ``tests.lint.repository.test_project_root_import`` → OK (102 → 0)
- 1621 unit tests → all pass
- Integration suite: 44 failures (baseline, pre-existing, none from
  this commit), 1 skipped

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:38:41 +02:00
kevinveenbirkenbachandClaude Opus 4.7 eb45f0ffaf feat(cache): inner-build override + TTL env-var + SPOT docs
Extend the local cache stack with three independent capabilities:

1. Inner-`dockerd` build coverage for HTTP-only package mirrors.
   `roles/sys-svc-compose/files/compose.py` (the runner's `compose`
   wrapper) auto-generates a transient `compose.cache.override.yml`
   per app when `INFINITO_PACKAGE_CACHE_FRONTEND_IP` is exported. The
   override pins `build.extra_hosts` for `deb.debian.org`,
   `archive.ubuntu.com`, and `security.ubuntu.com` to the frontend.
   `pull.py` now delegates to the wrapper so build/pull operations see
   the same `-f` set the wrapper auto-detects. nginx in the frontend
   gains a port-80 listener with path rewrites for those hostnames,
   and `package.sh` registers two new Nexus proxies
   (`apt-debian-security`, `apt-ubuntu-security`) for the security
   suites whose upstream URL prefix differs from the main archive.

   Alpine is intentionally excluded from the inner-build extra_hosts:
   3.18+ defaults to HTTPS, and inner-`dockerd` build containers do
   not import the frontend CA. Routing apk through the frontend would
   trip TLS verification.

2. General cache freshness env-var. `scripts/meta/env/cache/package.sh`
   exports `INFINITO_PACKAGE_CACHE_MAX_AGE_MIN` (default `129600`,
   i.e. 90 days). The bootstrap helper consumes it for every Nexus
   proxy's `contentMaxAge`, `metadataMaxAge`, and
   `negativeCache.timeToLive`, replacing the previous 1440-minute
   literal. Lower the value when chasing fast-moving upstreams.

3. SPOT cache documentation at `docs/contributing/environment/cache.md`.
   `compose.yml.md`, `variables.md`, the per-service READMEs, and
   `requirements/012-...` now link to it instead of restating the
   stack's behaviour. Net documentation lines drop substantially while
   the cache page becomes the single user-facing reference.

Also: code-comment cleanup per `docs/contributing/documentation.md`
guidelines across all touched .py / .sh / .yml files. Multi-paragraph
docstrings, banner separators, and restate-the-obvious comments are
removed; one-line WHY comments remain where the rationale is not
already visible in the code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 01:08:41 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1dbc44af2c refactor(cache): SPOT for compose-file flags + override env vars
Move the `docker compose -f ...` flag list and the cache-override
env-var setup into single helpers in `cli/deploy/development/common.py`:

  * `compose_file_args()` — decides whether to layer
    `compose/cache.override.yml` on top of `compose.yml`.
  * `cache_env_overrides()` — produces the env-var dict the
    override file requires strictly via `${VAR:?…}`.

Both `cli.deploy.development.compose` (up flow) and
`cli.deploy.development.down` (down flow) now call these helpers, so
up and down resolve to the same compose-file set and the same env
vars. Previously the cache env-var setup was duplicated inline in
compose.py and missing entirely from down.py, which made `make down`
fail under the `cache` profile with a confusing "required variable
missing" error before even reaching the orphan-cleanup step.

Single SPOT discipline: the host CA directory default lives only in
`scripts/meta/env/cache/package.sh` (sourced via BASH_ENV by every
Makefile recipe). `cache_env_overrides()` refuses to fall back to a
duplicate literal — a missing env var raises SystemExit pointing at
the env script. Tests that bypass the env layer set the var
explicitly so the SPOT-strict guard does not raise.

Containerize cert-gen: the original
`scripts/docker/cache/package-frontend-certs.sh` ran `mkdir`/`openssl`
host-side, which fails when `/var/cache/infinito` is not writable from
the invoking shell (sandboxed tooling, non-root operators). Move the
openssl logic into `compose/package-cache-frontend/cert-gen.sh` and
have the host wrapper docker-run it inside a throw-away alpine
container so the docker daemon (root) creates the bind-mount source
paths. Same pattern as how rpardini's registry-cache generates its CA
in-container.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:47:34 +02:00
kevinveenbirkenbachandClaude Opus 4.7 334c915598 feat(cache): transparent TLS frontend + override-only gating
Add a TLS-terminating reverse proxy in front of the Sonatype Nexus 3 OSS
pull-through cache so the `infinito` runner can hit upstream
package-manager URLs (pypi.org, registry.npmjs.org, rubygems.org,
repo.packagist.org, proxy.golang.org, dl-cdn.alpinelinux.org,
files.pythonhosted.org, raw.githubusercontent.com, codeload.github.com)
unchanged and still flow through Nexus.

Mechanism:
  * scripts/docker/cache/package-frontend-certs.sh — host-side helper,
    idempotently generates a dedicated CA + per-hostname leaf certs.
  * compose/package-cache-frontend/upstreams.conf — nginx server-blocks
    that match SNI per hostname and reverse-proxy onto the matching
    Nexus repo path.
  * compose/cache.override.yml `infinito.extra_hosts` — DNS-hijack so
    the runner's clients resolve each upstream onto the frontend's
    static IP.
  * scripts/docker/cache/package-frontend-ca.sh — runner-side helper,
    drops the CA into the system trust store post-startup so apt/pip/
    curl/etc. accept TLS against the hijacked hostnames.
  * cli/deploy/development/compose.py — orchestrates cert gen before
    compose-up, CA install after health.

Scope today: runner-only. App-service Dockerfile builds executed by
the inner dockerd are NOT routed through the frontend; that requires
build-time CA injection and is deferred.

Also extends scripts/docker/cache/package.sh with five additional
Nexus proxies registered for transparent use: gem-proxy, go-proxy,
yum-rocky, yum-fedora, raw-packagist, raw-alpine, raw-codeload-github.

Refactor: move all cache services + runner-side cache mounts/depends_on/
extra_hosts from compose.yml into compose/cache.override.yml. Base
compose.yml now describes only the bare CI/runtime stack (coredns +
infinito). The override file is the single gate for the cache stack;
`profiles: ["cache"]` attributes and `--profile cache` flag emission
were redundant once file inclusion controls existence and removed
accordingly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 18:26:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fe54854fe5 refactor(resolver): variant-aware combined resolver and per-round include
Single SPOT for matrix-deploy topology
=======================================

Before this change two paths derived the deploy plan from different
sources and could disagree:

 * Group inclusion (`--include` / `group_names`): driven by
   `cli.meta.applications.resolution.combined.CombinedResolver`,
   which read each role's `meta/services.yml` raw and ignored
   `meta/variants.yml` entirely. A variant that flipped
   `services.<X>.enabled` to a literal `true` did NOT pull `<X>`'s
   provider role into the deploy plan.

 * Inventory bake (`host_vars/applications.<app>.*`): driven by the
   variant-aware payload merge in `cli.deploy.development.inventory`,
   so the role saw the variant-merged services map at deploy time.

The split surfaced concretely for `web-app-wordpress` variant 0
(`services.discourse.enabled: true` in `meta/variants.yml`): the
inventory baked `discourse.enabled = true`, the wp-discourse plugin
task gate fired, and the install loop tried `docker exec discourse`
against a container the variant-blind resolver had never pulled.

This commit collapses both consumers onto the same per-round,
variant-merged services map. The variant-aware resolver (Phase 1+2)
is the additive plumbing, but the deploy/init wiring (Phase 3-5) is
what actually changes behaviour at runtime.

Variant-aware resolver (Phase 1+2)
----------------------------------

`role_introspection.load_shared_service_roles_for_app` accepts a
`services_override` kwarg; `CombinedResolver.__init__` accepts a
`{role_name: services_dict}` map and routes it through the loader.
A `CombinedResolver` instance is round-specific when overrides are
supplied — its per-role edge cache is variant-resolved, do NOT reuse
across rounds. The combined-resolver CLI gains
`--services-overrides <path>`.

`cli.deploy.development.deps.resolve_run_after` threads the
in-container path of the overrides JSON into the resolver
invocation.

Per-round include planner (Phase 3)
-----------------------------------

`cli.deploy.development.inventory.plan_dev_inventory_matrix` now
takes `primary_apps` (just the user-named apps) instead of a
pre-resolved `include` list and returns a 4-tuple per round
`(round_index, inv_dir, round_variants, include_R)`. Per round it:

 1. Computes `round_variants` clamped to each primary's variant
    count.
 2. Builds variant-merged services overrides for every role with a
    `meta/variants.yml`, picking that role's variant for the round
    (clamped) — so transitive deps with their own variants follow
    the round.
 3. Runs `CombinedResolver(services_overrides=...)` per primary and
    unions the result with the primary apps in user order.
 4. Extends `round_variants` with the discovered deps' clamped
    variant indices so the deploy step's per-round purge logic stays
    accurate.

`build_dev_inventory_matrix` keeps its name but signature changes:
it now takes `primary_apps` and an optional `include_filter` for
SERVICES_DISABLED-style consumer filters.

Init / deploy wiring (Phase 4-5)
--------------------------------

`init.py` and `deploy.py` consume the per-round `include_R`
directly, drop the old `resolve_deploy_ids_for_apps` pre-resolution
step, and apply the SERVICES_DISABLED provider filter on the
include set so the inventory `--include` flag and the final ansible
deploy ids stay in lockstep.

Behaviour invariance for non-variant roles
------------------------------------------

Apps with no `meta/variants.yml`, or without a `services.<X>.enabled`
override on any variant, behave identically to before:

 * The override map for those roles is empty — resolver falls
   through to the disk-only path.
 * Per-round `include_R` collapses to the same set every round.
 * Inventory bake outputs the same `applications.<app>.*` shape.

The behaviour change is scoped to roles that legitimately use
variants to flip topology. In the current repo only
`web-app-wordpress` is affected (variant 0 pulls
`web-app-discourse` in; variant 1 leaves it out, exercising the
multisite-without-discourse path).

Test surface
------------

 * `tests/unit/cli/deploy/development/test_inventory.py` updated
   for the new planner signature, mocking
   `_resolve_round_include` / `_build_services_overrides_for_round`
   so the planner's tuple shape is asserted independently of the
   live resolver.
 * `tests/unit/cli/deploy/development/test_matrix_deploy.py` adds a
   `_entry()` helper that defaults `include` to the keys of
   `round_variants`, so existing fixtures stay terse — round 0 of
   the deploy loop deploys whatever is in `include_R`, and the
   variant-aware planner naturally emits the same set.
 * `tests/unit/utils/cache/test_data.py` pins the planner contract
   (`primary_apps=`, 4-tuple plan entries with `include_R`).
 * `tests/utils/__init__.py` added so the
   `tests.lint.filesystem.test_init_files` lint test passes
   end-to-end (the directory existed in the tree without an
   __init__.py and tripped the lint).

Verification
------------

 * `make test` green (162 tests, 1 skipped) on the merged tree.
 * `make deploy-fresh-kept-apps APPS="web-app-wordpress"
   FULL_CYCLE=true SERVICES_DISABLED="matomo,email"` end-to-end:
   round 0 (variant 0, single-site + Discourse integration) and
   round 1 (variant 1, multisite) each ran PASS 1 (sync) and PASS 2
   (async), all four passes with `failed=0 rescued=0`. Round 0's
   per-round include contained `web-app-discourse`; round 1's did
   not — exactly the variant-driven topology the resolver is
   supposed to produce.

The unrelated Joomla CLI install race (CI run 25079786137 `Deploy
web-app-joomla`) is parked separately as infinito-nexus/core#209;
its candidate fix is in that issue body, not in this commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:28:12 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b135953558 fix(deploy/dev): align matrix planning with init (use deploy_ids, not primary_app_ids)
`cli.deploy.development.init` plans matrix rounds with the FULL include
set (`deploy_ids` = primary apps + transitive run_after deps); deploy
was planning with `primary_app_ids` (just what the user named). When
the only multi-variant role was a transitive dep, init created
`<dir>-0` / `<dir>-1` while deploy looked at `<dir>` and the inventory
file silently went missing. Fix passes `deploy_ids` to
`plan_dev_inventory_matrix` so both stages agree on the round count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 03:11:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b34131ca1d feat(package-cache): add Sonatype Nexus 3 OSS pull-through cache (req-012)
Brings up the package-cache compose service alongside the existing
registry-cache, gated by the same `cache` profile. Bootstrap helper
(scripts/docker/cache/package.sh) auto-rotates the admin password from
the well-known default, accepts the Sonatype CE EULA (3.71+ blocks
artifact downloads with HTTP 403 until /v1/system/eula is POSTed —
disclaimer must be sent verbatim with its unicode quotes), enables
anonymous read, creates one default blobstore, and registers the MVP
proxy set: apt-debian, apt-ubuntu, pypi-proxy, npm-proxy, helm-bitnami,
raw-githubusercontent. The runner gets pip.conf / .npmrc /
sources.list snippets bind-mounted in (each defaulting to /dev/null
when the cache profile is inactive) so the deploy is byte-identical
with cache off.

Cache plumbing:
- scripts/meta/env/cache/{registry,package}.sh: per-cache env defaults,
  hostname-derived admin password seed (uses `hostname` command, not
  $HOSTNAME shell var, so the value is identical across interactive
  shells, non-interactive subshells, and Python subprocesses).
- scripts/docker/cache/{registry-ca,package}.sh: container-side
  bootstrap helpers, both moved under scripts/docker/cache/.
- scripts/system/cache/clean.sh + `make cache-clean`: wipes both
  /var/cache/infinito/core/cache/{registry,package} dirs and stops
  the cache containers.
- cli/deploy/development/compose.py: Profile gate exports the three
  new INFINITO_PACKAGE_CACHE_* client-snippet paths and runs the
  bootstrap helper after the package-cache reports healthy.

Compose service uses sonatype/nexus3:3.91.1-ubi pinned, runs as
root (UBI image refuses to chown a fresh bind-mount), and forces
NEXUS_SECURITY_RANDOMPASSWORD=false so the bootstrap can log in with
admin/admin123 before rotating; the random-password mode otherwise
auto-completes onboarding silently and leaves no readable secret on
disk for an automated bootstrap to pick up.

Tests: tests/unit/cli/deploy/development/test_compose.py grows two
new assertions (cache services listed in `up` args when local; build
tests pin CI=true to retain ci-only assertions).

Closes req-012.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 03:11:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 94936f198b feat(compose): gate registry-cache to local dev only via Profile class
Move the registry-cache pull-through proxy out of the always-on `ci`
profile into its own `cache` profile, and select it based on the runtime
environment instead of statically.

Why
   The proxy only earns its keep when image pulls amortize across many
   runs on the same host. That holds on developer machines (cross-run
   image dedup, persistent host disk) but not on GitHub-hosted runners
   (fresh disk per job), where the proxy startup overhead is net loss
   without the corresponding payoff.

How
* Compose: split `registry-cache` into its own `cache` profile;
  `infinito.depends_on.registry-cache` gets `required: false` so Compose
  does not auto-start the proxy via the dependency when the profile is
  inactive; the systemd drop-in bind-mount source is gated by the new
  `INFINITO_REGISTRY_CACHE_PROXY_CONF` env var with `/dev/null` default,
  so dockerd loads an empty drop-in (no `HTTP_PROXY`, no `ExecStartPre`)
  when the proxy is not running and pulls images directly.
* New `cli/deploy/development/profile.py` `Profile` class is the SPOT
  for which compose profiles activate. KISS + SRP: `is_ci()` reads the
  three CI signals (`GITHUB_ACTIONS`, `RUNNING_ON_GITHUB`, `CI`),
  `registry_cache_active()` is its strict inverse, and `args()` returns
  the matching `--profile ...` flag list.
* `Compose` and `down.py` consume the same `Profile` so up/down cannot
  drift on which services are part of the stack.
* Documentation under `compose/registry-cache/README.md`,
  `docs/contributing/artefact/files/compose.yml.md` and
  `docs/contributing/environment/variables.md` describes the new toggle
  and the `INFINITO_REGISTRY_CACHE_PROXY_CONF` env var. Pre-existing
  documentation.md violations in those files (one SPOT self-label, one
  em dash, two path-as-link-text instances) corrected in the same pass
  per the fix-nearby rule.
* Tests: 14 new `Profile` unit tests in `test_profile.py` plus two
  scenario tests in `test_compose.py` that lock down the
  `--profile ci` (CI runner) vs `--profile ci --profile cache` (local)
  argument shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:25:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1dd8d3f5c0 feat(compose): add pull-through registry-cache + reorg compose/ subdirs
Introduces a `registry-cache` service (rpardini/docker-registry-proxy) in
the `ci` profile that MITM-caches every outbound docker registry pull
made by `infinito`'s inner dockerd, drastically cutting bandwidth on
re-deploys, parallel CI shards, and local fresh-purges.

Wiring (all bind-mounted via compose.yml, no Dockerfile or entry.sh
staging, so edits hot-reload on next container start):
- CA bundle at /opt/registry-cache-ca (read-only),
- systemd drop-in at /etc/systemd/system/docker.service.d/registry-cache-proxy.conf,
- registry-cache-ca.sh at /usr/local/bin/registry-cache-ca.sh.

The drop-in registers registry-cache-ca.sh as ExecStartPre. Compose
gates `infinito` on `registry-cache` `service_healthy` so the CA is on
disk before dockerd boots; the install script supports both Debian
(`update-ca-certificates`) and Arch/Fedora (`update-ca-trust extract`)
trust stores, detected by command availability.

Also reorganizes compose/ into per-service subdirectories
(compose/coredns/, compose/registry-cache/) with README.md per directory
following the documentation guidelines, adds scripts/docker/README.md,
and nests the registry-cache host paths under /tmp/infinito/core/
(mirror/ for blobs, ca/ for the MITM CA bundle).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:14:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ed04dad5d5 refactor(meta): land req-008/009/010 role-meta layout/networks/ports/run_after migration
Single atomic landing for three back-to-back role-meta refactor requirements
(see docs/requirements/008-role-meta-layout.md, 009-per-role-networks-and-ports.md,
010-role-meta-runafter-lifecycle-migration.md).

req-008 — role meta layout
  * Move every role's metadata under meta/<topic>.yml with file-root
    convention (no wrapping key matching the basename):
      - schema/main.yml + config/main.yml.credentials → meta/schema.yml
      - users/main.yml                                → meta/users.yml
      - config/main.yml.server                        → meta/server.yml
      - config/main.yml.rbac                          → meta/rbac.yml
      - config/main.yml.compose.services + every other top-level key
        of config/main.yml inlined under <entity>.<key>
                                                      → meta/services.yml
      - config/main.yml.compose.volumes               → meta/volumes.yml
  * Drop the legacy schema/, users/, config/ directories from every role.
  * meta/schema.yml: support nested credential keys, optional `default:`
    field (Jinja literal, not rendered/validated/regenerated), implicit
    `algorithm: plain`.
  * Materialised paths drop the `compose.` prefix:
      compose.services.<e>.<…> → services.<e>.<…>
      compose.volumes.<k>      → volumes.<k>
      <former-top-level>.<…>   → services.<entity_name>.<former-top-level>.<…>
  * Update every Python consumer (utils/cache, utils/manager, utils/applications,
    plugins/lookup, plugins/filter, cli/*) plus every Jinja template, vars/*.yml
    and task file to the new paths.
  * No legacy fallback. Repository-wide grep for `compose.services.` /
    `compose.volumes.` / `schema/main.yml` / `users/main.yml` / `config/main.yml`
    is empty outside docs/requirements/ and tasks/utils/migrate_meta_layout.py.
  * Add tests/lint/repository/test_role_meta_layout.py guarding against the
    old paths returning, and an explicit lint guard that meta/variants.yml
    files do not carry a `compose:` wrapper.

req-009 — per-role networks/ports
  * Move group_vars/all/08_networks.yml.defaults_networks.local.<role>.{subnet,
    dns_resolver} into roles/<role>/meta/server.yml.networks.local.{subnet,
    dns_resolver}.
  * Delete group_vars/all/09_ports.yml entirely; ports now live on the
    service entity that exposes them at
    roles/<role>/meta/services.yml.<entity>.ports.{inter,local,public}.
  * Rename + int-cast every former top-level `services.<entity>.port`
    field to `services.<entity>.ports.inter` (single int).
  * Always-category-keyed maps for ports.local / ports.public; relay
    spans use {start, end} integers.
  * Add PORT_BANDS map to group_vars/all/08_networks.yml — single source
    of truth for the per-<scope>.<category> band ranges, consumed by both
    the new CLI helpers and the lint test.
  * New CLI helpers (cli meta ports suggest, cli meta networks suggest)
    walk the live role tree gap-first then increment-fallback, with
    deterministic output and non-zero exit on capacity overflow.
  * cli create role consumes both helpers when scaffolding a new role.
  * Lint extensions: bare-int port shape rejection, host-bound port
    collision check (single ints + relay span integers, inter excluded),
    band-membership check with documented allow-list for the legacy
    web-app-bigbluebutton.bigbluebutton.local.http: 48087 entry.

req-010 — run_after / lifecycle migration
  * Move galaxy_info.run_after and galaxy_info.lifecycle out of
    meta/main.yml into meta/services.yml.<primary_entity>.{run_after,
    lifecycle}; <primary_entity> is the value returned by
    get_entity_name(role_name).
  * For multi-entity roles whose primary entity has no compose service
    (web-app-bluesky → bluesky, web-app-matrix → matrix, desk/dev/drv/
    gen roles), create a dedicated metadata-only top-level entry under
    <primary_entity>.
  * Introduce utils/roles/meta_lookup.py with get_role_run_after /
    get_role_lifecycle that resolve <primary_entity> once and read from
    meta/services.yml; degrade gracefully when meta/services.yml is
    absent OR the field is absent; raise MetaServicesShapeError when the
    file exists but is malformed.
  * Update every consumer (utils/service_registry, utils/invokable,
    cli/meta/applications/resolution/run_after/__main__, cli/meta/roles/
    lifecycle_filter/__main__, cli/meta/applications/resolution/combined,
    cli/build/{tree,graph,role_include}, cli/deploy/development/{common,
    init,deps,deploy}, plugins/filter/canonical_domains_map, etc.) to
    use the helper.
  * Lint guards: no run_after/lifecycle in meta/main.yml; lifecycle
    values restricted to {planned,pre-alpha,alpha,beta,stable,deprecated};
    run_after/lifecycle never on a non-primary entity; empty run_after
    lists rejected.

Also folded into this change set
  * services_disabler now writes `services.<svc>.{enabled,shared}` into
    host_vars instead of the old `compose.services.<svc>.{...}` path; the
    discover/find pair walks meta/services.yml and only treats entries
    carrying enabled/shared/provides as provider declarations (so
    metadata-only entries from req-010 do not create spurious provider
    rows).
  * service_registry skips `run_after` deps whose target was filtered
    out via SERVICES_DISABLED instead of failing the whole load.
  * cli/mirror/resolver and cli/create/inventory/mirror_overrides emit/
    consume the new applications.<role>.services.<svc>.image shape
    (mirror.md updated to match).
  * utils/domains/list.build_applications_from_roles now reads
    meta/server.yml directly (it had been silently returning {} since
    config/main.yml stopped existing).
  * utils/roles/resource_validation.filter_roles_by_min_storage reads
    meta/services.yml.<entity>.min_storage instead of the legacy
    compose.services path.
  * web-app-bluesky pre-clones the social-app build context BEFORE
    sys-svc-compose so the build context is on disk by the time
    sys-stk-front-proxy/tasks/main.yml flushes the docker build handler.
  * 4 meta/variants.yml files (svc-ai-ollama, web-app-phpmyadmin,
    web-app-wordpress, web-app-keycloak) lose their `compose: services:`
    wrapper so deep-merge over meta/services.yml lands on the new
    file-root shape.

Validation
  * Unit tests:        1571 passed.
  * Lint tests:        50 passed.
  * Integration tests: 161 passed (1 skipped).
  * Full-cycle fresh-purged-apps deploy succeeded for the 8-app validation
    set: web-app-{nextcloud,wordpress,yourls,bigbluebutton,bluesky,gitea,
    discourse,mattermost}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:58:26 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3f2021b07d refactor: collapse INFINITO_DISTRO/INFINITO_CONTAINER to a single SPOT and align all consumers
Follow-up to d002796a5. The formula `infinito_nexus_${INFINITO_DISTRO}` is
now defined in exactly one place — `scripts/meta/env/defaults.sh:18`,
exported outside the load-once guard so it tracks INFINITO_DISTRO across
matrix iterations (caller re-sources defaults.sh after distro changes).
Every other site reads INFINITO_CONTAINER strictly from env; nothing
re-derives the formula, no lazy/colon-dash fallbacks, no per-call defaults.

Production code aligned:

- cli/deploy/development/common.py: add resolve_container() (strict env
  reader, raises SystemExit on unset). make_compose() calls it as an
  early-fail guard so a missing defaults.sh source surfaces here instead
  of as a confusing compose.yml interpolation error later.
- cli/deploy/development/deploy.py: stop re-deriving from compose.distro;
  use resolve_container().
- compose.yml: container_name reads ${INFINITO_CONTAINER:?...} strictly,
  so callers that bypass defaults.sh fail fast with a clear hint.
- scripts/meta/resolve/image/local.sh: drop ${INFINITO_DISTRO:-debian}
  fallback; require INFINITO_DISTRO via :? error.
- scripts/tests/deploy/ci/dedicated.sh: drop local _container fallback.
- scripts/tests/deploy/local/deploy/fresh-kept-app.sh: drop redundant
  ${INFINITO_DISTRO:-debian} reassignment after sourcing env/all.sh
  (defaults.sh already defaults the value).
- scripts/tests/deploy/local/deploy/fresh-purged-app.sh: drop
  target_container fallback; INFINITO_CONTAINER is already in env.
- scripts/tests/deploy/local/exec/container.sh: drop the lazy
  ${INFINITO_CONTAINER:=infinito_nexus_...} default; defaults.sh source
  in env/all.sh already guarantees the value.
- scripts/tests/dns.sh: source defaults.sh so callers only need to set
  INFINITO_DISTRO (the implicit dependency was previously a footgun).
- scripts/system/tls/trust/{wsl2,linux}.sh: same — source defaults.sh,
  drop the ${INFINITO_CONTAINER:-infinito_nexus_debian} fallback in
  linux.sh.
- .github/workflows/test-dns.yml: drop redundant
  INFINITO_CONTAINER: infinito_nexus_${{ matrix.distro }} from per-step
  env blocks (3 sites). The env now flows from INFINITO_DISTRO via the
  defaults.sh sourced inside the scripts.
- tests/unit/cli/deploy/development/test_matrix_deploy.py: replace the
  module-level os.environ.setdefault('INFINITO_CONTAINER', 'infinito_nexus_arch')
  shim — which itself encoded the formula — with a module-level patch of
  resolve_container that returns an opaque test-fixture string. Drop the
  now-unused distro="arch" field from the _args() Namespace fixture
  (deploy.handler stopped consuming it after the --distro arg removal).

Documentation aligned:

- docs/contributing/environment/variables.md: rewrite the INFINITO_CONTAINER
  row to spell out the SPOT contract — set automatically by defaults.sh,
  never re-derived elsewhere, callers re-source defaults.sh after
  changing INFINITO_DISTRO mid-process.
- docs/contributing/artefact/files/compose.yml.md: replace the
  INFINITO_DISTRO row (which described the now-removed
  `infinito_nexus_${INFINITO_DISTRO:-debian}` interpolation) with an
  INFINITO_CONTAINER row noting the strict ${INFINITO_CONTAINER:?...} read.

Verified:
  - Formula derivation: 1 production hit (the SPOT in defaults.sh:18).
  - INFINITO_CONTAINER fallback patterns (:-/:=): 0.
  - --distro CLI arg refs: 0 (one comment explaining the removal).
  - make test green: 1553 unit + 168 integration tests, no skipped suites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:21:35 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3e31630d58 fix(ci/deploy)+refactor(cli/deploy/development): single-SPOT INFINITO_DISTRO/INFINITO_CONTAINER
Two related improvements anchored on the same principle: the env var
INFINITO_DISTRO is the single source of truth for the development distro,
and INFINITO_CONTAINER is purely derived from it.

1. Fix the matrix-deploy purge failure (CI run 24942172018, job 73038526513,
   web-app-wordpress @ fedora):
   scripts/meta/env/defaults.sh eager-exported INFINITO_CONTAINER once
   inside its load-once guard; ci/all.sh's iteration loop re-exported
   INFINITO_DISTRO per round but the cached INFINITO_CONTAINER stayed at
   the initial-default infinito_nexus_debian, so _purge_app_entities()
   between rounds tried to docker exec into a container from the wrong
   distro and failed with "No such container".

   Move the INFINITO_CONTAINER derivation OUTSIDE the load-once guard so
   every re-source picks up a CONTAINER name in lock-step with the current
   INFINITO_DISTRO. ci/all.sh now re-sources defaults.sh after each
   export INFINITO_DISTRO=\$distro to trigger that re-derivation. defaults.sh
   stays the single spot that owns the formula.

2. Retire the --distro CLI arg from cli.deploy.development {up,down,stop,
   restart,init,deploy,exec,logs,build}. The arg duplicated INFINITO_DISTRO
   (its default already read from the env), creating a hidden second source
   of truth that drifted from the env across matrix iterations. Replace it
   with cli.deploy.development.common.resolve_distro() which reads
   INFINITO_DISTRO strictly (raises SystemExit on unset/invalid), and
   simplify make_compose() to take no args. All bash callers
   (scripts/tests/deploy/{ci,local}/**, Makefile restart target) drop the
   redundant --distro \"\${INFINITO_DISTRO}\" pass-through.

Verified: make test passes (1553 unit + 168 integration). The mechanism of
the WP fix is verified by a synthetic distro-cycling test of defaults.sh
re-derivation; the original CI multi-distro repro can only be confirmed
end-to-end by the next CI push (local single-distro deploys do not exercise
the stale-env path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:21:34 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9c35909752 refactor(utils/cache): split data.py into base/applications/users/domains
The 934-line `utils/cache/data.py` monolith mixed cross-cutting helpers
(constants, yaml-loaders, deep-merge, signatures, render machinery)
with three independent domain caches (applications, users, domains).
Per the KISS/SPOT/DRY guidelines, split it along domain boundaries so
each file has a single, well-scoped responsibility:

  utils/cache/__init__.py     — package; owns _reset_cache_for_tests()
                                orchestrator
  utils/cache/base.py         — cross-cutting helpers: constants,
                                yaml-loaders, _deep_merge, cache-key +
                                content-fingerprint signatures, the
                                cross-domain re-entry guard, and the
                                templar-render machinery (the only
                                ansible-coupled symbol, lazy-imports
                                its dependency)
  utils/cache/applications.py — variants + defaults +
                                get_merged_applications. Strictly
                                ansible-free at import time so the
                                GitHub Actions runner-host CLI path
                                (cli.deploy.development.init →
                                plan_dev_inventory_matrix →
                                get_variants) keeps working without
                                ansible installed
  utils/cache/users.py        — user defs, token store hydration,
                                alias materialization,
                                get_user_defaults, get_merged_users
  utils/cache/domains.py      — get_merged_domains, derived from the
                                merged applications view
  utils/cache/data.py         — DELETED. Façade is gone; the package
                                exports nothing the domain modules
                                don't already

Backward-compat: the facade was deleted (per operator request) and all
36+ consumers — 18 plugins, 2 CLI entry points, 9 test files, 4 doc
files, plus a role-internal lookup_plugin — were rewired to import
directly from the owning domain module. `_reset_cache_for_tests` is
now exposed at `from utils.cache import _reset_cache_for_tests`.

Tests added (per-module, focused):
  tests/unit/utils/cache/test_base.py            (31 cases)
  tests/unit/utils/cache/test_applications.py    ( 9 cases)
  tests/unit/utils/cache/test_users_module.py    (21 cases; suffix
                                                  disambiguates from
                                                  the existing
                                                  tests/unit/plugins/
                                                  lookup/test_users.py)
  tests/unit/utils/cache/test_domains.py         ( 6 cases)

The ansible-block invariants from CI runs 24934007615 and 24935979190
are now pinned via subprocess isolation: each ansible-less import test
spawns a fresh `python3 -c '…'` with a meta_path filter denying every
`ansible*` import and asserts the import + invocation succeed in that
clean process. The earlier in-process sys.modules-manipulation shims
fragmented module identity and broke under Python 3.13 inside the
infinito container; subprocess isolation has neither problem.

Doc-style audit (per docs/contributing/documentation.md):
  - Removed em-dash from utils/cache/README.md (split into two
    sentences)
  - Updated stale `utils.runtime_data` reference in
    docs/contributing/design/matrix-deploy.md
  - Removed two SPOT self-declarations
    (docs/agents/files/group_vars/applications.md and
    docs/contributing/artefact/files/plugins/lookup/users.md) per
    the documentation rule that SPOT status is conferred by
    cross-links, not by the page asserting it about itself

Empirical proof:
  - All four domain modules import without ansible (subprocess
    isolation tests)
  - get_variants(roles_dir=ROLES_DIR) returns 99 apps in the
    ansible-blocked subprocess
  - 142 cache tests + 1085 wider-suite tests pass; the only
    remaining failures (test_host_vars free-function fixture bug,
    test_runtime_performance timing flake) are pre-existing and
    unrelated to this refactor (verified via `git stash`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:27:57 +02:00
kevinveenbirkenbach 12dba01156 refactor(utils): consolidate cache modules into utils/cache/ package
Both in-process cache layers — the cached YAML loader and the cached
runtime-data getters — now live next to each other under
`utils/cache/`. The package gains a README that documents the
"orthogonal layers, no third cache" rule so future contributors hit
the right primitive on the first look.

Moves:
- `utils/yaml_cache.py` -> `utils/cache/yaml.py`
- `utils/runtime_data.py` -> `utils/cache/data.py`
- `tests/unit/utils/test_yaml_cache.py` -> `tests/unit/utils/cache/test_yaml.py`
- `tests/unit/utils/test_runtime_data.py` -> `tests/unit/utils/cache/test_data.py`
- `tests/unit/utils/test_variants.py` -> `tests/unit/utils/cache/test_variants.py`

New:
- `utils/cache/__init__.py` (empty per project convention)
- `utils/cache/README.md` describing the two layers, when to reach for
  which, lifetime, test-only helpers, and design guidelines (RFC 2119
  keywords, emoji-after-headline, no self-SPOT declaration, no
  slash-in-link-text).
- `tests/unit/utils/cache/__init__.py`

Mechanical updates:
- All imports across cli/, plugins/lookup/, tests/, and roles/ rewritten
  from `utils.yaml_cache` -> `utils.cache.yaml` and
  `utils.runtime_data` -> `utils.cache.data` (40+ files).
- `REPO_ROOT` parents-index in the moved tests bumped from `parents[3]`
  to `parents[4]` to reflect the deeper nesting.
- Six broken markdown links in docs/ updated to the new module paths
  (test_markdown_links lint test stays green).
- Two test files that used the `from utils import runtime_data` form
  now use `from utils.cache import data as runtime_data` so the local
  alias remains.

Tests: 848 passed across unit + lint suites (1 pre-existing collection
error in test_host_vars.py unrelated to this change).
2026-04-25 17:17:14 +02:00
kevinveenbirkenbach f5e81ee56b refactor(modes): retire MODE_CI in favour of a direct RUNTIME check
The Playwright E2E gate previously fired through `MODE_CI`, a boolean
that OR-ed three env-var markers (`GITHUB_ACTIONS`, `ACT`,
`INFINITO_MAKE_DEPLOY`) AND-ed with an `INFINITO_SKIP_E2E` opt-out.
That mechanism duplicated information `RUNTIME` already carries:

- `make deploy-*` already bakes `RUNTIME=dev` into the inventory at
  init time (`RUNTIME_VARS_JSON='{"RUNTIME":"dev"}'`).
- CI runners (act / GitHub Actions) detect `RUNTIME=act|github` from
  their canonical env markers via `cli.meta.runtime.detect_runtime`
  during init.
- A bare `ansible-playbook` from a production shell stays at the
  default `RUNTIME=host`.

Switching the gate to `RUNTIME in ['dev', 'act', 'github']` therefore
covers every case `MODE_CI` covered, AND folds the gate into the
single existing runtime SoT rather than maintaining a parallel one.

Changes:
- `tasks/stages/02_server.yml` E2E include now keys on `RUNTIME`.
- `group_vars/all/01_modes.yml` drops the `MODE_CI` block; a short
  note explains the retirement.
- `scripts/meta/env/ci.sh` deleted (it only existed to export
  `INFINITO_MAKE_DEPLOY=1`).
- The 5 `scripts/tests/deploy/local/deploy/*.sh` entry-points stop
  sourcing `ci.sh`.
- `cli/deploy/development/deploy.py` no longer forwards the four
  retired env markers into the container; the runtime decision is
  already serialised into the inventory the deploy stage consumes.
- `scripts/tests/deploy/local/deploy/reuse-kept-app.sh` drops the
  same four `-e` flags from its `docker exec`.
- `docs/requirements/006-playwright-service-gated-tests.md` gets a
  prepended "MODE_CI retired" update note; the historical sections
  remain for context but the dead `ci.sh` link target was reduced to
  inline code.

Behaviour: any runtime where E2E was fired before still fires it now,
and the previous opt-outs have a single replacement: pass
`RUNTIME=host` (or `--vars '{"RUNTIME":"host"}'`) to skip Playwright on
a deploy that would otherwise have run it.
2026-04-25 14:21:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d97ed7a9e8 feat(deploy): per-role matrix-deploy variants with folder-per-round model
Introduces per-role `meta/variants.yml` for declaring multi-variant
deploys (e.g. WordPress Single-Site vs. Multisite) and reshapes the
development deploy CLI to materialise one inventory folder per variant
and iterate them with co-located sync+async passes.

Why:
- The previous single-inventory + Pass-1-then-Pass-2 split deployed each
  variant twice and ran the async pass against a host left in a
  different variant's state, masking real bugs.
- Inventory and runtime concerns were tangled: variant selection lived
  as a runtime extra-var and required cross-stage coordination.

How:
- `roles/<role>/meta/variants.yml` (file format docs at
  docs/contributing/artefact/files/role/variants.md): YAML list of
  deep-merge overrides on top of `config/main.yml`. Empty entry `{}` is
  variant 0 (= legacy payload). 6 roles get their dev overrides moved
  here from inventories/dev.yml; WordPress declares 2 variants.
- `utils/runtime_data.py` gains `_build_variants` / `get_variants` and
  drops the retired `_active_variants` runtime selector. Loader always
  returns variant 0 as the default; non-zero variants reach
  `get_merged_applications` as inventory-level `applications.<app>`
  overrides baked at init time.
- New `cli/deploy/development/inventory.py`: `DevInventorySpec`,
  `plan_dev_inventory_matrix` (pure planner shared between init and
  deploy), `build_dev_inventory_matrix` (materialises one folder per
  round at `<dir>-<R>`, or `<dir>` when single-variant). Single-folder
  paths preserve full backward compat.
- `cli/deploy/development/deploy.py`: matrix loop with per-round
  cleanup (only apps whose variant changed AND that are about to
  re-deploy), `--variant <idx>` (or `VARIANT=` env) to pin a single
  round, and `--full-cycle` (or `FULL_CYCLE=true`) which interleaves
  sync + async per round so the async re-deploy targets exactly the
  host state the matching sync deploy just produced.
- `cli/deploy/development/exec.py` gains repeatable `--env KEY=VAL` so
  bash callers can inject per-run context into in-container helpers
  without inlining heredocs.
- `scripts/meta/env/inventory.sh` exports `INVENTORY_VARS_FILE`
  (Python mirror in `cli.deploy.development.common.DEV_INVENTORY_VARS_FILE`;
  test_inventory_vars_file_spot.py locks them against drift).
- `make deploy-fresh-purged-apps` and `scripts/tests/deploy/ci/dedicated.sh`
  switch to a single init + single deploy `--full-cycle`. The
  `make deploy-reuse-*` bash scripts honour `VARIANT=` by suffixing
  `INVENTORY_DIR`/`INVENTORY_FILE`.
- The 5 `scripts/tests/deploy/local/deploy/*.sh` scripts now delegate
  their in-container heredocs to dedicated helpers in
  `scripts/tests/deploy/local/utils/`, removing the bash-in-bash
  quoting hell.

Tests:
- `tests/unit/utils/test_variants.py` (renamed from
  `test_application_variants.py`): loader edge cases, variant-zero
  default, retired-`_active_variants` guard.
- `tests/unit/cli/deploy/development/test_inventory.py` (new): planner,
  variant baking, builder, `filter_plan_to_variant`, `DevInventorySpec`.
- `tests/unit/cli/deploy/development/test_matrix_deploy.py` (new):
  per-round deploy filter, cleanup decisions, `--variant` pinning,
  `--full-cycle` per-round interleave.
- `tests/unit/cli/deploy/development/test_inventory_vars_file_spot.py`
  (new): drift guard for the bash <-> Python literal pair.
- `tests/unit/cli/deploy/development/test_exec.py` extended for `--env`.

Docs:
- `docs/contributing/artefact/files/role/variants.md` (new): file format.
- `docs/contributing/design/matrix-deploy.md` (new): runtime mechanism
  (folder layout, --variant, --full-cycle, cleanup logic).
- `docs/contributing/actions/deploy.md` (new): contributor dev-deploy
  flow (make targets, --variant pinning).
- `docs/administration/deploy.md` slimmed to admin-only (was mixed
  admin+dev), cross-links to the new contributor doc.
- `docs/agents/action/iteration/role.md` keeps only agent-iteration
  recipes; mechanism explanation moved out and linked.
- `docs/contributing/documentation.md` adds the "no self-SPOT
  declaration" rule; `applications.md` renamed its SoT header for
  consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:47:48 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d009a35116 feat(rbac): hierarchical /roles/<app>/<role> + service-gated Playwright + Discourse round-trip
Implements requirements 005, 006, and 007.

005: per-application Keycloak `group-ldap-mapper`s anchor each app's
role groups under `/roles/<application_id>/<role_name>` (and, for
tenant-aware apps, `/roles/<application_id>/<tenant>/<role_name>`).
The flat-CN LDAP layout keeps each CN globally unique so Keycloak's
group-tree resolver never collides; per-app mappers use the LDAP
`description` as the visible Keycloak group name to keep the path
free of redundant prefixes. Migration purges legacy pre-005 entries
and forces a Keycloak `logout-all` so stale tokens drop pre-005
group paths.

006: per-service `<SERVICE>_SERVICE_ENABLED` env flags and the
`service-gating.js` helper let role specs `skipUnlessServiceEnabled`
cleanly when a shared service is missing or `SERVICES_DISABLED` is
set; `MODE_CI` markers (`INFINITO_MAKE_DEPLOY`, `GITHUB_ACTIONS`,
`ACT`) propagate through the deploy CLI into the test stage.

007: WordPress->Discourse round-trip Playwright scenario gated on the
`web-app-discourse` deployment.

WordPress Playwright runs 5/5 green on the single-site flow; the
multisite + Discourse scenarios are correctly skipped when their
service prerequisites are absent. The `rbac_group_path` lookup is the
single sanctioned producer for OIDC group paths and is wired into
every consumer (yourls, lam, minio, openwebui, pgadmin, phpldapadmin,
phpmyadmin, prometheus, xwiki).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 08:51:01 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 3cfdc4bdf7 feat(services-disabler): propagate SERVICES_DISABLED through deploy stack and add conflict detection
Adds inventory consistency guard that fails fast when SERVICES_DISABLED
conflicts with an already-deployed inventory, and ensures the env var is
forwarded through the dedicated runner, development exec command, and
local test scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 19:36:16 +02:00
Environment TestandClaude Sonnet 4.6 1530788dbe feat(mirror): add repository segment to GHCR image path
Image names now follow {owner}/{repository}/mirror/{registry}/{name}
instead of {owner}/mirror/{registry}/{name}.

- GHCRProvider: add `repository` param + `add_args()`/`from_args()` classmethods
  as SPOT for CLI argument registration and provider construction
- sync, resolver: use GHCRProvider.add_args() / from_args() (no duplication)
- Workflows: resolve repository name via scripts/meta/resolve/repository/name.sh
  and pass --ghcr-repository to sync and publish
- mirrors.sh: require and forward GHCR_REPOSITORY env var
- deploy/development/mirrors.py: derive repository from GITHUB_REPOSITORY
- Tests: update expected paths, argv and env fixtures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 00:20:41 +02:00
kevinveenbirkenbach 9cbb12d540 Propagate host MTU to the local compose network 2026-03-31 21:00:51 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 a50c90c733 fix(deploy): resolve SERVICES_DISABLED service names to app IDs and verify inventory
- deploy.py: use find_provider_roles() to map service names (e.g. 'matomo')
  to provider app IDs (e.g. 'web-app-matomo') before filtering deploy_ids;
  forward SERVICES_DISABLED into the container via extra_env
- container.sh: rename INSPECT_CMD to CMD for consistency; fix indentation
- Makefile: expose CMD variable on exec target for non-interactive use
- environment.sh: source scripts/meta/env/all.sh for INVENTORY_DIR; add
  inventory verification after SERVICES_DISABLED deploy using make exec
- variables.md: document CMD variable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 01:26:44 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 135ceefa98 fix(build): add SPOT for local image name and fix missing REPO_PREFIX
Add scripts/meta/resolve/image/local.sh as SPOT for the local Docker
image tag (<repo-basename>/<distro>). Wire it into Makefile build
targets (IMAGE_TAG), compose.py (_base_env sets INFINITO_IMAGE), and
compose.yml (no default fallback). Fix build.sh to accept a pre-set
IMAGE_TAG without requiring REPO_PREFIX. Add make build + build-no-cache
to environment.sh test flow with correct section ordering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 00:16:27 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 1d34ff585a fix(deploy): forward SERVICES_DISABLED into container during inventory init
compose.exec runs inside the container, so host env vars are not
inherited. Pass SERVICES_DISABLED via -e so services_disabler.py
can suppress the correct entries when creating the inventory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:31:52 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 06a688c59a refactor(deploy): rename APP to APPS throughout the project
Makes clear that multiple space- or comma-separated app ids are
supported. Renames the env var, CLI flag (--app → --apps), and all
references in scripts, workflows, and documentation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 11:41:31 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 8d8e3c7475 feat(deploy): support multiple space- or comma-separated apps in --app
Resolves deps for each app individually and merges the results,
enabling APP="web-app-taiga web-app-pixelfed make deploy-fresh-purged-app".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 10:49:23 +02:00
kevinveenbirkenbach 67fadb57b8 refactor: normalize inventory filename to devices 2026-03-28 05:40:13 +01:00
kevinveenbirkenbach f2304bb35b feat: make health wait timeout configurable 2026-03-28 02:00:50 +01:00
kevinveenbirkenbach 003a3a0b84 Merge branch 'main' into feature/lint
Resolve merge conflicts, keep the cleanup script move to scripts/image, and fix the resulting lint regressions.
2026-03-21 01:36:58 +01:00
kevinveenbirkenbach 0466359631 Merge branch 'main' into feature/lint
# Conflicts:
#	scripts/image/push.sh
#	scripts/meta/env.sh
#	scripts/meta/wait/mirrors.sh
#	scripts/tests/deploy/ci/all_distros.sh
2026-03-19 03:21:31 +01:00
kevinveenbirkenbach 38b1d08ef7 Improve distro compatibility and dev environment testing 2026-03-18 20:00:33 +01:00
kevinveenbirkenbach 292feaa5da Add highest missing release workflow 2026-03-18 18:01:49 +01:00
kevinveenbirkenbach 67d17d8488 Merge branch 'main' into feature/web-app-mattermost
# Conflicts:
#	scripts/ci/wait_prebuilt_ci_image.sh
#	scripts/meta/wait/mirrors.sh
2026-03-18 11:33:27 +01:00
kevinveenbirkenbach ca1bf7832e Reorganize image scripts and PR wait logic 2026-03-18 11:26:12 +01:00
Alejandro Roman cb25eef0b4 fix(lowercase-ci): Adding the last part lowercase to ensure all usernames are lowercase and avboid braking test for username with uppercase 2026-03-17 10:50:26 +01:00
kevinveenbirkenbach 43e623ccbd Simplify deploy streaming by removing tail buffering 2026-03-16 12:24:53 +01:00
kevinveenbirkenbach 8cbbfb4d06 fix(deploy): use infinito CLI for inventory creation in container flows 2026-02-22 14:09:48 +01:00
kevinveenbirkenbach ec30948546 fix(deploy): use bash -lc for commands with pipefail 2026-02-21 16:05:42 +01:00
kevinveenbirkenbach 0710c41f91 fix(ci): run mirror resolver with venv python in container 2026-02-20 11:25:46 +01:00
kevinveenbirkenbach 3e9de04704 refactor: move server inventories into bundles 2026-02-13 10:14:17 +01:00
kevinveenbirkenbach 32fe49c00f feat(cli/deploy): retry docker compose up on transient registry errors
Wrap `docker compose up` in a retry loop (6 attempts, 30s delay) to mitigate
temporary Docker Hub / registry 5xx pull failures. Add unit tests covering
success-after-retries, exhaustion behavior, and no-sleep on first success.

https://chatgpt.com/share/698b8d67-0084-800f-ad33-ec8933dfba15
2026-02-10 20:56:11 +01:00
kevinveenbirkenbach 84782f3b22 refactor(invokable): centralize app discovery, add type grouping, and derive deploy stages from inventory groups
https://chatgpt.com/share/698b0039-c4c4-800f-b962-7033324c07a8
2026-02-10 10:53:53 +01:00
kevinveenbirkenbach 7104f4486c Added more meta information for infinito nexus deployer 2026-02-09 11:41:36 +01:00
kevinveenbirkenbach 4e5e8d2a7d Fix compose down behavior and harden templar rendering
- Restore full teardown semantics in Compose.down by delegating to down_stack
  (including volume removal and CI cleanup)
- Normalize templar render output to string to avoid None-related failures
- Clean up unused variable in templating fallback path
- Reorder test imports without changing behavior

https://chatgpt.com/share/69890935-4f1c-800f-8f2c-1b18a1df0ea5
2026-02-08 23:07:44 +01:00
kevinveenbirkenbach 24025bc37c Fix Ruff F821: make Compose a forward reference in deps.py
https://chatgpt.com/share/69890394-46c4-800f-abe0-b7046357872f
2026-02-08 22:43:39 +01:00
kevinveenbirkenbach 88429c716b Merge branch 'alert-autofix-182' 2026-02-08 22:15:56 +01:00
kevinveenbirkenbach 35ee61975d Merge branch 'alert-autofix-189' 2026-02-08 20:32:14 +01:00