diff --git a/app_dart/lib/src/model/bbv2_extension.dart b/app_dart/lib/src/model/bbv2_extension.dart index f3ac928a5..4201de5ad 100644 --- a/app_dart/lib/src/model/bbv2_extension.dart +++ b/app_dart/lib/src/model/bbv2_extension.dart @@ -2,21 +2,28 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:buildbucket/buildbucket_pb.dart' as bbv2; +import 'package:buildbucket/buildbucket_pb.dart' as bb; import 'package:cocoon_common/task_status.dart'; -extension StatusExtension on bbv2.Status { - /// Converts from a [bbv2.Status] to a [TaskStatus]. +extension StatusExtension on bb.Status { + /// Converts from a [bb.Status] to a [TaskStatus]. /// /// An unrecognized status is disallowed. TaskStatus toTaskStatus() { return switch (this) { - bbv2.Status.SCHEDULED || bbv2.Status.STARTED => TaskStatus.inProgress, - bbv2.Status.SUCCESS => TaskStatus.succeeded, - bbv2.Status.CANCELED => TaskStatus.cancelled, - bbv2.Status.INFRA_FAILURE => TaskStatus.infraFailure, - bbv2.Status.FAILURE => TaskStatus.failed, + bb.Status.SCHEDULED || bb.Status.STARTED => TaskStatus.inProgress, + bb.Status.SUCCESS => TaskStatus.succeeded, + bb.Status.CANCELED => TaskStatus.cancelled, + bb.Status.INFRA_FAILURE => TaskStatus.infraFailure, + bb.Status.FAILURE => TaskStatus.failed, _ => throw StateError('Unexpected status: $this'), }; } } + +extension TaskFailedExtension on bb.Status { + bool isTaskFailed() => switch (this) { + bb.Status.FAILURE || bb.Status.CANCELED || bb.Status.INFRA_FAILURE => true, + _ => false, + }; +} diff --git a/app_dart/lib/src/model/common/presubmit_guard_conclusion.dart b/app_dart/lib/src/model/common/presubmit_guard_conclusion.dart index fe14c82b3..2adf49fa4 100644 --- a/app_dart/lib/src/model/common/presubmit_guard_conclusion.dart +++ b/app_dart/lib/src/model/common/presubmit_guard_conclusion.dart @@ -38,7 +38,7 @@ enum PresubmitGuardConclusionResult { class PresubmitGuardConclusion { final PresubmitGuardConclusionResult result; final int remaining; - final int? checkRunId; + final String checkRunJson; final int failed; final String summary; final String details; @@ -46,7 +46,7 @@ class PresubmitGuardConclusion { const PresubmitGuardConclusion({ required this.result, required this.remaining, - required this.checkRunId, + required this.checkRunJson, required this.failed, required this.summary, required this.details, @@ -66,16 +66,22 @@ class PresubmitGuardConclusion { (other is PresubmitGuardConclusion && other.result == result && other.remaining == remaining && - other.checkRunId == checkRunId && + other.checkRunJson == checkRunJson && other.failed == failed && other.summary == summary && other.details == details); @override - int get hashCode => - Object.hashAll([result, remaining, checkRunId, failed, summary, details]); + int get hashCode => Object.hashAll([ + result, + remaining, + checkRunJson, + failed, + summary, + details, + ]); @override String toString() => - 'BuildConclusion("$result", "$remaining", "$checkRunId", "$failed", "$summary", "$details")'; + 'BuildConclusion("$result", "$remaining", "$failed", "$summary", "$details", "$checkRunJson")'; } diff --git a/app_dart/lib/src/model/firestore/presubmit_guard.dart b/app_dart/lib/src/model/firestore/presubmit_guard.dart index c94cae042..5514afefd 100644 --- a/app_dart/lib/src/model/firestore/presubmit_guard.dart +++ b/app_dart/lib/src/model/firestore/presubmit_guard.dart @@ -185,9 +185,15 @@ final class PresubmitGuard extends AppDocument { CheckRun get checkRun { final jsonData = jsonDecode(fields[fieldCheckRun]!.stringValue!) as Map; + // Workaround for https://github.com/SpinlockLabs/github.dart/issues/412 + if (jsonData['conclusion'] == 'null') { + jsonData.remove('conclusion'); + } return CheckRun.fromJson(jsonData); } + String get checkRunJson => fields[fieldCheckRun]!.stringValue!; + /// The repository that this stage is recorded for. RepositorySlug get slug { // Read it from the document name. diff --git a/app_dart/lib/src/request_handlers/presubmit_luci_subscription.dart b/app_dart/lib/src/request_handlers/presubmit_luci_subscription.dart index 941b95f92..3fb17f78b 100644 --- a/app_dart/lib/src/request_handlers/presubmit_luci_subscription.dart +++ b/app_dart/lib/src/request_handlers/presubmit_luci_subscription.dart @@ -9,6 +9,7 @@ import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_server/logging.dart'; import 'package:github/github.dart'; +import '../model/bbv2_extension.dart'; import '../model/ci_yaml/ci_yaml.dart'; import '../model/ci_yaml/target.dart'; import '../model/commit_ref.dart'; @@ -102,65 +103,70 @@ final class PresubmitLuciSubscription extends SubscriptionHandler { final userData = PresubmitUserData.fromBytes(pubSubCallBack.userData); var rescheduled = false; - if (_githubChecksService.taskFailed(build.status)) { - final currentAttempt = _nextAttempt(tagSet); + if (build.status.isTaskFailed()) { + // if failed summary stored in github check run and unified check run. + build.summaryMarkdown = (await _luciBuildService.getBuildById( + build.id, + buildMask: bbv2.BuildMask( + // Need to use allFields as there is a bug with fieldMask and summaryMarkdown. + allFields: true, + ), + )).summaryMarkdown; final maxAttempt = await _getMaxAttempt( userData.commit, builderName, tagSet, ); - if (currentAttempt < maxAttempt) { + if (tagSet.currentAttempt < maxAttempt) { rescheduled = true; log.info('Rerunning failed task: $builderName'); await _luciBuildService.reschedulePresubmitBuild( builderName: builderName, build: build, - nextAttempt: currentAttempt + 1, + nextAttempt: tagSet.currentAttempt + 1, userData: userData, ); } } - await _githubChecksService.updateCheckStatus( - checkRunId: userData.checkRunId, - build: build, - luciBuildService: _luciBuildService, - slug: userData.commit.slug, - rescheduled: rescheduled, - ); + if (userData.checkRunId != null) { + await _githubChecksService.updateCheckStatus( + checkRunId: userData.checkRunId!, + build: build, + luciBuildService: _luciBuildService, + slug: userData.commit.slug, + rescheduled: rescheduled, + ); - if (!rescheduled && config.flags.closeMqGuardAfterPresubmit) { - // Process to the check-run status in the merge queue document during - // the LUCI callback. - final conclusion = _githubChecksService.conclusionForResult(build.status); - await _scheduler.processCheckRunCompleted( - cocoon_checks.CheckRun( - id: userData.checkRunId, - name: builderName, - headSha: userData.commit.sha, - conclusion: '$conclusion', - checkSuite: CheckSuite( - id: userData.checkSuiteId, - headBranch: userData.commit.branch, + if (!rescheduled && config.flags.closeMqGuardAfterPresubmit) { + // Process to the check-run status in the merge queue document during + // the LUCI callback. + final conclusion = _githubChecksService.conclusionForResult( + build.status, + ); + await _scheduler.processCheckRunCompleted( + cocoon_checks.CheckRun( + id: userData.checkRunId, + name: builderName, headSha: userData.commit.sha, - conclusion: conclusion, - pullRequests: [], + conclusion: '$conclusion', + checkSuite: CheckSuite( + id: userData.checkSuiteId, + headBranch: userData.commit.branch, + headSha: userData.commit.sha, + conclusion: conclusion, + pullRequests: [], + ), ), - ), - userData.commit.slug, - ); + userData.commit.slug, + ); + } + } else if (!rescheduled) { + await _scheduler.processUnifiedCheckRunCompleted(build, userData); } return Response.emptyOk; } - /// Returns the current reschedule attempt. - /// - /// It returns 1 if this is the first run. - static int _nextAttempt(BuildTags buildTags) { - final attempt = buildTags.getTagOfType(); - return attempt?.attemptNumber ?? 1; - } - Future _getMaxAttempt( CommitRef commit, String builderName, diff --git a/app_dart/lib/src/service/firestore/unified_check_run.dart b/app_dart/lib/src/service/firestore/unified_check_run.dart index 981febb6d..044a93a0b 100644 --- a/app_dart/lib/src/service/firestore/unified_check_run.dart +++ b/app_dart/lib/src/service/firestore/unified_check_run.dart @@ -249,7 +249,7 @@ final class UnifiedCheckRun { return PresubmitGuardConclusion( result: PresubmitGuardConclusionResult.missing, remaining: presubmitGuard.remainingBuilds ?? -1, - checkRunId: guardId.checkRunId, + checkRunJson: presubmitGuard.checkRunJson, failed: presubmitGuard.failedBuilds ?? -1, summary: 'Check run "${state.buildName}" not present in ${guardId.stage} CI stage', @@ -336,7 +336,7 @@ final class UnifiedCheckRun { return PresubmitGuardConclusion( result: PresubmitGuardConclusionResult.internalError, remaining: -1, - checkRunId: null, + checkRunJson: '', failed: failed, summary: 'Internal server error', details: @@ -371,7 +371,7 @@ $stack ? PresubmitGuardConclusionResult.ok : PresubmitGuardConclusionResult.internalError, remaining: remaining, - checkRunId: guardId.checkRunId, + checkRunJson: presubmitGuard.checkRunJson, failed: failed, summary: valid ? 'Successfully updated presubmit guard status' diff --git a/app_dart/lib/src/service/github_checks_service.dart b/app_dart/lib/src/service/github_checks_service.dart index 06bc505bd..52147996b 100644 --- a/app_dart/lib/src/service/github_checks_service.dart +++ b/app_dart/lib/src/service/github_checks_service.dart @@ -7,6 +7,7 @@ import 'package:cocoon_server/logging.dart'; import 'package:github/github.dart' as github; import '../foundation/github_checks_util.dart'; +import '../model/bbv2_extension.dart'; import 'config.dart'; import 'luci_build_service.dart'; @@ -72,7 +73,7 @@ class GithubChecksService { final 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)) { + if (build.status.isTaskFailed()) { log.info( 'failed presubmit task, ${build.id} has failed, status = ${build.status.toString()}', ); @@ -114,14 +115,6 @@ class GithubChecksService { return true; } - /// Check if task has completed with failure. - bool taskFailed(bbv2.Status status) { - final checkRunStatus = statusForResult(status); - final conclusion = conclusionForResult(status); - return (checkRunStatus == github.CheckRunStatus.completed) && - failedStatesSet.contains(conclusion); - } - /// Appends triage wiki page to `summaryMarkdown` from LUCI build so that people can easily /// reference from github check run page. String getGithubSummary(String? summary) { diff --git a/app_dart/lib/src/service/luci_build_service.dart b/app_dart/lib/src/service/luci_build_service.dart index 99c1d28ad..94f7bbd23 100644 --- a/app_dart/lib/src/service/luci_build_service.dart +++ b/app_dart/lib/src/service/luci_build_service.dart @@ -11,7 +11,6 @@ import 'package:cocoon_common/is_release_branch.dart'; import 'package:cocoon_common/task_status.dart'; import 'package:cocoon_server/logging.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:github/github.dart' as github; import 'package:github/github.dart'; import 'package:googleapis/firestore/v1.dart' hide Status; import 'package:meta/meta.dart'; @@ -20,6 +19,7 @@ import '../../cocoon_service.dart'; import '../foundation/github_checks_util.dart'; import '../model/ci_yaml/target.dart'; import '../model/commit_ref.dart'; +import '../model/firestore/base.dart'; import '../model/firestore/pr_check_runs.dart' as fs; import '../model/firestore/task.dart' as fs; import '../model/github/checks.dart' as cocoon_checks; @@ -98,7 +98,7 @@ class LuciBuildService { /// /// Returns a list of BuildBucket [Build]s for a given Github [PullRequest]. Future> getTryBuildsByPullRequest({ - required github.PullRequest pullRequest, + required PullRequest pullRequest, }) async { final slug = pullRequest.base!.repo!.slug(); return _getBuilds( @@ -226,8 +226,10 @@ class LuciBuildService { /// framework tests, provide [EngineArtifacts.noFrameworkTests]. Future> scheduleTryBuilds({ required List targets, - required github.PullRequest pullRequest, + required PullRequest pullRequest, required EngineArtifacts engineArtifacts, + CheckRun? checkRunGuard, + CiStage? stage, }) async { if (targets.isEmpty) { return targets; @@ -235,30 +237,51 @@ class LuciBuildService { final batchRequestList = []; final commitSha = pullRequest.head!.sha!; - final isFusion = pullRequest.base!.repo!.slug() == Config.flutterSlug; + final slug = pullRequest.base!.repo!.slug(); + final commitBranch = pullRequest.base!.ref!.replaceAll('refs/heads/', ''); + final isFusion = slug == Config.flutterSlug; + final isUnifiedCheckRunFlow = _config.flags + .isUnifiedCheckRunFlowEnabledForUser(pullRequest.user!.login!); final cipdVersion = await _getAndCheckRecipeVersion( slug: pullRequest.base!.repo!.slug(), branch: pullRequest.base!.ref!, ); - final checkRuns = []; - for (var target in targets) { - final checkRun = await _githubChecksUtil.createCheckRun( - _config, - target.slug, - commitSha, - target.name, - ); - checkRuns.add(checkRun); - - final slug = pullRequest.base!.repo!.slug(); - final commitBranch = pullRequest.base!.ref!.replaceAll('refs/heads/', ''); - final userData = PresubmitUserData( + final checkRuns = []; + late PresubmitUserData userData; + // If the unified check run flow is enabled, do not create individual + // check runs for each target but use the guard check run instead. + if (isUnifiedCheckRunFlow && checkRunGuard != null) { + userData = PresubmitUserData( commit: CommitRef(slug: slug, sha: commitSha, branch: commitBranch), - checkRunId: checkRun.id!, - checkSuiteId: checkRun.checkSuiteId!, + guardCheckRunId: checkRunGuard.id!, + checkSuiteId: checkRunGuard.checkSuiteId, + pullRequestNumber: pullRequest.number!, + stage: stage, ); + // We need to store PR to checkrun mapping in order to get PR later in + // [Scheduler.proceedUnifiedCheckRunToTestingStage] method + checkRuns.add(checkRunGuard); + } + for (var target in targets) { + // If the unified check run flow is disabled create individual check runs + // for each target. + if (!isUnifiedCheckRunFlow || checkRunGuard == null) { + final checkRun = await _githubChecksUtil.createCheckRun( + _config, + target.slug, + commitSha, + target.name, + ); + checkRuns.add(checkRun); + + userData = PresubmitUserData( + commit: CommitRef(slug: slug, sha: commitSha, branch: commitBranch), + checkRunId: checkRun.id, + checkSuiteId: checkRun.checkSuiteId, + ); + } final properties = target.getProperties(); properties.putIfAbsent('git_branch', () => commitBranch); @@ -314,9 +337,14 @@ class LuciBuildService { cipdVersion: cipdVersion, userData: userData, properties: properties, - tags: BuildTags([ - GitHubCheckRunIdBuildTag(checkRunId: checkRun.id!), - ]), + // if unified check run flow is enabled, use guard check run othervise check run id. + tags: isUnifiedCheckRunFlow && checkRunGuard != null + ? BuildTags([ + GuardCheckRunIdBuildTag(guardCheckRunId: checkRunGuard.id!), + ]) + : BuildTags([ + GitHubCheckRunIdBuildTag(checkRunId: userData.checkRunId!), + ]), dimensions: requestedDimensions, ), ), @@ -356,7 +384,7 @@ class LuciBuildService { /// Cancels all the current builds on [pullRequest] with [reason]. Future cancelBuilds({ - required github.PullRequest pullRequest, + required PullRequest pullRequest, required String reason, }) async { log.info( @@ -472,7 +500,7 @@ class LuciBuildService { /// /// Returns a [List] of prefixed label names as [String]s. static List? _extractPrefixedLabels({ - List? issueLabels, + List? issueLabels, required String prefix, }) { return issueLabels @@ -744,7 +772,7 @@ class LuciBuildService { /// Create a Presubmit ScheduleBuildRequest using the [slug], [sha], and /// [checkName] for the provided [build] with the provided [checkRunId]. bbv2.ScheduleBuildRequest _createPresubmitScheduleBuild({ - required github.RepositorySlug slug, + required RepositorySlug slug, required String sha, required String checkName, required int pullRequestNumber, diff --git a/app_dart/lib/src/service/luci_build_service/build_tags.dart b/app_dart/lib/src/service/luci_build_service/build_tags.dart index e472f6544..ed9b43f47 100644 --- a/app_dart/lib/src/service/luci_build_service/build_tags.dart +++ b/app_dart/lib/src/service/luci_build_service/build_tags.dart @@ -337,11 +337,11 @@ final class GitHubCheckRunIdBuildTag extends BuildTag { /// A link back to the GitHub checkRun for this build. final class GuardCheckRunIdBuildTag extends BuildTag { static const _keyName = 'guard_checkrun'; - GuardCheckRunIdBuildTag({required this.checkRunId}) - : super(_keyName, '$checkRunId'); + GuardCheckRunIdBuildTag({required this.guardCheckRunId}) + : super(_keyName, '$guardCheckRunId'); /// ID of the checkRun. - final int checkRunId; + final int guardCheckRunId; } /// A build tag that specifies the ID of the scheduling job. diff --git a/app_dart/lib/src/service/luci_build_service/user_data.dart b/app_dart/lib/src/service/luci_build_service/user_data.dart index a4a74172b..f432f024a 100644 --- a/app_dart/lib/src/service/luci_build_service/user_data.dart +++ b/app_dart/lib/src/service/luci_build_service/user_data.dart @@ -11,6 +11,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; import '../../model/commit_ref.dart'; +import '../../model/firestore/base.dart'; import '../../model/firestore/task.dart'; part 'user_data.g.dart'; @@ -65,8 +66,11 @@ abstract final class PresubmitUserData extends BuildBucketUserData { /// [checkRunId] and [checkSuiteId]. factory PresubmitUserData({ required CommitRef commit, - required int checkRunId, - required int checkSuiteId, + int? checkRunId, + int? checkSuiteId, + int? guardCheckRunId, + int? pullRequestNumber, + CiStage? stage, }) { return _PresubmitUserData( repoOwner: commit.slug.owner, @@ -75,6 +79,9 @@ abstract final class PresubmitUserData extends BuildBucketUserData { commitSha: commit.sha, checkRunId: checkRunId, checkSuiteId: checkSuiteId, + guardCheckRunId: guardCheckRunId, + pullRequestNumber: pullRequestNumber, + stage: stage, ); } @@ -102,45 +109,64 @@ abstract final class PresubmitUserData extends BuildBucketUserData { CommitRef get commit; /// Which GitHub check run this build reports status to. - int get checkRunId; + int? get checkRunId; - int get checkSuiteId; + /// The check suite ID to which check run belongs. + int? get checkSuiteId; + + /// The check run ID of the MQ guard build associated with this presubmit. + /// Used for Unified check runs. + int? get guardCheckRunId; + + /// The pull request number associated with this presubmit. + /// Used for Unified check runs. + int? get pullRequestNumber; + + /// The CI stage associated with this presubmit. + /// Used for Unified check runs. + CiStage? get stage; } -@JsonSerializable(checked: true) +@JsonSerializable(checked: true, fieldRename: FieldRename.snake) final class _PresubmitUserData extends PresubmitUserData { const _PresubmitUserData({ required this.repoOwner, required this.repoName, required this.commitBranch, required this.commitSha, - required this.checkRunId, - required this.checkSuiteId, + this.checkRunId, + this.checkSuiteId, + this.guardCheckRunId, + this.pullRequestNumber, + this.stage, }) : super._(); /// The owner of the GitHub repo, i.e. `flutter` or `matanlurey`. - @JsonKey(name: 'repo_owner') final String repoOwner; /// The name of the GitHub repo, i.e. `flutter` or `packages`. - @JsonKey(name: 'repo_name') final String repoName; /// The branch the [commitSha] is on. - @JsonKey(name: 'commit_branch') final String commitBranch; /// The commit SHA being built at. - @JsonKey(name: 'commit_sha') final String commitSha; - @JsonKey(name: 'check_run_id') @override - final int checkRunId; + final int? checkRunId; + + @override + final int? checkSuiteId; + + @override + final int? guardCheckRunId; + + @override + final int? pullRequestNumber; - @JsonKey(name: 'check_suite_id', defaultValue: 0) @override - final int checkSuiteId; + final CiStage? stage; @override CommitRef get commit { @@ -156,7 +182,11 @@ final class _PresubmitUserData extends PresubmitUserData { } /// Represents the data passed to Buildbucket as part of a postsubmit build. -@JsonSerializable(checked: true, includeIfNull: false) +@JsonSerializable( + checked: true, + includeIfNull: false, + fieldRename: FieldRename.snake, +) final class PostsubmitUserData extends BuildBucketUserData { PostsubmitUserData({required this.checkRunId, required this.taskId}); @@ -183,11 +213,10 @@ final class PostsubmitUserData extends BuildBucketUserData { /// Which GitHub check run this build reports status to. /// /// If this postsubmit is marked bringup (`bringup: true`) there is no associated ID. - @JsonKey(name: 'check_run_id', includeIfNull: false) final int? checkRunId; /// The firestore task document name storing results of this build. - @JsonKey(name: 'task_id', fromJson: TaskId.parse, toJson: _documentToString) + @JsonKey(fromJson: TaskId.parse, toJson: _documentToString) final TaskId taskId; static String _documentToString(TaskId firestoreTask) { return firestoreTask.documentId; diff --git a/app_dart/lib/src/service/luci_build_service/user_data.g.dart b/app_dart/lib/src/service/luci_build_service/user_data.g.dart index a670aec1b..c6f4412f5 100644 --- a/app_dart/lib/src/service/luci_build_service/user_data.g.dart +++ b/app_dart/lib/src/service/luci_build_service/user_data.g.dart @@ -20,11 +20,23 @@ _PresubmitUserData _$PresubmitUserDataFromJson(Map json) => commitSha: $checkedConvert('commit_sha', (v) => v as String), checkRunId: $checkedConvert( 'check_run_id', - (v) => (v as num).toInt(), + (v) => (v as num?)?.toInt(), ), checkSuiteId: $checkedConvert( 'check_suite_id', - (v) => (v as num?)?.toInt() ?? 0, + (v) => (v as num?)?.toInt(), + ), + guardCheckRunId: $checkedConvert( + 'guard_check_run_id', + (v) => (v as num?)?.toInt(), + ), + pullRequestNumber: $checkedConvert( + 'pull_request_number', + (v) => (v as num?)?.toInt(), + ), + stage: $checkedConvert( + 'stage', + (v) => $enumDecodeNullable(_$CiStageEnumMap, v), ), ); return val; @@ -36,6 +48,8 @@ _PresubmitUserData _$PresubmitUserDataFromJson(Map json) => 'commitSha': 'commit_sha', 'checkRunId': 'check_run_id', 'checkSuiteId': 'check_suite_id', + 'guardCheckRunId': 'guard_check_run_id', + 'pullRequestNumber': 'pull_request_number', }, ); @@ -47,8 +61,16 @@ Map _$PresubmitUserDataToJson(_PresubmitUserData instance) => 'commit_sha': instance.commitSha, 'check_run_id': instance.checkRunId, 'check_suite_id': instance.checkSuiteId, + 'guard_check_run_id': instance.guardCheckRunId, + 'pull_request_number': instance.pullRequestNumber, + 'stage': _$CiStageEnumMap[instance.stage], }; +const _$CiStageEnumMap = { + CiStage.fusionEngineBuild: 'fusionEngineBuild', + CiStage.fusionTests: 'fusionTests', +}; + PostsubmitUserData _$PostsubmitUserDataFromJson(Map json) => $checkedCreate( 'PostsubmitUserData', diff --git a/app_dart/lib/src/service/scheduler.dart b/app_dart/lib/src/service/scheduler.dart index 9acbb720a..c57539073 100644 --- a/app_dart/lib/src/service/scheduler.dart +++ b/app_dart/lib/src/service/scheduler.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'package:buildbucket/buildbucket_pb.dart' as bbv2; import 'package:cocoon_common/task_status.dart'; import 'package:cocoon_server/logging.dart'; import 'package:collection/collection.dart'; @@ -17,13 +18,17 @@ import 'package:meta/meta.dart'; import 'package:retry/retry.dart'; import '../foundation/utils.dart'; +import '../model/bbv2_extension.dart'; import '../model/ci_yaml/ci_yaml.dart'; import '../model/ci_yaml/target.dart'; import '../model/commit_ref.dart'; +import '../model/common/presubmit_check_state.dart'; +import '../model/common/presubmit_guard_conclusion.dart'; import '../model/firestore/base.dart'; import '../model/firestore/ci_staging.dart'; import '../model/firestore/commit.dart' as fs; import '../model/firestore/pr_check_runs.dart'; +import '../model/firestore/presubmit_guard.dart'; import '../model/firestore/task.dart' as fs; import '../model/github/checks.dart' as cocoon_checks; import '../model/github/checks.dart' show MergeGroup; @@ -35,11 +40,14 @@ import 'config.dart'; import 'content_aware_hash_service.dart'; import 'exceptions.dart'; import 'firestore.dart'; +import 'firestore/unified_check_run.dart'; import 'get_files_changed.dart'; import 'github_checks_service.dart'; import 'luci_build_service.dart'; +import 'luci_build_service/build_tags.dart'; import 'luci_build_service/engine_artifacts.dart'; import 'luci_build_service/pending_task.dart'; +import 'luci_build_service/user_data.dart'; import 'scheduler/ci_yaml_fetcher.dart'; import 'scheduler/files_changed_optimization.dart'; import 'scheduler/process_check_run_result.dart'; @@ -368,18 +376,19 @@ class Scheduler { 'triggerPresubmitTargets($slug, $sha){frameworkOnly}'; log.info('$logCrumb: FRAMEWORK_ONLY_TESTING_PR'); - await CiStaging.initializeDocument( + await UnifiedCheckRun.initializeCiStagingDocument( firestoreService: _firestore, slug: slug, sha: sha, stage: CiStage.fusionEngineBuild, tasks: [], - checkRunGuard: '', + pullRequest: pullRequest, + config: _config, ); await _runCiTestingStage( pullRequest: pullRequest, - checkRunGuard: '$lock', + checkRunGuard: lock, logCrumb: logCrumb, // The if-branch already skips the engine build phase. @@ -405,13 +414,15 @@ class Scheduler { // to complete before we can schedule more tests (i.e. build engine artifacts before testing against them). final EngineArtifacts engineArtifacts; if (isFusion) { - await CiStaging.initializeDocument( + await UnifiedCheckRun.initializeCiStagingDocument( firestoreService: _firestore, slug: slug, sha: sha, stage: CiStage.fusionEngineBuild, tasks: [...presubmitTriggerTargets.map((t) => t.name)], - checkRunGuard: '$lock', + pullRequest: pullRequest, + config: _config, + checkRun: lock, ); // Even though this appears to be an engine build, it could be a @@ -433,6 +444,8 @@ class Scheduler { targets: presubmitTriggerTargets, pullRequest: pullRequest, engineArtifacts: engineArtifacts, + checkRunGuard: lock, + stage: CiStage.fusionEngineBuild, ); } on FormatException catch (e, s) { log.warn( @@ -558,7 +571,7 @@ class Scheduler { } } log.info('$logCrumb: scheduling merge group checks'); - return triggerTargetsForMergeGroup( + return await triggerTargetsForMergeGroup( baseRef: baseRef, headSha: headSha, headRef: headRef, @@ -625,13 +638,14 @@ class Scheduler { // Create the staging doc that will track our engine progress and allow us to unlock // the merge group lock later. - await CiStaging.initializeDocument( + await UnifiedCheckRun.initializeCiStagingDocument( firestoreService: _firestore, slug: slug, sha: headSha, stage: CiStage.fusionEngineBuild, tasks: [...availableTargets.map((t) => t.name)], - checkRunGuard: '$lock', + config: _config, + checkRun: lock, ); // Create the minimal Commit needed to pass the next stage. @@ -1051,17 +1065,176 @@ $s case CiStage.fusionEngineBuild: if (isMergeGroup) { await _completeArtifacts(sha, true); + await _closeMergeQueue( + mergeQueueGuard: stagingConclusion.checkRunGuard!, + slug: slug, + sha: sha, + stage: CiStage.fusionEngineBuild, + logCrumb: logCrumb, + ); + } else { + await _closeSuccessfulEngineBuildStage( + checkRun: checkRun, + mergeQueueGuard: stagingConclusion.checkRunGuard!, + slug: slug, + sha: sha, + logCrumb: logCrumb, + ); + } + case CiStage.fusionTests: + await _closeSuccessfulTestStage( + mergeQueueGuard: stagingConclusion.checkRunGuard!, + slug: slug, + sha: sha, + logCrumb: logCrumb, + ); + } + return true; + } + + /// Process a completed unified check run. + /// + /// Handles pull requests only + Future processUnifiedCheckRunCompleted( + bbv2.Build build, + PresubmitUserData userData, + ) async { + final name = build.builder.builder; + final sha = userData.commit.sha; + final status = build.status.toTaskStatus(); + final slug = userData.commit.slug; + final stage = userData.stage!; + final tagSet = BuildTags.fromStringPairs(build.tags); + + /// Create a fake check run to pass to existing methods. + final checkRun = cocoon_checks.CheckRun( + id: userData.guardCheckRunId, + name: 'Merge Queue Guard', + headSha: userData.commit.sha, + checkSuite: CheckSuite( + id: userData.checkSuiteId, + headBranch: userData.commit.branch, + headSha: userData.commit.sha, + conclusion: CheckRunConclusion.empty, + pullRequests: [], + ), + ); + + if (kCheckRunsToIgnore.contains(name)) { + return true; + } + + final logCrumb = + 'processUnifiedCheckRunCompleted($name, $slug, $sha, $status)'; + + final isFusion = slug == Config.flutterSlug; + if (!isFusion) { + return true; + } + + final isMergeGroup = detectMergeGroup(checkRun); + + final stagingConclusion = await _markUnifiedCheckRunConclusion( + slug: slug, + stage: stage, + name: name, + status: status, + pullRequestNumber: userData.pullRequestNumber!, + guardCheckRunId: userData.guardCheckRunId!, + attempt: tagSet.currentAttempt, + startTime: build.startTime.toDateTime().microsecondsSinceEpoch, + endTime: build.endTime.toDateTime().microsecondsSinceEpoch, + summary: build.summaryMarkdown, + ); + + // First; check if we even recorded anything. This can occur if we've already passed the check_run and + // have moved on to running more tests (which wouldn't be present in our document). + if (!stagingConclusion.isOk) { + return false; + } + + // If an internal error happened in Cocoon, we need human assistance to + // figure out next steps. + if (stagingConclusion.result == + PresubmitGuardConclusionResult.internalError) { + // If an internal error happened in the merge group, there may be no further + // signals from GitHub that would cause the merge group to either land or + // fail. The safest thing to do is to kick the pull request out of the queue + // and let humans sort it out. If the group is left hanging in the queue, it + // will hold up all other PRs that are trying to land. + if (isMergeGroup) { + await _completeArtifacts(sha, false); + final guard = checkRunFromString(stagingConclusion.checkRunJson); + await failGuardForMergeGroup( + slug, + sha, + stagingConclusion.summary, + stagingConclusion.details, + guard, + ); + } + return false; + } + + // Are there tests remaining? Keep waiting. + if (stagingConclusion.isPending) { + log.info( + '$logCrumb: not progressing, remaining work count: ${stagingConclusion.remaining}', + ); + return false; + } + + if (stagingConclusion.isFailed) { + // Something failed in the current CI stage: + // + // * If this is a pull request: keep the merge guard open and do not proceed + // to the next stage. Let the author sort out what's up. + // * If this is a merge group: kick the pull request out of the queue, and + // let the author sort it out. + if (isMergeGroup) { + await _completeArtifacts(sha, false); + final guard = checkRunFromString(stagingConclusion.checkRunJson); + await failGuardForMergeGroup( + slug, + sha, + stagingConclusion.summary, + stagingConclusion.details, + guard, + ); + } + return true; + } + + // The logic for finishing a stage is different between build and test stages: + // + // * If this is a build stage, then: + // * If this is a pull request presubmit, then start the test stage. + // * If this is a merge group (in MQ), then close the MQ guard, letting + // GitHub land it. + // * If this is a test stage, then close the MQ guard (allowing the PR to + // enter the MQ). + switch (stage) { + case CiStage.fusionEngineBuild: + if (isMergeGroup) { + await _completeArtifacts(sha, true); + await _closeMergeQueue( + mergeQueueGuard: stagingConclusion.checkRunJson, + slug: slug, + sha: sha, + stage: CiStage.fusionEngineBuild, + logCrumb: logCrumb, + ); } await _closeSuccessfulEngineBuildStage( checkRun: checkRun, - mergeQueueGuard: stagingConclusion.checkRunGuard!, + mergeQueueGuard: stagingConclusion.checkRunJson, slug: slug, sha: sha, logCrumb: logCrumb, ); case CiStage.fusionTests: await _closeSuccessfulTestStage( - mergeQueueGuard: stagingConclusion.checkRunGuard!, + mergeQueueGuard: stagingConclusion.checkRunJson, slug: slug, sha: sha, logCrumb: logCrumb, @@ -1101,20 +1274,6 @@ $s required String sha, required String logCrumb, }) async { - // We know that we're in a fusion repo; now we need to figure out if we are - // 1) in a presubmit test or - // 2) in the merge queue - if (detectMergeGroup(checkRun)) { - await _closeMergeQueue( - mergeQueueGuard: mergeQueueGuard, - slug: slug, - sha: sha, - stage: CiStage.fusionEngineBuild, - logCrumb: logCrumb, - ); - return; - } - log.info( '$logCrumb: Stage completed successfully: ${CiStage.fusionEngineBuild}', ); @@ -1178,7 +1337,7 @@ $s /// Schedules post-engine build tests (i.e. engine tests, and framework tests). Future _runCiTestingStage({ required PullRequest pullRequest, - required String checkRunGuard, + required CheckRun checkRunGuard, required String logCrumb, required _FlutterRepoTestsToRun testsToRun, }) async { @@ -1222,13 +1381,15 @@ $s tasks = [...presubmitTargets.map((t) => t.name)]; } - await CiStaging.initializeDocument( + await UnifiedCheckRun.initializeCiStagingDocument( firestoreService: _firestore, slug: pullRequest.base!.repo!.slug(), sha: pullRequest.head!.sha!, stage: CiStage.fusionTests, tasks: tasks, - checkRunGuard: checkRunGuard, + config: _config, + pullRequest: pullRequest, + checkRun: checkRunGuard, ); // Here is where it gets fun: how do framework tests* know what engine @@ -1255,6 +1416,8 @@ $s targets: presubmitTargets, pullRequest: pullRequest, engineArtifacts: engineArtifacts, + checkRunGuard: checkRunGuard, + stage: CiStage.fusionTests, ); } } on FormatException catch (e, s) { @@ -1314,7 +1477,7 @@ $s try { await _runCiTestingStage( pullRequest: pullRequest, - checkRunGuard: '$checkRunGuard', + checkRunGuard: checkRunGuard, logCrumb: logCrumb, testsToRun: _FlutterRepoTestsToRun.engineTestsAndFrameworkTests, ); @@ -1382,6 +1545,48 @@ $stacktrace }); } + Future _markUnifiedCheckRunConclusion({ + required RepositorySlug slug, + required CiStage stage, + required String name, + required TaskStatus status, + required int pullRequestNumber, + required int guardCheckRunId, + required int attempt, + int? startTime, + int? endTime, + String? summary, + }) async { + final logCrumb = 'checkCompleted($name, $stage, $slug, $status)'; + final guardId = PresubmitGuardId( + slug: slug, + pullRequestId: pullRequestNumber, + checkRunId: guardCheckRunId, + stage: stage, + ); + final state = PresubmitCheckState( + buildName: name, + status: status, + attemptNumber: attempt, + startTime: startTime, + endTime: endTime, + summary: summary, + ); + log.info('$logCrumb: ${guardId.documentId}'); + // We're doing a transactional update, which could fail if multiple tasks + // are running at the same time so retry a sane amount of times before + // giving up. + const r = RetryOptions(maxAttempts: 3, delayFactor: Duration(seconds: 2)); + + return r.retry(() { + return UnifiedCheckRun.markConclusion( + firestoreService: _firestore, + guardId: guardId, + state: state, + ); + }); + } + /// Reschedules a failed build using a [CheckRunEvent]. The CheckRunEvent is /// generated when someone clicks the re-run button from a failed build from /// the Github UI. @@ -1510,6 +1715,8 @@ $stacktrace targets: [target], pullRequest: pullRequest, engineArtifacts: engineArtifacts, + checkRunGuard: null, + stage: null, ); } else { log.debug('Rescheduling postsubmit build.'); diff --git a/app_dart/test/request_handlers/presubmit_luci_subscription_test.dart b/app_dart/test/request_handlers/presubmit_luci_subscription_test.dart index 5584cb84d..a402b9c5b 100644 --- a/app_dart/test/request_handlers/presubmit_luci_subscription_test.dart +++ b/app_dart/test/request_handlers/presubmit_luci_subscription_test.dart @@ -91,7 +91,6 @@ void main() { ), ).thenAnswer((_) async => true); - when(mockGithubChecksService.taskFailed(any)).thenAnswer((_) => false); when( mockGithubChecksService.conclusionForResult(any), ).thenAnswer((_) => github.CheckRunConclusion.empty); @@ -136,7 +135,7 @@ void main() { slug: anyNamed('slug'), ), ).thenAnswer((_) async => true); - when(mockGithubChecksService.taskFailed(any)).thenAnswer((_) => true); + when( mockGithubChecksService.conclusionForResult(any), ).thenAnswer((_) => github.CheckRunConclusion.empty); @@ -155,14 +154,14 @@ void main() { ); tester.message = createPushMessage( Int64(1), - status: bbv2.Status.SUCCESS, + status: bbv2.Status.FAILURE, builder: 'Linux A', userData: userData, ); final buildsPubSub = createBuild( Int64(1), - status: bbv2.Status.SUCCESS, + status: bbv2.Status.FAILURE, builder: 'Linux A', ); @@ -196,11 +195,10 @@ void main() { rescheduled: true, ), ).thenAnswer((_) async => true); - when(mockGithubChecksService.taskFailed(any)).thenAnswer((_) => true); tester.message = createPushMessage( Int64(1), - status: bbv2.Status.SUCCESS, + status: bbv2.Status.FAILURE, builder: 'Linux presubmit_max_attempts=2', userData: PresubmitUserData( commit: CommitRef( @@ -236,7 +234,9 @@ void main() { rescheduled: true, ), ).thenAnswer((_) async => true); - when(mockGithubChecksService.taskFailed(any)).thenAnswer((_) => true); + when( + mockLuciBuildService.getBuildById(any, buildMask: anyNamed('buildMask')), + ).thenAnswer((_) async => bbv2.Build(summaryMarkdown: 'test summary')); tester.message = createPushMessage( Int64(1), @@ -315,7 +315,7 @@ void main() { rescheduled: false, ), ).thenAnswer((_) async => true); - when(mockGithubChecksService.taskFailed(any)).thenAnswer((_) => true); + when( mockGithubChecksService.conclusionForResult(any), ).thenAnswer((_) => github.CheckRunConclusion.empty); @@ -335,14 +335,14 @@ void main() { tester.message = createPushMessage( Int64(1), - status: bbv2.Status.SUCCESS, + status: bbv2.Status.FAILURE, builder: 'Linux C', userData: userData, ); final buildsPubSub = createBuild( Int64(1), - status: bbv2.Status.SUCCESS, + status: bbv2.Status.FAILURE, builder: 'Linux C', ); @@ -364,7 +364,7 @@ void main() { rescheduled: false, ), ).called(1); - verify(mockGithubChecksService.taskFailed(any)).called(1); + verify(mockScheduler.processCheckRunCompleted(any, any)).called(1); }); @@ -378,7 +378,7 @@ void main() { rescheduled: false, ), ).thenAnswer((_) async => true); - when(mockGithubChecksService.taskFailed(any)).thenAnswer((_) => true); + when( mockGithubChecksService.conclusionForResult(any), ).thenAnswer((_) => github.CheckRunConclusion.empty); @@ -397,14 +397,14 @@ void main() { ); tester.message = createPushMessage( Int64(1), - status: bbv2.Status.SUCCESS, + status: bbv2.Status.FAILURE, builder: 'Linux C', userData: userData, ); final buildsPubSub = createBuild( Int64(1), - status: bbv2.Status.SUCCESS, + status: bbv2.Status.FAILURE, builder: 'Linux C', ); @@ -430,8 +430,6 @@ void main() { }); test('Pubsub rejected if branch is not enabled.', () async { - when(mockGithubChecksService.taskFailed(any)).thenAnswer((_) => true); - final userData = PresubmitUserData( checkRunId: 1, checkSuiteId: 2, @@ -443,7 +441,7 @@ void main() { ); tester.message = createPushMessage( Int64(1), - status: bbv2.Status.SUCCESS, + status: bbv2.Status.FAILURE, builder: 'Linux C', userData: userData, ); @@ -470,11 +468,13 @@ void main() { rescheduled: anyNamed('rescheduled'), ), ).thenAnswer((_) async => true); - when(mockGithubChecksService.taskFailed(any)).thenAnswer((_) => true); + when( + mockLuciBuildService.getBuildById(any, buildMask: anyNamed('buildMask')), + ).thenAnswer((_) async => bbv2.Build(summaryMarkdown: 'test summary')); tester.message = createPushMessage( Int64(1), - status: bbv2.Status.SUCCESS, + status: bbv2.Status.FAILURE, builder: 'Linux presubmit_max_attempts=2', userData: PresubmitUserData( checkRunId: 1, @@ -545,7 +545,6 @@ void main() { rescheduled: anyNamed('rescheduled'), ), ).thenAnswer((_) async => true); - when(mockGithubChecksService.taskFailed(any)).thenAnswer((_) => false); tester.message = createPushMessage( Int64(1), diff --git a/app_dart/test/service/luci_build_service/schedule_try_builds_test.dart b/app_dart/test/service/luci_build_service/schedule_try_builds_test.dart index 814a8339f..8665f4a87 100644 --- a/app_dart/test/service/luci_build_service/schedule_try_builds_test.dart +++ b/app_dart/test/service/luci_build_service/schedule_try_builds_test.dart @@ -7,9 +7,13 @@ import 'package:cocoon_common_test/cocoon_common_test.dart'; import 'package:cocoon_server/logging.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/commit_ref.dart'; +import 'package:cocoon_service/src/model/firestore/base.dart'; import 'package:cocoon_service/src/model/firestore/pr_check_runs.dart'; import 'package:cocoon_service/src/service/cache_service.dart'; +import 'package:cocoon_service/src/service/flags/dynamic_config.dart'; +import 'package:cocoon_service/src/service/flags/unified_check_run_flow_flags.dart'; import 'package:cocoon_service/src/service/luci_build_service.dart'; +import 'package:cocoon_service/src/service/luci_build_service/build_tags.dart'; import 'package:cocoon_service/src/service/luci_build_service/engine_artifacts.dart'; import 'package:cocoon_service/src/service/luci_build_service/user_data.dart'; import 'package:fixnum/fixnum.dart'; @@ -400,6 +404,151 @@ void main() { ); }); + group('Unified Check Run Flow', () { + test( + 'schedules try builds with unified check run flow and guard', + () async { + final pullRequest = generatePullRequest( + id: 1, + repo: 'flutter', + headSha: 'headsha123', + ); + + final buildTarget = generateTarget( + 1, + properties: {'os': 'abc'}, + slug: RepositorySlug.full('flutter/flutter'), + name: 'Linux foo', + ); + + // Enable Unified Check Run Flow + luci = LuciBuildService( + config: FakeConfig( + dynamicConfig: DynamicConfig( + unifiedCheckRunFlow: UnifiedCheckRunFlow(useForAll: true), + ), + ), + cache: CacheService(inMemory: true), + buildBucketClient: mockBuildBucketClient, + githubChecksUtil: mockGithubChecksUtil, + pubsub: pubSub, + gerritService: gerritService, + firestore: firestore, + ); + + final checkRunGuard = generateCheckRun(1234, name: 'Guard'); + + await expectLater( + luci.scheduleTryBuilds( + pullRequest: pullRequest, + targets: [buildTarget], + engineArtifacts: EngineArtifacts.builtFromSource( + commitSha: pullRequest.head!.sha!, + ), + checkRunGuard: checkRunGuard, + stage: CiStage.fusionTests, + ), + completion([isTarget.hasName('Linux foo')]), + ); + + // Should NOT create individual check runs + verifyNever(mockGithubChecksUtil.createCheckRun(any, any, any, any)); + + final bbv2.ScheduleBuildRequest scheduleBuild; + { + final batchRequest = bbv2.BatchRequest().createEmptyInstance(); + batchRequest.mergeFromProto3Json(pubSub.messages.single); + scheduleBuild = batchRequest.requests.single.scheduleBuild; + } + + final userData = PresubmitUserData.fromBytes( + scheduleBuild.notify.userData, + ); + expect(userData.guardCheckRunId, 1234); + expect(userData.stage, CiStage.fusionTests); + expect(userData.checkRunId, isNull); + + final tags = BuildTags.fromStringPairs(scheduleBuild.tags); + expect( + tags.buildTags.contains( + GuardCheckRunIdBuildTag(guardCheckRunId: 1234), + ), + isTrue, + reason: 'Should have GuardCheckRunIdBuildTag', + ); + }, + ); + + test('falls back to individual check runs when guard is missing', () async { + final pullRequest = generatePullRequest( + id: 1, + repo: 'flutter', + headSha: 'headsha123', + ); + + final buildTarget = generateTarget( + 1, + properties: {'os': 'abc'}, + slug: RepositorySlug.full('flutter/flutter'), + name: 'Linux foo', + ); + + // Enable Unified Check Run Flow but provide NO guard + luci = LuciBuildService( + config: FakeConfig( + dynamicConfig: DynamicConfig( + unifiedCheckRunFlow: UnifiedCheckRunFlow(useForAll: true), + ), + ), + cache: CacheService(inMemory: true), + buildBucketClient: mockBuildBucketClient, + githubChecksUtil: mockGithubChecksUtil, + pubsub: pubSub, + gerritService: gerritService, + firestore: firestore, + ); + + when( + mockGithubChecksUtil.createCheckRun(any, any, any, any), + ).thenAnswer((_) async => generateCheckRun(456, name: 'Linux foo')); + + await expectLater( + luci.scheduleTryBuilds( + pullRequest: pullRequest, + targets: [buildTarget], + engineArtifacts: EngineArtifacts.builtFromSource( + commitSha: pullRequest.head!.sha!, + ), + checkRunGuard: null, // No guard provided + ), + completion([isTarget.hasName('Linux foo')]), + ); + + // Should create individual check run as fallback + verify( + mockGithubChecksUtil.createCheckRun( + any, + RepositorySlug.full('flutter/flutter'), + 'headsha123', + 'Linux foo', + ), + ).called(1); + + final bbv2.ScheduleBuildRequest scheduleBuild; + { + final batchRequest = bbv2.BatchRequest().createEmptyInstance(); + batchRequest.mergeFromProto3Json(pubSub.messages.single); + scheduleBuild = batchRequest.requests.single.scheduleBuild; + } + + final userData = PresubmitUserData.fromBytes( + scheduleBuild.notify.userData, + ); + expect(userData.checkRunId, 456); + expect(userData.guardCheckRunId, isNull); + }); + }); + group('CIPD', () { final loggedFallingBackToDefaultRecipe = bufferedLoggerOf( contains(logThat(message: contains('Falling back to default recipe'))), diff --git a/app_dart/test/service/luci_build_service/user_data_test.dart b/app_dart/test/service/luci_build_service/user_data_test.dart index 3c4305114..d810103ed 100644 --- a/app_dart/test/service/luci_build_service/user_data_test.dart +++ b/app_dart/test/service/luci_build_service/user_data_test.dart @@ -30,6 +30,9 @@ void main() { 'commit_branch': 'main', 'repo_owner': 'repo-owner', 'repo_name': 'repo-name', + 'guard_check_run_id': null, + 'pull_request_number': null, + 'stage': null, }); }); @@ -42,6 +45,8 @@ void main() { 'commit_branch': 'main', 'repo_owner': 'repo-owner', 'repo_name': 'repo-name', + 'guard_check_run_id': null, + 'stage': null, }), PresubmitUserData( checkRunId: 1234, diff --git a/app_dart/test/service/scheduler/hash_workflow_test.dart b/app_dart/test/service/scheduler/hash_workflow_test.dart index ddf1e70f2..7bee9f98e 100644 --- a/app_dart/test/service/scheduler/hash_workflow_test.dart +++ b/app_dart/test/service/scheduler/hash_workflow_test.dart @@ -60,6 +60,7 @@ void main() { targets: anyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), ), ).thenAnswer((inv) async { return []; diff --git a/app_dart/test/service/scheduler_test.dart b/app_dart/test/service/scheduler_test.dart index ac1b1d178..8602c9e97 100644 --- a/app_dart/test/service/scheduler_test.dart +++ b/app_dart/test/service/scheduler_test.dart @@ -12,20 +12,25 @@ import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/ci_yaml/ci_yaml.dart'; import 'package:cocoon_service/src/model/ci_yaml/target.dart'; +import 'package:cocoon_service/src/model/commit_ref.dart'; import 'package:cocoon_service/src/model/firestore/base.dart'; import 'package:cocoon_service/src/model/firestore/ci_staging.dart'; import 'package:cocoon_service/src/model/firestore/commit.dart' as fs; import 'package:cocoon_service/src/model/firestore/pr_check_runs.dart'; +import 'package:cocoon_service/src/model/firestore/presubmit_check.dart'; +import 'package:cocoon_service/src/model/firestore/presubmit_guard.dart'; import 'package:cocoon_service/src/model/firestore/task.dart' as fs; import 'package:cocoon_service/src/model/github/checks.dart' as cocoon_checks; import 'package:cocoon_service/src/service/big_query.dart'; +import 'package:cocoon_service/src/service/firestore/unified_check_run.dart'; import 'package:cocoon_service/src/service/flags/dynamic_config.dart'; +import 'package:cocoon_service/src/service/flags/unified_check_run_flow_flags.dart'; import 'package:cocoon_service/src/service/luci_build_service/engine_artifacts.dart'; import 'package:cocoon_service/src/service/luci_build_service/pending_task.dart'; +import 'package:cocoon_service/src/service/luci_build_service/user_data.dart'; import 'package:cocoon_service/src/service/scheduler/process_check_run_result.dart'; import 'package:fixnum/fixnum.dart'; import 'package:github/github.dart'; -import 'package:github/hooks.dart'; import 'package:googleapis/bigquery/v2.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -956,6 +961,8 @@ void main() { targets: anyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ).thenAnswer((inv) async { return []; @@ -1369,6 +1376,8 @@ targets: targets: anyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ).thenAnswer((inv) async { return []; @@ -1454,6 +1463,8 @@ targets: targets: captureAnyNamed('targets'), pullRequest: captureAnyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ); expect(result.callCount, 1); @@ -1506,6 +1517,8 @@ targets: targets: anyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ).thenAnswer((inv) async { return []; @@ -1595,6 +1608,8 @@ targets: targets: captureAnyNamed('targets'), pullRequest: captureAnyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ); expect(result.callCount, 1); @@ -1830,6 +1845,8 @@ targets: targets: anyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ).thenAnswer((Invocation i) async { engineArtifacts = @@ -2153,6 +2170,8 @@ targets: targets: anyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ).thenAnswer((inv) async { return []; @@ -2226,6 +2245,8 @@ targets: targets: captureAnyNamed('targets'), pullRequest: captureAnyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ); expect(result.callCount, 1); @@ -2728,6 +2749,8 @@ targets: targets: anyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ).thenAnswer((inv) async { return []; @@ -2796,6 +2819,8 @@ targets: targets: captureAnyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ); expect(result.callCount, 1); @@ -2858,6 +2883,8 @@ targets: targets: anyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ).thenAnswer((inv) async { return []; @@ -2994,6 +3021,8 @@ targets: targets: anyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ).thenAnswer((inv) async { return []; @@ -3122,6 +3151,8 @@ targets: targets: anyNamed('targets'), pullRequest: anyNamed('pullRequest'), engineArtifacts: anyNamed('engineArtifacts'), + checkRunGuard: anyNamed('checkRunGuard'), + stage: anyNamed('stage'), ), ).thenAnswer((inv) async { return []; @@ -3504,6 +3535,273 @@ targets: ); }); }); + group('process unified check run', () { + late _CapturingFakeLuciBuildService fakeLuciBuildService; + + setUp(() { + fakeLuciBuildService = _CapturingFakeLuciBuildService(); + scheduler = Scheduler( + cache: cache, + config: config, + githubChecksService: GithubChecksService( + config, + githubChecksUtil: mockGithubChecksUtil, + ), + getFilesChanged: getFilesChanged, + ciYamlFetcher: ciYamlFetcher, + luciBuildService: fakeLuciBuildService, + contentAwareHash: fakeContentAwareHash, + firestore: firestore, + bigQuery: bigQuery, + ); + }); + + test('schedules tests after engine stage', () async { + final pullRequest = generatePullRequest(); + final checkRunGuard = generateCheckRun(1234, name: 'Merge Queue Guard'); + + await PrCheckRuns.initializeDocument( + firestoreService: firestore, + checks: [checkRunGuard], + pullRequest: pullRequest, + ); + + // Initialize presubmit guard for engine stage + await UnifiedCheckRun.initializePresubmitGuardDocument( + firestoreService: firestore, + slug: pullRequest.base!.repo!.slug(), + pullRequestId: pullRequest.number!, + checkRun: checkRunGuard, + stage: CiStage.fusionEngineBuild, + commitSha: pullRequest.head!.sha!, + creationTime: DateTime.now().millisecondsSinceEpoch, + author: pullRequest.user!.login!, + tasks: ['Linux engine_build'], + ); + + // Initialize check run for the task + firestore.putDocument( + PresubmitCheck.init( + buildName: 'Linux engine_build', + checkRunId: checkRunGuard.id!, + creationTime: DateTime.now().millisecondsSinceEpoch, + ), + ); + + // Enable fusion + ciYamlFetcher.setCiYamlFrom(singleCiYaml, engine: fusionCiYaml); + config.dynamicConfig = DynamicConfig( + unifiedCheckRunFlow: UnifiedCheckRunFlow(useForAll: true), + ); + + final userData = PresubmitUserData( + commit: CommitRef( + slug: pullRequest.base!.repo!.slug(), + sha: pullRequest.head!.sha!, + branch: pullRequest.head!.ref!, + ), + guardCheckRunId: checkRunGuard.id, + stage: CiStage.fusionEngineBuild, + checkSuiteId: 2, + pullRequestNumber: pullRequest.number, + ); + + final build = generateBbv2Build( + Int64(1), + name: 'Linux engine_build', + tags: [ + bbv2.StringPair(key: 'current_attempt', value: '1'), + bbv2.StringPair( + key: 'buildset', + value: 'sha/git/${pullRequest.head!.sha!}', + ), + ], + ); + + expect( + await scheduler.processUnifiedCheckRunCompleted(build, userData), + isTrue, + ); + + // Should schedule tests for the next stage (fusionTests) + expect(fakeLuciBuildService.scheduledTryBuilds, isNotEmpty); + expect(fakeLuciBuildService.stage, CiStage.fusionTests); + // Verify state update in Firestore + final guards = await firestore.query(PresubmitGuard.collectionId, {}); + final guard = guards + .map(PresubmitGuard.fromDocument) + .firstWhere((g) => g.stage == CiStage.fusionEngineBuild); + expect(guard.builds?['Linux engine_build'], TaskStatus.succeeded); + expect(guard.remainingBuilds, 0); + }); + + test( + 'fails the merge queue guard when a test check run fails (merge group)', + () async { + final pullRequest = generatePullRequest(); + final checkRunGuard = generateCheckRun( + 1234, + name: 'Merge Queue Guard', + startedAt: DateTime.now(), + ); + + await PrCheckRuns.initializeDocument( + firestoreService: firestore, + checks: [checkRunGuard], + pullRequest: pullRequest, + ); + + // Make it look like a merge group + // checkRunGuard.checkSuite!.headBranch = 'gh-readonly-queue/master/pr-123-abc'; + + // Initialize presubmit guard for tests stage + await UnifiedCheckRun.initializePresubmitGuardDocument( + firestoreService: firestore, + slug: pullRequest.base!.repo!.slug(), + pullRequestId: pullRequest.number!, + checkRun: checkRunGuard, + stage: CiStage.fusionTests, + commitSha: pullRequest.head!.sha!, + creationTime: DateTime.now().millisecondsSinceEpoch, + author: pullRequest.user!.login!, + tasks: ['Linux test'], + ); + + // Initialize check run for the task + firestore.putDocument( + PresubmitCheck.init( + buildName: 'Linux test', + checkRunId: checkRunGuard.id!, + creationTime: DateTime.now().millisecondsSinceEpoch, + ), + ); + + final userData = PresubmitUserData( + commit: CommitRef( + slug: pullRequest.base!.repo!.slug(), + sha: pullRequest.head!.sha!, + branch: 'gh-readonly-queue/master/pr-123-abc', + ), + guardCheckRunId: checkRunGuard.id, + stage: CiStage.fusionTests, + checkSuiteId: 2, + pullRequestNumber: pullRequest.number, + ); + + final build = generateBbv2Build( + Int64(1), + name: 'Linux test', + status: bbv2.Status.FAILURE, + tags: [bbv2.StringPair(key: 'current_attempt', value: '1')], + ); + + expect( + await scheduler.processUnifiedCheckRunCompleted(build, userData), + isTrue, + ); + + verify( + mockGithubChecksUtil.updateCheckRun( + any, + any, + any, + status: anyNamed('status'), + conclusion: CheckRunConclusion.failure, // Merge queue failure + output: anyNamed('output'), + ), + ).called(1); + + final guards = await firestore.query(PresubmitGuard.collectionId, {}); + final guard = PresubmitGuard.fromDocument(guards.single); + expect(guard.failedBuilds, 1); + }, + ); + + test('closes merge queue guard in merge group success', () async { + final pullRequest = generatePullRequest(); + final checkRunGuard = generateCheckRun( + 1234, + name: 'Merge Queue Guard', + startedAt: DateTime.now(), + ); + + await PrCheckRuns.initializeDocument( + firestoreService: firestore, + checks: [checkRunGuard], + pullRequest: pullRequest, + ); + + // Make it look like a merge group + // checkRunGuard.checkSuite!.headBranch = 'gh-readonly-queue/master/pr-123-abc'; + + // Initialize presubmit guard for tests stage + await UnifiedCheckRun.initializePresubmitGuardDocument( + firestoreService: firestore, + slug: pullRequest.base!.repo!.slug(), + pullRequestId: pullRequest.number!, + checkRun: checkRunGuard, + stage: CiStage.fusionTests, + commitSha: pullRequest.head!.sha!, + creationTime: DateTime.now().millisecondsSinceEpoch, + author: pullRequest.user!.login!, + tasks: ['Linux test'], + ); + + // Initialize check run for the task + firestore.putDocument( + PresubmitCheck.init( + buildName: 'Linux test', + checkRunId: checkRunGuard.id!, + creationTime: DateTime.now().millisecondsSinceEpoch, + ), + ); + + final userData = PresubmitUserData( + commit: CommitRef( + slug: pullRequest.base!.repo!.slug(), + sha: pullRequest.head!.sha!, + branch: 'gh-readonly-queue/master/pr-123-abc', + ), + guardCheckRunId: checkRunGuard.id, + stage: CiStage.fusionTests, + checkSuiteId: 2, + pullRequestNumber: pullRequest.number, + ); + + final build = generateBbv2Build( + Int64(1), + name: 'Linux test', + status: bbv2.Status.SUCCESS, + tags: [ + bbv2.StringPair(key: 'current_attempt', value: '1'), + bbv2.StringPair( + key: 'buildset', + value: 'sha/git/${pullRequest.head!.sha!}', + ), + ], + ); + + expect( + await scheduler.processUnifiedCheckRunCompleted(build, userData), + isTrue, + ); + + verify( + mockGithubChecksUtil.updateCheckRun( + any, + any, + any, + status: anyNamed('status'), + conclusion: CheckRunConclusion.success, // Merge queue success + output: anyNamed('output'), + ), + ).called(1); + + final guards = await firestore.query(PresubmitGuard.collectionId, {}); + final guard = PresubmitGuard.fromDocument(guards.single); + expect(guard.remainingBuilds, 0); + }); + }); }); } @@ -3511,16 +3809,23 @@ final class _CapturingFakeLuciBuildService extends Fake implements LuciBuildService { List scheduledTryBuilds = []; EngineArtifacts? engineArtifacts; + PullRequest? pullRequest; + CheckRun? checkRunGuard; + CiStage? stage; @override Future> scheduleTryBuilds({ required List targets, required PullRequest pullRequest, - CheckSuiteEvent? checkSuiteEvent, - EngineArtifacts? engineArtifacts, + required EngineArtifacts engineArtifacts, + CheckRun? checkRunGuard, + CiStage? stage, }) async { scheduledTryBuilds = targets; this.engineArtifacts = engineArtifacts; + this.pullRequest = pullRequest; + this.checkRunGuard = checkRunGuard; + this.stage = stage; return targets; } diff --git a/app_dart/test/src/utilities/mocks.mocks.dart b/app_dart/test/src/utilities/mocks.mocks.dart index afa677853..9aa263480 100644 --- a/app_dart/test/src/utilities/mocks.mocks.dart +++ b/app_dart/test/src/utilities/mocks.mocks.dart @@ -12,13 +12,14 @@ import 'package:buildbucket/buildbucket_pb.dart' as _i6; import 'package:cocoon_common/rpc_model.dart' as _i19; import 'package:cocoon_service/cocoon_service.dart' as _i17; import 'package:cocoon_service/src/foundation/github_checks_util.dart' as _i10; -import 'package:cocoon_service/src/model/ci_yaml/ci_yaml.dart' as _i40; +import 'package:cocoon_service/src/model/ci_yaml/ci_yaml.dart' as _i41; import 'package:cocoon_service/src/model/ci_yaml/target.dart' as _i28; -import 'package:cocoon_service/src/model/commit_ref.dart' as _i32; -import 'package:cocoon_service/src/model/firestore/commit.dart' as _i38; -import 'package:cocoon_service/src/model/firestore/task.dart' as _i33; -import 'package:cocoon_service/src/model/github/checks.dart' as _i31; -import 'package:cocoon_service/src/model/github/workflow_job.dart' as _i39; +import 'package:cocoon_service/src/model/commit_ref.dart' as _i33; +import 'package:cocoon_service/src/model/firestore/base.dart' as _i30; +import 'package:cocoon_service/src/model/firestore/commit.dart' as _i39; +import 'package:cocoon_service/src/model/firestore/task.dart' as _i34; +import 'package:cocoon_service/src/model/github/checks.dart' as _i32; +import 'package:cocoon_service/src/model/github/workflow_job.dart' as _i40; import 'package:cocoon_service/src/service/big_query.dart' as _i18; import 'package:cocoon_service/src/service/commit_service.dart' as _i21; import 'package:cocoon_service/src/service/config.dart' as _i2; @@ -26,18 +27,18 @@ import 'package:cocoon_service/src/service/discord_service.dart' as _i25; import 'package:cocoon_service/src/service/flags/dynamic_config.dart' as _i24; import 'package:cocoon_service/src/service/github_service.dart' as _i9; import 'package:cocoon_service/src/service/luci_build_service/build_tags.dart' - as _i36; + as _i37; import 'package:cocoon_service/src/service/luci_build_service/cipd_version.dart' as _i23; import 'package:cocoon_service/src/service/luci_build_service/engine_artifacts.dart' as _i29; import 'package:cocoon_service/src/service/luci_build_service/pending_task.dart' - as _i35; + as _i36; import 'package:cocoon_service/src/service/luci_build_service/user_data.dart' - as _i30; + as _i31; import 'package:cocoon_service/src/service/scheduler/process_check_run_result.dart' - as _i41; -import 'package:fixnum/fixnum.dart' as _i34; + as _i42; +import 'package:fixnum/fixnum.dart' as _i35; import 'package:github/github.dart' as _i7; import 'package:github/hooks.dart' as _i22; import 'package:googleapis/bigquery/v2.dart' as _i4; @@ -49,7 +50,7 @@ import 'package:http/http.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i20; import 'package:neat_cache/neat_cache.dart' as _i16; -import 'package:process/src/interface/process_manager.dart' as _i37; +import 'package:process/src/interface/process_manager.dart' as _i38; import '../../service/cache_service_test.dart' as _i26; @@ -1886,14 +1887,6 @@ class MockGithubChecksService extends _i1.Mock ) as _i13.Future); - @override - bool taskFailed(_i6.Status? status) => - (super.noSuchMethod( - Invocation.method(#taskFailed, [status]), - returnValue: false, - ) - as bool); - @override String getGithubSummary(String? summary) => (super.noSuchMethod( @@ -4074,12 +4067,16 @@ class MockLuciBuildService extends _i1.Mock implements _i17.LuciBuildService { required List<_i28.Target>? targets, required _i7.PullRequest? pullRequest, required _i29.EngineArtifacts? engineArtifacts, + _i7.CheckRun? checkRunGuard, + _i30.CiStage? stage, }) => (super.noSuchMethod( Invocation.method(#scheduleTryBuilds, [], { #targets: targets, #pullRequest: pullRequest, #engineArtifacts: engineArtifacts, + #checkRunGuard: checkRunGuard, + #stage: stage, }), returnValue: _i13.Future>.value(<_i28.Target>[]), ) @@ -4120,7 +4117,7 @@ class MockLuciBuildService extends _i1.Mock implements _i17.LuciBuildService { required String? builderName, required _i6.Build? build, required int? nextAttempt, - required _i30.PresubmitUserData? userData, + required _i31.PresubmitUserData? userData, }) => (super.noSuchMethod( Invocation.method(#reschedulePresubmitBuild, [], { @@ -4145,10 +4142,10 @@ class MockLuciBuildService extends _i1.Mock implements _i17.LuciBuildService { @override _i13.Future reschedulePostsubmitBuildUsingCheckRunEvent( - _i31.CheckRunEvent? checkRunEvent, { - required _i32.CommitRef? commit, + _i32.CheckRunEvent? checkRunEvent, { + required _i33.CommitRef? commit, required _i28.Target? target, - required _i33.Task? task, + required _i34.Task? task, }) => (super.noSuchMethod( Invocation.method( @@ -4163,7 +4160,7 @@ class MockLuciBuildService extends _i1.Mock implements _i17.LuciBuildService { @override _i13.Future<_i6.Build> getBuildById( - _i34.Int64? id, { + _i35.Int64? id, { _i6.BuildMask? buildMask, }) => (super.noSuchMethod( @@ -4192,9 +4189,9 @@ class MockLuciBuildService extends _i1.Mock implements _i17.LuciBuildService { as _i13.Future>); @override - _i13.Future> schedulePostsubmitBuilds({ - required _i32.CommitRef? commit, - required List<_i35.PendingTask>? toBeScheduled, + _i13.Future> schedulePostsubmitBuilds({ + required _i33.CommitRef? commit, + required List<_i36.PendingTask>? toBeScheduled, String? contentHash, }) => (super.noSuchMethod( @@ -4203,15 +4200,15 @@ class MockLuciBuildService extends _i1.Mock implements _i17.LuciBuildService { #toBeScheduled: toBeScheduled, #contentHash: contentHash, }), - returnValue: _i13.Future>.value( - <_i35.PendingTask>[], + returnValue: _i13.Future>.value( + <_i36.PendingTask>[], ), ) - as _i13.Future>); + as _i13.Future>); @override _i13.Future scheduleMergeGroupBuilds({ - required _i32.CommitRef? commit, + required _i33.CommitRef? commit, required List<_i28.Target>? targets, String? contentHash, }) => @@ -4228,7 +4225,7 @@ class MockLuciBuildService extends _i1.Mock implements _i17.LuciBuildService { @override _i13.Future<_i7.CheckRun> createPostsubmitCheckRun( - _i32.CommitRef? commit, + _i33.CommitRef? commit, _i28.Target? target, ) => (super.noSuchMethod( @@ -4244,10 +4241,10 @@ class MockLuciBuildService extends _i1.Mock implements _i17.LuciBuildService { @override _i13.Future rerunBuilder({ - required _i32.CommitRef? commit, + required _i33.CommitRef? commit, required _i28.Target? target, - required _i33.Task? task, - Iterable<_i36.BuildTag>? tags = const [], + required _i34.Task? task, + Iterable<_i37.BuildTag>? tags = const [], }) => (super.noSuchMethod( Invocation.method(#rerunBuilder, [], { @@ -4262,8 +4259,8 @@ class MockLuciBuildService extends _i1.Mock implements _i17.LuciBuildService { @override _i13.Future rerunDartInternalReleaseBuilder({ - required _i32.CommitRef? commit, - required _i33.Task? task, + required _i33.CommitRef? commit, + required _i34.Task? task, }) => (super.noSuchMethod( Invocation.method(#rerunDartInternalReleaseBuilder, [], { @@ -4278,7 +4275,7 @@ class MockLuciBuildService extends _i1.Mock implements _i17.LuciBuildService { /// A class which mocks [ProcessManager]. /// /// See the documentation for Mockito's code generation for more information. -class MockProcessManager extends _i1.Mock implements _i37.ProcessManager { +class MockProcessManager extends _i1.Mock implements _i38.ProcessManager { MockProcessManager() { _i1.throwOnMissingStub(this); } @@ -5438,7 +5435,7 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { } @override - _i13.Future addCommits(List<_i38.Commit>? commits) => + _i13.Future addCommits(List<_i39.Commit>? commits) => (super.noSuchMethod( Invocation.method(#addCommits, [commits]), returnValue: _i13.Future.value(), @@ -5508,7 +5505,7 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { @override _i13.Future handleMergeGroupEvent({ - required _i31.MergeGroupEvent? mergeGroupEvent, + required _i32.MergeGroupEvent? mergeGroupEvent, }) => (super.noSuchMethod( Invocation.method(#handleMergeGroupEvent, [], { @@ -5541,7 +5538,7 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { as _i13.Future); @override - _i13.Future processWorkflowJob(_i39.WorkflowJobEvent? event) => + _i13.Future processWorkflowJob(_i40.WorkflowJobEvent? event) => (super.noSuchMethod( Invocation.method(#processWorkflowJob, [event]), returnValue: _i13.Future.value(), @@ -5554,7 +5551,7 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { String? baseRef, _i7.RepositorySlug? slug, String? headSha, - dynamic stage, + _i30.CiStage? stage, ) => (super.noSuchMethod( Invocation.method(#getMergeGroupTargetsForStage, [ @@ -5572,7 +5569,7 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { String? baseRef, _i7.RepositorySlug? slug, String? headSha, { - _i40.CiType? type = _i40.CiType.any, + _i41.CiType? type = _i41.CiType.any, }) => (super.noSuchMethod( Invocation.method( @@ -5664,7 +5661,7 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { @override _i13.Future> getPresubmitTargets( _i7.PullRequest? pullRequest, { - _i40.CiType? type = _i40.CiType.any, + _i41.CiType? type = _i41.CiType.any, }) => (super.noSuchMethod( Invocation.method( @@ -5678,7 +5675,7 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { @override _i13.Future processCheckRunCompleted( - _i31.CheckRun? checkRun, + _i32.CheckRun? checkRun, _i7.RepositorySlug? slug, ) => (super.noSuchMethod( @@ -5688,7 +5685,21 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { as _i13.Future); @override - bool detectMergeGroup(_i31.CheckRun? checkRun) => + _i13.Future processUnifiedCheckRunCompleted( + _i6.Build? build, + _i31.PresubmitUserData? userData, + ) => + (super.noSuchMethod( + Invocation.method(#processUnifiedCheckRunCompleted, [ + build, + userData, + ]), + returnValue: _i13.Future.value(false), + ) + as _i13.Future); + + @override + bool detectMergeGroup(_i32.CheckRun? checkRun) => (super.noSuchMethod( Invocation.method(#detectMergeGroup, [checkRun]), returnValue: false, @@ -5697,7 +5708,7 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { @override _i13.Future proceedToCiTestingStage({ - required _i31.CheckRun? checkRun, + required _i32.CheckRun? checkRun, required _i7.RepositorySlug? slug, required String? sha, required String? mergeQueueGuard, @@ -5717,19 +5728,19 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { as _i13.Future); @override - _i13.Future<_i41.ProcessCheckRunResult> processCheckRun( - _i31.CheckRunEvent? checkRunEvent, + _i13.Future<_i42.ProcessCheckRunResult> processCheckRun( + _i32.CheckRunEvent? checkRunEvent, ) => (super.noSuchMethod( Invocation.method(#processCheckRun, [checkRunEvent]), - returnValue: _i13.Future<_i41.ProcessCheckRunResult>.value( - _i20.dummyValue<_i41.ProcessCheckRunResult>( + returnValue: _i13.Future<_i42.ProcessCheckRunResult>.value( + _i20.dummyValue<_i42.ProcessCheckRunResult>( this, Invocation.method(#processCheckRun, [checkRunEvent]), ), ), ) - as _i13.Future<_i41.ProcessCheckRunResult>); + as _i13.Future<_i42.ProcessCheckRunResult>); @override _i7.CheckRun checkRunFromString(String? input) =>