Skip to content

Releasing base-cli

The base-cli distribution is built and published from the standalone basefoundry/base-cli repository. The package name on PyPI is base-cli; the Python import name is base_cli.

base-cli releases are independent of Base releases. The shared provider, platform, license, and release-artifact rules are summarized in the Base ecosystem platform, license, and release policy.

Version and tag contract

VERSION is the release version source of truth. The build backend reads it for the wheel and sdist metadata, and base_cli.__version__ reports the same value from a source checkout or from installed distribution metadata.

Production releases use a matching annotated tag such as v0.1.0, created from main. Lightweight tags, tags pointing at a commit outside main, forced tag updates, and tags that do not exactly match v${VERSION} are rejected before publication. The workflow fetches the complete trusted main history so an ancestor check fails closed instead of relying on shallow checkout state. The default-branch and v* tag rulesets are release prerequisites. Before a production tag is created, an independent maintainer must verify in the live repository settings that the default branch requires one approving pull-request review from someone other than the last pusher, strict up-to-date status checks, and the policy, quality, runtime, and consumer checks listed in the ruleset. The same verification must confirm that tag deletion and non-fast-forward updates are disabled and that no unreviewed bypass actor is configured. This document describes the required controls; it does not treat a ruleset as active until that live readback has been recorded in the release evidence.

Release-path changes are additionally covered by .github/CODEOWNERS, including the package workflows, VERSION, the hash-locked release toolchain, the artifact/provenance validators, and this release procedure. CODEOWNERS is a review-routing control, not enforcement by itself: the protected-branch ruleset must require the matching owner review and protect the CODEOWNERS file. The independent reviewer must rerun the quality and release gates before a production tag is created. The tag ruleset must protect v* tags from deletion and force updates; bypass actors must be intentionally empty.

If a maintainer credential is suspected to be compromised, pause publication, revoke the affected token or OIDC trust, preserve the workflow run and review evidence, and rotate the release version rather than moving an existing tag. Restore access only after an independent maintainer has reviewed the ruleset, environment approvals, trusted-publisher claims, and the exact artifact digests.

Validation workflow

Pull requests and pushes to main start from a clean artifact destination, build one sdist and one wheel, enforce the source allowlist, run twine check, and install the reviewed wheel in an isolated environment. The installed-wheel smoke test exercises public API, lifecycle, and output behavior without the source tree on sys.path. Tests run across Python 3.10 through 3.14 on Linux, macOS, and Windows, with Debian, Fedora, and WSL validation retained. Blocking quality gates cover Ruff formatting/lint, strict public-sample and framework-wide typing, and an 80% combined statement-and-branch coverage threshold. Documentation/example checks and dependency/static security scans are separate required gates. The performance contract also checks fresh import and isolated invocation budgets, while the adversarial suite exercises redaction, protocol framing, persistence, concurrency, retention, and signal cleanup.

The publish job downloads that same reviewed artifact; it does not rebuild during publication. The build also emits a deterministic SHA256SUMS file, an SPDX 2.3 SBOM.spdx.json release artifact, and a RELEASE-BOM-ROW.json component record for the ecosystem compatibility BOM. The row binds the exact base-cli version, full source commit, API contract, supported platforms, and passing release evidence. On tag and protected dispatch runs, GitHub's OIDC-backed actions/attest job records both build provenance and an SBOM attestation for the exact artifact digests; no PyPI token or other long-lived publish secret is used.

The provenance job runs after build and smoke validation and before any publication, attestation, or GitHub Release write. It verifies the full source SHA, annotated tag object, exact tag target, trusted origin/main ancestry, non-shallow history, and push-event force/deletion flags. A TestPyPI dispatch from a branch remains available for rehearsal, but still requires full history and a full source SHA.

For a version tag, the same Package workflow creates a GitHub Release after the protected PyPI publication and attestations succeed. The release attaches the exact reviewed wheel, sdist, SHA256SUMS, SBOM.spdx.json, and RELEASE-BOM-ROW.json downloaded from the build job. GitHub-generated comparison notes are supplemented by the dated section in CHANGELOG.md; the tagged release is rejected when VERSION or that section does not match the tag. Once the live v* tag ruleset has been verified, published tags and release assets are treated as immutable. Before creating a release, the workflow verifies that the tag still resolves to the reviewed commit, that the wheel and sdist have provenance and SBOM attestations for that tag and commit, and that the release metadata binds the same version, commit, and assets. If an existing GitHub Release is found, the workflow downloads its assets and permits an idempotent rerun only when the published release is non-draft, has the same tag identity, and every filename and byte matches the reviewed artifacts. It makes no changes to an identical release. Any changed, missing, extra, or renamed asset fails with expected and observed checksums; corrections require a new patch version rather than overwriting published bytes. A failed first upload that leaves a partial release must be reviewed and recovered with a new release version.

Independent verification

The release job uses the reviewed, hash-locked toolchain in requirements/release.txt; it does not install mutable latest build or publishing packages. The lock includes transitive release-path dependencies, and the PEP 517 backend is pinned to the same setuptools and wheel versions in pyproject.toml. The job builds two clean source archives with the same SOURCE_DATE_EPOCH and rejects digest drift before publishing the reviewed artifacts.

To intentionally refresh the toolchain, edit the direct requirements in requirements/release.in and regenerate the lock with:

uv pip compile requirements/release.in --python-version 3.13 \
  --generate-hashes --output-file requirements/release.txt

Review the complete diff, run the package workflow on a pull request, and only then merge the update. Runtime dependency windows are deliberately not tied to this release-only toolchain.

Download the release metadata artifact from the successful Package workflow run (the artifact is named base-cli-release-metadata-<run-id>), alongside the wheel or sdist you downloaded from PyPI:

gh run download <run-id> \
  --repo basefoundry/base-cli \
  --name base-cli-release-metadata-<run-id> \
  --dir release-metadata
sha256sum -c release-metadata/SHA256SUMS

The SPDX document's namespace and comment include the source revision used by the workflow. For a tagged release, verify the matching GitHub attestations with the GitHub CLI:

gh attestation verify base_cli-<version>-py3-none-any.whl \
  --repo basefoundry/base-cli

The same command can verify the sdist. A clean-room verifier should compare the downloaded artifact's digest with SHA256SUMS, confirm the SBOM namespace contains the expected tag commit, and inspect the attestation's workflow and repository identity before installation.

Release-facing documentation guardrails

The package README is part of the published PyPI metadata. Before any tagged build, the Package workflow runs scripts/validate_release_docs.py to require a single release row whose version and GitHub release link match VERSION and to reject pre-publication wording. scripts/validate_package_artifact.py then compares the wheel metadata and sdist PKG-INFO description with the tagged README, and the release BOM records the README SHA-256.

After a production PyPI upload, the workflow runs scripts/verify_pypi_metadata.py before creating the GitHub Release. It reads the published PyPI JSON metadata, retries propagation delays, and requires the published version and long description to match the reviewed source. A mismatch blocks release completion; published PyPI bytes remain immutable and require a new patch version for correction.

Changelog and release notes

CHANGELOG.md follows Keep a Changelog. Keep [Unreleased] first, use one section per change category, and write user-facing bullets rather than internal implementation notes. Every released version must include its release date and a reference link at the bottom of the file. Move entries from [Unreleased] into the dated section when the release PR is prepared; do not rewrite an already published section.

The repository validates these rules in CI with python scripts/validate_changelog.py, including duplicate bullets, duplicate categories, missing release links, malformed dates, and accidental internal planning text.

Documentation site

The Documentation workflow builds this site with mkdocs build --strict and publishes the reviewed site to GitHub Pages after changes land on main. The canonical URL is https://basefoundry.github.io/base-cli/ and is exposed in the PyPI project metadata as the Documentation link.

Repository administrators should enable GitHub Pages for the repository using the GitHub Actions source and approve the github-pages environment the first time the workflow deploys. Pull requests run the strict build and repository link checks without publishing.

TestPyPI rehearsal

  1. Dispatch Package from the branch or tag to be rehearsed and choose testpypi.
  2. Approve the protected testpypi environment when prompted.
  3. Verify the published artifact from a clean environment:
python -m venv /tmp/base-cli-smoke
/tmp/base-cli-smoke/bin/python -m pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  base-cli
/tmp/base-cli-smoke/bin/python -c \
  'import base_cli; print(base_cli.__version__)'

The testpypi GitHub environment must be configured with PyPI trusted publishing for this repository and workflow before the dispatch can upload.

Production release

  1. Update VERSION and the changelog in a reviewed pull request.
  2. Merge to main and create the matching v${VERSION} tag.
  3. Approve the protected pypi environment. The workflow verifies the tag, dated changelog section, builds and tests the artifact, then publishes the exact artifact to PyPI via trusted publishing and creates the matching GitHub Release.
  4. Verify installation from PyPI and download the matching GitHub Release assets:
python -m venv /tmp/base-cli-smoke
/tmp/base-cli-smoke/bin/python -m pip install --upgrade base-cli
/tmp/base-cli-smoke/bin/python -c \
  'import base_cli; import importlib.metadata as m; assert base_cli.__version__ == m.version("base-cli"); print(base_cli.__version__)'

The pypi GitHub environment requires approval, rejects self-review, disallows administrator bypass, and is configured with the PyPI trusted publisher for .github/workflows/package.yml. No long-lived PyPI token is stored in the repository. The independent release/security reviewer is designated in GOVERNANCE.md; the release record must identify the reviewer who checked the exact artifact and commit. If a temporary solo- maintainer exception is ever needed, it must be time-bounded and record an owner, expiry, audit trail, and link to #252 before an administrator changes the environment policy.

Recovery

PyPI versions cannot be overwritten. If validation fails, fix the branch and rerun the workflow before creating a tag. If TestPyPI succeeds but a production publish fails, inspect the workflow logs and rerun the same approved tag only after confirming that neither artifact nor metadata needs correction. A version that was published successfully must be incremented for the next release.

0.5.0 preparation

Use the 0.5.0 checklist and 0.4.x migration notes. The changelog validator permits the newest section matching VERSION to precede its tag while preparing a release PR; all earlier published sections remain checked against their tags. Once the current tag exists, its section is immutable too.