in tools/release-notes-automator/generate_release_notes.py [0:0]
def main():
"""
Main function to generate release notes for the current milestone.
Prints the formatted release notes and a summary.
Also lists issues not included in the release notes, with reasons.
"""
# ANSI color codes for pretty console output
COLOR_RESET = "\033[0m"
COLOR_GREEN = "\033[92m"
COLOR_YELLOW = "\033[93m"
COLOR_RED = "\033[91m"
COLOR_CYAN = "\033[96m"
COLOR_BOLD = "\033[1m"
# Initialize log and output containers
log_lines = [] # Collect log output (without color codes)
month_heading = f"## {MILESTONE_TITLE}\n"
output_lines = [f"\033[1m## {MILESTONE_TITLE}\033[0m\n"] # colored for console
log_output_lines = [month_heading] # plain for log file
total_issues = 0
total_prs = 0 # <-- FIX: Initialize total_prs before use
excluded_issues = [] # List of (issue, [reasons]) tuples
# For categorized markdown output
categorized_issues = {header: [] for header in CATEGORY_LABELS.values()}
uncategorized_issues = []
categorized_prs = [] # For PRs matching the new criteria
print(f"\033[96m🔎 Generating release notes for: \033[1m{MILESTONE_TITLE}\033[0m\n")
log_lines.append(f"🔎 Generating release notes for: {MILESTONE_TITLE}\n")
# Remove per-repo "✅ {issue_count} issues added." output and log
for repo in REPOS:
print(f"\033[96m➡️ Checking `{repo}`...\033[0m")
log_lines.append(f"➡️ Checking `{repo}`...")
milestone_number = get_repo_milestone_number(repo, MILESTONE_TITLE)
if not milestone_number:
print(f"\033[93m ⚠️ No milestone '{MILESTONE_TITLE}' found.\033[0m")
log_lines.append(f" ⚠️ No milestone '{MILESTONE_TITLE}' found.")
continue
# Get all closed issues for this repo (for summary)
all_closed_issues = get_all_closed_issues(repo)
included_issues = get_closed_issues(repo, milestone_number)
included_issue_numbers = set(issue["number"] for issue in included_issues)
issue_count = 0
for issue in included_issues:
reasons = []
if "pull_request" in issue:
reasons.append("Is a pull request, not an issue")
if not has_linked_pr(repo, issue["number"]):
reasons.append("No linked pull request")
if reasons:
excluded_issues.append((issue, reasons))
continue
# Categorize for markdown output
category = categorize_issue(issue)
formatted = format_issue(issue)
if category:
categorized_issues[category].append(formatted)
else:
uncategorized_issues.append(formatted)
output_lines.append(formatted)
log_output_lines.append(formatted)
issue_count += 1
total_issues += issue_count
# Find other closed issues not included in the release notes
for issue in all_closed_issues:
# Skip PRs
if "pull_request" in issue:
continue
# Already processed above
if issue["number"] in included_issue_numbers:
continue
reasons = []
if not issue.get("milestone"):
reasons.append("Issue does not have a milestone")
elif issue.get("milestone", {}).get("title") != MILESTONE_TITLE:
reasons.append(f"Issue milestone is '{issue['milestone']['title']}', not '{MILESTONE_TITLE}'")
missing_labels = [label for label in LABELS if label not in [lbl["name"] for lbl in issue.get("labels", [])]]
if missing_labels:
reasons.append(f"Issue does not have label(s): {', '.join(missing_labels)}")
if not has_linked_pr(repo, issue["number"]):
reasons.append("No linked pull request")
if not reasons:
reasons.append("Unknown exclusion reason")
excluded_issues.append((issue, reasons))
# PRs: Only "Release-Candidate" label, current milestone, not linked to any issue
closed_prs = get_closed_prs(repo, milestone_number)
for pr in closed_prs:
categorized_prs.append(format_pr(pr))
total_prs += len(closed_prs)
# Output categorized release notes to console and log
print(f"\n{COLOR_BOLD}📝 Release Notes Output (by Category):{COLOR_RESET}\n")
log_lines.append("\n📝 Release Notes Output (by Category):\n")
print(f"# Release Notes\n")
log_lines.append("# Release Notes\n")
print(f"## {MILESTONE_TITLE}\n")
log_lines.append(f"## {MILESTONE_TITLE}\n")
for header in CATEGORY_LABELS.values():
print(f"### {header}\n")
log_lines.append(f"### {header}\n")
if categorized_issues[header]:
for line in categorized_issues[header]:
print(line)
log_lines.append(line)
else:
print("_No issues in this category._")
log_lines.append("_No issues in this category._")
print()
log_lines.append("")
print("### Uncategorized\n")
log_lines.append("### Uncategorized\n")
if uncategorized_issues:
for line in uncategorized_issues:
print(line)
log_lines.append(line)
else:
print("_No issues in this category._")
log_lines.append("_No issues in this category._")
print()
log_lines.append("")
# Add PRs section to both output and log
print("### Release Candidate Pull Requests with no linked issue\n")
log_lines.append("### Release Candidate Pull Requests with no linked issue\n")
if categorized_prs:
for line in categorized_prs:
print(line)
log_lines.append(line)
else:
print("_No PRs in this category._")
log_lines.append("_No PRs in this category._")
print()
log_lines.append("")
# Output summary
print(f"\n{COLOR_BOLD}📊 Summary:{COLOR_RESET}")
print(f" {COLOR_GREEN}🐞 Total issues added: {total_issues}{COLOR_RESET}")
print(f" {COLOR_GREEN}🔀 Total PRs added: {total_prs}{COLOR_RESET}")
log_lines.append("\n📊 Summary:")
log_lines.append(f" 🐞 Total issues added: {total_issues}")
log_lines.append(f" 🔀 Total PRs added: {total_prs}")
if total_issues > 0 or total_prs > 0:
print(f" {COLOR_GREEN}🚀 Release notes generated successfully!{COLOR_RESET}")
log_lines.append(" 🚀 Release notes generated successfully!")
else:
print(f" {COLOR_YELLOW}⚠️ No matching issues or PRs found for this milestone.{COLOR_RESET}")
log_lines.append(" ⚠️ No matching issues or PRs found for this milestone.")
# Output excluded issues summary (regression fix: always show this)
if excluded_issues:
print(f"\n{COLOR_YELLOW}🔎 Issues not added to release notes and ready for your review. These closed issues not included in the release notes. Reason(s) for exclusion are provided:{COLOR_RESET}\n")
log_lines.append("\n🔎 Issues not added to release notes and ready for your review. These closed issues not included in the release notes. Reason(s) for exclusion are provided:\n")
for issue, reasons in excluded_issues:
print(f" - {COLOR_BOLD}[{issue['title'].strip()} #{issue['number']}]({issue['html_url']}){COLOR_RESET} — {COLOR_RED}{'; '.join(reasons)}{COLOR_RESET}")
log_lines.append(f" - [{issue['title'].strip()} #{issue['number']}]({issue['html_url']}) — {'; '.join(reasons)}")
else:
print(f"\n{COLOR_GREEN}No excluded issues!{COLOR_RESET}")
log_lines.append("\nNo excluded issues!")
# Write log file
script_dir = os.path.dirname(os.path.abspath(__file__))
logs_dir = os.path.join(script_dir, "logs")
os.makedirs(logs_dir, exist_ok=True) # <-- fix: use exist_ok instead of exist_okay
script_name = os.path.splitext(os.path.basename(__file__))[0]
log_path = os.path.join(logs_dir, f"{script_name}.log")
with open(log_path, "w", encoding="utf-8") as log_file:
log_file.write("\n".join(log_lines))
print(f"\n\033[96m📝 Log file written to: {log_path}\033[0m")
# Write categorized markdown to _index_dummy.md
dummy_md_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_index_dummy.md")
with open(dummy_md_path, "w", encoding="utf-8") as f:
f.write(f"# Release Notes\n\n")
f.write(f"## {MILESTONE_TITLE}\n\n")
for header in CATEGORY_LABELS.values():
f.write(f"### {header}\n\n")
if categorized_issues[header]:
for line in categorized_issues[header]:
f.write(f"{line}\n")
else:
f.write("_No issues in this category._\n")
f.write("\n")
f.write("### Uncategorized\n\n")
if uncategorized_issues:
for line in uncategorized_issues:
f.write(f"{line}\n")
else:
f.write("_No issues in this category._\n")
f.write("\n")
f.write("### Release Candidate Pull Requests with no linked issue\n\n")
if categorized_prs:
for line in categorized_prs:
f.write(f"{line}\n")
else:
f.write("_No PRs in this category._\n")
f.write("\n")
print(f"\n\033[96m📄 Categorized markdown written to: {dummy_md_path}\033[0m")