feat(canon): M1.1 tagged-promote gate — canonical only advances to a published release tag
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:
autonomic-bot
2026-06-17 06:34:09 +00:00
co-authored by Claude Opus 4.8
parent 23c02c59b6
commit 27e06289f8
5 changed files with 74 additions and 18 deletions
+20 -9
View File
@@ -2,7 +2,8 @@
Pure predicate. The live promote (deploy canonical at latest → snapshot → registry) is proven on
custom-html (W3). is_enrolled is monkeypatched so the test doesn't depend on which recipes are
enrolled on disk.
enrolled on disk. canon §2.A adds the `tagged` requirement: a promote also requires the tested head
version to be a published release tag (computed by the caller via is_released_version).
"""
from __future__ import annotations
@@ -20,30 +21,40 @@ def _enrolled(monkeypatch, val):
monkeypatch.setattr(canonical, "is_enrolled", lambda r: val)
def test_promote_when_enrolled_green_cold_latest(monkeypatch):
def test_promote_when_enrolled_green_cold_latest_tagged(monkeypatch):
_enrolled(monkeypatch, True)
# green (overall 0), cold (quick False), latest (ref None) → promote
assert rr.should_promote_canonical("custom-html", None, 0, quick=False) is True
assert rr.should_promote_canonical("custom-html", "", 0, quick=False) is True
# green (overall 0), cold (quick False), latest (ref None), tagged → promote
assert rr.should_promote_canonical("custom-html", None, 0, quick=False, tagged=True) is True
assert rr.should_promote_canonical("custom-html", "", 0, quick=False, tagged=True) is True
def test_no_promote_when_not_enrolled(monkeypatch):
_enrolled(monkeypatch, False)
assert rr.should_promote_canonical("hedgedoc", None, 0, quick=False) is False
assert rr.should_promote_canonical("hedgedoc", None, 0, quick=False, tagged=True) is False
def test_no_promote_when_red(monkeypatch):
_enrolled(monkeypatch, True)
assert rr.should_promote_canonical("custom-html", None, 1, quick=False) is False
assert rr.should_promote_canonical("custom-html", None, 1, quick=False, tagged=True) is False
def test_no_promote_when_quick(monkeypatch):
# --quick never promotes (the canonical advances ONLY via cold)
_enrolled(monkeypatch, True)
assert rr.should_promote_canonical("custom-html", None, 0, quick=True) is False
assert rr.should_promote_canonical("custom-html", None, 0, quick=True, tagged=True) is False
def test_no_promote_on_pr_head(monkeypatch):
# a PR `!testme` carries REF=PR-head → must NOT advance the canonical to a PR's code
_enrolled(monkeypatch, True)
assert rr.should_promote_canonical("custom-html", "abc123def", 0, quick=False) is False
assert (
rr.should_promote_canonical("custom-html", "abc123def", 0, quick=False, tagged=True)
is False
)
def test_no_promote_when_untagged(monkeypatch):
# canon §2.A: a green cold latest run on an UNTAGGED state (head version is not a release tag)
# must NOT promote — the canonical is always a real release, never an arbitrary main commit.
_enrolled(monkeypatch, True)
assert rr.should_promote_canonical("custom-html", None, 0, quick=False, tagged=False) is False
+23 -2
View File
@@ -33,6 +33,24 @@ def test_latest_none_when_no_tags():
assert wr.latest_version(["main", "feature-x"]) is None
def test_is_released_version(monkeypatch):
# canon §2.A tagged-promote gate. is_released_version(recipe, version) → True iff version is a
# published release tag (exact or by version_key). recipe_tags is the only I/O — monkeypatch it.
tags = ["1.13.0+1.31.1", "1.12.0+1.30.0", "main", "feature-x"]
monkeypatch.setattr(wr, "recipe_tags", lambda r: tags)
assert wr.is_released_version("custom-html", "1.13.0+1.31.1") is True # exact tag
assert wr.is_released_version("custom-html", "1.12.0+1.30.0") is True
assert wr.is_released_version("custom-html", "9.9.9+9.9.9") is False # not a tag → untagged
assert wr.is_released_version("custom-html", None) is False
assert wr.is_released_version("custom-html", "") is False
def test_is_released_version_no_tags(monkeypatch):
# a recipe that has never cut a release → no version is a release
monkeypatch.setattr(wr, "recipe_tags", lambda r: ["main"])
assert wr.is_released_version("never-released", "0.1.0") is False
def test_minor_patch_bump_not_major():
# recipe-semver 10.7.0 -> 10.7.1 (patch); app 26.6.1 -> 26.6.2 (patch). Auto-apply.
assert wr.is_major_bump("10.7.0+26.6.1", "10.7.1+26.6.2") is False
@@ -56,11 +74,14 @@ def test_app_major_bump_held_even_if_no_plus_on_current():
def test_traefik_spec_is_stateless_with_setup():
# WC1.1 traefik = stateless (version-rollback-only, NO snapshot) + its own cert/file-provider
# setup + health probed on a ROUTED host (the dashboard), not traefik's own domain.
# setup. Health is probed on traefik's OWN /api/version endpoint (phase pxgate A1: probing the
# dashboard host caused a cold-boot deadlock since deploy-dashboard is After=deploy-proxy, so the
# spec carries no `health_domain` override and health_code() falls back to the app domain).
t = wr.SPECS["traefik"]
assert t["stateful"] is False
assert callable(t.get("setup"))
assert t["health_domain"] == "ci.commoninternet.net"
assert "health_domain" not in t # probes traefik's own domain, not a routed dashboard host
assert t["health_path"] == "/api/version"
assert t["domain"] == "traefik.ci.commoninternet.net"
# keycloak stays stateful with no custom setup (default path)
assert wr.SPECS["keycloak"]["stateful"] is True