# CMS ↔ Forgejo integration as a SAMA stress-test ## Goal Make every admin direct-write a real git commit in `git.tdd.md/syntaxai/tdd.md`, without putting a `.git` working tree, ssh keys, or the `git` binary's repo-write capabilities inside the container. The container only ever speaks HTTP to Forgejo (which it already does for users/repos/webhooks). This closes the loop: admin edits become permanent in git automatically, no manual download-patch-and-commit bridge required. ## Architecture delta ``` BEFORE AFTER admin POST /edit/... admin POST /edit/... → Bun.write content/...md ←┐ → Bun.write content/...md → SQLite proposal=approved │ → fetch Forgejo PUT contents → render "applied live" │ → real commit in syntaxai/tdd.md │ → SQLite proposal=approved + sha manual: download patch ─────┘ → render "applied live + commit" manual: git commit on dev manual: deploy ``` Layer touchpoints: | Layer | File | Change | |---|---|---| | c14 | `c14_forgejo.ts` | + `getFileSha()`, + `commitFile()` | | c31 | `c31_forgejo_commit.ts` (new) | input/result types + validator | | c31 | `c31_forgejo_commit.test.ts` (new) | sibling test | | c13 | `c13_database.ts` | + `forgejo_commit_sha` column on proposals + setter | | c21 | `c21_handlers_edit.ts` | call commitFile in admin fork | | c51 | `c51_render_edit.ts` | show commit SHA + Forgejo URL on applied-live + admin detail | ## Failure semantics (the bit SAMA didn't have an opinion on) Order: **Forgejo first, filesystem second**. Trade ~150ms latency for guaranteed consistency. Reasons: 1. Forgejo's `PUT contents` requires the previous SHA → free optimistic concurrency. If two admin tabs save concurrently, the second one fails with 409, we surface a "conflict — refresh and retry" message. 2. If we wrote FS first and Forgejo failed, we'd have a live-but-uncommitted change that vanishes at the next deploy. That recreates the exact problem we're trying to eliminate. 3. The container is the single writer. No one else touches the repo from the admin path, so reading-the-SHA / writing-with-the-SHA is a tight loop with no contention. Failure matrix: | Forgejo result | Filesystem step | Proposal status | UI | |---|---|---|---| | 200/201 | execute write | `approved` + commit_sha | "applied live · commit ``" | | 409 conflict | skip | `pending` (kept for retry) | "conflict — content changed elsewhere; refresh" | | 5xx / network | skip | `pending` | "Forgejo unreachable; queued for retry" | | 4xx other | skip | `pending` | "Forgejo rejected: ``" | ## Implementation steps (in order) 1. **c14_forgejo.commitFile()** — pure HTTP wrapper, no business logic. Returns `{ ok: true, sha } | { ok: false, status, message }`. 2. **c31_forgejo_commit.ts** — typed input contract + validator + a parser that turns Forgejo's response shape into the typed result. Gets a sibling test that uses fetch-mocking via `mock.module` or a small interface. 3. **c13_database** — schema migration: `ALTER TABLE proposals ADD COLUMN forgejo_commit_sha TEXT`. Setter `setProposalCommitSha(id, sha)`. `IF NOT EXISTS` semantics via try/catch since SQLite has no `IF NOT EXISTS` for column adds. 4. **c21_handlers_edit admin fork** — call commitFile *before* applyLiveEdit. On success, write FS + setProposalStatus(approved) + setProposalCommitSha. On failure, render an error variant, set status to pending, do NOT touch FS. 5. **c51 render** — applied-live page shows the commit SHA short (7 chars) and links to `https://git.tdd.md/syntaxai/tdd.md/commit/`. Admin proposal detail does the same. 6. **Tests + deploy + verify** — bun test green, Playwright green, deploy to p620, then a curl `GET /api/v1/repos/syntaxai/tdd.md/commits` to confirm the commit really landed. ## Out of scope for this PR - **Deploy script switching to pull-from-Forgejo**. Right now `deploy-tdd-md.sh` rsyncs from dev's working tree. Until that switches to `git pull` from Forgejo on dev (or directly on p620), admin commits in Forgejo will be overwritten on the next deploy unless dev pulls them first. That's a separate, scarier workflow change. - **Webhook from Forgejo back to tdd.md to refresh the in-container `content/`**. Tempting (would make container restart resync) but introduces a feedback loop potential. - **Non-admin proposals committing as branches**. Right now they stay in SQLite as pending. Could become `proposal/` branches in Forgejo. Out of scope. - **GitHub mirror**. Forgejo can mirror to GitHub, that's a Forgejo config, not code. ## SAMA-tension log (filled as we hit them during the build) > A running list. Each entry: the tension, the workaround, and a candidate > SAMA refinement (or "SAMA has nothing to say here, that's fine"). _(empty — start blank, populate as we hit them in the implementation)_ ## Done-criteria - [ ] Admin POST on /edit/sama/skill writes to FS *and* creates a real commit visible at `git.tdd.md/syntaxai/tdd.md/commits/main` - [ ] Applied-live page shows the commit SHA with a working link - [ ] Forgejo down → no FS write, proposal stays pending, UI surfaces the reason - [ ] Concurrent edit (409) handled with a useful error - [ ] All bun tests + Playwright tests green - [ ] SAMA-tension log has at least 2 concrete entries