Skip to content

feat(release): publish tar.gz/zip archives instead of raw binaries#730

Merged
hrygo merged 4 commits into
mainfrom
feat/729-release-archive
Jun 12, 2026
Merged

feat(release): publish tar.gz/zip archives instead of raw binaries#730
hrygo merged 4 commits into
mainfrom
feat/729-release-archive

Conversation

@hrygo

@hrygo hrygo commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Closes #729

Summary

  • Release 产物从裸二进制(69-74MB)改为 tar.gz / zip 压缩包(~22MB),节省 ~70% 下载带宽
  • 自更新 (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/makeZip helper,覆盖 tar.gz 解压、zip 解压、archive 内找不到 binary 等场景
  • scripts/install.sh: 下载 tar.gz → 校验 → 解压 → 验证文件存在 → 安装
  • scripts/install.ps1: 下载 zip → 校验 → Expand-Archive 解压 → 安装

Security

  • tar 解压 hdr.Typeflag == tar.TypeReg 过滤,仅匹配普通文件
  • zip 解压 f.Mode().IsRegular() 过滤
  • install.sh 解压后验证文件存在才继续
  • zip 循环内显式 rc.Close() 避免资源泄漏

Test plan

  • go test ./internal/updater/ -count=1 -race 全部通过(28 tests)
  • go vet / golangci-lint / gofmt 通过
  • 下次 release 发布后验证:下载 tar.gz 大小 ~22MB
  • 验证 hotplex update 能正确下载解压新格式
  • 验证 install.shinstall.ps1 正确解压安装

)

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 hrygo left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)。

建议修复(二选一):

  1. 过渡期方案:release workflow 同时发布裸二进制和压缩包,至少持续一个版本周期
  2. 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.shtar -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 -czfzip -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 hotplex-ai left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

调用链分析:

  1. release.yml:147 对归档文件生成 checksum:sha256sum hotplex-*.tar.gz hotplex-*.zip > checksums.txt
  2. Download() 下载归档 → 提取二进制 → 删除归档 → 返回提取后的二进制 temp 路径
  3. update.go:91result.AssetName(= "hotplex-darwin-arm64.tar.gz")在 checksums.txt 中查找 hash,但 tmpPath 指向提取后的裸二进制
  4. 提取后的二进制 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 hrygo left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
@hrygo hrygo requested a review from hotplex-ai June 12, 2026 15:10
@hrygo

hrygo commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Round 3 修复总结

最新 review (ID 4486466143) 中的 P0/P1 在 round 2 已全部修复,本轮处理剩余 P2:

P0 Checksum 验证对象 → 已在 round 2 修复:Download() 返回 archive 路径,VerifyChecksum 校验 archive,Extract() 是独立步骤
P1 http.Client.Timeout → 已在 round 2 修复:Download 使用独立 client(无 Timeout),仅依赖 context 控制
P1 向后兼容 → 已在 round 2 修复:IsLegacyBinary + binaryName() fallback
P2 Tar 路径遍历防御 → 已在 round 2 修复:!strings.Contains(..) && !filepath.IsAbs(..)

P2 artifact 过滤 → 本轮修复:upload-artifact 改为 path: dist/*.tar.gz + dist/*.zip
P2 文案更新 → 本轮修复:install.sh/install.ps1 头部注释改为 "Installer" + "archive"

Commit: 05678d7

@hrygo hrygo merged commit fb857af into main Jun 12, 2026
6 checks passed
@hrygo hrygo deleted the feat/729-release-archive branch June 12, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: release 压缩包替代裸二进制,节省 ~70% 下载带宽

2 participants