release

package
v0.3.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package release 负责「把某个版本的 handoff 二进制正确落到某个路径」。

职责:

  • 查 GitHub 的 latest release,解出 tag 与本平台资产的下载 URL
  • 下载、校 sha256、解包、自检、原子替换,并把旧二进制留成 .prev

边界:

  • **不决定何时替换**:那是 internal/selfupdate 的事。本包是一个执行器, 调用方说装就装
  • 不知道 agentd、不知道任务、不读 handoff 的配置
  • 不做自动回滚(D10):留下 .prev 供人工 handoff upgrade --rollback

install.go —— 下载、校验、自检、原子替换。

边界:

  • 临时文件**必须**落在目标二进制的同目录:os.Rename 的原子性只在同一 文件系统内成立,从 /tmp rename 到 /usr/local/bin 会因跨设备直接失败
  • 任何一步失败都清干净临时文件:留一份坏二进制在二进制目录里, 下一轮可能被误当成已就绪的 pending
  • 不做自动回滚(D10):只把旧二进制留成 .prev,回退是人工命令
  • 下载与安装是两件事,前者可跨平台、后者必须在目标平台执行

Index

Constants

View Source
const ChecksumsName = "checksums.txt"

ChecksumsName 是校验和文件名,与 .github/workflows/release.yml 产出一致。

View Source
const DefaultAPIBase = "https://api.github.com"

DefaultAPIBase 是 GitHub REST API 的根。

D11:自动更新链路一律打 GitHub 原生 URL,不走自有域名——域名过期、DNS 故障、 重定向规则改错,任何一样都会让所有机器的自动更新一起哑掉。

View Source
const DefaultRepo = "Xsxdot/handoff"

DefaultRepo 是 Release 所在的 GitHub 仓库。

注意 go.mod 的 module path(github.com/Xsxdot/handoff)已与 GitHub owner 一致:`go install github.com/Xsxdot/handoff@latest` 与下载链指向同一个仓库。

View Source
const DownloadBase = "https://github.com"

DownloadBase 是 release 资产的下载根(GitHub 的确定性地址)。

D11 同理:自动更新链路一律打 GitHub 原生地址,不走自有域名。

Variables

This section is empty.

Functions

func Activate

func Activate(newPath, target string) (string, error)

Activate 把新二进制换到目标路径,旧的留成 <target>.prev。

参数:

  • newPath: Fetch 返回的临时文件路径(必须与 target 同目录)
  • target: 目标二进制路径(应已 EvalSymlinks 解析过)

返回:

  • 留存的旧二进制路径
  • 错误:目录不可写、rename 失败

注意:

  • 两次 rename 都是同目录内操作,因而是原子的。中途失败最坏的结果是 「旧的已挪到 .prev、新的还没就位」——此时目标路径暂时缺失, 所以第二次 rename 失败时会把 .prev 挪回来
  • **两次 rename 的顺序在 Windows 上是承重的**:Windows 允许 rename 一个 正在运行的 exe,但不允许覆盖或删除它。所以「先把旧的挪走、再把新的挪进来」 恰好就是 Windows 自更新的标准手法。**不要**把它「优化」成先删后写—— 那在 unix 上照样绿,在 Windows 上当场炸

func AssetName

func AssetName(tag, goos, goarch string) string

AssetName 拼装某平台的资产名。

参数:

  • tag: 版本号,形如 v0.1.0
  • goos / goarch: 目标平台

返回:

  • 资产文件名

注意:

  • 格式必须与 .github/workflows/release.yml 里的产出**逐字一致**。 不一致的症状是查得到版本但下不到东西,且每轮重试
  • 扩展名按平台分(见 archiveExt),install.sh / install.ps1 两边也依赖这条

func AssetURL added in v0.3.0

func AssetURL(repo, tag, name string) string

AssetURL 拼一个 release 资产的下载地址。

参数:

  • repo: owner/name,如 Xsxdot/handoff
  • tag: 版本号,形如 v0.2.3
  • name: 资产文件名,用 AssetName 生成

返回:

  • 完整下载地址

注意:

  • GitHub 的这个地址是**确定性**的,不需要先查 API 就能拼出来。agentd 自拉时用的正是它——api.github.com 有 60 次/小时/IP 的匿名限流, 而多台执行机很可能共用一个代理出口 IP,走 API 迟早互相打架

func CurrentPlatform

func CurrentPlatform() (string, string)

CurrentPlatform 返回当前进程的 goos/goarch,便于调用方少写两个 runtime 引用。

func DesktopAssetName added in v0.3.3

func DesktopAssetName(tag, goos, goarch string) (string, bool)

DesktopAssetName 返回桌面薄壳在某平台的发布物文件名。

参数:tag 形如 v0.3.1;goos/goarch 用 runtime 的取值。 返回:文件名与「该平台有没有薄壳发布物」。ok 为 false 时文件名为空, 调用方必须先判 ok,不能拿空串去拼下载地址。

注意:

  • 前缀是 handoff-desktop_,与 CLI 的 handoff_ 不同。checksums.txt 里两者 并列,用 handoff_* 通配是匹配不到薄壳资产的(release.yml 的注释点名了这一条)。
  • 不与 AssetName 合并:两者的扩展名规则完全不同,薄壳按 goos 选 dmg/zip/AppImage,合并会让两套互不相干的分支挤在一起。
  • 发布流水线只构建 darwin/arm64、windows/amd64、linux/amd64 三种薄壳, 其余平台一律返回 false——判不出就说没有,不猜一个不存在的文件名。

func PrevPath

func PrevPath(target string) string

PrevPath 返回某目标路径对应的旧二进制留存路径。

func Rollback

func Rollback(target string) error

Rollback 把 <target>.prev 换回 target。

注意:

  • 这是 D10 的第二道防线,**只由人工命令触发**,自动更新循环永远不调它

func TempName

func TempName(tag string) string

TempName 返回某版本的临时文件名。

前导点让它在目录列表里不显眼;带上 tag 使多次尝试不同版本时互不覆盖。 Windows 上追加 .exe——selfCheck 要 exec 这个临时文件跑 version, 没有该后缀的文件在 Windows 上起不来。

Types

type Asset

type Asset struct {
	Name string
	URL  string
}

Asset 是一个 release 资产。

type Client

type Client struct {
	HTTP    *http.Client
	APIBase string
	Repo    string
}

Client 查 GitHub release。

func NewClient

func NewClient(tr http.RoundTripper) *Client

NewClient 构造 release 查询 client:30s 超时,打 GitHub 官方端点。

参数:

  • tr: HTTP transport;**nil = 用标准库默认**(认 HTTPS_PROXY 等环境变量), 与本参数加入前的行为一字不差。要走配置里的代理,传 proxycfg.Transport 的产物

注意:

  • 本包不读 handoff 配置(见 package 注释),所以收的是造好的 transport 而不是配置字符串——这条边界是刻意的,别"顺手"改成传 *config.Config
  • 30s 而不是更长:查版本是一个可以失败的后台动作(失败就等下一个 interval), 卡住一个 goroutine 几分钟没有任何好处

func (*Client) Latest

func (c *Client) Latest(ctx context.Context) (Release, error)

Latest 查最新发布。

参数:

  • ctx: 上下文,用于超时与取消

返回:

  • 解析后的 Release
  • 错误:网络失败、非 200、响应畸形

注意:

  • 匿名调用有 60 次/小时/IP 的限流,被限流时返回带 403 的错误。 **调用方不要重试**——interval 本身就是退避(spec §4.7)

type Installer

type Installer struct {
	HTTP *http.Client
	Log  *slog.Logger
	// DownloadBase 是资产下载根,默认 release.DownloadBase。
	// 存在的唯一理由是可测性:不覆盖它,FetchByTag 的测试必须真的打 github.com
	DownloadBase string
}

Installer 执行下载与安装。

func NewInstaller

func NewInstaller(log *slog.Logger, tr http.RoundTripper) *Installer

NewInstaller 构造默认 installer(10 分钟超时,覆盖慢网下的 20MB 下载)。

参数:

  • log: 日志入口
  • tr: HTTP transport;**nil = 用标准库默认**(认 HTTPS_PROXY 等环境变量)

func (*Installer) Fetch

func (i *Installer) Fetch(ctx context.Context, rel Release, destDir string) (string, error)

Fetch 下载本平台资产、校验、解包、自检,返回可供 Activate 的临时文件路径。

参数:

  • ctx: 上下文
  • rel: 目标发布
  • destDir: 临时文件落点,**必须**与目标二进制同目录

返回:

  • 临时二进制的完整路径(已 chmod 0755 并通过自检)
  • 错误:缺资产、下载失败、校验不过、解包失败、自检不过

注意:

  • 任何一步失败都会把临时文件删掉,不留残件
  • 本函数是 FetchArchive + InstallArchive 的本机组合,行为与拆分前一致

func (*Installer) FetchArchive

func (i *Installer) FetchArchive(ctx context.Context, rel Release, goos, goarch string) ([]byte, string, error)

FetchArchive 按指定平台下载资产并校验完整性,返回字节与期望哈希。

参数:

  • ctx: 上下文
  • rel: 目标发布
  • goos / goarch: **目标机器**的平台,不是本机——跨平台推送时远端可能 是 linux/amd64 而本机是 darwin/arm64,必须知道该下哪份资产

返回:

  • tgz: 资产原文(tar.gz 字节,**未经解包**)
  • 期望的 sha256(十六进制小写),**来自 checksums.txt 的声明**—— 这是信任链的第一道校验,消费方把它原样传给 InstallArchive 让两端 比同一个来自 release 的声明,不互相背书
  • 错误:缺资产、下载失败、校验不过

注意:

  • **不解包、不自检**。自检要 exec 执行新二进制,而本机执行别的平台的 二进制必然失败——自检必须在目标平台上做(agentd 收到推送后)
  • 不重试:完整性失败重试只会重下同一份坏数据(spec §4.7)

func (*Installer) FetchByTag added in v0.3.0

func (i *Installer) FetchByTag(ctx context.Context, repo, tag, goos, goarch, wantSum string) ([]byte, error)

FetchByTag 按 tag 拼出下载地址、下载资产并用**给定的** sha256 校验。

参数:

  • ctx: 上下文
  • repo: owner/name
  • tag: 目标版本
  • goos / goarch: 本机平台
  • wantSum: 期望的 sha256(十六进制小写)。自拉模式下**来自协调者下发**

返回:

  • 资产原文(tar.gz / zip 字节,未解包)
  • 错误:下载失败、sha256 不符

注意:

  • 与 FetchArchive 的区别是**不需要 Release 对象、不查 API**:地址由 AssetURL 确定性拼出,wantSum 由调用方给。这让执行机完全不碰 api.github.com(避开 60 次/小时/IP 的匿名限流)
  • wantSum 由调用方给而不是自己去取 checksums,是刻意的:校验和与资产 走两条不同的信任路径,本机代理/镜像被投毒时才抓得住(spec §5.5)。 **别"优化"成自己下 checksums**
  • 不重试:完整性失败重试只会重下同一份坏数据(spec §4.7)

func (*Installer) FetchChecksum added in v0.3.0

func (i *Installer) FetchChecksum(ctx context.Context, rel Release, goos, goarch string) (string, error)

FetchChecksum 只下载 checksums.txt 并解出本平台 CLI 资产的期望哈希。 保留是为了不动既有调用方;新代码请直接用 FetchChecksumFor。

参数:

  • ctx: 上下文
  • rel: 目标发布(需要它的 Assets 里有 checksums.txt 的 URL)
  • goos / goarch: 目标机器的平台

返回:

  • 该平台资产的 sha256(十六进制小写)
  • 错误:缺 checksums 资产、下载失败、文件里没有该资产的行

注意:

  • **不下资产**。这正是自拉模式的省流量点:协调者只下几百字节的 checksums, 20MB 的资产由执行机自己去下(spec §5.5)
  • 一次 upgrade --now 涉及多台机器时,调用方应当只调一次并缓存—— 同一个 release 的 checksums.txt 对所有平台是同一份

func (*Installer) FetchChecksumFor added in v0.3.3

func (i *Installer) FetchChecksumFor(ctx context.Context, rel Release, assetName string) (string, error)

FetchChecksumFor 只下载 checksums.txt,并按调用方给定的资产名解出期望哈希。

参数:

  • ctx:上下文,用于取消与超时
  • rel:目标发布,需要它的 Assets 里有 checksums.txt 的 URL
  • assetName:要查的完整资产名,由调用方给定;checksums.txt 里 CLI 与薄壳 资产并列,按平台推导只能推出其中一种

返回:

  • 指定资产的 sha256(十六进制小写)
  • 错误:缺 checksums 资产、下载失败、文件里没有该资产的行

注意:不下载资产本身;自拉模式只取几百字节的 checksums,20MB 的资产由 执行机自己下载。解析失败的错误会带上要找的资产名,便于定位发布清单问题。

func (*Installer) InstallArchive

func (i *Installer) InstallArchive(tgz []byte, wantSum, wantTag, destDir string) (string, error)

InstallArchive 校验、解包、自检一份已下载的资产,返回可供 Activate 的临时文件路径。

参数:

  • tgz: FetchArchive 返回的资产原文
  • wantSum: 期望的 sha256(十六进制小写),agentd 侧来自 CLI 推来的 query 参数,是信任链的第二道校验(传输完整性)
  • wantTag: 目标版本,自检时拿新二进制 version 首行与它比对
  • destDir: 临时文件落点,**必须**与目标二进制同目录

返回:

  • 临时二进制的完整路径(已 chmod 0755 并通过自检)
  • 错误:校验不过、解包失败、置位失败、自检不过

注意:

  • 任何一步失败都会把临时文件删掉,不留残件

type Release

type Release struct {
	Tag    string
	Assets []Asset
}

Release 是一次发布。

func (Release) AssetFor

func (r Release) AssetFor(goos, goarch string) (Asset, bool)

AssetFor 取本平台的资产。

返回:

  • 资产与是否找到。找不到说明这次发布漏了某平台

func (Release) Checksums

func (r Release) Checksums() (Asset, bool)

Checksums 取校验和文件资产。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL