From 70fbf219e2e506ab17796ddb0f60ed7ae661a992 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sat, 18 Jul 2026 13:18:05 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E4=BF=AE=E5=A4=8D=20+=20pre-commit=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=20+=20IDE=E6=8E=A8=E8=8D=90=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 scripts/ci/auto_fix_formatting.py: CI自动格式化脚本,black/isort失败时自动修复并推送回PR分支 - CI validate job增加 contents: write 权限,支持自动修复后push - 新增 .pre-commit-config.yaml: pre-commit钩子配置(black + isort + ruff) - 新增 .vscode/settings.json: VSCode推荐设置,保存时自动格式化 --- .gitea/workflows/ci-cd.yml | 8 ++ .gitignore | 4 + .pre-commit-config.yaml | 20 +++++ .vscode/settings.json | 12 +++ scripts/ci/auto_fix_formatting.py | 127 ++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+) create mode 100755 .pre-commit-config.yaml create mode 100644 .vscode/settings.json create mode 100644 scripts/ci/auto_fix_formatting.py diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 4e494df1e..bfb65909c 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -62,6 +62,8 @@ jobs: name: Validate Code Quality And Tests runs-on: ci-check timeout-minutes: 10 + permissions: + contents: write env: DATABASE_URL: postgresql+psycopg://postgres:postgres@127.0.0.1:5432/xiaoxia_saas USE_IN_MEMORY_DB: 'false' @@ -129,6 +131,12 @@ jobs: - name: Run code quality checks shell: sh run: "set -eu\n\nif [ \"$SCAN_MODE\" = \"incremental\" ]; then\n echo \"=== Incremental scan mode ===\"\n\n python3 -m compileall -q $CHANGED_PY_FILES\n\n python3 -m black --check --fast $CHANGED_PY_FILES\n\n python3 -m isort --check-only $CHANGED_PY_FILES\n\n RUFF_FILES=$(echo \"$CHANGED_PY_FILES\" | tr ' ' '\\n' | grep -v '^scripts/' | tr '\\n' ' ')\n if [ -n \"$RUFF_FILES\" ]; then\n python3 -m ruff check $RUFF_FILES --statistics\n else\n echo \"No ruff-checkable files changed, skipping\"\n fi\n\nelif [ \"$SCAN_MODE\" = \"skip_py\" ]; then\n echo \"No Python files changed - skipping Python lint checks\"\n\nelse\n echo \"=== Full scan mode ===\"\n\n python3 -m compileall -q alembic apps packages tests scripts\n\n python3 -m black --check --fast alembic apps packages tests scripts\n\n python3 -m isort --check-only alembic apps packages tests scripts\n\n python3 -m ruff check apps packages tests --statistics\nfi\n" + - name: Auto-fix formatting (black + isort) + if: failure() + shell: sh + env: + GITHUB_TOKEN: ${{ github.token }} + run: python3 scripts/ci/auto_fix_formatting.py - name: Type check (mypy, hard gate) shell: sh diff --git a/.gitignore b/.gitignore index d708ecd99..01597f162 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,7 @@ build/ tracker_tasks.json frontend-v21-ui-prototype-final.html + +!.vscode/ +!.vscode/settings.json +.vscode/extensions.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100755 index 000000000..d1218407a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/psf/black + rev: 26.5.1 + hooks: + - id: black + language_version: python3.12 + + - repo: https://github.com/pycqa/isort + rev: 8.0.1 + hooks: + - id: isort + args: ["--profile", "black"] + language_version: python3.12 + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.0 + hooks: + - id: ruff + args: [--fix] + language_version: python3.12 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..bd9401ace --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + }, + "isort.args": ["--profile", "black"], + "python.linting.ruffEnabled": true, + "python.analysis.typeCheckingMode": "basic" +} diff --git a/scripts/ci/auto_fix_formatting.py b/scripts/ci/auto_fix_formatting.py new file mode 100644 index 000000000..d6530fadf --- /dev/null +++ b/scripts/ci/auto_fix_formatting.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""CI中自动修复Python代码格式(black + isort),并推送回PR分支。 + +只在PR事件中执行,避免直接修改主干。 +当code quality检查因格式问题失败时触发,自动修复后push回原分支。 +""" + +import json +import os +import subprocess +import sys +import urllib.request + + +def run(cmd, check=True, capture=True): + """运行shell命令""" + result = subprocess.run(cmd, shell=True, capture_output=capture, text=True) + if check and result.returncode != 0: + print(f"命令失败: {cmd}", file=sys.stderr) + if result.stderr: + print(result.stderr, file=sys.stderr) + sys.exit(1) + return result + + +def get_changed_py_files(pr_number, api_url, token): + """获取PR中变更的Python文件列表""" + url = f"{api_url}/pulls/{pr_number}/files?limit=100" + req = urllib.request.Request(url, headers={"Authorization": f"token {token}"}) + with urllib.request.urlopen(req) as resp: + files = json.loads(resp.read()) + return [f["filename"] for f in files if f["filename"].endswith(".py") and f["status"] != "removed"] + + +def get_pr_head_branch(pr_number, api_url, token): + """获取PR的来源分支名""" + url = f"{api_url}/pulls/{pr_number}" + req = urllib.request.Request(url, headers={"Authorization": f"token {token}"}) + with urllib.request.urlopen(req) as resp: + pr = json.loads(resp.read()) + return pr["head"]["ref"] + + +def main(): + # 只在PR事件中执行 + event_name = os.environ.get("GITHUB_EVENT_NAME", "") + if event_name != "pull_request": + print("非PR事件,跳过自动修复") + return + + github_ref = os.environ.get("GITHUB_REF", "") + pr_number = github_ref.split("/")[2] if github_ref.startswith("refs/pull/") else "" + if not pr_number: + print("无法获取PR号,跳过自动修复") + return + + api_url = os.environ.get("GITHUB_API_URL", "") + repo = os.environ.get("GITHUB_REPOSITORY", "") + token = os.environ.get("GITHUB_TOKEN", "") + scan_mode = os.environ.get("SCAN_MODE", "full") + changed_py_files = os.environ.get("CHANGED_PY_FILES", "") + + if not token: + print("缺少GITHUB_TOKEN,无法推送修复", file=sys.stderr) + sys.exit(1) + + print("=== 检测到代码格式问题,尝试自动修复 ===") + print(f"PR #{pr_number}") + print(f"扫描模式: {scan_mode}") + + # 确定要修复的文件范围 + if scan_mode == "incremental" and changed_py_files: + target_files = changed_py_files.split() + print(f"增量模式,修复 {len(target_files)} 个变更的Python文件") + else: + target_files = ["alembic", "apps", "packages", "tests", "scripts"] + print("全量模式,修复所有Python文件") + + # 自动修复 + target_str = " ".join(target_files) + print() + print("--- black 格式化 ---") + result = run(f"python3 -m black {target_str}", check=False) + print(result.stdout[-500:] if result.stdout else "") + if result.returncode != 0: + print("black执行失败,但继续尝试isort", file=sys.stderr) + + print() + print("--- isort 排序 ---") + result = run(f"python3 -m isort {target_str}", check=False) + print(result.stdout[-500:] if result.stdout else "") + if result.returncode != 0: + print("isort执行失败", file=sys.stderr) + + # 检查是否有改动 + result = run("git status --porcelain") + if not result.stdout.strip(): + print() + print("没有需要提交的格式改动") + return + + print() + print("变更文件:") + for line in result.stdout.strip().split("\n"): + print(f" {line}") + + # 配置git + run('git config user.name "CI Bot"') + run('git config user.email "ci-bot@xiaoxiajianji.com"') + + # 提交修复 + run("git add -A") + run('git commit -m "style: auto-format with black + isort [ci skip]"') + + # 获取来源分支并推送 + head_branch = get_pr_head_branch(pr_number, f"{api_url}/repos/{repo}", token) + print(f"\nPR来源分支: {head_branch}") + + run(f'git push origin "HEAD:{head_branch}"') + + print() + print("✅ 格式已自动修复并推送回分支") + print("新的commit会重新触发CI检查") + + +if __name__ == "__main__": + main() -- 2.54.0