Schedule luci builds using CheckSuite and CheckRun events. (#828)
* Schedule luci builds using CheckSuite and CheckRun events.
This is required to add functionality to re-trigger tasks on failure.
Bug:
https://github.com/flutter/flutter/issues/56422
* Remove unused variable.
* Address code review comments.
* Adds docs explaining the retries functionality.
* Reformat comment as todo.
diff --git a/app_dart/lib/src/request_handlers/github_webhook.dart b/app_dart/lib/src/request_handlers/github_webhook.dart
index 89480d7..76f8367 100644
--- a/app_dart/lib/src/request_handlers/github_webhook.dart
+++ b/app_dart/lib/src/request_handlers/github_webhook.dart
@@ -110,7 +110,7 @@
await _checkForGoldenTriage(pullRequestEvent);
} else {
await luciBuildService.cancelBuilds(
- pr.head.repo.name,
+ pullRequestEvent.repository.slug(),
pr.number,
pr.head.sha,
'Pull request closed',
@@ -127,19 +127,19 @@
case 'reopened':
// These cases should trigger LUCI jobs.
await _checkForLabelsAndTests(pullRequestEvent);
- await _scheduleIfMergeable(pr);
+ await _scheduleIfMergeable(pullRequestEvent);
break;
case 'labeled':
// This should only trigger a LUCI job for flutter/flutter right now,
// since it is in the needsCQLabelList.
if (kNeedsCQLabelList.contains(pr.base.repo.fullName.toLowerCase())) {
- await _scheduleIfMergeable(pr);
+ await _scheduleIfMergeable(pullRequestEvent);
}
break;
case 'synchronize':
// This indicates the PR has new commits. We need to cancel old jobs
// and schedule new ones.
- await _scheduleIfMergeable(pr);
+ await _scheduleIfMergeable(pullRequestEvent);
break;
case 'unlabeled':
// Cancel the jobs if someone removed the label on a repo that needs
@@ -149,7 +149,7 @@
}
if (!await _checkForCqLabel(pr.labels)) {
await luciBuildService.cancelBuilds(
- pr.head.repo.name,
+ pullRequestEvent.repository.slug(),
pr.number,
pr.head.sha,
'Tryjobs canceled (label removed)',
@@ -173,11 +173,11 @@
/// without a details link. Once the test starts running then the state is set
/// to "pending" with a details link pointing to the build in LUCI infrastructure.
Future<void> _scheduleIfMergeable(
- PullRequest pr,
+ PullRequestEvent pullRequestEvent,
) async {
// The mergeable flag may be null. False indicates there's a merge conflict,
// null indicates unknown. Err on the side of allowing the job to run.
-
+ final PullRequest pr = pullRequestEvent.pullRequest;
// For flutter/flutter tests need to be optimized before enforcing CQ.
if (kNeedsCQLabelList.contains(pr.base.repo.fullName.toLowerCase())) {
if (!await _checkForCqLabel(pr.labels)) {
@@ -187,15 +187,15 @@
// Always cancel running builds so we don't ever schedule duplicates.
await luciBuildService.cancelBuilds(
- pr.head.repo.name,
+ pullRequestEvent.repository.slug(),
pr.number,
pr.head.sha,
'Newer commit available',
);
await luciBuildService.scheduleBuilds(
+ slug: pullRequestEvent.repository.slug(),
prNumber: pr.number,
commitSha: pr.head.sha,
- repositoryName: pr.head.repo.name,
);
await githubStatusService.setBuildsPendingStatus(
pr.number, pr.head.sha, pr.head.repo.slug());
diff --git a/app_dart/lib/src/request_handlers/luci_status.dart b/app_dart/lib/src/request_handlers/luci_status.dart
index ad1502d..a1444ee 100644
--- a/app_dart/lib/src/request_handlers/luci_status.dart
+++ b/app_dart/lib/src/request_handlers/luci_status.dart
@@ -72,11 +72,9 @@
final PushMessageEnvelope envelope = PushMessageEnvelope.fromJson(
json.decode(requestString) as Map<String, dynamic>,
);
- final BuildPushMessage buildMessage = BuildPushMessage.fromJson(
+ final BuildPushMessage buildPushMessage = BuildPushMessage.fromJson(
json.decode(envelope.message.data) as Map<String, dynamic>);
- final Build build = buildMessage.build;
- final Map<String, dynamic> userData =
- jsonDecode(buildMessage.userData) as Map<String, dynamic>;
+ final Build build = buildPushMessage.build;
final String builderName = build.tagsByName('builder').single;
final RepositorySlug slug = await config.repoNameForBuilder(builderName);
@@ -91,15 +89,13 @@
.tagsByName('buildset')
.firstWhere((String tag) => tag.startsWith(shaPrefix))
.substring(shaPrefix.length);
- log.debug('Setting status: ${buildMessage.toJson()} for $builderName');
- switch (buildMessage.build.status) {
+ log.debug('Setting status: ${buildPushMessage.toJson()} for $builderName');
+ switch (buildPushMessage.build.status) {
case Status.completed:
- await _rescheduleOrMarkCompleted(
+ await _markCompleted(
sha: sha,
builderName: builderName,
build: build,
- retries: userData['retries'] as int,
- luciBuildService: luciBuildService,
githubStatusService: githubStatusService,
slug: slug,
);
@@ -120,50 +116,18 @@
return Body.empty;
}
- /// Reschedules jobs that failed for infra reasons up to
- /// [CocoonConfig.luciTryInfraFailureRetries] times, and updates statuses on
- /// GitHub for all other cases.
- Future<void> _rescheduleOrMarkCompleted({
+ /// Updates the github status using the push_message [build] sent by LUCI
+ /// as a pub/sub message.
+ Future<void> _markCompleted({
@required String sha,
@required String builderName,
@required Build build,
- @required int retries,
- @required LuciBuildService luciBuildService,
@required GithubStatusService githubStatusService,
@required RepositorySlug slug,
}) async {
assert(sha != null);
assert(builderName != null);
assert(build != null);
- if (build.result == Result.failure) {
- switch (build.failureReason) {
- case FailureReason.buildbucketFailure:
- case FailureReason.infraFailure:
- log.info('Retrying: $builderName for $sha');
- final bool rescheduled = await luciBuildService.rescheduleBuild(
- commitSha: sha,
- builderName: builderName,
- build: build,
- retries: retries,
- );
- if (rescheduled) {
- final bool success = await githubStatusService.setPendingStatus(
- ref: sha,
- builderName: builderName,
- buildUrl: '',
- slug: slug,
- );
- if (!success) {
- log.warning('Failed to set status for $builderName');
- }
- return;
- }
- break;
- case FailureReason.invalidBuildDefinition:
- case FailureReason.buildFailure:
- break;
- }
- }
await githubStatusService.setCompletedStatus(
ref: sha,
builderName: builderName,
diff --git a/app_dart/lib/src/service/github_status_service.dart b/app_dart/lib/src/service/github_status_service.dart
index a194baa..c468a55 100644
--- a/app_dart/lib/src/service/github_status_service.dart
+++ b/app_dart/lib/src/service/github_status_service.dart
@@ -27,9 +27,8 @@
) async {
final GitHub gitHubClient =
await config.createGitHubClient(slug.owner, slug.name);
- final String repositoryName = slug.name;
final Map<String, bb.Build> builds = await luciBuildService
- .buildsForRepositoryAndPr(repositoryName, prNumber, commitSha);
+ .buildsForRepositoryAndPr(slug, prNumber, commitSha);
final List<String> builderNames = config.luciTryBuilders
.map((Map<String, dynamic> entry) => entry['name'] as String)
.toList();
diff --git a/app_dart/lib/src/service/luci_build_service.dart b/app_dart/lib/src/service/luci_build_service.dart
index cbbb82e..a7e466c 100644
--- a/app_dart/lib/src/service/luci_build_service.dart
+++ b/app_dart/lib/src/service/luci_build_service.dart
@@ -4,7 +4,11 @@
import 'dart:convert';
+import 'package:appengine/appengine.dart';
+import 'package:cocoon_service/src/foundation/github_checks_util.dart';
+import 'package:cocoon_service/src/model/github/checks.dart';
import 'package:cocoon_service/src/request_handling/exceptions.dart';
+import 'package:github/github.dart' as github;
import 'package:meta/meta.dart';
import '../../cocoon_service.dart';
@@ -17,22 +21,34 @@
/// and cancel builds for github repos. It uses [config.luciTryBuilders] to
/// get the list of available builders.
class LuciBuildService {
- LuciBuildService(this.config, this.buildBucketClient, this.serviceAccount);
+ LuciBuildService(this.config, this.buildBucketClient, this.serviceAccount,
+ {GithubChecksUtil githubChecksUtil})
+ : githubChecksUtil = githubChecksUtil ?? const GithubChecksUtil();
BuildBucketClient buildBucketClient;
Config config;
ServiceAccountInfo serviceAccount;
+ Logging log;
+ GithubChecksUtil githubChecksUtil;
+
static const Set<Status> failStatusSet = <Status>{
Status.canceled,
Status.failure,
Status.infraFailure
};
- /// Returns a map of the BuildBucket builds for a given [repositoryName]
+ /// Sets the appengine [log] used by this class to log debug and error
+ /// messages. This method has to be called before any other method in this
+ /// class.
+ void setLogger(Logging log) {
+ this.log = log;
+ }
+
+ /// Returns a map of the BuildBucket builds for a given Github [slug]
/// [prNumber] and [commitSha] using the [builderName] as key and [Build]
/// as value.
Future<Map<String, Build>> buildsForRepositoryAndPr(
- String repositoryName,
+ github.RepositorySlug slug,
int prNumber,
String commitSha,
) async {
@@ -49,7 +65,7 @@
tags: <String, List<String>>{
'buildset': <String>['pr/git/$prNumber'],
'github_link': <String>[
- 'https://github.com/flutter/$repositoryName/pull/$prNumber'
+ 'https://github.com/${slug.owner}/${slug.name}/pull/$prNumber'
],
'user_agent': const <String>['flutter-cocoon'],
},
@@ -81,44 +97,48 @@
}
/// Schedules BuildBucket builds for a given [prNumber], [commitSha]
- /// and repositoryName. It returns [true] if it was able to schedule
- /// build or [false] otherwise.
+ /// and Github [slug].
Future<bool> scheduleBuilds({
@required int prNumber,
@required String commitSha,
- @required String repositoryName,
+ @required github.RepositorySlug slug,
+ CheckSuiteEvent checkSuiteEvent,
}) async {
assert(prNumber != null);
assert(commitSha != null);
- assert(repositoryName != null);
- if (!config.githubPresubmitSupportedRepo(repositoryName)) {
+ assert(slug != null);
+ final github.GitHub githubClient =
+ await config.createGitHubClient(slug.owner, slug.name);
+ if (!config.githubPresubmitSupportedRepo(slug.name)) {
throw BadRequestException(
- 'Repository $repositoryName is not supported by this service.');
+ 'Repository ${slug.name} is not supported by this service.');
}
final Map<String, Build> builds = await buildsForRepositoryAndPr(
- repositoryName,
+ slug,
prNumber,
commitSha,
);
-
if (builds != null &&
builds.values.any((Build build) {
return build.status == Status.scheduled ||
build.status == Status.started;
})) {
+ log.error(
+ 'Either builds are empty or they are already scheduled or started. '
+ 'PR: $prNumber, Commit: $commitSha, Owner: ${slug.owner} '
+ 'Repo: ${slug.name}');
return false;
}
final List<Map<String, dynamic>> builders = config.luciTryBuilders;
final List<String> builderNames = builders
- .where(
- (Map<String, dynamic> builder) => builder['repo'] == repositoryName)
+ .where((Map<String, dynamic> builder) => builder['repo'] == slug.name)
.map<String>(
(Map<String, dynamic> builder) => builder['name'] as String)
.toList();
if (builderNames.isEmpty) {
- throw InternalServerError('$repositoryName does not have any builders');
+ throw InternalServerError('${slug.name} does not have any builders');
}
final List<Request> requests = <Request>[];
@@ -128,6 +148,20 @@
bucket: 'try',
builder: builder,
);
+ final Map<String, dynamic> userData = <String, dynamic>{'retries': 0};
+ if (checkSuiteEvent != null) {
+ final github.CheckRun checkRun =
+ await githubClient.checks.checkRuns.createCheckRun(
+ checkSuiteEvent.repository.slug(),
+ name: builder,
+ headSha: commitSha,
+ );
+ userData['check_suite_id'] = checkSuiteEvent.checkSuite.id;
+ userData['check_run_id'] = checkRun.id;
+ userData['repo_owner'] = slug.owner;
+ userData['repo_name'] = slug.name;
+ userData['user_agent'] = 'flutter-cocoon';
+ }
requests.add(
Request(
scheduleBuild: ScheduleBuildRequest(
@@ -136,11 +170,11 @@
'buildset': <String>['pr/git/$prNumber', 'sha/git/$commitSha'],
'user_agent': const <String>['flutter-cocoon'],
'github_link': <String>[
- 'https://github.com/flutter/$repositoryName/pull/$prNumber'
+ 'https://github.com/${slug.owner}/${slug.name}/pull/$prNumber'
],
},
properties: <String, String>{
- 'git_url': 'https://github.com/flutter/$repositoryName',
+ 'git_url': 'https://github.com/${slug.owner}/${slug.name}',
'git_ref': 'refs/pull/$prNumber/head',
},
notify: NotificationConfig(
@@ -159,14 +193,14 @@
/// Cancels all the current builds for a given [repositoryName], [prNumber]
/// and [commitSha] adding a message for the cancelation reason.
- Future<void> cancelBuilds(String repositoryName, int prNumber,
+ Future<void> cancelBuilds(github.RepositorySlug slug, int prNumber,
String commitSha, String reason) async {
- if (!config.githubPresubmitSupportedRepo(repositoryName)) {
+ if (!config.githubPresubmitSupportedRepo(slug.name)) {
throw BadRequestException(
- 'This service does not support repository $repositoryName.');
+ 'This service does not support repository ${slug.name}');
}
final Map<String, Build> builds = await buildsForRepositoryAndPr(
- repositoryName,
+ slug,
prNumber,
commitSha,
);
@@ -192,14 +226,20 @@
/// Gets a list of failed builds for a given [repositoryName], [prNumber] and
/// [commitSha].
Future<List<Build>> failedBuilds(
- String repositoryName,
+ github.RepositorySlug slug,
int prNumber,
String commitSha,
) async {
final Map<String, Build> builds =
- await buildsForRepositoryAndPr(repositoryName, prNumber, commitSha);
+ await buildsForRepositoryAndPr(slug, prNumber, commitSha);
+ final List<String> builderNames = config.luciTryBuilders
+ .map((Map<String, dynamic> entry) => entry['name'] as String)
+ .toList();
+ // Return only builds that exist in the configuration file.
return builds.values
- .where((Build build) => failStatusSet.contains(build.status))
+ .where((Build build) =>
+ failStatusSet.contains(build.status) &&
+ builderNames.contains(build.builderId.builder))
.toList();
}
@@ -214,7 +254,7 @@
Future<bool> rescheduleBuild({
@required String commitSha,
@required String builderName,
- @required push_message.Build build,
+ @required push_message.BuildPushMessage buildPushMessage,
@required int retries,
}) async {
if (retries >= config.luciTryInfraFailureRetries) {
@@ -224,27 +264,135 @@
// Ensure we are using V2 bucket name istead of V1.
// V1 bucket name is "luci.flutter.prod" while the api
// is expecting just the last part after "."(prod).
- final String bucketName = build.bucket.split('.').last;
+ final String bucketName = buildPushMessage.build.bucket.split('.').last;
+ final Map<String, dynamic> userData =
+ jsonDecode(buildPushMessage.userData) as Map<String, dynamic>;
+ userData['retries'] += 1;
await buildBucketClient.scheduleBuild(ScheduleBuildRequest(
builderId: BuilderId(
- project: build.project,
+ project: buildPushMessage.build.project,
bucket: bucketName,
builder: builderName,
),
tags: <String, List<String>>{
- 'buildset': build.tagsByName('buildset'),
- 'user_agent': build.tagsByName('user_agent'),
- 'github_link': build.tagsByName('github_link'),
+ 'buildset': buildPushMessage.build.tagsByName('buildset'),
+ 'user_agent': buildPushMessage.build.tagsByName('user_agent'),
+ 'github_link': buildPushMessage.build.tagsByName('github_link'),
},
- properties: (build.buildParameters['properties'] as Map<String, dynamic>)
+ properties: (buildPushMessage.build.buildParameters['properties']
+ as Map<String, dynamic>)
.cast<String, String>(),
notify: NotificationConfig(
pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds',
- userData: json.encode(<String, dynamic>{
- 'retries': retries + 1,
- }),
+ userData: json.encode(userData),
),
));
return true;
}
+
+ /// Sends a [BuildBucket.scheduleBuild] request using [CheckRunEvent]. It
+ /// returns [true] if it is able to send the scheduleBuildRequest or [false]
+ /// if not.
+ Future<bool> rescheduleUsingCheckRunEvent(CheckRunEvent checkRunEvent) async {
+ final github.RepositorySlug slug = checkRunEvent.repository.slug();
+ final Map<String, dynamic> userData = <String, dynamic>{};
+ final github.PullRequest pr = checkRunEvent.checkRun.pullRequests[0];
+ final github.GitHub gitHubClient =
+ await config.createGitHubClient(slug.owner, slug.name);
+ final github.CheckRun githubCheckRun =
+ await githubChecksUtil.createCheckRun(
+ gitHubClient,
+ slug,
+ checkRunEvent.checkRun.name,
+ pr.head.sha,
+ );
+ userData['check_suite_id'] = checkRunEvent.checkRun.checkSuite.id;
+ userData['check_run_id'] = githubCheckRun.id;
+ userData['repo_owner'] = slug.owner;
+ userData['repo_name'] = slug.name;
+ userData['user_agent'] = 'flutter-cocoon';
+ userData['retries'] = 1;
+ await buildBucketClient.scheduleBuild(ScheduleBuildRequest(
+ builderId: BuilderId(
+ project: 'flutter',
+ bucket: 'try',
+ builder: checkRunEvent.checkRun.name,
+ ),
+ tags: <String, List<String>>{
+ 'buildset': <String>['pr/git/${pr.number}', 'sha/git/${pr.head.sha}'],
+ 'user_agent': const <String>['flutter-cocoon'],
+ 'github_link': <String>[
+ 'https://github.com/${slug.owner}/${slug.name}/pull/${pr.number}'
+ ],
+ },
+ properties: <String, String>{
+ 'git_url': 'https://github.com/${slug.owner}/${slug.name}',
+ 'git_ref': 'refs/pull/${pr.number}/head',
+ },
+ notify: NotificationConfig(
+ pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds',
+ userData: json.encode(userData),
+ ),
+ ));
+ return true;
+ }
+
+ /// Sends a [BuildBucket.scheduleBuild] request using [CheckSuiteEvent],
+ /// [gitgub.CheckRun] and [RepositorySlug]. It returns [true] if it is able to
+ /// send the scheduleBuildRequest or [false] if not.
+ Future<bool> rescheduleUsingCheckSuiteEvent(
+ CheckSuiteEvent checkSuiteEvent, github.CheckRun checkRun) async {
+ final github.RepositorySlug slug = checkSuiteEvent.repository.slug();
+ final Map<String, dynamic> userData = <String, dynamic>{};
+ final github.PullRequest pr = checkSuiteEvent.checkSuite.pullRequests[0];
+ final github.GitHub gitHubClient =
+ await config.createGitHubClient(slug.owner, slug.name);
+ final github.CheckRun githubCheckRun =
+ await githubChecksUtil.createCheckRun(
+ gitHubClient,
+ slug,
+ checkRun.name,
+ pr.head.sha,
+ );
+ userData['check_suite_id'] = checkSuiteEvent.checkSuite.id;
+ userData['check_run_id'] = githubCheckRun.id;
+ userData['repo_owner'] = slug.owner;
+ userData['repo_name'] = slug.name;
+ userData['user_agent'] = 'flutter-cocoon';
+ // Retries were used to auto re-run builds when they failed with infra
+ // failure. Now with github checks api support automated retries won't be
+ // needed anymore and will be removed:
+ // TODO(godofredoc): remove retries https://github.com/flutter/flutter/issues/60942.
+ userData['retries'] = 1;
+ await buildBucketClient.scheduleBuild(ScheduleBuildRequest(
+ builderId: BuilderId(
+ project: 'flutter',
+ bucket: 'try',
+ builder: checkRun.name,
+ ),
+ tags: <String, List<String>>{
+ 'buildset': <String>['pr/git/${pr.number}', 'sha/git/${pr.head.sha}'],
+ 'user_agent': const <String>['flutter-cocoon'],
+ 'github_link': <String>[
+ 'https://github.com/${slug.owner}/${slug.name}/pull/${pr.number}'
+ ],
+ },
+ properties: <String, String>{
+ 'git_url': 'https://github.com/${slug.owner}/${slug.name}',
+ 'git_ref': 'refs/pull/${pr.number}/head',
+ },
+ notify: NotificationConfig(
+ pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds',
+ userData: json.encode(userData),
+ ),
+ ));
+ return true;
+ }
+
+ /// Gets a [buildbucket.Build] using its [id] and passing the additional
+ /// fields to be populated in the response.
+ Future<Build> getBuildById(int id, {String fields}) async {
+ final GetBuildRequest request = GetBuildRequest(id: id, fields: fields);
+ return buildBucketClient.getBuild(request);
+ }
}
diff --git a/app_dart/test/request_handlers/github_webhook_test.dart b/app_dart/test/request_handlers/github_webhook_test.dart
index dc7612f..2fe2648 100644
--- a/app_dart/test/request_handlers/github_webhook_test.dart
+++ b/app_dart/test/request_handlers/github_webhook_test.dart
@@ -1050,9 +1050,9 @@
"sha": "the_head_sha",
"repo": {
"name": "cocoon",
- "full_name": "digiter/cocoon",
+ "full_name": "flutter/cocoon",
"owner": {
- "login": "abc"
+ "login": "flutter"
}
}
}
@@ -1075,6 +1075,11 @@
final String hmac = getHmac(body, key);
request.headers.set('X-Hub-Signature', 'sha1=$hmac');
+ config.luciTryBuildersValue = (json.decode(
+ '[{"name": "Cocoon", "repo": "cocoon"},{"name": "Linux", "repo": "flutter"}, {"name": "Mac", "repo": "flutter"}]')
+ as List<dynamic>)
+ .cast<Map<String, dynamic>>();
+
await tester.post(webhook);
if (never) {
diff --git a/app_dart/test/request_handlers/luci_status_test.dart b/app_dart/test/request_handlers/luci_status_test.dart
index 86f6a8f..d82400c 100644
--- a/app_dart/test/request_handlers/luci_status_test.dart
+++ b/app_dart/test/request_handlers/luci_status_test.dart
@@ -8,7 +8,6 @@
import 'package:cocoon_service/cocoon_service.dart';
import 'package:cocoon_service/src/model/appengine/service_account_info.dart';
-import 'package:cocoon_service/src/model/luci/buildbucket.dart';
import 'package:cocoon_service/src/request_handling/exceptions.dart';
import 'package:http/testing.dart' as http_test;
import 'package:http/http.dart' as http;
@@ -255,70 +254,6 @@
);
});
- test('Reschedules an infra failure', () async {
- request.bodyBytes = utf8.encode(pushMessageJson('COMPLETED',
- builderName: 'Linux',
- result: 'FAILURE',
- failureReason: 'INFRA_FAILURE')) as Uint8List;
- request.headers.add(HttpHeaders.authorizationHeader, authHeader);
- mockGitHubClient = MockGitHub();
- config.githubClient = mockGitHubClient;
- final List<RepositoryStatus> repositoryStatuses = <RepositoryStatus>[
- RepositoryStatus()
- ..context = 'Linux'
- ..state = 'failure',
- ];
- when(mockGitHubClient.repositories).thenReturn(mockRepositoriesService);
- when(mockRepositoriesService.listStatuses(any, ref)).thenAnswer((_) {
- return Stream<RepositoryStatus>.fromIterable(repositoryStatuses);
- });
-
- await tester.post(handler);
- expect(
- jsonEncode(
- verify(buildBucketClient.scheduleBuild(captureAny))
- .captured
- .single
- .toJson(),
- ),
- jsonEncode(
- ScheduleBuildRequest(
- builderId: const BuilderId(
- project: 'flutter',
- bucket: 'prod',
- builder: 'Linux',
- ),
- tags: const <String, List<String>>{
- 'buildset': <String>['pr/git/37647', 'sha/git/$ref'],
- 'user_agent': <String>['flutter-cocoon'],
- 'github_link': <String>[
- 'https://github.com/flutter/flutter/pull/37647'
- ],
- },
- properties: const <String, String>{
- 'git_ref': 'refs/pull/37647/head',
- 'git_url': 'https://github.com/flutter/flutter',
- },
- notify: NotificationConfig(
- pubsubTopic: 'projects/flutter-dashboard/topics/luci-builds',
- userData: json.encode(<String, dynamic>{
- 'retries': 1,
- }),
- ),
- ).toJson(),
- ),
- );
- expect(
- verify(mockRepositoriesService.createStatus(
- RepositorySlug('flutter', 'flutter'),
- ref,
- captureAny,
- )).captured.single.toJson(),
- jsonDecode(
- '{"state":"pending","target_url":"","description":"Flutter LUCI Build: Linux","context":"Linux"}'),
- );
- });
-
test('Does not schedule after too many retries with infra failure', () async {
request.bodyBytes = utf8.encode(pushMessageJson('COMPLETED',
builderName: 'Linux',
diff --git a/app_dart/test/service/luci_build_service_test.dart b/app_dart/test/service/luci_build_service_test.dart
index 9473abb..3861944 100644
--- a/app_dart/test/service/luci_build_service_test.dart
+++ b/app_dart/test/service/luci_build_service_test.dart
@@ -11,10 +11,12 @@
as push_message;
import 'package:cocoon_service/src/request_handling/exceptions.dart';
import 'package:cocoon_service/src/service/luci_build_service.dart';
+import 'package:github/github.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import '../src/datastore/fake_cocoon_config.dart';
+import '../src/request_handling/fake_logging.dart';
import '../src/utilities/mocks.dart';
import '../src/utilities/push_message.dart';
@@ -23,6 +25,7 @@
FakeConfig config;
MockBuildBucketClient mockBuildBucketClient;
LuciBuildService service;
+ RepositorySlug slug;
group('buildsForRepositoryAndPr', () {
const Build macBuild = Build(
id: 999,
@@ -50,6 +53,7 @@
mockBuildBucketClient = MockBuildBucketClient();
service =
LuciBuildService(config, mockBuildBucketClient, serviceAccountInfo);
+ slug = RepositorySlug('flutter', 'cocoon');
});
test('Empty responses are handled correctly', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
@@ -64,7 +68,7 @@
);
});
final Map<String, Build> builds =
- await service.buildsForRepositoryAndPr('cocoon', 1, 'abcd');
+ await service.buildsForRepositoryAndPr(slug, 1, 'abcd');
expect(builds.keys, isEmpty);
});
@@ -86,7 +90,7 @@
);
});
final Map<String, Build> builds =
- await service.buildsForRepositoryAndPr('cocoon', 1, 'abcd');
+ await service.buildsForRepositoryAndPr(slug, 1, 'abcd');
expect(builds,
equals(<String, Build>{'Mac': macBuild, 'Linux': linuxBuild}));
});
@@ -98,6 +102,8 @@
mockBuildBucketClient = MockBuildBucketClient();
service =
LuciBuildService(config, mockBuildBucketClient, serviceAccountInfo);
+ service.setLogger(FakeLogging());
+ slug = RepositorySlug('flutter', 'cocoon');
});
test('try to schedule builds already started', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
@@ -124,7 +130,7 @@
final bool result = await service.scheduleBuilds(
prNumber: 1,
commitSha: 'abc',
- repositoryName: 'cocoon',
+ slug: slug,
);
expect(result, isFalse);
});
@@ -153,7 +159,7 @@
final bool result = await service.scheduleBuilds(
prNumber: 1,
commitSha: 'abc',
- repositoryName: 'cocoon',
+ slug: slug,
);
expect(result, isFalse);
});
@@ -170,16 +176,17 @@
final bool result = await service.scheduleBuilds(
prNumber: 1,
commitSha: 'abc',
- repositoryName: 'cocoon',
+ slug: slug,
);
expect(result, isTrue);
});
test('Try to schedule build on a unsupported repo', () async {
+ slug = RepositorySlug('flutter', 'notsupported');
expect(
() async => await service.scheduleBuilds(
prNumber: 1,
commitSha: 'abc',
- repositoryName: 'notsupported',
+ slug: slug,
),
throwsA(const TypeMatcher<BadRequestException>()));
});
@@ -192,6 +199,7 @@
mockBuildBucketClient = MockBuildBucketClient();
service =
LuciBuildService(config, mockBuildBucketClient, serviceAccountInfo);
+ slug = RepositorySlug('flutter', 'cocoon');
});
test('Cancel builds when build list is empty', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
@@ -199,7 +207,7 @@
responses: <Response>[],
);
});
- await service.cancelBuilds('cocoon', 1, 'abc', 'new builds');
+ await service.cancelBuilds(slug, 1, 'abc', 'new builds');
verify(mockBuildBucketClient.batch(any)).called(1);
});
test('Cancel builds that are scheduled', () async {
@@ -221,7 +229,7 @@
],
);
});
- await service.cancelBuilds('cocoon', 1, 'abc', 'new builds');
+ await service.cancelBuilds(slug, 1, 'abc', 'new builds');
expect(
verify(mockBuildBucketClient.batch(captureAny))
.captured[1]
@@ -231,9 +239,10 @@
json.decode('{"id": "998", "summaryMarkdown": "new builds"}'));
});
test('Cancel builds from unsuported repo', () async {
+ slug = RepositorySlug('flutter', 'notsupported');
expect(
() async => await service.cancelBuilds(
- 'notsupported',
+ slug,
1,
'abc',
'new builds',
@@ -249,6 +258,7 @@
mockBuildBucketClient = MockBuildBucketClient();
service =
LuciBuildService(config, mockBuildBucketClient, serviceAccountInfo);
+ slug = RepositorySlug('flutter', 'cocoon');
});
test('Failed builds from an empty list', () async {
when(mockBuildBucketClient.batch(any)).thenAnswer((_) async {
@@ -256,7 +266,8 @@
responses: <Response>[],
);
});
- final List<Build> result = await service.failedBuilds('cocoon', 1, 'abc');
+ config.luciTryBuildersValue = <Map<String, dynamic>>[];
+ final List<Build> result = await service.failedBuilds(slug, 1, 'abc');
expect(result, isEmpty);
});
test('Failed builds from a list of builds with failures', () async {
@@ -278,12 +289,16 @@
],
);
});
- final List<Build> result = await service.failedBuilds('cocoon', 1, 'abc');
+ config.luciTryBuildersValue = (json.decode(
+ '[{"name": "Linux", "repo": "flutter", "taskName": "linux_bot"}]')
+ as List<dynamic>)
+ .cast<Map<String, dynamic>>();
+ final List<Build> result = await service.failedBuilds(slug, 1, 'abc');
expect(result, hasLength(1));
});
});
group('rescheduleBuild', () {
- push_message.Build build;
+ push_message.BuildPushMessage buildPushMessage;
setUp(() {
serviceAccountInfo = const ServiceAccountInfo(email: 'abc@abcd.com');
@@ -296,18 +311,24 @@
'COMPLETED',
result: 'FAILURE',
builderName: 'Linux Host Engine',
- ))['build'] as Map<String, dynamic>;
- build = push_message.Build.fromJson(json);
+ )) as Map<String, dynamic>;
+ buildPushMessage = push_message.BuildPushMessage.fromJson(json);
});
test('Reschedule an existing build', () async {
final bool rescheduled = await service.rescheduleBuild(
- commitSha: 'abc', builderName: 'mybuild', build: build, retries: 1);
+ commitSha: 'abc',
+ builderName: 'mybuild',
+ buildPushMessage: buildPushMessage,
+ retries: 1);
expect(rescheduled, isTrue);
verify(mockBuildBucketClient.scheduleBuild(any)).called(1);
});
test('Reschedule after too many retries', () async {
final bool rescheduled = await service.rescheduleBuild(
- commitSha: 'abc', builderName: 'mybuild', build: build, retries: 3);
+ commitSha: 'abc',
+ builderName: 'mybuild',
+ buildPushMessage: buildPushMessage,
+ retries: 3);
expect(rescheduled, isFalse);
verifyNever(mockBuildBucketClient.scheduleBuild(any));
});