Skip to content
Merged
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
7 changes: 5 additions & 2 deletions src/aio_fleet/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@
from aio_fleet.manifest import FleetManifest, ManifestError, RepoConfig, load_manifest
from aio_fleet.poll import poll_targets
from aio_fleet.registry import compute_registry_tags, verify_registry_tags
from aio_fleet.release import find_release_target_commit, latest_changelog_version
from aio_fleet.release import (
find_release_publish_target_commit,
latest_changelog_version,
)
from aio_fleet.release_plan import release_plan_for_manifest, release_plan_for_repo
from aio_fleet.report import (
fleet_report_json_schema,
Expand Down Expand Up @@ -1686,7 +1689,7 @@ def cmd_release_publish(args: argparse.Namespace) -> int:
latest_version = latest_changelog_version(
repo.path / "CHANGELOG.md", semver=repo.publish_profile == "template"
)
release_target = find_release_target_commit(repo.path, latest_version)
release_target = find_release_publish_target_commit(repo.path, latest_version)
notes = _run(
[
sys.executable,
Expand Down
20 changes: 20 additions & 0 deletions src/aio_fleet/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
AIO_CHANGELOG_HEADING = re.compile(
r"^##\s+(?:\[(?P<linked>[^\]]+)\]\([^)]+\)|(?P<plain>[^\s]+))"
)
RELEASE_FORMAT_SUBJECT = re.compile(
r"^chore\(release\): format .+ changelog(?: \(#\d+\))?$"
)


def main(argv: list[str] | None = None) -> int:
Expand Down Expand Up @@ -339,6 +342,23 @@ def find_release_target_commit(repo_path: Path, version: str) -> str:
return release_commit


def find_release_publish_target_commit(repo_path: Path, version: str) -> str:
release_target = find_release_target_commit(repo_path, version)
head = git(repo_path, "rev-parse", "HEAD").strip()
if release_target == head:
return release_target
if not git_is_ancestor(repo_path, release_target, head):
return release_target
subjects = git(repo_path, "log", "--format=%s", f"{release_target}..{head}")
if subjects.strip() and all(
RELEASE_FORMAT_SUBJECT.match(subject.strip())
for subject in subjects.splitlines()
if subject.strip()
):
return head
return release_target


def git_tags(repo_path: Path) -> list[str]:
output = git(repo_path, "tag", "--list")
return [line.strip() for line in output.splitlines() if line.strip()]
Expand Down
49 changes: 49 additions & 0 deletions tests/test_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pathlib import Path

from aio_fleet.release import (
find_release_publish_target_commit,
latest_changelog_version,
main,
next_aio_release_version,
Expand All @@ -16,6 +17,12 @@ def _git(repo: Path, *args: str) -> None:
subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True) # nosec


def _git_output(repo: Path, *args: str) -> str:
return subprocess.check_output( # nosec B603 B607
["git", *args], cwd=repo, text=True
).strip()


def _commit(repo: Path, message: str) -> None:
_git(repo, "add", ".")
_git(repo, "commit", "-m", message)
Expand Down Expand Up @@ -72,6 +79,48 @@ def test_latest_changelog_version_supports_linked_headings(tmp_path: Path) -> No
assert latest_changelog_version(changelog) == "v1.2.3-aio.1" # nosec B101


def test_release_publish_target_allows_changelog_format_followup(
tmp_path: Path,
) -> None:
_init_repo(tmp_path)
(tmp_path / "CHANGELOG.md").write_text("## Unreleased\n\n- initial\n")
_commit(tmp_path, "feat(test): initial")
(tmp_path / "CHANGELOG.md").write_text(
"## v1.0.0-aio.1 - 2026-05-10\n\n- initial\n"
)
_commit(tmp_path, "chore(release): v1.0.0-aio.1")
release_commit = _git_output(tmp_path, "rev-parse", "HEAD")
(tmp_path / "CHANGELOG.md").write_text(
"## v1.0.0-aio.1 - 2026-05-10\n\n- initial\n\n"
)
_commit(tmp_path, "chore(release): format test changelog")
publish_commit = _git_output(tmp_path, "rev-parse", "HEAD")

assert release_commit != publish_commit # nosec B101
assert ( # nosec B101
find_release_publish_target_commit(tmp_path, "v1.0.0-aio.1") == publish_commit
)


def test_release_publish_target_rejects_arbitrary_post_release_commit(
tmp_path: Path,
) -> None:
_init_repo(tmp_path)
(tmp_path / "CHANGELOG.md").write_text("## Unreleased\n\n- initial\n")
_commit(tmp_path, "feat(test): initial")
(tmp_path / "CHANGELOG.md").write_text(
"## v1.0.0-aio.1 - 2026-05-10\n\n- initial\n"
)
_commit(tmp_path, "chore(release): v1.0.0-aio.1")
release_commit = _git_output(tmp_path, "rev-parse", "HEAD")
(tmp_path / "README.md").write_text("later\n")
_commit(tmp_path, "fix(runtime): later change")

assert ( # nosec B101
find_release_publish_target_commit(tmp_path, "v1.0.0-aio.1") == release_commit
)


def test_release_cli_supports_component_suffix(tmp_path: Path, capsys) -> None:
_init_repo(tmp_path)
(tmp_path / "components").mkdir()
Expand Down
Loading