mbedtls/scripts/generate_release_notes.py
Dave Rodgman 9005b1f513 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>
2020-08-25 14:33:15 +01:00

225 lines
7.2 KiB
Python
Executable File

#!/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()