From fcc98bcad1effbbef6cb9ec6a12aeafc693a7092 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 25 Sep 2026 18:42:23 +0200 Subject: [PATCH] fix(docs): require a README behind every directory link The lint accepted a link that resolved to a file or a directory. myst builds a document reference only when the target is_file(), so a link to a directory without a README is a bare file listing on GitHub and a dead cross-reference in the documentation build. It now reports which of the two reasons applies. The tightened rule found 41 links. Where a README backs the directory, bd15eae9fa's resolver carries the link and it stays. Where none exists, the link becomes a code span rather than an invented README: 15 of them point at a role's meta/addons/, whose addon table already sits below the sentence, and the rest at code directories such as utils/ or cli/administration/deploy/runner/. The changed sentences are msgids, so these entries return to the translator on the next extract. Co-Authored-By: Claude Opus 5 (1M context) --- cli/contributing/network/README.md | 4 +-- docs/agents/action/develop.md | 2 +- docs/agents/action/pull-request.md | 2 +- .../artefact/files/plugins/lookup/users.md | 2 +- docs/contributing/design/architecture.md | 4 +-- docs/contributing/design/iam/common.md | 2 +- docs/contributing/design/iam/oidc.md | 4 +-- .../contributing/design/role/services/base.md | 6 ++--- docs/contributing/environment/variables.md | 4 +-- docs/requirements/014-svc-runner-ci.md | 2 +- docs/requirements/023-docker-swarm-nfs.md | 2 +- roles/dsk-chromium/README.md | 2 +- roles/dsk-firefox/README.md | 2 +- roles/dsk-gnome/README.md | 2 +- roles/update/README.md | 2 +- roles/web-app-discourse/README.md | 2 +- roles/web-app-docs/README.md | 2 +- roles/web-app-friendica/README.md | 2 +- roles/web-app-joomla/README.md | 2 +- roles/web-app-matrix/README.md | 2 +- roles/web-app-mediawiki/README.md | 2 +- roles/web-app-nextcloud/README.md | 4 +-- roles/web-app-odoo/README.md | 4 +-- roles/web-app-pixelfed/README.md | 2 +- roles/web-app-pretix/README.md | 2 +- roles/web-app-wordpress/README.md | 2 +- .../files/mu-plugins/README.md | 2 +- roles/web-app-xwiki/README.md | 2 +- .../documentation/test_markdown_links.py | 25 +++++++++++++++++-- tests/unit/python/utils/certs/README.md | 4 +-- utils/cache/README.md | 2 +- utils/env/README.md | 2 +- utils/install/README.md | 2 +- 33 files changed, 64 insertions(+), 43 deletions(-) diff --git a/cli/contributing/network/README.md b/cli/contributing/network/README.md index aae887512d..7da680660a 100644 --- a/cli/contributing/network/README.md +++ b/cli/contributing/network/README.md @@ -6,11 +6,11 @@ Contributor-side network tooling: probe the live container network stack, sugges | Path | Purpose | |---|---| -| [diagnose/](diagnose/) | Run `python -m cli.contributing.network.diagnose` to print a structured DNS / TCP / TLS / PMTU report (IPv4 + IPv6) plus interface MTU, `/etc/resolv.conf`, `/etc/hosts`, proxy env vars, and CA bundle summaries from inside the running infinito container. Exposed as `make diagnose-network`. | +| `diagnose/` | Run `python -m cli.contributing.network.diagnose` to print a structured DNS / TCP / TLS / PMTU report (IPv4 + IPv6) plus interface MTU, `/etc/resolv.conf`, `/etc/hosts`, proxy env vars, and CA bundle summaries from inside the running infinito container. Exposed as `make diagnose-network`. | | [address/](address/) | `cli contributing network address suggest`: propose the next free per-role IPv4 subnet(s) gap-first within the established umbrella blocks. | | [ports/](ports/) | `cli contributing network ports suggest`: propose free host-bound ports inside `PORT_BANDS..`. | ## Related Documentation 📚 - [docs/contributing/actions/debugging/network.md](../../../docs/contributing/actions/debugging/network.md) explains how to read the diagnose output and how to fix MTU / IPv6 / connectivity issues. -- [docs/contributing/tools/network/](../../../docs/contributing/tools/network/) covers the contract of the `address` and `ports` suggesters. +- `docs/contributing/tools/network/` covers the contract of the `address` and `ports` suggesters. diff --git a/docs/agents/action/develop.md b/docs/agents/action/develop.md index 68fe424cae..fedf007792 100644 --- a/docs/agents/action/develop.md +++ b/docs/agents/action/develop.md @@ -12,7 +12,7 @@ ## New Apps - You MUST use [CONTRIBUTING.md](../../../CONTRIBUTING.md) as the SPOT for the general development workflow, testing, review, and the code and development guides. -- You MUST treat the matching Pull Request template described in [Pull Request Templates](../../contributing/artefact/git/pull-request.md) and stored in [PULL_REQUEST_TEMPLATE](../../../.github/PULL_REQUEST_TEMPLATE) as the SPOT for requirements, file checks, validation expectations, and the Definition of Done. +- You MUST treat the matching Pull Request template described in [Pull Request Templates](../../contributing/artefact/git/pull-request.md) and stored in `.github/PULL_REQUEST_TEMPLATE` as the SPOT for requirements, file checks, validation expectations, and the Definition of Done. - You MUST read [Role `style.css`](../files/role/style.css.md) BEFORE you implement role-local CSS so you can see how CSS should be implemented before you add or rewrite overrides. - You MUST read [Role `javascript.js`](../files/role/javascript.js.md) BEFORE you implement role-local JavaScript so you can see how browser-side behavior should be implemented before you add or rewrite injected scripts. - You MUST read [Role `playwright.env.j2`](../files/role/playwright.env.j2.md) BEFORE you implement role-local Playwright environment templates so the rendered test inputs match the supported user journey. diff --git a/docs/agents/action/pull-request.md b/docs/agents/action/pull-request.md index 34f8ff4d25..6c8c8518f3 100644 --- a/docs/agents/action/pull-request.md +++ b/docs/agents/action/pull-request.md @@ -2,6 +2,6 @@ - You MUST choose the matching Pull Request template before you write a PR description. - You MUST follow the template selection guidance in [CONTRIBUTING.md](../../../CONTRIBUTING.md). -- You MUST use the templates from [PULL_REQUEST_TEMPLATE](../../../.github/PULL_REQUEST_TEMPLATE). +- You MUST use the templates from `.github/PULL_REQUEST_TEMPLATE`. - You MUST treat the selected template as the SPOT for PR structure, author input, and review context. - PRs MUST target the canonical repository that redirects to, unless the operator explicitly names a different target. Before creating the PR, the agent MUST resolve the redirect (e.g. `curl -sSIL https://s.infinito.nexus/code | grep -i '^location:'`) to obtain the canonical repository URL, then compare it against `git remote get-url origin`. If the two do not point to the same repository (host + owner + repo, ignoring `.git` suffix and `https://` vs. `git@` scheme), stop and ask the operator before creating the PR. diff --git a/docs/contributing/artefact/files/plugins/lookup/users.md b/docs/contributing/artefact/files/plugins/lookup/users.md index c3ab7ffcb9..d7d6ac46c1 100644 --- a/docs/contributing/artefact/files/plugins/lookup/users.md +++ b/docs/contributing/artefact/files/plugins/lookup/users.md @@ -81,7 +81,7 @@ users: | File | Purpose | |---|---| | [users.py](../../../../../../plugins/lookup/users.py) | Runtime entry point for the `users` lookup. | -| [users/ (cache)](../../../../../../utils/cache/users/) | Shared aggregation helper package that builds and caches defaults, handles reserved users, UID/GID allocation, and uniqueness validation. Placeholder substitution (DOMAIN_PRIMARY / ORGANIZATION / SOFTWARE_NAME) lives in [users/placeholders.py](../../../../../../utils/cache/users/placeholders.py). | +| `utils/cache/users/` | Shared aggregation helper package that builds and caches defaults, handles reserved users, UID/GID allocation, and uniqueness validation. Placeholder substitution (DOMAIN_PRIMARY / ORGANIZATION / SOFTWARE_NAME) lives in [users/placeholders.py](../../../../../../utils/cache/users/placeholders.py). | | [test_users.py](../../../../../../tests/unit/python/plugins/lookup/test_users.py) | Unit tests covering the full-dict, single-entry, override, strict missing, and non-strict missing cases. | For the related applications pattern see [applications.md](applications.md). diff --git a/docs/contributing/design/architecture.md b/docs/contributing/design/architecture.md index ddb677689b..85fadbe17c 100644 --- a/docs/contributing/design/architecture.md +++ b/docs/contributing/design/architecture.md @@ -47,7 +47,7 @@ If a role cannot answer those questions, it is probably doing too much. Bundles are the assembly layer. -[inventories/bundles/servers/](../../../inventories/bundles/servers) describe concrete deployment shapes such as a personal server, a community hub, or a sovereign cloud setup. They decide which roles are active together and which ones stay out. +`inventories/bundles/servers/` describes concrete deployment shapes such as a personal server, a community hub, or a sovereign cloud setup. They decide which roles are active together and which ones stay out. For example, the repository already has server bundles such as `community-hub`, `personal-nexus`, and `sovereign-cloud`. Each one combines the same building blocks in a different way for a different operating model. @@ -192,7 +192,7 @@ See [workflow.md](../workflow.md) for the full contribution flow. If you are trying to understand a change, start here: -- [roles/](../../../roles/) for behavior +- `roles/` for behavior - [inventories/bundles/](../../../inventories/bundles/) for deployment shapes - [Makefile](../../../Makefile) and [scripts/](../../../scripts/) for supported commands - [lint.md](../actions/testing/lint.md) for code and framework rules diff --git a/docs/contributing/design/iam/common.md b/docs/contributing/design/iam/common.md index 238cdefa8f..84219b1590 100644 --- a/docs/contributing/design/iam/common.md +++ b/docs/contributing/design/iam/common.md @@ -31,7 +31,7 @@ When you add OIDC or LDAP support to a role, you MUST: identifier on both sides. 4. Keep app-specific Keycloak protocol mappers in a per-client scope file under the Keycloak - [scopes](../../../../roles/web-app-keycloak/templates/import/scopes/) + `roles/web-app-keycloak/templates/import/scopes/` directory. Do not extend [default.json.j2](../../../../roles/web-app-keycloak/templates/import/clients/default.json.j2). 5. Hide or disable application UI fields that are sourced from OIDC or LDAP, diff --git a/docs/contributing/design/iam/oidc.md b/docs/contributing/design/iam/oidc.md index 2116e650d5..bb2dbc83b4 100644 --- a/docs/contributing/design/iam/oidc.md +++ b/docs/contributing/design/iam/oidc.md @@ -66,7 +66,7 @@ Rules: ## Keycloak Client Templates 📦 Keycloak client definitions are imported from -[clients](../../../../roles/web-app-keycloak/templates/import/clients/). The +`roles/web-app-keycloak/templates/import/clients/`. The default template in [default.json.j2](../../../../roles/web-app-keycloak/templates/import/clients/default.json.j2) is shared by every application client. @@ -107,7 +107,7 @@ Every application role SHOULD follow the same shape: discovery on, the `OIDC.CLI | [11_oidc.yml](../../../../group_vars/all/11_oidc.yml) | OIDC SPOT, defines `OIDC.*` | | [web-app-keycloak](../../../../roles/web-app-keycloak/) | OIDC provider role | | [default.json.j2](../../../../roles/web-app-keycloak/templates/import/clients/default.json.j2) | Shared client template, MUST stay app-agnostic | -| [scopes](../../../../roles/web-app-keycloak/templates/import/scopes/) | Per-app client scopes for extra mappers | +| `roles/web-app-keycloak/templates/import/scopes/` | Per-app client scopes for extra mappers | | [realm.json.j2](../../../../roles/web-app-keycloak/templates/import/realm.json.j2) | Realm definition, including the `preferred_username` mapper | | [ldap.md](ldap.md) | LDAP-side counterpart, paired user identifier | | [common.md](common.md) | Cross-cutting IAM principles and integration checklist | diff --git a/docs/contributing/design/role/services/base.md b/docs/contributing/design/role/services/base.md index c089f599da..5bdcfefae2 100644 --- a/docs/contributing/design/role/services/base.md +++ b/docs/contributing/design/role/services/base.md @@ -12,7 +12,7 @@ Examples: - `web-app-keycloak` provides `oidc` - `svc-db-mariadb` provides `mariadb` -Each service entry lives in the provider role's own [meta/services.yml](../../../../../roles), where the file root IS the services map keyed by `` (no `compose:` and no `services:` wrapper). +Each service entry lives in the provider role's own `meta/services.yml`, where the file root IS the services map keyed by `` (no `compose:` and no `services:` wrapper). See [layout.md](layout.md) for the full per-role meta layout. ## Role-Local Service Metadata 🏷️ @@ -68,7 +68,7 @@ Primary implementation files: The discovery layer: -- scans role configs from [roles/](../../../../../roles) +- scans role configs from `roles/` - discovers provider entries from `services` - derives deploy type and loader bucket from the role name - resolves `provides:` and `canonical:` @@ -88,7 +88,7 @@ Global bucket order: 4. `web-svc` 5. `web-app` -Within the same bucket, ordering is refined by `run_after:` declared on the provider role's primary entity in [meta/services.yml](../../../../../roles) (i.e. `services..run_after`). +Within the same bucket, ordering is refined by `run_after:` declared on the provider role's primary entity in `meta/services.yml` (i.e. `services..run_after`). Rules: diff --git a/docs/contributing/environment/variables.md b/docs/contributing/environment/variables.md index a14e12cd88..87e36d3b63 100644 --- a/docs/contributing/environment/variables.md +++ b/docs/contributing/environment/variables.md @@ -25,7 +25,7 @@ Already-set env vars take precedence over `default.env` defaults. ## Pipeline - [`env/`](../../../utils/env/) (under `utils/`) holds the parser, runtime helpers, writer, and one handler module per dynamically-computed variable under [`handlers/`](../../../utils/env/handlers/). -- [`env/`](../../../cli/meta/env/) (under `cli/meta/`) provides the CLI entry point invoked as `python -m cli.meta.env`. +- `cli/meta/env/` provides the CLI entry point invoked as `python -m cli.meta.env`. - [`load.sh`](../../../scripts/meta/env/load.sh) is the shell loader; it sources `.env` and auto-generates it on a cold clone. ## Variables NOT in `default.env` @@ -37,7 +37,7 @@ A handful of variables are documented next to their consumer because they cannot - **Computed at container start** for `INFINITO_PATH`, set by [entry.sh](../../../scripts/docker/entry.sh). - **Persisted on the deploy target** for `INFINITO_VERSION`, written by the [sys-version](../../../roles/sys-version/) role. - **Compose literals** with no env-side knob, namely `GIT_SSL_NO_VERIFY` and `PYTHONHTTPSVERIFY`, documented in [compose.yml.md](../artefact/files/compose.yml.md). -- **Fork identity** for the operator's CI-redeploy shortcuts: `INFINITO_FORK_ACCOUNT` (defaults to the local OS username) and `INFINITO_FORK_REPOSITORY_URL` (composed as `https://github.com/$INFINITO_FORK_ACCOUNT/$INFINITO_FORK_REPOSITORY_NAME`), both set by handlers under [`utils/env/handlers/infinito/`](../../../utils/env/handlers/infinito/) (`fork_account.py`, `fork_repository_url.py`). The fork repository name itself lives in `default.env` as `INFINITO_FORK_REPOSITORY_NAME`. +- **Fork identity** for the operator's CI-redeploy shortcuts: `INFINITO_FORK_ACCOUNT` (defaults to the local OS username) and `INFINITO_FORK_REPOSITORY_URL` (composed as `https://github.com/$INFINITO_FORK_ACCOUNT/$INFINITO_FORK_REPOSITORY_NAME`), both set by handlers under `utils/env/handlers/infinito/` (`fork_account.py`, `fork_repository_url.py`). The fork repository name itself lives in `default.env` as `INFINITO_FORK_REPOSITORY_NAME`. ## Role-scoped variables diff --git a/docs/requirements/014-svc-runner-ci.md b/docs/requirements/014-svc-runner-ci.md index 8ad7faeec0..9c97445d99 100644 --- a/docs/requirements/014-svc-runner-ci.md +++ b/docs/requirements/014-svc-runner-ci.md @@ -19,7 +19,7 @@ Add an `svc-runner` role that provisions a dedicated machine as an Infinito.Nexu ### CLI: `cli/administration/deploy/runner/` -- [x] A new CLI entry point at `cli/administration/deploy/runner/` (path to be created) is wired into the `infinito` CLI tree the same way the existing [`cli/administration/deploy/dedicated/`](../../cli/administration/deploy/runner/) and [`cli/administration/deploy/development/`](../../cli/administration/deploy/development/) commands are. +- [x] A new CLI entry point at `cli/administration/deploy/runner/` (path to be created) is wired into the `infinito` CLI tree the same way the existing `cli/administration/deploy/dedicated/` and [`cli/administration/deploy/development/`](../../cli/administration/deploy/development/) commands are. - [x] Argument parsing MUST use Python's standard-library `argparse` module, matching the convention used by [cli/administration/deploy/dedicated/command.py](../../cli/administration/deploy/runner/command.py). Hand-rolled `sys.argv` parsing or third-party CLI frameworks (`click`, `typer`, etc.) MUST NOT be introduced. - [x] The script accepts the following parameters: - `hostname` (**required**) — the target server that will host the runner. diff --git a/docs/requirements/023-docker-swarm-nfs.md b/docs/requirements/023-docker-swarm-nfs.md index 3cb0989d1e..da6eed854e 100644 --- a/docs/requirements/023-docker-swarm-nfs.md +++ b/docs/requirements/023-docker-swarm-nfs.md @@ -149,7 +149,7 @@ The implementation MUST keep a clean abstraction line between the render backend ### CI: pilot validation workflow - [x] A new GitHub Actions workflow at `.github/workflows/test-deploy-swarm-nfs.yml` (path to be created) provisions a multi-node Swarm cluster on a single **GitHub-hosted** runner using Docker-in-Docker with multiple Docker daemons. The topology is fixed at **three simulated nodes**: exactly one manager (`swarm-mgr-01`) plus exactly two workers (`swarm-wrk-01`, `swarm-wrk-02`). Three nodes are mandatory so that the rescheduling test has a genuine choice of target (drain the worker running the MediaWiki application service → scheduler MUST pick the OTHER worker, not the manager). One additional container hosts the NFS server reachable from all three simulated nodes. Then the workflow deploys `web-app-mediawiki` as a Swarm stack with the MariaDB service pinned to the manager and the application service free to schedule on either worker. -- [x] The workflow MUST create the inventory via the canonical [cli/administration/inventory/provision/](../../cli/administration/inventory/provision/) CLI (the same `infinito` entry point developers use locally — "Create or update a full inventory for a host"). The workflow MUST NOT hand-roll a YAML inventory from scratch. After the provision step, the workflow's only inventory edits MUST be: adding the three simulated hosts to the `svc-swarm-node` group, adding `swarm-mgr-01` to `svc-swarm-manager`, and adding all three simulated hosts to the `web-app-mediawiki` group so the pilot role's `DEPLOYMENT_MODE` resolves to `swarm`. Any other inventory mutation is forbidden. +- [x] The workflow MUST create the inventory via the canonical `cli/administration/inventory/provision/` CLI (the same `infinito` entry point developers use locally — "Create or update a full inventory for a host"). The workflow MUST NOT hand-roll a YAML inventory from scratch. After the provision step, the workflow's only inventory edits MUST be: adding the three simulated hosts to the `svc-swarm-node` group, adding `swarm-mgr-01` to `svc-swarm-manager`, and adding all three simulated hosts to the `web-app-mediawiki` group so the pilot role's `DEPLOYMENT_MODE` resolves to `swarm`. Any other inventory mutation is forbidden. - [x] The runner choice is fixed to GitHub-hosted DinD for reproducibility — every PR run is identical and depends on no self-hosted infrastructure. Migrating to a self-hosted runner (e.g. [014](014-svc-runner-ci.md)) is a Future Extension, not part of this requirement. - [x] The workflow drains the node currently running the MediaWiki container, waits for the swarm scheduler to migrate the container to another node, then asserts: 1. the new task transitions to `running` within a documented timeout; diff --git a/roles/dsk-chromium/README.md b/roles/dsk-chromium/README.md index 28d7c7f8a5..edd557fc6e 100644 --- a/roles/dsk-chromium/README.md +++ b/roles/dsk-chromium/README.md @@ -74,7 +74,7 @@ infinito administration deploy dedicated "$INVENTORY/devices.yml" \ ## Addons -This role force-installs the following Chromium browser extensions via managed Enterprise Policies. They are declared in [meta/addons/](meta/addons/) and rendered into the CRX install list. +This role force-installs the following Chromium browser extensions via managed Enterprise Policies. They are declared in `meta/addons/` and rendered into the CRX install list. | Addon | Mechanism | Default state | Bridges | |---|---|---|---| diff --git a/roles/dsk-firefox/README.md b/roles/dsk-firefox/README.md index fe17500981..44a348306a 100644 --- a/roles/dsk-firefox/README.md +++ b/roles/dsk-firefox/README.md @@ -73,7 +73,7 @@ infinito administration deploy dedicated "$INVENTORY/devices.yml" \ ## Addons -Role-level extensions are declared in [`meta/addons/`](./meta/addons/) (unified addon contract, requirement 026): +Role-level extensions are declared in `meta/addons/` (unified addon contract, requirement 026): | Addon | Mechanism | Default state | Bridges | |-------|-----------|---------------|---------| diff --git a/roles/dsk-gnome/README.md b/roles/dsk-gnome/README.md index 5b817f8d30..c42563c621 100644 --- a/roles/dsk-gnome/README.md +++ b/roles/dsk-gnome/README.md @@ -78,7 +78,7 @@ infinito administration deploy dedicated "$INVENTORY/devices.yml" \ ## Addons -Role-level GNOME Shell extensions are declared in [`meta/addons/`](./meta/addons/) (unified addon contract, requirement 026). +Role-level GNOME Shell extensions are declared in `meta/addons/` (unified addon contract, requirement 026). They are installed through `cli-gnome-extension-manager`, which receives the `action`, `uuid`, and `url` from each addon's `config` payload: | Addon | Mechanism | Default state | Bridges | diff --git a/roles/update/README.md b/roles/update/README.md index fbdb3e21fd..462d2520e4 100644 --- a/roles/update/README.md +++ b/roles/update/README.md @@ -96,7 +96,7 @@ To extend support for a new distribution, add the distribution name to the lookup dict in [main.yml](./tasks/main.yml). When the new distribution shares a package manager with an existing entry, point it at that entry's task file. When it brings a new package manager, add a new family file under -[tasks/](./tasks/) and keep [meta/main.yml](./meta/main.yml) `platforms` in +`tasks/` and keep [meta/main.yml](./meta/main.yml) `platforms` in sync. ## Further Resources diff --git a/roles/web-app-discourse/README.md b/roles/web-app-discourse/README.md index 1148390c0a..4434b86e6a 100644 --- a/roles/web-app-discourse/README.md +++ b/roles/web-app-discourse/README.md @@ -121,7 +121,7 @@ docker run --rm -it \ ## Addons -Addons are declared in [`meta/addons/`](./meta/addons/) and read at deploy time via `lookup('config', application_id, 'addons')`. +Addons are declared in `meta/addons/` and read at deploy time via `lookup('config', application_id, 'addons')`. | Addon | Mechanism | Default state | Bridges | |---|---|---|---| diff --git a/roles/web-app-docs/README.md b/roles/web-app-docs/README.md index 700bb684e1..2653898cf8 100644 --- a/roles/web-app-docs/README.md +++ b/roles/web-app-docs/README.md @@ -6,7 +6,7 @@ This role publishes the Infinito.Nexus documentation for the latest commit and f ## Overview -One `python:-slim` container runs the service in [files/python/infinito_docs/](./files/python/infinito_docs/): +One `python:-slim` container runs the service in `files/python/infinito_docs/`: 1. It keeps a git mirror of `services.docs.source_repository` in the `docs_sites` volume and fetches it every `services.docs.fetch_interval` seconds, or later when a build is still running. 2. `latest` is the last commit of the default branch. It is built at start and rebuilt whenever a fetch finds a new commit; the previous build stays online until the new one replaces it. diff --git a/roles/web-app-friendica/README.md b/roles/web-app-friendica/README.md index e5c597b785..3b10739f25 100644 --- a/roles/web-app-friendica/README.md +++ b/roles/web-app-friendica/README.md @@ -109,7 +109,7 @@ docker run --rm -it \ ## Addons -Role-level extensions are declared in [`meta/addons/`](./meta/addons/) +Role-level extensions are declared in `meta/addons/` (unified addon contract, requirement 026): | Addon | Mechanism | Default state | Bridges | diff --git a/roles/web-app-joomla/README.md b/roles/web-app-joomla/README.md index 8bd60585b9..fcb7440928 100644 --- a/roles/web-app-joomla/README.md +++ b/roles/web-app-joomla/README.md @@ -61,7 +61,7 @@ Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional ( ## Addons -Role-level extensions are declared in [`meta/addons/`](./meta/addons/) +Role-level extensions are declared in `meta/addons/` (unified addon contract, requirement 026): | Addon | Mechanism | Default state | Bridges | diff --git a/roles/web-app-matrix/README.md b/roles/web-app-matrix/README.md index a350c60ad4..858dc3a05d 100644 --- a/roles/web-app-matrix/README.md +++ b/roles/web-app-matrix/README.md @@ -186,7 +186,7 @@ The ansible flavor maps each true flag to the matching `matrix__enabled` ## Addons The mautrix network bridges are declared in -[`meta/addons/`](./meta/addons/) as `mechanism: bridge` addons +`meta/addons/` as `mechanism: bridge` addons (requirement 026, Decision 13). Each is `required: false` and **disabled by default**; its per-network DB password and its appservice `as_token`/`hs_token` pair are referenced from [`meta/secrets.yml`](./meta/secrets.yml) `credentials:`, never inlined. The role renders each diff --git a/roles/web-app-mediawiki/README.md b/roles/web-app-mediawiki/README.md index 4681affb8c..9fe64cfb74 100644 --- a/roles/web-app-mediawiki/README.md +++ b/roles/web-app-mediawiki/README.md @@ -105,7 +105,7 @@ docker run --rm -it \ ## Addons -This role ships its OIDC login stack and its AI editing stack as unified addons declared in [`meta/addons/`](meta/addons/). All four are MediaWiki extensions installed from upstream at the `REL_` branch matching the pinned image, and each is gated on a service flag. Secrets are rendered through the role's templates and never inlined into an addon declaration. +This role ships its OIDC login stack and its AI editing stack as unified addons declared in `meta/addons/`. All four are MediaWiki extensions installed from upstream at the `REL_` branch matching the pinned image, and each is gated on a service flag. Secrets are rendered through the role's templates and never inlined into an addon declaration. | Addon | Mechanism | Default state | Bridges | |---|---|---|---| diff --git a/roles/web-app-nextcloud/README.md b/roles/web-app-nextcloud/README.md index e3a3df494f..e6ada822e7 100644 --- a/roles/web-app-nextcloud/README.md +++ b/roles/web-app-nextcloud/README.md @@ -176,7 +176,7 @@ docker run --rm -it \ ## Addons -The config-bearing Nextcloud apps are declared in [`meta/addons/`](./meta/addons/) under the unified addon contract (requirement 026). +The config-bearing Nextcloud apps are declared in `meta/addons/` under the unified addon contract (requirement 026). Each declaration carries its full `occ config:app:set` payload under `config:`. The enable-only appstore apps stay under `nextcloud.plugins` in [`meta/services.yml`](./meta/services.yml). @@ -275,7 +275,7 @@ A detailed documentation for the use and administration of Nextcloud on Infinito ## Persona contract opt-outs -The shared `biber` and `administrator` persona helpers are declared blocked in [templates/playwright.env.j2](./templates/playwright.env.j2). Nextcloud presents three different login surfaces depending on the variant (native, `oidc_login`, `sociallogin`), the native administrator authenticates with the role-local `credentials.administrator_password` rather than the Keycloak secret, and the `#firstrunwizard` modal intercepts the user-menu click the generic logout depends on. Both personas are therefore driven by the role's own login specs — `test-login-admin-native.js`, `test-login-admin-oidc.js`, `test-login-biber-oidc.js` and `test-login-biber-ldap.js` under [files/playwright](./files/playwright/) — which carry the flavor switch, the modal dismissal and the login retry the shared helper lacks. +The shared `biber` and `administrator` persona helpers are declared blocked in [templates/playwright.env.j2](./templates/playwright.env.j2). Nextcloud presents three different login surfaces depending on the variant (native, `oidc_login`, `sociallogin`), the native administrator authenticates with the role-local `credentials.administrator_password` rather than the Keycloak secret, and the `#firstrunwizard` modal intercepts the user-menu click the generic logout depends on. Both personas are therefore driven by the role's own login specs — `test-login-admin-native.js`, `test-login-admin-oidc.js`, `test-login-biber-oidc.js` and `test-login-biber-ldap.js` under `files/playwright/` — which carry the flavor switch, the modal dismissal and the login retry the shared helper lacks. ## Credits diff --git a/roles/web-app-odoo/README.md b/roles/web-app-odoo/README.md index 674855a5f1..d538748e7e 100644 --- a/roles/web-app-odoo/README.md +++ b/roles/web-app-odoo/README.md @@ -149,11 +149,11 @@ Odoo's functionality is delivered through a modular architecture. The following | **project** | Project management with Kanban boards, Gantt charts, and time tracking | | **stock** | Inventory and warehouse management with barcode support | -Additional modules can be enabled by declaring them as `group: optional` addons in [`meta/addons/`](meta/addons/). +Additional modules can be enabled by declaring them as `group: optional` addons in `meta/addons/`. ## Addons -Odoo modules are declared as addons in [`meta/addons/`](meta/addons/) per the unified addon contract (requirement 026). The install path reads them from `applications.web-app-odoo.addons`. +Odoo modules are declared as addons in `meta/addons/` per the unified addon contract (requirement 026). The install path reads them from `applications.web-app-odoo.addons`. | Addon | Mechanism | Default state | Bridges | |-------|-----------|---------------|---------| diff --git a/roles/web-app-pixelfed/README.md b/roles/web-app-pixelfed/README.md index 7a7b9487b5..003c5dbecc 100644 --- a/roles/web-app-pixelfed/README.md +++ b/roles/web-app-pixelfed/README.md @@ -116,7 +116,7 @@ docker run --rm -it \ ## Persona contract opt-outs -The shared `biber` and `administrator` persona helpers are declared blocked in [templates/playwright.env.j2](./templates/playwright.env.j2). Pixelfed's login id is the account e-mail rather than the Keycloak username, and the artisan-bootstrapped administrator name is reserved, so pixelfed's first-time OIDC registration cannot mint it either. `biber` additionally needs his Keycloak e-mail rewritten to a resolvable domain through the Keycloak admin API before the first OIDC login, because pixelfed's `dns,spoof` validator rejects `.example` addresses. Both setups live in [files/playwright/_shared.js](./files/playwright/_shared.js) and drive the dedicated login specs in [files/playwright](./files/playwright/). +The shared `biber` and `administrator` persona helpers are declared blocked in [templates/playwright.env.j2](./templates/playwright.env.j2). Pixelfed's login id is the account e-mail rather than the Keycloak username, and the artisan-bootstrapped administrator name is reserved, so pixelfed's first-time OIDC registration cannot mint it either. `biber` additionally needs his Keycloak e-mail rewritten to a resolvable domain through the Keycloak admin API before the first OIDC login, because pixelfed's `dns,spoof` validator rejects `.example` addresses. Both setups live in [files/playwright/_shared.js](./files/playwright/_shared.js) and drive the dedicated login specs in `files/playwright/`. ## Credits diff --git a/roles/web-app-pretix/README.md b/roles/web-app-pretix/README.md index fdb54f9798..d51be734c4 100644 --- a/roles/web-app-pretix/README.md +++ b/roles/web-app-pretix/README.md @@ -118,7 +118,7 @@ docker run --rm -it \ ## Addons -Role-level extensions are declared in [`meta/addons/`](meta/addons/) following the unified addon contract (requirement 026). +Role-level extensions are declared in `meta/addons/` following the unified addon contract (requirement 026). | Addon | Mechanism | Default state | Bridges | |---|---|---|---| diff --git a/roles/web-app-wordpress/README.md b/roles/web-app-wordpress/README.md index c5706f3f42..532bbc87fa 100644 --- a/roles/web-app-wordpress/README.md +++ b/roles/web-app-wordpress/README.md @@ -150,7 +150,7 @@ Operator-facing instructions for assigning these groups live in [Administration ## Addons -Every plugin and mu-plugin is declared in [`meta/addons/`](./meta/addons/) under +Every plugin and mu-plugin is declared in `meta/addons/` under the unified addon contract. The OIDC and WP-Discourse runtime config lives in each addon's `config:` block. Both install paths read those declarations: a `required` addon that fails to install stops the deploy, an optional one warns and is skipped. | Addon | Mechanism | Default state | Bridges | diff --git a/roles/web-app-wordpress/files/mu-plugins/README.md b/roles/web-app-wordpress/files/mu-plugins/README.md index 90a82213d3..81d5748c18 100644 --- a/roles/web-app-wordpress/files/mu-plugins/README.md +++ b/roles/web-app-wordpress/files/mu-plugins/README.md @@ -39,7 +39,7 @@ It hooks `http_api_curl` rather than `http_request_args` because the args array ## Deployment 🚚 -Every file here is declared as an addon under [meta/addons/](../../meta/addons/) with `mechanism: mu_plugin` and `source: vendored`, and the addon id is the file stem. [04_mu_plugins.yml](../../tasks/04_mu_plugins.yml) loops over those declarations and copies `.php` into the container on every deploy; a new file therefore needs its `meta/addons/.yml` entry and a Playwright spec before it is installed. +Every file here is declared as an addon under `meta/addons/` with `mechanism: mu_plugin` and `source: vendored`, and the addon id is the file stem. [04_mu_plugins.yml](../../tasks/04_mu_plugins.yml) loops over those declarations and copies `.php` into the container on every deploy; a new file therefore needs its `meta/addons/.yml` entry and a Playwright spec before it is installed. ## Credits 🙏 diff --git a/roles/web-app-xwiki/README.md b/roles/web-app-xwiki/README.md index ce81909a21..b911ff3fb4 100644 --- a/roles/web-app-xwiki/README.md +++ b/roles/web-app-xwiki/README.md @@ -118,7 +118,7 @@ docker run --rm -it \ ## Addons -Extensions are declared in [`meta/addons/`](./meta/addons/) under the unified addon contract. +Extensions are declared in `meta/addons/` under the unified addon contract. Each one is installed through the XWiki Extension Manager and pins its upstream version; the Maven coordinate is carried under the addon's `config.id`: | Addon | Mechanism | Default state | Bridges | diff --git a/tests/lint/repository/documentation/test_markdown_links.py b/tests/lint/repository/documentation/test_markdown_links.py index c43ff3e20e..826111e25b 100644 --- a/tests/lint/repository/documentation/test_markdown_links.py +++ b/tests/lint/repository/documentation/test_markdown_links.py @@ -40,6 +40,25 @@ class BrokenLink(NamedTuple): line: int target: str resolved: Path + reason: str + + +def _unresolvable(resolved: Path) -> str: + """Return why ``resolved`` cannot back a link, empty when it can. + + A directory is only a link target because its README stands in for it: + that is what GitHub opens, and what the documentation build resolves the + link against. A directory without one renders as a file listing in the one + place and as a missing cross-reference in the other. + + Args: + resolved: the absolute path a link target resolved to. + """ + if not resolved.exists(): + return "no such path" + if resolved.is_dir() and not (resolved / "README.md").is_file(): + return "directory without a README.md to stand in for it" + return "" def _tracked_md_files(root: Path) -> list[Path]: @@ -133,13 +152,15 @@ def _check_file(file: Path, root: Path) -> list[BrokenLink]: else: resolved = (base / path_part).resolve() - if not resolved.exists(): + reason = _unresolvable(resolved) + if reason: broken.append( BrokenLink( file=file, line=line_no, target=raw_target, resolved=resolved, + reason=reason, ) ) @@ -170,7 +191,7 @@ class TestMarkdownLinks(unittest.TestCase): ] for item in sorted(broken, key=lambda b: (b.file.as_posix(), b.line)): rel = item.file.relative_to(root).as_posix() - lines.append(f" {rel}:{item.line}: {item.target!r}") + lines.append(f" {rel}:{item.line}: {item.target!r} - {item.reason}") self.fail("\n".join(lines)) diff --git a/tests/unit/python/utils/certs/README.md b/tests/unit/python/utils/certs/README.md index 91346e5abc..be75ec3357 100644 --- a/tests/unit/python/utils/certs/README.md +++ b/tests/unit/python/utils/certs/README.md @@ -1,7 +1,7 @@ # Certificate & TLS Unit Tests 🔐 -Unit tests for the top-level certificate and TLS helper modules in [`utils/`](../../../../utils/): `cert_utils.py` (wildcard/exact match logic, newest-certificate selection) and `tls_common.py` (flavor handling, SAN overrides, domain normalisation). +Unit tests for the top-level certificate and TLS helper modules in `utils/`: `cert_utils.py` (wildcard/exact match logic, newest-certificate selection) and `tls_common.py` (flavor handling, SAN overrides, domain normalisation). -Tests in this directory MUST only cover the pure-function behaviour of `utils.cert_utils` and `utils.tls_common`. Lookup-plugin integration that depends on these modules MUST live under [`tests/unit/python/plugins/lookup/`](../../plugins/lookup/). +Tests in this directory MUST only cover the pure-function behaviour of `utils.cert_utils` and `utils.tls_common`. Lookup-plugin integration that depends on these modules MUST live under `tests/unit/python/plugins/lookup/`. For framework and `make test-unit` usage see [unit.md](../../../../../docs/contributing/actions/testing/unit.md). diff --git a/utils/cache/README.md b/utils/cache/README.md index a4d0080e34..67d6fd11f9 100644 --- a/utils/cache/README.md +++ b/utils/cache/README.md @@ -11,7 +11,7 @@ This package contains the in-process cache layers Infinito.Nexus uses to keep CL | [base.py](base.py) | Cross-cutting helpers: filesystem constants (`PROJECT_ROOT`, `ROLES_DIR`, `DEFAULT_TOKENS_FILE`), `_deep_merge`, cache-key + content-fingerprint signatures, the cross-domain re-entry guard, and the templar-render machinery. The only ansible-coupled symbol is `_render_with_templar`, which lazy-imports its dependency. | | [applications.py](applications.py) | Per-app variants + defaults + `get_merged_applications`. **Strictly ansible-free at import time** so the GitHub Actions runner-host CLI path (`cli.administration.deploy.development.init` → `plan_dev_inventory_matrix` → `get_variants`) keeps working without ansible installed. | | [carrier.py](carrier.py) | The play-scoped carrier of the rendered applications payload: the fact name `_INFINITO_APPLICATIONS_RENDERED`, `merged_applications_cache_key`, and the reader `get_merged_applications` consults before it renders. | -| [users/](users/) | User definitions, token store hydration, alias materialization, `get_user_defaults`, `get_merged_users` (package; `users/placeholders.py` substitutes `DOMAIN_PRIMARY` / `ORGANIZATION` / `SOFTWARE_NAME` ahead of the templar render pass). | +| `users/` | User definitions, token store hydration, alias materialization, `get_user_defaults`, `get_merged_users` (package; `users/placeholders.py` substitutes `DOMAIN_PRIMARY` / `ORGANIZATION` / `SOFTWARE_NAME` ahead of the templar render pass). | | [domains.py](domains.py) | Canonical-domains map derived from the merged applications view: `get_merged_domains`. A read issued while the applications render is in progress (the re-entry guard is set) sees the unrendered view, so it is cached under its own key and never answers a later read. | | [`__init__.py`](__init__.py) | Owns the package-level `_reset_cache_for_tests()` orchestrator that clears every cache plus the shared fingerprint memo in one call. | diff --git a/utils/env/README.md b/utils/env/README.md index 92251b8d25..0c14ccd69d 100644 --- a/utils/env/README.md +++ b/utils/env/README.md @@ -27,7 +27,7 @@ Shell consumers source [load.sh](../../scripts/meta/env/load.sh) under `scripts/ - Module names MUST stay lowercase snake_case. - Pure passthrough or simple parser/writer helpers MUST live directly under `utils/env/`. - Per-variable computation MUST live under `handlers/` (see [handlers README](handlers/README.md) for the naming convention used there). -- Test files live in [tests/unit/python/utils/env/](../../tests/unit/python/utils/env/) and mirror the module name with a `test_` prefix. +- Test files live in `tests/unit/python/utils/env/` and mirror the module name with a `test_` prefix. ## Machine Facts vs Run Facts 🧭 diff --git a/utils/install/README.md b/utils/install/README.md index 176f9638b3..e6bbba2b1d 100644 --- a/utils/install/README.md +++ b/utils/install/README.md @@ -27,7 +27,7 @@ The matching dispatcher [wrapper.sh](../../scripts/install/wrapper.sh) routes `m - Module names MUST stay lowercase snake_case. - Channel-agnostic helpers MUST live directly under `utils/install/`. - Per-tool policy (one tool per file) MUST live under `lint/` (see [lint README](lint/README.md)). -- Test files live in [tests/unit/python/utils/install/](../../tests/unit/python/utils/install/) and mirror the module name with a `test_` prefix. +- Test files live in `tests/unit/python/utils/install/` and mirror the module name with a `test_` prefix. ## Import Rules