feat(2): Q4.6 discourse — recipe_meta + postgres P4 overlays + health (WIP, §4.3 create-topic next)

discourse (forum: postgres+redis+sidekiq). HEALTH_PATH=/srv/status (slow Rails boot, DEPLOY_TIMEOUT=1800).
P4 via postgres ci_marker (db service, pg_dump backupbot — matrix-synapse pattern). Health functional
test. §4.3 create-a-topic + PARITY.md to follow after smoke discovers the admin/API bootstrap path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 20:38:25 +01:00
parent e36656f688
commit ca7acf3d52
5 changed files with 130 additions and 0 deletions

View File

@ -0,0 +1,20 @@
"""discourse — health/readiness functional test (Phase 2).
SOURCE: no recipe-maintainer corpus exists for discourse (P2 vacuous — see PARITY.md). This is a
Phase-2 health check aligned with the parity-port convention: /srv/status returns 200 only once the
Rails app is actually serving (the recipe's HEALTH_PATH and the canonical "is discourse up" signal).
"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
from harness import http as harness_http # noqa: E402
def test_discourse_srv_status_ok(live_app):
url = f"https://{live_app}/srv/status"
status, _ = harness_http.retry_http_get(url, expect_status=200, max_wait=120, interval=5)
assert status == 200, f"GET {url} HTTP {status} (expected 200 — discourse not serving)"

47
tests/discourse/ops.py Normal file
View File

@ -0,0 +1,47 @@
"""discourse — pre-op seed hooks (Phase 1e HC3 / Phase 2 P4 backup data-integrity).
Discourse persists in postgres (`db` service, user/db=discourse). The recipe's backupbot pre-hook
pg_dumps that DB, and restore reloads the dump — so real backup data-integrity = seed a `ci_marker`
row into the discourse DB, back up, mutate, restore, and prove the row survived (the same DB the
recipe actually backs up). Mirrors the matrix-synapse postgres pattern.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
from harness import lifecycle # noqa: E402
def _psql(domain, sql):
cmd = (
'PGPASSWORD=$(cat /run/secrets/db_password) '
f'psql -U discourse -d discourse -tAc "{sql}"'
)
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
def _seed(domain, value):
_psql(
domain,
"CREATE TABLE IF NOT EXISTS ci_marker(v text); DELETE FROM ci_marker; "
f"INSERT INTO ci_marker VALUES('{value}');",
)
got = _psql(domain, "SELECT v FROM ci_marker;")
assert got == value, f"seed did not commit (read back {got!r}, expected {value!r})"
def pre_upgrade(domain, meta):
_seed(domain, "upgrade-survives")
def pre_backup(domain, meta):
_seed(domain, "original")
def pre_restore(domain, meta):
# diverge from the backup so a successful restore is observable
_psql(domain, "DROP TABLE IF EXISTS ci_marker;")
assert _psql(domain, "SELECT to_regclass('public.ci_marker');") in ("", "NULL"), (
"drop did not take"
)

View File

@ -0,0 +1,11 @@
# Per-recipe harness config for discourse (Phase 2 Q4.6 — forum; postgres + redis + sidekiq).
#
# Discourse (bitnami/discourse) is a slow-booting Rails app: the recipe healthcheck polls
# /srv/status with a 5-minute start_period, and a cold first boot (DB migrate + asset precompile)
# regularly takes 8-15 min, so the deploy/HTTP timeouts are generous. /srv/status returns 200 only
# once the app is actually serving (the canonical "is discourse up" signal — NOT "/", which may
# redirect to setup).
HEALTH_PATH = "/srv/status"
HEALTH_OK = (200,)
DEPLOY_TIMEOUT = 1800
HTTP_TIMEOUT = 1200

View File

@ -0,0 +1,26 @@
"""discourse — BACKUP overlay (Phase 1e HC3 / Phase 2 P4): assertion-only + additive.
ops.pre_backup seeded ci_marker='original' into the discourse postgres DB (which the recipe's
backupbot pre-hook pg_dumps). This overlay ADDS: the seeded row is intact at backup time. The
backup→restore divergence (dropping the table) is in ops.pre_restore.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
from harness import lifecycle # noqa: E402
def _psql(domain, sql):
cmd = (
'PGPASSWORD=$(cat /run/secrets/db_password) '
f'psql -U discourse -d discourse -tAc "{sql}"'
)
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
def test_backup_captures_state(live_app):
assert _psql(live_app, "SELECT v FROM ci_marker;") == "original", (
"the seeded discourse postgres state was not present at backup time"
)

View File

@ -0,0 +1,26 @@
"""discourse — RESTORE overlay (Phase 1e HC3 / Phase 2 P4): data-integrity, assertion-only.
ops.pre_restore dropped the ci_marker table (diverge); the recipe's restore reloads the pg_dump.
This overlay ADDS: the restored DB carries the pre-mutation 'original' marker — proving the seeded
data survived backup→restore, not just that the service came back up.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
from harness import lifecycle # noqa: E402
def _psql(domain, sql):
cmd = (
'PGPASSWORD=$(cat /run/secrets/db_password) '
f'psql -U discourse -d discourse -tAc "{sql}"'
)
return lifecycle.exec_in_app(domain, ["sh", "-c", cmd], service="db").strip()
def test_restore_returns_state(live_app):
assert _psql(live_app, "SELECT v FROM ci_marker;") == "original", (
"restore did not return the pre-mutation discourse postgres state (data-integrity failure)"
)