From 5fb416ae340f4d2be3415ff54be7b256bccc7079 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 21 Mar 2019 22:47:17 -0700 Subject: [PATCH] Add a script to generate twitter and mailing list announcements Announcements taken from https://github.com/ansible/community/wiki/RelEng:-ReleaseProcess and then cleaned up: * Update issue reporting blurb from feedback from acozine and gundalow * Add a subject and to line for email output * Ignore long line tests on the jinja templates (as jinja doesn't give enough control to get rid of newlines when text wrapping) * Skip shebang and compile tests for older pythons since this is a release engineer-only script. (ok'd by mattclay) --- hacking/release-announcement.py | 292 +++++++++++++++++++++++++ test/sanity/code-smell/shebang.py | 2 + test/sanity/compile/python2.6-skip.txt | 2 + test/sanity/compile/python2.7-skip.txt | 2 + test/sanity/compile/python3.5-skip.txt | 2 + 5 files changed, 300 insertions(+) create mode 100755 hacking/release-announcement.py create mode 100644 test/sanity/compile/python2.6-skip.txt create mode 100644 test/sanity/compile/python2.7-skip.txt create mode 100644 test/sanity/compile/python3.5-skip.txt diff --git a/hacking/release-announcement.py b/hacking/release-announcement.py new file mode 100755 index 0000000000..f04431425d --- /dev/null +++ b/hacking/release-announcement.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +# coding: utf-8 +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +import argparse +import asyncio +import datetime +import hashlib +import sys +from collections import UserString +from distutils.version import LooseVersion + +import aiohttp +from jinja2 import Environment, DictLoader + +# pylint: disable= +VERSION_FRAGMENT = """ +{%- if versions | length > 1 %} + {% for version in versions %} + {% if loop.last %}and {{ version }}{% else %} + {% if versions | length == 2 %}{{ version }} {% else %}{{ version }}, {% endif -%} + {% endif -%} + {% endfor -%} +{%- else %}{{ versions[0] }}{% endif -%} +""" + +LONG_TEMPLATE = """ +{% set plural = True if versions | length == 1 else False %} +{% set latest_ver = (versions | sort(attribute='ver_obj'))[-1] %} + +To: ansible-devel@googlegroups.com, ansible-project@googlegroups.com, ansible-announce@googlegroups.com +Subject: New Ansible release{% if plural %}s{% endif %} {{ version_str }} + +{% filter wordwrap %} +Hi all- we're happy to announce that the general release of Ansible {{ version_str }}{% if plural %} are{%- else %} is{%- endif %} now available! +{% endfilter %} + + + +How do you get it? +------------------ + +{% for version in versions %} +$ pip install ansible=={{ version }} --user +{% if not loop.last %} +or +{% endif %} +{% endfor %} + +The tar.gz of the release{% if plural %}s{% endif %} can be found here: + +{% for version in versions %} +* {{ version }} + https://releases.ansible.com/ansible/ansible-{{ version }}.tar.gz + SHA256: {{ hashes[version] }} +{% endfor %} + + +What's new in {{ version_str }} +{{ '-' * (14 + version_str | length) }} + +{% filter wordwrap %} +{% if plural %}This release is a{% else %}These releases are{% endif %} maintenance release{% if plural %}s{% endif %} containing numerous bugfixes. The full {% if versions | length <= 1 %} changelog is{% else %} changelogs are{% endif %} at: +{% endfilter %} + + +{% for version in versions %} +* {{ version }} + https://github.com/ansible/ansible/blob/stable-{{ version.split('.')[:2] | join('.') }}/changelogs/CHANGELOG-v{{ version.split('.')[:2] | join('.') }}.rst +{% endfor %} + + +What's the schedule for future maintenance releases? +---------------------------------------------------- + +{% filter wordwrap %} +Future maintenance releases will occur approximately every 3 weeks. So expect the next one around {{ next_release.strftime('%Y-%m-%d') }}. +{% endfilter %} + + + +Porting Help +------------ + +{% filter wordwrap %} +We've published a porting guide at +https://docs.ansible.com/ansible/devel/porting_guides/porting_guide_{{ latest_ver.split('.')[:2] | join('.') }}.html to help migrate your content to {{ latest_ver.split('.')[:2] | join('.') }}. +{% endfilter %} + + + +{% filter wordwrap %} +If you discover any errors or if any of your working playbooks break when you upgrade to {{ latest_ver }}, please report the regression via https://github.com/ansible/ansible/issues/new/choose In your issue, be sure to mention the Ansible version that works and the one that doesn't. +{% endfilter %} + + +Thanks! + +-{{ name }} + +""" # noqa for E501 (line length). +# jinja2 is horrid about getting rid of extra newlines so we have to have a single per paragraph for +# proper wrapping to occur + +SHORT_TEMPLATE = """ +@ansible +{{ version_str }} +{% if versions | length > 1 %} + have +{% else %} + has +{% endif %} +been released! Get +{% if versions | length > 1 %} +them +{% else %} +it +{% endif %} +on PyPI: pip install ansible=={{ (versions|sort(attribute='ver_obj'))[-1] }}, +https://releases.ansible.com/ansible/, the Ansible PPA on Launchpad, or GitHub. Happy automating! +""" # noqa for E501 (line length). +# jinja2 is horrid about getting rid of extra newlines so we have to have a single per paragraph for +# proper wrapping to occur + +JINJA_ENV = Environment( + loader=DictLoader({'long': LONG_TEMPLATE, + 'short': SHORT_TEMPLATE, + 'version_string': VERSION_FRAGMENT, + }), + extensions=['jinja2.ext.i18n'], + trim_blocks=True, + lstrip_blocks=True, +) + + +class VersionStr(UserString): + def __init__(self, string): + super().__init__(string.strip()) + self.ver_obj = LooseVersion(string) + + +def parse_args(args): + parser = argparse.ArgumentParser(description="Generate email and twitter announcements" + " from template") + parser.add_argument("--version", dest="versions", type=str, required=True, action='append', + help="Versions of Ansible to announce") + parser.add_argument("--name", type=str, required=True, help="Real name to use on emails") + parser.add_argument("--email-out", type=str, default="-", + help="Filename to place the email announcement into") + parser.add_argument("--twitter-out", type=str, default="-", + help="Filename to place the twitter announcement into") + + args = parser.parse_args(args) + + # Make it possible to sort versions in the jinja2 templates + new_versions = [] + for version in args.versions: + new_versions.append(VersionStr(version)) + args.versions = new_versions + + return args + + +async def calculate_hash_from_tarball(session, version): + tar_url = f'https://releases.ansible.com/ansible/ansible-{version}.tar.gz' + tar_task = asyncio.create_task(session.get(tar_url)) + tar_response = await tar_task + + tar_hash = hashlib.sha256() + while True: + chunk = await tar_response.content.read(1024) + if not chunk: + break + tar_hash.update(chunk) + + return tar_hash.hexdigest() + + +async def parse_hash_from_file(session, version): + filename = f'ansible-{version}.tar.gz' + hash_url = f'https://releases.ansible.com/ansible/{filename}.sha' + hash_task = asyncio.create_task(session.get(hash_url)) + hash_response = await hash_task + + hash_content = await hash_response.read() + precreated_hash, precreated_filename = hash_content.split(None, 1) + if filename != precreated_filename.strip().decode('utf-8'): + raise ValueError(f'Hash file contains hash for a different file: {precreated_filename}') + + return precreated_hash.decode('utf-8') + + +async def get_hash(session, version): + calculated_hash = await calculate_hash_from_tarball(session, version) + precreated_hash = await parse_hash_from_file(session, version) + + if calculated_hash != precreated_hash: + raise ValueError(f'Hash in file ansible-{version}.tar.gz.sha {precreated_hash} does not' + f' match hash of tarball {calculated_hash}') + + return calculated_hash + + +async def get_hashes(versions): + hashes = {} + requestors = {} + async with aiohttp.ClientSession() as aio_session: + for version in versions: + requestors[version] = asyncio.create_task(get_hash(aio_session, version)) + + for version, request in requestors.items(): + await request + hashes[version] = request.result() + + return hashes + + +def next_release_date(weeks=3): + days_in_the_future = weeks * 7 + today = datetime.datetime.now() + numeric_today = today.weekday() + + # We release on Thursdays + if numeric_today == 3: + # 3 is Thursday + pass + elif numeric_today == 4: + # If this is Friday, we can adjust back to Thursday for the next release + today -= datetime.timedelta(days=1) + elif numeric_today < 3: + # Otherwise, slide forward to Thursday + today += datetime.timedelta(days=(3 - numeric_today)) + else: + # slightly different formula if it's past Thursday this week. We need to go forward to + # Thursday of next week + today += datetime.timedelta(days=(10 - numeric_today)) + + next_release = today + datetime.timedelta(days=days_in_the_future) + return next_release + + +def generate_long_message(versions, name): + hashes = asyncio.run(get_hashes(versions)) + + version_template = JINJA_ENV.get_template('version_string') + version_str = version_template.render(versions=versions).strip() + + next_release = next_release_date() + + template = JINJA_ENV.get_template('long') + message = template.render(versions=versions, version_str=version_str, + name=name, hashes=hashes, next_release=next_release) + return message + + +def generate_short_message(versions): + version_template = JINJA_ENV.get_template('version_string') + version_str = version_template.render(versions=versions).strip() + + template = JINJA_ENV.get_template('short') + message = template.render(versions=versions, version_str=version_str) + message = ' '.join(message.split()) + '\n' + return message + + +def write_message(filename, message): + if filename != '-': + with open(filename, 'w') as out_file: + out_file.write(message) + else: + sys.stdout.write('\n\n') + sys.stdout.write(message) + + +def main(): + args = parse_args(sys.argv[1:]) + + twitter_message = generate_short_message(args.versions) + email_message = generate_long_message(args.versions, args.name) + + write_message(args.twitter_out, twitter_message) + write_message(args.email_out, email_message) + + +if __name__ == '__main__': + main() diff --git a/test/sanity/code-smell/shebang.py b/test/sanity/code-smell/shebang.py index 7a64cf1661..e4f534d467 100755 --- a/test/sanity/code-smell/shebang.py +++ b/test/sanity/code-smell/shebang.py @@ -34,6 +34,8 @@ def main(): 'test/integration/targets/win_module_utils/library/legacy_only_old_way_win_line_ending.ps1', 'test/utils/shippable/timing.py', 'test/integration/targets/old_style_modules_posix/library/helloworld.sh', + # Python 3-only. Only run by release engineers + 'hacking/release-announcement.py', ]) # see https://unicode.org/faq/utf_bom.html#bom1 diff --git a/test/sanity/compile/python2.6-skip.txt b/test/sanity/compile/python2.6-skip.txt new file mode 100644 index 0000000000..a57ac4a272 --- /dev/null +++ b/test/sanity/compile/python2.6-skip.txt @@ -0,0 +1,2 @@ +# Only run by release engineers who can be asked to have newer Python3 on their systems +hacking/release-announcement.py diff --git a/test/sanity/compile/python2.7-skip.txt b/test/sanity/compile/python2.7-skip.txt new file mode 100644 index 0000000000..a57ac4a272 --- /dev/null +++ b/test/sanity/compile/python2.7-skip.txt @@ -0,0 +1,2 @@ +# Only run by release engineers who can be asked to have newer Python3 on their systems +hacking/release-announcement.py diff --git a/test/sanity/compile/python3.5-skip.txt b/test/sanity/compile/python3.5-skip.txt new file mode 100644 index 0000000000..a57ac4a272 --- /dev/null +++ b/test/sanity/compile/python3.5-skip.txt @@ -0,0 +1,2 @@ +# Only run by release engineers who can be asked to have newer Python3 on their systems +hacking/release-announcement.py