From 4bf9e1d43d27c8bc7e7560811b68a7eb47a772c8 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Sun, 31 May 2026 05:07:44 +0000 Subject: [PATCH] feat(mumble F2-14c): drop cc-ci compose.host-ports.yml fork; deploy 0.2.0 base minimally, add native host-ports on upgrade-to-latest via new UPGRADE_EXTRA_ENV harness hook + COMPOSE_FILE-aware READY_PROBE/install skip --- runner/harness/abra.py | 23 ++++++++++- runner/harness/generic.py | 13 +++++- runner/harness/lifecycle.py | 8 ++-- runner/run_recipe_ci.py | 2 +- tests/mumble/compose.host-ports.yml | 19 --------- tests/mumble/install_steps.sh | 29 -------------- tests/mumble/recipe_meta.py | 62 +++++++++++++++++++---------- tests/mumble/test_install.py | 25 ++++++++++-- 8 files changed, 101 insertions(+), 80 deletions(-) delete mode 100644 tests/mumble/compose.host-ports.yml delete mode 100644 tests/mumble/install_steps.sh diff --git a/runner/harness/abra.py b/runner/harness/abra.py index f90cd16..a17d24f 100644 --- a/runner/harness/abra.py +++ b/runner/harness/abra.py @@ -81,8 +81,8 @@ def recipe_checkout(recipe: str, version: str) -> None: path = os.path.expanduser(f"~/.abra/recipes/{recipe}") # -f (force): the version-pinning checkout must yield the EXACT ref tree. Without it, a cc-ci - # install_steps-provided overlay (e.g. mumble's compose.host-ports.yml, copied into a version that - # predates it) is an UNTRACKED file that collides with the same path TRACKED in a later ref, and + # install_steps-provided overlay (e.g. discourse's compose.ccci.yml, copied into the pinned base) + # is an UNTRACKED file that collides with the same path TRACKED in a later ref, and # `git checkout ` aborts ("untracked working tree files would be overwritten"). Force resolves # it by writing the ref's tracked version. Safe: we never want local recipe-tree state preserved # across a version switch (and chaos deploys re-provide the overlay via install_steps when needed). @@ -137,6 +137,25 @@ def env_set(domain: str, key: str, value: str) -> None: fh.write("\n".join(out) + "\n") +def env_get(domain: str, key: str) -> str | None: + """Read a key from the app's .env (last uncommented assignment wins). None if absent. Symmetric + with env_set; abra has no getter. Strips surrounding quotes from the value.""" + import os + import re + + path = os.path.expanduser(f"~/.abra/servers/default/{domain}.env") + if not os.path.exists(path): + return None + pat = re.compile(rf"^\s*{re.escape(key)}=(.*)$") + val = None + with open(path) as fh: + for ln in fh.read().splitlines(): + m = pat.match(ln) + if m: + val = m.group(1).strip().strip('"').strip("'") + return val + + def secret_generate(domain: str, timeout: int = 300) -> None: # -m avoids the TTY/table (ioctl) path; output (which contains the generated values) is # captured by _run and never logged. -C -o keep the recipe at the PR checkout (without -o it diff --git a/runner/harness/generic.py b/runner/harness/generic.py index b714a33..216b1ae 100644 --- a/runner/harness/generic.py +++ b/runner/harness/generic.py @@ -18,7 +18,7 @@ import socket import ssl import time -from . import lifecycle +from . import abra, lifecycle # A recipe is backup-capable iff a compose file carries a truthy backupbot.backup label. _BACKUPBOT_RE = re.compile(r"backupbot\.backup\b[^\n]*\btrue\b", re.IGNORECASE) @@ -244,6 +244,17 @@ def perform_upgrade( before = lifecycle.deployed_identity(domain) if head_ref: lifecycle.recipe_checkout_ref(recipe, head_ref) + # UPGRADE_EXTRA_ENV (F2-14c): a recipe may need different app .env for the upgrade-TARGET deploy + # than for the base — e.g. mumble's `compose.host-ports.yml` overlay exists ONLY in the newer + # (target) version, so the base deploys minimally WITHOUT it and the upgrade adds it to COMPOSE_FILE + # here, after the PR-head checkout (which ships the overlay) and before the chaos redeploy that + # picks up the new .env. Dict or callable(domain)->dict. No-op for recipes without it. + upgrade_env = meta.get("UPGRADE_EXTRA_ENV") or {} + if callable(upgrade_env): + upgrade_env = upgrade_env(domain) or {} + for k, v in upgrade_env.items(): + print(f" upgrade-env: {k}={v}", flush=True) + abra.env_set(domain, k, v) # HQ1: warm the NEW-version image set before the chaos redeploy (the head_ref checkout's pinned # tags) so a pull failure is a clear pre-deploy error and convergence isn't pull-bound. lifecycle.prepull_images(recipe, domain) diff --git a/runner/harness/lifecycle.py b/runner/harness/lifecycle.py index cc0d999..f7a168c 100644 --- a/runner/harness/lifecycle.py +++ b/runner/harness/lifecycle.py @@ -231,10 +231,10 @@ def deploy_app( flush=True, ) chaos = True - # A recipe may force a chaos base deploy via recipe_meta CHAOS_BASE_DEPLOY=True when cc-ci adds - # an untracked compose overlay to the recipe checkout (e.g. mumble's host-ports.yml, provided - # by install_steps for older versions that predate it). The untracked file makes abra's - # pinned-deploy clean-tree check FATA ('has locally unstaged changes'); chaos skips lint + + # A recipe may force a chaos base deploy via recipe_meta CHAOS_BASE_DEPLOY=True when an + # install_steps hook adds an untracked compose overlay to the recipe checkout (e.g. discourse's + # compose.ccci.yml, provided by install_steps for the pinned base). The untracked file makes + # abra's pinned-deploy clean-tree check FATA ('has locally unstaged changes'); chaos skips lint + # the clean-tree gate and deploys the EXPLICITLY-checked-out pinned version (we already ran # recipe_checkout(version) above) — NOT latest. Same mechanism as the lightweight-tag branch. elif _recipe_meta_flag(recipe, "CHAOS_BASE_DEPLOY"): diff --git a/runner/run_recipe_ci.py b/runner/run_recipe_ci.py index 93f7705..31f8a50 100644 --- a/runner/run_recipe_ci.py +++ b/runner/run_recipe_ci.py @@ -194,7 +194,7 @@ def _load_meta(recipe: str) -> dict: ns: dict = {} with open(path) as fh: exec(compile(fh.read(), path, "exec"), ns) # noqa: S102 (trusted, in-repo) - for k in list(meta) + ["BACKUP_CAPABLE", "SKIP_GENERIC", "OIDC_AT_INSTALL", "READY_PROBE", "UPGRADE_BASE_VERSION", "BACKUP_VERIFY"]: + for k in list(meta) + ["BACKUP_CAPABLE", "SKIP_GENERIC", "OIDC_AT_INSTALL", "READY_PROBE", "UPGRADE_BASE_VERSION", "BACKUP_VERIFY", "UPGRADE_EXTRA_ENV"]: if k in ns: meta[k] = ns[k] return meta diff --git a/tests/mumble/compose.host-ports.yml b/tests/mumble/compose.host-ports.yml deleted file mode 100644 index dd246ee..0000000 --- a/tests/mumble/compose.host-ports.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- -# cc-ci-owned copy of the upstream mumble `compose.host-ports.yml` overlay (identical content). -# Provided to the recipe checkout by tests/mumble/install_steps.sh so that 64738 is published on the -# cc-ci host for EVERY version under test — the upstream overlay only exists from recipe version -# 1.0.0+, but the upgrade tier's base deploy is the previous published version (0.2.0+), which -# predates it. On-host tests (cc-ci-run) reach the voice server at 127.0.0.1:64738 via this publish. -version: "3.8" - -services: - app: - ports: - - target: 64738 - published: 64738 - protocol: tcp - mode: host - - target: 64738 - published: 64738 - protocol: udp - mode: host diff --git a/tests/mumble/install_steps.sh b/tests/mumble/install_steps.sh deleted file mode 100644 index 1074fa8..0000000 --- a/tests/mumble/install_steps.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# mumble — INSTALL-TIME hook (Phase 2 Q4.2). Runs during the install tier AFTER `abra app new` + -# EXTRA_ENV + `abra app secret generate` and BEFORE the single `abra app deploy` -# (lifecycle.py::_run_install_steps), with CCCI_RECIPE / CCCI_APP_DOMAIN / CCCI_APP_ENV in env. -# -# Purpose: guarantee `compose.host-ports.yml` exists in the recipe checkout for EVERY version under -# test. mumble's voice server speaks a non-HTTP TLS protocol on 64738; cc-ci's tests run on-host -# (cc-ci-run) and reach it at 127.0.0.1:64738 via a host-published port. The upstream recipe ships -# compose.host-ports.yml only from version 1.0.0+, but the upgrade tier's base deploy is the previous -# published version (0.2.0+), which predates it — so EXTRA_ENV's COMPOSE_FILE (which references the -# overlay) would fail to resolve on that base deploy. We provide an identical overlay here so the -# overlay is present whether the checked-out version ships it natively (no-op) or not (copied). -set -euo pipefail - -: "${CCCI_RECIPE:?missing CCCI_RECIPE}" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -RECIPE_DIR="${HOME}/.abra/recipes/${CCCI_RECIPE}" - -if [ ! -d "$RECIPE_DIR" ]; then - echo " mumble install_steps: recipe dir $RECIPE_DIR missing — cannot provide host-ports overlay" >&2 - exit 1 -fi - -if [ -f "$RECIPE_DIR/compose.host-ports.yml" ]; then - echo " mumble install_steps: compose.host-ports.yml already present (native to this version)" -else - cp "$SCRIPT_DIR/compose.host-ports.yml" "$RECIPE_DIR/compose.host-ports.yml" - echo " mumble install_steps: provided compose.host-ports.yml to recipe checkout (${CCCI_RECIPE})" -fi diff --git a/tests/mumble/recipe_meta.py b/tests/mumble/recipe_meta.py index eaed723..1b99031 100644 --- a/tests/mumble/recipe_meta.py +++ b/tests/mumble/recipe_meta.py @@ -1,28 +1,33 @@ # Per-recipe harness config for mumble (Phase 2 Q4.2 — a TCP/voice recipe, not HTTP-native). # # Mumble's voice server speaks its own TLS protocol on 64738 (no HTTP API). To fit cc-ci's -# HTTP-readiness + on-host test model we deploy two recipe overlays: +# HTTP-readiness + on-host test model we deploy upstream recipe overlays: # - compose.mumbleweb.yml -> a mumble-web HTTP client routed through Traefik on the app domain, # giving the generic harness a real HTTP readiness/serving signal (HEALTH_PATH "/") AND the -# web_client.py parity surface. -# - compose.host-ports.yml -> publishes 64738 (tcp+udp) directly on the cc-ci host (mode: host). -# Tests run on-host (cc-ci-run), so the protocol tests connect to 127.0.0.1:64738. -# Both overlays are shipped by the upstream recipe; this is a documented deployment mode, not a fork. +# web_client.py parity surface. Shipped upstream from 0.1.0 (present on the 0.2.0 base too). +# - compose.host-ports.yml -> publishes 64738 (tcp+udp, mode:host) on the cc-ci host so on-host +# tests (cc-ci-run) reach the voice server at 127.0.0.1:64738. Shipped upstream ONLY from 1.0.0. +# +# F2-14c disposition (Adversary REVIEW-2 @16:22:07Z VETO): the upgrade tier's base is the previous +# published version 0.2.0+v1.6.870-0, which PREDATES compose.host-ports.yml (added in 1.0.0). We do +# NOT carry a cc-ci copy of that upstream overlay (no fork). Instead: +# - the BASE 0.2.0 deploys MINIMALLY with `compose.yml:compose.mumbleweb.yml` (HTTP health via +# mumble-web works; the voice port is NOT host-published on 0.2.0), and the on-host voice/protocol +# custom tests are SKIPPED on 0.2.0 (they run in the CUSTOM tier, which executes once on the +# post-upgrade LATEST); +# - the UPGRADE to latest (1.0.0+, which ships compose.host-ports.yml NATIVELY) adds host-ports to +# COMPOSE_FILE via UPGRADE_EXTRA_ENV (applied by generic.perform_upgrade after the PR-head +# checkout, before the chaos redeploy), so the voice port IS host-published on latest and the +# voice tests run there. The current version's native overlay is untouched (no cc-ci fork). # # Distinctive config markers (read back by the recipe-specific functional tests, proving our config # actually propagated into the running server — version-independent, not hard-coded upstream values): # WELCOME_TEXT -> MUMBLE_CONFIG_WELCOMETEXT, surfaced in the ServerSync welcome_text. # USERS -> MUMBLE_CONFIG_USERS (max users), surfaced in the ServerConfig.max_users. -HEALTH_PATH = "/" # mumble-web client UI +HEALTH_PATH = "/" # mumble-web client UI (present on both 0.2.0 base and 1.0.0 latest) HEALTH_OK = (200,) -# install_steps.sh provides compose.host-ports.yml to recipe versions that predate it (the upgrade -# tier's base deploy is the previous published version, 0.2.0+, which lacks the upstream overlay). -# That untracked file makes abra's PINNED base-deploy clean-tree check FATA, so deploy the -# explicitly-checked-out pinned version with chaos (skips lint/clean-tree; deploys the version, not -# LATEST). No-op for the upgrade tier (already a PR-head chaos redeploy). See DECISIONS.md. -CHAOS_BASE_DEPLOY = True DEPLOY_TIMEOUT = 900 # two images to pull (mumble-server + mumble-web) on a cold node HTTP_TIMEOUT = 300 @@ -31,19 +36,34 @@ WELCOME_TEXT_MARKER = "cc-ci-mumble-welcome-7f3a9c" # A distinctive max-users value (not the recipe default 100) the server_config test asserts. MAX_USERS = 42 +# BASE deploy (0.2.0): mumble-web only — NO host-ports (0.2.0 predates it). The voice-config env is +# set here and persists across the upgrade so it takes effect on the latest (where the custom config +# round-trip tests assert it). EXTRA_ENV = { - "COMPOSE_FILE": "compose.yml:compose.mumbleweb.yml:compose.host-ports.yml", + "COMPOSE_FILE": "compose.yml:compose.mumbleweb.yml", "WELCOME_TEXT": WELCOME_TEXT_MARKER, "USERS": str(MAX_USERS), } +# UPGRADE-target deploy (latest 1.0.0+): add the NATIVE compose.host-ports.yml so 64738 is +# host-published and the on-host voice/protocol custom tests can run on latest. +UPGRADE_EXTRA_ENV = { + "COMPOSE_FILE": "compose.yml:compose.mumbleweb.yml:compose.host-ports.yml", +} + def READY_PROBE(domain): - # HEALTH_PATH "/" only proves the mumble-web HTTP sidecar; it does NOT reflect the voice server. - # After a chaos upgrade redeploy the host-mode 64738 port must be released by the old task and - # rebound by the new one — a window where the app (voice) container isn't yet serving while - # mumble-web still returns 200. backup-bot then execs its sqlite pre-hook into a not-running app - # container → 409. Gate readiness on the voice port being STABLY listening (3 consecutive - # connects) before the harness proceeds to the backup tier. The port is host-published - # (compose.host-ports.yml), so we probe it on the cc-ci host where the run executes. - return [{"tcp_host": "127.0.0.1", "tcp_port": 64738, "stable": 3}] + # The voice server on 64738 is testable on-host ONLY when compose.host-ports.yml is active — i.e. + # the post-upgrade LATEST, not the minimal 0.2.0 base. Read the live COMPOSE_FILE to decide, so the + # SAME probe fn is correct in both phases: the post-install probe (base, no host-ports) returns [] + # (HTTP health alone gates the base), the post-upgrade probe (latest, host-ports) gates readiness + # on the voice port being STABLY listening (3 consecutive connects) before the harness proceeds to + # backup — after the chaos upgrade redeploy the host-mode 64738 must be released by the old task and + # rebound by the new one (a window where mumble-web 200s while the voice container isn't yet up, and + # backup-bot would then exec into a not-running app container -> 409). + from harness import abra # lazy: recipe_meta is exec'd with `harness` importable at call time + + cf = abra.env_get(domain, "COMPOSE_FILE") or "" + if "compose.host-ports.yml" in cf: + return [{"tcp_host": "127.0.0.1", "tcp_port": 64738, "stable": 3}] + return [] diff --git a/tests/mumble/test_install.py b/tests/mumble/test_install.py index ab24c65..b6d7185 100644 --- a/tests/mumble/test_install.py +++ b/tests/mumble/test_install.py @@ -2,16 +2,35 @@ install tier (which proves the mumble-web HTTP sidecar serves over Traefik — the readiness signal). This overlay ADDS the assertion that mumble's actual purpose — the voice server — is up: the murmur -control channel accepts a TLS connection on the host-published 64738 right after install. (The full -protocol handshake + channel presence is exercised in the custom tier; here we assert the install -produced a listening voice server, not only a web UI.) +control channel accepts a TLS connection on the host-published 64738. + +F2-14c: the install tier runs against the upgrade BASE, which is the previous published version +0.2.0+v1.6.870-0. That version PREDATES compose.host-ports.yml (added upstream in 1.0.0), so the base +deploys minimally without it and the voice port is NOT host-published — this on-host voice check is then +not applicable on the base and is SKIPPED (recorded). The voice server is asserted listening on the +post-upgrade LATEST via the READY_PROBE (tcp 3x, gates backup) and the custom-tier full TLS protocol +handshake. When this overlay runs against a host-ports deploy (latest), it asserts the listening server. """ +import os import socket +import sys import time +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) +from harness import abra # noqa: E402 + def test_voice_server_listening(live_app): + cf = abra.env_get(live_app, "COMPOSE_FILE") or "" + if "compose.host-ports.yml" not in cf: + pytest.skip( + "upgrade base (0.2.0) predates compose.host-ports.yml (added in 1.0.0) → voice port not " + "host-published; voice listening asserted on post-upgrade latest (READY_PROBE tcp 3x + " + "custom-tier protocol handshake)" + ) deadline = time.time() + 120 last_err = None while time.time() < deadline: