The akaunting service is built from the akaunting/docker repository
(ref master), whose Dockerfile downloads the latest Akaunting release into
/var/www/html, the shared data volume. Since 2026-10-26 that Dockerfile
builds on php:8.2, and Akaunting 3.3.0 (released 2026-10-27) requires
PHP >= 8.2.
The swarm setup job instead ran the upstream tag akaunting/akaunting:3.1.21
(PHP 8.1.34) against the same volume. It executed the 3.3.0 code there and
failed with "Ask your hosting provider to use PHP 8.2.0 or higher" (main
run 40507642995, Akaunting#0). Compose mode was unaffected because its
setup uses `compose run akaunting`, i.e. the built image.
Read the image from the service spec and start the setup job with it, so
code and runtime always come from the same build. Drop the now unused
AKAUNTING_IMAGE/AKAUNTING_VERSION vars.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The install one-shot seeds the administrator from ADMIN_PASSWORD and runs only while
the first-run marker is absent, so after a credential rotation the account keeps the
secret the deployment no longer holds. The playwright trace of the run shows the
consequence: the native login never leaves /auth/login.
Akaunting ships no first-party command for this. user:seed only invokes a seeder and
sets no password, so the realignment goes through artisan tinker, which composer.json
lists under require rather than require-dev and is therefore present in the served
image. ADMIN_EMAIL and ADMIN_PASSWORD reach it through the environment, never through
an argument.
The password is assigned in clear text deliberately: App\Models\Auth\User defines
setPasswordAttribute, which bcrypts on save, so hashing before assignment would store
a hash of a hash. The fourth test case pins that down by asserting the raw value on
the model after save.
The role had no host/cid resolution at all -- setup/swarm.yml sidesteps it by scaling
to zero and running a one-shot service, which is too coarse for a per-run task -- so
it gains resolve_host_cid and addresses the container directly.
The snippet is not standalone PHP: it carries no opening tag because tinker evaluates
what it is given. The mirrored test therefore stubs App\Models\Auth\User and
Illuminate\Support\Facades\Hash and evaluates the file, covering the missing user, the
missing email, the already-matching password (no save) and the differing one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).
This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.
Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:
* 7 `src:` values built from a role variable holding a bare filename
(svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
The variable doubles as the destination name inside the container, so the
language segment belongs at the call site, not in the variable.
* `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
18 roles ship exactly that and have no template fallback: the play would
have died on each.
* `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
interface now reads `files/javascript/javascript.js`.
* `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
path through `path_absolute(... 'files', ...)`.
* 4 `ansible.builtin.script:` invocations naming a bare command
(pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
* `dev-python/files/shell/install.sh` pointed at a sibling role's script.
Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
meta/volumes.yml says where a volume lands inside its container. Wherever a
second file spelled the same path out, the two drifted independently: the mount
moves, the consumer keeps pointing at the old place, and nothing fails until the
process looks for content that is not there. e031b7f61 fixed exactly this for
web-app-moodle; this generalises it and makes the rule hold.
The lookup gains what a caller needs. `targets` maps each mounting service to
its container path, and `target` is the one path an entry mounts at -- present
only when every mount agrees. An entry that lands at different paths per service
omits `target` rather than picking one, and the caller names the service:
"{{ lookup('volume', application_id, 'prometheus_config').target }}"
"{{ lookup('volume', 'web-app-prometheus', 'host_sys').targets['node-exporter'] }}"
66 literals across 48 role files now derive. Replacement only happened where the
path stood alone -- /srv/app inside /srv/app/data is a different path -- and
every rewritten file was re-parsed, Jinja and YAML alike. Three sites the tool
refused rather than guess were done by hand: zammad's path sat in a Jinja list
literal, seaweedfs' inside a quoted string feeding a filter, and openresty's
/var/run/docker.sock is not a mount target at all but the socket on the host,
which only happens to spell the same string. That one carries the single
nocheck.
tests/lint/repository/spot/test_volumes_path_single_spot.py keeps it that way.
roles/*/files/ is out of scope by construction: it is copied verbatim into an
image and executed there, so a Dockerfile or entrypoint has no Ansible to
evaluate a lookup with.
One defect this found, and it was in the tooling rather than the roles: deciding
ambiguity AFTER filtering short paths made web-app-prometheus' host_sys look
single-valued -- it mounts /sys for cadvisor and /host/sys for node-exporter,
and /sys is below the length filter. The template would have rendered an
undefined `.target`. Both the converter and the rule now decide ambiguity over
every target and filter afterwards, and that call reads targets['node-exporter'].
Verified by resolving rather than by reading: all 70 accesses in the diff were
run against the real registry and every one yields a non-empty path equal to the
literal it replaced. make test-lint: 328 passed.
Not verified: no Ansible render, so a call site whose surrounding context cannot
build the applications registry would still fail at deploy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AKAUNTING_SETUP_MARKER was built from docker_volume_path, which hardcodes
/var/lib/docker/volumes/<name>/_data. The CI swarm lab relocates each
node's docker data-root to /mnt/docker-fs/docker, so the marker touch hit
ENOENT on a directory that was never there, while the volume itself sat
healthy at the relocated path.
Same root cause, silent: the marker stat reported exists=false on every
relocated host, so the one-shot installer re-ran on each deploy.
The mountpoint now comes from the daemon. The resolve sits behind
sys-stk-full because on a genuine first deploy the volume does not exist
yet and volume inspect would abort the very first install. The stack-host
gate moved to a single include, so nothing evaluates the marker on hosts
where the register is undefined.
docker_volume_path stays for now; web-app-magento is its last caller and
is converted in the follow-up.
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>
The install and realm-import waits accepted Shutdown as success, so a crashed job passed silently: akaunting went green without ever installing, and a keycloak import crash left synapse against a realm-less provider, surfacing as matrix Playwright OIDC timeouts. The state probe moves to a shared sys-svc-compose script where only Complete passes, Shutdown fails loudly with the task error, and Failed/Rejected count as pending because the jobs now restart on-failure with three bounded attempts, turning the observed import race from a coin flip into self-healing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The setup passed --no-start as the container command, which the ca-trust wrapper exec'd as a binary (exit 127); the flag does not exist in the image's entrypoint at all. Invoking /usr/local/bin/akaunting.sh with no arguments is the real one-shot contract: with AKAUNTING_SETUP=true it runs artisan install and exits without starting Apache. The swarm wait gate also accepted Shutdown as success and let the crashed job pass without ever installing; the state probe now lives in install_job_state.sh and only Complete passes, while terminal states fail loudly with the task error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The include-gated header is the formal task_gate directive the compose-verb and compose-chdir lints read; deleting it as narration broke both. The install task name drops below the 80-char limit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AKAUNTING_SETUP=true in the rendered env re-ran the installer on every container recreation between deploys (restore drill, repair restart) and crash-looped against the installed database. The flag is gone from the env entirely: compose now installs like swarm through an ephemeral one-shot (stop, compose run --rm -e AKAUNTING_SETUP=true akaunting --no-start, up --force-recreate), included mode-agnostically from tasks/setup/{{ DEPLOYMENT_MODE }}.yml with the swarm job renamed into the same folder. The first-run gate reads the marker stat directly instead of a set_fact indirection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The one-shot install service wrote .env (with the DB name) into an unmounted throwaway container, so replicas read an .env without DB_DATABASE and fell back to Laravel's default db 'forge', 500ing every request. Mount the shared data volume into the install job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
autoformat reformats (settings sort, complexity/model.py, test_database_query.py, keycloak vars wrap); shorten akaunting swarm task name to <=80 chars; delete narration comments from keycloak vars; remove 'invariant' from the comment linter's valid marker set. make autoformat + make test verified green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swarm: deploy N replicas without AKAUNTING_SETUP, run install exactly once in a one-shot swarm job (scale 0 -> install -> scale N), marker-gated. The one-shot runs akaunting.sh with AKAUNTING_SETUP=true and --no-start so it installs then exits 0. Compose: single container self-installs via AKAUNTING_SETUP. SESSION_DRIVER=database keeps sessions consistent across replicas. Repeated lookups extracted to vars; run_once core guard split (main -> 01_core -> 02_swarm).
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>
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
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>
Pure 1:1 rename (60 insertions / 60 deletions across 30 files) to
align the variable names with the role that owns them and to drop the
misleading ``docker_git_`` prefix — these knobs configure the
compose-stack's git clone step, not the docker CLI:
- docker_git_repository_address → compose_repository_url
- docker_git_repository_version → compose_repository_ref
- docker_git_repository_pull → compose_pull_repository
- docker_repository_path → compose_repository_path
``ref`` better captures git refs (branches/tags/SHAs are not
"versions"); ``url`` is the idiomatic name for the clone source.
Touches every consumer (web-app-akaunting/bigbluebutton/dashboard/
fediwall/hugo/littlejs/mini-qr/minio/openproject/openwebui/
roulette-wheel/taiga + svc-db-openldap/svc-prx-openresty/sys-svc-rdbms
+ web-svc-collabora/coturn/logout/onlyoffice + sys-front-inj-all), the
role's own defaults/tasks, the bigbluebutton compose_mods filter and
its unit test, the declared-in-services lint, and the hugo
requirements doc.
Breaking change for any external override that still sets the old
names — no alias is provided.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-locates every git-clone URL with its owning entity in the role's
`meta/services.yml` so the unified `image`/`version` ↔ `repository`/`ref`
naming convention covers secondary repos (plugins, build helpers,
bootstrap sources) just as cleanly as the primary entity source.
Migrations:
- sys-aur, pkgmgr (non-invokable bootstrap roles): `meta/services.yml`
gains `repository:`/`ref:` on the role's own entity; `vars/main.yml`
renders them via `lookup('config', role_name, …)` because these
roles never set `application_id`.
- web-app-{akaunting,bigbluebutton,bluesky,discourse,roulette-wheel,
taiga}: declare the secondary git URL on the most fitting entity
(`akaunting` itself for the docker-helper repo; `view` for bluesky's
social-app web client; `plugins.discourse-ldap-auth` for that
Discourse plugin; `oidc` for taiga's OIDC plugin). Each role gains
named `<ROLE>_REPOSITORY` / `<ROLE>_REF` constants in `vars/main.yml`
so tasks and templates stay readable instead of inlining a
`lookup('config', application_id, …)` string at every call site.
- svc-db-postgres `templates/Dockerfile.j2`: keep the `pgvector`
extension clone but mark the line with `{# nocheck: repository-declared #}`
since pgvector is an extension source, not the role's primary repo.
New integration test
`tests/integration/roles/meta/services/test_repository_declared_in_services.py`
walks every role's text files and fails if a literal `https://…/<…>.git`
URL is not declared as a `repository:` value somewhere in that role's
`meta/services.yml` (recursive — top-level entity, sub-entity, plugin
map). Legitimate exceptions opt out with `# nocheck: repository-declared`
on the same line or the preceding line; Jinja templates may use the
`{# … #}` form so a `\`-continued Dockerfile shell command stays valid.
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>
Rename docker_git_repository_branch to docker_git_repository_version
to correctly reflect git refs (branch, tag, commit) and pin
universal-logout to v1.1.0 for reproducible deployments.
https://chatgpt.com/share/697a2741-bc3c-800f-9713-1b43d353e962
- Drop deprecated docker_compose_file_creation_enabled flag (defaults + all consumers)
- Always run docker-compose file routines and bring stack up (caller can disable auto-flush via docker_compose_flush_handlers)
- Treat docker-compose.yml.j2 as optional via first_found (skip if missing)
- Clean up role template vars (remove removed flag, fix alignment)
- Record new E2E CI failures for Akaunting (502) and Baserow (timeout)
Fixes BigBlueButton CI failure caused by missing .env in repository:
Previously file routines could be skipped, so /opt/docker/bigbluebutton/services/repository/.env
was never created/linked and scripts/generate-compose failed with:
"cat: .env: No such file or directory" and "EXTERNAL_IPv4 is not set in .env".
With file management always enabled, the env file is copied/linked before compose generation.
https://chatgpt.com/share/69681be9-f944-800f-9b67-8afdc3a256fc
This commit updates multiple roles to ensure compatibility with Ansible 2.20.
Several include paths and task-loading mechanisms required adjustments,
as Ansible 2.20 applies stricter evaluation rules for complex Jinja expressions
and no longer resolves certain relative include paths the way Ansible 2.18 did.
Key changes:
- Replaced legacy once_finalize.yml and once_flag.yml with the new structure
under tasks/utils/once/finalize.yml and tasks/utils/once/flag.yml.
- Updated all include_tasks statements to use 'path_join' with playbook_dir,
ensuring deterministic and absolute file resolution across roles.
- Fixed all network helper includes by converting direct relative paths such as
'roles/docker-compose/tasks/utils/network.yml' to proper Jinja-evaluated paths.
- Normalized MATOMO_* variable names for consistency with the updated variable
scope behavior in Ansible 2.20.
- Removed deprecated patterns that were implicitly supported in Ansible 2.18
but break under the more strict variable and path resolution model in 2.20.
These changes are part of the full migration step required to ensure the
infinito-nexus roles remain stable, deterministic, and forward-compatible with
Ansible 2.20.
Details of the discussion and reasoning can be found in this conversation:
https://chatgpt.com/share/69300a8d-24d4-800f-bec0-e895a695618a
Updated tests/integration/test_vars_usage_in_yaml.py:
- Variables immediately followed by '(' are now treated as function calls,
not as set variables. This prevents false errors.
- Fixed detection of redirect_domain_mappings so it is no longer flagged
as unused.
See: https://chatgpt.com/share/68c3542d-f44c-800f-a483-b3e43739f315
- Improved CSP filter to properly include web-svc-cdn and use protocol-aware domains
- Added Todo.md with redis and OIDC notes
- Enhanced Akaunting role config with CSP flags and redis option
- Updated schema to include app_key validation
- Reworked tasks to handle first-run marker logic cleanly
- Fixed docker-compose template (marker, healthcheck, setup flag)
- Expanded env.j2 with cache, email, proxy, and redis options
- Added javascript.js.j2 template for SSO warning
- Introduced structured vars for Akaunting role
- Removed deprecated update-repository-with-files.yml task
See conversation: https://chatgpt.com/share/68af00df-2c74-800f-90b6-6ac5b29acdcb