1# Copyright 2018 The gRPC Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from __future__ import print_function
16import os
17import sys
18import json
19import time
20import datetime
21import traceback
22
23import requests
24import jwt
25
26_GITHUB_API_PREFIX = 'https://api.github.com'
27_GITHUB_REPO = 'grpc/grpc'
28_GITHUB_APP_ID = 22338
29_INSTALLATION_ID = 519109
30
31_ACCESS_TOKEN_CACHE = None
32_ACCESS_TOKEN_FETCH_RETRIES = 6
33_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S = 15
34
35
36def _jwt_token():
37    github_app_key = open(
38        os.path.join(os.environ['KOKORO_KEYSTORE_DIR'],
39                     '73836_grpc_checks_private_key'), 'rb').read()
40    return jwt.encode(
41        {
42            'iat': int(time.time()),
43            'exp': int(time.time() + 60 * 10),  # expire in 10 minutes
44            'iss': _GITHUB_APP_ID,
45        },
46        github_app_key,
47        algorithm='RS256')
48
49
50def _access_token():
51    global _ACCESS_TOKEN_CACHE
52    if _ACCESS_TOKEN_CACHE == None or _ACCESS_TOKEN_CACHE['exp'] < time.time():
53        for i in range(_ACCESS_TOKEN_FETCH_RETRIES):
54            resp = requests.post(
55                url='https://api.github.com/app/installations/%s/access_tokens'
56                % _INSTALLATION_ID,
57                headers={
58                    'Authorization': 'Bearer %s' % _jwt_token().decode('ASCII'),
59                    'Accept': 'application/vnd.github.machine-man-preview+json',
60                })
61
62            try:
63                _ACCESS_TOKEN_CACHE = {
64                    'token': resp.json()['token'],
65                    'exp': time.time() + 60
66                }
67                break
68            except (KeyError, ValueError) as e:
69                traceback.print_exc(e)
70                print('HTTP Status %d %s' % (resp.status_code, resp.reason))
71                print("Fetch access token from Github API failed:")
72                print(resp.text)
73                if i != _ACCESS_TOKEN_FETCH_RETRIES - 1:
74                    print('Retrying after %.2f second.' %
75                          _ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S)
76                    time.sleep(_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S)
77        else:
78            print("error: Unable to fetch access token, exiting...")
79            sys.exit(0)
80
81    return _ACCESS_TOKEN_CACHE['token']
82
83
84def _call(url, method='GET', json=None):
85    if not url.startswith('https://'):
86        url = _GITHUB_API_PREFIX + url
87    headers = {
88        'Authorization': 'Bearer %s' % _access_token(),
89        'Accept': 'application/vnd.github.antiope-preview+json',
90    }
91    return requests.request(method=method, url=url, headers=headers, json=json)
92
93
94def _latest_commit():
95    resp = _call(
96        '/repos/%s/pulls/%s/commits' %
97        (_GITHUB_REPO, os.environ['KOKORO_GITHUB_PULL_REQUEST_NUMBER']))
98    return resp.json()[-1]
99
100
101def check_on_pr(name, summary, success=True):
102    """Create/Update a check on current pull request.
103
104    The check runs are aggregated by their name, so newer check will update the
105    older check with the same name.
106
107    Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request
108    should be updated.
109
110    Args:
111      name: The name of the check.
112      summary: A str in Markdown to be used as the detail information of the check.
113      success: A bool indicates whether the check is succeed or not.
114    """
115    if 'KOKORO_GIT_COMMIT' not in os.environ:
116        print('Missing KOKORO_GIT_COMMIT env var: not checking')
117        return
118    if 'KOKORO_KEYSTORE_DIR' not in os.environ:
119        print('Missing KOKORO_KEYSTORE_DIR env var: not checking')
120        return
121    if 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' not in os.environ:
122        print('Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking')
123        return
124    completion_time = str(
125        datetime.datetime.utcnow().replace(microsecond=0).isoformat()) + 'Z'
126    resp = _call('/repos/%s/check-runs' % _GITHUB_REPO,
127                 method='POST',
128                 json={
129                     'name': name,
130                     'head_sha': os.environ['KOKORO_GIT_COMMIT'],
131                     'status': 'completed',
132                     'completed_at': completion_time,
133                     'conclusion': 'success' if success else 'failure',
134                     'output': {
135                         'title': name,
136                         'summary': summary,
137                     }
138                 })
139    print('Result of Creating/Updating Check on PR:',
140          json.dumps(resp.json(), indent=2))
141