mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 11:20:43 +00:00
加 EzHub(Gitea)发布支持 + 一次发两个平台的脚本
用户要求:除了 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 账号里之后才能推。**
私钥在仓库外,不会进版本库。
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
加 EzHub(Gitea)发布支持 + 一次发两个平台的脚本
|
||||
|
||||
用户要求:除了 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 账号里之后才能推。**
|
||||
私钥在仓库外,不会进版本库。
|
||||
@@ -0,0 +1,219 @@
|
||||
# -*- 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())
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# 确认线上公开页面干净 + 产物完整
|
||||
set -u
|
||||
|
||||
BASE=https://relay.xxmxxs.top
|
||||
UA='LanRemote/1.8.0'
|
||||
|
||||
echo "════════ 1. 线上官网有没有管理后台字样 ════════"
|
||||
curl -s --max-time 25 -H "User-Agent: $UA" "$BASE/" -o /tmp/site.html
|
||||
echo " 官网 $(wc -c < /tmp/site.html) 字节"
|
||||
hits=0
|
||||
for kw in zhongzhuanzhan "/admin" 管理后台 管理页 管理入口 admin_password; do
|
||||
n=$(grep -c "$kw" /tmp/site.html 2>/dev/null || echo 0)
|
||||
if [ "$n" -gt 0 ]; then
|
||||
echo " **命中 $kw x$n**"
|
||||
hits=$((hits + 1))
|
||||
fi
|
||||
done
|
||||
[ "$hits" = "0" ] && echo " ✓ 六个关键词一个都没有"
|
||||
|
||||
echo
|
||||
echo " 官网里的所有站内链接:"
|
||||
grep -o 'href="/[^"]*"' /tmp/site.html | sort -u | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
echo "════════ 2. 三个产物在不在 ════════"
|
||||
for f in ykxmz-setup-1.8.0.exe ykxmz-server-1.8.0.zip ykxmz-watch-1.8.0.apk; do
|
||||
hdr=$(curl -sI --max-time 30 -H "User-Agent: $UA" "$BASE/download/$f")
|
||||
code=$(echo "$hdr" | head -1 | awk '{print $2}')
|
||||
len=$(echo "$hdr" | grep -i '^content-length' | tr -d '\r' | awk '{print $2}')
|
||||
printf " %-30s HTTP %-4s %s 字节\n" "$f" "$code" "${len:-?}"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "════════ 3. 服务端 zip 里有没有 CHANGELOG(不该有)════════"
|
||||
python3 - <<'PY'
|
||||
import zipfile, urllib.request
|
||||
url = "https://relay.xxmxxs.top/download/ykxmz-server-1.8.0.zip"
|
||||
try:
|
||||
data = urllib.request.urlopen(url, timeout=60).read()
|
||||
import io
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as z:
|
||||
names = z.namelist()
|
||||
print(" 条目 %d 个" % len(names))
|
||||
bad = [n for n in names
|
||||
if "CHANGELOG" in n.upper() or "RELEASE" in n.upper()]
|
||||
print(" %s 没有把更新日志打进包里"
|
||||
% ("✓" if not bad else "**有 " + str(bad) + "**"))
|
||||
# 运维文档里提到管理页是允许的
|
||||
for n in names:
|
||||
if n.endswith(".txt"):
|
||||
try:
|
||||
t = z.read(n).decode("utf-8", "replace")
|
||||
except Exception:
|
||||
continue
|
||||
c = t.count("管理页")
|
||||
if c:
|
||||
print(" (运维文档 %s 提到管理页 %d 次 —— 按约定允许)"
|
||||
% (n.split("/")[-1], c))
|
||||
except Exception as e:
|
||||
print(" 取包失败: %s" % e)
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "════════ 4. 管理页本身还正常吗(不公开提,但得能登录)════════"
|
||||
PASS=$(cat /root/lanremote-relay/data/admin_password.txt 2>/dev/null)
|
||||
curl -s -c /tmp/ck.txt -o /dev/null --max-time 20 \
|
||||
-d "username=xmz7891&password=$PASS" "$BASE/admin/login"
|
||||
echo " 登录后访问: HTTP $(curl -s -o /dev/null -w '%{http_code}' -b /tmp/ck.txt --max-time 20 -H "User-Agent: $UA" "$BASE/zhongzhuanzhan")"
|
||||
echo " 未登录: HTTP $(curl -s -o /dev/null -w '%{http_code}' --max-time 20 -H "User-Agent: $UA" "$BASE/zhongzhuanzhan")"
|
||||
|
||||
echo
|
||||
echo "════════ 5. 中转版本 ════════"
|
||||
curl -s --max-time 15 "$BASE/v1/health" | head -c 150
|
||||
echo
|
||||
@@ -0,0 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""一次把代码和发行版发到两个平台。
|
||||
|
||||
平台:
|
||||
origin gitee.com:xmz7891/ykxmz (Gitee,API v5)
|
||||
ezhub git.261826.xyz/xmz/ykxmz (EzHub = Gitea 1.22,API v1)
|
||||
|
||||
用法:
|
||||
python _build/release_all.py <gitee令牌> <ezhub令牌> [版本号] [--dry]
|
||||
|
||||
--dry 只看会做什么,不真的推/建
|
||||
|
||||
为什么合成一个脚本:
|
||||
两个平台的产物、更新内容完全一样,分开写会漂移
|
||||
(比如改了一个忘了改另一个)。这里把"要发什么"算一次,
|
||||
然后分别喂给各自的接口。
|
||||
|
||||
令牌只从命令行读,不落盘、不进仓库。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
GIT = r"C:\Program Files\Git\bin\git.exe"
|
||||
SSH = r"C:\Program Files\Git\usr\bin\ssh.exe"
|
||||
|
||||
PLATFORMS = {
|
||||
"gitee": {
|
||||
"remote": "origin",
|
||||
"web": "https://gitee.com/xmz7891/ykxmz",
|
||||
"kind": "gitee",
|
||||
},
|
||||
"ezhub": {
|
||||
"remote": "ezhub",
|
||||
"web": "https://git.261826.xyz/xmz/ykxmz",
|
||||
"kind": "gitea",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def run_git(args: list, check: bool = False) -> subprocess.CompletedProcess:
|
||||
import os
|
||||
env = dict(os.environ)
|
||||
env["GIT_SSH_COMMAND"] = '"%s"' % SSH
|
||||
return subprocess.run([GIT] + args, cwd=str(ROOT), env=env,
|
||||
capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace")
|
||||
|
||||
|
||||
def read_changelog(version: str) -> str:
|
||||
"""取 CHANGELOG 里这个版本那一节 —— 发行版说明只放更新内容。
|
||||
|
||||
⚠ 注意:按 "\\n## " 切分时,**第一块会带着自己的 "## 版本号"**
|
||||
(因为它前面没有换行),后面几块没有。
|
||||
所以匹配版本号前必须先把开头的 # 去掉 ——
|
||||
原来只写 startswith("1.8.0"),对第一块永远匹配不上,
|
||||
全靠"取不到就退回第一块"的兜底碰巧对了。
|
||||
"""
|
||||
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()]
|
||||
|
||||
def strip_heading(block: str) -> str:
|
||||
lines = block.splitlines()
|
||||
if lines and lines[0].lstrip().startswith("#"):
|
||||
lines = lines[1:]
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
for block in blocks:
|
||||
first = block.splitlines()[0].lstrip("# ").strip()
|
||||
if first == version or first == "v" + version:
|
||||
return strip_heading(block)
|
||||
return strip_heading(blocks[0]) if blocks else ""
|
||||
|
||||
|
||||
def artifacts(version: str) -> list:
|
||||
return [
|
||||
ROOT / "dist" / f"ykxmz-setup-{version}.exe",
|
||||
ROOT / "dist" / f"ykxmz-server-{version}.zip",
|
||||
ROOT / "dist" / f"ykxmz-watch-{version}.apk",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Gitee
|
||||
|
||||
def gitee_release(token: str, version: str, body: str,
|
||||
files: list) -> bool:
|
||||
import mimetypes
|
||||
import uuid
|
||||
|
||||
api = "https://gitee.com/api/v5/repos/xmz7891/ykxmz"
|
||||
tag = "v" + version
|
||||
|
||||
payload = json.dumps({
|
||||
"access_token": token, "tag_name": tag,
|
||||
"name": "ykxmz " + version, "body": body,
|
||||
"target_commitish": "main", "prerelease": False,
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
api + "/releases", data=payload, method="POST",
|
||||
headers={"Content-Type": "application/json;charset=UTF-8",
|
||||
"User-Agent": "ykxmz-release"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=90) as resp:
|
||||
reply = json.loads(resp.read().decode("utf-8", "replace"))
|
||||
rid = reply.get("id")
|
||||
print(" ✓ 发行版已建 id=%s" % rid)
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace")[:300]
|
||||
print(" ✗ 建发行版失败 HTTP %s: %s" % (exc.code, detail))
|
||||
return False
|
||||
|
||||
ok = 0
|
||||
for path in files:
|
||||
boundary = "----yk" + uuid.uuid4().hex
|
||||
ctype = (mimetypes.guess_type(path.name)[0]
|
||||
or "application/octet-stream")
|
||||
head = (
|
||||
"--%s\r\n"
|
||||
'Content-Disposition: form-data; name="access_token"\r\n\r\n'
|
||||
"%s\r\n"
|
||||
"--%s\r\n"
|
||||
'Content-Disposition: form-data; name="file"; '
|
||||
'filename="%s"\r\n'
|
||||
"Content-Type: %s\r\n\r\n"
|
||||
% (boundary, token, boundary, path.name, ctype)
|
||||
).encode("utf-8")
|
||||
data = head + path.read_bytes() + \
|
||||
("\r\n--%s--\r\n" % boundary).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
"%s/releases/%s/attach_files" % (api, rid), data=data,
|
||||
method="POST",
|
||||
headers={"Content-Type":
|
||||
"multipart/form-data; boundary=" + boundary,
|
||||
"Content-Length": str(len(data)),
|
||||
"User-Agent": "ykxmz-release"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=900) as resp:
|
||||
if resp.status in (200, 201):
|
||||
print(" ✓ %s" % path.name)
|
||||
ok += 1
|
||||
except urllib.error.HTTPError as exc:
|
||||
print(" ✗ %s HTTP %s" % (path.name, exc.code))
|
||||
return ok == len(files)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Gitea
|
||||
|
||||
def gitea_release(token: str, version: str, body: str,
|
||||
files: list) -> bool:
|
||||
import mimetypes
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
api = "https://git.261826.xyz/api/v1/repos/xmz/ykxmz"
|
||||
tag = "v" + version
|
||||
auth = {"Authorization": "token " + token,
|
||||
"User-Agent": "ykxmz-release"}
|
||||
|
||||
# 先看标签有没有发行版(重发时复用,别报错)
|
||||
rid = None
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
"%s/releases/tags/%s" % (api, tag), headers=auth)
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
rid = json.loads(resp.read().decode("utf-8", "replace")).get("id")
|
||||
print(" • 该标签已有发行版 id=%s,复用" % rid)
|
||||
except urllib.error.HTTPError:
|
||||
pass
|
||||
|
||||
if not rid:
|
||||
payload = json.dumps({
|
||||
"tag_name": tag, "name": "ykxmz " + version, "body": body,
|
||||
"target_commitish": "main", "draft": False,
|
||||
"prerelease": False,
|
||||
}).encode("utf-8")
|
||||
headers = dict(auth)
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(api + "/releases", data=payload,
|
||||
method="POST", headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=90) as resp:
|
||||
reply = json.loads(resp.read().decode("utf-8", "replace"))
|
||||
rid = reply.get("id")
|
||||
print(" ✓ 发行版已建 id=%s" % rid)
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace")[:300]
|
||||
print(" ✗ 建发行版失败 HTTP %s: %s" % (exc.code, detail))
|
||||
return False
|
||||
|
||||
ok = 0
|
||||
for path in files:
|
||||
boundary = "----yk" + uuid.uuid4().hex
|
||||
ctype = (mimetypes.guess_type(path.name)[0]
|
||||
or "application/octet-stream")
|
||||
head = (
|
||||
"--%s\r\n"
|
||||
'Content-Disposition: form-data; name="attachment"; '
|
||||
'filename="%s"\r\n'
|
||||
"Content-Type: %s\r\n\r\n" % (boundary, path.name, ctype)
|
||||
).encode("utf-8")
|
||||
data = head + path.read_bytes() + \
|
||||
("\r\n--%s--\r\n" % boundary).encode("utf-8")
|
||||
url = ("%s/releases/%s/assets?name=%s"
|
||||
% (api, rid, urllib.parse.quote(path.name)))
|
||||
headers = dict(auth)
|
||||
headers["Content-Type"] = "multipart/form-data; boundary=" + boundary
|
||||
headers["Content-Length"] = str(len(data))
|
||||
req = urllib.request.Request(url, data=data, method="POST",
|
||||
headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=900) as resp:
|
||||
if resp.status in (200, 201):
|
||||
print(" ✓ %s" % path.name)
|
||||
ok += 1
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace")[:200]
|
||||
print(" ✗ %s HTTP %s %s" % (path.name, exc.code, detail))
|
||||
return ok == len(files)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
dry = "--dry" in sys.argv
|
||||
if len(args) < 2:
|
||||
print(" 用法: python _build/release_all.py <gitee令牌> <ezhub令牌>"
|
||||
" [版本号] [--dry]")
|
||||
return 1
|
||||
gitee_token, ezhub_token = args[0].strip(), args[1].strip()
|
||||
version = args[2] if len(args) > 2 else "1.8.0"
|
||||
tag = "v" + version
|
||||
|
||||
print(" 版本: %s" % version)
|
||||
print(" 模式: %s" % ("演练(不真的发)" if dry else "正式发布"))
|
||||
print()
|
||||
|
||||
# ---- 0. 前置检查 ----
|
||||
print(" 一、前置检查")
|
||||
files = artifacts(version)
|
||||
missing = [f for f in files if not f.is_file()]
|
||||
for path in files:
|
||||
mark = "✓" if path.is_file() else "✗ 缺失"
|
||||
size = (path.stat().st_size / 1024 / 1024) if path.is_file() else 0
|
||||
print(" %s %-32s %6.2f MB" % (mark, path.name, size))
|
||||
if missing:
|
||||
print(" [失败] 产物不全,先构建")
|
||||
return 1
|
||||
|
||||
body = read_changelog(version)
|
||||
if not body:
|
||||
print(" [失败] CHANGELOG 里找不到 %s 那一节" % version)
|
||||
return 1
|
||||
print(" 更新说明 %d 字符(%d 行)" % (len(body), body.count("\n") + 1))
|
||||
|
||||
# 工作区必须干净,否则推上去的和测过的不是一份
|
||||
st = run_git(["status", "--porcelain"])
|
||||
if (st.stdout or "").strip():
|
||||
print(" [失败] 工作区不干净,先提交")
|
||||
for line in (st.stdout or "").strip().splitlines()[:5]:
|
||||
print(" " + line)
|
||||
return 1
|
||||
print(" ✓ 工作区干净")
|
||||
|
||||
# 对外材料清洁度:绝不能让管理页字眼进发行版说明
|
||||
for word in ("zhongzhuanzhan", "/admin", "管理后台", "管理页"):
|
||||
if word in body:
|
||||
print(" [失败] 更新说明里出现了「%s」—— 违反对外口径" % word)
|
||||
return 1
|
||||
print(" ✓ 更新说明里没有管理页字眼")
|
||||
|
||||
if dry:
|
||||
print()
|
||||
print(" 演练结束。要真发就去掉 --dry。")
|
||||
return 0
|
||||
|
||||
# ---- 1. 推代码 ----
|
||||
print()
|
||||
print(" 二、推代码")
|
||||
for name, info in PLATFORMS.items():
|
||||
r = run_git(["push", info["remote"], "main"])
|
||||
ok = r.returncode == 0
|
||||
tail = ((r.stderr or r.stdout or "").strip().splitlines() or [""])[-1]
|
||||
print(" %-6s %s %s" % (name, "✓" if ok else "✗", tail[:90]))
|
||||
if not ok:
|
||||
print(" (代码没推上去,先修这个再建发行版)")
|
||||
return 1
|
||||
|
||||
# ---- 2. 建发行版 ----
|
||||
print()
|
||||
print(" 三、建发行版")
|
||||
results = {}
|
||||
|
||||
print(" [Gitee]")
|
||||
results["gitee"] = gitee_release(gitee_token, version, body, files)
|
||||
|
||||
print(" [EzHub]")
|
||||
results["ezhub"] = gitea_release(ezhub_token, version, body, files)
|
||||
|
||||
print()
|
||||
print(" ================ 结果 ================")
|
||||
for name, info in PLATFORMS.items():
|
||||
mark = "✓" if results.get(name) else "✗"
|
||||
print(" %s %-6s %s/releases/tag/%s"
|
||||
% (mark, name, info["web"], tag))
|
||||
|
||||
return 0 if all(results.values()) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user