102 lines
3.8 KiB
Python
102 lines
3.8 KiB
Python
#!/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()
|