cms-forgejo-plan.md
raw
· source
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:
- Forgejo's
PUT contentsrequires 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. - 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.
- 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 <sha>" |
| 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: <message>" |
Implementation steps (in order)
- c14_forgejo.commitFile() — pure HTTP wrapper, no business logic.
Returns
{ ok: true, sha } | { ok: false, status, message }. - 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.moduleor a small interface. - c13_database — schema migration:
ALTER TABLE proposals ADD COLUMN forgejo_commit_sha TEXT. SettersetProposalCommitSha(id, sha).IF NOT EXISTSsemantics via try/catch since SQLite has noIF NOT EXISTSfor column adds. - 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.
- c51 render — applied-live page shows the commit SHA short (7 chars) and
links to
https://git.tdd.md/syntaxai/tdd.md/commit/<sha>. Admin proposal detail does the same. - Tests + deploy + verify — bun test green, Playwright green, deploy to
p620, then a curl
GET /api/v1/repos/syntaxai/tdd.md/commitsto confirm the commit really landed.
Out of scope for this PR
- Deploy script switching to pull-from-Forgejo. Right now
deploy-tdd-md.shrsyncs from dev's working tree. Until that switches togit pullfrom 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/<id>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