LoginSignup
30
25

More than 5 years have passed since last update.

CircleCIでGitHubのReleaseを更新する

Posted at

動機

最近、Electronのアプリを作り始めました。

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

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

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

例:

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

Kobito.ZaH3Rz.png

手順

CircleCIの環境変数を設定

GitHub上のリソースを変更するため、access tokenを設定します。
こちらより、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の作成

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

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

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はこちらです。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
25