diff --git a/src/plugins/github/plugins/publish/utils.py b/src/plugins/github/plugins/publish/utils.py index 845b8154..d91ca325 100644 --- a/src/plugins/github/plugins/publish/utils.py +++ b/src/plugins/github/plugins/publish/utils.py @@ -4,6 +4,7 @@ from githubkit.exception import RequestFailed from nonebot import logger +from nonebot.adapters.github import ActionFailed from src.plugins.github import plugin_config from src.plugins.github.constants import ( @@ -132,7 +133,11 @@ async def resolve_conflict_pull_requests( logger.info("拉取请求为草稿,跳过处理") continue - issue_handler = await handler.to_issue_handler(issue_number) + try: + issue_handler = await handler.to_issue_handler(issue_number) + except (ActionFailed, RequestFailed): + logger.error(f"议题 #{issue_number} 不存在,跳过处理 {pull.title}") + continue try: artifact = await get_noneflow_artifact(issue_handler) diff --git a/src/plugins/github/plugins/remove/utils.py b/src/plugins/github/plugins/remove/utils.py index cb1d212d..21bf072b 100644 --- a/src/plugins/github/plugins/remove/utils.py +++ b/src/plugins/github/plugins/remove/utils.py @@ -1,5 +1,6 @@ from githubkit.exception import RequestFailed from nonebot import logger +from nonebot.adapters.github import ActionFailed from pydantic_core import PydanticCustomError from src.plugins.github import plugin_config @@ -97,7 +98,11 @@ async def resolve_conflict_pull_requests( # 根据标签获取发布类型 publish_type = get_type_by_labels(pull.labels) - issue_handler = await handler.to_issue_handler(issue_number) + try: + issue_handler = await handler.to_issue_handler(issue_number) + except (ActionFailed, RequestFailed): + logger.error(f"议题 #{issue_number} 不存在,跳过处理 {pull.title}") + continue if publish_type: # 验证作者信息 diff --git a/tests/plugins/github/resolve/test_resolve_pull_request.py b/tests/plugins/github/resolve/test_resolve_pull_request.py index 400c02fe..2ba5d044 100644 --- a/tests/plugins/github/resolve/test_resolve_pull_request.py +++ b/tests/plugins/github/resolve/test_resolve_pull_request.py @@ -3,6 +3,9 @@ from pathlib import Path from unittest.mock import MagicMock +import httpx +from githubkit import Response +from githubkit.exception import RequestFailed from inline_snapshot import snapshot from nonebot.adapters.github import PullRequestClosed from nonebug import App @@ -257,3 +260,222 @@ async def test_resolve_pull_request( ) assert not mocked_api["homepage"].called + + +async def test_resolve_pull_request_issue_not_found( + app: App, + mocker: MockerFixture, + mock_installation: MagicMock, + mock_installation_token: MagicMock, + mocked_api: MockRouter, + tmp_path: Path, +) -> None: + """测试当 PR 对应的议题不存在时,跳过该 PR 继续处理后续 PR""" + from src.plugins.github.plugins.resolve import pr_close_matcher + from src.providers.models import ( + REGISTRY_DATA_NAME, + BotPublishInfo, + Color, + RegistryArtifactData, + Tag, + ) + + mock_subprocess_run = mock_subprocess_run_with_side_effect(mocker) + + mock_issue = MockIssue( + body=generate_issue_body_remove(type="Bot"), number=76 + ).as_mock(mocker) + mock_issues_resp = mocker.MagicMock() + mock_issues_resp.parsed_data = mock_issue + + # 议题不存在的发布 PR + mock_missing_pull = mocker.MagicMock() + mock_missing_pull.title = "Bot: missing" + mock_missing_pull.draft = False + mock_missing_pull.head.ref = "publish/issue999" + mock_missing_pull.labels = get_pr_labels(["Publish", "Bot"]) + + # 正常存在的发布 PR + mock_publish_issue = MockIssue(body=generate_issue_body_bot(), number=100).as_mock( + mocker + ) + mock_publish_issue_resp = mocker.MagicMock() + mock_publish_issue_resp.parsed_data = mock_publish_issue + mock_publish_pull = mocker.MagicMock() + mock_publish_pull.title = "Bot: test" + mock_publish_pull.draft = False + mock_publish_pull.head.ref = "publish/issue100" + mock_publish_pull.labels = get_pr_labels(["Publish", "Bot"]) + + mock_publish_issue_comment = mocker.MagicMock() + mock_publish_issue_comment.body = """ +
+历史测试 +

+
  • ⚠️ 2025-03-28 02:21:18 CST
  • 2025-03-28 02:21:18 CST
  • 2025-03-28 02:22:18 CST
  • ⚠️ 2025-03-28 02:22:18 CST
  • +
    +
    + +""" + mock_publish_list_comments_resp = mocker.MagicMock() + mock_publish_list_comments_resp.parsed_data = [mock_publish_issue_comment] + + mock_publish_artifact = mocker.MagicMock() + mock_publish_artifact.name = "noneflow" + mock_publish_artifact.id = 123456789 + mock_publish_artifacts = mocker.MagicMock() + mock_publish_artifacts.artifacts = [mock_publish_artifact] + mock_publish_artifact_resp = mocker.MagicMock() + mock_publish_artifact_resp.parsed_data = mock_publish_artifacts + + raw_data = { + "module_name": "module_name", + "project_link": "project_link", + "time": "2025-03-28T02:21:18Z", + "version": "1.0.0", + "name": "name", + "desc": "desc", + "author": "he0119", + "author_id": 1, + "homepage": "https://nonebot.dev", + "tags": [Tag(label="test", color=Color("#ffffff"))], + "is_official": False, + } + info = BotPublishInfo.model_construct(**raw_data) + registry_data = RegistryArtifactData.from_info(info) + + zip_buffer = BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: + json_content = registry_data.model_dump_json(indent=2) + zip_file.writestr(REGISTRY_DATA_NAME, json_content) + + publish_zip_content = zip_buffer.getvalue() + + mock_publish_download_artifact_resp = mocker.MagicMock() + mock_publish_download_artifact_resp.content = publish_zip_content + + mock_pulls_resp = mocker.MagicMock() + mock_pulls_resp.parsed_data = [mock_missing_pull, mock_publish_pull] + + async with app.test_matcher() as ctx: + _adapter, bot = get_github_bot(ctx) + + event = get_mock_event(PullRequestClosed) + event.payload.pull_request.labels = get_pr_labels(["Remove", "Bot"]) + event.payload.pull_request.merged = True + + ctx.should_call_api( + "rest.apps.async_get_repo_installation", + snapshot({"owner": "he0119", "repo": "action-test"}), + mock_installation, + ) + ctx.should_call_api( + "rest.apps.async_create_installation_access_token", + snapshot({"installation_id": mock_installation.parsed_data.id}), + mock_installation_token, + ) + ctx.should_call_api( + "rest.issues.async_get", + snapshot({"owner": "he0119", "repo": "action-test", "issue_number": 76}), + mock_issues_resp, + ) + ctx.should_call_api( + "rest.issues.async_update", + snapshot( + { + "owner": "he0119", + "repo": "action-test", + "issue_number": 76, + "state": "closed", + "state_reason": "completed", + } + ), + True, + ) + ctx.should_call_api( + "rest.pulls.async_list", + snapshot({"owner": "he0119", "repo": "action-test", "state": "open"}), + mock_pulls_resp, + ) + # 议题不存在,应该返回 404 + ctx.should_call_api( + "rest.issues.async_get", + snapshot({"owner": "he0119", "repo": "action-test", "issue_number": 999}), + exception=RequestFailed( + Response( + httpx.Response(404, request=httpx.Request("GET", "test")), + None, # type: ignore + ) + ), + ) + # 跳过不存在的议题,继续处理下一个 PR + ctx.should_call_api( + "rest.issues.async_get", + snapshot({"owner": "he0119", "repo": "action-test", "issue_number": 100}), + mock_publish_issue_resp, + ) + ctx.should_call_api( + "rest.issues.async_list_comments", + snapshot({"owner": "he0119", "repo": "action-test", "issue_number": 100}), + mock_publish_list_comments_resp, + ) + ctx.should_call_api( + "rest.actions.async_list_workflow_run_artifacts", + snapshot( + { + "owner": "he0119", + "repo": "action-test", + "run_id": 14156878699, + } + ), + mock_publish_artifact_resp, + ) + ctx.should_call_api( + "rest.actions.async_download_artifact", + snapshot( + { + "owner": "he0119", + "repo": "action-test", + "artifact_id": 123456789, + "archive_format": "zip", + } + ), + mock_publish_download_artifact_resp, + ) + ctx.receive_event(bot, event) + ctx.should_pass_rule(pr_close_matcher) + + # 测试 git 命令 - 应该只处理了 issue100 的 PR,跳过了 issue999 + assert_subprocess_run_calls( + mock_subprocess_run, + [ + ["git", "config", "--global", "safe.directory", "*"], + [ + "git", + "config", + "--global", + "url.https://x-access-token:test-token@github.com/.insteadOf", + "https://github.com/", + ], + ["git", "push", "origin", "--delete", "publish/issue76"], + # 处理发布(issue999 被跳过,只处理 issue100) + ["git", "checkout", "master"], + ["git", "pull"], + ["git", "switch", "-C", "publish/issue100"], + ["git", "add", str(tmp_path / "bots.json5")], + ["git", "config", "--global", "user.name", "test"], + [ + "git", + "config", + "--global", + "user.email", + "test@users.noreply.github.com", + ], + ["git", "commit", "-m", ":beers: publish bot name (#100)"], + ["git", "fetch", "origin"], + ["git", "diff", "origin/publish/issue100", "publish/issue100"], + ["git", "push", "origin", "publish/issue100", "-f"], + ], + ) + + assert not mocked_api["homepage"].called