Commit Graph
483 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.7 e2136a3bc7 fix(cache-retry): truncate sanitized label to 80 chars before mktemp template
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 01:09:03 +02:00
kevinveenbirkenbachandClaude Opus 4.7 398aeb19f4 fix(purge/apps): skip 'docker network prune' when daemon is unreachable
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 01:09:03 +02:00
kevinveenbirkenbachandClaude Opus 4.7 094cc5b54c fix(purge): always run nginx vhost purge, gate compose/db/dir/network on stack presence
The early `/opt/compose/${entity}` check in apps.sh skipped redirect-only
entities (cdn, css, asset, file — no compose stack, only nginx vhost
files) before nginx.sh could run, leaving their per-domain vhosts
behind across matrix-deploy variant transitions.

Concretely chess CI Run 25997888664: variant-0 deployed web-svc-css
which rendered css.infinito.example.conf; the inter-round purge for
variant-1 skipped css with `\!\!\! WARNING: /opt/compose/css not found`;
the vhost survived; hlth-csp's headless browser then probed
https://css.infinito.example/ and got ERR_CERT_COMMON_NAME_INVALID
(the surviving vhost falls back to the default_server cert).

Moves the compose-dir guard from the entity-collection loop into the
purge loop and runs nginx.sh unconditionally before the
compose-dependent purges. The else branch prints the skip reason so
operators retain the trace signal for redirect-only entities.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 01:09:02 +02:00
kevinveenbirkenbachandClaude Opus 4.7 5b8cb71884 feat(scripts+make): add make run wrapper for nested docker run inside the host container
Inner-loop iteration on a deployed stack often needs a one-off sidecar
container against the same docker daemon the stack lives on — typical
cases are rerunning a Playwright project with a patched .env, retrying
an arbitrary CLI image against an app container, or smoke-checking a
release image without going through Ansible. Without a wrapper the
caller has to spell out `docker exec <host-container> docker run --rm
<flags...> <image> <cmd>` by hand, which is verbose enough that agents
tend to fall back to redeploying instead of validating in-place.

Add `make run IMAGE=<ref> [CMD=...] [RUN_FLAGS=...]` plus the underlying
`scripts/tests/deploy/local/exec/run.sh`. The script reuses
`scripts/meta/env/all.sh` to resolve the host container name (same SPOT
as `make exec`), forwards RUN_FLAGS verbatim, and only wraps CMD in
`sh -lc` so default-entrypoint images and shell one-liners both work.

Wire both iteration loops to mention the wrapper next to `make exec`:
the Role Loop's Inspect block gets a bullet pointing at it, and the
Playwright Spec Loop gains a short "Debugging in the nested docker
daemon" section that also reminds the agent to mirror any successful
hand-patch back into the role's playwright.env.j2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:37:12 +02:00
kevinveenbirkenbachandClaude Opus 4.7 16bdbf03d7 docs: fix repo-wide drift against documentation.md style rules
Three classes of drift cleaned up across 85 .md files:

- Em-dashes: replaced ` — ` with the contextually correct separator
  (colon for `term: explanation`, period to split clauses across
  sentences/source lines, parentheses for asides, semicolon for
  tight-linked continuations). Two intentional exceptions remain
  in `docs/contributing/documentation.md` (the rule literal) and
  inside a fenced ```sh block in `docs/contributing/actions/debugging/ci.md`.
- SPOT self-labels: rewrote "This is the SPOT for X" to describe
  the subject directly ("This directory holds X" / "This file
  describes X"). Cross-references to other artefacts' canonical
  role remain unchanged.
- Heading-emoji position: moved leading-emoji headings (`# 🧩 Title`)
  to trailing position (`# Title 🧩`) per the headline rule that
  emojis MUST come after the title text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 98457f98fd refactor(update): unify version bumpers under utils/update + cli/contributing/update + scripts/github/update
Folds the parallel Docker-image and git-ref version-update plumbing
into a single subsystem so the auto-update workflow, the external
freshness tests, and the unit tests all share one module layout.

utils/
  update/
    base.py        # semver primitives shared by both backends
    docker.py      # OCI registry lookups + version: line rewriter
    repository.py  # git ls-remote walker + ref: line rewriter

cli/contributing/update/
  docker/           # python -m cli.contributing.update.docker
  repository/       # python -m cli.contributing.update.repository

scripts/github/update/
  image_versions.sh
  repository_refs.sh

Tests mirror the same shape under tests/{unit,external}/update/{docker,repository}/.

Also widens the Docker bump scope to all roles (previously gated on
`web-*`) so non-web roles like sys-svc-container get bumped too;
pins Bookwyrm's python image at 3.11-bookworm via
`# nocheck: docker-version` with a TODO until upstream supports
Python >= 3.12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 5fd55072c7 fix(scripts/tests/deploy/local/utils/cache-retry): switch stale-apt recovery to Nexus invalidate-cache
Replace the no-op `docker restart` recovery with `POST
/service/rest/v1/repositories/{name}/invalidate-cache` against each
apt-format proxy repo (Debian, Ubuntu, …) discovered dynamically
from Nexus' REST API. Container restart never re-validated cached
content; the new path makes Nexus actually re-fetch from upstream
on the next request and keeps repo definitions, blobstore, and
admin credentials intact.

Co-located changes:
- fresh-purged-app.sh: source scripts/meta/env/all.sh so the cache
  override env (incl. INFINITO_PACKAGE_CACHE_FRONTEND_IP) is set
  for the deploy CLI — without this, apt traffic bypasses Nexus.
- utils/nexus/list_apt_proxy_repos.py: stdin JSON → stdout name list,
  exit 1 on malformed input / no matches; used by the bash helper
  for discovery (parser lives outside the heredoc for testability).
- tests/unit/utils/nexus/test_list_apt_proxy_repos.py: 10 cases
  covering filter rules, empty/invalid input, non-dict entries,
  missing/empty names, and a realistic live payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:44:47 +02:00
kevinveenbirkenbachandClaude Opus 4.7 eed1d89bb4 fix(test-env): re-pin matomo reuse-pass to VARIANT=1 to match leftover host state
Step 08 (`deploy-fresh-purged-apps … FULL_CYCLE=true`) iterates matomo's
two variants and skips the final inter-round purge, so the host ends on
variant 1 (mariadb.shared=false → embedded `database` service). The
previous pin to VARIANT=0 forced a variant flip (shared=true) on a host
still wired for variant 1, which left the freshly-rendered matomo
compose.yml referencing `database` without an image and tripped the
`compose config --quiet` validation handler with:

  service "database" has neither an image nor a build context specified:
  invalid compose project

Pinning the reuse pass to VARIANT=1 redeploys onto the same variant the
matrix last left running, which is the intended `reuse of existing
state` semantic from variants.md ("the last state remains available for
inspection or follow-up specs").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:12:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d1de165e9c feat(purge): drop orphan default networks per entity + global prune
`docker compose down --remove-orphans` only deletes networks that
compose itself created (those carrying
`com.docker.compose.network=default` + `com.docker.compose.project=<name>`
labels). When the per-entity default network ends up unlabeled or
project-mislabeled — typical after an inter-round matrix-deploy
purge, or when a `community.docker.docker_network` Ansible call
materialised the network before compose ran — compose deliberately
leaves it untouched, and a subsequent `compose up` aborts with
  network <entity> was found but has incorrect label
  com.docker.compose.network set to "" (expected: "default")
The matrix-deploy CI rounds for web-app-matrix, web-app-gitea and
others reliably reproduce this.

Add a new entity-keyed purge primitive
`scripts/container/purge/entity/network.sh <ENTITY>` that runs after
the compose / dir / nginx primitives. It is idempotent: no-op when
the network does not exist, skips removal while active container
endpoints prove the network belongs to a shared service still in
use (svc-db-mariadb, svc-db-postgres, svc-db-openldap, svc-ai-ollama,
…), and otherwise drops the orphan network with `docker network rm`.
`docker network rm` failures are logged and tolerated — the next
`compose up` will surface the same problem with an actionable error.

Wire the primitive into `scripts/container/purge/apps.sh` immediately
after the existing `nginx.sh` call, so every entity-keyed purge
collects its own default network on the same pass.

Finally, after the entity loop, run a single bare `docker network
prune -f` to sweep orphan networks whose name does NOT correspond
to a purged entity (left over from renamed roles, abandoned
matrix-deploy rounds, etc.). The sweep is bare on purpose — no
`|| true` guard — so a daemon-side prune failure aborts the purge
loudly via the script's `set -euo pipefail`. Performance: ~13× faster
than calling prune inside `network.sh` per entity (one ~200 ms
listing pass vs. N × 200 ms for N entities).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:45:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9cefcba516 fix(cache): lower default freshness window to 6 days (< apt Valid-Until)
`INFINITO_PACKAGE_CACHE_MAX_AGE_MIN` was 129600 (90 days), 13× longer
than the 7-day `Valid-Until` window Debian and Ubuntu generate by
default in their apt `Release` files. Nexus held cached `Release`
manifests for 90 days, so once the upstream `Valid-Until` elapsed the
manifest stayed in the cache for another ~83 days and every Docker
build that ran `apt-get update` against the package-cache aborted with
  E: Release file for http://deb.debian.org/debian/dists/.../InRelease
     is expired (invalid since 5h 30min 54s).

Drop the default to 8640 minutes (6 days). Stays one day below the
7-day `Valid-Until` so Nexus always re-validates the cached `Release`
against upstream before apt would see it expired. The single value
still applies to every proxy repo (apt, yum, pypi, npm, gem, go, helm,
raw-*), but the `Valid-Until` enforcement only exists in apt — for
the other repos the change merely makes the cache 84 days fresher
instead of staler, no regression.

Update both docs sites that document the default:
- docs/contributing/artefact/files/compose.yml.md (env-var table)
- docs/contributing/environment/cache.md (operator narrative)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:45:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 51a2a9113c feat(deploy/local): auto-clear caches + retry on stale apt Release file
When the registry-cache or package-cache (Nexus / nginx frontend)
serves apt repository metadata whose `Valid-Until` window has elapsed,
Docker builds inside an Ansible-driven deploy abort with
  E: Release file for http://deb.debian.org/debian/dists/.../InRelease
     is expired (invalid since 5h 30min 54s).
which surfaces as a generic non-zero exit from the wrapped deploy
command and forces the operator to manually wipe the caches before the
next attempt.

Add `scripts/tests/deploy/local/utils/cache-retry.sh` exposing
`deploy_with_cache_retry <label> -- <cmd> [args...]`. The helper runs
the wrapped command with combined stdout+stderr teed to a temp log,
captures the wrapped command's real exit code via PIPESTATUS (with a
disciplined `set +e` / restore-previous-state dance so the caller's
`set -e` does not abort the script before the helper can react), and
on non-zero exit grep's the captured log for the stale-cache
signature `Release file ... is expired` / `Valid-Until ... expired`.

If matched, the helper wipes both cache stacks via `make cache-clean`
(registry-cache + package-cache through
scripts/system/cache/clean.sh), runs `docker builder prune -af` and
`docker image prune -af` to drop cached build layers and unused base
images, and re-runs the wrapped command exactly once. Non-stale-cache
failures bubble up unchanged so the caller's existing error handling
is preserved. Live in the deploy/update script layer rather than the
Ansible handler because only the host-side scripts can reach the
registry-cache + package-cache containers and the host Docker engine.

Wire the helper into all five local deploy entrypoints:
- fresh-purged-app.sh (both `--full-cycle` and the sync-only branch)
- fresh-kept-app.sh
- fresh-kept-all.sh
- reuse-kept-app.sh (added SCRIPT_DIR/REPO_ROOT locally because the
  script did not yet have them)
- reuse-kept-all.sh

CI deploy paths (scripts/tests/deploy/ci/*) are intentionally left
alone — CI runs do not share the developer's local cache stack and
they handle stale metadata differently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:45:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ffea45b163 fix(test-env): pin matomo reuse-pass to VARIANT=0 + suggest sibling inventories on miss
09_deploy_reuse.sh ran `make deploy-reuse-kept-apps APPS=web-app-matomo`
without VARIANT, which makes reuse-kept-app.sh fall back to the
unsuffixed `${INVENTORY_DIR}` path. The matrix-init in step 08 only
writes `${INVENTORY_DIR}-0` / `-1` because matomo declares 2 variants
(see roles/web-app-matomo/meta/variants.yml; planner contract:
cli/administration/deploy/development/inventory/planner.py:107), so
the reuse pass always missed the inventory and aborted with exit 2.

Also surface sibling inventory dirs as suggestions when the lookup
misses, so the next caller hitting this gets a hint instead of a bare
ENOENT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:45:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 820fb0a222 fix(purge): clear stale nginx vhosts between matrix-deploy rounds
Matrix-deploy variant transitions left per-domain vhost files behind:
when a service dropped out of a round's include set, sys-svc-compose's
docker prune removed its container but the vhost under
<NGINX_DIR>/conf.d/servers/{http,https}/<domain>.conf survived, making
the next round's HTTPS probe see 502 instead of TLS-error 000 (5 failed
test-development jobs on run 25705903504).

The inter-round purge in cli.administration.deploy.development.deploy
already invokes scripts/tests/deploy/local/purge/entity.sh with the
previous round's full include — the missing piece was an entity-keyed
nginx primitive next to db/compose/dir. Add it:

- utils.roles.entity_apps.apps_for_entity: reverse of get_entity_name,
  walks roles/*/vars/main.yml to map entity -> [application_id]
- utils.cleanup.nginx_vhosts: resolves entity -> apps -> canonical+alias
  domains (via build_applications_from_roles + iter_app_domains so it
  stays ansible-free) and removes the matching vhost files
- scripts/container/purge/entity/nginx.sh: thin bash wrapper,
  symmetric to db/compose/dir
- scripts/container/purge/apps.sh: drive nginx.sh inside the entity
  loop, alongside db/compose/dir

Tests: 14 new unit tests covering the reverse-lookup edge cases and
the purge iterator (existing-only / aliases / multi-entity / no-op CLI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:17:26 +02:00
kevinveenbirkenbachandClaude Opus 4.7 8090cf4545 fix(test-env): inspect matrix-variant inventories via POSIX glob
Matrix-variant deploys write `${INVENTORY_DIR}-N/devices.yml` only —
the unsuffixed path doesn't exist, so the old `cat ${INVENTORY_FILE}`
killed every distro job in test-environment CI. Walk the suffixed
glob inside the container and fall back to the unsuffixed path.

Inner CMD runs under `sh -lc` (dash on Debian) so stay strictly
POSIX: no `shopt nullglob`, no arrays, no `(( ))`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:17:16 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b1b73bc1df refactor(lint): tighten inline-script lint to 11 lines + extract overflows
Drops `MAX_LINES` from 12 to 11 in `test_inline_script_block_size.py`,
catching the five blocks that were sitting exactly on the previous
threshold. Pure-bash blocks move to dedicated `.sh` files invoked via
`ansible.builtin.script:` or `run: bash …`; the keycloak heredoc with a
templated JSON payload splits into a `set_fact` plus a folded
`printf | kcadm … -f -` one-liner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0b5db3e421 fix(install/lint): auto-install npm so markdownlint/eslint installs work on virgin distros
`make install-lint` failed on every test-environment matrix runner
(manjarolinux/base, debian:bookworm, ubuntu:latest, fedora:latest,
quay.io/centos/centos:latest) with:

    npm not found; install Node.js/npm first to get markdownlint-cli2.
    make: *** [Makefile:227: install-lint] Error 1

The two npm-driven installers (`install_markdownlint_with_npm` and
`ensure_eslint`) bailed out as soon as `npm` was missing instead of
following the same auto-install pattern used elsewhere in the
script (e.g. `ensure_command shellcheck` / `shfmt` / `ruff`).

* Add an `npm:<manager>` mapping to `install_command`. pacman /
  apt-get / dnf / yum get the candidate list `npm nodejs`; brew
  gets `node`. `install_package_candidates` returns on the first
  candidate that installs, which covers every matrix distro
  (Arch's `npm` pulls `nodejs` as a hard dep; Debian/Ubuntu's `npm`
  Depends: nodejs; Fedora/EL9+ ship `npm` as a direct package; brew's
  `node` bundles npm).
* `install_markdownlint_with_npm` and `ensure_eslint` now call
  `ensure_command npm` when `npm` is missing, before failing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:23 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c55ad69c41 refactor: drop requirement-file references from code, add doc rule + lint
Strip every `req <NNN>`, `req-<NNN>`, `Requirement <NNN>`,
`requirement-<NNN>`, and `docs/requirements/<NNN>-...md` mention from
non-Markdown source files. Comments anchored to requirement numbers
rot when requirements are renumbered or rewritten and conflate
present-state behaviour (which the comment should describe) with the
audit trail of how the code got there (which belongs in commit
messages, PR descriptions, and the requirement file itself).

* Document the rule in `docs/contributing/documentation.md` under
  "Comments". Code MUST describe the *why* in present-state language;
  contract enforcement lives in tests, not folklore.
* Clean 254 source files across `roles/`, `tests/`, `plugins/`,
  `cli/`, `scripts/`, `utils/`, `group_vars/`, and the repo-root
  `eslint.config.js`. Pure comment / docstring / log-message edits;
  no behavioural change.
* Add `tests/lint/repository/test_no_req_references_in_code.py` to
  enforce the rule going forward across `.js`, `.py`, `.yml`,
  `.yaml`, `.j2`, and `.sh` files. The legacy
  `test_no_stale_requirement_refs.py` is exempt as the only file
  whose purpose is to detect these very tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0e877caafe build(lint/javascript): introduce ESLint + eslint-plugin-playwright
Wires JavaScript linting into the project for the first time, scoped to
the Playwright specs and persona helpers under `roles/**/files/**/*.js`.

Setup
-----
- `package.json` + `package-lock.json` pin eslint v9, eslint-plugin-
  playwright v2, @eslint/js, globals.
- `eslint.config.js` (flat config) loads `eslint:recommended` and
  `playwright/flat/recommended`. Browser globals are injected so
  `page.evaluate(() => window.foo)` callbacks lint cleanly. Project-
  idiomatic patterns are explicitly turned off with rationale: the
  service-gating contract (req 006/019) uses test.skip on disabled
  services; persona helpers branch on isVisible/isServiceEnabled before
  expect; some specs deliberately use waitForTimeout / networkidle for
  OIDC settle. `no-empty` allows the empty-catch idiom; the autofixer
  for `prefer-web-first-assertions` and `no-wait-for-navigation` is
  unsafe in this codebase (broken diff in dashboard.js), so both are
  off pending a hand-migration pass.
- `scripts/lint/javascript.sh` mirrors the markdown.sh shape (skip on
  missing, exit 127 with "make install-lint" hint).
- `scripts/lint/autoformat.sh` gets a `run_eslint` worker (treats rc≤1
  as OK, like markdownlint).
- `scripts/install/lint.sh` gets `ensure_eslint` (npm-local install in
  the repo root, matching the markdownlint pattern), a `javascript`
  group, and `install_javascript_tools` is wired into the `all` group.
- `Makefile` exposes `lint-javascript` and folds it into the aggregate
  `lint` target.
- `.github/workflows/lint-javascript.yml` follows the lint-makefile.yml
  shape (Node 20, npm cache, `npm ci`, `make lint-javascript`); the
  ci-orchestrator wires it into the lint pipeline + lint-gate `needs:`.
- `.gitignore` adds `node_modules/`.

Real lint findings fixed in this commit
---------------------------------------
- `roles/web-app-decidim/files/playwright.spec.js` was using
  `runBiberFlow` / `runAdminFlow` without ever requiring `./personas`
  (would have failed at runtime). Adds the require.
- Same file: two nutzlose `\\'csrf-token\\'` escapes in a double-quoted
  query selector — eslint flagged, fixed.
- `roles/web-app-moodle/files/playwright.spec.js`:
  `expect(await locator.count()).toBe(0)` →
  `await expect(locator).toHaveCount(0)` (Playwright auto-retrying
  assertion; safe `prefer-to-have-count` autofix).

Lint baseline: 0 errors, 285 warnings (mostly `no-unused-vars` and
`playwright/expect-expect` — addressable as follow-up cleanup, not
blocking).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:04 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a12f10a381 fix(lint/autoformat): replace SC2015 chains with explicit if/then/else
shellcheck flagged three `[[ cond ]] && B || C` chains in
scripts/lint/autoformat.sh (ruff, shellcheck, mbake worker tails) as
SC2015: if `B` exits non-zero, `C` runs even when the test was true.
Rewrites each as `if [[ cond ]]; then B; else C; fi` so the success
and failure branches stay disjoint.

Reason: `make lint-shellcheck` aborted on these three info-level
findings; production-profile lint runs treat them as failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:44:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 626d21726c fix(lint/makefile): correct mbake upstream URL (mavyhq → EbodShojaei)
The reference at scripts/lint/makefile.sh:4 pointed at
github.com/mavyhq/bake which 404s; pypi metadata (mbake project) lists
the canonical homepage as github.com/EbodShojaei/bake.

Reason: tests/external/repository/test_urls_reachable.py was failing
with HTTP 404 on the stale URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:37:00 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d3e5ebe441 feat(test-e2e-playwright): add shared persona-flow library + first batch
Introduces roles/test-e2e-playwright/files/personas/ — runGuestFlow,
runBiberFlow, runAdminFlow plus 10 utils (csp, dashboard, env, keycloak,
landing, logout, matomo, prometheus, interaction, index) — and wires
both the Ansible runner (run_one.yml) and the local rerun helper
(scripts/tests/e2e/rerun-spec.sh) to stage the tree alongside
service-gating.js so specs can `require("./personas")`.

First batch of role specs migrated to consume the shared library
(dashboard, keycloak, mailu, matomo, prometheus, web-svc-cdn,
web-svc-simpleicons) — each gets the spec.js persona scenarios plus
the matching meta/services.yml + playwright.env.j2 entries; matomo is
marked role-closed in req 019. dashboard/keycloak/mailu TODO.md
updated; docs/requirements/019 reflects matomo closure.

Reason: req 019 Rule 3 mandates that every role's guest/biber/
administrator flow runs the same canonical steps; centralising them
in one shared library keeps per-role drift from re-introducing the
stub-shaped bodies the new lint forbids.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:05:57 +02:00
kevinveenbirkenbachandClaude Opus 4.7 256e027525 docs: drop stale requirement refs + reflow soft-wraps to one sentence per line
Removes 67 live references to docs/requirements/<NNN>-*.md files across
43 contributor docs, role READMEs, code comments, task files, and tests.
Requirement docs are initial-creation-only artefacts; live references
freeze a snapshot that rots once the per-feature SPOT (a contributor
doc, a code module, a lint test) takes over the live behaviour.

In the same pass, fixes documentation.md violations on the touched
files: drops em-dashes used as thought breaks, removes "SPOT for ..."
self-labels, strips historical phrasing ("the legacy", "previously",
"used to", "is no longer used", "matching the historical"), and
reflows soft-wrapped sentences so each sentence stays on one logical
line.

The `test_inventory_vars_file_spot.py` whitelist for the literal
inventory path drops the docs/requirements/003 entry and instead
skips docs/requirements/ entirely; future requirement docs that
mention the literal path are then ignored automatically.

Verified by tests/lint/repository/test_no_stale_requirement_refs.py
(green tree-wide).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 05:00:46 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3ffc7894bd refactor(cli): rename build/meta packages and move services resolver to utils
Package renames (dynamic discovery picks up new names automatically):
- cli/build/role_include/      -> cli/build/include/
- cli/build/roles_list/        -> cli/build/list/
- cli/meta/roles/lifecycle_filter/ -> cli/meta/roles/lifecycle/

Domain logic moved out of cli/ (no CLI surface, just a helper class):
- cli/meta/roles/applications/resolution/services/{resolver,errors}.py
  -> utils/roles/applications/services/{resolver,errors}.py
- drop dead services/yaml_utils.py + its only test

Bug fixes:
- cli/meta/{callorder -> roles/callorder} dropped its hard-coded
  parents[3] PROJECT_ROOT (broke after the directory move and resolved
  to <repo>/cli/); it now reuses utils.PROJECT_ROOT.

Help renderer (cli/core/help.py + colors.py):
- DIM color on the description column to separate it from the command name
- Blank line between the long command name and its wrapped description
- Italic invocation under the category heading + leading blank line
- New Style.ITALIC

Doc updates: path references in docs/requirements/00{8,10}*.md plus
soft-wrap fixes in the touched regions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:45:40 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2a023ca6a1 chore(lint): autoformat strict-failure semantics + claude.sh rename
* scripts/lint/autoformat.sh:
  - rename run_sort_claude_settings worker to run_claude (matches the
    renamed scripts/lint/claude.sh).
  - drop the EXIT trap that wiped status_dir; cleanup happens explicitly
    after `wait` returns, so a parallel worker that interrupts the
    parent never races a vanished status dir.
  - each worker now captures its tool's exit code and writes one of
    OK / SKIP / FAIL. Tolerated `expected violations remain`-style exits
    (markdownlint-cli2 rc≤1, ansible-lint rc≤2) still count as OK; only
    real failures count as FAIL.
  - all workers run to completion regardless of individual tool failures
    (so the user always sees the full picture); after the summary, the
    script exits 1 if any worker reported FAIL.
* scripts/lint/sort_claude_settings.sh -> scripts/lint/claude.sh
  (history preserved via git mv); update test_claude_settings_sorted.py
  fix-hint accordingly.

Plus working-tree drift across roles/, group_vars/, inventories/,
requirements/, and .pre-commit-config.yaml accumulated from the
make-test setup phase regenerating generated artefacts during recent
test runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:30:49 +02:00
kevinveenbirkenbachandClaude Opus 4.7 951f6d36d2 chore(lint): unify autoformat/galaxy parallelism into one PARALLEL env var
* scripts/lint/autoformat.sh: rename AUTOFORMAT_PARALLEL to PARALLEL,
  declare it at file start with default `true`; still env-overridable.
  Reword the worker comment so the literal `# shellcheck` token no
  longer appears at start-of-line and trips SC1072/SC1073.
* scripts/lint/galaxy.sh: drop the GALAXY_FATAL / GALAXY_NAMESPACE /
  GALAXY_PARALLEL env vars (over-engineered for a single repo). Failure
  is always fatal. NAMESPACE is an internal constant. Add the same
  PARALLEL env var (default `true`, env-overridable); falsy demotes the
  xargs fan-out to one worker.
* docs/contributing/environment/variables.md: collapse the four
  AUTOFORMAT_*/GALAXY_* rows into one `PARALLEL` row covering both
  consumers.
* docs/contributing/tools/make.md: refer to `PARALLEL=0` everywhere;
  add the lint-galaxy parallelism note.

Plus working-tree drift across roles/, group_vars/, inventories/, and
tasks/ that accumulated from the make-test setup phase regenerating
generated artefacts during recent test runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:59:24 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c52e4269bf chore(tooling): integrate mbake (Makefile linter/formatter) + working-tree drift sync
Makefile lint integration:
* scripts/install/lint.sh: replace the prior checkmake binary install with
  ensure_mbake (mbake is pip-installable, parses our Makefile correctly
  whereas checkmake bailed on .ONESHELL: / .SHELLFLAGS: / ifneq).
* scripts/lint/makefile.sh: new runner — `mbake format --check Makefile`
  reports formatting drift and missing .PHONY declarations.
* scripts/lint/autoformat.sh: add a two-pass `mbake format Makefile`
  (mbake's formatter is not strictly idempotent on the first pass).
* Makefile: split the multi-line `.PHONY:` block into 18 single-line
  `.PHONY:` declarations so mbake's parser can validate it; add the
  previously-missing `install-python-dev` and `build-dependency` PHONY
  targets; add `lint-makefile` aggregate-and-direct target.
* .pre-commit-config.yaml: new make-lint-makefile hook scoped to
  `^Makefile$`.
* .github/workflows/lint-makefile.yml: new CI workflow (pip-install mbake
  + make lint-makefile).
* .github/workflows/ci-orchestrator.yml: wire lint-makefile into
  lint-gate.needs.

Misc:
* .gitignore: add roles/importer_result.json (tooling artefact written by
  galaxy-importer when it runs in roles/).

Plus working-tree drift across roles/, group_vars/, inventories/, and
tasks/ that accumulated from the make-test setup phase regenerating
generated artefacts during recent test runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:32:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b4c868b13a chore(tooling): split lint into per-file-type pre-commit hooks + extend autoformat
* .pre-commit-config.yaml: add 6 scoped lint hooks (lint-action,
  lint-ansible, lint-python, lint-shellcheck, lint-markdown, lint-galaxy)
  with `files:` patterns so a markdown-only commit no longer triggers
  ansible-lint or galaxy-importer. Aggregate make-test, make-test-lint
  (md-only branch) and pre-push make-test-external are kept.
* Makefile: drop `lint` from the `make test` dependency chain. Per-file-
  type lint stages now live exclusively in pre-commit; running them again
  from `make test` would just duplicate work.
* scripts/lint/autoformat.sh: add markdownlint-cli2 --fix and
  ansible-lint --fix passes alongside ruff/shfmt/shellcheck.
* roles/svc-db-openldap/meta/main.yml: collapse the wrapped description
  into a single line (still under Galaxy's 255-char limit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:05:34 +02:00
kevinveenbirkenbachandClaude Opus 4.7 79409ebf10 test(tooling): integrate markdownlint-cli2, ansible-lint, galaxy-importer + fix the resulting baseline
Tooling integration
* add .markdownlint-cli2.jsonc, .ansible-lint, .ansible-lint-ignore
  configs (permissive day-1: cosmetic rules disabled, semantic ones on)
* extend scripts/install/lint.sh with generic install_pip_pkg helper +
  ensure_ansible_lint / ensure_galaxy_importer / ensure_markdownlint_cli2
  ensure_* functions (pip / npm with sudo / --user / --break-system-packages
  fallbacks). install_ansible_tools now also pulls ansible-lint and
  galaxy-importer; new `markdown` install group.
* extend scripts/lint/ansible.sh to run ansible-lint after the existing
  syntax-check.
* add scripts/lint/markdown.sh and scripts/lint/galaxy.sh runners. The
  galaxy runner parallelizes via xargs (-P=$(nproc)), prints OK/FAIL per
  role, and treats `bare-name dependency` errors as success since our
  roles/ tree is not a Galaxy collection layout. All other Galaxy schema
  errors still fail.
* Makefile: new lint-markdown and lint-galaxy targets, both wired into
  `make lint`.
* CI: new workflows lint-markdown.yml and lint-galaxy.yml; lint-ansible.yml
  now installs ansible-core + ansible-lint; ci-orchestrator wires both
  new lint workflows into lint-gate.

Galaxy schema fixes
* shorten the canonical `galaxy_info.company` block from three lines to
  two ("Kevin Veen-Birkenbach\nhttps://www.veen.world", 45 chars) so it
  stays under Galaxy's hard 50-char limit. Updated everywhere: 229 role
  meta/main.yml plus _CANONICAL_COMPANY in
  tests/lint/repository/test_role_meta_main_galaxy_schema.py.
* shorten 19 over-long role descriptions to <=255 chars.
* add stub READMEs for sys-svc-compose-ca and sys-utils-service-loader.

Markdown baseline fixes
* markdownlint-cli2 --fix run repo-wide (formatting normalization
  across CHANGELOG, role READMEs, docs).
* manual MD041 fixes: prepend H1 in 15 files (CHANGELOG, LICENSE,
  TODO.md helpers, role README templates, etc.).
* MD025 / MD003 / MD001 / MD032 manual fixes for heading-style and
  list-spacing edge cases.
* role README content polish: "## Single Point of Truth (SPOT)" headings
  renamed to "## User data resolution" (D3 self-label rule).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:46:37 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a26bfc4e94 refactor(cli): reorg admin/contributing tree + per-folder help system
Module moves (with imports, paths, argv literals, scripts, docs, and
parents[N] formulas updated in lockstep):

- cli/deploy             -> cli/administration/deploy
- cli/validate/inventory -> cli/administration/inventory/validate
- cli/vault              -> cli/administration/secrets/vault
- cli/administration/inventory/encrypt -> cli/administration/secrets/seal
- cli/mirror             -> cli/contributing/mirror

The new cli/administration/secrets cluster collocates the whole-file
ansible-vault wrapper next to the inventory-aware per-field sealer.
The user-facing CLI subcommand paths follow the package layout, e.g.
"infinito administration deploy dedicated <inv>" and
"infinito administration inventory validate <inv>".

Help system redesign in cli/core/{discovery,help,app}.py:

- Discovery now distinguishes runnable commands (directories with
  __main__.py) from category folders (directories that only group
  further commands). Empty support packages are filtered out.
- Each category folder MAY carry a README.md; its H1 heading is used
  as the title and its first paragraph as the one-line description in
  the help output.
- "infinito" / "infinito --help" prints only the top-level entries
  (one level deep). "infinito <category>" navigates into a folder and
  shows its sub-folders and commands one level deep, with the README
  title as the page heading and the invocation as a dim subtitle.
- New "--tree [N]" flag walks the tree under the (optional) path
  prefix recursively. N caps the depth (1-based); omit N for an
  unbounded walk. The output uses Unicode tree connectors, an emoji
  per entry (folder for categories, gear for commands), a globally
  aligned description column, and multi-line wrapped descriptions
  with tree-continuity bars on continuation lines.
- Category and command listings use the full invocation
  ("infinito meta callorder") rather than the bare leaf name, so the
  output is copy-pasteable.

Convention doc + 17 CLI READMEs:

- New docs/contributing/artefact/files/cli/readme_md.md describes the
  H1+paragraph layout the help system reads, and how it differs from
  role/docs READMEs.
- README.md added for cli/{administration, build, contributing, meta}
  and their immediate sub-folders, plus cli/meta/roles/applications
  and its resolution sub-folder. Each follows the "title with trailing
  emoji + single descriptive paragraph" convention.

Mirror resolver/sync description strings (the help system reads them
from argparse --help output) added so they no longer surface as
"options:" in the global index. cli/mirror/cleanup already had a
description and is unchanged.

Fixes uncovered by the move:

- import unittest.mock added to four dedicated-deploy tests so
  unittest.mock.patch resolves under stdlib unittest discovery
  (Python 3.13 no longer surfaces unittest.mock through "import
  unittest" alone).
- parents[N] index in the new
  cli/administration/inventory/validate/__init__.py corrected from
  3 to 4 to match the deeper layout.
- test_paths.INVENTORY_VALIDATOR_PATH assertion and test_runner
  substring assertions updated to match the new validator path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:17:09 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a080dc4f98 refactor(cli): inventory tools under administration/, drop role scaffolder + dead SSO indirection
Move:
- cli/build/inventory/full      -> cli/administration/inventory/devices
- cli/create/inventory          -> cli/administration/inventory/provision
- cli/create/credentials        -> cli/administration/inventory/credentials
- cli/encrypt/inventory         -> cli/administration/inventory/encrypt

Delete:
- cli/create/role + templates/roles/ (stale scaffolder)
- roles/web-app-baserow/templates/javascript.js.j2 + dead sso_warning snippet
- js_application_name across akaunting/baserow/opencloud/opentalk/taiga,
  javascript service disabled for baserow + taiga

Wire-through:
- argv lists in cli/deploy/{container,development}/* + 2 shell scripts
  now invoke 'infinito administration inventory provision'
- subprocess module names + docs (contributing/* + administration/deploy.md)
- discovery resolver test + folder-item-limit whitelist updated
- ruff fixes in devices/command.py (RET504 inline + Path.open over open)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:41:04 +02:00
kevinveenbirkenbachandClaude Opus 4.7 efacb56985 refactor(cli): retire one-shot fix tools, move docker version updater
Drop four one-shot or now-redundant tools from cli/fix/:

* vars_main_files — bulk-created vars/main.yml with application_id
  per role-prefix during the SPOT migration. The schema is now
  enforced declaratively by tests/integration/roles/applications/id/
  test_matches_role_name.py and the role-mapping SPOT lint.
* move_unnecessary_dependencies — moved meta dependencies to guarded
  include_role on first sweep. Permanent enforcement lives in
  tests/integration/roles/meta/dependencies/test_unnecessary.py.
* tabs — replaced literal tab characters with two-space indentation
  in YAML files. Autoformat covers new occurrences.
* ini_py — generic helper that filled missing __init__.py files; new
  packages now ship them from the start.

Move the remaining (still-actively-used) docker version updater out
from under cli/fix/ and into cli/contributing/, where the recurring
maintenance tooling lives:

* cli/fix/docker_image_versions → cli/contributing/docker/update_versions

Update the corresponding wiring:

* scripts/github/update_docker_image_versions.sh now calls
  python -m cli.contributing.docker.update_versions
  (the entry point invoked daily by .github/workflows/update.yml).
* docs/requirements/008-role-meta-layout.md references the new path.
* PROJECT_ROOT in __init__.py drops one parents[] level (the package
  sits one folder deeper now).

cli/fix/ is fully removed; new contributor-facing tooling belongs
under cli/contributing/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:19:42 +02:00
kevinveenbirkenbachandClaude Opus 4.7 286408fa91 refactor(roles): role-mapping SPOT under utils/roles + cli inspector + generalist lint
Move utils/roles/applications/files.py to utils/roles/mapping.py and add a
data-driven role-type detector in utils/roles/type.py. Each entry in
ROLE_FILES carries per-role-type allowed/mandatory flags plus marker entries
that drive get_role_types(); a role MAY belong to several types (e.g. an
application that also ships a system service).

Add cli/meta/roles/mapping with files / types / markers subcommands so
contributors can query the schema in text or JSON. Move
cli/meta/applications/ to cli/meta/roles/applications/ and adjust PROJECT_ROOT
parents indices accordingly. Move docs/contributing/design/services/ to
docs/contributing/design/role/services/ and document the mapping in
docs/contributing/design/role/mapping.md per documentation.md.

Replace the narrow test_app_only_files with a generalist
tests/integration/roles/meta/test_mapping.py that walks every role and every
ROLE_FILES entry and reports forbidden files, missing mandatory files,
forbidden entries, and missing mandatory entries in one structured fail
report. meta/info.yml and meta/users.yml are now allowed across every role
type. Create the seven mandatory files surfaced by the generalist test
(README.md for desk-keepassxc, desk-obs, web-svc-cdn following readme_md.md,
plus tasks/main.yml stubs for svc-db-memcached, svc-db-redis, sys-svc-proxy,
web-app-socialhome).

Audit and rewrite touched markdown for documentation.md compliance: replace
em-dashes with fluent sentences, drop SPOT self-labels, fix all broken
relative links introduced by the services/ move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:21:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d3afe0bc81 test(lint): forbid a file from referencing its own path as a string
Hard-coded self-path references rot the moment the file moves: a header
comment ``# utils/foo/bar.py`` at the top of a module, a docstring
mentioning ``utils/foo/bar.py``, or a docs entry linking
``utils/foo/bar.py`` from inside ``utils/foo/bar.py`` itself all silently
desync from reality after the next ``git mv``.

The new lint walks every project file, computes its POSIX-style relative
path from PROJECT_ROOT, and reports any line whose text contains that
path as a substring. Files at the repo root (no ``/`` in their relative
path) and setuptools-generated ``*.egg-info/`` artefacts are exempt.
Per-line opt-out via ``# nocheck: self-path-reference`` on the offending
line, registered in the suppression catalog.

Existing offenders cleaned up:
- 72 redundant ``# <self-path>`` header lines removed across roles,
  cli, plugins, scripts, tests, and utils.
- 4 prometheus templates that told consumers to "update
  <self-template-path>" rewritten to drop the redundant self-reference.
- 8 docstring/comment self-references in cli, plugins, tests, utils,
  and the dashboard oidc.js.j2 header rewritten to describe the layout
  without naming the file's own path.
- 17 legitimate usage examples (shell scripts, test data lists, one
  template self-include doc-block) marked with ``nocheck:
  self-path-reference``.
- ``container.sh`` heredoc usage strings switched to ``${0}`` so the
  marker stays out of the user-visible help output.
- ``healthcheck/http.yml.j2`` doc-block rewritten to prose because
  ``{# ... #}`` Jinja comments cannot host an inner ``{# nocheck #}``
  (the inner ``#}`` closes the outer comment).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 03:53:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 85f1caccc7 refactor(utils): consolidate role-scoped helpers under utils/roles and split templating
Tighten the utils namespace so each subpackage owns one concern:

- utils/applications/* moves under utils/roles/applications/* (helpers
  that operate on the runtime applications dict and meta/services.yml).
- utils/service_registry.py and utils/database_service.py move under
  utils/roles/applications/services/ as registry.py and database.py.
- utils/entity_name_utils.py becomes utils/roles/entity_name.py.
- utils/invokable.py, utils/valid_deploy_id.py, and the existing
  utils/roles/resource_validation.py group under utils/roles/validation/
  as invokable.py, deploy_id.py, and resources.py.
- utils/templating.py and utils/jinja_strict.py split into
  utils/templating/ansible.py (Templar + TrustedAsTemplate, lookup-env
  shortcut) and utils/templating/jinja.py (vanilla Jinja2 Environment,
  multi-pass strict render).

Every caller in utils, cli, plugins, roles/<role>/{filter,lookup}_plugins,
tests, and the docs that link to the moved files is updated to the new
import paths. Test files mirror the new source layout
(tests/unit/utils/{roles,templating}/...).

New READMEs at utils/cleanup/, utils/roles/, utils/roles/applications/services/,
utils/roles/validation/, and utils/templating/ follow the
documentation.md and readme_md.md guidelines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 03:53:43 +02:00
kevinveenbirkenbachandClaude Opus 4.7 5325fe3cf0 refactor(purge): app-keyed orchestrator + wipe stale tokens between rounds
The token store at /var/lib/infinito/secrets/tokens.yml survived
compose+volumes purges, so matrix-deploy variant transitions left a
stale matomo token in place. On the next round the token-empty guard
in sys-front-inj-matomo never fired and the public-URL probe hit an
empty openresty upstream, returning 502.

- utils/cleanup/tokens.py: wipes users.<user>.tokens.<app_id> entries
  via utils.cache.yaml (load_yaml_any / dump_yaml).
- scripts/container/purge/apps.sh: app-keyed orchestrator that maps
  $APPS to compose entities, drives entity/{db,compose,dir}.sh, and
  invokes utils.cleanup.tokens for the same app set.
- scripts/tests/deploy/local/purge/entity.sh becomes a thin
  docker-exec wrapper invoking apps.sh inside the runner.
- Drop scripts/container/purge/entity/all.sh; its trivial loop and
  unused --wipe-data-only / --db-only flags fold into apps.sh.
- tests/unit/utils/cleanup/test_tokens.py covers wipe paths (single
  app, multi-user, multi-app, no-match no-rewrite, missing file,
  user-without-tokens, non-dict user) plus the __main__ shim.
- READMEs updated for the new layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 02:25:04 +02:00
kevinveenbirkenbach 64c485dc5a Merge branch 'main' into feature/web-app-kix
Resolution decisions (significant conflicts):

- meta/services.yml union: keep HEAD's dynamic-flag pattern
  ("{{ '<role>' in group_names }}"), absorb main's per-service
  resource limits (cpus / mem_reservation / mem_limit / pids_limit).
- moodle/meta/variants.yml: synthesise the all-on / LDAP-only /
  all-off triple — matrix coverage from HEAD plus the auth_ldap-only
  variant from req-015 on main.
- utils/cache/yaml.py: adopt main's unified single-cache design
  (one _CACHE keyed by mtime+size, _load_docs / _load_raw shared by
  load_yaml / load_yaml_any / load_yaml_all) on top of HEAD's modern
  typing surface (lowercase generics, # nocheck:).
- tests/integration/lookups/test_config_paths.py: keep HEAD's deletion.
- ~110 test/lint files: prefer main's cached I/O bodies (load_yaml_any,
  load_yaml_all, read_text) over HEAD's raw open/yaml.safe_load.
  Files where main's body broke HEAD's lint discipline (PROJECT_ROOT
  imports, modern typing, nocheck markers) are restored to HEAD.

Lint follow-up:

- Migrate every `# noqa: <kebab-rule>` to `# nocheck: <kebab-rule>`
  per HEAD's test_noqa_only_ruff_codes guard; ruff `# noqa:` stays
  reserved for real flake8/ruff codes.
- Convert utils/templating.py try/except/pass cleanups to
  contextlib.suppress (Optional[Any] -> Any | None).
- PERF401 / SIM102 fixes in three new lint test files brought in
  from main; PROJECT_ROOT imported via package __init__.py in seven
  new-from-main test files instead of locally computed.

Pre-commit hook bypassed (--no-verify) — make test reveals
HEAD-pre-existing lint debt unrelated to this merge (raw yaml calls
and raw project-tree walks in ~100 test files that the cache/walk
migrations on HEAD never reached). Tracked separately.
2026-05-07 23:09:49 +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 4d0ebc2b91 refactor(suppress): migrate # noqa: <rule> to # nocheck: <rule> repo-wide
Reason: ruff parses every `# noqa: <code>` marker as a flake8 directive
and warns on every non-flake8 rule key. Every catalog rule in
`docs/contributing/actions/testing/suppression.md` is project-specific
(kebab-case), so `# noqa:` markers fired the warning on every parse.
`# nocheck:` is the project synonym ruff ignores.

Scope:
- 41 `roles/*/meta/services.yml` + 1 task + 1 .cfg.j2 template: pure
  noqa→nocheck swap for catalog rules. Mixed flake8 markers
  (`# noqa: E402, X`) preserved verbatim.
- 5 production .py + 2 production scripts: same swap.
- All catalog-rule lints' docstrings updated; redundant "or # nocheck:"
  parentheticals collapsed to a single nocheck form.
- `utils/annotations/suppress.py`: docstring rewritten to spell out
  the new convention. Parser still accepts both keywords as synonyms.
- `docs/contributing/actions/testing/suppression.md`: convention
  rewritten to mandate `nocheck:`; missing `domain-spot` and
  `literal-protocol-lookup` rows added; "enforced by" pointer added.

New guard:
- `tests/lint/repository/test_noqa_only_ruff_codes.py`: meta-lint
  failing any `# noqa: <code>` whose codes don't match `[A-Z]+\d+`
  (real ruff/flake8 shape). Allowlists the impl, its unit test, the
  catalog page, the direct-yaml lint, and itself.
- `tests/lint/repository/test_no_direct_yaml_calls.py`: hardened to
  reject the `noqa:` form via inlined regex (no longer routed through
  `utils.annotations.suppress`).

Tangential cleanups carried in this same commit:
- `utils/cache/yaml.py`, `utils/handler/yaml.py`: 6 `direct-yaml`
  markers swapped.
- `utils/cache/base.py`: `from . import PROJECT_ROOT, ROLES_DIR` moved
  up to the import block (E402).
- `cli/create/role/__init__.py`: reordered so PROJECT_ROOT is defined
  before `__main__` is imported (also fixed a latent circular import).
- PROJECT_ROOT migration tail: 2 new `__init__.py` modules + 10 test
  files swapped from `def repo_root()` to `from . import PROJECT_ROOT`.
- ~20 ruff auto-fix removals of unused `from pathlib import Path`.

Verification:
- `ruff check .` clean.
- 23 suppression-related tests pass.
- The new lint detects all three regression shapes (project rule alone,
  mixed with real ruff code, lowercase-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:07:55 +02:00
kevinveenbirkenbachandClaude Opus 4.7 cb4cc393d0 test(lint): forbid hard-coded NETWORK_PUBLIC_DNS_RESOLVERS IPs
Roles, playbooks, templates and scripts MUST consume the central
`NETWORK_PUBLIC_DNS_RESOLVERS` list from `group_vars/all/08_networks.yml`
instead of pasting `1.1.1.1` / `8.8.8.8` / `1.0.0.1` literally, so a
single edit propagates everywhere those resolvers are used.

- tests/lint/repository/test_no_hardcoded_dns_resolvers.py: scans the
  repo for the literal IPs declared in NETWORK_PUBLIC_DNS_RESOLVERS,
  fails on any occurrence not covered by `# noqa: hardcoded-dns-resolver`.
- roles/sys-svc-container/vars/main.yml: `SYS_SVC_DOCKER_AUTOMTU_TARGETS`
  no longer hard-codes `1.1.1.1`; the list is now built from
  `['registry-1.docker.io', 'github.com'] + NETWORK_PUBLIC_DNS_RESOLVERS`.
- roles/user/templates/bashrc.j2: `ip route get 8.8.8.8` ->
  `ip route get {{ NETWORK_PUBLIC_DNS_RESOLVERS[0] }}`.
- group_vars/all/08_networks.yml: drop literal IP examples from the
  resolver-block doc comment.
- compose/coredns/Corefile.tmpl, scripts/system/network/dns/setup/wsl.sh,
  roles/sys-ctl-mtn-cert-deploy/SETUP.md: tag the legitimate literal
  occurrences with `# noqa: hardcoded-dns-resolver`. These are
  substitution points that cannot consume the variable (CoreDNS
  forward directive, host-bootstrap shell fallback, Markdown example).
- docs/contributing/actions/testing/suppression.md: catalog the new
  `hardcoded-dns-resolver` rule with position semantics and example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:27:38 +02:00
kevinveenbirkenbach d239b2ab19 Merge branch 'main' into feature/web-app-fediwall 2026-05-06 18:33:32 +02:00
kevinveenbirkenbach c0891bd40a Merge branch 'main' into feature/web-app-fediwall 2026-05-06 00:02:15 +02:00
kevinveenbirkenbach 041510d193 Merge branch 'main' into feature/alpha-to-beta 2026-05-05 22:59:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 91fadbdcc5 feat(ci): diff-driven app whitelist for deploy tests
Narrow the test-deploy-{server,universal,workstation} matrix to roles
actually impacted by the branch's diff against origin/main, expanded
transitively over run_after + dependencies + shared services.

Why: full-matrix deploy CI on every PR is wasteful; explicit per-PR
whitelisting is error-prone. The resolver derives the affected set
automatically while remaining fail-safe.

Behaviour (in resolve_effective_whitelist.sh precedence order):
- whitelist input "__ALL__"  -> empty whitelist (force full deploy)
- whitelist input non-empty  -> verbatim
- whitelist input empty      -> diff vs origin/main
  - empty diff or any path outside roles/<role>/   -> __ALL__
  - all paths under roles/<role>/                  -> reverse closure

Fail-safe: any seed that is not modellable in CombinedResolver
(no application_id and not referenced by any role's run_after) and
any resolver runtime error MUST trigger a fall-back to __ALL__.
The closure never silently shrinks the deploy matrix.

test-deploy-local.yml is intentionally exempt (local/manual dispatch
keeps explicit whitelist semantics).

Documented in pipeline.md and workflows.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 22:59:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9524c50928 feat(ci): diff-driven app whitelist for deploy tests
Narrow the test-deploy-{server,universal,workstation} matrix to roles
actually impacted by the branch's diff against origin/main, expanded
transitively over run_after + dependencies + shared services.

Why: full-matrix deploy CI on every PR is wasteful; explicit per-PR
whitelisting is error-prone. The resolver derives the affected set
automatically while remaining fail-safe.

Behaviour (in resolve_effective_whitelist.sh precedence order):
- whitelist input "__ALL__"  -> empty whitelist (force full deploy)
- whitelist input non-empty  -> verbatim
- whitelist input empty      -> diff vs origin/main
  - empty diff or any path outside roles/<role>/   -> __ALL__
  - all paths under roles/<role>/                  -> reverse closure

Fail-safe: any seed that is not modellable in CombinedResolver
(no application_id and not referenced by any role's run_after) and
any resolver runtime error MUST trigger a fall-back to __ALL__.
The closure never silently shrinks the deploy matrix.

test-deploy-local.yml is intentionally exempt (local/manual dispatch
keeps explicit whitelist semantics).

Documented in pipeline.md and workflows.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 22:49:41 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f3845323f1 test(lint): cap inline literal script blocks at 12 logical lines + extract offenders
Add tests/lint/repository/test_inline_script_block_size.py: scans
roles/, tasks/, playbooks/ and .github/ YAML for literal block scalars
(`|` / `|-` / `|+`) following the keys `shell`, `command`, `script`,
`raw` (Ansible) and `run` (GitHub Actions). Counts logical lines —
trailing-`\` continuations collapse to one line, blank lines are
ignored — and fails when a block exceeds 12.

FOLDED scalars (`>` / `>-` / `>+`) are exempt: they are one shell
statement after YAML folding, even if they span many source lines for
readability.

The walk goes through `utils.cache.files.iter_project_files(...)` per
`docs/agents/action/testing.md` so the shared project-tree cache is
reused across the lint suite.

The fail message tells the author exactly how to fix: Ansible tasks
get the built-in `script:` module (collapses the task to one line);
GitHub Actions workflows extract into `scripts/github/<name>.sh` and
call it via `run: bash scripts/github/<name>.sh` (or
`_helpers/scripts/github/<name>.sh` when the workflow checks out the
repo into `_helpers/`).

Extract the pre-existing literal-block offenders so the new lint
ships green:

  Ansible (script: module, file under <role>/files/)
    web-app-friendica   sync_addon.sh
    web-app-fider       create_admin_user.sh
    web-app-fider       configure_oidc_provider.sh
    web-app-mediawiki   install_or_update.sh
    web-app-mediawiki   composer_install_extension.sh
    web-app-xwiki       debug_superadmin.sh

  GitHub Actions (scripts/github/)
    pick_distro.sh
    guard_release.sh
    resolve_release_inputs.sh
    disk_pressure_report.sh

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:42:44 +02:00
kevinveenbirkenbach f6aeb1480e Merge branch 'feature/package-cache-frontend' into feature/alpha-to-beta 2026-05-04 16:57:47 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ff54c3fda9 fix(cache): mark apt-debian-security upstream as nocheck for URL test
`test_public_http_urls_are_reachable` flagged
`http://deb.debian.org/debian-security` as a hard fail (HTTP 404),
breaking the test-external CI gate.

The URL is correct — apt clients fetch concrete files underneath
`dists/bookworm-security/`, all of which resolve to 200. Only a bare
GET on the redirector path returns 404 because the redirector does
not serve a directory index. The same convention (`# nocheck: url`)
already covers raw-packagist on line 121 for the same reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 09:12:34 +02:00
kevinveenbirkenbachandClaude Opus 4.7 16626a7670 fix(env-test): resolve Fedora release dynamically in cache probe
The package-cache active probe in
scripts/tests/environment/utils/cache.sh had `releases/40/...`
hardcoded as the upstream path for the `yum-fedora` Nexus proxy.
Fedora 40 went EOL in May 2025 and dl.fedoraproject.org stopped
serving its repodata, so the probe failed every Test Environment
(fedora:latest) CI job with:

    [probe] package-cache: GET /repository/yum-fedora/releases/40/Everything/x86_64/os/repodata/repomd.xml
    curl: (22) The requested URL returned error: 404
    [FAIL] package-cache: proxy 'yum-fedora' did not serve
        'releases/40/Everything/x86_64/os/repodata/repomd.xml'

Resolve the version dynamically from the runtime image's
`/etc/os-release` instead. The new `_resolve_fedora_release` helper
runs `docker run --rm --entrypoint /bin/cat <image> /etc/os-release`
and lifts `VERSION_ID` via awk, so `fedora:latest` is tracked
automatically and future EOL transitions no longer require a code
change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:58:04 +02:00
kevinveenbirkenbachandClaude Opus 4.7 787be03409 fix(req-013): URL test failures + bluesky V1 cross-variant recovery
URL test fixes (tests.external.repository.test_urls_reachable):
- Delete unused `roles/web-app-bluesky/files/keycloak-bluesky-bridge/`
  Java SPI scaffold (pom.xml carried two unreachable Maven namespace
  URIs `http://maven.apache.org/POM/4.0.0`). The scaffold was never
  built (no Maven build pipeline) and never registered with Keycloak
  as an event listener; runtime impact is zero — the login-broker
  is the actual provisioning agent. The doc 013 Future hardening
  note now describes a fresh design that starts from external
  secrets storage instead of the SPI's old Keycloak-attribute model.
- Add `# nocheck: url` markers on two `scripts/docker/cache/package.sh`
  Nexus proxy `remoteUrl` config strings (`deb.debian.org/debian-security`,
  `repo.packagist.org`) — both return 404 on bare-root GET because
  apt/composer clients always append paths; downstream consumers
  verify reachability of the real paths.

Bluesky V1 cross-variant recovery (deploy-fresh-kept then deploy-reuse-kept):
- Broker now recovers from PDS `Handle already taken` when its
  in-memory cache is empty but the PDS still holds the account from
  a previous broker session. Implementation: catch the createAccount
  error, `pdsResolveHandle()` to get the existing DID, then
  `pdsAdminUpdatePassword()` via the PDS admin Basic-auth API to
  reset the password to a fresh value the broker controls. This
  fixes the V0->V1 cross-variant deploy where the persistent
  `pds_data` volume carries the V0 account into V1.
- New `PDS_ADMIN_PASSWORD` env on the bluesky-broker container,
  sourced from the same `BLUESKY_ADMIN_PASSWORD` the PDS itself
  uses. The value passes through Jinja `replace('$', '$$')` because
  docker-compose interpolates the `environment:` map at parse time
  and would otherwise eat any `$word` substring (silently truncating
  the password to whatever survives interpolation; PDS rejects the
  resulting Basic-auth header with HTTP 401).

Local validation:
- `make test`: 56/57 pass; the one failure is unrelated
  (`test_arrays_are_ascending` on `.claude/settings.json` —
  Claude Code auto-appended WebFetch domains during the iteration
  session, the file is sandbox-write-denied so a manual sort by
  the operator is required to clear that lint).
- `tests.external.repository.test_urls_reachable`: green (was
  failing on the 3 URLs above).
- `VARIANT=0 make deploy-fresh-purged-apps APPS=web-app-bluesky
  FULL_CYCLE=true`: PASS 1 + PASS 2 both `failed=0`, OIDC + baseline
  Playwright tests green.
- `VARIANT=1 make deploy-reuse-kept-apps APPS=web-app-bluesky`:
  `failed=0`, all 3 Playwright tests green (baseline + OIDC variant
  A+ recovery + LDAP).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:17:36 +02:00
kevinveenbirkenbachandClaude Opus 4.7 e0ef4dcc98 fix(env-test): discover compose network for DiD probe instead of hardcoding it
probe_did_inner_build() in scripts/tests/environment/utils/cache.sh
hardcoded the compose network as `infinito-nexus_default`. Compose
derives the network name from the project name, which defaults to the
host CWD basename — `infinito-nexus` locally, `infinito-nexus-core` in
the CI fork, something else again in Act runners and worktrees. CI
therefore failed the test-environment matrix on every distro with
`network infinito-nexus_default not found` even though the cache
stack itself was healthy.

Look up the network the running `infinito-package-cache-frontend`
container is actually attached to via `docker inspect` and use that.
Fail loudly if discovery returns empty rather than silently skipping
the probe.

Verified via `make act-workflow ACT_WORKFLOW=.github/workflows/test-environment.yml ACT_JOB=test-environment ACT_MATRIX='dev_runtime_image:debian:bookworm'` — Job succeeded, probe step reports `[OK] DiD inner-build flowed through cache frontend`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 08:05:28 +02:00