logo
0
0
WeChat Login

selfupdate

Go Reference Go Version License

selfupdate 是一个为 Go 命令行工具提供自动更新能力的库。它通过 GitHub Releases、Gitea Releases 或 CNB OpenAPI 检测并下载最新版本,实现二进制的原地替换,并支持更新失败时自动回滚。

目录

功能特性

  • 多平台支持 — 根据运行时 GOOS/GOARCH 智能匹配对应的 release 产物
  • 多 Provider — 支持 GitHub、GitHub Enterprise、Gitea、CNB 四种发布源
  • 多压缩格式 — 支持 .zip.tar.gz.tgz.tar.xz.gz.xz.gzip 及裸二进制
  • 原子替换与回滚 — 更新过程采用写入临时文件 → 重命名的原子操作,失败时自动回滚至旧版本
  • 增量更新 — 内置 BSDiff patch 算法,支持仅下载差异部分进行增量更新
  • 完整性校验 — 支持 SHA256 摘要校验、ECDSA/RSA 签名验证
  • 私有仓库 — 支持 API Token 认证访问私有仓库的 release
  • Symlink 解析 — 自动追踪符号链接,更新实际二进制文件
  • 可定制日志 — 内置日志接口,支持 slog.Logger 适配、文本日志及静默模式

要求

  • Go 1.25 及以上版本
  • 依赖项(自动安装):
    • 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 返回 *UpdateResultStatus 字段用于表达"未找到 release / 已安装目标版本 / 本地已更新 / 本地版本更高"等显式状态;CurrentVersion 表示调用结束后本地应处于的版本;Release 则在命中远端 release 时提供发布元数据。

配置

完整仓库 URL 会自动推断 githubgiteacnb provider;私有实例或多 provider 混用场景下,建议显式构造 Updater

GitHub

默认 provider,支持公有和私有仓库。

up, _ := selfupdate.NewUpdater(selfupdate.Config{ Provider: selfupdate.ProviderGitHub, }) result, _ := up.UpdateSelf(version, "owner/repo")

认证令牌可通过以下方式提供(按优先级排序):

  1. Config.APIToken 字段
  2. GITHUB_TOKEN 环境变量
  3. git config github.token 配置项

GitHub Enterprise

up, _ := selfupdate.NewUpdater(selfupdate.Config{ Provider: selfupdate.ProviderGitHubEnterprise, APIToken: "token", EnterpriseBaseURL: "https://github.company.com/api/v3", EnterpriseUploadURL: "https://github.company.com/", })

Gitea

支持自建 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.comhttps://gitea.example.com/api/v1/ 两种格式,内部会自动规范化。

CNB

默认使用 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[:], }

ECDSA 签名验证

opts := selfupdate.ApplyOptions{} if err := opts.SetPublicKeyPEM(pemBytes); err != nil { log.Fatal(err) } result, err := selfupdate.UpdateSelfWithOptions(version, "owner/repo", opts)

增量更新(BSDiff)

当 release 产物为 BSDiff patch 文件时,使用 BinaryPatcher 应用增量更新:

opts := selfupdate.ApplyOptions{ Patcher: selfupdate.NewBSDiffPatcher(), } result, err := selfupdate.UpdateSelfWithOptions(version, "owner/repo", opts)

Validator 校验器

使用 SHA2ValidatorECDSAValidator 对 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))

API 文档

核心类型

Updater

管理 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同上,支持高级选项

Config

Updater 的运行配置。

字段类型说明
ProviderProvider发布源类型,可选 ProviderGitHubProviderGitHubEnterpriseProviderGiteaProviderCNB
APITokenstringAPI 认证令牌
EnterpriseBaseURLstringGitHub Enterprise API 地址(仅 GHE)
EnterpriseUploadURLstringGitHub Enterprise 上传地址(仅 GHE)
GiteaBaseURLstringGitea 实例地址,默认 https://gitea.com
CNBBaseURLstringCNB 实例地址,默认 https://api.cnb.cool/
ValidatorValidatorrelease 产物附加校验器
Filters[]string用于筛选 release assets 的正则表达式列表

UpdateResult

一次更新调用的显式结果。

字段类型说明
StatusUpdateStatus本次更新调用的最终状态
PreviousVersionstring调用时传入并规范化后的当前版本
CurrentVersionstring调用结束后本地程序应处于的版本
Release*Release命中的远端 release;未找到 release 时为 nil

方法:

方法签名说明
Updated() bool判断本次调用是否实际执行了更新
HasRelease() bool判断本次调用是否命中了远端 release

UpdateResult 只用于"会改写二进制"的更新接口;DetectLatest / DetectVersion 仍返回 *Release,因为它们只负责选择目标 release,不承担版本比较与状态表达。

UpdateStatus

常量说明
UpdateStatusNoRelease未找到可用 release
UpdateStatusAlreadyInstalled目标 release 已经安装在本地
UpdateStatusNewerThanTarget当前版本高于目标 release
UpdateStatusUpdated已成功完成更新

Release

匹配到的 release 产物信息。

字段类型说明
Versionstringrelease 版本号(不含 v 前缀)
AssetURLstring产物下载地址
AssetByteSizeint产物字节大小
AssetIDint64release asset 的 ID;URL-only provider 时可能为 0
ValidationAssetIDint64校验文件的 asset ID;URL-only provider 时可能为 0
ValidationAssetURLstring校验文件的下载地址
URLstringrelease 页面地址
ReleaseNotesstring更新日志(Markdown)
Namestringrelease 名称
PublishedAt*time.Timerelease 发布时间
RepoOwnerstring仓库 owner
RepoNamestring仓库名称

ApplyOptions

二进制替换阶段的高级选项。

字段类型说明
TargetModeos.FileMode新文件权限;为 0 时默认使用 0755
Checksum[]byte期望的 SHA256 摘要,用于校验最终写入的二进制内容
PublicKeycrypto.PublicKey签名验签使用的公钥;为空时不做签名校验
Signature[]byte新二进制签名;为空时不做签名校验
VerifierSignatureVerifier签名校验算法;为空时默认使用 ECDSA
Hashcrypto.Hash校验和与签名摘要算法;为空时默认使用 SHA256
PatcherBinaryPatcher增量更新 patch 算法;为空时按完整二进制替换
OldSavePathstring旧二进制备份路径,为空则删除旧文件

方法:

方法签名说明
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返回更新失败时的回滚错误

接口

Validator

定义 release 附加校验器接口。

方法签名说明
Validate(release, asset []byte) error校验 release 内容与附加资源内容是否匹配
Suffix() string返回用于定位附加校验资源的文件后缀

内置实现:

类型说明后缀
SHA2Validator使用 SHA256 摘要校验 release 内容.sha256
ECDSAValidator使用 ECDSA 公钥校验 release 签名.sig

Logger

定义 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 模式

项目采用 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 Enterprise
  • giteaReleaseClient — Gitea 实例
  • cnbReleaseClient — CNB OpenAPI

更新流程

  1. 检测阶段DetectLatest / DetectVersion → 选择匹配的 release 和 asset
  2. 下载阶段downloadAsset → 下载二进制产物
  3. 校验阶段Validator.Validate → 校验 checksum 或签名(可选)
  4. 解压阶段UncompressCommand → 根据格式解压(如需要)
  5. 替换阶段update.Apply → 原子替换二进制(写入临时文件 → 重命名)
  6. 回滚阶段:若替换失败,自动恢复旧版本

原子替换

更新操作采用原子替换策略确保安全性:

  1. 将新二进制写入临时文件(如 .myapp.*.new
  2. 将旧二进制重命名为备份路径(如 .myapp.old
  3. 将临时文件重命名为目标路径
  4. 若第 3 步失败,自动回滚第 2 步
  5. 成功后删除备份(或保留到 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

Release 资产命名规范

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 配合使用。

发布流程

  1. 更新版本号或确认 tag 版本将通过 -ldflags "-X main.version=..." 注入命令二进制
  2. 更新 CHANGELOG.md(如存在)
  3. 创建并推送 Git 标签:
git tag -a v1.2.3 -m "Release v1.2.3" git push origin v1.2.3
  1. GitHub Actions 会运行 CI、构建 release 资产并上传至 GitHub Releases。 本地可用同一脚本预检:
VERSION=1.2.3 ./scripts/build-release-assets.sh

贡献指南

贡献前请阅读以下指南:

开发流程

  1. Fork 本仓库
  2. 创建特性分支(git checkout -b feature/amazing-feature
  3. 提交更改(git commit -am 'Add amazing feature'
  4. 推送到分支(git push origin feature/amazing-feature
  5. 创建 Pull Request

代码规范

  • 遵循 Go Code Review Comments
  • 所有导出类型、函数、方法必须有完整的 Go doc 注释
  • 注释以导出名称开头,使用完整句子
  • 使用 gofmtgoimports 格式化代码
  • 每个提交应包含完整的测试用例

提交消息规范

遵循 Conventional Commits

feat: add support for Gitea provider fix: resolve symlink handling on Windows docs: update API documentation test: add integration tests for CNB provider

报告问题

遇到问题时,请提供:

  • Go 版本(go version
  • 操作系统和架构
  • 完整的错误信息
  • 复现步骤
  • 相关的 release 资产命名

许可证

本项目采用 MIT 许可证。详见 LICENSE 文件。

About

一个为 Go 命令行工具提供自动更新能力的库。

47.59 MiB
0 forks0 stars1 branches8 TagREADMEMIT license
Language
Go98.9%
Shell1.2%