From 274ba4ea5da875459f49b799da84cebcfe4139b7 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 07:26:22 +0000 Subject: [PATCH 01/15] update(images): update Docker image versions (web-app-socialhome) --- roles/web-app-socialhome/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-socialhome/meta/services.yml b/roles/web-app-socialhome/meta/services.yml index 43f68c9d7a..6f9a86ae8b 100644 --- a/roles/web-app-socialhome/meta/services.yml +++ b/roles/web-app-socialhome/meta/services.yml @@ -13,7 +13,7 @@ socialhome: # nocheck: default-placement-manager embedded sqlite in a node-local volume; no clustered storage placement: manager image: ghcr.io/social-home-io/socialhome - version: "2026.10.8" + version: "2026.10.9" ports: local: http: 8089 From 9f263eef539c80b107408054729dbca9e601760a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:36:26 +0000 Subject: [PATCH 02/15] chore(deps-dev): Bump @playwright/test from 1.63.0 to 1.64.0 Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.63.0 to 1.64.0. - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.63.0...v1.64.0) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.64.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 24 ++++++++++++------------ package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1d40cf4db5..19db992e0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "devDependencies": { "@eslint/js": "^10.0.1", - "@playwright/test": "^1.63.0", + "@playwright/test": "^1.64.0", "eslint": "^10.12.0", "eslint-plugin-playwright": "^2.12.1", "globals": "^17.13.0", @@ -484,13 +484,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", - "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.0.tgz", + "integrity": "sha512-oTN9FNYuN6bqQMwiho6jJKLCG2/QOYCixGXuCjRmzOq23eODeukuY2XiAVoVpAECbuezMt1MHQjAiQxK8F+Y+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.63.0" + "playwright": "1.64.0" }, "bin": { "playwright": "cli.js" @@ -1817,13 +1817,13 @@ } }, "node_modules/playwright": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", - "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0.tgz", + "integrity": "sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.63.0" + "playwright-core": "1.64.0" }, "bin": { "playwright": "cli.js" @@ -1833,9 +1833,9 @@ } }, "node_modules/playwright-core": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", - "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0.tgz", + "integrity": "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index d52cb43baa..529ad81295 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@playwright/test": "^1.63.0", + "@playwright/test": "^1.64.0", "eslint": "^10.12.0", "eslint-plugin-playwright": "^2.12.1", "globals": "^17.13.0", From cfaa9c61ac61db5518fac7f4faa6c53a9d7f1f79 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:33:56 +0000 Subject: [PATCH 03/15] update(images): update Docker image versions (svc-ai-ollama) --- roles/svc-ai-ollama/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/svc-ai-ollama/meta/services.yml b/roles/svc-ai-ollama/meta/services.yml index cba1342a36..77fc9801ed 100644 --- a/roles/svc-ai-ollama/meta/services.yml +++ b/roles/svc-ai-ollama/meta/services.yml @@ -13,7 +13,7 @@ ollama: backup: no_stop_required: true image: ollama/ollama - version: 0.40.2 + version: 0.40.3 name: ollama min_storage: 3473MB cpus: "4.0" From 8637ba92d47507cd5f2595a7bb08574694d8cc2c Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:34:01 +0000 Subject: [PATCH 04/15] update(images): update Docker image versions (web-app-bigbluebutton) --- roles/web-app-bigbluebutton/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-bigbluebutton/meta/services.yml b/roles/web-app-bigbluebutton/meta/services.yml index 1391350f6f..887ca0d0da 100644 --- a/roles/web-app-bigbluebutton/meta/services.yml +++ b/roles/web-app-bigbluebutton/meta/services.yml @@ -267,7 +267,7 @@ bbb-webrtc-recorder: # nocheck: dynamic-flag: fixed upstream-compose helper, not group-gated dockerize: image: ghcr.io/jwilder/dockerize - version: "v0.15.1" + version: "v0.15.2" enabled: false # nocheck: playwright-service-flag, dynamic-flag infrastructure backup consumer From a9aec3305d595b450b69183435fbc21876e71f5c Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:34:06 +0000 Subject: [PATCH 05/15] update(images): update Docker image versions (web-app-gitlab) --- roles/web-app-gitlab/meta/services.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/roles/web-app-gitlab/meta/services.yml b/roles/web-app-gitlab/meta/services.yml index 018c3f952a..27ac8c52a4 100644 --- a/roles/web-app-gitlab/meta/services.yml +++ b/roles/web-app-gitlab/meta/services.yml @@ -76,7 +76,7 @@ gitlab: lifecycle: beta webservice: image: registry.gitlab.com/gitlab-org/build/cng/gitlab-webservice-ce - version: v19.4.1 + version: v19.4.2 name: gitlab-webservice healthcheck: test: ["CMD", "/scripts/healthcheck"] @@ -95,7 +95,7 @@ sidekiq: flavor: tcp path: liveness image: registry.gitlab.com/gitlab-org/build/cng/gitlab-sidekiq-ce - version: v19.4.1 + version: v19.4.2 name: gitlab-sidekiq cpus: "1.0" mem_reservation: 2g @@ -106,7 +106,7 @@ sidekiq: http: 3808 workhorse: image: registry.gitlab.com/gitlab-org/build/cng/gitlab-workhorse-ce - version: v19.4.1 + version: v19.4.2 name: gitlab-workhorse healthcheck: test: ["CMD", "/scripts/healthcheck"] @@ -118,7 +118,7 @@ gitaly: # nocheck: single-replica replicas: 1 image: registry.gitlab.com/gitlab-org/build/cng/gitaly - version: v19.4.1 + version: v19.4.2 healthcheck: test: ["CMD", "/scripts/healthcheck"] name: gitlab-gitaly @@ -131,7 +131,7 @@ gitaly: pids_limit: 1024 shell: image: registry.gitlab.com/gitlab-org/build/cng/gitlab-shell - version: v19.4.1 + version: v19.4.2 name: gitlab-shell ports: internal: @@ -144,7 +144,7 @@ migrations: # nocheck: single-replica replicas: 1 image: registry.gitlab.com/gitlab-org/build/cng/gitlab-rails-ce - version: v19.4.1 + version: v19.4.2 name: gitlab-migrations cpus: "1.0" mem_reservation: 1g From b2f77234ee4157930ff1f7405cdbf40cad541b16 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:34:12 +0000 Subject: [PATCH 06/15] update(versions): update pins with a declared source (web-app-n8n) --- roles/web-app-n8n/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-n8n/meta/services.yml b/roles/web-app-n8n/meta/services.yml index 156c60abc5..64aa57e29f 100644 --- a/roles/web-app-n8n/meta/services.yml +++ b/roles/web-app-n8n/meta/services.yml @@ -67,7 +67,7 @@ n8n: backup: no_stop_required: false image: docker.n8n.io/n8nio/n8n - version: "2.42.3" + version: "2.43.4" update: type: registry_tags image: docker.n8n.io/n8nio/n8n From 2a37349a60442f463355c06e8b6d16cee6711260 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:34:13 +0000 Subject: [PATCH 07/15] update(images): update Docker image versions (web-app-socialhome) --- roles/web-app-socialhome/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-socialhome/meta/services.yml b/roles/web-app-socialhome/meta/services.yml index 6f9a86ae8b..d3ff755d73 100644 --- a/roles/web-app-socialhome/meta/services.yml +++ b/roles/web-app-socialhome/meta/services.yml @@ -13,7 +13,7 @@ socialhome: # nocheck: default-placement-manager embedded sqlite in a node-local volume; no clustered storage placement: manager image: ghcr.io/social-home-io/socialhome - version: "2026.10.9" + version: "2026.10.11" ports: local: http: 8089 From c9befebc7939b3cf7ddff2a55a426db4e3d8060b Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:34:18 +0000 Subject: [PATCH 08/15] update(images): update Docker image versions (web-app-stalwart) --- roles/web-app-stalwart/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-stalwart/meta/services.yml b/roles/web-app-stalwart/meta/services.yml index 9f0b082711..fcb805d964 100644 --- a/roles/web-app-stalwart/meta/services.yml +++ b/roles/web-app-stalwart/meta/services.yml @@ -43,7 +43,7 @@ stalwart: # nocheck: default-placement-manager mail edge at a known IP: MX A-record, PTR and SPF bind to the stack host IP (a floating task's egress IP breaks deliverability); the mail store is NFS-hostile and IS the app — same rationale as web-app-mailu placement: manager image: stalwartlabs/stalwart - version: "v0.16.25" + version: "v0.16.26" webui_version: "v1.0.11" update: key: webui_version From 8b887108cae8a34e3dabd114fc68fab66d7d6d0a Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:37:10 +0000 Subject: [PATCH 09/15] update(images): update Docker image versions (svc-ai-ollama) --- roles/svc-ai-ollama/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/svc-ai-ollama/meta/services.yml b/roles/svc-ai-ollama/meta/services.yml index cba1342a36..77fc9801ed 100644 --- a/roles/svc-ai-ollama/meta/services.yml +++ b/roles/svc-ai-ollama/meta/services.yml @@ -13,7 +13,7 @@ ollama: backup: no_stop_required: true image: ollama/ollama - version: 0.40.2 + version: 0.40.3 name: ollama min_storage: 3473MB cpus: "4.0" From 01f0ff139af0153d7696d08d7ec7a8eb47cdd5d5 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:42:16 +0000 Subject: [PATCH 10/15] update(versions): update pins with a declared source (web-app-n8n) --- roles/web-app-n8n/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-n8n/meta/services.yml b/roles/web-app-n8n/meta/services.yml index 156c60abc5..64aa57e29f 100644 --- a/roles/web-app-n8n/meta/services.yml +++ b/roles/web-app-n8n/meta/services.yml @@ -67,7 +67,7 @@ n8n: backup: no_stop_required: false image: docker.n8n.io/n8nio/n8n - version: "2.42.3" + version: "2.43.4" update: type: registry_tags image: docker.n8n.io/n8nio/n8n From ab9c23fa7639e737ff9ca9496d240f195cb15410 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 12 Oct 2026 12:52:50 +0200 Subject: [PATCH 11/15] fix(web-app-suitecrm): run Symfony's session through the Redis handler too 3cefb01d71 moved PHP sessions to Redis by pointing session.save_path at redis-central. SuiteCRM's own core_services.yaml wraps a NativeFileSessionHandler around '%session.save_path%', and that handler forces session.save_handler back to files and treats the Redis URL as a directory ("is_dir(): Unable to find the wrapper "tcp""). Symfony's session was empty on every request, so every POST /api/graphql answered 403 "Invalid CSRF token", the SPA logged the user out right after the SAML login and the biber logout spec failed with "session was lost before the logout attempt" (SuiteCRM#0 on PR #869, reproduced on main locally). core_services.yaml is imported after every extension service file, so the core handler cannot be redefined there. An extension packages file now points framework.session at our own handler and storage factory, which wrap PHP's built-in SessionHandler and therefore use phpredis from php.ini, with its locking, for Symfony and the legacy app alike. The entrypoint rebuilds the prod cache when an extension config is newer than it, so an existing installation picks the wiring up instead of keeping the compiled container. Verified with a local compose deploy of variant 0: on main the SAML logout spec failed with the 403 sequence above; after a mode=update redeploy the cache was rebuilt, all three SAML specs passed, and the prod log shows no tcp-wrapper warning or CSRF failure after the rebuild. Co-Authored-By: Claude Opus 5.5 (1M context) --- roles/web-app-suitecrm/files/session.yaml | 29 +++++++++++++++++++ .../files/shell/docker-entrypoint.sh | 5 ++++ roles/web-app-suitecrm/meta/volumes.yml | 8 +++++ .../web-app-suitecrm/tasks/01_manager_ops.yml | 8 +++++ roles/web-app-suitecrm/vars/main.yml | 1 + 5 files changed, 51 insertions(+) create mode 100644 roles/web-app-suitecrm/files/session.yaml diff --git a/roles/web-app-suitecrm/files/session.yaml b/roles/web-app-suitecrm/files/session.yaml new file mode 100644 index 0000000000..0edf781b3d --- /dev/null +++ b/roles/web-app-suitecrm/files/session.yaml @@ -0,0 +1,29 @@ +--- +framework: + session: + storage_factory_id: infinito.session.storage.factory.php_handler + handler_id: infinito.session.handler.php_handler + +services: + infinito.session.handler.php_native: + class: SessionHandler + + infinito.session.metadata_bag: + class: Symfony\Component\HttpFoundation\Session\Storage\MetadataBag + arguments: + - "%session.metadata.storage_key%" + - "%session.metadata.update_threshold%" + + infinito.session.handler.php_handler: + class: App\Security\Session\LegacyBridgeSessionHandler + arguments: + - "@infinito.session.handler.php_native" + - "@App\\Engine\\LegacyHandler\\DefaultLegacyHandler" + + infinito.session.storage.factory.php_handler: + class: App\Security\Session\LegacyBridgeSessionStorageFactory + arguments: + - "@App\\Engine\\LegacyHandler\\DefaultLegacyHandler" + - "@infinito.session.handler.php_handler" + - "@infinito.session.metadata_bag" + - "false" diff --git a/roles/web-app-suitecrm/files/shell/docker-entrypoint.sh b/roles/web-app-suitecrm/files/shell/docker-entrypoint.sh index 2c2d5c954a..1d96c0a4c1 100644 --- a/roles/web-app-suitecrm/files/shell/docker-entrypoint.sh +++ b/roles/web-app-suitecrm/files/shell/docker-entrypoint.sh @@ -112,6 +112,11 @@ if [ "$_have_lock" = "1" ]; then log "SuiteCRM already installed - skipping installer." fi + if [ -d "${APP_DIR}/cache/prod" ] && [ -n "$(find "${APP_DIR}/extensions" -path '*/config/*' -type f -newer "${APP_DIR}/cache/prod" -print -quit)" ]; then + CACHE_REFRESH=1 + log "Extension config is newer than the prod cache - refreshing it." + fi + if [ "$CACHE_REFRESH" = "1" ] || [ ! -d "${APP_DIR}/cache/prod" ]; then log "Clearing Symfony cache..." php bin/console cache:clear --no-warmup || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error diff --git a/roles/web-app-suitecrm/meta/volumes.yml b/roles/web-app-suitecrm/meta/volumes.yml index 5a1f48a29c..652e82b8a6 100644 --- a/roles/web-app-suitecrm/meta/volumes.yml +++ b/roles/web-app-suitecrm/meta/volumes.yml @@ -14,6 +14,14 @@ php_config: - service: suitecrm target: "/usr/local/etc/php/conf.d/{{ SOFTWARE_DOMAIN | replace('.', '-') }}.ini" +session_config: + type: config + source: "{{ [lookup('container', 'web-app-suitecrm', 'directories.config'), 'session.yaml'] | path_join }}" + mode: "0444" + mounts: + - service: suitecrm + target: "{{ ['/var/www/html/extensions', SOFTWARE_DOMAIN | replace('.', '-'), 'config/packages/session.yaml'] | path_join }}" + ldap_config: type: config source: "{{ [lookup('container', 'web-app-suitecrm', 'directories.config'), 'ldap.yaml'] | path_join }}" diff --git a/roles/web-app-suitecrm/tasks/01_manager_ops.yml b/roles/web-app-suitecrm/tasks/01_manager_ops.yml index e1801b6936..373126537c 100644 --- a/roles/web-app-suitecrm/tasks/01_manager_ops.yml +++ b/roles/web-app-suitecrm/tasks/01_manager_ops.yml @@ -14,6 +14,14 @@ notify: - compose-up +- name: "📄 Deploy SuiteCRM session handler wiring" + stack_host_copy: + src: "session.yaml" + dest: "{{ SUITECRM_SESSION_CONFIG_HOST }}" + mode: "0444" + notify: + - compose-up + - name: "🚀 Deploy '{{ SUITECRM_ENTRYPOINT_SCRIPT_HOST_ABS }}'" copy: src: "{{ SUITECRM_ENTRYPOINT_SCRIPT_SRC }}" diff --git a/roles/web-app-suitecrm/vars/main.yml b/roles/web-app-suitecrm/vars/main.yml index aaf9602ea6..ec66f96ded 100644 --- a/roles/web-app-suitecrm/vars/main.yml +++ b/roles/web-app-suitecrm/vars/main.yml @@ -32,6 +32,7 @@ SUITECRM_OIDC_SOCKS_PROXY: "{{ lookup('tor_socks_proxy') if SUITECRM_OIDC_ONION_ SUITECRM_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabled') }}" SUITECRM_LDAP_CONFIG_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'ldap.yaml'] | path_join }}" +SUITECRM_SESSION_CONFIG_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'session.yaml'] | path_join }}" SUITECRM_LDAP_EXTENSION_DIR: "{{ [SUITECRM_APP_DIR, 'extensions', SUITECRM_WELL_SOFTWARE_NAME, 'config/services/ldap'] | path_join }}" SUITECRM_ENTRYPOINT_SCRIPT_FILE: "docker-entrypoint.sh" From ba1e72a2878f53db607c00083e22147268980f29 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 12 Oct 2026 12:55:55 +0200 Subject: [PATCH 12/15] fix(sys-ctl-hlth-csp): pin csp-checker 3.0.5 csp-checker 3.0.5 starts Chromium without HTTPS-Upgrades. Before it, every http:// onion vhost was first tried as https://host:443 through the Tor SOCKS proxy, which broke the port-80 requests to the same onion service with net::ERR_SOCKS_CONNECTION_FAILED. hlth-csp then failed all 25 start attempts and took the deploy down in 11 swarm/tor jobs of main run 38038541631 (Gitea#0, Hugo#0, Listmonk#0, Mattermost#2, Nextcloud#0/#4/#7, Penpot#0, Pixelfed#0, YOURLS#0, odoo#0). Verified against a local compose stack over Tor: image 3.0.4 as shipped hit the SOCKS error on css and asset and reported extra redirects elsewhere; with the 3.0.5 change no SOCKS error and only the redirects the server sends. The csp-checker lint and all 16 E2E tests pass on the image built from 3.0.5. Co-Authored-By: Claude Opus 5.5 (1M context) --- roles/sys-ctl-hlth-csp/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/sys-ctl-hlth-csp/meta/services.yml b/roles/sys-ctl-hlth-csp/meta/services.yml index a6e9695221..2cff885e8c 100644 --- a/roles/sys-ctl-hlth-csp/meta/services.yml +++ b/roles/sys-ctl-hlth-csp/meta/services.yml @@ -8,4 +8,4 @@ csp: categories: [web] csp-checker: image: ghcr.io/kevinveenbirkenbach/csp-checker - version: 3.0.4 + version: 3.0.5 From eda6249d7b3aeda9a910992e74a1a42c3868e108 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 12 Oct 2026 15:07:19 +0200 Subject: [PATCH 13/15] fix(sys-svc-compose): probe provider networks by their exact name The network probe used community.docker.docker_network_info, whose get_network() scans GET /networks and returns the first entry whose Name equals the requested name OR whose Id starts with it, in a list order that changes between calls. In main run 38038541631 (Zammad#1, swarm) the central redis overlay got the ID n8nwzb1q39m3u4k7awwwmt6yg, the probe for the not yet created provider network "n8n" returned it as existing, the create step was skipped, docker stack deploy attached hermes to the redis overlay as "n8n", and the oauth2-proxy then reached the password-protected central redis by its bare host name: "NOAUTH Authentication required". The same scan can also return the ID-prefix twin of a network that does exist, so comparing the returned name alone would turn that network into "missing" and fail the create with "already exists". Both probes now run `container network ls --filter name= --format '{{json .}}'`, whose name filter never matches an ID. The new lookup network_driver picks the entry named exactly after the entity and returns its driver, '' when there is none; the drift checks, their messages and the create steps all read that one entry. No docker module is left in the file, so its two docker-module-uses-container-cli suppressions go too. Verified: on the host daemon GET /networks/ returns a network of another name and docker network ls --filter name= returns nothing. In ansible-playbook against real docker network ls output the lookup returned the exact network's driver in 20 of 20 probes beside an ID-prefix twin, an empty driver (create path) for a missing network and for a name only contained in a sibling, and the drift path for a foreign driver. Unit tests cover the same cases; with a substring match instead of the exact name the two sibling cases fail. Co-Authored-By: Claude Opus 5.5 (1M context) --- plugins/lookup/network_driver.py | 44 +++++++++++++ .../tasks/utils/network/create.yml | 38 +++++++---- .../plugins/lookup/test_network_driver.py | 66 +++++++++++++++++++ 3 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 plugins/lookup/network_driver.py create mode 100644 tests/unit/python/plugins/lookup/test_network_driver.py diff --git a/plugins/lookup/network_driver.py b/plugins/lookup/network_driver.py new file mode 100644 index 0000000000..801ba651f0 --- /dev/null +++ b/plugins/lookup/network_driver.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import json +from typing import Any + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase + +from utils.roles.entity.name import get_entity_name + + +class LookupModule(LookupBase): + """ + Usage: + {{ lookup('network_driver', probe, network_role_id) }} + + Args: + probe: the registered result of + `container network ls --filter name= --format '{{json .}}'`. + network_role_id: the role whose entity name names the network. + + Returns: + The driver of the network named exactly after that entity, '' when none exists. + """ + + def run(self, terms, variables: dict[str, Any] | None = None, **kwargs): + if not terms or len(terms) != 2: + raise AnsibleError( + "lookup('network_driver', probe, network_role_id) expects exactly 2 terms" + ) + + probe, network_role_id = terms + name = get_entity_name(str(network_role_id)) + lines = probe.get("stdout_lines") if isinstance(probe, dict) else None + for line in lines or []: + try: + network = json.loads(line) + except json.JSONDecodeError as exc: + raise AnsibleError( + f"lookup('network_driver'): unparsable probe line {line!r}" + ) from exc + if network.get("Name") == name: + return [str(network.get("Driver", ""))] + return [""] diff --git a/roles/sys-svc-compose/tasks/utils/network/create.yml b/roles/sys-svc-compose/tasks/utils/network/create.yml index 3e6c89ac78..92f7f3f68e 100644 --- a/roles/sys-svc-compose/tasks/utils/network/create.yml +++ b/roles/sys-svc-compose/tasks/utils/network/create.yml @@ -1,8 +1,16 @@ --- - name: "🔍 Probe existing overlay network for '{{ network_role_id }}' (swarm scope)" - community.docker.docker_network_info: # nocheck: docker-module-uses-container-cli read-only info module; returns structured network facts the wrapper's raw JSON would need parsed - name: "{{ network_role_id | get_entity_name }}" + ansible.builtin.command: + argv: + - container + - network + - ls + - --filter + - "name={{ network_role_id | get_entity_name }}" + - --format + - "{{ '{{json .}}' }}" register: sys_svc_compose_overlay_probe + changed_when: false when: (SWARM_CLUSTER_AVAILABLE | bool) and not (lookup('config', network_role_id, 'networks.local.force_bridge', false) | bool) run_once: true delegate_to: "{{ STACK_HOST }}" @@ -11,12 +19,11 @@ ansible.builtin.fail: msg: >- Network '{{ network_role_id | get_entity_name }}' exists with driver - '{{ sys_svc_compose_overlay_probe.network.Driver | default('unknown') }}' but 'overlay' is required. + '{{ lookup('network_driver', sys_svc_compose_overlay_probe, network_role_id) }}' but 'overlay' is required. Remove the stale network or revert the networks.local.force_bridge change. when: - (SWARM_CLUSTER_AVAILABLE | bool) and not (lookup('config', network_role_id, 'networks.local.force_bridge', false) | bool) - - sys_svc_compose_overlay_probe.exists | default(false) | bool - - (sys_svc_compose_overlay_probe.network.Driver | default('')) != 'overlay' + - lookup('network_driver', sys_svc_compose_overlay_probe, network_role_id) not in ['', 'overlay'] run_once: true delegate_to: "{{ STACK_HOST }}" @@ -44,27 +51,34 @@ - "{{ network_role_id | get_entity_name }}" when: - (SWARM_CLUSTER_AVAILABLE | bool) and not (lookup('config', network_role_id, 'networks.local.force_bridge', false) | bool) - - not (sys_svc_compose_overlay_probe.exists | default(false) | bool) + - lookup('network_driver', sys_svc_compose_overlay_probe, network_role_id) == '' run_once: true delegate_to: "{{ STACK_HOST }}" changed_when: true - name: "🔍 Probe existing bridge network for '{{ network_role_id }}' (compose)" - community.docker.docker_network_info: # nocheck: docker-module-uses-container-cli read-only info module; returns structured network facts the wrapper's raw JSON would need parsed - name: "{{ network_role_id | get_entity_name }}" + ansible.builtin.command: + argv: + - container + - network + - ls + - --filter + - "name={{ network_role_id | get_entity_name }}" + - --format + - "{{ '{{json .}}' }}" register: sys_svc_compose_bridge_probe + changed_when: false when: (not (SWARM_CLUSTER_AVAILABLE | bool)) or (lookup('config', network_role_id, 'networks.local.force_bridge', false) | bool) - name: "🛑 Fail on network driver drift for '{{ network_role_id }}' (compose)" ansible.builtin.fail: msg: >- Network '{{ network_role_id | get_entity_name }}' exists with driver - '{{ sys_svc_compose_bridge_probe.network.Driver | default('unknown') }}' but 'bridge' is required. + '{{ lookup('network_driver', sys_svc_compose_bridge_probe, network_role_id) }}' but 'bridge' is required. Remove the stale network or revert the networks.local.force_bridge change. when: - (not (SWARM_CLUSTER_AVAILABLE | bool)) or (lookup('config', network_role_id, 'networks.local.force_bridge', false) | bool) - - sys_svc_compose_bridge_probe.exists | default(false) | bool - - (sys_svc_compose_bridge_probe.network.Driver | default('')) != 'bridge' + - lookup('network_driver', sys_svc_compose_bridge_probe, network_role_id) not in ['', 'bridge'] - name: "✅ Ensure docker bridge network for '{{ network_role_id }}' exists (compose)" ansible.builtin.command: @@ -81,5 +95,5 @@ - "{{ network_role_id | get_entity_name }}" when: - (not (SWARM_CLUSTER_AVAILABLE | bool)) or (lookup('config', network_role_id, 'networks.local.force_bridge', false) | bool) - - not (sys_svc_compose_bridge_probe.exists | default(false) | bool) + - lookup('network_driver', sys_svc_compose_bridge_probe, network_role_id) == '' changed_when: true diff --git a/tests/unit/python/plugins/lookup/test_network_driver.py b/tests/unit/python/plugins/lookup/test_network_driver.py new file mode 100644 index 0000000000..7df4b95c5f --- /dev/null +++ b/tests/unit/python/plugins/lookup/test_network_driver.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import importlib.util +import json +import unittest + +from ansible.errors import AnsibleError + +from . import PROJECT_ROOT + + +def _load_lookup(): + spec = importlib.util.spec_from_file_location( + "lookup_network_driver", + str(PROJECT_ROOT / "plugins/lookup/network_driver.py"), + ) + mod = importlib.util.module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(mod) + return mod.LookupModule + + +def _probe(*networks): + return {"rc": 0, "stdout_lines": [json.dumps(network) for network in networks]} + + +class TestNetworkDriverLookup(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.LookupModule = _load_lookup() + + def _driver(self, probe): + return self.LookupModule().run([probe, "web-app-n8n"]) + + def test_the_network_named_after_the_entity_gives_its_driver(self): + probe = _probe({"Name": "n8n", "Driver": "overlay"}) + self.assertEqual(self._driver(probe), ["overlay"]) + + def test_a_sibling_listed_first_does_not_shadow_the_exact_name(self): + probe = _probe( + {"Name": "n8n-worker", "Driver": "bridge"}, + {"Name": "n8n", "Driver": "overlay"}, + ) + self.assertEqual(self._driver(probe), ["overlay"]) + + def test_only_a_sibling_containing_the_name_means_no_network(self): + probe = _probe({"Name": "n8n-worker", "Driver": "overlay"}) + self.assertEqual(self._driver(probe), [""]) + + def test_an_empty_listing_means_no_network(self): + self.assertEqual(self._driver(_probe()), [""]) + + def test_a_skipped_probe_means_no_network(self): + self.assertEqual(self._driver({"skipped": True, "changed": False}), [""]) + + def test_an_unparsable_line_is_rejected(self): + with self.assertRaises(AnsibleError): + self._driver({"rc": 0, "stdout_lines": ["not json"]}) + + def test_a_wrong_term_count_is_rejected(self): + with self.assertRaises(AnsibleError): + self.LookupModule().run([_probe()]) + + +if __name__ == "__main__": # pragma: no cover + unittest.main() From 373d5c9dc74c230e2b71622fc8712aebf8934a0d Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 12 Oct 2026 17:53:42 +0200 Subject: [PATCH 14/15] fix(web-app-odoo): run the onion suite on full Chromium so its origin is secure The shared Playwright config passes --unsafely-treat-insecure-origin-as-secure for every .onion origin in the environment, but Playwright's default chrome-headless-shell ignores that switch. On the tor axis the odoo origin therefore stayed an insecure context: navigator.clipboard was undefined and Odoo 20's onboarding tour crashed with "Cannot read properties of undefined (reading 'writeText')" and "Cannot destructure property 'TourInteractive'", opening error dialogs that the addon specs read as a missing module (odoo#2, swarm tor, main run 38038541631). When ODOO_BASE_URL is an onion URL the odoo specs now use channel "chromium", the full Chromium build in the Playwright image, and block service workers; clearnet runs keep every option unchanged. The worker is blocked because a secure context lets Odoo register /web/service-worker.js, whose fetch handler answers a failed navigation with a cached page or /odoo/offline: gotoOnion would get a 200 instead of the transport error it retries on. The switch stays in the role: a use.channel in the shared config would also reach the Firefox launch of onion-test.js, which rejects it with "Unsupported firefox channel", and would move every other role's onion run to a browser none of them was tested on. Verified with Playwright 1.63 in the runner's staging layout and the real odoo specs: an onion ODOO_BASE_URL resolves channel "chromium" and serviceWorkers "block", also for the contexts _shared.js creates itself, and launches chromium-/chrome-linux64/chrome; a clearnet one, and HEAD's specs on onion, keep the headless shell and "allow". All spec files stay on one worker, which the suite's one-worker test requires. Against Odoo 20's service worker with a dropped connection, full Chromium returned the cached /odoo page with status 200, while with the worker blocked page.goto threw net::ERR_CONNECTION_FAILED as the headless shell does. In the v1.64.0 image full Chromium made no :443 attempt to an onion host and the onion origin reported isSecureContext with crypto.subtle, which the headless shell did not. A local deploy of web-app-odoo variant 2 over Tor ran the odoo suite on /ms-playwright/chromium-1248/chrome-linux64/chrome and passed all seven addon specs, the one-worker test and the guest flow without a retry; in the variant 0 round the suites of the other roles kept chrome-headless-shell. Co-Authored-By: Claude Opus 5.5 (1M context) --- roles/web-app-odoo/files/playwright/_shared.js | 5 +++++ roles/web-app-odoo/files/playwright/addons/account.spec.js | 2 ++ roles/web-app-odoo/files/playwright/addons/contacts.spec.js | 2 ++ roles/web-app-odoo/files/playwright/addons/crm.spec.js | 2 ++ roles/web-app-odoo/files/playwright/addons/project.spec.js | 2 ++ .../files/playwright/addons/sale_management.spec.js | 2 ++ roles/web-app-odoo/files/playwright/addons/stock.spec.js | 2 ++ roles/web-app-odoo/files/playwright/addons/website.spec.js | 2 ++ roles/web-app-odoo/files/playwright/playwright.spec.js | 4 +++- 9 files changed, 22 insertions(+), 1 deletion(-) diff --git a/roles/web-app-odoo/files/playwright/_shared.js b/roles/web-app-odoo/files/playwright/_shared.js index 926cd7152b..86e16edf88 100644 --- a/roles/web-app-odoo/files/playwright/_shared.js +++ b/roles/web-app-odoo/files/playwright/_shared.js @@ -10,6 +10,10 @@ const env = { adminPassword: decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD || ""), }; +const onionOptions = /\bhttps?:\/\/[^/,\s"']+\.onion\b/i.test(env.odooBaseUrl) + ? { channel: "chromium", serviceWorkers: "block" } + : {}; + function baseUrl() { return env.odooBaseUrl.replace(/\/$/, ""); } @@ -152,6 +156,7 @@ async function openModule(page, modulePath) { module.exports = { env, + onionOptions, baseUrl, clickOdooSsoButton, loginToOdoo, diff --git a/roles/web-app-odoo/files/playwright/addons/account.spec.js b/roles/web-app-odoo/files/playwright/addons/account.spec.js index 49344827ed..39a712fabf 100644 --- a/roles/web-app-odoo/files/playwright/addons/account.spec.js +++ b/roles/web-app-odoo/files/playwright/addons/account.spec.js @@ -3,6 +3,8 @@ const { resolveTimeout } = require("../timeouts"); const { skipUnlessAddonEnabled } = require("../addon-gating"); const shared = require("../_shared"); +test.use(shared.onionOptions); + test("addon account: Accounting/Invoicing module is installed and serves its customer-invoices action", async ({ browser }) => { skipUnlessAddonEnabled("account"); diff --git a/roles/web-app-odoo/files/playwright/addons/contacts.spec.js b/roles/web-app-odoo/files/playwright/addons/contacts.spec.js index 7a20720884..05a65d4c7d 100644 --- a/roles/web-app-odoo/files/playwright/addons/contacts.spec.js +++ b/roles/web-app-odoo/files/playwright/addons/contacts.spec.js @@ -3,6 +3,8 @@ const { resolveTimeout } = require("../timeouts"); const { skipUnlessAddonEnabled } = require("../addon-gating"); const shared = require("../_shared"); +test.use(shared.onionOptions); + test("addon contacts: contacts module UI renders", async ({ browser }) => { skipUnlessAddonEnabled("contacts"); diff --git a/roles/web-app-odoo/files/playwright/addons/crm.spec.js b/roles/web-app-odoo/files/playwright/addons/crm.spec.js index d52797a5dc..d5d4ee7733 100644 --- a/roles/web-app-odoo/files/playwright/addons/crm.spec.js +++ b/roles/web-app-odoo/files/playwright/addons/crm.spec.js @@ -3,6 +3,8 @@ const { resolveTimeout } = require("../timeouts"); const { skipUnlessAddonEnabled } = require("../addon-gating"); const shared = require("../_shared"); +test.use(shared.onionOptions); + test("addon crm: CRM module is installed and its pipeline feature renders", async ({ browser }) => { skipUnlessAddonEnabled("crm"); diff --git a/roles/web-app-odoo/files/playwright/addons/project.spec.js b/roles/web-app-odoo/files/playwright/addons/project.spec.js index 458561fdcf..4e00fb836d 100644 --- a/roles/web-app-odoo/files/playwright/addons/project.spec.js +++ b/roles/web-app-odoo/files/playwright/addons/project.spec.js @@ -3,6 +3,8 @@ const { resolveTimeout } = require("../timeouts"); const { skipUnlessAddonEnabled } = require("../addon-gating"); const shared = require("../_shared"); +test.use(shared.onionOptions); + test("addon project: project module UI renders", async ({ browser }) => { skipUnlessAddonEnabled("project"); diff --git a/roles/web-app-odoo/files/playwright/addons/sale_management.spec.js b/roles/web-app-odoo/files/playwright/addons/sale_management.spec.js index 8092266219..9ab5980bde 100644 --- a/roles/web-app-odoo/files/playwright/addons/sale_management.spec.js +++ b/roles/web-app-odoo/files/playwright/addons/sale_management.spec.js @@ -3,6 +3,8 @@ const { resolveTimeout } = require("../timeouts"); const { skipUnlessAddonEnabled } = require("../addon-gating"); const shared = require("../_shared"); +test.use(shared.onionOptions); + test("addon sale_management: Sales module is installed and serves its quotations action", async ({ browser }) => { skipUnlessAddonEnabled("sale_management"); diff --git a/roles/web-app-odoo/files/playwright/addons/stock.spec.js b/roles/web-app-odoo/files/playwright/addons/stock.spec.js index 28192446a0..202114cf1c 100644 --- a/roles/web-app-odoo/files/playwright/addons/stock.spec.js +++ b/roles/web-app-odoo/files/playwright/addons/stock.spec.js @@ -3,6 +3,8 @@ const { resolveTimeout } = require("../timeouts"); const { skipUnlessAddonEnabled } = require("../addon-gating"); const shared = require("../_shared"); +test.use(shared.onionOptions); + test("addon stock: Inventory module is installed and serves its warehouse operations action", async ({ browser }) => { skipUnlessAddonEnabled("stock"); diff --git a/roles/web-app-odoo/files/playwright/addons/website.spec.js b/roles/web-app-odoo/files/playwright/addons/website.spec.js index 5a541bcc86..598f8d95c5 100644 --- a/roles/web-app-odoo/files/playwright/addons/website.spec.js +++ b/roles/web-app-odoo/files/playwright/addons/website.spec.js @@ -3,6 +3,8 @@ const { resolveTimeout } = require("../timeouts"); const { skipUnlessAddonEnabled } = require("../addon-gating"); const shared = require("../_shared"); +test.use(shared.onionOptions); + test("addon website: website module UI renders", async ({ browser }) => { skipUnlessAddonEnabled("website"); diff --git a/roles/web-app-odoo/files/playwright/playwright.spec.js b/roles/web-app-odoo/files/playwright/playwright.spec.js index f0dc42b44e..a67e38b230 100644 --- a/roles/web-app-odoo/files/playwright/playwright.spec.js +++ b/roles/web-app-odoo/files/playwright/playwright.spec.js @@ -3,8 +3,10 @@ const { resolveTimeout } = require("./timeouts"); const { decodeDotenvQuotedValue, isVisible, performKeycloakLoginForm, runAdminFlow, runBiberFlow, runGuestFlow, gotoOnion } = require("./personas"); const { isServiceEnabled } = require("./service-gating"); +const { onionOptions } = require("./_shared"); test.use({ - ignoreHTTPSErrors: true + ignoreHTTPSErrors: true, + ...onionOptions }); const oidcEnabled = isServiceEnabled("sso"); From e31b9557d26f714db0802d5a06fbdf7f348cb68b Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 12 Oct 2026 18:29:25 +0200 Subject: [PATCH 15/15] fix(env): keep the host .env when the runner container runs make setup The repository is bind-mounted into the infinito container, and every deploy runs `make setup` there, which depends on `dotenv`. The generator therefore rewrote the host's .env with the values the container sees: the host lost INFINITO_RUNNING_ON_GITHUB=true that way, and the guide replay pulled alpine:latest past the GHCR mirror. compose.yml now marks the infinito service with INFINITO_DOTENV_HOST_OWNED, and `python -m cli.meta.env` keeps an existing .env while that marker is "true". A missing .env is still generated, and `make dotenv-force` (env -i) still rewrites it. The key stays out of the env registry: only compose.yml sets it, it is no configuration value, and a .env entry could freeze the host .env. test_distros now also strips INFINITO_ENV_LOADED. load.sh returns early while that guard is set and the mtime of .env is unchanged, so the test only reloaded the budget keys it strips because the container kept rewriting .env. Verified: unit tests cover a kept .env, a missing one being generated, and a rewrite without the marker. Local compose deploys print "Kept .env: the host checkout owns it" where they printed "Wrote .env" before, the host .env checksum stayed the same across a deploy, and the four keys holding host paths resolve inside the container under the same paths. Co-Authored-By: Claude Opus 5.5 (1M context) --- cli/meta/env/__main__.py | 11 +++++ compose.yml | 1 + tests/unit/python/cli/meta/env/__init__.py | 0 tests/unit/python/cli/meta/env/test_main.py | 45 +++++++++++++++++++ .../scripts/tests/deploy/test_distros.py | 1 + 5 files changed, 58 insertions(+) create mode 100644 tests/unit/python/cli/meta/env/__init__.py create mode 100644 tests/unit/python/cli/meta/env/test_main.py diff --git a/cli/meta/env/__main__.py b/cli/meta/env/__main__.py index 9ab0ca4b71..d4f7e3764f 100644 --- a/cli/meta/env/__main__.py +++ b/cli/meta/env/__main__.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os import sys from utils.env.builder import build_env @@ -10,6 +11,8 @@ from utils.env.writer import write_dotenv from . import PROJECT_ROOT as REPO_ROOT +HOST_OWNED_KEY = "INFINITO_DOTENV_HOST_OWNED" # nocheck: dotenv-host-owned-marker Reason: set only by compose.yml inside the runner container; not a configuration key, a .env entry could freeze the host .env + def main() -> int: static_path = REPO_ROOT / "default.env" @@ -19,6 +22,14 @@ def main() -> int: print(f"ERROR: missing {static_path}", file=sys.stderr) return 2 + if out_path.is_file() and os.environ.get(HOST_OWNED_KEY) == "true": + print( + f"Kept {out_path.relative_to(REPO_ROOT)}: the host checkout owns it " + f"({HOST_OWNED_KEY})", + file=sys.stderr, + ) + return 0 + static, static_comments = parse_static_env_with_comments(static_path) eb = build_env(static, repo_root=REPO_ROOT, comments=static_comments) write_dotenv(eb, out_path) diff --git a/compose.yml b/compose.yml index 3bb9f3e536..8d12571046 100644 --- a/compose.yml +++ b/compose.yml @@ -78,6 +78,7 @@ services: INFINITO_DIR_VAR_LIB: ${INFINITO_DIR_VAR_LIB} INFINITO_DNS_IP: ${INFINITO_DNS_IP} INFINITO_DOMAIN: ${INFINITO_DOMAIN} + INFINITO_DOTENV_HOST_OWNED: "true" INFINITO_GHCR_MIRROR_PREFIX: ${INFINITO_GHCR_MIRROR_PREFIX} INFINITO_IMAGE_TAG: ${INFINITO_IMAGE_TAG} INFINITO_IP4: ${INFINITO_IP4} diff --git a/tests/unit/python/cli/meta/env/__init__.py b/tests/unit/python/cli/meta/env/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/python/cli/meta/env/test_main.py b/tests/unit/python/cli/meta/env/test_main.py new file mode 100644 index 0000000000..d2cc44c42f --- /dev/null +++ b/tests/unit/python/cli/meta/env/test_main.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +import cli.meta.env.__main__ as env_main + + +class TestDotenvHostOwned(unittest.TestCase): + def _run(self, *, dotenv_exists: bool, host_owned: str | None) -> tuple[int, bool]: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "default.env").write_text("INFINITO_DISTRO=debian\n") + if dotenv_exists: + (root / ".env").write_text("INFINITO_RUNNING_ON_GITHUB=true\n") + environ = ( + {} if host_owned is None else {env_main.HOST_OWNED_KEY: host_owned} + ) + with ( + patch.object(env_main, "REPO_ROOT", root), + patch.dict("os.environ", environ, clear=True), + patch.object( + env_main, "parse_static_env_with_comments", return_value=({}, {}) + ), + patch.object(env_main, "build_env") as build_env, + patch.object(env_main, "write_dotenv") as write_dotenv, + ): + build_env.return_value.values = {} + rc = env_main.main() + return rc, write_dotenv.called + + def test_a_host_owned_dotenv_is_kept(self) -> None: + self.assertEqual(self._run(dotenv_exists=True, host_owned="true"), (0, False)) + + def test_a_missing_dotenv_is_generated_even_when_host_owned(self) -> None: + self.assertEqual(self._run(dotenv_exists=False, host_owned="true"), (0, True)) + + def test_without_the_marker_the_dotenv_is_regenerated(self) -> None: + self.assertEqual(self._run(dotenv_exists=True, host_owned=None), (0, True)) + + +if __name__ == "__main__": # pragma: no cover + unittest.main() diff --git a/tests/unit/python/scripts/tests/deploy/test_distros.py b/tests/unit/python/scripts/tests/deploy/test_distros.py index 0abb3c7479..4b6cb76aee 100644 --- a/tests/unit/python/scripts/tests/deploy/test_distros.py +++ b/tests/unit/python/scripts/tests/deploy/test_distros.py @@ -29,6 +29,7 @@ STRIPPED = ( "INFINITO_DISTRO", "INFINITO_CI_DISTRO_BUDGET_SECONDS", "INFINITO_SWARM_STEP_TIMEOUT_MINUTES", + "INFINITO_ENV_LOADED", # nocheck: env-loader-internal-guard )