selfupdate 是一个为 Go 命令行工具提供自动更新能力的库。它通过 GitHub Releases、Gitea Releases 或 CNB OpenAPI 检测并下载最新版本,实现二进制的原地替换,并支持更新失败时自动回滚。
GOOS/GOARCH 智能匹配对应的 release 产物.zip、.tar.gz、.tgz、.tar.xz、.gz、.xz、.gzip 及裸二进制slog.Logger 适配、文本日志及静默模式go get cnb.cool/svn/selfupdate
要求 Go 1.25 及以上版本。
最简自更新 — 一行代码即可让 CLI 工具自我更新:
package main
import (
"log"
"cnb.cool/svn/selfupdate"
)
const version = "1.2.3"
func main() {
result, err := selfupdate.UpdateSelf(version, "owner/repo")
if err != nil {
log.Fatal(err)
}
switch result.Status {
case selfupdate.UpdateStatusUpdated:
println("已更新至", result.CurrentVersion)
if result.Release != nil {
println("更新日志:", result.Release.ReleaseNotes)
}
case selfupdate.UpdateStatusAlreadyInstalled, selfupdate.UpdateStatusNoRelease:
println("当前无需更新,版本:", result.CurrentVersion)
case selfupdate.UpdateStatusNewerThanTarget:
println("当前版本高于远端 release,保持本地版本:", result.CurrentVersion)
default:
println("未知更新状态:", result.Status)
}
}
更新相关 API 返回 UpdateResult。Status 用来表达“未找到 release / 已安装目标版本 / 本地更新 / 本地版本更高”等显式状态;CurrentVersion 表示调用结束后本地应处于的版本;Release 则在命中远端 release 时提供发布元数据。
完整仓库 URL 会自动推断 github、gitea 或 cnb provider;私有实例或多 provider 混用场景下,建议显式构造 Updater。
通过 Config.APIToken 或环境变量 GITHUB_TOKEN 提供认证令牌,令牌也可从 git config github.token 自动读取。
up, _ := selfupdate.NewUpdater(selfupdate.Config{
Provider: selfupdate.ProviderGitHub,
APIToken: "github_pat_xxx",
})
result, _ := up.UpdateSelf(version, "owner/repo")
up, _ := selfupdate.NewUpdater(selfupdate.Config{
Provider: selfupdate.ProviderGitHubEnterprise,
APIToken: "token",
EnterpriseBaseURL: "https://github.company.com/api/v3",
EnterpriseUploadURL: "https://github.company.com/",
})
支持自建 Gitea 实例,通过 GiteaBaseURL 覆盖默认地址。Token 可通过 Config.APIToken 或环境变量 GITEA_TOKEN 提供。
up, _ := selfupdate.NewUpdater(selfupdate.Config{
Provider: selfupdate.ProviderGitea,
GiteaBaseURL: "https://gitea.example.com",
})
result, _ := up.UpdateSelf(version, "owner/repo")
GiteaBaseURL 接受 https://gitea.example.com 或 https://gitea.example.com/api/v1/ 两种格式,内部会自动规范化。
默认使用 https://api.cnb.cool/,私有实例可通过 CNBBaseURL 覆盖。Token 可通过 Config.APIToken 或环境变量 CNB_TOKEN 提供。
up, _ := selfupdate.NewUpdater(selfupdate.Config{
Provider: selfupdate.ProviderCNB,
})
result, _ := up.UpdateSelf(version, "owner/repo")
先检测是否有新版本,经用户确认后再按选中的 release 执行更新:
latest, found, err := selfupdate.DetectLatest("owner/repo")
if err != nil {
log.Fatal(err)
}
if !found {
println("未找到 release")
return
}
isLatest, err := selfupdate.IsLatest(version, latest.Version)
if err != nil {
log.Fatal(err)
}
if isLatest {
println("当前已是最新版本")
return
}
print("是否更新至 ", latest.Version, "? (y/n): ")
input, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if input != "y\n" {
return
}
result, err := selfupdate.UpdateSelfToRelease(version, latest)
if err != nil {
log.Fatal(err)
}
switch result.Status {
case selfupdate.UpdateStatusUpdated:
println("已更新至", result.CurrentVersion)
case selfupdate.UpdateStatusAlreadyInstalled:
println("当前已安装目标版本", result.CurrentVersion)
case selfupdate.UpdateStatusNewerThanTarget:
println("当前版本高于目标版本", result.CurrentVersion)
}
更新非当前可执行文件的其他命令:
result, err := selfupdate.UpdateCommand("/usr/local/bin/mytool", version, "owner/repo")
if err != nil {
log.Fatal(err)
}
switch result.Status {
case selfupdate.UpdateStatusUpdated:
println("命令已更新至", result.CurrentVersion)
case selfupdate.UpdateStatusAlreadyInstalled, selfupdate.UpdateStatusNoRelease:
println("命令无需更新,当前版本:", result.CurrentVersion)
case selfupdate.UpdateStatusNewerThanTarget:
println("本地命令版本更高,保持现状:", result.CurrentVersion)
}
DetectLatest / DetectVersion 只负责选择远端 release,因此仍返回 *Release。当你想在检测完成后继续执行带状态的更新,可把返回值传给 UpdateSelfToRelease* 或 UpdateCommandToRelease*:
selected, found, err := selfupdate.DetectVersion("owner/repo", "v1.2.4")
if err != nil {
log.Fatal(err)
}
if !found {
println("目标版本不存在")
return
}
result, err := selfupdate.UpdateSelfToRelease(version, selected)
if err != nil {
log.Fatal(err)
}
switch result.Status {
case selfupdate.UpdateStatusUpdated:
println("已切换到", result.CurrentVersion)
case selfupdate.UpdateStatusAlreadyInstalled:
println("目标版本已经安装")
case selfupdate.UpdateStatusNewerThanTarget:
println("当前版本高于目标版本,未回退")
}
使用 ApplyOptions 配置校验和及备份路径:
import "crypto/sha256"
opts := selfupdate.ApplyOptions{
Checksum: sha256.Sum256(expectedBinary)[:],
OldSavePath: "./mycmd.backup",
}
result, err := selfupdate.UpdateSelfWithOptions(version, "owner/repo", opts)
opts := selfupdate.ApplyOptions{}
if err := opts.SetPublicKeyPEM(pemBytes); err != nil {
log.Fatal(err)
}
result, err := selfupdate.UpdateSelfWithOptions(version, "owner/repo", opts)
当 release 产物为 BSDiff patch 文件时,使用 BinaryPatcher 应用增量更新:
opts := selfupdate.ApplyOptions{
Patcher: selfupdate.NewBSDiffPatcher(),
}
result, err := selfupdate.UpdateSelfWithOptions(version, "owner/repo", opts)
使用 SHA2Validator 或 ECDSAValidator 对 release 产物进行附加校验。校验器会自动下载与产物同名的 .sha256 或 .sig 文件:
up, _ := selfupdate.NewUpdater(selfupdate.Config{
Provider: selfupdate.ProviderGitHub,
Validator: &selfupdate.SHA2Validator{},
})
result, _ := up.UpdateSelf(version, "owner/repo")
rel, found, err := selfupdate.DetectVersion("owner/repo", "1.2.3")
默认静默。启用 slog 日志:
selfupdate.SetSlogLogger(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: slog.LevelDebug, })))
或使用内置文本日志:
selfupdate.SetLogger(selfupdate.NewTextLogger(os.Stderr, selfupdate.LevelDebug))
管理 self-update 生命周期的核心结构体。通过 NewUpdater(Config) 构造,或使用 DefaultUpdater() 获取默认 GitHub 实例。
方法:
| 方法签名 | 说明 |
|---|---|
UpdateSelf(current, slug string) (*UpdateResult, error) | 检测并更新当前可执行文件至最新版本 |
UpdateSelfWithOptions(current, slug string, opts ApplyOptions) (*UpdateResult, error) | 同上,支持高级选项 |
UpdateSelfToRelease(current string, rel *Release) (*UpdateResult, error) | 更新当前可执行文件到已选中的 release |
UpdateSelfToReleaseWithOptions(current string, rel *Release, opts ApplyOptions) (*UpdateResult, error) | 同上,支持高级选项 |
UpdateCommand(cmdPath, current, slug string) (*UpdateResult, error) | 检测并更新指定路径的命令 |
UpdateCommandWithOptions(cmdPath, current, slug string, opts ApplyOptions) (*UpdateResult, error) | 同上,支持高级选项 |
UpdateCommandToRelease(cmdPath, current string, rel *Release) (*UpdateResult, error) | 更新指定路径的命令到已选中的 release |
UpdateCommandToReleaseWithOptions(cmdPath, current string, rel *Release, opts ApplyOptions) (*UpdateResult, error) | 同上,支持高级选项 |
UpdateTo(rel *Release, cmdPath string) error | 从已检测到的 Release 下载并替换指定命令 |
UpdateToWithOptions(rel *Release, cmdPath string, opts ApplyOptions) error | 同上,支持高级选项 |
DetectLatest(slug string) (*Release, bool, error) | 检测最新 release 版本 |
DetectVersion(slug, version string) (*Release, bool, error) | 检测指定版本的 release |
Updater 的运行配置:
| 字段 | 类型 | 说明 |
|---|---|---|
Provider | Provider | 发布源类型,可选 ProviderGitHub、ProviderGitHubEnterprise、ProviderGitea、ProviderCNB |
APIToken | string | API 认证令牌 |
EnterpriseBaseURL | string | GitHub Enterprise API 地址(仅 GHE) |
EnterpriseUploadURL | string | GitHub Enterprise 上传地址(仅 GHE) |
GiteaBaseURL | string | Gitea 实例地址,默认 https://gitea.com |
CNBBaseURL | string | CNB 实例地址,默认 https://api.cnb.cool/ |
Validator | Validator | release 产物附加校验器 |
一次更新调用的显式结果:
| 字段 | 类型 | 说明 |
|---|---|---|
Status | UpdateStatus | 本次更新调用的最终状态 |
PreviousVersion | string | 调用时传入并规范化后的当前版本 |
CurrentVersion | string | 调用结束后本地程序应处于的版本 |
Release | *Release | 命中的远端 release;未找到 release 时为 nil |
方法:
| 方法签名 | 说明 |
|---|---|
Updated() bool | 判断本次调用是否实际执行了更新 |
HasRelease() bool | 判断本次调用是否命中了远端 release |
UpdateResult 只用于“会改写二进制”的更新接口;DetectLatest / DetectVersion 仍返回 *Release,因为它们只负责选择目标 release,不承担版本比较与状态表达。
| 常量 | 说明 |
|---|---|
UpdateStatusNoRelease | 未找到可用 release |
UpdateStatusAlreadyInstalled | 目标 release 已经安装在本地 |
UpdateStatusNewerThanTarget | 当前版本高于目标 release |
UpdateStatusUpdated | 已成功完成更新 |
匹配到的 release 产物信息:
| 字段 | 类型 | 说明 |
|---|---|---|
Version | string | release 版本号(不含 v 前缀) |
AssetURL | string | 产物下载地址 |
AssetName | string | 产物文件名 |
ReleaseNotes | string | 更新日志(Markdown) |
URL | string | release 页面地址 |
二进制替换阶段的高级选项:
| 字段 | 类型 | 说明 |
|---|---|---|
Checksum | []byte | 期望的 SHA256 摘要,用于下载后校验 |
OldSavePath | string | 旧二进制备份路径,为空则删除旧文件 |
Patcher | BinaryPatcher | 增量更新 patch 算法 |
Verifier | SignatureVerifier | 签名校验算法 |
PublicKey | crypto.PublicKey | 签名验证公钥 |
方法:
| 方法签名 | 说明 |
|---|---|
CheckPermissions(cmdPath string) error | 预检目标路径是否可写 |
SetPublicKeyPEM(pembytes []byte) error | 从 PEM 编码填充 PublicKey 字段 |
使用 DefaultUpdater() 的快捷封装:
| 函数签名 | 说明 |
|---|---|
UpdateSelf(current, slug string) (*UpdateResult, error) | 自更新至最新版本 |
UpdateSelfWithOptions(current, slug string, opts ApplyOptions) (*UpdateResult, error) | 带选项自更新 |
UpdateSelfToRelease(current string, rel *Release) (*UpdateResult, error) | 自更新到已选中的 release |
UpdateSelfToReleaseWithOptions(current string, rel *Release, opts ApplyOptions) (*UpdateResult, error) | 带选项自更新到已选中的 release |
UpdateCommand(cmdPath, current, slug string) (*UpdateResult, error) | 更新指定命令 |
UpdateCommandWithOptions(cmdPath, current, slug string, opts ApplyOptions) (*UpdateResult, error) | 带选项更新指定命令 |
UpdateCommandToRelease(cmdPath, current string, rel *Release) (*UpdateResult, error) | 更新指定命令到已选中的 release |
UpdateCommandToReleaseWithOptions(cmdPath, current string, rel *Release, opts ApplyOptions) (*UpdateResult, error) | 带选项更新指定命令到已选中的 release |
UpdateTo(assetURL, cmdPath string) error | 从 URL 下载并替换 |
UpdateToWithOptions(assetURL, cmdPath string, opts ApplyOptions) error | 带选项从 URL 下载替换 |
DetectLatest(slug string) (*Release, bool, error) | 检测最新 release |
DetectVersion(slug, version string) (*Release, bool, error) | 检测指定版本 release |
| 函数签名 | 说明 |
|---|---|
CompareVersion(left, right string) (int, error) | 语义化版本比较,返回 -1、0、1 |
IsLatest(current, latest string) (bool, error) | 判断当前版本是否为最新 |
版本号支持 v1.2.3 或 1.2.3 格式,v 前缀会被自动去除。预发布版本(如 1.2.3-beta)在比较中小于对应的正式版本。
| 函数签名 | 说明 |
|---|---|
UncompressCommand(src io.Reader, url, cmd string) (io.Reader, error) | 从压缩流中提取指定命令名的二进制 |
根据 URL 后缀自动识别压缩格式,支持 .zip、.tar.gz、.tgz、.tar.xz、.gz、.xz、.gzip。
type Validator interface {
Validate(release, asset []byte) error
Suffix() string
}
| 实现类型 | Suffix() 返回值 | 说明 |
|---|---|---|
SHA2Validator | .sha256 | SHA256 摘要校验 |
ECDSAValidator | .sig | ECDSA 公钥签名校验 |
校验器会在下载 release 产物的同时,额外下载 {asset}{suffix} 文件(如 mycmd_linux_amd64.tar.gz.sha256),然后调用 Validate 进行校验。
| 函数签名 | 说明 |
|---|---|
NewUpdater(config Config) (*Updater, error) | 根据配置创建 Updater |
DefaultUpdater() *Updater | 返回默认 GitHub Updater |
NewBSDiffPatcher() BinaryPatcher | 创建 BSDiff 增量 patch 算法实例 |
NewECDSASignatureVerifier() SignatureVerifier | 创建 ECDSA 签名校验器 |
NewRSASignatureVerifier() SignatureVerifier | 创建 RSA 签名校验器 |
RollbackError(err error) error | 从更新错误中提取回滚错误(若有) |
| 函数签名 | 说明 |
|---|---|
SetLogger(l Logger) | 设置自定义日志实现 |
SetSlogLogger(logger *slog.Logger) | 设置 slog.Logger 适配 |
CurrentLogger() Logger | 获取当前日志实例 |
NewNopLogger() Logger | 创建静默日志 |
NewSlogLogger(logger *slog.Logger) Logger | 创建 slog 适配器 |
NewTextLogger(writer io.Writer, minLevel Level) Logger | 创建文本日志 |
Level 枚举值:LevelDebug、LevelInfo、LevelWarn、LevelError。
release 产物文件名需遵循以下格式,以便库自动匹配当前平台的二进制:
{command}_{os}_{arch}{.ext} {command}-{os}-{arch}{.ext}
示例:
mycmd_linux_amd64.tar.gzmycmd-darwin-arm64.zipmycmd_windows_amd64.exe.zipWindows 平台下会自动匹配带 .exe 后缀的产物。
使用语义化版本(semver),如 v1.2.3 或 1.2.3。预发布版本(如 v1.2.3-beta)在 DetectLatest 时默认跳过,但 DetectVersion 指定版本号时不受此限制。Release tag 中的常见前缀(如 release-、v)在检测时会被自动剥离。
若使用 Validator,需在 release 中附加校验文件:
{asset}.sha256,内容为 SHA256 摘要的十六进制字符串{asset}.sig,内容为二进制签名| 工具 | 路径 | 说明 |
|---|---|---|
go-get-release | cmd/go-get-release | 类似 go get,安装 release 二进制至 $GOBIN |
detect-latest-release | cmd/detect-latest-release | 命令行查询最新 release 版本及下载地址 |
selfupdate-example | cmd/selfupdate-example | 自更新示例程序 |
DetectLatest / DetectVersion │ ▼ 选择目标 Release │ ▼ Update* / Update*ToRelease* │ ▼ 版本比较(CompareVersion) │ ▼ 下载 Asset(HTTP GET) │ ▼ Validator 校验(可选,下载附加 .sha256/.sig) │ ▼ 解压(UncompressCommand) │ ▼ 原子替换(写入 .new → rename 旧 → rename 新) │ ┌────┴────┐ │ 成功 │ 失败 │ ▼ │ 自动回滚(rename .old 回原路径) ▼ 清理旧文件 / 保存备份 │ ▼ 返回 UpdateResult.Status
| 依赖 | 说明 |
|---|---|
github.com/google/go-github/v30 | GitHub API SDK |
github.com/ulikunitz/xz | XZ 压缩格式支持 |
golang.org/x/crypto | 加密算法(间接依赖) |