Review follow-up on the in-container probes of gitea, shopware and fider.
- fider: the tenant POST over /dev/tcp waited on `head` without a bound,
so a peer that accepts and stays silent blocked the task for good. It
now runs under `timeout` (TIMEOUT_FACTOR-scaled 30 s, the old uri
socket timeout) and reads the status line with the `read` builtin, so
the bound kills the whole probe. Only outcomes Fider never committed
are retried: a refused connect and a 5xx (Fider rolls the request
transaction back on a handler error). A timeout or a closed socket
after the request was sent fails the task instead of resending the
POST; the next run's tenant count decides again.
- fider: both bootstrap tasks get `no_log` behind MASK_CREDENTIALS_IN_LOGS,
as the body carries the administrator's name and email.
- shopware: each wget probe runs under `timeout`, as busybox wget waits
900 s per read by default.
- shopware, gitea: the first readiness wait derives its retries from
lookup('timeout', 300) again, the budget of the wait_for it replaced,
so TIMEOUT_FACTOR scales it.
Refs #1089
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In swarm, `docker stack deploy` drops the host IP of a short port mapping,
so a `127.0.0.1:<ports.local>` publish lands on the ingress routing mesh
on every node. The provisioning of these three roles only worked because
the mesh answered on the host:
- web-app-gitea waited for `DOCKER_REACH_HOST:<ports.local.http>` with
wait_for and polled `/api/v1/version` there with uri_retry.
- web-app-shopware waited for `DOCKER_REACH_HOST:<ports.local.http>`.
- web-app-fider bootstrapped its first tenant in swarm by POSTing to
`127.0.0.1:<ports.local.http>` on the hosting node.
As long as these calls depend on the mesh, container_ports cannot stop
publishing loopback ports in swarm. The probes now run inside the
service's own container through the resolved `<PREFIX>_LOCAL_CID` on
`<PREFIX>_HOST_NODE`, against the internal port:
- Gitea uses the curl it ships. The first wait accepts any HTTP answer,
as wait_for did. The post-migration wait still requires 200.
- Shopware runs the wget probe its compose healthcheck already uses.
- Fider ships no HTTP client, so bash writes the tenant POST to
/dev/tcp. The body comes in on stdin and the status line is checked
for 200/201.
Fider's compose path is unchanged. The compose render of all three roles
is unchanged. The host-side ingress guard from svc-swarm-node stays in
place until the remaining host-side consumers are moved.
Refs #1089
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At -vvv Ansible prints each module argument and result, so a password in
a command line, a request body, a header or a fact lands in the deploy
log. Nothing enforced masking outside the MCP task files, and no check
looked at parents at all.
A new lint, test_credential_tasks_are_masked, finds tasks whose printed
parts (module arguments, environment, loop) reference an identifier ending
in password, secret, token, api_key or private_key, or
secrets.credentials. A task passes when it or an enclosing block sets
no_log, when every file that pulls it in does so through include_tasks
with apply.no_log or import_tasks with no_log (transitively), when the
secret only reaches a parameter the module masks itself, or when the
module masks its values. no_log on an include_tasks task itself does not
count, because it does not reach the included tasks. The rule can be
suppressed per task with `# nocheck: credential-masking`, now listed in
the suppression catalog, and the lint tests its parent and suppression
logic on synthetic cases.
It found 39 unmasked tasks in 19 roles, all real: join tokens, client
secrets, API tokens in Cloudflare headers, database passwords in
command lines, the ChatGPT bot login body and more. Each now sets
no_log from MASK_CREDENTIALS_IN_LOGS. The sys-dns-hetzner-rdns alias
hetzner_no_log, which only restated that switch, is gone.
Not covered yet: template and copy tasks whose rendered file carries a
secret print it as a diff under --diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The contract check ran as an Ansible task in the middle of every provider deploy, and the two client checks ran the same way. A test that is part of the deploy cannot be run without deploying, cannot be re-run on its own, and reports its verdict as a deploy failure. Twenty-seven task files lose 491 lines to this.
It is now what it always was. Each provider ships files/test/test.sh, templates/test.env.j2 and meta/tests.yml, and test-e2e-cli stages one shared harness beside it: run.sh drives contract.sh in a throwaway container on the provider's own network, then isolation.sh for adapter-fronted providers. The read probe each provider offers moved from the calling task into meta/mcp.yml next to the allowlist it must satisfy, and the schema now rejects a read probe naming a tool outside that allowlist.
The bearer the test presents is the one a client resolves through mcp.credential, via the new mcp_client_bearer lookup over resolve_credential, which moved to utils.roles.applications.mcp so it is the same function mcp_servers renders every client connection with. The probe presented whatever the calling role passed, which for the fifteen adapter-fronted providers is a different secret than any client holds.
Sidecar isolation is read from the engine rather than measured inside the container, so it no longer needs a delegation to the node holding the task: attachment is compared by network name, which drops the gwbridge special case, and a swarm worker says it cannot measure instead of skipping silently.
Retrying is bounded by wall clock against MCP_DEADLINE_SECONDS, which scales through the same lookup('timeout') as the harness budget it has to stay under. A flag that is neither true nor false aborts rather than reading as off.
mcp-provider-probe becomes mcp-provider-cli-test: an enabled provider must ship the test rather than include the task. mcp-probe-bearer becomes mcp-adapter-bearer, tying ADAPTER_BEARER to the declared credential key now that no probe include carries it. A new integration test renders the shared env template for every provider through Ansible's own templar, so a later change to it breaks visibly instead of poisoning twenty deploys quietly.
Hermes and OpenClaw keep their client-side proof as CLI tests of their own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gitea-mcp sidecar holds no credential of its own and expects each
client to supply a Gitea token per request, so it answers initialize and
tools/list to anyone: the deploy probe measured a 200 disclosing the full
tool inventory. No sidecar setting closes that, because serving the
unauthenticated is how the upstream is built.
Front it with the svc-ai-mcp-adapter instance instead. Clients now present
mcp_bearer to compose service giteamcp and get a 401 otherwise; the
adapter presents the minted Gitea token upstream to giteamcpupstream,
which keeps the sidecar image but drops it to the role's own network.
Pin the 24 read-only tools the sidecar serves, captured from the running
instance, so the served surface cannot widen unnoticed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four roles created their MCP service account and never touched its password
again, while the same play authenticates as that account moments later. A
deployment whose declared password moved on therefore failed at the login,
not at the point the drift was introduced.
gitea now runs 'admin user change-password' before the token dance. The
--must-change-password=false flag is mandatory rather than cosmetic: at the
pinned v1.27.1 the flag defaults to true, so omitting it locks the account out
of the very API call that follows.
nextcloud resets through 'occ user:resetpassword --password-from-env' before
minting the app token that verifies the password.
snipe-it compared nothing at all - its docstring promised to pin the password
while the assignment sat inside the create branch. It now rehashes only when
Hash::check says the stored hash no longer matches, so the changed flag stays
honest.
home assistant swallowed a failed credential creation for accounts that already
existed and then logged in with them. It now calls admin_change_password and
falls back to creation only when the reply is credentials_not_found.
jellyfin and zammad already converged; wordpress, gitlab and moodle never
authenticate with this password and are left alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine roles reached their MCP surface as the deployment administrator or with
an account password kept beside the account in credentials. A Moodle site
admin bypasses every capability check, a Nextcloud app password is not
scopable, a Gitea token reads every private repository, and a GitLab token on
root reads every private project. Each role now declares its own principal in
meta/users.yml and provisions it, so the exposed surface is what the service
role permits. jellyfin, zammad and snipe-it kept their account but took its
password from a credential named token; they read the registry too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The image seeds app.ini from the flat DB_* variables only while the file is
absent, so a persisted install kept whatever credentials it was first given and
the role patched the current ones back in with sed. environment-to-ini runs
unconditionally on every start and overwrites the [database] keys in place, so
declaring them as GITEA__database__* converges the config on its own.
The restart that makes the new values take effect already comes from rendering
env.j2, whose handler the backend stack flushes before the manager tasks run,
so the patch task loses its last reason to exist.
Drops 02_database.yml and renumbers the following task files to keep the
sequence gapless.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three roles reach their MCP work from a manager-ops file rather than
from the role entry point, and the rename missed them, leaving them
including task files that no longer exist.
Every MCP role keeps its task tree under tasks/utils/ without numeric
prefixes, so a caller can reach any of them through include_role
tasks_from. utils/roles/mapping.py holds that path as the single source.
Ansible resolves a relative include against the including file's own
directory when that file was reached by path, but against tasks/ when it
was reached through tasks_from. The same file therefore loaded from its
own role and failed from a caller using tasks_from. Every nested include
now names {{ role_path }}/tasks/utils/..., which holds under both bases,
and a lint walks each tasks_from entrypoint transitively to keep it so.
The exec-gating lint learns the same form, otherwise it loses the include
chain and reports inherited IS_STACK_HOST gates as missing.
The expression resolving a provider's stored token appeared eighteen
times. plugins/lookup/mcp_credential.py replaces it, and mcp_probe_owner
disappears from eight roles: it was only ever a step towards the bearer
and no probe read it.
The Hermes and OpenClaw probes stop aborting the play while it is still
running. Roles deploy in dependency order, so a client probing during its
own role can run before a provider exists at all, and failing there
prevents that provider from ever deploying. The verdict moves to the
reconciliation pass, gated on MCP_RECONCILE_STRICT, which the reconciler
sets and group_vars declares so the readers need no default.
The sys-svc-mcp-reconcile role is added here; wiring it into a stage is a
separate change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The eight roles whose MCP server is built into the application now end their MCP
step with the shared contract probe. It builds URL and Authorization header from
the role's own declaration, calls initialize and tools/list, invokes one
deterministic read tool per role, and compares the served surface against the
declared allowlist. It cannot be a Playwright test: the check needs the real
provider credential and a Playwright run would capture it in its trace.
Eleven sites wrote the MCP token into the store under the administrator's
identity while their metadata declared an owner of their own. The schema lint
forbids administrator as a credential owner, so declaration and storage
disagreed, and the probe would have looked for the bearer under the declared
owner and found nothing. They now read the declared owner.
Gitea ships no MCP server, so the role runs the project-owned gitea-mcp
package as a sidecar next to it and points it at the instance over the
container network.
Two things the image forces. Its command carries the binary path
explicitly because the image declares no ENTRYPOINT, so a bare flag list
would replace the binary instead of extending it. The port is stringified
because Ansible's native types make a bare lookup an int, and compose
rejects a non-string in a command array.
Keep the surface internal. The public vhost answers a bearerless probe
with an SSO redirect, so the guest spec pins maxRedirects to 0 and
asserts no MCP response is ever served; following the redirect would land
on a login page and pass for the wrong reason.
Calls run with administrator rights, so the tool policy is pinned
read-only and the sidecar starts with the read-only flag and a scope
list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Commit 99bc9a94d gave every top-level task in cleanup/ldap.yml and cleanup/oidc.yml an identical delegate_to, tripping the include-uniform-apply-keyword lint and failing CI run 30160077828.
apply: on include_tasks wraps the included tasks in a Block, and delegate_to is Block-valid, so hoisting it to the include site is semantically identical: register still resolves on the original host, the per-task when: gate is unaffected, and the include-site when: does not leak into the block. Leaf-only keywords (failed_when/changed_when) stay per-task per 64eef3440.
Matches the established pattern in keycloak, mediawiki, mastodon, matomo, espocrm, collabora and shopware. setup/ldap.yml and setup/oidc.yml deliberately keep the per-task form, since their undelegated set_fact must not run on the hosting node.
make quality green: test-external, test-integration, test-lint, test-unit all pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
In swarm the ldap/oidc setup+cleanup 'container exec' tasks used
GITEA_CONTAINER_ADDRESS (a swarm-wide-resolved container id) and ran on
the manager without delegation. When gitea is scheduled on a worker,
'docker exec' is node-local, so the manager's daemon can't see that
container -> 'No such container', failing the deploy.
Bring these tasks to parity with the DB/manager-ops tasks (commit
6a5cf7b66): use GITEA_LOCAL_CID + delegate_to GITEA_HOST_NODE (facts
already set by resolve_host_cid.yml in 01_manager_ops before setup/
cleanup run), with the container-exec-resolver nocheck marker. Applies
to every exec incl. the id-lookup steps, else idempotency/cleanup
misfire on worker placement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In swarm the gitea service defaulted to replicas = group size (3), so three replicas on manager + both workers bind-mounted the same NFS-shared /data and concurrently rewrote app.ini with no cross-node locking, leaving it NUL-filled and failing migrate. Gitea is a single-writer app (external MySQL, repos + app.ini on disk), so pin it to one replica; it stays on the shared NFS volume and remains node-mobile for failover.
The DB-patch sed tasks ran manager-local via GITEA_CONTAINER_ADDRESS, which cannot reach a replica scheduled on a worker. Resolve the hosting node + container id first (the same resolve_host_cid idiom 01_manager_ops.yml already uses) and delegate each patch to GITEA_HOST_NODE, so the patches follow the single replica wherever swarm places it. Compose is unaffected: replicas is swarm-only and the resolver short-circuits to localhost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests/lint/repository/test_task_file_numbering.py: numbered task
files use exactly two leading digits, a numbered *_core.yml must be
00_core.yml, per directory the numbers are unique/consecutive and start
at 01 (00 with a core file), numbered includes appear in ascending
order, and shared helpers live in tasks/utils/ instead of the numbered
chain.
Apply it across every role: rename NN_core.yml to 00_core.yml with a
consecutive down-shift of the siblings, move letter-suffix children into
a subdirectory named after their parent task, relocate helpers included
from more than one place into tasks/utils/, and update every reference
(include_tasks, path_absolute, .ansible-lint-ignore, requirement docs).
Legacy narration comments in the touched files are deleted; the upstream
xwiki.properties reference block opts out via nocheck. The unused
MAILU_IP6_PUBLIC var (only ever named in now-removed comments) is dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The swarm reconciler may replace the gitea task container between
resolve-container-id and the exec; each retry re-resolves the id inline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'login source already exists' was matched inline in both the LDAP and the OIDC setup; the filter is the single marker source and normalizes case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a 'timeout' lookup plugin that returns round(base * TIMEOUT_FACTOR)
(group_vars default 1, so value-identical to the old literals). One knob
in group_vars stretches every wait in the stack for a slow uplink:
TIMEOUT_FACTOR: 2 doubles them all.
- plugins/lookup/timeout.py + unit tests (identity, factor scaling,
string coercion, error paths).
- group_vars/all/17_resource.yml: TIMEOUT_FACTOR: 1.
- Convert all 34 literal 'timeout:' task keywords / module args across 25
task+handler files to "{{ lookup('timeout', <base>) | int }}".
- tests/lint/repository/yaml/test_timeout_uses_lookup.py enforces the
lookup in every task/handler timeout (opt-out: # nocheck: timeout-lookup).
- swarm.yml pre-deploy build handler: add the missing timeout (same
network-build hang vector as compose.yml's Build compose).
- Drop 13 pre-existing narration comments in the touched files (comment
lint enforces whole-file on any edited file).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In swarm an app task can be scheduled on a worker, but the role exec'd
`container exec $(resolve-container-id ...)` on the manager -> "No such
container". Resolve the hosting node + node-local container id and delegate
each exec to that node.
- database.py: add a `service_name` want; resolve_host_cid/wait_converged
gain `*_service_name` for cross-stack services (mailu's shared mariadb)
- mailu wait-for-mariadb (DB probe + admin), mastodon db:migrate,
gitea migrate/admin-create, friendica manager-ops/patch/addons/bootstrap
(~13 exec sites) switch to resolve_host_cid + delegate_to
Compose-safe: resolve_host_cid degrades to HOST_NODE=inventory_hostname
(delegate is a no-op) + LOCAL_CID=bare compose container name, matching the
8 roles already using this pattern.
Note: friendica also needs service_replicas=1 (next bundle) so its resolved
container id does not churn across replicas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests/lint/repository/test_name_emoji_and_length.py: every Ansible task
name and GitHub Actions workflow/job/step name must start with an emoji (first
char not 0-9/A-Za-z) and be <= 80 chars.
- emit emoji names at the source: cli/build/include generator now writes
"🔧 setup <app>" / "🚿 flush handlers after <app>" so generated
tasks/groups/*-roles.yml satisfy the convention too
- convert existing task/step/job/workflow names to emoji-prefixed, choosing a
task-fitting icon per action (📦 install, ✅ ensure, 🔍 probe, 🥾 bootstrap,
🚀 deploy, 🧩 service-module load, ⬇️ checkout, 📂 include-routines, ...)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In swarm the stack redeploy recreates the Gitea task; the migrate task execed into the mid-restart container before its entrypoint regenerated /data/gitea/conf/app.ini, so 'gitea migrate' aborted with MustInstalled() 'Unable to load config file'. Add a wait-gate (container exec test -f GITEA_CONFIG, retries 30 delay 2 until rc==0) between the app.ini DB patch and the migration, mirroring the matomo post-redeploy readiness wait.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaced by deep-inspecting the swarm matrix CI failures:
- sys-ctl-cln-faild-bkps: the systemd ExecStart resolved cleanback via the
command_path lookup, which runs on the Ansible controller; the binary is
pip-installed system-wide on the target node, so shutil.which returned None
("command not found in PATH: 'cleanback'"). The ExecStart already runs via
/bin/sh -c, so use the bare command and let the target shell resolve PATH.
- svc-db-rabbitmq: RABBITMQ_EXEC_SHELL used `sh -lc`; a login shell re-inits
PATH from /etc/profile and drops the image's /opt/rabbitmq/sbin, so
rabbitmq-diagnostics was not found (rc 127). Use `sh -c`.
- web-app-friendica: `{{ FRIENDICA_CONTAINER_ADDRESS | quote }}` single-quotes
the swarm $(resolve-container-id ...) subshell, killing the command
substitution ("No such container"). Drop the quote, matching 02_manager_ops.
- web-app-gitea: `gitea migrate` ran without `-c {{ GITEA_CONFIG }}` (admin-create
already has it), so MustInstalled() aborted against the custom app.ini. Add it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Across 12 task files (gitea x5, shopware x5, wordpress install, mattermost
setup), strip the leftover `chdir: "{{ lookup('container', application_id,
'directories.instance') }}"` arg from shell tasks that already drive the
container via `container exec` through the wrapper. The chdir points at
the compose project root, which is only useful when calling docker-compose
CLI verbs. With `container exec` the cwd is the container's own working
directory; the host-side chdir does nothing. Empty `args:` blocks left
behind by the removal are collapsed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a mode-aware `container_address` lookup plugin and a
`resolve-container-id` helper script that resolve swarm task IDs at
exec time (compose mode returns bare names unchanged). Migrates ~25
roles + ~28 task files from `command:` to `shell:` so the lookup's
`$(...)` subshell evaluates correctly, and extends `database` lookup
with an `address` key for cross-role DB exec.
Three new lint tests enforce the contract: every container exec
target goes through the resolver, lives behind an IS_STACK_HOST gate
(walks the include graph one hop deep), and uses `shell:` rather than
`command:` when the target is a Jinja reference. Existing nocheck
suppressions on bare-name DB exec sites (nextcloud redis, listmonk,
moodle OIDC, mailu) are replaced with the lookup-based form.
Fixes the systemd Environment= injection in sys-ctl-cln-disc-space
(it never expanded $(...)) by passing STACK / SERVICE_KEY parts and
resolving inside the script. Extracts shopware admin and mediawiki
extension install bodies into dedicated .sh files invoked via env.
Rewrites password-bearing shell tasks to either `| quote` directly or
the `-e VAR=... | quote` + `sh -c '... ${VAR} ...'` env-bridge pattern.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Across roles/web-app-*, the typical body shape is:
- include_role: sys-stk-full (or sys-svc-compose) cluster prereqs
- <host-fs writes, container exec, compose exec> stack-host only
Under swarm-mode delegation the second group only makes sense on the
host that holds the compose stack (manager), but the role plays on
every host in groups[<role>] so workers abort on missing paths.
Two workflow sweeps (gate-manager-ops + gate-manager-ops-01core, 58
agents total, discover/plan/adversarial-verify per role) plus three
manual fixes surfaced 27 roles to refactor. Each role keeps
sys-stk-full ungated so worker-side prereqs (sys-svc-container, NFS
client) still bootstrap; the trailing manager-only block either moves
into a new tasks/02_manager_ops.yml included with
when: IS_STACK_HOST | bool (SUB_FILE pattern) or gets the gate
inlined on the one or two trailing tasks (INLINE pattern). In
compose mode IS_STACK_HOST stays true so single-host deploys are
unchanged.
Manual catches the workflow agents missed:
- web-app-fediwall: utils/once/flag.yml lives AT THE END of 01_core,
not at the start; preserved verbatim outside the gate so workers
still flip the run_once_<role> fact.
- web-app-xwiki: a pre-existing block(...) wraps sys-stk-full plus
_flush_config.yml; gated only the _flush_config.yml include so
sys-stk-full keeps running on workers.
- web-app-dashboard: 02_javascript.yml (creates /opt/compose/dashboard/
JS dirs) ran BEFORE sys-stk-full and was ungated; added the gate.
Roles touched (INLINE): akaunting, jenkins, magento, mobilizon
Roles touched (SUB_FILE): baserow, dashboard, decidim, erpnext,
fediwall, friendica, gitea, jitsi, keycloak, listmonk, littlejs,
mailu, matomo, moodle, odoo, openproject, opentalk, pixelfed,
shopware, suitecrm, taiga, xwiki, zammad
Registers web-app-gitea as a service provider in the registry by adding
`enabled: true, shared: true, provides: gitea` to its `gitea:` primary
entity in meta/services.yml, so consumers (e.g. matrix-deploy variants)
can pull the role in via a `services.gitea.enabled+shared` flag instead
of needing an explicit `--id`.
Aligns the tasks layout with the project's invokable-role convention:
the bulk of the role's logic moves from `tasks/main.yml` to
`tasks/01_core.yml` (with `include_tasks: utils/once/flag.yml` as the
first task), and `tasks/main.yml` becomes a thin guard that loads
01_core under `when: run_once_web_app_gitea is not defined`. The
remaining numbered task files shift up by one (01_database → 02,
02_setup → 03, 03_cleanup → 04) so 01 stays the canonical core file
the lint expects.
Splits the playwright spec into atomar `test-*.js` modules following
the mattermost/taiga convention: `_shared.js` carries env validation,
the healthz URL helper, and persona re-exports; `playwright.spec.js`
becomes a thin orchestrator that wires the `ignoreHTTPSErrors` setting,
the `beforeEach` env-presence guard, and `require()`s each test module
in turn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure 1:1 rename (69 insertions / 69 deletions across 66 files) to
finish the ``docker_*`` → ``compose_*`` alignment started in
8373468e9. The flag configures whether the compose include flushes
handlers right after run; the ``docker_`` prefix was misleading
(handlers are Ansible-side, not docker-side) and the variable is part
of the same ``sys-svc-compose`` SPOT cluster as ``compose_repository_*``.
Touches the role default, the implementation
(``sys-svc-compose/tasks/utils/up.yml``), all 60+ consumer roles,
the network-utility wrapper + its README, and the
``test_network_create_via_util.py`` lint (both the docstring guidance
and the asserted diff).
Breaking change for any external override that still sets the old
name — no alias is provided.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>
* 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>
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>
* .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>
Single atomic landing for three back-to-back role-meta refactor requirements
(see docs/requirements/008-role-meta-layout.md, 009-per-role-networks-and-ports.md,
010-role-meta-runafter-lifecycle-migration.md).
req-008 — role meta layout
* Move every role's metadata under meta/<topic>.yml with file-root
convention (no wrapping key matching the basename):
- schema/main.yml + config/main.yml.credentials → meta/schema.yml
- users/main.yml → meta/users.yml
- config/main.yml.server → meta/server.yml
- config/main.yml.rbac → meta/rbac.yml
- config/main.yml.compose.services + every other top-level key
of config/main.yml inlined under <entity>.<key>
→ meta/services.yml
- config/main.yml.compose.volumes → meta/volumes.yml
* Drop the legacy schema/, users/, config/ directories from every role.
* meta/schema.yml: support nested credential keys, optional `default:`
field (Jinja literal, not rendered/validated/regenerated), implicit
`algorithm: plain`.
* Materialised paths drop the `compose.` prefix:
compose.services.<e>.<…> → services.<e>.<…>
compose.volumes.<k> → volumes.<k>
<former-top-level>.<…> → services.<entity_name>.<former-top-level>.<…>
* Update every Python consumer (utils/cache, utils/manager, utils/applications,
plugins/lookup, plugins/filter, cli/*) plus every Jinja template, vars/*.yml
and task file to the new paths.
* No legacy fallback. Repository-wide grep for `compose.services.` /
`compose.volumes.` / `schema/main.yml` / `users/main.yml` / `config/main.yml`
is empty outside docs/requirements/ and tasks/utils/migrate_meta_layout.py.
* Add tests/lint/repository/test_role_meta_layout.py guarding against the
old paths returning, and an explicit lint guard that meta/variants.yml
files do not carry a `compose:` wrapper.
req-009 — per-role networks/ports
* Move group_vars/all/08_networks.yml.defaults_networks.local.<role>.{subnet,
dns_resolver} into roles/<role>/meta/server.yml.networks.local.{subnet,
dns_resolver}.
* Delete group_vars/all/09_ports.yml entirely; ports now live on the
service entity that exposes them at
roles/<role>/meta/services.yml.<entity>.ports.{inter,local,public}.
* Rename + int-cast every former top-level `services.<entity>.port`
field to `services.<entity>.ports.inter` (single int).
* Always-category-keyed maps for ports.local / ports.public; relay
spans use {start, end} integers.
* Add PORT_BANDS map to group_vars/all/08_networks.yml — single source
of truth for the per-<scope>.<category> band ranges, consumed by both
the new CLI helpers and the lint test.
* New CLI helpers (cli meta ports suggest, cli meta networks suggest)
walk the live role tree gap-first then increment-fallback, with
deterministic output and non-zero exit on capacity overflow.
* cli create role consumes both helpers when scaffolding a new role.
* Lint extensions: bare-int port shape rejection, host-bound port
collision check (single ints + relay span integers, inter excluded),
band-membership check with documented allow-list for the legacy
web-app-bigbluebutton.bigbluebutton.local.http: 48087 entry.
req-010 — run_after / lifecycle migration
* Move galaxy_info.run_after and galaxy_info.lifecycle out of
meta/main.yml into meta/services.yml.<primary_entity>.{run_after,
lifecycle}; <primary_entity> is the value returned by
get_entity_name(role_name).
* For multi-entity roles whose primary entity has no compose service
(web-app-bluesky → bluesky, web-app-matrix → matrix, desk/dev/drv/
gen roles), create a dedicated metadata-only top-level entry under
<primary_entity>.
* Introduce utils/roles/meta_lookup.py with get_role_run_after /
get_role_lifecycle that resolve <primary_entity> once and read from
meta/services.yml; degrade gracefully when meta/services.yml is
absent OR the field is absent; raise MetaServicesShapeError when the
file exists but is malformed.
* Update every consumer (utils/service_registry, utils/invokable,
cli/meta/applications/resolution/run_after/__main__, cli/meta/roles/
lifecycle_filter/__main__, cli/meta/applications/resolution/combined,
cli/build/{tree,graph,role_include}, cli/deploy/development/{common,
init,deps,deploy}, plugins/filter/canonical_domains_map, etc.) to
use the helper.
* Lint guards: no run_after/lifecycle in meta/main.yml; lifecycle
values restricted to {planned,pre-alpha,alpha,beta,stable,deprecated};
run_after/lifecycle never on a non-primary entity; empty run_after
lists rejected.
Also folded into this change set
* services_disabler now writes `services.<svc>.{enabled,shared}` into
host_vars instead of the old `compose.services.<svc>.{...}` path; the
discover/find pair walks meta/services.yml and only treats entries
carrying enabled/shared/provides as provider declarations (so
metadata-only entries from req-010 do not create spurious provider
rows).
* service_registry skips `run_after` deps whose target was filtered
out via SERVICES_DISABLED instead of failing the whole load.
* cli/mirror/resolver and cli/create/inventory/mirror_overrides emit/
consume the new applications.<role>.services.<svc>.image shape
(mirror.md updated to match).
* utils/domains/list.build_applications_from_roles now reads
meta/server.yml directly (it had been silently returning {} since
config/main.yml stopped existing).
* utils/roles/resource_validation.filter_roles_by_min_storage reads
meta/services.yml.<entity>.min_storage instead of the legacy
compose.services path.
* web-app-bluesky pre-clones the social-app build context BEFORE
sys-svc-compose so the build context is on disk by the time
sys-stk-front-proxy/tasks/main.yml flushes the docker build handler.
* 4 meta/variants.yml files (svc-ai-ollama, web-app-phpmyadmin,
web-app-wordpress, web-app-keycloak) lose their `compose: services:`
wrapper so deep-merge over meta/services.yml lands on the new
file-root shape.
Validation
* Unit tests: 1571 passed.
* Lint tests: 50 passed.
* Integration tests: 161 passed (1 skipped).
* Full-cycle fresh-purged-apps deploy succeeded for the 8-app validation
set: web-app-{nextcloud,wordpress,yourls,bigbluebutton,bluesky,gitea,
discourse,mattermost}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces inline `applications`/`users` merging with dedicated lookup
plugins and runtime hydration. Introduces `add_csp_hash` filter,
integrates sort into `find_dock_val_by_bkp_entr`, splits the Keycloak
one-shot realm import into `05a_realm_import_run.yml`, and fixes the
Mastodon administrator task YAML. Removes obsolete `csp_hashes` filter,
`sys-token-store` Jinja filter, and empty CLI setup modules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename lookup plugin from `docker` to `container`
- Switch all lookups from `lookup('docker', ...)` to `lookup('container', ...)`
- Normalize compose file keys:
- docker_compose → compose
- docker_compose_override → compose_override
- docker_compose_ca_override → compose_ca_override
- Centralize compose path resolution via docker_paths_utils
- Update compose_file_args to always derive paths from DIR_COMPOSITIONS
- Align all roles, templates, and vars with container/compose naming
- Update and rename unit tests accordingly
This prepares the codebase for engine-agnostic container backends
(e.g. Docker ↔ Podman) and removes legacy docker-specific assumptions.
https://chatgpt.com/share/6987a7cc-d374-800f-99ad-bb4c8f92c59e
- Replace lookup('docker', ..., want='...') with lookup('docker', ..., 'path')
- Allow both top-level keys (directories/files) and dotted paths
- Simplify docker lookup API and reduce lazy dict propagation
- Update unit tests to reflect new lookup semantics
https://chatgpt.com/share/6984ccea-8290-800f-a6d7-ec0e94df95a0
Remove implicit docker_compose variable usage and resolve all docker paths
(files, directories, env) via lookup('docker', application_id).
This prevents scope leakage, centralizes path resolution, and makes docker
context deterministic across roles and handlers.
https://chatgpt.com/share/6984ccea-8290-800f-a6d7-ec0e94df95a0
- add sed_escape Jinja filter for sed replacement safety
- escapes \, &, and the active sed delimiter
- designed explicitly for sed s/// replacement context
- patch all sed-based config updates across roles
- espocrm, friendica, gitea, matomo, moodle, wordpress
- ensure every Jinja value in sed replacements uses sed_escape
- use correct delimiter-specific escaping (/, |, #)
- add robust integration test to enforce sed_escape usage
- scans all roles/*/tasks/**/*.yml
- detects sed substitutions with Jinja in replacement part
- ignores delimiters inside {{ ... }} blocks
- fails CI if sed_escape is missing
- harden repository against sed injection, delimiter breakage,
and credential corruption in config patching tasks
https://chatgpt.com/share/6974fb45-1ee8-800f-80bd-c71122e4ac02
Ensure all password usages in shell commands are safely quoted using the
`| quote` filter to prevent shell injection issues and test failures.
Affected roles:
- svc-db-postgres
- web-app-gitea
- web-app-mediawiki
- web-app-moodle
- web-app-shopware
- web-app-xwiki
https://chatgpt.com/share/6974dd33-9470-800f-b469-d31fabcd59e1
* Move top-level features flags to docker.services.<feature>.enabled
* Replace features.local_ai with docker.services.ollama.shared for central AI networking
* Switch central LDAP network gating from docker.services.ldap.enabled to docker.services.ldap.shared
* Add per-app service feature entries like css, javascript, hcaptcha, recaptcha under docker.services
* Update Gitea to use derived GITEA_LDAP_ENABLED and adjust IAM conditions
https://chatgpt.com/share/69641f2f-5d6c-800f-8927-fdb3580e7964