def test_regression_error_before_update( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, check_run: CheckRun, ) -> None: branch_protection.requiresStatusChecks = True branch_protection.requiredStatusCheckContexts = ["ci/backend", "wip-app"] branch_protection.requiresStrictStatusChecks = True pull_request.mergeStateStatus = MergeStateStatus.BEHIND contexts = [StatusContext(context="ci/backend", state=StatusState.SUCCESS)] check_run.name = "wip-app" check_run.conclusion = CheckConclusionState.SUCCESS with pytest.raises(NeedsBranchUpdate): mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=1, reviews=[review], check_runs=[check_run], contexts=contexts, valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_require_automerge_label_false( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: """ If the automerge_label is missing, but we have require_automerge_label set to false, enqueue the PR for merge """ pull_request.labels = [] config.merge.automerge_label = "automerge" config.merge.require_automerge_label = False mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.merge, MergeMethod.squash], )
def test_dont_update_before_block( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: """ Regression test for when Kodiak would update a PR that is not mergeable. We were raising the NeedsBranchUpdate exception too early. """ pull_request.mergeStateStatus = MergeStateStatus.BEHIND branch_protection.requiresStrictStatusChecks = True with pytest.raises(NeedsBranchUpdate): mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_failing_checks( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, check_run: CheckRun, ) -> None: pull_request.mergeStateStatus = MergeStateStatus.BLOCKED branch_protection.requiredStatusCheckContexts = ["ci/backend", "wip-app"] context.context = "ci/backend" context.state = StatusState.SUCCESS check_run.name = "wip-app" check_run.conclusion = CheckConclusionState.FAILURE with pytest.raises(NotQueueable, match="failing required status checks"): mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[check_run], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_get_merge_body_empty(pull_request: queries.PullRequest) -> None: pull_request.body = "hello world" actual = get_merge_body( V1( version=1, merge=Merge( method=MergeMethod.squash, message=MergeMessage(body=MergeBodyStyle.empty), ), ), pull_request, ) expected = dict(merge_method="squash", commit_message="") assert actual == expected
def test_regression_mishandling_multiple_reviews_okay_reviews( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, check_run: CheckRun, context: StatusContext, ) -> None: pull_request.mergeStateStatus = MergeStateStatus.BEHIND branch_protection.requiresApprovingReviews = True branch_protection.requiredApprovingReviewCount = 1 first_review_date = datetime(2010, 5, 15) latest_review_date = first_review_date + timedelta(minutes=20) reviews = [ PRReview( state=PRReviewState.CHANGES_REQUESTED, createdAt=first_review_date, author=PRReviewAuthor(login="******"), authorAssociation=CommentAuthorAssociation.CONTRIBUTOR, ), PRReview( state=PRReviewState.COMMENTED, createdAt=latest_review_date, author=PRReviewAuthor(login="******"), authorAssociation=CommentAuthorAssociation.CONTRIBUTOR, ), PRReview( state=PRReviewState.APPROVED, createdAt=latest_review_date, author=PRReviewAuthor(login="******"), authorAssociation=CommentAuthorAssociation.CONTRIBUTOR, ), PRReview( state=PRReviewState.APPROVED, createdAt=latest_review_date, author=PRReviewAuthor(login="******"), authorAssociation=CommentAuthorAssociation.CONTRIBUTOR, ), ] with pytest.raises(NeedsBranchUpdate): mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=1, reviews=reviews, check_runs=[check_run], contexts=[context], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def pull_request() -> PullRequest: return PullRequest( id="FDExOlB1bGxSZXX1ZXN0MjgxODQ0Nzg7", number=142, mergeStateStatus=MergeStateStatus.CLEAN, state=PullRequestState.OPEN, mergeable=MergeableState.MERGEABLE, labels=["bugfix", "automerge"], latest_sha="f89be6c", baseRefName="master", headRefName="feature/hello-world", title="new feature", body="# some description", bodyText="some description", bodyHTML="<h1>some description</h1>", )
def test_config_merge_optimistic_updates( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule ) -> None: """ If optimisitc_updates are enabled, branch updates should be prioritized over waiting for running status checks to complete. Otherwise, status checks should be checked before updating. """ branch_protection.requiredApprovingReviewCount = 0 branch_protection.requiresStrictStatusChecks = True pull_request.mergeStateStatus = MergeStateStatus.BEHIND branch_protection.requiresStatusChecks = True branch_protection.requiredStatusCheckContexts = ["ci/lint", "ci/test"] contexts: List[StatusContext] = [] config.merge.optimistic_updates = True with pytest.raises(NeedsBranchUpdate): mergeable( app_id="1234", config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[], contexts=contexts, check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], ) config.merge.optimistic_updates = False with pytest.raises(WaitingForChecks): mergeable( app_id="1234", config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[], contexts=contexts, check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_get_merge_body_strip_html_comments(pull_request: queries.PullRequest, original: str, stripped: str) -> None: pull_request.body = "hello <!-- testing -->world" actual = get_merge_body( V1( version=1, merge=Merge( method=MergeMethod.squash, message=MergeMessage(body=MergeBodyStyle.pull_request_body, strip_html_comments=True), ), ), pull_request, ) expected = dict(merge_method="squash", commit_message="hello world") assert actual == expected
def test_unknown_blockage( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule ) -> None: branch_protection.requiredApprovingReviewCount = 0 branch_protection.requiresStatusChecks = False pull_request.mergeStateStatus = MergeStateStatus.BLOCKED with pytest.raises(NotQueueable, match="determine why PR is blocked"): mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[], contexts=[], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_missing_mergeability_state( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: with pytest.raises(MissingGithubMergeabilityState): pull_request.mergeable = MergeableState.UNKNOWN mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_need_update( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: with pytest.raises(NeedsBranchUpdate): pull_request.mergeStateStatus = MergeStateStatus.BEHIND mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_closed( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: with pytest.raises(NotQueueable, match="closed"): pull_request.state = PullRequestState.CLOSED mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_requires_signature( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: pull_request.mergeStateStatus = MergeStateStatus.BLOCKED branch_protection.requiresCommitSignatures = True with pytest.raises(NotQueueable, match="missing required signature"): mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_missing_automerge_label( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: pull_request.labels = ["bug"] config.merge.automerge_label = "automerge" with pytest.raises(NotQueueable, match="missing automerge_label"): mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.merge, MergeMethod.squash], )
def test_blocking_review( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: pull_request.mergeStateStatus = MergeStateStatus.BLOCKED review.state = PRReviewState.CHANGES_REQUESTED with pytest.raises(NotQueueable, match="blocking review"): mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_missing_required_context( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: pull_request.mergeStateStatus = MergeStateStatus.BLOCKED branch_protection.requiredStatusCheckContexts = ["ci/backend", "ci/frontend"] context.context = "ci/backend" with pytest.raises(WaitingForChecks, match="missing required status checks"): mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def test_blacklisted( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: # a PR with a blacklisted label should not be mergeable with pytest.raises(NotQueueable, match="blacklist"): pull_request.labels = ["automerge", "dont-merge"] config.merge.automerge_label = "automerge" config.merge.blacklist_labels = ["dont-merge"] mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.merge, MergeMethod.squash], )
def test_blacklist_title_match( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule, review: PRReview, context: StatusContext, ) -> None: # a PR with a blacklisted title should not be mergeable with pytest.raises(NotQueueable, match="blacklist_title") as e_info: config.merge.blacklist_title_regex = "^WIP:.*" pull_request.title = "WIP: add fleeb to plumbus" mergeable( config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[review], contexts=[context], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.merge, MergeMethod.squash], ) assert config.merge.blacklist_title_regex in str(e_info.value)
def test_merge_state_status_draft( pull_request: PullRequest, config: V1, branch_protection: BranchProtectionRule ) -> None: """ If optimisitc_updates are enabled, branch updates should be prioritized over waiting for running status checks to complete. Otherwise, status checks should be checked before updating. """ pull_request.mergeStateStatus = MergeStateStatus.DRAFT with pytest.raises(NotQueueable, match="draft state"): mergeable( app_id="1234", config=config, pull_request=pull_request, branch_protection=branch_protection, review_requests_count=0, reviews=[], contexts=[], check_runs=[], valid_signature=False, valid_merge_methods=[MergeMethod.squash], )
def create_event() -> EventInfoResponse: config = V1(version=1, merge=Merge(automerge_label="automerge", method=MergeMethod.squash)) pr = PullRequest( id="e14ff7599399478fb9dbc2dacb87da72", number=100, author=PullRequestAuthor(login="******", databaseId=49118, type="Bot"), mergeStateStatus=MergeStateStatus.BEHIND, state=PullRequestState.OPEN, isDraft=False, mergeable=MergeableState.MERGEABLE, isCrossRepository=False, labels=["automerge"], latest_sha="8d728d017cac4f5ba37533debe65730abe65730a", baseRefName="master", headRefName="df825f90-9825-424c-a97e-733522027e4c", title="Update README.md", body="", bodyText="", bodyHTML="", url="https://github.com/delos-corp/hive-mind/pull/324", ) rep_info = RepoInfo( merge_commit_allowed=False, rebase_merge_allowed=False, squash_merge_allowed=True, is_private=True, delete_branch_on_merge=False, ) branch_protection = BranchProtectionRule( requiresApprovingReviews=True, requiredApprovingReviewCount=2, requiresStatusChecks=True, requiredStatusCheckContexts=[ "ci/circleci: frontend_lint", "ci/circleci: frontend_test", ], requiresStrictStatusChecks=True, requiresCodeOwnerReviews=False, requiresCommitSignatures=False, restrictsPushes=False, pushAllowances=NodeListPushAllowance(nodes=[]), ) return EventInfoResponse( config=config, config_str="""\ version = 1 [merge] method = "squash" """, config_file_expression="master:.kodiak.toml", head_exists=True, pull_request=pr, repository=rep_info, branch_protection=branch_protection, review_requests=[], reviews=[], status_contexts=[], check_runs=[], valid_signature=True, valid_merge_methods=[MergeMethod.squash], subscription=None, )
def block_event() -> EventInfoResponse: config = V1(version=1, merge=Merge(automerge_label="automerge", method=MergeMethod.squash)) pr = PullRequest( id="e14ff7599399478fb9dbc2dacb87da72", number=100, author=PullRequestAuthor(login="******", databaseId=49118, type="Bot"), mergeStateStatus=MergeStateStatus.BEHIND, state=PullRequestState.OPEN, mergeable=MergeableState.MERGEABLE, isCrossRepository=False, labels=["automerge"], latest_sha="8d728d017cac4f5ba37533debe65730abe65730a", baseRefName="master", headRefName="df825f90-9825-424c-a97e-733522027e4c", title="Update README.md", body="", bodyText="", bodyHTML="", url="https://github.com/delos-corp/hive-mind/pull/324", ) rep_info = RepoInfo( merge_commit_allowed=False, rebase_merge_allowed=False, squash_merge_allowed=True, delete_branch_on_merge=True, is_private=True, ) branch_protection = BranchProtectionRule( requiresApprovingReviews=True, requiredApprovingReviewCount=2, requiresStatusChecks=True, requiredStatusCheckContexts=[ "ci/circleci: backend_lint", "ci/circleci: backend_test", "ci/circleci: frontend_lint", "ci/circleci: frontend_test", "WIP (beta)", ], requiresStrictStatusChecks=True, requiresCommitSignatures=False, restrictsPushes=True, pushAllowances=NodeListPushAllowance(nodes=[ PushAllowance(actor=PushAllowanceActor(databaseId=None)), PushAllowance(actor=PushAllowanceActor(databaseId=53453)), ]), ) return EventInfoResponse( config=config, config_str="""\ version = 1 [merge] method = "squash" """, config_file_expression="master:.kodiak.toml", head_exists=True, pull_request=pr, repository=rep_info, subscription=Subscription( account_id="D1606A79-A1A1-4550-BA7B-C9ED0D792B1E", subscription_blocker=None), branch_protection=branch_protection, review_requests=[ PRReviewRequest(name="ghost"), PRReviewRequest(name="ghost-team"), PRReviewRequest(name="ghost-mannequin"), ], reviews=[ PRReview( createdAt=arrow.get("2019-05-22T15:29:34Z").datetime, state=PRReviewState.COMMENTED, author=PRReviewAuthor(login="******", permission=Permission.WRITE), ), PRReview( createdAt=arrow.get("2019-05-22T15:29:52Z").datetime, state=PRReviewState.CHANGES_REQUESTED, author=PRReviewAuthor(login="******", permission=Permission.WRITE), ), PRReview( createdAt=arrow.get("2019-05-22T15:30:52Z").datetime, state=PRReviewState.COMMENTED, author=PRReviewAuthor(login="******", permission=Permission.ADMIN), ), PRReview( createdAt=arrow.get("2019-05-22T15:43:17Z").datetime, state=PRReviewState.APPROVED, author=PRReviewAuthor(login="******", permission=Permission.WRITE), ), PRReview( createdAt=arrow.get("2019-05-23T15:13:29Z").datetime, state=PRReviewState.APPROVED, author=PRReviewAuthor(login="******", permission=Permission.WRITE), ), PRReview( createdAt=arrow.get("2019-05-24T10:21:32Z").datetime, state=PRReviewState.APPROVED, author=PRReviewAuthor(login="******", permission=Permission.WRITE), ), ], status_contexts=[ StatusContext(context="ci/circleci: backend_lint", state=StatusState.SUCCESS), StatusContext(context="ci/circleci: backend_test", state=StatusState.SUCCESS), StatusContext(context="ci/circleci: frontend_lint", state=StatusState.SUCCESS), StatusContext(context="ci/circleci: frontend_test", state=StatusState.SUCCESS), ], check_runs=[ CheckRun(name="WIP (beta)", conclusion=CheckConclusionState.SUCCESS) ], valid_signature=True, valid_merge_methods=[MergeMethod.squash], )
def block_event(config_file_expression: str, config_str: str) -> EventInfoResponse: config = V1(version=1, merge=Merge(automerge_label="automerge", method=MergeMethod.squash)) pr = PullRequest( id="e14ff7599399478fb9dbc2dacb87da72", number=100, mergeStateStatus=MergeStateStatus.BEHIND, state=PullRequestState.OPEN, mergeable=MergeableState.MERGEABLE, isCrossRepository=False, labels=["automerge"], latest_sha="8d728d017cac4f5ba37533debe65730abe65730a", baseRefName="master", headRefName="df825f90-9825-424c-a97e-733522027e4c", title="Update README.md", body="", bodyText="", bodyHTML="", ) rep_info = RepoInfo( merge_commit_allowed=False, rebase_merge_allowed=False, squash_merge_allowed=True, ) branch_protection = BranchProtectionRule( requiresApprovingReviews=True, requiredApprovingReviewCount=2, requiresStatusChecks=True, requiredStatusCheckContexts=[ "ci/circleci: backend_lint", "ci/circleci: backend_test", "ci/circleci: frontend_lint", "ci/circleci: frontend_test", "WIP (beta)", ], requiresStrictStatusChecks=True, requiresCommitSignatures=False, ) return EventInfoResponse( config=config, config_str=config_str, config_file_expression=config_file_expression, head_exists=True, pull_request=pr, repo=rep_info, branch_protection=branch_protection, review_requests=[ PRReviewRequest(name="ghost"), PRReviewRequest(name="ghost-team"), PRReviewRequest(name="ghost-mannequin"), ], reviews=[ PRReview( createdAt=arrow.get("2019-05-22T15:29:34Z").datetime, state=PRReviewState.COMMENTED, author=PRReviewAuthor(login="******", permission=Permission.WRITE), ), PRReview( createdAt=arrow.get("2019-05-22T15:29:52Z").datetime, state=PRReviewState.CHANGES_REQUESTED, author=PRReviewAuthor(login="******", permission=Permission.WRITE), ), PRReview( createdAt=arrow.get("2019-05-22T15:30:52Z").datetime, state=PRReviewState.COMMENTED, author=PRReviewAuthor(login="******", permission=Permission.ADMIN), ), PRReview( createdAt=arrow.get("2019-05-22T15:43:17Z").datetime, state=PRReviewState.APPROVED, author=PRReviewAuthor(login="******", permission=Permission.WRITE), ), PRReview( createdAt=arrow.get("2019-05-23T15:13:29Z").datetime, state=PRReviewState.APPROVED, author=PRReviewAuthor(login="******", permission=Permission.WRITE), ), ], status_contexts=[ StatusContext(context="ci/circleci: backend_lint", state=StatusState.SUCCESS), StatusContext(context="ci/circleci: backend_test", state=StatusState.SUCCESS), StatusContext(context="ci/circleci: frontend_lint", state=StatusState.SUCCESS), StatusContext(context="ci/circleci: frontend_test", state=StatusState.SUCCESS), ], check_runs=[ CheckRun(name="WIP (beta)", conclusion=CheckConclusionState.SUCCESS) ], valid_signature=True, valid_merge_methods=[MergeMethod.squash], )