async def test_get_mergify_config_location_from_cache( redis_cache: utils.RedisCache, ) -> None: client = mock.AsyncMock() client.auth.owner = "foo" client.item.side_effect = [ http.HTTPNotFound("Not Found", request=mock.Mock(), response=mock.Mock()), http.HTTPNotFound("Not Found", request=mock.Mock(), response=mock.Mock()), github_types.GitHubContentFile({ "content": encodebytes("whatever".encode()).decode(), "type": "file", "path": ".github/mergify.yml", "sha": github_types.SHAType("zeazeaze"), }), ] installation = context.Installation( github_types.GitHubAccountIdType(0), github_types.GitHubLogin("foo"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository(installation, github_types.GitHubRepositoryName("bar")) await repository.get_mergify_config_file() assert client.item.call_count == 3 client.item.assert_has_calls([ mock.call("/repos/foo/bar/contents/.mergify.yml"), mock.call("/repos/foo/bar/contents/.mergify/config.yml"), mock.call("/repos/foo/bar/contents/.github/mergify.yml"), ]) client.item.reset_mock() client.item.side_effect = [ github_types.GitHubContentFile({ "content": encodebytes("whatever".encode()).decode(), "type": "file", "path": ".github/mergify.yml", "sha": github_types.SHAType("zeazeaze"), }), ] repository._cache = context.RepositoryCache() await repository.get_mergify_config_file() assert client.item.call_count == 1 client.item.assert_has_calls([ mock.call("/repos/foo/bar/contents/.github/mergify.yml"), ])
def _url_parser( url: str, ) -> typing.Tuple[github_types.GitHubLogin, typing.Optional[github_types.GitHubRepositoryName], typing.Optional[github_types.GitHubPullRequestNumber], ]: path = [ el for el in urllib.parse.urlparse(url).path.split("/") if el != "" ] pull_number: typing.Optional[str] repo: typing.Optional[str] try: owner, repo, _, pull_number = path except ValueError: pull_number = None try: owner, repo = path except ValueError: if len(path) == 1: owner = path[0] repo = None else: raise ValueError return ( github_types.GitHubLogin(owner), None if repo is None else github_types.GitHubRepositoryName(repo), None if pull_number is None else github_types.GitHubPullRequestNumber( int(pull_number)), )
async def test_get_mergify_config_invalid( invalid: str, redis_cache: utils.RedisCache) -> None: with pytest.raises(InvalidRules): async def item(*args, **kwargs): return github_types.GitHubContentFile({ "content": encodebytes(invalid.encode()).decode(), "path": ".mergify.yml", "type": "file", "sha": "azertyu", }) client = mock.Mock() client.item.return_value = item() installation = context.Installation( github_types.GitHubAccountIdType(0), github_types.GitHubLogin("foobar"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository( installation, github_types.GitHubRepositoryName("xyz"), github_types.GitHubRepositoryIdType(0), ) config_file = await repository.get_mergify_config_file() assert config_file is not None get_mergify_config(config_file)
def test_seats_renamed_account_repo() -> None: user1 = count_seats.SeatAccount( github_types.GitHubAccountIdType(123), github_types.GitHubLogin("user1"), ) user1bis = count_seats.SeatAccount( github_types.GitHubAccountIdType(123), github_types.GitHubLogin("user1bis"), ) user2 = count_seats.SeatAccount( github_types.GitHubAccountIdType(456), github_types.GitHubLogin("user2"), ) user2bis = count_seats.SeatAccount( github_types.GitHubAccountIdType(456), github_types.GitHubLogin("user2bis"), ) users = {user1, user2, user2bis, user1bis} assert len(users) == 2 assert list(users)[0].login == "user2" assert list(users)[1].login == "user1" repo1 = count_seats.SeatRepository( github_types.GitHubRepositoryIdType(123), github_types.GitHubRepositoryName("repo1"), ) repo1bis = count_seats.SeatRepository( github_types.GitHubRepositoryIdType(123), github_types.GitHubRepositoryName("repo1bis"), ) repo2 = count_seats.SeatRepository( github_types.GitHubRepositoryIdType(456), github_types.GitHubRepositoryName("repo2"), ) repo2bis = count_seats.SeatRepository( github_types.GitHubRepositoryIdType(456), github_types.GitHubRepositoryName("repo2bis"), ) repos = {repo1, repo2, repo2bis, repo1bis} assert repos == {repo1, repo2}
def repository(redis_cache, fake_client): installation = context.Installation( github_types.GitHubAccountIdType(123), github_types.GitHubLogin("user"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), fake_client, redis_cache, ) return context.Repository( installation, github_types.GitHubRepositoryName("name"), github_types.GitHubRepositoryIdType(123), )
def fake_repository( redis_links: redis_utils.RedisLinks, fake_subscription: subscription.Subscription, ) -> context.Repository: gh_owner = github_types.GitHubAccount({ "login": github_types.GitHubLogin("Mergifyio"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "full_name": "Mergifyio/mergify-engine", "name": github_types.GitHubRepositoryName("mergify-engine"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("main"), }) installation_json = github_types.GitHubInstallation({ "id": github_types.GitHubInstallationIdType(12345), "target_type": gh_owner["type"], "permissions": {}, "account": gh_owner, }) fake_client = mock.Mock() installation = context.Installation(installation_json, fake_subscription, fake_client, redis_links) return context.Repository(installation, gh_repo)
def fake_repository( redis_cache: utils.RedisCache, fake_subscription: subscription.Subscription, ) -> context.Repository: gh_owner = github_types.GitHubAccount( { "login": github_types.GitHubLogin("Mergifyio"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", } ) gh_repo = github_types.GitHubRepository( { "full_name": "Mergifyio/mergify-engine", "name": github_types.GitHubRepositoryName("mergify-engine"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("main"), } ) installation_json = github_types.GitHubInstallation( { "id": github_types.GitHubInstallationIdType(12345), "target_type": gh_owner["type"], "permissions": {}, "account": gh_owner, } ) fake_client = redis_queue = mock.Mock() # NOTE(Syffe): Since redis_queue is not used in fake_repository, we simply mock it, # otherwise a fixture is needed for it. This might change with future use of redis_queue. installation = context.Installation( installation_json, fake_subscription, fake_client, redis_cache, redis_queue ) return context.Repository(installation, gh_repo)
def repository(redis_cache, fake_client): gh_owner = github_types.GitHubAccount({ "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "full_name": "user/name", "name": github_types.GitHubRepositoryName("name"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("ref"), }) installation = context.Installation( github_types.GitHubAccountIdType(123), github_types.GitHubLogin("user"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), fake_client, redis_cache, ) return context.Repository(installation, gh_repo)
async def populate_with_active_users( self, redis_cache: redis_utils.RedisCache, owner_id: typing.Optional[github_types.GitHubAccountIdType] = None, ) -> None: async for key in get_active_users_keys( redis_cache, owner_id="*" if owner_id is None else owner_id): _, _owner_id, owner_login, repo_id, repo_name = key.split("~") org = SeatAccount( github_types.GitHubAccountIdType(int(_owner_id)), github_types.GitHubLogin(owner_login), ) repo = SeatRepository( github_types.GitHubRepositoryIdType(int(repo_id)), github_types.GitHubRepositoryName(repo_name), ) active_users = await get_active_users(redis_cache, key) repo_seats = self.seats[org][repo] if repo_seats["active_users"] is None: repo_seats["active_users"] = active_users else: repo_seats["active_users"] |= active_users
async def test_team_permission_cache(redis_cache: utils.RedisCache) -> None: class FakeClient(github.AsyncGithubInstallationClient): called: int def __init__(self, owner, repo): super().__init__(auth=None) self.owner = owner self.repo = repo self.called = 0 async def get(self, url, *args, **kwargs): self.called += 1 if (url == f"/orgs/{self.owner}/teams/team-ok/repos/{self.owner}/{self.repo}" ): return {} elif (url == f"/orgs/{self.owner}/teams/team-nok/repos/{self.owner}/{self.repo}" ): raise http.HTTPNotFound(message="Not found", request=mock.ANY, response=mock.ANY) elif (url == f"/orgs/{self.owner}/teams/team-also-nok/repos/{self.owner}/{self.repo}" ): raise http.HTTPNotFound(message="Not found", request=mock.ANY, response=mock.ANY) raise ValueError(f"Unknown test URL `{url}`") gh_owner = github_types.GitHubAccount({ "id": github_types.GitHubAccountIdType(123), "login": github_types.GitHubLogin("jd"), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "full_name": "", "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "name": github_types.GitHubRepositoryName("test"), "private": False, }) team_slug1 = github_types.GitHubTeamSlug("team-ok") team_slug2 = github_types.GitHubTeamSlug("team-nok") team_slug3 = github_types.GitHubTeamSlug("team-also-nok") sub = subscription.Subscription(redis_cache, 0, False, "", frozenset()) client = FakeClient(gh_owner["login"], gh_repo["name"]) installation = context.Installation(gh_owner["id"], gh_owner["login"], sub, client, redis_cache) repository = context.Repository(installation, gh_repo["name"], gh_repo["id"]) assert client.called == 0 assert await repository.team_has_read_permission(team_slug1) assert client.called == 1 assert await repository.team_has_read_permission(team_slug1) assert client.called == 1 assert not await repository.team_has_read_permission(team_slug2) assert client.called == 2 assert not await repository.team_has_read_permission(team_slug2) assert client.called == 2 assert not await repository.team_has_read_permission(team_slug3) assert client.called == 3 gh_repo = github_types.GitHubRepository({ "id": github_types.GitHubRepositoryIdType(1), "owner": gh_owner, "full_name": "", "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "name": github_types.GitHubRepositoryName("test2"), "private": False, }) client = FakeClient(gh_owner["login"], gh_repo["name"]) installation = context.Installation(gh_owner["id"], gh_owner["login"], sub, client, redis_cache) repository = context.Repository(installation, gh_repo["name"], gh_repo["id"]) assert client.called == 0 assert not await repository.team_has_read_permission(team_slug2) assert client.called == 1 assert not await repository.team_has_read_permission(team_slug2) assert client.called == 1 assert await repository.team_has_read_permission(team_slug1) assert client.called == 2 await context.Repository.clear_team_permission_cache_for_repo( redis_cache, gh_owner, gh_repo) assert await repository.team_has_read_permission(team_slug1) assert client.called == 3 assert not await repository.team_has_read_permission(team_slug3) assert client.called == 4 await context.Repository.clear_team_permission_cache_for_org( redis_cache, gh_owner) assert not await repository.team_has_read_permission(team_slug3) assert client.called == 5 assert not await repository.team_has_read_permission(team_slug2) assert client.called == 6 assert not await repository.team_has_read_permission(team_slug2) assert client.called == 6 await context.Repository.clear_team_permission_cache_for_team( redis_cache, gh_owner, team_slug2) assert not await repository.team_has_read_permission(team_slug2) assert client.called == 7
GH_REPO = github_types.GitHubRepository({ "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("main"), "id": github_types.GitHubRepositoryIdType(456), "full_name": "user/ref", "name": github_types.GitHubRepositoryName("name"), "private": False, "owner": GH_OWNER, }) GH_PULL = github_types.GitHubPullRequest( { "title": "", "id": github_types.GitHubPullRequestId(0), "maintainer_can_modify": False, "rebaseable": False, "draft": False, "merge_commit_sha": None, "labels": [], "number": github_types.GitHubPullRequestNumber(6),
def a_pull_request() -> github_types.GitHubPullRequest: gh_owner = github_types.GitHubAccount( { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", } ) gh_repo = github_types.GitHubRepository( { "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType(""), "id": github_types.GitHubRepositoryIdType(456), "full_name": "user/repo", "name": github_types.GitHubRepositoryName("repo"), "private": False, "owner": gh_owner, } ) return github_types.GitHubPullRequest( { "node_id": "42", "locked": False, "assignees": [], "requested_reviewers": [], "requested_teams": [], "milestone": None, "title": "", "updated_at": github_types.ISODateTimeType("2021-06-01T18:41:39Z"), "created_at": github_types.ISODateTimeType("2021-06-01T18:41:39Z"), "closed_at": None, "id": github_types.GitHubPullRequestId(0), "maintainer_can_modify": False, "rebaseable": False, "draft": False, "merge_commit_sha": None, "labels": [], "number": github_types.GitHubPullRequestNumber(6), "commits": 1, "merged": True, "state": "closed", "changed_files": 1, "html_url": "<html_url>", "base": { "label": "", "sha": github_types.SHAType("sha"), "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "ref": github_types.GitHubRefType("ref"), "repo": gh_repo, }, "head": { "label": "", "sha": github_types.SHAType("old-sha-one"), "ref": github_types.GitHubRefType("fork"), "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "repo": { "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType(""), "id": github_types.GitHubRepositoryIdType(123), "full_name": "fork/other", "name": github_types.GitHubRepositoryName("other"), "private": False, "owner": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, }, }, "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "merged_by": None, "merged_at": None, "mergeable_state": "clean", "mergeable": True, "body": None, } )
async def test_user_permission_cache(redis_cache: utils.RedisCache) -> None: class FakeClient(github.AsyncGithubInstallationClient): called: int def __init__(self, owner, repo): super().__init__(auth=None) self.owner = owner self.repo = repo self.called = 0 async def item(self, url, *args, **kwargs): self.called += 1 if self.repo == "test": if (url == f"/repos/{self.owner}/{self.repo}/collaborators/foo/permission" ): return {"permission": "admin"} elif url.startswith( f"/repos/{self.owner}/{self.repo}/collaborators/"): return {"permission": "loser"} elif self.repo == "test2": if (url == f"/repos/{self.owner}/{self.repo}/collaborators/bar/permission" ): return {"permission": "admin"} elif url.startswith( f"/repos/{self.owner}/{self.repo}/collaborators/"): return {"permission": "loser"} raise ValueError(f"Unknown test URL `{url}` for repo {self.repo}") gh_owner = github_types.GitHubAccount({ "id": github_types.GitHubAccountIdType(123), "login": github_types.GitHubLogin("jd"), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "full_name": "", "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "name": github_types.GitHubRepositoryName("test"), "private": False, }) user_1 = github_types.GitHubAccount({ "id": github_types.GitHubAccountIdType(1), "login": github_types.GitHubLogin("foo"), "type": "User", "avatar_url": "", }) user_2 = github_types.GitHubAccount({ "id": github_types.GitHubAccountIdType(2), "login": github_types.GitHubLogin("bar"), "type": "User", "avatar_url": "", }) user_3 = github_types.GitHubAccount({ "id": github_types.GitHubAccountIdType(3), "login": github_types.GitHubLogin("baz"), "type": "User", "avatar_url": "", }) sub = subscription.Subscription(redis_cache, 0, False, "", frozenset()) client = FakeClient(gh_owner["login"], gh_repo["name"]) installation = context.Installation(gh_owner["id"], gh_owner["login"], sub, client, redis_cache) repository = context.Repository(installation, gh_repo["name"], gh_repo["id"]) assert client.called == 0 assert await repository.has_write_permission(user_1) assert client.called == 1 assert await repository.has_write_permission(user_1) assert client.called == 1 assert not await repository.has_write_permission(user_2) assert client.called == 2 assert not await repository.has_write_permission(user_2) assert client.called == 2 assert not await repository.has_write_permission(user_3) assert client.called == 3 gh_repo = github_types.GitHubRepository({ "id": github_types.GitHubRepositoryIdType(1), "owner": gh_owner, "full_name": "", "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "name": github_types.GitHubRepositoryName("test2"), "private": False, }) client = FakeClient(gh_owner["login"], gh_repo["name"]) installation = context.Installation(gh_owner["id"], gh_owner["login"], sub, client, redis_cache) repository = context.Repository(installation, gh_repo["name"], gh_repo["id"]) assert client.called == 0 assert await repository.has_write_permission(user_2) assert client.called == 1 assert await repository.has_write_permission(user_2) assert client.called == 1 assert not await repository.has_write_permission(user_1) assert client.called == 2 await context.Repository.clear_user_permission_cache_for_repo( redis_cache, gh_owner, gh_repo) assert not await repository.has_write_permission(user_1) assert client.called == 3 assert not await repository.has_write_permission(user_3) assert client.called == 4 await context.Repository.clear_user_permission_cache_for_org( redis_cache, gh_owner) assert not await repository.has_write_permission(user_3) assert client.called == 5 assert await repository.has_write_permission(user_2) assert client.called == 6 assert await repository.has_write_permission(user_2) assert client.called == 6 await context.Repository.clear_user_permission_cache_for_user( redis_cache, gh_owner, gh_repo, user_2) assert await repository.has_write_permission(user_2) assert client.called == 7
async def _extract_pulls_from_stream( self, installation: context.Installation ) -> PullsToConsume: messages: typing.List[ typing.Tuple[T_MessageID, T_MessagePayload] ] = await self.redis_stream.xrange( installation.stream_name, count=config.STREAM_MAX_BATCH ) LOG.debug( "read stream", stream_name=installation.stream_name, messages_count=len(messages), ) statsd.histogram("engine.streams.size", len(messages)) statsd.gauge("engine.streams.max_size", config.STREAM_MAX_BATCH) # TODO(sileht): Put this cache in Repository context opened_pulls_by_repo: typing.Dict[ github_types.GitHubRepositoryName, typing.List[github_types.GitHubPullRequest], ] = {} # Groups stream by pull request pulls: PullsToConsume = PullsToConsume(collections.OrderedDict()) for message_id, message in messages: data = msgpack.unpackb(message[b"event"], raw=False) repo = github_types.GitHubRepositoryName(data["repo"]) source = typing.cast(context.T_PayloadEventSource, data["source"]) if data["pull_number"] is not None: key = (repo, github_types.GitHubPullRequestNumber(data["pull_number"])) group = pulls.setdefault(key, ([], [])) group[0].append(message_id) group[1].append(source) else: logger = daiquiri.getLogger( __name__, gh_repo=repo, gh_owner=installation.owner_login, source=source, ) if repo not in opened_pulls_by_repo: try: opened_pulls_by_repo[repo] = [ p async for p in installation.client.items( f"/repos/{installation.owner_login}/{repo}/pulls" ) ] except Exception as e: if exceptions.should_be_ignored(e): opened_pulls_by_repo[repo] = [] else: raise converted_messages = await self._convert_event_to_messages( installation, repo, source, opened_pulls_by_repo[repo], ) logger.debug("event unpacked into %s messages", len(converted_messages)) messages.extend(converted_messages) deleted = await self.redis_stream.xdel( installation.stream_name, message_id ) if deleted != 1: # FIXME(sileht): During shutdown, heroku may have already started # another worker that have already take the lead of this stream_name # This can create duplicate events in the streams but that should not # be a big deal as the engine will not been ran by the worker that's # shutdowning. contents = await self.redis_stream.xrange( installation.stream_name, start=message_id, end=message_id ) if contents: logger.error( "message `%s` have not been deleted has expected, " "(result: %s), content of current message id: %s", message_id, deleted, contents, ) return pulls
async def test_team_permission_cache(redis_cache: utils.RedisCache) -> None: class FakeClient(github.AsyncGithubInstallationClient): called: int def __init__(self, owner: str, repo: str) -> None: super().__init__(auth=None) # type: ignore[arg-type] self.owner = owner self.repo = repo self.called = 0 async def get(self, url: str, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: # type: ignore[override] self.called += 1 if ( url == f"/orgs/{self.owner}/teams/team-ok/repos/{self.owner}/{self.repo}" ): return {} elif ( url == f"/orgs/{self.owner}/teams/team-nok/repos/{self.owner}/{self.repo}" ): raise http.HTTPNotFound( message="Not found", request=mock.ANY, response=mock.ANY ) elif ( url == f"/orgs/{self.owner}/teams/team-also-nok/repos/{self.owner}/{self.repo}" ): raise http.HTTPNotFound( message="Not found", request=mock.ANY, response=mock.ANY ) raise ValueError(f"Unknown test URL `{url}`") gh_owner = github_types.GitHubAccount( { "id": github_types.GitHubAccountIdType(123), "login": github_types.GitHubLogin("jd"), "type": "User", "avatar_url": "", } ) gh_repo = github_types.GitHubRepository( { "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "full_name": "", "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType(""), "name": github_types.GitHubRepositoryName("test"), "private": False, } ) installation_json = github_types.GitHubInstallation( { "id": github_types.GitHubInstallationIdType(12345), "target_type": gh_owner["type"], "permissions": {}, "account": gh_owner, } ) team_slug1 = github_types.GitHubTeamSlug("team-ok") team_slug2 = github_types.GitHubTeamSlug("team-nok") team_slug3 = github_types.GitHubTeamSlug("team-also-nok") sub = subscription.Subscription( redis_cache, 0, "", frozenset([subscription.Features.PUBLIC_REPOSITORY]) ) client = FakeClient(gh_owner["login"], gh_repo["name"]) installation = context.Installation( installation_json, sub, client, redis_cache, mock.Mock() ) repository = context.Repository(installation, gh_repo) assert client.called == 0 assert await repository.team_has_read_permission(team_slug1) assert client.called == 1 assert await repository.team_has_read_permission(team_slug1) assert client.called == 1 assert not await repository.team_has_read_permission(team_slug2) assert client.called == 2 assert not await repository.team_has_read_permission(team_slug2) assert client.called == 2 assert not await repository.team_has_read_permission(team_slug3) assert client.called == 3 gh_repo = github_types.GitHubRepository( { "id": github_types.GitHubRepositoryIdType(1), "owner": gh_owner, "full_name": "", "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType(""), "name": github_types.GitHubRepositoryName("test2"), "private": False, } ) client = FakeClient(gh_owner["login"], gh_repo["name"]) installation = context.Installation( installation_json, sub, client, redis_cache, mock.Mock() ) repository = context.Repository(installation, gh_repo) assert client.called == 0 assert not await repository.team_has_read_permission(team_slug2) assert client.called == 1 # From local cache assert not await repository.team_has_read_permission(team_slug2) assert client.called == 1 # From redis repository._caches.team_has_read_permission.clear() assert not await repository.team_has_read_permission(team_slug2) assert client.called == 1 assert await repository.team_has_read_permission(team_slug1) assert client.called == 2 await context.Repository.clear_team_permission_cache_for_repo( redis_cache, gh_owner, gh_repo ) repository._caches.team_has_read_permission.clear() assert await repository.team_has_read_permission(team_slug1) assert client.called == 3 assert not await repository.team_has_read_permission(team_slug3) assert client.called == 4 await context.Repository.clear_team_permission_cache_for_org(redis_cache, gh_owner) repository._caches.team_has_read_permission.clear() assert not await repository.team_has_read_permission(team_slug3) assert client.called == 5 assert not await repository.team_has_read_permission(team_slug2) assert client.called == 6 # From local cache assert not await repository.team_has_read_permission(team_slug2) assert client.called == 6 # From redis repository._caches.team_has_read_permission.clear() assert not await repository.team_has_read_permission(team_slug2) assert client.called == 6 repository._caches.team_has_read_permission.clear() await context.Repository.clear_team_permission_cache_for_team( redis_cache, gh_owner, team_slug2 ) repository._caches.team_has_read_permission.clear() assert not await repository.team_has_read_permission(team_slug2) assert client.called == 7
async def report( url: str, ) -> typing.Union[context.Context, github.AsyncGithubInstallationClient, None]: redis_cache = utils.create_aredis_for_cache(max_idle_time=0) try: owner, repo, pull_number = _url_parser(url) except ValueError: print(f"{url} is not valid") return None try: client = github.aget_client(owner) except exceptions.MergifyNotInstalled: print(f"* Mergify is not installed on account {owner}") return None # Do a dumb request just to authenticate await client.get("/") if client.auth.installation is None: print("No installation detected") return None print(f"* INSTALLATION ID: {client.auth.installation['id']}") if client.auth.owner_id is None: raise RuntimeError("Unable to get owner_id") if repo is None: slug = None else: slug = owner + "/" + repo cached_sub = await subscription.Subscription.get_subscription( redis_cache, client.auth.owner_id) db_sub = await subscription.Subscription._retrieve_subscription_from_db( redis_cache, client.auth.owner_id) cached_tokens = await user_tokens.UserTokens.get(redis_cache, client.auth.owner_id) db_tokens = await user_tokens.UserTokens._retrieve_from_db( redis_cache, client.auth.owner_id) print(f"* SUBSCRIBED (cache/db): {cached_sub.active} / {db_sub.active}") print("* Features (cache):") for f in db_sub.features: print(f" - {f.value}") print("* Features (db):") for f in cached_sub.features: print(f" - {f.value}") await report_dashboard_synchro(client.auth.installation["id"], cached_sub, cached_tokens, "ENGINE-CACHE", slug) await report_dashboard_synchro(client.auth.installation["id"], db_sub, db_tokens, "DASHBOARD", slug) await report_worker_status(owner) installation = context.Installation(client.auth.owner_id, owner, cached_sub, client, redis_cache) if repo is not None: repo_info: github_types.GitHubRepository = await client.item( f"/repos/{owner}/{repo}") repository = context.Repository(installation, repo_info["name"], repo_info["id"]) print( f"* REPOSITORY IS {'PRIVATE' if repo_info['private'] else 'PUBLIC'}" ) print("* CONFIGURATION:") mergify_config = None config_file = await repository.get_mergify_config_file() if config_file is None: print(".mergify.yml is missing") else: print(f"Config filename: {config_file['path']}") print(config_file["decoded_content"].decode()) try: mergify_config = rules.get_mergify_config(config_file) except rules.InvalidRules as e: # pragma: no cover print(f"configuration is invalid {str(e)}") else: mergify_config["pull_request_rules"].rules.extend( engine.MERGIFY_BUILTIN_CONFIG["pull_request_rules"].rules) if pull_number is None: async for branch in typing.cast( typing.AsyncGenerator[github_types.GitHubBranch, None], client.items(f"/repos/{owner}/{repo}/branches"), ): # TODO(sileht): Add some informations on the train q: queue.QueueBase = naive.Queue(repository, branch["name"]) await report_queue("QUEUES", q) q = merge_train.Train(repository, branch["name"]) await q.load() await report_queue("TRAIN", q) else: repository = context.Repository( installation, github_types.GitHubRepositoryName(repo)) ctxt = await repository.get_pull_request_context( github_types.GitHubPullRequestNumber(int(pull_number))) # FIXME queues could also be printed if no pull number given # TODO(sileht): display train if any q = await naive.Queue.from_context(ctxt) print( f"* QUEUES: {', '.join([f'#{p}' for p in await q.get_pulls()])}" ) q = await merge_train.Train.from_context(ctxt) print( f"* TRAIN: {', '.join([f'#{p}' for p in await q.get_pulls()])}" ) print("* PULL REQUEST:") pr_data = await ctxt.pull_request.items() pprint.pprint(pr_data, width=160) is_behind = await ctxt.is_behind print(f"is_behind: {is_behind}") print(f"mergeable_state: {ctxt.pull['mergeable_state']}") print("* MERGIFY LAST CHECKS:") for c in await ctxt.pull_engine_check_runs: print( f"[{c['name']}]: {c['conclusion']} | {c['output'].get('title')}" ) print("> " + "\n> ".join( ("No Summary", ) if c["output"]["summary"] is None else c["output"]["summary"].split("\n"))) if mergify_config is not None: print("* MERGIFY LIVE MATCHES:") match = await mergify_config["pull_request_rules" ].get_pull_request_rule(ctxt) summary_title, summary = await actions_runner.gen_summary( ctxt, match) print(f"[Summary]: success | {summary_title}") print("> " + "\n> ".join(summary.strip().split("\n"))) return ctxt return client
"id": github_types.GitHubAccountIdType(0), "login": github_types.GitHubLogin(""), "type": "User", "avatar_url": "", }, "label": "", "ref": github_types.GitHubRefType(""), "sha": github_types.SHAType(""), "repo": { "url": "", "default_branch": github_types.GitHubRefType(""), "full_name": "", "archived": False, "id": github_types.GitHubRepositoryIdType(0), "private": False, "name": github_types.GitHubRepositoryName(""), "owner": { "login": github_types.GitHubLogin(""), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, }, }, "head": { "user": { "id": github_types.GitHubAccountIdType(0), "login": github_types.GitHubLogin(""), "type": "User", "avatar_url": "", },
async def test_get_mergify_config_location_from_cache( redis_cache: utils.RedisCache, ) -> None: client = mock.AsyncMock() client.auth.owner = "foo" client.item.side_effect = [ http.HTTPNotFound("Not Found", request=mock.Mock(), response=mock.Mock()), http.HTTPNotFound("Not Found", request=mock.Mock(), response=mock.Mock()), github_types.GitHubContentFile({ "content": encodebytes("whatever".encode()).decode(), "type": "file", "path": ".github/mergify.yml", "sha": github_types.SHAType("zeazeaze"), }), ] gh_owner = github_types.GitHubAccount({ "login": github_types.GitHubLogin("foobar"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "full_name": "foobar/xyz", "name": github_types.GitHubRepositoryName("xyz"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("ref"), }) installation = context.Installation( github_types.GitHubAccountIdType(0), github_types.GitHubLogin("foobar"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository(installation, gh_repo) await repository.get_mergify_config_file() assert client.item.call_count == 3 client.item.assert_has_calls([ mock.call("/repos/foobar/xyz/contents/.mergify.yml", params={}), mock.call("/repos/foobar/xyz/contents/.mergify/config.yml", params={}), mock.call("/repos/foobar/xyz/contents/.github/mergify.yml", params={}), ]) client.item.reset_mock() client.item.side_effect = [ github_types.GitHubContentFile({ "content": encodebytes("whatever".encode()).decode(), "type": "file", "path": ".github/mergify.yml", "sha": github_types.SHAType("zeazeaze"), }), ] repository._cache = context.RepositoryCache() await repository.get_mergify_config_file() assert client.item.call_count == 1 client.item.assert_has_calls([ mock.call("/repos/foobar/xyz/contents/.github/mergify.yml", params={}), ])
def test_url_parser_with_pr_ok(url: str) -> None: assert debug._url_parser(url) == ( github_types.GitHubLogin("mergifyio"), github_types.GitHubRepositoryName("mergify-engine"), github_types.GitHubPullRequestNumber(123), )
async def test_get_commits_to_cherry_pick_merge( commits: mock.PropertyMock, redis_cache: utils.RedisCache, ) -> None: c1 = github_types.GitHubBranchCommit({ "sha": github_types.SHAType("c1f"), "parents": [], "commit": { "message": "foobar" }, }) c2 = github_types.GitHubBranchCommit({ "sha": github_types.SHAType("c2"), "parents": [c1], "commit": { "message": "foobar" }, }) async def fake_commits(): return [c1, c2] commits.return_value = fake_commits() client = mock.Mock() client.auth.get_access_token.return_value = "<token>" gh_owner = github_types.GitHubAccount({ "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "full_name": "user/name", "name": github_types.GitHubRepositoryName("name"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("ref"), }) installation = context.Installation( github_types.GitHubAccountIdType(123), github_types.GitHubLogin("user"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository(installation, gh_repo) ctxt = await context.Context.create( repository, { "number": github_types.GitHubPullRequestNumber(6), "commits": 1, "merged": True, "state": "closed", "html_url": "<html_url>", "id": github_types.GitHubPullRequestId(0), "maintainer_can_modify": True, "labels": [], "rebaseable": True, "draft": True, "merge_commit_sha": None, "title": "foobar", "changed_files": 1, "base": { "label": "user:ref", "sha": github_types.SHAType("sha"), "ref": github_types.GitHubRefType("ref"), "user": gh_owner, "repo": github_types.GitHubRepository( { "full_name": "user/ref", "name": github_types.GitHubRepositoryName("name"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("ref"), }), }, "head": { "label": "user:ref", "sha": github_types.SHAType("sha"), "user": gh_owner, "ref": github_types.GitHubRefType("fork"), "repo": github_types.GitHubRepository( { "full_name": "fork/other", "name": github_types.GitHubRepositoryName("name"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("ref"), }), }, "user": gh_owner, "merged_at": None, "merged_by": None, "mergeable_state": "clean", }, ) base_branch = github_types.GitHubBranchCommit({ "sha": github_types.SHAType("base_branch"), "parents": [], "commit": { "message": "foobar" }, }) merge_commit = github_types.GitHubBranchCommit({ "sha": github_types.SHAType("merge_commit"), "parents": [base_branch, c2], "commit": { "message": "foobar" }, }) assert await duplicate_pull._get_commits_to_cherrypick(ctxt, merge_commit) == [ c1, c2, ]
async def test_get_mergify_config_invalid( invalid: str, redis_cache: utils.RedisCache) -> None: with pytest.raises(InvalidRules): async def item(*args, **kwargs): return github_types.GitHubContentFile({ "content": encodebytes(invalid.encode()).decode(), "path": ".mergify.yml", "type": "file", "sha": "azertyu", }) client = mock.Mock() client.item.return_value = item() gh_owner = github_types.GitHubAccount({ "login": github_types.GitHubLogin("foobar"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "full_name": "foobar/xyz", "name": github_types.GitHubRepositoryName("xyz"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("ref"), }) installation = context.Installation( github_types.GitHubAccountIdType(0), github_types.GitHubLogin("foobar"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository( installation, gh_repo, ) config_file = await repository.get_mergify_config_file() assert config_file is not None get_mergify_config(config_file)
async def test_summary_synchronization_cache( redis_cache: utils.RedisCache, ) -> None: gh_owner = github_types.GitHubAccount({ "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "id": github_types.GitHubRepositoryIdType(456), "full_name": "user/ref", "name": github_types.GitHubRepositoryName("name"), "private": False, "owner": gh_owner, }) async def items(*args, **kwargs): if False: yield return async def post_check(*args, **kwargs): return mock.Mock() client = mock.AsyncMock() client.auth.get_access_token.return_value = "<token>" client.items = items client.post.side_effect = post_check sub = subscription.Subscription(redis_cache, 0, False, "", frozenset()) installation = context.Installation( gh_owner["id"], gh_owner["login"], sub, client, redis_cache, ) repository = context.Repository(installation, gh_repo["name"], gh_repo["id"]) ctxt = await context.Context.create( repository, { "title": "", "id": github_types.GitHubPullRequestId(0), "maintainer_can_modify": False, "rebaseable": False, "draft": False, "merge_commit_sha": None, "labels": [], "number": github_types.GitHubPullRequestNumber(6), "commits": 1, "merged": True, "state": "closed", "changed_files": 1, "html_url": "<html_url>", "base": { "label": "", "sha": github_types.SHAType("sha"), "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "ref": github_types.GitHubRefType("ref"), "label": "", "repo": gh_repo, }, "head": { "label": "", "sha": github_types.SHAType("old-sha-one"), "ref": github_types.GitHubRefType("fork"), "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "repo": { "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "id": github_types.GitHubRepositoryIdType(123), "full_name": "fork/other", "name": github_types.GitHubRepositoryName("other"), "private": False, "owner": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, }, }, "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "merged_by": None, "merged_at": None, "mergeable_state": "clean", }, ) assert await ctxt.get_cached_last_summary_head_sha() is None await ctxt.set_summary_check( check_api.Result(check_api.Conclusion.SUCCESS, "foo", "bar")) assert await ctxt.get_cached_last_summary_head_sha() == "old-sha-one" await ctxt.clear_cached_last_summary_head_sha() assert await ctxt.get_cached_last_summary_head_sha() is None
async def test_get_pull_request_rule(redis_cache: utils.RedisCache) -> None: client = mock.Mock() get_reviews = [{ "user": { "login": "******", "id": 12321, "type": "User" }, "state": "APPROVED", "author_association": "MEMBER", }] get_files = [{"filename": "README.rst"}, {"filename": "setup.py"}] get_team_members = [{ "login": "******", "id": 12321 }, { "login": "******", "id": 2644 }] get_checks: typing.List[github_types.GitHubCheckRun] = [] get_statuses: typing.List[github_types.GitHubStatus] = [{ "context": "continuous-integration/fake-ci", "state": "success", "description": "foobar", "target_url": "http://example.com", "avatar_url": "", }] async def client_item(url, *args, **kwargs): if url == "/repos/another-jd/name/collaborators/sileht/permission": return {"permission": "write"} elif url == "/repos/another-jd/name/collaborators/jd/permission": return {"permission": "write"} raise RuntimeError(f"not handled url {url}") client.item.side_effect = client_item async def client_items(url, *args, **kwargs): if url == "/repos/another-jd/name/pulls/1/reviews": for r in get_reviews: yield r elif url == "/repos/another-jd/name/pulls/1/files": for f in get_files: yield f elif url == "/repos/another-jd/name/commits/<sha>/check-runs": for c in get_checks: yield c elif url == "/repos/another-jd/name/commits/<sha>/status": for s in get_statuses: yield s elif url == "/orgs/another-jd/teams/my-reviewers/members": for tm in get_team_members: yield tm else: raise RuntimeError(f"not handled url {url}") client.items.side_effect = client_items installation = context.Installation( github_types.GitHubAccountIdType(2644), github_types.GitHubLogin("another-jd"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository( installation, github_types.GitHubRepositoryName("name"), github_types.GitHubRepositoryIdType(123321), ) ctxt = await context.Context.create( repository, github_types.GitHubPullRequest({ "id": github_types.GitHubPullRequestId(0), "number": github_types.GitHubPullRequestNumber(1), "commits": 1, "html_url": "<html_url>", "merge_commit_sha": None, "maintainer_can_modify": True, "rebaseable": True, "state": "closed", "merged_by": None, "merged_at": None, "merged": False, "draft": False, "mergeable_state": "unstable", "labels": [], "changed_files": 1, "base": { "label": "repo", "ref": github_types.GitHubRefType("master"), "repo": { "id": github_types.GitHubRepositoryIdType(123321), "name": github_types.GitHubRepositoryName("name"), "full_name": "another-jd/name", "private": False, "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "owner": { "login": github_types.GitHubLogin("another-jd"), "id": github_types.GitHubAccountIdType(2644), "type": "User", "avatar_url": "", }, }, "user": { "login": github_types.GitHubLogin("another-jd"), "id": github_types.GitHubAccountIdType(2644), "type": "User", "avatar_url": "", }, "sha": github_types.SHAType("mew"), }, "head": { "label": "foo", "ref": github_types.GitHubRefType("myfeature"), "sha": github_types.SHAType("<sha>"), "repo": { "id": github_types.GitHubRepositoryIdType(123321), "name": github_types.GitHubRepositoryName("head"), "full_name": "another-jd/head", "private": False, "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "owner": { "login": github_types.GitHubLogin("another-jd"), "id": github_types.GitHubAccountIdType(2644), "type": "User", "avatar_url": "", }, }, "user": { "login": github_types.GitHubLogin("another-jd"), "id": github_types.GitHubAccountIdType(2644), "type": "User", "avatar_url": "", }, }, "title": "My awesome job", "user": { "login": github_types.GitHubLogin("another-jd"), "id": github_types.GitHubAccountIdType(2644), "type": "User", "avatar_url": "", }, }), ) # Empty conditions pull_request_rules = rules.PullRequestRules([ rules.Rule(name="default", conditions=rules.RuleConditions([]), actions={}) ]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["default"] assert [r.name for r in match.matching_rules] == ["default"] assert [ rules.EvaluatedRule.from_rule(r, rules.RuleMissingConditions([]), []) for r in match.rules ] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([{ "name": "hello", "conditions": ["base:master"], "actions": {} }]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["hello"] assert [r.name for r in match.matching_rules] == ["hello"] assert [ rules.EvaluatedRule.from_rule(r, rules.RuleMissingConditions([]), []) for r in match.rules ] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([ { "name": "hello", "conditions": ["base:master"], "actions": {} }, { "name": "backport", "conditions": ["base:master"], "actions": {} }, ]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["hello", "backport"] assert [r.name for r in match.matching_rules] == ["hello", "backport"] assert [ rules.EvaluatedRule.from_rule(r, rules.RuleMissingConditions([]), []) for r in match.rules ] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([ { "name": "hello", "conditions": ["author:foobar"], "actions": {} }, { "name": "backport", "conditions": ["base:master"], "actions": {} }, ]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["hello", "backport"] assert [r.name for r in match.matching_rules] == ["backport"] for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([ { "name": "hello", "conditions": ["author:another-jd"], "actions": {} }, { "name": "backport", "conditions": ["base:master"], "actions": {} }, ]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["hello", "backport"] assert [r.name for r in match.matching_rules] == ["hello", "backport"] assert [ rules.EvaluatedRule.from_rule(r, rules.RuleMissingConditions([]), []) for r in match.rules ] == match.matching_rules for rule in match.rules: assert rule.actions == {} # No match pull_request_rules = pull_request_rule_from_list([{ "name": "merge", "conditions": [ "base=xyz", "check-success=continuous-integration/fake-ci", "#approved-reviews-by>=1", ], "actions": {}, }]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["merge"] assert [r.name for r in match.matching_rules] == [] pull_request_rules = pull_request_rule_from_list([{ "name": "merge", "conditions": [ "base=master", "check-success=continuous-integration/fake-ci", "#approved-reviews-by>=1", ], "actions": {}, }]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["merge"] assert [r.name for r in match.matching_rules] == ["merge"] assert [ rules.EvaluatedRule.from_rule(r, rules.RuleMissingConditions([]), []) for r in match.rules ] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([ { "name": "merge", "conditions": [ "base=master", "check-success=continuous-integration/fake-ci", "#approved-reviews-by>=2", ], "actions": {}, }, { "name": "fast merge", "conditions": [ "base=master", "label=fast-track", "check-success=continuous-integration/fake-ci", "#approved-reviews-by>=1", ], "actions": {}, }, { "name": "fast merge with alternate ci", "conditions": [ "base=master", "label=fast-track", "check-success=continuous-integration/fake-ci-bis", "#approved-reviews-by>=1", ], "actions": {}, }, { "name": "fast merge from a bot", "conditions": [ "base=master", "author=mybot", "check-success=continuous-integration/fake-ci", ], "actions": {}, }, ]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == [ "merge", "fast merge", "fast merge with alternate ci", "fast merge from a bot", ] assert [r.name for r in match.matching_rules] == [ "merge", "fast merge", "fast merge with alternate ci", ] for rule in match.rules: assert rule.actions == {} assert match.matching_rules[0].name == "merge" assert len(match.matching_rules[0].missing_conditions) == 1 assert (str(match.matching_rules[0].missing_conditions[0]) == "#approved-reviews-by>=2") assert match.matching_rules[1].name == "fast merge" assert len(match.matching_rules[1].missing_conditions) == 1 assert str( match.matching_rules[1].missing_conditions[0]) == "label=fast-track" assert match.matching_rules[2].name == "fast merge with alternate ci" assert len(match.matching_rules[2].missing_conditions) == 2 assert str( match.matching_rules[2].missing_conditions[0]) == "label=fast-track" assert (str(match.matching_rules[2].missing_conditions[1]) == "check-success=continuous-integration/fake-ci-bis") # Team conditions with one review missing pull_request_rules = pull_request_rule_from_list([{ "name": "default", "conditions": [ "approved-reviews-by=@another-jd/my-reviewers", "#approved-reviews-by>=2", ], "actions": {}, }]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["default"] assert [r.name for r in match.matching_rules] == ["default"] assert match.matching_rules[0].name == "default" assert len(match.matching_rules[0].missing_conditions) == 1 assert (str(match.matching_rules[0].missing_conditions[0]) == "#approved-reviews-by>=2") get_reviews.append({ "user": { "login": "******", "id": 2644, "type": "User" }, "state": "APPROVED", "author_association": "MEMBER", }) del ctxt._cache["reviews"] del ctxt._cache["consolidated_reviews"] # Team conditions with no review missing pull_request_rules = pull_request_rule_from_list([{ "name": "default", "conditions": [ "approved-reviews-by=@another-jd/my-reviewers", "#approved-reviews-by>=2", ], "actions": {}, }]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["default"] assert [r.name for r in match.matching_rules] == ["default"] assert match.matching_rules[0].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0 # Forbidden labels, when no label set pull_request_rules = pull_request_rule_from_list([{ "name": "default", "conditions": ["-label~=^(status/wip|status/blocked|review/need2)$"], "actions": {}, }]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["default"] assert [r.name for r in match.matching_rules] == ["default"] assert match.matching_rules[0].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0 # Forbidden labels, when forbiden label set ctxt.pull["labels"] = [{ "id": 0, "color": "#1234", "default": False, "name": "status/wip" }] match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["default"] assert [r.name for r in match.matching_rules] == ["default"] assert match.matching_rules[0].name == "default" assert len(match.matching_rules[0].missing_conditions) == 1 assert str(match.matching_rules[0].missing_conditions[0]) == ( "-label~=^(status/wip|status/blocked|review/need2)$") # Forbidden labels, when other label set ctxt.pull["labels"] = [{ "id": 0, "color": "#1234", "default": False, "name": "allowed" }] match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["default"] assert [r.name for r in match.matching_rules] == ["default"] assert match.matching_rules[0].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0 # Test team expander pull_request_rules = pull_request_rule_from_list([{ "name": "default", "conditions": ["author~=^(user1|user2|another-jd)$"], "actions": {}, }]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["default"] assert [r.name for r in match.matching_rules] == ["default"] assert match.matching_rules[0].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0 # branch protection async def client_item_with_branch_protection_enabled(url, *args, **kwargs): if url == "/repos/another-jd/name/branches/master": return { "protection": { "enabled": True, "required_status_checks": { "contexts": ["awesome-ci"] }, }, } raise RuntimeError(f"not handled url {url}") client.item.side_effect = client_item_with_branch_protection_enabled pull_request_rules = pull_request_rule_from_list([{ "name": "default", "conditions": [], "actions": { "merge": {}, "comment": { "message": "yo" } }, }]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["default", "default"] assert list(match.matching_rules[0].actions.keys()) == ["merge"] assert [str(c) for c in match.matching_rules[0].conditions ] == ["check-success=awesome-ci"] assert [str(c) for c in match.matching_rules[0].missing_conditions ] == ["check-success=awesome-ci"] assert list(match.matching_rules[1].actions.keys()) == ["comment"] assert match.matching_rules[1].conditions == []
async def test_get_mergify_config_with_defaults( redis_cache: utils.RedisCache) -> None: config = """ defaults: actions: comment: bot_account: foo-bot rebase: bot_account: test-bot-account pull_request_rules: - name: ahah conditions: - base=master actions: comment: message: I love Mergify rebase: {} """ async def item(*args, **kwargs): return github_types.GitHubContentFile({ "content": encodebytes(config.encode()).decode(), "path": ".mergify.yml", "type": "file", "sha": "azertyu", }) client = mock.Mock() client.item.return_value = item() installation = context.Installation( github_types.GitHubAccountIdType(0), github_types.GitHubLogin("foobar"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository( installation, github_types.GitHubRepositoryName("xyz"), github_types.GitHubRepositoryIdType(0), ) config_file = await repository.get_mergify_config_file() assert config_file is not None schema = get_mergify_config(config_file) assert isinstance(schema, dict) assert len(schema["pull_request_rules"].rules) == 1 comment = schema["pull_request_rules"].rules[0].actions["comment"].config assert comment == {"message": "I love Mergify", "bot_account": "foo-bot"} rebase = schema["pull_request_rules"].rules[0].actions["rebase"].config assert rebase == {"bot_account": "test-bot-account"} config = """ defaults: actions: comment: message: I love Mergify bot_account: AutoBot pull_request_rules: - name: ahah conditions: - base=master actions: comment: message: I really love Mergify """ client = mock.Mock() client.item.return_value = item() installation = context.Installation( github_types.GitHubAccountIdType(0), github_types.GitHubLogin("foobar"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository( installation, github_types.GitHubRepositoryName("xyz"), github_types.GitHubRepositoryIdType(0), ) config_file = await repository.get_mergify_config_file() assert config_file is not None schema = get_mergify_config(config_file) assert isinstance(schema, dict) assert len(schema["pull_request_rules"].rules) == 1 comment = schema["pull_request_rules"].rules[0].actions["comment"].config assert comment == { "message": "I really love Mergify", "bot_account": "AutoBot" }
async def test_signals(redis_cache): gh_owner = github_types.GitHubAccount({ "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "id": github_types.GitHubRepositoryIdType(456), "full_name": "user/ref", "name": github_types.GitHubRepositoryName("name"), "private": False, "owner": gh_owner, }) client = mock.AsyncMock() client.auth.get_access_token.return_value = "<token>" sub = subscription.Subscription(redis_cache, 0, False, "", frozenset()) installation = context.Installation( gh_owner["id"], gh_owner["login"], sub, client, redis_cache, ) repository = context.Repository(installation, gh_repo["name"], gh_repo["id"]) ctxt = await context.Context.create( repository, { "title": "", "id": github_types.GitHubPullRequestId(0), "maintainer_can_modify": False, "rebaseable": False, "draft": False, "merge_commit_sha": None, "labels": [], "number": github_types.GitHubPullRequestNumber(6), "commits": 1, "merged": True, "state": "closed", "changed_files": 1, "html_url": "<html_url>", "base": { "label": "", "sha": github_types.SHAType("sha"), "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "ref": github_types.GitHubRefType("ref"), "label": "", "repo": gh_repo, }, "head": { "label": "", "sha": github_types.SHAType("old-sha-one"), "ref": github_types.GitHubRefType("fork"), "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "repo": { "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "id": github_types.GitHubRepositoryIdType(123), "full_name": "fork/other", "name": github_types.GitHubRepositoryName("other"), "private": False, "owner": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, }, }, "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "merged_by": None, "merged_at": None, "mergeable_state": "clean", }, ) assert len(signals.SIGNALS) == 0 signals.setup() assert len(signals.SIGNALS) == 1 with mock.patch( "mergify_engine_signals.noop.Signal.__call__") as signal_method: await signals.send(ctxt, "action.update") signal_method.assert_called_once_with(ctxt, "action.update")
async def _prepare_repo(self) -> count_seats.Seats: await self.setup_repo() await self.create_pr() await self.run_engine() # NOTE(sileht): we add active users only on the repository used for # recording the fixture repos = (await self.client_admin.get( url=f"{config.GITHUB_REST_API_URL}/orgs/mergifyio-testing/repos" )).json() members = (await self.client_admin.get( url=f"{config.GITHUB_REST_API_URL}/orgs/mergifyio-testing/members" )).json() write_users = { count_seats.SeatAccount( github_types.GitHubAccountIdType(member["id"]), github_types.GitHubLogin(member["login"]), ) for member in members } organization = {} for repo in repos: active_users = None key_repo = count_seats.SeatRepository( github_types.GitHubRepositoryIdType(repo["id"]), github_types.GitHubRepositoryName(repo["name"]), ) if repo["id"] == self.repository_ctxt.repo["id"]: active_users = { count_seats.ActiveUser( github_types.GitHubAccountIdType( config.TESTING_MERGIFY_TEST_1_ID), github_types.GitHubLogin("mergify-test1"), ), count_seats.ActiveUser( github_types.GitHubAccountIdType( config.TESTING_MERGIFY_TEST_2_ID), github_types.GitHubLogin("mergify-test2"), ), } write_users_used: typing.Union[typing.Set[count_seats.SeatAccount], mock.ANY] if repo["name"] == "functional-testing-repo": write_users_used = write_users else: write_users_used = mock.ANY organization[key_repo] = count_seats.CollaboratorsSetsT({ "write_users": write_users_used, "active_users": active_users, }) collaborators = { count_seats.SeatAccount( github_types.GitHubAccountIdType(config.TESTING_ORGANIZATION_ID), github_types.GitHubLogin(config.TESTING_ORGANIZATION_NAME), ): organization } return count_seats.Seats(collaborators)
async def test_get_commits_to_cherry_pick_rebase( commits: mock.PropertyMock, redis_cache: utils.RedisCache, ) -> None: gh_owner = github_types.GitHubAccount({ "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "full_name": "user/name", "name": github_types.GitHubRepositoryName("name"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("ref"), }) c1 = github_types.GitHubBranchCommit({ "sha": github_types.SHAType("c1f"), "parents": [], "commit": { "message": "foobar" }, }) c2 = github_types.GitHubBranchCommit({ "sha": github_types.SHAType("c2"), "parents": [c1], "commit": { "message": "foobar" }, }) commits.return_value = [c1, c2] client = mock.Mock() client.auth.get_access_token.return_value = "<token>" client.items.side_effect = fake_get_github_pulls_from_sha installation = context.Installation( github_types.GitHubAccountIdType(123), github_types.GitHubLogin("user"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository(installation, gh_repo) ctxt = await context.Context.create( repository, { "labels": [], "draft": False, "merge_commit_sha": github_types.SHAType(""), "title": "", "commits": 1, "rebaseable": False, "maintainer_can_modify": False, "id": github_types.GitHubPullRequestId(0), "number": github_types.GitHubPullRequestNumber(6), "merged": True, "state": "closed", "html_url": "<html_url>", "changed_files": 1, "base": { "label": "", "sha": github_types.SHAType("sha"), "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "ref": github_types.GitHubRefType("ref"), "repo": { "full_name": "user/ref", "name": github_types.GitHubRepositoryName("name"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType(""), }, }, "head": { "label": "", "sha": github_types.SHAType("sha"), "ref": github_types.GitHubRefType("fork"), "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "repo": { "full_name": "fork/other", "name": github_types.GitHubRepositoryName("other"), "private": False, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType(""), "id": github_types.GitHubRepositoryIdType(0), "owner": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, }, }, "user": { "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }, "merged_by": None, "merged_at": None, "mergeable_state": "clean", }, ) base_branch = github_types.GitHubBranchCommitParent( {"sha": github_types.SHAType("base_branch")}) rebased_c1 = github_types.GitHubBranchCommit({ "sha": github_types.SHAType("rebased_c1"), "parents": [base_branch], "commit": { "message": "hello c1" }, }) rebased_c2 = github_types.GitHubBranchCommit({ "sha": github_types.SHAType("rebased_c2"), "parents": [rebased_c1], "commit": { "message": "hello c2" }, }) async def fake_get_github_commit_from_sha(url, api_version=None): if url.endswith("/commits/rebased_c1"): return rebased_c1 if url.endswith("/commits/rebased_c2"): return rebased_c2 raise RuntimeError(f"Unknown URL {url}") client.item.side_effect = fake_get_github_commit_from_sha assert await duplicate_pull._get_commits_to_cherrypick(ctxt, rebased_c2) == [ rebased_c1, rebased_c2, ]
async def recorder( request: pytest.FixtureRequest, ) -> typing.Optional[RecorderFixture]: is_unittest_class = request.cls is not None marker = request.node.get_closest_marker("recorder") if not is_unittest_class and marker is None: return None if is_unittest_class: cassette_library_dir = os.path.join( CASSETTE_LIBRARY_DIR_BASE, request.cls.__name__, request.node.name, ) else: cassette_library_dir = os.path.join( CASSETTE_LIBRARY_DIR_BASE, request.node.module.__name__.replace( "mergify_engine.tests.functional.", "").replace(".", "/"), request.node.name, ) # Recording stuffs if RECORD: if os.path.exists(cassette_library_dir): shutil.rmtree(cassette_library_dir) os.makedirs(cassette_library_dir) recorder = vcr.VCR( cassette_library_dir=cassette_library_dir, record_mode="all" if RECORD else "none", match_on=["method", "uri"], ignore_localhost=True, filter_headers=[ ("Authorization", "<TOKEN>"), ("X-Hub-Signature", "<SIGNATURE>"), ("User-Agent", None), ("Accept-Encoding", None), ("Connection", None), ], before_record_response=pyvcr_response_filter, before_record_request=pyvcr_request_filter, ) if RECORD: github.CachedToken.STORAGE = {} else: # Never expire token during replay patcher = mock.patch.object(github_app, "get_or_create_jwt", return_value="<TOKEN>") patcher.start() request.addfinalizer(patcher.stop) patcher = mock.patch.object( github.GithubAppInstallationAuth, "get_access_token", return_value="<TOKEN>", ) patcher.start() request.addfinalizer(patcher.stop) # Let's start recording cassette = recorder.use_cassette("http.json") cassette.__enter__() request.addfinalizer(cassette.__exit__) record_config_file = os.path.join(cassette_library_dir, "config.json") if RECORD: with open(record_config_file, "w") as f: f.write( json.dumps( RecordConfigType({ "organization_id": config.TESTING_ORGANIZATION_ID, "organization_name": config.TESTING_ORGANIZATION_NAME, "repository_id": config.TESTING_REPOSITORY_ID, "repository_name": github_types.GitHubRepositoryName( config.TESTING_REPOSITORY_NAME), "branch_prefix": datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S"), }))) with open(record_config_file, "r") as f: return RecorderFixture( typing.cast(RecordConfigType, json.loads(f.read())), recorder)
async def report( url: str, ) -> typing.Union[context.Context, github.AsyncGithubInstallationClient, None]: redis_links = redis_utils.RedisLinks(max_idle_time=0) try: owner_login, repo, pull_number = _url_parser(url) except ValueError: print(f"{url} is not valid") return None try: installation_json = await github.get_installation_from_login( owner_login) client = github.aget_client(installation_json) except exceptions.MergifyNotInstalled: print(f"* Mergify is not installed on account {owner_login}") return None # Do a dumb request just to authenticate await client.get("/") print(f"* INSTALLATION ID: {installation_json['id']}") if repo is None: slug = None else: slug = owner_login + "/" + repo owner_id = installation_json["account"]["id"] cached_sub = await subscription.Subscription.get_subscription( redis_links.cache, owner_id) db_sub = await subscription.Subscription._retrieve_subscription_from_db( redis_links.cache, owner_id) cached_tokens = await user_tokens.UserTokens.get(redis_links.cache, owner_id) if config.SAAS_MODE: db_tokens = typing.cast( user_tokens.UserTokens, (await user_tokens.UserTokensSaas._retrieve_from_db( redis_links.cache, owner_id)), ) else: db_tokens = cached_tokens print("* Features (db):") for v in sorted(f.value for f in db_sub.features): print(f" - {v}") print("* Features (cache):") for v in sorted(f.value for f in cached_sub.features): print(f" - {v}") installation = context.Installation(installation_json, cached_sub, client, redis_links) await report_dashboard_synchro(installation.installation["id"], cached_sub, cached_tokens, "ENGINE-CACHE", slug) await report_dashboard_synchro(installation.installation["id"], db_sub, db_tokens, "DASHBOARD", slug) await report_worker_status(owner_login) if repo is not None: repository = await installation.get_repository_by_name(repo) print( f"* REPOSITORY IS {'PRIVATE' if repository.repo['private'] else 'PUBLIC'}" ) print(f"* DEFAULT BRANCH: {repository.repo['default_branch']}") print("* CONFIGURATION:") mergify_config = None config_file = await repository.get_mergify_config_file() if not config_file: print(".mergify.yml is missing") else: print(f"Config filename: {config_file['path']}") print(config_file["decoded_content"]) try: mergify_config = await repository.get_mergify_config() except rules.InvalidRules as e: # pragma: no cover print(f"configuration is invalid {str(e)}") if pull_number is None: async for branch in typing.cast( typing.AsyncGenerator[github_types.GitHubBranch, None], client.items( f"/repos/{owner_login}/{repo}/branches", resource_name="branches", page_limit=100, ), ): q = merge_train.Train(repository, branch["name"]) await q.load() await report_queue("TRAIN", q) else: repository = await installation.get_repository_by_name( github_types.GitHubRepositoryName(repo)) try: ctxt = await repository.get_pull_request_context( github_types.GitHubPullRequestNumber(int(pull_number))) except http.HTTPNotFound: print(f"Pull request `{url}` does not exist") return client # FIXME queues could also be printed if no pull number given # TODO(sileht): display train if any q = await merge_train.Train.from_context(ctxt) print( f"* TRAIN: {', '.join([f'#{p}' for p in await q.get_pulls()])}" ) print("* PULL REQUEST:") pr_data = await ctxt.pull_request.items() pprint.pprint(pr_data, width=160) is_behind = await ctxt.is_behind print(f"is_behind: {is_behind}") print(f"mergeable_state: {ctxt.pull['mergeable_state']}") print("* MERGIFY LAST CHECKS:") for c in await ctxt.pull_engine_check_runs: print( f"[{c['name']}]: {c['conclusion']} | {c['output'].get('title')} | {c['html_url']}" ) print("> " + "\n> ".join( ("No Summary", ) if c["output"]["summary"] is None else c["output"]["summary"].split("\n"))) if mergify_config is not None: print("* MERGIFY LIVE MATCHES:") pull_request_rules = mergify_config["pull_request_rules"] match = await pull_request_rules.get_pull_request_rule(ctxt) summary_title, summary = await actions_runner.gen_summary( ctxt, pull_request_rules, match) print(f"[Summary]: success | {summary_title}") print("> " + "\n> ".join(summary.strip().split("\n"))) return ctxt return client
async def prepare_context(client, redis_cache, subscribed=True): sub = subscription.Subscription( redis_cache, 123, subscribed, "sub or not to sub", frozenset( getattr(subscription.Features, f) for f in subscription.Features.__members__) if subscribed else frozenset(), ) gh_owner = github_types.GitHubAccount({ "login": github_types.GitHubLogin("user"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "full_name": "user/name", "name": github_types.GitHubRepositoryName("name"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("ref"), }) installation = context.Installation(123, "Mergifyio", sub, client, redis_cache) repository = context.Repository(installation, gh_repo) return await context.Context.create( repository, { "id": 12345, "number": 123, "state": None, "mergeable_state": "ok", "merged_by": None, "merged": None, "merged_at": None, "user": { "login": "******" }, "requested_reviewers": [{ "login": "******" }, { "login": "******" }], "requested_teams": [{ "slug": "foobar" }, { "slug": "foobaz" }], "base": { "sha": "sha", "ref": "main", "user": { "login": { "Mergifyio", }, }, "repo": { "full_name": "Mergifyio/demo", "name": "demo", "private": False, "owner": { "login": "******", }, }, }, }, )