-
-
Notifications
You must be signed in to change notification settings - Fork 0
refactor(fleet): use shared release helper shim #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,254 +1,38 @@ | ||
| #!/usr/bin/env python3 | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import pathlib | ||
| import re | ||
| import shutil | ||
| import subprocess # nosec B404 - release helpers shell out only to trusted local git | ||
| from typing import Iterable | ||
|
|
||
| try: | ||
| from components import get_component | ||
| except ImportError: # pragma: no cover - used when imported as a package module | ||
| from scripts.components import get_component | ||
|
|
||
| ROOT = pathlib.Path(__file__).resolve().parents[1] | ||
| DEFAULT_CHANGELOG = ROOT / "CHANGELOG.md" | ||
| GIT_BIN = shutil.which("git") | ||
|
|
||
|
|
||
| SEMVER_TAG = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$") | ||
|
|
||
|
|
||
| def git(*args: str) -> str: | ||
| if GIT_BIN is None: | ||
| raise SystemExit("git is required to run release helpers") | ||
| return subprocess.check_output( # nosec B603 - arguments are fixed git subcommands | ||
| [GIT_BIN, *args], | ||
| cwd=ROOT, | ||
| text=True, | ||
| ).strip() | ||
|
|
||
|
|
||
| def semver_key(tag: str) -> tuple[int, int, int] | None: | ||
| match = SEMVER_TAG.match(tag) | ||
| if not match: | ||
| return None | ||
| return tuple(int(part) for part in match.groups()) | ||
|
|
||
|
|
||
| def latest_semver_tag() -> str | None: | ||
| tags = [] | ||
| for tag in git("tag", "--list").splitlines(): | ||
| tag = tag.strip() | ||
| if not tag: | ||
| continue | ||
| key = semver_key(tag) | ||
| if key is not None: | ||
| tags.append((key, tag if tag.startswith("v") else f"v{tag}")) | ||
| if not tags: | ||
| return None | ||
| tags.sort(key=lambda item: item[0]) | ||
| return tags[-1][1] | ||
|
|
||
|
|
||
| def latest_release_tag() -> str | None: | ||
| return latest_semver_tag() | ||
|
|
||
|
|
||
| def commits_since(ref: str | None) -> Iterable[str]: | ||
| args = ["log", "--format=%s"] | ||
| if ref: | ||
| args.append(f"{ref}..HEAD") | ||
| output = git(*args) | ||
| return [line.strip() for line in output.splitlines() if line.strip()] | ||
|
|
||
|
|
||
| def has_unreleased_changes() -> bool: | ||
| latest = latest_semver_tag() | ||
| return any(commits_since(latest)) | ||
|
|
||
|
|
||
| def next_release_version() -> str: | ||
| latest = latest_semver_tag() | ||
| if latest is None: | ||
| return "v0.1.0" | ||
|
|
||
| major, minor, patch = semver_key(latest) # type: ignore[arg-type] | ||
| commit_messages = list(commits_since(latest)) | ||
|
|
||
| has_breaking = any( | ||
| "BREAKING CHANGE" in message or re.match(r"^[a-z]+(\(.+\))?!:", message) | ||
| for message in commit_messages | ||
| ) | ||
| has_feature = any( | ||
| re.match(r"^feat(\(.+\))?:", message) for message in commit_messages | ||
| ) | ||
|
|
||
| if has_breaking: | ||
| major += 1 | ||
| minor = 0 | ||
| patch = 0 | ||
| elif has_feature: | ||
| minor += 1 | ||
| patch = 0 | ||
| else: | ||
| patch += 1 | ||
|
|
||
| return f"v{major}.{minor}.{patch}" | ||
|
|
||
|
|
||
| def latest_changelog_version(changelog: pathlib.Path) -> str: | ||
| pattern = re.compile(r"^##\s+([^\s]+)") | ||
| for line in changelog.read_text().splitlines(): | ||
| match = pattern.match(line.strip()) | ||
| if match and match.group(1) != "Unreleased": | ||
| return match.group(1) | ||
| raise SystemExit(f"Unable to find a released version heading in {changelog}") | ||
|
|
||
|
|
||
| def extract_release_notes(version: str, changelog: pathlib.Path) -> str: | ||
| heading = re.compile(rf"^##\s+{re.escape(version)}(?:\s+-\s+.+)?$") | ||
| next_heading = re.compile(r"^##\s+") | ||
|
|
||
| lines = changelog.read_text().splitlines() | ||
| start = None | ||
| for index, line in enumerate(lines): | ||
| if heading.match(line.strip()): | ||
| start = index + 1 | ||
| break | ||
|
|
||
| if start is None: | ||
| raise SystemExit(f"Unable to find release section for {version} in {changelog}") | ||
|
|
||
| end = len(lines) | ||
| for index in range(start, len(lines)): | ||
| if next_heading.match(lines[index].strip()): | ||
| end = index | ||
| break | ||
|
|
||
| notes = "\n".join(lines[start:end]).strip() | ||
| if not notes: | ||
| raise SystemExit(f"Release section for {version} in {changelog} is empty") | ||
| return notes | ||
|
|
||
|
|
||
| def find_release_commit(version: str) -> str: | ||
| exact = f"chore(release): {version}" | ||
| with_suffix = re.compile(rf"^{re.escape(exact)} \(#\d+\)$") | ||
|
|
||
| output = git("log", "--format=%H\t%s", "HEAD") | ||
| for line in output.splitlines(): | ||
| if not line.strip(): | ||
| continue | ||
| sha, subject = line.split("\t", 1) | ||
| if subject == exact or with_suffix.match(subject): | ||
| return sha | ||
|
|
||
| raise SystemExit( | ||
| f"Unable to find a merged release commit for {version} on main. " | ||
| f"Expected '{exact}' or '{exact} (#123)'." | ||
| ) | ||
|
|
||
|
|
||
| def git_completed(*args: str) -> subprocess.CompletedProcess[str]: | ||
| if GIT_BIN is None: | ||
| raise SystemExit("git is required to run release helpers") | ||
| return subprocess.run( | ||
| [GIT_BIN, *args], cwd=ROOT, text=True, capture_output=True, check=False | ||
| ) # nosec | ||
|
|
||
|
|
||
| def git_is_ancestor(ancestor: str, descendant: str) -> bool: | ||
| return ( | ||
| git_completed("merge-base", "--is-ancestor", ancestor, descendant).returncode | ||
| == 0 | ||
| ) | ||
|
|
||
|
|
||
| def find_release_target_commit(version: str) -> str: | ||
| release_commit = find_release_commit(version) | ||
| head = git("rev-parse", "HEAD").strip() | ||
|
|
||
| if release_commit == head: | ||
| return release_commit | ||
|
|
||
| if not git_is_ancestor(release_commit, head): | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| def _add_local_aio_fleet() -> None: | ||
| repo_root = Path(__file__).resolve().parents[1] | ||
| for candidate in ( | ||
| repo_root / ".aio-fleet" / "src", | ||
| repo_root.parent / "aio-fleet" / "src", | ||
| ): | ||
| if candidate.exists(): | ||
| sys.path.insert(0, str(candidate)) | ||
| return | ||
|
|
||
|
|
||
| def main() -> int: | ||
| _add_local_aio_fleet() | ||
| try: | ||
| from aio_fleet.release import main as release_main | ||
| except ModuleNotFoundError as exc: | ||
| raise SystemExit( | ||
| f"Release commit {release_commit} for {version} is not reachable from HEAD." | ||
| "aio_fleet.release is required. Run from the standard workspace with " | ||
| "../aio-fleet present, or let the reusable aio-fleet workflows check " | ||
| "out .aio-fleet before invoking this shim." | ||
| ) from exc | ||
|
|
||
| return int( | ||
| release_main( | ||
| ["--repo-path", str(Path(__file__).resolve().parents[1]), *sys.argv[1:]] | ||
| ) | ||
|
|
||
| first_parent_commits = git( | ||
| "rev-list", "--first-parent", "--reverse", "HEAD" | ||
| ).splitlines() | ||
| for candidate in first_parent_commits: | ||
| if git_is_ancestor(release_commit, candidate): | ||
| return candidate | ||
|
|
||
| return release_commit | ||
|
|
||
|
|
||
| def main() -> None: | ||
| parser = argparse.ArgumentParser( | ||
| description="Release helpers for semver-based repos." | ||
| ) | ||
| parser.add_argument( | ||
| "--component", | ||
| help="Optional component name from components.toml. Semver template releases are repo-wide, so this only validates the component exists.", | ||
| ) | ||
| subparsers = parser.add_subparsers(dest="command", required=True) | ||
|
|
||
| subparsers.add_parser("next-version") | ||
| subparsers.add_parser("has-unreleased-changes") | ||
| subparsers.add_parser("latest-release-tag") | ||
|
|
||
| latest_parser = subparsers.add_parser("latest-changelog-version") | ||
| latest_parser.add_argument( | ||
| "--changelog", type=pathlib.Path, default=DEFAULT_CHANGELOG | ||
| ) | ||
|
|
||
| notes_parser = subparsers.add_parser("extract-release-notes") | ||
| notes_parser.add_argument("version") | ||
| notes_parser.add_argument( | ||
| "--changelog", type=pathlib.Path, default=DEFAULT_CHANGELOG | ||
| ) | ||
|
|
||
| commit_parser = subparsers.add_parser("find-release-commit") | ||
| commit_parser.add_argument("version") | ||
| target_parser = subparsers.add_parser("find-release-target-commit") | ||
| target_parser.add_argument("version") | ||
|
|
||
| args = parser.parse_args() | ||
| if args.component: | ||
| get_component(args.component) | ||
|
|
||
| if args.command == "next-version": | ||
| print(next_release_version()) | ||
| return | ||
| if args.command == "has-unreleased-changes": | ||
| print("true" if has_unreleased_changes() else "false") | ||
| return | ||
| if args.command == "latest-release-tag": | ||
| latest_tag = latest_release_tag() | ||
| if latest_tag: | ||
| print(latest_tag) | ||
| return | ||
| if args.command == "latest-changelog-version": | ||
| print(latest_changelog_version(args.changelog)) | ||
| return | ||
| if args.command == "extract-release-notes": | ||
| print(extract_release_notes(args.version, args.changelog)) | ||
| return | ||
| if args.command == "find-release-commit": | ||
| print(find_release_commit(args.version)) | ||
| return | ||
| if args.command == "find-release-target-commit": | ||
| print(find_release_target_commit(args.version)) | ||
| return | ||
|
|
||
| raise SystemExit(f"Unknown command: {args.command}") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| raise SystemExit(main()) | ||
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new shim hard-fails when
aio_fleetis not checked out at.aio-fleet/srcor../aio-fleet/src, so a normal clone of this repo can no longer runpython scripts/release.py ...(it exits with code 1 and the ModuleNotFoundError message). This is a regression from the previous self-contained helper and breaks local release automation/tests outside the reusable workflow context; at minimum, the shim needs a fallback path (vendored logic or installable dependency) so repo-local release commands continue to work.Useful? React with 👍 / 👎.