204 lines
6.4 KiB
Python
204 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
发版前预检查工具
|
||
|
||
检查内容:
|
||
1. 当前版本号是否相对最近 tag 有变化
|
||
2. 本次热更新清单中新增/修改/删除了哪些文件
|
||
3. 是否存在重命名、删除或未跟踪但会被纳入热更新的文件
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Tuple
|
||
|
||
from generate_update_manifest import (
|
||
generate_manifest,
|
||
get_previous_release_tag,
|
||
load_manifest_from_tag,
|
||
read_repo_config,
|
||
read_version,
|
||
run_git_command,
|
||
)
|
||
|
||
|
||
def get_latest_tag(base_dir: Path) -> str:
|
||
"""获取仓库中最新的版本 tag"""
|
||
try:
|
||
output = run_git_command(base_dir, ['tag', '--list', 'v*', '--sort=-version:refname'])
|
||
except Exception:
|
||
return ""
|
||
|
||
for raw_tag in output.splitlines():
|
||
tag = raw_tag.strip()
|
||
if tag:
|
||
return tag
|
||
|
||
return ""
|
||
|
||
|
||
def build_file_map(manifest: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
||
"""将 manifest 文件列表转换成路径索引"""
|
||
return {entry['path']: entry for entry in manifest.get('files', [])}
|
||
|
||
|
||
def get_manifest_diff(
|
||
current_manifest: Dict[str, Any],
|
||
previous_manifest: Dict[str, Any] | None,
|
||
) -> Tuple[List[str], List[str], List[str]]:
|
||
"""比较当前 manifest 与上一个版本 manifest 的差异"""
|
||
current_files = build_file_map(current_manifest)
|
||
previous_files = build_file_map(previous_manifest or {})
|
||
|
||
new_files = sorted(path for path in current_files if path not in previous_files)
|
||
changed_files = sorted(
|
||
path
|
||
for path, file_info in current_files.items()
|
||
if path in previous_files and previous_files[path].get('md5') != file_info.get('md5')
|
||
)
|
||
previous_deleted = {
|
||
entry['path']
|
||
for entry in (previous_manifest or {}).get('deleted_files', [])
|
||
if entry.get('path')
|
||
}
|
||
current_deleted = {
|
||
entry['path']
|
||
for entry in current_manifest.get('deleted_files', [])
|
||
if entry.get('path')
|
||
}
|
||
deleted_files = sorted(current_deleted - previous_deleted)
|
||
|
||
return new_files, changed_files, deleted_files
|
||
|
||
|
||
def get_git_name_status(base_dir: Path, base_ref: str) -> List[Tuple[str, List[str]]]:
|
||
"""读取 git diff 的 name-status 结果"""
|
||
if not base_ref:
|
||
return []
|
||
|
||
try:
|
||
output = run_git_command(base_dir, ['diff', '--name-status', '--find-renames=90%', base_ref])
|
||
except Exception:
|
||
return []
|
||
|
||
results: List[Tuple[str, List[str]]] = []
|
||
for raw_line in output.splitlines():
|
||
line = raw_line.strip()
|
||
if not line:
|
||
continue
|
||
|
||
parts = line.split('\t')
|
||
if not parts:
|
||
continue
|
||
|
||
status = parts[0]
|
||
paths = parts[1:]
|
||
results.append((status, paths))
|
||
|
||
return results
|
||
|
||
|
||
def get_untracked_files(base_dir: Path) -> List[str]:
|
||
"""获取未跟踪文件列表"""
|
||
try:
|
||
output = run_git_command(base_dir, ['ls-files', '--others', '--exclude-standard'])
|
||
except Exception:
|
||
return []
|
||
|
||
return sorted(line.strip() for line in output.splitlines() if line.strip())
|
||
|
||
|
||
def print_section(title: str, items: List[str], limit: int = 15):
|
||
"""打印列表区块"""
|
||
print(f"\n[{title}] {len(items)}")
|
||
if not items:
|
||
print(" - 无")
|
||
return
|
||
|
||
for item in items[:limit]:
|
||
print(f" - {item}")
|
||
|
||
if len(items) > limit:
|
||
print(f" - ... 还有 {len(items) - limit} 项")
|
||
|
||
|
||
def main() -> int:
|
||
base_dir = Path(__file__).parent
|
||
current_version = read_version(base_dir)
|
||
latest_tag = get_latest_tag(base_dir)
|
||
previous_tag = get_previous_release_tag(base_dir, current_version)
|
||
previous_manifest = load_manifest_from_tag(base_dir, previous_tag)
|
||
owner, repo = read_repo_config()
|
||
current_manifest = generate_manifest(
|
||
base_dir,
|
||
version=current_version,
|
||
owner=owner,
|
||
repo=repo,
|
||
previous_manifest=previous_manifest,
|
||
)
|
||
|
||
new_files, changed_files, deleted_files = get_manifest_diff(current_manifest, previous_manifest)
|
||
git_name_status = get_git_name_status(base_dir, latest_tag)
|
||
renamed_files = [
|
||
f"{paths[0]} -> {paths[1]}"
|
||
for status, paths in git_name_status
|
||
if status.startswith('R') and len(paths) == 2
|
||
]
|
||
removed_files = [paths[0] for status, paths in git_name_status if status == 'D' and paths]
|
||
untracked_files = get_untracked_files(base_dir)
|
||
tracked_paths = set(build_file_map(current_manifest))
|
||
untracked_updatable_files = sorted(path for path in untracked_files if path in tracked_paths)
|
||
|
||
version_unchanged = bool(latest_tag and current_version == latest_tag)
|
||
has_manifest_changes = bool(new_files or changed_files or deleted_files)
|
||
|
||
print("发版预检查")
|
||
print("=" * 60)
|
||
print(f"当前版本: {current_version}")
|
||
print(f"最新 tag: {latest_tag or '无'}")
|
||
print(f"用于对比的上一版本 tag: {previous_tag or '无'}")
|
||
print(f"本次将纳入热更新的文件数: {len(current_manifest.get('files', []))}")
|
||
print(f"累计待删除文件数: {len(current_manifest.get('deleted_files', []))}")
|
||
|
||
print_section('新增热更新文件', new_files)
|
||
print_section('修改的热更新文件', changed_files)
|
||
print_section('待删除的旧文件', deleted_files)
|
||
print_section('git 检测到的重命名', renamed_files)
|
||
print_section('git 检测到的删除', removed_files)
|
||
print_section('未跟踪但会纳入热更新的文件', untracked_updatable_files)
|
||
|
||
warnings: List[str] = []
|
||
blocked: List[str] = []
|
||
|
||
if version_unchanged and has_manifest_changes:
|
||
blocked.append(
|
||
f"检测到可热更新内容发生变化,但 static/version.txt 仍是 {current_version}。"
|
||
)
|
||
|
||
if renamed_files:
|
||
warnings.append("存在重命名文件。热更新虽然可新增新文件,但旧文件清理仍需确认。")
|
||
|
||
if deleted_files:
|
||
warnings.append("存在待删除文件。请确认本次删除符合预期。")
|
||
|
||
if untracked_updatable_files:
|
||
warnings.append("存在未跟踪但会纳入热更新规则的文件。若不提交到 Git,Action 不会包含它们。")
|
||
|
||
print("\n[结论]")
|
||
if blocked:
|
||
for item in blocked:
|
||
print(f" - 阻塞: {item}")
|
||
if warnings:
|
||
for item in warnings:
|
||
print(f" - 提示: {item}")
|
||
if not blocked and not warnings:
|
||
print(" - 可以直接发版")
|
||
|
||
return 1 if blocked else 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|