mirror of
https://github.com/yuzu-emu/mbedtls.git
synced 2024-11-25 17:15:42 +01:00
Add script to generate release notes
The script uses assemble_changelog.py to generate the bulk of the content, and adds some other headers, etc to create a standardised release note. The output includes placeholders for the sha256 hashes of the zip and the tarball which must be updated by hand. The script displays a checklist, which, when followed, results in a note that satisfies the release processes. Signed-off-by: Dave Rodgman <dave.rodgman@arm.com>
This commit is contained in:
parent
0ca6d38bc3
commit
9005b1f513
@ -218,13 +218,14 @@ class ChangeLog:
|
|||||||
category.name.decode('utf8'))
|
category.name.decode('utf8'))
|
||||||
self.categories[category.name] += category.body
|
self.categories[category.name] += category.body
|
||||||
|
|
||||||
def __init__(self, input_stream, changelog_format):
|
def __init__(self, input_stream, changelog_format, latest_only):
|
||||||
"""Create a changelog object.
|
"""Create a changelog object.
|
||||||
|
|
||||||
Populate the changelog object from the content of the file
|
Populate the changelog object from the content of the file
|
||||||
input_stream.
|
input_stream.
|
||||||
"""
|
"""
|
||||||
self.format = changelog_format
|
self.format = changelog_format
|
||||||
|
self.latest_only = latest_only
|
||||||
whole_file = input_stream.read()
|
whole_file = input_stream.read()
|
||||||
(self.header,
|
(self.header,
|
||||||
self.top_version_title, top_version_body,
|
self.top_version_title, top_version_body,
|
||||||
@ -247,12 +248,14 @@ class ChangeLog:
|
|||||||
"""Write the changelog to the specified file.
|
"""Write the changelog to the specified file.
|
||||||
"""
|
"""
|
||||||
with open(filename, 'wb') as out:
|
with open(filename, 'wb') as out:
|
||||||
|
if not self.latest_only:
|
||||||
out.write(self.header)
|
out.write(self.header)
|
||||||
out.write(self.top_version_title)
|
out.write(self.top_version_title)
|
||||||
for title, body in self.categories.items():
|
for title, body in self.categories.items():
|
||||||
if not body:
|
if not body:
|
||||||
continue
|
continue
|
||||||
out.write(self.format.format_category(title, body))
|
out.write(self.format.format_category(title, body))
|
||||||
|
if not self.latest_only:
|
||||||
out.write(self.trailer)
|
out.write(self.trailer)
|
||||||
|
|
||||||
|
|
||||||
@ -398,7 +401,7 @@ def check_output(generated_output_file, main_input_file, merged_files):
|
|||||||
if line not in generated_output:
|
if line not in generated_output:
|
||||||
raise LostContent(merged_file, line)
|
raise LostContent(merged_file, line)
|
||||||
|
|
||||||
def finish_output(changelog, output_file, input_file, merged_files):
|
def finish_output(changelog, output_file, input_file, merged_files, latest_only):
|
||||||
"""Write the changelog to the output file.
|
"""Write the changelog to the output file.
|
||||||
|
|
||||||
The input file and the list of merged files are used only for sanity
|
The input file and the list of merged files are used only for sanity
|
||||||
@ -412,6 +415,7 @@ def finish_output(changelog, output_file, input_file, merged_files):
|
|||||||
# then move it into place atomically.
|
# then move it into place atomically.
|
||||||
output_temp = output_file + '.tmp'
|
output_temp = output_file + '.tmp'
|
||||||
changelog.write(output_temp)
|
changelog.write(output_temp)
|
||||||
|
if not latest_only:
|
||||||
check_output(output_temp, input_file, merged_files)
|
check_output(output_temp, input_file, merged_files)
|
||||||
if output_temp != output_file:
|
if output_temp != output_file:
|
||||||
os.rename(output_temp, output_file)
|
os.rename(output_temp, output_file)
|
||||||
@ -438,7 +442,7 @@ def merge_entries(options):
|
|||||||
Remove the merged entries if options.keep_entries is false.
|
Remove the merged entries if options.keep_entries is false.
|
||||||
"""
|
"""
|
||||||
with open(options.input, 'rb') as input_file:
|
with open(options.input, 'rb') as input_file:
|
||||||
changelog = ChangeLog(input_file, TextChangelogFormat)
|
changelog = ChangeLog(input_file, TextChangelogFormat, options.latest_only)
|
||||||
files_to_merge = list_files_to_merge(options)
|
files_to_merge = list_files_to_merge(options)
|
||||||
if not files_to_merge:
|
if not files_to_merge:
|
||||||
sys.stderr.write('There are no pending changelog entries.\n')
|
sys.stderr.write('There are no pending changelog entries.\n')
|
||||||
@ -446,7 +450,7 @@ def merge_entries(options):
|
|||||||
for filename in files_to_merge:
|
for filename in files_to_merge:
|
||||||
with open(filename, 'rb') as input_file:
|
with open(filename, 'rb') as input_file:
|
||||||
changelog.add_file(input_file)
|
changelog.add_file(input_file)
|
||||||
finish_output(changelog, options.output, options.input, files_to_merge)
|
finish_output(changelog, options.output, options.input, files_to_merge, options.latest_only)
|
||||||
if not options.keep_entries:
|
if not options.keep_entries:
|
||||||
remove_merged_entries(files_to_merge)
|
remove_merged_entries(files_to_merge)
|
||||||
|
|
||||||
@ -494,6 +498,9 @@ def main():
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help=('Only list the files that would be processed '
|
help=('Only list the files that would be processed '
|
||||||
'(with some debugging information)'))
|
'(with some debugging information)'))
|
||||||
|
parser.add_argument('--latest-only',
|
||||||
|
action='store_true',
|
||||||
|
help=('Only generate the changes for the latest version'))
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
set_defaults(options)
|
set_defaults(options)
|
||||||
if options.list_files_only:
|
if options.list_files_only:
|
||||||
|
224
scripts/generate_release_notes.py
Executable file
224
scripts/generate_release_notes.py
Executable file
@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""This script generates the MbedTLS release notes in markdown format.
|
||||||
|
|
||||||
|
It does this by calling assemble_changelog.py to generate the bulk of
|
||||||
|
content, and also inserting other content such as a brief description,
|
||||||
|
hashes for the tar and zip files containing the release, etc.
|
||||||
|
|
||||||
|
Returns 0 on success, 1 on failure.
|
||||||
|
|
||||||
|
Note: must be run from Mbed TLS root."""
|
||||||
|
|
||||||
|
# Copyright (c) 2020, Arm Limited, All Rights Reserved
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# This file is part of Mbed TLS (https://tls.mbed.org)
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import os.path
|
||||||
|
import hashlib
|
||||||
|
import argparse
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
TEMPLATE = """## Description
|
||||||
|
|
||||||
|
These are the release notes for MbedTLS version {version}.
|
||||||
|
|
||||||
|
{description}
|
||||||
|
|
||||||
|
{changelog}
|
||||||
|
|
||||||
|
## Who should update
|
||||||
|
|
||||||
|
{whoshouldupdate}
|
||||||
|
|
||||||
|
## Checksum
|
||||||
|
|
||||||
|
The SHA256 hashes for the archives are:
|
||||||
|
|
||||||
|
```
|
||||||
|
{tarhash} mbedtls-{version}.tar.gz
|
||||||
|
{ziphash} mbedtls-{version}.zip
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
WHO_SHOULD_UPDATE_DEFAULT = 'We recommend all affected users should \
|
||||||
|
update to take advantage of the bug fixes contained in this release at \
|
||||||
|
an appropriate point in their development lifecycle.'
|
||||||
|
|
||||||
|
|
||||||
|
CHECKLIST = '''Please review the release notes to ensure that all of the \
|
||||||
|
following are documented (if needed):
|
||||||
|
- Missing functionality
|
||||||
|
- Changes in functionality
|
||||||
|
- Known issues
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
CUSTOM_WORDS = 'Hellman API APIs gz lifecycle Bugfix CMake inlined Crypto endian SHA xxx'
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_digest(filename):
|
||||||
|
"""Read given file and return a SHA256 digest"""
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
h.update(f.read())
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def error(text):
|
||||||
|
"""Display error message and exit"""
|
||||||
|
print(f'ERROR: {text}')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def warn(text):
|
||||||
|
"""Display warning message"""
|
||||||
|
print(f'WARNING: {text}')
|
||||||
|
|
||||||
|
|
||||||
|
def generate_content(args):
|
||||||
|
"""Return template populated with given content"""
|
||||||
|
for field in ('version', 'tarhash', 'ziphash', 'changelog',
|
||||||
|
'description', 'whoshouldupdate'):
|
||||||
|
if not field in args:
|
||||||
|
error(f'{field} not specified')
|
||||||
|
return TEMPLATE.format(**args)
|
||||||
|
|
||||||
|
|
||||||
|
def run_cmd(cmd, capture=True):
|
||||||
|
"""Run given command in a shell and return the command output"""
|
||||||
|
# Note: [:-1] strips the trailing newline introduced by the shell.
|
||||||
|
if capture:
|
||||||
|
return subprocess.check_output(cmd, shell=True, input=None,
|
||||||
|
universal_newlines=True)[:-1]
|
||||||
|
else:
|
||||||
|
subprocess.call(cmd, shell=True)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(args):
|
||||||
|
"""Parse command line arguments and return cleaned up args"""
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('-o', '--output', default='ReleaseNotes.md',
|
||||||
|
help='Output file (defaults to ReleaseNotes.md)')
|
||||||
|
parser.add_argument('-t', '--tar', action='store',
|
||||||
|
help='Optional tar containing release (to generate hash)')
|
||||||
|
parser.add_argument('-z', '--zip', action='store',
|
||||||
|
help='Optional zip containing release (to generate hash)')
|
||||||
|
parser.add_argument('-d', '--description', action='store', required=True,
|
||||||
|
help='Short description of release (or name of file containing this)')
|
||||||
|
parser.add_argument('-w', '--who', action='store', default=WHO_SHOULD_UPDATE_DEFAULT,
|
||||||
|
help='Optional short description of who should \
|
||||||
|
update (or name of file containing this)')
|
||||||
|
args = parser.parse_args(args)
|
||||||
|
|
||||||
|
# If these exist as files, interpret as files containing
|
||||||
|
# desired content rather than literal content.
|
||||||
|
for field in ('description', 'who'):
|
||||||
|
if os.path.exists(getattr(args, field)):
|
||||||
|
with open(getattr(args, field), 'r') as f:
|
||||||
|
setattr(args, field, f.read())
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def spellcheck(text):
|
||||||
|
with tempfile.NamedTemporaryFile() as temp_file:
|
||||||
|
with open(temp_file.name, 'w') as f:
|
||||||
|
f.write(text)
|
||||||
|
result = run_cmd(f'ispell -d american -w _- -a < {temp_file.name}')
|
||||||
|
input_lines = text.splitlines()
|
||||||
|
ispell_re = re.compile(r'& (\S+) \d+ \d+:.*')
|
||||||
|
bad_words = set()
|
||||||
|
bad_lines = set()
|
||||||
|
line_no = 1
|
||||||
|
for l in result.splitlines():
|
||||||
|
if l.strip() == '':
|
||||||
|
line_no += 1
|
||||||
|
elif l.startswith('&'):
|
||||||
|
m = ispell_re.fullmatch(l)
|
||||||
|
word = m.group(1)
|
||||||
|
if word.isupper():
|
||||||
|
# ignore all-uppercase words
|
||||||
|
pass
|
||||||
|
elif "_" in word:
|
||||||
|
# part of a non-English 'word' like PSA_CRYPTO_ECC
|
||||||
|
pass
|
||||||
|
elif word.startswith('-'):
|
||||||
|
# ignore flags
|
||||||
|
pass
|
||||||
|
elif word in CUSTOM_WORDS:
|
||||||
|
# accept known-good words
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
bad_words.add(word)
|
||||||
|
bad_lines.add(line_no)
|
||||||
|
if bad_words:
|
||||||
|
bad_lines = '\n'.join(' ' + input_lines[n] for n in sorted(bad_lines))
|
||||||
|
bad_words = ', '.join(bad_words)
|
||||||
|
warn('Release notes contain the following mis-spelled ' \
|
||||||
|
f'words: {bad_words}:\n{bad_lines}\n')
|
||||||
|
|
||||||
|
|
||||||
|
def gen_rel_notes(args):
|
||||||
|
"""Return release note content from given command line args"""
|
||||||
|
# Get version by parsing version.h. Assumption is that bump_version
|
||||||
|
# has been run and this contains the correct version number.
|
||||||
|
version = run_cmd('cat include/mbedtls/version.h | \
|
||||||
|
clang -Iinclude -dM -E - | grep "MBEDTLS_VERSION_STRING "')
|
||||||
|
version = version.split()[-1][1:-1]
|
||||||
|
|
||||||
|
# Get main changelog content.
|
||||||
|
assemble_path = os.path.join(os.getcwd(), 'scripts', 'assemble_changelog.py')
|
||||||
|
with tempfile.NamedTemporaryFile() as temp_file:
|
||||||
|
run_cmd(f'{assemble_path} -o {temp_file.name} --latest-only')
|
||||||
|
with open(temp_file.name) as f:
|
||||||
|
changelog = f.read()
|
||||||
|
|
||||||
|
arg_hash = {
|
||||||
|
'version': version,
|
||||||
|
'tarhash': '',
|
||||||
|
'ziphash': '',
|
||||||
|
'changelog': changelog.strip(),
|
||||||
|
'description': args.description.strip(),
|
||||||
|
'whoshouldupdate': args.who.strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
spellcheck(generate_content(arg_hash))
|
||||||
|
|
||||||
|
arg_hash['tarhash'] = sha256_digest(args.tar) if args.tar else "x" * 64
|
||||||
|
arg_hash['ziphash'] = sha256_digest(args.zip) if args.zip else "x" * 64
|
||||||
|
return generate_content(arg_hash)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Very basic check to see if we are in the root.
|
||||||
|
path = os.path.join(os.getcwd(), 'scripts', 'generate_release_notes.py')
|
||||||
|
if not os.path.exists(path):
|
||||||
|
error(f'{sys.argv[0]} must be run from the mbedtls root')
|
||||||
|
|
||||||
|
args = parse_args(sys.argv[1:])
|
||||||
|
|
||||||
|
content = gen_rel_notes(args)
|
||||||
|
with open(args.output, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print(CHECKLIST)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Loading…
Reference in New Issue
Block a user