# CI Start Here

> **Audience:** Maintainers and contributors of `mgf-common` (or
> any MGF-family project that adopts the same workflow).
> **Three flows:** cut a release, set up CI for a new project,
> diagnose a red CI run. **Each is a short, copy-pasteable recipe.**

The full WF-* discipline is in
[`docs/standards/WORKFLOWS.md`](../standards/WORKFLOWS.md); the
paste-ready pipeline catalog is in
[`docs/standards/WORKFLOWS_PATTERNS.md`](../standards/WORKFLOWS_PATTERNS.md).
This doc is the runbook — what to type when, what you'll see, what
to do when it goes red.

---

## I want to cut a release

```bash
# 1. Bump the version in three places (must all match — WF-05 enforces).
$EDITOR pyproject.toml                 # version = "0.X.Y"
$EDITOR src/mgf/common/__init__.py     # __version__ = "0.X.Y"
$EDITOR CHANGELOG.md                   # add a [0.X.Y] section above [0.X.(Y-1)]

# 2. Regenerate PUBLIC_API.json (embeds __version__).
uv run python tools/generate_public_api_json.py

# 3. Sanity check locally — gates green before tagging.
uv run pytest -q
uv build && uv run --with twine twine check dist/mgf_common-0.X.Y*

# 4. Commit + tag.
git add pyproject.toml src/mgf/common/__init__.py CHANGELOG.md PUBLIC_API.json uv.lock
git commit -m "release: v0.X.Y — <one-line summary>"
git tag -a v0.X.Y -m "v0.X.Y — <one-line summary>"

# 5. Push commits + tag together. CI takes over from here.
git push origin master --follow-tags
```

That's the entire release flow on your side. **Do not run
`twine upload` locally** — the CI publish step is the only path
to PyPI. Local upload bypasses the gates and the tag-coherence
check.

### What you watch on Woodpecker

After the push, the run appears at
`https://ci.codeberg.org/repos/<org>/mgf_common`. Steps run in
this order; each must be ✅ before the next starts:

```
push commit (master)              tag v0.X.Y
        │                              │
        ▼                              ▼
  ┌─────────────┐              ┌──────────────┐
  │ four gates  │ ✅ (parallel) │ four gates   │ ✅ (re-runs on tag too)
  │ + build     │              │ + build      │
  │ + vuln-scan │              │ + vuln-scan  │
  │ + compat    │              │ + compat     │
  └─────────────┘              └──────┬───────┘
                                      ▼
                              tag-coherence ✅
                                      ▼
                              smoke (ephemeral venv) ✅
                                      ▼
                              publish (twine → PyPI) ✅
                                      ▼
                              verify-pypi (poll + SHA256) ✅
                                      ▼
                          🎉 https://pypi.org/project/mgf-common/0.X.Y/
```

Total time end-to-end: ~5 min. Verify-pypi polls for up to 90s
after upload; the green tick is your signal that the release is
real.

---

## I want to set up CI for a new MGF-family project

```bash
# 0. (Codeberg only — one-time per user account.) Request Woodpecker
#    CI access. Codeberg gates ci.codeberg.org per user account to
#    prevent abuse; without this you'll see "access denied" trying
#    to log in or activate a repo.
#
#    Open the Woodpecker CI Access form directly:
#       https://codeberg.org/Codeberg-e.V./requests/issues/new?template=Woodpecker-CI.yaml
#
#    Six fields to fill:
#
#      Title              [CI] <your projects> — the [CI] prefix is
#                         MANDATORY (approval bot needs it).
#                         Example: "[CI] mgf-common, mgf-apiprobe"
#
#      Projects           Markdown bullet list of the projects.
#                         Each entry: name + Codeberg URL + one-line
#                         what-it-is + license. Approval is account-
#                         wide, so list every project you'll want CI
#                         for upfront — saves re-applying later.
#                         Example:
#                           - [mgf-common](https://codeberg.org/magogi-admin/mgf_common) —
#                             typed-Python observability + standards library
#                             for the MGF project family. MIT. README at root.
#
#      Expected Resource Usage   `minimal` for most MGF projects:
#                         <1GB RAM, 1 CPU per step, <5 min total
#                         runtime. (Lint + typecheck + pytest matrix
#                         + uv build all fit under "minimal".) Pick
#                         `medium` only if you have a real heavy step
#                         (Rust builds, large integration tests).
#
#      I would also like the following users to be added
#                         List @-mentioned collaborators who also need
#                         CI. Leave empty if it's just you.
#
#      Additional Information   Anything else; usually empty.
#
#      Other Work         CRITICAL for new Codeberg accounts. Link
#                         your GitHub / GitLab profile or prior open-
#                         source work. Without this, abuse-screening
#                         may reject the request.
#
#      Statement (3 required checkboxes — all must be checked):
#         [x] LICENSE file in repo (mgf-common: MIT in root)
#         [x] Understand CI may not always be available
#         [x] Understand you'll be added to the Codeberg-CI org
#
#    Submit. Approval typically lands in 1-2 days; you get added to
#    the Codeberg-CI org (https://codeberg.org/Codeberg-CI/) and
#    ci.codeberg.org becomes accessible.
#
#    Once approved, log in to https://ci.codeberg.org with your
#    Codeberg account, find your repo, click "Activate". That
#    installs the webhook so pushes trigger builds.

# 1. Copy the canonical pipeline.
cp /path/to/mgf_common/docs/standards/WORKFLOWS_PATTERNS.md  # read §8
$EDITOR .woodpecker.yml                                       # paste recipe

# 2. Replace the placeholders in .woodpecker.yml:
#    <PACKAGE_DIST_NAME>     e.g., mgf_common      (PyPI dist name; underscored)
#    <PACKAGE_IMPORT_NAME>   e.g., mgf.common      (Python import path)
#    <PACKAGE_DIR>           e.g., mgf             (subdirectory under src/)
#    <COVERAGE_FLOOR>        e.g., 85              (project-specific threshold)
#    <EXTRAS_FOR_STUBS>      e.g., observability   (extras whose stubs you import)
#    <TEST_EXTRAS>           e.g., observability   (extras your tests cover)
#    <RUNTIME_EXTRAS>        e.g., observability   (extras pip-audit scans)

# 3. Commit + push. The four-gate pipeline runs immediately
#    (assuming step 0 completed).
git add .woodpecker.yml && git commit -m "ci: initial Woodpecker pipeline"
git push

# 4. (For libraries that publish.) Add the PyPI token.
#    PyPI:       pypi.org/manage/account/token/  → issue project-scoped token
#    Woodpecker: project Settings → Secrets → Add `pypi_token`,
#                paste token, restrict to events: tag (uncheck others)
#    NOTE: Settings → Secrets is only reachable AFTER the repo is
#    activated in Woodpecker (step 0). If you see "access denied"
#    here, you skipped step 0.

# 5. (For libraries that publish.) First release.
git tag -a v0.1.0 -m "v0.1.0 — first release"
git push origin master --follow-tags
# Watch CI; wheel lands on PyPI within ~5 min.
```

That's the whole setup. The pipeline implements every WF-* rule
out of the box. No further wiring needed — branch-protection
rules on Codeberg/Forgejo can require the CI status check before
merge, which closes WF-02.

---

## CI is red — what now?

### The two fast diagnostics

```bash
# 1. Read the failed step's last 50 lines in the Woodpecker UI.
#    The step name names the failure mode; the log ends with the
#    exit-code-bearing command.

# 2. Reproduce locally. The `image:` field on the failed step
#    tells you the container; run the same commands inside it:
docker run --rm -it -v $(pwd):/work -w /work python:3.12-slim bash
# Then paste the failed step's `commands:` block.
```

### "Access denied" on ci.codeberg.org or in Woodpecker UI

This is a setup-precondition failure, not a pipeline failure —
the workflow file never ran. You're missing step 0 of "I want
to set up CI for a new project" above. Codeberg's Woodpecker
instance gates access per user account; you need a one-time
approval before any project's `.woodpecker.yml` will execute.

**Recovery:**

1. Open the Woodpecker CI Access form:
   [`codeberg.org/Codeberg-e.V./requests/issues/new?template=Woodpecker-CI.yaml`](https://codeberg.org/Codeberg-e.V./requests/issues/new?template=Woodpecker-CI.yaml).
   See step 0 above for field-by-field guidance — title MUST
   start with `[CI]` (approval-bot requirement).
2. Wait for approval (usually 1–2 days). You'll get added to
   the [Codeberg-CI org](https://codeberg.org/Codeberg-CI/).
3. Log in to `ci.codeberg.org`, find your repo, click "Activate".
4. Add the `pypi_token` secret (Settings → Secrets, events: tag).
5. (If a tag was already pushed before access was granted) Either
   manually trigger the run from the Woodpecker UI on that tag's
   commit, OR bump to the next patch version and re-cut. The
   previously pushed tag's commit is still in Codeberg; only the
   CI run never fired.

### Common failure modes + fixes

| Step that's red | Likely cause | Fix |
|---|---|---|
| **lint** | `ruff check` or `ruff format --check` flagged something | Run `uv run ruff check src tests` + `uv run ruff format src tests` locally; commit the fix |
| **typecheck** | `mypy --strict` found a type error or missing annotation | Reproduce with `uv run mypy --strict src/`; usually a missing return type or an `Any` leak |
| **test** | A unit test failed, OR coverage dropped below the floor | Read the `pytest --tb=short` output; if coverage failed, `uv run coverage report --show-missing` shows uncovered lines |
| **build** | `pyproject.toml` invalid, or `py.typed` missing from the wheel | Reproduce with `uv build`; check the wheel listing — `unzip -l dist/<pkg>-*.whl` |
| **compat-matrix-example-app** | A public-API regression broke a downstream consumer | The example_app's tests pin the contract; update example_app if the API change is intentional, otherwise revert the API change |
| **vuln-scan** | `pip-audit` flagged a CVE | Bump the offending dep; if no patched version exists, add to `pyproject.toml`'s `[tool.pip-audit] ignore-vulns` with rationale + expiry |
| **tag-coherence** | Tag's version doesn't match `pyproject.toml::version` or `__version__` | The error message names the mismatch. Delete the tag locally + remote (`git tag -d v0.X.Y; git push origin :refs/tags/v0.X.Y`), fix the version files, re-tag, re-push |
| **smoke** | Wheel installs but `import mgf.common` fails OR `__version__` mismatch | The latter usually means the version files weren't regenerated after a bump. Run step 2 of the release flow above |
| **publish** | `twine upload` rejected | Three sub-causes: (a) version already on PyPI — bump and re-cut; (b) token expired/wrong scope — rotate via PyPI UI + update the Woodpecker secret; (c) network/PyPI 5xx — re-run the step from the Woodpecker UI |
| **verify-pypi** | PyPI didn't surface the artifact in 90s, OR SHA256 mismatch | Check [status.python.org](https://status.python.org) for PyPI incidents. If healthy, re-run the verify step (it's idempotent). SHA mismatch is paranoid territory — escalate to PyPI support |

### When the publish actually shipped a bad release

PyPI doesn't allow re-upload of an existing version (per
**REL-05**). Recovery:

```bash
# 1. Yank the bad version (PyPI keeps it for explicit pins,
#    hides from default `pip install -U`).
#    Via PyPI UI: project page → Manage → Releases → Yank
#    (no CLI in stable PyPI — UI-only as of 2026).

# 2. Bump to the next patch and re-cut.
#    Follow the release flow from the top of this doc.
```

---

## What "good" looks like in the Woodpecker UI

For a release-cutting push:

- **Push commit (master) only:** 6 green ticks (lint, typecheck,
  test×3, build, compat, vuln-scan). No publish-flow steps fire
  — there's no tag yet.
- **Tag push (`v0.X.Y`):** 10 green ticks. The 6 above plus
  tag-coherence, smoke, publish, verify-pypi. Verify-pypi's log
  ends with `OK: PyPI artifact matches local build` and a link
  to the PyPI project page.

If you see **9 green + 1 red on verify-pypi**, the artifact IS
on PyPI but verification timed out. Click the failed step → if
the log shows the artifact appeared but SHA mismatched, escalate.
If the log shows it never appeared, wait 5 min and re-run the
step manually from the UI.

---

## Quick reference — the three commands

```bash
# Release
git tag -a v0.X.Y -m "..." && git push origin master --follow-tags

# New CI setup (one-time per project)
cp <recipe-from-WORKFLOWS_PATTERNS.md> .woodpecker.yml && git commit -am "ci: pipeline" && git push
# Then in Woodpecker UI: Settings → Secrets → add pypi_token (events: tag)

# Diagnose a red run
# Read failed step's last 50 lines in Woodpecker UI; cross-reference the table above.
```

---

## Where to look next

- **The standard:** [`docs/standards/WORKFLOWS.md`](../standards/WORKFLOWS.md)
  — twelve WF-* rules with rationale.
- **The patterns catalog:** [`docs/standards/WORKFLOWS_PATTERNS.md`](../standards/WORKFLOWS_PATTERNS.md)
  — paste-ready Woodpecker snippets for every WF-* rule, plus
  a Woodpecker → GitHub Actions → GitLab CI translation table.
- **Release semantics:** [`docs/standards/RELEASING.md`](../standards/RELEASING.md)
  — the *what* and *why* (REL-01..REL-08); this doc is the *how*.
- **Architecture (the wider library):** [`ARCHITECTURE.md`](ARCHITECTURE.md).
- **Adding a new public surface (before tagging a release):** [`LIFT_CHECKLIST.md`](LIFT_CHECKLIST.md).

*Maintainers: keep this doc short. If a flow grows past 10
lines, hoist the rationale into WORKFLOWS.md and link.*
