# Copyright 2018 The gRPC Authors # # 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. from __future__ import print_function import datetime import json import os import sys import time import traceback import jwt import requests _GITHUB_API_PREFIX = "https://api.github.com" _GITHUB_REPO = "grpc/grpc" _GITHUB_APP_ID = 22338 _INSTALLATION_ID = 519109 _ACCESS_TOKEN_CACHE = None _ACCESS_TOKEN_FETCH_RETRIES = 6 _ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S = 15 _CHANGE_LABELS = { -1: "improvement", 0: "none", 1: "low", 2: "medium", 3: "high", } _INCREASE_DECREASE = { -1: "decrease", 0: "neutral", 1: "increase", } def _jwt_token(): github_app_key = open( os.path.join( os.environ["KOKORO_KEYSTORE_DIR"], "73836_grpc_checks_private_key" ), "rb", ).read() return jwt.encode( { "iat": int(time.time()), "exp": int(time.time() + 60 * 10), # expire in 10 minutes "iss": _GITHUB_APP_ID, }, github_app_key, algorithm="RS256", ) def _access_token(): global _ACCESS_TOKEN_CACHE if _ACCESS_TOKEN_CACHE == None or _ACCESS_TOKEN_CACHE["exp"] < time.time(): for i in range(_ACCESS_TOKEN_FETCH_RETRIES): resp = requests.post( url="https://api.github.com/app/installations/%s/access_tokens" % _INSTALLATION_ID, headers={ "Authorization": "Bearer %s" % _jwt_token(), "Accept": "application/vnd.github.machine-man-preview+json", }, ) try: _ACCESS_TOKEN_CACHE = { "token": resp.json()["token"], "exp": time.time() + 60, } break except (KeyError, ValueError): traceback.print_exc() print("HTTP Status %d %s" % (resp.status_code, resp.reason)) print("Fetch access token from Github API failed:") print(resp.text) if i != _ACCESS_TOKEN_FETCH_RETRIES - 1: print( "Retrying after %.2f second." % _ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S ) time.sleep(_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S) else: print("error: Unable to fetch access token, exiting...") sys.exit(0) return _ACCESS_TOKEN_CACHE["token"] def _call(url, method="GET", json=None): if not url.startswith("https://"): url = _GITHUB_API_PREFIX + url headers = { "Authorization": "Bearer %s" % _access_token(), "Accept": "application/vnd.github.antiope-preview+json", } return requests.request(method=method, url=url, headers=headers, json=json) def _latest_commit(): resp = _call( "/repos/%s/pulls/%s/commits" % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"]) ) return resp.json()[-1] def check_on_pr(name, summary, success=True): """Create/Update a check on current pull request. The check runs are aggregated by their name, so newer check will update the older check with the same name. Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request should be updated. Args: name: The name of the check. summary: A str in Markdown to be used as the detail information of the check. success: A bool indicates whether the check is succeed or not. """ if "KOKORO_GIT_COMMIT" not in os.environ: print("Missing KOKORO_GIT_COMMIT env var: not checking") return if "KOKORO_KEYSTORE_DIR" not in os.environ: print("Missing KOKORO_KEYSTORE_DIR env var: not checking") return if "KOKORO_GITHUB_PULL_REQUEST_NUMBER" not in os.environ: print("Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking") return MAX_SUMMARY_LEN = 65400 if len(summary) > MAX_SUMMARY_LEN: # Drop some hints to the log should someone come looking for what really happened! print("Clipping too long summary") print(summary) summary = summary[:MAX_SUMMARY_LEN] + "\n\n\n... CLIPPED (too long)" completion_time = ( str(datetime.datetime.utcnow().replace(microsecond=0).isoformat()) + "Z" ) resp = _call( "/repos/%s/check-runs" % _GITHUB_REPO, method="POST", json={ "name": name, "head_sha": os.environ["KOKORO_GIT_COMMIT"], "status": "completed", "completed_at": completion_time, "conclusion": "success" if success else "failure", "output": { "title": name, "summary": summary, }, }, ) print( "Result of Creating/Updating Check on PR:", json.dumps(resp.json(), indent=2), ) def label_significance_on_pr(name, change, labels=_CHANGE_LABELS): """Add a label to the PR indicating the significance of the check. Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request should be updated. Args: name: The name of the label. value: A str in Markdown to be used as the detail information of the label. """ if change < min(list(labels.keys())): change = min(list(labels.keys())) if change > max(list(labels.keys())): change = max(list(labels.keys())) value = labels[change] if "KOKORO_GIT_COMMIT" not in os.environ: print("Missing KOKORO_GIT_COMMIT env var: not checking") return if "KOKORO_KEYSTORE_DIR" not in os.environ: print("Missing KOKORO_KEYSTORE_DIR env var: not checking") return if "KOKORO_GITHUB_PULL_REQUEST_NUMBER" not in os.environ: print("Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking") return existing = _call( "/repos/%s/issues/%s/labels" % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"]), method="GET", ).json() print("Result of fetching labels on PR:", existing) new = [x["name"] for x in existing if not x["name"].startswith(name + "/")] new.append(name + "/" + value) resp = _call( "/repos/%s/issues/%s/labels" % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"]), method="PUT", json=new, ) print("Result of setting labels on PR:", resp.text) def label_increase_decrease_on_pr(name, change, significant): if change <= -significant: label_significance_on_pr(name, -1, _INCREASE_DECREASE) elif change >= significant: label_significance_on_pr(name, 1, _INCREASE_DECREASE) else: label_significance_on_pr(name, 0, _INCREASE_DECREASE)