From 9a4961b504198dd36cf343fec3612ca544da70ec Mon Sep 17 00:00:00 2001 From: Aaruni Kaushik Date: Wed, 17 Dec 2025 14:12:41 +0100 Subject: [PATCH 1/6] Script copied from OSCAR --- dev/releases/release_notes.py | 461 ++++++++++++++++++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100755 dev/releases/release_notes.py diff --git a/dev/releases/release_notes.py b/dev/releases/release_notes.py new file mode 100755 index 0000000000..1bfe5f03cf --- /dev/null +++ b/dev/releases/release_notes.py @@ -0,0 +1,461 @@ +#!/usr/bin/env python3 +############################################################################# +# Usage: +# ./release_notes.py [VERSION] +# +# For example +# ./release_notes.py 4.13.1 +# +# This assumes that the tags named v4.13.1, 4.13dev (?) and v4.13.0 (???) already exists. +# +# A version ending in .0 is consider MAJOR, any other MINOR +# Don't use this with versions like 4.13.0-beta1 + +import json +import os +import re +import copy +import subprocess +import sys +from datetime import datetime +from typing import Any, Dict, List + + +ownpath = os.path.abspath(sys.argv[0]) +dirpath = os.path.dirname(ownpath) +repopath = os.path.dirname(os.path.dirname(os.path.dirname(ownpath))) +newfile = f"{dirpath}/new.md" +finalfile = f"{repopath}/CHANGELOG.md" + +def usage(name: str) -> None: + print(f"Usage: `{name} [NEWVERSION]`") + sys.exit(1) + + +def is_existing_tag(tag: str) -> bool: + print(tag) + res = subprocess.run( + [ + "gh", + "release", + "list", + "--json=name", + "-q", + f""".[] | select(.name | contains("{tag.strip()}"))""" + ], + shell=False, + check=False, # this subprocess is allowed to fail + capture_output=True + ) + return res.stdout.decode() != "" + + +def find_previous_version(version: str) -> str: + major, minor, patchlevel = map(int, version.split(".")) + if major != 1: + error("unexpected OSCAR version, not starting with '1.'") + if patchlevel != 0: + patchlevel -= 1 + return f"{major}.{minor}.{patchlevel}" + minor -= 1 + patchlevel = 0 + while True: + v = f"{major}.{minor}.{patchlevel}" + if not is_existing_tag("v" + v): + break + patchlevel += 1 + if patchlevel == 0: + error("could not determine previous version") + patchlevel -= 1 + return f"{major}.{minor}.{patchlevel}" + +def notice(s): + print(s) + +def error(s): + print(s) + exit() + +def warning(s): + print('===================================================') + print(s) + print('===================================================') + +# the following is a list of pairs [LABEL, DESCRIPTION]; the first entry is the name of a GitHub label +# (be careful to match them precisely), the second is a headline for a section the release notes; any PR with +# the given label is put into the corresponding section; each PR is put into only one section, the first one +# one from this list it fits in. +# See also . +topics = { + "release notes: highlight": "Highlights", + "topic: algebraic geometry": "Algebraic Geometry", + "topic: combinatorics": "Combinatorics", + "topic: commutative algebra": "Commutative Algebra", + "topic: FTheoryTools": "F-Theory Tools", + "topic: groups": "Groups", + "topic: lie theory": "Lie Theory", + "topic: number theory": "Number Theory", + "topic: polyhedral geometry": "Polyhedral Geometry", + "topic: toric geometry": "Toric Geometry", + "topic: tropical geometry": "Tropical Geometry", + "package: AbstractAlgebra": "Changes related to the package AbstractAlgebra", + "package: AlgebraicSolving": "Changes related to the package AlgebraicSolving", + "package: GAP": "Changes related to the package GAP", + "package: Hecke": "Changes related to the package Hecke", + "package: Nemo": "Changes related to the package Nemo", + "package: Polymake": "Changes related to the package Polymake", + "package: Singular": "Changes related to the package Singular", +} +prtypes = { + "renaming": "Renamings", + "serialization": "Changes related to serializing data in the MRDI file format", + "enhancement": "New features or extended functionality", + "experimental": "Only changes experimental parts of OSCAR", + "optimization": "Performance improvements or improved testing", + "bug: wrong result": "Fixed bugs that returned incorrect results", + "bug: crash": "Fixed bugs that could lead to crashes", + "bug: unexpected error": "Fixed bugs that resulted in unexpected errors", + "bug": "Other fixed bugs", + "documentation": "Improvements or additions to documentation", +} + + +def get_tag_date(tag: str) -> str: + if is_existing_tag(tag): + res = subprocess.run( + [ + "gh", + "release", + "view", + f"{tag}", + "--json=createdAt" + ], + shell=False, + check=True, + capture_output=True + ) + res = json.loads(res.stdout.decode()) + else: + error("tag does not exist!") + return res['createdAt'][0:10] + + +def get_pr_list(date: str, extra: str) -> List[Dict[str, Any]]: + query = f'merged:>={date} -label:"release notes: not needed" -label:"release notes: added" base:master {extra}' + print("query: ", query) + res = subprocess.run( + [ + "gh", + "pr", + "list", + "--search", + query, + "--json", + "number,title,closedAt,labels,mergedAt,body", + "--limit", + "200", + ], + check=True, + capture_output=True, + text=True, + ) + jsonList = json.loads(res.stdout.strip()) + jsonList = sorted(jsonList, key=lambda d: d['number']) # sort by ascending PR number + return jsonList + + +def pr_to_md(pr: Dict[str, Any]) -> str: + """Returns markdown string for the PR entry""" + k = pr["number"] + if has_label(pr, 'release notes: use body'): + mdstring = re.sub(r'^- ', f"- [#{k}](https://github.com/oscar-system/Oscar.jl/pull/{k}) ", pr["body"]) + else: + title = pr["title"] + mdstring = f"- [#{k}](https://github.com/oscar-system/Oscar.jl/pull/{k}) {title}\n" + return mdstring + +def body_to_release_notes(pr): + body = pr['body'] + index1 = body.lower().find("## release notes") + if index1 == -1: + ## not found + ## complain and return fallback + print(f"Release notes section not found in PR number {pr['number']}!!") + return body + index2 = body.find('\n', index1) + 1 # the first line after the release notes line + bodylines = body[index2:].splitlines() + mdstring = "" + for line in bodylines: + line = line.rstrip() + if not line: + continue + elif line.startswith('- '): + mdstring = f"{mdstring}\n{line}" + else: + break + if not mdstring: + warning(f"Empty release notes section for PR #{pr['number']} !") + return mdstring + + +def has_label(pr: Dict[str, Any], label: str) -> bool: + return any(x["name"] == label for x in pr["labels"]) + + +def changes_overview( + prs: List[Dict[str, Any]], new_version: str +) -> None: + """Writes files with information for release notes.""" + + date = datetime.now().strftime("%Y-%m-%d") + release_url = f"https://github.com/oscar-system/Oscar.jl/releases/tag/v{new_version}" + + # Could also introduce some consistency checks here for wrong combinations of labels + notice("Writing release notes into file " + newfile) + with open(newfile, "w", encoding="utf-8") as relnotes_file: + prs_with_use_title = [ + pr for pr in prs if has_label(pr, "release notes: use title") or has_label(pr, "release notes: use body") + ] + # Write out all PRs with 'use title' + relnotes_file.write( + f"""# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project +tries to adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [{new_version}]({release_url}) - {date} + +The following gives an overview of the changes compared to the previous release. This list is not +complete, many more internal or minor changes were made, but we tried to only list those changes +which we think might affect some users directly. + +""" + ) + totalPRs = len(prs) + print(f"Total number of PRs: {totalPRs}") + countedPRs = 0 + for priorityobject in topics: + matches = [ + pr for pr in prs_with_use_title if has_label(pr, priorityobject) + ] + original_length = len(matches) + print("PRs with label '" + priorityobject + "': ", len(matches)) + print(matches) + countedPRs = countedPRs + len(matches) + if len(matches) == 0: + continue + relnotes_file.write("### " + topics[priorityobject] + "\n\n") + if topics[priorityobject] == 'Highlights': + itervar = topics + else: + itervar = prtypes + for typeobject in itervar: + if typeobject == priorityobject: + continue + matches_type = [ + pr for pr in matches if has_label(pr, typeobject) + ] + print("PRs with label '" + priorityobject + "' and type '" + typeobject + "': ", len(matches_type)) + if len(matches_type) == 0: + continue + relnotes_file.write(f"#### {itervar[typeobject]}\n\n") + for pr in matches_type: + relnotes_file.write(pr_to_md(pr)) + prs_with_use_title.remove(pr) + matches.remove(pr) + relnotes_file.write('\n') + # Items without a type label + if len(matches) > 0: + if len(matches) != original_length: + relnotes_file.write("#### Miscellaneous changes\n\n") + for pr in matches: + relnotes_file.write(pr_to_md(pr)) + prs_with_use_title.remove(pr) + relnotes_file.write('\n') + + print(f"Remaining PRs: {totalPRs - countedPRs}") + # The remaining PRs have no "kind" or "topic" label from the priority list + # (may have other "kind" or "topic" label outside the priority list). + # Check their list in the release notes, and adjust labels if appropriate. + if len(prs_with_use_title) > 0: + relnotes_file.write("### Other changes\n\n") + for typeobject in prtypes: + matches_type = [ + pr for pr in prs_with_use_title if has_label(pr, typeobject) + ] + len(matches_type) + print("PRs with type '" + typeobject + "': ", len(matches_type)) + if len(matches_type) == 0: + continue + relnotes_file.write("#### " + prtypes[typeobject] + "\n\n") + + for pr in matches_type: + relnotes_file.write(pr_to_md(pr)) + prs_with_use_title.remove(pr) + relnotes_file.write("\n") + + # Report PRs that have to be updated before inclusion into release notes. + prs_to_be_added = [pr for pr in prs if has_label(pr, "release notes: to be added")] + if len(prs_to_be_added) > 0: + relnotes_file.write("### **TODO** release notes: to be added" + "\n\n") + relnotes_file.write( + "If there are any PRs listed below, check their title and labels.\n" + ) + relnotes_file.write( + 'When done, change their label to "release notes: use title".\n\n' + ) + for pr in prs_to_be_added: + relnotes_file.write(pr_to_md(pr)) + relnotes_file.write("\n") + if len(prs_with_use_title) > 0: + relnotes_file.write( + "### **TODO** insufficient labels for automatic classification\n\n" + "The following PRs have neither a topic label assigned to them, nor a PR type. \n" + "**Manual intervention required.**\n\n") + for pr in prs_with_use_title: + relnotes_file.write(pr_to_md(pr)) + relnotes_file.write('\n') + relnotes_file.write('\n') + + # remove PRs already handled earlier + prs = [pr for pr in prs if not has_label(pr, "release notes: to be added")] + prs = [pr for pr in prs if not has_label(pr, "release notes: added")] + prs = [pr for pr in prs if not has_label(pr, "release notes: use title")] + prs = [pr for pr in prs if not has_label(pr, "release notes: use body")] + + # Report PRs that have neither "to be added" nor "added" or "use title" label + if len(prs) > 0: + relnotes_file.write("### **TODO** Uncategorized PR" + "\n\n") + relnotes_file.write( + "If there are any PRs listed below, either apply the same steps\n" + ) + relnotes_file.write( + 'as above, or change their label to "release notes: not needed".\n\n' + ) + for pr in prs: + relnotes_file.write(pr_to_md(pr)) + relnotes_file.write('\n') + + # now read back the rest of changelog.md into newfile + with open(finalfile, 'r') as oldchangelog: + oldchangelog.seek(262) + for line in oldchangelog.readlines(): + relnotes_file.write(line) + # finally copy over this new file to changelog.md + os.rename(newfile, finalfile) + +def split_pr_into_changelog(prs: List): + childprlist = [] + toremovelist = [] + for pr in prs: + if has_label(pr, 'release notes: use body'): + mdstring = body_to_release_notes(pr).strip() + mdlines = mdstring.split('\n') + pattern = r'\{.*\}$' + for line in mdlines: + cpr = copy.deepcopy(pr) + mans = re.search(pattern, line) + if mans: + label_list = mans.group().strip('{').strip('}').split(',') + for label in label_list: + label = label.strip() + if not (label in prtypes or label in topics): + warning(f"PR number #{pr['number']}'s changelog body has label {label}, " + "which is not a label we recognize ! We are ignoring this label. " + "This might result in a TODO changelog item!") + continue + cpr['labels'].append({'name': label}) + mindex = mans.span()[0] + line = line[0:mindex] + pass + else: + warning(f"PR number #{pr['number']} is tagged as \"Use Body\", but the body " + "does not provide tags! This will result in TODO changelog items!") + cpr['body'] = f'{line.strip()}\n' + childprlist.append(cpr) + if pr not in toremovelist: + toremovelist.append(pr) + prs.extend(childprlist) + prlist = [pr for pr in prs if pr not in toremovelist] + return prlist + +def main(new_version: str) -> None: + major, minor, patchlevel = map(int, new_version.split(".")) + extra = "" + release_type = 0 # 0 by default, 1 for point release, 2 for patch release + if major != 1: + error("unexpected OSCAR version, not starting with '1.'") + if patchlevel == 0: + # "major" OSCAR release which changes just the minor version + release_type = 1 + previous_minor = minor - 1 + basetag = f"v{major}.{minor}dev" + # *exclude* PRs backported to previous stable-1.X branch + extra = f'-label:"backport {major}.{previous_minor}.x done"' + else: + # "minor" OSCAR release which changes just the patchlevel + release_type = 2 + previous_patchlevel = patchlevel - 1 + basetag = f"v{major}.{minor}.{previous_patchlevel}" + # *include* PRs backported to current stable-4.X branch + extra = f'label:"backport {major}.{minor}.x done"' + + if release_type == 2: + timestamp = get_tag_date(basetag) + else: + # Find the timestamp of the last shared commit + shared_commit = subprocess.run([ + "git", + "merge-base", + basetag, + "HEAD" + ], shell=False, check=True, capture_output=True).stdout.decode().strip() + timestamp = subprocess.run([ + "git", + "show", + "-s", + "--format=\"%cI\"", + shared_commit + ], shell=False, check=True, capture_output=True).stdout.decode().strip().replace('"', '') + print("Base tag is", basetag) + print("Last common commit at ", timestamp) + + print("Downloading filtered PR list") + prs = get_pr_list(timestamp, extra) + prs = split_pr_into_changelog(prs) + # print(json.dumps(prs, sort_keys=True, indent=4)) + + # reset changelog file to state tracked in git + + subprocess.run(f'git checkout -- {finalfile}'.split(), check=True) + + changes_overview(prs, new_version) + + +if __name__ == "__main__": + # the argument is the new version + if len(sys.argv) == 1: + itag = subprocess.run( + [ + "gh", + "release", + "list", + "--json=name,isLatest", + "-q", + ".[] | select(.isLatest == true)" + ], + shell=False, + check=True, + capture_output=True + ) + itag = json.loads(itag.stdout.decode())["name"][1:] + itag = itag.split('.') + itag[-1] = str(int(itag[-1])+1) + itag = ".".join(itag) + main(itag) + elif len(sys.argv) != 2: + usage(sys.argv[0]) + else: + main(sys.argv[1]) From 989c347a76b69208600277aa26f4a9d8029cfece Mon Sep 17 00:00:00 2001 From: Aaruni Kaushik Date: Wed, 17 Dec 2025 15:34:07 +0100 Subject: [PATCH 2/6] add 1 extra character to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaa523ab77..565e8a112b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project tries to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + ## [0.47.0] - 2025-09-06 ### BREAKING From 8c08d693410d8808a8f186b20da5641fcbaf0f74 Mon Sep 17 00:00:00 2001 From: Aaruni Kaushik Date: Wed, 17 Dec 2025 16:09:39 +0100 Subject: [PATCH 3/6] Release notes script --- dev/releases/config.toml | 21 +++++++ dev/releases/release_notes.py | 108 +++++++++++++++------------------- 2 files changed, 69 insertions(+), 60 deletions(-) create mode 100644 dev/releases/config.toml diff --git a/dev/releases/config.toml b/dev/releases/config.toml new file mode 100644 index 0000000000..8458703035 --- /dev/null +++ b/dev/releases/config.toml @@ -0,0 +1,21 @@ +majorversion = 0 +reponame = "Nemocas/AbstractAlgebra.jl" + +[topics] +"release notes: highlight" = "Highlights" +breaking = "Breaking Changes" +ideals = "Changes related to the functionality of Ideals" +"polynomial ring" = "Changes related to Polynomial Rings" + +[prtypes] +breaking = "Breaking changes" +renaming = "Renamings" +serialization = "Changes related to serializing data in the MRDI file format" +enhancement = "New features or extended functionality" +experimental = "Only changes experimental parts of OSCAR" +optimization = "Performance improvements or improved testing" +"bug: wrong result" = "Fixed bugs that returned incorrect results" +"bug: crash" = "Fixed bugs that could lead to crashes" +"bug: unexpected error" = "Fixed bugs that resulted in unexpected errors" +bug = "Other fixed bugs" +doc = "Improvements or additions to documentation" diff --git a/dev/releases/release_notes.py b/dev/releases/release_notes.py index 1bfe5f03cf..ada35f3ffb 100755 --- a/dev/releases/release_notes.py +++ b/dev/releases/release_notes.py @@ -19,7 +19,8 @@ import sys from datetime import datetime from typing import Any, Dict, List - +import tomli +import tomli_w ownpath = os.path.abspath(sys.argv[0]) dirpath = os.path.dirname(ownpath) @@ -27,6 +28,24 @@ newfile = f"{dirpath}/new.md" finalfile = f"{repopath}/CHANGELOG.md" +# read config file +with open('config.toml', 'rb') as conffile: + conf = tomli.load(conffile) + +MAJORVERSION = conf['majorversion'] +REPONAME = conf['reponame'] +PROJECTNAME = REPONAME.split('/')[-1] + +# the following is a list of pairs [LABEL, DESCRIPTION]; the first entry is the name of a GitHub label +# (be careful to match them precisely), the second is a headline for a section the release notes; any PR with +# the given label is put into the corresponding section; each PR is put into only one section, the first one +# one from this list it fits in. +# See also . + +TOPICS = conf['topics'] +PRTYPES = conf['prtypes'] + + def usage(name: str) -> None: print(f"Usage: `{name} [NEWVERSION]`") sys.exit(1) @@ -52,8 +71,8 @@ def is_existing_tag(tag: str) -> bool: def find_previous_version(version: str) -> str: major, minor, patchlevel = map(int, version.split(".")) - if major != 1: - error("unexpected OSCAR version, not starting with '1.'") + if major != MAJORVERSION: + error(f"unexpected {PROJECTNAME} version, not starting with '{MAJORVERSION}.'") if patchlevel != 0: patchlevel -= 1 return f"{major}.{minor}.{patchlevel}" @@ -81,43 +100,6 @@ def warning(s): print(s) print('===================================================') -# the following is a list of pairs [LABEL, DESCRIPTION]; the first entry is the name of a GitHub label -# (be careful to match them precisely), the second is a headline for a section the release notes; any PR with -# the given label is put into the corresponding section; each PR is put into only one section, the first one -# one from this list it fits in. -# See also . -topics = { - "release notes: highlight": "Highlights", - "topic: algebraic geometry": "Algebraic Geometry", - "topic: combinatorics": "Combinatorics", - "topic: commutative algebra": "Commutative Algebra", - "topic: FTheoryTools": "F-Theory Tools", - "topic: groups": "Groups", - "topic: lie theory": "Lie Theory", - "topic: number theory": "Number Theory", - "topic: polyhedral geometry": "Polyhedral Geometry", - "topic: toric geometry": "Toric Geometry", - "topic: tropical geometry": "Tropical Geometry", - "package: AbstractAlgebra": "Changes related to the package AbstractAlgebra", - "package: AlgebraicSolving": "Changes related to the package AlgebraicSolving", - "package: GAP": "Changes related to the package GAP", - "package: Hecke": "Changes related to the package Hecke", - "package: Nemo": "Changes related to the package Nemo", - "package: Polymake": "Changes related to the package Polymake", - "package: Singular": "Changes related to the package Singular", -} -prtypes = { - "renaming": "Renamings", - "serialization": "Changes related to serializing data in the MRDI file format", - "enhancement": "New features or extended functionality", - "experimental": "Only changes experimental parts of OSCAR", - "optimization": "Performance improvements or improved testing", - "bug: wrong result": "Fixed bugs that returned incorrect results", - "bug: crash": "Fixed bugs that could lead to crashes", - "bug: unexpected error": "Fixed bugs that resulted in unexpected errors", - "bug": "Other fixed bugs", - "documentation": "Improvements or additions to documentation", -} def get_tag_date(tag: str) -> str: @@ -168,10 +150,13 @@ def pr_to_md(pr: Dict[str, Any]) -> str: """Returns markdown string for the PR entry""" k = pr["number"] if has_label(pr, 'release notes: use body'): - mdstring = re.sub(r'^- ', f"- [#{k}](https://github.com/oscar-system/Oscar.jl/pull/{k}) ", pr["body"]) + mdstring = re.sub( + r'^- ', f"- [#{k}](https://github.com/{REPONAME}/pull/{k}) ", + pr["body"] + ) else: title = pr["title"] - mdstring = f"- [#{k}](https://github.com/oscar-system/Oscar.jl/pull/{k}) {title}\n" + mdstring = f"- [#{k}](https://github.com/{REPONAME}/pull/{k}) {title}\n" return mdstring def body_to_release_notes(pr): @@ -208,7 +193,7 @@ def changes_overview( """Writes files with information for release notes.""" date = datetime.now().strftime("%Y-%m-%d") - release_url = f"https://github.com/oscar-system/Oscar.jl/releases/tag/v{new_version}" + release_url = f"https://github.com/{REPONAME}/releases/tag/v{new_version}" # Could also introduce some consistency checks here for wrong combinations of labels notice("Writing release notes into file " + newfile) @@ -223,7 +208,7 @@ def changes_overview( All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project -tries to adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +tries to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [{new_version}]({release_url}) - {date} @@ -236,21 +221,22 @@ def changes_overview( totalPRs = len(prs) print(f"Total number of PRs: {totalPRs}") countedPRs = 0 - for priorityobject in topics: + for priorityobject in TOPICS: matches = [ pr for pr in prs_with_use_title if has_label(pr, priorityobject) ] original_length = len(matches) print("PRs with label '" + priorityobject + "': ", len(matches)) - print(matches) countedPRs = countedPRs + len(matches) if len(matches) == 0: continue - relnotes_file.write("### " + topics[priorityobject] + "\n\n") - if topics[priorityobject] == 'Highlights': - itervar = topics + relnotes_file.write("### " + TOPICS[priorityobject] + "\n\n") + if TOPICS[priorityobject] == "Breaking Changes": + relnotes_file.write("> !These changes break compatibility from previous versions!\n\n") + if TOPICS[priorityobject] == 'Highlights' or TOPICS[priorityobject] == "Breaking Changes": + itervar = TOPICS else: - itervar = prtypes + itervar = PRTYPES for typeobject in itervar: if typeobject == priorityobject: continue @@ -281,7 +267,7 @@ def changes_overview( # Check their list in the release notes, and adjust labels if appropriate. if len(prs_with_use_title) > 0: relnotes_file.write("### Other changes\n\n") - for typeobject in prtypes: + for typeobject in PRTYPES: matches_type = [ pr for pr in prs_with_use_title if has_label(pr, typeobject) ] @@ -289,7 +275,7 @@ def changes_overview( print("PRs with type '" + typeobject + "': ", len(matches_type)) if len(matches_type) == 0: continue - relnotes_file.write("#### " + prtypes[typeobject] + "\n\n") + relnotes_file.write("#### " + PRTYPES[typeobject] + "\n\n") for pr in matches_type: relnotes_file.write(pr_to_md(pr)) @@ -361,10 +347,12 @@ def split_pr_into_changelog(prs: List): label_list = mans.group().strip('{').strip('}').split(',') for label in label_list: label = label.strip() - if not (label in prtypes or label in topics): - warning(f"PR number #{pr['number']}'s changelog body has label {label}, " - "which is not a label we recognize ! We are ignoring this label. " - "This might result in a TODO changelog item!") + if not (label in PRTYPES or label in TOPICS): + warning( + f"PR number #{pr['number']}'s changelog body has label {label}, " + "which is not a label we recognize ! We are ignoring this label. " + "This might result in a TODO changelog item!" + ) continue cpr['labels'].append({'name': label}) mindex = mans.span()[0] @@ -385,22 +373,22 @@ def main(new_version: str) -> None: major, minor, patchlevel = map(int, new_version.split(".")) extra = "" release_type = 0 # 0 by default, 1 for point release, 2 for patch release - if major != 1: - error("unexpected OSCAR version, not starting with '1.'") + if major != MAJORVERSION: + error(f"unexpected {PROJECTNAME} version, not starting with '{MAJORVERSION}.'") if patchlevel == 0: # "major" OSCAR release which changes just the minor version release_type = 1 previous_minor = minor - 1 basetag = f"v{major}.{minor}dev" # *exclude* PRs backported to previous stable-1.X branch - extra = f'-label:"backport {major}.{previous_minor}.x done"' + #extra = f'-label:"backport {major}.{previous_minor}.x done"' else: # "minor" OSCAR release which changes just the patchlevel release_type = 2 previous_patchlevel = patchlevel - 1 basetag = f"v{major}.{minor}.{previous_patchlevel}" # *include* PRs backported to current stable-4.X branch - extra = f'label:"backport {major}.{minor}.x done"' + #extra = f'label:"backport {major}.{minor}.x done"' if release_type == 2: timestamp = get_tag_date(basetag) From 854587b3ee0888443b3da5b92d0bbb2bf1bf525b Mon Sep 17 00:00:00 2001 From: Aaruni Kaushik Date: Wed, 17 Dec 2025 16:10:46 +0100 Subject: [PATCH 4/6] requirements for script [skip ci] --- dev/releases/release_notes.py | 1 - dev/releases/requirements.txt | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 dev/releases/requirements.txt diff --git a/dev/releases/release_notes.py b/dev/releases/release_notes.py index ada35f3ffb..7d528f391c 100755 --- a/dev/releases/release_notes.py +++ b/dev/releases/release_notes.py @@ -20,7 +20,6 @@ from datetime import datetime from typing import Any, Dict, List import tomli -import tomli_w ownpath = os.path.abspath(sys.argv[0]) dirpath = os.path.dirname(ownpath) diff --git a/dev/releases/requirements.txt b/dev/releases/requirements.txt new file mode 100644 index 0000000000..7efb948aa0 --- /dev/null +++ b/dev/releases/requirements.txt @@ -0,0 +1,2 @@ +tomli + From dd9beadd5ceb78a6a54bb8c8b7742b1823587451 Mon Sep 17 00:00:00 2001 From: Aaruni Kaushik Date: Wed, 17 Dec 2025 16:48:14 +0100 Subject: [PATCH 5/6] Just making linter happy. no functional changes --- dev/releases/release_notes.py | 60 ++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/dev/releases/release_notes.py b/dev/releases/release_notes.py index 7d528f391c..27410e5004 100755 --- a/dev/releases/release_notes.py +++ b/dev/releases/release_notes.py @@ -24,8 +24,8 @@ ownpath = os.path.abspath(sys.argv[0]) dirpath = os.path.dirname(ownpath) repopath = os.path.dirname(os.path.dirname(os.path.dirname(ownpath))) -newfile = f"{dirpath}/new.md" -finalfile = f"{repopath}/CHANGELOG.md" +NEWFILE = f"{dirpath}/new.md" +FINALFILE = f"{repopath}/CHANGELOG.md" # read config file with open('config.toml', 'rb') as conffile: @@ -35,10 +35,10 @@ REPONAME = conf['reponame'] PROJECTNAME = REPONAME.split('/')[-1] -# the following is a list of pairs [LABEL, DESCRIPTION]; the first entry is the name of a GitHub label -# (be careful to match them precisely), the second is a headline for a section the release notes; any PR with -# the given label is put into the corresponding section; each PR is put into only one section, the first one -# one from this list it fits in. +# the following loads a dict of {LABEL: DESCRIPTION}; the first entry is the name of a GitHub label +# (be careful to match them precisely), the second is a headline for a section the release notes; +# any PR with the given label is put into the corresponding section; each PR is put into only one +# section, the first one one from this list it fits in. # See also . TOPICS = conf['topics'] @@ -92,7 +92,7 @@ def notice(s): def error(s): print(s) - exit() + sys.exit(1) def warning(s): print('===================================================') @@ -122,7 +122,10 @@ def get_tag_date(tag: str) -> str: def get_pr_list(date: str, extra: str) -> List[Dict[str, Any]]: - query = f'merged:>={date} -label:"release notes: not needed" -label:"release notes: added" base:master {extra}' + query = ( + f'merged:>={date} -label:"release notes: not needed" -label:"release notes: added"' + f'base:master {extra}' + ) print("query: ", query) res = subprocess.run( [ @@ -140,9 +143,9 @@ def get_pr_list(date: str, extra: str) -> List[Dict[str, Any]]: capture_output=True, text=True, ) - jsonList = json.loads(res.stdout.strip()) - jsonList = sorted(jsonList, key=lambda d: d['number']) # sort by ascending PR number - return jsonList + json_list = json.loads(res.stdout.strip()) + json_list = sorted(json_list, key=lambda d: d['number']) # sort by ascending PR number + return json_list def pr_to_md(pr: Dict[str, Any]) -> str: @@ -155,7 +158,7 @@ def pr_to_md(pr: Dict[str, Any]) -> str: ) else: title = pr["title"] - mdstring = f"- [#{k}](https://github.com/{REPONAME}/pull/{k}) {title}\n" + mdstring = f"- [#{k}](https://github.com/{REPONAME}/pull/{k}) {title}\n" return mdstring def body_to_release_notes(pr): @@ -173,7 +176,7 @@ def body_to_release_notes(pr): line = line.rstrip() if not line: continue - elif line.startswith('- '): + if line.startswith('- '): mdstring = f"{mdstring}\n{line}" else: break @@ -195,10 +198,12 @@ def changes_overview( release_url = f"https://github.com/{REPONAME}/releases/tag/v{new_version}" # Could also introduce some consistency checks here for wrong combinations of labels - notice("Writing release notes into file " + newfile) - with open(newfile, "w", encoding="utf-8") as relnotes_file: + notice("Writing release notes into file " + NEWFILE) + with open(NEWFILE, "w", encoding="utf-8") as relnotes_file: prs_with_use_title = [ - pr for pr in prs if has_label(pr, "release notes: use title") or has_label(pr, "release notes: use body") + pr for pr in prs if + has_label(pr, "release notes: use title") or + has_label(pr, "release notes: use body") ] # Write out all PRs with 'use title' relnotes_file.write( @@ -230,9 +235,10 @@ def changes_overview( if len(matches) == 0: continue relnotes_file.write("### " + TOPICS[priorityobject] + "\n\n") - if TOPICS[priorityobject] == "Breaking Changes": - relnotes_file.write("> !These changes break compatibility from previous versions!\n\n") - if TOPICS[priorityobject] == 'Highlights' or TOPICS[priorityobject] == "Breaking Changes": + if priorityobject == "Breaking Changes": + relnotes_file.write("> !These changes break compatibility from previous versions!") + relnotes_file.write("\n\n") + if priorityobject in ['release notes: highlight', 'breaking']: itervar = TOPICS else: itervar = PRTYPES @@ -242,7 +248,10 @@ def changes_overview( matches_type = [ pr for pr in matches if has_label(pr, typeobject) ] - print("PRs with label '" + priorityobject + "' and type '" + typeobject + "': ", len(matches_type)) + print( + f"PRs with label '{priorityobject}' and type '{typeobject}': " + f"{len(matches_type)}" + ) if len(matches_type) == 0: continue relnotes_file.write(f"#### {itervar[typeobject]}\n\n") @@ -259,7 +268,7 @@ def changes_overview( relnotes_file.write(pr_to_md(pr)) prs_with_use_title.remove(pr) relnotes_file.write('\n') - + print(f"Remaining PRs: {totalPRs - countedPRs}") # The remaining PRs have no "kind" or "topic" label from the priority list # (may have other "kind" or "topic" label outside the priority list). @@ -324,12 +333,12 @@ def changes_overview( relnotes_file.write('\n') # now read back the rest of changelog.md into newfile - with open(finalfile, 'r') as oldchangelog: + with open(FINALFILE, 'r', encoding='ascii') as oldchangelog: oldchangelog.seek(262) for line in oldchangelog.readlines(): relnotes_file.write(line) # finally copy over this new file to changelog.md - os.rename(newfile, finalfile) + os.rename(NEWFILE, FINALFILE) def split_pr_into_changelog(prs: List): childprlist = [] @@ -356,7 +365,6 @@ def split_pr_into_changelog(prs: List): cpr['labels'].append({'name': label}) mindex = mans.span()[0] line = line[0:mindex] - pass else: warning(f"PR number #{pr['number']} is tagged as \"Use Body\", but the body " "does not provide tags! This will result in TODO changelog items!") @@ -415,8 +423,8 @@ def main(new_version: str) -> None: # print(json.dumps(prs, sort_keys=True, indent=4)) # reset changelog file to state tracked in git - - subprocess.run(f'git checkout -- {finalfile}'.split(), check=True) + + subprocess.run(f'git checkout -- {FINALFILE}'.split(), check=True) changes_overview(prs, new_version) From 64f3d3e5d7e23c4d9b8d9b62d7c69240824f3782 Mon Sep 17 00:00:00 2001 From: Aaruni Kaushik Date: Wed, 17 Dec 2025 16:56:48 +0100 Subject: [PATCH 6/6] Add switches for multi level changelog [skip ci] --- dev/releases/config.toml | 7 ++----- dev/releases/release_notes.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/dev/releases/config.toml b/dev/releases/config.toml index 8458703035..d5ef05366a 100644 --- a/dev/releases/config.toml +++ b/dev/releases/config.toml @@ -1,14 +1,11 @@ majorversion = 0 reponame = "Nemocas/AbstractAlgebra.jl" +enabletwolevel = false +requiretwolevel = false [topics] "release notes: highlight" = "Highlights" breaking = "Breaking Changes" -ideals = "Changes related to the functionality of Ideals" -"polynomial ring" = "Changes related to Polynomial Rings" - -[prtypes] -breaking = "Breaking changes" renaming = "Renamings" serialization = "Changes related to serializing data in the MRDI file format" enhancement = "New features or extended functionality" diff --git a/dev/releases/release_notes.py b/dev/releases/release_notes.py index 27410e5004..d090949a1f 100755 --- a/dev/releases/release_notes.py +++ b/dev/releases/release_notes.py @@ -34,6 +34,10 @@ MAJORVERSION = conf['majorversion'] REPONAME = conf['reponame'] PROJECTNAME = REPONAME.split('/')[-1] +ENABLE_TWOLEVEL = conf["enabletwolevel"] +REQUIRE_TWOLEVEL = False +if ENABLE_TWOLEVEL: + REQUIRE_TWOLEVEL = conf['requiretwolevel'] # the following loads a dict of {LABEL: DESCRIPTION}; the first entry is the name of a GitHub label # (be careful to match them precisely), the second is a headline for a section the release notes; @@ -42,7 +46,9 @@ # See also . TOPICS = conf['topics'] -PRTYPES = conf['prtypes'] +PRTYPES = {} +if ENABLE_TWOLEVEL: + PRTYPES = conf['prtypes'] def usage(name: str) -> None: @@ -235,7 +241,7 @@ def changes_overview( if len(matches) == 0: continue relnotes_file.write("### " + TOPICS[priorityobject] + "\n\n") - if priorityobject == "Breaking Changes": + if priorityobject == "breaking": relnotes_file.write("> !These changes break compatibility from previous versions!") relnotes_file.write("\n\n") if priorityobject in ['release notes: highlight', 'breaking']: @@ -273,7 +279,7 @@ def changes_overview( # The remaining PRs have no "kind" or "topic" label from the priority list # (may have other "kind" or "topic" label outside the priority list). # Check their list in the release notes, and adjust labels if appropriate. - if len(prs_with_use_title) > 0: + if len(prs_with_use_title) > 0 and ENABLE_TWOLEVEL: relnotes_file.write("### Other changes\n\n") for typeobject in PRTYPES: matches_type = [