| #!/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'] as Map)[field]; |
| return (fieldObject as Map?)?['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.flag('help')) { |
| usage(parser); |
| } |
| |
| useStagingDatabase = options.flag('staging'); |
| |
| if (options.option('builder') == null) { |
| print('Option "--builder" is required\n'); |
| usage(parser); |
| } |
| builder = options.option('builder')!; |
| builderBase = builder.replaceFirst(RegExp('-try\$'), ''); |
| |
| if (options.option('build_number') == null) { |
| print('Option "--build_number" is required\n'); |
| usage(parser); |
| } |
| buildNumber = int.parse(options.option('build_number')!); |
| |
| if (options.option('auth_token') == null) { |
| print('Option "--auth_token" is required\n'); |
| usage(parser); |
| } |
| token = await readGcloudAuthToken(options.option('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) as List; |
| final document = (documents.first as Map)['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'] as Map)['name'] as String).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 documents = jsonDecode(response.body) as List; |
| final document = (documents.first as Map)['document'] as Map?; |
| if (document != null) { |
| return (document['name'] as String).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) as List).cast<Map>(); |
| for (final documentItem in documents) { |
| final document = documentItem['document'] as Map?; |
| if (document == null) continue; |
| final fields = document['fields'] as Map; |
| failures.putIfAbsent(configuration, () => []).add({ |
| 'name': (fields['name'] as Map)['stringValue'], |
| 'start_commit': await commitHash(int.parse( |
| (fields['blamelist_start_index'] as Map)['integerValue'])), |
| 'end_commit': await commitHash(int.parse( |
| (fields['blamelist_end_index'] as Map)['integerValue'])), |
| 'result': (fields['result'] as Map)['stringValue'], |
| 'expected': (fields['expected'] as Map)['stringValue'], |
| 'previous': (fields['previous_result'] as Map)['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'] as String).substring(0, 6), |
| if (failure['end_commit'] != failure['start_commit']) ...[ |
| '..', |
| (failure['end_commit'] as String).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} |
| } |
| } |
| } |
| }); |