Files
HomeAgent/deploy/scripts/upload_assets.py
JianFeeeee dbb787a658 refactor(ci,docs): gitcode 镜像仓名收为单一定义点,取值 HomeAgent(实测 200)
gitcode 镜像目标仓名此前在 3 个文件里各写死多份字面量,改名时要遍改且极
易漏(漏一处 = 一个静默死链,而且失败面在 release 里只有一格)。本批只做
「收成单点」,不改任何取值、不碰代码。

三个定义点(每个文件恰好 1 处,消费点全部走变量):
1. .github/workflows/ci.yml → job `sync-gitcode-main` 的 env.GC_REPO:232
   消费点 2 处命令行:git ls-remote 探测、git push main。
2. .github/workflows/release.yml → job `sync-gitcode` 的 env.GC_REPO:375
   消费点 4 处命令行:git ls-remote 探测、push tag、POST /releases、
   upload_assets.py 的 GITCODE_REPO 传入。
   (两个 workflow 是独立文件,不能共享 env,故各自一处定义。)
3. deploy/scripts/upload_assets.py → os.environ.get("GITCODE_REPO", ...) 默认值。
   手工跑脚本时的兜底,与两个 workflow 同值。

为什么取值 JianFeeeee/HomeAgent 而不是 HologramAgent:
GitHub 主仓已改名 HologramAgent,但 **gitcode 侧镜像仓没有跟着改名**。
curl 直连实测(2026-10-21):
  https://gitcode.com/JianFeeeee/HomeAgent      → 200   ← 当前取值
  https://gitcode.com/JianFeeeee/HologramAgent  → 404
指向 HologramAgent 会让每次发版/推 main 都在镜像同步这步红掉,且国内镜像
停止更新。

两个 job 的失败面不同(容易搞混,此处写明):
- release.yml 的 sync-gitcode:needs [prepare, publish]。探测 exit 1 ⇒
  **只红「镜像同步」这一格**;GitHub 侧 release 产物已经发出去了。
- ci.yml 的 sync-gitcode-main:needs [go, race, cross, gui, csrc, docs] 全绿
  后才跑。探测 exit 1 ⇒ **main 的整条 CI 红**(不是一格)。
所以仓名写错的真实后果是「必红一格 + 国内镜像不更新」,不是「发不出包」。

何时切回 HologramAgent:等 gitcode 上把镜像仓真正改名/新建成功、
https://gitcode.com/JianFeeeee/HologramAgent 返回 200 之后,
**只改上面这三行**即可,两个 workflow 的消费点与 runbook 均无需再动。

docs/zh/ci-cd-runbook.md 同步三节:单点化表格(定义处/消费点)、当前取值与
200/404 实测依据、两个 job 的失败面差异;并把 §3.4 的 curl 验活地址与
upload_assets 手工调用示例同步到 HomeAgent(原写法是已 404 的死链)。

判据(含变异,全部实跑):
- 两个 workflow 可被 yaml.safe_load 解析;每个 jobs[..].env.GC_REPO 恰好 1 处
- 每个 step 的 run 块抽出来喂 bash -n,全部无语法错
- 可执行形态里的 gitcode 目标仓名不含 HologramAgent(注释行豁免)
- 变异:把任一 GC_REPO 定义改回 HologramAgent、或把某消费点还原成字面量,
  判据均报红
- 零代码改动:git status 无 .go/.lua/go.mod/go.sum;go build ./... 通过
2026-10-21 19:55:59 +08:00

140 lines
5.5 KiB
Python
Executable File
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
"""上传 release 资产到 gitcode(两步:取签名 URL → PUT 到 OBS)。
用法: upload_assets.py <tag> <token> [file...]
不传 file 时上传 dist/release/ 下全部发布产物。
环境变量:
GITCODE_REPO 目标仓库,默认 JianFeeeee/HomeAgent(SDK 仓传 JianFeeeee/homeagent-sdk)
这是 gitcode 镜像仓名的**唯一定义处**(workflow 里通过 job 级
env.GC_REPO 传入同一个值)。改 gitcode 仓名时改这里 + 两个
workflow 的 GC_REPO,共三处(每个文件各一个定义点)。
取值依据(实测 2026-10-21):gitcode 上 JianFeeeee/HomeAgent
返回 200,JianFeeeee/HologramAgent 返回 404 —— GitHub 主仓
已改名 HologramAgent,但 gitcode 侧镜像仓未跟着改。
ASSET_DIR 资产目录,默认 <repo>/dist/release
为何两步:gitcode 的 release 附件不走 API 直传,而是先向
`releases/<tag>/upload_url` 要一个 OBS 预签名 URL(带 x-obs-* 回调头),
再把文件 PUT 到那个 URL。回调头必须原样透传,否则 OBS 收下了文件但
gitcode 侧不会登记为 release 附件。
"""
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
REPO = os.environ.get("GITCODE_REPO", "JianFeeeee/HomeAgent")
API = "https://gitcode.com/api/v5/repos"
# 发布产物后缀。注意 Windows 安装器是 HomeAgent_v*_win64.exe,
# 与 bin/ 里的裸 .exe 靠 _win64.exe 后缀区分。
ARTIFACT_SUFFIXES = (
".tar.gz",
".zip",
".deb",
".rpm",
".pkg",
"_win64.exe",
# 插件包。之前不在白名单里,会被静默跳过——而 release 本该带上它们,
# 否则用户要自己装 Go + hmapdev 逐插件构建(见 SDK 仓 scripts/build_plugin_bundles.sh)。
".hmap",
# 插件包汇总校验和(与 SHA256SUMS 同性质,独立文件免得混淆内核包与插件)
"SHA256SUMS.plugins",
)
def is_artifact(name: str) -> bool:
return name == "SHA256SUMS" or name.endswith(ARTIFACT_SUFFIXES)
def get_upload_url(tag: str, token: str, filename: str) -> tuple[str, dict]:
q = urllib.parse.urlencode({"file_name": filename})
url = f"{API}/{REPO}/releases/{tag}/upload_url?{q}"
req = urllib.request.Request(url, headers={"private-token": token})
with urllib.request.urlopen(req, timeout=30) as r:
data = json.loads(r.read())
return data["url"], data.get("headers", {})
def put_file(url: str, headers: dict, path: str) -> tuple[int, str]:
size = os.path.getsize(path)
with open(path, "rb") as f:
body = f.read()
req = urllib.request.Request(url, data=body, method="PUT")
for k, v in headers.items():
req.add_header(k, v)
req.add_header("Content-Length", str(size))
try:
# 大文件(Full 变体安装包近 100MB)给足超时。
with urllib.request.urlopen(req, timeout=900) as r:
return r.status, r.read().decode("utf-8", "replace")[:300]
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", "replace")[:300]
except Exception as e: # noqa: BLE001
return 0, f"{type(e).__name__}: {e}"
def project_root() -> str:
"""向上找带 go.mod 的目录作为仓库根。
为何不数 dirname:本脚本初版在 scripts/(深度 1),移到 deploy/scripts/
(深度 2)后写死的两层 dirname 就指向了 deploy/dist/release,上传直接
FileNotFoundError。这正是 v0.7.2 那次 package/ → deploy/packaging/ 打断
PROJECT_ROOT 的同一个坑,改成按标记文件定位以后怎么挑位置都不会错。
"""
d = os.path.dirname(os.path.abspath(__file__))
while d != os.path.dirname(d):
if os.path.exists(os.path.join(d, "go.mod")):
return d
d = os.path.dirname(d)
# 实在找不到(脚本被单独拷出仓库)就回退到 cwd,给 ASSET_DIR 一个机会
return os.getcwd()
def main() -> int:
if len(sys.argv) < 3:
print(__doc__)
return 2
tag, token = sys.argv[1], sys.argv[2]
outdir = os.environ.get("ASSET_DIR") or os.path.join(
project_root(), "dist", "release"
)
if not os.path.isdir(outdir):
print(f"error: 资产目录不存在: {outdir}")
print(" 用 ASSET_DIR=<目录> 显式指定,或先跑构建生成 dist/release/")
return 2
files = sys.argv[3:] or sorted(
f for f in os.listdir(outdir) if is_artifact(f)
)
if not files:
print(f"error: {outdir} 下没有可识别的发布产物")
return 2
print(f"repo={REPO} tag={tag} dir={outdir}", flush=True)
failed = []
for name in files:
path = os.path.join(outdir, name)
if not os.path.isfile(path):
print(f"skip (missing): {name}", flush=True)
continue
mib = os.path.getsize(path) / 1048576
print(f"==> {name} ({mib:.1f} MiB)", flush=True)
try:
url, headers = get_upload_url(tag, token, name)
except Exception as e: # noqa: BLE001
print(f" upload_url FAILED: {e}", flush=True)
failed.append(name)
continue
status, body = put_file(url, headers, path)
ok = 200 <= status < 300
print(f" PUT -> {status} {'OK' if ok else body}", flush=True)
if not ok:
failed.append(name)
print(f"\n{'ALL OK' if not failed else f'{len(failed)} FAILED: ' + ', '.join(failed)}")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())