From f36460c6de4fce5ebf0ce95d020d7fc2380b2bbb Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 04:14:02 +0000 Subject: [PATCH 1/4] 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 d8c4807895..94cc4f4124 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.5 + version: 0.40.6 name: ollama min_storage: 3473MB cpus: "4.0" From c66b79ce824093cf0690dfa9bd3f879dba73c10a Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sat, 17 Oct 2026 10:17:39 +0200 Subject: [PATCH 2/4] fix(web-app-openproject): probe migrations without creating tables Swarm Nextcloud#3 on main 748189e890 (job 117182318941) wedged the OpenProject schema: the convergence check gave up after 74 attempts while the seeder kept exiting 1, first with `relation "ar_internal_metadata" already exists` from its structure load, then with `relation "announcements" already exists` on every `db:migrate`. The seeder (`docker/prod/seeder`) loads `db/structure.sql` through psql, statement by statement, when `\dt` finds no relation. In swarm, `depends_on` is ignored, so web and worker start alongside it and poll `await_migrations.sh`. That gate called `rails db:abort_if_pending_migrations`, which runs `migration_context.open`, i.e. `Migrator.new`, whose constructor executes `schema_migration.create_table` and `internal_metadata.create_table` (Rails 8.0.4, activerecord/lib/active_record/migration.rb:1430-1431). The seeder saw an empty database at 05:33:20; by 05:33:38 the gate had created `ar_internal_metadata`, the structure load aborted at line 255 with everything alphabetically before it already committed, and every later attempt took the migrate path into tables that already existed. Probe with `rails db:migrate:status` instead. It aborts while `schema_migrations` is missing and otherwise only reads the recorded versions (database_tasks.rb:303, migration.rb:1318); the gate treats any `down` row as pending. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../files/shell/await_migrations.sh | 18 +++-- .../roles/web-app-openproject/__init__.py | 0 .../web-app-openproject/files/__init__.py | 0 .../files/test_await_migrations.py | 80 +++++++++++++++++++ 4 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 tests/unit/python/roles/web-app-openproject/__init__.py create mode 100644 tests/unit/python/roles/web-app-openproject/files/__init__.py create mode 100644 tests/unit/python/roles/web-app-openproject/files/test_await_migrations.py diff --git a/roles/web-app-openproject/files/shell/await_migrations.sh b/roles/web-app-openproject/files/shell/await_migrations.sh index 1057cc26f1..e09c8bc0f5 100755 --- a/roles/web-app-openproject/files/shell/await_migrations.sh +++ b/roles/web-app-openproject/files/shell/await_migrations.sh @@ -2,13 +2,11 @@ # Wait for the seeder's migrations, then exec the app entrypoint. # # Swarm ignores depends_on, so web and worker would start while the seeder is -# still migrating and die on ActiveRecord::PendingMigrationError. The gate is -# read-only -- it never migrates, so concurrent replicas cannot race. +# still migrating and die on ActiveRecord::PendingMigrationError. # -# The wait is bounded on purpose. `db:abort_if_pending_migrations` exits 1 both -# for pending migrations and for an unreachable database (measured against -# openproject/openproject:17), so an unbounded loop would spin forever whenever -# the database is down. Past the deadline the entrypoint starts regardless, +# The wait is bounded on purpose. The probe fails both for pending migrations +# and for an unreachable database, so an unbounded loop would spin forever +# whenever the database is down. Past the deadline the entrypoint starts regardless, # which is exactly today's behaviour: the worst case stays the crash loop that # already exists, never a service that refuses to come up. # @@ -26,9 +24,15 @@ shift 2 exit 2 } +schema_is_current() { + local status + status=$(bundle exec rails db:migrate:status 2>/dev/null) || return 1 + ! grep -q -E '^[[:space:]]*down[[:space:]]' <<<"$status" +} + deadline=$(($(date +%s) + deadline_seconds)) while [ "$(date +%s)" -lt "$deadline" ]; do - if bundle exec rails db:abort_if_pending_migrations >/dev/null 2>&1; then + if schema_is_current; then echo "await_migrations: schema is current, starting $1" exec "$@" fi diff --git a/tests/unit/python/roles/web-app-openproject/__init__.py b/tests/unit/python/roles/web-app-openproject/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/python/roles/web-app-openproject/files/__init__.py b/tests/unit/python/roles/web-app-openproject/files/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/python/roles/web-app-openproject/files/test_await_migrations.py b/tests/unit/python/roles/web-app-openproject/files/test_await_migrations.py new file mode 100644 index 0000000000..4cdf1a6ac4 --- /dev/null +++ b/tests/unit/python/roles/web-app-openproject/files/test_await_migrations.py @@ -0,0 +1,80 @@ +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + +from utils import PROJECT_ROOT + +SCRIPT = PROJECT_ROOT / "roles/web-app-openproject/files/shell/await_migrations.sh" + +BUNDLE_STUB = """#!/usr/bin/env bash +printf '%s\\n' "$*" >>"${STUB_DIR}/calls" +cat "${STUB_DIR}/status" +exit "${STUB_RC}" +""" + +HEADER = ( + "\ndatabase: openproject\n\n" + " Status Migration ID Migration Name\n" + "-" * 50 + "\n" +) +ALL_UP = HEADER + " up 1000016 Aggregated migrations\n" +ONE_DOWN = ALL_UP + " down 20261001000000 Add announcement flags\n" + + +class TestAwaitMigrations(unittest.TestCase): + def run_gate(self, status, rc=0): + """Run the gate against a stubbed `bundle` with a 2 s deadline. + + Args: + status: what the stubbed `rails db:migrate:status` prints. + rc: exit code of the stubbed `bundle`. + + Returns: + Tuple of the gate's stdout plus stderr and the recorded `bundle` calls. + """ + with tempfile.TemporaryDirectory() as tmp: + stub_dir = Path(tmp) + bundle = stub_dir / "bundle" + bundle.write_text(BUNDLE_STUB) + bundle.chmod(0o755) + (stub_dir / "status").write_text(status) + env = dict(os.environ) + env["PATH"] = f"{stub_dir}{os.pathsep}{env['PATH']}" + env["STUB_DIR"] = tmp + env["STUB_RC"] = str(rc) + done = subprocess.run( + ["bash", str(SCRIPT), "2", "1", "echo", "ENTRYPOINT"], + env=env, + capture_output=True, + text=True, + check=False, + ) + calls = ( + stub_dir / "calls" + ).read_text() # nocheck: cache-read tempdir fixture + return done.stdout + done.stderr, calls + + def test_a_current_schema_starts_the_entrypoint_at_once(self): + output, calls = self.run_gate(ALL_UP) + self.assertIn("schema is current", output) + self.assertIn("ENTRYPOINT", output) + self.assertEqual(calls.splitlines(), ["exec rails db:migrate:status"]) + + def test_a_pending_migration_keeps_the_gate_waiting(self): + output, calls = self.run_gate(ONE_DOWN) + self.assertIn("deadline of 2s reached", output) + self.assertGreater(len(calls.splitlines()), 1) + + def test_a_failing_probe_keeps_the_gate_waiting(self): + output, _ = self.run_gate("", rc=1) + self.assertIn("deadline of 2s reached", output) + + def test_the_probe_never_builds_a_migrator(self): + _, calls = self.run_gate(ONE_DOWN) + self.assertNotIn("abort_if_pending_migrations", calls) + self.assertEqual(set(calls.splitlines()), {"exec rails db:migrate:status"}) + + +if __name__ == "__main__": + unittest.main() From 159d7e6d012405349577ff4dd5aef21d39048f33 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sat, 17 Oct 2026 11:16:43 +0200 Subject: [PATCH 3/4] test(web-app-openproject): accept a single probe before the gate deadline `deadline` is whole seconds from `date +%s`. A run that starts just before a second boundary reaches the deadline after the first probe and one `sleep 1`, so the gate correctly gives up after one probe while the test demanded two. With a stubbed clock (1000, 1000, 1002) the gate probes once and prints `deadline of 2s reached`. The deadline message already proves the pending migration held the entrypoint back; require at least one probe. Reported by CodeRabbit on #979. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../roles/web-app-openproject/files/test_await_migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/python/roles/web-app-openproject/files/test_await_migrations.py b/tests/unit/python/roles/web-app-openproject/files/test_await_migrations.py index 4cdf1a6ac4..6fbf6d7f90 100644 --- a/tests/unit/python/roles/web-app-openproject/files/test_await_migrations.py +++ b/tests/unit/python/roles/web-app-openproject/files/test_await_migrations.py @@ -64,7 +64,7 @@ class TestAwaitMigrations(unittest.TestCase): def test_a_pending_migration_keeps_the_gate_waiting(self): output, calls = self.run_gate(ONE_DOWN) self.assertIn("deadline of 2s reached", output) - self.assertGreater(len(calls.splitlines()), 1) + self.assertGreaterEqual(len(calls.splitlines()), 1) def test_a_failing_probe_keeps_the_gate_waiting(self): output, _ = self.run_gate("", rc=1) From ee8e040502713424eacaf3a753f2451624af99b9 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:07:07 +0000 Subject: [PATCH 4/4] 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 8186d975af..a63a82eb63 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.16" + version: "2026.10.17" ports: local: http: 8089