selfupdate 是一个为 Go 命令行工具提供自动更新能力的库。它通过 GitHub Releases、Gitea Releases 或 CNB OpenAPI 检测并下载最新版本,实现二进制的原地替换,并支持更新失败时自动回滚。
GOOS/GOARCH 智能匹配对应的 release 产物.zip、.tar.gz、.tgz、.tar.xz、.gz、.xz、.gzip 及裸二进制slog.Logger 适配、文本日志及静默模式github.com/google/go-github/v30 — GitHub API 客户端github.com/ulikunitz/xz — XZ 压缩格式支持go get cnb.cool/svn/selfupdate
最简自更新 — 一行代码即可让 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。
默认 provider,支持公有和私有仓库。
up, _ := selfupdate.NewUpdater(selfupdate.Config{
Provider: selfupdate.ProviderGitHub,
})
result, _ := up.UpdateSelf(version, "owner/repo")
认证令牌可通过以下方式提供(按优先级排序):
Config.APIToken 字段GITHUB_TOKEN 环境变量git config github.token 配置项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 配置更新后目标二进制的校验和及备份路径。
Checksum 校验的是最终写入的命令二进制内容;如果 release 产物是 .zip / .tar.gz 等压缩包,这里应填写压缩包内可执行文件的 SHA256,而不是压缩包本身的 SHA256。
import "encoding/hex"
// Checksum 需要原始摘要字节;若发布流程产出的是 64 位 hex 字符串,先解码。
expectedChecksumHex := "0123456789abcdef..." // 替换为完整 64 位 SHA256 hex
expectedChecksum, err := hex.DecodeString(expectedChecksumHex)
if err != nil {
log.Fatal(err)
}
opts := selfupdate.ApplyOptions{
Checksum: expectedChecksum,
OldSavePath: "./mycmd.backup",
}
result, err := selfupdate.UpdateSelfWithOptions(version, "owner/repo", opts)
如果你手上已有期望二进制内容,也可以先计算摘要再传入:
import "crypto/sha256"
sum := sha256.Sum256(expectedBinary)
opts := selfupdate.ApplyOptions{
Checksum: sum[:],
}
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 实例。
方法:
| 方法签名 | 说明 |
|---|---|
DetectLatest(slug string) (*Release, bool, error) | 检测最新 release 版本 |
DetectVersion(slug, version string) (*Release, bool, error) | 检测指定版本的 release |
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 | 同上,支持高级选项 |
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 产物附加校验器 |
Filters | []string | 用于筛选 release assets 的正则表达式列表 |
一次更新调用的显式结果。
| 字段 | 类型 | 说明 |
|---|---|---|
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 | 产物下载地址 |
AssetByteSize | int | 产物字节大小 |
AssetID | int64 | release asset 的 ID;URL-only provider 时可能为 0 |
ValidationAssetID | int64 | 校验文件的 asset ID;URL-only provider 时可能为 0 |
ValidationAssetURL | string | 校验文件的下载地址 |
URL | string | release 页面地址 |
ReleaseNotes | string | 更新日志(Markdown) |
Name | string | release 名称 |
PublishedAt | *time.Time | release 发布时间 |
RepoOwner | string | 仓库 owner |
RepoName | string | 仓库名称 |
二进制替换阶段的高级选项。
| 字段 | 类型 | 说明 |
|---|---|---|
TargetMode | os.FileMode | 新文件权限;为 0 时默认使用 0755 |
Checksum | []byte | 期望的 SHA256 摘要,用于校验最终写入的二进制内容 |
PublicKey | crypto.PublicKey | 签名验签使用的公钥;为空时不做签名校验 |
Signature | []byte | 新二进制签名;为空时不做签名校验 |
Verifier | SignatureVerifier | 签名校验算法;为空时默认使用 ECDSA |
Hash | crypto.Hash | 校验和与签名摘要算法;为空时默认使用 SHA256 |
Patcher | BinaryPatcher | 增量更新 patch 算法;为空时按完整二进制替换 |
OldSavePath | string | 旧二进制备份路径,为空则删除旧文件 |
方法:
| 方法签名 | 说明 |
|---|---|
CheckPermissions(cmdPath string) error | 预检目标路径是否可写 |
SetPublicKeyPEM(pembytes []byte) error | 从 PEM 编码填充 PublicKey 字段 |
使用 DefaultUpdater() 的快捷封装:
| 函数签名 | 说明 |
|---|---|
DefaultUpdater() *Updater | 返回使用默认 GitHub 配置的 Updater |
DetectLatest(slug string) (*Release, bool, error) | 获取指定仓库的最新 release |
DetectVersion(slug, version string) (*Release, bool, error) | 获取指定仓库的目标版本 release |
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 下载并更新 |
CompareVersion(left, right string) (int, error) | 比较两个版本字符串 |
IsLatest(current, latest string) (bool, error) | 判断 current 是否已经大于或等于 latest |
NewBSDiffPatcher() BinaryPatcher | 返回 BSDiff patcher,用于增量更新 |
NewECDSASignatureVerifier() SignatureVerifier | 返回 ECDSA 签名校验器 |
NewRSASignatureVerifier() SignatureVerifier | 返回 RSA 签名校验器 |
RollbackError(err error) error | 返回更新失败时的回滚错误 |
定义 release 附加校验器接口。
| 方法签名 | 说明 |
|---|---|
Validate(release, asset []byte) error | 校验 release 内容与附加资源内容是否匹配 |
Suffix() string | 返回用于定位附加校验资源的文件后缀 |
内置实现:
| 类型 | 说明 | 后缀 |
|---|---|---|
SHA2Validator | 使用 SHA256 摘要校验 release 内容 | .sha256 |
ECDSAValidator | 使用 ECDSA 公钥校验 release 签名 | .sig |
定义 selfupdate 使用的日志接口。
| 方法签名 | 说明 |
|---|---|
Debug(args ...any) | 记录调试级日志 |
Debugf(format string, args ...any) | 记录调试级格式化日志 |
Info(args ...any) | 记录信息级日志 |
Infof(format string, args ...any) | 记录信息级格式化日志 |
Warn(args ...any) | 记录警告级日志 |
Warnf(format string, args ...any) | 记录警告级格式化日志 |
Error(args ...any) | 记录错误级日志 |
Errorf(format string, args ...any) | 记录错误级格式化日志 |
selfupdate/ ├── cmd/ # 命令行工具示例 │ ├── detect-latest-release/ # 检测最新 release 示例 │ ├── go-get-release/ # 获取 release 示例 │ ├── selfupdate-example/ # 自更新完整示例 │ └── internal/cmdutil/ # 命令行工具通用代码 ├── internal/ # 内部包(不对外暴露) │ ├── gitconfig/ # Git 配置读取 │ ├── semver/ # 语义化版本解析 │ ├── update/ # 二进制更新核心逻辑 │ │ ├── binarydist/ # BSDiff 算法实现 │ │ ├── hide_noop.go # 非 Windows 平台隐藏文件(空实现) │ │ └── hide_windows.go # Windows 平台隐藏文件 │ └── update/apply.go # 原子替换与回滚实现 │ update/patcher.go # Patch 算法接口 │ update/verifier.go # 签名校验接口 ├── apply_options.go # ApplyOptions 类型定义 ├── detect.go # 版本检测实现 ├── logger.go # 日志接口与实现 ├── provider.go # Provider 接口与通用逻辑 ├── provider_cnb.go # CNB Provider 实现 ├── provider_gitea.go # Gitea Provider 实现 ├── provider_github.go # GitHub Provider 实现 ├── release.go # Release 类型定义 ├── update.go # 更新核心逻辑 ├── update_result.go # UpdateResult 类型定义 ├── updater.go # Updater 类型与 Config 定义 ├── validate.go # Validator 接口与实现 └── version.go # 版本比较工具函数
项目采用 Provider 模式支持多种 release 源。核心接口为 releaseClient:
type releaseClient interface {
detectVersion(slug string, version string, filters []*regexp.Regexp, validator Validator) (*Release, bool, error)
downloadAsset(rel *Release, validation bool) (io.ReadCloser, error)
}
各 Provider 实现该接口:
githubReleaseClient — GitHub 和 GitHub EnterprisegiteaReleaseClient — Gitea 实例cnbReleaseClient — CNB OpenAPIDetectLatest / DetectVersion → 选择匹配的 release 和 assetdownloadAsset → 下载二进制产物Validator.Validate → 校验 checksum 或签名(可选)UncompressCommand → 根据格式解压(如需要)update.Apply → 原子替换二进制(写入临时文件 → 重命名)更新操作采用原子替换策略确保安全性:
.myapp.*.new).myapp.old)OldSavePath)# 运行所有测试
go test ./...
# 运行测试并查看覆盖率
go test -cover ./...
# 运行特定测试
go test -run TestDetectLatest ./...
# 启用详细输出
go test -v ./...
部分测试需要有效的 GitHub Token:
# 设置环境变量后运行集成测试
export GITHUB_TOKEN=your_token
go test -v -run TestE2E ./...
# 运行 go vet
go vet ./...
# 运行 golangci-lint(如已安装)
golangci-lint run
selfupdate 根据运行时 OS 和架构自动匹配资产文件。支持的命名模式:
# 格式:{命令名}_{GOOS}_{GOARCH}.{扩展名} myapp_linux_amd64.tar.gz myapp_darwin_arm64.zip myapp_windows_amd64.exe.zip # 也支持短横线分隔符 myapp-linux-amd64.tar.gz myapp-darwin-arm64.zip # 支持的扩展名 .zip, .tar.gz, .tgz, .tar.xz, .gz, .xz, .gzip, (无扩展名,裸二进制)
多命令仓库也可以按平台发布 bundle。本仓库的 GitHub Release workflow
会生成 selfupdate_${GOOS}_${GOARCH}.zip,每个 zip 内包含
cmd/* 下所有命令二进制。selfupdate 会先按资产名中的 OS/Arch 后缀选中
bundle,再从压缩包内提取调用方实际要更新的命令名。
每个 release zip 同时生成同名 .sha256 校验文件,可与 SHA2Validator
配合使用。
-ldflags "-X main.version=..."
注入命令二进制CHANGELOG.md(如存在)git tag -a v1.2.3 -m "Release v1.2.3"
git push origin v1.2.3
VERSION=1.2.3 ./scripts/build-release-assets.sh
贡献前请阅读以下指南:
git checkout -b feature/amazing-feature)git commit -am 'Add amazing feature')git push origin feature/amazing-feature)gofmt 或 goimports 格式化代码feat: add support for Gitea provider fix: resolve symlink handling on Windows docs: update API documentation test: add integration tests for CNB provider
遇到问题时,请提供:
go version)本项目采用 MIT 许可证。详见 LICENSE 文件。