Sparkleのアプリは、Info.plistのSUFeedURLに書かれたURLから、appcast.xmlを取得する。このURLは、アプリに埋め込まれるため、リリースのたびに変えられない。GitHub Releasesのreleases/latest/download/を使うと、最新のリリースに添付したappcast.xmlを、固定のURLで配信できる。

https://github.com/[ユーザー名]/[リポジトリ名]/releases/latest/download/appcast.xml

このURLは、最新のリリースのappcast.xmlにリダイレクトされる。新しいリリースを作ると、同じURLの先が最新のリリースに切り替わる。実行例はmacOS 27.0.1、GitHub CLI(gh)2.91.0のもの。

appcast.xmlの作り方は、generate_appcastの記事で紹介している。

参考: 【Sparkle】generate_appcastでappcast.xmlを作る

リリースにzipとappcast.xmlを添付する

gh release createで、タグを指定してリリースを作り、zipとappcast.xmlを添付する。

$ gh release create [タグ名] [アプリ名]-1.0.zip appcast.xml \
    --repo [ユーザー名]/[リポジトリ名] --title "[タイトル]" --notes "[説明]"
https://github.com/[ユーザー名]/[リポジトリ名]/releases/tag/[タグ名]

Sparkleは、認証なしでappcast.xmlを取得する。そのため、リポジトリは公開リポジトリにする。プライベートリポジトリのリリースでは、アセットの取得に認証が必要になる。

参考: 【GitHub CLI】gh repo createでリポジトリ作成からpushまでまとめて実行する

latest/downloadのリダイレクトを確認する

releases/latest/download/[アセット名]は、2段階のリダイレクトを経て、アセットのダウンロードになる。curl -sILで、リダイレクトの流れを確認できる。

$ curl -sIL "https://github.com/[ユーザー名]/[リポジトリ名]/releases/latest/download/appcast.xml" | grep -iE '^(HTTP|location)'
HTTP/2 302
location: https://github.com/[ユーザー名]/[リポジトリ名]/releases/download/[最新のタグ名]/appcast.xml
HTTP/2 302
location: https://release-assets.githubusercontent.com/github-production-release-asset/[...]
HTTP/2 200

1つ目のリダイレクトで、releases/download/[最新のタグ名]/appcast.xmlに移る。このURLは、最新のリリースのタグ名を含む。2つ目のリダイレクトで、署名付きのダウンロードURLに移る。署名付きのURLには、有効期限がある。

-Lを付けずにcurl -sIを実行すると、1つ目のリダイレクト先だけが表示される。latestが、どのタグを指しているかを確認できる。

$ curl -sI "https://github.com/[ユーザー名]/[リポジトリ名]/releases/latest/download/appcast.xml" | grep -i '^location'
location: https://github.com/[ユーザー名]/[リポジトリ名]/releases/download/[最新のタグ名]/appcast.xml

latestの対象はdraftとprereleaseを除く最新のリリース

latestが指すのは、draft(下書き)とprerelease(プレリリース)を除く、最新のリリースである。

prereleaseを作成しても、latestは切り替わらない。

$ gh release create [タグ名] appcast.xml --repo [ユーザー名]/[リポジトリ名] --prerelease --title "[タイトル]" --notes "[説明]"
$ gh release list --repo [ユーザー名]/[リポジトリ名]
[タイトル]	Pre-release	[タグ名]	[作成日時]
[前のタイトル]	Latest	[前のタグ名]	[作成日時]

gh apiで、latestのリリースを確認できる。prereleaseの作成後も、前のリリースのままである。

$ gh api repos/[ユーザー名]/[リポジトリ名]/releases/latest --jq .tag_name
[前のタグ名]

prereleaseは、latest/download/の対象から外れるが、タグを指定したURLでは取得できる。

$ curl -sIL -o /dev/null -w '%{http_code}\n' "https://github.com/[ユーザー名]/[リポジトリ名]/releases/download/[prereleaseのタグ名]/appcast.xml"
200

draftは、--draftで作成する。draftのリリースは、認証なしのAPIの一覧にも、公開ページにも表示されない。公開ページのURLは、404になる。

$ gh release create [タグ名] appcast.xml --repo [ユーザー名]/[リポジトリ名] --draft --title "[タイトル]" --notes "[説明]"
https://github.com/[ユーザー名]/[リポジトリ名]/releases/tag/untagged-[ハッシュ]

draftは、作成した時点ではタグが作られず、URLの末尾がuntagged-から始まる。latestにも影響しない。

リリースの前にappcast.xmlを確認したいときは、draftやprereleaseで作成する方法がある。latestが切り替わらないため、既存のユーザーのアプリには、影響しない。

リリースの作成から反映までに時間がかかる

新しいリリースの作成から、releases/latest/download/の指す先が新しいタグへ切り替わるまでは、時間を要する。gh release listとgh apiは、作成の直後から、新しいリリースをLatestとして表示する。一方、latest/download/のリダイレクトは、数十秒から数分の間、前のタグを指したままだった。

$ gh release list --repo [ユーザー名]/[リポジトリ名]
[新しいタイトル]	Latest	[新しいタグ名]	[作成日時]
$ curl -sI "https://github.com/[ユーザー名]/[リポジトリ名]/releases/latest/download/appcast.xml" | grep -i '^location'
location: https://github.com/[ユーザー名]/[リポジトリ名]/releases/download/[前のタグ名]/appcast.xml

反映の途中で、新しいタグのアセットのURLにアクセスすると、404が返る。この404は、しばらく残る場合があった。新しいzipのURLを反映の前に確認すると、反映の後も、しばらく404になる。

リリースの確認では、反映の遅延を考慮する。

  • リリースの正しさは、gh api repos/[ユーザー名]/[リポジトリ名]/releases/latestで確認する
  • latest/download/のURLは、1ホップ目のlocationが新しいタグに切り替わってから確認する
  • 切り替わる前に、アセットのURLへ何度もアクセスしない

旧バージョンのURLは失われる

appcast.xmlには、旧バージョンのエントリが含まれる。SUFeedURLのディレクトリがreleases/latest/download/であれば、旧バージョンのzipのurlも、releases/latest/download/になる。

<enclosure url="https://github.com/[ユーザー名]/[リポジトリ名]/releases/latest/download/[アプリ名]-1.0.zip" ... />

最新のリリースに、旧バージョンのzipが添付されていなければ、このURLは404になる。

$ curl -sIL -o /dev/null -w '%{http_code}\n' "https://github.com/[ユーザー名]/[リポジトリ名]/releases/latest/download/[アプリ名]-1.0.zip"
404

タグを指定したURL(releases/download/[旧タグ名]/[アプリ名]-1.0.zip)であれば、取得できる。

参考: 【Sparkle】古いバージョンから更新できることを動作確認する

$ curl -sIL -o /dev/null -w '%{http_code}\n' "https://github.com/[ユーザー名]/[リポジトリ名]/releases/download/[旧タグ名]/[アプリ名]-1.0.zip"
200

Sparkleは、appcast.xmlの最新のエントリのURLを使って更新する。旧バージョンのエントリのURLが404でも、最新のエントリが取得できれば、更新は成功する。

不要なリリースを削除する

検証用のリリースは、gh release deleteで削除する。1回のコマンドで指定できるタグは、1つだけである。--cleanup-tagを付けると、タグも削除される。

$ gh release delete [タグ名] --cleanup-tag --yes --repo [ユーザー名]/[リポジトリ名]

複数のタグを削除するときは、ループで実行する。

$ for t in [タグ名1] [タグ名2]; do gh release delete $t --cleanup-tag --yes --repo [ユーザー名]/[リポジトリ名]; done

draftのリリースには、タグがない。--cleanup-tagを付けると、HTTP 422: Reference does not existが表示されるが、リリースは削除される。