diff --git a/tools/changelog/generate_cl.py b/tools/changelog/generate_cl.py new file mode 100644 index 0000000000..362b4e8391 --- /dev/null +++ b/tools/changelog/generate_cl.py @@ -0,0 +1,92 @@ +""" +DO NOT MANUALLY RUN THIS SCRIPT. +--------------------------------- +This script is designed to generate and push a CL file that can be later compiled. +The body of the changelog is determined by the description of the PR that was merged. +If a commit is pushed without being associated with a PR, or if a PR is missing a CL, +the script is designed to exit as a failure. This is to help keep track of PRs without +CLs and direct commits. See the relating comments in the below source to disable this function. +This script depends on the tags.yml file located in the same directory. You can use that +file to configure the exact tags you'd like this script to use when generating changelog entries. +If this is being used in a /tg/ or Bee downstream, the default tags should work. +Expected envrionmental variables: +----------------------------------- +GIT_NAME: Username of the github account to be used as the commited (User provided) +GIT_EMAIL: Email associated with the above (User provided) +GITHUB_REPOSITORY: Github action variable representing the active repo (Action provided) +GITHUB_TOKEN: A snowflake token generated by the action, this will allow the action to push the changes (User provided, action generated) +GITHUB_SHA: The SHA associated with the commit that triggered the action (Action provided) +""" +import os +import io +import re +from pathlib import Path +from ruamel import yaml +from github import Github, InputGitAuthor + +CL_BODY = re.compile(r":cl:(.+)?\r\n((.|\n|\r)+?)\r\n\/:cl:", re.MULTILINE) +CL_SPLIT = re.compile(r"(^\w+):\s+(\w.+)", re.MULTILINE) + +git_email = os.getenv("GIT_EMAIL") +git_name = os.getenv("GIT_NAME") + +# Blessed is the GoOnStAtIoN birb ZeWaKa for thinking of this first +repo = os.getenv("GITHUB_REPOSITORY") +token = os.getenv("GITHUB_TOKEN") +sha = os.getenv("GITHUB_SHA") + +git = Github(token) +repo = git.get_repo(repo) +commit = repo.get_commit(sha) +pr_list = commit.get_pulls() + +if not pr_list.totalCount: + print("Direct commit detected") + exit(1) # Change to '0' if you do not want the action to fail when a direct commit is detected + +pr = pr_list[0] + +pr_body = pr.body +pr_number = pr.number +pr_author = pr.user.login + +write_cl = {} +try: + cl = CL_BODY.search(pr_body) + cl_list = CL_SPLIT.findall(cl.group(2)) +except AttributeError: + print("No CL found!") + exit(1) # Change to '0' if you do not want the action to fail when no CL is provided + + +if cl.group(1) is not None: + write_cl['author'] = cl.group(1).lstrip() +else: + write_cl['author'] = pr_author + +write_cl['delete-after'] = True + +with open(Path.cwd().joinpath("tools/changelog/tags.yml")) as file: + tags = yaml.safe_load(file) + +write_cl['changes'] = [] + +for k, v in cl_list: + if k in tags['tags'].keys(): # Check to see if there are any valid tags, as determined by tags.yml + v = v.rstrip() + if v not in list(tags['defaults'].values()): # Check to see if the tags are associated with something that isn't the default text + write_cl['changes'].append({tags['tags'][k]: v}) + +if write_cl['changes']: + with io.StringIO() as cl_contents: + yaml = yaml.YAML() + yaml.indent(sequence=4, offset=2) + yaml.dump(write_cl, cl_contents) + cl_contents.seek(0) + + #Push the newly generated changelog to the master branch so that it can be compiled + repo.create_file(f"html/changelogs/AutoChangeLog-pr-{pr_number}.yml", f"Automatic changelog generation for PR #{pr_number} [ci skip]", content=f'{cl_contents.read()}', branch='master', committer=InputGitAuthor(git_name, git_email)) + print("Done!") +else: + print("No CL changes detected!") + exit(0) # Change to a '1' if you want the action to count lacking CL changes as a failure diff --git a/tools/changelog/makeChangelog.bat b/tools/changelog/makeChangelog.bat new file mode 100644 index 0000000000..0d9135837e --- /dev/null +++ b/tools/changelog/makeChangelog.bat @@ -0,0 +1,4 @@ +@echo off +rem Cheridan asked for this. - N3X +call python ss13_genchangelog.py ../html/changelog.html ../html/changelogs +pause diff --git a/tools/changelog/ss13_genchangelog.py b/tools/changelog/ss13_genchangelog.py new file mode 100644 index 0000000000..8f292dbe58 --- /dev/null +++ b/tools/changelog/ss13_genchangelog.py @@ -0,0 +1,215 @@ +''' +Usage: + $ python ss13_genchangelog.py [--dry-run] html/changelog.html html/changelogs/ +ss13_genchangelog.py - Generate changelog from YAML. +Copyright 2013 Rob "N3X15" Nelson +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +''' + +from __future__ import print_function +import yaml, os, glob, sys, re, time, argparse +from datetime import datetime, date, timedelta +from time import time + +today = date.today() + +dateformat = "%d %B %Y" + +opt = argparse.ArgumentParser() +opt.add_argument('-d', '--dry-run', dest='dryRun', default=False, action='store_true', help='Only parse changelogs and, if needed, the targetFile. (A .dry_changelog.yml will be output for debugging purposes.)') +opt.add_argument('-t', '--time-period', dest='timePeriod', default=9, type=int, help='Define how many weeks back the changelog should display') +opt.add_argument('targetFile', help='The HTML changelog we wish to update.') +opt.add_argument('ymlDir', help='The directory of YAML changelogs we will use.') + +args = opt.parse_args() + +all_changelog_entries = {} + +validPrefixes = [ + 'bugfix', + 'wip', + 'tweak', + 'soundadd', + 'sounddel', + 'rscdel', + 'rscadd', + 'imageadd', + 'imagedel', + 'spellcheck', + 'experiment', + 'tgs', + 'balance', + 'code_imp', + 'refactor', + 'config', + 'admin', + 'server' +] + +def dictToTuples(inp): + return [(k, v) for k, v in inp.items()] + +changelog_cache = os.path.join(args.ymlDir, '.all_changelog.yml') + +failed_cache_read = True +if os.path.isfile(changelog_cache): + try: + with open(changelog_cache,encoding='utf-8') as f: + (_, all_changelog_entries) = yaml.load_all(f) + failed_cache_read = False + + # Convert old timestamps to newer format. + new_entries = {} + for _date in all_changelog_entries.keys(): + ty = type(_date).__name__ + # print(ty) + if ty in ['str', 'unicode']: + temp_data = all_changelog_entries[_date] + _date = datetime.strptime(_date, dateformat).date() + new_entries[_date] = temp_data + else: + new_entries[_date] = all_changelog_entries[_date] + all_changelog_entries = new_entries + except Exception as e: + print("Failed to read cache:") + print(e, file=sys.stderr) + +if args.dryRun: + changelog_cache = os.path.join(args.ymlDir, '.dry_changelog.yml') + +if failed_cache_read and os.path.isfile(args.targetFile): + from bs4 import BeautifulSoup + from bs4.element import NavigableString + print(' Generating cache...') + with open(args.targetFile, 'r', encoding='utf-8') as f: + soup = BeautifulSoup(f) + for e in soup.find_all('div', {'class':'commit'}): + entry = {} + date = datetime.strptime(e.h2.string.strip(), dateformat).date() # key + for authorT in e.find_all('h3', {'class':'author'}): + author = authorT.string + # Strip suffix + if author.endswith('updated:'): + author = author[:-8] + author = author.strip() + + # Find