# Recipe: activate remote Woodpecker CI for a federation repo

> **Audience:** the federation supervisor turning a repo's (generated, WF-13)
> `.woodpecker.yml` from *present-but-dormant* into *actually running + gating*.
> **Makes real:** [WF-01](../standards/WORKFLOWS.md#WF-01) (CI runs on every push
> + PR) and [WF-02](../standards/WORKFLOWS.md#WF-02) (the four gates block merge).
> This is an **operational step, not a numbered rule** — WF-13 only guarantees the
> pipeline is generated + current; activation is what makes it execute.

A repo can ship a perfect `.woodpecker.yml` and still have **zero** CI runs: the
Codeberg repo must be **connected to a Woodpecker instance** (a webhook + repo
enable). Until then `mgf-fed status` "CI present" means *file present*, not *runs*.

## 0. One-time: a Woodpecker API token (human, interactive)
The Woodpecker API needs a bearer token that only an OAuth login can mint:
1. Open **https://ci.codeberg.org**, log in (OAuth via Codeberg), authorize the
   `magogi-admin` org/repos.
2. User settings → **create a personal API token**.
3. Save it (gitignored, AG-18) at `my_stuff/tokens/woodpecker_token.txt`.

```sh
export WP=https://ci.codeberg.org
export WPT=$(cat my_stuff/tokens/woodpecker_token.txt)
# $CB is the Codeberg *forge* API token (repo enable / branch protection / commit
# status below) — a git-forge credential, NOT the devpi publish password.
export CB=$(grep -oE '[0-9a-fA-F]{40}' my_stuff/tokens/codeberg_token.txt | head -1)
```

## 1. Enable the repo on Woodpecker
Woodpecker keys repos by their forge id. Resolve it from Codeberg, then activate:

```sh
RID=$(curl -fsS -H "Authorization: token $CB" \
  "https://codeberg.org/api/v1/repos/magogi-admin/<repo>" | jq .id)
curl -fsS -X POST -H "Authorization: Bearer $WPT" \
  "$WP/api/repos?forge_remote_id=$RID"        # activate
curl -fsS -H "Authorization: Bearer $WPT" "$WP/api/repos" | jq -r '.[].full_name'  # confirm
```

## 2. Gate-only (the federation default)
CI **gates** on push + PR; **publishing stays with `mgf-fed release-remote`** (see
[`remote-release.md`](remote-release.md)). So:
- **Publishing uses the tag-scoped `devpi_password` *org* secret** (+ the literal
  upload user `magogi`) — it already applies fleet-wide, so there is no per-repo
  publish secret to add here.
- Ensure the **release tags `mgf-fed` pushes do not trigger a *failing* CI publish**
  (the generated pipeline's publish steps are `when: event: tag`).
  Use the repo's Woodpecker event config to build on **push + pull_request only**, or
  the agreed `[skip ci]`-on-release-tag convention. (Switching to CI-publish later =
  allow tag events; the `devpi_password` org secret is already in place — nothing to add.)

## 3. Make the gate block merge (WF-02) — Codeberg branch protection
```sh
curl -fsS -X POST -H "Authorization: token $CB" -H 'Content-Type: application/json' \
  "https://codeberg.org/api/v1/repos/magogi-admin/<repo>/branch_protections" \
  -d '{"branch_name":"main","enable_status_check":true,"status_check_contexts":["ci/woodpecker/pr/woodpecker"]}'
```
(Confirm the exact status-check *context* string from a real run before pinning it.)

## 4. Trigger + verify
```sh
# push any commit, or trigger via API:
ID=$(curl -fsS -H "Authorization: Bearer $WPT" "$WP/api/repos" | jq -r '.[]|select(.full_name=="magogi-admin/<repo>").id')
curl -fsS -X POST -H "Authorization: Bearer $WPT" "$WP/api/repos/$ID/pipelines" -d '{"branch":"main"}'
# poll for green — ALWAYS via Woodpecker (self-hosted, unmetered); never loop on Codeberg (rate-budget below, PAPER-110):
curl -fsS -H "Authorization: Bearer $WPT" "$WP/api/repos/$ID/pipelines?perPage=1" | jq -r '.[0].status'
# Codeberg commit-status is rate-metered — one-shot only (e.g. a final cross-check), NEVER in a poll loop:
curl -fsS -H "Authorization: token $CB" "https://codeberg.org/api/v1/repos/magogi-admin/<repo>/commits/$(git -C <repo> rev-parse HEAD)/status" | jq .state
```

## 5. First-run reds are expected
A dormant pipeline's first real run surfaces what was never executed — most often
the per-repo **coverage floor** (CI-skipped integration tests drop coverage below
`--fail-under`). Fix per repo: set `[tool.mgf-fed].coverage_floor` to the real CI
coverage (or fix the test), then `mgf-fed scaffold <repo>` to re-render, and re-run.

## Rollout
Pilot one cornerstone + one library, confirm green end-to-end, then activate the rest
**paced** (Codeberg's shared Woodpecker has fair-use limits). Bespoke pipelines
(`policy.toml [exempt].woodpecker`) activate the same way — they just aren't generated.

## Codeberg API rate-budget — avoiding the 429 wall

Driving the fleet through the **Codeberg REST API** at automation speed — polling
commit-status, opening PRs, restarting pipelines, merging — exhausts Codeberg's shared
fair-use quota and returns **HTTP 429**. The 429 then **blocks merges** (`POST
/pulls/{i}/merge` 429s) for roughly an **hourly** window, so an agent can wedge itself
out of the very merges it was driving. (Observed: after ~9 PR opens + 8 commit-status
polls every ~20s + 5 pipeline restarts, *every* merge 429'd even spaced 25s apart; it
recovered only after **all** Codeberg calls stopped.) Budget for it:

- **Poll Woodpecker, never Codeberg, in loops** — pipeline status comes from the
  self-hosted (unmetered) Woodpecker API (§4, first form); never loop on
  `…/commits/<sha>/status`. Reserve Codeberg calls for the few state-changing ones.
- **Cap + back off** — keep Codeberg API calls to a low steady rate (a few per minute
  for state-changing calls) and apply **exponential backoff on 429**; retry-hammering
  only compounds the window.
- **Space + batch** PR opens and merges; a **distinct PR title** also dodges the
  separate "2 similarly-named issues in an hour" limiter.
- **Prefer SSH git** (unmetered) over the HTTP API where a choice exists (push over SSH
  rather than a REST/`tea` call).

*(The API-budget companion to the Woodpecker fair-use note above — see FEEDBACK.md
**PAPER-110**.)*
