xref: /aosp_15_r20/tools/external_updater/notifier.py (revision 3c875a214f382db1236d28570d1304ce57138f32)
1*3c875a21SAndroid Build Coastguard Worker# Copyright (C) 2018 The Android Open Source Project
2*3c875a21SAndroid Build Coastguard Worker#
3*3c875a21SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*3c875a21SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*3c875a21SAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*3c875a21SAndroid Build Coastguard Worker#
7*3c875a21SAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*3c875a21SAndroid Build Coastguard Worker#
9*3c875a21SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*3c875a21SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*3c875a21SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*3c875a21SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*3c875a21SAndroid Build Coastguard Worker# limitations under the License.
14*3c875a21SAndroid Build Coastguard Worker"""Send notification email if new version is found.
15*3c875a21SAndroid Build Coastguard Worker
16*3c875a21SAndroid Build Coastguard WorkerExample usage:
17*3c875a21SAndroid Build Coastguard Workerexternal_updater_notifier \
18*3c875a21SAndroid Build Coastguard Worker    --history ~/updater/history \
19*3c875a21SAndroid Build Coastguard Worker    --generate_change \
20*3c875a21SAndroid Build Coastguard Worker    --recipients [email protected] \
21*3c875a21SAndroid Build Coastguard Worker    googletest
22*3c875a21SAndroid Build Coastguard Worker"""
23*3c875a21SAndroid Build Coastguard Worker
24*3c875a21SAndroid Build Coastguard Workerfrom datetime import timedelta, datetime
25*3c875a21SAndroid Build Coastguard Workerimport argparse
26*3c875a21SAndroid Build Coastguard Workerimport json
27*3c875a21SAndroid Build Coastguard Workerimport os
28*3c875a21SAndroid Build Coastguard Workerimport re
29*3c875a21SAndroid Build Coastguard Workerimport subprocess
30*3c875a21SAndroid Build Coastguard Workerimport time
31*3c875a21SAndroid Build Coastguard Worker
32*3c875a21SAndroid Build Coastguard Worker# pylint: disable=invalid-name
33*3c875a21SAndroid Build Coastguard Worker
34*3c875a21SAndroid Build Coastguard Workerdef parse_args():
35*3c875a21SAndroid Build Coastguard Worker    """Parses commandline arguments."""
36*3c875a21SAndroid Build Coastguard Worker
37*3c875a21SAndroid Build Coastguard Worker    parser = argparse.ArgumentParser(
38*3c875a21SAndroid Build Coastguard Worker        description='Check updates for third party projects in external/.')
39*3c875a21SAndroid Build Coastguard Worker    parser.add_argument('--history',
40*3c875a21SAndroid Build Coastguard Worker                        help='Path of history file. If doesn'
41*3c875a21SAndroid Build Coastguard Worker                        't exist, a new one will be created.')
42*3c875a21SAndroid Build Coastguard Worker    parser.add_argument(
43*3c875a21SAndroid Build Coastguard Worker        '--recipients',
44*3c875a21SAndroid Build Coastguard Worker        help='Comma separated recipients of notification email.')
45*3c875a21SAndroid Build Coastguard Worker    parser.add_argument(
46*3c875a21SAndroid Build Coastguard Worker        '--generate_change',
47*3c875a21SAndroid Build Coastguard Worker        help='If set, an upgrade change will be uploaded to Gerrit.',
48*3c875a21SAndroid Build Coastguard Worker        action='store_true',
49*3c875a21SAndroid Build Coastguard Worker        required=False)
50*3c875a21SAndroid Build Coastguard Worker    parser.add_argument('paths', nargs='*', help='Paths of the project.')
51*3c875a21SAndroid Build Coastguard Worker    parser.add_argument('--all',
52*3c875a21SAndroid Build Coastguard Worker                        action='store_true',
53*3c875a21SAndroid Build Coastguard Worker                        help='Checks all projects.')
54*3c875a21SAndroid Build Coastguard Worker
55*3c875a21SAndroid Build Coastguard Worker    return parser.parse_args()
56*3c875a21SAndroid Build Coastguard Worker
57*3c875a21SAndroid Build Coastguard Worker
58*3c875a21SAndroid Build Coastguard Workerdef _get_android_top():
59*3c875a21SAndroid Build Coastguard Worker    return os.environ['ANDROID_BUILD_TOP']
60*3c875a21SAndroid Build Coastguard Worker
61*3c875a21SAndroid Build Coastguard Worker
62*3c875a21SAndroid Build Coastguard WorkerCHANGE_URL_PATTERN = r'(https:\/\/[^\s]*android-review[^\s]*) Upgrade'
63*3c875a21SAndroid Build Coastguard WorkerCHANGE_URL_RE = re.compile(CHANGE_URL_PATTERN)
64*3c875a21SAndroid Build Coastguard Worker
65*3c875a21SAndroid Build Coastguard Worker
66*3c875a21SAndroid Build Coastguard Workerdef _read_owner_file(proj):
67*3c875a21SAndroid Build Coastguard Worker    owner_file = os.path.join(_get_android_top(), 'external', proj, 'OWNERS')
68*3c875a21SAndroid Build Coastguard Worker    if not os.path.isfile(owner_file):
69*3c875a21SAndroid Build Coastguard Worker        return None
70*3c875a21SAndroid Build Coastguard Worker    with open(owner_file, 'r', encoding='utf-8') as f:
71*3c875a21SAndroid Build Coastguard Worker        return f.read().strip()
72*3c875a21SAndroid Build Coastguard Worker
73*3c875a21SAndroid Build Coastguard Worker
74*3c875a21SAndroid Build Coastguard Workerdef _send_email(proj, latest_ver, recipient, upgrade_log):
75*3c875a21SAndroid Build Coastguard Worker    print(f'Sending email for {proj}: {latest_ver}')
76*3c875a21SAndroid Build Coastguard Worker    msg = ""
77*3c875a21SAndroid Build Coastguard Worker    match = CHANGE_URL_RE.search(upgrade_log)
78*3c875a21SAndroid Build Coastguard Worker    if match is not None:
79*3c875a21SAndroid Build Coastguard Worker        subject = "[Succeeded]"
80*3c875a21SAndroid Build Coastguard Worker        msg = f'An upgrade change is generated at:\n{match.group(1)}'
81*3c875a21SAndroid Build Coastguard Worker    else:
82*3c875a21SAndroid Build Coastguard Worker        subject = "[Failed]"
83*3c875a21SAndroid Build Coastguard Worker        msg = 'Failed to generate upgrade change. See logs below for details.'
84*3c875a21SAndroid Build Coastguard Worker
85*3c875a21SAndroid Build Coastguard Worker    subject += f" {proj} {latest_ver}"
86*3c875a21SAndroid Build Coastguard Worker    owners = _read_owner_file(proj)
87*3c875a21SAndroid Build Coastguard Worker    if owners:
88*3c875a21SAndroid Build Coastguard Worker        msg += '\n\nOWNERS file: \n'
89*3c875a21SAndroid Build Coastguard Worker        msg += owners
90*3c875a21SAndroid Build Coastguard Worker
91*3c875a21SAndroid Build Coastguard Worker    msg += '\n\n'
92*3c875a21SAndroid Build Coastguard Worker    msg += upgrade_log
93*3c875a21SAndroid Build Coastguard Worker
94*3c875a21SAndroid Build Coastguard Worker    cc_recipient = ''
95*3c875a21SAndroid Build Coastguard Worker    for line in owners.splitlines():
96*3c875a21SAndroid Build Coastguard Worker        line = line.strip()
97*3c875a21SAndroid Build Coastguard Worker        if line.endswith('@google.com'):
98*3c875a21SAndroid Build Coastguard Worker            cc_recipient += line
99*3c875a21SAndroid Build Coastguard Worker            cc_recipient += ','
100*3c875a21SAndroid Build Coastguard Worker
101*3c875a21SAndroid Build Coastguard Worker    subprocess.run(['sendgmr',
102*3c875a21SAndroid Build Coastguard Worker                    f'--to={recipient}',
103*3c875a21SAndroid Build Coastguard Worker                    f'--cc={cc_recipient}',
104*3c875a21SAndroid Build Coastguard Worker                    f'--subject={subject}'],
105*3c875a21SAndroid Build Coastguard Worker                   check=True,
106*3c875a21SAndroid Build Coastguard Worker                   stdout=subprocess.PIPE,
107*3c875a21SAndroid Build Coastguard Worker                   stderr=subprocess.PIPE,
108*3c875a21SAndroid Build Coastguard Worker                   input=msg,
109*3c875a21SAndroid Build Coastguard Worker                   encoding='ascii')
110*3c875a21SAndroid Build Coastguard Worker
111*3c875a21SAndroid Build Coastguard Worker
112*3c875a21SAndroid Build Coastguard WorkerCOMMIT_PATTERN = r'^[a-f0-9]{40}$'
113*3c875a21SAndroid Build Coastguard WorkerCOMMIT_RE = re.compile(COMMIT_PATTERN)
114*3c875a21SAndroid Build Coastguard Worker
115*3c875a21SAndroid Build Coastguard Worker
116*3c875a21SAndroid Build Coastguard Workerdef is_commit(commit: str) -> bool:
117*3c875a21SAndroid Build Coastguard Worker    """Whether a string looks like a SHA1 hash."""
118*3c875a21SAndroid Build Coastguard Worker    return bool(COMMIT_RE.match(commit))
119*3c875a21SAndroid Build Coastguard Worker
120*3c875a21SAndroid Build Coastguard Worker
121*3c875a21SAndroid Build Coastguard WorkerNOTIFIED_TIME_KEY_NAME = 'latest_notified_time'
122*3c875a21SAndroid Build Coastguard Worker
123*3c875a21SAndroid Build Coastguard Worker
124*3c875a21SAndroid Build Coastguard Workerdef _should_notify(latest_ver, proj_history):
125*3c875a21SAndroid Build Coastguard Worker    if latest_ver in proj_history:
126*3c875a21SAndroid Build Coastguard Worker        # Processed this version before.
127*3c875a21SAndroid Build Coastguard Worker        return False
128*3c875a21SAndroid Build Coastguard Worker
129*3c875a21SAndroid Build Coastguard Worker    timestamp = proj_history.get(NOTIFIED_TIME_KEY_NAME, 0)
130*3c875a21SAndroid Build Coastguard Worker    time_diff = datetime.today() - datetime.fromtimestamp(timestamp)
131*3c875a21SAndroid Build Coastguard Worker    if is_commit(latest_ver) and time_diff <= timedelta(days=30):
132*3c875a21SAndroid Build Coastguard Worker        return False
133*3c875a21SAndroid Build Coastguard Worker
134*3c875a21SAndroid Build Coastguard Worker    return True
135*3c875a21SAndroid Build Coastguard Worker
136*3c875a21SAndroid Build Coastguard Worker
137*3c875a21SAndroid Build Coastguard Workerdef _process_results(args, history, results):
138*3c875a21SAndroid Build Coastguard Worker    for proj, res in results.items():
139*3c875a21SAndroid Build Coastguard Worker        if 'latest' not in res:
140*3c875a21SAndroid Build Coastguard Worker            continue
141*3c875a21SAndroid Build Coastguard Worker        latest_ver = res['latest']
142*3c875a21SAndroid Build Coastguard Worker        current_ver = res['current']
143*3c875a21SAndroid Build Coastguard Worker        if latest_ver == current_ver:
144*3c875a21SAndroid Build Coastguard Worker            continue
145*3c875a21SAndroid Build Coastguard Worker        proj_history = history.setdefault(proj, {})
146*3c875a21SAndroid Build Coastguard Worker        if _should_notify(latest_ver, proj_history):
147*3c875a21SAndroid Build Coastguard Worker            upgrade_log = _upgrade(proj) if args.generate_change else ""
148*3c875a21SAndroid Build Coastguard Worker            try:
149*3c875a21SAndroid Build Coastguard Worker                _send_email(proj, latest_ver, args.recipients, upgrade_log)
150*3c875a21SAndroid Build Coastguard Worker                proj_history[latest_ver] = int(time.time())
151*3c875a21SAndroid Build Coastguard Worker                proj_history[NOTIFIED_TIME_KEY_NAME] = int(time.time())
152*3c875a21SAndroid Build Coastguard Worker            except subprocess.CalledProcessError as err:
153*3c875a21SAndroid Build Coastguard Worker                msg = f"""Failed to send email for {proj} ({latest_ver}).
154*3c875a21SAndroid Build Coastguard Workerstdout: {err.stdout}
155*3c875a21SAndroid Build Coastguard Workerstderr: {err.stderr}"""
156*3c875a21SAndroid Build Coastguard Worker                print(msg)
157*3c875a21SAndroid Build Coastguard Worker
158*3c875a21SAndroid Build Coastguard Worker
159*3c875a21SAndroid Build Coastguard WorkerRESULT_FILE_PATH = '/tmp/update_check_result.json'
160*3c875a21SAndroid Build Coastguard Worker
161*3c875a21SAndroid Build Coastguard Worker
162*3c875a21SAndroid Build Coastguard Workerdef send_notification(args):
163*3c875a21SAndroid Build Coastguard Worker    """Compare results and send notification."""
164*3c875a21SAndroid Build Coastguard Worker    results = {}
165*3c875a21SAndroid Build Coastguard Worker    with open(RESULT_FILE_PATH, 'r', encoding='utf-8') as f:
166*3c875a21SAndroid Build Coastguard Worker        results = json.load(f)
167*3c875a21SAndroid Build Coastguard Worker    history = {}
168*3c875a21SAndroid Build Coastguard Worker    try:
169*3c875a21SAndroid Build Coastguard Worker        with open(args.history, 'r', encoding='utf-8') as f:
170*3c875a21SAndroid Build Coastguard Worker            history = json.load(f)
171*3c875a21SAndroid Build Coastguard Worker    except (FileNotFoundError, json.decoder.JSONDecodeError):
172*3c875a21SAndroid Build Coastguard Worker        pass
173*3c875a21SAndroid Build Coastguard Worker
174*3c875a21SAndroid Build Coastguard Worker    _process_results(args, history, results)
175*3c875a21SAndroid Build Coastguard Worker
176*3c875a21SAndroid Build Coastguard Worker    with open(args.history, 'w', encoding='utf-8') as f:
177*3c875a21SAndroid Build Coastguard Worker        json.dump(history, f, sort_keys=True, indent=4)
178*3c875a21SAndroid Build Coastguard Worker
179*3c875a21SAndroid Build Coastguard Worker
180*3c875a21SAndroid Build Coastguard Workerdef _upgrade(proj):
181*3c875a21SAndroid Build Coastguard Worker    # pylint: disable=subprocess-run-check
182*3c875a21SAndroid Build Coastguard Worker    out = subprocess.run([
183*3c875a21SAndroid Build Coastguard Worker        'out/soong/host/linux-x86/bin/external_updater', 'update', proj
184*3c875a21SAndroid Build Coastguard Worker    ],
185*3c875a21SAndroid Build Coastguard Worker                         stdout=subprocess.PIPE,
186*3c875a21SAndroid Build Coastguard Worker                         stderr=subprocess.PIPE,
187*3c875a21SAndroid Build Coastguard Worker                         cwd=_get_android_top())
188*3c875a21SAndroid Build Coastguard Worker    stdout = out.stdout.decode('utf-8')
189*3c875a21SAndroid Build Coastguard Worker    stderr = out.stderr.decode('utf-8')
190*3c875a21SAndroid Build Coastguard Worker    return f"""
191*3c875a21SAndroid Build Coastguard Worker====================
192*3c875a21SAndroid Build Coastguard Worker|    Debug Info    |
193*3c875a21SAndroid Build Coastguard Worker====================
194*3c875a21SAndroid Build Coastguard Worker-=-=-=-=stdout=-=-=-=-
195*3c875a21SAndroid Build Coastguard Worker{stdout}
196*3c875a21SAndroid Build Coastguard Worker
197*3c875a21SAndroid Build Coastguard Worker-=-=-=-=stderr=-=-=-=-
198*3c875a21SAndroid Build Coastguard Worker{stderr}
199*3c875a21SAndroid Build Coastguard Worker"""
200*3c875a21SAndroid Build Coastguard Worker
201*3c875a21SAndroid Build Coastguard Worker
202*3c875a21SAndroid Build Coastguard Workerdef _check_updates(args):
203*3c875a21SAndroid Build Coastguard Worker    params = [
204*3c875a21SAndroid Build Coastguard Worker        'out/soong/host/linux-x86/bin/external_updater', 'check',
205*3c875a21SAndroid Build Coastguard Worker        '--json_output', RESULT_FILE_PATH, '--delay', '30'
206*3c875a21SAndroid Build Coastguard Worker    ]
207*3c875a21SAndroid Build Coastguard Worker    if args.all:
208*3c875a21SAndroid Build Coastguard Worker        params.append('--all')
209*3c875a21SAndroid Build Coastguard Worker    else:
210*3c875a21SAndroid Build Coastguard Worker        params += args.paths
211*3c875a21SAndroid Build Coastguard Worker
212*3c875a21SAndroid Build Coastguard Worker    print(_get_android_top())
213*3c875a21SAndroid Build Coastguard Worker    # pylint: disable=subprocess-run-check
214*3c875a21SAndroid Build Coastguard Worker    subprocess.run(params, cwd=_get_android_top())
215*3c875a21SAndroid Build Coastguard Worker
216*3c875a21SAndroid Build Coastguard Worker
217*3c875a21SAndroid Build Coastguard Workerdef main():
218*3c875a21SAndroid Build Coastguard Worker    """The main entry."""
219*3c875a21SAndroid Build Coastguard Worker
220*3c875a21SAndroid Build Coastguard Worker    args = parse_args()
221*3c875a21SAndroid Build Coastguard Worker    _check_updates(args)
222*3c875a21SAndroid Build Coastguard Worker    send_notification(args)
223*3c875a21SAndroid Build Coastguard Worker
224*3c875a21SAndroid Build Coastguard Worker
225*3c875a21SAndroid Build Coastguard Workerif __name__ == '__main__':
226*3c875a21SAndroid Build Coastguard Worker    main()
227