feat(samever): step back to older base when last-green canonical == head version
resolve_upgrade_base now reads the head's published version (abra.head_compose_version, the coop-cloud.<stack>.version label) and, when the last-green warm-canonical version equals it, steps back to the newest published version strictly older than head instead of deploying a same-version no-op. warm_reconcile gains version_key + newest_older_version (single coop-cloud ordering source; sort_versions refactored onto version_key, no behavior change). Skip only when no older published predecessor exists. Step-back returns kind=version so it inherits F1d-2 pinned-tag checkout. Extends tests/unit/test_upgrade_base.py (13 pass).
This commit is contained in:
+45
-5
@@ -42,6 +42,7 @@ from typing import NamedTuple
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "runner"))
|
||||
import warm_reconcile # noqa: E402 (runner/ is on sys.path; owns coop-cloud version ordering)
|
||||
from harness import ( # noqa: E402
|
||||
abra,
|
||||
canonical,
|
||||
@@ -108,11 +109,21 @@ class BasePlan(NamedTuple):
|
||||
return self.kind in ("version", "ref")
|
||||
|
||||
|
||||
def resolve_upgrade_base(stages, meta, recipe: str, head_ref: str | None = None) -> BasePlan:
|
||||
def resolve_upgrade_base(
|
||||
stages, meta, recipe: str, head_ref: str | None = None, head_version: str | None = None
|
||||
) -> BasePlan:
|
||||
"""Dynamic upgrade-base resolution (phase prevb, replaces the static `recipe_versions[-2]`
|
||||
default). Order: explicit override → last-green (warm canonical) → target-branch (main) tip →
|
||||
skip. EXPECTED_NA[upgrade] / upgrade∉stages short-circuit to a declared skip first.
|
||||
|
||||
`head_version` is the head checkout's published version (the `coop-cloud.<stack>.version` label;
|
||||
see abra.head_compose_version). When the last-green warm-canonical version EQUALS it, deploying
|
||||
the canonical as the base would be a vacuous same-version no-op, so the resolver STEPS BACK to the
|
||||
newest published version strictly older than the head (phase samever) — the upgrade tier always
|
||||
crosses a real version delta. This is the nightly STEADY STATE: a green cold-on-latest run promotes
|
||||
canonical→latest, so the next night finds canonical == head and must step back. Skip only when no
|
||||
older published predecessor exists.
|
||||
|
||||
last-green is the PRIMARY base — the version cc-ci last recorded green for this recipe (the
|
||||
warm-canonical registry record). main-tip is the FALLBACK: the recipe repo's `main` HEAD, the
|
||||
real predecessor the PR merges on top of, used when there is no last-green. Else the tier is
|
||||
@@ -135,11 +146,37 @@ def resolve_upgrade_base(stages, meta, recipe: str, head_ref: str | None = None)
|
||||
return BasePlan("version", override, None, "explicit UPGRADE_BASE_VERSION override")
|
||||
rec = canonical.read_registry(recipe)
|
||||
if rec and rec.get("version"):
|
||||
canon = rec["version"]
|
||||
same = head_version is not None and warm_reconcile.version_key(
|
||||
canon
|
||||
) == warm_reconcile.version_key(head_version)
|
||||
if not same:
|
||||
# canonical ≠ head version (the common version-bump PR / nightly-with-new-version case):
|
||||
# the green-verified primary base, unchanged from prevb.
|
||||
return BasePlan(
|
||||
"version",
|
||||
canon,
|
||||
None,
|
||||
f"last-green (warm canonical, status={rec.get('status')})",
|
||||
)
|
||||
# canonical == head version → deploying it would be a same-version no-op. Step back to the
|
||||
# newest published version strictly older than the head (phase samever).
|
||||
older = warm_reconcile.newest_older_version(
|
||||
warm_reconcile.recipe_tags(recipe), head_version
|
||||
)
|
||||
if older:
|
||||
return BasePlan(
|
||||
"version",
|
||||
older,
|
||||
None,
|
||||
f"step-back: last-green canonical ({canon}) == head version {head_version}; "
|
||||
f"newest older published base",
|
||||
)
|
||||
return BasePlan(
|
||||
"version",
|
||||
rec["version"],
|
||||
"skip",
|
||||
None,
|
||||
f"last-green (warm canonical, status={rec.get('status')})",
|
||||
None,
|
||||
f"base == head ({head_version}) and no older published predecessor",
|
||||
)
|
||||
main_tip = lifecycle.recipe_branch_commit(recipe, "main")
|
||||
if main_tip and main_tip != head_ref:
|
||||
@@ -983,7 +1020,10 @@ def main() -> int:
|
||||
|
||||
domain = naming.app_domain(recipe, os.environ.get("PR", "0"), ref)
|
||||
|
||||
base_plan = resolve_upgrade_base(stages, meta, recipe, head_ref=head_ref)
|
||||
head_version = abra.head_compose_version(recipe)
|
||||
base_plan = resolve_upgrade_base(
|
||||
stages, meta, recipe, head_ref=head_ref, head_version=head_version
|
||||
)
|
||||
prev = base_plan.runs # gates the upgrade tier
|
||||
# base deploy target: a pinned published version (kind=version) or main-tip commit (kind=ref);
|
||||
# on skip fall back to the run's VERSION/head (target=None → chaos head deploy, as before).
|
||||
|
||||
Reference in New Issue
Block a user