From 39aaab6a6e058bbc00f9ac9650d9b943d9db3ca6 Mon Sep 17 00:00:00 2001 From: Haibo Huang <hhb@google.com> Date: Fri, 25 Jan 2019 12:23:03 -0800 Subject: [PATCH] [Updater] Send email when a new version is found Bug: 109748616 Test: ./updater.sh check kotlinc googletest --json_output=/tmp/res.json Test: out/soong/host/linux-x86/bin/external_updater_notifier \ --result_file=/tmp/res.json \ --history ~/updater/history \ --recipients=hhb@google.com Change-Id: I27a4c1c2604d38106a08ce3eee1bcd03fdce80d7 --- Android.bp | 19 +++++-- external_updater.py | 111 ++++++++++++++++++++++++-------------- git_updater.py | 23 +++++--- github_archive_updater.py | 17 +++--- notifier.py | 108 +++++++++++++++++++++++++++++++++++++ update_package.sh | 1 + 6 files changed, 219 insertions(+), 60 deletions(-) create mode 100644 notifier.py diff --git a/Android.bp b/Android.bp index b10515d..328cf7a 100644 --- a/Android.bp +++ b/Android.bp @@ -23,15 +23,24 @@ python_binary_host { ], } +python_binary_host { + name: "external_updater_notifier", + main: "notifier.py", + srcs: [ + "notifier.py", + ], +} + python_library_host { name: "external_updater_lib", srcs: [ - "*.py", + "archive_utils.py", + "fileutils.py", + "git_updater.py", + "git_utils.py", + "github_archive_updater.py", "metadata.proto", - ], - exclude_srcs: [ - "*_test.py", - "external_updater.py", + "updater_utils.py", ], libs: [ "python-symbol", diff --git a/external_updater.py b/external_updater.py index 314c1e6..0ee2e86 100644 --- a/external_updater.py +++ b/external_updater.py @@ -19,8 +19,10 @@ updater.sh update kotlinc """ import argparse +import json import os import subprocess +import time from google.protobuf import text_format # pylint: disable=import-error @@ -72,6 +74,11 @@ def build_updater(proj_path): return updater +def has_new_version(updater): + """Whether an updater found a new version.""" + return updater.get_current_version() != updater.get_latest_version() + + def check_update(proj_path): """Checks updates for a project. Prints result on console. @@ -84,56 +91,78 @@ def check_update(proj_path): end='') updater = build_updater(proj_path) if updater is None: - return (None, None) + return (None, 'Failed to create updater') try: - new_version = updater.check() - if new_version: + updater.check() + if has_new_version(updater): print(color_string(' Out of date!', 'STALE')) else: print(color_string(' Up to date.', 'FRESH')) - return (updater, new_version) + return (updater, None) except (IOError, ValueError) as err: print('{} {}.'.format(color_string('Failed.', 'ERROR'), err)) - return (None, None) + return (updater, str(err)) except subprocess.CalledProcessError as err: - print( - '{} {}\nstdout: {}\nstderr: {}.'.format( - color_string( - 'Failed.', - 'ERROR'), - err, - err.stdout, - err.stderr)) - return (None, None) + msg = 'stdout: {}\nstderr: {}.'.format( + err.stdout, + err.stderr) + print('{} {}.'.format(color_string('Failed.', 'ERROR'), + msg)) + return (updater, msg) + + +def _process_update_result(path): + res = {} + updater, err = check_update(path) + if err is not None: + res['error'] = str(err) + else: + res['current'] = updater.get_current_version() + res['latest'] = updater.get_latest_version() + return res + + +def _check_some(paths, delay): + results = {} + for path in paths: + results[path] = _process_update_result(path) + time.sleep(delay) + return results + + +def _check_all(delay): + results = {} + for path, dirs, files in os.walk(args.path): + dirs.sort(key=lambda d: d.lower()) + if fileutils.METADATA_FILENAME in files: + # Skip sub directories. + dirs[:] = [] + results[path] = _process_update_result(path) + time.sleep(delay) + return results def check(args): """Handler for check command.""" + if args.all: + results = _check_all(args.delay) + else: + results = _check_some(args.paths, args.delay) - check_update(args.path) + if args.json_output is not None: + with open(args.json_output, 'w') as f: + json.dump(results, f, sort_keys=True, indent=4) def update(args): """Handler for update command.""" - updater, new_version = check_update(args.path) + updater, err = check_update(args.path) if updater is None: return - if not new_version and not args.force: - return - - updater.update() - - -def checkall(args): - """Handler for checkall command.""" - for root, dirs, files in os.walk(args.path): - dirs.sort(key=lambda d: d.lower()) - if fileutils.METADATA_FILENAME in files: - # Skip sub directories. - dirs[:] = [] - check_update(root) + if has_new_version(updater) or args.force: + updater.update() def parse_args(): @@ -148,20 +177,20 @@ def parse_args(): check_parser = subparsers.add_parser( 'check', help='Check update for one project.') check_parser.add_argument( - 'path', - help='Path of the project. ' + 'paths', nargs='*', + help='Paths of the project. ' 'Relative paths will be resolved from external/.') + check_parser.add_argument( + '--json_output', + help='Path of a json file to write result to.') + check_parser.add_argument( + '--all', + help='If set, check updates for all supported projects.') + check_parser.add_argument( + '--delay', default=0, type=int, + help='Time in seconds to wait between checking two projects.') check_parser.set_defaults(func=check) - # Creates parser for checkall command. - checkall_parser = subparsers.add_parser( - 'checkall', help='Check update for all projects.') - checkall_parser.add_argument( - '--path', - default=fileutils.EXTERNAL_PATH, - help='Starting path for all projects. Default to external/.') - checkall_parser.set_defaults(func=checkall) - # Creates parser for update command. update_parser = subparsers.add_parser('update', help='Update one project.') update_parser.add_argument( diff --git a/git_updater.py b/git_updater.py index 15f0360..81ef922 100644 --- a/git_updater.py +++ b/git_updater.py @@ -60,21 +60,28 @@ class GitUpdater(): self._setup_remote() if git_utils.is_commit(self.metadata.third_party.version): # Update to remote head. - return self._check_head() + self._check_head() + else: + # Update to latest version tag. + self._check_tag() + + def get_current_version(self): + """Returns the latest version name recorded in METADATA.""" + return self.metadata.third_party.version - # Update to latest version tag. - return self._check_tag() + def get_latest_version(self): + """Returns the latest version name in upstream.""" + return self.new_version def _check_tag(self): tags = git_utils.list_remote_tags(self.proj_path, self.upstream_remote_name) - current_ver = self.metadata.third_party.version + current_ver = self.get_current_version() self.new_version = updater_utils.get_latest_version( current_ver, tags) self.merge_from = self.new_version print('Current version: {}. Latest version: {}'.format( current_ver, self.new_version), end='') - return self.new_version != current_ver def _check_head(self): commits = git_utils.get_commits_ahead( @@ -82,7 +89,8 @@ class GitUpdater(): self.android_remote_name + '/master') if not commits: - return False + self.new_version = self.get_current_version() + return self.new_version = commits[0] @@ -101,7 +109,6 @@ class GitUpdater(): time_behind = datetime.datetime.now() - commit_time print('{} commits ({} days) behind.'.format( len(commits), time_behind.days), end='') - return True def _write_metadata(self, path): updated_metadata = metadata_pb2.MetaData() @@ -129,4 +136,6 @@ class GitUpdater(): self.merge_from, len(commits), upstream_branch)) self._write_metadata(self.proj_path) + print("Running `git merge {merge_branch}`..." + .format(merge_branch=self.merge_from)) git_utils.merge(self.proj_path, self.merge_from) diff --git a/github_archive_updater.py b/github_archive_updater.py index bcb4bd5..f177b54 100644 --- a/github_archive_updater.py +++ b/github_archive_updater.py @@ -92,7 +92,7 @@ class GithubArchiveUpdater(): except IndexError: raise ValueError('Url format is not supported.') - def _get_latest_version(self): + def _fetch_latest_version(self): """Checks upstream and gets the latest release tag.""" url = 'https://api.github.com/repos/{}/{}/releases/latest'.format( @@ -115,7 +115,7 @@ class GithubArchiveUpdater(): self.new_url = choose_best_url(supported_assets, self.old_url.value) - def _get_latest_commit(self): + def _fetch_latest_commit(self): """Checks upstream and gets the latest commit to master.""" url = 'https://api.github.com/repos/{}/{}/commits/master'.format( @@ -126,10 +126,14 @@ class GithubArchiveUpdater(): self.new_url = 'https://github.com/{}/{}/archive/{}.zip'.format( self.owner, self.repo, self.new_version) - def _get_current_version(self): + def get_current_version(self): """Returns the latest version name recorded in METADATA.""" return self.metadata.third_party.version + def get_latest_version(self): + """Returns the latest version name in upstream.""" + return self.new_version + def _write_metadata(self, url, path): updated_metadata = metadata_pb2.MetaData() updated_metadata.CopyFrom(self.metadata) @@ -144,14 +148,13 @@ class GithubArchiveUpdater(): Returns True if a new version is available. """ - current = self._get_current_version() + current = self.get_current_version() if git_utils.is_commit(current): - self._get_latest_commit() + self._fetch_latest_commit() else: - self._get_latest_version() + self._fetch_latest_version() print('Current version: {}. Latest version: {}'.format( current, self.new_version), end='') - return current != self.new_version def update(self): """Updates the package. diff --git a/notifier.py b/notifier.py new file mode 100644 index 0000000..d33dc65 --- /dev/null +++ b/notifier.py @@ -0,0 +1,108 @@ +# Copyright (C) 2018 The Android Open Source Project +# +# 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. +"""Send notification email if new version is found. + +Example usage: +external_updater_notifier \ + --result_file ~/updater/new_result \ + --history ~/updater/history \ + --recipients xxx@xxx.xxx +""" + +import argparse +import json +import os +import subprocess +import time + + +def parse_args(): + """Parses commandline arguments.""" + + parser = argparse.ArgumentParser( + description='Check updates for third party projects in external/.') + parser.add_argument( + '--result_file', + help='Json check result generated by external updater.') + parser.add_argument( + '--history', + help='Path of history file. If doesn' + 't exist, a new one will be created.') + parser.add_argument( + '--recipients', + help='Comma separated recipients of notification email.') + + return parser.parse_args() + + +def _send_email(proj, latest_ver, recipient): + print('Sending email for {}: {}'.format(proj, latest_ver)) + msg = """New version: {} + +To upgrade: + tools/external_updater/updater.sh update {}""".format( + latest_ver, proj) + subprocess.run(['sendgmr', '--to=' + recipient, + '--subject=' + proj], check=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + input=msg, encoding='ascii') + + +def _process_results(history, results, recipient): + for proj, res in results.items(): + if 'latest' not in res: + continue + latest_ver = res['latest'] + current_ver = res['current'] + if latest_ver == current_ver: + continue + proj_history = history.setdefault(proj, {}) + if latest_ver not in proj_history: + try: + _send_email(proj, latest_ver, recipient) + proj_history[latest_ver] = int(time.time()) + except subprocess.CalledProcessError as err: + msg = """Failed to send email for {} ({}). +stdout: {} +stderr: {}""".format(proj, latest_ver, err.stdout, err.stderr) + print(msg) + + +def send_notification(args): + """Compare results and send notification.""" + results = {} + with open(args.result_file, 'r') as f: + results = json.load(f) + history = {} + try: + with open(args.history, 'r') as f: + history = json.load(f) + except FileNotFoundError: + pass + + _process_results(history, results, args.recipients) + + with open(args.history, 'w') as f: + json.dump(history, f, sort_keys=True) + + +def main(): + """The main entry.""" + + args = parse_args() + send_notification(args) + + +if __name__ == '__main__': + main() diff --git a/update_package.sh b/update_package.sh index 8d5e3c6..d7cfa42 100644 --- a/update_package.sh +++ b/update_package.sh @@ -43,6 +43,7 @@ CopyIfPresent ".git" CopyIfPresent ".gitignore" CopyIfPresent "patches" CopyIfPresent "post_update.sh" +CopyIfPresent "OWNERS" echo "Applying patches..." for p in $tmp_dir/patches/*.diff -- GitLab