docs:新增远程发版脚本,Release 可用一条命令发布
Gitea 的 Release 全流程走 REST API(实测可行),不用点网页: - docs/release/publish_release.py(纯标准库,无依赖):校验 tag → 建/更新 Release → 上传附件 → 回读自检;支持 --dry-run / --draft / --prerelease / --expect-commit / --no-asset / --insecure - 默认先比对本地 tag 与远端 tag 指向的提交,不一致直接拒绝 —— 2026.11.05 就是 tag 落后 12 个提交,源码包是旧代码且外表看不出来 - .url 里仍是 REPLACE_WITH_YOUR_LINK 时拒绝发版(--force 可跳过) - 附件同名先删再传,脚本可重复执行(幂等) - docs/release/README.md 改为「一条命令发版 + 手工兜底」两套流程,附实测记录 - 根 README 发布流程、AGENTS.md 发版清单挂上脚本用法 实测:临时 tag 走完整流程 创建 201 → 上传 201 → 下载 200(内容正确)→ 重复执行后只有一个同名附件 → 删除干净(release 204 / tag 已清)。 注意 Gitea 附件字段名必须是 attachment,用 file 会 500 "http: no such file"。
This commit is contained in:
1 parent
0897761c47
commit
b88e1a223b
4 files changed
+360
-2
No files matched your search
@@ -204,6 +204,8 @@ msedge --headless=new --disable-gpu --hide-scrollbars --window-size=800,720 \
|
|||||||
- [ ] **打 tag 前先确认它指向「实际发布的那个提交」**:`git log --oneline -1 v2026.11.xx`,
|
- [ ] **打 tag 前先确认它指向「实际发布的那个提交」**:`git log --oneline -1 v2026.11.xx`,
|
||||||
再用 `git ls-remote --tags origin` 看远端解析到的 `^{}` 是不是同一个 sha。
|
再用 `git ls-remote --tags origin` 看远端解析到的 `^{}` 是不是同一个 sha。
|
||||||
tag 与代码错位时,Release 的「源码包」是一份旧代码,排查起来极难想到
|
tag 与代码错位时,Release 的「源码包」是一份旧代码,排查起来极难想到
|
||||||
|
- [ ] 上面两项可以交给脚本,它先比对 tag 再动手:
|
||||||
|
`python docs/release/publish_release.py --version v2026.11.xx --dry-run`(看过计划后去掉 `--dry-run`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,15 @@ pvfexport -pvf Script.pvf -out dnf_items.json -compact
|
|||||||
6. **网盘地址**:`docs/release/网盘下载.url` 已指向百度网盘真实分享(提取码 `s33r`),作为 Release 附件上传——十几个 G 的包不适合直传,`.url` 是最省事的缓兵之计;重新分享后记得同步改模板表格
|
6. **网盘地址**:`docs/release/网盘下载.url` 已指向百度网盘真实分享(提取码 `s33r`),作为 Release 附件上传——十几个 G 的包不适合直传,`.url` 是最省事的缓兵之计;重新分享后记得同步改模板表格
|
||||||
7. **验证**:启动器首页版本号、更新日志条数、关于页二维码、GM 页三列是否都正常
|
7. **验证**:启动器首页版本号、更新日志条数、关于页二维码、GM 页三列是否都正常
|
||||||
|
|
||||||
|
> **第 5、6 步可以一条命令搞定**(Gitea REST API,脚本只用标准库):
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> set GITEA_TOKEN=xxxx
|
||||||
|
> python docs/release/publish_release.py --version v2026.11.06 --notes-file docs/release/发布说明模板.md
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> 它会先核对本地/远端 tag 是否指向同一提交(防「Release 挂在旧代码上」),再建或更新 Release、上传 `.url`,可重复执行。加 `--dry-run` 只打印计划。详见 [docs/release/README.md](docs/release/README.md)。
|
||||||
|
|
||||||
> ⚠️ **DLL 指纹绑定**:`90CN.dll` 的 SHA-256 在编译期编进 `DNF90Server.exe`(`boundDllSHA256`)。换 DLL 必须重编服务端,否则客户端连接会被拒绝。发布包里 DLL 与 EXE 一定要成对。
|
> ⚠️ **DLL 指纹绑定**:`90CN.dll` 的 SHA-256 在编译期编进 `DNF90Server.exe`(`boundDllSHA256`)。换 DLL 必须重编服务端,否则客户端连接会被拒绝。发布包里 DLL 与 EXE 一定要成对。
|
||||||
|
|
||||||
## 九、关键设计说明
|
## 九、关键设计说明
|
||||||
|
|||||||
+47
-2
@@ -1,13 +1,56 @@
|
|||||||
# 发布物料(docs/release)
|
# 发布物料(docs/release)
|
||||||
|
|
||||||
每次发版要贴到仓库 Release 里的两样东西。体积十几个 G 的整包不适合直传,所以走「说明文档 + 网盘快捷方式」的组合。
|
每次发版要贴到仓库 Release 里的几样东西。体积十几个 G 的整包不适合直传,所以走「说明文档 + 网盘快捷方式」的组合。
|
||||||
|
|
||||||
| 文件 | 用途 |
|
| 文件 | 用途 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| `publish_release.py` | **远程发版脚本**:校验 tag → 建/更新 Release → 上传附件(见下) |
|
||||||
| `发布说明模板.md` | Release 正文模板:本次更新、下载表格、安装步骤、说明 |
|
| `发布说明模板.md` | Release 正文模板:本次更新、下载表格、安装步骤、说明 |
|
||||||
| `网盘下载.url` | Windows 网络快捷方式,双击直接打开网盘页面;作为附件上传 |
|
| `网盘下载.url` | Windows 网络快捷方式,双击直接打开网盘页面;作为附件上传 |
|
||||||
|
|
||||||
## 发版步骤
|
## 一条命令发版(推荐)
|
||||||
|
|
||||||
|
Gitea 的 Release 全流程都能走 REST API,不用点网页。脚本只用 Python 标准库,不需要装任何依赖。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) token 放环境变量(只对当前窗口有效,别写进 .bat 提交上去)
|
||||||
|
set GITEA_TOKEN=xxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
# 2) 先干跑:只打印计划,不产生任何写操作
|
||||||
|
python docs/release/publish_release.py --version v2026.11.06 --dry-run
|
||||||
|
|
||||||
|
# 3) 正式发(说明正文从模板读)
|
||||||
|
python docs/release/publish_release.py --version v2026.11.06 --notes-file docs/release/发布说明模板.md
|
||||||
|
```
|
||||||
|
|
||||||
|
它按六步走,任何一步不对就停下,不会留下半个 Release:
|
||||||
|
|
||||||
|
| 步骤 | 做什么 | 失败是否中止 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 解析本地 tag,与远端 tag 双向比对指向的提交 | **是** |
|
||||||
|
| 2 | 读说明正文(`--notes-file` 优先,其次 `--notes`) | — |
|
||||||
|
| 3 | 该 tag 已有 Release 就更新,没有就创建 | **是** |
|
||||||
|
| 4 | 检查附件;`.url` 里若还是 `REPLACE_WITH_YOUR_LINK` 就拒绝 | **是** |
|
||||||
|
| 5 | 上传附件;同名先删再传,**可以反复执行** | **是** |
|
||||||
|
| 6 | 回读自检(附件清单、是否草稿),打印 `html_url` | — |
|
||||||
|
|
||||||
|
常用参数:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
--draft # 先存草稿(网页上只有自己可见),确认后去掉 --draft 重跑即转正式
|
||||||
|
--prerelease # 标记预发布
|
||||||
|
--expect-commit 3b240ec # 强制要求 tag 指向该提交,比默认更严
|
||||||
|
--asset 某个文件 # 可重复;默认只传 docs/release/网盘下载.url
|
||||||
|
--no-asset # 只发说明,不传附件
|
||||||
|
--insecure # 自签证书的内网 Gitea 才需要
|
||||||
|
--api / --repo # 换服务器或换仓库时用
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么第 1 步要卡 tag**:2026.11.05 发版时,Release 挂着的 tag 指向一个**落后 12 个提交**的旧提交,源码包于是是旧代码,从网页上完全看不出来。脚本现在把这件事挡在发版之前,`--expect-commit` 还能多加一道。
|
||||||
|
|
||||||
|
> 实测记录(2026-11-05):临时 tag 跑完整流程,创建 `201` → 上传 `201` → 下载 `200`(拿到正确的 `.url` 内容)→ 重复执行只保留一个同名附件 → 删除干净。字段名必须是 `attachment`,用 `file` 会返回 `500 http: no such file`。
|
||||||
|
|
||||||
|
## 手工发版(不想用脚本时)
|
||||||
|
|
||||||
1. 复制 `发布说明模板.md` 的内容到 Release 描述,按本次改动填「本次更新」
|
1. 复制 `发布说明模板.md` 的内容到 Release 描述,按本次改动填「本次更新」
|
||||||
2. 核对下载链接:模板里的下载表格已填当前百度网盘地址与提取码,换盘或重新分享时同步替换
|
2. 核对下载链接:模板里的下载表格已填当前百度网盘地址与提取码,换盘或重新分享时同步替换
|
||||||
@@ -23,9 +66,11 @@
|
|||||||
|
|
||||||
4. 附件区上传 `网盘下载.url`(注意:不是把整个网盘文件传上来)
|
4. 附件区上传 `网盘下载.url`(注意:不是把整个网盘文件传上来)
|
||||||
5. 检查版本号与更新日志:`launcher/server.py` 的 `CURRENT_VERSION`、发布包根目录的 `更新日志.txt`
|
5. 检查版本号与更新日志:`launcher/server.py` 的 `CURRENT_VERSION`、发布包根目录的 `更新日志.txt`
|
||||||
|
6. **确认 tag 指向**:`git log --oneline -1 v2026.11.xx` 与 `git ls-remote --tags origin` 里的 `^{}` 是同一个 sha
|
||||||
|
|
||||||
## 注意
|
## 注意
|
||||||
|
|
||||||
- 仓库里的 `网盘下载.url` 现在指向**真实分享**(百度网盘,提取码 `s33r`);重新分享会生成新链接,记得同步改 `.url` 与模板表格,避免玩家点开 404
|
- 仓库里的 `网盘下载.url` 现在指向**真实分享**(百度网盘,提取码 `s33r`);重新分享会生成新链接,记得同步改 `.url` 与模板表格,避免玩家点开 404
|
||||||
- 传完自己双击验证一次,确认能打开网盘页面而不是 404
|
- 传完自己双击验证一次,确认能打开网盘页面而不是 404
|
||||||
|
- token 只走环境变量 `GITEA_TOKEN` 或 `--token`,**不要**提交到仓库里的任何文件
|
||||||
- 发布包里 `Client\90CN.dll` 与 `Server\runtime\bin\DNF90Server.exe` 必须配套(DLL 指纹绑定),缺一个客户端就连不上
|
- 发布包里 `Client\90CN.dll` 与 `Server\runtime\bin\DNF90Server.exe` 必须配套(DLL 指纹绑定),缺一个客户端就连不上
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""DNF90 远程发版:创建 / 更新 Gitea Release 并上传附件(纯标准库,无需 pip)
|
||||||
|
|
||||||
|
它做的事,等价于你在网页上点的那几下,但可以重复执行、不会手抖:
|
||||||
|
|
||||||
|
1. 校验本地 tag 存在,并解析它指向的提交(可 --expect-commit 强制核对)
|
||||||
|
2. 该 tag 已有 Release → 更新说明;没有 → 创建(--draft 可先存草稿)
|
||||||
|
3. 上传附件(默认 docs/release/网盘下载.url),同名附件自动替换
|
||||||
|
4. 打印 Release 地址,附一次自检结果
|
||||||
|
|
||||||
|
用法
|
||||||
|
----
|
||||||
|
# token 放环境变量,别写进命令历史
|
||||||
|
set GITEA_TOKEN=xxxxxxxx
|
||||||
|
|
||||||
|
# 先干跑,看它准备做什么(不产生任何写操作)
|
||||||
|
python docs/release/publish_release.py --version v2026.11.06 --dry-run
|
||||||
|
|
||||||
|
# 正式发:说明从文件读,附件用默认的网盘 .url
|
||||||
|
python docs/release/publish_release.py --version v2026.11.06 --notes-file docs/release/发布说明模板.md
|
||||||
|
|
||||||
|
# 存成草稿先看看,确认后再去掉 --draft 重跑(会自动转为正式)
|
||||||
|
python docs/release/publish_release.py --version v2026.11.06 --notes-file x.md --draft
|
||||||
|
|
||||||
|
为什么要单独校验 tag 指向
|
||||||
|
----------------------
|
||||||
|
2026.11.05 发版时踩过:Release 挂在 tag 上,而 tag 指着一个落后 12 个提交的旧提交,
|
||||||
|
源码包于是是旧代码,而且从外表看不出来。所以本脚本默认会拿本地 `git rev-list -n1 <tag>`
|
||||||
|
和远端 tag 解析结果双向比对,对不上直接拒绝发版。
|
||||||
|
|
||||||
|
安全
|
||||||
|
----
|
||||||
|
- token 只从 `--token` 或环境变量 `GITEA_TOKEN` 读,不落盘、不进仓库
|
||||||
|
- `--insecure` 才跳过 TLS 校验(自签证书的内网 Gitea 用),默认严格校验
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
DEFAULT_API = "https://git.hanfuit.com/api/v1"
|
||||||
|
DEFAULT_REPO = "Lingbao/90CNServer"
|
||||||
|
DEFAULT_ASSET = os.path.join("docs", "release", "网盘下载.url")
|
||||||
|
PLACEHOLDER = "REPLACE_WITH_YOUR_LINK"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------- 输出
|
||||||
|
def say(msg=""):
|
||||||
|
try:
|
||||||
|
print(msg)
|
||||||
|
except UnicodeEncodeError: # 老控制台 cp936
|
||||||
|
print(msg.encode("gbk", "replace").decode("gbk", "replace"))
|
||||||
|
|
||||||
|
|
||||||
|
def ok(msg):
|
||||||
|
say(" [OK] " + msg)
|
||||||
|
|
||||||
|
|
||||||
|
def warn(msg):
|
||||||
|
say(" [警告] " + msg)
|
||||||
|
|
||||||
|
|
||||||
|
def die(msg, code=1):
|
||||||
|
say(" [失败] " + msg)
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ HTTP 层
|
||||||
|
class Api:
|
||||||
|
def __init__(self, base, repo, token, insecure=False):
|
||||||
|
self.base = base.rstrip("/")
|
||||||
|
self.repo = repo
|
||||||
|
self.token = token
|
||||||
|
self.ctx = ssl._create_unverified_context() if insecure else ssl.create_default_context()
|
||||||
|
|
||||||
|
def _url(self, path):
|
||||||
|
return "%s/repos/%s%s" % (self.base, self.repo, path)
|
||||||
|
|
||||||
|
def call(self, method, path, payload=None, raw=None, ctype=None):
|
||||||
|
"""返回 (status, 解析后的 json 或 None)。4xx/5xx 不抛异常,交由调用方判断。"""
|
||||||
|
data = None
|
||||||
|
if raw is not None:
|
||||||
|
data = raw
|
||||||
|
elif payload is not None:
|
||||||
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
|
ctype = "application/json"
|
||||||
|
req = urllib.request.Request(self._url(path), data=data, method=method)
|
||||||
|
req.add_header("Authorization", "token " + self.token)
|
||||||
|
req.add_header("Accept", "application/json")
|
||||||
|
if ctype:
|
||||||
|
req.add_header("Content-Type", ctype)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=60, context=self.ctx) as resp:
|
||||||
|
body = resp.read()
|
||||||
|
return resp.status, (json.loads(body.decode("utf-8")) if body.strip() else None)
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
body = exc.read().decode("utf-8", "replace")
|
||||||
|
try:
|
||||||
|
return exc.code, json.loads(body)
|
||||||
|
except Exception:
|
||||||
|
return exc.code, {"message": body[:300]}
|
||||||
|
except urllib.error.URLError as exc:
|
||||||
|
die("连不上 %s:%s" % (self.base, exc.reason))
|
||||||
|
|
||||||
|
def upload(self, release_id, filepath, name=None):
|
||||||
|
"""multipart 上传。注意字段名必须是 attachment —— 用 file 会 500 'no such file'。"""
|
||||||
|
name = name or os.path.basename(filepath)
|
||||||
|
with open(filepath, "rb") as fh:
|
||||||
|
blob = fh.read()
|
||||||
|
boundary = "----DNF90Release%s" % os.urandom(8).hex()
|
||||||
|
ctype = mimetypes.guess_type(name)[0] or "application/octet-stream"
|
||||||
|
parts = []
|
||||||
|
parts.append(("--%s\r\n" % boundary).encode())
|
||||||
|
parts.append(('Content-Disposition: form-data; name="attachment"; filename="%s"\r\n' % name).encode("utf-8"))
|
||||||
|
parts.append(("Content-Type: %s\r\n\r\n" % ctype).encode())
|
||||||
|
parts.append(blob)
|
||||||
|
parts.append(("\r\n--%s--\r\n" % boundary).encode())
|
||||||
|
body = b"".join(parts)
|
||||||
|
q = urllib.parse.urlencode({"name": name})
|
||||||
|
return self.call("POST", "/releases/%s/assets?%s" % (release_id, q), raw=body,
|
||||||
|
ctype="multipart/form-data; boundary=%s" % boundary)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- git
|
||||||
|
def git(*args):
|
||||||
|
try:
|
||||||
|
out = subprocess.run(["git"] + list(args), capture_output=True, text=True,
|
||||||
|
encoding="utf-8", errors="replace")
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
if out.returncode != 0:
|
||||||
|
return None
|
||||||
|
return out.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def local_tag_commit(tag):
|
||||||
|
return git("rev-list", "-n", "1", tag)
|
||||||
|
|
||||||
|
|
||||||
|
def current_branch_commit():
|
||||||
|
return git("rev-parse", "HEAD")
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- main
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="远程创建/更新 Gitea Release 并上传附件")
|
||||||
|
ap.add_argument("--version", required=True, help="tag 名,例如 v2026.11.06")
|
||||||
|
ap.add_argument("--name", help="Release 标题,默认「<version> 更新」")
|
||||||
|
ap.add_argument("--notes", help="Release 说明正文")
|
||||||
|
ap.add_argument("--notes-file", help="从文件读说明正文(优先于 --notes)")
|
||||||
|
ap.add_argument("--asset", action="append", default=None,
|
||||||
|
help="附件路径,可重复;默认 %s" % DEFAULT_ASSET)
|
||||||
|
ap.add_argument("--expect-commit", help="要求 tag 必须指向该提交(默认取本地 tag 解析值)")
|
||||||
|
ap.add_argument("--draft", action="store_true", help="创建为草稿(不公开)")
|
||||||
|
ap.add_argument("--prerelease", action="store_true", help="标记为预发布")
|
||||||
|
ap.add_argument("--no-asset", action="store_true", help="只发说明,不传附件")
|
||||||
|
ap.add_argument("--force", action="store_true", help="允许 .url 里仍是占位地址")
|
||||||
|
ap.add_argument("--dry-run", action="store_true", help="只打印计划,不做写操作")
|
||||||
|
ap.add_argument("--token", help="Gitea token(默认读环境变量 GITEA_TOKEN)")
|
||||||
|
ap.add_argument("--api", default=os.environ.get("GITEA_API", DEFAULT_API))
|
||||||
|
ap.add_argument("--repo", default=os.environ.get("GITEA_REPO", DEFAULT_REPO))
|
||||||
|
ap.add_argument("--insecure", action="store_true", help="跳过 TLS 校验(自签证书)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
token = args.token or os.environ.get("GITEA_TOKEN")
|
||||||
|
if not token:
|
||||||
|
die("缺少 token:设置环境变量 GITEA_TOKEN,或传 --token")
|
||||||
|
if os.environ.get("GITEA_TOKEN") and not args.token:
|
||||||
|
warn("正在使用环境变量里的 token(ps 历史里看不到,但别写进 bat 脚本)")
|
||||||
|
|
||||||
|
api = Api(args.api, args.repo, token, args.insecure)
|
||||||
|
tag = args.version
|
||||||
|
title = args.name or ("%s 更新" % tag)
|
||||||
|
|
||||||
|
say("== 1. 校验 tag ==")
|
||||||
|
local_sha = local_tag_commit(tag)
|
||||||
|
if not local_sha:
|
||||||
|
die("本地找不到了 tag %s:先 `git tag -a %s <commit>` 再推送" % (tag, tag))
|
||||||
|
ok("本地 %s → %s" % (tag, local_sha[:10]))
|
||||||
|
if args.expect_commit and not local_sha.startswith(args.expect_commit):
|
||||||
|
die("tag 指向 %s,与要求的 %s 不符 —— 拒绝发版" % (local_sha[:10], args.expect_commit[:10]))
|
||||||
|
|
||||||
|
head = current_branch_commit()
|
||||||
|
if head and head != local_sha:
|
||||||
|
behind = git("rev-list", "--count", "%s..%s" % (local_sha, head))
|
||||||
|
warn("HEAD 比 tag 多 %s 个提交(这是正常的:tag 只对应已发布的那个版本)" % (behind or "?"))
|
||||||
|
|
||||||
|
status, remote_tag = api.call("GET", "/tags/%s" % urllib.parse.quote(tag))
|
||||||
|
if status == 200:
|
||||||
|
rsha = ((remote_tag or {}).get("commit") or {}).get("sha")
|
||||||
|
if rsha and not rsha.startswith(local_sha[:10]):
|
||||||
|
die("远端 tag 指向 %s,本地是 %s —— 先把 tag 推上去再说" % (rsha[:10], local_sha[:10]))
|
||||||
|
ok("远端 tag 一致(%s)" % (rsha or "?")[:10])
|
||||||
|
elif status == 404:
|
||||||
|
die("远端没有 tag %s:先 `git push origin refs/tags/%s`" % (tag, tag))
|
||||||
|
else:
|
||||||
|
die("查询远端 tag 失败:HTTP %s %s" % (status, (remote_tag or {}).get("message")))
|
||||||
|
|
||||||
|
say("== 2. 说明正文 ==")
|
||||||
|
body = ""
|
||||||
|
if args.notes_file:
|
||||||
|
with open(args.notes_file, "r", encoding="utf-8") as fh:
|
||||||
|
body = fh.read()
|
||||||
|
ok("读取 %s(%d 字符)" % (args.notes_file, len(body)))
|
||||||
|
elif args.notes:
|
||||||
|
body = args.notes
|
||||||
|
ok("使用命令行说明(%d 字符)" % len(body))
|
||||||
|
else:
|
||||||
|
warn("没有说明正文,Release 描述会是空的")
|
||||||
|
|
||||||
|
say("== 3. 检查已有 Release ==")
|
||||||
|
status, exist = api.call("GET", "/releases/tags/%s" % urllib.parse.quote(tag))
|
||||||
|
if status == 200 and isinstance(exist, dict):
|
||||||
|
rid = exist.get("id")
|
||||||
|
suffix = ",并转正式" if (exist.get("draft") and not args.draft) else ""
|
||||||
|
ok("已存在 Release id=%s;本次将更新说明%s" % (rid, suffix))
|
||||||
|
action = "update"
|
||||||
|
have_assets = [a.get("name") for a in (exist.get("assets") or [])]
|
||||||
|
elif status == 404:
|
||||||
|
ok("还没有 Release,本次将创建")
|
||||||
|
action = "create"
|
||||||
|
rid, have_assets = None, []
|
||||||
|
else:
|
||||||
|
die("查询 Release 失败:HTTP %s %s" % (status, (exist or {}).get("message")))
|
||||||
|
|
||||||
|
say("== 4. 附件 ==")
|
||||||
|
assets = [] if args.no_asset else (args.asset or [DEFAULT_ASSET])
|
||||||
|
for path in assets:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
die("附件不存在:%s" % path)
|
||||||
|
size = os.path.getsize(path)
|
||||||
|
if path.lower().endswith(".url"):
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
text = fh.read().decode("utf-8", "replace")
|
||||||
|
if PLACEHOLDER in text and not args.force:
|
||||||
|
die("%s 里还是占位地址 %s,先填真实网盘链接(或用 --force 硬发)" % (path, PLACEHOLDER))
|
||||||
|
state = "(将替换同名)" if os.path.basename(path) in have_assets else ""
|
||||||
|
ok("%s %.1f KB%s" % (path, size / 1024.0, state))
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
say("")
|
||||||
|
say("== 干跑结束,未做任何写操作 ==")
|
||||||
|
say("将执行:%s Release「%s」(tag %s) %s" % (
|
||||||
|
"创建" if action == "create" else "更新", title, tag,
|
||||||
|
",附件 " + str(len(assets)) + " 个" if assets else ",无附件"))
|
||||||
|
return
|
||||||
|
|
||||||
|
say("== 5. 写入 ==")
|
||||||
|
if action == "create":
|
||||||
|
payload = {"tag_name": tag, "target_commitish": local_sha, "name": title,
|
||||||
|
"body": body, "draft": bool(args.draft), "prerelease": bool(args.prerelease)}
|
||||||
|
status, rel = api.call("POST", "/releases", payload=payload)
|
||||||
|
if status not in (201, 200):
|
||||||
|
die("创建失败:HTTP %s %s" % (status, (rel or {}).get("message")))
|
||||||
|
rid = rel.get("id")
|
||||||
|
ok("已创建 Release id=%s" % rid)
|
||||||
|
else:
|
||||||
|
payload = {"name": title, "body": body, "draft": bool(args.draft),
|
||||||
|
"prerelease": bool(args.prerelease)}
|
||||||
|
status, rel = api.call("PATCH", "/releases/%s" % rid, payload=payload)
|
||||||
|
if status not in (200, 201):
|
||||||
|
die("更新失败:HTTP %s %s" % (status, (rel or {}).get("message")))
|
||||||
|
ok("已更新 Release id=%s" % rid)
|
||||||
|
|
||||||
|
for path in assets:
|
||||||
|
name = os.path.basename(path)
|
||||||
|
# 同名先删,保证可重复执行
|
||||||
|
for a in (rel.get("assets") or []):
|
||||||
|
if a.get("name") == name:
|
||||||
|
api.call("DELETE", "/releases/%s/assets/%s" % (rid, a.get("id")))
|
||||||
|
ok("删除同名旧附件 %s" % name)
|
||||||
|
status, res = api.upload(rid, path, name)
|
||||||
|
if status not in (201, 200):
|
||||||
|
die("上传 %s 失败:HTTP %s %s" % (name, status, (res or {}).get("message")))
|
||||||
|
ok("已上传 %s(%.1f KB)" % (name, (res or {}).get("size", 0) / 1024.0))
|
||||||
|
|
||||||
|
say("== 6. 自检 ==")
|
||||||
|
status, final = api.call("GET", "/releases/%s" % rid)
|
||||||
|
if status != 200:
|
||||||
|
warn("回读失败 HTTP %s" % status)
|
||||||
|
else:
|
||||||
|
got = [a.get("name") for a in (final.get("assets") or [])]
|
||||||
|
ok("附件:%s" % (", ".join(got) if got else "无"))
|
||||||
|
ok("状态:%s" % ("草稿" if final.get("draft") else "已发布"))
|
||||||
|
say("")
|
||||||
|
say("Release 地址:%s" % final.get("html_url"))
|
||||||
|
if final.get("draft"):
|
||||||
|
say("提示:当前是草稿,网页上只有你自己可见;去掉 --draft 再跑一次即转正式")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user