syntaxai/tdd.md · main · cms-forgejo-plan.md

cms-forgejo-plan.md 114 lines · 5482 bytes 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:

  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 <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)

  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/<sha>. 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/<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