From 83c183d9854545eb06d48e3e351e6e715b6a89a1 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Wed, 17 Jun 2026 12:31:53 +0000 Subject: [PATCH] =?UTF-8?q?feat(canon):=20=C2=A72.G=20strip=20UPGRADE=5FBA?= =?UTF-8?q?SE=5FVERSION=20entirely=20(plausible=20verified=20dynamic-base?= =?UTF-8?q?=20green)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate satisfied — live: with the pin removed, plausible's upgrade tier resolves base 3.0.1+v2.0.0 via the same-version step-back (canonical 3.1.0 == head 3.1.0 → newest-older = 3.0.1, NOT the broken 3.0.0) and passes install+upgrade green (level 5/5). The pin is redundant, so removed everywhere: - meta.py KEYS entry (RecipeMeta field auto-drops; 15→14 keys). - run_recipe_ci.resolve_upgrade_base override branch + docstrings. - tests/unit/test_meta.py (count 15→14, dropped None-assert), test_upgrade_base.py (override test). - docs/recipe-customization.md (regenerated table + mentions), docs/testing.md. - tests/plausible/recipe_meta.py (pin removed), tests/bluesky-pds (re-enable note → dynamic base). 294 unit tests pass; lint clean. Co-Authored-By: Claude Opus 4.8 --- docs/recipe-customization.md | 5 ++--- docs/testing.md | 3 ++- runner/harness/meta.py | 6 ------ runner/run_recipe_ci.py | 16 +++++++------- tests/bluesky-pds/recipe_meta.py | 7 ++++--- tests/unit/test_meta.py | 7 +++---- tests/unit/test_upgrade_base.py | 36 +++++++++----------------------- 7 files changed, 28 insertions(+), 52 deletions(-) diff --git a/docs/recipe-customization.md b/docs/recipe-customization.md index edba963..d8cc89b 100644 --- a/docs/recipe-customization.md +++ b/docs/recipe-customization.md @@ -20,7 +20,7 @@ A recipe customizes its CI through **three distinct mechanisms**: | Surface | Form | Examples | |---|---|---| -| **Declarative settings** | Python assignments in `tests//recipe_meta.py` | `DEPLOY_TIMEOUT = 1500`, `UPGRADE_BASE_VERSION = "2.3.1+..."` | +| **Declarative settings** | Python assignments in `tests//recipe_meta.py` | `DEPLOY_TIMEOUT = 1500`, `HEALTH_PATH = "/api/health"` | | **Code hooks** | Callables in `recipe_meta.py`, `ops.py` functions, one shell hook | `def READY_PROBE(ctx): ...`, `pre_upgrade(ctx)`, `install_steps.sh` | | **File presence** | A file existing at a discovered path changes behavior | `test_upgrade.py` overlay, `custom/test_*.py`, `compose.ccci.yml` | @@ -122,7 +122,6 @@ _This table is GENERATED from the `runner/harness/meta.py` KEYS registry by `scr | `BACKUP_CAPABLE` | `bool` | `None` | Override the backup-tier capability auto-detect (compose `backupbot.backup` labels). `False` forces an intentional skip of the backup/restore rung; `True` forces the tier on; unset = auto-detect. | | `EXPECTED_NA` | `dict` | `None` | Declare a non-run rung an INTENTIONAL skip: `{rung: reason}` — the level climbs past it; an undeclared non-run rung is *unverified* and blocks the level above it (classification table: machine-docs/DECISIONS.md phase lvl5). Never overrides an exercised pass/fail; the `lint` rung has no escape hatch. Declaring `upgrade` also suppresses the upgrade-tier BASE deploy — the single deploy is the PR head itself — for recipes whose published versions exist but are genuinely undeployable (phase bsky). | | `READY_PROBE` | `hook` | `None` | Callable `(ctx) -> [probe, ...]` returning extra readiness probes, run after install AND after upgrade: HTTP `{host, path, ok}` or TCP `{tcp_host, tcp_port, stable}`. | -| `UPGRADE_BASE_VERSION` | `str` | `None` | 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. | | `BACKUP_VERIFY` | `hook` | `None` | Callable `(ctx) -> bool` post-backup data-capture check; `False` re-runs the backup (truncated-dump race guard), retried up to 3 attempts. | | `UPGRADE_EXTRA_ENV` | `dict_or_hook` | `None` | Extra `.env` keys applied after the PR-head checkout, before the chaos redeploy (env that exists only at head). Dict, or callable `(ctx) -> dict`. | | `EXTRA_ENV` | `dict_or_hook` | `{}` | Extra `.env` keys applied at EVERY deploy (base install AND upgrade old-app). Dict, or callable `(ctx) -> dict` deriving values from the per-run domain (`ctx.domain`). | @@ -292,7 +291,7 @@ One deploy chain per run (full detail: `docs/testing.md` §2): ``` [DEPS? provision deps FIRST → $CCCI_DEPS_FILE] -deploy BASE (dynamic: last-green → main-tip → skip, or UPGRADE_BASE_VERSION override; EXTRA_ENV; +deploy BASE (dynamic: last-green → same-version step-back → main-tip → skip; EXTRA_ENV; install_steps.sh; compose.ccci.yml [environmental] auto-copied + auto-chaos; tests//previous/ [version-specific, base-ONLY] applied if it matches the base) → INSTALL tier (READY_PROBE; generic + overlay asserts) diff --git a/docs/testing.md b/docs/testing.md index beba673..cf7a6db 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -206,7 +206,8 @@ Concretely, the upgrade tier: canonical, pinned-tag deploy) → else the target-branch `main` tip (chaos deploy of the branch HEAD — the real predecessor the PR merges onto) → else the upgrade tier is skipped. An optional `tests//previous/` supplies version-specific repair to the base ONLY (stripped before the - head redeploy). `UPGRADE_BASE_VERSION` may still pin an explicit tag override. + head redeploy). (The old explicit `UPGRADE_BASE_VERSION` pin was removed in phase canon §2.G — the + dynamic last-green/step-back resolution makes it redundant.) 2. orchestrator captures `head_ref` (preferring `$REF` — the PR head sha; falls back to the recipe checkout HEAD for non-PR `!testme`). 3. on the upgrade tier: re-checkout the recipe to `head_ref` (the prev-tag base deploy reset the diff --git a/runner/harness/meta.py b/runner/harness/meta.py index 9363024..947af37 100644 --- a/runner/harness/meta.py +++ b/runner/harness/meta.py @@ -85,12 +85,6 @@ KEYS: tuple[Key, ...] = ( "Callable `(ctx) -> [probe, ...]` returning extra readiness probes, run after install AND after upgrade: HTTP `{host, path, ok}` or TCP `{tcp_host, tcp_port, stable}`.", hook_params=("ctx",), ), - Key( - "UPGRADE_BASE_VERSION", - "str", - None, - "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", "hook", diff --git a/runner/run_recipe_ci.py b/runner/run_recipe_ci.py index dfa49c0..3a19c9d 100644 --- a/runner/run_recipe_ci.py +++ b/runner/run_recipe_ci.py @@ -92,8 +92,8 @@ def sso_dep_unverified(declared, deps_ready: bool, requires_deps_skipped: int) - class BasePlan(NamedTuple): """Resolved upgrade-base decision (phase prevb). `kind`: - - "version" → deploy a pinned published version (`version`): an explicit UPGRADE_BASE_VERSION - override, or last-green (warm-canonical) version. `previous/` may apply (version-guarded). + - "version" → deploy a pinned published version (`version`): the last-green (warm-canonical) + version (or its step-back). `previous/` may apply (version-guarded). - "ref" → deploy the target-branch (main) tip at commit `ref` (chaos): the true predecessor the PR merges onto, used when there is no last-green. `previous/` never applies to a ref base. - "skip" → no upgrade base; the single deploy is the PR head and the upgrade tier records a @@ -113,8 +113,8 @@ 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. + default). Order: last-green (warm canonical, with same-version step-back) → 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..version` label; see abra.head_compose_version). When the last-green warm-canonical version EQUALS it, deploying @@ -129,8 +129,9 @@ def resolve_upgrade_base( real predecessor the PR merges on top of, used when there is no last-green. Else the tier is skipped with a recorded reason (structural, declared — not a silent pass). - `UPGRADE_BASE_VERSION` is RETAINED as an optional explicit override (wins when set) for the rare - PR-adds-a-version-above-the-newest-tag case; it is no longer the default (DECISIONS prevb).""" + The old `UPGRADE_BASE_VERSION` explicit-override knob was REMOVED in phase canon (§2.G): the + dynamic last-green/step-back resolution makes it redundant (its only remaining user, plausible, + now resolves base 3.0.1 via step-back once its canonical is established). See DECISIONS.""" if "upgrade" not in stages: return BasePlan("skip", None, None, "upgrade tier not in requested stages") declared = (meta.EXPECTED_NA or {}).get("upgrade") @@ -141,9 +142,6 @@ def resolve_upgrade_base( flush=True, ) return BasePlan("skip", None, None, f"declared EXPECTED_NA[upgrade]: {declared}") - override = getattr(meta, "UPGRADE_BASE_VERSION", None) - if override: - return BasePlan("version", override, None, "explicit UPGRADE_BASE_VERSION override") rec = canonical.read_registry(recipe) if rec and rec.get("version"): canon = rec["version"] diff --git a/tests/bluesky-pds/recipe_meta.py b/tests/bluesky-pds/recipe_meta.py index 5fe3766..73eabf1 100644 --- a/tests/bluesky-pds/recipe_meta.py +++ b/tests/bluesky-pds/recipe_meta.py @@ -13,12 +13,13 @@ HTTP_TIMEOUT = 600 # as an upgrade base anymore: the base crash-loops MODULE_NOT_FOUND before the PR head is ever # exercised (phase bsky root cause; cc-ci-plan/upstream/bluesky-pds.md). Declared intentional # until a fixed exact-pinned version (0.3.0+v0.4.219, mirror PR #2) is merged AND published — -# then DROP this and set UPGRADE_BASE_VERSION = "0.3.0+v0.4.219" so the upgrade rung is -# exercised again from the first deployable base. +# then DROP this EXPECTED_NA and the upgrade rung resolves its base DYNAMICALLY (phase prevb: +# last-green warm canonical → same-version step-back → main tip), no explicit pin needed +# (UPGRADE_BASE_VERSION was removed in phase canon §2.G). EXPECTED_NA = { "upgrade": "no deployable upgrade base: every published version pins the moving tag " "pds:0.4, which upstream republished with incompatible main builds (index.js removed) — " - "re-enable via UPGRADE_BASE_VERSION once a fixed version is published post-merge", + "drop this once a fixed version is published post-merge (dynamic base then resolves it)", } # canon §2.B: enroll as a DATA-WARM canonical (all recipes enrolled — operator 2026-06-17). diff --git a/tests/unit/test_meta.py b/tests/unit/test_meta.py index 6f365d4..97df527 100644 --- a/tests/unit/test_meta.py +++ b/tests/unit/test_meta.py @@ -58,7 +58,6 @@ def test_missing_meta_yields_spec_baseline(tmp_path): assert meta.BACKUP_CAPABLE is None # None = auto-detect (tri-state, not False) assert meta.EXPECTED_NA is None assert meta.READY_PROBE is None - assert meta.UPGRADE_BASE_VERSION is None assert meta.BACKUP_VERIFY is None assert meta.UPGRADE_EXTRA_ENV is None assert meta.EXTRA_ENV == {} @@ -74,9 +73,9 @@ def test_registry_field_set_matches_dataclass(): import dataclasses assert [f.name for f in dataclasses.fields(RecipeMeta)] == [k.name for k in KEYS] - # the 15 final keys, no more (the 3 P2-deleted legacy keys are gone from the registry, - # so any recipe_meta still setting them hard-fails the typo gate) - assert len(KEYS) == 15 + # 14 final keys (UPGRADE_BASE_VERSION removed in phase canon §2.G; the 3 P2-deleted legacy keys + # are gone too — any recipe_meta still setting a removed key hard-fails the typo gate) + assert len(KEYS) == 14 assert not [k for k in KEYS if k.deprecated] for gone in ("CHAOS_BASE_DEPLOY", "OIDC_AT_INSTALL", "SKIP_GENERIC"): assert gone not in {k.name for k in KEYS} diff --git a/tests/unit/test_upgrade_base.py b/tests/unit/test_upgrade_base.py index 18ad4c7..38174ec 100644 --- a/tests/unit/test_upgrade_base.py +++ b/tests/unit/test_upgrade_base.py @@ -1,9 +1,10 @@ """Unit tests for `run_recipe_ci.resolve_upgrade_base` — the DYNAMIC upgrade-base decision (phase prevb). -Resolution order: upgrade∉stages / EXPECTED_NA[upgrade] (declared skip) → explicit UPGRADE_BASE_VERSION -override → last-green (warm canonical) → target-branch (`main`) tip → skip (no predecessor). The result -is a `BasePlan(kind, version, ref, reason)`: kind ∈ {"version", "ref", "skip"}; `.runs` is True for -version/ref (the upgrade tier runs). Replaces the old static `recipe_versions[-2]` default. +Resolution order: upgrade∉stages / EXPECTED_NA[upgrade] (declared skip) → last-green (warm canonical, +with same-version step-back) → target-branch (`main`) tip → skip (no predecessor). The result is a +`BasePlan(kind, version, ref, reason)`: kind ∈ {"version", "ref", "skip"}; `.runs` is True for +version/ref (the upgrade tier runs). Replaces the old static `recipe_versions[-2]` default; the old +explicit `UPGRADE_BASE_VERSION` override knob was removed in phase canon (§2.G). """ from __future__ import annotations @@ -22,8 +23,8 @@ HEAD = "aaaa1111head" MAIN = "bbbb2222main" -def _meta(expected_na=None, upgrade_base_version=None): - return SimpleNamespace(EXPECTED_NA=expected_na, UPGRADE_BASE_VERSION=upgrade_base_version) +def _meta(expected_na=None): + return SimpleNamespace(EXPECTED_NA=expected_na) def _no_canonical(monkeypatch): @@ -43,33 +44,16 @@ def test_upgrade_not_in_stages_skip(monkeypatch): assert plan.kind == "skip" and not plan.runs -def test_expected_na_upgrade_skip_even_with_canonical_and_override(monkeypatch): - # EXPECTED_NA[upgrade] is the strongest, declared fact — short-circuits before override/canonical. +def test_expected_na_upgrade_skip_even_with_canonical(monkeypatch): + # EXPECTED_NA[upgrade] is the strongest, declared fact — short-circuits before canonical. monkeypatch.setattr( canonical, "read_registry", lambda r: {"version": "9.9.9", "status": "warm"} ) declared = {"upgrade": "no deployable upgrade base (moving tag republished)"} - plan = run_recipe_ci.resolve_upgrade_base( - ALL, _meta(expected_na=declared, upgrade_base_version="0.2.0+v0.4"), "bluesky-pds" - ) + plan = run_recipe_ci.resolve_upgrade_base(ALL, _meta(expected_na=declared), "bluesky-pds") assert plan.kind == "skip" and "EXPECTED_NA" in plan.reason -def test_explicit_override_wins_over_canonical(monkeypatch): - monkeypatch.setattr( - canonical, "read_registry", lambda r: {"version": "9.9.9", "status": "warm"} - ) - monkeypatch.setattr( - lifecycle, - "recipe_branch_commit", - lambda r, b="main": (_ for _ in ()).throw(AssertionError("not consulted")), - ) - plan = run_recipe_ci.resolve_upgrade_base( - ALL, _meta(upgrade_base_version="0.7.0+3.3.1"), "discourse" - ) - assert plan.kind == "version" and plan.version == "0.7.0+3.3.1" and plan.runs - - def test_last_green_warm_canonical_is_primary(monkeypatch): # no override → last-green (warm canonical version) is the PRIMARY base; main is not consulted. monkeypatch.setattr(