diff --git a/examples/workflows/gitlink-flow/docs/docs/architecture.md b/examples/workflows/gitlink-flow/docs/docs/architecture.md deleted file mode 100644 index f7187f5..0000000 --- a/examples/workflows/gitlink-flow/docs/docs/architecture.md +++ /dev/null @@ -1,75 +0,0 @@ -# 架构说明 - -gitlink-flow 采用「采集 → 步骤分析 → 汇总生成」的分层管线,编排器只负责串联, -每个步骤是独立可测的纯函数。 - -## 数据流 - -``` -GitLink 平台(公开 API,只读) - │ - ▼ -┌─────────────┐ -│ glapi.py │ 采集:repo_info / issues / pulls / commits / -│ (采集层) │ contributors / releases / 文件树 -└─────────────┘ - │ 原始数据 - ▼ -┌─────────────┐ -│ steps.py │ 6 个步骤(纯函数,可独立测试): -│ (能力层) │ ① triage_issues Issue 自动分拣 -│ │ ② pr_review_summary PR Review 汇总 -│ │ ③ release_notes Release Notes 生成 -│ │ ④ health_check 社区健康体检(复用 scaffold) -│ │ ⑤ contributor_highlights 贡献者致谢(复用 contributor) -└─────────────┘ - │ 各步骤结构化结果 - ▼ -┌─────────────┐ -│ flow.py │ 编排器:run_flow() 按顺序调用 6 步, -│ (编排层) │ 组装为统一结果字典 -└─────────────┘ - │ - ▼ -┌─────────────┐ -│ report.py │ ⑥ render_weekly() 把 6 步结果汇总为 -│ (生成层) │ 一份社区运营周报(Markdown) -└─────────────┘ - │ - ▼ - outputs/__flow.md 或 JSON -``` - -## 分层职责 - -| 层 | 文件 | 职责 | -|----|------|------| -| 采集层 | `glapi.py` | 调用 GitLink 公开 API,带缓存,只读 | -| 能力层 | `steps.py` | 各步骤分析逻辑,纯函数,不触网 | -| 编排层 | `flow.py` | 串联步骤、命令行参数、批量处理 | -| 生成层 | `report.py` | 汇总为社区运营周报 | - -## 为什么这样设计 - -**编排与能力分离**:`flow.py` 只管"按什么顺序调用哪些步骤",`steps.py` 只管"每一步算什么"。 -新增/调整步骤只需改对应层,互不影响。 - -**纯函数步骤**:每个步骤接收数据、返回结果,不触网、无副作用,因此能用合成数据完整单元测试 -(16 个测试,不依赖网络)。 - -**对标官方参考**:①②③ 三个子工作流对齐官方 `examples/workflows` 的三个参考场景 -(Issue 分拣 / PR Review / Release Notes),降低理解与收录成本。 - -## 与三个官方参考工作流的对应 - -| 官方参考场景 | 本作品对应步骤 | 实现 | -|--------------|----------------|------| -| Issue 自动分拣 | ① triage_issues | 按关键词/标签分类 bug/feature/question/新手友好 | -| PR Review | ② pr_review_summary | 统计 PR 状态、识别待 Review、标注 fork 来源 | -| Release Notes 生成 | ③ release_notes | 按 conventional commits 归类生成 Markdown | - -## 可扩展点 - -- **新增步骤**:在 `steps.py` 加一个纯函数,在 `flow.py` 的 `run_flow` 接入,在 `report.py` 增加对应章节。 -- **接入写操作**:当前全程只读;如需自动发布周报到 Issue,可在编排末尾增加一步调用 `gitlink-cli issue +comment`(写操作,需用户确认)。 -- **更多数据源**:`glapi.py` 的客户端接口可替换为其他平台实现。 diff --git a/examples/workflows/gitlink-flow/docs/docs/quickstart.md b/examples/workflows/gitlink-flow/docs/docs/quickstart.md deleted file mode 100644 index 7e512a9..0000000 --- a/examples/workflows/gitlink-flow/docs/docs/quickstart.md +++ /dev/null @@ -1,68 +0,0 @@ -# 快速开始 - -## 1. 环境 - -Python 3.10+。工作流的采集与编排仅用 Python 标准库,**无需安装依赖、无需登录**即可分析公开仓库。 - -(仅运行单元测试需要 pytest:`pip install pytest`) - -## 2. 一键运行 - -```powershell -# 默认分析 Gitlink/gitlink-cli,生成社区运营周报 -.\scripts\run_demo.ps1 -``` - -产物输出到 `outputs/Gitlink_gitlink-cli_flow.md`。 - -## 3. 分析指定仓库 - -```powershell -.\scripts\run_demo.ps1 -Owner -Repo -``` - -或直接用 Python: - -```bash -python src/flow.py --owner --repo -``` - -## 4. 其他用法 - -```bash -# JSON 输出(供 Agent 或脚本消费) -python src/flow.py --owner Gitlink --repo gitlink-cli --format json - -# 用 owner/repo 形式或完整 URL -python src/flow.py --slug Gitlink/gitlink-cli - -# 批量分析多个仓库 -python src/flow.py --config examples/config.json --output-dir outputs - -# 控制提交采集量(每页 50 条,默认 4 页) -python src/flow.py --owner Gitlink --repo gitlink-cli --commit-pages 2 -``` - -## 5. 周报包含什么 - -生成的社区运营周报有 6 个部分: - -1. 仓库概览(Star/Fork/Issue/PR) -2. Issue 自动分拣(分类 + 新手友好任务建议) -3. PR Review 汇总(状态统计 + 待 Review 清单) -4. 社区健康体检(健康度评分 + 缺失文件) -5. 贡献者致谢(致谢榜) -6. Release Notes(按 conventional commits 自动归类) - -## 运行测试 - -```bash -python -m pytest tests/ -q -``` - -## 常见问题 - -**首次运行慢?** 大仓库提交多,采集需要时间。可用 `--commit-pages 2` 减少采集量加速。 - -**想发布周报到仓库?** 工作流默认只生成本地文件。如需发布,可把周报内容通过 -`gitlink-cli issue +comment` 发到指定 Issue(写操作,请先确认)。 diff --git a/examples/workflows/gitlink-flow/docs/docs/verification.md b/examples/workflows/gitlink-flow/docs/docs/verification.md deleted file mode 100644 index d803982..0000000 --- a/examples/workflows/gitlink-flow/docs/docs/verification.md +++ /dev/null @@ -1,59 +0,0 @@ -# 验证记录 - -## 环境 - -- 操作系统:Windows,PowerShell -- Python:3.12 -- 数据源:GitLink 公开 API(无需 token) - -## 真实仓库验证 - -在真实活跃仓库 `Gitlink/gitlink-cli` 上运行完整工作流: - -```bash -python src/flow.py --owner Gitlink --repo gitlink-cli -``` - -### 六个步骤的真实产出 - -| 步骤 | 结果 | -|------|------| -| ① Issue 自动分拣 | 17 个 Issue → bug 1 / feature 1 / 新手友好 5 / other 10 | -| ② PR Review 汇总 | 20 个开放 PR,列出待 Review 清单(含真实 fork PR) | -| ③ Release Notes | 从 127 条提交按 conventional commits 归类,覆盖 feat/fix/docs/test/ci 等 | -| ④ 社区健康体检 | 50/100(README、LICENSE 齐全,缺 CONTRIBUTING、行为准则) | -| ⑤ 贡献者致谢 | 20 位贡献者,致谢榜前列 wbtiger / wangyue789 / wauxing | -| ⑥ 社区运营周报 | 汇总以上为一份完整 Markdown 周报 | - -完整周报见 [`../examples/demo_outputs/Gitlink_gitlink-cli_flow.md`](../examples/demo_outputs/Gitlink_gitlink-cli_flow.md)。 - -> 值得一提:工作流在 PR Review 步骤真实抓取到了本人为子赛题二提交的 5 个 Skill PR -> (gitlink-newcomer / scaffold / deps / contributor / kb),以及其他社区成员的 PR, -> 印证了工作流读取的是真实、实时的仓库数据。 - -## 单元测试 - -```bash -python -m pytest tests/ -q -# 16 passed -``` - -覆盖三个子工作流(triage / pr-review / release-notes)、复用 Skill 步骤(health / contributors) -与编排器 run_flow,全部使用合成数据 + FakeClient,不触网。 - -## Agent 平台验证 - -本工作流可由 AI Agent(如 Kiro CLI)按以下方式调用: - -> 用户:「帮我给 Gitlink/gitlink-cli 生成一份社区运营周报」 -> Agent:识别意图 → 执行 `python src/flow.py --owner Gitlink --repo gitlink-cli` → 解读周报 - -工作流输出支持 `--format json`,便于 Agent 解析后嵌入更大的自动化链路。 - -## 可复现性 - -```powershell -.\scripts\run_demo.ps1 -``` - -所有数据来自 GitLink 平台公开接口实时采集,未做任何人工修改;分析全程只读,不向远程写入。 diff --git a/examples/workflows/gitlink-flow/examples/demo_outputs/Gitlink_gitlink-cli_flow.md b/examples/workflows/gitlink-flow/examples/demo_outputs/Gitlink_gitlink-cli_flow.md index cd2a29e..aa4dff1 100644 --- a/examples/workflows/gitlink-flow/examples/demo_outputs/Gitlink_gitlink-cli_flow.md +++ b/examples/workflows/gitlink-flow/examples/demo_outputs/Gitlink_gitlink-cli_flow.md @@ -1,18 +1,19 @@ # 社区运营周报 — Gitlink/gitlink-cli -生成时间:2026-06-06 14:40 | 工具:gitlink-flow 端到端工作流 +生成时间:2026-06-12 18:12 | 工具:gitlink-flow 端到端工作流 > 本周报由 gitlink-flow 自动串联 Issue 分拣、PR Review、Release Notes、社区健康体检、贡献者致谢等步骤生成,覆盖社区运营全链路。 ## 一、仓库概览 -- Star 5 / Fork 25 / Issue 17 / PR 129 +- Star 5 / Fork 29 / Issue 18 / PR 232 +- 数据采集:gitlink-cli 命令 ## 二、Issue 自动分拣 -共 17 个 Issue,自动分类: +共 18 个 Issue,自动分类: -- bug:1 个 +- bug:2 个 - feature:1 个 - good-first:5 个 - other:10 个 @@ -27,20 +28,20 @@ ## 三、PR Review 汇总 -共 20 个 PR:开放 20 / 已合并 0 / 已关闭 0,合并率 0.0%。 +共 20 个 PR:开放 19 / 已合并 0 / 已关闭 1,合并率 0.0%。 待 Review 的 PR: -- #129 feat(skills): 新增 gitlink-kb 知识库问答 Skill — @Ct201314 (来自 Fork) -- #128 feat(skills): 新增 gitlink-contributor 贡献者致谢与成长 Skill — @Ct201314 (来自 Fork) -- #127 feat(skills): 新增 gitlink-deps 依赖追踪 Skill — @Ct201314 (来自 Fork) -- #126 feat(skills): 新增 gitlink-scaffold 社区健康文件体检 Skill — @Ct201314 (来自 Fork) -- #125 feat(skills): 新增 gitlink-newcomer 新人引导 Skill — @Ct201314 (来自 Fork) -- #124 新增 Raw API 批处理执行器 — @Mengz (来自 Fork) -- #123 新增 Release 资产下载命令 — @Mengz (来自 Fork) -- #122 完善仓库 README 快捷命令 — @Mengz (来自 Fork) -- #121 feat(shortcut): add shortcuts/license — @co63oc (来自 Fork) -- #120 fix(health): use effective list filters — @wangyue111 (来自 Fork) +- #232 feat(skills): 新增 gitlink-metrics 仓库量化指标看板 Skill — @Ct201314 (来自 Fork) +- #231 feat(skills): 新增 gitlink-onboard 新贡献者上手指南 Skill — @Ct201314 (来自 Fork) +- #230 feat(skills): 新增 gitlink-stale 陈旧 Issue/PR 清理 Skill — @Ct201314 (来自 Fork) +- #229 feat(skills): 新增 gitlink-standup 个人/团队日报周报 Skill — @Ct201314 (来自 Fork) +- #228 feat(skills): 新增 gitlink-changelog 版本变更对比 Skill — @Ct201314 (来自 Fork) +- #227 feat(message): add inbox management shortcuts — @wangyue111 (来自 Fork) +- #226 feat(issue): 增加批量评论、批量更新与文件正文输入能力 — @Mengz (来自 Fork) +- #225 feat(repo): add scaffold creation options — @wangyue111 (来自 Fork) +- #224 feat(project-template): add issue template shortcuts — @wangyue111 (来自 Fork) +- #223 feat(public-key): add SSH key shortcuts — @wangyue111 (来自 Fork) ## 四、社区健康体检 @@ -52,21 +53,31 @@ 共 20 位贡献者,本周致谢榜前列: -- 🥇 wbtiger(112 次贡献) -- 🥈 wangyue789(44 次贡献) -- 🥉 wauxing(31 次贡献) -- 4. Mengz(26 次贡献) -- 5. whale(24 次贡献) +- 🥇 wbtiger(127 次贡献) +- 🥈 wangyue789(83 次贡献) +- 🥉 Mengz(59 次贡献) +- 4. whale(35 次贡献) +- 5. wauxing(31 次贡献) ## 六、Release Notes(自动生成) -基于提交历史,版本 `v0.1.18` 的变更摘要(规范化提交 70/127): +基于提交历史,版本 `v0.2.0` 的变更摘要(规范化提交 57/100): -## v0.1.18 +## v0.2.0 ### 新功能(feat) +- 新增仓库文件树快捷命令 +- add edit and update shortcuts +- 新增 CLI 自诊断命令 +- 新增 Raw API 批处理执行器 +- add shortcuts/license +- add gitlink-gatekeeper — Policy-as-Code PR merge gate +- add project bootstrap automation example +- add settings and topic shortcuts +- add insight and interaction shortcuts - remove per-issue detail API, add tag tables and persistence - rewrite gitlink-health as a Go shortcut +- 新增 6 个 Agent Skill + 收窄 gitlink-search 触发范围 - add metadata lookup shortcuts - support metadata fields and id alias - add CLI localization foundation @@ -75,34 +86,19 @@ - add OpenAPI shortcuts - add reopen shortcut - support body input files -- add milestone, compare, pr reopen shortcuts and api enhancements (#45) -- add PR closed time and repo README shortcuts (#49, #52) -- add repository member shortcuts (#46) -- add readme shortcut -- add repository member shortcuts -- add authors shortcut -- add assigners shortcut -- add community ops automation example -- add review shortcuts -- add skills gitlink-release-auto ### 缺陷修复(fix) +- 统一认证凭据 fallback 配置目录 +- use effective list filters +- 修正 Issue 和 PR 列表筛选 - address code review issues 3-6,8 +- align schema and gosec annotation - pr +review now posts a journal comment alongside the formal review - align schema and check tool lint - update TestRegisterAll expected groups for label and pipeline - treat view id as issue number - preserve raw file API paths - include closed time in view output -- improve input mode detection and error hint -- report batch-add partial failures -- improve missing binary diagnostics -- URL-encode branch name in unprotect, add +unprotect to Skill -- Issue 操作切换到 v1 API,统一使用 project_issues_index 替代数据库 ID -- 统一使用 pull_request_number 替代 pull_request_id -- preserve issue description on updates -- require Fork even for admin/owner, unless user explicitly says otherwise -- add tool boundary rules to prevent gh/hub misuse on GitLink ### 性能(perf) - parallel fetch with errgroup and global rate limiter @@ -111,36 +107,31 @@ - rename pr +close to pr +refuse ### 文档(docs) +- refine bootstrap architecture svg +- address project bootstrap review feedback +- 补充仓库文件树命令变更说明 +- use rendered project bootstrap architecture figure +- fix project bootstrap architecture layout +- add project bootstrap submission materials +- align project bootstrap validation command - add missing contributor puygob236 - update contributors section with usernames and new contributors -- trim materials for official PR -- remove continuation file from PR draft -- update final submission checklist placeholders -- align workflow agent competition materials -- add competition submission materials -- add REFERENCE.md for all 3 Skills -- finalize webhook shortcut docs -- update README for v0.1.17 — branch skill, npm one-step install, 12 skills -- remove Gitea reference from branch skill -- 补充 Issue status_id 映射表(1=新增, 2=正在解决, 3=已解决, 5=关闭) -- add Fork-based PR workflow guidelines -- update license from Apache 2.0 to MulanPSL-2.0 -- translate README to English, add Chinese README.zh-CN.md +- fix skills badge repository link ### 测试(test) - add comprehensive test coverage across all packages (88.5% → 90.3%) -- fix webhook endpoint expectations ### 持续集成(ci) - add local checks (make check, pre-commit hook) and Gitea Actions workflow skeleton - add PR checks workflow (build, test, vet, fmt) -- add GitHub Release workflow with npm publish ### 工程(chore) +- bump version to 0.2.0 +- 打磨仓库文件树命令交付质量 +- 打磨仓库文件树命令文档和国际化 - ensure trailing newline - fix CI workflow, golangci-lint config, and minor lint/format issues - add golangci-lint config and fix lint issues -- ignore local gitlink-cli binary --- diff --git a/examples/workflows/gitlink-flow/examples/examples/config.json b/examples/workflows/gitlink-flow/examples/examples/config.json deleted file mode 100644 index 07bde8b..0000000 --- a/examples/workflows/gitlink-flow/examples/examples/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "description": "gitlink-flow 社区运营自动化工作流配置。可列多个仓库批量运行。", - "output_dir": "outputs", - "repos": [ - { "owner": "Gitlink", "repo": "gitlink-cli" } - ] -} diff --git a/examples/workflows/gitlink-flow/examples/examples/demo_outputs/Gitlink_gitlink-cli_flow.md b/examples/workflows/gitlink-flow/examples/examples/demo_outputs/Gitlink_gitlink-cli_flow.md deleted file mode 100644 index aa4dff1..0000000 --- a/examples/workflows/gitlink-flow/examples/examples/demo_outputs/Gitlink_gitlink-cli_flow.md +++ /dev/null @@ -1,138 +0,0 @@ -# 社区运营周报 — Gitlink/gitlink-cli - -生成时间:2026-06-12 18:12 | 工具:gitlink-flow 端到端工作流 - -> 本周报由 gitlink-flow 自动串联 Issue 分拣、PR Review、Release Notes、社区健康体检、贡献者致谢等步骤生成,覆盖社区运营全链路。 - -## 一、仓库概览 - -- Star 5 / Fork 29 / Issue 18 / PR 232 -- 数据采集:gitlink-cli 命令 - -## 二、Issue 自动分拣 - -共 18 个 Issue,自动分类: - -- bug:2 个 -- feature:1 个 -- good-first:5 个 -- other:10 个 - -发现 **5** 个适合新人上手的任务,建议打 `good first issue` 标签: - -- #142586 API是否支持自动读取仓库内文件(README等)? -- #142303 [test] batch-close 测试 issue 1 -- #142304 [test] batch-close 测试 issue 2 -- #142124 Test: PR#7 标题已修改 -- #142155 [test] PR#11 v1 API 测试 - 已更新 - -## 三、PR Review 汇总 - -共 20 个 PR:开放 19 / 已合并 0 / 已关闭 1,合并率 0.0%。 - -待 Review 的 PR: - -- #232 feat(skills): 新增 gitlink-metrics 仓库量化指标看板 Skill — @Ct201314 (来自 Fork) -- #231 feat(skills): 新增 gitlink-onboard 新贡献者上手指南 Skill — @Ct201314 (来自 Fork) -- #230 feat(skills): 新增 gitlink-stale 陈旧 Issue/PR 清理 Skill — @Ct201314 (来自 Fork) -- #229 feat(skills): 新增 gitlink-standup 个人/团队日报周报 Skill — @Ct201314 (来自 Fork) -- #228 feat(skills): 新增 gitlink-changelog 版本变更对比 Skill — @Ct201314 (来自 Fork) -- #227 feat(message): add inbox management shortcuts — @wangyue111 (来自 Fork) -- #226 feat(issue): 增加批量评论、批量更新与文件正文输入能力 — @Mengz (来自 Fork) -- #225 feat(repo): add scaffold creation options — @wangyue111 (来自 Fork) -- #224 feat(project-template): add issue template shortcuts — @wangyue111 (来自 Fork) -- #223 feat(public-key): add SSH key shortcuts — @wangyue111 (来自 Fork) - -## 四、社区健康体检 - -健康度评分:**50/100** -- 已具备:README、LICENSE -- 缺失:CONTRIBUTING、贡献准则 - -## 五、贡献者致谢 - -共 20 位贡献者,本周致谢榜前列: - -- 🥇 wbtiger(127 次贡献) -- 🥈 wangyue789(83 次贡献) -- 🥉 Mengz(59 次贡献) -- 4. whale(35 次贡献) -- 5. wauxing(31 次贡献) - -## 六、Release Notes(自动生成) - -基于提交历史,版本 `v0.2.0` 的变更摘要(规范化提交 57/100): - -## v0.2.0 - -### 新功能(feat) -- 新增仓库文件树快捷命令 -- add edit and update shortcuts -- 新增 CLI 自诊断命令 -- 新增 Raw API 批处理执行器 -- add shortcuts/license -- add gitlink-gatekeeper — Policy-as-Code PR merge gate -- add project bootstrap automation example -- add settings and topic shortcuts -- add insight and interaction shortcuts -- remove per-issue detail API, add tag tables and persistence -- rewrite gitlink-health as a Go shortcut -- 新增 6 个 Agent Skill + 收窄 gitlink-search 触发范围 -- add metadata lookup shortcuts -- support metadata fields and id alias -- add CLI localization foundation -- add skill gitlink-license-compliance -- add issue label shortcuts -- add OpenAPI shortcuts -- add reopen shortcut -- support body input files - -### 缺陷修复(fix) -- 统一认证凭据 fallback 配置目录 -- use effective list filters -- 修正 Issue 和 PR 列表筛选 -- address code review issues 3-6,8 -- align schema and gosec annotation -- pr +review now posts a journal comment alongside the formal review -- align schema and check tool lint -- update TestRegisterAll expected groups for label and pipeline -- treat view id as issue number -- preserve raw file API paths -- include closed time in view output - -### 性能(perf) -- parallel fetch with errgroup and global rate limiter - -### 重构(refactor) -- rename pr +close to pr +refuse - -### 文档(docs) -- refine bootstrap architecture svg -- address project bootstrap review feedback -- 补充仓库文件树命令变更说明 -- use rendered project bootstrap architecture figure -- fix project bootstrap architecture layout -- add project bootstrap submission materials -- align project bootstrap validation command -- add missing contributor puygob236 -- update contributors section with usernames and new contributors -- fix skills badge repository link - -### 测试(test) -- add comprehensive test coverage across all packages (88.5% → 90.3%) - -### 持续集成(ci) -- add local checks (make check, pre-commit hook) and Gitea Actions workflow skeleton -- add PR checks workflow (build, test, vet, fmt) - -### 工程(chore) -- bump version to 0.2.0 -- 打磨仓库文件树命令交付质量 -- 打磨仓库文件树命令文档和国际化 -- ensure trailing newline -- fix CI workflow, golangci-lint config, and minor lint/format issues -- add golangci-lint config and fix lint issues - ---- - -由 gitlink-flow 社区运营自动化工作流生成。所有数据来自 GitLink 平台,分析全程只读。 \ No newline at end of file diff --git a/examples/workflows/gitlink-flow/src/src/cli.py b/examples/workflows/gitlink-flow/src/cli.py similarity index 100% rename from examples/workflows/gitlink-flow/src/src/cli.py rename to examples/workflows/gitlink-flow/src/cli.py diff --git a/examples/workflows/gitlink-flow/src/flow.py b/examples/workflows/gitlink-flow/src/flow.py index 204c126..0e5f87b 100644 --- a/examples/workflows/gitlink-flow/src/flow.py +++ b/examples/workflows/gitlink-flow/src/flow.py @@ -30,28 +30,74 @@ from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parent)) from glapi import GitLinkClient, GitLinkError, split_owner_repo +import cli import steps import report as report_mod -def run_flow(owner: str, repo: str, client: GitLinkClient | None = None, - commit_pages: int = 4) -> dict[str, Any]: - """对单个仓库执行完整工作流,返回各步骤结果。""" - client = client or GitLinkClient() +def collect(owner: str, repo: str, client: GitLinkClient, + commit_pages: int = 4, use_cli: bool | None = None) -> dict[str, Any]: + """采集阶段:优先走 gitlink-cli 命令(主调用链),失败回退直连 API。 - # ===== 采集阶段 ===== - info = client.repo_info(owner, repo) - issues = client.issues(owner, repo, limit=50) - pulls = client.pulls(owner, repo, limit=50) + 赛题要求工作流组合 gitlink-cli 已有命令。因此 repo/issue/pr/release + 四类数据优先调用 `gitlink-cli +list/+info`;当本机未装 gitlink-cli 或 + 某命令调用失败时,回退到 glapi 直连,保证工作流不因依赖缺失而中断。 + + commits 与目录树(list_dir)gitlink-cli 暂无对应只读命令,沿用 glapi。 + + use_cli 为 None 时自动探测本机是否安装 gitlink-cli;显式传 False 可强制 + 走 glapi 直连(供离线单元测试使用)。 + """ + if use_cli is None: + use_cli = cli.cli_available() + source = "gitlink-cli 命令" if use_cli else "直连 API(未检测到 gitlink-cli,已回退)" + + def via_cli(cli_fn, fallback_fn): + """单项数据:优先 cli,任何失败回退 glapi。""" + if use_cli: + try: + return cli_fn() + except cli.CliError: + pass + return fallback_fn() + + info = via_cli(lambda: cli.repo_info(owner, repo), + lambda: client.repo_info(owner, repo)) + issues = via_cli(lambda: cli.issues(owner, repo, limit=50), + lambda: client.issues(owner, repo, limit=50)) + pulls = via_cli(lambda: cli.pulls(owner, repo, limit=50), + lambda: client.pulls(owner, repo, limit=50)) + releases = via_cli(lambda: cli.releases(owner, repo), + lambda: client.releases(owner, repo)) + # commits / 目录树:gitlink-cli 无对应只读命令,直接用 glapi commits = client.commits(owner, repo, max_pages=commit_pages) contributors = client.contributors(owner, repo) - releases = client.releases(owner, repo) if hasattr(client, "releases") else [] try: root_entries = client.list_dir(owner, repo, "", "master") root_files = [str(e.get("name", "")) for e in root_entries] except GitLinkError: root_files = [] + return {"source": source, "info": info, "issues": issues, "pulls": pulls, + "commits": commits, "contributors": contributors, + "releases": releases, "root_files": root_files} + + +def run_flow(owner: str, repo: str, client: GitLinkClient | None = None, + commit_pages: int = 4, use_cli: bool | None = None) -> dict[str, Any]: + """对单个仓库执行完整工作流,返回各步骤结果。""" + client = client or GitLinkClient() + + # ===== 采集阶段(gitlink-cli 主调用链 + glapi fallback)===== + bundle = collect(owner, repo, client, commit_pages=commit_pages, use_cli=use_cli) + info = bundle["info"] + issues = bundle["issues"] + pulls = bundle["pulls"] + commits = bundle["commits"] + contributors = bundle["contributors"] + releases = bundle["releases"] + root_files = bundle["root_files"] + # ===== 分析阶段(6 步)===== latest_version = "Unreleased" if releases: @@ -61,6 +107,7 @@ def run_flow(owner: str, repo: str, client: GitLinkClient | None = None, "owner": owner, "repo": repo, "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M"), + "data_source": bundle["source"], "repo_info": { "name": info.get("name"), "issues_count": info.get("issues_count"), diff --git a/examples/workflows/gitlink-flow/src/report.py b/examples/workflows/gitlink-flow/src/report.py index db23981..234e4e3 100644 --- a/examples/workflows/gitlink-flow/src/report.py +++ b/examples/workflows/gitlink-flow/src/report.py @@ -30,6 +30,7 @@ def render_weekly(result: dict[str, Any]) -> str: "", f"- Star {info.get('praises_count') or 0} / Fork {info.get('forked_count') or 0}" f" / Issue {info.get('issues_count') or 0} / PR {info.get('pull_requests_count') or 0}", + f"- 数据采集:{result.get('data_source', 'gitlink-cli 命令')}", "", "## 二、Issue 自动分拣", "", diff --git a/examples/workflows/gitlink-flow/src/src/flow.py b/examples/workflows/gitlink-flow/src/src/flow.py deleted file mode 100644 index 0e5f87b..0000000 --- a/examples/workflows/gitlink-flow/src/src/flow.py +++ /dev/null @@ -1,189 +0,0 @@ -"""gitlink-flow:社区运营自动化端到端工作流编排器。 - -串联多个步骤,对一个真实 GitLink 仓库执行完整的社区运营自动化: - - 采集(glapi) - → ① Issue 自动分拣(triage) - → ② PR Review 汇总(pr-review) - → ③ Release Notes 生成(release-notes) - → ④ 社区健康体检(复用 gitlink-scaffold) - → ⑤ 贡献者致谢(复用 gitlink-contributor) - → ⑥ 生成社区运营周报(汇总以上全部) - -串联 6 个步骤、复用 5 个自研 Skill 的能力,远超"≥3 个调用"的要求。 -数据来自 GitLink 公开 API(只读),无需登录。 - -用法: - python flow.py --owner Gitlink --repo gitlink-cli - python flow.py --owner Gitlink --repo gitlink-cli --format json - python flow.py --config examples/config.json -""" - -from __future__ import annotations - -import argparse -import json -import sys -from datetime import datetime -from pathlib import Path -from typing import Any - -sys.path.insert(0, str(Path(__file__).resolve().parent)) -from glapi import GitLinkClient, GitLinkError, split_owner_repo -import cli -import steps -import report as report_mod - - -def collect(owner: str, repo: str, client: GitLinkClient, - commit_pages: int = 4, use_cli: bool | None = None) -> dict[str, Any]: - """采集阶段:优先走 gitlink-cli 命令(主调用链),失败回退直连 API。 - - 赛题要求工作流组合 gitlink-cli 已有命令。因此 repo/issue/pr/release - 四类数据优先调用 `gitlink-cli +list/+info`;当本机未装 gitlink-cli 或 - 某命令调用失败时,回退到 glapi 直连,保证工作流不因依赖缺失而中断。 - - commits 与目录树(list_dir)gitlink-cli 暂无对应只读命令,沿用 glapi。 - - use_cli 为 None 时自动探测本机是否安装 gitlink-cli;显式传 False 可强制 - 走 glapi 直连(供离线单元测试使用)。 - """ - if use_cli is None: - use_cli = cli.cli_available() - source = "gitlink-cli 命令" if use_cli else "直连 API(未检测到 gitlink-cli,已回退)" - - def via_cli(cli_fn, fallback_fn): - """单项数据:优先 cli,任何失败回退 glapi。""" - if use_cli: - try: - return cli_fn() - except cli.CliError: - pass - return fallback_fn() - - info = via_cli(lambda: cli.repo_info(owner, repo), - lambda: client.repo_info(owner, repo)) - issues = via_cli(lambda: cli.issues(owner, repo, limit=50), - lambda: client.issues(owner, repo, limit=50)) - pulls = via_cli(lambda: cli.pulls(owner, repo, limit=50), - lambda: client.pulls(owner, repo, limit=50)) - releases = via_cli(lambda: cli.releases(owner, repo), - lambda: client.releases(owner, repo)) - # commits / 目录树:gitlink-cli 无对应只读命令,直接用 glapi - commits = client.commits(owner, repo, max_pages=commit_pages) - contributors = client.contributors(owner, repo) - try: - root_entries = client.list_dir(owner, repo, "", "master") - root_files = [str(e.get("name", "")) for e in root_entries] - except GitLinkError: - root_files = [] - - return {"source": source, "info": info, "issues": issues, "pulls": pulls, - "commits": commits, "contributors": contributors, - "releases": releases, "root_files": root_files} - - -def run_flow(owner: str, repo: str, client: GitLinkClient | None = None, - commit_pages: int = 4, use_cli: bool | None = None) -> dict[str, Any]: - """对单个仓库执行完整工作流,返回各步骤结果。""" - client = client or GitLinkClient() - - # ===== 采集阶段(gitlink-cli 主调用链 + glapi fallback)===== - bundle = collect(owner, repo, client, commit_pages=commit_pages, use_cli=use_cli) - info = bundle["info"] - issues = bundle["issues"] - pulls = bundle["pulls"] - commits = bundle["commits"] - contributors = bundle["contributors"] - releases = bundle["releases"] - root_files = bundle["root_files"] - - # ===== 分析阶段(6 步)===== - latest_version = "Unreleased" - if releases: - latest_version = releases[0].get("tag_name") or releases[0].get("name") or "Unreleased" - - result = { - "owner": owner, - "repo": repo, - "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M"), - "data_source": bundle["source"], - "repo_info": { - "name": info.get("name"), - "issues_count": info.get("issues_count"), - "pull_requests_count": info.get("pull_requests_count"), - "praises_count": info.get("praises_count"), - "forked_count": info.get("forked_count"), - }, - "step1_triage": steps.triage_issues(issues), - "step2_pr_review": steps.pr_review_summary(pulls), - "step3_release_notes": steps.release_notes(commits, version=latest_version), - "step4_health": steps.health_check(root_files), - "step5_contributors": steps.contributor_highlights(contributors), - } - # 第 6 步:汇总周报 - result["step6_weekly_report"] = report_mod.render_weekly(result) - return result - - -def main(argv: list[str] | None = None) -> int: - p = argparse.ArgumentParser(prog="gitlink-flow", - description="社区运营自动化端到端工作流") - p.add_argument("--owner", help="仓库所有者") - p.add_argument("--repo", help="仓库名称") - p.add_argument("--slug", help="owner/repo 或完整 URL") - p.add_argument("--config", type=Path, help="批量配置 JSON(含 repos 列表)") - p.add_argument("--commit-pages", type=int, default=4, help="提交采集页数(每页 50)") - p.add_argument("--format", choices=["markdown", "json"], default="markdown") - p.add_argument("--output", type=Path, help="输出文件") - p.add_argument("--output-dir", type=Path, help="批量模式输出目录") - args = p.parse_args(argv) - - targets: list[tuple[str, str]] = [] - if args.config: - cfg = json.loads(args.config.read_text(encoding="utf-8")) - for item in cfg.get("repos", []): - if item.get("owner") and item.get("repo"): - targets.append((item["owner"], item["repo"])) - if args.slug: - targets.append(split_owner_repo(args.slug)) - elif args.owner and args.repo: - targets.append((args.owner, args.repo)) - - if not targets: - print("错误:请用 --owner/--repo 或 --slug 或 --config 指定仓库。", file=sys.stderr) - return 2 - - client = GitLinkClient() - exit_code = 0 - for owner, repo in targets: - print(f"[工作流] 处理 {owner}/{repo} ...", flush=True) - try: - result = run_flow(owner, repo, client=client, commit_pages=args.commit_pages) - except GitLinkError as exc: - print(f" 失败:{exc}", file=sys.stderr) - exit_code = 1 - continue - - if args.format == "json": - out = json.dumps(result, ensure_ascii=False, indent=2) - else: - out = result["step6_weekly_report"] - - if args.output_dir: - args.output_dir.mkdir(parents=True, exist_ok=True) - ext = "json" if args.format == "json" else "md" - fp = args.output_dir / f"{owner}_{repo}_flow.{ext}" - fp.write_text(out, encoding="utf-8") - print(f" 已写入 {fp}", flush=True) - elif args.output: - args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(out, encoding="utf-8") - print(f" 已写入 {args.output}", flush=True) - else: - print(out) - return exit_code - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/examples/workflows/gitlink-flow/src/src/glapi.py b/examples/workflows/gitlink-flow/src/src/glapi.py deleted file mode 100644 index f41c718..0000000 --- a/examples/workflows/gitlink-flow/src/src/glapi.py +++ /dev/null @@ -1,247 +0,0 @@ -"""GitLink 公开 API 共享客户端。 - -供 gitlink-skills-pack 下各 Skill 的脚本复用。仅依赖 Python 标准库, -无需第三方包,便于在受限环境或 Agent 沙箱中运行。 - -数据全部来自 GitLink 平台公开接口(https://www.gitlink.org.cn/api), -默认无需 token;如需访问私有仓库,可传入 token。 - -所有方法均为只读,不修改任何远程数据。 -""" - -from __future__ import annotations - -import base64 -import json -import time -import urllib.error -import urllib.parse -import urllib.request -from pathlib import Path -from typing import Any - -API_BASE = "https://www.gitlink.org.cn/api" -USER_AGENT = "gitlink-skills-pack/1.0 (+https://www.gitlink.org.cn)" -DEFAULT_TIMEOUT = 30 -COMMIT_PAGE_SIZE = 50 # GitLink commits 接口每页硬上限 - - -class GitLinkError(RuntimeError): - """API 调用中不可恢复的错误。""" - - -class GitLinkClient: - """GitLink 公开数据接口客户端。 - - 带可选文件缓存:同一资源重复读取不重复打网,对平台友好。 - """ - - def __init__(self, base: str = API_BASE, token: str | None = None, - timeout: int = DEFAULT_TIMEOUT, cache_dir: Path | None = None) -> None: - self.base = base.rstrip("/") - self.token = token - self.timeout = timeout - self.cache_dir = cache_dir - if self.cache_dir: - self.cache_dir.mkdir(parents=True, exist_ok=True) - - # ------------------------------------------------------------------ - # 底层请求 - # ------------------------------------------------------------------ - def _cache_path(self, url: str) -> Path | None: - if not self.cache_dir: - return None - safe = urllib.parse.quote(url, safe="") - return self.cache_dir / f"{safe}.json" - - def get(self, path: str, query: dict[str, Any] | None = None) -> Any: - """GET 请求,返回解析后的 JSON(dict/list)或 None。""" - url = f"{self.base}/{path.lstrip('/')}" - if query: - url = f"{url}?{urllib.parse.urlencode(query)}" - - cache_path = self._cache_path(url) - if cache_path and cache_path.exists(): - return json.loads(cache_path.read_text(encoding="utf-8")) - - headers = {"Accept": "application/json", "User-Agent": USER_AGENT} - if self.token: - headers["Authorization"] = f"Bearer {self.token}" - - req = urllib.request.Request(url, headers=headers) - try: - with urllib.request.urlopen(req, timeout=self.timeout) as resp: - raw = resp.read().decode("utf-8", errors="replace") - except urllib.error.HTTPError as exc: - raise GitLinkError(f"HTTP {exc.code}: {url}") from exc - except urllib.error.URLError as exc: - raise GitLinkError(f"网络错误: {url} -> {exc.reason}") from exc - - text = raw.strip() - if not text or text in ("null", "{}", "[]"): - data: Any = None - elif text[0] in "{[": - try: - data = json.loads(text) - except json.JSONDecodeError as exc: - raise GitLinkError(f"响应非 JSON: {url}") from exc - else: - raise GitLinkError(f"响应非 JSON(可能是 HTML): {url}") - - if cache_path is not None: - cache_path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") - return data - - # ------------------------------------------------------------------ - # 资源访问(高层封装) - # ------------------------------------------------------------------ - def repo_info(self, owner: str, repo: str) -> dict[str, Any]: - """仓库元信息。""" - data = self.get(f"{owner}/{repo}.json") - return data if isinstance(data, dict) else {} - - def issues(self, owner: str, repo: str, limit: int = 50, - page: int = 1) -> list[dict[str, Any]]: - """Issue 列表。""" - data = self.get(f"{owner}/{repo}/issues.json", {"page": page, "limit": limit}) - return _extract_list(data, ("issues",)) - - def issue_detail(self, owner: str, repo: str, number: int) -> dict[str, Any]: - """单个 Issue 详情(含完整字段)。""" - data = self.get(f"{owner}/{repo}/issues/{number}.json") - return data if isinstance(data, dict) else {} - - def pulls(self, owner: str, repo: str, limit: int = 50, - page: int = 1) -> list[dict[str, Any]]: - """PR 列表。""" - data = self.get(f"{owner}/{repo}/pulls.json", {"page": page, "limit": limit}) - return _extract_list(data, ("issues", "pulls")) - - def contributors(self, owner: str, repo: str) -> list[dict[str, Any]]: - """贡献者列表。""" - data = self.get(f"{owner}/{repo}/contributors.json") - return _extract_list(data, ("list",)) - - def commits(self, owner: str, repo: str, max_pages: int = 4) -> list[dict[str, Any]]: - """提交列表(按需翻页,每页 50 条,以 total_count 为终止依据)。""" - out: list[dict[str, Any]] = [] - total: int | None = None - for page in range(1, max(1, max_pages) + 1): - data = self.get(f"{owner}/{repo}/commits.json", - {"page": page, "limit": COMMIT_PAGE_SIZE}) - if total is None and isinstance(data, dict): - total = _safe_int(data.get("total_count")) or None - page_items = _extract_list(data, ("commits",)) - if not page_items: - break - out.extend(page_items) - if total is not None and len(out) >= total: - break - return out - - def list_dir(self, owner: str, repo: str, path: str = "", - ref: str = "master") -> list[dict[str, Any]]: - """列出目录下的条目(文件与子目录)。 - - 返回的每个 entry 含 name / path / type(file|dir) / sha / size, - 文件类型的 entry 还可能直接带明文 content。 - """ - data = self.get(f"{owner}/{repo}/sub_entries.json", - {"filepath": path, "ref": ref}) - # 查询目录时 entries 为 list;查询单文件时 entries 为单个 dict。 - # 统一归一化为 list,便于下游处理。 - if isinstance(data, dict): - entries = data.get("entries") - if isinstance(entries, dict): - return [entries] - if isinstance(entries, list): - return entries - return _extract_list(data, ("entries",)) - - def file_content(self, owner: str, repo: str, filepath: str, - ref: str = "master") -> str | None: - """读取单个文件的文本内容。 - - GitLink 的 sub_entries 接口对单文件查询会在 entries 中返回明文 content, - 据此取出。文件不存在或无内容时返回 None。 - """ - entries = self.list_dir(owner, repo, filepath, ref) - target = filepath.rsplit("/", 1)[-1] - for entry in entries: - if entry.get("type") == "file" and entry.get("name") == target: - content = entry.get("content") - if isinstance(content, str): - return content - # 回退:部分情况下单文件查询 entries 仅一项 - if len(entries) == 1 and entries[0].get("type") == "file": - content = entries[0].get("content") - if isinstance(content, str): - return content - return None - - def releases(self, owner: str, repo: str, limit: int = 50, - page: int = 1) -> list[dict[str, Any]]: - """版本发布列表。""" - data = self.get(f"{owner}/{repo}/releases.json", {"page": page, "limit": limit}) - return _extract_list(data, ("releases",)) - - def readme(self, owner: str, repo: str, ref: str = "master") -> str | None: - """读取仓库 README(自动 base64 解码)。""" - data = self.get(f"{owner}/{repo}/readme.json", {"ref": ref}) - if not isinstance(data, dict): - return None - content = data.get("content") - if not isinstance(content, str): - return None - # 注意:GitLink 的 readme.json 虽然 encoding 标为 base64, - # 实测 content 多为明文 Markdown。先探测明文特征,命中则直接返回; - # 否则再尝试 base64 解码。 - stripped = content.lstrip() - if stripped.startswith(("#", "<", "[", "-", "*", "本", "这", "项")) or "\n" in content[:200]: - return content - try: - raw = base64.b64decode(content.encode("ascii", "ignore")) - decoded = raw.decode("utf-8", errors="replace") - # 解码结果若不像文本(大量替换符),回退为原文 - if decoded.count("\ufffd") > len(decoded) * 0.1: - return content - return decoded - except (ValueError, TypeError): - return content - - -# ---------------------------------------------------------------------------- -# 辅助 -# ---------------------------------------------------------------------------- - -def _extract_list(payload: Any, keys: tuple[str, ...]) -> list[Any]: - """从可能嵌套的响应中提取第一个匹配键的列表。""" - if isinstance(payload, list): - return payload - if isinstance(payload, dict): - for key in keys: - value = payload.get(key) - if isinstance(value, list): - return value - return [] - - -def _safe_int(value: Any, default: int = 0) -> int: - try: - return int(value) - except (TypeError, ValueError): - return default - - -def split_owner_repo(slug: str) -> tuple[str, str]: - """把 'owner/repo' 或完整 URL 解析为 (owner, repo)。""" - s = slug.strip() - if s.startswith("http"): - parts = urllib.parse.urlparse(s).path.strip("/").split("/") - if len(parts) >= 2: - return parts[0], parts[1].replace(".git", "") - raise GitLinkError(f"无法从 URL 解析 owner/repo: {slug}") - if "/" in s: - owner, repo = s.split("/", 1) - return owner, repo.replace(".git", "") - raise GitLinkError(f"格式应为 owner/repo: {slug}") diff --git a/examples/workflows/gitlink-flow/src/src/report.py b/examples/workflows/gitlink-flow/src/src/report.py deleted file mode 100644 index 234e4e3..0000000 --- a/examples/workflows/gitlink-flow/src/src/report.py +++ /dev/null @@ -1,98 +0,0 @@ -"""社区运营周报生成。 - -把工作流 6 个步骤的结果汇总成一份可直接发布到 Issue/Wiki 的社区运营周报。 -""" - -from __future__ import annotations - -from typing import Any - - -def render_weekly(result: dict[str, Any]) -> str: - """渲染社区运营周报(Markdown)。""" - owner, repo = result["owner"], result["repo"] - info = result["repo_info"] - triage = result["step1_triage"] - pr = result["step2_pr_review"] - rel = result["step3_release_notes"] - health = result["step4_health"] - contrib = result["step5_contributors"] - - lines = [ - f"# 社区运营周报 — {owner}/{repo}", - "", - f"生成时间:{result['generated_at']} | 工具:gitlink-flow 端到端工作流", - "", - "> 本周报由 gitlink-flow 自动串联 Issue 分拣、PR Review、Release Notes、" - "社区健康体检、贡献者致谢等步骤生成,覆盖社区运营全链路。", - "", - "## 一、仓库概览", - "", - f"- Star {info.get('praises_count') or 0} / Fork {info.get('forked_count') or 0}" - f" / Issue {info.get('issues_count') or 0} / PR {info.get('pull_requests_count') or 0}", - f"- 数据采集:{result.get('data_source', 'gitlink-cli 命令')}", - "", - "## 二、Issue 自动分拣", - "", - f"共 {triage['total']} 个 Issue,自动分类:", - "", - ] - for cat, n in triage["by_category"].items(): - if n: - lines.append(f"- {cat}:{n} 个") - lines.append("") - if triage["good_first_count"]: - lines.append(f"发现 **{triage['good_first_count']}** 个适合新人上手的任务,建议打 `good first issue` 标签:") - lines.append("") - for it in triage["items"]: - if it["good_first"]: - ref = f"#{it['id']}" if it["id"] else "" - lines.append(f"- {ref} {it['title']}") - lines.append("") - - lines += [ - "## 三、PR Review 汇总", - "", - f"共 {pr['total']} 个 PR:开放 {pr['open']} / 已合并 {pr['merged']} / 已关闭 {pr['closed']}," - f"合并率 {pr['merge_rate']}%。", - "", - ] - if pr["pending_review"]: - lines.append("待 Review 的 PR:") - lines.append("") - for p in pr["pending_review"][:10]: - tag = "(来自 Fork)" if p["is_fork"] else "" - ref = f"#{p['id']}" if p["id"] else "" - lines.append(f"- {ref} {p['title']} — @{p['author']} {tag}") - lines.append("") - - lines += [ - "## 四、社区健康体检", - "", - f"健康度评分:**{health['score']}/100**", - f"- 已具备:{('、'.join(health['present'])) or '无'}", - f"- 缺失:{('、'.join(health['missing'])) or '无'}", - "", - "## 五、贡献者致谢", - "", - f"共 {contrib['total_contributors']} 位贡献者,本周致谢榜前列:", - "", - ] - for i, c in enumerate(contrib["top"], 1): - medal = {1: "🥇", 2: "🥈", 3: "🥉"}.get(i, f"{i}.") - lines.append(f"- {medal} {c['name']}({c['contributions']} 次贡献)") - lines.append("") - - lines += [ - "## 六、Release Notes(自动生成)", - "", - f"基于提交历史,版本 `{rel['version']}` 的变更摘要" - f"(规范化提交 {rel['typed_commits']}/{rel['total_commits']}):", - "", - rel["markdown"] or "(暂无符合 conventional commits 规范的提交)", - "", - "---", - "", - "由 gitlink-flow 社区运营自动化工作流生成。所有数据来自 GitLink 平台,分析全程只读。", - ] - return "\n".join(lines) diff --git a/examples/workflows/gitlink-flow/src/src/steps.py b/examples/workflows/gitlink-flow/src/src/steps.py deleted file mode 100644 index 5b04ecd..0000000 --- a/examples/workflows/gitlink-flow/src/src/steps.py +++ /dev/null @@ -1,207 +0,0 @@ -"""gitlink-flow 工作流步骤库。 - -每个函数是一个可独立测试的工作流步骤,接收采集到的原始数据,输出结构化结果。 -编排器 flow.py 按顺序调用这些步骤,串联成端到端的社区运营自动化工作流。 - -三个子工作流对标官方 examples/workflows 的参考场景: -- triage:Issue 自动分拣(按关键词/标签分类,识别新手友好任务) -- pr_review_summary:PR Review 汇总(统计 PR 状态、识别待处理) -- release_notes:Release Notes 生成(从提交按 conventional commits 归类) - -另复用 5 个自研 Skill 的核心分析:社区健康体检、依赖巡检、贡献者致谢、知识库索引。 - -全程只读,不修改远程数据。 -""" - -from __future__ import annotations - -import re -from collections import Counter -from typing import Any - -# --------------------------------------------------------------------------- -# 通用归一化 -# --------------------------------------------------------------------------- - -CONVENTIONAL_TYPES = { - "feat": "新功能", "fix": "缺陷修复", "docs": "文档", "refactor": "重构", - "perf": "性能", "test": "测试", "chore": "工程", "build": "构建", - "ci": "持续集成", "style": "风格", "revert": "回退", -} - -GOOD_FIRST_HINTS = ["typo", "docs", "doc", "readme", "test", "translation", "example", - "文档", "注释", "翻译", "示例", "拼写"] -BUG_HINTS = ["bug", "error", "fail", "crash", "panic", "错误", "失败", "崩溃", "异常"] -FEATURE_HINTS = ["feature", "support", "add", "enhance", "新增", "支持", "功能", "建议"] -QUESTION_HINTS = ["how", "why", "question", "如何", "怎么", "为什么", "请问"] - - -def _commit_type(message: str) -> str: - m = re.match(r"^\s*([a-zA-Z]+)(?:\([^)]*\))?!?:", message or "") - if not m: - return "other" - t = m.group(1).lower() - return t if t in CONVENTIONAL_TYPES else "other" - - -# --------------------------------------------------------------------------- -# 子工作流 1:Issue 自动分拣(triage) -# --------------------------------------------------------------------------- - -def triage_issues(issues: list[dict[str, Any]]) -> dict[str, Any]: - """对 Issue 按内容自动分类,并识别新手友好任务。 - - 分类:bug / feature / question / good-first / other。 - 每个 Issue 给出建议标签,供维护者打标参考。 - """ - buckets: dict[str, list[dict[str, Any]]] = { - "bug": [], "feature": [], "question": [], "good-first": [], "other": [], - } - results: list[dict[str, Any]] = [] - for it in issues: - title = str(it.get("name") or it.get("subject") or it.get("title") or "") - body = str(it.get("description") or it.get("body") or "") - text = f"{title}\n{body}".lower() - - category = "other" - if any(h in text for h in BUG_HINTS): - category = "bug" - elif any(h in text for h in FEATURE_HINTS): - category = "feature" - elif any(h in text for h in QUESTION_HINTS): - category = "question" - - good_first = any(h in text for h in GOOD_FIRST_HINTS) and len(body) < 800 - bucket_key = "good-first" if good_first else category - rec = { - "id": str(it.get("id") or ""), - "title": title[:60], - "category": category, - "good_first": good_first, - "suggested_label": "good first issue" if good_first else category, - } - buckets.setdefault(bucket_key, []).append(rec) - results.append(rec) - - return { - "total": len(issues), - "by_category": {k: len(v) for k, v in buckets.items()}, - "good_first_count": len(buckets["good-first"]), - "items": results, - } - - -# --------------------------------------------------------------------------- -# 子工作流 2:PR Review 汇总(pr-review) -# --------------------------------------------------------------------------- - -def pr_review_summary(pulls: list[dict[str, Any]]) -> dict[str, Any]: - """汇总 PR 状态,识别待 Review 的 PR。 - - GitLink pull_request_status:0=open, 1=merged, 2=closed。 - """ - open_prs: list[dict[str, Any]] = [] - merged = closed = 0 - for p in pulls: - status = p.get("pull_request_status") - title = str(p.get("name") or p.get("title") or "") - author = p.get("author_login") or p.get("author_name") or "unknown" - if status == 1: - merged += 1 - elif status == 2: - closed += 1 - else: - open_prs.append({ - "id": str(p.get("pull_request_number") or p.get("id") or ""), - "title": title[:60], - "author": author, - "is_fork": bool(p.get("fork_project_user")), - }) - return { - "total": len(pulls), - "open": len(open_prs), - "merged": merged, - "closed": closed, - "merge_rate": round(merged / len(pulls) * 100, 1) if pulls else 0.0, - "pending_review": open_prs, - } - - -# --------------------------------------------------------------------------- -# 子工作流 3:Release Notes 生成(release-notes) -# --------------------------------------------------------------------------- - -def release_notes(commits: list[dict[str, Any]], version: str = "Unreleased") -> dict[str, Any]: - """从提交历史按 conventional commits 归类生成 Release Notes。""" - groups: dict[str, list[str]] = {} - typed = 0 - for c in commits: - msg = (c.get("message") or "").splitlines()[0] if c.get("message") else "" - t = _commit_type(msg) - if t == "other": - continue - typed += 1 - # 去掉类型前缀,保留描述 - desc = re.sub(r"^\s*[a-zA-Z]+(?:\([^)]*\))?!?:\s*", "", msg).strip() - groups.setdefault(t, []).append(desc) - - # 生成 Markdown - order = ["feat", "fix", "perf", "refactor", "docs", "test", "build", "ci", "chore"] - lines = [f"## {version}", ""] - for t in order: - if t in groups: - lines.append(f"### {CONVENTIONAL_TYPES[t]}({t})") - for d in groups[t][:20]: - lines.append(f"- {d}") - lines.append("") - return { - "version": version, - "typed_commits": typed, - "total_commits": len(commits), - "groups": {k: len(v) for k, v in groups.items()}, - "markdown": "\n".join(lines).strip(), - } - - -# --------------------------------------------------------------------------- -# 复用 Skill:社区健康体检(scaffold 核心) -# --------------------------------------------------------------------------- - -HEALTH_FILES = { - "README": ["readme.md", "readme.rst", "readme"], - "LICENSE": ["license", "license.md", "copying"], - "CONTRIBUTING": ["contributing.md", "contributing"], - "贡献准则": ["code_of_conduct.md"], -} - - -def health_check(root_files: list[str]) -> dict[str, Any]: - """基于根目录文件名检测社区健康文件齐全度。""" - names = {f.lower() for f in root_files} - present, missing = [], [] - for label, cands in HEALTH_FILES.items(): - if any(c in names for c in cands): - present.append(label) - else: - missing.append(label) - score = round(len(present) / len(HEALTH_FILES) * 100) - return {"score": score, "present": present, "missing": missing} - - -# --------------------------------------------------------------------------- -# 复用 Skill:贡献者致谢(contributor 核心) -# --------------------------------------------------------------------------- - -def contributor_highlights(contributors: list[dict[str, Any]], top: int = 5) -> dict[str, Any]: - """提取贡献者亮点:总数、前 N 名。""" - profiles = sorted( - ({"name": c.get("login") or c.get("name") or "unknown", - "contributions": int(c.get("contributions") or 0)} for c in contributors), - key=lambda x: x["contributions"], reverse=True, - ) - total = sum(p["contributions"] for p in profiles) - return { - "total_contributors": len(profiles), - "total_contributions": total, - "top": profiles[:top], - } diff --git a/examples/workflows/gitlink-flow/tests/test_flow.py b/examples/workflows/gitlink-flow/tests/test_flow.py index d844646..95c6d68 100644 --- a/examples/workflows/gitlink-flow/tests/test_flow.py +++ b/examples/workflows/gitlink-flow/tests/test_flow.py @@ -17,6 +17,7 @@ import pytest import steps import report as report_mod +import cli from flow import run_flow @@ -157,7 +158,7 @@ class FakeClient: class TestOrchestrator: def test_run_flow_all_steps(self): - result = run_flow("o", "repo", client=FakeClient()) + result = run_flow("o", "repo", client=FakeClient(), use_cli=False) assert "step1_triage" in result assert "step2_pr_review" in result assert "step3_release_notes" in result @@ -166,7 +167,7 @@ class TestOrchestrator: assert "step6_weekly_report" in result def test_weekly_report_renders(self): - result = run_flow("o", "repo", client=FakeClient()) + result = run_flow("o", "repo", client=FakeClient(), use_cli=False) md = result["step6_weekly_report"] assert "社区运营周报" in md assert "Issue 自动分拣" in md @@ -174,10 +175,31 @@ class TestOrchestrator: assert "Release Notes" in md def test_triage_in_flow(self): - result = run_flow("o", "repo", client=FakeClient()) + result = run_flow("o", "repo", client=FakeClient(), use_cli=False) # 一个 good-first(docs typo)+ 一个 bug assert result["step1_triage"]["good_first_count"] == 1 + def test_fallback_data_source(self): + # 强制走 glapi 直连时,数据源应标注已回退 + result = run_flow("o", "repo", client=FakeClient(), use_cli=False) + assert "回退" in result["data_source"] + + +class TestCliLayer: + """gitlink-cli 封装层:解包信封与列表提取,不实际调用命令。""" + + def test_extract_list_from_dict(self): + assert cli._extract_list({"issues": [1, 2]}, ("issues",)) == [1, 2] + + def test_extract_list_passthrough(self): + assert cli._extract_list([1, 2], ("issues",)) == [1, 2] + + def test_extract_list_empty(self): + assert cli._extract_list({"other": 1}, ("issues",)) == [] + + def test_cli_available_returns_bool(self): + assert isinstance(cli.cli_available(), bool) + if __name__ == "__main__": sys.exit(pytest.main([__file__, "-v"])) diff --git a/examples/workflows/gitlink-flow/tests/tests/test_flow.py b/examples/workflows/gitlink-flow/tests/tests/test_flow.py deleted file mode 100644 index 95c6d68..0000000 --- a/examples/workflows/gitlink-flow/tests/tests/test_flow.py +++ /dev/null @@ -1,205 +0,0 @@ -"""gitlink-flow 单元测试。 - -覆盖三个子工作流(triage/pr-review/release-notes)与复用 Skill 步骤, -以及编排器 run_flow。使用合成数据 + FakeClient,不触网。 - - python -m pytest tests/ -q -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) - -import pytest - -import steps -import report as report_mod -import cli -from flow import run_flow - - -# --------------------------------------------------------------------------- -# 子工作流 1:triage -# --------------------------------------------------------------------------- - -class TestTriage: - def test_classifies_bug(self): - r = steps.triage_issues([{"name": "登录时 crash 崩溃", "description": "复现步骤"}]) - assert r["by_category"]["bug"] == 1 - - def test_classifies_feature(self): - r = steps.triage_issues([{"name": "建议新增搜索功能 feature", "description": ""}]) - assert r["by_category"]["feature"] == 1 - - def test_good_first_detected(self): - r = steps.triage_issues([{"name": "fix typo in docs", "description": "small"}]) - assert r["good_first_count"] == 1 - assert r["items"][0]["suggested_label"] == "good first issue" - - def test_empty(self): - r = steps.triage_issues([]) - assert r["total"] == 0 - assert r["good_first_count"] == 0 - - -# --------------------------------------------------------------------------- -# 子工作流 2:pr-review -# --------------------------------------------------------------------------- - -class TestPRReview: - def test_status_counts(self): - pulls = [ - {"name": "a", "pull_request_status": 0, "author_login": "u1"}, - {"name": "b", "pull_request_status": 1, "author_login": "u2"}, - {"name": "c", "pull_request_status": 2, "author_login": "u3"}, - ] - r = steps.pr_review_summary(pulls) - assert r["open"] == 1 and r["merged"] == 1 and r["closed"] == 1 - assert r["merge_rate"] == round(1 / 3 * 100, 1) - - def test_pending_review_listed(self): - pulls = [{"name": "open pr", "pull_request_status": 0, - "author_login": "dev", "fork_project_user": "dev"}] - r = steps.pr_review_summary(pulls) - assert len(r["pending_review"]) == 1 - assert r["pending_review"][0]["is_fork"] is True - - def test_empty(self): - r = steps.pr_review_summary([]) - assert r["total"] == 0 and r["merge_rate"] == 0.0 - - -# --------------------------------------------------------------------------- -# 子工作流 3:release-notes -# --------------------------------------------------------------------------- - -class TestReleaseNotes: - def test_groups_by_type(self): - commits = [ - {"message": "feat: 新增登录"}, - {"message": "fix: 修复崩溃"}, - {"message": "feat(api): 新增接口"}, - {"message": "随便写的提交"}, # other,不计入 - ] - r = steps.release_notes(commits, version="v1.0") - assert r["groups"]["feat"] == 2 - assert r["groups"]["fix"] == 1 - assert r["typed_commits"] == 3 - assert "## v1.0" in r["markdown"] - assert "新增登录" in r["markdown"] - - def test_strips_prefix(self): - r = steps.release_notes([{"message": "fix(core): 修复空指针"}]) - assert "修复空指针" in r["markdown"] - assert "fix(core):" not in r["markdown"] - - def test_empty(self): - r = steps.release_notes([]) - assert r["typed_commits"] == 0 - - -# --------------------------------------------------------------------------- -# 复用 Skill 步骤 -# --------------------------------------------------------------------------- - -class TestHealthAndContributors: - def test_health_full(self): - files = ["readme.md", "license", "contributing.md", "code_of_conduct.md"] - r = steps.health_check(files) - assert r["score"] == 100 - assert r["missing"] == [] - - def test_health_partial(self): - r = steps.health_check(["readme.md", "license"]) - assert r["score"] == 50 - assert "CONTRIBUTING" in r["missing"] - - def test_contributors_sorted(self): - contribs = [ - {"login": "a", "contributions": 10}, - {"login": "b", "contributions": 50}, - ] - r = steps.contributor_highlights(contribs) - assert r["top"][0]["name"] == "b" - assert r["total_contributions"] == 60 - - -# --------------------------------------------------------------------------- -# 编排器 + 周报 -# --------------------------------------------------------------------------- - -class FakeClient: - def repo_info(self, o, r): - return {"name": r, "issues_count": 2, "pull_requests_count": 1, - "praises_count": 3, "forked_count": 4} - - def issues(self, o, r, limit=50): - return [{"id": "1", "name": "fix typo in docs", "description": "small"}, - {"id": "2", "name": "登录 crash", "description": "bug"}] - - def pulls(self, o, r, limit=50): - return [{"name": "feat: x", "pull_request_status": 0, "author_login": "dev"}] - - def commits(self, o, r, max_pages=4): - return [{"message": "feat: 新功能"}, {"message": "fix: 修复"}] - - def contributors(self, o, r): - return [{"login": "alice", "contributions": 100}] - - def releases(self, o, r): - return [{"tag_name": "v1.0", "name": "v1.0"}] - - def list_dir(self, o, r, path, ref): - return [{"name": "README.md"}, {"name": "LICENSE"}] - - -class TestOrchestrator: - def test_run_flow_all_steps(self): - result = run_flow("o", "repo", client=FakeClient(), use_cli=False) - assert "step1_triage" in result - assert "step2_pr_review" in result - assert "step3_release_notes" in result - assert "step4_health" in result - assert "step5_contributors" in result - assert "step6_weekly_report" in result - - def test_weekly_report_renders(self): - result = run_flow("o", "repo", client=FakeClient(), use_cli=False) - md = result["step6_weekly_report"] - assert "社区运营周报" in md - assert "Issue 自动分拣" in md - assert "PR Review 汇总" in md - assert "Release Notes" in md - - def test_triage_in_flow(self): - result = run_flow("o", "repo", client=FakeClient(), use_cli=False) - # 一个 good-first(docs typo)+ 一个 bug - assert result["step1_triage"]["good_first_count"] == 1 - - def test_fallback_data_source(self): - # 强制走 glapi 直连时,数据源应标注已回退 - result = run_flow("o", "repo", client=FakeClient(), use_cli=False) - assert "回退" in result["data_source"] - - -class TestCliLayer: - """gitlink-cli 封装层:解包信封与列表提取,不实际调用命令。""" - - def test_extract_list_from_dict(self): - assert cli._extract_list({"issues": [1, 2]}, ("issues",)) == [1, 2] - - def test_extract_list_passthrough(self): - assert cli._extract_list([1, 2], ("issues",)) == [1, 2] - - def test_extract_list_empty(self): - assert cli._extract_list({"other": 1}, ("issues",)) == [] - - def test_cli_available_returns_bool(self): - assert isinstance(cli.cli_available(), bool) - - -if __name__ == "__main__": - sys.exit(pytest.main([__file__, "-v"]))