blob: 2d32ee4de0fc217f9b6a6cd84c300901457f1528 [file]
import datetime
import pathlib
from tools.private.release import changelog_news, gh, git
from tools.private.release.release_issue import (
parse_checklist_state,
update_task_in_body,
)
from tools.private.release.utils import (
determine_next_version,
replace_version_next,
)
def cmd_prepare(args):
"""Executes the prepare subcommand."""
print("Fetching upstream to verify fresh release history...")
git.fetch(tags=True, force=True)
# Run pre-check: verify there are no local edits
status = git.status()
if status:
print(
"Error: Local edits detected. Workspace must be completely clean"
" before running release preparation."
)
for line in status.splitlines():
print(f" {line}")
return 1
print("Pre-check passed: Workspace is clean.")
version = args.version
if version is None:
version = determine_next_version()
print(f"Running preparation pipeline for {version}...")
# 1. Find or create tracking issue (EARLY)
# We do this before any write operations (branch creation, commit, push)
issue_num = args.issue
if not issue_num:
try:
issue_num = gh.get_release_tracking_issue(version)
print(f"Tracking issue: #{issue_num}")
except gh.MultipleTrackingIssuesError as e:
print(f"Error: {e}")
return 1
except gh.NoTrackingIssueError:
# Not found, we need the template
template_path = pathlib.Path(
".github/ISSUE_TEMPLATE/release_tracking_template.md"
)
if not template_path.exists():
raise FileNotFoundError(f"Template file not found at {template_path}")
template_content = template_path.read_text(encoding="utf-8")
if args.dry_run:
print(
f"[DRY RUN] No active tracking issue found for {version}. Would create a new one."
)
print(f"[DRY RUN] Title: Release {version}\n{template_content}")
issue_num = None # Keep it None for dry-run prints later
else:
print(
f"No active tracking issue found for {version}. Creating a new one..."
)
issue_num = gh.create_tracking_issue(version, template_content)
print(f"Tracking issue: #{issue_num}")
else:
print(f"Tracking issue: #{issue_num}")
branch_name = f"prepare-{version}"
# 2. Interleaved git and write operations
# --- Branch selection/creation ---
if git.branch_exists(branch_name):
if args.dry_run:
print(
f"[DRY RUN] Branch {branch_name} already exists. Would checkout existing branch."
)
else:
print(f"Branch {branch_name} already exists. Checking it out...")
git.checkout(branch_name)
else:
if args.dry_run:
print(f"[DRY RUN] Would create and checkout branch {branch_name}")
else:
git.checkout(branch_name, create_branch=True)
# --- Update files ---
if args.dry_run:
print(
f"[DRY RUN] Would update CHANGELOG.md and version placeholders for {version}"
)
else:
print("Updating changelog and placeholders...")
release_date = datetime.date.today().strftime("%Y-%m-%d")
changelog_news.update_changelog(version, release_date)
replace_version_next(version)
# --- Commit and Push ---
if args.dry_run:
print(f"[DRY RUN] Would push branch {branch_name} to origin")
else:
modified_files = git.status()
if modified_files:
# Stage all modified and deleted tracked files
git.add_modified_and_deleted()
git.commit(f"Prepare release {version}")
else:
print("No files modified by the release tool. Nothing to commit.")
print(f"Pushing branch {branch_name} to origin...")
# Force push to overwrite the remote branch if it already exists (e.g. from a previous run)
git.push("origin", branch_name, set_upstream=True, force=True)
# --- Create PR ---
# Determine if we need to create a PR or reuse an existing one
open_pr = gh.get_open_pr(branch_name)
associated_pr = None
if not open_pr and issue_num:
body = gh.get_issue_body(issue_num)
state = parse_checklist_state(body)
associated_pr = state["prepare_release"]["pr"]
if open_pr:
pr_num = open_pr["number"]
pr_url = open_pr["url"]
print(f"Open Pull Request already exists: {pr_url} (PR #{pr_num})")
elif associated_pr:
pr_num = associated_pr.lstrip("#")
pr_url = f"https://github.com/bazel-contrib/rules_python/pull/{pr_num}"
print(
f"PR #{pr_num} is already associated in tracking issue #{issue_num}. Using it."
)
else:
if args.dry_run:
target_issue = f"#{issue_num}" if issue_num else "<NEW_ISSUE>"
print(
f"[DRY RUN] Would create Pull Request for branch {branch_name} targeting issue {target_issue}"
)
pr_num = "<NEW_PR>"
else:
pr_url = gh.create_pr(version, issue_num)
pr_num = pr_url.split("/")[-1]
print(f"Created Pull Request: {pr_url} (PR #{pr_num})")
# --- Update checklist ---
if args.dry_run:
target_issue = f"#{issue_num}" if issue_num else "<NEW_ISSUE>"
print(
f"[DRY RUN] Would update tracking issue {target_issue} checklist 'Prepare Release' task status to PENDING"
)
else:
print(
f"Updating tracking issue #{issue_num} checklist 'Prepare Release' task status to PENDING..."
)
body = gh.get_issue_body(issue_num)
metadata = {"status": "pending", "pr": f"#{pr_num}"}
updated_body = update_task_in_body(
body, "Prepare Release", checked=False, metadata=metadata
)
gh.update_issue_body(issue_num, updated_body)
print("Preparation pipeline completed successfully!")
return 0