From 0aa46dbe728fc361bc6a8ccac41593d564898462 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Thu, 11 Jun 2026 22:03:29 +0000 Subject: [PATCH] =?UTF-8?q?fix(drone-dep):=20ADV-drone-02=20=E2=80=94=20te?= =?UTF-8?q?ardown=20fallback=20when=20SSO=20enrichment=20fails=20after=20d?= =?UTF-8?q?eploy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When _enrich_deps_with_sso raises after deploy_deps succeeds (e.g., gitea API call fails), deps_state stays {} and the finally block's `if deps_state:` guard skips teardown, orphaning the dep at its deterministic domain. Fix: add an `else` branch after the `if deps_state:` block that reads $CCCI_DEPS_FILE (the legacy-list written by deploy_deps) and calls teardown_deps on the cold entries so no dep is left running. Unit tests: test_load_run_state_provides_fallback_for_enrichment_failure and test_fallback_skips_warm_entries verify the data-flow that the fallback relies on. 19/19 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 --- runner/run_recipe_ci.py | 17 +++++++++++++++ tests/unit/test_deps.py | 47 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/runner/run_recipe_ci.py b/runner/run_recipe_ci.py index 306dad0..40b6cf0 100644 --- a/runner/run_recipe_ci.py +++ b/runner/run_recipe_ci.py @@ -1221,6 +1221,23 @@ def main() -> int: except lifecycle.TeardownError as e: dep_teardown_error = str(e) print(f"!! {dep_teardown_error}", flush=True) + else: + # ADV-drone-02 fix: deps_state is empty (enrichment failed after a successful + # deploy_deps call). The raw deployed list is still in $CCCI_DEPS_FILE — read it + # and tear down any cold deps so they don't orphan at their deterministic domain. + raw = deps_mod.load_run_state() + if raw: + cold_raw = [ + e + for e in (raw if isinstance(raw, list) else list(raw.values())) + if isinstance(e, dict) and not e.get("warm") + ] + if cold_raw: + print( + "\n===== DEPS teardown (enrichment-failure fallback) =====", flush=True + ) + with contextlib.suppress(lifecycle.TeardownError): + deps_mod.teardown_deps(cold_raw) # ---- deploy-count assertion (DG4.1) ---- with open(countfile) as f: diff --git a/tests/unit/test_deps.py b/tests/unit/test_deps.py index 1a99a7e..1d2c83d 100644 --- a/tests/unit/test_deps.py +++ b/tests/unit/test_deps.py @@ -79,3 +79,50 @@ def test_write_run_state_no_env_is_noop(monkeypatch): require setting up the env).""" monkeypatch.delenv("CCCI_DEPS_FILE", raising=False) deps.write_run_state([{"recipe": "x", "domain": "y"}]) # must not raise + + +# --------------------------------------------------------------------------- +# ADV-drone-02 fallback: load_run_state provides teardown data when deps_state={} +# --------------------------------------------------------------------------- + + +def test_load_run_state_provides_fallback_for_enrichment_failure(tmp_path, monkeypatch): + """ADV-drone-02: deploy_deps writes legacy-list to $CCCI_DEPS_FILE; if _enrich_deps_with_sso + raises, deps_state = {} in main(). The fallback in the finally block reads load_run_state() + to recover the deployed-but-unenriched entries for teardown. + + This test verifies that load_run_state() returns the entries written by deploy_deps (legacy + list shape) so the fallback teardown receives the correct cold entries.""" + state_path = tmp_path / "deps.json" + monkeypatch.setenv("CCCI_DEPS_FILE", str(state_path)) + # deploy_deps writes the legacy list shape (before enrichment converts to dict) + deployed_by_deploy_deps = [ + {"recipe": "gitea", "domain": "gite-aabbcc.ci.commoninternet.net"}, + ] + deps.write_run_state(deployed_by_deploy_deps) + + raw = deps.load_run_state() + assert isinstance(raw, list), "load_run_state must return the legacy list shape" + assert len(raw) == 1 + cold_raw = [e for e in raw if isinstance(e, dict) and not e.get("warm")] + assert len(cold_raw) == 1 + assert cold_raw[0]["recipe"] == "gitea" + assert cold_raw[0]["domain"] == "gite-aabbcc.ci.commoninternet.net" + + +def test_fallback_skips_warm_entries(tmp_path, monkeypatch): + """ADV-drone-02 fallback filtering: warm entries (keycloak live-warm provider) must NOT be + undeployed — only cold entries should be in the teardown list.""" + state_path = tmp_path / "deps.json" + monkeypatch.setenv("CCCI_DEPS_FILE", str(state_path)) + mixed = [ + {"recipe": "keycloak", "domain": "keyc-live.ci.commoninternet.net", "warm": True}, + {"recipe": "gitea", "domain": "gite-aabbcc.ci.commoninternet.net"}, + ] + deps.write_run_state(mixed) + + raw = deps.load_run_state() + cold_raw = [e for e in (raw if isinstance(raw, list) else list(raw.values())) + if isinstance(e, dict) and not e.get("warm")] + assert len(cold_raw) == 1 + assert cold_raw[0]["recipe"] == "gitea"