feat(canon): M1.1 tagged-promote gate — canonical only advances to a published release tag
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- should_promote_canonical gains a `tagged` requirement (canon §2.A): a green cold latest run promotes only when the tested head version is a published release tag; an untagged main commit never becomes a canonical. - warm_reconcile.is_released_version(recipe, version): release-tag membership (exact or by version_key). Caller computes `tagged` so the gate stays pure. - unit tests: untagged -> no promote; is_released_version cases. - drive-by (pre-existing reds, unrelated to canon, now green): test_warm_reconcile traefik assertion was stale vs the phase-pxgate spec (probes /api/version, no health_domain); meta.py UPGRADE_BASE_VERSION KEYS help synced to the prevb doc text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
23c02c59b6
commit
27e06289f8
@@ -89,7 +89,7 @@ KEYS: tuple[Key, ...] = (
|
||||
"UPGRADE_BASE_VERSION",
|
||||
"str",
|
||||
None,
|
||||
"Exact published tag overriding the upgrade tier's base (default: `recipe_versions[-2]`).",
|
||||
"Optional explicit override pinning the upgrade tier's base to an exact published tag (rare; for a PR that adds a version *above* the newest tag). When unset (the norm) the base is resolved DYNAMICALLY (phase prevb): last-green (warm canonical) → target-branch (`main`) tip → else skip. See `run_recipe_ci.resolve_upgrade_base` + DECISIONS.",
|
||||
),
|
||||
Key(
|
||||
"BACKUP_VERIFY",
|
||||
|
||||
+16
-6
@@ -907,12 +907,18 @@ def run_quick(
|
||||
return overall
|
||||
|
||||
|
||||
def should_promote_canonical(recipe: str, ref: str | None, overall: int, quick: bool) -> bool:
|
||||
def should_promote_canonical(
|
||||
recipe: str, ref: str | None, overall: int, quick: bool, tagged: 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
|
||||
(WARM_CANONICAL), the run was GREEN (overall==0), it was COLD (not --quick), it ran on LATEST
|
||||
(no PR head → `ref` empty: the nightly sweep or a manual `RECIPE=<r>` run), AND the tested head
|
||||
version corresponds to a published release TAG (`tagged`, phase canon §2.A). A PR `!testme`
|
||||
carries REF=PR-head and must NOT promote to a PR's code. An UNTAGGED head (a `main` commit with
|
||||
no release tag for its version) must never become a canonical — the canonical is always a real
|
||||
release. `tagged` is computed by the caller via warm_reconcile.is_released_version so this gate
|
||||
stays pure. Only cold-on-latest-and-tagged advances it."""
|
||||
return canonical.is_enrolled(recipe) and overall == 0 and not quick and not ref and tagged
|
||||
|
||||
|
||||
def promote_canonical(recipe: str, head_ref: str | None) -> None:
|
||||
@@ -1482,7 +1488,11 @@ def main() -> int:
|
||||
# (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):
|
||||
# canon §2.A tagged-promote gate: only promote when the tested head version is a published
|
||||
# release tag (never an arbitrary untagged `main` commit). head_version is the compose `version`
|
||||
# label of the code under test; is_released_version checks it against the recipe's release tags.
|
||||
tagged = warm_reconcile.is_released_version(recipe, head_version)
|
||||
if should_promote_canonical(recipe, ref, overall, quick=False, tagged=tagged):
|
||||
try:
|
||||
promote_canonical(recipe, head_ref)
|
||||
except Exception as e: # noqa: BLE001 — promote is a post-green bonus; never fail a green run
|
||||
|
||||
@@ -185,6 +185,20 @@ def latest_version(tags) -> str | None:
|
||||
return s[-1] if s else None
|
||||
|
||||
|
||||
def is_released_version(recipe: str, version: str | None) -> bool:
|
||||
"""True iff `version` corresponds to a PUBLISHED RELEASE TAG of the recipe (phase canon §2.A:
|
||||
the canonical may only ever advance to a real release — never an arbitrary untagged `main`
|
||||
commit). Match is exact, or by `version_key` so a re-formatted-but-equal version still counts.
|
||||
A recipe with no release tags, or a `version` that matches no tag, is NOT a release."""
|
||||
if not version:
|
||||
return False
|
||||
tags = recipe_tags(recipe)
|
||||
if version in tags:
|
||||
return True
|
||||
vk = version_key(version)
|
||||
return any(is_version_tag(t) and version_key(t) == vk for t in tags)
|
||||
|
||||
|
||||
def _major(semver: str) -> int:
|
||||
return _numtuple(semver)[0] if semver else 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user