1e16e81344
CI/CD Pipeline / Check if frontend-only change (push) Waiting to run
CI/CD Pipeline / Validate - Code Quality (push) Waiting to run
CI/CD Pipeline / Validate - Type Check (mypy) (push) Waiting to run
CI/CD Pipeline / Validate - Migration (alembic) (push) Waiting to run
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Waiting to run
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Waiting to run
CI/CD Pipeline / PR Build Web Image (push) Waiting to run
CI/CD Pipeline / PR Build Worker Image (push) Waiting to run
CI/CD Pipeline / Build Staging API Image (push) Waiting to run
CI/CD Pipeline / Build Staging Web Image (push) Waiting to run
CI/CD Pipeline / Build Staging Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Waiting to run
CI/CD Pipeline / Build Production Web Image (push) Waiting to run
CI/CD Pipeline / Build Production Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
#451 数据库迁移检查升级 新增5项完整检查: 1. 命名规范检查(文件名格式/编号连续/revision一致性) 2. 链完整性检查(revision/down_revision链) 3. upgrade head(原有逻辑) 4. downgrade -1 + re-upgrade(双向一致性) 5. alembic check(未生成migration的model变更检测,警告模式)
138 lines
4.7 KiB
Python
Executable File
138 lines
4.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
检查 Alembic migration 文件命名规范。
|
||
|
||
规则:
|
||
1. 文件名必须以数字前缀开头(3位补零),如 001_xxx.py、052_add_table.py
|
||
2. 数字前缀必须连续递增(与 check_migration_chain.py 一致,但只看文件名)
|
||
3. 数字前缀后必须跟有描述性后缀(不能只有数字)
|
||
4. 文件名使用小写+下划线(snake_case)
|
||
5. revision 变量值必须与文件名数字前缀一致(可选带描述后缀)
|
||
|
||
用法:
|
||
python3 scripts/ci/check_migration_naming.py [alembic_versions_dir]
|
||
|
||
默认目录: alembic/versions/
|
||
|
||
退出码:
|
||
0 - 全部通过
|
||
1 - 有命名违规
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# 文件名格式: 3位数字_描述.py
|
||
FILE_NAME_PATTERN = re.compile(r"^(\d{3})_[a-z][a-z0-9_]*\.py$")
|
||
# 纯数字文件名(不允许)
|
||
PURE_NUM_PATTERN = re.compile(r"^\d{3}\.py$")
|
||
# revision 值的数字前缀
|
||
REV_NUM_PATTERN = re.compile(r"^(\d{3})")
|
||
# revision 变量行
|
||
REV_LINE_PATTERN = re.compile(
|
||
r'^\s*revision\s*(?::\s*str\s*)?=\s*["\']([^"\']+)["\']',
|
||
re.MULTILINE,
|
||
)
|
||
|
||
|
||
def check_naming(versions_dir: Path) -> list[str]:
|
||
"""检查 migration 文件命名,返回错误列表。"""
|
||
errors: list[str] = []
|
||
|
||
if not versions_dir.is_dir():
|
||
return [f"目录不存在: {versions_dir}"]
|
||
|
||
py_files = sorted(f for f in versions_dir.iterdir() if f.suffix == ".py")
|
||
if not py_files:
|
||
return [f"目录下没有 migration 文件: {versions_dir}"]
|
||
|
||
print(f"检查 migration 文件命名: {versions_dir}")
|
||
print(f"共 {len(py_files)} 个文件")
|
||
print()
|
||
|
||
# 1. 文件名格式检查
|
||
print("1. 文件名格式检查...")
|
||
file_nums: list[int] = []
|
||
for f in py_files:
|
||
name = f.name
|
||
if PURE_NUM_PATTERN.match(name):
|
||
errors.append(f" ❌ {name}: 只有数字编号,缺少描述性后缀")
|
||
continue
|
||
m = FILE_NAME_PATTERN.match(name)
|
||
if not m:
|
||
errors.append(f" ❌ {name}: 命名格式不规范,应为 NNN_description.py " f"(3位数字前缀+下划线+小写描述)")
|
||
continue
|
||
file_nums.append(int(m.group(1)))
|
||
|
||
if not any("命名格式不规范" in e or "缺少描述性后缀" in e for e in errors):
|
||
print(f" ✅ 全部 {len(py_files)} 个文件名格式正确")
|
||
else:
|
||
for e in errors:
|
||
if "命名格式不规范" in e or "缺少描述性后缀" in e:
|
||
print(e)
|
||
|
||
# 2. 编号连续性检查(基于文件名数字前缀)
|
||
print()
|
||
print("2. 编号连续性检查...")
|
||
if file_nums:
|
||
expected = set(range(min(file_nums), max(file_nums) + 1))
|
||
actual = set(file_nums)
|
||
missing = sorted(expected - actual)
|
||
if missing:
|
||
errors.append(f" ❌ 编号不连续,缺少: {', '.join(f'{n:03d}' for n in missing)}")
|
||
print(f" ❌ 编号不连续,缺少 {len(missing)} 个: " f"{', '.join(f'{n:03d}' for n in missing)}")
|
||
else:
|
||
print(f" ✅ 编号连续({min(file_nums):03d} ~ {max(file_nums):03d})")
|
||
|
||
# 3. revision 变量与文件名前缀一致性检查
|
||
print()
|
||
print("3. revision变量与文件名一致性检查...")
|
||
rev_mismatch = 0
|
||
for f in py_files:
|
||
m = FILE_NAME_PATTERN.match(f.name)
|
||
if not m:
|
||
continue # 格式不对的已经报过了
|
||
file_num = m.group(1)
|
||
content = f.read_text(encoding="utf-8")
|
||
rev_match = REV_LINE_PATTERN.search(content)
|
||
if not rev_match:
|
||
errors.append(f" ❌ {f.name}: 未找到 revision 变量定义")
|
||
rev_mismatch += 1
|
||
continue
|
||
rev_value = rev_match.group(1)
|
||
rev_num_match = REV_NUM_PATTERN.match(rev_value)
|
||
if not rev_num_match or rev_num_match.group(1) != file_num:
|
||
errors.append(f" ❌ {f.name}: revision='{rev_value}' 与文件名前缀 {file_num} 不一致")
|
||
rev_mismatch += 1
|
||
|
||
if rev_mismatch == 0:
|
||
print(f" ✅ 全部 {len(py_files)} 个文件的 revision 与文件名一致")
|
||
|
||
return errors
|
||
|
||
|
||
def main() -> int:
|
||
versions_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("alembic/versions")
|
||
|
||
errors = check_naming(versions_dir)
|
||
|
||
print()
|
||
if errors:
|
||
print(f"❌ 发现 {len(errors)} 个命名问题")
|
||
print()
|
||
print("命名规范:")
|
||
print(" - 文件名格式: NNN_description.py(3位数字前缀 + 下划线 + 小写描述)")
|
||
print(" - 编号必须连续,不能跳号")
|
||
print(" - revision 变量的数字前缀必须与文件名一致")
|
||
return 1
|
||
|
||
print("✅ 所有 migration 文件命名规范检查通过")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|