ci: 纳入 Prettier 到两层防御体系(pre-commit + CI auto-fix + VSCode)
- pre-commit: 新增 prettier hook,覆盖前端文件 - CI auto_fix: 增加 prettier 自动修复(增量/全量模式) - VSCode: 前端文件保存自动格式化 - 新增 apps/web/.prettierrc.json 配置 - 新增 apps/web/.prettierignore 忽略配置 - package.json 增加 format / format:check 脚本
This commit is contained in:
@@ -18,3 +18,12 @@ repos:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
language_version: python3.12
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.11
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: prettier (frontend)
|
||||
files: ^apps/web/.*\.(ts|tsx|js|jsx|css|scss|less|json|html|md|yaml|yml)$
|
||||
additional_dependencies:
|
||||
- prettier@3.4.2
|
||||
|
||||
+37
@@ -6,6 +6,43 @@
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"prettier.requireConfig": true,
|
||||
"isort.args": ["--profile", "black"],
|
||||
"python.linting.ruffEnabled": true,
|
||||
"python.analysis.typeCheckingMode": "basic"
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false
|
||||
}
|
||||
@@ -4,16 +4,18 @@
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"dev": "vite",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ci": "npx playwright test --project=chromium --reporter=line",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"test:ui": "vitest --ui",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
Regular → Executable
+101
-27
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CI中自动修复Python代码格式(black + isort),并推送回PR分支。
|
||||
"""CI中自动修复代码格式(Python: black + isort | Frontend: prettier),并推送回PR分支。
|
||||
|
||||
只在PR事件中执行,避免直接修改主干。
|
||||
当code quality检查因格式问题失败时触发,自动修复后push回原分支。
|
||||
@@ -12,9 +12,9 @@ import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
def run(cmd, check=True, capture=True):
|
||||
def run(cmd, check=True, capture=True, cwd=None):
|
||||
"""运行shell命令"""
|
||||
result = subprocess.run(cmd, shell=True, capture_output=capture, text=True)
|
||||
result = subprocess.run(cmd, shell=True, capture_output=capture, text=True, cwd=cwd)
|
||||
if check and result.returncode != 0:
|
||||
print(f"命令失败: {cmd}", file=sys.stderr)
|
||||
if result.stderr:
|
||||
@@ -23,13 +23,13 @@ def run(cmd, check=True, capture=True):
|
||||
return result
|
||||
|
||||
|
||||
def get_changed_py_files(pr_number, api_url, token):
|
||||
"""获取PR中变更的Python文件列表"""
|
||||
def get_changed_files(pr_number, api_url, token):
|
||||
"""获取PR中变更的文件列表"""
|
||||
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"]
|
||||
return [f["filename"] for f in files if f["status"] != "removed"]
|
||||
|
||||
|
||||
def get_pr_head_branch(pr_number, api_url, token):
|
||||
@@ -41,6 +41,70 @@ def get_pr_head_branch(pr_number, api_url, token):
|
||||
return pr["head"]["ref"]
|
||||
|
||||
|
||||
def fix_python(target_py_files, scan_mode):
|
||||
"""修复 Python 文件格式 (black + isort)"""
|
||||
if not target_py_files:
|
||||
print("没有需要修复的 Python 文件,跳过")
|
||||
return
|
||||
|
||||
target_str = " ".join(target_py_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)
|
||||
|
||||
|
||||
def fix_frontend(target_fe_files, scan_mode, repo_root):
|
||||
"""修复前端文件格式 (prettier)"""
|
||||
if not target_fe_files:
|
||||
print("没有需要修复的前端文件,跳过")
|
||||
return
|
||||
|
||||
# 检查 prettier 是否可用
|
||||
web_dir = os.path.join(repo_root, "apps", "web")
|
||||
prettier_bin = os.path.join(web_dir, "node_modules", ".bin", "prettier")
|
||||
|
||||
if not os.path.exists(prettier_bin):
|
||||
print()
|
||||
print("--- 安装前端依赖 (prettier) ---")
|
||||
result = run("npm install --no-audit --no-fund --prefer-offline", check=False, cwd=web_dir)
|
||||
if result.returncode != 0:
|
||||
print("npm install 失败,跳过 prettier 修复", file=sys.stderr)
|
||||
return
|
||||
print("依赖安装完成")
|
||||
|
||||
if not os.path.exists(prettier_bin):
|
||||
print("prettier 仍不可用,跳过", file=sys.stderr)
|
||||
return
|
||||
|
||||
print()
|
||||
print("--- prettier 格式化 ---")
|
||||
|
||||
if scan_mode == "incremental":
|
||||
# 增量模式:只格式化变更的前端文件
|
||||
# 转换为相对于 apps/web 的路径或用绝对路径
|
||||
target_str = " ".join(target_fe_files)
|
||||
# 从项目根目录运行,prettier 会找配置文件
|
||||
cmd = f"{prettier_bin} --write {target_str}"
|
||||
else:
|
||||
# 全量模式:格式化整个前端目录
|
||||
cmd = f"{prettier_bin} --write ."
|
||||
|
||||
result = run(cmd, check=False, cwd=web_dir if scan_mode != "incremental" else repo_root)
|
||||
print(result.stdout[-800:] if result.stdout else "")
|
||||
if result.stderr:
|
||||
print(result.stderr[-500:], file=sys.stderr)
|
||||
|
||||
|
||||
def main():
|
||||
# 只在PR事件中执行
|
||||
event_name = os.environ.get("GITHUB_EVENT_NAME", "")
|
||||
@@ -58,39 +122,49 @@ def main():
|
||||
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", "")
|
||||
changed_files_env = os.environ.get("CHANGED_FILES", "")
|
||||
|
||||
if not token:
|
||||
print("缺少GITHUB_TOKEN,无法推送修复", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
repo_root = os.getcwd()
|
||||
|
||||
print("=== 检测到代码格式问题,尝试自动修复 ===")
|
||||
print(f"PR #{pr_number}")
|
||||
print(f"扫描模式: {scan_mode}")
|
||||
|
||||
# 前端文件扩展名
|
||||
fe_extensions = (
|
||||
".ts", ".tsx", ".js", ".jsx",
|
||||
".css", ".scss", ".less",
|
||||
".json", ".html", ".md",
|
||||
".yaml", ".yml",
|
||||
)
|
||||
# Python 文件扩展名
|
||||
py_extensions = (".py",)
|
||||
|
||||
# 确定要修复的文件范围
|
||||
if scan_mode == "incremental" and changed_py_files:
|
||||
target_files = changed_py_files.split()
|
||||
print(f"增量模式,修复 {len(target_files)} 个变更的Python文件")
|
||||
if scan_mode == "incremental" and changed_files_env:
|
||||
all_changed = changed_files_env.split()
|
||||
target_py_files = [f for f in all_changed if f.endswith(py_extensions)]
|
||||
target_fe_files = [f for f in all_changed if f.endswith(fe_extensions)]
|
||||
print(f"增量模式: {len(target_py_files)} 个Python文件, {len(target_fe_files)} 个前端文件")
|
||||
else:
|
||||
target_files = ["alembic", "apps", "packages", "tests", "scripts"]
|
||||
print("全量模式,修复所有Python文件")
|
||||
target_py_files = ["alembic", "apps", "packages", "tests", "scripts"]
|
||||
# 全量模式下 prettier 在前端目录内部运行,无需传文件列表
|
||||
target_fe_files = ["apps/web"] # 标记为有前端文件需要处理
|
||||
print("全量模式,修复所有文件")
|
||||
|
||||
# 自动修复
|
||||
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)
|
||||
# Python 格式化
|
||||
fix_python(target_py_files, scan_mode)
|
||||
|
||||
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)
|
||||
# 前端格式化
|
||||
# 全量模式下直接传 web 目录标记
|
||||
if scan_mode != "incremental":
|
||||
fix_frontend(["apps/web"], scan_mode, repo_root)
|
||||
else:
|
||||
fix_frontend(target_fe_files, scan_mode, repo_root)
|
||||
|
||||
# 检查是否有改动
|
||||
result = run("git status --porcelain")
|
||||
@@ -110,7 +184,7 @@ def main():
|
||||
|
||||
# 提交修复
|
||||
run("git add -A")
|
||||
run('git commit -m "style: auto-format with black + isort [ci skip]"')
|
||||
run('git commit -m "style: auto-format with black + isort + prettier [ci skip]"')
|
||||
|
||||
# 获取来源分支并推送
|
||||
head_branch = get_pr_head_branch(pr_number, f"{api_url}/repos/{repo}", token)
|
||||
|
||||
Reference in New Issue
Block a user