Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and versions are tracked in the repo-root `VERSION` file.

- Continue compatibility hardening and adoption work for the next release.

## [0.5.0] - 2026-10-03

### Added

- Publish versioned, comparative CLI benchmark reports with lifecycle and
Expand Down Expand Up @@ -391,7 +393,8 @@ the API stability policy and migration guide before upgrading from `0.3.x`.
- Pinned the build backend to metadata compatible with the bundled publication
action and made license-file validation portable across setuptools versions.

[Unreleased]: https://github.com/basefoundry/base-cli/compare/v0.4.3...HEAD
[Unreleased]: https://github.com/basefoundry/base-cli/compare/v0.5.0...HEAD
[0.5.0]: https://github.com/basefoundry/base-cli/compare/v0.4.3...v0.5.0
[0.4.3]: https://github.com/basefoundry/base-cli/compare/v0.4.2...v0.4.3
[0.4.2]: https://github.com/basefoundry/base-cli/compare/v0.4.1...v0.4.2
[0.4.1]: https://github.com/basefoundry/base-cli/compare/v0.4.0...v0.4.1
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

| Version | License | Install | Release notes |
| --- | --- | --- | --- |
| `0.4.3` | [Apache-2.0](LICENSE) | `python -m pip install base-cli` | [v0.4.3](https://github.com/basefoundry/base-cli/releases/tag/v0.4.3) |
| `0.5.0` | [Apache-2.0](LICENSE) | `python -m pip install base-cli` | [v0.5.0](https://github.com/basefoundry/base-cli/releases/tag/v0.5.0) |

Source release candidate: 0.5.0. Publication is pending the [release checklist](docs/release-0.5.0-checklist.md); PyPI remains the authority for available versions.

`base-cli` is the production lifecycle layer for Click and Typer Python CLIs.
It standardizes context, logging, configuration, cleanup, and machine-readable
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.3
0.5.0
28 changes: 28 additions & 0 deletions docs/migration-0.5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Migrating from 0.4.x to 0.5.0

0.5.0 is a pre-1.0 minor release. It retains the Click/Typer lifecycle APIs while
strengthening configuration, output, logging, retention, and release contracts.
The published 0.4.3 tag and distributions remain immutable.

- Convenience-profile project configuration now validates POSIX ownership and
permissions, refuses symlink/reparse paths, and stops discovery at `.git`, a
filesystem boundary, or the configured ancestor limit. Repair permissions or
make the shared-workspace opt-out explicit; see [local configuration](local-config.md).
- YAML inputs are bounded before alias construction. Split unusually large
configuration files and remove recursive or excessive alias graphs.
- JSON mode captures descriptor and inherited child stdout. Wait for children
and flush native stdio before returning. Output over 8 MiB produces an error;
use NDJSON for larger streams. See [JSON contracts](json-contracts.md).
- Consumer logging handlers and configured levels survive CLI cleanup. A host
level may filter persistent DEBUG messages; configure the host logger at DEBUG
when those are required. See [integrations](integrations.md).
- Native Windows enforces bundle retention through pinned directory handles;
contended maintenance passes skip without blocking command execution.
- Repeated source paths and human log timestamps are cached; log-sidecar I/O
errors use logging's error path without aborting commands.
- Click 8.5 is supported within the declared Click window. Typer consumers must
use the documented compatible Click/Typer pairs.

Review the dated [changelog](https://github.com/basefoundry/base-cli/blob/main/CHANGELOG.md) and the [platform boundary](platform-support.md)
when upgrading. Test the installed wheel in a clean environment, including your
JSON consumers, config permissions, and host logging integration.
26 changes: 26 additions & 0 deletions docs/release-0.5.0-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 0.5.0 release candidate checklist

This is release preparation. A dated changelog section does not mean the version
has been tagged or published. The proposed date must be refreshed if publication
happens on another day. Issue #307 remains open until publication evidence exists.

1. Merge the reviewed 0.5.0 issue train and all remaining 0.5.0 PRs. Refresh the
release PR against their final integrated commit and include their changelog
entries in 0.5.0 before approval.
2. Obtain independent approval and passing required checks on the exact release
head. Keep the repository and environment approval rules in place.
3. Validate the 0.4.x migration boundary in [migration guidance](migration-0.5.md),
then run the full local gate and hosted platform, dependency, consumer,
benchmark, package, and provenance checks.
4. Rehearse the reviewed artifact through the protected TestPyPI environment.
Install exactly `base-cli==0.5.0` from a new environment and run lifecycle/JSON
smoke checks; retain the artifact digest and workflow URL.
5. After the release PR is merged, create annotated `v0.5.0` on the independently
approved protected-main commit. Never alter `v0.4.3` or its distributions.
6. Approve the protected production environment. Publish the exact reviewed
wheel/sdist, then verify matching SHA256SUMS, SPDX SBOM, provenance and SBOM
attestations, RELEASE-BOM-ROW, tag target, and GitHub release assets.
7. Verify a clean installation of exactly `base-cli==0.5.0` from PyPI. Record the
immutable release URL and evidence on #307, then close it.

The operational commands and recovery rules remain in [Releasing](releasing.md).
8 changes: 8 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,11 @@ 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](release-0.5.0-checklist.md) and
[0.4.x migration notes](migration-0.5.md). 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.
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ nav:
- Dependency support: dependency-support.md
- Optional output dependencies: optional-output-dependencies.md
- Migration guide: migrations.md
- Migrating to 0.5.0: migration-0.5.md
- Click migration: migration-click.md
- Typer migration: migration-typer.md
- Cement migration: migration-cement.md
Expand All @@ -66,3 +67,4 @@ nav:
- Runtime threat model: security-threat-model.md
- Security release review: security-review.md
- Releasing base-cli: releasing.md
- 0.5.0 release checklist: release-0.5.0-checklist.md
14 changes: 14 additions & 0 deletions scripts/validate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def _validate_published_sections(
"""Ensure every released section remains identical to its version tag."""

errors: list[str] = []
version_file = path.parent / "VERSION"
candidate = version_file.read_text(encoding="utf-8").strip() if version_file.is_file() else None
for version in versions:
if version == "Unreleased":
continue
Expand All @@ -141,6 +143,18 @@ def _validate_published_sections(
text=True,
)
except (OSError, subprocess.CalledProcessError) as exc:
# A release PR necessarily precedes its protected-main tag. Only
# the newest section matching VERSION may be an untagged candidate;
# every earlier release remains bound to its immutable tag.
if version == candidate and len(versions) > 1 and version == versions[1]:
exists = subprocess.run(
["git", "-C", str(path.parent), "rev-parse", "--verify", f"refs/tags/{tag}"],
capture_output=True,
text=True,
check=False,
)
if exists.returncode == 1 or exists.returncode == 128:
continue
detail = getattr(exc, "stderr", None) or str(exc)
errors.append(
f"cannot verify [{version}] against tag {tag}: {detail.strip()}; "
Expand Down
2 changes: 2 additions & 0 deletions scripts/validate_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@
"migration-click.md",
"migration-typer.md",
"migrations.md",
"migration-0.5.md",
"output-contracts.md",
"optional-output-dependencies.md",
"performance.md",
"testing.md",
"platform-support.md",
"releasing.md",
"release-0.5.0-checklist.md",
"security-review.md",
"security-threat-model.md",
"schemas.md",
Expand Down
13 changes: 13 additions & 0 deletions tests/test_validate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ def test_rejects_edits_to_a_published_section(self) -> None:
errors = validate_changelog.validate_changelog(path, verify_tags=True)
self.assertIn("published changelog section [1.0.0] differs from tag v1.0.0", errors)

def test_current_untagged_release_candidate_can_be_prepared(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "CHANGELOG.md"
path.write_text(VALID_CHANGELOG, encoding="utf-8")
(path.parent / "VERSION").write_text("1.0.0\n")
missing = subprocess.CalledProcessError(128, "git", stderr="missing tag")
with mock.patch(
"scripts.validate_changelog.subprocess.run",
side_effect=[missing, subprocess.CompletedProcess("git", 128)],
):
errors = validate_changelog.validate_changelog(path, verify_tags=True)
self.assertEqual(errors, [])

def test_reports_unavailable_release_tags(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "CHANGELOG.md"
Expand Down
Loading