Initial panel project deploy skill snapshot
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
交付压缩包构建脚本(合同对齐)。
|
||||
|
||||
产物(位于 --out-dir,默认 .Codex/delivery/):
|
||||
1) <zip-name> 给甲方/终端使用的交付压缩包
|
||||
2) 交付清单.md 与 zip 并列的单独文件(不打入包内)
|
||||
|
||||
zip 内部固定结构:
|
||||
README.md 包根目录唯一一份 README
|
||||
docs/screenshots/ README 引用的全部配图(解压可显示)
|
||||
截图/界面截图/ 当前产品界面截图(中性文件名)
|
||||
录屏/
|
||||
文档/环境依赖清单.md
|
||||
文档/部署注意事项.md
|
||||
|
||||
用法见 --help。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def copy_file(src: Path, dst: Path):
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="构建交付压缩包与并列交付清单")
|
||||
ap.add_argument("--project-dir", required=True)
|
||||
ap.add_argument("--readme", default="README.md")
|
||||
ap.add_argument("--out-dir", default=".Codex/delivery")
|
||||
ap.add_argument("--assets-product", default=".Codex/delivery/assets/product")
|
||||
ap.add_argument("--recordings", default=".Codex/delivery/recordings")
|
||||
ap.add_argument("--dep-doc", default=".Codex/delivery/环境依赖清单.md")
|
||||
ap.add_argument("--notes-doc", default=".Codex/delivery/部署注意事项.md")
|
||||
ap.add_argument("--manifest-src", default=".Codex/delivery/manifest.md")
|
||||
ap.add_argument("--zip-name", default=None, help="默认 smart-parking-delivery-<YYYYMMDD>.zip")
|
||||
ap.add_argument("--video-name", default="full-feature-demo.mp4")
|
||||
args = ap.parse_args()
|
||||
|
||||
root = Path(args.project_dir).resolve()
|
||||
out_dir = root / args.out_dir
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
readme_path = root / args.readme
|
||||
if not readme_path.exists():
|
||||
print("[FAIL] README 不存在:%s" % readme_path)
|
||||
return 1
|
||||
|
||||
zip_name = args.zip_name or ("smart-parking-delivery-%s.zip" % _today())
|
||||
zip_path = out_dir / zip_name
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
stage = Path(td)
|
||||
|
||||
# 1) README(包根唯一一份)
|
||||
copy_file(readme_path, stage / "README.md")
|
||||
|
||||
# 2) README 引用图片 -> docs/...(保持相对路径)
|
||||
readme_text = readme_path.read_text(encoding="utf-8", errors="ignore")
|
||||
img_refs = re.findall(r"!\[[^\]]*\]\(([^)]+)\)", readme_text)
|
||||
copied_img = set()
|
||||
for ref in img_refs:
|
||||
ref = ref.strip()
|
||||
if "://" in ref or ref.startswith("#"):
|
||||
continue
|
||||
src_img = (readme_path.parent / ref).resolve()
|
||||
if not src_img.exists():
|
||||
print("[FAIL] README 引用图片缺失:%s" % ref)
|
||||
return 1
|
||||
rel = src_img.relative_to(root)
|
||||
if str(rel) not in copied_img:
|
||||
copy_file(src_img, stage / rel)
|
||||
copied_img.add(str(rel))
|
||||
|
||||
# 3) 产品界面截图(中性命名,不体现版本对比或来源宣传)
|
||||
product_assets = root / args.assets_product
|
||||
if product_assets.is_dir():
|
||||
shutil.copytree(product_assets, stage / "截图" / "界面截图", dirs_exist_ok=True)
|
||||
|
||||
# 4) 录屏(只取指定 mp4)
|
||||
rec_dir = root / args.recordings
|
||||
video = rec_dir / args.video_name
|
||||
if video.exists():
|
||||
copy_file(video, stage / "录屏" / args.video_name)
|
||||
|
||||
# 5) 文档目录:仅依赖清单 + 注意事项(无 README、无交付清单)
|
||||
for d in (args.dep_doc, args.notes_doc):
|
||||
p = root / d
|
||||
if p.exists():
|
||||
copy_file(p, stage / "文档" / p.name)
|
||||
|
||||
# 打包
|
||||
if zip_path.exists():
|
||||
zip_path.unlink()
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z:
|
||||
for p in sorted(stage.rglob("*")):
|
||||
if p.is_file():
|
||||
z.write(p, p.relative_to(stage).as_posix())
|
||||
|
||||
# 交付清单:复制为与 zip 并列的单独文件
|
||||
manifest_src = root / args.manifest_src
|
||||
checklist_path = out_dir / "交付清单.md"
|
||||
if manifest_src.exists():
|
||||
shutil.copy2(manifest_src, checklist_path)
|
||||
|
||||
# 校验:解压核对 README 图片
|
||||
bad = _verify_zip(zip_path)
|
||||
if bad:
|
||||
print("[FAIL] 交付包校验未通过:%s" % bad)
|
||||
return 1
|
||||
|
||||
print("交付包:%s (%.2f MB)" % (zip_path, zip_path.stat().st_size/1024/1024))
|
||||
if checklist_path.exists():
|
||||
print("并列交付清单:%s" % checklist_path)
|
||||
return 0
|
||||
|
||||
|
||||
def _today() -> str:
|
||||
from datetime import datetime
|
||||
return datetime.now().strftime("%Y%m%d")
|
||||
|
||||
|
||||
def _verify_zip(zip_path: Path) -> list[str]:
|
||||
problems: list[str] = []
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
with zipfile.ZipFile(zip_path) as z:
|
||||
z.extractall(td)
|
||||
v = Path(td)
|
||||
names = [p.relative_to(v).as_posix() for p in v.rglob("*") if p.is_file()]
|
||||
readmes = [n for n in names if n.lower().endswith("readme.md")]
|
||||
if readmes != ["README.md"]:
|
||||
problems.append("README 必须且只能在包根一份,实际:%s" % readmes)
|
||||
if any("交付清单" in n for n in names):
|
||||
problems.append("交付清单不得打入包内")
|
||||
if any(re.search(r"(before|after|改造前|原版)", n, re.I) for n in names):
|
||||
problems.append("包内不得出现对比期/before/after 文件命名")
|
||||
docs = [n for n in names if n.startswith("文档/")]
|
||||
if any(n.lower().endswith("readme.md") for n in docs):
|
||||
problems.append("文档目录不得放 README")
|
||||
if (v / "README.md").exists():
|
||||
txt = (v / "README.md").read_text(encoding="utf-8", errors="ignore")
|
||||
for ref in re.findall(r"!\[[^\]]*\]\(([^)]+)\)", txt):
|
||||
if "://" in ref or ref.startswith("#"):
|
||||
continue
|
||||
if not (v / ref).exists():
|
||||
problems.append("README 图片解压后缺失:%s" % ref)
|
||||
forbidden_in_readme = [
|
||||
"致谢", "原始开源项目", "原项目链接", "原作者", "基于开源",
|
||||
"He Rui", "CrazyHer", "campus-surveillance",
|
||||
]
|
||||
for term in forbidden_in_readme:
|
||||
if term.lower() in txt.lower():
|
||||
problems.append("README 出现不应主动展示的来源表述:%s" % term)
|
||||
return problems
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user