With the psql command tags gone the OIDC plugin task stays `ok`, but PeerTube
was still force-recreated on every deploy: "Back-fill column defaults so
bootstrap INSERT keeps NOT NULL" runs an unconditional `ALTER TABLE …
SET DEFAULT`, database_query reports every non-SELECT as changed, and the
task notifies compose-up, which runs `compose up -d --force-recreate`.
A SELECT on information_schema.columns now lists the bootstrap columns
(`nodeVersion`, `nodeABIVersion`) that still have no default, and the ALTER
with its compose-up notification runs only when that list is not empty. A
fresh database still gets the defaults before PeerTube's bootstrap INSERT
retries; every later deploy skips the task. The unused register of the
ALTER task is gone, and both read-only checks now live under
files/sql/check-application/ as the sibling-prefix rule requires.
Refs #1164
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ansible warns that the name is already in use and carries on. Carrying on is the problem: inside the loop the original value is gone, so every lookup, template and nested include reading that name sees the loop item instead, and the task still reports success. The damage lands in a log nobody greps.
The lint collects every key declared in group_vars and in each role's vars or defaults, then reports any loop_var matching one. A denylist of known-bad names would have found application_id and missed domain, which is the same class in four more places.
All seven existing sites are marked, each with the reason read at the source rather than inferred. The three application_id loops pass the role id as the item, and the include_vars that follows loads that role's own application_id with the same value, so both meanings coincide by construction. The four domain loops hand the vhost to sys-stk-front-proxy, whose tasks read front_proxy_domain from exactly that name, so the shadowing is the interface.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A handler name was doing two jobs at once: the line an operator reads in the
play output, and the identifier a `notify:` binds to. The two pull in opposite
directions -- the log wants "restart the SSH service, with diagnostics", the
binding wants a token that survives quoting, YAML flow style and a shell
round-trip -- and a name that changes for readability silently changes which
handler runs.
Every handler that a notify reached by name now carries a `listen:` with that
exact wording, so the binding is explicit before anything is renamed. The
topics themselves become identifiers: `compose up` reads `compose-up`,
`Restart NFS ganesha` reads `restart-nfs-ganesha`, 22 of them across both
sides of every notify. Where the topic is assembled at runtime, both branches
moved together: the conditional in sys-service carries two literals, and the
LDAP renderer interpolates `import-{{ folder }}-ldif-files`, which is what
makes those handlers reachable at all.
Freed from the binding, the names take the emoji prefix every other task line
already has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PeerTube distributes video over WebRTC and the role proxies its tracker socket on purpose (templates/proxy.conf.j2:48, with a raised read timeout because peers ping every fifteen minutes). Tor carries no UDP and an onion client cannot hold peer connections, so an onion PeerTube is the app without the feature it exists for. What remained was plain HTTP video at Tor throughput, and a login path that only worked through a sed over the vendored dist of a third-party plugin.
That patch is gone with this change, along with PEERTUBE_OIDC_ALLOW_INSECURE_ISSUER, the variable that gated it, and both tor pins in meta/variants.yml. The root TODO entry describing it is removed too - it carried a three-step exit path (upstream MR, fork, delete) that no longer needs walking. The https-on-onion entry keeps its analysis and loses only the sentence that named peertube among the per-consumer workarounds.
The discovery gate stays. Failing the deploy when the plugin registered without building a client is not tor-specific and was the honest half of e64ed1cd9.
tor.enabled becomes a literal false rather than the block being deleted. Two lints define the contract: test_tor_contract requires every domain-serving role to declare the block, and test_sso_variant_requires_tor names a literal false as the first of three cases that may opt out of pinning the onion on with a reason. Deleting the block failed both; this is the language they specify.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
openid-client v6 refuses a non-HTTPS issuer, and an onion-exclusive Keycloak has
none: the onion publishes only HiddenServicePort 80, and https there is blocked
on four separate things (see TODO.md). PeerTube's plugin therefore threw
OAUTH_HTTP_REQUEST_FORBIDDEN during discovery, never built a client, and answered
2326 logins with "OpenID provider unavailable" — while `test -d` on the plugin
directory reported the deploy as ok.
Two changes, one honest and one not:
The gate at the end of enable.yml now fails when the container log carries
"Initial OpenID discovery failed", so a plugin that registered without building a
client stops the deploy instead of surfacing hours later in the browser suite.
The sed over the installed dist is a hack and is marked as one in TODO.md. It
widens the trigger of the escape hatch upstream already implements — the plugin
gates `openid.allowInsecureRequests` on NODE_ENV alone, and setting NODE_ENV=dev
in production would swap PeerTube's whole runtime profile. The `grep -q` after
the substitution is what keeps it tolerable: an upstream release that moves the
line fails the deploy loudly rather than silently doing nothing.
Both are driven by one variable, PEERTUBE_OIDC_INSECURE_ISSUER, which requires
SSO enabled AND tor enabled for peertube AND an .onion issuer, so no deploy
without Tor can execute the patch or ever fail on it. Verified with Ansible's own
templar across seven cases, including https://onionsoup.example.com/ to catch a
too-greedy match. The sed itself was run against the real 2.0.1 tarball: exactly
one hit, `node --check` clean, and the patched expression exercised in four
environment combinations.
Exit path in TODO.md: the plugin is an official Framasoft one (AGPL-3.0 in the
repo root, the npm package merely ships no license field), and the proper fix is
a `registerSetting({ name: 'allow-insecure-issuer' })` OR-ed into the same
condition. Issue text and patch are written; upstream filing is waiting on a
framagit account.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An untyped inspect searches every docker object kind, and a role's overlay network carries the same entity name as its main container, so the call can resolve the network and report a missing State key instead of naming the container. That is how the openresty assert died on every swarm job of run 30709070050.
Extend container-exec-resolver to cover inspect, stripping raw markers and quoted spans first so the flag walker survives Go template format strings. Add container-inspect-type, scoped to roles/ because the collision follows the entity naming convention and the outer act namespace carries no role networks. Sweep 40 call sites; suppress the delegate_to-resolved CIDs and the compose co-location probes with a stated reason.
Move the five container rules into tests/lint/ansible/shell/container/ and drop the container_ prefix from their filenames. Register both markers in the suppression docs, where the resolver marker was missing entirely.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The new handler had neither retries nor an assertion. update_config carries no failure_action, so Docker's default is pause: one flapping healthcheck inside the monitor window pauses the rolling update, docker service update exits non-zero, pipefail propagates it and any_errors_fatal kills the whole deploy - where the neighbouring converge handler would have retried. Give it five attempts fifteen seconds apart, matching how the registry-sync handler beside it already treats a transient action.
The registration probe only notified; nothing checked that the recreation actually registered the plugin. A plugin that can never register - corrupt pnpm tree, engine mismatch, invalid library code - therefore reported changed on every run, recreated the stack every time and still left the play green. Flush the recreation, re-resolve the container id the recreation invalidated, and assert the registration with a bounded wait, so that case fails loudly in the run where it happens instead of churning forever in silence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docker stack deploy leaves tasks running when the service spec is unchanged, so a change made inside a container never reaches the process. PeerTube's CLI installs plugins with register:false, so the OIDC plugin sat installed and enabled in the database while the running server had loaded nothing: /login rendered only the local form and both OIDC specs timed out through all three attempts. Compose never showed it because its handler force-recreates.
Add a container recreate notify. In swarm it forces a task recreation per stack service and waits for convergence; in compose it maps onto the existing --force-recreate, so a role states the intent once and both modes honour it.
In the peertube role the install probe now tests node_modules/<npm-name>, where pnpm puts the plugin code; the old path was the data directory that registerPlugin creates at boot, so it never existed and the plugin was reinstalled on every run. The settings upsert derives changed from the rowcount via RETURNING, because database_query reports changed from the kind of statement rather than from what it touched. A registration probe notifies when the running process has not loaded the plugin, so an already-broken instance heals itself instead of walking past both earlier probes. The plugin:enable task is gone: npm has no such script, the call always failed and failed_when swallowed it.
The spec now polls the button lookup instead of counting once twenty milliseconds after goto, while still asserting on the URL so a failure keeps printing the address the browser actually sat on.
Co-Authored-By: Claude Opus 5 (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>
Replace scripts/system/diagnostics/rescue.sh with utils/diagnostics/
container.py: one workflow call on failure captures the current level
(containers with logs+inspect, swarm services, journal, host resources,
in-play role dumps) and recurses itself through every DiD boundary via
self-copy, from the runner down to the innermost runtime (RESCUE_MAX_DEPTH).
compose and swarm now share an identical failure step; the swarm per-node
collector wrapper is gone. All 17 script-only Ansible rescue: blocks are
deleted and their bare block: wrappers inlined (plus a wordpress leftover);
the three justified blocks (postgres pg_hba, xwiki extensions, keycloak
cross-node recovery) keep their evidence tasks and now end in a fail task.
Two lints enforce the state: rescue: is forbidden without a
'# nocheck: rescue-<reason>' first-task marker, and a block without
rescue/always/when or another scoping key must be inlined.
sys-service-terminator hoists become to the include apply. Also move
routine/08_cleanup.sh to utils/clean/teardown.sh and topology/ to
utils/topology/ so routine/ holds only the sequenced flow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every Ansible rescue: block used to carry ad-hoc per-role failure dumps that
drifted apart and captured different subsets. Replace them all with one
diagnostics script (scripts/system/diagnostics/rescue.sh) that snapshots
containers (full logs plus inspect per container), swarm services (ps plus
logs per service), journal, host resources, runtime info and stats into a
per-run folder and always exits 1 so the deploy still fails. A new lint
enforces the shape: every rescue ends with the script, at most one
nocheck-justified extra task before it (postgres pg_hba dump, xwiki
extension-repo probe), and a nocheck rescue-recovery exemption for the one
block that must genuinely recover: keycloak object updates, where a
cross-node reschedule needs a swarm-wide node re-resolve plus a delegate_to
change that only a rescue can express; its recovery is kept and now also
captures diagnostics before the terminal failure.
The snapshot folder path is a group_vars SPOT: DIR_RESCUE_DIAGNOSTICS and
DIR_VAR_LIB live in group_vars/all/05_paths.yml as plain strings; new env
handlers derive INFINITO_RESCUE_DIAGNOSTICS_DIR and INFINITO_DIR_VAR_LIB
from it for the generated .env, replacing the default.env entries, and the
play-level environment forwards the value to every task host so rescue.sh
needs no second default. CI uploads the folder like the inventory and
playwright artifacts: compose copies it out of the deploy container in
dedicated.sh, swarm captures per node (manager and both workers) into
node-named subfolders.
Deleting the ad-hoc dumps orphaned helper files (postgres inspect_container,
terminator docker_diagnostics, peertube diagnostics, matomo bootstrap debug,
xwiki and openresty rescue includes) and 14 role vars; all removed. Generic
pieces of those dumps (runtime version and info, stats, dmesg oom evidence,
swapon) moved into the SPOT script instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The role rendered its vhost directly via stack_host_template, bypassing
sys-svc-webserver-https and sys-front-tls entirely, so no HTTPS server
block or certificate ever existed for peertube domains. Swap the loop
body to include_role: sys-stk-front-proxy per domain (mastodon pattern)
and rename the template to proxy.conf.j2 so the front proxy's fileglob
discovery picks it up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
oidc/enable.yml ran the plugin check/install/enable and the two mem_limit container updates against PEERTUBE_CONTAINER_ADDRESS on the manager, but peertube lives on a worker in swarm -> "No such container". Worse, the two updates used command:, which never expands the $(...) swarm address (passed literally). Resolve the hosting node + local CID (resolve_host_cid) and delegate_to it for all five container ops, and convert the two command: tasks to shell:. Compose unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The container update mem-limit tasks used the raw compose name, which does not exist as a container in swarm. Use PEERTUBE_CONTAINER_ADDRESS like the other container ops in the file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
container exec/inspect ran on the swarm manager while the task may run on
a worker, yielding No such container or an empty container id. Use the
existing resolve_host_cid helper (HOST_NODE + LOCAL_CID + delegate_to)
for the ollama model pre-pull, the taiga admin migrations and the
peertube post-backfill health inspect, matching the fider/matomo/decidim
pattern. The helper resolves to the local host + compose cid in compose
mode, so that path is unchanged.
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>
Resolve 69 conflicts across ~70 files: union main's image-version bumps and new service fields (bond, resource limits) with the swarm branch's default_placement / NFS / IS_STACK_HOST config; retire the three per-deploy-type test workflows main still edited (consolidated into compose+swarm); adopt main's run-once 01_core dispatchers while re-applying each role's swarm guard. Fix the post-merge lint surfaced by main's stricter rules: bond + resource-budget keys, container-exec stack-host gating + resolver routing, leaky/dead proxy vars, suitecrm once-flag, xwiki variant nocheck, nextcloud oversized-block to gated include, matrix-mdad capped at 3g. make autoformat clean, make test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
database_query opens query_file with Path().open() on the managed host. In the swarm test the managed host is a remote SSH node that does not carry the repo files, so the controller-side path '{{ role_path }}/files/sql/...' does not exist there and the query aborts with 'could not read query_file ...: No such file or directory'. Pass the SQL as content read on the controller via lookup('file', ...) instead, which works in both compose and swarm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The schema back-fill uses database_query, whose module opens query_file on the remote host. In swarm the play targets all nodes, so the task ran on workers that have no checked-out repo and failed with 'could not read query_file check-application-table.sql'. Move the schema patch and the OIDC plugin lifecycle into 02_stack_host.yml, included from main.yml when IS_STACK_HOST, so they run only on the stack host that owns the repo; renumber the task includes (01_create-domains, 03_fix-application-schema). Compose (IS_STACK_HOST true) is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test_service_core_first_task_run_once requires tasks/01_core.yml to set the
run_once flag (include_tasks: utils/once/flag.yml) as its very first task so the
guard is in place before the role body runs and the variant-aware loader cannot
recurse. Move the flag include from the end to the top in the shared-provider
roles that had it last (bigbluebutton, flowise, gitlab, matrix, mattermost,
moodle, openproject, openwebui, peertube, suitecrm, xwiki).
Also give the template-only providers svc-db-redis and svc-db-memcached the
run-once guard (main.yml includes 01_core.yml when run_once_<role> is undefined;
01_core.yml sets the flag) so they satisfy the shared-provider run_once guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Declare the 14 services that consuming roles reference but no role provided
(bigbluebutton, xwiki, openproject, gitlab, mattermost, matrix, zammad,
openwebui, flowise, peertube, moodle, suitecrm, redis, memcached) as shared
providers: set enabled/shared/bond on each entity so the service registry
registers them and the dangling references resolve.
Add test_shared_service_provider.py (lint): fail when a referenced shared
service has no provider and its name matches a role's entity/provider that does
not register it (suppressable via '# nocheck: shared-provider').
Guard the 9 web-app shared services with the run-once 01_core.yml pattern
(main.yml includes 01_core.yml only when run_once_<role> is undefined; the core
flags itself via utils/once/flag.yml) so they deploy once when pulled in by
multiple consumers. svc-db-redis/memcached are template-only (no runtime tasks).
Co-Authored-By: Claude Opus 4.8 <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>
Adds plugins/action/conf_template.py, a drop-in `template:` replacement
that returns `skipped: true` whenever IS_STACK_HOST evaluates to false.
Routes the rendered Jinja through ansible.module_utils.parsing.convert_bool
so both Python bool and string "True"/"False" templar outputs resolve
correctly. Inherits every standard template module argument.
Migrates seven nginx-tree consumers that previously aborted on workers
with "Destination directory does not exist": sys-front-tls,
svc-db-openldap, sys-svc-webserver-core (nginx.conf + default_server.conf),
web-app-matrix (synapse vhost), web-app-peertube (per-domain vhost),
web-opt-rdr-domains (redirect vhost), and web-app-prometheus.
Tags twelve legitimate bare `template:` callers with
`# nocheck: conf-template`: per-host system configs (locale, nix, systemd
manager defaults, journald, msmtprc) and per-app compose-dir configs
(magento x2, mailu x2, moodle, odoo, taiga) that intentionally bypass the
project's nginx tree.
Adds tests/lint/ansible/modules/test_conf_template_migration.py as the
drift guard: any future `template:` task whose `src:` ends in `.conf.j2`
must adopt conf_template or carry the nocheck marker. Adds seven unit
tests for the plugin covering skip semantics, dispatch, mode defaults,
mode overrides, full-arg passthrough, and the string-bool templar path.
Co-Authored-By: Claude Opus 4.7 <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>
- `library/database_query.py`: drop `no_log: True` from `config` /
`named_args` argument_spec; add `mask_values` bool param (default
True). When True, the module copies their string values into
`module.no_log_values` at runtime — same effective redaction as
before, but now toggleable per task.
- Roll out `mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"` to
every `database_query` call (38 sites across web-app-{odoo,
openproject, peertube}) so the playbook constant in
group_vars/all/00_general.yml drives the redaction end-to-end. Set
it to false on demand for debugging unredacted rc/stderr.
Bundled: roles/web-app-openproject/tasks/02_ldap.yml also drops the
`async:`/`poll:` lines from Update/Create LDAP auth source. The
combination of `async` + `database_query` local-action + `no_log`
masked the async wrapper's result fetch with "Action failed: Unknown
error" while the SQL had actually succeeded (verified: 1 row in
`ldap_auth_sources` after the failing run). Sync execution matches the
pattern used by every other `database_query` caller in the repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Newer Ansible refuses to bool-coerce list values in conditional contexts
(`Conditional result (False) was derived from value of type 'list'`).
The `until:` block of the wait-for-application-table task used
`(_peertube_app_table_check.rows | default([]))` directly as a sub-clause,
which is a bare list. Wrap with `| length > 0` for an explicit bool result.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces controller-side `community.postgresql.postgresql_query` /
`community.mysql.mysql_query` calls with a project-local `database_query`
Ansible module that wraps `container exec -i <container> <psql|mysql>`
and pipes SQL via stdin. The module routes via the DB container's own
network so the same task works identically in shared mode (central
`svc-db-*` container) and non-shared mode (per-role `<entity>-database`
container) — the `container` field of `lookup('database', …)` is
shared-aware and the module just reads it. SQL payloads come from a
file (`query_file:`, preferred) or as a raw string (`query:`, escape
hatch). `named_args:` substitutes `%(name)s` placeholders with engine-
appropriate escaping (postgres single-quote-doubling, mariadb backslash
escapes), so role tasks stop string-interpolating Jinja into SQL.
New backends extend the module's `_ENGINES` dispatch table; everything
else is engine-agnostic. The module ships at `plugins/modules/` (Ansible
collection-style layout) — the shebang lint test learned to recognise
that path as a valid module location alongside the existing per-role
`library/` layout.
Two lint tests enforce the convention:
* `tests/lint/ansible/modules/test_no_raw_db_query_module_usage.py`
forbids `community.postgresql.postgresql_query` /
`community.mysql.mysql_query` in role tasks. Opt out per-line with
`# nocheck: database-query` (used by `svc-db-postgres/tasks/02_init.yml`
where the role manages its own DB and the `database` lookup would be
self-referential).
* `tests/lint/ansible/modules/test_no_raw_database_query_string.py`
forbids inline `query:` on `database_query:` tasks. Opt out with
`# nocheck: database-query-raw` (no opt-outs in the current tree —
every SQL payload reaches the module through a `.sql` file under
`files/sql/`).
Role migrations:
* `web-app-peertube` — 3 tasks migrated, 3 new `.sql` files,
`templates/upsert-oidc-plugin.sql.j2` deleted in favour of
`files/sql/upsert-oidc-plugin.sql` + `named_args:`.
* `web-app-openproject` — 3 tasks migrated, 3 new `.sql` files for
the LDAP-auth-source check/update/insert chain.
* `web-app-odoo` — 33 tasks migrated, 9 new `.sql` files for the
LDAP / admin / init-database flows; 17 pre-existing OIDC `.sql`
files renamed snake_case → kebab-case and relocated from
`files/oidc/` to `files/sql/oidc/` (via `git mv` so rename history
stays intact).
Unit tests cover every dispatch-table entry, the named_args escaper
(strings, ints, floats, bools, None, single-quote and backslash
injection vectors), and the password-redactor that hides
PGPASSWORD/MYSQL_PWD assignments from the module's returned `cmd`
field.
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>
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>
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>
Peertube v8.x (and intermittently under the mutable `production-bookworm`
tag) ships a migration that declares `application.nodeVersion` and
`application.nodeABIVersion` as NOT NULL without column defaults, while
the bootstrap `Application.loadApplication()` only INSERTs
`migrationVersion` into the single-row `application` table. On a fresh
database that INSERT fails with
null value in column "nodeVersion" of relation "application"
violates not-null constraint
and peertube crash-loops before it can add the OIDC plugin or serve
traffic. The failure blocks both the local fresh-purged deploy and the
corresponding CI job.
Patch it at the database level once peertube has created the table on
its first crashing startup: wait for `public.application` to exist, run
an ALTER TABLE that sets defaults on the two offending columns
(`nodeVersion='', nodeABIVersion=0`), and wait for the container to
come back healthy. Peertube itself overwrites the values with real
node/ABI version strings on every subsequent boot, so the defaults only
cover the bootstrap INSERT.
Validated via make deploy-fresh-purged-apps APPS="web-app-joomla
web-app-peertube" SERVICES_DISABLED="matomo,email" FULL_CYCLE=true:
Pass 1 ok=1576 failed=0, Pass 2 ok=1444 failed=0, peertube 3/3
Playwright scenarios green in both passes.
Removes the placeholder interpreter (utils/dict_renderer.py) in favor of
standard {{ lookup('config', ...) }} / {{ lookup('domain', ...) }} calls
inside role configs, and eliminates the `domains` set_fact stage in
tasks/stages/01_constructor.yml by introducing a cached get_merged_domains
helper plus a new `domains` lookup plugin (full map + by-app subtree).
The `domain` lookup no longer depends on a pre-computed `domains` variable;
it delegates to get_merged_domains, which respects inventory-level overrides
from both `applications:` (authoritative canonical source) and `domains:`
(direct override map). Caching mirrors the applications pipeline — keyed on
(roles_dir, variables signature, override fingerprint) with a re-entry guard.
Replaced `{{ domains[app_id] }}` / `{{ domains }}` usages across web-app-matrix,
-bluesky, -mastodon, -peertube, -keycloak, test-e2e-playwright, and
group_vars. Added two performance tests for lookup('domains') mirroring the
applications cache assertions (≥50x warm-to-hot ratio, 120s cold budget).
Plugin install via `container exec` shares the cgroup with the long-running
peertube node process. Combined RSS was tripping the 8g mem_limit and
SIGKILLing peertube, which surfaced as "Container is restarting" on
subsequent exec attempts.
- Temp raise mem_limit + unlimited swap for the install window, restore
the compose-defined value on the happy path only
- Derive the bumped ceiling from PEERTUBE_MEM_LIMIT + install heap cap
via a new role-local `install_mem_limit` filter (overhead=4 covers
native RSS and spawned npm/node-gyp workers)
- Extract the SQL upsert into templates/upsert-oidc-plugin.sql.j2
- Move enable/disable OIDC tasks under tasks/oidc/ and rename the
diagnostics script
- Document the extract-code convention in docs/contributing/tools/ansible.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wraps the auth-openid-connect plugin install in block/rescue so that when
the container exec trips (e.g. "Container is restarting", cgroup OOM kill),
a dedicated script dumps container state, health, resource limits, stats,
last 250 log lines, host memory+swap, and dmesg OOM-killer evidence before
the task fails explicitly. Makes the actual cause visible in CI without
another round-trip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`npm run plugin:install` inherits the container-wide NODE_OPTIONS (~35%
of mem_limit, multi-GB) and pushes constrained CI runners over their
host RAM, getting SIGKILLed (rc=137) a few seconds in. Override
NODE_OPTIONS just for the install exec with a tight heap cap
(PEERTUBE_OIDC_PLUGIN_INSTALL_MAX_OLD_SPACE_MB, default 1024 MB) and
add 3 retries (15s delay) to absorb transient host memory pressure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Update all remaining nginx_paths lookups to use the strict positional API:
lookup('nginx_paths', want_path, domain)
This removes legacy argument ordering (domain first) and aligns all callers
with the documented nginx_paths contract.
https://chatgpt.com/share/6985d642-b40c-800f-9cdf-ed54fc484a92
with explicit per-application TLS resolution.
Key changes:
- Introduce sys-front-tls-layer as the single TLS abstraction
- Centralize certificate paths via TLS_CERT_FILE / TLS_KEY_FILE / TLS_CA_FILE
- Replace sys-svc-letsencrypt SSL templates with generic TLS layer includes
- Add tls_* Jinja filters (enabled, mode, protocol, port, websocket)
- Resolve TLS mode per application (letsencrypt | self_signed | off)
- Remove implicit defaults and WEB_* globals to fail fast on misconfiguration
- Support self-signed certificates with SAN awareness
- Make TLS behavior explicit and consistent across all roles
This decouples application logic from certificate providers and
allows switching TLS flavor without touching app roles.
https://chatgpt.com/share/69727c61-8e68-800f-a267-473a7e627978
Add dedicated database_reach_host variable to sys-svc-rdbms and use it for
direct database access in application roles.
This separates control-plane database access from generic Docker reach logic
and improves portability and CI stability.
Updated:
- sys-svc-rdbms vars: add database_reach_host
- web-app-openproject: use database_reach_host for LDAP DB queries
- web-app-peertube: use database_reach_host for OIDC DB updates
https://chatgpt.com/share/696863e8-4370-800f-bea0-ad48b98ba0f6