GitHub
curl
jq

privateなGitHub Releaseページのリリース物をcurl+jqでダウンロードするワンライナー

More than 1 year has passed since last update.

はじめに

Goでツールを書いてると、ビルド済みのバイナリをどうやって配布するのか問題があります。
一番手軽なのはGitHubのReleaseページにリリース物を添付ファイルとしてアップロードしておいて、curlのワンライナーでダウンロードしてもらうという方法です。例えば、publicなリポジトリ minamijoyo/myaws だとこんなかんじです。

$ curl -sLJO https://github.com/minamijoyo/myaws/releases/download/v0.0.8/myaws_v0.0.8_darwin_amd64.tar.gz

しかしながらprivateなリポジトリだとこの方法はGitHubの認証を超えられなくて使えません。
社内向けのツールなどprivateなリポジトリに置いたものも配布したいので、これなんとかならんかなぁと思って試行錯誤した結果、curl+jqでダウンロードするワンライナーを編み出したので共有します。

結論

$ curl -sLJO -H 'Accept: application/octet-stream' \
    "https://$GITHUB_TOKEN@api.github.com/repos/minamijoyo/myaws/releases/assets/$( \
      curl -sL https://$GITHUB_TOKEN@api.github.com/repos/minamijoyo/myaws/releases/tags/v0.0.8 \
        | jq '.assets[] | select(.name | contains("darwin")) | .id')"

説明の都合上、先ほどと同じpublicなリポジトリですが、この方法ならprivateなリポジトリでもダウンロードできます。
$GITHUB_TOKEN はprivateなリポジトリにアクセス可能なGitHubのアクセストークンを発行してください。
minamijoyo/myaws がリポジトリ名、 v0.0.8 はリリースタグ、 darwin はファイル名からMac用のものを取得しているので、適宜読み替えてください。

解説

見ての通り、GitHubのAPIをcurlで叩いてるだけなんですが、基本的な戦略としては

https://api.github.com/repos/:owner/:repo/releases/assets/:id

'Accept: application/octet-stream' をHTTPヘッダにセットして送ると、リリース物がダウンロードできます。(ちなみにこのヘッダをセットしないとただのメタデータのJSONが返ってきます)

curlからGitHub APIを叩くときにアクセストークンをセットするには https://$GITHUB_TOKEN@api.github.com というようにURLのホスト名の部分にセットすればよいです。ただのシェル上でのコマンド実行なので環境変数などの埋め込みも可能です。

curl -sLJO -H のオプションの意味は詳しくはmanを読んでもらえばよいですが、
簡単に補足しておくと、それぞれ

  • s : サイレントモードで余分な出力を抑制
  • L : リダイレクトされた場合はLocation先を追従してアクセス
  • O : リモートファイルをローカルに保存する
  • J : O と組み合わせて、保存するファイル名はURLではなく、リモート側から提示されたContent-Dispositionヘッダのfilenameをファイル名を使う
  • H : HTTPリクエストヘッダにセットする

ところで、上記のダウンロードURLにアクセスするには、URLに含まれているassetsのidを特定する必要があります。
この情報は、以下のURLから特定のタグに紐付くリリース情報から取得できます。

https://api.github.com/repos/:owner/:repo/releases/tags/:tag

これで返ってくレスポンスがこんなかんじ。 ちょっと見づらいですが、これをjqでパースしたいので参考までに全体を貼っておきます。

{
  "url": "https://api.github.com/repos/minamijoyo/myaws/releases/5011817",
  "assets_url": "https://api.github.com/repos/minamijoyo/myaws/releases/5011817/assets",
  "upload_url": "https://uploads.github.com/repos/minamijoyo/myaws/releases/5011817/assets{?name,label}",
  "html_url": "https://github.com/minamijoyo/myaws/releases/tag/v0.0.8",
  "id": 5011817,
  "tag_name": "v0.0.8",
  "target_commitish": "master",
  "name": null,
  "draft": false,
  "author": {
    "login": "minamijoyo",
    "id": 6985802,
    "avatar_url": "https://avatars.githubusercontent.com/u/6985802?v=3",
    "gravatar_id": "",
    "url": "https://api.github.com/users/minamijoyo",
    "html_url": "https://github.com/minamijoyo",
    "followers_url": "https://api.github.com/users/minamijoyo/followers",
    "following_url": "https://api.github.com/users/minamijoyo/following{/other_user}",
    "gists_url": "https://api.github.com/users/minamijoyo/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/minamijoyo/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/minamijoyo/subscriptions",
    "organizations_url": "https://api.github.com/users/minamijoyo/orgs",
    "repos_url": "https://api.github.com/users/minamijoyo/repos",
    "events_url": "https://api.github.com/users/minamijoyo/events{/privacy}",
    "received_events_url": "https://api.github.com/users/minamijoyo/received_events",
    "type": "User",
    "site_admin": false
  },
  "prerelease": false,
  "created_at": "2016-12-26T01:30:05Z",
  "published_at": "2016-12-26T01:31:36Z",
  "assets": [
    {
      "url": "https://api.github.com/repos/minamijoyo/myaws/releases/assets/2885298",
      "id": 2885298,
      "name": "myaws_v0.0.8_darwin_amd64.tar.gz",
      "label": "",
      "uploader": {
        "login": "minamijoyo",
        "id": 6985802,
        "avatar_url": "https://avatars.githubusercontent.com/u/6985802?v=3",
        "gravatar_id": "",
        "url": "https://api.github.com/users/minamijoyo",
        "html_url": "https://github.com/minamijoyo",
        "followers_url": "https://api.github.com/users/minamijoyo/followers",
        "following_url": "https://api.github.com/users/minamijoyo/following{/other_user}",
        "gists_url": "https://api.github.com/users/minamijoyo/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/minamijoyo/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/minamijoyo/subscriptions",
        "organizations_url": "https://api.github.com/users/minamijoyo/orgs",
        "repos_url": "https://api.github.com/users/minamijoyo/repos",
        "events_url": "https://api.github.com/users/minamijoyo/events{/privacy}",
        "received_events_url": "https://api.github.com/users/minamijoyo/received_events",
        "type": "User",
        "site_admin": false
      },
      "content_type": "application/octet-stream",
      "state": "uploaded",
      "size": 5539335,
      "download_count": 1,
      "created_at": "2016-12-26T01:31:38Z",
      "updated_at": "2016-12-26T01:31:42Z",
      "browser_download_url": "https://github.com/minamijoyo/myaws/releases/download/v0.0.8/myaws_v0.0.8_darwin_amd64.tar.gz"
    },
    {
      "url": "https://api.github.com/repos/minamijoyo/myaws/releases/assets/2885297",
      "id": 2885297,
      "name": "myaws_v0.0.8_linux_amd64.tar.gz",
      "label": "",
      "uploader": {
        "login": "minamijoyo",
        "id": 6985802,
        "avatar_url": "https://avatars.githubusercontent.com/u/6985802?v=3",
        "gravatar_id": "",
        "url": "https://api.github.com/users/minamijoyo",
        "html_url": "https://github.com/minamijoyo",
        "followers_url": "https://api.github.com/users/minamijoyo/followers",
        "following_url": "https://api.github.com/users/minamijoyo/following{/other_user}",
        "gists_url": "https://api.github.com/users/minamijoyo/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/minamijoyo/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/minamijoyo/subscriptions",
        "organizations_url": "https://api.github.com/users/minamijoyo/orgs",
        "repos_url": "https://api.github.com/users/minamijoyo/repos",
        "events_url": "https://api.github.com/users/minamijoyo/events{/privacy}",
        "received_events_url": "https://api.github.com/users/minamijoyo/received_events",
        "type": "User",
        "site_admin": false
      },
      "content_type": "application/octet-stream",
      "state": "uploaded",
      "size": 5548098,
      "download_count": 0,
      "created_at": "2016-12-26T01:31:38Z",
      "updated_at": "2016-12-26T01:31:42Z",
      "browser_download_url": "https://github.com/minamijoyo/myaws/releases/download/v0.0.8/myaws_v0.0.8_linux_amd64.tar.gz"
    }
  ],
  "tarball_url": "https://api.github.com/repos/minamijoyo/myaws/tarball/v0.0.8",
  "zipball_url": "https://api.github.com/repos/minamijoyo/myaws/zipball/v0.0.8",
  "body": null
}

この例だと .assets[0].id で取れる 2885298 がほしいidなんだけど、
darwin(=Mac用)とlinuxで2種類のassetが添付されていて、毎回インデックスが 0 かどうかはわからないので、ちょっと工夫してこれをフィルタしてあげる必要があります。
assets[*].name にファイル名が入っているので、

| jq '.assets[] | select(.name | contains("darwin")) | .id'

としてやれば、 namedarwin という文字列を含んでいるassetだけを取り出して id を取得できます。

あとはシェル芸でよくやる $(任意のコマンド) でコマンドの実行結果を文字列として評価して別のコマンドに埋め込めるので、これを利用して、assetのidを抽出した結果を元のダウンロードするcurlコマンドのURLに埋め込んであげればワンライナーの完成です。

おわりに

privateなGitHub Releaseページのリリース物をcurl+jqのワンライナーで取得できるようになりました。
どうぞご利用ください。

あと、ワンライナー長いので、できればこれをprivateなHomebrew tapに組み込みたいのだけど、Accept: application/octet-stream をHTTPヘッダにセットするところがネックで、brewのダウンロード処理のカスタマイズ方法が分からん(´・ω・`)
もしprivateなbrew tapへの組み込みができた人いたら教えてください。

(2017/02/04追記)プライベートなGitHubリポジトリでbrew installできるようになったので、やり方を以下に書いた。
HomebrewでプライベートなGitHubリポジトリのReleaseページから社内ツールを配布する

参考