blob: 0677221068be05cebd3f890e3d9dcb960913bea3 [file] [log] [blame]
// Copyright 2022 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:auto_submit/model/auto_submit_query_result.dart' as auto;
import 'package:auto_submit/service/config.dart';
import 'package:auto_submit/service/github_service.dart';
import 'package:auto_submit/validations/required_check_runs.dart';
import 'package:auto_submit/validations/validation.dart';
import 'package:github/github.dart' as github;
import 'package:retry/retry.dart';
import '../exception/retryable_exception.dart';
import '../service/log.dart';
class Revert extends Validation {
Revert({
required super.config,
RetryOptions? retryOptions,
}) : retryOptions = retryOptions ?? Config.requiredChecksRetryOptions;
static const Set<String> allowedReviewers = <String>{ORG_MEMBER, ORG_OWNER};
final RetryOptions retryOptions;
@override
Future<ValidationResult> validate(auto.QueryResult result, github.PullRequest messagePullRequest) async {
final auto.PullRequest pullRequest = result.repository!.pullRequest!;
final String authorAssociation = pullRequest.authorAssociation!;
final String? author = pullRequest.author!.login;
final auto.Commit commit = pullRequest.commits!.nodes!.single.commit!;
String? sha = commit.oid;
if (!isValidAuthor(author, authorAssociation)) {
String message = 'The author $author does not have permissions to make this request.';
log.info(message);
return ValidationResult(false, Action.REMOVE_LABEL, message);
}
bool? canMerge = messagePullRequest.mergeable;
if (canMerge == null || !canMerge) {
String message =
'This pull request cannot be merged due to conflicts. Please resolve conflicts and re-add the revert label.';
log.info(message);
return ValidationResult(false, Action.REMOVE_LABEL, message);
}
String? pullRequestBody = messagePullRequest.body;
String? revertLink = extractLinkFromText(pullRequestBody);
if (revertLink == null) {
String message =
'A reverts link could not be found or was formatted incorrectly. Format is \'Reverts owner/repo#id\'';
log.info(message);
return ValidationResult(false, Action.REMOVE_LABEL, message);
}
github.RepositorySlug repositorySlug = _getSlugFromLink(revertLink);
GithubService githubService = await config.createGithubService(repositorySlug);
bool requiredChecksCompleted = await waitForRequiredChecks(
githubService: githubService,
slug: repositorySlug,
sha: sha!,
checkNames: requiredCheckRunsMapping[repositorySlug.name]!,
);
if (!requiredChecksCompleted) {
return ValidationResult(
false,
Action.IGNORE_TEMPORARILY,
'Some of the required checks did not complete in time.',
);
}
int pullRequestId = _getPullRequestNumberFromLink(revertLink);
github.PullRequest requestToRevert = await githubService.getPullRequest(repositorySlug, pullRequestId);
bool requestsMatch = await githubService.comparePullRequests(repositorySlug, requestToRevert, messagePullRequest);
if (requestsMatch) {
return ValidationResult(
true,
Action.IGNORE_FAILURE,
'Revert request has been verified and will be queued for merge.',
);
}
return ValidationResult(
false,
Action.REMOVE_LABEL,
'Validation of the revert request has failed. Verify the files in the revert request are the same as the original PR and resubmit the revert request.',
);
}
/// Only a team member and code owner can submit a revert request without a review.
bool isValidAuthor(String? author, String authorAssociation) {
return config.rollerAccounts.contains(author) || allowedReviewers.contains(authorAssociation);
}
/// The full text here is 'Reverts flutter/cocoon#XXXXX' as output by github
/// the link must be in the form github.com/flutter/repo/pull/id
String? extractLinkFromText(String? bodyText) {
if (bodyText == null) {
return null;
}
final RegExp regExp = RegExp(r'[Rr]everts[\s]+([-\.a-zA-Z_]+/[-\.a-zA-Z_]+#[0-9]+)', multiLine: true);
Iterable<RegExpMatch> matches = regExp.allMatches(bodyText);
if (matches.isNotEmpty && matches.length == 1) {
return matches.elementAt(0).group(1);
} else if (matches.isNotEmpty && matches.length != 1) {
log.warning('Detected more than 1 revert link. Cannot process more than one link.');
}
return null;
}
/// Split a reverts link on the '#' then the '/' to get the parts of the repo
/// slug. It is assumed that the link has the format flutter/repo#id.
github.RepositorySlug _getSlugFromLink(String link) {
List<String> linkSplit = link.split('#');
List<String> slugSplit = linkSplit.elementAt(0).split('/');
return github.RepositorySlug(slugSplit.elementAt(0), slugSplit.elementAt(1));
}
/// Split a reverts link on the '#' to get the id part of the link.
/// It is assumed that the link has the format flutter/repo#id.
int _getPullRequestNumberFromLink(String link) {
List<String> linkSplit = link.split('#');
return int.parse(linkSplit.elementAt(1));
}
/// Wait for the required checks to complete, and if repository has no checks
/// true is returned.
Future<bool> waitForRequiredChecks({
required GithubService githubService,
required github.RepositorySlug slug,
required String sha,
required List<String> checkNames,
}) async {
List<github.CheckRun> targetCheckRuns = [];
for (var element in checkNames) {
targetCheckRuns.addAll(
await githubService.getCheckRunsFiltered(
slug: slug,
ref: sha,
checkName: element,
),
);
}
bool checksCompleted = true;
try {
for (github.CheckRun checkRun in targetCheckRuns) {
await retryOptions.retry(
() async {
await _verifyCheckRunCompleted(
slug,
githubService,
checkRun,
);
},
retryIf: (Exception e) => e is RetryableException,
);
}
} catch (e) {
log.warning('Required check has not completed in time. ${e.toString()}');
checksCompleted = false;
}
return checksCompleted;
}
}
/// Function signature that will be executed with retries.
typedef RetryHandler = Function();
/// Simple function to wait on completed checkRuns with retries.
Future<void> _verifyCheckRunCompleted(
github.RepositorySlug slug,
GithubService githubService,
github.CheckRun targetCheckRun,
) async {
List<github.CheckRun> checkRuns = await githubService.getCheckRunsFiltered(
slug: slug,
ref: targetCheckRun.headSha!,
checkName: targetCheckRun.name,
);
if (checkRuns.first.name != targetCheckRun.name || checkRuns.first.conclusion != github.CheckRunConclusion.success) {
throw RetryableException('${targetCheckRun.name} has not yet completed.');
}
}