feat(projects): M1 — the multi-project schema spine (§22)
Lays the additive foundation for hosting N projects per deployment, with today's single corpus as the N=1 case. No behavior change: the app runs exactly as before, single project, with the spine underneath. - migration 025: `projects` + `project_members` tables; seed the `default` project (visibility=public, preserving open-by-default); thread `project_id NOT NULL DEFAULT 'default'` onto all 19 slug-bearing tables, backfilling existing rows. Additive — no table rebuilds; the slug-keyed uniqueness/PK rework is enumerated in the migration header and deferred to the slice that activates project #2. - app/projects.py: §22.13 startup backfill of the default project's content_repo from META_REPO (idempotent — never clobbers a value the future registry mirror sets). - config: REGISTRY_REPO wired (optional through M1; consumed by the M3 mirror), documented in .env.example as META_REPO's successor. - tests: 6 vertical assertions on the spine (seed, backfill, column shape, role CHECK, idempotency, config). Full suite 381 passed. - docs: align Part C M1/M3 boundaries with the landed code (registry mirror + redirect move to M3). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ class Config:
|
||||
gitea_bot_token: str
|
||||
gitea_org: str
|
||||
meta_repo: str
|
||||
registry_repo: str
|
||||
oauth_client_id: str
|
||||
oauth_client_secret: str
|
||||
app_url: str
|
||||
@@ -80,6 +81,13 @@ def load_config() -> Config:
|
||||
gitea_bot_token=_required("GITEA_BOT_TOKEN"),
|
||||
gitea_org=_required("GITEA_ORG"),
|
||||
meta_repo=_optional("META_REPO", "meta"),
|
||||
# §22.2: the multi-project registry repo the framework reads to learn
|
||||
# which projects exist. Optional through Slice M1 — the registry
|
||||
# *mirror* lands in M3; until then the default project (seeded by
|
||||
# migration 025 + the §22.13 startup backfill) is the only project,
|
||||
# and its content_repo comes from META_REPO. Becomes required when
|
||||
# the mirror lands and META_REPO is retired.
|
||||
registry_repo=_optional("REGISTRY_REPO"),
|
||||
oauth_client_id=_required("OAUTH_CLIENT_ID"),
|
||||
oauth_client_secret=_required("OAUTH_CLIENT_SECRET"),
|
||||
app_url=_optional("APP_URL", "http://localhost:8000").rstrip("/"),
|
||||
|
||||
@@ -28,6 +28,7 @@ from . import (
|
||||
invites as invites_mod,
|
||||
otc,
|
||||
passcode as passcode_mod,
|
||||
projects,
|
||||
providers as providers_mod,
|
||||
ratelimit,
|
||||
turnstile,
|
||||
@@ -99,6 +100,7 @@ async def lifespan(app: FastAPI):
|
||||
config = load_config()
|
||||
db.run_migrations(config)
|
||||
db.init(config)
|
||||
projects.seed_default_project(config) # §22.13 step 1
|
||||
gitea = Gitea(config)
|
||||
bot = Bot(gitea)
|
||||
reconciler = cache.Reconciler(config, gitea)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Project registry — the §22 multi-project layer.
|
||||
|
||||
A deployment hosts one or more projects (§22.1). Through Slice M1 the only
|
||||
project is the migration-seeded `default` one (the N=1 case, §22.13); the git
|
||||
registry mirror that lets a deployment declare more projects lands in M3 and
|
||||
will grow this module. For now this holds just the §22.13 step-1 backfill:
|
||||
completing the default project's row from deployment config, which pure-SQL
|
||||
migration 025 could not read.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import db
|
||||
from .config import Config
|
||||
|
||||
DEFAULT_PROJECT_ID = "default"
|
||||
|
||||
|
||||
def seed_default_project(config: Config) -> None:
|
||||
"""Fill the default project's content_repo from config.
|
||||
|
||||
Migration 025 seeds `default` with content_repo NULL because SQL cannot
|
||||
read the environment. This sets it from META_REPO (the pre-multi-project
|
||||
single-corpus repo) the first time the configured app boots, so the row
|
||||
accurately names the corpus the existing rows belong to. Idempotent: only
|
||||
touches the row while content_repo is still unset, so a later registry
|
||||
mirror (M3) that has populated it is never clobbered.
|
||||
|
||||
The display `name` is deliberately left as-is: the deployment's
|
||||
user-visible name lives in the frontend build (VITE_APP_NAME) today and
|
||||
moves to the registry + GET /api/deployment in M3 (§22.9); the backend has
|
||||
no authoritative name to backfill from yet.
|
||||
"""
|
||||
db.conn().execute(
|
||||
"""
|
||||
UPDATE projects
|
||||
SET content_repo = ?, updated_at = datetime('now')
|
||||
WHERE id = ? AND content_repo IS NULL
|
||||
""",
|
||||
(config.meta_repo, DEFAULT_PROJECT_ID),
|
||||
)
|
||||
Reference in New Issue
Block a user