Commit Graph
2780 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.7 0639fb2fa7 fix(deploy): unblock matrix-deploy of web-apps on dev runtime
Four independent bugs were each blocking the autonomous
fresh-purged-apps iteration of web-app-{openwebui,wordpress,nextcloud,
phpmyadmin,yourls}; bundling them so the pipeline keeps running.

- cli/core/discovery.py: stop walking argv prefixes at the first flag
  or path-shaped arg and ignore OSError so the matrix-init's
  `infinito create inventory <abs-path> --vars '<huge JSON>'` no longer
  hits ENAMETOOLONG inside `is_dir()`. New regression tests pin both.
- utils/cache/data.py: when this module moved to utils/cache/data.py,
  PROJECT_ROOT silently slid one level deep, pointing ROLES_DIR at a
  non-existent `<repo>/utils/roles`. Implicit lookups returned empty
  defaults so `lookup('users','contact')` failed at deploy time as if
  the user weren't defined. Use parents[2] and pin the invariant in a
  new regression test.
- roles/web-app-keycloak/vars/main.yml: KEYCLOAK_PER_APP_LDAP_ROLES_MAPPER
  inherited `subComponents`/`id` from the realm-import dictionary's base
  `ldap-roles` mapper. kcadm rejects those keys on `create components`
  ("Unrecognized field 'subComponents'"), with stderr swallowed by
  `2>/dev/null` in upsert_app_mapper.sh. Strip both keys exactly like
  update/05_ldap.yml already does for the shared mapper.
- scripts/tests/deploy/local/deploy/fresh-purged-app.sh: pass
  `RUNTIME=dev` through `--vars`, otherwise the host process running
  init falls back to `detect_runtime() == "host"` and the Playwright
  E2E gate (`RUNTIME in [dev, act, github]`) never fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:17:14 +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 dea5191d12 perf(cli): migrate hot-path resolution + build modules to utils.yaml_cache
Phase 2 of the YAML-cache migration. The cli/meta/applications/resolution/*
and cli/build/* modules walk the same `roles/<role>/<config|vars|meta>/...yml`
files repeatedly during a single invocation. Direct `yaml.safe_load` calls
re-parsed each file every time. This commit routes those reads through the
in-process cached loader added in the previous commit so each absolute
path is parsed at most once per Python process.

utils/yaml_cache:
- New `load_yaml_any(path, *, default_if_missing=...)` for files whose
  root is not a mapping (Ansible task lists, etc.). Shares the cache
  with `load_yaml`, so a hot path that mixes both still pays parse
  cost only once per file.
- `load_yaml` keeps its strict dict-root validation; non-mapping roots
  now point callers at `load_yaml_any` in the error message.
- Internal `_load_raw` shared between both entry points.
- Tests: +5 cases covering list/scalar roots, empty-file → {} coercion,
  cache sharing across the two entry points, and strict rejection.

Migrated call sites (each preserves its existing public surface and
domain-error wrapper):
- cli/meta/applications/in_group_deps/__main__.py: per-role vars/main.yml
  in `find_role_dirs_by_app_id` + the one-shot applications file load.
- cli/meta/applications/role_name/__main__.py: per-role vars/main.yml
  scan in `get_role`.
- cli/meta/applications/resolution/run_after/__main__.py: per-role
  meta/main.yml read.
- cli/meta/applications/resolution/dependencies/__main__.py: per-role
  vars/main.yml AND meta/main.yml reads inside the transitive
  dependency walk.
- cli/meta/applications/resolution/services/resolver.py: per-role
  config/main.yml read in `_load_role_config` (the inner-most loop of
  service-dependency resolution).
- cli/build/role_include/__main__.py: per-role meta/main.yml +
  vars/main.yml reads inside the topological-sort build.
- cli/build/graph/__main__.py: per-role meta/main.yml read; the
  tasks/main.yml reader uses `load_yaml_any` (root is a list).

`yaml` import is preserved in two files (cli/build/graph/__main__.py and
cli/meta/applications/in_group_deps/__main__.py) because they still call
`yaml.safe_dump` on output; only the read paths moved.

Tests: full unit + lint suites green (594 passed, 1 pre-existing
unrelated collection error in test_host_vars.py).

Out of scope for this commit (one-off readers where the cache adds
nothing): cli/validate/inventory, cli/fix/vars_main_files,
cli/create/inventory/{host_vars,services_disabler,mirror_overrides},
cli/create/role, cli/meta/callorder, cli/meta/roles/lifecycle_filter.
Those each read their target file at most once per invocation, so
caching would just add an indirection without saving disk I/O.
2026-04-25 17:17:14 +02:00
kevinveenbirkenbach 35b47a2048 perf(cli): cache YAML loads in-process via utils/yaml_cache
Many CLI tools walk the same `roles/<role>/<config|vars|meta>/...yml`
files multiple times during a single invocation (dependency
resolution, service resolution, role introspection, ...). Each direct
`yaml.safe_load(path.read_text())` re-parses YAML and re-touches disk;
for `infinito create inventory` over the ~600-role tree this dominates
wall-clock time.

This change introduces `utils/yaml_cache.py`, a small in-process,
per-absolute-path memoising loader, and migrates the three existing
ad-hoc loaders to delegate to it.

`utils/yaml_cache.py` exposes:
- `load_yaml(path, *, default_if_missing=_MISSING)`: cached read,
  dict only at the YAML root, raises `FileNotFoundError` by default
  for missing files (callers can opt into a default value).
- `dump_yaml(path, data)`: write-through that evicts the cached entry,
  so a CLI tool that edits a file mid-run sees the new content on the
  next read.
- `invalidate(path)`: explicit per-path drop for the rare cases where
  an external process touches the file.
- `_reset_cache_for_tests()` for unit-test isolation.

Cache semantics are documented in the module docstring; the key point
is that the same dict instance is returned on cache hit (no deep
copy), so callers that want to mutate the result MUST copy first.
This trades that constraint for ~5x fewer disk reads during the hot
resolution loops without measurably increasing memory.

Migrated wrappers (each keeps its existing public surface + domain
error type so call sites stay untouched):

- `cli/create/inventory/yaml_io.py` — `load_yaml(path)` /
  `dump_yaml(path, data)`. The legacy "missing file -> {}" contract
  stays via `default_if_missing={}`. Non-mapping roots still surface
  as `SystemExit`.
- `cli/meta/applications/resolution/combined/yaml_utils.py` —
  `load_yaml_file(path)` re-raises errors as `CombinedResolutionError`.
- `cli/meta/applications/resolution/services/yaml_utils.py` —
  `load_yaml_file(path)` re-raises errors as `ServicesResolutionError`.

Tests: `tests/unit/utils/test_yaml_cache.py` covers cache hit,
path-resolution sharing, missing-file behaviour with and without the
default, non-mapping rejection, write-through eviction, and explicit
invalidation. All existing unit + lint test suites stay green.

Phase 2 (auditing the remaining direct `yaml.safe_load` call sites in
`cli/` and migrating the hot ones) is intentionally out of scope here
to keep the diff focused; this commit is the prerequisite SPOT.
2026-04-25 17:17:14 +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
kevinveenbirkenbach 14a1eb651a Merge branch 'main' into fix/web-app-joomla/sh-pipefail-dash/194 2026-04-25 10:18:06 +02:00
kevinveenbirkenbach 43e18d1ee2 Merge branch 'main' into feature/rbac-hierarchy-and-service-gates 2026-04-25 09:38:27 +02:00
kevinveenbirkenbachandCopilot Autofix powered by AI 22b05f49de Potential fix for code scanning alert no. 277: Explicit returns mixed with implicit (fall through) returns
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-25 09:38:27 +02:00
kevinveenbirkenbachandCopilot Autofix powered by AI 999a01a231 Potential fix for code scanning alert no. 278: Empty except
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-25 09:38:26 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9beee8dc03 refactor(web-app-keycloak): SPOT per-app mapper naming via filter plugin
Add `roles/web-app-keycloak/filter_plugins/rbac_per_app.py` with two
filters that centralise the strings that vary per application:

  kc_per_app_mapper_name(app_id) -> "ldap-roles-<app_id>"
  kc_per_app_ldap_filter(app_id) -> "(&(objectClass=groupOfNames)(cn=<app_id>-*))"

Consume them from:

* `vars/main.yml::KEYCLOAK_PER_APP_LDAP_ROLES_MAPPER` for the mapper
  name and the LDAP filter, instead of inlining the string templates.
* `tasks/update/ldap/per_app_mapper_one.yml` as the `MAPPER_NAME` env
  variable handed to the upsert script.
* `files/scripts/per_app_mapper/upsert_app_mapper.sh`, which now reads
  `MAPPER_NAME` from the environment instead of building it from
  `APP_ID`.

A future rename of the per-app mapper convention is now a one-line
change in the filter plugin and stays consistent across vars, tasks,
and shell scripts. Unit tests cover both filters and the
`FilterModule` registration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:25:13 +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
kevinveenbirkenbach fab099c1c1 feat(version-updater): support flavored semver tags and centralise logic
Docker Official Image tags of the form `<semver>-<flavor>` (e.g. the
joomla:5.4.5-php8.3-apache pin) were previously skipped by the
`web-app` image version checker because its strict regex only matched
pure `x.y.z` shapes. As a result the auto-update workflow and the
external warning test never proposed a newer 5.4.x-php8.3-apache tag,
and flavored pins silently drifted out of date.

Widen the regex in `utils/docker/version_updater.py` to accept the
`<semver>-<flavor>` shape, with the flavor suffix treated as an opaque
discriminator: a candidate tag is only considered an upgrade when it
shares the *same* flavor. That guarantees a 5.4.5-php8.3-apache pin
never gets silently switched to 5.4.6-php8.4-apache or 5.4.6-fpm.

Refactor `tests/external/docker/test_image_versions.py` to import
directly from the util: ~130 lines of duplicated semver / Docker Hub /
GHCR helper code deleted, and the file now has exactly one code path
to keep in sync with the one used by the auto-update CLI
(`cli/fix/docker_image_versions`).

Chain impact:
  * `.github/workflows/update.yml` -> `scripts/github/update_docker_image_versions.sh`
    -> `python3 -m cli.fix.docker_image_versions` -> same util. No
    workflow change needed; the next scheduled update run will pick
    up flavored upgrades automatically.
  * Live external run now reports 5 previously-silent upgrades:
    web-app-joomla/joomla, web-app-magento/php, web-app-bridgy-fed,
    web-app-suitecrm/php, web-app-shopware/redis.

Tests: existing `test_latest_semver_respects_depth` still covers the
pure-semver case; three new unit tests pin the flavor behaviour
(`is_semver` accepts flavored tags, `version_key/depth/flavor` strip
the flavor, `latest_semver` refuses cross-flavor upgrades).

Also drop an unused `autocreate_users: true` marker at the top of
`roles/web-app-joomla/config/main.yml` that was left over from an
earlier LDAP-autocreate experiment.
2026-04-24 23:04:47 +02:00
kevinveenbirkenbachandCopilot Autofix powered by AI aefe7dfde3 Potential fix for code scanning alert no. 277: Explicit returns mixed with implicit (fall through) returns
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-24 21:23:25 +02:00
kevinveenbirkenbachandCopilot Autofix powered by AI baddc1fec8 Potential fix for code scanning alert no. 278: Empty except
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-24 19:26:04 +02:00
kevinveenbirkenbach 3fdbe8d9e2 fix(web-app-joomla): use bash for CLI install argv (dash lacks pipefail)
The retry-loop install task introduced in f7d32109e used
`argv: [sh, -lc, …]` with `set -o pipefail` in the body. On Debian/Ubuntu
CI runners `/bin/sh` is dash, which aborts with
`sh: 1: set: Illegal option -o pipefail`, so all 5 retries die on the
very first line and the Joomla deploy fails.

Switch argv[0] to `bash` so the task works on every distro and document
the constraint inline. Regression is tracked in infinito-nexus/core#194;
the retry-loop itself was the fix for #186.

Add tests/lint/ansible/test_no_sh_pipefail_in_ansible_tasks.py guarding
the three ways this class of bug can come back:

  * `command` with `argv: […, sh, -lc, body_with_pipefail]`
  * `args.executable` set to any non-bash shell
  * removal of the global `ansible_shell_executable: /bin/bash` default
    from group_vars/all/00_general.yml
2026-04-24 16:32:27 +02:00
kevinveenbirkenbachandClaude Opus 4.7 bebae545eb feat(rbac): generic OIDC group RBAC for WordPress (requirement 004)
Auto-provision one OpenLDAP group per role declared under each application's
`rbac.roles` in `config/main.yml`, gated on `group_names` so only apps
deployed on the host contribute. Keycloak syncs the groups from LDAP, emits
the `groups` claim via the shared RBAC client scope, and WordPress maps that
claim to a local role in a mu-plugin (auto-loaded, cannot be deactivated
from the admin UI). No direct LDAP bind from WordPress.

Non-obvious design choice: the `groups` client scope MUST be a *default*
client scope, not optional. When optional + `include.in.token.scope=false`,
Keycloak 26 drops the group-membership mapper from userinfo even when the
scope is explicitly requested, so the mu-plugin receives no `groups` and
falls back to `subscriber` for every user. ID/access tokens still carry
the claim, but WP consumes userinfo.

Playwright verifies the three-role cycle (subscriber/editor/administrator)
serially and idempotently per requirement 004: the add step runs via the
Keycloak admin UI (detects "already a member" via a disabled picker
checkbox and skips); teardown runs via the Keycloak Admin REST API. Only
memberships the test itself added are removed, so any pre-existing
membership is preserved. Each hop (super-admin, biber, WP-admin) runs in
its own browser context so WP session cookies and Keycloak SSO cookies
cannot leak.

Resolves: docs/requirements/004-generic-rbac-ldap-auto-provisioning.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 07:26:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f722e676ca refactor(service-lookup): rename 'needed' flag to 'required'
Rename the third flag returned by lookup('service', ...) from 'needed'
to 'required' across code, tests and design docs. Same semantics: True
when some deployed app has the service with both enabled: true AND
shared: true, directly or transitively through its own enabled service
dependencies.

The new name reads as a contract ("this service is required by a real
consumer") rather than a suggestion ("something needs it"), which
matches the stricter-than-enabled semantic the flag already carries.

Touchpoints:
- plugins/lookup/service.py: _is_service_needed → _is_service_required,
  any_needed var, dict key in _compute_flags, docstring rationale.
- roles/sys-utils-service-loader/tasks/main.yml: caller switched to
  .required.
- roles/web-app-wordpress/config/main.yml + tasks/plugins/wp-discourse.yml:
  wp-discourse plugin gate uses lookup('service', 'discourse').required;
  compose.services.discourse declared as shared-service dependency so
  the service loader auto-loads web-app-discourse when inventory sets
  enabled: true. The legacy include_tasks: utils/load_app.yml in
  plugins/wp-discourse.yml is removed — the service loader now handles
  it.
- tests/unit/plugins/lookup/test_service.py: 25 assertions and 6 test
  method names renamed (test_needed_* → test_required_*,
  test_transitive_needed_resolved,
  test_direct_provider_is_not_needed_without_shared_flag).
- tests/integration/services/test_transitive_dependencies.py: one
  assertion renamed.
- docs/contributing/design/services/base.md: Lookup-Plugins section
  replaces the 'needed' bullet with an expanded 'required' entry that
  documents when consumers should gate on it and why plain enabled is
  not enough.

Also moves wordpress-app-specific keys (title, max_upload_size,
plugins) under compose.services.wordpress to match the convention
the other role keys already follow, and updates vars/main.yml lookup
paths accordingly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 00:06:47 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b56554a6f3 test(lookups): assert every defined lookup plugin is actually used
Symmetric to tests/integration/filters/test_usage.py: enumerates every
plugins/lookup/<name>.py that declares a LookupModule class and fails
if any of them is never referenced in production code. Three invocation
styles count as usage:

- lookup('<name>', ...) — canonical Jinja/YAML form.
- query('<name>', ...) — list-returning variant.
- from plugins.lookup.<name> import ... — direct Python import, used
  by utils/runtime_data.py which embeds several LookupModule classes
  as regular helpers instead of dispatching through Ansible.

Matches under tests/ alone count as "only used in tests" and still fail
the build. An unused lookup is a maintenance hazard: it stays in the
search path, rots silently, and confuses plugin browsing.

Uses the inverted-scan approach from the filter-usage test (one pass
over all matching project files, a single master alternation regex
covering every defined lookup name), so cost scales with M_files not
N_lookups × M_files. Walk and file contents are served from
tests.utils.fs process-level caches per the testing.md rule.

All 27 currently-defined lookups pass — no migration backlog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 23:37:28 +02:00
kevinveenbirkenbachandClaude Opus 4.7 e6eb5115bf test(utils/fs): extend cache coverage + invert filters/test_usage scan
Second-wave rollout of tests/utils/fs:

- 6 tests switched to the shared content cache via read_text() (replacing
  raw open()/Path.read_text()): roles/includes/test_existence,
  roles/dependencies/test_unnecessary, vars/test_facts_not_overridden,
  services/test_sys_requires_id, lint/ansible/roles/
  test_service_core_first_task_run_once, lint/ansible/roles/
  test_compose_resource_limits.

- 4 unit-lookup tests moved their fixture root from <repo>/.tmp_test_* to
  tempfile.TemporaryDirectory() so transient files land in /tmp (matching
  the AGENTS.md rule). Manual recursive-delete teardowns removed —
  TemporaryDirectory.cleanup() covers that path safely.

- filters/test_usage.py: algorithmic inversion. Old loop was
  O(N_filters × M_files × 4_regex); new scan walks files once, applies two
  master-alternation regexes (bare `| name` and `{% filter name %}`) plus
  one combined regex over all Python callables, and records hits per filter
  via the capture group. The two dropped Jinja patterns were strict
  subsets of the bare pattern, so the hit set is identical — only the
  complexity changes (3.6s → 0.2s, ~18x).

- docs/agents/action/testing.md: new "Filesystem Scans — Use the Shared
  Cache" section codifies the rule so future agent-written tests route
  through tests.utils.fs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 22:19:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9c8cc1ddcb test(utils/fs): cache project file walks + contents across test run
Adds tests/utils/fs.py with process-level @lru_cache for both the
project-wide os.walk and per-file read_text(), then migrates 26 tests
(24 modified + 2 new) that previously re-walked/re-read the tree
independently to share one cached pass.

Headline win: tests/integration/jinja/test_variable_usage.py drops from
~26s to ~1.3s (≈19x), jinja cluster as a whole ~14x. Other migrated
tests benefit from the shared content cache when the same file is read
by multiple scans in one pytest run.

Non-caching logic changes (kept intentionally):
- test_variable_usage.py: added `if varname not in content: continue`
  fast-path before the line loop (pure short-circuit, identical result).
- test_test_file_naming.py: exempt tests/utils/ from the test_*.py
  prefix rule so the new shared helpers don't trip the lint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 20:29:54 +02:00
kevinveenbirkenbach b2914b7cd1 Merge branch 'main' into fix/users-applications.yml 2026-04-23 18:14:17 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f0475e393f test(lint): forbid literal no_log: true in Ansible YAML
Adds a lint test that scans roles/, tasks/, playbooks/ for
`no_log: true` (and the YAML-truthy variants `yes`/`on`, all cases).
Hard-coded `no_log: true` blocks operators from surfacing masked
values when diagnosing a failing task — the fix is to route every
no_log through `MASK_CREDENTIALS_IN_LOGS`, which defaults to true
but can be flipped for a single play with
`-e MASK_CREDENTIALS_IN_LOGS=false`.

Failure message points at the exact file:line and shows the
required replacement form:

    no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 10:46:24 +02:00
kevinveenbirkenbachandClaude Opus 4.7 e26ce8a630 test(email-lookup): isolate token store from container filesystem
setUp/tearDown patches `runtime_data._load_store_users` to return `{}`
so `_hydrate_users_tokens` can't pull a real `no-reply` token out of
`/var/lib/infinito/secrets/tokens.yml` (or any DIR_SECRETS / DIR_VAR_LIB
candidate that happens to exist in the test container). Without this,
`test_returns_plugin_defaults_when_no_vars` failed inside the CI
container with `password='395d6871414ffcbfbc3253db32328001'` because
`email._compute('password')` reads `users['no-reply'].tokens['web-app-mailu']`
and a prior deploy had persisted that token to disk. Patches the loader
rather than env vars because `_resolve_tokens_file` iterates four
candidate sources and any one of them existing reintroduces the leak.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 10:17:18 +02:00
Alejandro Roman 58aa904129 feat(prometheus-comm): fix last comments and fix new issue in matrix 2026-04-23 00:31:24 +04:00
Alejandro Roman 9a37c3f8bc feat(prometheus-comm): fix lint 2026-04-23 00:31:23 +04:00
Alejandro Roman 0c02f19f87 feat(prometheus-comm): fix matrix issue + find mattemost doesnt support metrics for free plan, deffered to a mattemrost future task 2026-04-23 00:31:23 +04:00
Alejandro Roman 5ada2e930c feat(prometheus-comm): Gitea fixed and avoid deffered 2026-04-23 00:31:22 +04:00
Alejandro Roman faf02d26a4 feat(prometheus-comm): fix gitea issues to have prometheus 2026-04-23 00:31:22 +04:00
Alejandro Roman e3a1797ba9 feat(prometheus-comm): fix previous issues + add comms correctly and avoid deffered logic for this PR - WIP - some tests still fails 2026-04-23 00:31:21 +04:00
Alejandro Roman 15505b2547 feat(prometheus-comm): following best practices 2026-04-23 00:31:21 +04:00
Alejandro Roman 682eedb58f feat(prometheus-comm): fix lats issues + additionals I found 2026-04-23 00:31:20 +04:00
Alejandro Roman acc407a9b0 feat(prometheus-comm): add filters for comms apps taht are not enabled yet + fix tests issues 2026-04-23 00:31:20 +04:00
Alejandro Roman eacd2b9696 feat(prometheus-comm): fix latest comments 2026-04-23 00:31:19 +04:00
Alejandro Roman e5b26818d2 feat(prometheus-comm): fix tests issues 2026-04-23 00:31:19 +04:00
Alejandro Roman 6bc5512806 feat(prometheus-comm): fix CI fails tests 2026-04-23 00:31:18 +04:00
Alejandro Roman 48a99a2bb9 feat(prometheus-comm): move logic to each role and remove old files 2026-04-23 00:31:17 +04:00
Alejandro Roman e73dc1412b feat(prometheus-comm): fix Python lint issues 2026-04-23 00:31:15 +04:00
Alejandro Roman 6388984dbe feat(prometheus-comm): remove defaults and refactor code 2026-04-23 00:31:14 +04:00
Alejandro Roman 3f82fb1ac8 feat(prometheus-comm): make sure to move logic into separate temapltes and fix issues in code 2026-04-23 00:31:14 +04:00
Alejandro Roman a4c46aed65 feat(prometheus-comm): fix issues, make test working and ensure communication works correctly as expected 2026-04-23 00:31:13 +04:00
Alejandro Roman 82c40e6415 feat(prometheus-comm): Add all the logic to handle metrics and health int oall apps 2026-04-23 00:31:12 +04:00
Alejandro Roman b117c9ed3c fix(prometheus-intergration): add missing test 2026-04-23 00:31:12 +04:00
Alejandro Roman 6daa486e72 fix(prometheus-intergration): solve make test issues 2026-04-23 00:31:12 +04:00
Alejandro Roman b61359c85c feat(prometheus-intergration): Add all the logic need it for all apps with prometheus, missin playwright still 2026-04-23 00:31:11 +04:00
kevinveenbirkenbach 591983af7d fix(email-lookup): render Jinja in computed defaults
email._compute('domain') returned variables['DOMAIN_PRIMARY'] verbatim. When
DOMAIN_PRIMARY held an unrendered template (e.g. the group_vars default
"{{ lookup('env','DOMAIN') | default('infinito.localhost', true) }}"), the raw
expression leaked into rendered templates. In discourse's containers/discourse.yml
the literal "{{ ..." opened a YAML flow mapping and made Ruby Psych fail with
"did not find expected ',' or '}' while parsing a flow mapping", aborting
`./launcher rebuild discourse` in CI.

Route every _compute() result through the same _render() helper that already
covered SYSTEM_EMAIL_* overrides, and add a regression test that exercises a
templated DOMAIN_PRIMARY default.
2026-04-22 22:22:57 +02:00
kevinveenbirkenbachandClaude Opus 4.7 247d8ccdeb fix(lookup:oidc_flavor): resolve via merged applications, not raw overrides
lookup('oidc_flavor') was reading applications[APP].compose.services
straight off variables["applications"], which in Ansible is the
pre-merge override slice — role defaults from config/main.yml (like
compose.services.ldap.enabled: true) are not yet visible there. As a
result ldap.enabled resolved to the lookup's default False on every
invocation and the Nextcloud flavor silently fell through to
"sociallogin" even when LDAP was enabled.

Switch the lookup to get_merged_applications(), matching the path
lookup('config') already uses, so role-level defaults and inventory
overrides are both visible. Tests now patch that merge function rather
than inject raw variables.

Verified live against the reuse-kept stack: lookup('oidc_flavor')
resolves to 'oidc_login', plugins.oidc_login.enabled=True and
plugins.sociallogin.enabled=False.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 15:35:58 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1e085de616 test(lint): drop git subprocess from test-location check
Replace `git ls-files` with an inline `.gitignore` parser + `rglob` walk so
the lint runs inside the `make exec` container where `.git/` isn't mounted.
The matcher covers the pattern shapes used in this repo's `.gitignore`
(bare basenames, `prefix/*`, `*.ext`, path-qualified exact matches).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 02:12:02 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4867ffe66c feat(web-app-decidim): OIDC registration via Playwright TOS form, extracted Ruby
Playwright drives Decidim's first-time OIDC registration (TOS + newsletter
modal) so the biber user is created on first login and reused idempotently
on subsequent runs. Ruby snippets previously inlined in tasks/01_setup.yml
and patch.py are now separate .rb files under files/ruby/, copied into the
image alongside patch.py. patch.py now also fixes Decidim 0.31.3's broken
omniauth_registrations_path view helper (both new.html.erb and
new_tos_fields.html.erb).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 02:10:04 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d7a1683d1f test(decidim): move patch_decidim tests into tests/unit/roles
Relocates the role-local test file under `tests/unit/roles/web-app-decidim/`
so the new `test_*.py`-location lint has no offenders. Required for the
pairing commit b67b39221; got unstaged by pre-commit hook churn the
first time around.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 00:55:31 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b67b392210 test(lint): enforce test_*.py files live under tests/
Adds a repository lint that walks `git ls-files -co --exclude-standard`
and fails if any `test_*.py` lives outside the top-level `tests/` dir,
so .gitignored artefacts stay excluded. Moves the existing decidim role
test into `tests/unit/roles/web-app-decidim/` and rewrites it as
`unittest.TestCase` so `unittest discover` actually executes it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 00:54:36 +02:00