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