claim(2w): WC5 promote-on-green-cold proven — green cold run advances canonical (1.10.0→1.11.0); --quick never promotes; only cold advances

should_promote_canonical (enrolled+green+cold+latest) + promote_canonical
(re-seed canonical at green-verified latest, snapshot+registry, old known-good
replaced only on green). +5 unit (70 pass). Live: custom-html canonical advanced
1.10.0+1.28.0 → 1.11.0+1.29.0 via a full green cold run; snapshot refreshed; idle;
per-run app torn down. WC6 nightly sweep next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 04:08:14 +01:00
parent cf5999cdda
commit 125453df20
4 changed files with 140 additions and 1 deletions

View File

@ -603,6 +603,42 @@ def run_quick(recipe: str, ref: str | None, head_ref: str | None, repo_local: st
return overall
def should_promote_canonical(recipe: str, ref: str | None, overall: int, quick: bool) -> bool:
"""WC5 gate (pure): a run advances/seeds the canonical iff the recipe is enrolled
(WARM_CANONICAL), the run was GREEN (overall==0), it was COLD (not --quick), and it ran on LATEST
(no PR head → `ref` empty: the nightly sweep or a manual `RECIPE=<r>` run). A PR `!testme` carries
REF=PR-head and must NOT promote the canonical to a PR's code. Only cold-on-latest advances it."""
return canonical.is_enrolled(recipe) and overall == 0 and not quick and not ref
def promote_canonical(recipe: str, head_ref: str | None) -> None:
"""WC5: (re)seed the canonical at the green-verified LATEST. Deploy `warm-<recipe>` at latest
(reattaching the retained canonical volume if one exists — an in-place version bump — else a fresh
install), wait healthy, undeploy, snapshot + record the registry (atomic replace of the
last-known-good). The OLD known-good is replaced ONLY here, after green (never lost on a red run)."""
import warm_reconcile as wr
domain = canonical.canonical_domain(recipe)
wr.fetch_recipe(recipe)
latest = wr.latest_version(wr.recipe_tags(recipe))
if not latest:
print(f"WC5 promote: no version tags for {recipe} — skip", flush=True)
return
meta = _load_meta(recipe)
# The cold run's deploy-count was already asserted + the countfile removed; don't perturb it.
os.environ.pop("CCCI_DEPLOY_COUNT_FILE", None)
print(f"\n===== WC5 promote-on-green-cold: (re)seed canonical {recipe} @ {latest} =====", flush=True)
lifecycle.deploy_app(recipe, domain, version=latest, secrets=True,
deploy_timeout=int(meta.get("DEPLOY_TIMEOUT", 900)))
lifecycle.wait_healthy(domain, ok_codes=tuple(meta["HEALTH_OK"]), path=meta["HEALTH_PATH"],
deploy_timeout=meta["DEPLOY_TIMEOUT"], http_timeout=meta["HTTP_TIMEOUT"])
abra.undeploy(domain)
_wait_undeployed(domain)
canonical.seed_canonical(recipe, latest, commit=head_ref)
print(f"WC5 promote: canonical {recipe} advanced to known-good {latest} (idle, volume retained)",
flush=True)
def main() -> int:
recipe = os.environ.get("RECIPE")
if not recipe:
@ -914,6 +950,18 @@ def main() -> int:
if not results:
print("no tiers ran", file=sys.stderr)
return 1
# WC5 promote-on-green-cold: a GREEN COLD run on LATEST (no PR head) of an enrolled
# (WARM_CANONICAL) recipe advances/seeds the canonical. ONLY cold-on-latest advances it (a PR
# `!testme` carries REF and must NOT promote; `--quick` never promotes — handled in run_quick).
# Non-fatal: a promote failure leaves the OLD known-good intact (never lose it) and is logged.
if should_promote_canonical(recipe, ref, overall, quick=False):
try:
promote_canonical(recipe, head_ref)
except Exception as e: # noqa: BLE001 — promote is a post-green bonus; never fail a green run
print(f"!! WC5 promote failed (non-fatal; known-good unchanged): {_scrub(str(e))}",
flush=True)
return overall