GitHubのリリース作成＆ファイル添付を自動化する

こういうのを作ります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/67488/8f460ceb-a884-8773-d382-d8cf7e3f0dd2.png)

## 自動化したいこと
GitHubのリリースタスクを[gulp](http://gulpjs.com/)で自動化できるようにします。

- GitHub上にバージョンに応じたリリースを作成する
- 作成したリリースに`my-package-v0.1.0.zip`のようなファイルを添付する

配布したいバイナリやzipがある場合、リポジトリに含めるのではなくリリースの添付アセットにしておくと、ダンロード数が計測できたり、`https://github.com/<user>/<repo>/releases/latest`というURLで最新リリースへと誘導できたりしてなにかと便利です。

### ワークフロー
実際のワークフローは以下のようになると思いますが、この記事で扱うのは (4) と (5) です。

1. package.jsonの`version`を更新する
2. ビルドして配布用のバイナリを生成する
3. master（とタグ）をGitHubにプッシュする
4. **GitHubにリリースを作成する**
5. **リリースにファイルを添付する**

リリース用のGitタグは (3) の段階で付けておいてもいいですが、作らなかった場合でも (4) を実行すればリモートには自動的にタグが設定されます。

**【追記】**(1)〜(3)は、[npm-version](https://docs.npmjs.com/cli/version)を使えば自動化できます。
参考 : [npm version, preversion, postversionの使い分け](http://qiita.com/htanjo/items/d16d8531bc22e0a43217)

### サンプルコード
紹介する内容は、デモプロジェクトとしてGitHubに公開しています。
<https://github.com/htanjo/github-release-demo>

## パッケージ情報の取得
リリースのバージョンや配布するファイル名は、package.jsonの値を使います。

**package.json:**

```json
{
  "name": "my-package",
  "version": "0.1.0",
  ...
}
```

`require('./package.json')`だとキャッシュされることがあるため、`fs.readFileSync()`で取得します。

```js
var fs = require('fs');
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
```

## GitHub APIの利用
[GitHub API](https://developer.github.com/v3/)を使うことで、リリースの作成やファイルのアップロードができます。
Node.js上でGitHub APIを扱うために[github4](https://github.com/kaizensoze/node-github)をインストールします。（オリジナルの[github](https://github.com/mikedeboer/node-github)モジュールはメンテが止まっていました。）  

```sh
$ npm install github4 --save-dev
```

```js
var GitHubApi = require('github4');
var github = new GitHubApi();

github.authenticate({
  type: 'oauth',
  token: 'xxxxxxxxxxxxxxxx'
});

// リリースの作成
github.repos.createRelease(...);

// ファイルのアップロード
github.repos.uploadAsset(...);
```

APIへのアクセストークンは[Personal Access Tokens](https://github.com/settings/tokens)のページから生成することができます。トークンは作成直後に一度しか表示されないので注意！

実際のプロジェクトでは、`.authenticate()`に渡す値は環境変数や[config](https://github.com/lorenwest/node-config)モジュールを使い、認証情報がソースコードに残らないようにします。

## リリースの作成
github4の[`.createRelease()`](https://kaizensoze.github.io/node-github/#api-repos-createRelease)メソッドを使います。
標準ではcallbackでレスポンスを返しますが、Promiseで扱った方がgulpタスクにしやすいため、[pify](https://github.com/sindresorhus/pify)でラップしました。

```sh
$ npm install pify --save-dev
```

```js
var pify = require('pify');

pify(github.repos.createRelease)({
    user: 'htanjo',
    repo: 'github-release-demo',
    tag_name: 'v' + pkg.version,
    body: 'Release v' + pkg.version   // リリースノート（Markdownに対応）
  })
  .then(function (res) {
    console.log(res);                 // APIからのレスポンス
    console.log(res.id);              // リリースID（ファイルアップロード時に必要になる）
  });
```

## ファイルのアップロード
github4の[`.uploadAsset()`](https://kaizensoze.github.io/node-github/#api-repos-uploadAsset)を使います。
どのリリースに添付するかは、リリースIDでの指定が必要です。タグ名ではダメなようです。
ファイルの指定は`filePath`オプションを使います。この例では"dist/my-package.zip"がすでに生成されているとします。

```js
pify(github.repos.uploadAsset)({
    user: 'htanjo',
    repo: 'github-release-demo',
    id: id,                                         // .createRelease()のレスポンスで取得したID
    name: pkg.name + '-v' + pkg.version + '.zip',   // 配布時のファイル名
    filePath: 'dist/' + pkg.name + '.zip'           // アップロードするファイル
  })
  .then(function (res) {
    console.log(res);       // APIからのレスポンス
  });
```

## gulpタスクの作成
以上をまとめてgulpタスクにします。
非同期処理がいくつもありますが、Promise化してあるのでそのまま`gulp.task()`に`return`すればOKです。

```sh
npm install gulp gulp-util --save-dev
```

**gulpfile.js:**

```js
var gulp = require('gulp');
var gutil = require('gulp-util');
var GitHubApi = require('github4');
var pify = require('pify');
var fs = require('fs');

var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
var github = new GitHubApi();

github.authenticate({
  type: 'oauth',
  token: 'xxxxxxxxxxxxxxxx'
});

gulp.task('release', function () {
  return pify(github.repos.createRelease)({
      user: 'htanjo',
      repo: 'github-release-demo',
      tag_name: 'v' + pkg.version,
      body: 'Release v' + pkg.version
    })
    .then(function (res) {
      gutil.log('Release "' + res.tag_name + '" created');
      return res.id;
    })
    .then(function (id) {
      return pify(github.repos.uploadAsset)({
        user: 'htanjo',
        repo: 'github-release-demo',
        id: id,
        name: pkg.name + '-v' + pkg.version + '.zip',
        filePath: 'dist/' + pkg.name + '.zip'
      })
    })
    .then(function (res) {
      gutil.log('Asset "' + res.name + '" uploaded');
    });
});
```

リリース作成／ファイルアップロードを2つのタスクに分けておくと、後からファイル添付だけできるようになったり便利ですが、別途リリースIDを取得する必要があります。その場合は[`.getReleaseByTag()`](https://kaizensoze.github.io/node-github/#api-repos-getReleaseByTag)を使えばタグ名からIDが取得できます。

## リリースしてみる
gulpタスクを実行！

```sh
$ gulp release
[18:01:42] Using gulpfile ~/lab/github-release-demo/gulpfile.js
[18:01:42] Starting 'release'...
[18:01:43] Release "v0.1.0" created
[18:01:44] Asset "my-package-v0.1.0.zip" uploaded
[18:01:44] Finished 'release' after 1.9 s
```

リリース完了、ファイルも添付されました！

![release.png](https://qiita-image-store.s3.amazonaws.com/0/67488/ac6b85bc-10fe-da26-d1c4-f3994c5f1f0b.png)

**デモプロジェクト**
<https://github.com/htanjo/github-release-demo>

実際のリリースページ
<https://github.com/htanjo/github-release-demo/releases>