fix(drone-dep): ADV-drone-02 — teardown fallback when SSO enrichment fails after deploy
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
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 <noreply@anthropic.com>
This commit is contained in:
@ -1221,6 +1221,23 @@ def main() -> int:
|
|||||||
except lifecycle.TeardownError as e:
|
except lifecycle.TeardownError as e:
|
||||||
dep_teardown_error = str(e)
|
dep_teardown_error = str(e)
|
||||||
print(f"!! {dep_teardown_error}", flush=True)
|
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) ----
|
# ---- deploy-count assertion (DG4.1) ----
|
||||||
with open(countfile) as f:
|
with open(countfile) as f:
|
||||||
|
|||||||
@ -79,3 +79,50 @@ def test_write_run_state_no_env_is_noop(monkeypatch):
|
|||||||
require setting up the env)."""
|
require setting up the env)."""
|
||||||
monkeypatch.delenv("CCCI_DEPS_FILE", raising=False)
|
monkeypatch.delenv("CCCI_DEPS_FILE", raising=False)
|
||||||
deps.write_run_state([{"recipe": "x", "domain": "y"}]) # must not raise
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user