feat(settings): server settings.toml loader + SKIP_CANONICALS_FOR_UPGRADE + release-tag-first no-canonical fallback
Some checks failed
continuous-integration/drone/push Build is failing

- harness/settings.py: stdlib tomllib loader, [upgrade].skip_canonicals_for_upgrade
  (bool, default false), _SCHEMA single-source defaults+validation; graceful on
  absent/malformed (WARN+defaults), warn-and-ignore unknown keys/tables, TypeError on
  wrong type. Path $CCCI_SETTINGS / /etc/cc-ci/settings.toml. + tracked settings.toml.example.
- resolve_upgrade_base: flag true bypasses the canonical lookup -> no-canonical fallback;
  canonical-present path (incl. samever step-back) unchanged when false.
- _no_canonical_base (always-on, §2.C): newest release tag < head (reuse
  warm_reconcile.newest_older_version) -> main-tip -> skip; replaces jump-to-main-tip.
- unit: full resolution matrix + loader tests; 315 unit pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 16:55:05 +00:00
committed by autonomic-bot
parent 90228cffc4
commit cd19c1b172
8 changed files with 556 additions and 5 deletions

View File

@ -75,6 +75,9 @@ from harness import ( # noqa: E402
from harness import ( # noqa: E402
screenshot as screenshot_mod,
)
from harness import ( # noqa: E402
settings as settings_mod,
)
ALL_STAGES = ("install", "upgrade", "backup", "restore", "custom")
@ -113,8 +116,14 @@ 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: 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.
default). Chain: last-green (warm canonical, with same-version step-back) → newest release tag
older than head → main-tip → skip. EXPECTED_NA[upgrade] / upgrade∉stages short-circuit to a
declared skip first.
SKIP_CANONICALS_FOR_UPGRADE (phase settings, server settings.toml, default false): when true, the
canonical lookup is bypassed entirely — the resolver behaves as if no canonical exists and takes
the no-canonical release-tag-first fallback (`_no_canonical_base`). Scope is the upgrade BASE only;
canonical promotion and the `--quick` warm-reattach are unaffected (see DECISIONS, phase settings).
`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
@ -142,8 +151,9 @@ def resolve_upgrade_base(
flush=True,
)
return BasePlan("skip", None, None, f"declared EXPECTED_NA[upgrade]: {declared}")
skip_canonicals = settings_mod.get().skip_canonicals_for_upgrade
rec = canonical.read_registry(recipe)
if rec and rec.get("version"):
if rec and rec.get("version") and not skip_canonicals:
canon = rec["version"]
same = head_version is not None and warm_reconcile.version_key(
canon
@ -176,13 +186,46 @@ def resolve_upgrade_base(
None,
f"base == head ({head_version}) and no older published predecessor",
)
# No canonical in play — none recorded, OR SKIP_CANONICALS_FOR_UPGRADE=true (canonical lookup
# bypassed entirely, behaving as if none exists). Improved fallback (phase settings §2.C): prefer
# a REAL published predecessor (newest release tag < head) over the raw main-tip.
return _no_canonical_base(recipe, head_ref, head_version)
def _no_canonical_base(recipe: str, head_ref: str | None, head_version: str | None) -> BasePlan:
"""Upgrade base when no canonical is used (none recorded, its promote failed, or
SKIP_CANONICALS_FOR_UPGRADE is true). Release-tag-first fallback (phase settings §2.C):
1. most recent release TAG with version strictly older than the PR head — a clean published
predecessor (reuses samever's `newest_older_version` helper, the single source of version
ordering, so this and the step-back never diverge);
2. raw `main`-tip (target-branch tip) — only if the recipe has NO prior release tag at all;
3. skip — no predecessor (no older tag and head == main-tip, or no main at all).
This replaces the old jump-straight-to-main-tip path, so an un-promoted recipe upgrades from a real
release base instead of a possibly-untagged WIP commit."""
older = (
warm_reconcile.newest_older_version(warm_reconcile.recipe_tags(recipe), head_version)
if head_version
else None
)
if older:
return BasePlan(
"version",
older,
None,
f"no-canonical fallback: newest release tag older than head {head_version}",
)
main_tip = lifecycle.recipe_branch_commit(recipe, "main")
if main_tip and main_tip != head_ref:
return BasePlan("ref", None, main_tip, "target-branch (main) tip")
return BasePlan(
"ref",
None,
main_tip,
"no-canonical fallback: target-branch (main) tip (no prior release tag)",
)
if main_tip and main_tip == head_ref:
return BasePlan("skip", None, None, "head == main tip (no predecessor delta)")
return BasePlan(
"skip", None, None, "no last-green and no main tip (new recipe / no predecessor)"
"skip", None, None, "no release tag and no main tip (new recipe / no predecessor)"
)