# -*- coding: utf-8 -*- r"""在 Gitee 上创建发行版(Release)并上传三个文件。 用法: python _build/gitee_release.py <令牌> [版本号] 令牌只从命令行参数读,**不写进任何文件、不进仓库**。 Gitee 的两个接口: 建发行版 POST /api/v5/repos/{owner}/{repo}/releases 传附件 POST /api/v5/repos/{owner}/{repo}/releases/{id}/attach_files SSH 密钥只能推代码,建发行版必须用这个令牌。 """ from __future__ import annotations import json import mimetypes import sys import urllib.error import urllib.request import uuid from pathlib import Path ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") OWNER = "xmz7891" REPO = "ykxmz" API = f"https://gitee.com/api/v5/repos/{OWNER}/{REPO}" def api(url: str, payload: dict | None = None, method: str = "GET", timeout: int = 60): data = None headers = {"User-Agent": "ykxmz-release"} if payload is not None: data = json.dumps(payload).encode("utf-8") headers["Content-Type"] = "application/json;charset=UTF-8" request = urllib.request.Request(url, data=data, headers=headers, method=method) with urllib.request.urlopen(request, timeout=timeout) as response: body = response.read().decode("utf-8", "replace") return response.status, (json.loads(body) if body.strip() else {}) def upload_file(url: str, path: Path, token: str, timeout: int = 600): """multipart 上传单个文件。""" boundary = "----ykxmz" + uuid.uuid4().hex content_type = (mimetypes.guess_type(path.name)[0] or "application/octet-stream") head = ( f"--{boundary}\r\n" f'Content-Disposition: form-data; name="access_token"\r\n\r\n' f"{token}\r\n" f"--{boundary}\r\n" f'Content-Disposition: form-data; name="file"; ' f'filename="{path.name}"\r\n' f"Content-Type: {content_type}\r\n\r\n" ).encode("utf-8") tail = f"\r\n--{boundary}--\r\n".encode("utf-8") body = head + path.read_bytes() + tail request = urllib.request.Request( url, data=body, method="POST", headers={ "Content-Type": f"multipart/form-data; boundary={boundary}", "Content-Length": str(len(body)), "User-Agent": "ykxmz-release", }) with urllib.request.urlopen(request, timeout=timeout) as response: text = response.read().decode("utf-8", "replace") return response.status, (json.loads(text) if text.strip() else {}) def main() -> int: if len(sys.argv) < 2: print(" 用法: python _build/gitee_release.py <令牌> [版本号]") return 1 token = sys.argv[1] version = sys.argv[2] if len(sys.argv) > 2 else "1.6.0" tag = f"v{version}" # 更新内容从哪来: # 1) 命令行第三个参数(直接给一段话) # 2) _build/CHANGELOG.md 里最新一节 changelog = sys.argv[3] if len(sys.argv) > 3 else '' if not changelog: note = ROOT / "_build" / "CHANGELOG.md" if note.is_file(): text = note.read_text(encoding="utf-8") blocks = [b.strip() for b in text.split(chr(10) + "## ") if b.strip()] for b in blocks: if b.startswith(version) or b.startswith("v" + version): changelog = b break if not changelog and blocks: changelog = blocks[0] # 去掉第一行的 "## 版本号" —— 发行版标题已经有了, # 正文里再来一条就重复了 if changelog: lines = changelog.splitlines() if lines and lines[0].strip().startswith("#"): changelog = "\n".join(lines[1:]).strip() if not changelog: changelog = f"版本 {version}" # 电脑端给**安装包**(不是 zip 了,用户要求); # 服务器端给 zip;手表端直接给 APK。 files = [ ROOT / "dist" / f"ykxmz-setup-{version}.exe", ROOT / "dist" / f"ykxmz-server-{version}.zip", ROOT / "dist" / f"ykxmz-watch-{version}.apk", ] for path in files: if not path.is_file(): print(f" [失败] 找不到 {path}") return 1 print(f" 仓库: {OWNER}/{REPO}") print(f" 标签: {tag}") print() # 发行版说明**只放更新内容**(用户要求)。 # 不再套一层 "## 更新内容" —— CHANGELOG 每段自带 "## 版本号", # 套一层会变成两个标题叠在一起。 body = changelog.strip() if not body: body = f"版本 {version}" # 保险:Gitee 上 markdown 首行若被吃掉,这里补个空行 if not body.endswith("\n"): body += "\n" print(" 建发行版…") try: status, reply = api( f"{API}/releases", {"access_token": token, "tag_name": tag, "name": f"ykxmz {version}", "body": body, "target_commitish": "main", "prerelease": False}, method="POST") except urllib.error.HTTPError as exc: detail = exc.read().decode("utf-8", "replace")[:400] print(f" [失败] HTTP {exc.code}: {detail}") return 1 if status not in (200, 201): print(f" [失败] HTTP {status}: {reply}") return 1 release_id = reply.get("id") print(f" ✓ 发行版已建,id={release_id}") print(f" {reply.get('html_url', '')}") # 如果标签已经存在(重复发布),走"找已有发行版再传附件"的路 if not release_id: print(" [提示] 没拿到 id,尝试按标签查已有的发行版…") try: _s, existing = api(f"{API}/releases/tags/{tag}?access_token={token}") release_id = existing.get("id") print(f" 已有发行版 id={release_id}") except Exception as exc: # noqa: BLE001 print(f" [失败] 查不到: {exc}") return 1 # ---- 2. 传附件 ---- print() print(" 上传文件:") attach = f"{API}/releases/{release_id}/attach_files" for path in files: size_mb = path.stat().st_size / 1024 / 1024 try: status, reply = upload_file(attach, path, token) if status in (200, 201): print(f" ✓ {path.name} ({size_mb:.2f} MB)") else: print(f" ✗ {path.name}: HTTP {status} {reply}") except urllib.error.HTTPError as exc: detail = exc.read().decode("utf-8", "replace")[:200] print(f" ✗ {path.name}: HTTP {exc.code} {detail}") except Exception as exc: # noqa: BLE001 print(f" ✗ {path.name}: {exc}") print() print(f" 完成:https://gitee.com/{OWNER}/{REPO}/releases/tag/{tag}") return 0 if __name__ == "__main__": sys.exit(main())