Skip to content

Commit

Permalink
feat: 支持修改配置并重新测试 (#295)
Browse files Browse the repository at this point in the history
close #286
  • Loading branch information
he0119 authored Nov 27, 2024
1 parent c287da2 commit fed3f0f
Show file tree
Hide file tree
Showing 12 changed files with 917 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/
### Added

- 从插件测试中提取环境信息
- 支持修改插件配置并重新测试

### Fixed

Expand Down
1 change: 1 addition & 0 deletions src/plugins/github/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
SKIP_COMMENT = "/skip"

REMOVE_LABEL = "Remove"
CONFIG_LABEL = "Config"
158 changes: 158 additions & 0 deletions src/plugins/github/plugins/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
from githubkit.exception import RequestFailed
from nonebot import logger, on_type
from nonebot.adapters.github import GitHubBot
from nonebot.adapters.github.event import (
IssueCommentCreated,
IssuesEdited,
IssuesOpened,
IssuesReopened,
PullRequestReviewSubmitted,
)
from nonebot.params import Depends

from src.plugins.github.constants import CONFIG_LABEL, TITLE_MAX_LENGTH
from src.plugins.github.depends import (
RepoInfo,
bypass_git,
get_installation_id,
get_issue_handler,
get_repo_info,
get_type_by_labels_name,
install_pre_commit_hooks,
is_bot_triggered_workflow,
)
from src.plugins.github.models import IssueHandler
from src.plugins.github.plugins.publish.render import render_comment
from src.plugins.github.plugins.remove.depends import check_labels
from src.plugins.github.typing import IssuesEvent
from src.plugins.github.utils import run_shell_command
from src.providers.validation.models import PublishType

from .constants import BRANCH_NAME_PREFIX, COMMIT_MESSAGE_PREFIX, RESULTS_BRANCH
from .utils import (
update_file,
validate_info_from_issue,
)


async def check_rule(
event: IssuesEvent,
is_config: bool = check_labels(CONFIG_LABEL),
is_bot: bool = Depends(is_bot_triggered_workflow),
publish_type: PublishType = Depends(get_type_by_labels_name),
) -> bool:
if is_bot:
logger.info("机器人触发的工作流,已跳过")
return False
if publish_type != PublishType.PLUGIN:
logger.info("与插件无关,已跳过")
return False
if event.payload.issue.pull_request:
logger.info("评论在拉取请求下,已跳过")
return False
if is_config is False:
logger.info("非配置工作流,已跳过")
return False
return True


config_check_matcher = on_type(
(IssuesOpened, IssuesReopened, IssuesEdited, IssueCommentCreated), rule=check_rule
)


@config_check_matcher.handle(
parameterless=[Depends(bypass_git), Depends(install_pre_commit_hooks)]
)
async def handle_remove_check(
bot: GitHubBot,
installation_id: int = Depends(get_installation_id),
handler: IssueHandler = Depends(get_issue_handler),
):
async with bot.as_installation(installation_id):
if handler.issue.state != "open":
logger.info("议题未开启,已跳过")
await config_check_matcher.finish()

# 需要先切换到结果分支
run_shell_command(["git", "fetch", "origin", RESULTS_BRANCH])
run_shell_command(["git", "checkout", RESULTS_BRANCH])

# 检查是否满足发布要求
# 仅在通过检查的情况下创建拉取请求
result = await validate_info_from_issue(handler)

# 渲染评论信息
comment = await render_comment(result, True)

# 对议题评论
await handler.comment_issue(comment)

branch_name = f"{BRANCH_NAME_PREFIX}{handler.issue_number}"

# 设置拉取请求与议题的标题
# 限制标题长度,过长的标题不好看
title = f"{result.type}: {result.name[:TITLE_MAX_LENGTH]}"

if result.valid:
commit_message = f"{COMMIT_MESSAGE_PREFIX} {result.type.value.lower()} {result.name} (#{handler.issue_number})"

# 创建新分支
run_shell_command(["git", "switch", "-C", branch_name])
# 更新文件
update_file(result)
handler.commit_and_push(commit_message, branch_name, handler.author)
# 创建拉取请求
try:
await handler.create_pull_request(
RESULTS_BRANCH,
title,
branch_name,
[result.type.value, CONFIG_LABEL],
)
except RequestFailed:
await handler.update_pull_request_status(title, branch_name)
logger.info("该分支的拉取请求已创建,请前往查看")
else:
# 如果之前已经创建了拉取请求,则将其转换为草稿
await handler.draft_pull_request(branch_name)


async def review_submitted_rule(
event: PullRequestReviewSubmitted,
is_config: bool = check_labels(CONFIG_LABEL),
) -> bool:
if not is_config:
logger.info("拉取请求与配置无关,已跳过")
return False
if event.payload.review.author_association not in ["OWNER", "MEMBER"]:
logger.info("审查者不是仓库成员,已跳过")
return False
if event.payload.review.state != "approved":
logger.info("未通过审查,已跳过")
return False

return True


auto_merge_matcher = on_type(PullRequestReviewSubmitted, rule=review_submitted_rule)


@auto_merge_matcher.handle(
parameterless=[Depends(bypass_git), Depends(install_pre_commit_hooks)]
)
async def handle_auto_merge(
bot: GitHubBot,
event: PullRequestReviewSubmitted,
installation_id: int = Depends(get_installation_id),
repo_info: RepoInfo = Depends(get_repo_info),
) -> None:
async with bot.as_installation(installation_id):
# 如果有冲突的话,不会触发 Github Actions
# 所以直接合并即可
await bot.rest.pulls.async_merge(
**repo_info.model_dump(),
pull_number=event.payload.pull_request.number,
merge_method="rebase",
)
logger.info(f"已自动合并 #{event.payload.pull_request.number}")
4 changes: 4 additions & 0 deletions src/plugins/github/plugins/config/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
RESULTS_BRANCH = "results"

COMMIT_MESSAGE_PREFIX = "chore: edit config"
BRANCH_NAME_PREFIX = "config/issue"
134 changes: 134 additions & 0 deletions src/plugins/github/plugins/config/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from typing import Any

from nonebot import logger

from src.plugins.github.models import AuthorInfo
from src.plugins.github.models.issue import IssueHandler
from src.plugins.github.plugins.publish.constants import (
PLUGIN_CONFIG_PATTERN,
PLUGIN_MODULE_NAME_PATTERN,
PROJECT_LINK_PATTERN,
)
from src.plugins.github.plugins.publish.render import render_summary
from src.plugins.github.plugins.publish.validation import add_step_summary, strip_ansi
from src.plugins.github.utils import extract_issue_info_from_issue
from src.providers.constants import PYPI_KEY_TEMPLATE
from src.providers.docker_test import DockerPluginTest, Metadata
from src.providers.models import RegistryPlugin, StoreTestResult
from src.providers.utils import dump_json, load_json_from_file
from src.providers.validation import PublishType, ValidationDict, validate_info
from src.providers.validation.models import PluginPublishInfo


async def validate_info_from_issue(handler: IssueHandler) -> ValidationDict:
"""从议题中获取插件信息,并且运行插件测试加载且获取插件元信息后进行验证"""
body = handler.issue.body if handler.issue.body else ""

# 从议题里提取插件所需信息
raw_data: dict[str, Any] = extract_issue_info_from_issue(
{
"module_name": PLUGIN_MODULE_NAME_PATTERN,
"project_link": PROJECT_LINK_PATTERN,
"test_config": PLUGIN_CONFIG_PATTERN,
},
body,
)
# 从历史插件中获取标签
previous_plugins: dict[str, RegistryPlugin] = {
PYPI_KEY_TEMPLATE.format(
project_link=plugin["project_link"], module_name=plugin["module_name"]
): RegistryPlugin(**plugin)
for plugin in load_json_from_file("plugins.json")
}
raw_data["tags"] = previous_plugins[PYPI_KEY_TEMPLATE.format(**raw_data)].tags
# 更新作者信息
raw_data.update(AuthorInfo.from_issue(handler.issue).model_dump())

module_name: str = raw_data.get("module_name", None)
project_link: str = raw_data.get("project_link", None)
test_config: str = raw_data.get("test_config", "")

# 因为修改插件重新测试,所以上次的数据不需要加载,不然会报错重复
previous_data = []

# 修改插件配置肯定是为了通过插件测试,所以一定不跳过测试
raw_data["skip_test"] = False

# 运行插件测试
test = DockerPluginTest(project_link, module_name, test_config)
test_result = await test.run("3.12")

# 去除颜色字符
test_output = strip_ansi("\n".join(test_result.outputs))
metadata = test_result.metadata
if metadata:
# 从插件测试结果中获得元数据
raw_data.update(metadata)

# 更新插件测试结果
raw_data["version"] = test_result.version
raw_data["load"] = test_result.load
raw_data["test_output"] = test_output
raw_data["metadata"] = bool(metadata)

# 输出插件测试相关信息
add_step_summary(await render_summary(test_result, test_output, project_link))
logger.info(
f"插件 {project_link}({test_result.version}) 插件加载{'成功' if test_result.load else '失败'} {'插件已尝试加载' if test_result.run else '插件并未开始运行'}"
)
logger.info(f"插件元数据:{metadata}")
logger.info("插件测试输出:")
for output in test_result.outputs:
logger.info(output)

# 验证插件相关信息
result = validate_info(PublishType.PLUGIN, raw_data, previous_data)

if not result.valid_data.get("metadata"):
# 如果没有跳过测试且缺少插件元数据,则跳过元数据相关的错误
# 因为这个时候这些项都会报错,错误在此时没有意义
metadata_keys = Metadata.__annotations__.keys()
# 如果是重复报错,error["loc"] 是 ()
result.errors = [
error
for error in result.errors
if error["loc"] == () or error["loc"][0] not in metadata_keys
]
# 元数据缺失时,需要删除元数据相关的字段
for key in metadata_keys:
result.valid_data.pop(key, None)

return result


def update_file(result: ValidationDict) -> None:
"""更新文件"""
if not isinstance(result.info, PluginPublishInfo):
raise ValueError("仅支持修改插件配置")

logger.info("正在更新配置文件和最新测试结果")

# 读取文件
previous_plugins: dict[str, RegistryPlugin] = {
PYPI_KEY_TEMPLATE.format(
project_link=plugin["project_link"], module_name=plugin["module_name"]
): RegistryPlugin(**plugin)
for plugin in load_json_from_file("plugins.json")
}
previous_results: dict[str, StoreTestResult] = {
key: StoreTestResult(**value)
for key, value in load_json_from_file("results.json").items()
}
plugin_configs: dict[str, str] = load_json_from_file("plugin_configs.json")

# 更新信息
plugin = RegistryPlugin.from_publish_info(result.info)
previous_plugins[plugin.key] = plugin
previous_results[plugin.key] = StoreTestResult.from_info(result.info)
plugin_configs[plugin.key] = result.info.test_config

dump_json("plugins.json", list(previous_plugins.values()))
dump_json("results.json", previous_results)
dump_json("plugin_configs.json", plugin_configs, False)

logger.info("文件更新完成")
5 changes: 4 additions & 1 deletion src/plugins/github/plugins/publish/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from src.plugins.github.constants import (
BRANCH_NAME_PREFIX,
CONFIG_LABEL,
REMOVE_LABEL,
TITLE_MAX_LENGTH,
)
Expand Down Expand Up @@ -59,11 +60,13 @@ async def publish_related_rule(
"""确保与发布相关
通过标签判断
仅包含发布相关标签,不包含 remove 标签
仅包含发布相关标签,不包含 remove/config 标签
"""
for label in labels:
if label == REMOVE_LABEL:
return False
if label == CONFIG_LABEL:
return False
return True


Expand Down
4 changes: 2 additions & 2 deletions src/providers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pydantic_core import to_jsonable_python


def load_json_from_file(file_path: Path):
def load_json_from_file(file_path: str | Path):
"""从文件加载 JSON5 文件"""
with open(file_path, encoding="utf-8") as file:
return pyjson5.decode_io(file) # type: ignore
Expand Down Expand Up @@ -36,7 +36,7 @@ def dumps_json(data: Any, minify: bool = True) -> str:
return data


def dump_json(path: Path, data: Any, minify: bool = True) -> None:
def dump_json(path: str | Path, data: Any, minify: bool = True) -> None:
"""保存 JSON 文件"""
data = to_jsonable_python(data)

Expand Down
Loading

0 comments on commit fed3f0f

Please sign in to comment.