mirror of
https://github.com/bitwarden/android.git
synced 2026-10-22 11:02:51 -05:00
Add release notes processing script and unit tests
This commit is contained in:
2 files changed
+255
No files matched your search
@@ -0,0 +1,160 @@
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
from typing import List, Tuple
|
||||
|
||||
def get_linked_issues(owner: str, repo: str, pr_number: int) -> List[int]:
|
||||
"""Call GitHub API to get linked issue numbers for a PR.
|
||||
|
||||
Args:
|
||||
owner: Repository owner
|
||||
repo: Repository name
|
||||
pr_number: Pull request number
|
||||
|
||||
Returns:
|
||||
List of linked issue numbers
|
||||
"""
|
||||
|
||||
|
||||
query = """
|
||||
query ($owner: String!, $repo: String!, $pr: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pr) {
|
||||
closingIssuesReferences(first: 100) {
|
||||
nodes {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
try:
|
||||
result = subprocess.run([
|
||||
'gh', 'api', 'graphql',
|
||||
'-F', f'owner={owner}',
|
||||
'-F', f'repo={repo}',
|
||||
'-F', f'pr={pr_number}',
|
||||
'-f', f'query={query}',
|
||||
'--jq', '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'
|
||||
], capture_output=True, text=True, check=True)
|
||||
|
||||
# Split output into lines and convert to integers
|
||||
if result.stdout.strip():
|
||||
return [int(num) for num in result.stdout.strip().split('\n')]
|
||||
return []
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
print(f"Error fetching linked issues for PR #{pr_number}")
|
||||
return []
|
||||
|
||||
|
||||
|
||||
def extract_jira_tickets(line: str) -> List[str]:
|
||||
# Find all Jira tickets in format ABC-123 (with any prefix/suffix)
|
||||
return re.findall(r'[A-Z]+-\d+', line)
|
||||
|
||||
def extract_pr_numbers(line: str) -> List[str]:
|
||||
# Match PR numbers from GitHub format (#123)
|
||||
return re.findall(r'#(\d+)', line)
|
||||
|
||||
def process_line(line: str) -> str:
|
||||
"""Process a single line from release notes by removing Jira tickets, conventional commit prefixes and other common patterns.
|
||||
|
||||
Args:
|
||||
line: A single line from release notes
|
||||
|
||||
Returns:
|
||||
Processed line with tickets and prefixes removed
|
||||
|
||||
Example:
|
||||
>>> process_line("[ABC-123] feat(ui): Add new button")
|
||||
"Add new button"
|
||||
"""
|
||||
original = line
|
||||
|
||||
# Remove Jira ticket patterns:
|
||||
# line = re.sub(r'\[[A-Z]+-\d+\]', '', line) # [ABC-123] -> ""
|
||||
# line = re.sub(r'[A-Z]+-\d+:\s', '', line) # ABC-123: -> ""
|
||||
# line = re.sub(r'[A-Z]+-\d+\s-\s', '', line) # ABC-123 - -> ""
|
||||
|
||||
# Remove keywords and their variations
|
||||
patterns = [
|
||||
r'BACKPORT', # BACKPORT -> ""
|
||||
r'[deps]:', # [deps]: -> ""
|
||||
r'feat(?:\([^)]*\))?:', # feat: or feat(ui): -> ""
|
||||
r'bug(?:\([^)]*\))?:', # bug: or bug(core): -> ""
|
||||
r'ci(?:\([^)]*\))?:' # ci: or ci(workflow): -> ""
|
||||
]
|
||||
for pattern in patterns:
|
||||
line = re.sub(pattern, '', line)
|
||||
|
||||
cleaned = line.strip()
|
||||
if cleaned != original.strip():
|
||||
print(f"Processed: {original.strip()} -> {cleaned}")
|
||||
return cleaned
|
||||
|
||||
def process_file(input_file: str) -> Tuple[List[str], List[str], List[str]]:
|
||||
jira_tickets: List[str] = []
|
||||
pr_numbers: List[str] = []
|
||||
processed_lines: List[str] = []
|
||||
|
||||
print("Processing file: ", input_file)
|
||||
|
||||
with open(input_file, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
should_process = line and not line.endswith(':')
|
||||
|
||||
if should_process:
|
||||
tickets = extract_jira_tickets(line)
|
||||
jira_tickets.extend(tickets)
|
||||
|
||||
prs = extract_pr_numbers(line)
|
||||
pr_numbers.extend(prs)
|
||||
processed_lines.append(process_line(line))
|
||||
else:
|
||||
processed_lines.append(line)
|
||||
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
jira_tickets = list(dict.fromkeys(jira_tickets))
|
||||
pr_numbers = list(dict.fromkeys(pr_numbers))
|
||||
|
||||
print("Jira tickets:", ",".join(jira_tickets))
|
||||
print("PR numbers:", ",".join(pr_numbers))
|
||||
print("Finished processing file: ", input_file)
|
||||
return jira_tickets, pr_numbers, processed_lines
|
||||
|
||||
def save_results(jira_tickets: List[str], pr_numbers: List[str], processed_lines: List[str],
|
||||
jira_file: str = 'jira_tickets.txt',
|
||||
pr_file: str = 'pr_numbers.txt',
|
||||
processed_file: str = 'processed_notes.txt') -> None:
|
||||
with open(jira_file, 'w') as f:
|
||||
f.write('\n'.join(jira_tickets))
|
||||
|
||||
with open(pr_file, 'w') as f:
|
||||
f.write('\n'.join(pr_numbers))
|
||||
|
||||
with open(processed_file, 'w') as f:
|
||||
f.write('\n'.join(processed_lines))
|
||||
|
||||
if __name__ == '__main__':
|
||||
input_file = 'release_notes.txt'
|
||||
jira_file = 'jira_tickets.txt'
|
||||
pr_file = 'pr_numbers.txt'
|
||||
processed_file = 'processed_notes.txt'
|
||||
|
||||
if len(sys.argv) >= 2:
|
||||
input_file = sys.argv[1]
|
||||
if len(sys.argv) >= 3:
|
||||
jira_file = sys.argv[2]
|
||||
if len(sys.argv) >= 4:
|
||||
pr_file = sys.argv[3]
|
||||
if len(sys.argv) >= 5:
|
||||
processed_file = sys.argv[4]
|
||||
|
||||
jira_tickets, pr_numbers, processed_lines = process_file(input_file)
|
||||
save_results(jira_tickets, pr_numbers, processed_lines, jira_file, pr_file, processed_file)
|
||||
@@ -0,0 +1,95 @@
|
||||
import unittest
|
||||
import tempfile
|
||||
import os
|
||||
from process_release_notes import extract_jira_tickets, extract_pr_numbers, process_line, process_file, get_linked_issues
|
||||
|
||||
class TestProcessReleaseNotes(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.test_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
|
||||
def tearDown(self):
|
||||
os.unlink(self.test_file.name)
|
||||
|
||||
def test_extract_jira_tickets(self):
|
||||
test_cases = [
|
||||
("[ABC-123] Some text", ["ABC-123"]),
|
||||
("DEF-456: Some text", ["DEF-456"]),
|
||||
("GHI-789 - Some text", ["GHI-789"]),
|
||||
("Multiple [ABC-123] and DEF-456: tickets", ["ABC-123", "DEF-456"]),
|
||||
("No tickets here", []),
|
||||
("Mixed formats ABC-123 [DEF-456] GHI-789:", ["ABC-123", "DEF-456", "GHI-789"])
|
||||
]
|
||||
for input_text, expected in test_cases:
|
||||
with self.subTest(input_text=input_text):
|
||||
result = extract_jira_tickets(input_text)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_extract_pr_numbers(self):
|
||||
test_cases = [
|
||||
("PR #123 text", ["123"]),
|
||||
("Multiple PRs #456 and #789", ["456", "789"]),
|
||||
("No PR numbers", [])
|
||||
]
|
||||
for input_text, expected in test_cases:
|
||||
with self.subTest(input_text=input_text):
|
||||
result = extract_pr_numbers(input_text)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_process_line(self):
|
||||
test_cases = [
|
||||
("[ABC-123] BACKPORT Some text", "Some text"),
|
||||
("DEF-456: feat(component): Some text", "Some text"),
|
||||
("GHI-789 - bug(fix): Some text", "Some text"),
|
||||
("ci: Some text", "Some text"),
|
||||
("ci(workflow): Some text", "Some text"),
|
||||
("feat: Direct feature", "Direct feature"),
|
||||
("bug: Simple bugfix", "Simple bugfix"),
|
||||
("Normal text", "Normal text")
|
||||
]
|
||||
for input_text, expected in test_cases:
|
||||
with self.subTest(input_text=input_text):
|
||||
result = process_line(input_text)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_process_file(self):
|
||||
content = """
|
||||
### Features:
|
||||
[ABC-123] feat(comp): Feature 1 #123
|
||||
DEF-456: bug(fix): Bug fix #456
|
||||
GHI-789 - BACKPORT Some text #789
|
||||
|
||||
### Bug Fixes:
|
||||
Another line without changes
|
||||
"""
|
||||
with open(self.test_file.name, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
jira_tickets, pr_numbers, processed_lines = process_file(self.test_file.name)
|
||||
|
||||
self.assertEqual(jira_tickets, ["ABC-123", "DEF-456", "GHI-789"])
|
||||
self.assertEqual(pr_numbers, ["123", "456", "789"])
|
||||
self.assertEqual(processed_lines, [
|
||||
'',
|
||||
'### Features:',
|
||||
'Feature 1 #123',
|
||||
'Bug fix #456',
|
||||
'Some text #789',
|
||||
'',
|
||||
'### Bug Fixes:',
|
||||
'Another line without changes'
|
||||
])
|
||||
|
||||
def test_get_linked_issues(self):
|
||||
test_cases = [
|
||||
("bitwarden", "android", 4696, [4659]),
|
||||
("bitwarden", "android", 4809, [])
|
||||
]
|
||||
|
||||
for owner, repo, pr_id, expected_linked_issues in test_cases:
|
||||
with self.subTest(msg=f"Testing PR #{pr_id} for {owner}/{repo}"):
|
||||
result = get_linked_issues(owner, repo, pr_id)
|
||||
self.assertEqual(sorted(result), sorted(expected_linked_issues))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user