#!/usr/bin/env python3 """使用 Azure 发布配置文件 (.PublishSettings) 将本项目部署到 Function App。""" import os import sys import zipfile import xml.etree.ElementTree as ET from pathlib import Path from urllib.parse import urlparse try: import requests except ImportError: print("请先安装: pip install requests") sys.exit(1) EXCLUDE = { ".venv", "venv", "__pycache__", ".git", ".azure", ".python_packages", "local.settings.json", "publish_with_profile.py", ".gitignore", "deploy.zip", } def find_zipdeploy_profile(profile_path: Path) -> ET.Element: """优先取 ZipDeploy 的 publishProfile。""" tree = ET.parse(profile_path) for profile in tree.getroot().findall(".//publishProfile"): if (profile.get("publishMethod") or "").strip() == "ZipDeploy": return profile for profile in tree.getroot().findall(".//publishProfile"): if "MSDeploy" in (profile.get("publishMethod") or ""): return profile first = tree.getroot().find(".//publishProfile") return first def get_scm_url(profile: ET.Element, destination_app_url: str) -> str: """从 profile 或 destinationAppUrl 得到 SCM 的 base URL。""" # ZipDeploy 的 publishUrl 即 SCM 主机:port pub_url = (profile.get("publishUrl") or "").strip() if pub_url and "scm." in pub_url: host_port = pub_url.split("/")[0] return f"https://{host_port}" parsed = urlparse(destination_app_url) host = parsed.netloc # 区域化: xxx.southeastasia-01.azurewebsites.net -> xxx.scm.southeastasia-01.azurewebsites.net if ".azurewebsites.net" in host and ".scm." not in host: parts = host.split(".", 1) host = parts[0] + ".scm." + parts[1] if len(parts) == 2 else host return f"{parsed.scheme}://{host}" def make_zip(project_dir: Path, zip_path: Path) -> None: with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for root, dirs, files in os.walk(project_dir): dirs[:] = [d for d in dirs if d not in EXCLUDE and not d.startswith(".")] rel_root = Path(root).relative_to(project_dir) for f in files: if f in EXCLUDE or f.endswith(".pyc"): continue zf.write(Path(root) / f, rel_root / f) def main() -> None: script_dir = Path(__file__).resolve().parent profile_path = Path(sys.argv[1]) if len(sys.argv) >= 2 else script_dir / "agnetdoc.PublishSettings" if not profile_path.exists(): print(f"未找到: {profile_path}") sys.exit(1) try: profile = find_zipdeploy_profile(profile_path) if profile is None: print("未找到 publishProfile") sys.exit(1) user = (profile.get("userName") or "").strip() pwd = (profile.get("userPWD") or "").strip() dest = profile.get("destinationAppUrl") or "https://agnetdoc.azurewebsites.net" if not user or not pwd: print("发布配置中缺少 userName 或 userPWD,请从 Azure 门户重新下载发布配置文件") sys.exit(1) except ET.ParseError as e: print(f"解析失败: {e}") sys.exit(1) scm_url = get_scm_url(profile, dest) zip_deploy_url = f"{scm_url}/api/zipdeploy" zip_path = script_dir / "deploy.zip" print("打包中...") make_zip(script_dir, zip_path) print(f"上传到 {scm_url} ...") try: with open(zip_path, "rb") as f: r = requests.post(zip_deploy_url, auth=(user, pwd), data=f, headers={"Content-Type": "application/zip"}, timeout=600) finally: zip_path.unlink(missing_ok=True) if r.status_code not in (200, 201, 202): print(f"部署失败 HTTP {r.status_code}:", r.text[:400]) sys.exit(1) print("部署已提交,可访问:", dest.rstrip("/")) if __name__ == "__main__": main()