mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 19:30:42 +00:00
用户要求:除了 Gitee,还要发到朋友自建的代码托管平台 EzHub
(ssh://git@git.261826.xyz/xmz/ykxmz.git),release 也要建。
探测结果:EzHub 是 **Gitea 1.22.0**
(/api/v1/version 返回 16.0.5+gitea-1.22.0),
仓库 xmz/ykxmz 存在,目前还没有任何发行版。
Gitea 和 Gitee 的接口差别(踩过的点):
项目 Gitee Gitea / EzHub
API 前缀 /api/v5/repos/... /api/v1/repos/...
认证 参数 access_token 请求头 Authorization: token
传附件路径 /releases/{id}/ /releases/{id}/
attach_files assets?name=<文件名>
附件字段名 file attachment
草稿字段 无 draft / prerelease
新增:
* _build/ezhub_release.py 单独发 EzHub 用
* _build/release_all.py 一次发两个平台(推荐)
- 前置检查:产物齐不齐、更新说明能不能取到、工作区干不干净
- **对外口径检查**:更新说明里出现管理页字眼直接拒绝发布
- 推代码到两个 remote,再分别在两边建发行版 + 传三个附件
- 支持 --dry 演练(不真的发),已经过通
修掉的 bug:
CHANGELOG 解析。按 "\n## " 切分时第一块带着自己的 "## 版本号"
(前面没换行),后面几块没有 —— 原来只写 startswith("1.8.0"),
对第一块永远匹配不上,全靠"取不到就退回第一块"的兜底碰巧对了。
现在先把开头的 # 去掉再比。
SSH:
为 EzHub 单独生成了一把密钥(~/.ssh/id_ed25519_ezhub),
不复用 Gitee 那把,权限隔离。已写好 ~/.ssh/config 的 Host 段。
**公钥需要用户加到 EzHub 账号里之后才能推。**
私钥在仓库外,不会进版本库。
220 lines
8.0 KiB
Python
220 lines
8.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
r"""在 EzHub(Gitea 1.22)上创建发行版并上传三个文件。
|
||
|
||
用法:
|
||
python _build/ezhub_release.py <令牌> [版本号]
|
||
|
||
令牌只从命令行参数读,**不写进任何文件、不进仓库**。
|
||
|
||
和 Gitee 的差别(Gitea 的接口跟 Gitee 不一样):
|
||
|
||
项目 Gitee Gitea / EzHub
|
||
------------------------------------------------------------------
|
||
API 前缀 /api/v5/repos/... /api/v1/repos/...
|
||
认证 参数 access_token 请求头 Authorization: token
|
||
建发行版 POST /releases POST /releases
|
||
传附件 POST /releases/{id}/ POST /releases/{id}/
|
||
attach_files assets?name=<文件名>
|
||
附件字段名 file attachment
|
||
按标签查 /releases/tags/{tag} /releases/tags/{tag}(一样)
|
||
草稿字段 (无) draft / prerelease
|
||
|
||
SSH 密钥只能推代码,建发行版必须用令牌。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import mimetypes
|
||
import sys
|
||
import urllib.error
|
||
import urllib.parse
|
||
import urllib.request
|
||
import uuid
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||
OWNER = "xmz"
|
||
REPO = "ykxmz"
|
||
API = f"https://git.261826.xyz/api/v1/repos/{OWNER}/{REPO}"
|
||
WEB = f"https://git.261826.xyz/{OWNER}/{REPO}"
|
||
|
||
|
||
def api(url: str, payload: dict | None = None, method: str = "GET",
|
||
token: str = "", timeout: int = 60):
|
||
"""JSON 接口。Gitea 用请求头带令牌,不是查询参数。"""
|
||
data = None
|
||
headers = {
|
||
"User-Agent": "ykxmz-release",
|
||
"Accept": "application/json",
|
||
"Authorization": f"token {token}",
|
||
}
|
||
if payload is not None:
|
||
data = json.dumps(payload).encode("utf-8")
|
||
headers["Content-Type"] = "application/json"
|
||
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_asset(url: str, path: Path, token: str, timeout: int = 900):
|
||
"""multipart 上传。
|
||
|
||
Gitea 的字段名是 **attachment**(Gitee 是 file),
|
||
文件名走查询参数 name=,不是 Content-Disposition。
|
||
"""
|
||
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="attachment"; '
|
||
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)),
|
||
"Authorization": f"token {token}",
|
||
"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 read_changelog(version: str) -> str:
|
||
"""从 _build/CHANGELOG.md 里取这个版本那一节(只放更新内容)。"""
|
||
note = ROOT / "_build" / "CHANGELOG.md"
|
||
if not note.is_file():
|
||
return ""
|
||
text = note.read_text(encoding="utf-8")
|
||
blocks = [b.strip() for b in text.split("\n## ") if b.strip()]
|
||
for block in blocks:
|
||
if block.startswith(version) or block.startswith("v" + version):
|
||
lines = block.splitlines()
|
||
if lines and lines[0].strip().startswith("#"):
|
||
lines = lines[1:]
|
||
return "\n".join(lines).strip()
|
||
if blocks:
|
||
lines = blocks[0].splitlines()
|
||
if lines and lines[0].strip().startswith("#"):
|
||
lines = lines[1:]
|
||
return "\n".join(lines).strip()
|
||
return ""
|
||
|
||
|
||
def main() -> int:
|
||
if len(sys.argv) < 2:
|
||
print(" 用法: python _build/ezhub_release.py <令牌> [版本号]")
|
||
return 1
|
||
token = sys.argv[1].strip()
|
||
version = sys.argv[2] if len(sys.argv) > 2 else "1.8.0"
|
||
tag = f"v{version}"
|
||
|
||
changelog = sys.argv[3] if len(sys.argv) > 3 else ""
|
||
if not changelog:
|
||
changelog = read_changelog(version)
|
||
if not changelog:
|
||
changelog = f"版本 {version}"
|
||
|
||
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(" 仓库: %s/%s @ git.261826.xyz" % (OWNER, REPO))
|
||
print(" 标签: %s" % tag)
|
||
print()
|
||
|
||
# ---- 0. 先确认令牌能用 ----
|
||
try:
|
||
_s, me = api(f"https://git.261826.xyz/api/v1/user", token=token)
|
||
print(" 令牌身份: %s" % me.get("login", "?"))
|
||
except urllib.error.HTTPError as exc:
|
||
detail = exc.read().decode("utf-8", "replace")[:200]
|
||
print(" [失败] 令牌无效 HTTP %s: %s" % (exc.code, detail))
|
||
return 1
|
||
|
||
# ---- 1. 建发行版 ----
|
||
# 已存在同名标签就先查出来复用(重发时不报错)
|
||
release_id = None
|
||
try:
|
||
_s, existing = api(f"{API}/releases/tags/{tag}", token=token)
|
||
release_id = existing.get("id")
|
||
if release_id:
|
||
print(" 该标签已有发行版 id=%s,复用它" % release_id)
|
||
except urllib.error.HTTPError:
|
||
pass
|
||
|
||
if not release_id:
|
||
print(" 建发行版…")
|
||
body = changelog.strip()
|
||
if not body.endswith("\n"):
|
||
body += "\n"
|
||
try:
|
||
status, reply = api(
|
||
f"{API}/releases",
|
||
{"tag_name": tag, "name": f"ykxmz {version}", "body": body,
|
||
"target_commitish": "main", "draft": False,
|
||
"prerelease": False},
|
||
method="POST", token=token)
|
||
except urllib.error.HTTPError as exc:
|
||
detail = exc.read().decode("utf-8", "replace")[:400]
|
||
print(" [失败] HTTP %s: %s" % (exc.code, detail))
|
||
return 1
|
||
if status not in (200, 201):
|
||
print(" [失败] HTTP %s: %s" % (status, reply))
|
||
return 1
|
||
release_id = reply.get("id")
|
||
print(" ✓ 发行版已建,id=%s" % release_id)
|
||
print(" %s" % reply.get("html_url", ""))
|
||
|
||
if not release_id:
|
||
print(" [失败] 没拿到发行版 id")
|
||
return 1
|
||
|
||
# ---- 2. 传附件 ----
|
||
print()
|
||
print(" 上传文件:")
|
||
ok_count = 0
|
||
for path in files:
|
||
size_mb = path.stat().st_size / 1024 / 1024
|
||
# Gitea 要求文件名走查询参数
|
||
url = ("%s/releases/%s/assets?name=%s"
|
||
% (API, release_id, urllib.parse.quote(path.name)))
|
||
try:
|
||
status, reply = upload_asset(url, path, token)
|
||
if status in (200, 201):
|
||
print(" ✓ %s (%.2f MB)" % (path.name, size_mb))
|
||
ok_count += 1
|
||
else:
|
||
print(" ✗ %s: HTTP %s %s" % (path.name, status, reply))
|
||
except urllib.error.HTTPError as exc:
|
||
detail = exc.read().decode("utf-8", "replace")[:250]
|
||
print(" ✗ %s: HTTP %s %s" % (path.name, exc.code, detail))
|
||
except Exception as exc: # noqa: BLE001
|
||
print(" ✗ %s: %s" % (path.name, exc))
|
||
|
||
print()
|
||
if ok_count == len(files):
|
||
print(" 完成:%s/releases/tag/%s" % (WEB, tag))
|
||
return 0
|
||
print(" 部分失败(%d/%d)" % (ok_count, len(files)))
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|