fix(ci): 修复 MegaLinter 工作区歧义

- 修复 MegaLinter 的 dotnet format workspace 指向,避免 solution 与 csproj 歧义导致 CI warning
- 更新 gframework-pr-review skill 与抓取脚本,提取 GitHub Actions 发布的 MegaLinter detailed issues
- 补充 coroutine optimization 跟踪与 trace,记录本次 PR 页面 warning 的收口与验证结果
This commit is contained in:
GeWuYou 2026-04-20 11:20:14 +08:00
parent d369118351
commit 90b9e2a4c9
5 changed files with 117 additions and 6 deletions

View File

@ -1,6 +1,6 @@
--- ---
name: gframework-pr-review name: gframework-pr-review
description: Repository-specific GitHub PR review workflow for the GFramework repo. Use when Codex needs to inspect the GitHub pull request for the current branch, extract CodeRabbit summary/comments, read failed checks or failed test signals from the PR page, and then verify which findings should be fixed in the local codebase. Trigger explicitly with $gframework-pr-review or with prompts such as "look at the current PR", "extract CodeRabbit comments", or "check Failed Tests on the PR". description: Repository-specific GitHub PR review workflow for the GFramework repo. Use when Codex needs to inspect the GitHub pull request for the current branch, extract CodeRabbit summary/comments, read failed checks, MegaLinter warnings, or failed test signals from the PR page, and then verify which findings should be fixed in the local codebase. Trigger explicitly with $gframework-pr-review or with prompts such as "look at the current PR", "extract CodeRabbit comments", or "check Failed Tests on the PR".
--- ---
# GFramework PR Review # GFramework PR Review
@ -16,12 +16,12 @@ Shortcut: `$gframework-pr-review`
3. Run `scripts/fetch_current_pr_review.py` to: 3. Run `scripts/fetch_current_pr_review.py` to:
- locate the PR for the current branch through the GitHub PR API - locate the PR for the current branch through the GitHub PR API
- fetch PR metadata, issue comments, reviews, and review comments through the GitHub API - fetch PR metadata, issue comments, reviews, and review comments through the GitHub API
- extract `Summary by CodeRabbit` and CTRF test reports from issue comments - extract `Summary by CodeRabbit`、GitHub Actions bot comments such as `MegaLinter analysis: Success with warnings`and CTRF test reports from issue comments
- fetch the latest head commit review threads from the GitHub PR API - fetch the latest head commit review threads from the GitHub PR API
- prefer unresolved review threads on the latest head commit over older summary-only signals - prefer unresolved review threads on the latest head commit over older summary-only signals
- extract failed checks and test-report signals such as `Failed Tests` or `No failed tests in this run` - extract failed checks, MegaLinter detailed issues, and test-report signals such as `Failed Tests` or `No failed tests in this run`
4. Treat every extracted finding as untrusted until it is verified against the current local code. 4. Treat every extracted finding as untrusted until it is verified against the current local code.
5. Only fix comments that still apply to the checked-out branch. Ignore stale or already-resolved findings. 5. Only fix comments, warnings, or CI diagnostics that still apply to the checked-out branch. Ignore stale or already-resolved findings.
6. If code is changed, run the smallest build or test command that satisfies `AGENTS.md`. 6. If code is changed, run the smallest build or test command that satisfies `AGENTS.md`.
## Commands ## Commands
@ -43,6 +43,7 @@ The script should produce:
- Latest head commit review metadata and review threads - Latest head commit review metadata and review threads
- Unresolved latest-commit review threads after reply-thread folding - Unresolved latest-commit review threads after reply-thread folding
- Pre-merge failed checks, if present - Pre-merge failed checks, if present
- Latest MegaLinter status and any detailed issues posted by `github-actions[bot]`
- Test summary, including failed-test signals when present - Test summary, including failed-test signals when present
- Parse warnings only when both the primary API source and the intended fallback signal are unavailable - Parse warnings only when both the primary API source and the intended fallback signal are unavailable
@ -52,6 +53,7 @@ The script should produce:
- If GitHub access fails because of proxy configuration, rerun the fetch with proxy variables removed. - If GitHub access fails because of proxy configuration, rerun the fetch with proxy variables removed.
- Prefer GitHub API results over PR HTML. The PR HTML page is now a fallback/debugging source, not the primary source of truth. - Prefer GitHub API results over PR HTML. The PR HTML page is now a fallback/debugging source, not the primary source of truth.
- If the summary block and the latest head review threads disagree, trust the latest unresolved head-review threads and treat older summary findings as stale until re-verified locally. - If the summary block and the latest head review threads disagree, trust the latest unresolved head-review threads and treat older summary findings as stale until re-verified locally.
- Treat GitHub Actions comments with `Success with warnings` as actionable review input when they include concrete linter diagnostics such as `MegaLinter` detailed issues; do not skip them just because the parent check is green.
## Example Triggers ## Example Triggers

View File

@ -24,6 +24,7 @@ DEFAULT_WINDOWS_GIT = "/mnt/d/Tool/Development Tools/Git/cmd/git.exe"
GIT_ENVIRONMENT_KEY = "GFRAMEWORK_WINDOWS_GIT" GIT_ENVIRONMENT_KEY = "GFRAMEWORK_WINDOWS_GIT"
USER_AGENT = "codex-gframework-pr-review" USER_AGENT = "codex-gframework-pr-review"
CODERABBIT_LOGIN = "coderabbitai[bot]" CODERABBIT_LOGIN = "coderabbitai[bot]"
GITHUB_ACTIONS_LOGIN = "github-actions[bot]"
REVIEW_COMMENT_ADDRESSED_MARKER = "<!-- <review_comment_addressed> -->" REVIEW_COMMENT_ADDRESSED_MARKER = "<!-- <review_comment_addressed> -->"
VISIBLE_ADDRESSED_IN_COMMIT_PATTERN = re.compile(r"\s*Addressed in commit\s+[0-9a-f]{7,40}", re.I) VISIBLE_ADDRESSED_IN_COMMIT_PATTERN = re.compile(r"\s*Addressed in commit\s+[0-9a-f]{7,40}", re.I)
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60 DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
@ -156,6 +157,10 @@ def strip_tags(text: str) -> str:
return collapse_whitespace(re.sub(r"<[^>]+>", " ", text)) return collapse_whitespace(re.sub(r"<[^>]+>", " ", text))
def strip_markdown_links(text: str) -> str:
return re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str | None: def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str | None:
start = text.find(start_marker) start = text.find(start_marker)
if start < 0: if start < 0:
@ -252,6 +257,60 @@ def parse_actionable_comments(actionable_block: str) -> dict[str, Any]:
} }
def parse_megalinter_comment(comment_body: str) -> dict[str, Any]:
normalized_body = html.unescape(comment_body).strip()
summary_match = re.search(
r"##\s*(?P<badges>.*?)\[MegaLinter\]\([^)]+\)\s+analysis:\s+\[(?P<status>[^\]]+)\]\((?P<run_url>[^)]+)\)",
normalized_body,
)
report: dict[str, Any] = {
"status": summary_match.group("status").strip() if summary_match else "",
"run_url": summary_match.group("run_url").strip() if summary_match else "",
"badges": collapse_whitespace(summary_match.group("badges")) if summary_match else "",
"descriptor_rows": [],
"detailed_issues": [],
"raw": normalized_body,
}
table_match = re.search(
r"\| Descriptor .*?\|\n\|[-| :]+\|\n(?P<rows>(?:\|.*\|\n?)+)",
normalized_body,
re.S,
)
if table_match is not None:
for raw_line in table_match.group("rows").splitlines():
line = raw_line.strip()
if not line.startswith("|"):
continue
parts = [collapse_whitespace(strip_markdown_links(part)) for part in line.strip("|").split("|")]
if len(parts) != 7:
continue
report["descriptor_rows"].append(
{
"descriptor": parts[0],
"linter": parts[1],
"files": parts[2],
"fixed": parts[3],
"errors": parts[4],
"warnings": parts[5],
"elapsed_time": parts[6],
}
)
for summary, details in re.findall(r"<summary>(.*?)</summary>\s*```(.*?)```", normalized_body, re.S):
report["detailed_issues"].append(
{
"summary": collapse_whitespace(strip_tags(summary)),
"details": details.strip(),
}
)
return report
def parse_test_report(block: str) -> dict[str, Any]: def parse_test_report(block: str) -> dict[str, Any]:
report: dict[str, Any] = { report: dict[str, Any] = {
"raw": block.strip(), "raw": block.strip(),
@ -475,11 +534,18 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
issue_comments, issue_comments,
lambda body: "CTRF PR COMMENT TAG:" in body or "### Test Results" in body, lambda body: "CTRF PR COMMENT TAG:" in body or "### Test Results" in body,
) )
megalinter_block = select_latest_comment_body(
issue_comments,
lambda body: "MegaLinter" in body and "Detailed Issues" in body,
required_user=GITHUB_ACTIONS_LOGIN,
)
if not summary_block: if not summary_block:
warnings.append("CodeRabbit summary block was not found in issue comments.") warnings.append("CodeRabbit summary block was not found in issue comments.")
if not test_blocks: if not test_blocks:
warnings.append("PR test-report block was not found in issue comments.") warnings.append("PR test-report block was not found in issue comments.")
if not megalinter_block:
warnings.append("MegaLinter report block was not found in issue comments.")
latest_commit_review: dict[str, Any] = {} latest_commit_review: dict[str, Any] = {}
try: try:
@ -506,6 +572,7 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
}, },
"coderabbit_comments": parse_actionable_comments(actionable_block) if actionable_block else {}, "coderabbit_comments": parse_actionable_comments(actionable_block) if actionable_block else {},
"latest_commit_review": latest_commit_review, "latest_commit_review": latest_commit_review,
"megalinter_report": parse_megalinter_comment(megalinter_block) if megalinter_block else {},
"test_reports": [parse_test_report(block) for block in test_blocks], "test_reports": [parse_test_report(block) for block in test_blocks],
"parse_warnings": warnings, "parse_warnings": warnings,
} }
@ -569,6 +636,31 @@ def format_text(result: dict[str, Any]) -> str:
" Note: thread is still open; treat the visible 'Addressed in commit ...' text as unverified until local code matches." " Note: thread is still open; treat the visible 'Addressed in commit ...' text as unverified until local code matches."
) )
megalinter_report = result.get("megalinter_report", {})
if megalinter_report:
lines.append("")
lines.append(
"MegaLinter: "
f"{megalinter_report.get('status', 'unknown')}"
+ (
f" ({megalinter_report.get('run_url', '')})"
if megalinter_report.get("run_url")
else ""
)
)
descriptor_rows = megalinter_report.get("descriptor_rows", [])
for descriptor_row in descriptor_rows:
lines.append(
"- "
f"{descriptor_row['descriptor']} / {descriptor_row['linter']}: "
f"errors={descriptor_row['errors']} warnings={descriptor_row['warnings']} files={descriptor_row['files']}"
)
for issue in megalinter_report.get("detailed_issues", []):
lines.append(f"- Detailed issue: {issue['summary']}")
lines.append(f" {collapse_whitespace(issue['details'])}")
lines.append("") lines.append("")
lines.append(f"Test reports: {len(result['test_reports'])}") lines.append(f"Test reports: {len(result['test_reports'])}")
for index, report in enumerate(result["test_reports"], start=1): for index, report in enumerate(result["test_reports"], start=1):

View File

@ -45,6 +45,9 @@ ENABLE_LINTERS:
# 设置 C# 代码风格检查的参数和验证级别 # 设置 C# 代码风格检查的参数和验证级别
# ======================== # ========================
CSHARP_DOTNET_FORMAT_ARGUMENTS: CSHARP_DOTNET_FORMAT_ARGUMENTS:
# 仓库根目录同时存在 GFramework.sln 与 GFramework.csproj
# 显式指定 workspace避免 dotnet format 在 CI 中因自动探测歧义直接异常退出。
- "GFramework.sln"
- "--severity" - "--severity"
- "info" - "info"
- "--verify-no-changes" - "--verify-no-changes"
@ -83,4 +86,3 @@ GITHUB_COMMENT_REPORTER: true
PARALLEL: true PARALLEL: true
SHOW_ELAPSED_TIME: true SHOW_ELAPSED_TIME: true
VALIDATE_ALL_CODEBASE: false VALIDATE_ALL_CODEBASE: false

View File

@ -13,6 +13,7 @@
- 已为 `Timing` 补齐纯托管测试宿主入口,允许在 `dotnet test` 下验证 Godot 协程宿主阶段语义,而不依赖原生 `Node` 构造 - 已为 `Timing` 补齐纯托管测试宿主入口,允许在 `dotnet test` 下验证 Godot 协程宿主阶段语义,而不依赖原生 `Node` 构造
- 已补充 `GFramework.Godot.Tests/Coroutine/TimingTests.cs`锁定暂停、segment 路由和阶段型等待指令的回归覆盖 - 已补充 `GFramework.Godot.Tests/Coroutine/TimingTests.cs`锁定暂停、segment 路由和阶段型等待指令的回归覆盖
- 已根据 PR #259 的最新 CodeRabbit review 收口测试宿主清理对称性,并将 `TimingTests` 固定为非并行执行 - 已根据 PR #259 的最新 CodeRabbit review 收口测试宿主清理对称性,并将 `TimingTests` 固定为非并行执行
- 已根据 PR #259`MegaLinter analysis: Success with warnings` 结果修复 `dotnet format` workspace 歧义,并增强 PR review skill 以提取此类 CI warning
- 下一轮优先补“仍需真实场景树参与”的归属协程 / 退树语义或转入文档迁移收口不再回到“Godot 宿主没有自动化回归”的旧状态 - 下一轮优先补“仍需真实场景树参与”的归属协程 / 退树语义或转入文档迁移收口不再回到“Godot 宿主没有自动化回归”的旧状态
## 当前状态摘要 ## 当前状态摘要
@ -36,6 +37,9 @@
- 针对 PR #259 的最新未解决 review 线程,已补充两项收口: - 针对 PR #259 的最新未解决 review 线程,已补充两项收口:
- `TimingTests` 已添加 `[NonParallelizable]`,避免共享静态实例槽位在 NUnit 并行执行时互相污染 - `TimingTests` 已添加 `[NonParallelizable]`,避免共享静态实例槽位在 NUnit 并行执行时互相污染
- `Timing` 的测试清理与运行时退树清理现仅在当前实例持有共享 `_instance` 引用时才会清空单例状态 - `Timing` 的测试清理与运行时退树清理现仅在当前实例持有共享 `_instance` 引用时才会清空单例状态
- 针对 PR #259`MegaLinter` warning已补充两项收口
- `.mega-linter.yml` 现为 `CSHARP_DOTNET_FORMAT_ARGUMENTS` 显式指定 `GFramework.sln`,避免仓库根目录同时存在 `*.sln``*.csproj` 时触发 workspace 歧义
- `.codex/skills/gframework-pr-review/` 现会抓取并解析 `github-actions[bot]` 发布的 `MegaLinter analysis: Success with warnings` comment默认把其中的 detailed issues 视为待验证输入
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --no-restore` 当前通过,合计 `58` 个测试 - `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --no-restore` 当前通过,合计 `58` 个测试
## 当前风险 ## 当前风险
@ -65,9 +69,12 @@
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~TimingTests" --no-restore` - `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~TimingTests" --no-restore`
- 结果:通过 - 结果:通过
- 备注:针对 PR #259 review 修复后的 `TimingTests``5` 个测试全部通过 - 备注:针对 PR #259 review 修复后的 `TimingTests``5` 个测试全部通过
- `dotnet build GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --no-restore`
- 结果:通过
- 备注CI/MegaLinter 配置与 PR review skill 更新后,目标测试项目仍保持 `0 warning / 0 error`
## 下一步 ## 下一步
1. 若继续补验证,优先只做真实场景树相关的节点归属 / 退树 / `queue_free` 回归,不再重新设计 `Timing` 纯托管宿主 1. 若继续补验证,优先只做真实场景树相关的节点归属 / 退树 / `queue_free` 回归,不再重新设计 `Timing` 纯托管宿主
2. 当前 PR 合并前可直接回到 GitHub 处理这两条 review 线程的回复与 resolve,避免后续重复审阅同一问题 2. 当前 PR 合并前可直接回到 GitHub 确认最新 push 是否已消除 `MegaLinter analysis` warning并顺手处理 review 线程的回复与 resolve
3. 若转入文档收口,优先清理其余 `StartCoroutine()/StopCoroutine()` 残留,并补 Godot 新入口与阶段等待的迁移对照 3. 若转入文档收口,优先清理其余 `StartCoroutine()/StopCoroutine()` 残留,并补 Godot 新入口与阶段等待的迁移对照

View File

@ -55,6 +55,14 @@
- 将 `Timing``_instance` 清理改为“仅当当前实例仍持有共享单例引用时才执行”,同时覆盖运行时 `_ExitTree()` 与测试入口 `DisposeForTests()` - 将 `Timing``_instance` 清理改为“仅当当前实例仍持有共享单例引用时才执行”,同时覆盖运行时 `_ExitTree()` 与测试入口 `DisposeForTests()`
- 额外完成验证: - 额外完成验证:
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~TimingTests" --no-restore` - `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~TimingTests" --no-restore`
- 同日继续收口 PR #259 页面上的 `MegaLinter analysis: Success with warnings`
- 确认 detailed issue 实际不是格式差异,而是 `dotnet format` 在仓库根目录同时发现 `GFramework.sln``GFramework.csproj` 后因未指定 workspace 直接抛异常
- 更新 `.mega-linter.yml`,为 `CSHARP_DOTNET_FORMAT_ARGUMENTS` 显式指定 `GFramework.sln`
- 更新 `.codex/skills/gframework-pr-review/SKILL.md``scripts/fetch_current_pr_review.py`,使 skill 默认抓取并输出 `github-actions[bot]` 的 MegaLinter comments 和 detailed issues
- 额外完成验证:
- `python3 -c "from pathlib import Path; compile(Path('.codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py').read_text(encoding='utf-8'), '.codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py', 'exec'); print('syntax-ok')"`
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --branch feat/coroutine-optimization --format json`
- `dotnet build GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --no-restore`
### 下一步 ### 下一步