Skip to content

Commit a1619b1

Browse files
committed
Create Prepare Release workflow
1 parent ac19c4d commit a1619b1

File tree

3 files changed

+203
-0
lines changed

3 files changed

+203
-0
lines changed

.github/workflows/prepare_release.yml

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Release Prep
2+
3+
on:
4+
push:
5+
workflow_dispatch:
6+
inputs:
7+
branch:
8+
description: 'Branch to merge release notes and code analysis into.'
9+
required: true
10+
default: 'main'
11+
version:
12+
description:
13+
'Version to use for the release. Must be in format: X.Y.Z.'
14+
date:
15+
description:
16+
'Date of the release. Must be in format YYYY-MM-DD.'
17+
18+
jobs:
19+
preparerelease:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
- name: Set up Python 3.10
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: '3.10'
27+
28+
- name: Install dependencies
29+
run: |
30+
python -m pip install --upgrade pip
31+
python -m pip install requests==2.31.0
32+
python -m pip install bandit==1.7.7
33+
python -m pip install .[test]
34+
35+
- name: Generate release notes
36+
env:
37+
GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
38+
run: >
39+
python scripts/release_notes_generator.py
40+
-v ${{ inputs.version }}
41+
-d ${{ inputs.date }}
42+
43+
- name: Save static code analysis
44+
env:
45+
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
46+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
47+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
48+
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
49+
run: >
50+
python -m scripts.bandit.bandit_report_generator
51+
52+
- name: Create pull request
53+
id: cpr
54+
uses: peter-evans/create-pull-request@v4
55+
with:
56+
token: ${{ secrets.GH_ACCESS_TOKEN }}
57+
commit-message: Prepare release for v${{ inputs.version }}
58+
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
59+
committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
60+
title: v${{ inputs.version }} Release Preparation
61+
body: "This is an auto-generated PR to prepare the release."
62+
branch: prepared-release
63+
branch-suffix: short-commit-hash
64+
base: ${{ inputs.branch }}

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ exclude = [
188188
".ipynb_checkpoints",
189189
"*.ipynb",
190190
"tasks.py",
191+
"scripts/**.py",
191192
]
192193

193194
[tool.ruff.lint]

scripts/release_notes_generator.py

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Script to generate release notes."""
2+
import argparse
3+
import os
4+
from collections import defaultdict
5+
import requests
6+
7+
8+
LABEL_TO_HEADER = {
9+
'feature request': 'New Features',
10+
'bug': 'Bugs Fixed',
11+
'internal': 'Internal',
12+
'maintenance': 'Maintenance',
13+
'customer success': 'Customer Success',
14+
'documentation': 'Documentation',
15+
'misc': 'Miscellaneous'
16+
}
17+
ISSUE_LABELS = [
18+
'documentation',
19+
'maintenance',
20+
'internal',
21+
'bug',
22+
'feature request',
23+
'customer success'
24+
]
25+
NEW_LINE = '\n'
26+
GITHUB_URL = 'https://api.github.com/repos/sdv-dev/sdv'
27+
GITHUB_TOKEN = os.getenv('GH_ACCESS_TOKEN')
28+
29+
30+
def _get_milestone_number(milestone_title):
31+
url = f'{GITHUB_URL}/milestones'
32+
headers = {
33+
'Authorization': f'Bearer {GITHUB_TOKEN}'
34+
}
35+
query_params = {
36+
'milestone': milestone_title,
37+
'state': 'all',
38+
'per_page': 100
39+
}
40+
response = requests.get(url, headers=headers, params=query_params)
41+
body = response.json()
42+
if response.status_code != 200:
43+
raise Exception(str(body))
44+
milestones = body
45+
for milestone in milestones:
46+
if milestone.get('title') == milestone_title:
47+
return milestone.get('number')
48+
raise ValueError(f'Milestone {milestone_title} not found in past 100 milestones.')
49+
50+
51+
def _get_issues_by_milestone(milestone):
52+
headers = {
53+
'Authorization': f'Bearer {GITHUB_TOKEN}'
54+
}
55+
# get milestone number
56+
milestone_number = _get_milestone_number(milestone)
57+
url = f'{GITHUB_URL}/issues'
58+
page = 1
59+
query_params = {
60+
'milestone': milestone_number,
61+
'state': 'all'
62+
}
63+
issues = []
64+
while True:
65+
query_params['page'] = page
66+
response = requests.get(url, headers=headers, params=query_params)
67+
body = response.json()
68+
if response.status_code != 200:
69+
raise Exception(str(body))
70+
issues_on_page = body
71+
if not issues_on_page:
72+
break
73+
issues.extend(issues_on_page)
74+
page += 1
75+
return issues
76+
77+
78+
def _get_issues_by_category(release_issues):
79+
category_to_issues = defaultdict(list)
80+
for issue in release_issues:
81+
issue_title = issue['title']
82+
issue_number = issue['number']
83+
issue_url = issue['html_url']
84+
line = f'* {issue_title} - Issue [#{issue_number}]({issue_url})'
85+
assignee = issue.get('assignee')
86+
if assignee:
87+
login = assignee['login']
88+
line += f' by @{login}'
89+
# Check if any known label is marked on the issue
90+
labels = [label['name'] for label in issue['labels']]
91+
found_category = False
92+
for category in ISSUE_LABELS:
93+
if category in labels:
94+
category_to_issues[category].append(line)
95+
found_category = True
96+
break
97+
if not found_category:
98+
category_to_issues['misc'].append(line)
99+
return category_to_issues
100+
101+
102+
def _create_release_notes(issues_by_category, version, date):
103+
title = f'## v{version} - {date}'
104+
release_notes = f'{title}{NEW_LINE}{NEW_LINE}'
105+
for category in ISSUE_LABELS + ['misc']:
106+
issues = issues_by_category.get(category)
107+
if issues:
108+
section_text = (
109+
f'### {LABEL_TO_HEADER[category]}{NEW_LINE}{NEW_LINE}'
110+
f'{NEW_LINE.join(issues)}{NEW_LINE}{NEW_LINE}'
111+
)
112+
release_notes += section_text
113+
return release_notes
114+
115+
116+
def update_release_notes(release_notes):
117+
"""Add the release notes for the new release to the ``HISTORY.md``."""
118+
file_path = 'HISTORY.md'
119+
with open(file_path, 'r') as history_file:
120+
history = history_file.read()
121+
token = '# HISTORY\n\n'
122+
split_index = history.find(token) + len(token) + 1
123+
header = history[:split_index]
124+
new_notes = f'{header}{release_notes}{history[split_index:]}'
125+
with open(file_path, 'w') as new_history_file:
126+
new_history_file.write(new_notes)
127+
128+
129+
if __name__ == '__main__':
130+
parser = argparse.ArgumentParser()
131+
parser.add_argument('-v', '--version', type=str, help='Release version number (ie. v1.0.1)')
132+
parser.add_argument('-d', '--date', type=str, help='Date of release in format YYYY-MM-DD')
133+
args = parser.parse_args()
134+
release_number = args.version
135+
release_issues = _get_issues_by_milestone(release_number)
136+
issues_by_category = _get_issues_by_category(release_issues)
137+
release_notes = _create_release_notes(issues_by_category, release_number, args.date)
138+
update_release_notes(release_notes)

0 commit comments

Comments
 (0)