Skip to content

Commit 38e7d45

Browse files
authored
Create Prepare Release workflow (#445)
1 parent 827851f commit 38e7d45

File tree

3 files changed

+209
-35
lines changed

3 files changed

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

.github/workflows/static_code_analysis.yml

Lines changed: 0 additions & 35 deletions
This file was deleted.

scripts/release_notes_generator.py

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

0 commit comments

Comments
 (0)