• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
16
17import datetime
18import json
19import os
20import sys
21import time
22import traceback
23
24import jwt
25import requests
26
27_GITHUB_API_PREFIX = "https://api.github.com"
28_GITHUB_REPO = "grpc/grpc"
29_GITHUB_APP_ID = 22338
30_INSTALLATION_ID = 519109
31
32_ACCESS_TOKEN_CACHE = None
33_ACCESS_TOKEN_FETCH_RETRIES = 6
34_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S = 15
35
36_CHANGE_LABELS = {
37    -1: "improvement",
38    0: "none",
39    1: "low",
40    2: "medium",
41    3: "high",
42}
43
44_INCREASE_DECREASE = {
45    -1: "decrease",
46    0: "neutral",
47    1: "increase",
48}
49
50
51def _jwt_token():
52    github_app_key = open(
53        os.path.join(
54            os.environ["KOKORO_KEYSTORE_DIR"], "73836_grpc_checks_private_key"
55        ),
56        "rb",
57    ).read()
58    return jwt.encode(
59        {
60            "iat": int(time.time()),
61            "exp": int(time.time() + 60 * 10),  # expire in 10 minutes
62            "iss": _GITHUB_APP_ID,
63        },
64        github_app_key,
65        algorithm="RS256",
66    )
67
68
69def _access_token():
70    global _ACCESS_TOKEN_CACHE
71    if _ACCESS_TOKEN_CACHE == None or _ACCESS_TOKEN_CACHE["exp"] < time.time():
72        for i in range(_ACCESS_TOKEN_FETCH_RETRIES):
73            resp = requests.post(
74                url="https://api.github.com/app/installations/%s/access_tokens"
75                % _INSTALLATION_ID,
76                headers={
77                    "Authorization": "Bearer %s" % _jwt_token(),
78                    "Accept": "application/vnd.github.machine-man-preview+json",
79                },
80            )
81
82            try:
83                _ACCESS_TOKEN_CACHE = {
84                    "token": resp.json()["token"],
85                    "exp": time.time() + 60,
86                }
87                break
88            except (KeyError, ValueError):
89                traceback.print_exc()
90                print("HTTP Status %d %s" % (resp.status_code, resp.reason))
91                print("Fetch access token from Github API failed:")
92                print(resp.text)
93                if i != _ACCESS_TOKEN_FETCH_RETRIES - 1:
94                    print(
95                        "Retrying after %.2f second."
96                        % _ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S
97                    )
98                    time.sleep(_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S)
99        else:
100            print("error: Unable to fetch access token, exiting...")
101            sys.exit(0)
102
103    return _ACCESS_TOKEN_CACHE["token"]
104
105
106def _call(url, method="GET", json=None):
107    if not url.startswith("https://"):
108        url = _GITHUB_API_PREFIX + url
109    headers = {
110        "Authorization": "Bearer %s" % _access_token(),
111        "Accept": "application/vnd.github.antiope-preview+json",
112    }
113    return requests.request(method=method, url=url, headers=headers, json=json)
114
115
116def _latest_commit():
117    resp = _call(
118        "/repos/%s/pulls/%s/commits"
119        % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"])
120    )
121    return resp.json()[-1]
122
123
124def check_on_pr(name, summary, success=True):
125    """Create/Update a check on current pull request.
126
127    The check runs are aggregated by their name, so newer check will update the
128    older check with the same name.
129
130    Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request
131    should be updated.
132
133    Args:
134      name: The name of the check.
135      summary: A str in Markdown to be used as the detail information of the check.
136      success: A bool indicates whether the check is succeed or not.
137    """
138    if "KOKORO_GIT_COMMIT" not in os.environ:
139        print("Missing KOKORO_GIT_COMMIT env var: not checking")
140        return
141    if "KOKORO_KEYSTORE_DIR" not in os.environ:
142        print("Missing KOKORO_KEYSTORE_DIR env var: not checking")
143        return
144    if "KOKORO_GITHUB_PULL_REQUEST_NUMBER" not in os.environ:
145        print("Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking")
146        return
147    MAX_SUMMARY_LEN = 65400
148    if len(summary) > MAX_SUMMARY_LEN:
149        # Drop some hints to the log should someone come looking for what really happened!
150        print("Clipping too long summary")
151        print(summary)
152        summary = summary[:MAX_SUMMARY_LEN] + "\n\n\n... CLIPPED (too long)"
153    completion_time = (
154        str(datetime.datetime.utcnow().replace(microsecond=0).isoformat()) + "Z"
155    )
156    resp = _call(
157        "/repos/%s/check-runs" % _GITHUB_REPO,
158        method="POST",
159        json={
160            "name": name,
161            "head_sha": os.environ["KOKORO_GIT_COMMIT"],
162            "status": "completed",
163            "completed_at": completion_time,
164            "conclusion": "success" if success else "failure",
165            "output": {
166                "title": name,
167                "summary": summary,
168            },
169        },
170    )
171    print(
172        "Result of Creating/Updating Check on PR:",
173        json.dumps(resp.json(), indent=2),
174    )
175
176
177def label_significance_on_pr(name, change, labels=_CHANGE_LABELS):
178    """Add a label to the PR indicating the significance of the check.
179
180    Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request
181    should be updated.
182
183    Args:
184      name: The name of the label.
185      value: A str in Markdown to be used as the detail information of the label.
186    """
187    if change < min(list(labels.keys())):
188        change = min(list(labels.keys()))
189    if change > max(list(labels.keys())):
190        change = max(list(labels.keys()))
191    value = labels[change]
192    if "KOKORO_GIT_COMMIT" not in os.environ:
193        print("Missing KOKORO_GIT_COMMIT env var: not checking")
194        return
195    if "KOKORO_KEYSTORE_DIR" not in os.environ:
196        print("Missing KOKORO_KEYSTORE_DIR env var: not checking")
197        return
198    if "KOKORO_GITHUB_PULL_REQUEST_NUMBER" not in os.environ:
199        print("Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking")
200        return
201    existing = _call(
202        "/repos/%s/issues/%s/labels"
203        % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"]),
204        method="GET",
205    ).json()
206    print("Result of fetching labels on PR:", existing)
207    new = [x["name"] for x in existing if not x["name"].startswith(name + "/")]
208    new.append(name + "/" + value)
209    resp = _call(
210        "/repos/%s/issues/%s/labels"
211        % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"]),
212        method="PUT",
213        json=new,
214    )
215    print("Result of setting labels on PR:", resp.text)
216
217
218def label_increase_decrease_on_pr(name, change, significant):
219    if change <= -significant:
220        label_significance_on_pr(name, -1, _INCREASE_DECREASE)
221    elif change >= significant:
222        label_significance_on_pr(name, 1, _INCREASE_DECREASE)
223    else:
224        label_significance_on_pr(name, 0, _INCREASE_DECREASE)
225