🤖

プライベートリポジトリで管理しているCLIのセルフアップデート機能を実装する

2024/04/22に公開

はじめに

GitHubのプライベートリポジトリでCLIアプリケーションを管理しており、今まではReleasesページからダウンロードできるようになっていました。
しかしながら、更新があるたびにブラウザからダウンロードして、バイナリを置き換えるのは頻度が少ないとはいえ、まあ手間です。
今回は、CLIアプリケーションの機能としてセルフアップデートを実装できたので、手順をまとめます。

手順

Release一覧から最新バージョンの名前を取得

プライベートリポジトリの情報を取得するため、ghコマンドを使います。
ただし、直接ghコマンドを扱うわけではなく、公式がライブラリを提供しているのでこちらを使います。

import (
	"encoding/json"
	"fmt"

	"github.com/cli/go-gh/v2"
	"github.com/cockroachdb/errors"
)

const REPO = "org/repo" // リポジトリ名

type ReleaseInfo struct {
	Name     string `json:"name"`
	IsLatest bool   `json:"isLatest"`
}

func GetLatestReleaseName() (string, error) {
	releaseListRes, _, err := gh.Exec("release", "list", "-R", REPO, "--json", "isLatest,name")
	if err != nil {
		return "", errors.Wrap(err, "failed to get latest release. have you set up gh?")
	}

	var releases []ReleaseInfo
	if err := json.Unmarshal(releaseListRes.Bytes(), &releases); err != nil {
		return "", errors.Wrap(err, "failed to unmarshal release list")
	}

	var latestRelease *ReleaseInfo
	for i := range releases {
		release := releases[i]
		if release.IsLatest {
			latestRelease = &release
			break
		}
	}
	if latestRelease == nil {
		return "", errors.New("failed to find latest release")
	}

	return latestRelease.Name, nil
}

現在のバージョンと最新バージョンを比較

ここではsemverを使って比較します。

import "golang.org/x/mod/semver"

func shouldUpdate(currentVersion, latestVersion string) bool {
	return semver.Compare(currentVersion, latestVersion) < 0
}

成果物を取得

タグ名と成果物名を指定してダウンロードします。

import (
	"github.com/cli/go-gh/v2"
	"github.com/cockroachdb/errors"
)

const ASSET_NAME = "asset" // 成果物名

func fetchRelease(tag string) ([]byte, error) {
	res, _, err := gh.Exec("release", "download", "-O", "-", "-R", REPO, "-p", ASSET_NAME, tag)
	if err != nil {
		return nil, errors.Wrap(err, "failed to download release")
	}

	return res.Bytes(), nil
}

セルフアップデート

go-updateを使用してセルフアップデートを行います。

import (
	"bytes"

	"github.com/cockroachdb/errors"
	"github.com/inconshreveable/go-update"
)

func selfUpdate(b []byte) error {
	if err := update.Apply(bytes.NewReader(b), update.Options{}); err != nil {
		return errors.Wrap(err, "failed to apply update")
	}
	return nil
}

参考

https://cli.github.com/manual/gh
https://github.com/cli/go-gh
https://github.com/inconshreveable/go-update
https://zenn.dev/kamina_zzz/articles/e9d68da8a88cf2

GitHubで編集を提案

Discussion