refactor(HDMI-Tool): 划分 src/assets/release 三区目录, 补齐人读/AI 读双文档与发布流水线
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
创建/更新 Gitea Release 并上传发布 zip(纯标准库, 无需安装依赖)
|
||||
================================================================
|
||||
用法:
|
||||
python publish_gitea.py HDMI-Tool-v0.1.0-win64.zip
|
||||
python publish_gitea.py <zip> --notes-file release/notes-v0.1.0.md
|
||||
python publish_gitea.py <zip> --tag HDMI-Tool-v0.1.0 --replace
|
||||
|
||||
行为:
|
||||
- tag 与版本默认取 zip 文件名(HDMI-Tool-vX.Y.Z-win64.zip → HDMI-Tool-vX.Y.Z)
|
||||
- release 不存在则创建(tag 不存在时 Gitea 在默认分支头自动建 tag);
|
||||
已存在则复用, 仅上传/更新资产
|
||||
- release 上已有同名资产时: 默认跳过, --replace 删旧再传
|
||||
- API token: --token 参数或环境变量 GITEA_TOKEN(需该仓库写权限)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
DEFAULT_NOTES = """## HDMI-Tool {version}
|
||||
|
||||
HDMI 源端调试工具(LT8619C → DLPC3421 转接板配套): EDID 查看/逐字节解析、
|
||||
分辨率切换、纯色与测试图案全屏投屏。
|
||||
|
||||
### 安装
|
||||
下载 `HDMI-Tool-v{version}-win64.zip`, 解压后双击 `HDMI-Tool.exe`(Windows 10/11 x64, 免安装)。
|
||||
|
||||
### 包内容
|
||||
- `HDMI-Tool.exe` — 主程序
|
||||
- `pattern_slots.json` — 预置自定义栏位(指向 bw_*.png)
|
||||
- `bw_*.png` ×5 / `solid_*.png` ×10 — 测试图案
|
||||
- `README.md` — 完整使用说明
|
||||
"""
|
||||
|
||||
|
||||
def git_remote_info():
|
||||
"""从 origin 远程地址解析 API base 与 owner/repo(ssh 端口不影响 https API)"""
|
||||
url = subprocess.check_output(['git', 'remote', 'get-url', 'origin']).decode().strip()
|
||||
m = re.match(r'(?:ssh://git@|git@)([\w.-]+)(?::\d+)?[:/]([\w.-]+)/([\w.-]+?)(?:\.git)?$', url)
|
||||
if not m:
|
||||
sys.exit(f'无法从 origin 解析 Gitea 地址: {url}')
|
||||
host, owner, repo = m.groups()
|
||||
return f'https://{host}', owner, repo
|
||||
|
||||
|
||||
class Api:
|
||||
def __init__(self, base, owner, repo, token, insecure):
|
||||
self.api = f'{base}/api/v1/repos/{owner}/{repo}'
|
||||
self.headers = {'Authorization': f'token {token}'}
|
||||
self.ctx = ssl._create_unverified_context() if insecure else None
|
||||
|
||||
def request(self, method, path, data=None, headers=None):
|
||||
req = urllib.request.Request(self.api + path, data=data, method=method,
|
||||
headers={**self.headers, **(headers or {})})
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=self.ctx) as resp:
|
||||
body = resp.read()
|
||||
return resp.status, json.loads(body) if body else None
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, json.loads(e.read() or b'{}')
|
||||
|
||||
def get_json(self, path):
|
||||
status, data = self.request('GET', path)
|
||||
return data if status == 200 else None
|
||||
|
||||
def upload_asset(self, release_id, filepath, replace):
|
||||
name = os.path.basename(filepath)
|
||||
status, rel = self.request('GET', f'/releases/{release_id}')
|
||||
if status != 200:
|
||||
sys.exit(f'读取 release {release_id} 失败: {status} {rel}')
|
||||
for asset in (rel.get('assets') or {}).get('items', []):
|
||||
if asset['name'] == name:
|
||||
if not replace:
|
||||
print(f'资产 {name} 已存在, 跳过(覆盖请加 --replace)')
|
||||
return
|
||||
self.request('DELETE', f"/releases/{release_id}/assets/{asset['id']}")
|
||||
print(f'已删除旧资产 {name}')
|
||||
boundary = uuid.uuid4().hex
|
||||
payload = open(filepath, 'rb').read()
|
||||
body = (f'--{boundary}\r\n'
|
||||
f'Content-Disposition: form-data; name="attachment"; filename="{name}"\r\n'
|
||||
f'Content-Type: application/octet-stream\r\n\r\n').encode() + payload + \
|
||||
f'\r\n--{boundary}--\r\n'.encode()
|
||||
status, data = self.request(
|
||||
'POST', f'/releases/{release_id}/assets?name={name}', body,
|
||||
{'Content-Type': f'multipart/form-data; boundary={boundary}'})
|
||||
if status != 201:
|
||||
sys.exit(f'上传资产失败: {status} {data}')
|
||||
print(f'已上传资产: {name} ({len(payload) / 1024 / 1024:.1f} MB)')
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('zipfile_path', help='发布 zip(release/ 下)')
|
||||
ap.add_argument('--tag', help='release 标签, 默认取 zip 文件名去掉 -win64.zip')
|
||||
ap.add_argument('--notes-file', help='release 说明 markdown 文件, 默认用内置模板')
|
||||
ap.add_argument('--token', default=os.environ.get('GITEA_TOKEN'), help='API token')
|
||||
ap.add_argument('--replace', action='store_true', help='覆盖已存在的同名资产')
|
||||
ap.add_argument('--insecure', action='store_true', help='跳过 TLS 证书校验(自签证书)')
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.zipfile_path):
|
||||
sys.exit(f'找不到 {args.zipfile_path}')
|
||||
if not args.token:
|
||||
sys.exit('缺少 API token: 传 --token 或设环境变量 GITEA_TOKEN')
|
||||
|
||||
tag = args.tag or re.sub(r'-win64\.zip$', '', os.path.basename(args.zipfile_path))
|
||||
version = tag.removeprefix('HDMI-Tool-v')
|
||||
notes = open(args.notes_file, encoding='utf-8').read() if args.notes_file \
|
||||
else DEFAULT_NOTES.format(version=version)
|
||||
|
||||
base, owner, repo = git_remote_info()
|
||||
api = Api(base, owner, repo, args.token, args.insecure)
|
||||
print(f'目标: {base}/{owner}/{repo} tag={tag}')
|
||||
|
||||
rel = api.get_json(f'/releases/tags/{tag}')
|
||||
if rel:
|
||||
print(f'release 已存在(id={rel["id"]}), 复用')
|
||||
else:
|
||||
status, rel = api.request('POST', '/releases', json.dumps({
|
||||
'tag_name': tag,
|
||||
'name': f'HDMI-Tool v{version}',
|
||||
'body': notes,
|
||||
}).encode(), {'Content-Type': 'application/json'})
|
||||
if status != 201:
|
||||
sys.exit(f'创建 release 失败: {status} {rel}')
|
||||
print(f'release 已创建(id={rel["id"]})')
|
||||
api.upload_asset(rel['id'], args.zipfile_path, args.replace)
|
||||
print(f'完成: {rel.get("html_url", "") or f"{base}/{owner}/{repo}/releases/tag/{tag}"}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user