Skip to content

Release procedure

Releases start with a reviewed version change and an explicitly pushed tag. GitHub Actions validates, builds distributions, publishes to PyPI, then creates a GitHub Release containing the changelog entry and the same wheel and source distribution. A push to a branch does not publish a package.

Choosing a version

The changes after 0.3.2 justify 0.4.0. Collaboration modes and human-input handling add public API, while failed-turn handling, cancellation outcomes, and manual approval ownership require migration notes. The package remains in its pre-1.0 development phase.

Below 1.0, increment the minor version for new public features or changes that require application adjustments. Use a patch version for compatible fixes. Version 1.0 should follow an explicit commitment to a stable public API. This policy follows the development-phase guidance in Semantic Versioning.

Tags use v followed by the exact canonical package version. Stable releases use X.Y.Z; prereleases use Python's a, b, or rc notation, for example 0.5.0rc1 with tag v0.5.0rc1. Development, local, and epoch versions are not publishable through this workflow. Prereleases are marked accordingly on GitHub.

Version inventory

Location Meaning and maintenance
pyproject.toml, [project].version Authoritative package version. Change it with uv version.
uv.lock, package entry for codex-app-server-sdk Generated copy. uv version updates the lockfile and installed development package.
src/codex_app_server_sdk/_version.py Reads installed distribution metadata; contains no release number to bump.
Public codex_app_server_sdk.__version__ and default clientInfo.version Both use the installed package version. The old hard-coded handshake value was removed.
CHANGELOG.md One nonempty ## X.Y.Z section per prepared release. This section supplies GitHub Release notes.
Git tag Exactly v plus the package version, pointing to the commit being published.
Wheel and sdist filenames and metadata Generated by Hatchling from pyproject.toml; checked before upload.
README default-handshake example Uses an installed-version placeholder rather than another number to maintain.
README custom clientInfo.version example Identifies the caller's application; independent of this package.
Protocol version, dependency constraints, lockfile dependency versions, and GitHub Action/uv versions Independent version domains; do not change them merely because the SDK version changes.
openai-codex comparison version in README and docs Records the external implementation audited for that comparison. Update only after rechecking it.
Historical changelog entries, tags, and release examples Historical or illustrative values, not synchronized package metadata.
tests/test_release_checks.py Synthetic version fixtures for valid and invalid releases; do not bump them with the SDK.

Use uv 0.11.18, matching the workflows, when preparing releases. uv may migrate an older lockfile schema during a version change; inspect the diff and confirm unrelated dependency versions were not upgraded. Install the package with uv sync before importing it from a checkout so distribution metadata exists.

Release tooling requires Twine 7 and packaging 26.3 or newer. Current Hatchling produces core metadata 2.5, which the previously locked tools rejected; strict artifact validation now detects this incompatibility before upload.

The source README keeps relative links to CHANGELOG.md and RELEASE.md. The hatch-fancy-pypi-readme metadata hook converts those links to absolute GitHub URLs under the package's version tag when building distributions. This keeps links usable on PyPI and tied to the published release. The plugin is a build dependency, installed automatically by isolated builds; builds without isolation must install the declared build dependencies themselves.

When changing README links, inspect the description in the built wheel's METADATA and the sdist's PKG-INFO, not only the source rendered by GitHub. Extend the substitutions in pyproject.toml if adding more relative file links. PyPI retains the first uploaded metadata for a version, so correcting an already-published description requires a new release; repository edits alone do not change historical PyPI pages.

Prepare and validate locally

Work on a branch based on the default branch (master) with complete tag history. Choose the version deliberately; the workflow does not infer release semantics from commit messages or change versions after review.

git fetch origin --tags
uv version 0.4.0

The version is already prepared as 0.4.0 in this change. For later releases, use the selected version or uv version --bump minor / uv version --bump patch. This updates both the project metadata and lockfile. Add or update the matching changelog section, including migration instructions for changed behavior. Put compatibility notices first in that section so they appear prominently in the generated GitHub Release. For behavior changes, maintain a migration guide (such as the 0.4.0 guide) and link it from the README, documentation home/navigation, affected usage guides, and PR description.

Run the checks below. v0.3.2 is the previous release for 0.4.0; for subsequent releases use the previous reachable release tag. Quality checks run in parallel on Python files changed since that baseline, including staged and untracked files. CI fails on Ruff, Black, mypy, or basedpyright errors; basedpyright warnings are not currently a release gate.

uv sync --locked --group dev
uv run --locked python scripts/check_quality.py --base v0.3.2
uv run --locked python -m pytest -q
uv run --locked python scripts/check_chat_method_links.py
uv run --locked zensical build
uv run --locked python scripts/check_release.py --tag v0.4.0
uv build --no-sources --out-dir dist/0.4.0
uv run --locked twine check --strict dist/0.4.0/*
uv run --locked python scripts/check_release.py --tag v0.4.0 --dist-dir dist/0.4.0 --check-pypi
uv run --isolated --no-project --with ./dist/0.4.0/codex_app_server_sdk-0.4.0-py3-none-any.whl python -I -c 'from codex_app_server_sdk import __version__; print(__version__)'
git diff --check

The artifact check requires exactly one wheel and one sdist of the expected version. Use a fresh version-specific output directory to avoid including older builds. The isolated import must print the prepared version.

Live model tests are optional and disabled in CI. The existing smoke test uses only gpt-6-luna and is enabled explicitly:

CODEX_SDK_LIVE_TESTS=1 uv run --locked python -m pytest -q tests/test_live_app_server.py

Commit the version, lockfile, changelog, and related changes together. Push the branch, review the PR, and merge after its checks pass. Passing PR checks alone does not publish anything.

Publish the reviewed commit

After release preparation is merged, update the local default branch and ensure its working tree is clean. The tag push below publishes the package.

git switch master
git pull --ff-only
git status --short
uv run --locked python scripts/check_release.py --tag v0.4.0
git tag -a v0.4.0 -m "v0.4.0"
git push origin v0.4.0

In Actions, watch the publish workflow through all four stages:

  1. Validate: require a tag even on manual dispatch; check its exact version, its commit, lockfile consistency, and changelog notes. Determine the previous reachable version tag for quality checks.
  2. Checks: call the same ci.yml used on PRs and default-branch pushes. Run tests on Python 3.12, 3.13, and 3.14, run changed-file quality checks, build documentation, build the wheel and sdist, run strict Twine validation, check filenames and archive metadata, and import the wheel in an isolated environment. Confirm any same-named artifacts on PyPI have identical hashes.
  3. Publish: upload those artifacts through PyPI trusted publishing. Existing files may be skipped only after the hash check above.
  4. GitHub Release: create a draft for the existing tag, attach the exact artifacts from the same run, and publish it with the matching changelog section. A failed asset upload leaves a draft that a retry can complete.

There is no second build between PyPI and GitHub. Check the resulting PyPI version and GitHub Release assets before announcing the release.

Manual dispatch is supported for an existing tag, using that tag as the workflow ref. Selecting a branch fails validation. Runs for the same ref are serialized and publishing runs are not cancelled by a newer run.

Failures and retries

  • If validation, tests, quality, documentation, or packaging fails, publication does not run. Fix and review the source before selecting a new release tag.
  • If upload or GitHub Release creation fails transiently, rerun the failed jobs in the same Actions run. This reuses the original distributions instead of rebuilding files already published to PyPI.
  • Existing PyPI files must have matching SHA-256 hashes. A fresh full run that rebuilds different bytes for the same filenames is rejected. Retrieve the original successful build's artifacts or prepare a new version.
  • For a partial PyPI upload, rerunning the failed publish job skips files already uploaded from that validated build and uploads the remaining files. PyPI publication is not transactional, so inspect both artifacts.
  • If PyPI succeeded and GitHub failed, rerun only the failed GitHub Release job. It resumes an existing draft and publishes it after uploading assets. An already-published GitHub Release is left intact.
  • Never move a published version tag or replace a published package with different contents. Correct released defects in another version.

Actions artifacts have finite retention. Recover the original run promptly; once its artifacts expire, a new version may be needed. Ordinary releases should not use manual twine upload or uv publish followed by a tag push, as the previous checklist suggested: that starts two publication paths.

Repository configuration and documentation deployment

PyPI trusted publishing must authorize repository emsi/codex-app-server-sdk, workflow publish.yml, and environment pypi. The publishing job requests id-token: write; only the GitHub Release job requests contents: write. Keep the workflow filename and environment aligned with the PyPI publisher configuration if either is renamed. No PyPI API-token secret is needed.

At the 2026-09-23 audit, the pypi environment had no reviewer requirement or branch/tag restrictions. These settings are managed on GitHub, separately from the workflow files. Required PR checks or environment protections may be configured independently by a repository administrator.

docs.yml checks documentation and links on PRs and default-branch pushes. It deploys GitHub Pages after a successful push check on main or master. Tags do not deploy the documentation; manual docs dispatch only checks it. The site tracks the default branch, rather than a versioned release.

GitHub Release automation starts with releases using the new workflow; it does not backfill historical tags such as v0.3.2.

References: uv package versioning and publishing, PyPI publishing action, and GitHub Release CLI.