Skip to content

Commit

Permalink
Add BuildBucket v2 migration service file changes (Part 3) (flutter#3665
Browse files Browse the repository at this point in the history
)

Adds the service class changes to support the v2 build bucket api migration. These classes support the rest endpoint classes and facilitate scheduling and are the main changes needed for scheduling and requesting builds vi the v2 api.

Includes files from flutter#3664.

*List which issues are fixed by this PR. You must list at least one issue.*
Part 3 of flutter/flutter#135934

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
  • Loading branch information
ricardoamador authored Apr 23, 2024
1 parent 0abc6c9 commit 2eeeddd
Show file tree
Hide file tree
Showing 19 changed files with 9,709 additions and 1,900 deletions.
28 changes: 28 additions & 0 deletions app_dart/bin/gae_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import 'dart:io';
import 'package:appengine/appengine.dart';
import 'package:cocoon_service/cocoon_service.dart';
import 'package:cocoon_service/server.dart';
import 'package:cocoon_service/src/service/build_bucket_v2_client.dart';
import 'package:cocoon_service/src/service/commit_service.dart';
// import 'package:cocoon_service/src/service/github_checks_service_v2.dart';
// import 'package:cocoon_service/src/service/luci_build_service_v2.dart';
// import 'package:cocoon_service/src/service/scheduler_v2.dart';
import 'package:gcloud/db.dart';

Future<void> main() async {
Expand All @@ -18,23 +22,40 @@ Future<void> main() async {
final Config config = Config(dbService, cache);
final AuthenticationProvider authProvider = AuthenticationProvider(config: config);
final AuthenticationProvider swarmingAuthProvider = SwarmingAuthenticationProvider(config: config);

final BuildBucketClient buildBucketClient = BuildBucketClient(
accessTokenService: AccessTokenService.defaultProvider(config),
);

final BuildBucketV2Client buildBucketV2Client = BuildBucketV2Client(
accessTokenService: AccessTokenService.defaultProvider(config),
);

/// LUCI service class to communicate with buildBucket service.
final LuciBuildService luciBuildService = LuciBuildService(
config: config,
cache: cache,
buildBucketClient: buildBucketClient,
buildBucketV2Client: buildBucketV2Client,
pubsub: const PubSub(),
);

// final LuciBuildServiceV2 luciBuildServiceV2 = LuciBuildServiceV2(
// config: config,
// cache: cache,
// buildBucketV2Client: buildBucketV2Client,
// pubsub: const PubSub(),
// );

/// Github checks api service used to provide luci test execution status on the Github UI.
final GithubChecksService githubChecksService = GithubChecksService(
config,
);

// final GithubChecksServiceV2 githubChecksServiceV2 = GithubChecksServiceV2(
// config,
// );

// Gerrit service class to communicate with GoB.
final GerritService gerritService = GerritService(config: config);

Expand All @@ -46,6 +67,13 @@ Future<void> main() async {
luciBuildService: luciBuildService,
);

// final SchedulerV2 schedulerV2 = SchedulerV2(
// cache: cache,
// config: config,
// githubChecksService: githubChecksServiceV2,
// luciBuildService: luciBuildServiceV2,
// );

final BranchService branchService = BranchService(
config: config,
gerritService: gerritService,
Expand Down
28 changes: 28 additions & 0 deletions app_dart/bin/local_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import 'package:appengine/appengine.dart';
import 'package:cocoon_service/cocoon_service.dart';
import 'package:cocoon_service/server.dart';
import 'package:cocoon_service/src/model/appengine/cocoon_config.dart';
import 'package:cocoon_service/src/service/build_bucket_v2_client.dart';
import 'package:cocoon_service/src/service/commit_service.dart';
import 'package:cocoon_service/src/service/datastore.dart';
// import 'package:cocoon_service/src/service/github_checks_service_v2.dart';
// import 'package:cocoon_service/src/service/luci_build_service_v2.dart';
// import 'package:cocoon_service/src/service/scheduler_v2.dart';
import 'package:gcloud/db.dart';

import '../test/src/datastore/fake_datastore.dart';
Expand All @@ -25,23 +29,40 @@ Future<void> main() async {
final Config config = Config(dbService, cache);
final AuthenticationProvider authProvider = AuthenticationProvider(config: config);
final AuthenticationProvider swarmingAuthProvider = SwarmingAuthenticationProvider(config: config);

final BuildBucketClient buildBucketClient = BuildBucketClient(
accessTokenService: AccessTokenService.defaultProvider(config),
);

final BuildBucketV2Client buildBucketV2Client = BuildBucketV2Client(
accessTokenService: AccessTokenService.defaultProvider(config),
);

/// LUCI service class to communicate with buildBucket service.
final LuciBuildService luciBuildService = LuciBuildService(
config: config,
cache: cache,
buildBucketClient: buildBucketClient,
buildBucketV2Client: buildBucketV2Client,
pubsub: const PubSub(),
);

// final LuciBuildServiceV2 luciBuildServiceV2 = LuciBuildServiceV2(
// config: config,
// cache: cache,
// buildBucketV2Client: buildBucketV2Client,
// pubsub: const PubSub(),
// );

/// Github checks api service used to provide luci test execution status on the Github UI.
final GithubChecksService githubChecksService = GithubChecksService(
config,
);

// final GithubChecksServiceV2 githubChecksServiceV2 = GithubChecksServiceV2(
// config,
// );

// Gerrit service class to communicate with GoB.
final GerritService gerritService = GerritService(config: config);

Expand All @@ -53,6 +74,13 @@ Future<void> main() async {
luciBuildService: luciBuildService,
);

// final SchedulerV2 schedulerV2 = SchedulerV2(
// cache: cache,
// config: config,
// githubChecksService: githubChecksServiceV2,
// luciBuildService: luciBuildServiceV2,
// );

final BranchService branchService = BranchService(
config: config,
gerritService: gerritService,
Expand Down
261 changes: 261 additions & 0 deletions app_dart/lib/src/service/github_checks_service_v2.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// Copyright 2020 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 'package:cocoon_service/src/service/scheduler_v2.dart';
import 'package:github/github.dart' as github;
import 'package:github/hooks.dart';

import '../foundation/github_checks_util.dart';
import 'package:buildbucket/buildbucket_pb.dart' as bbv2;
import 'config.dart';
import 'github_service.dart';
import 'logging.dart';
import 'luci_build_service_v2.dart';

const String kGithubSummary = '''
**[Understanding a LUCI build failure](https://github.com/flutter/flutter/wiki/Understanding-a-LUCI-build-failure)**
''';

final List<bbv2.Status> terminalStatuses = [
bbv2.Status.CANCELED,
bbv2.Status.FAILURE,
bbv2.Status.INFRA_FAILURE,
bbv2.Status.SUCCESS,
];

/// Controls triggering builds and updating their status in the Github UI.
class GithubChecksServiceV2 {
GithubChecksServiceV2(
this.config, {
GithubChecksUtil? githubChecksUtil,
}) : githubChecksUtil = githubChecksUtil ?? const GithubChecksUtil();

Config config;
GithubChecksUtil githubChecksUtil;

static Set<github.CheckRunConclusion> failedStatesSet = <github.CheckRunConclusion>{
github.CheckRunConclusion.cancelled,
github.CheckRunConclusion.failure,
};

/// Takes a [CheckSuiteEvent] and trigger all the relevant builds if this is a
/// new commit or only failed builds if the event was generated by a click on
/// the re-run all button in the Github UI.
/// Relevant API docs:
/// https://docs.github.com/en/rest/reference/checks#create-a-check-suite
/// https://docs.github.com/en/rest/reference/checks#rerequest-a-check-suite
Future<void> handleCheckSuite(
github.PullRequest pullRequest,
CheckSuiteEvent checkSuiteEvent,
SchedulerV2 scheduler,
) async {
switch (checkSuiteEvent.action) {
case 'requested':
// Trigger all try builders.
log.info('Check suite request for pull request ${pullRequest.number}, ${pullRequest.title}');
await scheduler.triggerPresubmitTargets(
pullRequest: pullRequest,
);
break;
case 'rerequested':
log.info('Check suite re-request for pull request ${pullRequest.number}, ${pullRequest.title}');
pullRequest.head = github.PullRequestHead(sha: checkSuiteEvent.checkSuite?.headSha);
return scheduler.retryPresubmitTargets(
pullRequest: pullRequest,
checkSuiteEvent: checkSuiteEvent,
);
}
}

/// Updates the Github build status using a [BuildPushMessage] sent by LUCI in
/// a pub/sub notification.
/// Relevant APIs:
/// https://docs.github.com/en/rest/reference/checks#update-a-check-run
Future<bool> updateCheckStatus({
required bbv2.Build build,
required Map<String, dynamic> userDataMap,
required LuciBuildServiceV2 luciBuildService,
required github.RepositorySlug slug,
bool rescheduled = false,
}) async {
if (userDataMap.isEmpty) {
return false;
}

if (!userDataMap.containsKey('check_run_id') ||
!userDataMap.containsKey('repo_owner') ||
!userDataMap.containsKey('repo_name')) {
log.severe(
'UserData did not contain check_run_id,'
'repo_owner, or repo_name: $userDataMap',
);
return false;
}

github.CheckRunStatus status = statusForResult(build.status);
log.info('status for build ${build.id} is ${status.value}');

// Only `id` and `name` in the CheckRun are needed.
// Instead of making an API call to get the details of each check run, we
// generate the check run with only necessary info.
final github.CheckRun checkRun = github.CheckRun.fromJson({
'id': userDataMap['check_run_id'] as int?,
'status': status,
'check_suite': const {'id': null},
'started_at': build.startTime.toDateTime().toString(),
'conclusion': null,
'name': build.builder.builder,
});

github.CheckRunConclusion? conclusion =
(terminalStatuses.contains(build.status)) ? conclusionForResult(build.status) : null;
log.info('conclusion for build ${build.id} is ${(conclusion != null) ? conclusion.value : null}');

final String url = 'https://cr-buildbucket.appspot.com/build/${build.id}';
github.CheckRunOutput? output;
// If status has completed with failure then provide more details.
if (taskFailed(build.status)) {
log.info('failed presubmit task, ${build.id} has failed, status = ${build.status.toString()}');
if (rescheduled) {
status = github.CheckRunStatus.queued;
conclusion = null;
output = github.CheckRunOutput(
title: checkRun.name!,
summary: 'Note: this is an auto rerun. The timestamp above is based on the first attempt of this check run.',
);
} else {
// summaryMarkdown should be present
final bbv2.Build buildbucketBuild = await luciBuildService.getBuildById(
build.id,
buildMask: bbv2.BuildMask(
// Need to use allFields as there is a bug with fieldMask and summaryMarkdown.
allFields: true,
),
);
output = github.CheckRunOutput(
title: checkRun.name!,
summary: getGithubSummary(buildbucketBuild.summaryMarkdown),
);
log.fine('Updating check run with output: [${output.toJson().toString()}]');
}
}
await githubChecksUtil.updateCheckRun(
config,
slug,
checkRun,
status: status,
conclusion: conclusion,
detailsUrl: url,
output: output,
);
return true;
}

/// Check if task has completed with failure.
bool taskFailed(bbv2.Status status) {
final github.CheckRunStatus checkRunStatus = statusForResult(status);
final github.CheckRunConclusion conclusion = conclusionForResult(status);
return (checkRunStatus == github.CheckRunStatus.completed) && failedStatesSet.contains(conclusion);
}

/// Returns current reschedule attempt.
///
/// It returns 1 if this is the first run, and +1 with each reschedule.
int currentAttempt(final List<bbv2.StringPair> tags) {
final bbv2.StringPair attempt = tags.firstWhere(
(element) => element.key == 'current_attempt',
orElse: () => bbv2.StringPair().createEmptyInstance(),
);
if (!attempt.hasKey()) {
return 1;
} else {
return int.parse(attempt.value);
}
}

/// Appends triage wiki page to `summaryMarkdown` from LUCI build so that people can easily
/// reference from github check run page.
String getGithubSummary(String? summary) {
if (summary == null) {
return '${kGithubSummary}Empty summaryMarkdown';
}
// This is an imposed GitHub limit
const int checkSummaryLimit = 65535;
// This is to give buffer room incase GitHub lowers the amount.
const int checkSummaryBufferLimit = checkSummaryLimit - 10000 - kGithubSummary.length;
// Return the last [checkSummaryBufferLimit] characters as they are likely the most relevant.
if (summary.length > checkSummaryBufferLimit) {
final String truncatedSummary = summary.substring(summary.length - checkSummaryBufferLimit);
summary = '[TRUNCATED...] $truncatedSummary';
}
return '$kGithubSummary$summary';
}

/// Relevant APIs:
/// https://developer.github.com/v3/checks/runs/#check-runs
github.CheckRunConclusion conclusionForResult(bbv2.Status status) {
if (status == bbv2.Status.CANCELED || status == bbv2.Status.FAILURE || status == bbv2.Status.INFRA_FAILURE) {
return github.CheckRunConclusion.failure;
} else if (status == bbv2.Status.SUCCESS) {
return github.CheckRunConclusion.success;
} else {
// Now that result is gone this is a non terminal step.
return github.CheckRunConclusion.empty;
}
}

/// Transforms a [push_message.Status] to a [github.CheckRunStatus].
/// Relevant APIs:
/// https://developer.github.com/v3/checks/runs/#check-runs
// TODO temporary as this needs to be adjusted as a COMPLETED state is no longer
// a valid state from buildbucket v2.
github.CheckRunStatus statusForResult(bbv2.Status status) {
// ignore: exhaustive_cases
switch (status) {
case bbv2.Status.SUCCESS:
case bbv2.Status.FAILURE:
case bbv2.Status.CANCELED:
case bbv2.Status.INFRA_FAILURE:
return github.CheckRunStatus.completed;
case bbv2.Status.SCHEDULED:
return github.CheckRunStatus.queued;
case bbv2.Status.STARTED:
return github.CheckRunStatus.inProgress;
default:
throw StateError('unreachable');
}
}

/// Given a [headSha] and [checkSuiteId], finds the [PullRequest] that matches.
Future<github.PullRequest?> findMatchingPullRequest(
github.RepositorySlug slug,
String headSha,
int checkSuiteId,
) async {
final GithubService githubService = await config.createDefaultGitHubService();

// There could be multiple PRs that have the same [headSha] commit.
final List<github.Issue> prIssues = await githubService.searchIssuesAndPRs(slug, '$headSha type:pr');

for (final prIssue in prIssues) {
final int prNumber = prIssue.number;

// Each PR can have multiple check suites.
final List<github.CheckSuite> checkSuites = await githubChecksUtil.listCheckSuitesForRef(
githubService.github,
slug,
ref: 'refs/pull/$prNumber/head',
);

// Use check suite ID equality to verify that we have iterated to the correct PR.
final bool doesPrIncludeMatchingCheckSuite = checkSuites.any((checkSuite) => checkSuite.id! == checkSuiteId);
if (doesPrIncludeMatchingCheckSuite) {
return githubService.getPullRequest(slug, prNumber);
}
}

return null;
}
}
Loading

0 comments on commit 2eeeddd

Please sign in to comment.