ci(foundation): Forgejo development CI and GitHub tag-driven release pipeline (issue #97) #98

Merged
adrian.tkacz merged 2 commits from ci/release-foundation into develop 2026-09-19 18:56:07 +00:00
Member

Implements #97 (one commit from current develop, targeting develop; left open for human review — do not merge by automation, no release is performed).

What lands

Forgejo development CI — .forgejo/workflows/ci.yml (workflow ci, job check)

  • Runs on every PR to develop and every push to develop; per-ref concurrency with cancellation.
  • Executes exactly the authoritative gate (uv sync --only-dev, make check, uv run basedpyright, git diff --check) plus make package-check — no duplicate test list maintained in the workflow.
  • contents: read only; Python pinned to 3.12; caching off; the only two actions (actions/checkout, astral-sh/setup-uv) pinned to full commit SHAs.
  • Job name check is the frozen required-status-check contract for develop branch protection.

GitHub public release pipeline — .github/workflows/release.yml (the only GitHub workflow; no duplicate PR CI)

  • Triggered solely by v*.*.* tags. verify job fails closed unless: strict SemVer tag, package version equals the tag (single source __version__ in scarcity_router/__init__.py), tagged commit reachable from stable main, and event commit == tagged commit.
  • build job builds wheel + sdist from the exact tagged commit via make package-check, generates SHA256SUMS, and attests Sigstore build provenance (short-lived OIDC; id-token: write, attestations: write).
  • publish-github-release (contents: write) creates the Release and attaches artifacts; publish-pypi (environment pypi, id-token: write) publishes via PyPI Trusted Publishing — no long-lived token exists or is introduced.
  • GHCR server-image and Windows worker (MSIX/ZIP) contracts are recorded with insertion points only — no placeholder artifacts, no fake publish jobs.

Tooling — tools/package_check.py

  • Now derives version, catalog-version and policy-version expectations from the authoritative artifacts instead of pinning 0.1.0/catalog 2/policy 6. The old hardcodes were stale (catalog is v3 per D-038, policy v8) and undetectable because nothing ran the check in CI; a leftover hardcoded path prefix in the sdist allowlist is removed. No runtime behavior changes.

Documentation

  • New authoritative docs/release-engineering.md: Forgejo/GitHub authority split, development CI, frozen CI trust model, runner boundary, branch-protection setting, release/versioning invariants, pipeline mechanics, artifact inventory with explicit implemented now / configured externally / future contract / not yet supported statuses, installation contract (uv tool install / pipx after first PyPI publication; no custom Linux repos), owner-action checklist, and how #95 consumes the foundation.
  • D-046 recorded in docs/decisions.md; CI trust-boundary bullet added to docs/security.md; README quick-start/doc-map/development updates; roadmap notes #97 precedes module implementation.

Owner actions documented (not claimed configured)

  1. Forgejo runner with ubuntu-latest label meeting the documented isolation boundary (ephemeral per job; no Docker socket/host mounts/privileged containers/private-network reachability; no secrets in PR jobs).
  2. develop branch protection requiring status check check (plus PR-only merges, no force pushes).
  3. PyPI project + Trusted Publisher entry (owner creatidy, repo scarcity-router, workflow release.yml, environment pypi) and a protected GitHub pypi environment. Until configured, publish-pypi fails closed by design.

Validation performed

  • Full gate: uv sync --only-dev, make check (1045 tests OK), uv run basedpyright (0/0/0), git diff --check — all clean.
  • uv build produces wheel + sdist; make package-check fully green through the same mechanism CI uses (isolated uv tool install, console scripts, packaged resources, MCP discovery, loopback REST healthz).
  • The release verify script was extracted and executed against real repo state: happy path passes, and each fail-closed invariant (version mismatch, tag not on main, non-SemVer tag) refuses correctly.
  • Both workflow files pass actionlint v1.7.12 clean.
  • Version-agnosticism proven end-to-end: a scratch copy with __version__ set to 0.2.0 builds scarcity_router-0.2.0 artifacts and passes the identical smoke check.

Nothing is published, tagged, released or promoted by this PR.

Implements #97 (one commit from current `develop`, targeting `develop`; left open for human review — do not merge by automation, no release is performed). ## What lands **Forgejo development CI — `.forgejo/workflows/ci.yml` (workflow `ci`, job `check`)** - Runs on every PR to `develop` and every push to `develop`; per-ref concurrency with cancellation. - Executes exactly the authoritative gate (`uv sync --only-dev`, `make check`, `uv run basedpyright`, `git diff --check`) plus `make package-check` — no duplicate test list maintained in the workflow. - `contents: read` only; Python pinned to 3.12; caching off; the only two actions (`actions/checkout`, `astral-sh/setup-uv`) pinned to full commit SHAs. - Job name `check` is the frozen required-status-check contract for `develop` branch protection. **GitHub public release pipeline — `.github/workflows/release.yml` (the only GitHub workflow; no duplicate PR CI)** - Triggered solely by `v*.*.*` tags. `verify` job fails closed unless: strict SemVer tag, package version equals the tag (single source `__version__` in `scarcity_router/__init__.py`), tagged commit reachable from stable `main`, and event commit == tagged commit. - `build` job builds wheel + sdist from the exact tagged commit via `make package-check`, generates `SHA256SUMS`, and attests Sigstore build provenance (short-lived OIDC; `id-token: write`, `attestations: write`). - `publish-github-release` (`contents: write`) creates the Release and attaches artifacts; `publish-pypi` (environment `pypi`, `id-token: write`) publishes via PyPI Trusted Publishing — no long-lived token exists or is introduced. - GHCR server-image and Windows worker (MSIX/ZIP) contracts are recorded with insertion points only — no placeholder artifacts, no fake publish jobs. **Tooling — `tools/package_check.py`** - Now derives version, catalog-version and policy-version expectations from the authoritative artifacts instead of pinning `0.1.0`/catalog 2/policy 6. The old hardcodes were stale (catalog is v3 per D-038, policy v8) and undetectable because nothing ran the check in CI; a leftover hardcoded path prefix in the sdist allowlist is removed. No runtime behavior changes. **Documentation** - New authoritative `docs/release-engineering.md`: Forgejo/GitHub authority split, development CI, frozen CI trust model, runner boundary, branch-protection setting, release/versioning invariants, pipeline mechanics, artifact inventory with explicit **implemented now / configured externally / future contract / not yet supported** statuses, installation contract (`uv tool install` / `pipx` after first PyPI publication; no custom Linux repos), owner-action checklist, and how #95 consumes the foundation. - D-046 recorded in `docs/decisions.md`; CI trust-boundary bullet added to `docs/security.md`; README quick-start/doc-map/development updates; roadmap notes #97 precedes module implementation. ## Owner actions documented (not claimed configured) 1. Forgejo runner with `ubuntu-latest` label meeting the documented isolation boundary (ephemeral per job; no Docker socket/host mounts/privileged containers/private-network reachability; no secrets in PR jobs). 2. `develop` branch protection requiring status check `check` (plus PR-only merges, no force pushes). 3. PyPI project + Trusted Publisher entry (owner `creatidy`, repo `scarcity-router`, workflow `release.yml`, environment `pypi`) and a protected GitHub `pypi` environment. Until configured, `publish-pypi` fails closed by design. ## Validation performed - Full gate: `uv sync --only-dev`, `make check` (1045 tests OK), `uv run basedpyright` (0/0/0), `git diff --check` — all clean. - `uv build` produces wheel + sdist; `make package-check` fully green through the same mechanism CI uses (isolated `uv tool install`, console scripts, packaged resources, MCP discovery, loopback REST healthz). - The release verify script was extracted and executed against real repo state: happy path passes, and each fail-closed invariant (version mismatch, tag not on `main`, non-SemVer tag) refuses correctly. - Both workflow files pass `actionlint` v1.7.12 clean. - Version-agnosticism proven end-to-end: a scratch copy with `__version__` set to `0.2.0` builds `scarcity_router-0.2.0` artifacts and passes the identical smoke check. Nothing is published, tagged, released or promoted by this PR.
ci(foundation): Forgejo development CI and GitHub tag-driven release pipeline (issue #97)
All checks were successful
ci / check (pull_request) Successful in 56s
ff13d58ba8
Forgejo Actions workflow ci/check runs the documented validation gate plus
make package-check on PRs to develop and pushes to develop; it is the stable
required check for develop branch protection. The only GitHub workflow is the
tag-driven release pipeline: fail-closed SemVer/version/main-ancestry/exact-
commit verification, wheel+sdist via make package-check, SHA256SUMS, Sigstore
attestations, GitHub Release publication, and PyPI Trusted Publishing (OIDC).
tools/package_check.py now derives version/catalog/policy expectations from
the authoritative artifacts instead of pinning 0.1.0/catalog 2/policy 6, so
one smoke check validates release builds of any version. Contracts, trust
model and owner actions are recorded in docs/release-engineering.md (D-046).
No release is performed and no runtime behavior changes.
fix(release): structurally separate the PyPI payload from the release bundle (issue #97)
All checks were successful
ci / check (pull_request) Successful in 49s
9c632ff257
The single dist artifact handed SHA256SUMS to pypa/gh-action-pypi-publish,
where twine would see it as a distribution. The build job now uploads two
structurally separated artifacts: pypi-packages, populated only by the
*.whl / *.tar.gz globs and re-checked by a guard step before twine runs
(packages-dir: pypi-packages/), and release-bundle (distributions plus
SHA256SUMS) for the GitHub Release, whose asset list is now explicit. A
fail-closed payload-set check pins dist/ to exactly one wheel, one sdist
and SHA256SUMS (hidden files are documented uv residue that matches no
upload glob). Docs now state the real checksum semantics: SHA256SUMS holds
entries for the wheel and sdist only and does not hash itself. No release
architecture redesign; GHCR and Windows contracts untouched.
adrian.tkacz deleted branch ci/release-foundation 2026-09-19 18:56:07 +00:00
Sign in to join this conversation.
No description provided.