# Self-hosted Woodpecker CI for the MGF federation

Codeberg's shared CI (`ci.codeberg.org`) is approval-gated and denied `magogi-admin`,
so the federation runs **its own** Woodpecker. This bundle stands up server + agent +
auto-TLS (Caddy) against the Codeberg (Forgejo) forge. **Gate-only** — no publish
secrets; `mgf-fed release-remote` still publishes. Rule context (WF-01 "CI runs on
push/PR" + WF-02 "gates block merge"): `../../../mgf-standard/docs/recipes/activate-ci.md`.

> 📸 **New here? Read the illustrated, screenshot-backed walkthrough first:**
> [`SETUP-WALKTHROUGH.md`](SETUP-WALKTHROUGH.md). This README is the terse runbook.
>
> ✅ **Reference build is live** (2026-06-16): `woodpecker-prod` on Infomaniak
> `dc3-a`, `https://195.15.241.48.sslip.io`, server+agent+caddy healthy, Codeberg
> OAuth login working.

## The infra is one command (`mgf-fed provision-ci-host`)

`mgf-fed provision-ci-host` creates the security group (22/80/443) + a VM with a
Docker-installing cloud-init that drops this bundle into `/opt/woodpecker/`, on
Infomaniak's `ext-net1` (direct-public — no floating IP needed). Secrets stay OUT of
user-data; the `.env` is added afterward.

```sh
# 1. an SSH key to reach the host (register the .pub in OpenStack as "woodpecker-key")
ssh-keygen -t ed25519 -f my_stuff/infomaniak/woodpecker_id_ed25519 -N ''

# 2. provision (2 vCPU / 4 GB / 40 GB) — prints the public IP + a next-steps checklist
mgf-fed provision-ci-host \
  --hostname woodpecker --env prod \
  --flavor a2-ram4-disk20-perf1 --volume-size 40 --key-name woodpecker-key \
  --clouds-yaml my_stuff/infomaniak/PCU-HOESFJV-clouds.yaml --cloud PCP-HOESFJV-dc3-a \
  --yes      # --dry-run to plan without touching the cloud
```

## What you still provide (the two browser steps)

1. **A domain** pointing at the host IP. Quickest: **`<IP>.sslip.io`** (no DNS setup
   — auto-resolves; Let's Encrypt issues for it). Or an A record `woodpecker.<domain>`.
2. **A Codeberg OAuth2 app** — Codeberg → Settings → Applications → *Create a new
   OAuth2 Application*:
   - **Redirect URI:** `https://<your-domain>/authorize` (exact match).
   - ⚠️ **Check "Confidential Client"** — otherwise login fails with
     `PKCE is required for public clients` (Woodpecker is a server-side client).
   - copy the **Client ID** + **Client Secret**.

## Bring it up

The provisioner pre-fills `WOODPECKER_AGENT_SECRET` + `WOODPECKER_ADMIN` in
`/opt/woodpecker/.env`. Fill the three remaining values and start it (over SSH):

```sh
ssh -i my_stuff/infomaniak/woodpecker_id_ed25519 ubuntu@<host-ip>
sudo sed -i 's|^WOODPECKER_HOST=.*|WOODPECKER_HOST=https://<domain>|'      /opt/woodpecker/.env
sudo sed -i 's|^WOODPECKER_DOMAIN=.*|WOODPECKER_DOMAIN=<domain>|'          /opt/woodpecker/.env
sudo sed -i 's|^WOODPECKER_FORGEJO_CLIENT=.*|WOODPECKER_FORGEJO_CLIENT=<id>|'     /opt/woodpecker/.env
sudo sed -i 's|^WOODPECKER_FORGEJO_SECRET=.*|WOODPECKER_FORGEJO_SECRET=<secret>|' /opt/woodpecker/.env
cd /opt/woodpecker && sudo docker compose up -d        # Caddy gets TLS in ~30–60 s
```

Verify: `sudo docker compose ps` (all healthy) and `curl -I https://<domain>/`
(HTTP/2 200). Open `https://<domain>` → **Login with codeberg.org** → **Authorize**
(grant the `magogi-admin` org) → you're in as admin.

## Private registry auth (CI must read the Codeberg PyPI registry)

Each generated `.woodpecker.yml` installs deps from the **private** Codeberg PyPI
registry (`--index-url …codeberg.org/api/packages/magogi-admin/pypi/…`). Without
auth, every build's install step fails (`No matching distribution found for
mgf-standard`; pip 401 → EOF). The fix is a **netrc mounted into every build step**
(the agent's `WOODPECKER_BACKEND_DOCKER_VOLUMES`, already wired in the compose):

```sh
# on the host — a read:package Codeberg token (Settings → Applications → Access Tokens)
printf 'machine codeberg.org login magogi-admin password <READ_PACKAGE_TOKEN>\n' \
  | sudo tee /opt/woodpecker/netrc >/dev/null
sudo chmod 600 /opt/woodpecker/netrc
cd /opt/woodpecker && sudo docker compose up -d woodpecker-agent
```

pip then authenticates to the existing `--index-url` host via netrc — no pipeline
change, clean logs. (`WOODPECKER_ENVIRONMENT` is **not** usable for this: its
`KEY:VALUE` parser mangles the URL's `https://` → `https=//`.) The token is
read-only and stays on the host (gitignore `/opt/woodpecker/netrc`).

## Then enable repos (finish WF-11)

**Repositories → + Add repository** → enable the `mgf-*` repos (this creates each
repo's webhook), **or** hand me a Woodpecker API token (user settings) and I'll
enable + set each repo to **push + pull_request only** + verify a green pilot
(`mgf-common` + `mgf-alembic`) + set Codeberg branch-protection via the API.

## Gate-only / tags

No publish secrets are set, so the generated pipeline's `event: tag` publish steps
must not run-and-fail. Easiest: set each repo to build on **push + pull_request
only** in Woodpecker. `mgf-fed release-remote` keeps doing the publishing.

## Upkeep (the real cost of self-hosting)

Pin + update the server/agent/caddy image tags (security), watch disk (build
volumes), treat the host as **federation-critical infra**. Install the agent
**self-heal cron** (`self-heal.sh`, see SETUP-WALKTHROUGH §5) — it reaps leaked
`wp_*` Docker debris every 5 min and un-wedges a stalled agent
(`worker_count`→0). Tear down: `mgf-cloud vm delete woodpecker-prod --yes` then
delete its boot volume (`delete_on_termination=False`). Fallback: apply for
Codeberg shared-CI access.

> `.env` holds the OAuth secret — it stays only on the host and is gitignored here
> (`deploy/woodpecker/.env`). The bundle ships only `.env.example`.
