gitlink-cli/examples/workflows/gitlink-flow/src/flow.py

190 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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_dirgitlink-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())