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'},
34 'embedded-book': {'adamgreig', 'andre-richter', 'jamesmunns', 'therealprof'},
35 'edition-guide': {'ehuss', 'steveklabnik'},
36 'rustc-dev-guide': {'mark-i-m', 'spastorino', 'amanjeev', 'JohnTitor'},
40 'miri': ['A-miri', 'C-bug'],
41 'rls': ['A-rls', 'C-bug'],
42 'rustfmt': ['A-rustfmt', 'C-bug'],
45 'reference': ['C-bug'],
46 'rust-by-example': ['C-bug'],
47 'embedded-book': ['C-bug'],
48 'edition-guide': ['C-bug'],
49 'rustc-dev-guide': ['C-bug'],
53 'miri': 'https://github.com/rust-lang/miri',
54 'rls': 'https://github.com/rust-lang/rls',
55 'rustfmt': 'https://github.com/rust-lang/rustfmt',
56 'book': 'https://github.com/rust-lang/book',
57 'nomicon': 'https://github.com/rust-lang/nomicon',
58 'reference': 'https://github.com/rust-lang/reference',
59 'rust-by-example': 'https://github.com/rust-lang/rust-by-example',
60 'embedded-book': 'https://github.com/rust-embedded/book',
61 'edition-guide': 'https://github.com/rust-lang/edition-guide',
62 'rustc-dev-guide': 'https://github.com/rust-lang/rustc-dev-guide',
65 def load_json_from_response(resp):
67 if isinstance(content, bytes):
68 content = content.decode('utf-8')
70 print("Refusing to decode " + str(type(content)) + " to str")
71 return json.loads(content)
73 def validate_maintainers(repo, github_token):
74 '''Ensure all maintainers are assignable on a GitHub repo'''
75 next_link_re = re.compile(r'<([^>]+)>; rel="next"')
77 # Load the list of assignable people in the GitHub repo
79 url = 'https://api.github.com/repos/%s/collaborators?per_page=100' % repo
80 while url is not None:
81 response = urllib2.urlopen(urllib2.Request(url, headers={
82 'Authorization': 'token ' + github_token,
83 # Properly load nested teams.
84 'Accept': 'application/vnd.github.hellcat-preview+json',
86 assignable.extend(user['login'] for user in load_json_from_response(response))
87 # Load the next page if available
89 link_header = response.headers.get('Link')
91 matches = next_link_re.match(link_header)
92 if matches is not None:
93 url = matches.group(1)
96 for tool, maintainers in MAINTAINERS.items():
97 for maintainer in maintainers:
98 if maintainer not in assignable:
101 "error: %s maintainer @%s is not assignable in the %s repo"
102 % (tool, maintainer, repo),
107 print(" To be assignable, a person needs to be explicitly listed as a")
108 print(" collaborator in the repository settings. The simple way to")
109 print(" fix this is to ask someone with 'admin' privileges on the repo")
110 print(" to add the person or whole team as a collaborator with 'read'")
111 print(" privileges. Those privileges don't grant any extra permissions")
112 print(" so it's safe to apply them.")
114 print("The build will fail due to this.")
118 def read_current_status(current_commit, path):
119 '''Reads build status of `current_commit` from content of `history/*.tsv`
121 with open(path, 'rU') as f:
123 (commit, status) = line.split('\t', 1)
124 if commit == current_commit:
125 return json.loads(status)
130 return os.environ['TOOLSTATE_ISSUES_API_URL']
133 def maybe_delink(message):
134 if os.environ.get('TOOLSTATE_SKIP_MENTIONS') is not None:
135 return message.replace("@", "")
147 # Open an issue about the toolstate failure.
148 if status == 'test-fail':
149 status_description = 'has failing tests'
151 status_description = 'no longer builds'
152 request = json.dumps({
153 'body': maybe_delink(textwrap.dedent('''\
154 Hello, this is your friendly neighborhood mergebot.
155 After merging PR {}, I observed that the tool {} {}.
156 A follow-up PR to the repository {} is needed to fix the fallout.
158 cc @{}, do you think you would have time to do the follow-up work?
159 If so, that would be great!
161 And nominating for compiler team prioritization.
164 relevant_pr_number, tool, status_description,
165 REPOS.get(tool), relevant_pr_user
167 'title': '`{}` no longer builds after {}'.format(tool, relevant_pr_number),
168 'assignees': list(assignees),
171 print("Creating issue:\n{}".format(request))
172 response = urllib2.urlopen(urllib2.Request(
176 'Authorization': 'token ' + github_token,
177 'Content-Type': 'application/json',
191 '''Updates `_data/latest.json` to match build result of the given commit.
193 with open('_data/latest.json', 'r+') as f:
194 latest = json.load(f, object_pairs_hook=collections.OrderedDict)
197 os: read_current_status(current_commit, 'history/' + os + '.tsv')
198 for os in ['windows', 'linux']
201 slug = 'rust-lang/rust'
202 message = textwrap.dedent('''\
203 📣 Toolstate changed by {}!
205 Tested on commit {}@{}.
206 Direct link to PR: <{}>
208 ''').format(relevant_pr_number, slug, current_commit, relevant_pr_url)
209 anything_changed = False
210 for status in latest:
211 tool = status['tool']
213 create_issue_for_status = None # set to the status that caused the issue
215 for os, s in current_status.items():
217 new = s.get(tool, old)
219 maintainers = ' '.join('@'+name for name in MAINTAINERS.get(tool, ()))
220 # comparing the strings, but they are ordered appropriately:
221 # "test-pass" > "test-fail" > "build-fail"
223 # things got fixed or at least the status quo improved
225 message += '🎉 {} on {}: {} → {} (cc {}).\n' \
226 .format(tool, os, old, new, maintainers)
228 # tests or builds are failing and were not failing before
230 title = '💔 {} on {}: {} → {}' \
231 .format(tool, os, old, new)
232 message += '{} (cc {}).\n' \
233 .format(title, maintainers)
234 # See if we need to create an issue.
236 # Create issue if tests used to pass before. Don't open a *second*
237 # issue when we regress from "test-fail" to "build-fail".
238 if old == 'test-pass':
239 create_issue_for_status = new
241 # Create issue if things no longer build.
242 # (No issue for mere test failures to avoid spurious issues.)
243 if new == 'build-fail':
244 create_issue_for_status = new
246 if create_issue_for_status is not None:
249 tool, create_issue_for_status, MAINTAINERS.get(tool, ''),
250 relevant_pr_number, relevant_pr_user, LABELS.get(tool, ''),
252 except urllib2.HTTPError as e:
253 # network errors will simply end up not creating an issue, but that's better
254 # than failing the entire build job
255 print("HTTPError when creating issue for status regression: {0}\n{1}"
256 .format(e, e.read()))
258 print("I/O error when creating issue for status regression: {0}".format(e))
260 print("Unexpected error when creating issue for status regression: {0}"
261 .format(sys.exc_info()[0]))
265 status['commit'] = current_commit
266 status['datetime'] = current_datetime
267 anything_changed = True
269 if not anything_changed:
274 json.dump(latest, f, indent=4, separators=(',', ': '))
278 if __name__ == '__main__':
279 repo = os.environ.get('TOOLSTATE_VALIDATE_MAINTAINERS_REPO')
281 github_token = os.environ.get('TOOLSTATE_REPO_ACCESS_TOKEN')
283 validate_maintainers(repo, github_token)
285 print('skipping toolstate maintainers validation since no GitHub token is present')
286 # When validating maintainers don't run the full script.
289 cur_commit = sys.argv[1]
290 cur_datetime = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
291 cur_commit_msg = sys.argv[2]
292 save_message_to_path = sys.argv[3]
293 github_token = sys.argv[4]
295 # assume that PR authors are also owners of the repo where the branch lives
296 relevant_pr_match = re.search(
297 r'Auto merge of #([0-9]+) - ([^:]+):[^,]+, r=(\S+)',
300 if relevant_pr_match:
301 number = relevant_pr_match.group(1)
302 relevant_pr_user = relevant_pr_match.group(2)
303 relevant_pr_number = 'rust-lang/rust#' + number
304 relevant_pr_url = 'https://github.com/rust-lang/rust/pull/' + number
305 pr_reviewer = relevant_pr_match.group(3)
308 relevant_pr_user = 'ghost'
309 relevant_pr_number = '<unknown PR>'
310 relevant_pr_url = '<unknown>'
311 pr_reviewer = 'ghost'
313 message = update_latest(
322 print('<Nothing changed>')
328 print('Dry run only, not committing anything')
331 with open(save_message_to_path, 'w') as f:
334 # Write the toolstate comment on the PR as well.
335 issue_url = gh_url() + '/{}/comments'.format(number)
336 response = urllib2.urlopen(urllib2.Request(
338 json.dumps({'body': maybe_delink(message)}),
340 'Authorization': 'token ' + github_token,
341 'Content-Type': 'application/json',