From beeb72a1bd4a313e1986281494777a06937732aa Mon Sep 17 00:00:00 2001 From: Seasons Change <1204992313@qq.com> Date: Fri, 4 Sep 2026 23:42:19 +0800 Subject: [PATCH] chore(governance): TML-815 bootstrap repository gates Multica-Task: TML-815 Multica-Run: 24e7050e-f57e-417e-b577-c1faa8f801ff Co-authored-by: multica-agent --- .git-management.json | 16 ++++++ .github/PULL_REQUEST_TEMPLATE.md | 19 +++++++ .github/scripts/test_validate_pr.py | 83 +++++++++++++++++++++++++++++ .github/scripts/validate_pr.py | 82 ++++++++++++++++++++++++++++ .github/workflows/ci.yml | 69 ++++++++++++++++++++++++ .github/workflows/pr-policy.yml | 30 +++++++++++ 6 files changed, 299 insertions(+) create mode 100644 .git-management.json create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/scripts/test_validate_pr.py create mode 100644 .github/scripts/validate_pr.py create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pr-policy.yml diff --git a/.git-management.json b/.git-management.json new file mode 100644 index 0000000..e13386a --- /dev/null +++ b/.git-management.json @@ -0,0 +1,16 @@ +{ + "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"], + "review_policy": { + "enabled": false, + "required_approvals": 0, + "require_resolved_conversations": false + }, + "require_up_to_date_branch": true, + "merge_method": "squash" +} diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ee7fe2c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ + + + + +## 目标 + +## 变更范围 + +## 验收标准 + +- [ ] + +## 测试结果 + +## 风险和回滚 diff --git a/.github/scripts/test_validate_pr.py b/.github/scripts/test_validate_pr.py new file mode 100644 index 0000000..c2a2fe3 --- /dev/null +++ b/.github/scripts/test_validate_pr.py @@ -0,0 +1,83 @@ +import importlib.util +import unittest +from pathlib import Path + + +MODULE_PATH = Path(__file__).with_name("validate_pr.py") +SPEC = importlib.util.spec_from_file_location("validate_pr", MODULE_PATH) +VALIDATE_PR = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(VALIDATE_PR) + +POLICY = { + "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": "", + "head": {"ref": "feat/example"}, + "base": {"ref": "main"}, + } + } + self.assertEqual([], VALIDATE_PR.validate(event, POLICY)) + + def test_rejects_missing_metadata_and_invalid_branch(self): + event = { + "pull_request": { + "body": "", + "head": {"ref": "random"}, + "base": {"ref": "main"}, + } + } + failures = VALIDATE_PR.validate(event, POLICY) + self.assertIn("branch name does not match policy: random", failures) + 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": "", + "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": "", + "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": "", + "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() diff --git a/.github/scripts/validate_pr.py b/.github/scripts/validate_pr.py new file mode 100644 index 0000000..9252e88 --- /dev/null +++ b/.github/scripts/validate_pr.py @@ -0,0 +1,82 @@ +import argparse +import json +import re +from pathlib import Path + + +def parse_metadata(body): + block = re.search(r"", body or "", re.S | re.I) + if not block: + raise ValueError("PR must contain a multica metadata block") + + fields = {} + for line in block.group(1).splitlines(): + if ":" in line: + key, value = line.split(":", 1) + fields[key.strip()] = value.strip() + return fields + + +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 "" + failures = [] + + if base_ref != policy["default_branch"]: + failures.append( + f"target branch must be {policy['default_branch']}, got {base_ref or ''}" + ) + + if not re.fullmatch(policy["branch_pattern"], head_ref): + failures.append(f"branch name does not match policy: {head_ref or ''}") + + try: + metadata = parse_metadata(body) + except ValueError as exc: + failures.append(str(exc)) + metadata = {} + + for field in policy.get("required_pr_metadata", []): + value = metadata.get(field, "") + 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"(?