# Local Gitea CI (fallback) > **Status:** fallback / opt-in. The primary CI target is now > `gitea.lan` with its host-mode `act_runner`. The per-stage CI gate > closes against `gitea.lan`, not against this stack. Use this > directory when you want to validate `.gitea/workflows/*` without > reaching `gitea.lan` — for example, when iterating on a workflow > file from a flight without LAN access — or when isolating a runner > issue from production-shaped infrastructure. Self-contained Gitea + Actions runner for verifying `.gitea/workflows/*` honestly before pushing to `gitea.lan`. Runs natively on arm64 (Apple Silicon) — every image below has an arm64 variant, so Docker pulls the right architecture and the runner executes workflow steps without QEMU emulation. ## Prerequisites - Docker (Colima or Docker Desktop) - `python3`, `curl`, `bash` — all built into macOS ## First time ```sh make -C tools/local-ci up ``` This: 1. brings up the Gitea container; 2. creates an admin user (`galaxy` / `galaxy-dev`); 3. creates the `galaxy/galaxy` repo; 4. fetches a runner registration token from the Gitea API; 5. brings up the runner with that token (the runner persists its credentials in a Docker volume and ignores the token on subsequent restarts). The script is idempotent — re-running it is safe. ## Pushing a branch ```sh make -C tools/local-ci push ``` This adds a `local-gitea` remote on the first run and then pushes the current `HEAD`. Equivalent manual flow: ```sh git remote add local-gitea \ http://galaxy:galaxy-dev@localhost:3000/galaxy/galaxy.git git push local-gitea HEAD ``` The Tier 1 workflow fires on `push` to any branch and the Tier 2 workflow fires on tags matching `v*`. Watch runs at: ## Operational targets | Target | What it does | | ---------------- | -------------------------------------------- | | `make up` | Bring up Gitea + runner (idempotent) | | `make down` | Stop both containers (state preserved) | | `make logs` | Tail logs from both containers | | `make status` | Show container status | | `make push` | Push current `HEAD` to local Gitea | | `make clean` | Stop and wipe all local state (full reset) | ## What's in the box | Component | Image | Role | | ---------- | ---------------------------------- | ------------------------------------------- | | Gitea | `gitea/gitea:1.23` | Server with SQLite backend | | act_runner | `gitea/act_runner:0.6.1` | Single-capacity runner registered on boot | | Workflow | `catthehacker/ubuntu:act-latest` | Image spawned per job (multi-arch) | The runner mounts the host Docker socket and spawns workflow containers on the same Docker network as Gitea, so `actions/checkout` reaches the server at `http://gitea:3000` from inside spawned containers. ## Caveats - Gitea's `ROOT_URL` is set to `http://gitea:3000/` so spawned workflow containers reach the server through the compose network. The web UI works at `http://localhost:3000` via port mapping, but copy-paste URLs in the UI may show `gitea:3000` instead of `localhost:3000`. Harmless for local dev; switch the host part by hand when copying. - The runner is single-capacity (`runner.capacity: 1` in `config.yaml`). Concurrent jobs queue. Bump if you need parallel jobs. - First push from a fresh checkout uploads the full repo history (~tens of MB). Subsequent pushes are deltas. - `actions/upload-artifact@v4` requires Gitea ≥ 1.21 — we pin `1.23` to stay above the cutoff. - Workflow steps run as `root` inside the spawned container; this matches the upstream catthehacker behaviour. Keep that in mind if you add steps that touch host-mounted directories. - On Apple Silicon the runner image and its catthehacker child run natively as arm64. Some pre-built tools that ship in the image are amd64-only and would fall back to QEMU; `setup-go`, `setup-node`, and `pnpm/action-setup` all download arm64 binaries themselves, so the workflow steps we care about stay native.