diff --git a/backend/.env.example b/backend/.env.example index 245371b..9fa08d9 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -9,19 +9,18 @@ GITEA_URL=http://localhost:3000 GITEA_BOT_USER=rfc-bot GITEA_BOT_TOKEN= -# The Gitea org or user that owns the meta repo and every RFC repo -# the bot will create on graduation. +# The Gitea org or user that owns every RFC repo the bot will create on +# graduation. GITEA_ORG=wiggleverse -META_REPO=meta -# --- Multi-project registry (§22) --- -# The registry repo (under GITEA_ORG) the framework reads to learn which -# projects this deployment hosts. Optional through Slice M1: the registry -# mirror lands in M3, and until then the single default project's content -# repo is taken from META_REPO above. The repo's name is the deployment's -# choice. When the mirror lands, REGISTRY_REPO becomes required and -# supersedes META_REPO. -# REGISTRY_REPO=wiggleverse-registry +# §22.2 — the project registry repo (REQUIRED). The framework reads +# `projects.yaml` at its root to learn which projects exist. The repo name is +# the deployment's choice; the app fails to start if this is unset. +REGISTRY_REPO=registry +# §22.13 — optional id for the bootstrap/default project. Reserved for the +# Plan B re-stamp; leave unset in Plan A (the default project id stays +# `default`). When set, it must match an `id` in projects.yaml. +# DEFAULT_PROJECT_ID=ohm # --- OAuth (Gitea) --- # In Gitea: Site Administration → Applications → Add OAuth2 Application. diff --git a/backend/app/hygiene.py b/backend/app/hygiene.py index 4a18004..7431966 100644 --- a/backend/app/hygiene.py +++ b/backend/app/hygiene.py @@ -299,7 +299,14 @@ async def _delete_branch_via_bot( log.warning("hygiene: cannot delete %s/%s — slug missing from cache", slug, branch) return False if not rfc["repo"]: - owner, repo = config.gitea_org, (projects_mod.default_content_repo(config) or "") + repo = projects_mod.default_content_repo(config) + if not repo: + log.warning( + "hygiene: default project has no content_repo; skipping branch delete for %s/%s", + slug, branch, + ) + return False + owner = config.gitea_org elif "/" in rfc["repo"]: owner, repo = rfc["repo"].split("/", 1) else: diff --git a/backend/app/webhooks.py b/backend/app/webhooks.py index 272a310..56d0412 100644 --- a/backend/app/webhooks.py +++ b/backend/app/webhooks.py @@ -80,16 +80,21 @@ def make_router(config: Config, gitea: Gitea) -> APIRouter: payload = {} repo_full = (payload.get("repository") or {}).get("full_name") or "" registry_full = f"{config.gitea_org}/{config.registry_repo}" - meta_repo = projects_mod.default_content_repo(config) - meta_full = f"{config.gitea_org}/{meta_repo}" if meta_repo else None + content_repo = projects_mod.default_content_repo(config) + if not content_repo: + log.warning("webhook: default project content_repo is unknown; corpus refresh skipped") + content_full = f"{config.gitea_org}/{content_repo}" if content_repo else None try: if repo_full == registry_full: # §22.2: a registry-repo push re-mirrors the projects table. + # Tolerate a malformed projects.yaml (keep last-good rows); let a + # transport error bubble to the outer 500 so an unreachable Gitea + # on a registry push is loud rather than silently dropped. try: await registry_mod.refresh_registry(config, gitea) except registry_mod.RegistryError: log.exception("registry webhook: invalid projects.yaml; keeping last-good") - elif meta_full and (repo_full == meta_full or not repo_full): + elif content_full and (repo_full == content_full or not repo_full): await cache.refresh_meta_repo(config, gitea) await cache.refresh_meta_branches(config, gitea) await cache.refresh_meta_pulls(config, gitea) diff --git a/backend/tests/test_multi_project_spine_vertical.py b/backend/tests/test_multi_project_spine_vertical.py index aa7a3ad..35ce1dd 100644 --- a/backend/tests/test_multi_project_spine_vertical.py +++ b/backend/tests/test_multi_project_spine_vertical.py @@ -110,15 +110,21 @@ def test_registry_mirror_is_idempotent(app_with_fake_gitea): """Re-running the registry mirror is safe — it upserts (overwrites) the projects row from projects.yaml each time without raising. M3 retirement of seed_default_project: the registry mirror is now the sole authority.""" - from app import db + import asyncio + from app import db, registry as registry_mod app, _ = app_with_fake_gitea with TestClient(app): - # After startup the row should have content_repo from projects.yaml. - got = db.conn().execute( - "SELECT content_repo FROM projects WHERE id = 'default'" - ).fetchone()["content_repo"] - assert got == "meta" + before = db.conn().execute("SELECT COUNT(*) AS n FROM projects").fetchone()["n"] + cfg = app.state.config + gitea = app.state.gitea + asyncio.run(registry_mod.refresh_registry(cfg, gitea)) + after = db.conn().execute("SELECT COUNT(*) AS n FROM projects").fetchone()["n"] + assert after == before + row = db.conn().execute( + "SELECT content_repo FROM projects WHERE id='default'" + ).fetchone() + assert row["content_repo"] == "meta" def test_registry_repo_config_wired(app_with_fake_gitea, monkeypatch):