You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by lh...@apache.org on 2025/07/31 13:18:46 UTC
(pulsar-site) 02/03: Add script for moving PR line items to categories in the release notes
This is an automated email from the ASF dual-hosted git repository.
lhotari pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pulsar-site.git
commit 76575abadab326e9411900e09591a7865bdd14fa
Author: Lari Hotari <lh...@users.noreply.github.com>
AuthorDate: Thu Jul 31 15:56:01 2025 +0300
Add script for moving PR line items to categories in the release notes
---
scripts/release_notes_reorder_script.py | 287 ++++++++++++++++++++++++++++++++
1 file changed, 287 insertions(+)
diff --git a/scripts/release_notes_reorder_script.py b/scripts/release_notes_reorder_script.py
new file mode 100755
index 00000000000..76be65c00a1
--- /dev/null
+++ b/scripts/release_notes_reorder_script.py
@@ -0,0 +1,287 @@
+#!/usr/bin/env python3
+"""
+Script to reorder Pulsar release notes based on a reference file structure
+or reorganize a single file using component-based section assignment.
+
+Single file mode: Takes one markdown file and reorganizes it by moving items
+to appropriate sections based on their component tags ([broker], [client], etc.)
+
+Reference mode: Takes two markdown files and reorders the second file to match
+the section organization of the first (reference) file.
+"""
+
+import re
+import sys
+from typing import Dict, List, Tuple, Optional
+from dataclasses import dataclass
+
+@dataclass
+class ReleaseItem:
+ """Represents a single release note item"""
+ full_line: str
+ pr_number: Optional[str] = None
+ component: Optional[str] = None
+ type_tag: Optional[str] = None
+
+class PulsarReleaseReorderer:
+ def __init__(self):
+ self.sections = [
+ "Library updates",
+ "Broker",
+ "Client",
+ "Pulsar IO and Pulsar Functions",
+ "Others",
+ "Tests & CI"
+ ]
+
+ def extract_pr_number(self, line: str) -> Optional[str]:
+ """Extract PR number from a line like ([#24360](...)"""
+ match = re.search(r'\(\[#(\d+)\]', line)
+ return match.group(1) if match else None
+
+ def extract_component_and_type(self, line: str) -> Tuple[Optional[str], Optional[str]]:
+ """Extract component and type from lines like [fix][broker] or [improve][client]"""
+ match = re.match(r'- \[(\w+)\]\[(\w+)\]', line.strip())
+ if match:
+ return match.group(2), match.group(1) # component, type
+ return None, None
+
+ def parse_file(self, filename: str) -> Dict[str, List[ReleaseItem]]:
+ """Parse a release notes file and return items organized by section"""
+ sections = {section: [] for section in self.sections}
+ current_section = None
+
+ with open(filename, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ lines = content.split('\n')
+
+ for line in lines:
+ # Check if line is a section header
+ if line.startswith('### ') and line[4:] in self.sections:
+ current_section = line[4:]
+ continue
+
+ # Check if line is a release item (starts with '- [')
+ if line.strip().startswith('- [') and current_section:
+ pr_number = self.extract_pr_number(line)
+ component, type_tag = self.extract_component_and_type(line)
+
+ item = ReleaseItem(
+ full_line=line,
+ pr_number=pr_number,
+ component=component,
+ type_tag=type_tag
+ )
+ sections[current_section].append(item)
+
+ return sections
+
+ def determine_section_for_item(self, item: ReleaseItem, reference_sections: Dict[str, List[ReleaseItem]]) -> str:
+ """Determine which section an item should go in based on reference file"""
+
+ # First, try to find the item by PR number in reference (if reference exists)
+ if item.pr_number and reference_sections:
+ for section_name, ref_items in reference_sections.items():
+ for ref_item in ref_items:
+ if ref_item.pr_number == item.pr_number:
+ return section_name
+
+ # Check if line contains "upgrade" (case insensitive) - always goes to Library updates
+ if "upgrade" in item.full_line.lower():
+ return 'Library updates'
+
+ # Use component-based heuristics
+ if item.component:
+ component_to_section = {
+ # Library updates
+ 'sec': 'Library updates', # Security updates often involve library upgrades
+
+ # Broker
+ 'admin': 'Broker',
+ 'broker': 'Broker',
+ 'meta': 'Broker',
+ 'ml': 'Broker', # Managed ledger
+ 'monitor': 'Broker',
+ 'offload': 'Broker',
+ 'schema': 'Broker',
+ 'storage': 'Broker',
+ 'txn': 'Broker', # Transaction
+ 'zk': 'Broker', # ZooKeeper
+ 'bk': 'Broker', # BookKeeper
+
+ # Client
+ 'client': 'Client',
+
+ # Pulsar IO and Pulsar Functions
+ 'fn': 'Pulsar IO and Pulsar Functions', # Functions
+ 'io': 'Pulsar IO and Pulsar Functions',
+
+ # Others
+ 'proxy': 'Others',
+ 'misc': 'Others',
+ 'doc': 'Others',
+ 'site': 'Others',
+ 'pip': 'Others',
+ 'ws': 'Others', # WebSocket
+ 'cli': 'Others', # Command line interface
+
+ # Tests & CI
+ 'build': 'Tests & CI',
+ 'ci': 'Tests & CI',
+ 'test': 'Tests & CI'
+ }
+
+ if item.component in component_to_section:
+ return component_to_section[item.component]
+
+ # Default fallback - place in Others
+ return 'Others'
+
+ def get_section_order_position(self, item: ReleaseItem, reference_items: List[ReleaseItem]) -> int:
+ """Get the relative position of an item within its section based on reference"""
+ if not item.pr_number:
+ return 999 # Put items without PR numbers at the end
+
+ # Find the item in reference and return its position (if reference exists)
+ if reference_items:
+ for i, ref_item in enumerate(reference_items):
+ if ref_item.pr_number == item.pr_number:
+ return i
+
+ # If not found in reference or no reference, use component and type for ordering
+ type_priority = {'fix': 0, 'improve': 1, 'feature': 2, 'cleanup': 3, 'refactor': 4, 'revert': 5}
+ component_priority = {
+ # Core components first
+ 'broker': 0, 'client': 1, 'admin': 2,
+ # Storage and ledger
+ 'ml': 3, 'storage': 4, 'bk': 5, 'offload': 6,
+ # Functions and IO
+ 'fn': 7, 'io': 8,
+ # Infrastructure
+ 'proxy': 9, 'meta': 10, 'zk': 11, 'monitor': 12,
+ # Protocol and communication
+ 'txn': 13, 'schema': 14,
+ # Security and build
+ 'sec': 15, 'build': 16, 'ci': 17, 'test': 18,
+ # Documentation and tooling
+ 'doc': 19, 'site': 20, 'pip': 21,
+ # Others components
+ 'ws': 22, 'cli': 23,
+ # Miscellaneous
+ 'misc': 24
+ }
+
+ type_pos = type_priority.get(item.type_tag, 999)
+ component_pos = component_priority.get(item.component, 999)
+
+ return 900 + type_pos * 10 + component_pos # Put new items near end but ordered
+
+ def reorder_file(self, reference_file: Optional[str], target_file: str, output_file: str):
+ """Main function to reorder the target file based on reference file structure"""
+
+ # Handle reference file parsing
+ if reference_file:
+ print(f"Parsing reference file: {reference_file}")
+ reference_sections = self.parse_file(reference_file)
+ else:
+ print("Single file mode: Using component-based reorganization")
+ reference_sections = {section: [] for section in self.sections}
+
+ print(f"Parsing target file: {target_file}")
+ target_sections = self.parse_file(target_file)
+
+ # Read the original target file to preserve header and structure
+ with open(target_file, 'r', encoding='utf-8') as f:
+ original_content = f.read()
+
+ # Extract header (everything before first ###)
+ header_match = re.search(r'^(.*?)(^### )', original_content, re.MULTILINE | re.DOTALL)
+ header = header_match.group(1) if header_match else ""
+
+ # Collect all items from target file
+ all_target_items = []
+ for section_items in target_sections.values():
+ all_target_items.extend(section_items)
+
+ print(f"Found {len(all_target_items)} items to reorder")
+
+ # Reorganize items by determining their proper sections
+ new_sections = {section: [] for section in self.sections}
+
+ for item in all_target_items:
+ proper_section = self.determine_section_for_item(item, reference_sections)
+ new_sections[proper_section].append(item)
+
+ # Sort items within each section based on reference order
+ for section_name in self.sections:
+ reference_items = reference_sections.get(section_name, [])
+ new_sections[section_name].sort(
+ key=lambda item: self.get_section_order_position(item, reference_items)
+ )
+
+ # Rebuild the file content
+ new_content = header
+
+ for section_name in self.sections:
+ items = new_sections[section_name]
+ if items: # Only add section if it has items
+ new_content += f"### {section_name}\n\n"
+ for item in items:
+ new_content += f"{item.full_line}\n"
+ new_content += "\n"
+
+ # Add the changelog link if it exists in original
+ changelog_match = re.search(r'For the complete list.*', original_content)
+ if changelog_match:
+ new_content += f"{changelog_match.group(0)}\n"
+
+ # Write the reordered file
+ with open(output_file, 'w', encoding='utf-8') as f:
+ f.write(new_content)
+
+ print(f"Reordered file written to: {output_file}")
+
+ if reference_file:
+ print(f"Used reference file: {reference_file}")
+ else:
+ print("Used component-based reorganization (no reference file)")
+
+ # Print summary
+ print("\nSection summary:")
+ for section_name in self.sections:
+ count = len(new_sections[section_name])
+ if count > 0:
+ print(f" {section_name}: {count} items")
+
+def main():
+ if len(sys.argv) < 2 or len(sys.argv) > 4:
+ print("Usage: python reorder_pulsar_release.py <target_file>")
+ print(" or: python reorder_pulsar_release.py <reference_file> <target_file> [output_file]")
+ print()
+ print("Single file mode: Reorganizes the file using component-based section assignment")
+ print(" Example: python reorder_pulsar_release.py pulsar-3.3.8.md")
+ print()
+ print("Reference mode: Reorganizes target file based on reference file structure")
+ print(" Example: python reorder_pulsar_release.py pulsar-3.0.13.md pulsar-3.3.8.md")
+ print(" Example: python reorder_pulsar_release.py pulsar-3.0.13.md pulsar-3.3.8.md pulsar-3.3.8-reordered.md")
+ print()
+ print("If output_file is not provided, the target_file will be updated in place.")
+ sys.exit(1)
+
+ if len(sys.argv) == 2:
+ # Single file mode - reorganize based on components only
+ reference_file = None
+ target_file = sys.argv[1]
+ output_file = target_file
+ else:
+ # Reference mode - use reference file for organization
+ reference_file = sys.argv[1]
+ target_file = sys.argv[2]
+ output_file = sys.argv[3] if len(sys.argv) == 4 else target_file
+
+ reorderer = PulsarReleaseReorderer()
+ reorderer.reorder_file(reference_file, target_file, output_file)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file