mirror of
https://github.com/bitwarden/android.git
synced 2026-05-09 05:20:24 -05:00
Compare commits
4 Commits
agalles/fd
...
release-no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf2ac7cac9 | ||
|
|
5bbbf7c5e4 | ||
|
|
39d10fa77d | ||
|
|
f0f240f8e5 |
64
.github/scripts/release-notes/linked_issues.py
vendored
Normal file
64
.github/scripts/release-notes/linked_issues.py
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
import sys
|
||||
import subprocess
|
||||
from typing import List
|
||||
|
||||
def create_linked_issue_comment(repo_owner: str, repo_name: str, release_name: str, release_link: str, pr_numbers: List[int]) -> str:
|
||||
if len(pr_numbers) == 0:
|
||||
return ""
|
||||
|
||||
pr_links = [f"* https://github.com/{repo_owner}/{repo_name}/pull/{pr_number}" for pr_number in pr_numbers]
|
||||
|
||||
return f":shipit: Pull Request(s) linked to this issue released in [{release_name}]({release_link}):\n\n"+ "\n".join(pr_links)
|
||||
|
||||
def comment_linked_issues_in_pr(owner: str, repo: str, pr_number: int) -> None:
|
||||
"""Use GitHub CLI to comment all issues linked to a PR.
|
||||
"""
|
||||
|
||||
|
||||
linked_issues = get_linked_issues(owner, repo, pr_number)
|
||||
for issue_number in linked_issues:
|
||||
comment_github_issue(owner, repo, issue_number, comment)
|
||||
|
||||
def comment_github_issue(owner: str, repo: str, issue_number: int, comment: str) -> None:
|
||||
"""Use GitHub CLI to comment on an issue.
|
||||
"""
|
||||
subprocess.run([
|
||||
'gh', 'issue', 'comment', str(issue_number), '--body', comment, '--repo', f'{owner}/{repo}'
|
||||
], check=True)
|
||||
|
||||
def get_linked_issues(owner: str, repo: str, pr_number: int) -> List[int]:
|
||||
"""Use GitHub CLI to retrieve linked issue numbers for a PR.
|
||||
"""
|
||||
|
||||
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 []
|
||||
112
.github/scripts/release-notes/process_release_notes.py
vendored
Normal file
112
.github/scripts/release-notes/process_release_notes.py
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
from typing import List, Tuple
|
||||
|
||||
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)
|
||||
4
.github/scripts/release-notes/pyproject.toml
vendored
Normal file
4
.github/scripts/release-notes/pyproject.toml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[project]
|
||||
name = "release-notes-processor"
|
||||
description = "Process GitHub release notes to clean up formatting and extract relevant IDs."
|
||||
requires-python = ">=3.13"
|
||||
30
.github/scripts/release-notes/test_linked_issues.py
vendored
Normal file
30
.github/scripts/release-notes/test_linked_issues.py
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
import unittest
|
||||
from linked_issues import get_linked_issues, create_linked_issue_comment
|
||||
|
||||
class TestLinkedIssues(unittest.TestCase):
|
||||
def test_create_linked_issue_comment(self):
|
||||
test_cases = [
|
||||
("bitwarden", "android", "v2025.1.0", "https://github.com/bitwarden/android/releases/tag/v2025.1.0", [4696]),
|
||||
("bitwarden", "android", "v2025.2.0", "https://github.com/bitwarden/android/releases/tag/v2025.2.0", [4809, 1, 2, 3]),
|
||||
("bitwarden", "android", "v2025.3.0", "https://github.com/bitwarden/android/releases/tag/v2025.3.0", []),
|
||||
]
|
||||
|
||||
for owner, repo, release_name, release_link, pr_numbers in test_cases:
|
||||
with self.subTest(msg=f"Creating comment for issue in release {release_name}"):
|
||||
comment = create_linked_issue_comment(owner, repo, release_name, release_link, pr_numbers)
|
||||
print(comment + "\n")
|
||||
|
||||
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()
|
||||
95
.github/scripts/release-notes/test_process_release_notes.py
vendored
Normal file
95
.github/scripts/release-notes/test_process_release_notes.py
vendored
Normal 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()
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -28,3 +28,9 @@ user.properties
|
||||
/app/src/standardBeta/google-services.json
|
||||
/app/src/standardRelease/google-services.json
|
||||
/authenticator/src/google-services.json
|
||||
|
||||
# python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
Reference in New Issue
Block a user