From c161cecc9d8a431203698972466ab3c3dfb8aa21 Mon Sep 17 00:00:00 2001 From: mattie726 Date: Tue, 25 Aug 2026 23:35:49 -0700 Subject: [PATCH] Update CLAUDE.md: Forgejo pipeline is live, document runner internals and tradeoffs --- CLAUDE.md | 121 +++++++++++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 55 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ef933c4..923ec77 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,66 +70,77 @@ container user (`99:100` / `nobody:users`), healthcheck, log rotation, `init: true`. Reverse-proxied by Nginx Proxy Manager at `hippofam.com/cards` with per-user HTTP Basic Auth. -**Current deploy mechanism (as of 2026-08-25): manual.** Changes are copied -to the server by hand (`scp`) and the container is rebuilt over SSH; the two -copies of the repo (local Mac, server) are kept in sync by committing on -both sides. There is currently no automated push-to-deploy path connected — -see below. +**Current deploy mechanism (live as of 2026-08-25): Forgejo push-to-deploy.** +Self-hosted Forgejo (soft-fork of Gitea) runs as its own container on the +same Unraid box (`/mnt/user/appdata/forgejo`), proxied at +`https://git.hippofam.com` via Nginx Proxy Manager (wildcard Let's Encrypt +cert issued via Porkbun DNS-01 challenge — Cox blocks inbound port 80, so +the default HTTP-01 challenge doesn't work on this network; DNS challenge +sidesteps that entirely). Forgejo Actions is enabled instance-wide, with a +self-hosted runner (`/mnt/user/appdata/forgejo-runner`, container name +`forgejo-runner`) registered **globally** (not repo-scoped — see tradeoff +below). -**Superseded approach, fully torn down: git-shell restricted deploy.** A +This repo has a `forgejo` remote (`https://git.hippofam.com/ninja_hippo/card-grader.git`, +HTTPS not SSH — see note below) and pushing to `main` triggers +`.forgejo/workflows/deploy.yml`, which rsyncs the checkout into +`/mnt/user/appdata/card-grader` (excluding `.git`, `.env`, `data/` — those +stay server-side only) and runs `docker compose up -d --build`. Typical +deploy completes in well under a minute. + +**Why HTTPS, not SSH, for git:** Forgejo's git-SSH listens on host port +`2222` (port 22 was already taken by Unraid's own sshd), but the router +only forwards 80/443 to this box — 2222 was never opened externally, and +setting that up requires the user's own router admin access, which Claude +doesn't have. Git auth instead uses a Forgejo access token stored in macOS +Keychain via `git config --global credential.helper osxkeychain` — pushing +just works, no per-push prompt. + +**Runner internals, only useful if the pipeline breaks:** the runner's job +containers are the `catthehacker/ubuntu:act-latest` image (has git/Node +bundled for the `actions/checkout` JS action, but *not* rsync — installed +as an explicit workflow step each run) with the host Docker socket +automounted (`container.docker_host: automount` in the runner's +`config.yaml`) and `/mnt/user/appdata` bind-mounted into every job +container at the identical path (`container.options`), which also required +adding `/mnt/user/appdata` to `container.valid_volumes` (empty by default — +Forgejo silently drops any bind mount not explicitly allow-listed there, +with only a debug-log line, no visible job failure). The identical-path +mount trick matters because `docker compose`'s relative volume paths are +resolved by the *host* daemon (Docker-outside-of-Docker via the automounted +socket), so a mismatched path silently bind-mounts the wrong location. + +**Known tradeoff, accepted, not yet resolved:** the runner is registered +globally rather than scoped per-repo, and it has full Docker socket access +— a workflow file pushed to *any* repo on this Forgejo instance can control +any container on the box, not just its own. The access boundary is "who +can push a workflow file to a repo on this instance," not anything +narrower. This runner also now serves three other projects on the same +box — `lunch-notifier`, `n64-tracker`, `arr-summary` — each with the +identical deploy pattern (see their own `CLAUDE.md`s). Revisit with a +`docker-socket-proxy` (scoped per compose project) if this needs tightening +later; not done yet because the basic pipeline mattered more first. + +**Superseded, fully torn down: git-shell restricted deploy.** A `carddeploy` service account (login shell `git-shell`, one bare repo, a `post-receive` hook triggering a single root-owned deploy script via one narrowly-scoped `sudo` rule) was built, tested, and verified working -end-to-end — then explicitly torn down at the user's request in favor of -Forgejo (below). Nothing from it remains: the account, sudoers rule, bare -repo, `/boot/config/go` persistence block, and the `sshd_config` -`AllowUsers` addition were all removed and verified reverted. The local git -remote and SSH keypair for it were deleted too. If this pattern (restricted -git-shell account + sudo-scoped deploy script) is useful as a reference for -scoping *other* access requests later, it's a solid model — it was -abandoned for tooling reasons (moving to a real git host), not because it -didn't work. +end-to-end — then explicitly torn down in favor of Forgejo. Nothing from it +remains: account, sudoers rule, bare repo, `/boot/config/go` persistence +block, and the `sshd_config` `AllowUsers` addition were all removed and +verified reverted. If this pattern (restricted git-shell account + +sudo-scoped deploy script) is useful as a reference for scoping *other* +access requests later, it's a solid model — abandoned for tooling reasons +(moving to a real git host), not because it didn't work. -**In progress: Forgejo.** Decision made 2026-08-25 to self-host Forgejo -(soft-fork of Gitea) as the git remote and CI/CD trigger, replacing the -git-shell approach entirely. Plan, run by the user themselves (not by -Claude — user explicitly wants to execute server-side setup steps -personally): - -1. Forgejo itself runs as its own Docker container on the same Unraid box - (`/mnt/user/appdata/forgejo`), proxied at `git.hippofam.com` via Nginx - Proxy Manager. -2. This repo gets a `forgejo` remote and is pushed there (full history, - not a fresh copy). -3. Forgejo Actions (instance + repo level) is enabled, and a self-hosted - runner is registered **at the repo level** (not instance-wide) as its - own container (`/mnt/user/appdata/forgejo-runner`), with the host - Docker socket mounted in so it can rebuild the `card-grader` container. -4. A `.forgejo/workflows/deploy.yml` workflow (checkout + `docker compose - up -d --build` on push to `main`) does the actual deploy. - -**Known tradeoff, not yet resolved:** unlike the git-shell mailbox (which -could only ever trigger one hardcoded script), a CI runner executes -whatever a workflow YAML says — mounting the Docker socket gives it control -over *any* container on the box, not just this one. The access boundary -becomes "who can push a workflow file to this repo" rather than a sudoers -rule. If tighter isolation is wanted later, a `docker-socket-proxy` in -front of the runner (scoped to just the `card-grader` compose project) was -proposed but not yet built — revisit if this matters more once the basic -pipeline is working. - -**Status:** walkthrough delivered to the user 2026-08-25; not yet executed. -**Do not assume any of this is live** — confirm current state before -relying on it or making claims about deploy mechanism to the user. - -A prior root SSH key/access to the whole box was explicitly revoked by the -user before any of the above; every access grant since then (including a -one-time temporary root key used only to run the git-shell setup script, -itself removed immediately after) has been narrowly scoped and time-boxed -on request. Treat that as the standing expectation for any future -server-access ask on this project — default to the smallest scope that -does the job, time-box it, and confirm teardown/persistence explicitly -rather than leaving it open-ended. +All server access for this migration was narrowly scoped and time-boxed: a +temporary root SSH key was generated solely to do the Forgejo/runner/NPM +setup work above, then removed from the server's `authorized_keys` and +deleted locally once the pipeline was verified end-to-end working — checked +by confirming the key no longer authenticates. Treat that as the standing +expectation for any future server-access ask on this project — default to +the smallest scope that does the job, time-box it, and confirm +teardown/persistence explicitly rather than leaving it open-ended. ## Conventions and constraints established for this project