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:
@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user