2 # -*- coding: utf-8 -*-
4 # This script computes the new "current" toolstate for the toolstate repo (not to be
5 # confused with publishing the test results, which happens in `src/bootstrap/toolstate.rs`).
6 # It gets called from `src/ci/publish_toolstate.sh` when a new commit lands on `master`
7 # (i.e., after it passed all checks on `auto`).
9 from __future__ import print_function
21 import urllib.request as urllib2
23 # List of people to ping when the status of a tool or a book changed.
24 # These should be collaborators of the rust-lang/rust repository (with at least
25 # read privileges on it). CI will fail otherwise.
27 'miri': {'oli-obk', 'RalfJung', 'eddyb'},
29 'rustfmt': {'topecongiro', 'calebcartwright'},
30 'book': {'carols10cents', 'steveklabnik'},
31 'nomicon': {'frewsxcv', 'Gankra'},
32 'reference': {'steveklabnik', 'Havvy', 'matthewjasper', 'ehuss'},
33 'rust-by-example': {'steveklabnik', 'marioidival'},
35 'adamgreig', 'andre-richter', 'jamesmunns', 'korken89',
36 'ryankurte', 'thejpster', 'therealprof',
38 'edition-guide': {'ehuss', 'steveklabnik'},
39 'rustc-dev-guide': {'mark-i-m', 'spastorino', 'amanjeev', 'JohnTitor'},
43 'miri': ['A-miri', 'C-bug'],
44 'rls': ['A-rls', 'C-bug'],
48 'reference': ['C-bug'],
49 'rust-by-example': ['C-bug'],
50 'embedded-book': ['C-bug'],
51 'edition-guide': ['C-bug'],
52 'rustc-dev-guide': ['C-bug'],
56 'miri': 'https://github.com/rust-lang/miri',
57 'rls': 'https://github.com/rust-lang/rls',
58 'rustfmt': 'https://github.com/rust-lang/rustfmt',
59 'book': 'https://github.com/rust-lang/book',
60 'nomicon': 'https://github.com/rust-lang/nomicon',
61 'reference': 'https://github.com/rust-lang/reference',
62 'rust-by-example': 'https://github.com/rust-lang/rust-by-example',
63 'embedded-book': 'https://github.com/rust-embedded/book',
64 'edition-guide': 'https://github.com/rust-lang/edition-guide',
65 'rustc-dev-guide': 'https://github.com/rust-lang/rustc-dev-guide',
68 def load_json_from_response(resp):
70 if isinstance(content, bytes):
71 content = content.decode('utf-8')
73 print("Refusing to decode " + str(type(content)) + " to str")
74 return json.loads(content)
76 def validate_maintainers(repo, github_token):
77 '''Ensure all maintainers are assignable on a GitHub repo'''
78 next_link_re = re.compile(r'<([^>]+)>; rel="next"')
80 # Load the list of assignable people in the GitHub repo
82 url = 'https://api.github.com/repos/%s/collaborators?per_page=100' % repo
83 while url is not None:
84 response = urllib2.urlopen(urllib2.Request(url, headers={
85 'Authorization': 'token ' + github_token,
86 # Properly load nested teams.
87 'Accept': 'application/vnd.github.hellcat-preview+json',
89 assignable.extend(user['login'] for user in load_json_from_response(response))
90 # Load the next page if available
92 link_header = response.headers.get('Link')
94 matches = next_link_re.match(link_header)
95 if matches is not None:
96 url = matches.group(1)
99 for tool, maintainers in MAINTAINERS.items():
100 for maintainer in maintainers:
101 if maintainer not in assignable:
104 "error: %s maintainer @%s is not assignable in the %s repo"
105 % (tool, maintainer, repo),
110 print(" To be assignable, a person needs to be explicitly listed as a")
111 print(" collaborator in the repository settings. The simple way to")
112 print(" fix this is to ask someone with 'admin' privileges on the repo")
113 print(" to add the person or whole team as a collaborator with 'read'")
114 print(" privileges. Those privileges don't grant any extra permissions")
115 print(" so it's safe to apply them.")
117 print("The build will fail due to this.")
121 def read_current_status(current_commit, path):
122 '''Reads build status of `current_commit` from content of `history/*.tsv`
124 with open(path, 'rU') as f:
126 (commit, status) = line.split('\t', 1)
127 if commit == current_commit:
128 return json.loads(status)
133 return os.environ['TOOLSTATE_ISSUES_API_URL']
136 def maybe_delink(message):
137 if os.environ.get('TOOLSTATE_SKIP_MENTIONS') is not None:
138 return message.replace("@", "")
150 # Open an issue about the toolstate failure.
151 if status == 'test-fail':
152 status_description = 'has failing tests'
154 status_description = 'no longer builds'
155 request = json.dumps({
156 'body': maybe_delink(textwrap.dedent('''\
157 Hello, this is your friendly neighborhood mergebot.
158 After merging PR {}, I observed that the tool {} {}.
159 A follow-up PR to the repository {} is needed to fix the fallout.
161 cc @{}, do you think you would have time to do the follow-up work?
162 If so, that would be great!
164 And nominating for compiler team prioritization.
167 relevant_pr_number, tool, status_description,
168 REPOS.get(tool), relevant_pr_user
170 'title': '`{}` no longer builds after {}'.format(tool, relevant_pr_number),
171 'assignees': list(assignees),
174 print("Creating issue:\n{}".format(request))
175 response = urllib2.urlopen(urllib2.Request(
179 'Authorization': 'token ' + github_token,
180 'Content-Type': 'application/json',
194 '''Updates `_data/latest.json` to match build result of the given commit.
196 with open('_data/latest.json', 'r+') as f:
197 latest = json.load(f, object_pairs_hook=collections.OrderedDict)
200 os: read_current_status(current_commit, 'history/' + os + '.tsv')
201 for os in ['windows', 'linux']
204 slug = 'rust-lang/rust'
205 message = textwrap.dedent('''\
206 📣 Toolstate changed by {}!
208 Tested on commit {}@{}.
209 Direct link to PR: <{}>
211 ''').format(relevant_pr_number, slug, current_commit, relevant_pr_url)
212 anything_changed = False
213 for status in latest:
214 tool = status['tool']
216 create_issue_for_status = None # set to the status that caused the issue
218 for os, s in current_status.items():
220 new = s.get(tool, old)
222 maintainers = ' '.join('@'+name for name in MAINTAINERS.get(tool, ()))
223 # comparing the strings, but they are ordered appropriately:
224 # "test-pass" > "test-fail" > "build-fail"
226 # things got fixed or at least the status quo improved
228 message += '🎉 {} on {}: {} → {} (cc {}).\n' \
229 .format(tool, os, old, new, maintainers)
231 # tests or builds are failing and were not failing before
233 title = '💔 {} on {}: {} → {}' \
234 .format(tool, os, old, new)
235 message += '{} (cc {}).\n' \
236 .format(title, maintainers)
237 # See if we need to create an issue.
239 # Create issue if tests used to pass before. Don't open a *second*
240 # issue when we regress from "test-fail" to "build-fail".
241 if old == 'test-pass':
242 create_issue_for_status = new
244 # Create issue if things no longer build.
245 # (No issue for mere test failures to avoid spurious issues.)
246 if new == 'build-fail':
247 create_issue_for_status = new
249 if create_issue_for_status is not None:
252 tool, create_issue_for_status, MAINTAINERS.get(tool, ''),
253 relevant_pr_number, relevant_pr_user, LABELS.get(tool, ''),
255 except urllib2.HTTPError as e:
256 # network errors will simply end up not creating an issue, but that's better
257 # than failing the entire build job
258 print("HTTPError when creating issue for status regression: {0}\n{1}"
259 .format(e, e.read()))
261 print("I/O error when creating issue for status regression: {0}".format(e))
263 print("Unexpected error when creating issue for status regression: {0}"
264 .format(sys.exc_info()[0]))
268 status['commit'] = current_commit
269 status['datetime'] = current_datetime
270 anything_changed = True
272 if not anything_changed:
277 json.dump(latest, f, indent=4, separators=(',', ': '))
281 if __name__ == '__main__':
282 repo = os.environ.get('TOOLSTATE_VALIDATE_MAINTAINERS_REPO')
284 github_token = os.environ.get('TOOLSTATE_REPO_ACCESS_TOKEN')
286 validate_maintainers(repo, github_token)
288 print('skipping toolstate maintainers validation since no GitHub token is present')
289 # When validating maintainers don't run the full script.
292 cur_commit = sys.argv[1]
293 cur_datetime = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
294 cur_commit_msg = sys.argv[2]
295 save_message_to_path = sys.argv[3]
296 github_token = sys.argv[4]
298 # assume that PR authors are also owners of the repo where the branch lives
299 relevant_pr_match = re.search(
300 r'Auto merge of #([0-9]+) - ([^:]+):[^,]+, r=(\S+)',
303 if relevant_pr_match:
304 number = relevant_pr_match.group(1)
305 relevant_pr_user = relevant_pr_match.group(2)
306 relevant_pr_number = 'rust-lang/rust#' + number
307 relevant_pr_url = 'https://github.com/rust-lang/rust/pull/' + number
308 pr_reviewer = relevant_pr_match.group(3)
311 relevant_pr_user = 'ghost'
312 relevant_pr_number = '<unknown PR>'
313 relevant_pr_url = '<unknown>'
314 pr_reviewer = 'ghost'
316 message = update_latest(
325 print('<Nothing changed>')
331 print('Dry run only, not committing anything')
334 with open(save_message_to_path, 'w') as f:
337 # Write the toolstate comment on the PR as well.
338 issue_url = gh_url() + '/{}/comments'.format(number)
339 response = urllib2.urlopen(urllib2.Request(
341 json.dumps({'body': maybe_delink(message)}),
343 'Authorization': 'token ' + github_token,
344 'Content-Type': 'application/json',