feat(bsky): EXPECTED_NA['upgrade'] suppresses the upgrade-tier base deploy — single deploy = PR head; bluesky-pds declares it (no deployable base: every published tag pins the republished moving :0.4). upgrade_base() extracted pure + 6 unit tests; meta-key doc regenerated. 253 unit tests + repo lint PASS
This commit is contained in:
@ -116,7 +116,7 @@ _This table is GENERATED from the `runner/harness/meta.py` KEYS registry by `scr
|
||||
| `DEPLOY_TIMEOUT` | `int` | `600` | Max seconds to wait for swarm convergence per deploy. |
|
||||
| `HTTP_TIMEOUT` | `int` | `300` | Max seconds to wait for HTTP health after convergence. |
|
||||
| `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. |
|
||||
| `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` | Exact published tag overriding the upgrade tier's base (default: `recipe_versions[-2]`). |
|
||||
| `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. |
|
||||
|
||||
@ -76,7 +76,7 @@ KEYS: tuple[Key, ...] = (
|
||||
"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.",
|
||||
"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).",
|
||||
),
|
||||
Key(
|
||||
"READY_PROBE",
|
||||
|
||||
@ -88,6 +88,38 @@ def sso_dep_unverified(declared, deps_ready: bool, requires_deps_skipped: int) -
|
||||
return bool(declared) and not deps_ready and requires_deps_skipped > 0
|
||||
|
||||
|
||||
def upgrade_base(stages, meta, recipe: str) -> str | None:
|
||||
"""Deploy-once base version decision (pure given meta + the published-version lookup):
|
||||
previous published version when the upgrade tier will run and one exists (so upgrade goes
|
||||
previous→target in place), else None (the caller falls back to the target / PR head).
|
||||
(DECISIONS.)
|
||||
|
||||
A recipe may override the base via recipe_meta UPGRADE_BASE_VERSION when the harness default
|
||||
(recipe_versions[-2]) is NOT the PR's true predecessor — e.g. a PR that adds a version ABOVE the
|
||||
newest published tag, where the correct base is [-1] (the newest published), not [-2]. The
|
||||
override must be an exact published version tag (deployed as a pinned base). (Adversary §7.1.)
|
||||
|
||||
A recipe that declares the upgrade rung in EXPECTED_NA gets NO base: published versions may
|
||||
exist yet be genuinely undeployable — e.g. bluesky-pds, where every published tag pins the
|
||||
moving image tag `:0.4` that upstream republished with incompatible main builds, so no
|
||||
published version can come up as an upgrade base (phase bsky, DECISIONS). Deploying one would
|
||||
fail the INSTALL tier before the PR-head code is ever exercised. With no base, the single
|
||||
deploy is the PR head itself and the upgrade tier records "skip", which derive_rungs
|
||||
classifies as the DECLARED intentional skip (reason from EXPECTED_NA — visible in
|
||||
results.json `skips.intentional`, never reported as a pass)."""
|
||||
if "upgrade" not in stages:
|
||||
return None
|
||||
if "upgrade" in (meta.EXPECTED_NA or {}):
|
||||
print(
|
||||
"== upgrade tier: declared EXPECTED_NA['upgrade'] — no upgrade base will be "
|
||||
f"deployed; the single deploy is the target/PR head. Reason: "
|
||||
f"{(meta.EXPECTED_NA or {}).get('upgrade')}",
|
||||
flush=True,
|
||||
)
|
||||
return None
|
||||
return meta.UPGRADE_BASE_VERSION or lifecycle.previous_version(recipe)
|
||||
|
||||
|
||||
def _truthy(v: str | None) -> bool:
|
||||
return str(v or "").strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
@ -905,16 +937,7 @@ def main() -> int:
|
||||
|
||||
domain = naming.app_domain(recipe, os.environ.get("PR", "0"), ref)
|
||||
|
||||
# Deploy-once base version: previous published version when the upgrade tier will run and one
|
||||
# exists (so upgrade goes previous→target in place), else the target (current/$REF). (DECISIONS.)
|
||||
# A recipe may override the base via recipe_meta UPGRADE_BASE_VERSION when the harness default
|
||||
# (recipe_versions[-2]) is NOT the PR's true predecessor — e.g. a PR that adds a version ABOVE the
|
||||
# newest published tag, where the correct base is [-1] (the newest published), not [-2]. The
|
||||
# override must be an exact published version tag (deployed as a pinned base). (Adversary §7.1.)
|
||||
want_upgrade = "upgrade" in stages
|
||||
prev = (
|
||||
(meta.UPGRADE_BASE_VERSION or lifecycle.previous_version(recipe)) if want_upgrade else None
|
||||
)
|
||||
prev = upgrade_base(stages, meta, recipe)
|
||||
base = prev or target
|
||||
backup_cap = generic.backup_capable(recipe, meta)
|
||||
hook = discovery.install_steps(recipe, repo_local)
|
||||
@ -1091,7 +1114,7 @@ def main() -> int:
|
||||
junit_dir=junit_dir,
|
||||
)
|
||||
if prev
|
||||
else "skip" # only one published version → nothing to upgrade from
|
||||
else "skip" # no upgrade base: single published version, or declared EXPECTED_NA
|
||||
)
|
||||
# ---- BACKUP + RESTORE tiers (backup-capable only; else clean N/A) ----
|
||||
if "backup" in stages:
|
||||
@ -1274,7 +1297,7 @@ def main() -> int:
|
||||
records=records,
|
||||
results=results,
|
||||
backup_capable=backup_cap,
|
||||
has_upgrade_target=prev is not None, # structural: a previous published version exists
|
||||
has_upgrade_target=prev is not None, # structural: a deployable upgrade base exists
|
||||
lint=lint_result, # L5 rung (phase lvl5)
|
||||
clean_teardown=clean_teardown,
|
||||
no_secret_leak=True, # narrowed below by an actual scan of the serialised artifact
|
||||
|
||||
@ -6,3 +6,17 @@ HEALTH_PATH = "/xrpc/_health" # PDS health endpoint; returns {"version": ...} o
|
||||
HEALTH_OK = (200,)
|
||||
DEPLOY_TIMEOUT = 600
|
||||
HTTP_TIMEOUT = 600
|
||||
|
||||
# UPGRADE rung: published versions exist (0.1.1+v0.4, 0.2.0+v0.4) but BOTH pin the moving image
|
||||
# tag ghcr.io/bluesky-social/pds:0.4, which upstream republished with main-branch builds
|
||||
# (@atproto/pds 0.5.1, Node 24, /app/index.ts — no index.js), so NO published version can deploy
|
||||
# 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.
|
||||
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",
|
||||
}
|
||||
|
||||
79
tests/unit/test_upgrade_base.py
Normal file
79
tests/unit/test_upgrade_base.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""Unit tests for `run_recipe_ci.upgrade_base` — the deploy-once base-version decision.
|
||||
|
||||
Phase bsky: a recipe whose published versions ALL pin a moving image tag that upstream
|
||||
republished with incompatible builds (bluesky-pds) has no deployable upgrade base — deploying
|
||||
one fails the INSTALL tier before the PR head is ever exercised. The sanctioned escape hatch is
|
||||
the EXISTING declared-intentional-skip mechanism: EXPECTED_NA["upgrade"] now also suppresses
|
||||
the base deploy (single deploy = PR head; the tier records "skip"; derive_rungs classifies it
|
||||
intentional with the declared reason). These tests lock the decision matrix; derive_rungs'
|
||||
classification of the resulting skip is already covered in test_results.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
|
||||
import run_recipe_ci # noqa: E402
|
||||
from harness import lifecycle # noqa: E402
|
||||
|
||||
ALL = {"install", "upgrade", "backup", "restore", "custom"}
|
||||
|
||||
|
||||
def _meta(expected_na=None, upgrade_base_version=None):
|
||||
return SimpleNamespace(EXPECTED_NA=expected_na, UPGRADE_BASE_VERSION=upgrade_base_version)
|
||||
|
||||
|
||||
def test_default_prev_published(monkeypatch):
|
||||
# upgrade in stages, ≥2 published versions, nothing declared → recipe_versions[-2]
|
||||
monkeypatch.setattr(lifecycle, "previous_version", lambda r: "0.1.0+v1")
|
||||
assert run_recipe_ci.upgrade_base(ALL, _meta(), "somerecipe") == "0.1.0+v1"
|
||||
|
||||
|
||||
def test_single_published_version_no_base(monkeypatch):
|
||||
monkeypatch.setattr(lifecycle, "previous_version", lambda r: None)
|
||||
assert run_recipe_ci.upgrade_base(ALL, _meta(), "somerecipe") is None
|
||||
|
||||
|
||||
def test_upgrade_not_in_stages_no_base(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
lifecycle,
|
||||
"previous_version",
|
||||
lambda r: (_ for _ in ()).throw(AssertionError("not consulted")),
|
||||
)
|
||||
assert run_recipe_ci.upgrade_base(ALL - {"upgrade"}, _meta(), "somerecipe") is None
|
||||
|
||||
|
||||
def test_upgrade_base_version_override_wins(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
lifecycle,
|
||||
"previous_version",
|
||||
lambda r: (_ for _ in ()).throw(AssertionError("not consulted")),
|
||||
)
|
||||
meta = _meta(upgrade_base_version="0.7.0+3.3.1")
|
||||
assert run_recipe_ci.upgrade_base(ALL, meta, "discourse") == "0.7.0+3.3.1"
|
||||
|
||||
|
||||
def test_expected_na_upgrade_suppresses_base(monkeypatch):
|
||||
# bluesky-pds shape: published versions exist but are undeployable — declared EXPECTED_NA
|
||||
# upgrade → NO base (single deploy is the PR head), even though previous_version would
|
||||
# return one and even if UPGRADE_BASE_VERSION is set (the declaration is the stronger,
|
||||
# documented fact).
|
||||
monkeypatch.setattr(lifecycle, "previous_version", lambda r: "0.1.1+v0.4")
|
||||
declared = {"upgrade": "no deployable upgrade base (moving tag republished)"}
|
||||
assert run_recipe_ci.upgrade_base(ALL, _meta(expected_na=declared), "bluesky-pds") is None
|
||||
assert (
|
||||
run_recipe_ci.upgrade_base(
|
||||
ALL, _meta(expected_na=declared, upgrade_base_version="0.2.0+v0.4"), "bluesky-pds"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_expected_na_other_rung_does_not_suppress(monkeypatch):
|
||||
# an EXPECTED_NA for backup_restore (custom-html-tiny shape) must NOT touch the upgrade base
|
||||
monkeypatch.setattr(lifecycle, "previous_version", lambda r: "0.1.0+v1")
|
||||
meta = _meta(expected_na={"backup_restore": "stateless"})
|
||||
assert run_recipe_ci.upgrade_base(ALL, meta, "custom-html-tiny") == "0.1.0+v1"
|
||||
Reference in New Issue
Block a user