CIによる機械的なコードレビューを可能にするDangerを使ってみた

  • 30
    Like
  • 0
    Comment

TL;DR

この記事はgithub上でのコードレビューに役立つdanger(-js)というツールの紹介です。dangerのセットアップから簡単なレビュールールを記述するところまでのチュートリアルです。

dangerはプルリクエストをトリガとしCI上で実行され、チームメンバがコードレビューを始まる前に、機械的にレビューできる様々な項目をCIがレビューしてくれるツールです。レポジトリは以下です。

https://github.com/danger/danger-js

オリジナルのdangerはrubyで開発されていますが今回はjavascript版のdangerを紹介します。javascript版はNode.jsで動作します。

DangerによるPull Requestの機械的レビュー

Pull Requestの機械的レビューと言われてもなかなかピンとこないかもしれません。例えば

  • ブランチ名やブランチの向き先をチェックする
    • 「向き先間違えてるよー 」とか
  • Pull Requestの詳細説明のフォーマットをチェックする
    • 「JIRAへのリンクがないぞー」とか
  • 特定のメンバーをメンションする
    • 「この人もメンションしときたい @hogehoge san 」とか
  • PRのサイズに制限をもうける
    • 「このPRでかすぎだから、機能ごとに分割して投げなおしてほしいっす!」とか
  • コードの特定のパターンを検出してメッセージを投げる
    • 「# TODO残ってるよー 」、とか

など、パッと見で分かることに関して人力でレビューを行ってしまうことは多いんじゃないかと思います。このような人間だったらまず最初に気になるよね、っというレビュー項目をルールとして定義してチェックするのがdangerの役割ということになります。

dangerでは定義されたレビュールールに違反したPRが投げられた場合、

add_circle_yml_for_danger_by_toshiya_·_Pull_Request__1_·_toshiya_app_auth_twitter_reader.png

こんな感じでレビューメッセージをコメントしたり、Pull Requestを落としたりすることが可能です。PRを投げる人は、0人目のレビュワーとしてCIにレビューをしてもらう、というわけです。

Dangerのセットアップ

実際にdanger(-js)をcircle ci上で動かしてみます。まず、適切な権限が付与されたgithub API tokenが必要ですのでgithubのアカウントページから取得してください。

それ以外のインストール手順はレポジトリのREADMEの通りにやれば基本的に問題はないかと思います。追加 or 変更が必要なファイルは

  • package.json
  • dangerfile.js: レビュールールを記述するjsファイル。詳細は後述。
  • CI設定ファイル(circle.ymlやtravis.yml)

の3つだけです。

対象レポジトリのトップディレクトリで

npm install --save-dev danger@0.15

でdangerのnpmモジュールをインストールします。danger-jsは開発が活発で、最新版は少し不安定なことがあるため自分は少し古いバージョンである0.15に固定して利用しています。

次にレビュールールを記述するdangerfile.jsを用意します。

echo 'message("hello danger")' > dangerfile.js

とします。これは何もチェックをせず、hello dangerという文字列をコメントするだけです。

最後にCIの設定が必要です。circle ciを使う場合の設定例は以下のようになります。

package.jsonのscript欄に

    "danger": "danger"

を追加します。最後に以下のようなcircle.ymlを用意します。

machine:
  node:
    version: v6.1.0
  environment:
    DANGER_GITHUB_API_TOKEN: your_token

test:
  override:
    - npm run danger

実際には環境変数DANGER_GITHUB_API_TOKENは、シークレットとCIのコンソール上に登録して扱うべきですがここでは簡単のためcircle.ymlの中で定義する形で書いています。

これらの3つのファイルの追加・変更を含んだPull Requestを投げると、該当PRに以下のようなdangerからのコメントがつくはずです。

add_circle_yml_for_danger_by_toshiya_·_Pull_Request__1_·_toshiya_app_auth_twitter_reader.png

Dangerでレビュールールを書く

danger(-js)ではレビュールールを、dangerfile.jsという名前のファイルにjavascriptを用いて記述していきます。

ローカルモード

dangerfile.jsはCI上で動作するものですが、デバッグ用にローカルモードが用意されています。以下のコマンドでローカルマシンから既存のPRに対して、ローカルのdangerfile.jsを動作させることができます。PRへのレビュー結果の投稿はされません。

$ DANGER_GITHUB_API_TOKEN=xxx ./node_modules/.bin/danger pr https://github.com/toshiya/app_auth_twitter_reader/pull/1
{
  fails: [
    {
      message: "fail with danger"
    }
  ],
  warnings: [],
  messages: [],
  markdowns: []
}

danger オブジェクト

dangerfileの中では、dangerという変数名でレビュー対象のPull Requestの情報を格納したオブジェクトが渡されます。これをよしなに参照しつつ、レビュールールを記述していきます。

dangerオブジェクトの中でどのような情報を参照できるかについては、ローカルモード実行のオプションに -r を渡すことで確認することができます(デバッグモード)。githubのPRを用いた開発に慣れている方であれば、ドキュメントなどを参照せずとも必要な情報がどの変数に格納されているかは想像できるんじゃないかな、と思います。

$ DANGER_GITHUB_API_TOKEN=xxx ./node_modules/.bin/danger pr -r https://github.com/toshiya/app_auth_twitter_reader/pull/1
> danger
{ git: 
   { modified_files: [],
     created_files: [ 'circle.yml', 'dangerfile.js', 'package.json' ],
     deleted_files: [],
     diffForFile: [Function: diffForFile],
     commits: [ [Object], [Object], [Object], [Object] ],
     JSONPatchForFile: [Function: JSONPatchForFile],
     JSONDiffForFile: [Function: JSONDiffForFile] },
  utils: { sentence: [Function: sentence], href: [Function: href] },
  github: 
   { issue: { labels: [] },
     pr: 
      { url: 'https://api.github.com/repos/toshiya/app_auth_twitter_reader/pulls/1',
        id: 126996678,
        html_url: 'https://github.com/toshiya/app_auth_twitter_reader/pull/1',
        diff_url: 'https://github.com/toshiya/app_auth_twitter_reader/pull/1.diff',
        patch_url: 'https://github.com/toshiya/app_auth_twitter_reader/pull/1.patch',
        issue_url: 'https://api.github.com/repos/toshiya/app_auth_twitter_reader/issues/1',
        number: 1,
        state: 'open',
        locked: false,
        title: 'add circle.yml for danger',
        user: [Object],
        body: '',
        created_at: '2017-06-22T13:48:18Z',
        updated_at: '2017-06-22T14:17:33Z',
        closed_at: null,
        merged_at: null,
        merge_commit_sha: 'a9feb730bd46327e1dd63312a66d20fea7720e26',
        assignee: null,
        assignees: [],
        requested_reviewers: [],
        milestone: null,
        commits_url: 'https://api.github.com/repos/toshiya/app_auth_twitter_reader/pulls/1/commits',
        review_comments_url: 'https://api.github.com/repos/toshiya/app_auth_twitter_reader/pulls/1/comments',
        review_comment_url: 'https://api.github.com/repos/toshiya/app_auth_twitter_reader/pulls/comments{/number}',
        comments_url: 'https://api.github.com/repos/toshiya/app_auth_twitter_reader/issues/1/comments',
        statuses_url: 'https://api.github.com/repos/toshiya/app_auth_twitter_reader/statuses/f8a6f49a47cc8bc0655396fe36393e24fa677ddf',
        head: [Object],
        base: [Object],
        _links: [Object],
        merged: false,
        mergeable: true,
        rebaseable: true,
        mergeable_state: 'unstable',
        merged_by: null,
        comments: 1,
        review_comments: 0,
        maintainer_can_modify: false,
        commits: 4,
        additions: 30,
        deletions: 0,
        changed_files: 3 },
     commits: [ [Object], [Object], [Object], [Object] ],
     reviews: [],
     requested_reviewers: [],
     utils: { fileLinks: [Function: fileLinks] } },
  schedule: [Function: schedule],
  fail: [Function: fail],
  warn: [Function: warn],
  message: [Function: message],
  markdown: [Function: markdown],
  console: 
   Console {
     log: [Function: bound log],
     info: [Function: bound log],
     warn: [Function: bound warn],
     error: [Function: bound warn],
     dir: [Function: bound dir],
     time: [Function: bound time],
     timeEnd: [Function: bound timeEnd],
     trace: [Function: bound trace],
     assert: [Function: bound assert],
     Console: [Function: Console] },
  results: 
   { fails: [ [Object] ],
     warnings: [],
     messages: [],
     markdowns: [],
     scheduled: [] } }

アサーション

dangerfile.js の中では、レビュー結果をgithubのコメントに表示するためのアサーションが利用できます。

アサーション 用途
message('メッセージ') 文字列をコメントするだけ
warn('警告文') 文字列を警告文としてコメントする。PR自体はfailさせない。
fail('PRを落とす') 文字列を警告文としてコメントする。PRをfailさせる。

dangerオブジェクトの中身を確認して、問題があればこれらのアサーションを用いてPRに警告を出したり、CIをfailさせたりすることができるわけです。

具体例

例えば、以下のdangerfile.jsでは

  • PRの差分サイズ
  • 差分のあるファイル数
  • コミットメッセージの長さ

の3つをチェックしています。

var checkOk = true

// 300行以上の差分がある場合、警告を出す
var diffSize = Math.max(danger.github.pr.additions, danger.github.pr.deletions);
if (diffSize > 300 ) {
    warn("This PR is too big. You should divide this PR into smaller PRs.");
    checkOk = false;
}

// 10個以上のファイルを編集している場合、警告を出す
if (danger.github.pr.changed_files > 10 ) {
    warn("This PR changes too many files. You should divide this PR into smaller PRs.");
    checkOk = false;
}

// コミットメッセージが短すぎる場合は警告を出す
for (c of danger.github.commits) {
    if (c.commit.message.length < 5) {
        warn("There is a commit with very short message: " +  c.commit.message)
        checkOk = false;
    }
}

// 全てのルールが通っていれば、'ok'とコメントする
if (checkOk) {
    message("check ok.")
}

終わりに

danger(-js)はレビュールールの記述の中で一般のjavascriptを自由に使うことができる汎用的なツールです。自分のチームにあったレビュールールを定義して、いろいろと遊んでみるとコードレビューが楽しくかつ効率化するかもしれません:)

参考

  • オリジナルのruby版danger。奇抜なデザインのLPを持っていますが、レビュールールの例はこちらを参照するといろいろなものが見つかります。