Files
HW-Debug-Tools/HDMI-Tool/src/publish_gitea.py
T

148 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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}')
# Gitea 版本差异: assets 可能是 {items:[...]} 或纯数组
assets = rel.get('assets') or []
if isinstance(assets, dict):
assets = assets.get('items', [])
for asset in assets:
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()