blob: 73dcfda4df8b1bf93346581b41194a62189cbc59 [file] [log] [blame] [edit]
#!/usr/bin/env dart
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// Find the newest commit that has a full set of results on the builders.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:args/args.dart';
import 'package:glob/glob.dart';
void main(List<String> args) async {
final parser = ArgParser();
parser.addMultiOption("builder",
abbr: "b",
help: "Select the builders matching the glob [option is repeatable]",
splitCommas: false);
parser.addOption("branch",
abbr: "B",
help: "Select the builders building this branch",
defaultsTo: "main");
parser.addOption("count",
abbr: "c", help: "List this many commits", defaultsTo: "1");
parser.addFlag("help", help: "Show the program usage.", negatable: false);
final options = parser.parse(args);
if (options.flag("help")) {
print("""
Usage: find_base_commit.dart [OPTION]...
Find the newest commit that has a full set of results on the builders.
The options are as follows:
${parser.usage}""");
return;
}
int count = int.parse(options.option("count")!);
final globs = List<Glob>.from(
options.multiOption("builder").map((String pattern) => Glob(pattern)));
// Download the most recent builds from buildbucket.
const maxBuilds = 1000;
final url = Uri.parse("https://cr-buildbucket.appspot.com"
"/prpc/buildbucket.v2.Builds/SearchBuilds");
const maxRetries = 3;
const timeout = Duration(seconds: 30);
final query = jsonEncode({
"predicate": {
"builder": {"project": "dart", "bucket": "ci.sandbox"},
"status": "ENDED_MASK"
},
"pageSize": maxBuilds,
"fields": "builds.*.builder.builder,builds.*.input"
});
late Map<String, dynamic> searchResult;
for (int i = 1; i <= maxRetries; i++) {
try {
final client = HttpClient();
final request = await client.postUrl(url).timeout(timeout)
..headers.contentType = ContentType.json
..headers.add(HttpHeaders.acceptHeader, ContentType.json)
..write(query);
final response = await request.close().timeout(timeout);
if (response.statusCode != 200) {
print("Failed to search for builds: "
"${response.statusCode}:${response.reasonPhrase}");
exit(1);
}
const prefix = ")]}'";
searchResult = await (response
.cast<List<int>>()
.transform(Utf8Decoder())
.map((event) =>
event.startsWith(prefix) ? event.substring(prefix.length) : event)
.transform(JsonDecoder())
.cast<Map<String, dynamic>>()
.first
.timeout(timeout));
client.close();
break;
} on TimeoutException catch (e) {
final inSeconds = e.duration?.inSeconds;
stderr.writeln(
"Attempt $i of $maxRetries timed out after $inSeconds seconds");
if (i == maxRetries) {
stderr.writeln("error: Failed to download $url");
exit(1);
}
}
}
// Locate the builds we're interested in and map them to each commit. The
// builds returned by the API are sorted with the newest first. Since builders
// don't build back in time and always build the latest commit whenever they
// can, the first time we see a commit, we know it's newer than all commits
// we haven't seen yet. The insertion order into the buildersForCommits map
// will then sorted with the newest commit first.
final builds = searchResult["builds"] as List?;
if (builds == null) {
print("No builds found");
exit(1);
}
final buildersForCommits = <String, Set<String>>{};
for (final build in builds) {
final builder = (build["builder"] as Map?)?["builder"];
if (builder is! String ||
builder.endsWith("-beta") ||
builder.endsWith("-dev") ||
builder.endsWith("-stable")) {
// Ignore the release builders. The -try builders aren't in the
// bucket we're reading.
continue;
}
if (globs.isNotEmpty && !globs.any((glob) => glob.matches(builder))) {
// Filter way builders we're not interested in.
continue;
}
final input = (build["input"] as Map?)?["gitilesCommit"] as Map?;
if (input == null) {
// Ignore builds not triggered by a commit, e.g. fuzz-linux.
continue;
}
final ref = input["ref"];
if (ref != "refs/heads/${options.option('branch')}") {
// Ignore builds on the wrong branch.
continue;
}
final commit = input["id"] as String;
final buildersForCommit =
buildersForCommits.putIfAbsent(commit, () => <String>{});
buildersForCommit.add(builder);
}
if (buildersForCommits.isEmpty) {
print("Failed to locate any commits having run on the builders");
exitCode = 1;
return;
}
int maxBots = 0;
for (final builders in buildersForCommits.values) {
maxBots = max(maxBots, builders.length);
}
// List commits run on the most builders.
for (final commit in buildersForCommits.keys
.where((commit) => buildersForCommits[commit]!.length == maxBots)
.take(count)) {
print(commit);
}
}