Skip to content

Releasing

A release is a version bump, and everything else is automation. This page assumes you can run the gate from Development setup and that your change is already green. For whether a deployment is fit to upgrade, which is a different question, The release gate owns that.

There are no release branches and no manual tagging. The single source of truth is version in pyproject.toml, and .github/workflows/publish.yml compares it against the existing tags.

push to main
ci.yml lint · lint-imports · typecheck · test
├─ red ──▶ nothing else runs
└─ green ─▶ workflow_dispatch?
├─ no ──▶ stop here, the gate ran and that is all
└─ yes ─▶ v<version> already a tag?
├─ yes ─▶ already released, no-op
└─ no ──▶ uv build
publish to PyPI trusted publishing, skip-existing
tag v<version>
GitHub release with generated notes

Two details in that flow are deliberate. The publish step runs before the tag, so a failed upload leaves no tag behind and the next attempt simply retries. And skip-existing is on, so re-running after a partial failure is idempotent rather than an error.

The CI job is reused rather than reimplemented. publish.yml calls ci.yml through workflow_call, so the gate a release passes is byte for byte the gate a pull request passes.

The release job carries if: github.event_name == 'workflow_dispatch', so an ordinary push to main runs the full gate and stops. AIZK currently exercises a PostgreSQL row security API from one reviewed SQLAlchemy commit. The bootstrap script installs that commit after the frozen environment, but package metadata cannot require an unreleased commit while remaining a normal PyPI dependency.

This is temporary. Once the row security API ships in SQLAlchemy, pyproject.toml can name the released version directly. The manual guard can then come out and a version bump becomes the whole release again.

  1. Bump version in pyproject.toml.
  2. Move the Unreleased section of CHANGELOG.md under the new version with today’s date.
  3. Update README.md and these docs if the change is user-visible, in the same commit.
  4. Run the local gate.
  5. Merge to main and confirm CI is green.
  6. Trigger publish.yml from the Actions tab or with gh workflow run publish.yml.
  7. Check the PyPI project page and the docs site.

Run every Python gate through the frozen .venv after the setup guide installs the reviewed SQLAlchemy revision.

Terminal window
uv run --no-sync ruff check . && uv run --no-sync ruff format --check .
uv run --no-sync lint-imports
uv run --no-sync pyrefly check
uv run --no-sync ty check --python .venv --exit-zero-on-warning
uv run --no-sync mypy src/aizk src/eval
uv run --no-sync python -m pytest -n 4 --dist loadscope --benchmark-disable
pnpm --dir docs check && pnpm --dir docs build

Building the wheel is the one step CI owns rather than you, using uv build on a clean checkout inside the workflow.

.github/workflows/docs.yml builds the Astro site on any change under docs/ and runs the page gate over the result, checking the reading budget, the diagram rule, and every internal link. It publishes nothing. The site is served from the deployment itself through the docs Compose service, so this job only proves the build is green. Writing these docs is the contract it enforces.

PyPI publishing uses trusted publishing over OIDC, so there is no API token anywhere. The publisher is registered against this repository, the workflow file path publish.yml, and the pypi environment.