CircleCIでGitHubのReleaseを更新する


## 動機

最近、[Electronのアプリ](https://github.com/noboru-i/issue-hub)を作り始めました。

出来上がった機能をすぐに公開したいので、CircleCIでmac用のappファイルを作成し、GitHubのReleaseに上げる事にしました。

ただ、masterの変更の度にバージョン番号を上げるのも面倒だし、Releaseのタグが大量にあるのも微妙な気がしました。
（なにより、毎回tagを手動で作るのもめんどい）

そこで、Releaseは毎回"更新"し、package.jsonの`version`が変わっている場合のみReleaseを"追加"することにしました。
普通は逆というか、普通のフローとは違いますね。。

例：

- Releaseに`v0.0.1`が存在するとき、`version`が`0.0.1`のままmasterが更新  
以前のタグ・Releaseを破棄、最新のmasterで`v0.0.1`を再作成。
- Releaseに`v0.0.1`が存在するとき、`version`を`0.0.2`にしてmasterを更新  
`v0.0.1`はそのままで、最新のmasterで`v0.0.2`を作成。

![Kobito.ZaH3Rz.png](https://qiita-image-store.s3.amazonaws.com/0/8708/8f3799b0-3402-709a-73cf-55d41424a446.png "Kobito.ZaH3Rz.png")

## 手順

### CircleCIの環境変数を設定

GitHub上のリソースを変更するため、access tokenを設定します。
[こちら](https://github.com/settings/tokens)より、`repo`の権限を持つtokenを取得します。

CircleCI上のEnvironment variablesの設定（ https://circleci.com/gh/:user/:repo/edit#env-vars 形式のURL）にて、`GITHUB_API_TOKEN`というkeyで設定します。

### CircleCIの鍵を更新

後述のスクリプトで、remoteのgitよりタグの削除・追加を行う必要があるので、CircleCIの持つ鍵を更新します。（標準ではread権限のみ）

Checkout SSH keysの設定（ https://circleci.com/gh/:user/:repo/edit#checkout 形式のURL）にて、現在の鍵をRemoveし、"Add deploy key"より新たな鍵を追加します。（ボタンをポチポチするだけで出来るのは便利ですね。）

上記の設定が完了したら、 https://github.com/settings/ssh のページに"CircleCI: :user/:repo"といった鍵が追加されているはずです。

### scriptの作成

次に、実際に処理を行うスクリプトです。
`user`や`repo`、中にあるメールアドレスなどは環境に合わせて設定する必要があります。
（user/repoぐらいは、CircleCI上から取れますね。。手抜きです。）

下記を、今回は`bin/release`として置きました。
また、`chmod a+x bin/release`として実行権限を与えてあります。

```python:release
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import subprocess
import httplib
import urllib2
import json
import copy

user = 'noboru-i'
repo = 'issue-hub'
token = os.getenv('GITHUB_API_TOKEN')
branch = os.getenv('CIRCLE_BRANCH')

host = 'api.github.com'

headers = {
    'Authorization' : 'token %s' % token,
    'Content-Type' : 'application/json',
    'User-Agent' : 'issue-hub release'
}

# get file name from arg
argvs = sys.argv
argc = len(argvs)
if (argc != 2):
    print 'Usage: # python %s filename' % argvs[0]
    quit()
file_name = argvs[1]

# get version from package.json
package_info = json.loads(open('package.json').read())
tag_version = package_info['version']

# get current release id or None
def get_release_id():
    print 'get_release_id'
    path = '/repos/%s/%s/releases/tags/v%s' % (user, repo, tag_version)
    conn = httplib.HTTPSConnection(host)
    conn.request('GET', path, None, headers)
    resp = conn.getresponse()
    print resp.status
    if resp.status == 404:
        return None
    res = json.loads(resp.read())
    return res['id']

# delete current release by id
def delete_release(id):
    print 'delete_release'
    path = '/repos/%s/%s/releases/%s' % (user, repo, id)
    conn = httplib.HTTPSConnection(host)
    conn.request('DELETE', path, None, headers)
    resp = conn.getresponse()
    print resp.status

    # delete tag
    subprocess.call('git config --global user.email ishikura.noboru@gmail.com'.split(' '))
    subprocess.call('git config --global user.name "CircleCI"'.split(' '))
    subprocess.call(('git tag -d v%s' % tag_version).split(' '))
    subprocess.call(('git push origin :v%s' % tag_version).split(' '))

# create new release
def create_release():
    print 'create_release'
    path = '/repos/%s/%s/releases' % (user, repo)
    params = {
        'tag_name' : 'v%s' % tag_version,
        'target_commitish' : branch,
        'name' : 'v%s' % tag_version,
        'prerelease' : True
    }
    conn = httplib.HTTPSConnection(host)
    conn.request('POST', path, json.dumps(params), headers)
    resp = conn.getresponse()
    print resp.status
    res = json.loads(resp.read())
    return res

# upload binary file
def upload_file(upload_url, file_name):
    print 'upload_file'
    name = os.path.basename(file_name)
    url = upload_url.replace('{?name,label}', '?name=%s' % name)
    print url
    length = os.path.getsize(file_name)
    data = open(file_name, 'rb')
    req = urllib2.Request(url, data)
    req.add_header('Content-Length', '%d' % length)
    req.add_header('Content-Type', 'application/octet-stream')
    req.add_header('Authorization', 'token %s' % token)
    res = urllib2.urlopen(req).read()
    return res

id = get_release_id()
print id

if id is not None:
    delete_release(id)

    # sometime create draft release
    import time
    time.sleep(5)

create_result = create_release()
print create_result['id']
print create_result['upload_url']

upload_result = upload_file(create_result['upload_url'], file_name)
```

### circle.ymlの編集

私の場合は、npmのタスクを叩き、tarコマンドで`dist/issue-hub-darwin-x64.tar.gz`を作っているので、それを引数として実行します。
私の実際のcircle.ymlは[こちら](https://github.com/noboru-i/issue-hub/blob/v0.0.1/circle.yml)です。

```
deployment:
  master:
    branch: master
    commands:
      - # ここでバイナリの作成
      - ./bin/release dist/issue-hub-darwin-x64.tar.gz
```