Files
Gitea/docs/migrate-to-gitea-scm.md
JMR-devandClaude Opus 5.5 ec4de0c6c4 Run Gitea Actions jobs on a dedicated GKE cluster
gitea:runnersEnabled creates a zonal GKE Standard cluster, used for
nothing else, running the official gitea/runner image in rootless
Docker-in-Docker, so workflows can `docker build`. Off by default; with
the gate off the stack only gains the container API.

- private spot nodes, egress through Cloud NAT on a static IP; control
  plane reachable only through its IAM-gated DNS endpoint
- the registration token is a Secret Manager secret created by
  scripts/bootstrap.sh; Gitea reads it from a file, the runner pod
  mounts it through the Secret Manager CSI add-on with Workload
  Identity. Pulumi never sees the value
- a NetworkPolicy keeps job containers off the metadata server and
  private ranges, so jobs hold no GCP identity
- Caddy routes /api/actions, /api/actions_pipeline and the artifact
  service around the WAF (protobuf and PUT/PATCH uploads); fail2ban
  ignores the NAT address
- docs/runners.md: threat model, cost, enable/disable, token rotation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:31:15 +07:00

2.9 KiB

Moving CI off GitHub and onto this Gitea instance

Today the repository lives on GitHub and Cloud Build watches it through a cloudbuildv2 connection. The intent is to move the repository into the Gitea instance this repo deploys. Doing that well is mostly about not building a loop that can strand itself.

The bootstrapping problem

The runner that deploys the VM must not live on the VM it deploys. If a Gitea Actions runner on gitea-vm executes the infra pipeline, then any change that restarts Gitea — a config sync, an image rollout, a reboot — kills the job mid-flight. Worse, a bad change locks you out of the tool needed to fix it.

Two workable shapes:

  1. Keep Cloud Build as the executor. Gitea fires a webhook; Cloud Build runs the build. Cloud Build has no first-class Gitea trigger, so this means a small authenticated endpoint (Cloud Run or a Cloud Function) that validates the webhook signature and calls cloudbuild.projects.triggers.run. The deploy path stays entirely outside the VM. This is the recommended option.
  2. A Gitea Actions runner on separate infrastructure. That infrastructure now exists: gitea:runnersEnabled creates a dedicated GKE cluster (runners.md). But it is built for CI, not deploys. Job pods deliberately hold no Google Cloud identity, and the threat model assumes jobs cannot reach GCP. Running the infra pipeline there would mean giving a deploy-labelled job a Workload Identity that can run pulumi up with cb-infra@-level permissions, from a pod whose jobs can already read each other's runner credentials. That needs its own design. It is not a config change.

Either way, keep the GitHub trigger working until the replacement has successfully deployed at least once.

Order of operations

  1. [actions] ENABLED = true is already set in vm/config/app.ini.tmpl, so no Gitea-side config change is needed to start.
  2. Mirror the repository into Gitea and verify history, LFS objects, and tags.
  3. Stand up the chosen executor and prove it can run cloudbuild/image.yaml end to end, including the IAP rollout step.
  4. Point gitea:githubOwner at nothing / remove the GitHub trigger. The Pulumi program already tolerates GitHub being unconfigured — build.New logs a warning and skips the triggers — so the stack still applies cleanly.
  5. Keep the GitHub repo as an archived mirror for a while. It is the cheapest possible disaster recovery for the repo that contains the deployment.

Do not forget

  • The cb-infra@ service account and the Pulumi state bucket are created by scripts/bootstrap.sh, not by Pulumi. They survive this migration untouched.
  • Backups (gitea-backup.timer) become considerably more important once the repository that describes the infrastructure lives on the infrastructure. Verify a restore before you cut over, not after.