# STATUS — phase `settings`

**Phase:** server-level `settings.toml` + `SKIP_CANONICALS_FOR_UPGRADE` + release-tag-first no-canonical
fallback. Plan: `/srv/cc-ci/cc-ci-plan/plan-phase-settings-ci-server-config.md`.

## DONE

All Definition-of-Done items Adversary-verified with fresh PASSes (no standing VETO), 2026-06-17:
- **M1 PASS** — REVIEW-settings.md @17:00Z (claim fed2678 / code cd19c1b): settings loader (stdlib
  tomllib, defaults, graceful absent/malformed, warn-and-ignore unknown, TypeError on wrong type) +
  `SKIP_CANONICALS_FOR_UPGRADE` wired into `resolve_upgrade_base` + release-tag-first no-canonical
  fallback reusing samever's helper; 32 + 315 unit pass; scope narrow; stdlib-only; no secrets.
- **M2 PASS** — REVIEW-settings.md @17:35Z (claim a9ff941 / deployed `/etc/cc-ci`@99d6bbc, byte-identical
  runner to cd19c1b): live on cc-ci — keycloak (no canonical) → release tag `10.7.1+26.6.2` not main-tip;
  gitea (canonical) unchanged `last-green` under default false; scratch `true` bypasses gitea's canonical
  to the release-tag path; restored to false; harness file-pickup proven via the real `/etc/cc-ci/settings.toml`.

Server in steady state: `/etc/cc-ci/settings.toml` ABSENT (default false), checkout clean @99d6bbc.

## Gate: M1 PASS (Adversary @2026-06-17T17:00Z, REVIEW-settings.md) · M2 CLAIMED (see below)

**M1 commit:** `cd19c1b` (feat: settings loader + flag + fallback + unit tests) — Adversary cold-PASS,
no VETO. **M2 deployed:** `99d6bbc` on `/etc/cc-ci`. Tree clean, pushed to origin/main.

### WHAT is claimed (M1 — implemented + unit-tested)
1. **Settings loader** `runner/harness/settings.py` — stdlib `tomllib`, one `[upgrade]` table with
   `skip_canonicals_for_upgrade` (bool, **default false**). `_SCHEMA` (table→{key:(type,default)}) is the
   single source of defaults + validation. Graceful on absent/unreadable/malformed file (WARN +
   all-defaults — never crashes); unknown table/key → warn-and-ignore; present known key of wrong type →
   `TypeError`. Path = `$CCCI_SETTINGS` else `/etc/cc-ci/settings.toml`. Tracked `settings.toml.example`
   documents the key (no secrets).
2. **`SKIP_CANONICALS_FOR_UPGRADE` wired into `resolve_upgrade_base`** (`runner/run_recipe_ci.py`): when
   true, the canonical (`version`) branch is skipped entirely → no-canonical fallback. Guard:
   `if rec and rec.get("version") and not skip_canonicals:`. Scope = upgrade base only (promotion /
   `--quick` untouched).
3. **Release-tag-first no-canonical fallback** `_no_canonical_base` (`runner/run_recipe_ci.py`),
   always-on: (1) newest release **tag** with version strictly older than `head_version` — reuses
   `warm_reconcile.newest_older_version(warm_reconcile.recipe_tags(recipe), head_version)`; (2) raw
   `main`-tip — only if no prior release tag; (3) skip. (Guarded: tag lookup skipped when
   `head_version` is falsy → main-tip, preserving prevb behavior for that caller.)

### HOW to verify (re-runnable from a fresh clone)
All commands from the repo root of a clone at `cd19c1b` (or later). Unit tests need pytest from nixpkgs:

```
# 1. Full upgrade-base matrix + settings loader tests
nix shell nixpkgs#python311Packages.pytest -c pytest tests/unit/test_upgrade_base.py tests/unit/test_settings.py -v
# 2. Whole unit suite (regression — nothing else broke)
nix shell nixpkgs#python311Packages.pytest -c pytest tests/unit/ -q
# 3. Lint (my files) — ruff check + format
nix shell nixpkgs#ruff -c ruff check runner/ tests/unit/test_settings.py tests/unit/test_upgrade_base.py
nix shell nixpkgs#ruff -c ruff format --check runner/harness/settings.py runner/run_recipe_ci.py tests/unit/test_settings.py tests/unit/test_upgrade_base.py settings.toml.example
# 4. Default is false WITHOUT any file (this server unchanged):
nix shell nixpkgs#python311Packages.pytest -c python3 -c "import sys; sys.path.insert(0,'runner'); from harness import settings; print(settings.load('/no/such/file.toml').skip_canonicals_for_upgrade)"
#   -> False
```

### EXPECTED outcome
- (1) → **32 passed**. Key cases proving the matrix:
  - `test_flag_false_canonical_present_unchanged` — flag false + canonical (≠head) → canonical, `last-green`
    reason, tags/main NOT consulted (byte-for-byte prevb).
  - `test_no_canonical_prefers_release_tag_over_main_tip` — flag false + no canonical → version=`10.7.1+26.6.2`
    (newest tag `< head 10.8.0+26.6.3`), main NOT consulted; reason contains `no-canonical fallback`/`release tag`.
  - `test_no_canonical_no_older_tag_falls_back_to_main_tip` — no older tag → `ref`=main-tip.
  - `test_no_canonical_no_tag_no_main_skips` — neither → `skip`.
  - `test_flag_true_bypasses_canonical_into_release_tag_fallback` — flag true + canonical present → resolves to
    release tag `10.7.1+26.6.2`, NOT the canonical `10.6.0+26.5.0`.
  - `test_flag_true_canonical_present_no_older_tag_uses_main` — flag true routes through the FULL chain → main-tip.
  - `test_canonical_equals_head_steps_back_to_newest_older` / `..._no_older_published_skips` — samever step-back
    unchanged (older→version; none→skip, NOT main-tip).
  - loader: `test_absent_file_yields_defaults`, `test_malformed_toml_degrades_to_defaults`,
    `test_wrong_type_errors_clearly`, `test_int_not_accepted_for_bool`, `test_unknown_key/table_warns_and_ignored`,
    `test_env_var_path_override`, `test_flag_true_read`/`_false_read`.
- (2) → **315 passed** (full suite, no regression).
- (3) → `All checks passed!` and `... already formatted`.
- (4) → `False` (default with no file → this server behaves as today).

### WHERE
- `runner/harness/settings.py` (loader, `DEFAULT_PATH = /etc/cc-ci/settings.toml`, `Settings`, `load()`, `get()`).
- `settings.toml.example` (repo root, tracked).
- `runner/run_recipe_ci.py`: `resolve_upgrade_base` (flag guard) + new `_no_canonical_base` helper; import
  `settings as settings_mod`.
- `tests/unit/test_settings.py` (loader, 13 tests), `tests/unit/test_upgrade_base.py` (matrix, +8 new tests).

### NOTE (pre-existing, out of scope — see DECISIONS)
`scripts/lint.sh` (pinned ruff) flags `dashboard/dashboard.py` + `tests/unit/test_dashboard.py` as
needing reformat — confirmed present at HEAD f68f1c5, NOT in this phase's diff. Not fixed here (narrow
scope). My 5 phase files are ruff-clean + format-clean.

---

## Gate: M2 CLAIMED, awaiting Adversary  @2026-06-17T17:25Z

M1 is Adversary-PASS (REVIEW-settings.md verdict @17:00Z, fed2678/cd19c1b). M2 = verified live on cc-ci.

**Deployed:** `/etc/cc-ci` at **99d6bbc** (pushed to origin/main; the deployed checkout the nightly
sweep runs from, and the absolute path the Drone recipe-CI runner reads). No nixos-rebuild needed — the
change is pure runner Python loaded at runtime from the checkout. Live settings file
`/etc/cc-ci/settings.toml` is **ABSENT** → default false (server steady state restored after the test).

### WHAT is claimed (M2)
- The live server harness reads the settings file from the host path `/etc/cc-ci/settings.toml`
  (absent → default false), confirmed by the flag value flipping with the file's presence.
- **(a)** A recipe **without** a canonical (`keycloak`, no `canonical.json`) resolves its upgrade base
  to the **newest release tag `< head`** (`10.7.1+26.6.2`), NOT the raw main-tip.
- **(b)** With `SKIP_CANONICALS_FOR_UPGRADE = true` (scratch file), a **canonical-bearing** recipe
  (`gitea`, canonical `3.5.3+1.24.2-rootless`) resolves to the **release-tag base** (canonical
  BYPASSED) — proven by the reason changing from `last-green (warm canonical, status=idle)` to
  `no-canonical fallback: newest release tag older than head 3.6.0+1.24.2-rootless`. Scratch file then
  removed → restored to false (reason back to `last-green (warm canonical)`).
- Default false ⇒ this server's canonical-bearing path is unchanged (gitea false → `last-green` base).

### HOW to verify (cold, on the server, from /etc/cc-ci or your own clone)
The probe runs the EXACT deployed `resolve_upgrade_base` against live settings + live canonical registry
(`/var/lib/ci-warm/<r>/canonical.json`) + live recipe tags (`~/.abra/recipes/<r>`). Faithful, no
deploy/teardown.

```
ssh cc-ci
cd /etc/cc-ci && git rev-parse --short HEAD          # 99d6bbc (or later)
ls /etc/cc-ci/settings.toml                          # ABSENT -> default false

# CASE 1 — flag false (default, no file): (a) keycloak, plus gitea unchanged
HOME=/root cc-ci-run scripts/show-upgrade-base.py keycloak gitea

# CASE 2 — flag true (scratch), then RESTORE
printf '[upgrade]\nskip_canonicals_for_upgrade = true\n' > /etc/cc-ci/settings.toml
HOME=/root cc-ci-run scripts/show-upgrade-base.py gitea keycloak
rm -f /etc/cc-ci/settings.toml                       # restore default false
HOME=/root cc-ci-run scripts/show-upgrade-base.py gitea
```

### EXPECTED (verbatim BasePlan lines observed @17:20–17:25Z)
- CASE 1 (false):
  - `keycloak` → `BasePlan(kind='version', version='10.7.1+26.6.2', ref='', reason='no-canonical fallback: newest release tag older than head 10.8.0+26.6.3')`  (canonical=None; newest_release_tag<head=10.7.1+26.6.2; NOT main-tip 12ac6db8…)
  - `gitea` → `BasePlan(kind='version', version='3.5.3+1.24.2-rootless', ref='', reason='last-green (warm canonical, status=idle)')`  (canonical=3.5.3 present → used)
- CASE 2 (true):
  - `gitea` → `BasePlan(kind='version', version='3.5.3+1.24.2-rootless', ref='', reason='no-canonical fallback: newest release tag older than head 3.6.0+1.24.2-rootless')`  (canonical 3.5.3 present but BYPASSED — reason is the release-tag path)
  - `keycloak` → same as CASE 1 (no canonical either way)
- RESTORE (file removed → false):
  - `gitea` → reason back to `last-green (warm canonical, status=idle)`; flag reads `False`.

### WHERE
- Deployed code: `/etc/cc-ci` @ 99d6bbc (origin/main). Probe: `scripts/show-upgrade-base.py`.
- Live registry: `/var/lib/ci-warm/{keycloak (none),gitea}/canonical.json`. Recipe tags:
  `~/.abra/recipes/{keycloak,gitea}`. Settings path: `/etc/cc-ci/settings.toml` (absent now).

Server left in steady state: `/etc/cc-ci/settings.toml` ABSENT (default false), checkout clean @99d6bbc.

→ On a fresh Adversary PASS of M2 (with M1 PASS standing), I will write `## DONE`.
