| // Copyright 2019 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:gcloud/db.dart'; |
| import 'package:github/github.dart'; |
| import 'package:googleapis/bigquery/v2.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import '../datastore/cocoon_config.dart'; |
| import '../foundation/providers.dart'; |
| import '../foundation/typedefs.dart'; |
| import '../foundation/utils.dart'; |
| import '../model/appengine/commit.dart'; |
| import '../model/appengine/task.dart'; |
| import '../model/devicelab/manifest.dart'; |
| import '../request_handling/api_request_handler.dart'; |
| import '../request_handling/authentication.dart'; |
| import '../request_handling/body.dart'; |
| import '../request_handling/exceptions.dart'; |
| import '../service/datastore.dart'; |
| import '../service/github_service.dart'; |
| |
| /// Queries GitHub for the list of recent commits according to different branches, |
| /// and creates corresponding rows in the cloud datastore and the BigQuery for any commits |
| /// not yet there. Then creates new task rows in the datastore for any commits that |
| /// were added. The task rows that it creates are driven by the Flutter [Manifest]. |
| @immutable |
| class RefreshGithubCommits extends ApiRequestHandler<Body> { |
| const RefreshGithubCommits( |
| Config config, |
| AuthenticationProvider authenticationProvider, { |
| @visibleForTesting |
| this.datastoreProvider = DatastoreService.defaultProvider, |
| @visibleForTesting this.httpClientProvider = Providers.freshHttpClient, |
| @visibleForTesting |
| this.branchHttpClientProvider = Providers.freshHttpClient, |
| @visibleForTesting this.gitHubBackoffCalculator = twoSecondLinearBackoff, |
| }) : assert(datastoreProvider != null), |
| assert(httpClientProvider != null), |
| assert(branchHttpClientProvider != null), |
| assert(gitHubBackoffCalculator != null), |
| super(config: config, authenticationProvider: authenticationProvider); |
| |
| final DatastoreServiceProvider datastoreProvider; |
| final HttpClientProvider httpClientProvider; |
| final HttpClientProvider branchHttpClientProvider; |
| final GitHubBackoffCalculator gitHubBackoffCalculator; |
| |
| @override |
| Future<Body> get() async { |
| final RepositorySlug slug = RepositorySlug('flutter', 'flutter'); |
| final GithubService githubService = |
| await config.createGithubService(slug.owner, slug.name); |
| final DatastoreService datastore = datastoreProvider(config.db); |
| |
| for (String branch in await config.flutterBranches) { |
| final List<Commit> lastProcessedCommit = |
| await datastore.queryRecentCommits(limit: 1, branch: branch).toList(); |
| |
| /// That [lastCommitTimestampMills] equals 0 means a new release branch is detected. |
| int lastCommitTimestampMills = 0; |
| if (lastProcessedCommit.isNotEmpty) { |
| lastCommitTimestampMills = lastProcessedCommit[0].timestamp; |
| } |
| |
| final List<RepositoryCommit> commits = await githubService.listCommits( |
| slug, branch, lastCommitTimestampMills); |
| |
| final List<Commit> newCommits = |
| await _getNewCommits(commits, datastore, branch); |
| |
| if (newCommits.isEmpty) { |
| // Nothing to do. |
| continue; |
| } |
| log.debug( |
| 'Found ${newCommits.length} new commits for branch $branch on GitHub'); |
| |
| //Save [Commit] to BigQuery and create [Task] in Datastore. |
| await _saveData(newCommits, datastore); |
| } |
| return Body.empty; |
| } |
| |
| Future<void> _saveData( |
| List<Commit> newCommits, |
| DatastoreService datastore, |
| ) async { |
| const String projectId = 'flutter-dashboard'; |
| const String dataset = 'cocoon'; |
| const String table = 'Checklist'; |
| |
| final TabledataResourceApi tabledataResourceApi = |
| await config.createTabledataResourceApi(); |
| final List<Map<String, Object>> tableDataInsertAllRequestRows = |
| <Map<String, Object>>[]; |
| |
| for (Commit commit in newCommits) { |
| /// Consolidate [commits] together |
| /// |
| /// Prepare for bigquery [insertAll] |
| tableDataInsertAllRequestRows.add(<String, Object>{ |
| 'json': <String, Object>{ |
| 'ID': commit.id, |
| 'CreateTimestamp': commit.timestamp, |
| 'FlutterRepositoryPath': commit.repository, |
| 'CommitSha': commit.sha, |
| 'CommitAuthorLogin': commit.author, |
| 'CommitAuthorAvatarURL': commit.authorAvatarUrl, |
| 'Branch': commit.branch, |
| }, |
| }); |
| |
| final List<Task> tasks = await _createTasks( |
| commitKey: commit.key, |
| sha: commit.sha, |
| createTimestamp: DateTime.now().millisecondsSinceEpoch, |
| ); |
| |
| try { |
| await datastore.withTransaction<void>((Transaction transaction) async { |
| transaction.queueMutations(inserts: <Commit>[commit]); |
| transaction.queueMutations(inserts: tasks); |
| await transaction.commit(); |
| log.debug( |
| 'Committed ${tasks.length} new tasks for commit ${commit.sha}'); |
| }); |
| } catch (error) { |
| log.error('Failed to add commit ${commit.sha}: $error'); |
| } |
| } |
| |
| /// Final [rows] to be inserted to [BigQuery] |
| final TableDataInsertAllRequest rows = TableDataInsertAllRequest.fromJson( |
| <String, Object>{'rows': tableDataInsertAllRequestRows}); |
| |
| /// Insert [commits] to [BigQuery] |
| try { |
| await tabledataResourceApi.insertAll(rows, projectId, dataset, table); |
| } on ApiRequestError { |
| log.warning('Failed to add commits to BigQuery: $ApiRequestError'); |
| } |
| } |
| |
| Future<List<Commit>> _getNewCommits(List<RepositoryCommit> commits, |
| DatastoreService datastore, String branch) async { |
| final List<Commit> newCommits = <Commit>[]; |
| for (RepositoryCommit commit in commits) { |
| final String id = 'flutter/flutter/$branch/${commit.sha}'; |
| final Key key = datastore.db.emptyKey.append(Commit, id: id); |
| |
| if (await datastore.db.lookupValue<Commit>(key, orElse: () => null) == |
| null) { |
| newCommits.add(Commit( |
| key: key, |
| timestamp: commit.commit.committer.date.millisecondsSinceEpoch, |
| repository: 'flutter/flutter', |
| sha: commit.sha, |
| author: commit.author.login, |
| authorAvatarUrl: commit.author.avatarUrl, |
| branch: branch, |
| )); |
| } else { |
| // Once we've found a commit that's already been recorded, we stop looking. |
| break; |
| } |
| } |
| return newCommits; |
| } |
| |
| Future<List<Task>> _createTasks({ |
| @required Key commitKey, |
| @required String sha, |
| @required int createTimestamp, |
| }) async { |
| Task newTask( |
| String name, |
| String stageName, |
| List<String> requiredCapabilities, |
| bool isFlaky, |
| int timeoutInMinutes, |
| ) { |
| return Task( |
| key: commitKey.append(Task), |
| commitKey: commitKey, |
| createTimestamp: createTimestamp, |
| startTimestamp: 0, |
| endTimestamp: 0, |
| name: name, |
| attempts: 0, |
| isFlaky: isFlaky, |
| timeoutInMinutes: timeoutInMinutes, |
| requiredCapabilities: requiredCapabilities, |
| stageName: stageName, |
| status: Task.statusNew, |
| ); |
| } |
| |
| final List<Task> tasks = <Task>[ |
| // These built-in tasks are not listed in the manifest. |
| newTask('cirrus', 'cirrus', <String>['can-update-github'], false, 0), |
| newTask( |
| 'mac_bot', 'chromebot', <String>['can-update-chromebots'], false, 0), |
| newTask('linux_bot', 'chromebot', <String>['can-update-chromebots'], |
| false, 0), |
| newTask('windows_bot', 'chromebot', <String>['can-update-chromebots'], |
| false, 0), |
| ]; |
| |
| final YamlMap yaml = await _loadDevicelabManifest(sha); |
| final Manifest manifest = Manifest.fromJson(yaml); |
| manifest.tasks.forEach((String taskName, ManifestTask info) { |
| tasks.add(newTask( |
| taskName, |
| info.stage, |
| info.requiredAgentCapabilities, |
| info.isFlaky, |
| info.timeoutInMinutes, |
| )); |
| }); |
| |
| return tasks; |
| } |
| |
| Future<YamlMap> _loadDevicelabManifest(String sha) async { |
| final String path = '/flutter/flutter/$sha/dev/devicelab/manifest.yaml'; |
| final Uri url = Uri.https('raw.githubusercontent.com', path); |
| |
| final HttpClient client = httpClientProvider(); |
| try { |
| for (int attempt = 0; attempt < 3; attempt++) { |
| final HttpClientRequest clientRequest = await client.getUrl(url); |
| |
| try { |
| final HttpClientResponse clientResponse = await clientRequest.close(); |
| final int status = clientResponse.statusCode; |
| |
| if (status == HttpStatus.ok) { |
| final String content = |
| await utf8.decoder.bind(clientResponse).join(); |
| return loadYaml(content) as YamlMap; |
| } else { |
| log.warning( |
| 'Attempt to download manifest.yaml failed (HTTP $status)'); |
| } |
| } catch (error, stackTrace) { |
| log.error( |
| 'Attempt to download manifest.yaml failed:\n$error\n$stackTrace'); |
| } |
| |
| await Future<void>.delayed(gitHubBackoffCalculator(attempt)); |
| } |
| } finally { |
| client.close(force: true); |
| } |
| |
| log.error('GitHub not responding; giving up'); |
| response.headers.set(HttpHeaders.retryAfterHeader, '120'); |
| throw const HttpStatusException( |
| HttpStatus.serviceUnavailable, 'GitHub not responding'); |
| } |
| } |