Files
teahub/docs/rebuild.md
T
teahubxyzandClaude Opus 5.5 9551a8658a Teahub's look back on prod, and Tea MCP 2.2 for the dashboard's Domains tab
gitea/custom: the old factory's home page and dashboard, ported to Gitea 28.
The signed-out home is Teahub = Git + Tea + Time beside the cup; the
dashboard keeps the feed and puts Domains | Repository | Organization above
the contribution heatmap. Green tea colours for Gitea's light, dark and auto
themes, and the 🍵 mark. gitea/sync-custom.sh puts it on green-tea's volume;
gitea/fly.toml names the site Teahub and sets the meta tags.

mcp 2.2: GET /domains, the old factory's Cloudflare Registrar search (a live
check of the exact names, suggestions, prices). Only GITEA_URL may read it
from a browser; 8 searches a minute per client and 20 in all; answers kept
ten minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYHXFvWMCs1GUmRmpySqQ6
2026-10-14 20:47:34 -07:00

6.6 KiB

Rebuild log

Every step the v1 rebuild needed beyond "clone the spine, run bootstrap". Short means portable. Each line is a step a future rebuild should not have to rediscover.

Gitea on Fly (app green-tea, 2026-10-14)

  • Image docker.gitea.com/gitea:28.0.0 (1.24 until 2026-10-15), SQLite on a 3 GB volume at /data, config via GITEA__* env in gitea/fly.toml in this repo.
  • Upgrading: snapshot the gitea_data volume, change the tag in gitea/fly.toml, fly deploy -a green-tea from gitea/, read the migration lines in the log. A migrated database refuses an older image, so the way back is the snapshot. Rehearse first on a copy of /data (sqlite .backup plus the repos) in Docker.
  • Look: gitea/custom (home and dashboard templates, colours, the 🍵 mark, the dashboard's Domains tab) goes onto the volume with gitea/sync-custom.sh, then a restart. The name and meta tags are env in gitea/fly.toml (GITEA____APP_NAME for the top-level APP_NAME; [ui.meta] is GITEA__ui_0X2E_meta__*). Gitea 28 sets a CSP in a meta tag (script-src * 'nonce-...'), so custom scripts must be files, not inline.
  • Gitea's built-in SSH and the image's sshd collide: START_SSH_SERVER=false.
  • The image's sshd on :22 collides with Fly's hallpass on the 6PN address (kernel panic, init killed): SSH_LISTEN_PORT=2222, service 2222→2222, advertised SSH_PORT=2222.
  • fly deploy of an image-only app does not allocate public IPs: fly ips allocate-v4 --shared + fly ips allocate-v6. Verify from OUTSIDE Fly; fly ssh checks lie about reachability.
  • First admin: gitea admin user create --admin --random-password inside the machine. Boring's exec lane printed the password once (its redaction keys on the word "password" in output; the create command's output didn't contain it). Rotate immediately.
  • Registration: DISABLE_REGISTRATION=false + ALLOW_ONLY_EXTERNAL_REGISTRATION=true, or OAuth sign-up is blocked.
  • GitHub login: gitea admin auth add-oauth --provider github with scopes read:user and user:email (no email claim without it). [oauth2_client] ENABLE_AUTO_REGISTRATION=true, USERNAME=nickname, ACCOUNT_LINKING=auto → no sign-up form. Gitea registers OAuth providers at start: restart after adding one.
  • A user with must_change_password set cannot use API tokens. Promote the GitHub-born admin via sqlite: UPDATE user SET is_admin=1 WHERE lower_name='…'.
  • GitHub OAuth app creation needs sudo mode on github.com for the client secret; an agent driving Chrome cannot clear it. Secret went page → 0600 file → gitea admin auth add-oauth over fly ssh, never through chat.
  • Cert for the real domain before the DNS flip: fly certs add + _acme-challenge CNAME → issued → then A/AAAA. No TLS gap.
  • A failed repos/migrate leaves a private, empty mirror stub; delete it (DELETE /repos/{owner}/{repo} with a token that has all) before creating the real repo.
  • Temp admin tokens: gitea admin user generate-access-token --username … --scopes … --raw over fly ssh. delete:repository is not a scope; write:repository covers DELETE. Tokens made this way show in the user's settings; revoke when done.

Green Tea MCP (app green-tea-mcp)

  • Register the OAuth2 app on Gitea as the owner (POST /user/applications/oauth2, confidential, redirect PUBLIC_URL/auth/callback); pipe client id/secret straight into fly secrets set on the Mac; never print.
  • Volume mcp_data for /data (FastMCP OAuth store). Dockerfile must chown it to the app user or FastMCP silently falls back.
  • IPs and cert as above.
  • First connect from claude.ai after a deploy sometimes fails ("Authentication failed", no /token call); the retry works. Unexplained.
  • FASTMCP_LOG_LEVEL=DEBUG logs tool arguments (file contents). Never leave it on.
  • GET /domains (the dashboard's Domains tab) needs the secrets CLOUDFLARE_API_TOKEN (Registrar read) and CLOUDFLARE_ACCOUNT_ID; without them it answers 503.

Actions runner on Fly (app green-tea-runner, 2026-10-14)

  • Image docker.gitea.com/act_runner:5.0.0-dind (the 0.2.x tags no longer exist; -dind-rootless never gets a daemon up in a Fly VM). Registration token: gitea actions generate-runner-token over fly ssh, set as GITEA_RUNNER_REGISTRATION_TOKEN.
  • Fly's rootfs is overlayfs, so Docker cannot nest overlay on /var/lib/docker: every job container fails with "failed to mount … overlay". Fix: mount the volume at /data and in [experimental] entrypoint symlink /var/lib/docker → /data/docker before exec s6-svscan /etc/s6 (the image's entrypoint).
  • Labels ubuntu-latest:docker://node:22-bookworm. actions/checkout@v5, setup-node@v4 resolve from github.com by default.
  • Logs: GET /api/v1/repos/{o}/{r}/actions/jobs/{job_id}/logs with a token (the web …/runs/N/jobs/0/logs route 404s for the API).

Time on prod (2026-10-14)

  • Migrated once with POST /repos/migrate (service: github, issues, labels, milestones, PRs, releases; mirror: false). Issue numbers survive. The earlier git-only mirror had to be deleted first (a mirror is read-only; the factory's own repo must be native).
  • Gitea reads .github/workflows/; nothing moved. permissions: blocks are ignored. vars.* and secrets.* work. The Actions user is gitea-actions; Gitea fires events for its writes (GitHub does not), so every event-driven workflow filters that login.
  • The Actions token is not a repo admin: it cannot read /collaborators/{u}/permission (403) or org membership. Trust in Time = repo owner, TIME_TRUSTED (repo variable, comma-separated — org owners go here), or a collaborator (GET /collaborators/{u} → 204).
  • API differences that bit: limit not per_page; no sort on issues; pull_request key always present (null); label add/remove by id; issue creation labels by id; POST /pulls/N/merge {"Do":"squash"}; POST /branches; /pulls/N.diff; commit statuses only (no check-runs), each with a status field.
  • Secrets on prod so far: none beyond infra. VENICE_API_KEY on teahubxyz/time is the first product secret and needs a decision entry when added.

Cutover

  • teahub.xyz moved to green-tea by A/AAAA. The old Tea MCP's OAuth client was registered on the old Gitea at teahub.xyz, so the old MCP broke on cutover: old Gitea ROOT_URL/DOMAIN → matcha.teahub.xyz, old MCP GITEA_URL likewise. Then mcp.teahub.xyz moved to the new MCP; the old one is unreachable from the public internet.
  • teahub is native here; time is now native too (see above). GitHub teahubxyz/time is archived with a pointer.

Still Mac-shaped (to retire, #106 on the old instance)

  • Old Gitea, old Tea MCP container, two act runners, launchd entries, tea-worker user, Chrome profiles. None of it is needed by prod.