diff --git a/cc-ci-plan/agents.toml b/cc-ci-plan/agents.toml index 542b242..1cb8eda 100644 --- a/cc-ci-plan/agents.toml +++ b/cc-ci-plan/agents.toml @@ -147,4 +147,11 @@ phases = [ { id = "ghost", plan = "plan-phase-ghost-reeval.md", status = "STATUS-ghost.md" }, { id = "cf48", plan = "plan-phase-cf48-opus-cfold-review.md", status = "STATUS-cf48.md", models = { builder = "claude-opus-4-8", adversary = "claude-opus-4-8" } }, { id = "pxgate", plan = "plan-phase-pxgate-proxy-healthgate.md", status = "STATUS-pxgate.md" }, + # agent-orchestrator build (builder=opus, adversary=sonnet) — see plan-agent-orchestrator.md + { id = "aoeng", plan = "plan-phase-aoeng-engine.md", status = "STATUS-aoeng.md", models = { builder = "claude-opus-4-8", adversary = "claude-sonnet-4-6" } }, + { id = "aotest", plan = "plan-phase-aotest-verify.md", status = "STATUS-aotest.md", models = { builder = "claude-opus-4-8", adversary = "claude-sonnet-4-6" } }, + { id = "porepo", plan = "plan-phase-porepo-project-orchestrator.md", status = "STATUS-porepo.md", models = { builder = "claude-opus-4-8", adversary = "claude-sonnet-4-6" } }, + { id = "poe2e", plan = "plan-phase-poe2e-end-to-end.md", status = "STATUS-poe2e.md", models = { builder = "claude-opus-4-8", adversary = "claude-sonnet-4-6" } }, + # gitea full-test enrollment + LFS PR #1 verification — see plan-phase-gtea-gitea-fulltests.md (operator 2026-06-15) + { id = "gtea", plan = "plan-phase-gtea-gitea-fulltests.md", status = "STATUS-gtea.md", models = { builder = "claude-sonnet-4-6", adversary = "claude-sonnet-4-6" } }, ] diff --git a/cc-ci-plan/plan-phase-gtea-gitea-fulltests.md b/cc-ci-plan/plan-phase-gtea-gitea-fulltests.md new file mode 100644 index 0000000..96c1484 --- /dev/null +++ b/cc-ci-plan/plan-phase-gtea-gitea-fulltests.md @@ -0,0 +1,178 @@ +# Phase `gtea` — enroll gitea as a FULLY-TESTED recipe (+ verify LFS PR #1) + +**Mission (operator-specified 2026-06-15):** gitea currently exists in cc-ci only as a +**dependency provider** for the `drone` phase — `tests/gitea/recipe_meta.py` is the lone +file and its header says "NOT a standalone recipe-under-test". Turn gitea into a +**fully-tested recipe** (install + upgrade + backup + restore + custom functional tests + +lint + screenshot), to the same standard as `cryptpad`/`keycloak`. **Then, once the suite +is basically working, verify it against the open LFS PR** +`https://git.autonomic.zone/recipe-maintainers/gitea/pulls/1` (`feat: support Git LFS on +plain gitea`, branch `lfs-plain-gitea` → `main`) — the suite's LFS capstone test is the +mechanism that proves the PR. + +**The non-negotiable constraint that shapes everything below: do NOT break drone.** gitea's +`recipe_meta.py` is loaded by the SAME code path whether gitea is drone's dep or the +recipe-under-test. Drone's CI must stay green throughout (it provisions gitea as an +install-time dep, sqlite3 + relaxed auth, via `sso.setup_gitea_oauth`). + +State files: `STATUS-gtea.md`, `BACKLOG-gtea.md`, `REVIEW-gtea.md`, `JOURNAL-gtea.md`. +DECISIONS.md shared. + +## 0. Prerequisites (verify before deploying) + +- **`/etc/timezone` host fix is already live** (shipped with the `drone` phase — gitea binds + `/etc/timezone:ro`; the Nix `environment.etc."timezone"` fix is deployed). Builder's first + action: confirm `test -f /etc/timezone` on the cc-ci host (content `UTC`). It should exist + — drone already deploys gitea. If it's somehow absent, write a BLOCKED note in + STATUS-gtea.md ("P0 host deploy needed — orchestrator") and do meta/test authoring that + needs no deploy until the orchestrator restores it. +- abra over a pseudo-TTY (`ssh cc-ci 'script -qec "abra … -n" /dev/null'`); creds baked into + the mirror origin where needed; stash the untracked overlay. CI host has no python3 on the + default PATH. + +## 1. Scope — the file manifest for `tests/gitea/` + +Build to the recipe-under-test anatomy (registry/contracts: `runner/harness/meta.py`, +`runner/harness/discovery.py`; exemplars `tests/cryptpad/`, `tests/keycloak/`): + +1. **`recipe_meta.py` (UPDATE — carefully; this is the conflict surface).** + - Keep the EXISTING dep behaviour intact (sqlite3 `COMPOSE_FILE`, relaxed-auth env) so + drone's gitea-dep deploy is byte-for-byte unchanged. Update the header comment: gitea is + now **both** a dep provider AND a recipe-under-test. + - Add the recipe-under-test keys that are harmless to the dep path: `HEALTH_PATH` + (`/api/healthz` already set), `HEALTH_OK`, sensible `DEPLOY_TIMEOUT`/`HTTP_TIMEOUT`, + `BACKUP_CAPABLE` (the gitea recipe HAS `backupbot.backup` labels — backup/restore are + REAL tiers, not skips), and `SCREENSHOT` (gitea has a real landing/login UI — default + capture should work; follow the `shot`-phase standard). Consider `READY_PROBE` for a + gitea-specific readiness gate (API reachable, not just TCP). + - **DB:** sqlite3 (matches the dep config + lightest footprint; gitea ships + `compose.sqlite3.yml`). Do not switch the dep to mariadb/postgres. + - **No new ALL-CAPS meta keys** without adding them to the `meta.py` registry + regenerating + docs (unknown caps = hard MetaError, unit-tested). + +2. **`ops.py` (NEW)** — uniform `HookCtx` hooks (`pre_backup`/`pre_restore`/`pre_upgrade`, + and `pre_install` if needed for the LFS secret — see §3). Seed a **data-integrity marker** + via the gitea admin REST API: create a repo (+ a user/org and a committed file) before the + op so `test_backup`/`test_restore`/`test_upgrade` can assert data continuity. Model on + `tests/keycloak/ops.py` (API-based markers) — NOT file-in-container hacks. + +3. **Lifecycle overlays (NEW)** — additive on the `_generic` floor: + - `test_install.py` — generic `assert_serving()` + gitea-specific: `/api/healthz` → 200 and + the admin API is reachable/authenticated. + - `test_backup.py` — the seeded repo/user marker is present after backup. + - `test_restore.py` — **mutation pattern**: `ops.pre_restore` deletes/mutates the marker; + restore must return it to the backed-up state; assert reverted (real divergence — a + no-op restore must fail this). + - `test_upgrade.py` — the marker survives the upgrade to PR-head (data continuity across + the chaos redeploy). + +4. **`custom/` functional tests (NEW)** — discovered from `tests/gitea/custom/test_*.py`. + Floor is ≥2 tests **beyond** parity. Deliver: + - `custom/test_health.py` — **parity port** of `recipe-info/gitea/tests/health_check.py`. + - `custom/test_git_push.py` — **parity port** of `recipe-info/gitea/tests/git_push.py`: + create repo via API → clone → commit → push over HTTPS → verify commit landed via API → + clean up. (Has real teeth: a broken SCM fails the push.) + - `custom/test_admin_api.py` — beyond-parity: user + org + token lifecycle via the REST API + (create, read-back, delete), proving admin-API CRUD works. + - `custom/test_lfs_roundtrip.py` — beyond-parity **and the PR-#1 capstone** (see §3). + +5. **`PARITY.md` (NEW)** — map the upstream corpus (`recipe-info/gitea/tests/health_check.py`, + `git_push.py`) to the cc-ci ports; document the beyond-parity custom tests + why they're + non-vacuous; record that backup/restore are REAL tiers (backupbot labels present); record + the sqlite3 DB choice; declare any deferrals with reasons (`EXPECTED_NA` for anything that + structurally can't run on `main`, e.g. LFS before PR #1 merges — see §3). + +## 2. The gitea-dep conflict — the central design problem (READ FIRST) + +`meta_mod.load("gitea")` returns ONE object used by BOTH roles. The two roles never run +concurrently (different `RECIPE` values: `RECIPE=drone` provisions gitea as a dep; +`RECIPE=gitea` is the recipe-under-test), but they SHARE `recipe_meta.py`. Therefore: + +- **Hard gate: drone CI stays green.** After any `recipe_meta.py` change, run drone through + its CI/`!testme` path and confirm GREEN — gitea-as-dep provisioning (`sso.setup_gitea_oauth`, + `tests/unit/test_gitea_dep.py`) must be unaffected. Treat a drone regression as a failed + gate, never "acceptable collateral". +- **Do not let recipe-under-test config leak into the dep deploy.** In particular the LFS + overlay (§3) must apply ONLY when gitea is the recipe-under-test, never to drone's dep + deploy. Investigate whether `EXTRA_ENV(ctx)` can distinguish the roles (e.g. via `ctx.op` / + `ctx.deps` / domain shape) or whether the overlay belongs on a recipe-under-test-only knob. + **Decide, document the mechanism in PARITY.md + DECISIONS.md, and prove both paths.** +- If a clean split proves impossible inside one `recipe_meta.py`, raise it in STATUS before + forking any shared harness behaviour — do not silently special-case gitea in `runner/`. + +## 3. Verify the LFS PR (#1) — the capstone + +PR #1 (`lfs-plain-gitea` → `main`) adds **opt-in** Git LFS to plain gitea: a new +`compose.lfs.yml` overlay (`GITEA_LFS_START_SERVER=true` + a mounted `lfs_jwt_secret`), an +`app.ini.tmpl` change emitting a stable `LFS_JWT_SECRET`, `.env.sample` docs, and a version +bump `3.5.2 → 3.6.0`. The PR's own "Tested on cctest" evidence is exactly the test to encode: +**an LFS object upload→download round-trip via the batch API (downloaded bytes hash to the +OID), plus the LFS JWT secret stable across `abra app restart`** (the regression it fixes). + +- `custom/test_lfs_roundtrip.py` enables LFS (`COMPOSE_FILE` includes `compose.lfs.yml`; + generate the `lfs_jwt_secret` via `abra app secret generate … lfs_jwt_secret v1` + + `SECRET_LFS_JWT_SECRET_VERSION=v1`, length 43 — wire through `EXTRA_ENV`/`ops.pre_install` + for the recipe-under-test deploy only, never the drone dep), then: init a repo with + `git lfs`, push an LFS-tracked object, fetch it from a clean clone, assert the OID hash + matches; then `abra app restart` and assert the rendered `app.ini` `LFS_JWT_SECRET` is + unchanged and tokens still validate. +- **Because `compose.lfs.yml` is NEW in PR #1, this is the proof the suite verifies the PR:** + on gitea `main` the overlay is absent → the LFS test **structurally skips** (declare it in + `EXPECTED_NA` with reason "LFS overlay lands in PR #1"); on the `lfs-plain-gitea` head the + overlay exists → the LFS test **runs and must pass**. Red/absent on main, green on the PR. +- **How to verify against the PR:** once the suite is green on `main`, run the harness with the + PR head checked out — the `ci-test-review`/`recipe-upgrade` PR path (`RECIPE=gitea + REF=lfs-plain-gitea` via `verify-pr.sh`), and/or post `!testme` on PR #1 so the result lands + in the PR for the operator. Full lifecycle (install/upgrade/backup/restore/custom) must stay + green on the PR head, AND the LFS capstone must go green there. +- **We VERIFY PR #1; we do NOT merge it** (operator's call — mirror PR-only rules). If the + verification surfaces a real defect in the PR, leave a PR comment per the `recipe-upgrade` + discipline; do not "fix" it by weakening the test. + +## 4. Gates + +**M1 — Suite built + green locally (on gitea `main`).** All chosen tiers green on the harness +path with evidence: install + upgrade + backup + restore + custom (`test_health`, +`test_git_push`, `test_admin_api`) + lint (L5) + screenshot. `recipe_meta.py` updated WITHOUT +breaking drone — **drone's gitea-dep deploy still green (proven, not assumed)**. The LFS test +correctly **skips** on `main` (overlay absent), declared in `EXPECTED_NA`. Unit tests for any +new harness-visible surface; no gate weakening anywhere. Adversary cold-verifies from a clean +checkout: tests have teeth (a misconfigured gitea fails install/health; `git_push` actually +pushes + verifies via API; the backup/restore mutation genuinely diverges then reverts); +declared skips justified against the published recipe; **no drone regression**; the +dep-vs-recipe-under-test split (§2) is real and documented. + +**M2 — Proven in real CI + PR #1 verified.** Full gitea lifecycle green via the real CI / +`!testme` path on `main`; drone CI re-confirmed green (dep path intact); screenshot real + +visually verified; level recorded under the de-capped semantics; canonical/warm enrollment +decision documented. **Then the PR-#1 verification:** harness on `lfs-plain-gitea` head → +full lifecycle green AND the LFS round-trip + JWT-stability capstone GREEN (and demonstrably +red/skipped on `main`), result posted to PR #1; nothing merged. Operator summary in +STATUS-gtea.md. Fresh Adversary PASS on both milestones → `## DONE`. + +## 5. Guardrails (binding) + +- **Drone stays green — the dep path is sacred.** Never relax/weaken gitea's dep behaviour to + make the standalone suite pass. A drone regression fails the phase. +- **Recipe mirrors are PR-only.** Never push the mirror's `main`, never merge. PR #1 is the + operator's — we verify it (run the harness / post `!testme`), we do not merge it. A recipe + defect found → PR/comment per `recipe-upgrade`, operator decides. +- **Never weaken a test** to turn red green. Bounded changes (the enrollment + LFS capstone), + not harness rewrites. No new meta keys without registry + doc regeneration. +- **Secrets** (gitea admin password, OAuth client secret, `lfs_jwt_secret`) are generated + per-run and MUST stay out of logs/commits/artifacts — manifest redaction rules apply. +- **Shared swarm:** ≤2–3 concurrent deploys; one `dev-`/test gitea at a time; **tear down + every deploy on every exit path** (success, red, or abort), dep included. The drone path + brings up two deploys (gitea + drone) — budget for it. +- Host changes are orchestrator/operator-only (file them in STATUS, don't improvise). Commit + author `autonomic-bot `; push every commit. + +## 6. Definition of Done + +gitea enrolled as a fully-tested recipe (install/upgrade/backup/restore + custom functional +tests + lint + screenshot) green in **real CI**, **without breaking drone's gitea-dep path**; +PARITY.md complete (upstream `health_check`/`git_push` ported, beyond-parity tests + LFS +documented); the LFS PR #1 (`lfs-plain-gitea`) **verified GREEN** via the LFS capstone +(round-trip + JWT-secret stability) on the PR head, with the same test demonstrably +red/skipped on `main` — proving the suite catches the feature; result posted to PR #1, nothing +merged; levels/records reconciled; M1 + M2 fresh Adversary PASSes recorded in REVIEW-gtea.md.