# 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 \ --history ~/updater/history \ --generate_change \ --recipients xxx@xxx.xxx \ googletest """ from datetime import timedelta, datetime import argparse import json import os import re import subprocess import time # pylint: disable=invalid-name def parse_args(): """Parses commandline arguments.""" parser = argparse.ArgumentParser( description='Check updates for third party projects in external/.') 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.') parser.add_argument( '--generate_change', help='If set, an upgrade change will be uploaded to Gerrit.', action='store_true', required=False) parser.add_argument('paths', nargs='*', help='Paths of the project.') parser.add_argument('--all', action='store_true', help='Checks all projects.') return parser.parse_args() def _get_android_top(): return os.environ['ANDROID_BUILD_TOP'] CHANGE_URL_PATTERN = r'(https:\/\/[^\s]*android-review[^\s]*) Upgrade' CHANGE_URL_RE = re.compile(CHANGE_URL_PATTERN) def _read_owner_file(proj): owner_file = os.path.join(_get_android_top(), 'external', proj, 'OWNERS') if not os.path.isfile(owner_file): return None with open(owner_file, 'r') as f: return f.read().strip() def _send_email(proj, latest_ver, recipient, upgrade_log): print(f'Sending email for {proj}: {latest_ver}') msg = "" match = CHANGE_URL_RE.search(upgrade_log) if match is not None: subject = "[Succeeded]" msg = f'An upgrade change is generated at:\n{match.group(1)}' else: subject = "[Failed]" msg = 'Failed to generate upgrade change. See logs below for details.' subject += f" {proj} {latest_ver}" owners = _read_owner_file(proj) if owners: msg += '\n\nOWNERS file: \n' msg += owners msg += '\n\n' msg += upgrade_log cc_recipient = '' for line in owners.splitlines(): line = line.strip() if line.endswith('@google.com'): cc_recipient += line cc_recipient += ',' subprocess.run(['sendgmr', f'--to={recipient}', f'--cc={cc_recipient}', f'--subject={subject}'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=msg, encoding='ascii') COMMIT_PATTERN = r'^[a-f0-9]{40}$' COMMIT_RE = re.compile(COMMIT_PATTERN) def is_commit(commit: str) -> bool: """Whether a string looks like a SHA1 hash.""" return bool(COMMIT_RE.match(commit)) NOTIFIED_TIME_KEY_NAME = 'latest_notified_time' def _should_notify(latest_ver, proj_history): if latest_ver in proj_history: # Processed this version before. return False timestamp = proj_history.get(NOTIFIED_TIME_KEY_NAME, 0) time_diff = datetime.today() - datetime.fromtimestamp(timestamp) if is_commit(latest_ver) and time_diff <= timedelta(days=30): return False return True def _process_results(args, history, results): 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 _should_notify(latest_ver, proj_history): upgrade_log = _upgrade(proj) if args.generate_change else "" try: _send_email(proj, latest_ver, args.recipients, upgrade_log) proj_history[latest_ver] = int(time.time()) proj_history[NOTIFIED_TIME_KEY_NAME] = int(time.time()) except subprocess.CalledProcessError as err: msg = f"""Failed to send email for {proj} ({latest_ver}). stdout: {stdout} stderr: {err.stderr}""" print(msg) RESULT_FILE_PATH = '/tmp/update_check_result.json' def send_notification(args): """Compare results and send notification.""" results = {} with open(RESULT_FILE_PATH, 'r') as f: results = json.load(f) history = {} try: with open(args.history, 'r') as f: history = json.load(f) except (FileNotFoundError, json.decoder.JSONDecodeError): pass _process_results(args, history, results) with open(args.history, 'w') as f: json.dump(history, f, sort_keys=True, indent=4) def _upgrade(proj): # pylint: disable=subprocess-run-check out = subprocess.run([ 'out/soong/host/linux-x86/bin/external_updater', 'update', proj ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=_get_android_top()) stdout = out.stdout.decode('utf-8') stderr = out.stderr.decode('utf-8') return f""" ==================== | Debug Info | ==================== -=-=-=-=stdout=-=-=-=- {stdout} -=-=-=-=stderr=-=-=-=- {stderr} """ def _check_updates(args): params = [ 'out/soong/host/linux-x86/bin/external_updater', 'check', '--json_output', RESULT_FILE_PATH, '--delay', '30' ] if args.all: params.append('--all') else: params += args.paths print(_get_android_top()) # pylint: disable=subprocess-run-check subprocess.run(params, cwd=_get_android_top()) def main(): """The main entry.""" args = parse_args() _check_updates(args) send_notification(args) if __name__ == '__main__': main()