blob: 02602f27b0f50ab7537f914f94387e84e8f1af9b [file] [log] [blame]
// 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');
}
}