# CI Workflow Patterns

> **Conformance:** L0 / L1 / L2 (see `README.md`)
> **Audience:** Maintainers setting up CI for an MGF-family project.

> **Scope.** Paste-ready Woodpecker pipeline snippets that
> implement every rule in [`WORKFLOWS.md`](WORKFLOWS.md). Each
> pattern is annotated with the WF-* rule it satisfies and notes
> the per-project parameters you adjust (project name, version
> file path, coverage threshold, supported Python versions).
>
> When starting a new MGF-family library, the four-gate +
> publish recipe at the bottom of this document is the canonical
> starting point. Copy verbatim, adjust the marked parameters,
> commit.
>
> Adapting these snippets for **other CI platforms** (GitHub
> Actions, GitLab CI) is a translation exercise — the discipline
> is platform-agnostic; the syntax differs. The
> [§7 platform-translation table](#7-platform-translation-table)
> below maps Woodpecker concepts onto the major alternatives.

---

## How to use this document

1. **Read [`WORKFLOWS.md`](WORKFLOWS.md) first** for the *why*.
   This document is the *how* — it assumes the rules are accepted.
2. **Find the section matching your need:** four-gate baseline,
   build, publish, matrix testing, post-publish verification.
3. **Copy the snippet verbatim** into your project's
   `.woodpecker.yml` (or `.woodpecker/` directory for multi-pipeline
   projects).
4. **Adjust the marked parameters** — placeholders use the
   `<UPPERCASE>` convention (e.g., `<PACKAGE_NAME>`,
   `<COVERAGE_FLOOR>`).
5. **Verify against the WF-* rule** in the annotation at the top
   of each snippet — the rule is the contract; the snippet is
   one valid implementation.

---

## 1. The four-gate baseline (rules WF-01, WF-02, WF-10)

Every project starts here. Lint + typecheck + test + import-linter,
parallel by default, blocking on every push and PR.

### 1.1 Trigger block

```yaml
# .woodpecker.yml
when:
  - event: push
    branch: master           # adjust to your default branch
  - event: pull_request
  - event: tag
    ref: refs/tags/v*        # tag publishes (see §4)
  - event: manual            # for ad-hoc reruns from the UI
```

**Annotates:** WF-01 (every push + PR), WF-03 (tag-driven for
publish-capable projects).

### 1.2 The four gates

```yaml
steps:
  # ----- Gate 1: Lint (ruff) -----
  - name: lint
    image: python:3.12-slim         # WF-09: pinned minor
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet -e ".[dev]"
      - ruff check src tests
      - ruff format --check src tests
      - lint-imports                # rule WF-02: import-linter as gate

  # ----- Gate 2: Typecheck (mypy --strict) -----
  - name: typecheck
    image: python:3.12-slim
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet -e ".[dev,<EXTRAS_NEEDED_FOR_STUBS>]"
      - mypy --strict src/

  # ----- Gate 3: Test (pytest matrix; coverage on canonical version) -----
  - name: test
    image: python:${PYTHON_VERSION}-slim
    matrix:
      PYTHON_VERSION:
        - "3.11"            # adjust to your supported versions
        - "3.12"
        - "3.13"
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet -e ".[dev,<TEST_EXTRAS>]"
      - |
        if [ "${PYTHON_VERSION}" = "3.12" ]; then
          # WF-10: coverage on canonical version only — the test
          # suite is the same on every version; running coverage
          # three times is wasted CI minutes.
          coverage run --source=src/<PACKAGE_DIR> -m pytest --tb=short -q
          coverage report --show-missing --fail-under=<COVERAGE_FLOOR>
        else
          pytest --tb=short -q
        fi
```

**Annotates:** WF-02 (four gates), WF-09 (pinned versions),
WF-10 (parallel matrix).

**Per-project parameters:**
- `<EXTRAS_NEEDED_FOR_STUBS>` — extras that ship type stubs your
  source imports (e.g., `[observability]` for opentelemetry).
- `<TEST_EXTRAS>` — extras whose code paths your tests cover.
- `<PACKAGE_DIR>` — the directory under `src/` (e.g.,
  `mgf` for `src/mgf/`).
- `<COVERAGE_FLOOR>` — your project's threshold (mgf-common: 85;
  vmanager: 85; new projects: start at 80, raise as you stabilise).
- Python versions in the matrix — your `requires-python` minimum
  through the latest stable.

---

## 2. Build + packaging-regression check (rule WF-06)

For every project that publishes (or might one day publish), the
wheel + sdist build is a CI gate. A packaging regression that
breaks the wheel MUST fail loudly on PR, not silently after a
release ships.

```yaml
  # ----- Build: sdist + wheel + twine check -----
  - name: build
    image: python:3.12-slim
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet build twine
      - python -m build                       # → dist/<pkg>-<version>-*
      - twine check dist/*                    # PyPI metadata validators
      # Sanity: the PEP 561 marker MUST ship in the wheel (rule AP-09)
      - unzip -l dist/<PACKAGE_DIST_NAME>-*.whl | grep py.typed
```

**Annotates:** WF-06 spirit (CI builds the artifact), and the
twine-check guards PyPI's accept-time validators.

**Per-project parameters:**
- `<PACKAGE_DIST_NAME>` — the dashed dist name (e.g.,
  `mgf_common`; underscored on PyPI per PEP 503 normalisation).

---

## 3. Pre-publish smoke (rule WF-07)

The wheel that built and twine-checked might still ImportError
on a fresh consumer install. The smoke catches this.

```yaml
  # ----- Pre-publish smoke (runs in publish step; see §4) -----
  - name: smoke
    image: python:3.12-slim
    depends_on: [build]                       # WF-10: explicit dep
    commands:
      - python -m venv /tmp/smoke
      - /tmp/smoke/bin/pip install --no-cache-dir dist/<PACKAGE_DIST_NAME>-*.whl
      - |
        /tmp/smoke/bin/python -c "
        import <PACKAGE_IMPORT_NAME>
        expected = '${CI_COMMIT_TAG#v}'
        actual = <PACKAGE_IMPORT_NAME>.__version__
        assert actual == expected, f'wheel ships __version__={actual}, tag is v{expected}'
        print(f'OK: import + __version__ match ({actual})')
        "
      # If the project ships a console-script, smoke that too:
      - /tmp/smoke/bin/<CONSOLE_SCRIPT_NAME> --version
```

**Annotates:** WF-07 (smoke is mandatory before upload).

**Per-project parameters:**
- `<PACKAGE_DIST_NAME>` — dashed dist name.
- `<PACKAGE_IMPORT_NAME>` — Python import name (e.g.,
  `mgf.common` for the `mgf-common` PyPI package).
- `<CONSOLE_SCRIPT_NAME>` — only if `[project.scripts]` defines
  one; omit the line otherwise.

---

## 4. Tag-driven publish (rules WF-03, WF-04, WF-05, WF-08)

The full publish pipeline. Triggered by pushing a `v<X.Y.Z>` tag.

### 4.1 Pre-flight: tag-version coherence (rule WF-05)

```yaml
  # ----- Tag-coherence check: tag MUST match pyproject + __version__ -----
  - name: tag-coherence
    image: python:3.12-slim
    when:
      event: tag
      ref: refs/tags/v*
    commands:
      - pip install --quiet -e .
      - |
        TAG_VERSION="${CI_COMMIT_TAG#v}"
        PYPROJECT_VERSION="$(grep -E '^version = ' pyproject.toml | head -1 | cut -d'"' -f2)"
        PACKAGE_VERSION="$(python -c 'import <PACKAGE_IMPORT_NAME>; print(<PACKAGE_IMPORT_NAME>.__version__)')"
        echo "tag=$TAG_VERSION pyproject=$PYPROJECT_VERSION package=$PACKAGE_VERSION"
        if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then
            echo "ERROR: tag $CI_COMMIT_TAG does not match pyproject.toml::version ($PYPROJECT_VERSION)"
            echo "Did you forget to bump the version before tagging? Per RELEASING.md REL-06."
            exit 1
        fi
        if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
            echo "ERROR: tag $CI_COMMIT_TAG does not match __version__ ($PACKAGE_VERSION)"
            echo "Did you forget to update src/<PACKAGE_DIR>/__init__.py? Per AP-13."
            exit 1
        fi
        echo "OK: tag $CI_COMMIT_TAG matches pyproject + __version__"
```

**Annotates:** WF-05 (tag-version coherence), with explicit
error messages pointing at the relevant standards.

### 4.2 Upload + post-publish verify (rules WF-04, WF-08)

```yaml
  # ----- Upload to PyPI -----
  - name: publish
    image: python:3.12-slim
    when:
      event: tag
      ref: refs/tags/v*
    depends_on: [tag-coherence, smoke]        # WF-10: gate ordering
    environment:
      TWINE_USERNAME: __token__
      TWINE_PASSWORD:
        from_secret: pypi_token               # WF-04: from secret store
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet twine
      - twine upload --non-interactive dist/<PACKAGE_DIST_NAME>-${CI_COMMIT_TAG#v}*

  # ----- Post-publish verify -----
  - name: verify-pypi
    image: python:3.12-slim
    when:
      event: tag
      ref: refs/tags/v*
    depends_on: [publish]
    commands:
      - pip install --quiet --upgrade pip
      - |
        VERSION="${CI_COMMIT_TAG#v}"
        WHEEL_SHA256_LOCAL="$(sha256sum dist/<PACKAGE_DIST_NAME>-${VERSION}-*.whl | awk '{print $1}')"
        echo "Local wheel SHA256: $WHEEL_SHA256_LOCAL"
        # Poll PyPI for up to 90s — CDN propagation is usually <30s
        # but can spike to 60s+ during PyPI maintenance windows.
        for attempt in 1 2 3 4 5 6; do
            sleep 15
            REMOTE_JSON="$(curl -sSL https://pypi.org/pypi/<PACKAGE_DIST_NAME>/${VERSION}/json || true)"
            if echo "$REMOTE_JSON" | python -c "import sys,json; d=json.load(sys.stdin); sys.exit(0 if d.get('urls') else 1)" 2>/dev/null; then
                REMOTE_WHEEL_SHA256="$(echo "$REMOTE_JSON" | python -c "import sys,json; d=json.load(sys.stdin); print([u for u in d['urls'] if u['packagetype']=='bdist_wheel'][0]['digests']['sha256'])")"
                echo "Remote wheel SHA256: $REMOTE_WHEEL_SHA256 (attempt $attempt)"
                if [ "$WHEEL_SHA256_LOCAL" = "$REMOTE_WHEEL_SHA256" ]; then
                    echo "OK: PyPI artifact matches local build"
                    exit 0
                else
                    echo "ERROR: PyPI artifact SHA256 differs from local build"
                    echo "  local:  $WHEEL_SHA256_LOCAL"
                    echo "  remote: $REMOTE_WHEEL_SHA256"
                    exit 1
                fi
            fi
            echo "PyPI hasn't surfaced ${VERSION} yet (attempt $attempt of 6)..."
        done
        echo "ERROR: PyPI did not surface ${VERSION} within 90s. Check pypi.org/project/<PACKAGE_DIST_NAME>/${VERSION}/ manually."
        exit 1
```

**Annotates:** WF-04 (token from secret store), WF-08 (verify
artifact + SHA256 match).

**Per-project parameters:**
- `pypi_token` — name of the secret in the Woodpecker UI; you set
  this once per project (project settings → secrets → add `pypi_token`,
  events: tag).
- `<PACKAGE_DIST_NAME>` — dashed dist name, e.g., `mgf_common`.
- `<PACKAGE_IMPORT_NAME>` — import name, e.g., `mgf.common`.
- `<PACKAGE_DIR>` — directory under `src/`, e.g., `mgf`.

### 4.3 One-time setup: the `pypi_token` secret

Before the publish workflow can run, add the token to Woodpecker:

0. **(Codeberg-hosted Woodpecker only — one-time per user account.)**
   Request CI access via the Woodpecker CI Access template at
   [`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).
   Title MUST start with `[CI]` (approval-bot requirement). Pick
   `minimal` for Expected Resource Usage. Codeberg gates
   `ci.codeberg.org` per user account; without this every step
   below errors with "access denied". Approval is account-wide
   (one issue covers all your future projects); turnaround is
   usually 1–2 days. After approval, log in to `ci.codeberg.org`
   and activate the repo. See
   [`docs/internal/CI_STARTHERE.md`](../internal/CI_STARTHERE.md)
   "I want to set up CI for a new project" Step 0 for
   field-by-field guidance.
1. PyPI: [pypi.org/manage/account/token/](https://pypi.org/manage/account/token/)
   → "Add API token" → scope to **the single project** (not
   account-wide). Copy the token (`pypi-AgE...`).
2. Woodpecker: project page → **Settings** → **Secrets** → **Add
   secret**:
   - Name: `pypi_token`
   - Value: paste the token
   - **Events:** check `tag` only. Uncheck `push`, `pull_request`,
     `manual`. This binds the secret to tag-event runs only — a
     push to master that tries to read it gets nothing.
3. Save. Trigger a release with `git push --follow-tags`.

After this, the `from_secret: pypi_token` reference in the workflow
resolves at run time. The token never appears in the workflow file.

---

## 5. Vulnerability scan (rule SC-10)

Per [`SECURITY_STANDARD.md`](SECURITY_STANDARD.md) **SC-10**:
dependencies pinned + scanned for CVEs.

```yaml
  # ----- Vulnerability scan (rule SC-10) -----
  - name: vuln-scan
    image: python:3.12-slim
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet pip-audit -e ".[dev,<RUNTIME_EXTRAS>]"
      # --strict exits non-zero on any finding so PRs cannot merge
      # until the finding is triaged: fix (bump dep), justify (add to
      # ignore-vulns with rationale), or embargo (with expiry date).
      # Retry once on transient OSV-database fetch errors.
      - pip-audit --strict --desc || (sleep 30 && pip-audit --strict --desc)
```

**Per-project parameters:**
- `<RUNTIME_EXTRAS>` — extras whose deps you want scanned
  (typically the same as your test extras + service-shape extras).

---

## 6. Compat matrix — second-consumer regression check

For libraries with at least one in-repo consumer (`mgf-common`'s
`example_app/`; vmanager's smoke fixtures; any project shipping
a "downstream consumer" demo):

```yaml
  # ----- Compat matrix: install in-repo consumer + run its tests -----
  - name: compat-matrix-<CONSUMER_NAME>
    image: python:3.12-slim
    commands:
      - pip install --quiet --upgrade pip
      # Library installed editable from the in-repo source first.
      - pip install --quiet -e ".[dev,<EXTRAS>]"
      # Consumer installed editable on top — its pyproject path-source
      # already points at the in-repo library.
      - pip install --quiet -e "./<CONSUMER_DIR>[dev]"
      - cd <CONSUMER_DIR> && pytest --tb=short -q
```

**Per-project parameters:**
- `<CONSUMER_NAME>` — name suffix for the step (visible in
  Woodpecker UI).
- `<CONSUMER_DIR>` — directory holding the consumer
  (`example_app`, `examples/integration`, etc.).

---

## 7. Platform-translation table

The discipline is platform-agnostic; the syntax differs. Map
Woodpecker concepts onto your platform:

| Concept | Woodpecker | GitHub Actions | GitLab CI |
|---|---|---|---|
| Trigger filter | `when: event: tag` + `ref: refs/tags/v*` | `on: push: tags: ['v*']` | `rules: - if: $CI_COMMIT_TAG =~ /^v/` |
| Step image | `image: python:3.12-slim` | `runs-on: ubuntu-latest` + `uses: actions/setup-python@v5 with: python-version: '3.12'` | `image: python:3.12-slim` |
| Matrix | `matrix: PYTHON_VERSION: ["3.11", "3.12"]` | `strategy: matrix: python: ["3.11", "3.12"]` | `parallel: matrix: PYTHON_VERSION: ["3.11", "3.12"]` |
| Secret | `from_secret: pypi_token` | `secrets.PYPI_TOKEN` | `$PYPI_TOKEN` (masked variable) |
| Step dependency | `depends_on: [build]` | `needs: [build]` | `needs: [build]` |
| Tag variable | `${CI_COMMIT_TAG}` | `${{ github.ref_name }}` | `${CI_COMMIT_TAG}` |
| OIDC for PyPI | not yet supported (Codeberg/Forgejo) | `permissions: id-token: write` + `pypa/gh-action-pypi-publish@release/v1` | OIDC supported as of GitLab 17.0 |

---

## 8. Full canonical recipe (paste-ready)

The minimal `.woodpecker.yml` for a publish-capable MGF library.
Copy verbatim; replace the `<PLACEHOLDER>` values; commit.

```yaml
# .woodpecker.yml — canonical CI for an MGF-family library that
# publishes to PyPI. Implements every rule in
# docs/standards/WORKFLOWS.md.
#
# One-time setup before the first release:
#   - PyPI: issue a project-scoped token at
#     pypi.org/manage/account/token/
#   - Woodpecker: add the token as secret `pypi_token`,
#     events: tag (project Settings → Secrets).

when:
  - event: push
    branch: master                            # WF-01
  - event: pull_request                       # WF-01
  - event: tag
    ref: refs/tags/v*                         # WF-03
  - event: manual

steps:
  # ===== Four-gate baseline (WF-02) =====

  - name: lint
    image: python:3.12-slim                   # WF-09
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet -e ".[dev]"
      - ruff check src tests
      - ruff format --check src tests
      - lint-imports

  - name: typecheck
    image: python:3.12-slim
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet -e ".[dev,<EXTRAS_FOR_STUBS>]"
      - mypy --strict src/

  - name: test
    image: python:${PYTHON_VERSION}-slim
    matrix:
      PYTHON_VERSION: ["3.11", "3.12", "3.13"]
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet -e ".[dev,<TEST_EXTRAS>]"
      - |
        if [ "${PYTHON_VERSION}" = "3.12" ]; then
          coverage run --source=src/<PACKAGE_DIR> -m pytest --tb=short -q
          coverage report --show-missing --fail-under=<COVERAGE_FLOOR>
        else
          pytest --tb=short -q
        fi

  - name: vuln-scan
    image: python:3.12-slim
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet pip-audit -e ".[dev,<RUNTIME_EXTRAS>]"
      - pip-audit --strict --desc || (sleep 30 && pip-audit --strict --desc)

  # ===== Build (every push, gates packaging regressions) (WF-06) =====

  - name: build
    image: python:3.12-slim
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet build twine
      - python -m build
      - twine check dist/*
      - unzip -l dist/<PACKAGE_DIST_NAME>-*.whl | grep py.typed

  # ===== Tag-coherence check (WF-05) — only on tag push =====

  - name: tag-coherence
    image: python:3.12-slim
    when:
      event: tag
      ref: refs/tags/v*
    depends_on: [build]
    commands:
      - pip install --quiet -e .
      - |
        TAG_VERSION="${CI_COMMIT_TAG#v}"
        PYPROJECT_VERSION="$(grep -E '^version = ' pyproject.toml | head -1 | cut -d'"' -f2)"
        PACKAGE_VERSION="$(python -c 'import <PACKAGE_IMPORT_NAME>; print(<PACKAGE_IMPORT_NAME>.__version__)')"
        echo "tag=$TAG_VERSION pyproject=$PYPROJECT_VERSION package=$PACKAGE_VERSION"
        if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ] || [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
            echo "ERROR: tag $CI_COMMIT_TAG does not match pyproject ($PYPROJECT_VERSION) or __version__ ($PACKAGE_VERSION)"
            exit 1
        fi
        echo "OK: tag $CI_COMMIT_TAG coherent with pyproject + __version__"

  # ===== Pre-publish smoke (WF-07) — only on tag push =====

  - name: smoke
    image: python:3.12-slim
    when:
      event: tag
      ref: refs/tags/v*
    depends_on: [tag-coherence]
    commands:
      - python -m venv /tmp/smoke
      - /tmp/smoke/bin/pip install --no-cache-dir dist/<PACKAGE_DIST_NAME>-${CI_COMMIT_TAG#v}-*.whl
      - |
        /tmp/smoke/bin/python -c "
        import <PACKAGE_IMPORT_NAME>
        expected = '${CI_COMMIT_TAG#v}'
        actual = <PACKAGE_IMPORT_NAME>.__version__
        assert actual == expected, f'wheel __version__={actual} vs tag v{expected}'
        print(f'OK: smoke import + version match ({actual})')
        "

  # ===== Publish to PyPI (WF-04) — only on tag push =====

  - name: publish
    image: python:3.12-slim
    when:
      event: tag
      ref: refs/tags/v*
    depends_on: [smoke, lint, typecheck, test, vuln-scan]
    environment:
      TWINE_USERNAME: __token__
      TWINE_PASSWORD:
        from_secret: pypi_token
    commands:
      - pip install --quiet --upgrade pip
      - pip install --quiet twine
      - twine upload --non-interactive dist/<PACKAGE_DIST_NAME>-${CI_COMMIT_TAG#v}*

  # ===== Post-publish verify (WF-08) =====

  - name: verify-pypi
    image: python:3.12-slim
    when:
      event: tag
      ref: refs/tags/v*
    depends_on: [publish]
    commands:
      - |
        VERSION="${CI_COMMIT_TAG#v}"
        WHEEL_LOCAL_SHA="$(sha256sum dist/<PACKAGE_DIST_NAME>-${VERSION}-*.whl | awk '{print $1}')"
        for attempt in 1 2 3 4 5 6; do
            sleep 15
            REMOTE_JSON="$(curl -sSL https://pypi.org/pypi/<PACKAGE_DIST_NAME>/${VERSION}/json || true)"
            if echo "$REMOTE_JSON" | python -c "import sys,json; d=json.load(sys.stdin); sys.exit(0 if d.get('urls') else 1)" 2>/dev/null; then
                REMOTE_SHA="$(echo "$REMOTE_JSON" | python -c "import sys,json; d=json.load(sys.stdin); print([u for u in d['urls'] if u['packagetype']=='bdist_wheel'][0]['digests']['sha256'])")"
                if [ "$WHEEL_LOCAL_SHA" = "$REMOTE_SHA" ]; then
                    echo "OK: PyPI artifact matches local build (sha256: ${WHEEL_LOCAL_SHA:0:12}...)"
                    exit 0
                fi
                echo "ERROR: SHA256 mismatch — local=$WHEEL_LOCAL_SHA remote=$REMOTE_SHA"
                exit 1
            fi
            echo "PyPI hasn't surfaced ${VERSION} yet (attempt $attempt of 6)..."
        done
        echo "ERROR: PyPI did not surface ${VERSION} within 90s"
        exit 1
```

---

## 9. Maintenance + evolution

### 9.1 When to update this document

- A new pattern emerges in any MGF project's CI that's worth
  lifting (similar to `LIFT_CHECKLIST.md` for code).
- A WF-* rule changes — the patterns must follow.
- A new CI platform joins the supported list (e.g., when
  Codeberg adds PyPI Trusted Publisher support, the OIDC
  pattern lands here).

### 9.2 When to NOT add a pattern

- Project-specific one-offs (vmanager's libvirt smoke,
  apiprobe's hardware-pin tests). These belong in the
  project's own `.woodpecker.yml`, not here.
- Patterns that work on one platform but not another. Either
  generalise (and add a §7 translation row) or leave out.

### 9.3 Cross-references

- [`WORKFLOWS.md`](WORKFLOWS.md) — the standard. WF-* rules
  this document implements.
- [`RELEASING.md`](RELEASING.md) — REL-* rules around release
  semantics; the publish pipeline here is the mechanical
  enforcement.
- [`SECURITY_STANDARD.md`](SECURITY_STANDARD.md) — SC-10
  (dependency scanning) implemented in §5.
- [Woodpecker docs](https://woodpecker-ci.org/docs/intro) —
  platform reference.
- [PyPI Trusted Publishers](https://docs.pypi.org/trusted-publishers/) —
  the OIDC-based future for token-free publishing.
