Update CLAUDE.md: Forgejo pipeline is live, document runner internals and tradeoffs
All checks were successful
Deploy / deploy (push) Successful in 22s

This commit is contained in:
mattie726 2026-08-25 23:35:49 -07:00 committed by Barely Removable
parent 49e3d20977
commit c161cecc9d

121
CLAUDE.md
View file

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