6.2 KiB
REVIEW — phase samever (Adversary writes here)
Phase: samever — step back to older base when canonical == head version (no same-version upgrade)
SSOT: /srv/cc-ci/cc-ci-plan/plan-phase-samever-older-base-fallback.md
Adversary loop started: 2026-06-17T04:09Z
Adversary clone: /srv/cc-ci/cc-ci-adv
Gate verdicts
M1: PASS @2026-06-17T04:27Z
Cold-verified from own clone /srv/cc-ci/cc-ci-adv @ b29bb3f (claim c5a0d20). Implemented + unit-tested
gate. Independent (not trusting Builder's tests) — re-ran the suite AND wrote my own break-it probes.
Evidence:
- Unit suite cold:
pytest tests/unit/test_upgrade_base.py -v→ 13 passed (8 prior unchanged- 5 new). The 8 prior (override / EXPECTED_NA / main-tip / head==main-tip skip / no-predecessor / other-rung) still green ⇒ override/ref/skip paths untouched.
- My own primitive probes (direct import, adversarial inputs):
newest_older_versionstrictly-older semantics: suffix tags (-rootless) ordered correctly; head-version BETWEEN tags → newest strictly older; equal-key tag EXCLUDED (1.0.0+3.5.3 vs 1.0.0+3.5.3 → None); head-is-oldest → None; None/empty safe; recipe-major ordering beats app (9.9.9+99.0.0 < 10.0.0+1.0.0). ✓_VERSION_LABEL_RE: parses quoted, unquoted, single-quoted labels;.chaos-version→ None (not matched); chaos-then-real picks the real label. ✓
- My own resolver-chain probes (monkeypatched canonical + recipe_tags, direct
resolve_upgrade_base):- canonical==head (TEETH):
10.8.0+26.6.3→ base10.7.1+26.6.2,kind=version,reason="step-back: …"; assertedversion != headANDversion_key(base) < version_key(head). Never a same-version no-op; strictly older. ✓ - canonical≠head (version-bump path): uses canonical unchanged AND
recipe_tagsis NOT consulted (patched it to raise — no raise) ⇒ discourse #4 / version-bump PRs cannot be perturbed by this gate. ✓ - canonical==head, no older tag:
kind=skip, reason"base == head (…) and no older published predecessor"⇒ declared, not silent. ✓ - head_version=None (compose unreadable): canonical stays primary (prevb behavior preserved). ✓
- canonical==head (TEETH):
- sort_versions refactor behavior-preserving:
version_keylifted verbatim from the old inline key;test_warm_reconcile.pyversion-ordering tests pass (8 passed; single failure unrelated). - Pre-existing failures disclosed honestly:
test_meta::test_generated_doc_table_in_syncandtest_warm_reconcile::test_traefik_spec_is_stateless_with_setupFAIL on parent279d84dtoo (re-ran in a temp worktree — both fail there); samever diff touches neither SPECS nor the doc table. Out of scope, NOT a regression.
F1d-2: step-back returns kind="version" ⇒ inherits the same pinned-tag deploy path as any
canonical base (no new deploy code) — the on-disk tree is checked out at the pinned older tag. This is
an M1 (unit) claim; the REAL pinned-deploy proof belongs to M2 (live CI, evidenced base<head delta).
Verdict: M1 PASS. Implementation matches plan §2 chain exactly; teeth hold; no regression to override/ref/skip/version-bump paths. (Consulted JOURNAL only after writing this — did not need it.)
Orientation @2026-06-17T04:09Z
Phase samever plan created 2026-06-17T03:56Z. Builder has not yet started (no STATUS-samever.md).
Root cause confirmed (cold-read of resolver, lines 133–148 of run_recipe_ci.py):
rec = canonical.read_registry(recipe)
if rec and rec.get("version"):
return BasePlan(
"version",
rec["version"],
None,
f"last-green (warm canonical, status={rec.get('status')})",
)
The warm-canonical path returns canonical["version"] WITHOUT checking if it equals the head version.
The resolver is not passed the head's semantic version (only head_ref, a commit sha), so it cannot compare.
Current unit tests (8 tests in tests/unit/test_upgrade_base.py) — none cover canonical==head:
- test_upgrade_not_in_stages_skip
- test_expected_na_upgrade_skip_even_with_canonical_and_override
- test_explicit_override_wins_over_canonical
- test_last_green_warm_canonical_is_primary ← uses canonical["version"]="0.6.0+3.1.1", HEAD="aaaa1111head" (different version — correct but doesn't test the same-version edge)
- test_main_tip_fallback_when_no_last_green
- test_head_equals_main_tip_skip
- test_no_canonical_no_main_skip
- test_expected_na_other_rung_does_not_suppress_upgrade
Key utilities available for the fix:
warm_reconcile.recipe_tags(recipe)— returns all git tags from recipe clonewarm_reconcile.sort_versions(tags)— ascending sort of version tags (coop-cloud semver)warm_reconcile.latest_version(tags)— the newest tag- Head version read from compose.yml:
coop-cloud.${STACK_NAME}.versionlabel atabra.recipe_dir(recipe)/compose.yml(head checkout already at that path when resolver runs)
M1 verification plan (what I'll cold-verify when claimed):
- Resolver reads head version from compose.yml (inspect the parsing — look for compose YAML read +
coop-cloud.*versionlabel extraction) - New chain: override → (canonical if canonical≠head_version) → (newest older published if canonical==head_version) → main-tip → skip
- Unit tests added: at minimum canonical==head→step_back, canonical≠head→unchanged, no_older_published→skip, version ordering correct
- Run
python -m pytest tests/unit/test_upgrade_base.py -vcold from own clone - Confirm OVERRIDE, EXPECTED_NA, main-tip, skip paths are untouched (regression: existing 8 tests still pass)
- Teeth check: a "broken base" scenario should still fail (unit test or from plan F1d-2 evidence)
M2 verification plan:
- Cold-on-latest run on an enrolled recipe whose canonical == latest (seed the canonical to latest, then trigger cold run)
- Evidence in logs:
base_version < head_version(not a no-op, not a skip) - Re-run discourse #4 or equivalent version-bump PR → UNAFFECTED (canonical→head path still uses canonical)
- Spot-check ≥1 other recipe
Adversary findings
(empty — phase not yet started)
Break-it probes log
(none yet)