Release Guide

This repository publishes only the abi-agent core to PyPI. Eight optional plugins live in abi-plugin and are distributed only on GitHub. 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/ -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 eight 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
abi --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 verified GitHub Release draft for v* tags. A maintainer publishes the draft to emit 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 release chain is:

verified master commit → v<version> tag → reusable CI quality gate
→ build and smoke-test wheel/sdist → GitHub Release draft with exact distributions
  and Linux capability evidence → authenticated maintainer publishes draft
→ 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, abi --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.

Distribution manifest

The core sdist and core wheel form the PyPI release set. Plugin wheels are rejected by this manifest. verify_release_artifacts.py --dist-dir dist --tag v<VERSION> --write-manifest validates the exact set and writes an immutable release-artifacts.json SHA-256 manifest. After downloading the Release assets, run the same command without --write-manifest; missing/extra packages, version or dependency mismatch, and changed bytes stop publication. The publisher downloads these exact artifacts and does not rebuild them.

The Release workflow creates a verified draft. Once its quality gate and artifact checks pass, publish that draft using an authenticated maintainer identity:

gh release edit v<VERSION> --draft=false

This emits the top-level release.published event for the existing Trusted Publisher. A release created directly by the workflow’s GITHUB_TOKEN would not trigger that second workflow (GitHub event rules). Do not rebuild the downloaded artifacts or replace the publisher with a reusable workflow.

GitHub-only plugins

Initialize the pinned plugin source with git submodule update --init --recursive. CI builds those plugins for integration and installation checks but does not copy them into the core Release or upload them to PyPI. The core sdist excludes plugin source; Docker and source development use the checked-out submodule.

The abi-plugin repository has its own build matrix and GitHub Release workflow. After its exact pinned commit passes CI, create an unused matching version tag there, verify all eight wheels against SHA256SUMS.json, and publish the plugin Release draft once the matching abi-agent version is available on PyPI. Users install only the GitHub wheel URLs they need. No plugin Pending Trusted Publisher is required.