# 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.