# 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. - `tea_domains` and `GET /domains` (the dashboard's Domains tab) need the secrets `CLOUDFLARE_API_TOKEN` (Registrar read) and `CLOUDFLARE_ACCOUNT_ID`; without them they say domain search is not set up. ## 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.