From 2d0016d27be459e2f82862cd830e633d0b48493f Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Wed, 28 Oct 2026 22:39:00 +0100 Subject: [PATCH 1/4] fix(svc-net-tor): let a port probe attempt outlast Tor's SocksTimeout The onion port probe gave every SOCKS connect 60s, half of Tor's SocksTimeout (120s by default; torrc does not set it), although its own docstring argued that abandoning an attempt before SocksTimeout is destructive. Every attempt authenticates with fresh credentials, so Tor builds a whole new rendezvous for it and keeps working on it until SocksTimeout. The probe walked away first and called the port dead. The other raw onion connects in the repository, the svc-db-* CLI tests and test-e2e-playwright's wait_onion_reachable.sh, wait 120s per attempt. Run 40128751852 shows how often an attempt needs more than that. Of 178 svc-net-tor CLI runs in its 89 compose onion jobs, three lost their first attempt to the 60s timeout and connected on the second within seconds (Shopware#1, Hugo#1, n8n#1; the CLI test took 77-79s against the usual 1-16s), and Hugo#1's other run took 40s on a single attempt. When the stall outlasts the 70s until the second attempt, the probe fails while the HTTP probe reaches the same onion port seconds earlier: OpenBao#1 (run 39166850273, job 118050491920, the original failure of #1048) and Socialhome#1 (run 40088473634, job 121019758461). In the Socialhome job tor.log holds no line for either attempt, because Tor never reached its own verdict. ONION_PORT_TIMEOUT is now 130: SocksTimeout plus room for Tor's once-per-second expiry sweep, so an attempt ends with Tor's own answer (reply 0x06, and a "Giving up" notice in tor.log, which the rescue artifact captures) instead of the probe's. The second attempt now starts at 140s and ends at 270s, so the probe rides out every stall the old budget did, and any stall shorter than about 260s instead of about 120s. A green run still costs one connect per port, and a refused or unmapped port still fails at once. A unit test pins the window above Tor's SocksTimeout and fails as soon as torrc sets its own. That doubles what a dead port costs, and meta/tests.yml caps the whole CLI test. The largest compose onion deploy in run 40128751852 probes 19 domains and 5 ports; with everything dead that is 19 x 350 + 5 x 260 + 10 = 7960s, beyond the 7200s that 069df39027 set for the same reason. 9000 keeps the full failure output for up to 22 domains. Closes #1214 Co-Authored-By: Claude Opus 5.5 --- roles/svc-net-tor/files/test/onion_ports.py | 15 ++++---- roles/svc-net-tor/meta/tests.yml | 2 +- roles/svc-net-tor/templates/test.env.j2 | 2 +- .../svc-net-tor/test_onion_port_window.py | 38 +++++++++++++++++++ 4 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 tests/unit/python/roles/svc-net-tor/test_onion_port_window.py diff --git a/roles/svc-net-tor/files/test/onion_ports.py b/roles/svc-net-tor/files/test/onion_ports.py index adb6a81c66..7b06449a63 100755 --- a/roles/svc-net-tor/files/test/onion_ports.py +++ b/roles/svc-net-tor/files/test/onion_ports.py @@ -25,12 +25,13 @@ Env (rendered into test.env from templates/test.env.j2): ONION_HOST the node onion host ONION_PORTS space-separated forwarded ports; empty means there is nothing to check - ONION_PORT_TIMEOUT seconds per connect attempt. 60 for the same reason the - domain probe uses it: Tor's SocksTimeout default is 120s - and abandoning earlier is destructive, because the next - attempt opens a fresh SOCKS connection and throws away - the descriptor fetches and rendezvous retries the last - one was still working through + ONION_PORT_TIMEOUT seconds per connect attempt, above Tor's SocksTimeout + (120s by default; torrc does not set it). Every attempt + builds a fresh rendezvous, which Tor keeps working on + until SocksTimeout and then ends itself with reply 0x06 + and a "Giving up" notice in its log. An attempt abandoned + earlier throws away a rendezvous Tor may still complete + and leaves Tor's log without a trace of the failure ONION_PORT_RETRIES passes over the failing ports. Below the domain probe's RETRIES because this phase multiplies by the port count, and meta/tests.yml caps the whole CLI test @@ -148,7 +149,7 @@ def main() -> int: retries = int(_required("ONION_PORT_RETRIES", "passes over the failing ports")) sleep_seconds = float(_required("SLEEP_SECONDS", "wait between passes")) timeout = float( - _required("ONION_PORT_TIMEOUT", "seconds per connect, flavor-dependent") + _required("ONION_PORT_TIMEOUT", "seconds per connect, above Tor's SocksTimeout") ) ports = [int(entry) for entry in os.environ.get("ONION_PORTS", "").split()] diff --git a/roles/svc-net-tor/meta/tests.yml b/roles/svc-net-tor/meta/tests.yml index 272c1e148d..036f2dec6c 100644 --- a/roles/svc-net-tor/meta/tests.yml +++ b/roles/svc-net-tor/meta/tests.yml @@ -1,3 +1,3 @@ --- cli: - timeout: 7200 + timeout: 9000 diff --git a/roles/svc-net-tor/templates/test.env.j2 b/roles/svc-net-tor/templates/test.env.j2 index 51288356b2..0788bce017 100644 --- a/roles/svc-net-tor/templates/test.env.j2 +++ b/roles/svc-net-tor/templates/test.env.j2 @@ -2,7 +2,7 @@ NGINX_SERVERS_DIR={{ lookup('nginx', 'directories.configuration.servers') }} ONION_HOST={{ lookup('config', 'svc-net-tor', 'services.tor.node') }} ONION_PORTS={{ lookup('tor_ports') | map(attribute='onion_port') | unique | sort | join(' ') | dotenv_quote }} ONION_PORT_RETRIES=2 -ONION_PORT_TIMEOUT=60 +ONION_PORT_TIMEOUT=130 RETRIES=5 SLEEP_SECONDS=10 TOR_CONTAINER={{ lookup('container_address', application_id, 'tor') }} diff --git a/tests/unit/python/roles/svc-net-tor/test_onion_port_window.py b/tests/unit/python/roles/svc-net-tor/test_onion_port_window.py new file mode 100644 index 0000000000..0072ac161e --- /dev/null +++ b/tests/unit/python/roles/svc-net-tor/test_onion_port_window.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import re +import unittest + +from utils.cache.files import read_text + +from . import PROJECT_ROOT + +ROLE = PROJECT_ROOT / "roles" / "svc-net-tor" +TEST_ENV = ROLE / "templates" / "test.env.j2" +TORRC = ROLE / "templates" / "torrc.j2" +TOR_DEFAULT_SOCKS_TIMEOUT = 120 + + +class TestOnionPortWindow(unittest.TestCase): + def test_an_attempt_outlasts_tors_socks_timeout(self) -> None: + self.assertNotRegex( + read_text(str(TORRC)), + r"(?m)^SocksTimeout\b", + "torrc now sets SocksTimeout; ONION_PORT_TIMEOUT has to be derived " + "from it instead of from Tor's default", + ) + declared = re.search( + r"(?m)^ONION_PORT_TIMEOUT=(\d+)$", read_text(str(TEST_ENV)) + ) + self.assertIsNotNone(declared, "test.env.j2 declares no ONION_PORT_TIMEOUT") + self.assertGreater( + int(declared.group(1)), + TOR_DEFAULT_SOCKS_TIMEOUT, + "a port probe attempt that ends before Tor's SocksTimeout walks away " + "from a rendezvous Tor is still building, and Tor never reaches its " + "own verdict or logs it", + ) + + +if __name__ == "__main__": + unittest.main() From c59e2838d6c567eb1f002f4b08cc9507dc49ec57 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 29 Oct 2026 00:01:20 +0100 Subject: [PATCH 2/4] fix(swarm): reach gitea, shopware and fider from inside their containers In swarm, `docker stack deploy` drops the host IP of a short port mapping, so a `127.0.0.1:` publish lands on the ingress routing mesh on every node. The provisioning of these three roles only worked because the mesh answered on the host: - web-app-gitea waited for `DOCKER_REACH_HOST:` with wait_for and polled `/api/v1/version` there with uri_retry. - web-app-shopware waited for `DOCKER_REACH_HOST:`. - web-app-fider bootstrapped its first tenant in swarm by POSTing to `127.0.0.1:` on the hosting node. As long as these calls depend on the mesh, container_ports cannot stop publishing loopback ports in swarm. The probes now run inside the service's own container through the resolved `_LOCAL_CID` on `_HOST_NODE`, against the internal port: - Gitea uses the curl it ships. The first wait accepts any HTTP answer, as wait_for did. The post-migration wait still requires 200. - Shopware runs the wget probe its compose healthcheck already uses. - Fider ships no HTTP client, so bash writes the tenant POST to /dev/tcp. The body comes in on stdin and the status line is checked for 200/201. Fider's compose path is unchanged. The compose render of all three roles is unchanged. The host-side ingress guard from svc-swarm-node stays in place until the remaining host-side consumers are moved. Refs #1089 Co-Authored-By: Claude Opus 5.5 --- roles/web-app-fider/tasks/01_setup_oidc.yml | 20 ++++++++----- roles/web-app-fider/vars/main.yml | 3 +- roles/web-app-gitea/tasks/01_manager_ops.yml | 30 +++++++++++-------- roles/web-app-gitea/vars/main.yml | 1 + .../web-app-shopware/tasks/02_manager_ops.yml | 18 ++++++----- 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/roles/web-app-fider/tasks/01_setup_oidc.yml b/roles/web-app-fider/tasks/01_setup_oidc.yml index 2985fc2793..ef2cd8981d 100644 --- a/roles/web-app-fider/tasks/01_setup_oidc.yml +++ b/roles/web-app-fider/tasks/01_setup_oidc.yml @@ -45,15 +45,21 @@ - DEPLOYMENT_MODE == 'compose' - (fider_tenant_count.rows[0][0] | int) == 0 -- name: "🥾 Fider | Bootstrap initial tenant via API (swarm, routing-mesh port)" - uri_retry: - url: "http://127.0.0.1:{{ lookup('config', application_id, 'services.fider.ports.local.http') }}/_api/tenants" - method: POST - body_format: json - body: "{{ FIDER_TENANT_BOOTSTRAP_BODY }}" - status_code: [200, 201] +- name: "🥾 Fider | Bootstrap initial tenant via API (swarm in-container)" + ansible.builtin.shell: + cmd: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node + container exec -i {{ FIDER_LOCAL_CID | quote }} bash -c + 'set -e; body=$(cat); length=$(printf %s "$body" | wc -c); + exec 3<>"/dev/tcp/127.0.0.1/$1"; + printf "POST /_api/tenants HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s" "$length" "$body" >&3; + head -n1 <&3 | cut -d" " -f2' _ {{ FIDER_INTERNAL_PORT | quote }} + stdin: "{{ FIDER_TENANT_BOOTSTRAP_BODY | to_json }}" register: fider_tenant_bootstrap changed_when: true + failed_when: (fider_tenant_bootstrap.stdout | trim) not in ['200', '201'] + retries: 30 + delay: 2 + until: fider_tenant_bootstrap.rc == 0 delegate_to: "{{ FIDER_HOST_NODE }}" when: - DEPLOYMENT_MODE == 'swarm' diff --git a/roles/web-app-fider/vars/main.yml b/roles/web-app-fider/vars/main.yml index 3cd44bb98f..2297ea2a8e 100644 --- a/roles/web-app-fider/vars/main.yml +++ b/roles/web-app-fider/vars/main.yml @@ -3,6 +3,7 @@ application_id: "web-app-fider" FIDER_CONTAINER: "{{ lookup('config', application_id, 'services.fider.name') }}" +FIDER_INTERNAL_PORT: "{{ lookup('config', application_id, 'services.fider.ports.internal.http') }}" FIDER_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') }}" @@ -20,7 +21,7 @@ FIDER_MCP_PORT: "{{ lookup('config', application_id, 'services.fidermcp.ports.in FIDER_MCP_BEARER: "{{ lookup('config', application_id, 'secrets.credentials.mcp_bearer') }}" FIDER_MCP_INSTANCE_DIR: "{{ [lookup('container', application_id, 'directories.instance'), 'mcp'] | path_join }}" FIDER_MCP_UPSTREAM_HOST: "{{ lookup('config', application_id, 'services.fider.name') }}" -FIDER_MCP_UPSTREAM_PORT: "{{ lookup('config', application_id, 'services.fider.ports.internal.http') }}" +FIDER_MCP_UPSTREAM_PORT: "{{ FIDER_INTERNAL_PORT }}" FIDER_MCP_UPSTREAM_URL: "http://{{ FIDER_MCP_UPSTREAM_HOST }}:{{ FIDER_MCP_UPSTREAM_PORT }}" FIDER_MCP_CONTRACT: provider: "{{ application_id }}" diff --git a/roles/web-app-gitea/tasks/01_manager_ops.yml b/roles/web-app-gitea/tasks/01_manager_ops.yml index 2eb113d931..f04d4aacc6 100644 --- a/roles/web-app-gitea/tasks/01_manager_ops.yml +++ b/roles/web-app-gitea/tasks/01_manager_ops.yml @@ -1,11 +1,4 @@ --- -- name: ⏳ Wait for Gitea HTTP endpoint - wait_for: - host: "{{ DOCKER_REACH_HOST }}" - port: "{{ lookup('config', application_id, 'services.gitea.ports.local.http') }}" - delay: 5 - timeout: "{{ lookup('timeout', 300) | int }}" - - name: "🔍 Resolve {{ application_id }} hosting node + container id" ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}" vars: @@ -13,6 +6,17 @@ resolve_output_prefix: GITEA resolve_compose_cid: "{{ GITEA_CONTAINER_ADDRESS }}" +- name: ⏳ Wait for Gitea HTTP endpoint + ansible.builtin.shell: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node + container exec {{ GITEA_LOCAL_CID | quote }} + {{ 10 | curl(retries=0) }} -S -o /dev/null {{ GITEA_LOOPBACK_VERSION_URL | quote }} + delegate_to: "{{ GITEA_HOST_NODE }}" + register: gitea_http_ready + changed_when: false + retries: 60 + delay: 5 + until: gitea_http_ready.rc == 0 + - name: "⏳ Wait for Gitea app.ini to be present after redeploy" shell: | # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node container exec -i --user {{ GITEA_USER }} {{ GITEA_LOCAL_CID | quote }} \ @@ -48,13 +52,13 @@ failed_when: create_admin.rc != 0 and 'user already exists' not in create_admin.stderr - name: "⏳ Wait until Gitea setup and migrations are ready" - uri_retry: - url: "http://{{ DOCKER_REACH_HOST }}:{{ lookup('config', application_id, 'services.gitea.ports.local.http') }}/api/v1/version" - method: GET - status_code: 200 - return_content: false + ansible.builtin.shell: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node + container exec {{ GITEA_LOCAL_CID | quote }} + {{ 10 | curl(retries=0) }} -fS -o /dev/null {{ GITEA_LOOPBACK_VERSION_URL | quote }} + delegate_to: "{{ GITEA_HOST_NODE }}" register: gitea_ready - until: gitea_ready.status == 200 + changed_when: false + until: gitea_ready.rc == 0 retries: 20 delay: 5 when: (GITEA_OIDC_ENABLED | bool) or (GITEA_LDAP_ENABLED | bool) diff --git a/roles/web-app-gitea/vars/main.yml b/roles/web-app-gitea/vars/main.yml index 7b36c9e8e4..9282a3dc5f 100644 --- a/roles/web-app-gitea/vars/main.yml +++ b/roles/web-app-gitea/vars/main.yml @@ -22,6 +22,7 @@ GITEA_CONTAINER: "{{ lookup('config', application_id, 'services.gitea.name') }}" GITEA_CONTAINER_ADDRESS: "{{ lookup('container_address', application_id, entity_name) }}" GITEA_USER: "git" GITEA_CONFIG: "/data/gitea/conf/app.ini" +GITEA_LOOPBACK_VERSION_URL: "http://127.0.0.1:{{ lookup('config', application_id, 'services.gitea.ports.internal.http') }}/api/v1/version" GITEA_REDIS_ENABLED: "{{ lookup('config', application_id, 'services.redis.enabled') }}" GITEA_REDIS_ADDRESS: "{{ lookup('engine', 'redis', application_id, 'host') }}:{{ lookup('engine', 'redis', application_id, 'port') }}" diff --git a/roles/web-app-shopware/tasks/02_manager_ops.yml b/roles/web-app-shopware/tasks/02_manager_ops.yml index b8b9040167..d8ed62617a 100644 --- a/roles/web-app-shopware/tasks/02_manager_ops.yml +++ b/roles/web-app-shopware/tasks/02_manager_ops.yml @@ -37,13 +37,6 @@ - name: "🚿 Flush compose handlers" meta: flush_handlers -- name: ⏳ Wait for Shopware HTTP endpoint - wait_for: - host: "{{ DOCKER_REACH_HOST }}" - port: "{{ lookup('config', application_id, 'services.shopware.ports.local.http') }}" - delay: 5 - timeout: "{{ lookup('timeout', 300) | int }}" - - name: "🔍 Resolve Shopware web hosting node + container id" ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}" vars: @@ -53,6 +46,17 @@ resolve_converge_retries: 120 resolve_converge_delay: 5 +- name: ⏳ Wait for Shopware HTTP endpoint + ansible.builtin.shell: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node + container exec {{ SHOPWARE_WEB_LOCAL_CID | quote }} + sh -c 'wget -q --spider "$1/robots.txt" || wget -q --spider "$1/"' _ {{ SHOPWARE_API_BASE | quote }} + delegate_to: "{{ SHOPWARE_WEB_HOST_NODE }}" + register: shopware_http_ready + changed_when: false + retries: 60 + delay: 5 + until: shopware_http_ready.rc == 0 + - name: "✅ Ensure admin user exists with correct password" ansible.builtin.include_tasks: file: 01_admin.yml From c2577560f8e0cef696327b412628f16cd107bd24 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 29 Oct 2026 01:41:55 +0100 Subject: [PATCH 3/4] fix(swarm): bound the in-container probes and retry only safe fider POSTs Review follow-up on the in-container probes of gitea, shopware and fider. - fider: the tenant POST over /dev/tcp waited on `head` without a bound, so a peer that accepts and stays silent blocked the task for good. It now runs under `timeout` (TIMEOUT_FACTOR-scaled 30 s, the old uri socket timeout) and reads the status line with the `read` builtin, so the bound kills the whole probe. Only outcomes Fider never committed are retried: a refused connect and a 5xx (Fider rolls the request transaction back on a handler error). A timeout or a closed socket after the request was sent fails the task instead of resending the POST; the next run's tenant count decides again. - fider: both bootstrap tasks get `no_log` behind MASK_CREDENTIALS_IN_LOGS, as the body carries the administrator's name and email. - shopware: each wget probe runs under `timeout`, as busybox wget waits 900 s per read by default. - shopware, gitea: the first readiness wait derives its retries from lookup('timeout', 300) again, the budget of the wait_for it replaced, so TIMEOUT_FACTOR scales it. Refs #1089 Co-Authored-By: Claude Opus 5.5 --- roles/web-app-fider/tasks/01_setup_oidc.yml | 14 +++++++++----- roles/web-app-gitea/tasks/01_manager_ops.yml | 2 +- roles/web-app-shopware/tasks/02_manager_ops.yml | 5 +++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/roles/web-app-fider/tasks/01_setup_oidc.yml b/roles/web-app-fider/tasks/01_setup_oidc.yml index ef2cd8981d..fe5cc89d09 100644 --- a/roles/web-app-fider/tasks/01_setup_oidc.yml +++ b/roles/web-app-fider/tasks/01_setup_oidc.yml @@ -41,6 +41,7 @@ body_format: json body: "{{ FIDER_TENANT_BOOTSTRAP_BODY }}" status_code: [200, 201] + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" when: - DEPLOYMENT_MODE == 'compose' - (fider_tenant_count.rows[0][0] | int) == 0 @@ -48,18 +49,21 @@ - name: "🥾 Fider | Bootstrap initial tenant via API (swarm in-container)" ansible.builtin.shell: cmd: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node - container exec -i {{ FIDER_LOCAL_CID | quote }} bash -c + container exec -i {{ FIDER_LOCAL_CID | quote }} + timeout {{ lookup('timeout', 30) | int }} bash -c 'set -e; body=$(cat); length=$(printf %s "$body" | wc -c); - exec 3<>"/dev/tcp/127.0.0.1/$1"; + exec 3<>"/dev/tcp/127.0.0.1/$1" || exit 75; printf "POST /_api/tenants HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s" "$length" "$body" >&3; - head -n1 <&3 | cut -d" " -f2' _ {{ FIDER_INTERNAL_PORT | quote }} + read -r _ status _ <&3; printf %s "$status"; + case $status in 5*) exit 75;; esac' _ {{ FIDER_INTERNAL_PORT | quote }} stdin: "{{ FIDER_TENANT_BOOTSTRAP_BODY | to_json }}" register: fider_tenant_bootstrap changed_when: true - failed_when: (fider_tenant_bootstrap.stdout | trim) not in ['200', '201'] + failed_when: fider_tenant_bootstrap.rc != 0 or (fider_tenant_bootstrap.stdout | trim) not in ['200', '201'] retries: 30 delay: 2 - until: fider_tenant_bootstrap.rc == 0 + until: fider_tenant_bootstrap.rc != 75 + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" delegate_to: "{{ FIDER_HOST_NODE }}" when: - DEPLOYMENT_MODE == 'swarm' diff --git a/roles/web-app-gitea/tasks/01_manager_ops.yml b/roles/web-app-gitea/tasks/01_manager_ops.yml index f04d4aacc6..9c4754fe17 100644 --- a/roles/web-app-gitea/tasks/01_manager_ops.yml +++ b/roles/web-app-gitea/tasks/01_manager_ops.yml @@ -13,7 +13,7 @@ delegate_to: "{{ GITEA_HOST_NODE }}" register: gitea_http_ready changed_when: false - retries: 60 + retries: "{{ ((lookup('timeout', 300) | int) / 5) | round(0, 'ceil') | int }}" delay: 5 until: gitea_http_ready.rc == 0 diff --git a/roles/web-app-shopware/tasks/02_manager_ops.yml b/roles/web-app-shopware/tasks/02_manager_ops.yml index d8ed62617a..d49edced27 100644 --- a/roles/web-app-shopware/tasks/02_manager_ops.yml +++ b/roles/web-app-shopware/tasks/02_manager_ops.yml @@ -49,11 +49,12 @@ - name: ⏳ Wait for Shopware HTTP endpoint ansible.builtin.shell: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node container exec {{ SHOPWARE_WEB_LOCAL_CID | quote }} - sh -c 'wget -q --spider "$1/robots.txt" || wget -q --spider "$1/"' _ {{ SHOPWARE_API_BASE | quote }} + sh -c 'timeout "$2" wget -q --spider "$1/robots.txt" || timeout "$2" wget -q --spider "$1/"' + _ {{ SHOPWARE_API_BASE | quote }} {{ lookup('timeout', 10) | int }} delegate_to: "{{ SHOPWARE_WEB_HOST_NODE }}" register: shopware_http_ready changed_when: false - retries: 60 + retries: "{{ ((lookup('timeout', 300) | int) / 5) | round(0, 'ceil') | int }}" delay: 5 until: shopware_http_ready.rc == 0 From 618a640d4a7abee05fc031f1ccc888901094edac Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 29 Oct 2026 12:29:08 +0100 Subject: [PATCH 4/4] fix(lint): make mbake format the Makefile the same on every machine The repository installs mbake unpinned and shipped no .bake.toml, so the Makefile's final newline depended on the mbake version: - mbake 1.4.6 (e.g. the pkgmgr venv that wins on a host PATH) defaults to ensure_final_newline = false: `mbake format` strips the newline and `mbake format --check` accepts the stripped file. - mbake 1.5.0 (what pip installs today) rejects the same file with "Missing final newline", which is what CI reported on PR #1255. So a local `make autoformat` could strip the newline, the local lint-makefile stayed green, and CI failed; 6f653ce633 and #1262 had to restore the newline by hand. The tracked .bake.toml sets only ensure_final_newline = true, so every other formatter rule keeps its default. autoformat.sh and makefile.sh pass it with --config, so the result no longer depends on the version or on the directory mbake runs from. Checked with both versions: main's Makefile passes `--check --config .bake.toml`, and a copy without the final newline fails it with "Missing final newline". Co-Authored-By: Claude Opus 5.5 --- .bake.toml | 2 ++ scripts/lint/autoformat.sh | 4 ++-- scripts/lint/makefile.sh | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .bake.toml diff --git a/.bake.toml b/.bake.toml new file mode 100644 index 0000000000..2adc00dfd2 --- /dev/null +++ b/.bake.toml @@ -0,0 +1,2 @@ +[formatter] +ensure_final_newline = true diff --git a/scripts/lint/autoformat.sh b/scripts/lint/autoformat.sh index 907e51eaee..1a4fa210a3 100755 --- a/scripts/lint/autoformat.sh +++ b/scripts/lint/autoformat.sh @@ -139,8 +139,8 @@ run_mbake() { return 0 fi local rc=0 - mbake format Makefile >/dev/null 2>&1 || rc=$? - [[ $rc -eq 0 ]] && { mbake format Makefile >/dev/null 2>&1 || rc=$?; } + mbake format --config .bake.toml Makefile >/dev/null 2>&1 || rc=$? + [[ $rc -eq 0 ]] && { mbake format --config .bake.toml Makefile >/dev/null 2>&1 || rc=$?; } if [[ $rc -eq 0 ]]; then write_status mbake OK else diff --git a/scripts/lint/makefile.sh b/scripts/lint/makefile.sh index 84e3b27f7e..07819375df 100755 --- a/scripts/lint/makefile.sh +++ b/scripts/lint/makefile.sh @@ -17,4 +17,4 @@ if ! command -v mbake >/dev/null 2>&1; then exit 127 fi -mbake format --check Makefile +mbake format --check --config .bake.toml Makefile