blob: 77eeea66fe047bea2659cf6664f448c75f60228b [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';
import 'package:auto_submit/validations/validation.dart';
import 'package:github/github.dart' as github;
import '../service/log.dart';
/// Validates that a PR has been approved in accordance with the code review
/// guidelines.
class Approval extends Validation {
Approval({
required super.config,
});
@override
/// Implements the code review approval logic.
Future<ValidationResult> validate(QueryResult result, github.PullRequest messagePullRequest) async {
final PullRequest pullRequest = result.repository!.pullRequest!;
final String authorAssociation = pullRequest.authorAssociation!;
final String? author = pullRequest.author!.login;
final List<ReviewNode> reviews = pullRequest.reviews!.nodes!;
bool approved = config.rollerAccounts.contains(author) ||
_checkApproval(
author,
authorAssociation,
reviews,
);
String message = '- Please get at least one approved review if you are already '
'a member or two member reviews if you are not a member before re-applying this '
'label. __Reviewers__: If you left a comment approving, please use '
'the "approve" review action instead.';
return ValidationResult(approved, Action.REMOVE_LABEL, message);
}
/// Parses the restApi response reviews.
///
/// If author is a MEMBER or OWNER then it only requires a single review from
/// another MEMBER or OWNER. If the author is not a MEMBER or OWNER then it
/// requires two reviews from MEMBERs or OWNERS.
///
/// If there are any CHANGES_REQUESTED reviews, checks if the same author has
/// subsequently APPROVED. From testing, dismissing a review means it won't
/// show up in this list since it will have a status of DISMISSED and we only
/// ask for CHANGES_REQUESTED or APPROVED - however, adding a new review does
/// not automatically dismiss the previous one.
///
///
/// Returns false if no approved reviews or any oustanding change request
/// reviews.
///
/// Returns true if at least one approved review and no outstanding change
/// request reviews.
bool _checkApproval(
String? author,
String? authorAssociation,
List<ReviewNode> reviewNodes,
) {
final Set<String?> changeRequestAuthors = <String?>{};
const Set<String> allowedReviewers = <String>{ORG_MEMBER, ORG_OWNER};
final Set<String?> approvers = <String?>{};
if (allowedReviewers.contains(authorAssociation)) {
approvers.add(author);
}
for (ReviewNode review in reviewNodes) {
// Ignore reviews from non-members/owners.
if (!allowedReviewers.contains(review.authorAssociation)) {
continue;
}
// Reviews come back in order of creation.
final String? state = review.state;
final String? authorLogin = review.author!.login;
if (state == APPROVED_STATE) {
approvers.add(authorLogin);
changeRequestAuthors.remove(authorLogin);
} else if (state == CHANGES_REQUESTED_STATE) {
changeRequestAuthors.add(authorLogin);
}
}
final bool approved = (approvers.length > 1) && changeRequestAuthors.isEmpty;
log.info('PR approved $approved, approvers: $approvers, change request authors: $changeRequestAuthors');
return (approvers.length > 1) && changeRequestAuthors.isEmpty;
}
}