feat(release): publish tar.gz/zip archives instead of raw binaries#730
Conversation
) Compress release assets with gzip (~31% ratio, 71MB→22MB). Updater and install scripts now download and extract from archives. - CI: pack tar.gz (unix) / zip (windows) after build, upload archives - updater: assetName() returns archive name, Download() extracts binary - install.sh: download tar.gz → verify → extract → install - install.ps1: download zip → verify → Expand-Archive → install - Security: tar/zip extraction filters for regular files only
hrygo
left a comment
There was a problem hiding this comment.
Code Review — hrygo
Verdict: REQUEST_CHANGES | P0:1 P1:2 P2:2 P3:1
[P0] 旧版自更新通道断裂
internal/updater/updater.go + .github/workflows/release.yml
当前 main 分支的 assetName() 返回裸二进制名(如 hotplex-darwin-arm64)。新 release 只上传 .tar.gz/.zip 压缩包,不再上传裸二进制。所有已部署的旧版本将永久无法通过 hotplex update 自更新到新版本(asset 404)。
建议修复(二选一):
- 过渡期方案:release workflow 同时发布裸二进制和压缩包,至少持续一个版本周期
- fallback 方案:updater 先尝试裸二进制,404 后尝试下载压缩包解压(更健壮)
[P1] install.ps1 解压后未验证文件存在
scripts/install.ps1 — Expand-Archive 后直接 Copy-Item
Expand-Archive 后如果 zip 内结构与预期不符(如多了一层目录),Copy-Item $ExtractedBinary 会抛出 PowerShell 原生错误,对终端用户不友好。
建议:在 Copy-Item 前加 if (-not (Test-Path $ExtractedBinary)) { Write-Err "Binary $BinaryName not found in archive"; return }
[P1] install.sh tar 解压硬编码 member 名
scripts/install.sh — tar -xzf 指定了精确的 member 过滤
tar -xzf "$ARCHIVE_PATH" -C "$WORK_DIR" "hotplex-${OS}-${ARCH}" 如果 archive 内文件名变化(如加目录前缀),tar 不报错但不解压,后续 [[ -f ]] 的 die 信息不明确。
建议:去掉 member 过滤解压整个 archive,或解压后用 find 定位二进制。
[P2] Download() archive temp 文件清理不完整
internal/updater/updater.go — Download 函数
extractBinary 失败时,archive temp 文件残留(只在成功路径 os.Remove(archivePath),错误路径未清理)。
建议:改为 defer os.Remove(archivePath) 确保所有路径都清理。
[P2] tar/zip 元数据不可重现
.github/workflows/release.yml — 打包步骤
tar -czf 和 zip -9 未使用确定性元数据(时间戳/owner),不同构建的 checksum 不一致,影响供应链审计。
建议:tar 加 --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner,zip 加 -X。
[P3] CI apt-get install zip 步骤缺少注释
.github/workflows/release.yml:85
apt-get install zip 只在 matrix.os == 'windows' 时执行,依赖 ubuntu runner 交叉编译的隐含假设。应加注释说明此约束。
总结:P0 是阻塞合并的破坏性变更——必须先解决旧版自更新兼容问题。P1 的脚本健壮性也建议在合入前修复。
hotplex-ai
left a comment
There was a problem hiding this comment.
Code Review — hotplex-ai
Verdict: ❌ REQUEST_CHANGES | P0:1 P1:2 P2:4
P0 — 阻断性 Bug
[P0] Checksum 验证对象不匹配,自更新必失败 (cmd/hotplex/update.go:91 + .github/workflows/release.yml:147)
调用链分析:
release.yml:147对归档文件生成 checksum:sha256sum hotplex-*.tar.gz hotplex-*.zip > checksums.txtDownload()下载归档 → 提取二进制 → 删除归档 → 返回提取后的二进制 temp 路径update.go:91以result.AssetName(="hotplex-darwin-arm64.tar.gz")在 checksums.txt 中查找 hash,但tmpPath指向提取后的裸二进制- 提取后的二进制 SHA256 ≠ 归档的 SHA256 → 每次更新都因 "checksum mismatch" 失败
修复方案(二选一):
- 方案 A(推荐):调整
Download()使其在提取前返回归档路径,调用方先VerifyChecksum(归档)再extractBinary() - 方案 B:CI 额外对归档内二进制生成 checksum(
checksums-binaries.txt),updater 对二进制验证
P1 — 必须修复
[P1] Download 3 分钟 context 被 http.Client.Timeout(30s) 截断 (internal/updater/updater.go:63,152)
New() 设置 http.Client{Timeout: 30s}。Download() 新增了 context.WithTimeout(ctx, 3*time.Minute),但 http.Client.Timeout 覆盖整个请求生命周期(含读 body),实际超时 = min(30s, 3min) = 30s。慢网络下大归档下载会被截断。
修复:在 Download() 中使用独立的 http.Client(无 Timeout 或设更大值),仅依赖 context 控制超时。
[P1] 无向后兼容:已安装旧版二进制无法自更新到首个归档格式 release (internal/updater/updater.go:70-77)
旧版 assetName() 返回 "hotplex-darwin-arm64"(裸二进制名),新版返回 "hotplex-darwin-arm64.tar.gz"。首个归档格式 release 不含裸二进制 asset,旧版 Check() 返回 "no archive found"。
建议:首个归档 release 同时发布裸二进制 + 归档,过渡一个版本后再移除裸二进制。
P2 — 建议改进
[P2] Tar 解压缺路径遍历防御 (internal/updater/updater.go:219) — filepath.Base 匹配虽然安全(写入 os.CreateTemp),但作为自更新安全关键代码,建议显式 continue 掉含 .. 或绝对路径的 hdr.Name
[P2] upload-artifact 包含裸二进制和归档,CI artifact 臃肿 (.github/workflows/release.yml:123-127) — path: dist/ 上传了全部文件。考虑构建后删除裸二进制或过滤 artifact 路径
[P2] 文案 "binary" 未更新为 "archive" — update.go:26 Long 描述、install.sh:3,5 / install.ps1:4,7 头部注释、updater.go:152 注释仍引用 "binary"
[P2] 缺少 Check→Download→VerifyChecksum E2E 测试 (internal/updater/updater_test.go) — 若有此测试,P0 会被立即捕获
2 个 parallel agent 审查,7 个 findings 去重合并为 7 个(过滤 <75 置信度 6 个)
P0: Checksum verification target mismatch — split Download and Extract
so VerifyChecksum runs on the archive before extracting the binary.
P1: http.Client.Timeout(30s) cutting short the 3min download context —
Download now uses a client without Timeout, relying solely on context.
P1: Backward compatibility — Check falls back to legacy raw binary asset
when archive not found; update.go skips Extract for IsLegacyBinary.
P2: Tar path traversal defense — reject entries with ".." or absolute paths.
P2: Release includes both archives and raw binaries for transition period.
P2: E2E coverage — added TestExtract_*, TestCheck_LegacyBinaryFallback.
hrygo
left a comment
There was a problem hiding this comment.
Code Review — hotplex-ai
Verdict: REQUEST_CHANGES | P0:1 P1:1 P2:3
⚠️ 提交为 COMMENT(非 REQUEST_CHANGES)因为当前 auth 是 PR 作者 hrygo,GitHub 不允许作者对自己提交 REQUEST_CHANGES。hotplex-ai token 文件缺失,待修复。
[P0] release.yml 同时上传 archive 和裸二进制 (.github/workflows/release.yml:160-166)
files glob 同时包含 archive 和裸二进制:
dist/hotplex-*.tar.gz/dist/hotplex-*.zip— archive(正确)dist/hotplex-darwin-*/dist/hotplex-linux-*/dist/hotplex-windows-*.exe— 裸二进制(不应上传)
由于 download-artifact 将 build 步骤的全部产物(含裸二进制)合并到 dist/,后三个 glob 会匹配未打包的裸二进制并上传。同时 checksums.txt 仅覆盖 archive(第147行 sha256sum hotplex-*.tar.gz hotplex-*.zip),裸二进制没有 checksum。
违反 PR 目标"publish archives instead of raw binaries"。应删除第164-165行的裸二进制 glob。
[P1] legacy fallback 时 checksum 验证必定失败 (internal/updater/updater.go:146 + cmd/hotplex/update.go:91)
Check() 中 CheckResult.AssetName 固定设为 u.assetName()(archive 名如 hotplex-darwin-arm64.tar.gz),但 legacy fallback 下载的是裸二进制。VerifyChecksum 以 archive 名查找 checksums.txt,而旧 release 的 checksums.txt 只有裸二进制名(如 hotplex-darwin-arm64),查找失败报 "not found in checksums.txt"。
这会破坏从旧版本自更新。修复建议:legacy 模式下将 AssetName 设为 u.binaryName() 或在 VerifyChecksum 中对 legacy 做 fallback 查找。
[P2] Download client 覆盖抵消无 Timeout 意图 (internal/updater/updater.go:166-169)
注释说要创建无 Timeout 的 client 让 context 控制截止时间,但 u.Client != nil 时直接覆盖回带 30s Timeout 的原始 client。3 分钟的 context deadline 形同虚设。建议:复制 u.Client 的 Transport 但清零 Timeout。
[P2] install.sh stat 命令平台兼容性 (scripts/install.sh:156)
stat -f%z(macOS)|| stat -c%s(Linux)的 fallback 在 set -e 下虽然子 shell 不触发退出,但建议用 OS 条件判断选择正确语法,更清晰。
[P2] DRY 违反:install.sh curl/wget 下载逻辑重复 (scripts/install.sh:147-151, 164-168)
curl/wget 下载模式出现两次(archive 和 checksum),可提取为 download_url() 函数。
P0: Remove raw binary upload from release — archives only.
P1: Legacy fallback sets AssetName to binaryName() so VerifyChecksum
finds the correct entry in pre-archive checksums.txt.
P2: Download uses a fresh http.Client inheriting Transport/Jar but
without Timeout, so context deadline controls the download.
- Filter upload-artifact to only include archives (no raw binaries) - Update install script headers from "Binary Installer" to "Installer" - Clarify description: "archive, verifies checksum, extracts and installs"
Round 3 修复总结最新 review (ID 4486466143) 中的 P0/P1 在 round 2 已全部修复,本轮处理剩余 P2: P0 Checksum 验证对象 → 已在 round 2 修复: P2 artifact 过滤 → 本轮修复: Commit: 05678d7 |
Closes #729
Summary
hotplex update) 和安装脚本同步改为下载压缩包后解压Changes
.github/workflows/release.yml: build 步骤后打包 tar.gz (unix) / zip (windows),release 上传压缩包 + checksums 校验压缩包internal/updater/updater.go:assetName()返回压缩包名,新增binaryName(),Download()下载后解压提取二进制(tar.gz / zip 双路径)internal/updater/updater_test.go: 全量更新为新 archive 格式,新增makeTarGz/makeZiphelper,覆盖 tar.gz 解压、zip 解压、archive 内找不到 binary 等场景scripts/install.sh: 下载 tar.gz → 校验 → 解压 → 验证文件存在 → 安装scripts/install.ps1: 下载 zip → 校验 →Expand-Archive解压 → 安装Security
hdr.Typeflag == tar.TypeReg过滤,仅匹配普通文件f.Mode().IsRegular()过滤install.sh解压后验证文件存在才继续rc.Close()避免资源泄漏Test plan
go test ./internal/updater/ -count=1 -race全部通过(28 tests)go vet/golangci-lint/gofmt通过hotplex update能正确下载解压新格式install.sh和install.ps1正确解压安装