diff --git a/app_dart/lib/src/model/common/presubmit_completed_check.dart b/app_dart/lib/src/model/common/presubmit_completed_check.dart deleted file mode 100644 index 6aba3c4f2e..0000000000 --- a/app_dart/lib/src/model/common/presubmit_completed_check.dart +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2024 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:buildbucket/buildbucket_pb.dart'; -import 'package:cocoon_common/task_status.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:github/github.dart'; -import 'package:meta/meta.dart'; - -import '../../foundation/utils.dart'; -import '../../service/config.dart'; -import '../../service/luci_build_service/build_tags.dart'; -import '../../service/luci_build_service/user_data.dart'; -import '../bbv2_extension.dart'; -import '../firestore/base.dart'; -import '../firestore/presubmit_guard.dart'; -import '../github/checks.dart' as cocoon_checks; -import 'checks_extension.dart'; -import 'presubmit_job_state.dart'; - -/// Unified representation of a completed presubmit job. -/// -/// This class abstracts away the source of the job (GitHub CheckRun or BuildBucket Build) -/// to allow unified processing logic. -@immutable -class PresubmitCompletedJob { - final String name; - final String sha; - final RepositorySlug slug; - final TaskStatus status; - final bool isMergeGroup; - - final int checkRunId; - final int? checkSuiteId; - final String? headBranch; - final bool isUnifiedCheckRun; - final CiStage? stage; - final int? prNum; - final int attempt; - final int? startTime; - final int? endTime; - final String? summary; - final int? buildNumber; - final Int64? buildId; - - const PresubmitCompletedJob({ - required this.name, - required this.sha, - required this.slug, - required this.status, - required this.isMergeGroup, - required this.checkRunId, - required this.checkSuiteId, - required this.headBranch, - required this.isUnifiedCheckRun, - this.stage, - this.prNum, - this.attempt = 1, - this.startTime, - this.endTime, - this.summary, - this.buildNumber, - this.buildId, - }); - - /// Creates a [PresubmitCompletedJob] from a GitHub [CheckRun]. - factory PresubmitCompletedJob.fromCheckRun( - cocoon_checks.CheckRun checkRun, - RepositorySlug slug, - ) { - return PresubmitCompletedJob( - name: checkRun.name!, - sha: checkRun.headSha!, - slug: slug, - status: ChecksExtension.fromConclusion(checkRun.conclusion), - isMergeGroup: _isMergeGroup(checkRun.checkSuite?.headBranch), - checkRunId: checkRun.id!, - checkSuiteId: checkRun.checkSuite?.id, - headBranch: checkRun.checkSuite?.headBranch, - isUnifiedCheckRun: false, - // CheckRun model doesn't have time/summary fields currently - startTime: null, - endTime: null, - summary: null, - buildNumber: null, - buildId: null, - ); - } - - /// Creates a [PresubmitCompletedJob] from a BuildBucket [Build]. - factory PresubmitCompletedJob.fromBuild( - Build build, - PresubmitUserData userData, { - TaskStatus? status, - }) { - return PresubmitCompletedJob( - name: build.builder.builder, - sha: userData.commit.sha, - slug: userData.commit.slug, - status: status ?? build.status.toTaskStatus(), - isMergeGroup: _isMergeGroup(userData.commit.branch), - checkRunId: userData.guardCheckRunId ?? userData.checkRunId!, - checkSuiteId: userData.checkSuiteId, - headBranch: userData.commit.branch, - isUnifiedCheckRun: userData.guardCheckRunId != null, - stage: userData.stage, - prNum: userData.pullRequestNumber, - attempt: _getAttempt(build), - startTime: build.startTime.toDateTime().millisecondsSinceEpoch, - endTime: build.endTime.toDateTime().millisecondsSinceEpoch, - summary: build.summaryMarkdown, - buildNumber: build.number, - buildId: build.id, - ); - } - - static int _getAttempt(Build build) { - final tagSet = BuildTags.fromStringPairs(build.tags); - return tagSet.currentAttempt; - } - - cocoon_checks.CheckRun get checkRun { - return cocoon_checks.CheckRun( - id: checkRunId, - name: isUnifiedCheckRun ? Config.kDashboardCheckName : name, - headSha: sha, - conclusion: status.toConclusion(), - checkSuite: CheckSuite( - id: checkSuiteId, - headBranch: headBranch, - headSha: sha, - conclusion: CheckRunConclusion.empty, - pullRequests: [], - ), - ); - } - - PresubmitGuardId get guardId { - return PresubmitGuardId( - slug: slug, - prNum: prNum ?? 0, - checkRunId: checkRunId, - stage: stage ?? CiStage.fusionTests, - ); - } - - PresubmitJobState get state { - return PresubmitJobState( - jobName: name, - status: status, - attemptNumber: attempt, - startTime: startTime, - endTime: endTime, - summary: summary, - buildNumber: buildNumber, - buildId: buildId, - ); - } - - static bool _isMergeGroup(String? headBranch) { - if (headBranch == null) { - return false; - } - return tryParseGitHubMergeQueueBranch(headBranch).parsed; - } -} 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 10e09508c4..9efe454696 100644 --- a/app_dart/lib/src/model/common/presubmit_guard_conclusion.dart +++ b/app_dart/lib/src/model/common/presubmit_guard_conclusion.dart @@ -32,56 +32,75 @@ enum PresubmitGuardConclusionResult { internalError, } +/// Represents the current state of jobs in a CI stage. +class PresubmitGuardState { + final int remaining; + final int failed; + + const PresubmitGuardState({required this.remaining, required this.failed}); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PresubmitGuardState && + other.remaining == remaining && + other.failed == failed); + + @override + int get hashCode => Object.hash(remaining, failed); + + @override + String toString() => 'PresubmitGuardState("$remaining", "$failed")'; +} + /// Results from attempting to mark a staging task as completed. /// /// See: [PresubmitGuard.markConclusion] class PresubmitGuardConclusion { final PresubmitGuardConclusionResult result; - final int remaining; + final PresubmitGuardState previousState; + final PresubmitGuardState currentState; final String? checkRunGuard; - final int failed; final String summary; final String details; const PresubmitGuardConclusion({ required this.result, - required this.remaining, + required this.previousState, + required this.currentState, required this.checkRunGuard, - required this.failed, required this.summary, required this.details, }); bool get isOk => result == PresubmitGuardConclusionResult.ok; - bool get isPending => isOk && remaining > 0; + bool get isPending => isOk && currentState.remaining > 0; - bool get isFailed => isOk && !isPending && failed > 0; - - bool get isComplete => isOk && !isPending && !isFailed; + bool get isFailed => isOk && currentState.failed > 0; @override bool operator ==(Object other) => identical(this, other) || (other is PresubmitGuardConclusion && other.result == result && - other.remaining == remaining && + other.previousState == previousState && + other.currentState == currentState && other.checkRunGuard == checkRunGuard && - other.failed == failed && other.summary == summary && other.details == details); @override int get hashCode => Object.hashAll([ result, - remaining, + previousState, + currentState, checkRunGuard, - failed, summary, details, ]); @override String toString() => - 'BuildConclusion("$result", "$remaining", "$failed", "$summary", "$details", "$checkRunGuard")'; + 'BuildConclusion("$result", "$previousState", "$currentState", "$summary", "$details", "$checkRunGuard")'; } diff --git a/app_dart/lib/src/model/common/presubmit_job_state.dart b/app_dart/lib/src/model/common/presubmit_job_state.dart index 856b18d40f..c017de465b 100644 --- a/app_dart/lib/src/model/common/presubmit_job_state.dart +++ b/app_dart/lib/src/model/common/presubmit_job_state.dart @@ -1,22 +1,42 @@ -// Copyright 2025 The Flutter Authors. All rights reserved. +// Copyright 2024 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. -/// @docImport 'presubmit_job_state.dart'; -library; - -import 'package:buildbucket/buildbucket_pb.dart' as bbv2; +import 'package:buildbucket/buildbucket_pb.dart'; import 'package:cocoon_common/task_status.dart'; import 'package:fixnum/fixnum.dart'; +import 'package:github/github.dart'; +import 'package:meta/meta.dart'; +import '../../foundation/utils.dart'; +import '../../service/config.dart'; import '../../service/luci_build_service/build_tags.dart'; +import '../../service/luci_build_service/user_data.dart'; import '../bbv2_extension.dart'; +import '../firestore/base.dart'; +import '../firestore/presubmit_guard.dart'; +import '../github/checks.dart' as cocoon_checks; +import 'checks_extension.dart'; -/// Represents the current state of a check run. +/// Unified representation of a completed presubmit job. +/// +/// This class abstracts away the source of the job (GitHub CheckRun or BuildBucket Build) +/// to allow unified processing logic. +@immutable class PresubmitJobState { - final String jobName; + final String name; + final String sha; + final RepositorySlug slug; final TaskStatus status; - final int attemptNumber; //static int _currentAttempt(BuildTags buildTags) + final bool isMergeGroup; + + final int checkRunId; + final int? checkSuiteId; + final String? headBranch; + final bool isUnifiedCheckRun; + final CiStage? stage; + final int? prNum; + final int attempt; final int? startTime; final int? endTime; final String? summary; @@ -24,26 +44,110 @@ class PresubmitJobState { final Int64? buildId; const PresubmitJobState({ - required this.jobName, + required this.name, + required this.sha, + required this.slug, required this.status, - required this.attemptNumber, + required this.isMergeGroup, + required this.checkRunId, + required this.checkSuiteId, + required this.headBranch, + required this.isUnifiedCheckRun, + this.stage, + this.prNum, + this.attempt = 1, this.startTime, this.endTime, this.summary, this.buildNumber, this.buildId, }); -} -extension BuildToPresubmitJobState on bbv2.Build { - PresubmitJobState toPresubmitJobState() => PresubmitJobState( - jobName: builder.builder, - status: status.toTaskStatus(), - attemptNumber: BuildTags.fromStringPairs(tags).currentAttempt, - startTime: startTime.toDateTime().millisecondsSinceEpoch, - endTime: endTime.toDateTime().millisecondsSinceEpoch, - summary: summaryMarkdown, - buildNumber: number, - buildId: id, - ); + /// Creates a [PresubmitJobState] from a GitHub [CheckRun]. + factory PresubmitJobState.fromCheckRun( + cocoon_checks.CheckRun checkRun, + RepositorySlug slug, + ) { + return PresubmitJobState( + name: checkRun.name!, + sha: checkRun.headSha!, + slug: slug, + status: ChecksExtension.fromConclusion(checkRun.conclusion), + isMergeGroup: _isMergeGroup(checkRun.checkSuite?.headBranch), + checkRunId: checkRun.id!, + checkSuiteId: checkRun.checkSuite?.id, + headBranch: checkRun.checkSuite?.headBranch, + isUnifiedCheckRun: false, + // CheckRun model doesn't have time/summary fields currently + startTime: null, + endTime: null, + summary: null, + buildNumber: null, + buildId: null, + ); + } + + /// Creates a [PresubmitJobState] from a BuildBucket [Build]. + factory PresubmitJobState.fromBuild( + Build build, + PresubmitUserData userData, { + TaskStatus? status, + }) { + return PresubmitJobState( + name: build.builder.builder, + sha: userData.commit.sha, + slug: userData.commit.slug, + status: status ?? build.status.toTaskStatus(), + isMergeGroup: _isMergeGroup(userData.commit.branch), + checkRunId: userData.guardCheckRunId ?? userData.checkRunId!, + checkSuiteId: userData.checkSuiteId, + headBranch: userData.commit.branch, + isUnifiedCheckRun: userData.guardCheckRunId != null, + stage: userData.stage, + prNum: userData.pullRequestNumber, + attempt: _getAttempt(build), + startTime: build.startTime.toDateTime().millisecondsSinceEpoch, + endTime: build.endTime.toDateTime().millisecondsSinceEpoch, + summary: build.summaryMarkdown, + buildNumber: build.number, + buildId: build.id, + ); + } + + static int _getAttempt(Build build) { + final tagSet = BuildTags.fromStringPairs(build.tags); + return tagSet.currentAttempt; + } + + cocoon_checks.CheckRun get checkRun { + return cocoon_checks.CheckRun( + id: checkRunId, + name: isUnifiedCheckRun ? Config.kDashboardCheckName : name, + headSha: sha, + conclusion: status.toConclusion(), + checkSuite: CheckSuite( + id: checkSuiteId, + headBranch: headBranch, + headSha: sha, + conclusion: CheckRunConclusion.empty, + pullRequests: [], + ), + ); + } + + PresubmitGuardId get guardId { + return PresubmitGuardId( + slug: slug, + prNum: prNum ?? 0, + checkRunId: checkRunId, + stage: stage ?? CiStage.fusionTests, + ); + } + + static bool _isMergeGroup(String? headBranch) { + if (headBranch == null) { + return false; + } + return tryParseGitHubMergeQueueBranch(headBranch).parsed; + } } diff --git a/app_dart/lib/src/model/firestore/ci_staging.dart b/app_dart/lib/src/model/firestore/ci_staging.dart index cc93406187..703d20b10a 100644 --- a/app_dart/lib/src/model/firestore/ci_staging.dart +++ b/app_dart/lib/src/model/firestore/ci_staging.dart @@ -232,6 +232,7 @@ final class CiStaging extends AppDocument { var failed = -1; var total = -1; var valid = false; + var previousState = const PresubmitGuardState(remaining: -1, failed: -1); String? checkRunGuard; TaskConclusion? recordedConclusion; @@ -273,6 +274,7 @@ final class CiStaging extends AppDocument { throw '$logCrumb: missing field "$kTotalField" for $transaction / ${doc.fields}'; } total = maybeTotal; + previousState = PresubmitGuardState(remaining: remaining, failed: failed); // We will have check_runs scheduled after the engine was built successfully, so missing the checkRun field // is an OK response to have. All fields should have been written at creation time. @@ -286,9 +288,12 @@ final class CiStaging extends AppDocument { await firestoreService.rollback(transaction); return PresubmitGuardConclusion( result: PresubmitGuardConclusionResult.missing, - remaining: remaining, + previousState: previousState, + currentState: PresubmitGuardState( + remaining: remaining, + failed: failed, + ), checkRunGuard: null, - failed: failed, summary: 'Check run "$checkRun" not present in $stage CI stage', details: 'Change $changeCrumb', ); @@ -352,9 +357,9 @@ final class CiStaging extends AppDocument { await firestoreService.rollback(transaction); return PresubmitGuardConclusion( result: PresubmitGuardConclusionResult.internalError, - remaining: -1, + previousState: previousState, + currentState: PresubmitGuardState(remaining: -1, failed: failed), checkRunGuard: null, - failed: failed, summary: 'Internal server error', details: ''' @@ -390,9 +395,9 @@ $stack result: valid ? PresubmitGuardConclusionResult.ok : PresubmitGuardConclusionResult.internalError, - remaining: remaining, + previousState: previousState, + currentState: PresubmitGuardState(remaining: remaining, failed: failed), checkRunGuard: checkRunGuard ?? '', - failed: failed, summary: valid ? 'All tests passed' : 'Not a valid state transition for $checkRun', 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 5878d32117..538cfba8c7 100644 --- a/app_dart/lib/src/request_handlers/presubmit_luci_subscription.dart +++ b/app_dart/lib/src/request_handlers/presubmit_luci_subscription.dart @@ -14,7 +14,7 @@ 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_completed_check.dart'; +import '../model/common/presubmit_job_state.dart'; import '../request_handling/exceptions.dart'; import '../request_handling/subscription_handler.dart'; import '../service/extensions/cache_service_test_suppression.dart'; @@ -168,12 +168,12 @@ final class PresubmitLuciSubscription extends SubscriptionHandler { // Process to the check-run status in the merge queue document during // the LUCI callback. if (config.flags.closeMqGuardAfterPresubmit || isUnifiedCheckRun) { - final check = PresubmitCompletedJob.fromBuild( + final check = PresubmitJobState.fromBuild( build, userData, status: override == .neutral ? .neutral : null, ); - await _scheduler.processCheckRunCompleted(check); + await _scheduler.processJobStatusUpdate(check); } } 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 7d621829a3..9f504bdb09 100644 --- a/app_dart/lib/src/service/firestore/unified_check_run.dart +++ b/app_dart/lib/src/service/firestore/unified_check_run.dart @@ -521,7 +521,7 @@ final class UnifiedCheckRun { final changeCrumb = '${guardId.slug.owner}_${guardId.slug.name}_${guardId.prNum}_${guardId.checkRunId}'; final logCrumb = - 'markConclusion(${changeCrumb}_${guardId.stage}, ${state.jobName}, ${state.status}, ${state.attemptNumber})'; + 'markConclusion(${changeCrumb}_${guardId.stage}, ${state.name}, ${state.status}, ${state.attempt})'; // Marking needs to happen while in a transaction to ensure `remaining` is // updated correctly. For that to happen correctly; we need to perform a @@ -532,6 +532,7 @@ final class UnifiedCheckRun { var remaining = -1; var failed = -1; var valid = false; + var previousState = const PresubmitGuardState(remaining: -1, failed: -1); late final PresubmitGuard presubmitGuard; late final PresubmitJob presubmitJob; @@ -549,20 +550,27 @@ final class UnifiedCheckRun { transaction: transaction, ); presubmitGuard = PresubmitGuard.fromDocument(presubmitGuardDocument); + previousState = PresubmitGuardState( + remaining: presubmitGuard.remainingJobs, + failed: presubmitGuard.failedJobs, + ); // Check if the build is present in the guard before trying to load it. - if (presubmitGuard.jobs[state.jobName] == null) { + if (presubmitGuard.jobs[state.name] == null) { log.info( - '$logCrumb: ${state.jobName} with attemptNumber ${state.attemptNumber} not present for $transaction / ${presubmitGuardDocument.fields}', + '$logCrumb: ${state.name} with attempt ${state.attempt} not present for $transaction / ${presubmitGuardDocument.fields}', ); await firestoreService.rollback(transaction); return PresubmitGuardConclusion( result: PresubmitGuardConclusionResult.missing, - remaining: presubmitGuard.remainingJobs, + previousState: previousState, + currentState: PresubmitGuardState( + remaining: presubmitGuard.remainingJobs, + failed: presubmitGuard.failedJobs, + ), checkRunGuard: presubmitGuard.checkRunJson, - failed: presubmitGuard.failedJobs, summary: - 'Check run "${state.jobName}" not present in ${guardId.stage} CI stage', + 'Check run "${state.name}" not present in ${guardId.stage} CI stage', details: 'Change $changeCrumb', ); } @@ -570,8 +578,8 @@ final class UnifiedCheckRun { final checkDocName = PresubmitJob.documentNameFor( slug: guardId.slug, checkRunId: guardId.checkRunId, - jobName: state.jobName, - attemptNumber: state.attemptNumber, + jobName: state.name, + attemptNumber: state.attempt, ); final presubmitJobDocument = await firestoreService.getDocument( checkDocName, @@ -582,7 +590,7 @@ final class UnifiedCheckRun { remaining = presubmitGuard.remainingJobs; failed = presubmitGuard.failedJobs; final jobs = presubmitGuard.jobs; - var status = jobs[state.jobName]!; + var status = jobs[state.name]!; // If job is waiting for backfill, that means its initiated by github // or re-run. So no processing needed, we should only update appropriate @@ -641,7 +649,7 @@ final class UnifiedCheckRun { valid = true; } } - jobs[state.jobName] = status; + jobs[state.name] = status; presubmitGuard.jobs = jobs; presubmitJob.status = status; } on DetailedApiRequestError catch (e, stack) { @@ -653,9 +661,9 @@ final class UnifiedCheckRun { await firestoreService.rollback(transaction); return PresubmitGuardConclusion( result: PresubmitGuardConclusionResult.internalError, - remaining: -1, + previousState: previousState, + currentState: PresubmitGuardState(remaining: -1, failed: failed), checkRunGuard: null, - failed: failed, summary: 'Internal server error', details: ''' @@ -688,19 +696,19 @@ $stack result: valid ? PresubmitGuardConclusionResult.ok : PresubmitGuardConclusionResult.internalError, - remaining: remaining, + previousState: previousState, + currentState: PresubmitGuardState(remaining: remaining, failed: failed), checkRunGuard: presubmitGuard.checkRunJson, - failed: failed, summary: valid ? 'Successfully updated presubmit guard status' - : 'Not a valid state transition for ${state.jobName}', + : 'Not a valid state transition for ${state.name}', details: valid ? ''' For CI stage ${guardId.stage}: Pending: $remaining Failed: $failed ''' - : 'Attempted to set the state of job ${state.jobName} ' + : 'Attempted to set the state of job ${state.name} ' 'to "${state.status.name}".', ); } catch (e) { diff --git a/app_dart/lib/src/service/scheduler.dart b/app_dart/lib/src/service/scheduler.dart index ced9831a60..3c32ccf980 100644 --- a/app_dart/lib/src/service/scheduler.dart +++ b/app_dart/lib/src/service/scheduler.dart @@ -20,14 +20,12 @@ import '../model/ci_yaml/ci_yaml.dart'; import '../model/ci_yaml/target.dart'; import '../model/commit_ref.dart'; import '../model/common/checks_extension.dart'; -import '../model/common/presubmit_completed_check.dart'; import '../model/common/presubmit_guard_conclusion.dart'; import '../model/common/presubmit_job_state.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; @@ -1121,186 +1119,237 @@ detailsUrl: $detailsUrl /// /// Handles both fusion engine build and test stages, and both pull requests /// and merge groups. - Future processCheckRunCompleted(PresubmitCompletedJob check) async { - if (kCheckRunsToIgnore.contains(check.name)) { - return true; + Future processJobStatusUpdate(PresubmitJobState job) async { + if (kCheckRunsToIgnore.contains(job.name)) { + return; } - final flow = check.isUnifiedCheckRun ? 'unified' : 'github'; - final requestor = check.isMergeGroup ? 'merge group' : 'pull request'; + final flow = job.isUnifiedCheckRun ? 'unified' : 'github'; + final requestor = job.isMergeGroup ? 'merge group' : 'pull request'; final logCrumb = - 'checkCompleted(${check.name}, $flow, $requestor, ${check.slug}, ${check.sha}, ${check.status})'; + 'checkCompleted(${job.name}, $flow, $requestor, ${job.slug}, ${job.sha}, ${job.status})'; - final isFusion = check.slug == Config.flutterSlug; - if (!isFusion && !check.isUnifiedCheckRun) { - return true; + final isFusion = job.slug == Config.flutterSlug; + if (!isFusion && !job.isUnifiedCheckRun) { + return; } - late CiStage stage; - late PresubmitGuardConclusion stagingConclusion; - - if (check.isUnifiedCheckRun) { - stage = check.stage!; - stagingConclusion = await _markUnifiedCheckRunConclusion( - guardId: check.guardId, - state: check.state, - ); + if (job.isUnifiedCheckRun) { + await _processUnifiedCheckRunFlowJobStatusUpdate(logCrumb, job); } else { - // for github flow check runs are processed only if the build succeeded or - // some kind of failure occurred. - if (!check.status.isComplete) { - return true; - } - // Check runs are fired at every stage. However, at this point it is unknown - // if this check run belongs in the engine build stage or in the test stage. - // So first look for it in the engine stage, and if it's missing, look for - // it in the test stage. - stage = CiStage.fusionEngineBuild; - stagingConclusion = await _recordCurrentCiStage( - slug: check.slug, - sha: check.sha, - stage: stage, - name: check.name, - conclusion: check.status.toTaskConclusion(), - ); + await _processGitHubFlowCheckStatusUpdate(logCrumb, job); + } + } + + Future _processUnifiedCheckRunFlowJobStatusUpdate( + String logCrumb, + PresubmitJobState job, + ) async { + final stage = job.stage!; + final conclusion = await _markUnifiedCheckRunConclusion(state: job); - if (stagingConclusion.result == PresubmitGuardConclusionResult.missing) { - // Check run not found in the engine stage. Look for it in the test stage. - stage = CiStage.fusionTests; - stagingConclusion = await _recordCurrentCiStage( - slug: check.slug, - sha: check.sha, - stage: stage, - name: check.name, - conclusion: check.status.toTaskConclusion(), + if (!conclusion.isOk) { + return; + } + + if (conclusion.isFailed) { + // For unified check runs, we fail the guard only for the first failed job. + // Subsequent failures are ignored, but we still log them. + if (conclusion.previousState.failed > 0) { + log.info( + '$logCrumb: check run remains failing, ' + 'previously failed checks ${conclusion.previousState.failed}, ' + 'currently failed checks ${conclusion.currentState.failed}, ' + 'remaining checks ${conclusion.currentState.remaining}', ); + return; } + + final guard = checkRunFromString(conclusion.checkRunGuard!); + final detailsUrl = + 'https://flutter-dashboard.appspot.com/#/presubmit?repo=${job.slug.name}&sha=${job.sha}'; + await _requireActionForGuard( + slug: job.slug, + lock: guard, + headSha: job.sha, + summary: _githubChecksService.getGithubSummaryWithHeader(''' +**[Failed Checks Details]($detailsUrl)** + +''', kDashboardChecksDescription), + details: + 'For ${job.stage} CI stage ${conclusion.currentState.failed} checks failed', + detailsUrl: detailsUrl, + ); + return; } - // 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; + + // Are there tests remaining? Keep waiting. + if (conclusion.isPending) { + log.info( + '$logCrumb: not progressing, remaining work count: ${conclusion.currentState.remaining}', + ); + return; } - // 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 (check.isMergeGroup) { - await _completeArtifacts(check.sha, false); - final guard = checkRunFromString(stagingConclusion.checkRunGuard!); - await failGuardForMergeGroup( - slug: check.slug, - lock: guard, - headSha: check.sha, - summary: stagingConclusion.summary, - details: stagingConclusion.details, + switch (stage) { + case CiStage.fusionEngineBuild: + await _closeSuccessfulEngineBuildStage( + checkRun: job.checkRun, + mergeQueueGuard: conclusion.checkRunGuard!, + slug: job.slug, + sha: job.sha, + logCrumb: logCrumb, + ); + case CiStage.fusionTests: + case CiStage.genericTests: + await _closeSuccessfulTestStage( + mergeQueueGuard: conclusion.checkRunGuard!, + slug: job.slug, + sha: job.sha, + logCrumb: logCrumb, ); - } - return false; } + } - // Are there tests remaining? Keep waiting. - if (stagingConclusion.isPending) { + Future _processGitHubFlowCheckStatusUpdate( + String logCrumb, + PresubmitJobState job, + ) async { + if (!job.status.isComplete) { + return; + } + + var stage = CiStage.fusionEngineBuild; + var conclusion = await _recordCurrentCiStage( + slug: job.slug, + sha: job.sha, + stage: stage, + name: job.name, + conclusion: job.status.toTaskConclusion(), + ); + + if (conclusion.result == PresubmitGuardConclusionResult.missing) { + stage = CiStage.fusionTests; + conclusion = await _recordCurrentCiStage( + slug: job.slug, + sha: job.sha, + stage: stage, + name: job.name, + conclusion: job.status.toTaskConclusion(), + ); + } + + if (job.isMergeGroup) { + await _processMergeGroupCheckStatusUpdate( + logCrumb, + job, + stage, + conclusion, + ); + } else { + await _processRegularCheckStatusUpdate(logCrumb, job, stage, conclusion); + } + } + + Future _processRegularCheckStatusUpdate( + String logCrumb, + PresubmitJobState job, + CiStage stage, + PresubmitGuardConclusion conclusion, + ) async { + if (!conclusion.isOk) { + return; + } + + if (conclusion.isPending || conclusion.isFailed) { log.info( - '$logCrumb: not progressing, remaining work count: ${stagingConclusion.remaining}', + '$logCrumb: not progressing, ' + 'remaining checks count: ${conclusion.currentState.remaining}, ' + 'failed checks count: ${conclusion.currentState.failed}', ); - return false; + return; } - 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 its a unified check run we need to require action on the guard. - if (check.isMergeGroup) { - await _completeArtifacts(check.sha, false); - final guard = checkRunFromString(stagingConclusion.checkRunGuard!); - await failGuardForMergeGroup( - slug: check.slug, - lock: guard, - headSha: check.sha, - summary: stagingConclusion.summary, - details: stagingConclusion.details, + switch (stage) { + case CiStage.fusionEngineBuild: + await _closeSuccessfulEngineBuildStage( + checkRun: job.checkRun, + mergeQueueGuard: conclusion.checkRunGuard!, + slug: job.slug, + sha: job.sha, + logCrumb: logCrumb, ); - } else if (check.isUnifiedCheckRun) { - final guard = checkRunFromString(stagingConclusion.checkRunGuard!); - final detailsUrl = - 'https://flutter-dashboard.appspot.com/#/presubmit?repo=${check.slug.name}&sha=${check.sha}'; - await _requireActionForGuard( - slug: check.slug, - lock: guard, - headSha: check.sha, - summary: _githubChecksService.getGithubSummaryWithHeader(''' -**[Failed Checks Details]($detailsUrl)** + case CiStage.fusionTests: + await _closeSuccessfulTestStage( + mergeQueueGuard: conclusion.checkRunGuard!, + slug: job.slug, + sha: job.sha, + logCrumb: logCrumb, + ); + case CiStage.genericTests: + log.warn('$logCrumb: generic tests have no merge queue guard.'); + break; + } + } -''', kDashboardChecksDescription), - details: - 'For ${check.stage} CI stage ${stagingConclusion.failed} checks failed', - detailsUrl: detailsUrl, + Future _processMergeGroupCheckStatusUpdate( + String logCrumb, + PresubmitJobState job, + CiStage stage, + PresubmitGuardConclusion conclusion, + ) async { + if (!conclusion.isOk) { + if (conclusion.result == PresubmitGuardConclusionResult.internalError && + conclusion.checkRunGuard != null) { + await _completeArtifacts(job.sha, false); + final guard = checkRunFromString(conclusion.checkRunGuard!); + await failGuardForMergeGroup( + slug: job.slug, + lock: guard, + headSha: job.sha, + summary: conclusion.summary, + details: conclusion.details, ); } - return true; + return; + } + + if (conclusion.isPending) { + log.info( + '$logCrumb: not progressing, remaining work count: ${conclusion.currentState.remaining}', + ); + return; + } + + if (conclusion.isFailed) { + await _completeArtifacts(job.sha, false); + final guard = checkRunFromString(conclusion.checkRunGuard!); + await failGuardForMergeGroup( + slug: job.slug, + lock: guard, + headSha: job.sha, + summary: conclusion.summary, + details: conclusion.details, + ); + return; } - // 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 (check.isMergeGroup) { - await _completeArtifacts(check.sha, true); - await _closeMergeQueue( - mergeQueueGuard: stagingConclusion.checkRunGuard!, - slug: check.slug, - sha: check.sha, - stage: CiStage.fusionEngineBuild, - logCrumb: logCrumb, - ); - } else { - await _closeSuccessfulEngineBuildStage( - checkRun: check.checkRun, - mergeQueueGuard: stagingConclusion.checkRunGuard!, - slug: check.slug, - sha: check.sha, - logCrumb: logCrumb, - ); - } - case CiStage.fusionTests: - await _closeSuccessfulTestStage( - mergeQueueGuard: stagingConclusion.checkRunGuard!, - slug: check.slug, - sha: check.sha, + await _completeArtifacts(job.sha, true); + await _closeMergeQueue( + mergeQueueGuard: conclusion.checkRunGuard!, + slug: job.slug, + sha: job.sha, + stage: CiStage.fusionEngineBuild, logCrumb: logCrumb, ); + case CiStage.fusionTests: + log.warn('$logCrumb: fusion tests have no merge group.'); + break; case CiStage.genericTests: - if (check.isUnifiedCheckRun) { - await _closeSuccessfulTestStage( - mergeQueueGuard: stagingConclusion.checkRunGuard!, - slug: check.slug, - sha: check.sha, - logCrumb: logCrumb, - ); - } else { - // generic tests do not have a staging document nor are associated - // with a merge group - they are only used to collect commit stats. - log.warn('$logCrumb: generic tests have no merge queue guard.'); - } + log.warn('$logCrumb: generic tests have no merge group.'); break; } - return true; } Future _completeArtifacts(String commitSha, bool successful) async { @@ -1584,11 +1633,11 @@ $stacktrace } Future _markUnifiedCheckRunConclusion({ - required PresubmitGuardId guardId, required PresubmitJobState state, }) async { + final guardId = state.guardId; final logCrumb = - 'checkCompleted(${state.jobName}, ${state.buildNumber}, ${guardId.stage}, ${guardId.slug}, ${state.status})'; + 'checkCompleted(${state.name}, ${state.buildNumber}, ${guardId.stage}, ${guardId.slug}, ${state.status})'; log.info('$logCrumb: ${guardId.documentId}'); // We're doing a transactional update, which could fail if multiple tasks @@ -1626,8 +1675,8 @@ $stacktrace switch (checkRunEvent.action) { case 'completed': if (!_config.flags.closeMqGuardAfterPresubmit) { - await processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( + await processJobStatusUpdate( + PresubmitJobState.fromCheckRun( checkRunEvent.checkRun!, checkRunEvent.repository!.slug(), ), diff --git a/app_dart/test/model/common/presubmit_check_state_test.dart b/app_dart/test/model/common/presubmit_check_state_test.dart deleted file mode 100644 index ced2acb665..0000000000 --- a/app_dart/test/model/common/presubmit_check_state_test.dart +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2025 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:buildbucket/buildbucket_pb.dart' as bbv2; -import 'package:cocoon_common/task_status.dart'; -import 'package:cocoon_server_test/test_logging.dart'; -import 'package:cocoon_service/src/model/common/presubmit_job_state.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:test/test.dart'; - -void main() { - useTestLoggerPerTest(); - group('PresubmitJobState', () { - test('BuildToPresubmitJobState extension maps build number', () { - final build = bbv2.Build( - id: Int64.MAX_VALUE, - builder: bbv2.BuilderID(builder: 'linux_test'), - status: bbv2.Status.SUCCESS, - number: 12345, - startTime: bbv2.Timestamp(seconds: Int64(1000)), - endTime: bbv2.Timestamp(seconds: Int64(2000)), - summaryMarkdown: 'Summary', - tags: [bbv2.StringPair(key: 'github_checkrun', value: '123')], - ); - - final state = build.toPresubmitJobState(); - expect(state.jobName, 'linux_test'); - expect(state.status, TaskStatus.succeeded); - expect(state.buildNumber, 12345); - expect(state.buildId, Int64.MAX_VALUE); - }); - }); -} diff --git a/app_dart/test/model/common/presubmit_completed_check_test.dart b/app_dart/test/model/common/presubmit_completed_check_test.dart index 79f2ca50d0..8a93327056 100644 --- a/app_dart/test/model/common/presubmit_completed_check_test.dart +++ b/app_dart/test/model/common/presubmit_completed_check_test.dart @@ -6,7 +6,7 @@ import 'package:buildbucket/buildbucket_pb.dart'; import 'package:cocoon_common/task_status.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/src/model/commit_ref.dart'; -import 'package:cocoon_service/src/model/common/presubmit_completed_check.dart'; +import 'package:cocoon_service/src/model/common/presubmit_job_state.dart'; import 'package:cocoon_service/src/model/firestore/base.dart'; import 'package:cocoon_service/src/model/github/checks.dart' as cocoon_checks; import 'package:cocoon_service/src/service/config.dart'; @@ -30,7 +30,7 @@ void main() { conclusion: 'success', ); - final check = PresubmitCompletedJob.fromCheckRun(checkRun, slug); + final check = PresubmitJobState.fromCheckRun(checkRun, slug); expect(check.name, 'test_check'); expect(check.sha, sha); @@ -63,7 +63,7 @@ void main() { checkSuiteId: 456, ); - final check = PresubmitCompletedJob.fromBuild(build, userData); + final check = PresubmitJobState.fromBuild(build, userData); expect(check.name, 'test_builder'); expect(check.sha, sha); @@ -99,7 +99,7 @@ void main() { checkSuiteId: 456, ); - final check = PresubmitCompletedJob.fromBuild(build, userData); + final check = PresubmitJobState.fromBuild(build, userData); expect(check.name, 'test_builder'); expect(check.sha, sha); diff --git a/app_dart/test/model/firestore/ci_staging_test.dart b/app_dart/test/model/firestore/ci_staging_test.dart index fec100b633..ec2886c69d 100644 --- a/app_dart/test/model/firestore/ci_staging_test.dart +++ b/app_dart/test/model/firestore/ci_staging_test.dart @@ -160,9 +160,9 @@ void main() { expect( result, const PresubmitGuardConclusion( - remaining: 1, + previousState: PresubmitGuardState(remaining: 1, failed: 0), + currentState: PresubmitGuardState(remaining: 1, failed: 0), result: PresubmitGuardConclusionResult.missing, - failed: 0, checkRunGuard: null, summary: 'Check run "test" not present in engine CI stage', details: 'Change flutter_flutter_1234', @@ -228,9 +228,9 @@ void main() { expect( result, const PresubmitGuardConclusion( - remaining: 0, + previousState: PresubmitGuardState(remaining: 1, failed: 0), + currentState: PresubmitGuardState(remaining: 0, failed: 0), result: PresubmitGuardConclusionResult.ok, - failed: 0, checkRunGuard: '{}', summary: 'All tests passed', details: ''' @@ -270,9 +270,9 @@ For CI stage engine: expect( result, const PresubmitGuardConclusion( - remaining: 1, + previousState: PresubmitGuardState(remaining: 1, failed: 0), + currentState: PresubmitGuardState(remaining: 1, failed: 0), result: PresubmitGuardConclusionResult.internalError, - failed: 0, checkRunGuard: '{}', summary: 'Not a valid state transition for MacOS build_test', details: @@ -309,9 +309,9 @@ For CI stage engine: expect( result, const PresubmitGuardConclusion( - remaining: 1, + previousState: PresubmitGuardState(remaining: 1, failed: 1), + currentState: PresubmitGuardState(remaining: 1, failed: 0), result: PresubmitGuardConclusionResult.ok, - failed: 0, checkRunGuard: '{}', summary: 'All tests passed', details: ''' @@ -352,9 +352,9 @@ For CI stage engine: expect( result, const PresubmitGuardConclusion( - remaining: 1, + previousState: PresubmitGuardState(remaining: 1, failed: 1), + currentState: PresubmitGuardState(remaining: 1, failed: 0), result: PresubmitGuardConclusionResult.ok, - failed: 0, checkRunGuard: '{}', summary: 'All tests passed', details: ''' @@ -394,9 +394,9 @@ For CI stage engine: expect( result, const PresubmitGuardConclusion( - remaining: 1, + previousState: PresubmitGuardState(remaining: 1, failed: 1), + currentState: PresubmitGuardState(remaining: 1, failed: 1), result: PresubmitGuardConclusionResult.internalError, - failed: 1, checkRunGuard: '{}', summary: 'Not a valid state transition for MacOS build_test', details: @@ -432,9 +432,9 @@ For CI stage engine: expect( result, const PresubmitGuardConclusion( - remaining: 1, + previousState: PresubmitGuardState(remaining: 1, failed: 0), + currentState: PresubmitGuardState(remaining: 1, failed: 1), result: PresubmitGuardConclusionResult.ok, - failed: 1, checkRunGuard: '{}', summary: 'All tests passed', details: ''' diff --git a/app_dart/test/request_handlers/presubmit_luci_subscription_neutral_test.dart b/app_dart/test/request_handlers/presubmit_luci_subscription_neutral_test.dart index 1dfae51952..d880a9d22c 100644 --- a/app_dart/test/request_handlers/presubmit_luci_subscription_neutral_test.dart +++ b/app_dart/test/request_handlers/presubmit_luci_subscription_neutral_test.dart @@ -9,7 +9,7 @@ import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/commit_ref.dart'; -import 'package:cocoon_service/src/model/common/presubmit_completed_check.dart'; +import 'package:cocoon_service/src/model/common/presubmit_job_state.dart'; import 'package:cocoon_service/src/service/luci_build_service/user_data.dart'; import 'package:fixnum/fixnum.dart'; import 'package:github/github.dart' as github; @@ -109,9 +109,7 @@ void main() { ), ).thenAnswer((_) async => true); - when( - mockScheduler.processCheckRunCompleted(any), - ).thenAnswer((_) async => true); + when(mockScheduler.processJobStatusUpdate(any)).thenAnswer((_) async {}); tester.message = createPushMessage( Int64(1), @@ -141,12 +139,12 @@ void main() { // Verify that processCheckRunCompleted was called with TaskStatus.neutral final captured = verify( - mockScheduler.processCheckRunCompleted(captureAny), + mockScheduler.processJobStatusUpdate(captureAny), ).captured; expect(captured, hasLength(1)); expect( captured[0], - isA().having( + isA().having( (e) => e.status, 'status', TaskStatus.neutral, @@ -181,9 +179,7 @@ void main() { ), ).thenAnswer((_) async => true); - when( - mockScheduler.processCheckRunCompleted(any), - ).thenAnswer((_) async => true); + when(mockScheduler.processJobStatusUpdate(any)).thenAnswer((_) async {}); tester.message = createPushMessage( Int64(1), @@ -208,12 +204,12 @@ void main() { // Verify that processCheckRunCompleted was called with TaskStatus.failed final captured = verify( - mockScheduler.processCheckRunCompleted(captureAny), + mockScheduler.processJobStatusUpdate(captureAny), ).captured; expect(captured, hasLength(1)); expect( captured[0], - isA().having( + isA().having( (e) => e.status, 'status', TaskStatus.failed, 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 54f1bb0476..8041ded437 100644 --- a/app_dart/test/request_handlers/presubmit_luci_subscription_test.dart +++ b/app_dart/test/request_handlers/presubmit_luci_subscription_test.dart @@ -9,7 +9,7 @@ import 'package:cocoon_server_test/mocks.dart'; import 'package:cocoon_server_test/test_logging.dart'; import 'package:cocoon_service/cocoon_service.dart'; import 'package:cocoon_service/src/model/commit_ref.dart'; -import 'package:cocoon_service/src/model/common/presubmit_completed_check.dart'; +import 'package:cocoon_service/src/model/common/presubmit_job_state.dart'; import 'package:cocoon_service/src/request_handling/exceptions.dart'; import 'package:cocoon_service/src/service/luci_build_service/build_tags.dart'; import 'package:cocoon_service/src/service/luci_build_service/user_data.dart'; @@ -88,9 +88,7 @@ void main() { when( mockGithubChecksService.conclusionForResult(any), ).thenAnswer((_) => github.CheckRunConclusion.empty); - when( - mockScheduler.processCheckRunCompleted(any), - ).thenAnswer((_) async => true); + when(mockScheduler.processJobStatusUpdate(any)).thenAnswer((_) async {}); tester.message = createPushMessage( Int64(1), @@ -117,7 +115,7 @@ void main() { ), ).called(1); - verify(mockScheduler.processCheckRunCompleted(any)).called(1); + verify(mockScheduler.processJobStatusUpdate(any)).called(1); }); test('Requests when task failed but no need to reschedule', () async { @@ -133,9 +131,7 @@ void main() { when( mockGithubChecksService.conclusionForResult(any), ).thenAnswer((_) => github.CheckRunConclusion.empty); - when( - mockScheduler.processCheckRunCompleted(any), - ).thenAnswer((_) async => true); + when(mockScheduler.processJobStatusUpdate(any)).thenAnswer((_) async {}); final userData = PresubmitUserData( commit: CommitRef( @@ -176,7 +172,7 @@ void main() { slug: anyNamed('slug'), ), ).called(1); - verify(mockScheduler.processCheckRunCompleted(any)).called(1); + verify(mockScheduler.processJobStatusUpdate(any)).called(1); }); test('Requests when task failed but need to reschedule', () async { @@ -215,7 +211,7 @@ void main() { rescheduled: true, ), ).called(1); - verifyNever(mockScheduler.processCheckRunCompleted(any)); + verifyNever(mockScheduler.processJobStatusUpdate(any)); }); test('Build rescheduled when in merge queue', () async { @@ -297,7 +293,7 @@ void main() { rescheduled: true, ), ).called(1); - verifyNever(mockScheduler.processCheckRunCompleted(any)); + verifyNever(mockScheduler.processJobStatusUpdate(any)); }); test('Build not rescheduled if not found in ciYaml list.', () async { @@ -314,9 +310,7 @@ void main() { when( mockGithubChecksService.conclusionForResult(any), ).thenAnswer((_) => github.CheckRunConclusion.empty); - when( - mockScheduler.processCheckRunCompleted(any), - ).thenAnswer((_) async => true); + when(mockScheduler.processJobStatusUpdate(any)).thenAnswer((_) async {}); final userData = PresubmitUserData( commit: CommitRef( @@ -360,7 +354,7 @@ void main() { ), ).called(1); - verify(mockScheduler.processCheckRunCompleted(any)).called(1); + verify(mockScheduler.processJobStatusUpdate(any)).called(1); }); test('Build not rescheduled if ci.yaml fails validation.', () async { @@ -377,9 +371,7 @@ void main() { when( mockGithubChecksService.conclusionForResult(any), ).thenAnswer((_) => github.CheckRunConclusion.empty); - when( - mockScheduler.processCheckRunCompleted(any), - ).thenAnswer((_) async => true); + when(mockScheduler.processJobStatusUpdate(any)).thenAnswer((_) async {}); final userData = PresubmitUserData( checkRunId: 1, @@ -421,7 +413,7 @@ void main() { rescheduled: false, ), ).called(1); - verify(mockScheduler.processCheckRunCompleted(any)).called(1); + verify(mockScheduler.processJobStatusUpdate(any)).called(1); }); test('Pubsub rejected if branch is not enabled.', () async { @@ -528,7 +520,7 @@ void main() { // Check that the build.input.properties extracted from build_large_fields // contains the git_ref property encoded in the test data. expect(build.input.properties.fields, contains('git_ref')); - verifyNever(mockScheduler.processCheckRunCompleted(any)); + verifyNever(mockScheduler.processJobStatusUpdate(any)); }); test('Close the MQ guard once presubmit compleated', () async { @@ -560,19 +552,17 @@ void main() { when( mockGithubChecksService.conclusionForResult(bbv2.Status.SUCCESS), ).thenAnswer((_) => github.CheckRunConclusion.success); - when( - mockScheduler.processCheckRunCompleted(any), - ).thenAnswer((_) async => true); + when(mockScheduler.processJobStatusUpdate(any)).thenAnswer((_) async {}); await tester.post(handler); final captured = verify( - mockScheduler.processCheckRunCompleted(captureAny), + mockScheduler.processJobStatusUpdate(captureAny), ).captured; expect(captured, hasLength(1)); expect( captured[0], - isA() + isA() .having((e) => e.name, 'name', 'Linux C') .having((e) => e.sha, 'sha', 'abc') .having((e) => e.checkRunId, 'checkRunId', 1) @@ -622,9 +612,7 @@ void main() { ), ).thenAnswer((_) async => true); - when( - mockScheduler.processCheckRunCompleted(any), - ).thenAnswer((_) async => true); + when(mockScheduler.processJobStatusUpdate(any)).thenAnswer((_) async {}); tester.message = createPushMessage( Int64(1), diff --git a/app_dart/test/service/firestore/unified_check_run_test.dart b/app_dart/test/service/firestore/unified_check_run_test.dart index 1f8e0f2fc1..5e64cef324 100644 --- a/app_dart/test/service/firestore/unified_check_run_test.dart +++ b/app_dart/test/service/firestore/unified_check_run_test.dart @@ -170,14 +170,21 @@ void main() { }); test('updates check status and remaining count on success', () async { - final state = const PresubmitJobState( - jobName: 'linux', + final state = PresubmitJobState( + name: 'linux', status: TaskStatus.succeeded, - attemptNumber: 1, + attempt: 1, startTime: 2000, endTime: 3000, buildNumber: 456, buildId: Int64.MAX_VALUE, + checkRunId: 123, + checkSuiteId: 2, + headBranch: 'master', + isMergeGroup: false, + sha: '', + slug: slug, + isUnifiedCheckRun: false, ); final result = await UnifiedCheckRun.markConclusion( @@ -187,8 +194,10 @@ void main() { ); expect(result.result, PresubmitGuardConclusionResult.ok); - expect(result.remaining, 1); - expect(result.failed, 0); + expect(result.previousState.remaining, 2); + expect(result.previousState.failed, 0); + expect(result.currentState.remaining, 1); + expect(result.currentState.failed, 0); final checkDoc = await PresubmitJob.fromFirestore( firestoreService, @@ -211,37 +220,57 @@ void main() { final result1 = await UnifiedCheckRun.markConclusion( firestoreService: firestoreService, guardId: guardId, - state: const PresubmitJobState( - jobName: 'linux', + state: PresubmitJobState( + name: 'linux', status: TaskStatus.succeeded, - attemptNumber: 1, + attempt: 1, startTime: 2000, endTime: 3000, + buildId: Int64.MAX_VALUE, + buildNumber: 456, + checkRunId: 123, + checkSuiteId: 2, + headBranch: 'master', + isMergeGroup: false, + sha: '', + slug: slug, + isUnifiedCheckRun: false, ), ); - expect(result1.remaining, 1); - expect(result1.failed, 0); + expect(result1.previousState.remaining, 2); + expect(result1.previousState.failed, 0); + expect(result1.currentState.remaining, 1); + expect(result1.currentState.failed, 0); expect(result1.isOk, true); - expect(result1.isComplete, false); expect(result1.isPending, true); final result2 = await UnifiedCheckRun.markConclusion( firestoreService: firestoreService, guardId: guardId, - state: const PresubmitJobState( - jobName: 'mac', + state: PresubmitJobState( + name: 'mac', status: TaskStatus.succeeded, - attemptNumber: 1, + attempt: 1, startTime: 2000, endTime: 3000, + buildId: Int64.MAX_VALUE, + buildNumber: 456, + checkRunId: 123, + checkSuiteId: 2, + headBranch: 'master', + isMergeGroup: false, + sha: '', + slug: slug, + isUnifiedCheckRun: false, ), ); - expect(result2.remaining, 0); - expect(result2.failed, 0); + expect(result2.previousState.remaining, 1); + expect(result2.previousState.failed, 0); + expect(result2.currentState.remaining, 0); + expect(result2.currentState.failed, 0); expect(result2.isOk, true); - expect(result2.isComplete, true); expect(result2.isPending, false); final checkDoc = await PresubmitJob.fromFirestore( @@ -259,12 +288,21 @@ void main() { ); test('updates check status and failed count on failure', () async { - final state = const PresubmitJobState( - jobName: 'linux', + final state = PresubmitJobState( + name: 'linux', status: TaskStatus.failed, - attemptNumber: 1, + attempt: 1, startTime: 2000, endTime: 3000, + buildId: Int64.MAX_VALUE, + buildNumber: 456, + checkRunId: 123, + checkSuiteId: 2, + headBranch: 'master', + isMergeGroup: false, + sha: '', + slug: slug, + isUnifiedCheckRun: false, ); final result = await UnifiedCheckRun.markConclusion( @@ -274,15 +312,28 @@ void main() { ); expect(result.result, PresubmitGuardConclusionResult.ok); - expect(result.remaining, 1); - expect(result.failed, 1); + expect(result.previousState.remaining, 2); + expect(result.previousState.failed, 0); + expect(result.currentState.remaining, 1); + expect(result.currentState.failed, 1); + expect(result.isFailed, true); + expect(result.isPending, true); }); test('handles missing check gracefully', () async { - final state = const PresubmitJobState( - jobName: 'windows', // Missing + final state = PresubmitJobState( + name: 'windows', // Missing status: TaskStatus.succeeded, - attemptNumber: 1, + attempt: 1, + buildId: Int64.MAX_VALUE, + buildNumber: 456, + checkRunId: 123, + checkSuiteId: 2, + headBranch: 'master', + isMergeGroup: false, + sha: '', + slug: slug, + isUnifiedCheckRun: false, ); final result = await UnifiedCheckRun.markConclusion( @@ -294,13 +345,20 @@ void main() { expect(result.result, PresubmitGuardConclusionResult.missing); }); test('updates check status and build number on inProgress', () async { - final state = const PresubmitJobState( - jobName: 'linux', + final state = PresubmitJobState( + name: 'linux', status: TaskStatus.inProgress, - attemptNumber: 1, + attempt: 1, startTime: 2000, buildNumber: 456, buildId: Int64.MAX_VALUE, + checkRunId: 123, + checkSuiteId: 2, + headBranch: 'master', + isMergeGroup: false, + sha: '', + slug: slug, + isUnifiedCheckRun: false, ); final result = await UnifiedCheckRun.markConclusion( diff --git a/app_dart/test/service/scheduler_test.dart b/app_dart/test/service/scheduler_test.dart index 7f849316c5..01f0cbd8aa 100644 --- a/app_dart/test/service/scheduler_test.dart +++ b/app_dart/test/service/scheduler_test.dart @@ -14,7 +14,7 @@ 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/common/presubmit_completed_check.dart'; +import 'package:cocoon_service/src/model/common/presubmit_job_state.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/task.dart' as fs; @@ -1652,14 +1652,11 @@ targets: ); for (final ignored in Scheduler.kCheckRunsToIgnore) { - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun(name: ignored, sha: 'abc123'), - createGithubRepository().slug(), - ), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun(name: ignored, sha: 'abc123'), + createGithubRepository().slug(), ), - isTrue, ); } @@ -1687,14 +1684,11 @@ targets: firestore.failOnWriteDocument(document); - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun(name: 'Bar bar', sha: 'abc123'), - createGithubRepository().slug(), - ), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun(name: 'Bar bar', sha: 'abc123'), + createGithubRepository().slug(), ), - isFalse, ); expect( @@ -1726,14 +1720,11 @@ targets: checkRunGuard: '{}', ); - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun(name: 'Bar bar', sha: 'abc123'), - createGithubRepository().slug(), - ), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun(name: 'Bar bar', sha: 'abc123'), + createGithubRepository().slug(), ), - isFalse, ); expect( @@ -1758,6 +1749,52 @@ targets: ); }); + test( + 'does not fail immediately when a test check run fails if tests remain', + () async { + await CiStaging.initializeDocument( + firestoreService: firestore, + slug: Config.flutterSlug, + sha: 'abc123', + stage: CiStage.fusionEngineBuild, + tasks: ['Foo foo', 'Bar bar'], + checkRunGuard: '{}', + ); + + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun( + name: 'Bar bar', + sha: 'abc123', + conclusion: 'failure', + ), + createGithubRepository().slug(), + ), + ); + + expect( + firestore, + existsInStorage(CiStaging.metadata, [ + isCiStaging.hasCheckRuns({ + 'Foo foo': TaskConclusion.scheduled, + 'Bar bar': TaskConclusion.failure, + }), + ]), + ); + + verifyNever( + mockGithubChecksUtil.updateCheckRun( + any, + any, + any, + status: anyNamed('status'), + conclusion: anyNamed('conclusion'), + output: anyNamed('output'), + ), + ); + }, + ); + // The merge guard is not closed until both engine build and tests // complete and are successful. // This behavior is explained here: @@ -1780,14 +1817,11 @@ targets: checkRunGuard: checkRunFor(name: 'GUARD TEST'), ); - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun(name: 'Bar bar', sha: 'abc123'), - createGithubRepository().slug(), - ), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun(name: 'Bar bar', sha: 'abc123'), + createGithubRepository().slug(), ), - isTrue, ); expect( @@ -1890,14 +1924,11 @@ targets: checkRunGuard: checkRunFor(name: 'GUARD TEST'), ); - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun(name: 'Bar bar', sha: 'testSha'), - createGithubRepository().slug(), - ), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun(name: 'Bar bar', sha: 'testSha'), + createGithubRepository().slug(), ), - isTrue, ); verify( @@ -2034,18 +2065,15 @@ targets: checkRunGuard: checkRunFor(name: 'GUARD TEST'), ); - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun( - name: 'Bar bar', - sha: 'testSha', - checkSuiteId: 0, - ), - createGithubRepository().slug(), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun( + name: 'Bar bar', + sha: 'testSha', + checkSuiteId: 0, ), + createGithubRepository().slug(), ), - isTrue, ); verify( @@ -2143,14 +2171,11 @@ targets: checkRunGuard: checkRunFor(name: 'GUARD TEST'), ); - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun(name: 'Bar bar', sha: 'testSha'), - createGithubRepository().slug(), - ), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun(name: 'Bar bar', sha: 'testSha'), + createGithubRepository().slug(), ), - isTrue, ); // The first invocation looks in the fusionEngineBuild stage, which @@ -2407,18 +2432,15 @@ targets: checkRunGuard: checkRunFor(name: 'GUARD TEST'), ); - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun( - name: 'Bar bar', - sha: 'testSha', - conclusion: 'failure', - ), - createGithubRepository().slug(), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun( + name: 'Bar bar', + sha: 'testSha', + conclusion: 'failure', ), + createGithubRepository().slug(), ), - isTrue, ); // The first invocation looks in the fusionEngineBuild stage, which @@ -2490,19 +2512,16 @@ targets: ), ); - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun( - name: 'Bar bar', - sha: 'testSha', - conclusion: 'failure', - headBranch: headBranch, - ), - createGithubRepository().slug(), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun( + name: 'Bar bar', + sha: 'testSha', + conclusion: 'failure', + headBranch: headBranch, ), + createGithubRepository().slug(), ), - isTrue, ); // The first invocation looks in the fusionEngineBuild stage, which @@ -2573,18 +2592,15 @@ targets: ), ); - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun( - name: 'Bar bar', - sha: 'testSha', - headBranch: headBranch, - ), - createGithubRepository().slug(), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun( + name: 'Bar bar', + sha: 'testSha', + headBranch: headBranch, ), + createGithubRepository().slug(), ), - isTrue, ); // The first invocation looks in the fusionEngineBuild stage, which @@ -2696,14 +2712,11 @@ targets: checkRunGuard: checkRunFor(name: 'GUARD TEST'), ); - expect( - await scheduler.processCheckRunCompleted( - PresubmitCompletedJob.fromCheckRun( - createCocoonCheckRun(name: 'Bar bar', sha: 'testSha'), - createGithubRepository().slug(), - ), + await scheduler.processJobStatusUpdate( + PresubmitJobState.fromCheckRun( + createCocoonCheckRun(name: 'Bar bar', sha: 'testSha'), + createGithubRepository().slug(), ), - isTrue, ); verifyNever( @@ -4242,9 +4255,9 @@ targets: ], ); - final check = PresubmitCompletedJob.fromBuild(build, userData); + final check = PresubmitJobState.fromBuild(build, userData); - expect(await scheduler.processCheckRunCompleted(check), isTrue); + await scheduler.processJobStatusUpdate(check); // Should schedule tests for the next stage (fusionTests) expect(fakeLuciBuildService.scheduledTryBuilds, isNotEmpty); @@ -4258,13 +4271,97 @@ targets: expect(guard.remainingJobs, 0); }); + test('fails the guard check when a test check run fails', () async { + final pullRequest = generatePullRequest(); + final checkRunGuard = generateCheckRun( + 1234, + name: Config.kDashboardCheckName, + 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 + firestore.putDocument( + PresubmitGuard( + checkRun: checkRunGuard, + headSha: pullRequest.head!.sha!, + slug: pullRequest.base!.repo!.slug(), + prNum: pullRequest.number!, + stage: CiStage.fusionTests, + author: pullRequest.user!.login!, + creationTime: DateTime.now().millisecondsSinceEpoch, + jobs: {'Linux test': TaskStatus.waitingForBackfill}, + remainingJobs: 1, + failedJobs: 0, + ), + ); + + // Initialize check run for the task + firestore.putDocument( + PresubmitJob.init( + slug: pullRequest.base!.repo!.slug(), + jobName: 'Linux test', + checkRunId: checkRunGuard.id!, + creationTime: DateTime.now().millisecondsSinceEpoch, + ), + ); + + final userData = PresubmitUserData( + commit: CommitRef( + slug: pullRequest.base!.repo!.slug(), + sha: pullRequest.head!.sha!, + branch: 'master', + ), + 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')], + ); + + final check = PresubmitJobState.fromBuild(build, userData); + + await scheduler.processJobStatusUpdate(check); + + verify( + mockGithubChecksUtil.updateCheckRun( + any, + any, + any, + status: anyNamed('status'), + conclusion: CheckRunConclusion.actionRequired, + detailsUrl: anyNamed('detailsUrl'), + output: anyNamed('output'), + actions: anyNamed('actions'), + ), + ).called(1); + + final guards = await firestore.query(PresubmitGuard.collectionId, {}); + final guard = PresubmitGuard.fromDocument(guards.single); + expect(guard.failedJobs, 1); + }); + test( - 'fails the merge queue guard when a test check run fails (merge group)', + 'fails the guard check immediately when a test check run fails even if jobs remain', () async { final pullRequest = generatePullRequest(); final checkRunGuard = generateCheckRun( 1234, - name: Config.kMergeQueueLockName, + name: Config.kDashboardCheckName, startedAt: DateTime.now(), ); @@ -4274,10 +4371,6 @@ targets: 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 firestore.putDocument( PresubmitGuard( checkRun: checkRunGuard, @@ -4287,17 +4380,19 @@ targets: stage: CiStage.fusionTests, author: pullRequest.user!.login!, creationTime: DateTime.now().millisecondsSinceEpoch, - jobs: {'Linux test': TaskStatus.waitingForBackfill}, - remainingJobs: 1, + jobs: { + 'Linux test 1': TaskStatus.waitingForBackfill, + 'Linux test 2': TaskStatus.waitingForBackfill, + }, + remainingJobs: 2, failedJobs: 0, ), ); - // Initialize check run for the task firestore.putDocument( PresubmitJob.init( slug: pullRequest.base!.repo!.slug(), - jobName: 'Linux test', + jobName: 'Linux test 1', checkRunId: checkRunGuard.id!, creationTime: DateTime.now().millisecondsSinceEpoch, ), @@ -4307,7 +4402,7 @@ targets: commit: CommitRef( slug: pullRequest.base!.repo!.slug(), sha: pullRequest.head!.sha!, - branch: 'gh-readonly-queue/master/pr-123-abc', + branch: 'main', ), guardCheckRunId: checkRunGuard.id, stage: CiStage.fusionTests, @@ -4317,14 +4412,20 @@ targets: final build = generateBbv2Build( Int64(1), - name: 'Linux test', + name: 'Linux test 1', status: bbv2.Status.FAILURE, - tags: [bbv2.StringPair(key: 'current_attempt', value: '1')], + tags: [ + bbv2.StringPair(key: 'current_attempt', value: '1'), + bbv2.StringPair( + key: 'buildset', + value: 'sha/git/${pullRequest.head!.sha!}', + ), + ], ); - final check = PresubmitCompletedJob.fromBuild(build, userData); + final check = PresubmitJobState.fromBuild(build, userData); - expect(await scheduler.processCheckRunCompleted(check), isTrue); + await scheduler.processJobStatusUpdate(check); verify( mockGithubChecksUtil.updateCheckRun( @@ -4332,15 +4433,17 @@ targets: any, any, status: anyNamed('status'), - conclusion: CheckRunConclusion.failure, // Merge queue failure + conclusion: CheckRunConclusion.actionRequired, detailsUrl: anyNamed('detailsUrl'), output: anyNamed('output'), + actions: anyNamed('actions'), ), ).called(1); final guards = await firestore.query(PresubmitGuard.collectionId, {}); final guard = PresubmitGuard.fromDocument(guards.single); expect(guard.failedJobs, 1); + expect(guard.remainingJobs, 1); }, ); @@ -4412,9 +4515,9 @@ targets: ], ); - final check = PresubmitCompletedJob.fromBuild(build, userData); + final check = PresubmitJobState.fromBuild(build, userData); - expect(await scheduler.processCheckRunCompleted(check), isTrue); + await scheduler.processJobStatusUpdate(check); verify( mockGithubChecksUtil.updateCheckRun( @@ -4499,9 +4602,9 @@ targets: ], ); - final check = PresubmitCompletedJob.fromBuild(build, userData); + final check = PresubmitJobState.fromBuild(build, userData); - expect(await scheduler.processCheckRunCompleted(check), isTrue); + await scheduler.processJobStatusUpdate(check); verify( mockGithubChecksUtil.updateCheckRun( diff --git a/packages/cocoon_common/lib/guard_status.dart b/packages/cocoon_common/lib/guard_status.dart index bef6aa1b18..e9f4d43d11 100644 --- a/packages/cocoon_common/lib/guard_status.dart +++ b/packages/cocoon_common/lib/guard_status.dart @@ -33,14 +33,14 @@ enum GuardStatus { required int remainingBuilds, required int totalBuilds, }) { - if (failedBuilds == 0 && remainingBuilds == 0) { + if (failedBuilds > 0) { + return GuardStatus.failed; + } else if (remainingBuilds == 0) { return GuardStatus.succeeded; } else if (remainingBuilds == totalBuilds) { return GuardStatus.waitingForBackfill; - } else if (remainingBuilds > 0) { - return GuardStatus.inProgress; } else { - return GuardStatus.failed; + return GuardStatus.inProgress; } } } diff --git a/packages/cocoon_common/test/guard_status_test.dart b/packages/cocoon_common/test/guard_status_test.dart index b7d10e7a9b..086f0211c4 100644 --- a/packages/cocoon_common/test/guard_status_test.dart +++ b/packages/cocoon_common/test/guard_status_test.dart @@ -50,5 +50,16 @@ void main() { GuardStatus.inProgress, ); }); + + test('returns failed even if there are remaining builds', () { + expect( + GuardStatus.calculate( + failedBuilds: 1, + remainingBuilds: 5, + totalBuilds: 10, + ), + GuardStatus.failed, + ); + }); }); } diff --git a/packages/cocoon_integration_test/lib/src/utilities/mocks.mocks.dart b/packages/cocoon_integration_test/lib/src/utilities/mocks.mocks.dart index 5d69d2556d..f52ce291d8 100644 --- a/packages/cocoon_integration_test/lib/src/utilities/mocks.mocks.dart +++ b/packages/cocoon_integration_test/lib/src/utilities/mocks.mocks.dart @@ -16,7 +16,7 @@ import 'package:cocoon_service/src/foundation/github_checks_util.dart' as _i10; import 'package:cocoon_service/src/model/ci_yaml/ci_yaml.dart' as _i37; import 'package:cocoon_service/src/model/ci_yaml/target.dart' as _i27; import 'package:cocoon_service/src/model/commit_ref.dart' as _i31; -import 'package:cocoon_service/src/model/common/presubmit_completed_check.dart' +import 'package:cocoon_service/src/model/common/presubmit_job_state.dart' as _i38; import 'package:cocoon_service/src/model/github/checks.dart' as _i30; import 'package:cocoon_service/src/model/github/workflow_job.dart' as _i36; @@ -5835,14 +5835,13 @@ class MockScheduler extends _i1.Mock implements _i17.Scheduler { as _i13.Future>); @override - _i13.Future processCheckRunCompleted( - _i38.PresubmitCompletedJob? check, - ) => + _i13.Future processJobStatusUpdate(_i38.PresubmitJobState? job) => (super.noSuchMethod( - Invocation.method(#processCheckRunCompleted, [check]), - returnValue: _i13.Future.value(false), + Invocation.method(#processJobStatusUpdate, [job]), + returnValue: _i13.Future.value(), + returnValueForMissingStub: _i13.Future.value(), ) - as _i13.Future); + as _i13.Future); @override bool detectMergeGroup(_i30.CheckRun? checkRun) =>