779ba6db59
Brings the §1 bot wrapper, the §4 cache (webhook + reconciler), the §5 schema (six numbered migrations), Gitea OAuth + §6 user provisioning, the §7 catalog left pane, and the propose-to-merge vertical: propose modal opens an idea PR against the meta repo, an owner merges from the pending-idea view, the cache picks it up via webhook or reconciler sweep, and the catalog renders the new super-draft. Per §1 the bot is the only Git writer; every commit, branch creation, and PR merge carries the §6.5 On-behalf-of: trailer and an `actions` audit row. Per §4 the cache is never written from a user action — it's webhook+reconciler only. Covered by `backend/tests/test_propose_vertical.py` against an in-process Gitea simulator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
158 lines
5.1 KiB
Python
Executable File
158 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Seed a fresh meta repository on a local Gitea instance.
|
|
|
|
Creates `<org>/<meta_repo>` if it does not exist, seeds it with the
|
|
hand-authored files §2 describes (README.md, PHILOSOPHY.md,
|
|
CONTRIBUTING.md, the workflow file, and an empty rfcs/ directory),
|
|
and registers the webhook the app needs per §4.1.
|
|
|
|
Run this once after standing up Gitea + the bot account + the .env.
|
|
Re-running is safe; everything is upsert-shaped.
|
|
|
|
Usage:
|
|
cd backend && .venv/bin/python ../scripts/seed_meta_repo.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import base64
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "backend"))
|
|
|
|
from app.config import load_config # noqa: E402
|
|
from app.gitea import Gitea, GiteaError # noqa: E402
|
|
|
|
|
|
PHILOSOPHY_PATH = Path(__file__).resolve().parent.parent / "PHILOSOPHY.md"
|
|
|
|
README_HEADER = """# Wiggleverse RFCs
|
|
|
|
*A standards process for shared meaning between humans and machines.*
|
|
|
|
This repository is the meta repo for the Wiggleverse RFC framework — the
|
|
authoritative directory of every RFC in the system, super-drafts and
|
|
graduated entries alike. Every active or in-progress RFC has exactly one
|
|
markdown file in `rfcs/` describing it; once graduated, the RFC itself
|
|
lives in its own dedicated repository, linked from its entry here.
|
|
|
|
See [PHILOSOPHY.md](./PHILOSOPHY.md) for the full statement of why this
|
|
framework exists.
|
|
|
|
<!-- INDEX:START -->
|
|
*The index below is regenerated by CI on every merge to main.*
|
|
<!-- INDEX:END -->
|
|
"""
|
|
|
|
CONTRIBUTING = """# Contributing to the Wiggleverse RFC framework
|
|
|
|
All contribution flows through the RFC app, which talks to this Gitea
|
|
instance on your behalf via a single bot service account. Raw `git
|
|
clone` plus `git push` is not a supported contribution path.
|
|
|
|
To propose a new RFC, sign in at the app and use the **"+ Propose New
|
|
RFC"** affordance at the bottom of the catalog. Your proposal opens a
|
|
PR against this repository adding one file under `rfcs/`. An owner or
|
|
admin reviews and either merges (creating the super-draft) or declines
|
|
(with a comment to you).
|
|
|
|
See the app for the full revision, conversation, and graduation flows.
|
|
"""
|
|
|
|
ACTIONS_WORKFLOW = """# Regenerates README.md's index section on every merge to main.
|
|
# See §2 of SPEC.md. Implementation is a follow-up — this workflow
|
|
# file is present as a marker so the meta repo's shape matches the
|
|
# spec from day one.
|
|
name: regenerate-readme-index
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
noop:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo "Index regeneration is a follow-up (see SPEC §2)"
|
|
"""
|
|
|
|
|
|
async def main() -> None:
|
|
config = load_config()
|
|
gitea = Gitea(config)
|
|
try:
|
|
await ensure_repo(config, gitea)
|
|
await seed_files(config, gitea)
|
|
await ensure_webhook(config, gitea)
|
|
finally:
|
|
await gitea.close()
|
|
|
|
|
|
async def ensure_repo(config, gitea: Gitea) -> None:
|
|
existing = await gitea.get_repo(config.gitea_org, config.meta_repo)
|
|
if existing:
|
|
print(f"meta repo {config.meta_repo_full} already exists")
|
|
return
|
|
print(f"creating meta repo {config.meta_repo_full}")
|
|
# Create unauto-initialized so we control the initial commit.
|
|
await gitea._request( # noqa: SLF001 — bootstrap-only direct call
|
|
"POST",
|
|
f"/orgs/{config.gitea_org}/repos",
|
|
json={
|
|
"name": config.meta_repo,
|
|
"description": "Wiggleverse RFC framework — meta repository",
|
|
"private": False,
|
|
"auto_init": True,
|
|
"default_branch": "main",
|
|
},
|
|
)
|
|
|
|
|
|
async def seed_files(config, gitea: Gitea) -> None:
|
|
"""Write README.md, PHILOSOPHY.md, CONTRIBUTING.md, and the workflow.
|
|
|
|
Each file is created if missing, left alone if present — re-running
|
|
the seed will not overwrite manual edits made post-bootstrap.
|
|
"""
|
|
files = {
|
|
"PHILOSOPHY.md": PHILOSOPHY_PATH.read_text() if PHILOSOPHY_PATH.exists() else "(placeholder)\n",
|
|
"README.md": README_HEADER,
|
|
"CONTRIBUTING.md": CONTRIBUTING,
|
|
".gitea/workflows/regenerate-readme-index.yml": ACTIONS_WORKFLOW,
|
|
"rfcs/.gitkeep": "",
|
|
}
|
|
for path, content in files.items():
|
|
existing = await gitea.get_contents(config.gitea_org, config.meta_repo, path, ref="main")
|
|
if existing:
|
|
print(f" {path} already present — skipping")
|
|
continue
|
|
print(f" seeding {path}")
|
|
await gitea.create_file(
|
|
config.gitea_org,
|
|
config.meta_repo,
|
|
path,
|
|
content=content,
|
|
message=f"Bootstrap: add {path}",
|
|
branch="main",
|
|
)
|
|
|
|
|
|
async def ensure_webhook(config, gitea: Gitea) -> None:
|
|
if not config.webhook_secret:
|
|
print("skipping webhook setup (GITEA_WEBHOOK_SECRET not set)")
|
|
return
|
|
url = f"{config.app_url}/api/webhooks/gitea"
|
|
print(f"ensuring webhook on {config.meta_repo_full} -> {url}")
|
|
await gitea.ensure_webhook(
|
|
config.gitea_org,
|
|
config.meta_repo,
|
|
url=url,
|
|
secret=config.webhook_secret,
|
|
events=["push", "pull_request", "create", "delete", "repository"],
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|