Add release note extraction script.

Introduces a crude Python script that can extract release notes from the
changelog for a given version and convert it to one of three different
output formats:

- `github` for GitHub Releases. This is just the Markdown itself, but
  with the very first line (the version) removed, because the version is
also the title of the release itself.

- `spigot` for Spigot Resources. This is the BBCode format used on the
  forums and in the resource descriptions.

- `curse` for CurseForge File Uploads. Curse uses a so-called "WYSIWYG"
  format that's really just HTML underneath.

The formats for Spigot and CurseForge are straightforward to convert to
as long as we only use simple text formatting, bullet lists, and links,
but that is really all the changelog should consist of anyway.

While this script already makes the release process quite a bit easier
on its own, the end goal is to _automate_ releases as much as possible,
and to do that, we need to be able to extract release notes, and we need
to be able to do it from GitHub Actions, which is quite a bit simpler if
we don't use third-party libraries.

Publishing releases on GitHub is almost trivial, while CurseForge is
pretty easy, and Hangar should be very doable as well. Spigot, on the
other hand, is stuck in the dark ages, so we must continue to upload
files manually there.
This commit is contained in:
Andreas Troelsen 2023-12-31 03:25:14 +01:00
parent 590f877756
commit 84776990b9
1 changed files with 224 additions and 0 deletions

224
scripts/extract-release-notes Executable file
View File

@ -0,0 +1,224 @@
#!/usr/bin/env python3
import argparse
import os
import re
import sys
VERSION_PREFIX = '## '
SECTION_PREFIX = '### '
LIST_ITEM_PREFIX = '- '
def main():
args = parse_args()
lines = extract(args.version)
output(lines, args.format)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
'version',
help='the version to extract release notes from the changelog for',
)
parser.add_argument(
'--format',
'-f',
choices=['github', 'spigot', 'curse'],
help='the format to output the release notes in',
)
return parser.parse_args()
def extract(target):
filename = 'changelog.md'
if not os.path.isfile(filename):
filename = os.path.join('..', filename)
if not os.path.isfile(filename):
print('error: changelog.md not found!')
sys.exit(1)
lines = []
with open(filename) as changelog:
found = False
for entry in changelog:
if entry.startswith(VERSION_PREFIX):
if found:
break
i = entry.find('[') + 1
j = entry.find(']')
version = entry[i:j]
if version == target:
if version[0].isdigit():
version = f'v{version}'
lines.append(f'{VERSION_PREFIX}{version}')
lines.append('')
found = True
continue
if not found:
continue
lines.append(entry.strip())
if not found:
print(f'error: version {target} not found!')
sys.exit(1)
return lines
def output(lines, fmt):
if fmt == 'github':
output_as_github_markdown(lines)
elif fmt == 'spigot':
output_as_spigot_bbcode(lines)
elif fmt == 'curse':
output_as_curseforge_html(lines)
else:
output_raw(lines)
def output_as_github_markdown(lines):
"""
GitHub Releases Markdown is printed as the raw output from the changelog
except for the version header (the first line), because the version number
is already used as the release title, so we don't want it to appear twice.
"""
output_raw(lines[1:])
def output_as_spigot_bbcode(lines):
"""
Spigot uses BBCode for resource update descriptions. It's very similar to
regular HTML, which makes it fairly easy to convert from Markdown. We just
need to use a [FONT] tag with Courier New for code bits.
"""
listing = False
for line in lines:
line = line.strip()
if line.startswith(VERSION_PREFIX):
i = len(VERSION_PREFIX)
version = line[i:]
print(f'[B]{version}[/B]')
continue
if line.startswith(SECTION_PREFIX):
if listing:
print('[/LIST]')
listing = False
i = len(SECTION_PREFIX)
section = line[i:]
print(f'[B]{section}:[/B]')
continue
if line.startswith(LIST_ITEM_PREFIX):
if not listing:
print('[LIST]')
listing = True
i = len(LIST_ITEM_PREFIX)
item = line[i:]
# Replace **bold** text
item = re.sub(r'\*\*(.*?)\*\*', r'[B]\1[/B]', item)
# Replace _italic_ text
item = re.sub(r'_(.*?)_', r'[I]\1[/I]', item)
# Replace `code` text
item = re.sub(r'`(.*?)`', r'[FONT=Courier New]\1[/FONT]', item)
# Replace [links](url)
item = re.sub(r'\[([^\]]+)]\(([^)]+)\)', r'[URL=\2]\1[/URL]', item)
print(f'[*]{item}')
continue
if len(line) > 0:
print(line)
if listing:
print('[/LIST]')
def output_as_curseforge_html(lines):
"""
CurseForge uses regular HTML for file update descriptions, which makes it
fairly easy to convert from Markdown. Angled brackets need to be replaced
with their HTML entity equivalents, but other than that it's very similar
to the Spigot BBCode conversion.
"""
listing = False
for line in lines:
line = line.strip()
if line.startswith(VERSION_PREFIX):
i = len(VERSION_PREFIX)
version = line[i:]
print(f'<p><strong>{version}</strong></p>')
continue
if line.startswith(SECTION_PREFIX):
if listing:
print('</ul>')
listing = False
i = len(SECTION_PREFIX)
section = line[i:]
print(f'<p><strong>{section}:</strong></p>')
continue
if line.startswith(LIST_ITEM_PREFIX):
if not listing:
print('<ul>')
listing = True
i = len(LIST_ITEM_PREFIX)
item = line[i:]
# Replace angled brackets
item = item.replace('<', '&lt;')
item = item.replace('>', '&gt;')
# Replace **bold** text
item = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', item)
# Replace _italic_ text
item = re.sub(r'_(.*?)_', r'<emph>\1</emph>', item)
# Replace `code` text
item = re.sub(r'`(.*?)`', r'<code>\1</code>', item)
# Replace [links](url)
item = re.sub(r'\[([^\]]+)]\(([^)]+)\)', r'<a href="\2">\1</a>', item)
print(f'<li>{item}</li>')
continue
if len(line) > 0:
print(line)
if listing:
print('</ul>')
def output_raw(lines):
[print(line.strip()) for line in lines]
if __name__ == '__main__':
main()