dart-sdk/tools/bots/get_builder_status.dart
William Hesse bc9f2bdc88 [infra] Remove old pub/sub processing of results
The results are now updated by the builder, so the builder status
does not need to poll the database for the results to finish processing.

The script is also migrated to null safety.

Change-Id: I7913f7fb0a8eedc074a4b79eb75c1c2813525c17
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/221780
Commit-Queue: William Hesse <whesse@google.com>
Reviewed-by: Alexander Thomas <athom@google.com>
2021-12-02 13:14:27 +00:00

296 lines
8.9 KiB
Dart
Executable file

#!/usr/bin/env dart
// Copyright (c) 2019, 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 success/failure status for a builder that is written to
// Firestore by the cloud functions that process results.json.
// These cloud functions write a success/failure result to the
// builder table based on the approvals in Firestore.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:http/http.dart' as http;
const numAttempts = 20;
const failuresPerConfiguration = 20;
late bool useStagingDatabase;
Uri get _queryUrl {
final project = useStagingDatabase ? 'dart-ci-staging' : 'dart-ci';
return Uri.https('firestore.googleapis.com',
'/v1/projects/$project/databases/(default)/documents:runQuery');
}
late String builder;
late String builderBase;
late int buildNumber;
late String token;
late http.Client client;
String get buildTable => builder.endsWith('-try') ? 'try_builds' : 'builds';
String get resultsTable => builder.endsWith('-try') ? 'try_results' : 'results';
bool booleanFieldOrFalse(Map<String, dynamic> document, String field) {
final fieldObject = document['fields'][field];
return fieldObject?['booleanValue'] ?? false;
}
void usage(ArgParser parser) {
print('''
Usage: get_builder_status.dart [OPTIONS]
Gets the builder status from the Firestore database.
Polls until it gets a completed builder status, or times out.
The options are as follows:
${parser.usage}''');
exit(1);
}
Future<String> readGcloudAuthToken(String path) async {
final token = await File(path).readAsString();
return token.split('\n').first;
}
void main(List<String> args) async {
final parser = ArgParser();
parser.addFlag('help', help: 'Show the program usage.', negatable: false);
parser.addOption('auth_token',
abbr: 'a', help: 'Authorization token with cloud-platform scope');
parser.addOption('builder', abbr: 'b', help: 'The builder name');
parser.addOption('build_number', abbr: 'n', help: 'The build number');
parser.addFlag('staging',
abbr: 's', help: 'use staging database', defaultsTo: false);
final options = parser.parse(args);
if (options['help']) {
usage(parser);
}
useStagingDatabase = options['staging'];
builder = options['builder'];
buildNumber = int.parse(options['build_number']);
builderBase = builder.replaceFirst(RegExp('-try\$'), '');
if (options['auth_token'] == null) {
print('Option "--auth_token (-a)" is required\n');
usage(parser);
}
token = await readGcloudAuthToken(options['auth_token']);
client = http.Client();
final response = await runFirestoreQuery(buildQuery());
if (response.statusCode != HttpStatus.ok) {
print('HTTP status ${response.statusCode} received '
'when fetching build data');
exit(2);
}
final documents = jsonDecode(response.body);
final document = documents.first['document'];
if (document == null) {
print('No results received for build $buildNumber of $builder');
exit(2);
}
final success = booleanFieldOrFalse(document, 'success');
print(success
? 'No new unapproved failures'
: 'There are new unapproved failures on this build');
if (builder.endsWith('-try')) exit(success ? 0 : 1);
final configurations = await getConfigurations();
final failures = await fetchActiveFailures(configurations);
if (failures.isNotEmpty) {
print('There are unapproved failures');
printActiveFailures(failures);
exit(1);
} else {
print('There are no unapproved failures');
exit(0);
}
}
Future<List<String>> getConfigurations() async {
final response = await runFirestoreQuery(configurationsQuery());
if (response.statusCode != HttpStatus.ok) {
print('Could not fetch configurations for $builderBase');
return [];
}
final documents = jsonDecode(response.body);
final groups = <String>{
for (Map document in documents)
if (document.containsKey('document'))
document['document']['name'].split('/').last
};
return groups.toList();
}
Map<int, Future<String>> commitHashes = {};
Future<String> commitHash(int index) =>
commitHashes.putIfAbsent(index, () => fetchCommitHash(index));
Future<String> fetchCommitHash(int index) async {
final response = await runFirestoreQuery(commitQuery(index));
if (response.statusCode == HttpStatus.ok) {
final document = jsonDecode(response.body).first['document'];
if (document != null) {
return document['name'].split('/').last;
}
}
print('Could not fetch commit with index $index');
return 'missing hash for commit $index';
}
Future<Map<String, List<Map<String, dynamic>>>> fetchActiveFailures(
List<String> configurations) async {
final failures = <String, List<Map<String, dynamic>>>{};
for (final configuration in configurations) {
final response =
await runFirestoreQuery(unapprovedFailuresQuery(configuration));
if (response.statusCode == HttpStatus.ok) {
final documents = jsonDecode(response.body);
for (final documentItem in documents) {
final document = documentItem['document'];
if (document == null) continue;
final fields = document['fields'];
failures.putIfAbsent(configuration, () => []).add({
'name': fields['name']['stringValue'],
'start_commit': await commitHash(
int.parse(fields['blamelist_start_index']['integerValue'])),
'end_commit': await commitHash(
int.parse(fields['blamelist_end_index']['integerValue'])),
'result': fields['result']['stringValue'],
'expected': fields['expected']['stringValue'],
'previous': fields['previous_result']['stringValue'],
});
}
}
}
return failures;
}
void printActiveFailures(Map<String, List<Map<String, dynamic>>> failures) {
failures.forEach((configuration, failureList) {
print('($configuration):');
for (final failure in failureList) {
print([
' ',
failure['name'],
' (',
failure['previous'],
' -> ',
failure['result'],
', expected ',
failure['expected'],
') at ',
failure['start_commit'].substring(0, 6),
if (failure['end_commit'] != failure['start_commit']) ...[
'..',
failure['end_commit'].substring(0, 6)
]
].join(''));
}
});
}
Future<http.Response> runFirestoreQuery(String query) {
final headers = {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Content-Type': 'application/json'
};
return client.post(_queryUrl, headers: headers, body: query);
}
String buildQuery() => jsonEncode({
'structuredQuery': {
'from': [
{'collectionId': buildTable}
],
'limit': 1,
'where': {
'compositeFilter': {
'op': 'AND',
'filters': [
{
'fieldFilter': {
'field': {'fieldPath': 'build_number'},
'op': 'EQUAL',
'value': {'integerValue': buildNumber}
}
},
{
'fieldFilter': {
'field': {'fieldPath': 'builder'},
'op': 'EQUAL',
'value': {'stringValue': builder}
}
}
]
}
}
}
});
String configurationsQuery() => jsonEncode({
'structuredQuery': {
'from': [
{'collectionId': 'configurations'}
],
'where': {
'fieldFilter': {
'field': {'fieldPath': 'builder'},
'op': 'EQUAL',
'value': {'stringValue': builderBase}
}
}
}
});
String unapprovedFailuresQuery(String configuration) => jsonEncode({
'structuredQuery': {
'from': [
{'collectionId': resultsTable}
],
'limit': failuresPerConfiguration,
'where': {
'compositeFilter': {
'op': 'AND',
'filters': [
{
'fieldFilter': {
'field': {'fieldPath': 'active_configurations'},
'op': 'ARRAY_CONTAINS',
'value': {'stringValue': configuration}
}
},
{
'fieldFilter': {
'field': {'fieldPath': 'approved'},
'op': 'EQUAL',
'value': {'booleanValue': false}
}
}
]
}
}
}
});
String commitQuery(int index) => jsonEncode({
'structuredQuery': {
'from': [
{'collectionId': 'commits'}
],
'limit': 1,
'where': {
'fieldFilter': {
'field': {'fieldPath': 'index'},
'op': 'EQUAL',
'value': {'integerValue': index}
}
}
}
});