2 # -*- coding: utf-8 -*-
4 # This script publishes the new "current" toolstate in the toolstate repo (not to be
5 # confused with publishing the test results, which happens in
6 # `src/ci/docker/x86_64-gnu-tools/checktools.sh`).
7 # It is set as callback for `src/ci/docker/x86_64-gnu-tools/repo.sh` by the CI scripts
8 # when a new commit lands on `master` (i.e., after it passed all checks on `auto`).
10 from __future__ import print_function
22 import urllib.request as urllib2
24 # List of people to ping when the status of a tool or a book changed.
25 # These should be collaborators of the rust-lang/rust repository (with at least
26 # read privileges on it). CI will fail otherwise.
28 'miri': {'oli-obk', 'RalfJung', 'eddyb'},
30 'Manishearth', 'llogiq', 'mcarton', 'oli-obk', 'phansch', 'flip1995',
34 'rustfmt': {'topecongiro'},
35 'book': {'carols10cents', 'steveklabnik'},
36 'nomicon': {'frewsxcv', 'Gankra'},
37 'reference': {'steveklabnik', 'Havvy', 'matthewjasper', 'ehuss'},
38 'rust-by-example': {'steveklabnik', 'marioidival'},
40 'adamgreig', 'andre-richter', 'jamesmunns', 'korken89',
41 'ryankurte', 'thejpster', 'therealprof',
43 'edition-guide': {'ehuss', 'Centril', 'steveklabnik'},
44 'rustc-guide': {'mark-i-m', 'spastorino', 'amanjeev', 'JohnTitor'},
48 'miri': 'https://github.com/rust-lang/miri',
49 'clippy-driver': 'https://github.com/rust-lang/rust-clippy',
50 'rls': 'https://github.com/rust-lang/rls',
51 'rustfmt': 'https://github.com/rust-lang/rustfmt',
52 'book': 'https://github.com/rust-lang/book',
53 'nomicon': 'https://github.com/rust-lang/nomicon',
54 'reference': 'https://github.com/rust-lang/reference',
55 'rust-by-example': 'https://github.com/rust-lang/rust-by-example',
56 'embedded-book': 'https://github.com/rust-embedded/book',
57 'edition-guide': 'https://github.com/rust-lang/edition-guide',
58 'rustc-guide': 'https://github.com/rust-lang/rustc-guide',
62 def validate_maintainers(repo, github_token):
63 '''Ensure all maintainers are assignable on a GitHub repo'''
64 next_link_re = re.compile(r'<([^>]+)>; rel="next"')
66 # Load the list of assignable people in the GitHub repo
68 url = 'https://api.github.com/repos/%s/collaborators?per_page=100' % repo
69 while url is not None:
70 response = urllib2.urlopen(urllib2.Request(url, headers={
71 'Authorization': 'token ' + github_token,
72 # Properly load nested teams.
73 'Accept': 'application/vnd.github.hellcat-preview+json',
75 assignable.extend(user['login'] for user in json.load(response))
76 # Load the next page if available
78 link_header = response.headers.get('Link')
80 matches = next_link_re.match(link_header)
81 if matches is not None:
82 url = matches.group(1)
85 for tool, maintainers in MAINTAINERS.items():
86 for maintainer in maintainers:
87 if maintainer not in assignable:
90 "error: %s maintainer @%s is not assignable in the %s repo"
91 % (tool, maintainer, repo),
96 print(" To be assignable, a person needs to be explicitly listed as a")
97 print(" collaborator in the repository settings. The simple way to")
98 print(" fix this is to ask someone with 'admin' privileges on the repo")
99 print(" to add the person or whole team as a collaborator with 'read'")
100 print(" privileges. Those privileges don't grant any extra permissions")
101 print(" so it's safe to apply them.")
103 print("The build will fail due to this.")
107 def read_current_status(current_commit, path):
108 '''Reads build status of `current_commit` from content of `history/*.tsv`
110 with open(path, 'rU') as f:
112 (commit, status) = line.split('\t', 1)
113 if commit == current_commit:
114 return json.loads(status)
119 return os.environ['TOOLSTATE_ISSUES_API_URL']
122 def maybe_delink(message):
123 if os.environ.get('TOOLSTATE_SKIP_MENTIONS') is not None:
124 return message.replace("@", "")
136 # Open an issue about the toolstate failure.
137 if status == 'test-fail':
138 status_description = 'has failing tests'
140 status_description = 'no longer builds'
141 request = json.dumps({
142 'body': maybe_delink(textwrap.dedent('''\
143 Hello, this is your friendly neighborhood mergebot.
144 After merging PR {}, I observed that the tool {} {}.
145 A follow-up PR to the repository {} is needed to fix the fallout.
147 cc @{}, do you think you would have time to do the follow-up work?
148 If so, that would be great!
150 cc @{}, the PR reviewer, and nominating for compiler team prioritization.
153 relevant_pr_number, tool, status_description,
154 REPOS.get(tool), relevant_pr_user, pr_reviewer
156 'title': '`{}` no longer builds after {}'.format(tool, relevant_pr_number),
157 'assignees': list(assignees),
158 'labels': ['T-compiler', 'I-nominated'],
160 print("Creating issue:\n{}".format(request))
161 response = urllib2.urlopen(urllib2.Request(
165 'Authorization': 'token ' + github_token,
166 'Content-Type': 'application/json',
180 '''Updates `_data/latest.json` to match build result of the given commit.
182 with open('_data/latest.json', 'rb+') as f:
183 latest = json.load(f, object_pairs_hook=collections.OrderedDict)
186 os: read_current_status(current_commit, 'history/' + os + '.tsv')
187 for os in ['windows', 'linux']
190 slug = 'rust-lang/rust'
191 message = textwrap.dedent('''\
192 📣 Toolstate changed by {}!
194 Tested on commit {}@{}.
195 Direct link to PR: <{}>
197 ''').format(relevant_pr_number, slug, current_commit, relevant_pr_url)
198 anything_changed = False
199 for status in latest:
200 tool = status['tool']
202 create_issue_for_status = None # set to the status that caused the issue
204 for os, s in current_status.items():
206 new = s.get(tool, old)
208 maintainers = ' '.join('@'+name for name in MAINTAINERS[tool])
209 # comparing the strings, but they are ordered appropriately:
210 # "test-pass" > "test-fail" > "build-fail"
212 # things got fixed or at least the status quo improved
214 message += '🎉 {} on {}: {} → {} (cc {}, @rust-lang/infra).\n' \
215 .format(tool, os, old, new, maintainers)
217 # tests or builds are failing and were not failing before
219 title = '💔 {} on {}: {} → {}' \
220 .format(tool, os, old, new)
221 message += '{} (cc {}, @rust-lang/infra).\n' \
222 .format(title, maintainers)
223 # See if we need to create an issue.
225 # Create issue if tests used to pass before. Don't open a *second*
226 # issue when we regress from "test-fail" to "build-fail".
227 if old == 'test-pass':
228 create_issue_for_status = new
230 # Create issue if things no longer build.
231 # (No issue for mere test failures to avoid spurious issues.)
232 if new == 'build-fail':
233 create_issue_for_status = new
235 if create_issue_for_status is not None:
238 tool, create_issue_for_status, MAINTAINERS.get(tool, ''),
239 relevant_pr_number, relevant_pr_user, pr_reviewer,
241 except urllib2.HTTPError as e:
242 # network errors will simply end up not creating an issue, but that's better
243 # than failing the entire build job
244 print("HTTPError when creating issue for status regression: {0}\n{1}"
245 .format(e, e.read()))
247 print("I/O error when creating issue for status regression: {0}".format(e))
249 print("Unexpected error when creating issue for status regression: {0}"
250 .format(sys.exc_info()[0]))
254 status['commit'] = current_commit
255 status['datetime'] = current_datetime
256 anything_changed = True
258 if not anything_changed:
263 json.dump(latest, f, indent=4, separators=(',', ': '))
267 if __name__ == '__main__':
268 repo = os.environ.get('TOOLSTATE_VALIDATE_MAINTAINERS_REPO')
270 github_token = os.environ.get('TOOLSTATE_REPO_ACCESS_TOKEN')
272 validate_maintainers(repo, github_token)
274 print('skipping toolstate maintainers validation since no GitHub token is present')
275 # When validating maintainers don't run the full script.
278 cur_commit = sys.argv[1]
279 cur_datetime = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
280 cur_commit_msg = sys.argv[2]
281 save_message_to_path = sys.argv[3]
282 github_token = sys.argv[4]
284 # assume that PR authors are also owners of the repo where the branch lives
285 relevant_pr_match = re.search(
286 r'Auto merge of #([0-9]+) - ([^:]+):[^,]+, r=(\S+)',
289 if relevant_pr_match:
290 number = relevant_pr_match.group(1)
291 relevant_pr_user = relevant_pr_match.group(2)
292 relevant_pr_number = 'rust-lang/rust#' + number
293 relevant_pr_url = 'https://github.com/rust-lang/rust/pull/' + number
294 pr_reviewer = relevant_pr_match.group(3)
297 relevant_pr_user = 'ghost'
298 relevant_pr_number = '<unknown PR>'
299 relevant_pr_url = '<unknown>'
300 pr_reviewer = 'ghost'
302 message = update_latest(
311 print('<Nothing changed>')
317 print('Dry run only, not committing anything')
320 with open(save_message_to_path, 'w') as f:
323 # Write the toolstate comment on the PR as well.
324 issue_url = gh_url() + '/{}/comments'.format(number)
325 response = urllib2.urlopen(urllib2.Request(
327 json.dumps({'body': maybe_delink(message)}),
329 'Authorization': 'token ' + github_token,
330 'Content-Type': 'application/json',