Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.8 ee633e6e5d fix(svc-opt-keyboard-color): mark pre-alpha so the AUR build is excluded from CI deploy
svc-opt-keyboard-color (lifecycle beta) was provisioned into the swarm and compose deploy matrices. Its sys-aur-install dependency runs the yay task with become_user: aur_builder, but sys-aur only creates that user on Archlinux; the CI hosts are Ubuntu, so the task failed on every node with 'sudo: unknown user aur_builder'. Mark the role pre-alpha so scripts/meta/resolve/apps.sh (--lifecycles alpha beta rc stable) drops it from the deploy matrix, matching the other AUR-consuming desk-* roles. Also remove the now-redundant when-gate on the AUR install task.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fb5c575823 fix(web-app-peertube): run the application-schema patch only on the stack host
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>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ce236c5fb4 fix(web-app-taiga): give the manager service a name so container_address resolves
container_address('manager') hard-requires services.manager.name to be a non-empty string before it branches on compose/swarm; taiga's manager service had no name:, so 'Run Taiga migrations' failed finalizing _raw_params (the embedded TAIGA_MANAGER_CONTAINER_ADDRESS lookup raised). Add name: '{{ ... | get_entity_name }}-manager' (identical to the previously-constructed compose container_name, so compose is unchanged) and reference it via TAIGA_MANAGER_CONTAINER, mirroring the mattermost/nextcloud SPOT convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6dba126d3d fix(web-app-gitea): wait for app.ini before running DB migrations
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>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b6e201e215 fix(web-app-matrix): stage the mdad runner build context on the stack host
The matrix-mdad runner image's compose build.context pointed at the Ansible controller's role tree ({{ application_id | abs_role_path_by_application_id }}/files/flavor/ansible/runner). On swarm the pre-deploy 'compose build' handler runs on the manager (delegate_to STACK_HOST run_once), where roles/web-app-matrix/files/ does not exist, so buildx failed: 'unable to prepare context: path ... not found'. Stage the four runner build-context files onto STACK_HOST via stack_host_copy and point the build context at that host-local instance dir, mirroring svc-db-openldap. sys-stk-backend's flush is deferred so the build runs after the stage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 3b068ae3dd fix(web-app-moodle): point swarm proxy upstream at the nginx sidecar
moodle is php-fpm (FastCGI on 9000, no HTTP); the HTTP front is the nginx sidecar on internal port 80. The swarm proxy_pass resolve_upstream hard-failed: 'swarm upstream for web-app-moodle needs services.moodle.ports.internal.http'. Add ports.internal.http: 80 and proxy.service_key: nginx so the upstream resolves to moodle_nginx:80. Compose is unchanged: resolve_upstream early-returns for non-swarm to 127.0.0.1:8026.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7a738d75e2 fix(web-app-mediawiki): make composer_install_extension mode-aware
In swarm the bare compose name 'mediawiki' does not exist (docker stack deploy suffixes the task id), so the composer-install extension task failed with 'No such container: mediawiki' on the manager. Resolve the live container id via resolve-container-id in swarm and use the bare name in compose, mirroring the already-working install_extension.sh in the same role; 02_extensions.yml passes the mode-aware env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 45df36e9c4 fix(web-app-pihole): gate sso-proxy CA-trust on IS_STACK_HOST
The oauth2-proxy-keycloak.cfg the lineinfile edits is rendered only on the stack host; on swarm workers it does not exist, so the task failed with 'Destination ... oauth2-proxy-keycloak.cfg does not exist' (rc 257) on wrk-01/wrk-02 while the manager succeeded. Gate the task on IS_STACK_HOST so it runs only where the file exists; compose (IS_STACK_HOST true) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e82fdfe76e fix(sys-svc-compose): de-prefix locally_built so build-siblings are not pulled
A service that builds an image locally produces a <prefix>X image; sibling services may reference that same already-prefixed image without their own build:. swarm_registry_sync compared the de-prefixed upstream against a locally_built set holding the PREFIXED form, so the siblings missed the membership check and fell through to docker pull X for a local-only image with no registry upstream (espocrm/bookwyrm/shopware 'pull access denied'). Store locally_built de-prefixed so it matches upstream.

Add test_swarm_registry_sync.py covering the build-sibling skip, the real-upstream pull, the already-in-registry skip and the empty-prefix short-circuit; the first test fails on the pre-fix logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d5d17f241b refactor(swarm): shared resolve_host_cid + wait_converged utils
Replace the per-role swarm host-node/container-id resolve duplicated across fider, matomo, decidim, keycloak (login + realm-import), collabora and listmonk with two shared task utils under roles/sys-svc-compose/tasks/utils/swarm/. resolve_host_cid sets <prefix>_HOST_NODE / <prefix>_LOCAL_CID (swarm: converge + node + cid delegated to the node; compose: inventory host + given cid); wait_converged is the bare convergence gate.

Teach test_variable_definitions to read templated set_fact keys like {{ prefix }}_HOST_NODE as a *_HOST_NODE name pattern (guarded so a pure {{ x }} key cannot become a catch-all), and add the playbook_dir-relative include load-failure exemptions to .ansible-lint-ignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 028a073086 fix(svc-db-mariadb): run container-exec DB tasks on the swarm manager
resolve-container-id only works on a manager node, but the root-credential probes (01_core) and the database/user creation (02_init) ran on swarm workers and failed with 'must run on a Swarm manager node' (empty id makes container exec "" exit rc 1). Delegate those container-exec tasks to STACK_HOST. The 01_core probes also run_once (01_core is gated to one invocation per playbook); the per-app 02_init creates rely on the idempotent CREATE ... IF NOT EXISTS SQL instead of run_once so every consumer still gets its database. In compose STACK_HOST is the inventory host, so delegation is a no-op.

Also extract the recovery container name/image and the swarm service name into MARIADB_RECOVERY_NAME / MARIADB_RECOVERY_IMAGE / MARIADB_SWARM_SERVICE vars, used by 03_reset_root_password.yml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 09698e2b95 fix(web-app-keycloak): resolve sso-proxy redis url via engine lookup
Hardcoded redis://redis:6379 (no auth, fixed host) made the oauth2-proxy session store crash-loop in swarm, where the shared redis runs with --requirepass (NOAUTH / connection refused). openresty auth_request then returned 502/500 and every sso-gated app failed e2e readiness. Derive the url from the central engine model instead: authenticated central url when shared, embedded redis:6379 when local, matching the established consumer pattern (baserow, gitea, mastodon).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:38 +02:00
infinito-nexus-updater[bot] c187de6a7f update(images): update Docker image versions 2026-06-25 14:27:36 +00:00
kevinveenbirkenbach 0e7f8da3d9 Merge pull request #342 from infinito-nexus/update/docker-image-versions-20260624-4393ad8
update(images): update Docker image versions
2026-06-25 16:02:11 +02:00
kevinveenbirkenbach 79a535097c Update services.yml 2026-06-25 16:01:36 +02:00
kevinveenbirkenbachandClaude Opus 4.8 9994918f94 fix(swarm-test): make the in-container reachability probe curl-less
11_assert_state.sh and 09_seed_content.sh probed the rescheduled app with 'docker exec <ctr> curl', but images without curl (e.g. the official ollama/ollama, which still listens on :11434) made the probe fail though the server was up ('ollama not reachable after reschedule'). Fall back curl -> wget -> bash /dev/tcp connect, treating any HTTP response or open port as reachable; the curl path runs first and is unchanged for images that have it, so no regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:59:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 48753de136 feat(deploy-verify): scope required_by coverage to deploy modes (compose/swarm)
The required_by coverage verifier treated every flat 'required_by: {categories/roles}' as required in BOTH deploy modes. But sys-ctl-rpr-container-hard/soft repair compose compositions (DIR_COMPOSITIONS) and gate their body to compose; declaring required_by:[web] flat made the verifier expect them on swarm too, where they correctly skip, so web-opt-rdr-domains swarm CI failed 'required role(s) did not execute'. Let required_by nest under 'compose:'/'swarm:'; the verifier infers the deploy mode from the role set, mirroring DEPLOYMENT_MODE's own groups['svc-swarm-node'] definition, and enforces only the matching mode.

Migrate all 21 existing flat declarations to mirror under both modes (behavior unchanged) and mark sys-ctl-rpr-container-* compose-only. role_has_required_by reads both forms. New lint test_required_by_mode_keys requires the explicit compose:/swarm: form so the flat-vs-gate contradiction can't recur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:59:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4a7009b21e fix(svc-db-mariadb): use the swarm-registry-qualified ref for the recovery container
The root-password-reset recovery container referenced the bare locally-built image MARIADB_CUSTOM_IMAGE (mariadb_custom:<ver>), which exists only as a transient build tag on the manager and 404s when a swarm worker tries to pull it ('pull access denied'). Prefix it with SWARM_REGISTRY_PREFIX so it resolves the image swarm_registry_sync.py pushed - the handler pushes with the same '--registry-prefix {{ SWARM_REGISTRY_PREFIX }}', so push-ref and pull-ref match (the web-app-mediawiki precedent). The prefix is empty in compose mode, so that path is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:59:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 43ad94bc88 fix(sys-aur): gate the Arch-only AUR bootstrap to Archlinux hosts
sys-aur bootstraps the yay AUR helper via makepkg, an Arch-only tool, but had no distro gate, so on a Debian host the 'Build and install yay' step died (rc 127, 'makepkg: command not found'), breaking every consumer (svc-opt-keyboard-color, sys-cli, ...). Gate the include on ansible_facts['os_family'] == 'Archlinux' (the codebase's existing Arch-gate idiom, e.g. svc-net-wireguard-core); on non-Arch the role is a clean no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:59:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 46566215a2 fix(svc-db-typesense): create the scoped API key without curl (image ships none)
Once a deploy reached the consumer 'Create scoped API key' step, the curl-based POST /keys died with rc 127 'curl: command not found' (the typesense image has no curl), failing every typesense consumer (funkwhale et al.). The svc-db-typesense role alone never hits this (the key-creation only runs for a consumer) and every consumer died earlier at the container_ports render, so CI masked it. Replace curl with an in-container bash /dev/tcp POST mirroring the health probe: body via stdin, Content-Length via a Jinja length, key embedded, status parsed with cut. Runtime-verified: returns 201.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:59:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8093c6327d fix(container_ports): re-trust application_id before templating on ansible-core >=2.19
container_ports reads application_id from the play vars, where sys-stk-full leaves it as the include var '{{ sys_stk_full_application_id }}'. _as_str() stripped its TrustedAsTemplate tag, so templar.template() silently refused it on ansible-core >=2.19, leaving the raw string and failing every shared-service compose/swarm render ('Application ID {{ ... }} not found'). Re-trust via trust_as_template, mirroring plugins/action/stack_host_template.py, and import it directly (drop the dead <2.19 ImportError fallback).

Also fix a stale import in utils/roles/applications/config.py: AnsibleUndefined moved from ansible.utils.unsafe_proxy to ansible.template in ansible 2.20, so the old ImportError fallback always used a no-op stub and isinstance(obj, AnsibleUndefined) silently never matched. Import it directly from ansible.template.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:59:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 413188e6ce docs(cheatsheet): tighten roundtrip prompt
Cut redundant phrasing from the Cross-Mode Roundtrip prompt, fold the duplicate stop-early constraint, add the 15-minute container liveness check, and drop the stray 15-minute line from the Requirement Creation prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:59:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e101a066f4 docs(cheatsheet): fix stale playwright SPOT link
Point the Playwright Tests SPOT at the iteration spec-loop procedure named in the prompt, not the contributing Playwright Tests page. Also tighten the Cross-Mode Roundtrip prompt to the no-apps sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:59:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f887bcc164 test(web-app-nextcloud): assert the Zammad client_id is plaintext, not an ICrypto ciphertext
The request-capture assertion proves the authorize request carries a client_id but not that the partner accepted it, so it would pass even with a garbage (encrypted) client_id - masking an NC_ENC_MODE/encryption regression. Add a cheap guard: the captured client_id must not contain a '|', which is the ICrypto ciphertext separator (cipher|iv|hmac|version) and never appears in a valid OAuth uid. No effect on valid couplings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:37:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0ac3efd031 fix(web-app-nextcloud): store integration_mattermost OAuth credentials as sensitive app values
integration_mattermost reads its OAuth client_id/client_secret via IAppConfig getAppValueString() (plain, relying on the sensitive-value auto-decrypt) - only the per-user access token is decrypted via ICrypto. Like integration_zammad, the shared set_encrypted_appconfig.php stored these ICrypto-encrypted (default mode), so mattermost read back the raw ciphertext as its client_id and the personal connect could not build a valid /oauth/authorize request. Pass NC_ENC_MODE=sensitive so getAppValueString() returns the plaintext client_id (gitlab keeps the icrypto default - it decrypts explicitly).

This latent break was never caught in CI because the mattermost variant (v2) was blocked by the v0 files_linkeditor failure and never ran.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:37:10 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f2fa81e01f test(web-app-nextcloud): capture the Zammad OAuth authorize request, not the post-login redirect
The integration_zammad connect control is a button that runs window.location.replace(<partner>/oauth/authorize?client_id=...&response_type=code). Zammad's SPA immediately bounces the unauthenticated test browser to its login at the root path, so the settled page URL is no longer the authorize endpoint and the assertion saw the root. Capture the /oauth/authorize request itself (page.waitForRequest) - it is the coupling signal that carries the provisioned client_id to the partner - instead of the URL the browser ends up on after Zammad's redirect.

Verified: the spec passes against the live variant-4 coupling (client_id and response_type present, partner host).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:56:55 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2eb0bab818 fix(web-app-nextcloud): store integration_zammad OAuth credentials as sensitive app values
integration_zammad reads its OAuth client_id via IAppConfig::getValueString() (plain, relying on the sensitive-value auto-decrypt), unlike integration_gitlab/mattermost which decrypt explicitly via ICrypto getClearAppValue(). The shared set_encrypted_appconfig.php stored every value ICrypto-encrypted, so zammad read back the raw ciphertext as its client_id and the personal connect could not build the /oauth/authorize URL. Add an NC_ENC_MODE switch (default icrypto, unchanged for gitlab/mattermost) and have the zammad provisioning pass sensitive so getValueString() returns the plaintext client_id.

Verified in-container: sensitive round-trips to plaintext, icrypto still yields a pipe-separated ciphertext, and the live Zammad Doorkeeper app uid matches the stored client_id. This also restores idempotency: the ICrypto ciphertext contained a pipe, tripping the re-provision guard on every deploy; the sensitive plaintext does not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:56:54 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c4206db8e3 fix(web-app-nextcloud): dismiss the firstrunwizard modal before the Files New-menu click
On a freshly provisioned instance the firstrunwizard what's-new modal mounts together with the Files app (after the post-goto dismissal already ran) and overlays the New button, so files_linkeditor's newMenuButton.click() was swallowed and the test hit its 120s timeout (proven by the failure screenshot: the modal sits on top of the visible New button). Re-dismiss once the app has rendered, right before driving the menu. The helper is a no-op when no modal is present, so the added call is idempotent: verified passing both on a fresh deploy with the modal and on a reused stack without it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:56:54 +02:00
kevinveenbirkenbachandClaude Opus 4.8 210c6e551c fix(deploy-verify): strip the ansible log_path line prefix before the required-role check
The required_by coverage verifier (_role_body_executed) reads ANSIBLE_LOG_PATH, but the swarm and compose deploy point that at a log FILE, which ansible prefixes every line with '<ts> p=<pid> u=<user> n=<name> <LVL>| '. That prefix defeated both the line-anchored non-skip status match and the TASK-header block split, so every required role (e.g. sys-ctl-cln-bkps under any svc-bkp deploy) false-positived as 'did not execute' although it actually ran on the manager. Strip the prefix before matching; reproduced locally with a minimal multi-host include and verified the file slice flips while raw-stdout matching is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 05:43:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c97392ed44 style: apply make autoformat reformatting
Formatter-only changes from make autoformat: wrap a long Jinja
expression in the dashboard manager-ops task and reformat the swarm
extend_inventory test util. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 05:43:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5dd5c421aa feat(cli): add --run to deploy ci trigger
--run URL selects the run whose results determine the --failed apps;
without it the latest deploy run on the branch is used. Repo + run id are
parsed from the URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 05:43:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a08dddfed7 fix(swarm-test): keep the svc-swarm-manager marker group manager-only in the test inventory
extend_inventory._host_topology placed the app-under-test on the workers whenever the role had no default_placement: manager. When APP_ID is svc-swarm-manager itself, that leaked the workers into the svc-swarm-manager group, which is the marker group IS_STACK_HOST keys on, so IS_STACK_HOST went truthy on the workers and sys-ca-selfsigned skipped its CA fetch (when: not IS_STACK_HOST). root-ca.crt never landed and 'CA | enforce permissions' failed on both workers. Guard the worker extension so this marker group stays manager-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 05:43:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c1ad4f85fa fix(svc-bkp-local-2-device): suppress the deploy-time flush of the plug-triggered backup oneshot
The unit only Wants= the USB .mount (no Requires/ConditionPathIsMountPoint), so under MODE_DEBUG (CI) sys-service's default state=restarted ExecStarted the oneshot at deploy time. With no USB device present files/script.py exits 1 and the sys-service diagnostics task hard-failed the deploy. Suppress the flush, as btrfs-balancer and svc-bkp-container-2-local already do; the unit install and the .mount plug-trigger still run for production.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 05:43:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c2311e9ccc fix(svc-ai-ollama): inherit the official ollama image, drop the alpine stub
variant 0 forced image: alpine/ollama, a 74MB stub with no 'ollama serve', so it never bound :11434 and the swarm post-reschedule reachability probe failed. Drop the override so the variant inherits ollama/ollama from meta/services.yml; preload_models stays empty so the runner still skips the multi-GB model pulls. Also strip the file's comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 05:43:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ebdb2ad8b3 fix(sys-aur): ensure the AUR builder primary group exists before the user
ansible.builtin.user set the builder primary group to AUR_BUILDER_GROUP (default wheel) without creating it first, so on a base image lacking wheel the user task hard-failed ('Group wheel does not exist') and broke every sys-aur consumer (e.g. svc-opt-keyboard-color) in swarm. Add an ansible.builtin.group task before it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 05:43:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5aae5a0be3 fix(swarm-deploy): evaluate resolve-container-id in listmonk + joomla; hoist joomla PHP to files
In swarm mode *_CONTAINER_ADDRESS resolves to "$(/usr/bin/resolve-container-id
<stack> <service>)", which must reach /bin/sh unquoted so the subshell runs.

- listmonk: drop the `| quote` filter; shlex.quote single-quoted the whole
  "$(...)", suppressing the substitution, so docker received the literal token
  ("No such container: $(...)").
- joomla: rewrite the docker/container exec tasks from the shell argv-list form
  (executed directly, never via /bin/sh, so the $() was never evaluated) to the
  free-form string shell, and switch the raw `docker exec` calls to the
  `container` wrapper (the argv form had hidden them from the no-raw-docker lint).
- joomla: move the three inline PHP scripts (configuration.php debug toggle, DB
  settings patch, OIDC plugin ZIP packaging) out of inline `vars:` into
  files/php/*.php, fed via `lookup('file', ...)` over stdin to
  `container exec -i ... php --`, with their inputs passed as J_* env vars
  (the files/php + stdin convention already used by friendica/espocrm/snipe-it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 05:43:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 099887f016 fix(container_ports): resolve an unrendered application_id via the templar
When sys-stk-full renders a shared service (web-svc-logout, web-svc-collabora)
it restores application_id="{{ sys_stk_full_application_id }}"; that reaches the
container_ports lookup as the raw, un-templated string (the lookup reads it from
`variables`, not as a templated positional arg), so it raised "Application ID
'{{ sys_stk_full_application_id }}' not found" and broke the swarm deploy of
every app pulling in those shared services. Template it through self._templar,
the way container_depends_on already resolves the vars it reads. Add a
regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 05:43:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 52e601b38d fix(cli): satisfy lint + url-reachability for deploy ci
Drop the unused shebangs (EXE001), mark the PASS emoji constant noqa
S105, annotate the test fixture as ClassVar, and use reserved example.com
hosts / a nocheck marker for the placeholder run URLs so the
url-reachability test stops flagging them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 05:43:30 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0f921a69be feat(cli): add deploy ci status + trigger commands
Add 'infinito administration deploy ci status' and '... trigger' for
inspecting and re-running the compose/swarm deploy-test matrix of a
branch's CI run.

status: per-role docker (compose) and swarm result plus an aggregated
total column (green only when both modes are green), a trailing 'run'
column with the job URLs, --failed [total|swarm|compose] filtering, and
table/string output. Default reads the latest deploy run on the current
branch; --url reads a specific run.

trigger: dispatches entry-manual.yml for the current branch -- all roles
by default, only the last run's failures with --failed [scope], or an
explicit --apps list.

The target repo is resolved from the branch's tracking remote (a fork),
not gh's default repo. Covered by unit tests for run parsing, status
rendering, and trigger dispatch logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 05:43:30 +02:00
kevinveenbirkenbachandClaude Opus 4.8 62d2156c61 fix(svc-db): curl-less in-container health probes for typesense + memcached
typesense (typesense/typesense:0.25.2 ships no curl) used curl for its /health
readiness probes, so the deploy hung on "Wait until the engine answers /health"
(rc 127, "curl: not found", reproduced locally). Switch TYPESENSE_EXEC_SHELL to
bash -lc and replace both health probes plus the compose healthcheck with the
bash /dev/tcp pattern already proven by svc-db-qdrant (GET /health, grep
"200 OK").

memcached: the version-handshake probe MEMCACHED_PROBE wrapped the printf format
in single quotes (printf 'version\r\n'), which collided with the surrounding
sh -lc '...' quoting and broke the command. Use double quotes.

The typesense POST /keys task still uses curl; it only runs for a typesense
consumer (not the standalone job) and is left for a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 05:43:30 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a043b842db fix(compose-ports): reconcile lint + config with the container_ports migration
The container_ports migration left make test red; reconcile it:

- Delete 10 port vars orphaned when their compose mappings moved to the
  container_ports lookup (OPENLDAP_DOCKER_PORT_OPEN, RABBITMQ_INTERNAL_PORT,
  REGISTRY_CACHE_PORT, ERPNEXT_FRONTEND_PORT, FLOWISE_PORT_PUBLIC,
  JITSI_WEB_HTTP_PORT, KEYCLOAK_SERVER_HOST, MINIO_API_PORT_INTERNAL,
  OPENCLOUD_HTTP_PORT, OPENWEBUI_PORT_PUBLIC).
- Add PORT_BANDS.public entries for the fixed protocol ports the migration
  declared (smtp/smtps/submission/pop3/pop3s/imap/imaps/sieve, rtmp, c2s/s2s).
- Route the container_ports internal-declared lint through the SPOT constants
  (ROLE_FILE_TEMPL_COMPOSE / ROLE_FILE_META_SERVICES), cached read_text, and
  load_yaml_any, per repo conventions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 594109018a refactor(compose): publish every container port through the container_ports lookup
Take the host IP as an optional third element of each term
([service, protocol, host_ip]) instead of an ip= kwarg, and build the whole
ports: block from declared ports.local/public (host) + ports.internal
(container). Convert every compose template across 84 roles, folding the
remaining raw var/numeric/specific-ip mappings (including mailu's per-interface
mail ports bound to MAILU_IP4_PUBLIC). Multi-term calls render one term per line.

Add the compose-ports-must-use-container-ports lint: it forbids any raw
"<host>:<published>:<container>" publish item under a ports: block and requires
every term to declare ports.internal. Remove
tests/lint/roles/test_swarm_internal_ports.py, now subsumed.

Fix a container_depends_on rendering regression. The lookup self-indented
(default indent=4), so the added 4-space placement + | indent(4) triple-indented
the block. Flip the lookup default to column 0 (consistent with
container_networks), make every call use the 4-space + | indent(4) form, and add
a lint enforcing it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4c8a1ca59d refactor(compose): source published container ports from ports.internal
Rewrite every hardcoded literal container port in a compose.yml.j2 publish
mapping (- "<host>:<...ports.local|public.kind...>:<LITERAL>") to read the
declared ports.internal.<kind> via lookup, across 22 roles, so the published
(local/public) port and the container (internal) port are a single declared
source of truth. Mappings whose container already references the lookup or a
variable are left as-is.

This surfaced and fixed a real swarm bug: web-app-openproject declared
ports.internal.http: 8080 while the container actually listens on 80, so the
swarm resolve_upstream pointed openproject at the wrong port. Reconciled to 80
(the mapping's real container port).

Extend tests/lint/roles/test_swarm_internal_ports.py with a second check: a
published mapping's container segment must come from ports.internal via a lookup
(or a variable referencing it), never a hardcoded literal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6ca9a506f8 test(lint): extend swarm-internal-port rule to all published kinds
Generalize tests/lint/roles/test_swarm_internal_ports.py from http-only to every
published port kind (http, websocket, ssh, ...) referenced in a compose port
mapping (ports.<local|public>.<kind>): each must declare the matching
ports.internal.<kind> so the role's swarm port surface is complete and
consistent. Fix the roles it flagged:

- pihole, shopware: ports.internal.http (80, 8000)
- erpnext, mastodon, odoo: ports.internal.websocket (9000, 4000, 8072)
- gitea, gitlab: ports.internal.ssh (22)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 066db426de fix(swarm-deploy): keycloak running-replica resolver + bookwyrm local-image pull
- web-app-keycloak: KEYCLOAK_CID_RESOLVE_CMD filtered status=running AND
  health=healthy, so during swarm converge (no replica healthy yet) it returned
  empty; the kcadm dead-container recovery then looped against the same dead cid,
  burned its retries and fail_json'd. Drop health=healthy so a freshly
  rescheduled running replica is usable.
- sys-svc-compose/swarm_registry_sync.py: a service sharing a sibling's locally
  built <entity>_custom image (no build: of its own, already registry-prefixed)
  fell into the prefix branch and ran `docker pull <entity>_custom` against the
  registry -> 404. Collect the locally-built refs first and skip the upstream
  pull when the prefix-stripped ref is one of them (it is retagged and pushed by
  the building service).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e1176b9613 test(lint): require ports.internal for published compose ports
The swarm resolve_upstream (utils/networks/proxy.py) reads
services.<entity>.ports.internal.<kind> and hard-fails (ValueError) when a
proxied role exposes a compose port mapping (services.<entity>.ports.local.<kind>)
but never declares its container-side internal port. Compose ignores
ports.internal, so the gap is invisible until a swarm deploy.

Add tests/lint/roles/test_swarm_internal_ports.py to catch the whole class, and
fix the three roles it flagged: web-app-funkwhale (http: 80), web-app-kix
(http: 80), web-app-zammad (http: 8080). web-app-espocrm was fixed earlier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 583ecb5982 fix(swarm-deploy): port-aware chaos probe + espocrm internal ports
- scripts/tests/deploy/swarm: the 09/11 in-container reachability probe
  hardcoded http://localhost:80, but ollama listens on 11434 and gitea on 3000,
  so the assert looped 30x against the wrong port. _context.sh now exports
  PROBE_PORT from the role's meta/services.yml ports.internal.http (default 80),
  used by 09_seed_content and 11_assert_state.
- web-app-espocrm: declare ports.internal {http: 80, websocket: 8080}; the swarm
  resolve_upstream (utils/networks/proxy.py) hard-fails without it (only
  ports.local was set). Compose is unaffected (reads ports.local).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 569d52a38f fix(swarm-deploy): cross-host, login-shell and quoting deploy bugs
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>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 9c388f3fb5 fix(test-swarm): match SERVICE_NAME to the deploy + skip service-less roles
The chaos/converge harness derived ENTITY from a hand-rolled prefix-strip chain
in _context.sh, which diverges from the deploy's application_id | get_entity_name
(longest category-path strip via roles/categories.yml): svc-dns-unbound resolved
to `dns-unbound` not `unbound`, svc-registry-cache to `registry-cache` not
`cache`, so 07_wait_converge polled a `<x>_<x>` service that never exists and
timed out (Ansible PLAY RECAP failed=0; the job died only in the post-deploy
bash gate).

Resolve ENTITY via get_entity_name so SERVICE_NAME always matches the stack the
deploy creates. Add a HAS_SWARM_SERVICE flag (no image: in meta/services.yml and
no compose template, or skip: [swarm]) and a skip_if_no_swarm_service guard in
07/09/10/11, so node-level roles (drivers, swap, wireguard, nfs-client) and
swarm-skipped roles short-circuit the converge/chaos gates instead of polling a
nonexistent service. Mirrors the existing skip_chaos_if_manager_pinned helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:10 +02:00
infinito-nexus-updater[bot] 3bc3cf9b5d update(images): update Docker image versions 2026-06-24 23:36:34 +00:00