1import json 2import logging 3import os 4import subprocess 5import sys 6import tempfile 7 8import requests 9 10here = os.path.abspath(os.path.dirname(__file__)) 11wpt_root = os.path.abspath(os.path.join(here, os.pardir, os.pardir)) 12 13if not(wpt_root in sys.path): 14 sys.path.append(wpt_root) 15 16from tools.wpt.testfiles import get_git_cmd 17 18logging.basicConfig(level=logging.INFO) 19logger = logging.getLogger(__name__) 20 21 22class Status(object): 23 SUCCESS = 0 24 FAIL = 1 25 26 27def run(cmd, return_stdout=False, **kwargs): 28 logger.info(" ".join(cmd)) 29 if return_stdout: 30 f = subprocess.check_output 31 else: 32 f = subprocess.check_call 33 return f(cmd, **kwargs) 34 35 36def create_manifest(path): 37 run(["./wpt", "manifest", "-p", path]) 38 39 40def compress_manifest(path): 41 for args in [["gzip", "-k", "-f", "--best"], 42 ["bzip2", "-k", "-f", "--best"], 43 ["zstd", "-k", "-f", "--ultra", "-22", "-q"]]: 44 run(args + [path]) 45 46 47def request(url, desc, method=None, data=None, json_data=None, params=None, headers=None): 48 github_token = os.environ.get("GITHUB_TOKEN") 49 default_headers = { 50 "Authorization": "token %s" % github_token, 51 "Accept": "application/vnd.github.machine-man-preview+json" 52 } 53 54 _headers = default_headers 55 if headers is not None: 56 _headers.update(headers) 57 58 kwargs = {"params": params, 59 "headers": _headers} 60 try: 61 logger.info("Requesting URL %s" % url) 62 if json_data is not None or data is not None: 63 if method is None: 64 method = requests.post 65 kwargs["json"] = json_data 66 kwargs["data"] = data 67 elif method is None: 68 method = requests.get 69 70 resp = method(url, **kwargs) 71 72 except Exception as e: 73 logger.error("%s failed:\n%s" % (desc, e)) 74 return None 75 76 try: 77 resp.raise_for_status() 78 except requests.HTTPError: 79 logger.error("%s failed: Got HTTP status %s. Response:" % 80 (desc, resp.status_code)) 81 logger.error(resp.text) 82 return None 83 84 try: 85 return resp.json() 86 except ValueError: 87 logger.error("%s failed: Returned data was not JSON Response:" % desc) 88 logger.error(resp.text) 89 90 91def get_pr(owner, repo, sha): 92 data = request("https://api.github.com/search/issues?q=type:pr+is:merged+repo:%s/%s+sha:%s" % 93 (owner, repo, sha), "Getting PR") 94 if data is None: 95 return None 96 97 items = data["items"] 98 if len(items) == 0: 99 logger.error("No PR found for %s" % sha) 100 return None 101 if len(items) > 1: 102 logger.warning("Found multiple PRs for %s" % sha) 103 104 pr = items[0] 105 106 return pr["number"] 107 108 109def create_release(manifest_path, owner, repo, sha, tag, body): 110 logger.info("Creating a release for tag='%s', target_commitish='%s'" % (tag, sha)) 111 create_url = "https://api.github.com/repos/%s/%s/releases" % (owner, repo) 112 create_data = {"tag_name": tag, 113 "target_commitish": sha, 114 "name": tag, 115 "body": body, 116 "draft": True} 117 create_resp = request(create_url, "Release creation", json_data=create_data) 118 if not create_resp: 119 return False 120 121 # Upload URL contains '{?name,label}' at the end which we want to remove 122 upload_url = create_resp["upload_url"].split("{", 1)[0] 123 124 upload_exts = [".gz", ".bz2", ".zst"] 125 for upload_ext in upload_exts: 126 upload_filename = "MANIFEST-%s.json%s" % (sha, upload_ext) 127 params = {"name": upload_filename, 128 "label": "MANIFEST.json%s" % upload_ext} 129 130 with open("%s%s" % (manifest_path, upload_ext), "rb") as f: 131 upload_data = f.read() 132 133 logger.info("Uploading %s bytes" % len(upload_data)) 134 135 upload_resp = request(upload_url, "Manifest upload", data=upload_data, params=params, 136 headers={'Content-Type': 'application/octet-stream'}) 137 if not upload_resp: 138 return False 139 140 release_id = create_resp["id"] 141 edit_url = "https://api.github.com/repos/%s/%s/releases/%s" % (owner, repo, release_id) 142 edit_data = create_data.copy() 143 edit_data["draft"] = False 144 edit_resp = request(edit_url, "Release publishing", method=requests.patch, json_data=edit_data) 145 if not edit_resp: 146 return False 147 148 logger.info("Released %s" % edit_resp["html_url"]) 149 return True 150 151 152def should_dry_run(): 153 with open(os.environ["GITHUB_EVENT_PATH"]) as f: 154 event = json.load(f) 155 logger.info(json.dumps(event, indent=2)) 156 157 if "pull_request" in event: 158 logger.info("Dry run for PR") 159 return True 160 if event.get("ref") != "refs/heads/master": 161 logger.info("Dry run for ref %s" % event.get("ref")) 162 return True 163 return False 164 165 166def main(): 167 dry_run = should_dry_run() 168 169 manifest_path = os.path.join(tempfile.mkdtemp(), "MANIFEST.json") 170 171 create_manifest(manifest_path) 172 173 compress_manifest(manifest_path) 174 175 owner, repo = os.environ["GITHUB_REPOSITORY"].split("/", 1) 176 177 git = get_git_cmd(wpt_root) 178 head_rev = git("rev-parse", "HEAD").strip() 179 body = git("show", "--no-patch", "--format=%B", "HEAD") 180 181 if dry_run: 182 return Status.SUCCESS 183 184 pr = get_pr(owner, repo, head_rev) 185 if pr is None: 186 return Status.FAIL 187 tag_name = "merge_pr_%s" % pr 188 189 if not create_release(manifest_path, owner, repo, head_rev, tag_name, body): 190 return Status.FAIL 191 192 return Status.SUCCESS 193 194 195if __name__ == "__main__": 196 code = main() # type: ignore 197 assert isinstance(code, int) 198 sys.exit(code) 199