2017-03-27

Homebrewの独自リポジトリをGithub Contents APIで自動更新する

HomebrewのFormulaを更新する場合

を変更する必要があります。毎回手作業でやるには面倒な上に、Travis CIなどでバイナリをビルドしてGithubのReleasesページに自動アップロードする処理を書いていたりすると、tarコマンド(GNU or BSD)やアーカイブ時間、UID/GIDの差異によって、tar.gzのバイナリが変わってしまうためハッシュ値取得のところでハマりやすいです。例えば、macOSで作成したtar.gzのハッシュ値とTravisの環境で作成したハッシュ値が異なるので、macOSからFormulaをgit commit/pushで更新する場合は、GithubのReleasesページからダウンロードしてきたものに対してハッシュ値を取る必要があります。

ということで今回はTravis CIでHomebrewの独自リポジトリをGithub Contents APIを使って自動更新する方法を紹介します。

Makefileや.travis.ymlの設定はこちらのリポジトリも参考にしてもらえればと思います↓ tzmfreedom/goroon: Cybozu garoon library and command line interface by golang

Github Contents APIの使い方

Github Contents APIは単一ファイルをピンポイントに取得、作成、更新するAPIになります。作成のAPIの場合は、clone/pull + add + commit + pushを1回のAPIコールで行うことができます。

例えば、ファイル情報の取得は以下のcurlコマンドで実行可能です。

$ curl -i -X GET 'https://api.github.com/repos/{owner}/{repo}/contents/{path}'

レスポンスはこんな感じです↓

{
  "name": "goroon.rb",
  "path": "goroon.rb",
  "sha": "85be41caa3bc8cc3b2b4a639ef479a39fe8fee34",
  "size": 679,
  "url": "https://api.github.com/repos/tzmfreedom/homebrew-goroon/contents/goroon.rb?ref=master",
  "html_url": "https://github.com/tzmfreedom/homebrew-goroon/blob/master/goroon.rb",
  "git_url": "https://api.github.com/repos/tzmfreedom/homebrew-goroon/git/blobs/85be41caa3bc8cc3b2b4a639ef479a39fe8fee34",
  "download_url": "https://raw.githubusercontent.com/tzmfreedom/homebrew-goroon/master/goroon.rb",
  "type": "file",
  "content": "Y2xhc3MgR29yb29uIDwgRm9ybXVsYQogIGRlc2MgIkdhcm9vbiBDb21tYW5k\nIExpbmUgSW50ZXJmYWNlIgogIGhvbWVwYWdlICJodHRwczovL2dpdGh1Yi5j\nb20vdHptZnJlZWRvbS9nb3Jvb24iCgogIEBAdmVyc2lvbiA9ICIwLjEuMCIK\nICB2ZXJzaW9uIEBAdmVyc2lvbgoKICBpZiBIYXJkd2FyZTo6Q1BVLmlzXzY0\nX2JpdD8KICAgIHVybCAiaHR0cHM6Ly9naXRodWIuY29tL3R6bWZyZWVkb20v\nZ29yb29uL3JlbGVhc2VzL2Rvd25sb2FkL3Yje0BAdmVyc2lvbn0vZ29yb29u\nLSN7QEB2ZXJzaW9ufS1kYXJ3aW4tYW1kNjQudGFyLmd6IgogICAgc2hhMjU2\nICcwYTJlYTlhMjM5NTRmODA5YmFiNzM3NzNkM2JmOGY5ZDgzMGE2ZDMwNWI3\nMmRiMmI5NGFjOGY3YTNhNzA2M2RiJwogIGVsc2UKICAgIHVybCAiaHR0cHM6\nLy9naXRodWIuY29tL3R6bWZyZWVkb20vZ29yb29uL3JlbGVhc2VzL2Rvd25s\nb2FkL3Yje0BAdmVyc2lvbn0vZ29yb29uLSN7QEB2ZXJzaW9ufS1kYXJ3aW4t\nMzg2LnRhci5neiIKICAgIHNoYTI1NiAnMzg2YjYzZjI0MWY1YzcwODg4OTU0\nZjhjMDAxM2NiMjdiOGFlY2JmN2JmYzhmNDM0Yzg5MTQ4NGNhNDQyYjU4MycK\nICBlbmQKCiAgZGVmIGluc3RhbGwKICAgIGJpbi5pbnN0YWxsICdnb3Jvb24n\nCiAgZW5kCgogIHRlc3QgZG8KICAgIHN5c3RlbSAiZmFsc2UiCiAgZW5kCmVu\nZAoKCg==\n",
  "encoding": "base64",
  "_links": {
    "self": "https://api.github.com/repos/tzmfreedom/homebrew-goroon/contents/goroon.rb?ref=master",
    "git": "https://api.github.com/repos/tzmfreedom/homebrew-goroon/git/blobs/85be41caa3bc8cc3b2b4a639ef479a39fe8fee34",
    "html": "https://github.com/tzmfreedom/homebrew-goroon/blob/master/goroon.rb"
  }
}

ファイルの中身はcontentプロパティにBase64エンコードされて入っており、gitのblobのsha1はshaプロパティに入っています。

更新は以下のように行います。

$ curl -i -X PUT \
   -H "Content-Type:application/json" \
   -H "Authorization:token {GH_TOKEN}" \
   -d \
'{
  "path":"{path}",
  "sha":"{古いファイルのblobのsha1}",
  "content":"{新しいファイルのbase64エンコード}",
  "message":"{commit message}"
}' \
 'https://api.github.com/repos/{owner}/{repo}/contents/{path}'

blobのsha1はgitのハッシュになり、以下のコマンドで取得可能です。

$ git hash-object {path}

自動更新のスクリプト

Makefileで表現すると以下のようなスクリプトになります。
SHA256_386 = $(shell cat dist/goroon-$(VERSION)-darwin-386.tar.gz | openssl dgst -sha256 | sed 's/^.* //')
SHA256_AMD64 = $(shell cat dist/goroon-$(VERSION)-darwin-amd64.tar.gz | openssl dgst -sha256 | sed 's/^.* //')

release:
	@cat formula/goroon.rb.tmpl | \
	sed -e 's/{VERSION}/$(VERSION)/g' -e 's/{SHA256_AMD64}/$(SHA256_AMD64)/g' \
	  -e 's/{SHA256_386}/$(SHA256_386)/g' > formula.rb
	@wget https://raw.githubusercontent.com/tzmfreedom/homebrew-$(NAME)/master/$(NAME).rb
	@curl -i -X PUT \
	  -H "Content-Type:application/json" \
	  -H "Authorization:token ${GH_TOKEN}" \
	  -d \
	  "{\"path\":\"$(NAME).rb\",\"sha\":\"$$(git hash-object $(NAME).rb)\",\"content\":\"$$(cat formula.rb | openssl enc -e -base64 | tr -d '\n ')\",\"message\":\"Update version $(VERSION)\"}" \
	  'https://api.github.com/repos/tzmfreedom/homebrew-$(NAME)/contents/$(NAME).rb'

まず、cat/sedはリポジトリにformulaのテンプレートを持たせて、sedで置換することでsha1の値やバージョン番号をバインドしています。古いファイルのsha1を取得するために、wgetでmasterブランチのHEADのファイルを取得します。最後にcurlでGithub Contents APIを叩いてファイルを更新しています。

formula/goroon.rb.tmplはこんな感じです↓

class Goroon < Formula
  desc "Garoon Command Line Interface"
  homepage "https://github.com/tzmfreedom/goroon"

  @@version = "{VERSION}"
  version @@version

  if Hardware::CPU.is_64_bit?
    url "https://github.com/tzmfreedom/goroon/releases/download/v#{@@version}/goroon-#{@@version}-darwin-amd64.tar.gz"
    sha256 '{SHA256_AMD64}'
  else
    url "https://github.com/tzmfreedom/goroon/releases/download/v#{@@version}/goroon-#{@@version}-darwin-386.tar.gz"
    sha256 '{SHA256_386}'
  end

  def install
    bin.install 'goroon'
  end

  test do
    system "false"
  end
end

git hash-objectで古いファイル(=wgetしてきたファイル)のgit blobのハッシュを取っています。contentはopensslでbase64エンコードした値をセットしています。opensslのbase64はPEMコンテキストのため、64文字の改行区切りです。そのため、opensslでbase64したあとはtrを噛ませて改行や空白を削除しています。

あとはこのmakeタスクを.travis.ymlのafter_deployあたりで実行すればOKです。

language: go
before_install:
- sudo add-apt-repository ppa:masterminds/glide -y
- sudo apt-get update -q
- sudo apt-get install glide -y
install:
- go get -t ./...
before_deploy:
- make cross-build
- make dist
deploy:
  provider: releases
  api_key:
    secure: {secure_token}
  file_glob: true
  file: dist/*.{tar.gz}
  on:
    repo: tzmfreedom/goroon
    tags: true
after_deploy:
- make release
env:
  global:
  - secure: {secure_token}

env.global.secureのtokenは$ travis encrypt GH_TOKEN='xxxx' --addで追加したトークンです。

所感

暗号化するとはいえ秘密鍵をパブリックリポジトリに置くのは微妙かなーと思いGithub APIで試してみました。APIだとある程度権限制御できそうなのがGoodです。gitでcommitするとなるとリポジトリごとgit clone/pullしないといけないので、ネットワークの効率も良さそう。

そもそも論になっちゃうけど配布方法はHomebrewじゃなくて、curl -L http://xxx/install.sh | shのインストールスクリプトで/usr/local/binに置く方がLinuxもいけるしFormulaのメンテが不要になるので、そっちの方が良かったかもです。Homebrewの方が配布物のバージョンアップに対応しやすそうだけど。

参考URL

このエントリーをはてなブックマークに追加