diff --git a/README.md b/README.md index 722fb229..f1c540db 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Playbook:工程规范与智能体规则合集,当前覆盖: - `docs/typescript/`:TypeScript 规范(Google 基线、prettier/eslint/vitest) - `docs/markdown/`:Markdown 规范(仅代码格式化) -落地模板:`templates/cpp/`、`templates/python/`、`templates/ci/`。 +落地模板:`templates/cpp/`、`templates/python/`。 详见 `docs/index.md`。 diff --git a/templates/README.md b/templates/README.md index 5bf20902..ce59591a 100644 --- a/templates/README.md +++ b/templates/README.md @@ -149,11 +149,10 @@ templates/ 语言和 CI 配置模板,`install_mode = "snapshot"` 安装快照时会复制这些模板: -- `ci/gitea/`:Gitea Actions 工作流与辅助脚本,部署到快照 `templates/ci/` - `cpp/`:`.clang-format`、`.clangd`、`CMakeLists.txt` 等文件,部署到快照 `templates/cpp/` - `python/`:`pyproject.toml`、`.editorconfig` 等文件,部署到快照 `templates/python/` -**使用方式**:这些模板保留在快照中供参考,需手动复制到项目根目录使用。其中 `ci/gitea/` 应按 `templates/ci/README.md` 的说明,整块复制 `.gitea/` 目录。 +**使用方式**:这些模板保留在快照中供参考,需手动复制到项目根目录使用。 ## 技术细节 diff --git a/templates/ci/README.md b/templates/ci/README.md deleted file mode 100644 index e8ebb24e..00000000 --- a/templates/ci/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# CI 模板(templates/ci) - -本目录提供“目标项目可复制启用”的 CI 模板示例,用于在 CI 中自动化校验部分 Playbook 规范。 - -当前提供: - -- `gitea/`:Gitea Actions(GitHub Actions 语法) - -说明:`templates/ci/gitea/.gitea/` 结构用于与目标项目根目录的 `.gitea/` -保持一致,便于直接复制到项目根目录。 - -## 使用(Gitea Actions) - -前提:目标项目已经把 Playbook 部署到项目内(例如 `docs/standards/playbook/`)。 - -复制到目标项目根目录: - -```sh -cp -R docs/standards/playbook/templates/ci/gitea/.gitea ./ -``` - -提交: - -```sh -git add .gitea -git commit -m ":memo: docs(ci): add standards check workflow" -``` - -## commit message 校验 - -工作流会运行 `.gitea/ci/commit_message_lint.py`: - -- 规范来源(自动探测其一): - - `docs/common/commit_message.md` - - `docs/standards/playbook/docs/common/commit_message.md` -- 默认要求 emoji;如需允许无 emoji:在 workflow 中设置 - `COMMIT_LINT_REQUIRE_EMOJI=0`。 diff --git a/templates/ci/gitea/.gitea/ci/commit_message_lint.py b/templates/ci/gitea/.gitea/ci/commit_message_lint.py deleted file mode 100644 index 6baa5386..00000000 --- a/templates/ci/gitea/.gitea/ci/commit_message_lint.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import json -import os -import pathlib -import re -import subprocess -import sys -from typing import Dict, List, Optional, Tuple - - -def _eprint(*args: object) -> None: - print(*args, file=sys.stderr) - - -def _git(*args: str) -> str: - return subprocess.check_output(["git", *args], text=True).strip() - - -def _repo_root() -> pathlib.Path: - return pathlib.Path(_git("rev-parse", "--show-toplevel")) - - -def _find_commit_spec(root: pathlib.Path) -> pathlib.Path: - candidates = [ - root / "docs" / "common" / "commit_message.md", - root / "docs" / "standards" / "playbook" / "docs" / "common" / "commit_message.md", - ] - for path in candidates: - if path.is_file(): - return path - raise FileNotFoundError( - "commit_message.md not found; expected one of:\n" - + "\n".join(f"- {p}" for p in candidates) - ) - - -def _parse_type_emoji_mapping(md_text: str) -> Dict[str, str]: - mapping: Dict[str, str] = {} - for raw_line in md_text.splitlines(): - line = raw_line.strip() - if not (line.startswith("|") and line.endswith("|")): - continue - if "type" in line and "emoji" in line: - continue - if re.fullmatch(r"\|\s*-+\s*(\|\s*-+\s*)+\|", line): - continue - - cols = [c.strip() for c in line.strip("|").split("|")] - if len(cols) < 2: - continue - - m_type = re.search(r"`([^`]+)`", cols[0]) - m_emoji = re.search(r"`(:[^`]+:)`", cols[1]) - if not m_type or not m_emoji: - continue - - type_name = m_type.group(1).strip() - emoji_code = m_emoji.group(1).strip() - mapping[type_name] = emoji_code - - if not mapping: - raise ValueError("failed to parse type/emoji mapping from commit_message.md") - return mapping - - -def _validate_subject_line( - line: str, - mapping: Dict[str, str], - *, - require_emoji: bool, -) -> Optional[str]: - subject = line.strip() - if not subject: - return "empty subject" - - m = re.match( - r"^(?:(?P:[a-z0-9_+-]+:)\s+)?" - r"(?P[a-z]+)" - r"(?P\([a-z0-9_-]+\))?" - r":\s+(?P.+)$", - subject, - ) - if not m: - return "does not match ':emoji: type(scope): subject' or 'type(scope): subject'" - - emoji = m.group("emoji") - type_name = m.group("type") - text = (m.group("text") or "").rstrip() - - if type_name not in mapping: - return f"unknown type: {type_name}" - - if emoji: - expected = mapping[type_name] - if emoji != expected: - return f"emoji/type mismatch: got {emoji} {type_name}, expected {expected} for type {type_name}" - elif require_emoji: - return "missing emoji (set COMMIT_LINT_REQUIRE_EMOJI=0 to allow)" - - if text.endswith((".", "。")): - return "subject should not end with a period" - - return None - - -def _load_event_payload() -> Tuple[str, Optional[dict]]: - event_name = os.getenv("GITHUB_EVENT_NAME") or os.getenv("GITEA_EVENT_NAME") or "" - event_path = os.getenv("GITHUB_EVENT_PATH") or os.getenv("GITEA_EVENT_PATH") or "" - if not event_path: - return event_name, None - - path = pathlib.Path(event_path) - if not path.is_file(): - return event_name, None - - try: - return event_name, json.loads(path.read_text(encoding="utf-8")) - except Exception as exc: - _eprint(f"WARN: failed to parse event payload: {path} ({exc})") - return event_name, None - - -def _gather_subjects(event_name: str, payload: Optional[dict]) -> List[Tuple[str, str]]: - subjects: List[Tuple[str, str]] = [] - - if isinstance(payload, dict): - if event_name.startswith("pull_request"): - pr = payload.get("pull_request") - if isinstance(pr, dict): - title = (pr.get("title") or "").strip() - if title: - subjects.append(("pull_request.title", title.splitlines()[0].strip())) - - if event_name == "push": - commits = payload.get("commits") - if isinstance(commits, list): - for commit in commits: - if not isinstance(commit, dict): - continue - msg = (commit.get("message") or "").strip() - if not msg: - continue - subject = msg.splitlines()[0].strip() - sha = commit.get("id") or commit.get("sha") or "" - label = f"push.commit {sha[:7]}" if sha else "push.commit" - subjects.append((label, subject)) - - if subjects: - return subjects - - try: - subjects.append(("HEAD", _git("log", "-1", "--format=%s", "HEAD"))) - except Exception: - pass - return subjects - - -def main() -> int: - try: - root = _repo_root() - except Exception as exc: - _eprint(f"ERROR: not a git repository: {exc}") - return 2 - - os.chdir(root) - - require_emoji = os.getenv("COMMIT_LINT_REQUIRE_EMOJI", "1") not in ("0", "false", "False") - - try: - spec_path = _find_commit_spec(root) - except FileNotFoundError as exc: - _eprint(f"ERROR: {exc}") - return 2 - - try: - mapping = _parse_type_emoji_mapping(spec_path.read_text(encoding="utf-8")) - except Exception as exc: - _eprint(f"ERROR: failed to read/parse {spec_path}: {exc}") - return 2 - - event_name, payload = _load_event_payload() - subjects = _gather_subjects(event_name, payload) - - print(f"commit spec: {spec_path}") - if event_name: - print(f"event: {event_name}") - print(f"require emoji: {require_emoji}") - print(f"checks: {len(subjects)} subject(s)") - - errors: List[str] = [] - for label, subject in subjects: - err = _validate_subject_line(subject, mapping, require_emoji=require_emoji) - if err: - errors.append(f"- {label}: {err}\n subject: {subject}") - - if errors: - _eprint("ERROR: commit message lint failed:") - for item in errors: - _eprint(item) - return 1 - - print("OK") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) - diff --git a/templates/ci/gitea/.gitea/workflows/standards-check.yml b/templates/ci/gitea/.gitea/workflows/standards-check.yml deleted file mode 100644 index 4c018272..00000000 --- a/templates/ci/gitea/.gitea/workflows/standards-check.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: ✅ Standards Check - -on: - push: - pull_request: - workflow_dispatch: # 允许手动触发 - -concurrency: - group: standards-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: true - -# ========================================== -# 🔧 配置区域 - 标准校验参数 -# ========================================== -env: - COMMIT_LINT_REQUIRE_EMOJI: "1" - WORKSPACE_DIR: "/home/workspace" - -jobs: - commit-message: - name: 🔍 Commit message lint - runs-on: ubuntu-22.04 - - steps: - - name: 📥 准备仓库 - run: | - echo "========================================" - echo "📥 准备仓库到 WORKSPACE_DIR" - echo "========================================" - - REPO_NAME="${{ github.event.repository.name }}" - TOKEN="${{ secrets.WORKFLOW }}" - mkdir -p "${{ env.WORKSPACE_DIR }}" - REPO_DIR="$(mktemp -d "${{ env.WORKSPACE_DIR }}/${REPO_NAME}.XXXXXX")" - if [ -n "$TOKEN" ]; then - REPO_URL="https://oauth2:${TOKEN}@${GITHUB_SERVER_URL#https://}/${{ github.repository }}.git" - else - REPO_URL="${GITHUB_SERVER_URL}/${{ github.repository }}.git" - fi - - git clone "$REPO_URL" "$REPO_DIR" - - TARGET_SHA="${{ github.sha }}" - TARGET_REF="${{ github.ref }}" - if git -C "$REPO_DIR" cat-file -e "$TARGET_SHA^{commit}" 2>/dev/null; then - git -C "$REPO_DIR" checkout -f "$TARGET_SHA" - else - if [ -n "$TARGET_REF" ]; then - git -C "$REPO_DIR" fetch origin "$TARGET_REF" - git -C "$REPO_DIR" checkout -f FETCH_HEAD - else - git -C "$REPO_DIR" checkout -f "${{ github.ref_name }}" - fi - fi - - git config --global --add safe.directory "$REPO_DIR" - echo "REPO_DIR=$REPO_DIR" >> "$GITHUB_ENV" - - name: 🧪 Lint commit message / PR title - run: | - cd "$REPO_DIR" - python3 .gitea/ci/commit_message_lint.py - - - name: 🧹 清理临时仓库 - if: always() - run: | - rm -rf "$REPO_DIR"