# CI — Forgejo Actions (self-hosted, Tailscale-only admin)

Forgejo Actions is GitHub-Actions compatible at the workflow-syntax layer (derived from Gitea Actions, which re-uses the `actions/*` runtime via `act`). A workflow that runs on GH usually runs on Forgejo with only the runner labels and registry URLs changed. Pair with RULE 0.1 — KeiGit repos MUST stay on private Forgejo, never mirror to github.com.

## Layout

Workflows live under `.forgejo/workflows/*.yml` (primary) — `.gitea/workflows/` also works for legacy repos. Keep the same narrow split as GH:

- `ci.yml` — build + test
- `release.yml` — tag-driven
- `security.yml` — scheduled scanners

## Self-hosted runner

Forgejo has no SaaS runner fleet — you provide the compute. Install `forgejo-runner` [VERIFIED: https://code.forgejo.org/forgejo/runner] on a node that is reachable ONLY over Tailscale.

Registration:

```bash
forgejo-runner register \
  --no-interactive \
  --instance http://100.91.246.53:3000 \
  --name kgl-runner-01 \
  --labels "self-hosted,linux,x64,docker" \
  --token "$FORGEJO_RUNNER_TOKEN"       # from secrets/runner.env (RULE 0.8)
```

`FORGEJO_RUNNER_TOKEN` stays in `secrets/runner.env` — reference via env name only, never paste the literal value.

Target in workflow:

```yaml
jobs:
  build:
    runs-on: [self-hosted, linux, x64]
```

## GitHub-compat surface

Works out of the box: `actions/checkout@v4`, `actions/cache@v4`, `actions/setup-node@v4`, `Swatinem/rust-cache@v2`, shell/docker steps, matrix, reusable workflows (`uses: <forgejo-host>/<owner>/<repo>/.forgejo/workflows/<file>@<sha>`).

Does NOT work: `permissions:` block (Forgejo token is scoped at the runner level, not per-job), OIDC federation to AWS/GCP (no JWKS endpoint served by Forgejo), GitHub-Marketplace actions that call `api.github.com` directly.

Workaround for OIDC: for cloud deploys from Forgejo, prefer short-lived STS tokens minted by a bastion that has an IAM role, passed into the runner via a sealed env file rotated daily.

## Tailscale-only admin posture

Forgejo Web UI is http://100.91.246.53:3000, SSH is `ssh://git@100.91.246.53:2222/...`. Both on Tailscale CGNAT. NEVER bind Forgejo to a public IP — runner tokens, PATs, and repo contents are unfiled patent IP (RULE 0.1).

Key fingerprint for the existing KeiGit host: `SHA256:TxHcs7YuEZiy4Gu0yZOoVidVqlvj8TPC+QgUGjmh0Mw` labelled `macbook`.

## Secrets

Forgejo repo secrets (`Repo → Settings → Actions → Secrets`) mirror GH secrets syntactically: `${{ secrets.FOO }}`. Organisation-scope secrets also supported. Every secret still references the canonical `~/.claude/secrets/.env` / `secrets/*.env` source — repo secrets are cache copies, rotated when the source rotates.

## Forbidden

- Exposing Forgejo port 3000 or 2222 on a public IP
- Running `forgejo-runner` on a host that is also a production application node
- Mirroring a KeiGit repo to github.com to "get free CI" (RULE 0.1)
- Hard-coded runner tokens in workflow YAML (always `${{ secrets.* }}`)
