Skip to content

自动更新 ​

用 MyGo 构建的应用可以从签名的发布版本自我更新,发布版本可以放在 GitHub 或任何 HTTPS 服务器上。mygo build 用你的密钥为每个平台的构建签名,应用中的 mygo.Updater 下载新版本,对照内置于应用中的公钥验证签名,并用它替换应用。

设置 ​

创建用于为更新签名的密钥对:

sh
mygo keygen

它会生成私钥 mygo-update.key 和公钥 mygo-update.pub,写入配置目录下的 mygo/update-keys,并打印位置(-o 可选择其他目录)。请把私钥排除在仓库之外,存放在密码管理器和 CI 的 secrets 中(见 GitHub Actions):已安装的应用只接受用它签名的更新,因此丢失它会让它们无法更新,而得到它的任何人都可以向你的用户推送代码。

把公钥添加到配置中,连同发布版本的位置,这里写在 mygo.config.ts 中(在 mygo.json 中则是 JSON 格式的相同字段):

ts
export default defineConfig({
  version: "1.2.0",
  updates: {
    publicKey: "…the contents of mygo-update.pub…",
    github: "you/my-app",
  },
});

github 是一个公开仓库,其以 v1.2.0 等形式打标签的 release 存放着更新。应用读取其最新 release 的清单文件。

仓库中可能还有其他 release,例如某个命令行工具的 release,它一直是仓库的最新 release。可以给应用的 release 设置自己的 tagPrefix,例如对 desktop-v1.2.0 这样的标签用 desktop-v;此后应用、install.sh 和增量更新改通过 GitHub API 查找以该前缀开头的最新 release(既非草稿也非预发布版本):每次检查一个请求,在 API 对每个地址每小时 60 次请求的限制之内。存在 GH_TOKEN 或 GITHUB_TOKEN 时,mygo build 会用它。

如果使用自己的服务器或 S3 存储桶,请改为设置 url,即某个目录的 HTTPS URL:

ts
export default defineConfig({
  updates: {
    publicKey: "…",
    url: "https://downloads.example.com/my-app",
  },
});

构建与发布 ​

把私钥交给 mygo build,可通过 MYGO_UPDATER_PRIVATE_KEY 环境变量,或在配置中用 updates.privateKey 指定文件(路径,可以 ~/ 开头):

sh
MYGO_UPDATER_PRIVATE_KEY="$(cat path/to/mygo-update.key)" bun run build        # a web frontend
MYGO_UPDATER_PRIVATE_KEY="$(cat path/to/mygo-update.key)" go tool mygo build   # native UI

它在每个平台的安装程序旁还会写入:

  • my-app-1.2.0-darwin-arm64.tar.gz,即安装后的应用,名称取自应用的 slug,
  • 从先前版本生成的增量更新,例如 my-app-1.1.0-to-1.2.0-darwin-arm64.delta,以及
  • update-darwin-arm64.json,即清单文件:版本、其发布说明、日期,以及归档包和每个增量包的 URL、大小和签名。

没有密钥时,mygo build 会构建应用并跳过更新文件,但 Linux 归档包除外,因为 install.sh 会安装它们。mygo build -upload 需要密钥:它会在构建前停止,因为没有清单的 release 是已安装应用无法更新到的。

发布说明取自 CHANGELOG.md 中该版本的小节(updates.changelog 可指定其他文件),位于 ## 1.2.0 标题下,或像 Keep a Changelog 那样的 ## [1.2.0] - 2026-09-19。文件存在时,必须包含该小节。

把文件发布到 updates 指向的位置:

  • GitHub:mygo build -upload 把它们连同安装程序上传到该版本的草稿 release。发布该 release 会使它成为最新 release,应用检查的正是最新 release:它们从 https://github.com/you/my-app/releases/latest/download/ 读取清单。若使用了 tagPrefix,草稿不应成为最新 release(最新位置应留给其他 release):用 gh release edit desktop-v1.2.0 --draft=false --latest=false 发布,或取消勾选"Set as the latest release",应用会通过其标签找到它。
  • S3:mygo build -upload 把它们连同安装程序上传到 updates.s3 的存储桶;见下文。
  • 自己的服务器:把归档包、增量包和清单上传到 url 目录,连同 install.sh、install.ps1 和 SHA256SUMS(包含每个平台的行),并保留先前版本的文件。最后上传清单,以免应用看到文件缺失的清单。

每个平台,例如 darwin-arm64、darwin-universal 或 windows-amd64,都有自己的清单,构建只查看自己那份。

预发布版本 ​

GitHub 标记为预发布的 release,例如 1.3.0-beta.1,永远不会是最新 release,因此已安装的应用会跳过它。有 beta 渠道的应用可以让选择了它的用户通过 mygo.Updater.SetPrereleases(true) 看到预发布版本:它们的检查改为通过 GitHub API 读取版本号最高的非草稿 release,因此能得到下一个预发布版本,以及其后的正式 release。版本比较遵循语义化版本规则,1.3.0-beta.2 早于 1.3.0-beta.10,后者早于 1.3.0。关闭它不会安装更旧的版本:当某个 release 比应用正在运行的预发布版本更新时,应用会再次更新。该选择只在应用运行期间有效;应用应自行保存它(例如存到设置中),并在启动时调用 SetPrereleases。

go
mygo.Updater.SetPrereleases(settings.Beta)

发布预发布版本时请将其标记为预发布,以免它成为每个应用都会检查的最新 release:gh release edit v1.3.0-beta.1 --draft=false --prerelease,或勾选"Set as a pre-release"。检查会从最新的 release 往下读取,直到最新的非预发布 release;通常一次检查只需一个请求,在 API 对每个地址每小时 60 次的限制之内,无论是否使用 tagPrefix。增量包以已发布的 release 为起点,因此运行预发布版本的应用会完整下载下一个版本。无论如何,从 url 更新的应用读取的是服务器上现有的清单。

发布到 S3 ​

当 url 由 Amazon S3 的存储桶,或 Cloudflare R2 等兼容服务的存储桶提供(直接提供或经由 CDN)时,请在 updates.s3 中指明它:

ts
export default defineConfig({
  updates: {
    publicKey: "…",
    url: "https://downloads.example.com/my-app",
    s3: {
      bucket: "downloads",
      prefix: "my-app", // the directory in the bucket that url serves
      endpoint: "https://<account>.r2.cloudflarestorage.com", // not for Amazon S3
    },
  },
});

此后 mygo build -upload 会把安装程序、归档包、增量包和安装脚本上传到那里,最后上传清单,即发布每个平台的更新。它不需要其他工具:凭据来自 AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY,临时凭据还需要 AWS_SESSION_TOKEN;区域取自 s3.region,否则 AWS_REGION(或 AWS_DEFAULT_REGION),再否则 us-east-1,R2 接受该值。清单、安装脚本和 SHA256SUMS 的名称在各版本间保持不变,上传时带有 Cache-Control: no-cache,因此 CDN 会重新检查它们。

在 Amazon S3 上,存储桶名称出现在请求的主机名中;使用 endpoint 时则出现在路径中;对于只把桶名放在主机名中的服务,请将 pathStyle 设为 false,设为 true 则强制使用路径形式。

增量更新 ​

与 Sparkle 一样,应用只下载自其版本以来发生变化的部分,通常只是整个应用的一小部分:Go 可执行文件的新版本大多是在移动代码位置,而二进制补丁能以小得多的篇幅描述这种变化。

当 mygo build 为更新签名时,它会读取每个平台已发布的清单——即应用检查的那份——并下载最近 3 个版本的归档包:已发布的那个,以及其清单中列出的更早版本(updates.deltas 可改变数量,0 表示不生成增量)。它会验证这些归档包是用你的密钥签名的,并为每个旧版本生成到新版本的增量:未变化的文件从已安装的应用复制,变化的文件打补丁(bsdiff),其余文件完整包含。钉扎在 macOS 应用上的公证票据(Contents/CodeResources)总是完整包含:归档包中的应用带有它,但从磁盘镜像复制出的应用没有。清单会列出增量包,以及先前版本的归档包,供下次构建使用。不比归档包更小的增量会被舍弃。因此构建时需要下载已发布的更新;无法下载时,例如第一个版本,就不生成增量并说明原因。

对于当前版本存在增量包的应用,它会下载增量、验证签名,并在自身旁边生成新版本,同时检查每个文件的大小和 SHA-256 与增量给出的值一致,从而确保结果与发布的版本完全相同,包括代码签名。任何一步失败时,例如应用自安装后被改动过,应用会改为下载完整归档包。

更新窗口 ​

updater 插件在所有平台上为应用提供 Mac 用户熟悉的 Sparkle 式更新窗口:它在后台检查更新,显示新版本的发布说明,并提供安装、跳过或稍后提醒用户。

go
import "github.com/egoist/mygo/plugins/updater"

mygo.Use(updater.Plugin)

其页面介绍了"检查更新……"菜单项、它的选项、它保存的偏好设置、它支持的语言,以及 native.Plugin——面向不显示网页的应用,以原生 UI 提供的同一个窗口。

自定义更新 UI ​

mygo.Updater 正是该插件的基础。可以用它构建你自己的界面,例如应用页面中的横幅:

go
// UpdateProgress 告知页面下载进度。
var UpdateProgress = mygo.NewEvent[float64]("update-progress")

func checkForUpdates(ctx context.Context) {
	if !mygo.Updater.Enabled() {
		return
	}
	up, err := mygo.Updater.Check(ctx)
	if err != nil || up == nil {
		return // 离线,或已是最新
	}
	res, _ := mygo.Dialog.Message(mygo.MessageOptions{
		Message: "Version " + up.Version + " is available.",
		Detail:  up.Notes, // Markdown
		Buttons: []string{"Install and Restart", "Later"},
	})
	if res.Button != 0 {
		return
	}
	err = up.Install(ctx, func(downloaded, total int64) {
		UpdateProgress.Broadcast(float64(downloaded) / float64(total))
	})
	if err == nil {
		mygo.App.Relaunch()
	}
}
  • Check 返回更新的版本,带有其 Version、Notes 和 Date;应用已是最新时返回 nil。
  • Install 下载归档包,或正在运行版本对应的增量包,验证签名并替换应用。进度回调收到的是增量包的字节数;增量失败时,会从头开始按归档包的字节数重新计数。正在运行的应用不受影响:新版本在 App.Relaunch 之后或下次启动时运行。在 macOS 上,新的 bundle 会在应用退出后替换应用的 bundle,以便 Dock 保留应用的图块。
  • Enabled 报告应用能否自我更新:构建时启用了更新,且它对安装位置有写权限。开发构建不能自我更新,其中 Check 返回 mygo.ErrUpdatesDisabled。
  • Check 和 Install 走系统设置中的代理,设置了 HTTPS_PROXY 时则走它(见代理)。

自定义更新服务 ​

mygo.Updater 读取 mygo.config.ts 中 updates 指定的订阅源。拥有自己更新服务的应用——需要用户凭据的端点,或运行期间由用户选择的渠道——可以用 mygo.Updater.SetSource 给更新器提供一个数据源代替。数据源返回该平台的清单及清单所指名的文件;更新器以与检查订阅源相同的方式检查它们,因此未用 updates.publicKey 对应密钥签名的归档包或增量包会被拒绝,应用保持原样。数据源提供 mygo build 写出的内容,因此应用仍需带 updates 构建:公钥,以及文件发布位置的 github 或 url。

例如,文件存放在只向已登录用户提供服务的服务器上:把 updates.url 设为其目录,使清单指向它的文件,并在每个请求中加入用户的令牌:

go
// accountUpdates 使用用户令牌下载更新。
type accountUpdates struct{ token string }

func (s accountUpdates) Manifest(ctx context.Context, prereleases bool) (io.ReadCloser, error) {
	// 应用构建目标平台的清单;通用 macOS 构建读取 update-darwin-universal.json。
	return s.Open(ctx, "https://downloads.example.com/my-app/update-"+runtime.GOOS+"-"+runtime.GOARCH+".json")
}

func (s accountUpdates) Open(ctx context.Context, url string) (io.ReadCloser, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+s.token)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, fmt.Errorf("GET %s: %s", url, resp.Status)
	}
	return resp.Body, nil
}

// 用户登录后:
mygo.Updater.SetSource(accountUpdates{token: session.Token})

此后 Check 从数据源读取清单,Install 通过其 Open 下载归档包或增量包;prereleases 是 SetPrereleases 设置的值,供带 beta 渠道的数据源使用。URL 以清单为准,因此从 mygo build 的清单生成自己清单的服务可以用自己的 id 命名文件,只要大小和签名保持不变。SetSource(nil) 回到订阅源。

应用可以更新的位置 ​

应用会替换自身,因此它必须对安装位置有写权限:

  • macOS:应用 bundle 所在处,例如管理员安装到 /Applications,或用户的 ~/Applications。
  • Windows:安装程序的每用户安装,位于 %LOCALAPPDATA%\Programs。
  • Linux:用户的某个目录,例如 install.sh 安装应用的位置 ~/.local/my-app.app,或解压后的构建产物。

由包管理器安装的应用,例如 /opt 中的 Debian 包,则由包管理器负责更新:它们的 Enabled 为 false。