Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .git-management.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
{
"policy_version": 1,
"policy_version": 3,
"default_branch": "main",
"branch_pattern": "^(ai|feat|fix|refactor|chore)/.+$",
"required_pr_metadata": ["task_id", "run_id"],
"task_id_pattern": "^[A-Z][A-Z0-9]*-[0-9]+$",
"require_task_id_in_pr_title": true,
"required_checks": ["ci", "git-governance"],
"required_approvals": 1,
"require_resolved_conversations": true,
"review_policy": {
"enabled": false,
"required_approvals": 0,
"require_resolved_conversations": false
},
"require_up_to_date_branch": true,
"merge_method": "squash"
}
2 changes: 2 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ run_id: <必填运行标识>
change_id: <可选变更标识>
-->

<!-- PR 标题必须包含与上方 task_id 完全相同的完整任务号。 -->

## 目标

## 变更范围
Expand Down
39 changes: 39 additions & 0 deletions .github/scripts/test_validate_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
"default_branch": "main",
"branch_pattern": r"^(ai|feat|fix|refactor|chore)/.+$",
"required_pr_metadata": ["task_id", "run_id"],
"task_id_pattern": r"^[A-Z][A-Z0-9]*-[0-9]+$",
"require_task_id_in_pr_title": True,
}


class ValidatePullRequestTest(unittest.TestCase):
def test_accepts_valid_pull_request(self):
event = {
"pull_request": {
"title": "feat(example): MT-1 add example",
"body": "<!-- multica\ntask_id: MT-1\nrun_id: RUN-1\n-->",
"head": {"ref": "feat/example"},
"base": {"ref": "main"},
Expand All @@ -39,6 +42,42 @@ def test_rejects_missing_metadata_and_invalid_branch(self):
self.assertIn("missing PR metadata: task_id", failures)
self.assertIn("missing PR metadata: run_id", failures)

def test_rejects_title_without_task_id(self):
event = {
"pull_request": {
"title": "feat(home): add team homepage",
"body": "<!-- multica\ntask_id: TML-741\nrun_id: RUN-1\n-->",
"head": {"ref": "feat/home"},
"base": {"ref": "main"},
}
}
failures = VALIDATE_PR.validate(event, POLICY)
self.assertIn("PR title must contain task_id: TML-741", failures)

def test_rejects_title_with_different_task_id(self):
event = {
"pull_request": {
"title": "feat(home): TML-742 add team homepage",
"body": "<!-- multica\ntask_id: TML-741\nrun_id: RUN-1\n-->",
"head": {"ref": "feat/home"},
"base": {"ref": "main"},
}
}
failures = VALIDATE_PR.validate(event, POLICY)
self.assertIn("PR title must contain task_id: TML-741", failures)

def test_rejects_task_id_prefix_match(self):
event = {
"pull_request": {
"title": "feat(home): TML-7410 add team homepage",
"body": "<!-- multica\ntask_id: TML-741\nrun_id: RUN-1\n-->",
"head": {"ref": "feat/home"},
"base": {"ref": "main"},
}
}
failures = VALIDATE_PR.validate(event, POLICY)
self.assertIn("PR title must contain task_id: TML-741", failures)


if __name__ == "__main__":
unittest.main()
14 changes: 14 additions & 0 deletions .github/scripts/validate_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def parse_metadata(body):

def validate(event, policy):
pull_request = event.get("pull_request") or {}
title = pull_request.get("title") or ""
body = pull_request.get("body") or ""
head_ref = pull_request.get("head", {}).get("ref") or ""
base_ref = pull_request.get("base", {}).get("ref") or ""
Expand All @@ -43,6 +44,19 @@ def validate(event, policy):
if not value or value.startswith("<"):
failures.append(f"missing PR metadata: {field}")

task_id = metadata.get("task_id", "")
task_id_is_present = bool(task_id) and not task_id.startswith("<")
task_id_is_valid = task_id_is_present
task_id_pattern = policy.get("task_id_pattern")
if task_id_is_present and task_id_pattern and not re.fullmatch(task_id_pattern, task_id):
failures.append(f"invalid PR metadata: task_id: {task_id}")
task_id_is_valid = False

if policy.get("require_task_id_in_pr_title") and task_id_is_valid:
exact_task_id = rf"(?<![A-Za-z0-9]){re.escape(task_id)}(?![A-Za-z0-9])"
if not re.search(exact_task_id, title):
failures.append(f"PR title must contain task_id: {task_id}")

return failures


Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@

## 文件说明

- `.git-management.json`:仓库治理策略,定义允许的分支格式、必需检查、审批数量和合并方式
- `.git-management.json`:仓库治理策略,定义分支格式、必需检查、可选 Review 和合并方式
- `.github/workflows/ci.yml`:通用项目检查,支持 Makefile、Go、Bun 和 npm 项目。
- `.github/workflows/pr-policy.yml`:运行 `git-governance` 门禁。
- `.github/scripts/validate_pr.py`:校验目标分支、分支名和 PR 元数据。
- `.github/workflows/pr-policy.yml`:运行原生 `git-governance` 门禁。
- `.github/scripts/validate_pr.py`:校验目标分支、分支名、PR 标题任务号和 PR 元数据。
- `.github/scripts/test_validate_pr.py`:治理校验器的单元测试。
- `.github/PULL_REQUEST_TEMPLATE.md`:开发 Agent 创建 PR 时使用的统一模板。
- `.github/CODEOWNERS`:项目关键路径的 Review 所有者配置
- `.github/CODEOWNERS`:为以后启用 Review 保留;初期不要求 Code Owner Review

## 使用边界
## 默认策略

开发 Agent 负责创建分支、提交代码和创建 PR。治理 Agent 只读取 PR 状态并发布 `git-governance` 检查结果。
- Required Checks:`ci`、`git-governance`。
- PR 标题:必须包含与正文元数据完全相同的完整 `task_id`。
- Review:通过 `review_policy.enabled: false` 关闭。
- 合并方式:Squash。

从本模板创建仓库后,使用 Git 治理工具包中的 `bootstrap-repository.ps1` 配置分支保护。
开发团队负责创建分支、Commit 和 PR。仓库内 GitHub Actions 负责原生门禁;接入外部 PR 检查 Agent 后,Agent 只检查状态、维护一条 PR 规范检查评论,并定向通知创建 PR 的开发 Agent。

从本模板创建仓库后,由 Git 治理管理员使用工具包中的 `bootstrap-repository.ps1` 配置分支保护。治理 Agent 不执行初始化。
Loading