def main()

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")