From ab25bf57c6472712f5f4526ee1eec9539a56a4b9 Mon Sep 17 00:00:00 2001 From: uy_sun Date: Wed, 23 Aug 2023 12:06:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=83=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=90=8E=E5=A6=82=E6=9E=9C=E5=8F=91=E7=8E=B0?= =?UTF-8?q?=E4=B8=8D=E9=80=9A=E8=BF=87=E5=B0=86=E6=8B=89=E5=8F=96=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E8=BD=AC=E6=88=90=E8=8D=89=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + src/plugins/publish/__init__.py | 23 +- src/plugins/publish/utils.py | 10 + tests/publish/process/test_publish_check.py | 375 +++++++++++++++++++- 4 files changed, 399 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1187e807..f2cb1254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/ ### Added - 使用 jinja 渲染评论 +- 发布信息更新后如果发现不通过将拉取请求转成草稿 ### Changed diff --git a/src/plugins/publish/__init__.py b/src/plugins/publish/__init__.py index 5a827f9d..fa8c2814 100644 --- a/src/plugins/publish/__init__.py +++ b/src/plugins/publish/__init__.py @@ -189,10 +189,10 @@ async def handle_publish_check( # 限制标题长度,过长的标题不好看 title = f"{publish_type.value}: {result['name'][:MAX_NAME_LENGTH]}" + # 分支命名示例 publish/issue123 + branch_name = f"{BRANCH_NAME_PREFIX}{issue_number}" if result["valid"]: # 创建新分支 - # 命名示例 publish/issue123 - branch_name = f"{BRANCH_NAME_PREFIX}{issue_number}" run_shell_command(["git", "switch", "-C", branch_name]) # 更新文件并提交更改 update_file(result) @@ -202,7 +202,24 @@ async def handle_publish_check( bot, repo_info, result, branch_name, issue_number, title ) else: - logger.info("发布没通过检查,暂不创建拉取请求") + # 如果之前已经创建了拉取请求,则将其转换为草稿 + pulls = ( + await bot.rest.pulls.async_list( + **repo_info.dict(), head=f"{repo_info.owner}:{branch_name}" + ) + ).parsed_data + if pulls and (pull := pulls[0]) and not pull.draft: + await bot.async_graphql( + query="""mutation convertPullRequestToDraft($pullRequestId: ID!) { + convertPullRequestToDraft(input: {pullRequestId: $pullRequestId}) { + clientMutationId + } + }""", + variables={"pullRequestId": pull.node_id}, + ) + logger.info("发布没通过检查,已将之前的拉取请求转换为草稿") + else: + logger.info("发布没通过检查,暂不创建拉取请求") # 修改议题标题 # 需要等创建完拉取请求并打上标签后执行 diff --git a/src/plugins/publish/utils.py b/src/plugins/publish/utils.py index 3e1fa8c6..ad56b774 100644 --- a/src/plugins/publish/utils.py +++ b/src/plugins/publish/utils.py @@ -402,6 +402,16 @@ async def create_pull_request( **repo_info.dict(), pull_number=pull.number, title=title ) logger.info(f"拉取请求标题已修改为 {title}") + if pull.draft: + await bot.async_graphql( + query="""mutation markPullRequestReadyForReview($pullRequestId: ID!) { + markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) { + clientMutationId + } + }""", + variables={"pullRequestId": pull.node_id}, + ) + logger.info("拉取请求已标记为可评审") async def comment_issue( diff --git a/tests/publish/process/test_publish_check.py b/tests/publish/process/test_publish_check.py index 4acfa861..4499388f 100644 --- a/tests/publish/process/test_publish_check.py +++ b/tests/publish/process/test_publish_check.py @@ -249,6 +249,7 @@ async def test_edit_title( mock_pull = mocker.MagicMock() mock_pull.number = 2 + mock_pull.draft = False mock_pulls_resp = mocker.MagicMock() mock_pulls_resp.parsed_data = [mock_pull] @@ -461,10 +462,8 @@ async def test_edit_title_too_long( mock_list_comments_resp = mocker.MagicMock() mock_list_comments_resp.parsed_data = [mock_comment] - mock_pull = mocker.MagicMock() - mock_pull.number = 2 mock_pulls_resp = mocker.MagicMock() - mock_pulls_resp.parsed_data = [mock_pull] + mock_pulls_resp.parsed_data = [] with open(tmp_path / "bots.json", "w") as f: json.dump([], f) @@ -498,7 +497,16 @@ async def test_edit_title_too_long( {"owner": "he0119", "repo": "action-test", "issue_number": 80}, mock_list_comments_resp, ) - # 修改标题,应该被阶段,且不会更新拉取请求的标题 + ctx.should_call_api( + "rest.pulls.async_list", + { + "owner": "he0119", + "repo": "action-test", + "head": "he0119:publish/issue80", + }, + mock_pulls_resp, + ) + # 修改标题,应该被截断,且不会更新拉取请求的标题 ctx.should_call_api( "rest.issues.async_update", { @@ -574,6 +582,9 @@ async def test_process_publish_check_not_pass( mock_issues_resp = mocker.MagicMock() mock_issues_resp.parsed_data = mock_issue + mock_pulls_resp = mocker.MagicMock() + mock_pulls_resp.parsed_data = [] + mock_comment = mocker.MagicMock() mock_comment.body = "Bot: test" mock_list_comments_resp = mocker.MagicMock() @@ -612,6 +623,15 @@ async def test_process_publish_check_not_pass( {"owner": "he0119", "repo": "action-test", "issue_number": 80}, mock_list_comments_resp, ) + ctx.should_call_api( + "rest.pulls.async_list", + { + "owner": "he0119", + "repo": "action-test", + "head": "he0119:publish/issue80", + }, + mock_pulls_resp, + ) # 检查是否可以复用评论 ctx.should_call_api( "rest.issues.async_list_comments", @@ -847,10 +867,8 @@ async def test_skip_plugin_check( mock_list_comments_resp = mocker.MagicMock() mock_list_comments_resp.parsed_data = [mock_comment] - mock_pull = mocker.MagicMock() - mock_pull.number = 2 mock_pulls_resp = mocker.MagicMock() - mock_pulls_resp.parsed_data = mock_pull + mock_pulls_resp.parsed_data = [] with open(tmp_path / "plugins.json", "w") as f: json.dump([], f) @@ -894,6 +912,15 @@ async def test_skip_plugin_check( }, True, ) + ctx.should_call_api( + "rest.pulls.async_list", + { + "owner": "he0119", + "repo": "action-test", + "head": "he0119:publish/issue70", + }, + mock_pulls_resp, + ) # 修改标题 ctx.should_call_api( "rest.issues.async_update", @@ -943,3 +970,337 @@ async def test_skip_plugin_check( check_json_data(plugin_config.input_config.plugin_path, []) assert mocked_api["project_link"].called + + +async def test_convert_pull_request_to_draft( + app: App, mocker: MockerFixture, mocked_api: MockRouter, tmp_path: Path +) -> None: + """未通过时将拉取请求转换为草稿""" + from src.plugins.publish import publish_check_matcher + from src.plugins.publish.config import plugin_config + + mock_subprocess_run = mocker.patch( + "subprocess.run", side_effect=lambda *args, **kwargs: mocker.MagicMock() + ) + + mock_installation = mocker.MagicMock() + mock_installation.id = 123 + mock_installation_resp = mocker.MagicMock() + mock_installation_resp.parsed_data = mock_installation + + mock_issue = mocker.MagicMock() + mock_issue.pull_request = None + mock_issue.title = "Bot: test" + mock_issue.number = 1 + mock_issue.state = "open" + mock_issue.body = generate_issue_body_bot( + name="test", homepage="https://www.baidu.com" + ) + mock_issue.user.login = "test" + + mock_issues_resp = mocker.MagicMock() + mock_issues_resp.parsed_data = mock_issue + + mock_pull = mocker.MagicMock() + mock_pull.number = 2 + mock_pull.title = "Bot: test" + mock_pull.draft = False + mock_pull.node_id = "123" + mock_pulls_resp = mocker.MagicMock() + mock_pulls_resp.parsed_data = [mock_pull] + + mock_comment = mocker.MagicMock() + mock_comment.body = "Bot: test" + mock_list_comments_resp = mocker.MagicMock() + mock_list_comments_resp.parsed_data = [mock_comment] + + with open(tmp_path / "bots.json", "w") as f: + json.dump([], f) + + check_json_data(plugin_config.input_config.bot_path, []) + + async with app.test_matcher(publish_check_matcher) as ctx: + adapter = get_adapter(Adapter) + bot = ctx.create_bot( + base=GitHubBot, + adapter=adapter, + self_id=GitHubApp(app_id="1", private_key="1"), # type: ignore + ) + bot = cast(GitHubBot, bot) + event_path = Path(__file__).parent.parent / "events" / "issue-open.json" + event = Adapter.payload_to_event("1", "issues", event_path.read_bytes()) + assert isinstance(event, IssuesOpened) + + ctx.should_call_api( + "rest.apps.async_get_repo_installation", + {"owner": "he0119", "repo": "action-test"}, + mock_installation_resp, + ) + ctx.should_call_api( + "rest.issues.async_get", + {"owner": "he0119", "repo": "action-test", "issue_number": 80}, + mock_issues_resp, + ) + # 检查是否需要跳过插件测试 + ctx.should_call_api( + "rest.issues.async_list_comments", + {"owner": "he0119", "repo": "action-test", "issue_number": 80}, + mock_list_comments_resp, + ) + ctx.should_call_api( + "rest.pulls.async_list", + { + "owner": "he0119", + "repo": "action-test", + "head": "he0119:publish/issue80", + }, + mock_pulls_resp, + ) + # 将拉取请求转换为草稿 + ctx.should_call_api( + "async_graphql", + { + "query": "mutation convertPullRequestToDraft($pullRequestId: ID!) {\n convertPullRequestToDraft(input: {pullRequestId: $pullRequestId}) {\n clientMutationId\n }\n }", + "variables": {"pullRequestId": "123"}, + }, + True, + ) + # 检查是否可以复用评论 + ctx.should_call_api( + "rest.issues.async_list_comments", + {"owner": "he0119", "repo": "action-test", "issue_number": 80}, + mock_list_comments_resp, + ) + + ctx.should_call_api( + "rest.issues.async_create_comment", + { + "owner": "he0119", + "repo": "action-test", + "issue_number": 80, + "body": """# 📃 商店发布检查结果\n\n> Bot: test\n\n**⚠️ 在发布检查过程中,我们发现以下问题:**\n\n
  • ⚠️ 项目 主页 返回状态码 404。
    请确保你的项目主页可访问。
  • \n\n
    \n详情\n
  • ✅ 标签: test-#ffffff。
  • \n
    \n\n---\n\n💡 如需修改信息,请直接修改 issue,机器人会自动更新检查结果。\n💡 当插件加载测试失败时,请发布新版本后在当前页面下评论任意内容以触发测试。\n\n\n💪 Powered by [NoneFlow](https://github.com/nonebot/noneflow)\n\n""", + }, + True, + ) + + ctx.receive_event(bot, event) + + # 测试 git 命令 + mock_subprocess_run.assert_has_calls( + [ + mocker.call( + ["git", "config", "--global", "safe.directory", "*"], + check=True, + capture_output=True, + ), + mocker.call( + ["pre-commit", "install", "--install-hooks"], + check=True, + capture_output=True, + ), + ] # type: ignore + ) + + # 检查文件是否正确 + check_json_data(plugin_config.input_config.bot_path, []) + + assert mocked_api["homepage_failed"].called + + +async def test_process_publish_check_ready_for_review( + app: App, mocker: MockerFixture, mocked_api: MockRouter, tmp_path: Path +) -> None: + """当之前失败后再次通过测试时,应该将拉取请求标记为 ready for review""" + from src.plugins.publish import publish_check_matcher + from src.plugins.publish.config import plugin_config + + mock_subprocess_run = mocker.patch( + "subprocess.run", side_effect=lambda *args, **kwargs: mocker.MagicMock() + ) + + mock_installation = mocker.MagicMock() + mock_installation.id = 123 + mock_installation_resp = mocker.MagicMock() + mock_installation_resp.parsed_data = mock_installation + + mock_issue = mocker.MagicMock() + mock_issue.pull_request = None + mock_issue.title = "Bot: test" + mock_issue.number = 80 + mock_issue.state = "open" + mock_issue.body = generate_issue_body_bot(name="test") + mock_issue.user.login = "test" + + mock_event = mocker.MagicMock() + mock_event.issue = mock_issue + + mock_issues_resp = mocker.MagicMock() + mock_issues_resp.parsed_data = mock_issue + + mock_comment = mocker.MagicMock() + mock_comment.body = "Bot: test" + mock_list_comments_resp = mocker.MagicMock() + mock_list_comments_resp.parsed_data = [mock_comment] + + mock_pull = mocker.MagicMock() + mock_pull.number = 2 + mock_pull.title = "Bot: test" + mock_pull.draft = True + mock_pull.node_id = "123" + mock_pulls_resp = mocker.MagicMock() + mock_pulls_resp.parsed_data = [mock_pull] + + with open(tmp_path / "bots.json", "w") as f: + json.dump([], f) + + check_json_data(plugin_config.input_config.bot_path, []) + + async with app.test_matcher(publish_check_matcher) as ctx: + adapter = get_adapter(Adapter) + bot = ctx.create_bot( + base=GitHubBot, + adapter=adapter, + self_id=GitHubApp(app_id="1", private_key="1"), # type: ignore + ) + bot = cast(GitHubBot, bot) + event_path = Path(__file__).parent.parent / "events" / "issue-open.json" + event = Adapter.payload_to_event("1", "issues", event_path.read_bytes()) + assert isinstance(event, IssuesOpened) + + ctx.should_call_api( + "rest.apps.async_get_repo_installation", + {"owner": "he0119", "repo": "action-test"}, + mock_installation_resp, + ) + ctx.should_call_api( + "rest.issues.async_get", + {"owner": "he0119", "repo": "action-test", "issue_number": 80}, + mock_issues_resp, + ) + ctx.should_call_api( + "rest.issues.async_list_comments", + {"owner": "he0119", "repo": "action-test", "issue_number": 80}, + mock_list_comments_resp, + ) + ctx.should_call_api( + "rest.pulls.async_create", + { + "owner": "he0119", + "repo": "action-test", + "title": "Bot: test", + "body": "resolve #80", + "base": "master", + "head": "publish/issue80", + }, + exception=RequestFailed( + Response( + httpx.Response(422, request=httpx.Request("test", "test")), None + ) + ), + ) + ctx.should_call_api( + "rest.pulls.async_list", + { + "owner": "he0119", + "repo": "action-test", + "head": "he0119:publish/issue80", + }, + mock_pulls_resp, + ) + # 将拉取请求标记为可供审阅 + ctx.should_call_api( + "async_graphql", + { + "query": "mutation markPullRequestReadyForReview($pullRequestId: ID!) {\n markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) {\n clientMutationId\n }\n }", + "variables": {"pullRequestId": "123"}, + }, + True, + ) + ctx.should_call_api( + "rest.issues.async_list_comments", + {"owner": "he0119", "repo": "action-test", "issue_number": 80}, + mock_list_comments_resp, + ) + ctx.should_call_api( + "rest.issues.async_create_comment", + { + "owner": "he0119", + "repo": "action-test", + "issue_number": 80, + "body": """# 📃 商店发布检查结果\n\n> Bot: test\n\n**✅ 所有测试通过,一切准备就绪!**\n\n\n
    \n详情\n
  • ✅ 项目 主页 返回状态码 200。
  • ✅ 标签: test-#ffffff。
  • \n
    \n\n---\n\n💡 如需修改信息,请直接修改 issue,机器人会自动更新检查结果。\n💡 当插件加载测试失败时,请发布新版本后在当前页面下评论任意内容以触发测试。\n\n\n💪 Powered by [NoneFlow](https://github.com/nonebot/noneflow)\n\n""", + }, + True, + ) + + ctx.receive_event(bot, event) + + # 测试 git 命令 + mock_subprocess_run.assert_has_calls( + [ + mocker.call( + ["git", "config", "--global", "safe.directory", "*"], + check=True, + capture_output=True, + ), + mocker.call( + ["pre-commit", "install", "--install-hooks"], + check=True, + capture_output=True, + ), + mocker.call( + ["git", "switch", "-C", "publish/issue80"], + check=True, + capture_output=True, + ), + mocker.call( + ["git", "config", "--global", "user.name", "test"], + check=True, + capture_output=True, + ), + mocker.call( + [ + "git", + "config", + "--global", + "user.email", + "test@users.noreply.github.com", + ], + check=True, + capture_output=True, + ), + mocker.call(["git", "add", "-A"], check=True, capture_output=True), + mocker.call( + ["git", "commit", "-m", ":beers: publish bot test (#80)"], + check=True, + capture_output=True, + ), + mocker.call(["git", "fetch", "origin"], check=True, capture_output=True), + mocker.call( + ["git", "diff", "origin/publish/issue80", "publish/issue80"], + check=True, + capture_output=True, + ), + mocker.call( + ["git", "push", "origin", "publish/issue80", "-f"], + check=True, + capture_output=True, + ), + ] # type: ignore + ) + + # 检查文件是否正确 + check_json_data( + plugin_config.input_config.bot_path, + [ + { + "name": "test", + "desc": "desc", + "author": "test", + "homepage": "https://nonebot.dev", + "tags": [{"label": "test", "color": "#ffffff"}], + "is_official": False, + } + ], + ) + + assert mocked_api["homepage"].called