Skip to content

Commit 0d4012d

Browse files
authored
Multi-repo release notes (#37)
Parent issue: sequentech/meta#67 - Problem Description Partners and technical users of the software want to know what's new in each new release without having to read each of the 10+ repositories involved in each release. - Solution description Improve the release tool to not only generate the release notes for each repository, but also a general release notes that includes all the changes from all repos without duplications. - Tasks - Review copyright status on output by chatgpt. - New per-repository release-notes generation script that includes not only PRs but also the parent issue instead if it does exist - New script to update old PRs to add relations to parent issues when existing - Create a bot that ensures there's a relation the parent issue automatically when needed - Create a release script to include the generation of release-notes in the meta repository, creating comprehensive release notes from all the other repositories. - Update the release documentation to reflect step by step how to do a release in the future - Generate the comprehensive release notes for the 7.3.0 release - Create some issue templates in the meta repositories
2 parents 676cdad + 1e89d65 commit 0d4012d

File tree

10 files changed

+1485
-3
lines changed

10 files changed

+1485
-3
lines changed

.github/workflows/ort.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# SPDX-FileCopyrightText: 2014-2023 Sequent Tech Inc <legal@sequentech.io>
32
#
43
# SPDX-License-Identifier: AGPL-3.0-only

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
notes.*
2+
3+
__pycache__/

.ort.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
# SPDX-FileCopyrightText: 2014-2021 Sequent Tech Inc <legal@sequentech.io>
1+
# SPDX-FileCopyrightText: 2014-2023 Sequent Tech Inc <legal@sequentech.io>
22
#
33
# SPDX-License-Identifier: AGPL-3.0-only
44

55
---
66
resolutions:
77
rule_violations:
8-
- message: "The package PyPI::certifi:2022.12.7 has the declared ScanCode copyleft-limited categorized license MPL-2.0."
8+
- message: "The package PyPI::certifi:.* has the declared ScanCode copyleft-limited categorized license MPL-2.0."
99
reason: "DYNAMIC_LINKAGE_EXCEPTION"
1010
comment: "We are not modifying certifi and we dynamically link to it, so acording to MPL-2.0 this allows us to keep our code with a completely different license. In this kind of case, MPL-2.0 is not viral. https://www.mozilla.org/en-US/MPL/2.0/FAQ/"
11+
- message: "The package PyPI::.* has the declared ScanCode copyleft-limited categorized license LGPL.*"
12+
reason: "DYNAMIC_LINKAGE_EXCEPTION"
13+
comment: "This is not a problem because python modules are always dynamic, see https://stackoverflow.com/questions/8580223/using-python-module-on-lgpl-license-in-commercial-product and https://mail.python.org/pipermail/tutor/2015-June/105759.html."

comprehensive_release_notes.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/usr/bin/env python3
2+
# SPDX-FileCopyrightText: 2023 Sequent Tech Inc <legal@sequentech.io>
3+
#
4+
# SPDX-License-Identifier: AGPL-3.0-only
5+
6+
import os
7+
import re
8+
import yaml
9+
import argparse
10+
from github import Github
11+
from collections import defaultdict
12+
from release_notes import (
13+
create_new_branch,
14+
get_sem_release,
15+
get_release_head,
16+
get_release_notes,
17+
create_release_notes_md,
18+
parse_arguments,
19+
verbose_print
20+
)
21+
22+
META_REPOSITORY = "sequentech/meta"
23+
24+
REPOSITORIES = [
25+
"sequentech/common-ui",
26+
"sequentech/admin-console",
27+
"sequentech/election-portal",
28+
"sequentech/voting-booth",
29+
"sequentech/ballot-box",
30+
"sequentech/deployment-tool",
31+
"sequentech/tally-methods",
32+
"sequentech/tally-pipes",
33+
"sequentech/election-verifier",
34+
"sequentech/frestq",
35+
"sequentech/election-orchestra",
36+
"sequentech/iam",
37+
"sequentech/misc-tools",
38+
"sequentech/mixnet",
39+
"sequentech/documentation",
40+
"sequentech/ballot-verifier",
41+
"sequentech/release-tool",
42+
]
43+
44+
def get_comprehensive_release_notes(args, token, repos, prev_release, new_release, config):
45+
"""
46+
Generate comprehensive release notes for a list of repositories.
47+
48+
Args:
49+
token (str): GitHub access token.
50+
repos (list): A list of repository paths, e.g., ["org/repo1", "org/repo2"].
51+
prev_release (str): The previous release version (e.g. "1.1.0").
52+
new_release (str): The new release version (e.g. "1.2.0").
53+
config (dict): the configuration for generating release notes.
54+
55+
:return: dict, the release notes categorized by their labels.
56+
"""
57+
gh = Github(token)
58+
release_notes = defaultdict(list)
59+
60+
for repo_path in repos:
61+
verbose_print(args, f"Generating release notes for repo {repo_path}..")
62+
repo = gh.get_repo(repo_path)
63+
repo_notes = get_release_notes(gh, repo, prev_release, new_release, config)
64+
verbose_print(args, f"..generated")
65+
for category, notes in repo_notes.items():
66+
release_notes[category].extend(notes)
67+
68+
# Deduplicate notes by removing duplicates based on links
69+
deduplicated_release_notes = {}
70+
links = set()
71+
for category, notes in release_notes.items():
72+
deduplicated_notes = []
73+
for note in notes:
74+
link = re.search(r'https://\S+', note)
75+
if link and link.group(0) not in links:
76+
deduplicated_notes.append(note)
77+
links.add(link.group(0))
78+
deduplicated_release_notes[category] = deduplicated_notes
79+
80+
return deduplicated_release_notes
81+
82+
def parse_arguments():
83+
"""
84+
Parse command-line arguments specific for the comprehensive release notes script.
85+
86+
Returns:
87+
argparse.Namespace: An object containing parsed arguments.
88+
"""
89+
parser = argparse.ArgumentParser(
90+
description='Generate comprehensive release notes for multiple repositories.'
91+
)
92+
parser.add_argument(
93+
'previous_release',
94+
help='Previous release version in format `<major>.<minor>`, i.e. `7.2`'
95+
)
96+
parser.add_argument(
97+
'new_release',
98+
help=(
99+
'New release version in format `<major>.<minor>`, i.e. `7.2` '
100+
'or full semver release if it already exists i.e. `7.3.0`'
101+
)
102+
)
103+
parser.add_argument(
104+
'--dry-run',
105+
action='store_true',
106+
help=(
107+
'Output the release notes but do not create any tag, release or '
108+
'new branch.'
109+
)
110+
)
111+
parser.add_argument(
112+
'--silent',
113+
action='store_true',
114+
help='Disables verbose output'
115+
)
116+
parser.add_argument(
117+
'--draft',
118+
action='store_true',
119+
help='Mark the new release be as draft'
120+
)
121+
parser.add_argument(
122+
'--prerelease',
123+
action='store_true',
124+
help='Mark the new release be as a prerelease'
125+
)
126+
return parser.parse_args()
127+
128+
129+
def main():
130+
args = parse_arguments()
131+
132+
previous_release = args.previous_release
133+
new_release = args.new_release
134+
dry_run = args.dry_run
135+
github_token = os.getenv("GITHUB_TOKEN")
136+
137+
g = Github(github_token)
138+
meta_repo = g.get_repo(META_REPOSITORY)
139+
140+
with open(".github/release.yml") as f:
141+
config = yaml.safe_load(f)
142+
143+
prev_major, prev_minor, prev_patch = get_sem_release(previous_release)
144+
new_major, new_minor, new_patch = get_sem_release(new_release)
145+
146+
prev_release_head = get_release_head(prev_major, prev_minor, prev_patch)
147+
if new_patch or prev_major == new_major:
148+
new_release_head = get_release_head(new_major, new_minor, new_patch)
149+
else:
150+
new_release_head = meta_repo.default_branch
151+
152+
verbose_print(args, f"Input Parameters: {args}")
153+
verbose_print(args, f"Previous Release Head: {prev_release_head}")
154+
verbose_print(args, f"New Release Head: {new_release_head}")
155+
156+
release_notes = get_comprehensive_release_notes(
157+
args, github_token, REPOSITORIES, prev_release_head, new_release_head,
158+
config
159+
)
160+
161+
if not new_patch:
162+
latest_release = meta_repo.get_releases()[0]
163+
latest_tag = latest_release.tag_name
164+
major, minor, new_patch = map(int, latest_tag.split("."))
165+
if new_major == major and new_minor == minor:
166+
new_patch += 1
167+
else:
168+
new_patch = 0
169+
170+
new_tag = f"{new_major}.{new_minor}.{new_patch}"
171+
new_title = f"{new_tag} release"
172+
verbose_print(args, f"New Release Tag: {new_tag}")
173+
174+
release_notes_md = create_release_notes_md(release_notes, new_tag)
175+
176+
verbose_print(args, f"Generated Release Notes: {release_notes_md}")
177+
178+
if not dry_run:
179+
if prev_major < new_major:
180+
verbose_print(args, "Creating new branch")
181+
create_new_branch(meta_repo, new_release_head)
182+
else:
183+
branch = None
184+
try:
185+
branch = meta_repo.get_branch(new_release_head)
186+
except:
187+
verbose_print(args, "Creating new branch")
188+
create_new_branch(meta_repo, new_release_head)
189+
branch = meta_repo.get_branch(new_release_head)
190+
191+
verbose_print(args, "Creating new release")
192+
meta_repo.create_git_tag_and_release(
193+
tag=new_tag,
194+
tag_message=new_title,
195+
type='commit',
196+
object=branch.commit.sha,
197+
release_name=new_title,
198+
release_message=release_notes_md,
199+
prerelease=args.prerelease,
200+
draft=args.draft
201+
)
202+
verbose_print(args, f"Executed Actions: Branch created and new release created")
203+
else:
204+
verbose_print(args, "Dry Run: No actions executed")
205+
206+
if __name__ == "__main__":
207+
main()

0 commit comments

Comments
 (0)