Release Guide

This repository publishes one PyPI distribution, abi-agent. A release is built from a verified master commit and keeps the Git tag, package version, GitHub Release, and PyPI artifacts tied to the same identity.

Pre-Release Checks

Start with the consolidated release check:

scripts/release_check.sh

Before pushing a candidate, confirm that CHANGELOG.md contains the exact project.version from pyproject.toml. CI enforces this through scripts/check_release_identity.py, which also compares the Claude Code and Codex plugin manifests with the package version.

Choose a version that is absent from both PyPI and remote Git tags. Tags and PyPI versions are immutable identities: never move or reuse a tag after it has been pushed. If a tag points to mismatched package metadata, abandon that version and increment again. Historical example: 1.5.4 was abandoned because its tag was created against 1.5.3 metadata. Later versions must still be checked independently; never infer availability from that example.

The release policy requires the tag to point at the verified master commit. This is a release-operator precondition: release.yml validates tag/package identity and reuses CI, but it does not itself prove that the tagged commit is the current master tip. Verify ancestry and remote tag absence before pushing.

The script creates a POSIX temporary directory under /tmp by default and exports TMPDIR, TMP, and TEMP before running tests. This keeps permission-sensitive checks off WSL/Windows-mounted temporary directories. Use ABI_RELEASE_TMPDIR or ABI_RELEASE_TMP_ROOT to override the location.

The script runs the local Python/package subset below:

ruff check src/ tests/
ruff format --check src/ tests/
mypy src/abi/ --ignore-missing-imports
python -m pytest tests/ src/abi/sciplot/tests/ -v --tb=short \
  --strict-markers -m "not requires_tools" --capture=no \
  --cov=src/abi --cov-branch --cov-report=term-missing:skip-covered \
  --cov-report=xml --cov-report=json:coverage.json --cov-fail-under=75
python scripts/check_module_coverage.py --coverage coverage.json

python -m build
abi query --type metagenomic_plasmid --what stages

It does not replace every release-surface gate. Run bash docs/build_docs.sh, docker compose -f docker/docker-compose.yml config --quiet, the Docker configuration regression test, strict contract lint for all seven plugins, and python -m twine check dist/* separately. Run applicable real-tool acceptance checks, and run container acceptance only when container inputs changed or an image will be published. The GitHub release workflow reruns the reusable CI gate before creating a Release.

After building a wheel, install it with its [mcp] extra and smoke-test the installed commands in a clean environment when possible:

abi list-types
abi query --type metagenomic_plasmid --what stages
abi query --type rnaseq_expression --what tools
autoplasm --help
abi dry-run --type metagenomic_plasmid --config examples/config_minimal.yaml --profile dry_run
abi doctor-agent --type metatranscriptomics
abi export-openai-tools --type metatranscriptomics --format json
abi install-skills --target /tmp/abi-smoke-skills
abi-mcp --help 2>/dev/null || python -m abi.mcp.server --help 2>/dev/null || true
for platform in claude-code opencode codex; do
  abi agent install "$platform" --scope project --project-dir "/tmp/abi-release-agent-$platform"
  abi agent doctor "$platform" --scope project --project-dir "/tmp/abi-release-agent-$platform"
done

Run this with the clean wheel environment’s bin directory on PATH, because the doctor verifies the installed abi-mcp entry point. integrations/ is a release input: it must be present in both distributions and every Docker /app context. When it or another container input changes, a manual Docker workflow run is recommended before a container release.

GitHub Actions

  • ci.yml runs lint, format check, mypy, tests, default builds and installed wheel checks on Python 3.10–3.13, native arm64 validation, and the shared installed-wheel capability verifier.

  • docker.yml is manual-only. It builds and smoke-tests a selected plugin (or all plugins) and publishes only when the operator explicitly enables push. Registry pushes include provenance and SBOM; non-push validation loads a stable local image without attestations. Published images are multi-platform except RNA-seq, which remains linux/amd64-only until its R/DESeq2 environment passes a native arm64 build and smoke test.

  • release.yml builds distributions, runs the wheel smoke outside the source checkout, attaches abi-linux-capability-v<version>.json generated by that installed wheel, creates a GitHub Release for v* tags, and emits the published event.

  • publish-pypi.yml downloads those exact Release artifacts and publishes the *.whl and *.tar.gz distributions through PyPI Trusted Publishing. The capability JSON remains GitHub Release evidence and is not uploaded to PyPI. PyPI binds the OIDC identity to this filename, so it remains required.

No optional bot or duplicate publishing workflow belongs in .github/workflows/. The required workflow set is exactly ci.yml, docker.yml, release.yml, and publish-pypi.yml.

The canonical automatic chain is:

verified master commit → v<version> tag → reusable CI quality gate
→ build and smoke-test wheel/sdist → GitHub Release with exact distributions
  and Linux capability evidence
→ top-level release.published event starts publish-pypi.yml
→ download Release artifacts → PyPI Trusted Publishing

Do not call publish-pypi.yml as a reusable workflow: PyPI does not support the parent workflow’s OIDC Build Config URI. release.published is the single automatic publication trigger. Recovery uses workflow_dispatch with the existing GitHub Release tag and never rebuilds locally. Renaming the publisher requires updating the trusted publisher configuration on PyPI first.

This chain combines policy and automation. The v* filter is enforced by release.yml; the separate PyPI workflow reacts to a published GitHub Release and manual recovery input. Release operators must therefore publish only the verified v<version> Release and must not use the manual path to bypass the identity checks.

Before merging packaging changes, require a successful default sdist-to-wheel build. Docker is not a strict PR or PyPI release gate. When container inputs change, a manual Docker workflow run is recommended; before publishing an image, that run must cover build, local load, and abi list-types. A successful BuildKit setup or Conda solve alone is not sufficient. Select the plasmid image explicitly because of its size.

gh workflow run docker.yml --ref master \
  -f plugin=<plugin> -f push=false -f push_to_dockerhub=false

The Docker workflow is never started by a tag or GitHub Release. Container publication is a separate, intentional operator action and is outside the automatic GitHub Release → PyPI chain. To publish, select the exact verified v<version> tag as the manual workflow ref, first record a successful push=false run, and then rerun the same tag with push=true. The workflow rejects branch and non-version-tag publication and repeats the local smoke test before pushing registry tags.

Post-Release Verification

Verify that the GitHub Release includes abi-linux-capability-v<version>.json, that its matrix has exactly 21 environments and the declared x86_64/aarch64 cells, and that unsupported cells remain explicit. PyPI should contain only the wheel and sdist.

After publication, verify the GitHub Release and PyPI version, confirm Trusted Publishing provenance and file hashes, install the wheel in a clean environment, and run abi list-types, autoplasm --help, and representative plugin dry-runs. For container tags, verify the GHCR image can be pulled and runs abi list-types. Record links to the Release, PyPI project, release workflow, publish job, and container workflow in the release handoff.