Kesinの知見置き場

知見を共有していきたいじゃないですか

BigQueryでinsert or updateを実現する

BigQueryにデータをインポートする基本的な手段はloadCSVJSONのデータからテーブルを作成したり、既存のテーブルへの追記です。
ただ、今回は要件的にRDBでいうところのinsert or update(またはupsertとも呼ばれる)を行いたかったので、BigQueryで可能なのか調べてみました。

tl;dl

MERGEを使いこなせば可能。ただし料金には注意。

INSERT/UPDATE

BigQueryはSQLのINSERT/UPDATE/DELETEによってテーブルのデータを直接編集することが可能です。
https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax

ただし、残念なことにMySQLにおけるINSERT ON DUPLICATE KEY UPDATEpostgresqlINSERT ON CONFLICE UPDATEに相当するいわゆるinsert or updateの機能は存在していません。

なんとか実現するとしたら、更新したいデータのうちBigQueryのテーブルに既に存在するものと、まだ存在していない2種類に分割してそれぞれをINSERT/UPDATEすることで一応可能でしょう。
しかし、データを分割するのに手間がかかるだけではなく、BigQueryの料金は1クエリ毎に計算される仕組み(料金については後述)なのでその点でも非効率です。

MERGE

BigQueryにはINSERT/UPDATE/DELETEに加えて、MERGEという操作が存在します。 詳しい説明はドキュメントにあるのですが、説明がなかなか複雑なのでどういう挙動をするのか初見では理解するのが難しいと思います。
自分の理解でざっくりと説明すると、MERGEは操作する対象のテーブル(target)と変更するためのデータのテーブル(source)をJOINして、その結果に応じてINSERT/UPDATE/DELETEが行える操作です。

という挙動であることを把握してからドキュメントのサンプル事例を見ると、これを活用することでinsert or updateの挙動が実現できることに気がつきます。
まず、変更したいテーブルをtarget、変更するためのデータを適当なテーブルにアップロードしてsourceとし、それらをPrimary Keyと考えたいカラムでJOINします。そして、既にPrimary Keyが存在する場合はUPDATE、存在しない場合はINSERTを実行します。

実際にBigQueryのSQLにすると以下になります。

#standardSQL
MERGE
  `blog.target` AS T
USING
  `blog.source` AS S
ON
  T.user_id = S.user_id
WHEN MATCHED  THEN
  UPDATE SET T.comment = S.comment
WHEN NOT MATCHED THEN
  INSERT (user_id, comment)
  VALUES (user_id, comment)

これを以下のtargetとsourceのテーブルに対して実行してみます。

targetテーブル

user_id comment
A null
B null

sourceテーブル

user_id comment
B 'Merge update'
C 'Merge insert'

MERGE実行後のtargetテーブル

user_id comment
A null
B 'Merge update'
C 'Merge insert'

狙い通り、1つのクエリだけでinsert or updateの挙動が実現できていますね。

料金について

MERGEは大変便利なのですが、BigQueryにデータを追加する基本手段のloadが無料であるのに対して、MERGEはSELECT同様に課金が発生しますのでここについても触れておきます。

INSERT/UPDATE/DELETEの料金は現在のドキュメントでは以下となっています。 https://cloud.google.com/bigquery/pricing#free-tier https://cloud.google.com/bigquery/pricing#on_demand_pricing

注意すべきは、どれだけテーブルのデータ数が少なかったとしても最低のデータ処理容量は10MBだということです。

料金は MB 単位のデータ処理容量(端数は四捨五入)で決まります。クエリが参照するテーブルあたりのデータ最小処理容量は 10 MB、クエリあたりのデータ最小処理容量は 10 MB とします。

BigQueryの無料枠は1GB/月ですので、100回を超えてクエリを実行するとそこから課金が発生します。ちなみに、SELECTも最小容量は10MBですがJOINすると倍の20MBになるので注意です。

そして、MERGEもJOINと同じく最小容量は20MBです。つまり50回を超えると課金発生です。1ヶ月に50回ですので案外少ない回数で無料枠を使い切ってしまいますね。

ちなみに先ほどのサンプルSQLを実行した結果はこのようになっており、Bytes Billedが実際に20MB分となっていることが分かります。

Bytes Processed  44 B
Bytes Billed    20.0 MB

20MBはあくまで最小容量ですので、操作対象となるテーブルのデータ量が多い場合は、当然1クエリあたりの料金は比例して多くなります。バッチ処理などで定期的にMERGEを使う想定の場合、一度クエリを実行してみてそのときの情報から一ヶ月単位での利用料を軽く見積もってみるといいでしょう。

注意

前述の料金の説明は2018/06/18日時点でのドキュメントを参考にしています。
また、MERGEについても執筆時点ではベータ版です。従って、将来的に使い方が変更される可能性もゼロではないです。

参考

Firestoreをもっと手軽に使えるfirestore-simpleを作った

Firebaseの新しいDBであるFirestoreは使っているでしょうか?
今まではどうしてもサーバー側の実装が必要だったDBへのデータの格納がクライアントだけで済むようになったので圧倒的に便利ですね。
そんなわけで自分もjsからFirestoreを使う機会が増えてきたのですが、毎回書くたびにAPIが微妙に使いづらいと感じていました。そこで、Firestoreをいい感じに扱えるfirestore-simpleというライブラリをリリースしました。

サンプルコード

まず、比較のためにFirestoreを素で使った場合のコードです。

const admin = require('firebase-admin')
const serviceAccount = require('./firebase_secret.javascripton')
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) })

const main = async () => {
  const firestore = admin.firestore()
  const collectionPath = 'users'
  await firestore.collection(collectionPath).add({ name: 'alice', age: 20 })
  await firestore.collection(collectionPath).add({ name: 'bob', age: 22 })

  const user_documents = await firestore.collection(collectionPath).get()
  const users = []
  user_documents.forEach((document) => {
    const data = document.data()
    users.push({ id: document.id, name: data.name, age: data.age })
  })
  // [ { id: 'lZ9HtQAEJvumL9grjGtc', name: 'bob', age: 22 },
  // { id: 'rFx0a6dZQZPIHEvXGGtO', name: 'alice', age: 20 } ]

  const batch = firestore.batch()
  users.forEach((user) => {
    const documentRef = firestore.collection(collectionPath).doc(user.id)
    batch.delete(documentRef)
  })
  await batch.commit()

}
main()

そしてこちらがfirestore-simpleを使った場合のコードです。

const admin = require('firebase-admin')
const { FirestoreSimple } = require ('firestore-simple')
const serviceAccount = require('./firebase_secret.json')
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) })

const main = async () => {
  const firestore = admin.firestore()
  const collectionPath = 'users'
  const dao = new FirestoreSimple(firestore, collectionPath)
  await dao.add({ name: 'alice', age: 20})
  await dao.add({ name: 'bob', age: 22 })

  const users = await dao.fetchCollection()
  // [ { id: '3mBYA0uXw0lw6b883jgk', age: 20, name: 'alice' },
  // { id: 'lE9SfP8f3H8w0Uwk0euQ', age: 22, name: 'bob' } ]

  const ids = users.map((user) => user.id)
  await dao.bulkDelete(ids)
}
main()

どうでしょうか。素のFirestoreに比べて短く、スッキリとしたコードで書けると思います。

firestone-simpleの特徴

1インスタンス1コレクション

素のFirestoreでは例えばuserにアクセスする場合、firestore.collection(‘user’)と書く必要があります。コードの各所からuserのドキュメントにアクセスしたい場合、毎回同じコードを書くのはイヤなので自分ならUserDaoとかUserModelみたいなクラスを作り、そのクラス経由でアクセスするようにしたいです。

firestore-simpleはコンストラクタにcollectionPathを渡してインスタンスを作ります。1インスタンス1コレクションという制約を設ける代わりに、collectionPathを指定するのは最初の1回だけで済みます。

プリミティブなObjectを返す

Firestoreのドキュメントを取得するとき、得られるのはDocumentSnapshotなのでidを参照するにはdoc.id、それ以外の要素にアクセスするにはdoc.data()が必要です。 そのため、{title: ‘test’}というドキュメントをFirestoreから取り出すときには、いちいちdoc.data().testとする必要があり地味に面倒くさいです。

firestore-simpleはfetchDocumentfetchCollectionの参照系だけではなくaddsetといった更新系も基本的にObjectを返します。このObjectにはドキュメントのデータに加えてidも追加されているためdoc.iddoc.testというようにアクセスすることが可能です。

プリミティブなArrayを返す

Firestoreでコレクションから複数のドキュメントを取得する場合、得られるのはQuerySnapshotです。ドキュメントのサンプルコードではforEachを使っているのでmapも使えると思いきや使えません。どうもQuerySnapshotのリファレンスを見る限りではforEachしか実装されていないようで、mapfilterが使えないので大変不便です。

firestore-simpleではfetchCollectionfetchByQueryといった複数のドキュメントを取得するメソッドはArrayを返します。従ってmapも普通に使うことが可能です。

キーの別名マッピングに対応

jsに限らずJSONでデータをやりとりするAPIのクライアントを書くときに地味に面倒くさいのが、キーの名前をsnake_caseやCamelCaseといったクライアント側の言語の流儀に合わせて変換することです。 この変換処理をコードの色々なところで繰り返したくないので、大抵はAPIクライアントかModelクラスにJSONのキーとのマッピングを書くことになると思います。

大規模なコードであればFirestoreのコレクションと1対1で対応するModelクラスを用意してもいいのですが、そこまでしたくない場合のためにfirestore-simpleではキーの別名マッピングに対応しています。

const dao = new FirestoreSimple(firestore, collectionPath, { mapping: {
  createdAt: "created_at"
}})
await dao.add({ name: 'alice', age: 20, createdAt: new Date() })

このようにすると、js側ではcreatedAtというキーで扱いますが、Firestore側のドキュメントではcreated_atというキーで保存されます。

メソッド

参照系

fetchDocument

1つのドキュメントをフェッチします。

await dao.fetchDocument(documentId)

fetchCollectoin

コレクション全てのドキュメントをフェッチします。

await dao.fetchCollection()

fetchByQuery

where, orderBy, limitなどを使ってコレクションから絞りこんだりソートしてフェッチします。

const query = dao.collectionRef
  .where('name', '==', 'alice')
  .orderBy('age')
  .limit(1)
await dao.fetchByQuery(query)

更新系

add

idを自動採番でFirestoreにドキュメントを追加します。

await dao.add({ name: 'alice', age: 20 })

set

引数のObjectのidに対応するFirestore側のドキュメントを更新します。
そのidに対応するドキュメントがまだFirestoreに存在しない場合はそのidで追加されるので、addによる自動採番ではなく任意のidで登録したい場合もsetを使います。

await dao.set({ id: '1111', name: 'bob', age: 22})

addOrSet

引数のオブジェクトにidが含まれていればset、含まれなければaddと同じ挙動になります。
元々のFirestoreには存在しないfirestore-simpleのオリジナル機能です。

await dao.addOrSet({ name: 'alice', age: 20 }) // add
await dao.setOrSet({ id: '1111', name: 'bob', age: 22}) // set

delete

引数のidに対応したドキュメントをFirestoreから削除します。

await dao.delete(documentId)

Bulk系

ドキュメントで一括書き込みと書かれている処理です。複数のドキュメントへの処理をアトミックに行いたい場合は使用します。

bulkSet

引数に渡した複数のオブジェクトでFirestoreのドキュメントを一括で更新します。

await dao.bulkSet([
  { id: '1111', name: 'alice', age: 20 },
  { id: '2222', name: 'bob', age: 22 }
])

bulkDelete

引数に渡した複数のidでFirestoreのドキュメントを一括で削除します。

await dao.bulkDelete(['1111', '2222'])

できないこと

update

Firestoreにはドキュメント更新の方法としてset以外にupdateも存在しますが、自分はupdateを使う必要性を感じなかったので実装していないです。

トランザクション

自分が使いたい用途としてはbulkSetbulkDeleteでほぼ十分だったことと、トランザクションをサポートするには実装が複雑になってしまうことから見送りました。

onSnapshot()

これも自分が今の所使う必要がなかったため未サポートです。 ただ、firestore-simpleのインスタンスからcollectionRefにアクセス可能なので、このようなコードでonSnapshot()を使うことは可能です。

dao.collectionRef.onSnapshot((snapshot) => {
  snapshot.docChanges.forEach((change) =>{
    console.log(change.type)
    console.log(change.doc.data())
  })
})

ReactNative

firestore-simpleはReactNativeFirebaseでも使用可能です。
ReactNativeFirebaseではfirestoreのインスタンスをゲットする方法が多少異なりますが、それ以後の使い方は全く同じです。

import { FirestoreSimple } from 'firestore-simple'
import firebase from 'react-native-firebase';

export default class App extends React.Component {
  // signInAnonymouslyAndRetrieveDataを使う場合はfirebaseのコンソールで匿名ログインをオンにする必要があります
  async getFirestore() {
    await firebase.auth().signInAnonymouslyAndRetrieveData()
    return firebase.firestore()
  }

  async componentDidMount() {
    const firestore = await this.getFirestore()
    const collectionPath = 'sample_react_native'
    const dao = new FirestoreSimple(firestore, collectionPath, {
      mapping: {
        createdAt: 'created_at'
      }
    })

    // ... other usage are same as nodejs client
  }
}

TypeScript

firestore-simpleはTypeScriptで書かれているため、型定義も同梱されています。
逆にflowには対応していません。

まとめ

Firestoreをもっと手軽に扱えるfirestore-simpleを作りました。
簡単な用途ならfirestore-simpleを素のまま使っても便利だと思いますが、firestore-simpleを組み込むか、継承することでより重厚なFirestoreクライアントを自作したいというニーズにも応えられると思います。

自分はオリジナルのFirestoreのAPIを忘れてしまうぐらい便利に使いまくっているので、ぜひ使ってみてください。

USB接続したiOSのudidをCLIから取得する方法あれこれ

iOS開発をしていると、ちょっとしたことを自動化したいということがたまにある。
今回は、開発用のMacに接続しているiOS実機のudid(とついでにnameも)をrubyから扱いたくなったので、CLIから情報を取得する方法を調べたのでまとめてみた。

3種類の方法を紹介します。

  • xcrun instruments -s
  • mobiledevice
  • FastlaneCore::DeviceManager

xcrun instruments -s

やりたいことをググると一番多く見つかるのがこれ。
実際に実行してみると、たくさんのiOSシミュレータのudidがズラズラと列挙されて分かりにくいが、3行目あたりに接続しているiOS実機のnameとudidが表示されているはず。 少々めんどくさいが、正規表現を使えば抽出することが可能。

ちなみに、よく-sの後にdeviceを付ける紹介を見かけるが、自分が試した感じではdeviceの有無で結果は全く変わらなかった。diffコマンドで確認しても差分はなかった。

そもそもxcrun instrumentsを実行したときに-sというオプションは表示されていないので出自からして若干謎である。隠しコマンドなのだろうか?

mobiledevice

imkira/mobiledevice

iOSバイスの様々な情報を表示したり、アプリのインストールなどができるCLIツール。

仕組みとしては、iTunesが使っているプライベートフレームワークであるMobileDevice.frameworkを叩いてCLIツールとして機能を提供しているようだ。
残念ながら自分はこのあたりに詳しくないが、このMobileDeviceを外から呼び出すライブラリは他にも存在するみたいでこの界隈では有名らしい。
MobileDevice Library - The iPhone Wiki

homebrewからインストールできるが、残念ながらHigh Sierraではインストールできなかった。幸い、git cloneして自分でmakeすればHigh Sierraでも普通に使える。
makeに失敗する場合はxcode-select --installでcommand line developer toolsをインストールすればmakeできるようになるはずだ。

使い方は、mobiledevice list_devicesで接続しているiOSバイスのudidだけが列挙される。シンプル。
udid以外の情報も取得可能で、例えばnameならmobiledevice get_device_prop -u UDID DeviceNameで取れる。UDIDにはlist_devicesで表示されたudidを入れてほしい。
mobiledevice list_device_propsで他にも取得可能な要素の一覧が表示されるので、興味があれば色々試してみると面白いかもしれない。

FastlaneCore::DeviceManager

自分は最終的にここに行き着いた。みんな大好きFastlane。
connected_devicesというメソッドを使うことで接続されているiOS実機の情報を取得できる。 fastlane/device_manager.rb at 2.91.0 · fastlane/fastlane · GitHub

ドキュメントは多分存在しないが、使い方はこんな感じで簡単だ。

require 'fastlane_core/device_manager'
require 'pp'

devices = FastlaneCore::DeviceManager.connected_devices('iOS')

pp devices

# [#<FastlaneCore::DeviceManager::Device:0x007f883c2a3bb0
#  @ios_version="11.2.6",
#  @is_simulator=false,
#  @name="Kesin's iPhone",
#  @os_type="iOS",
#  @os_version="11.2.6",
#  @state="Booted",
#  @udid="*****">] 自分のiPhoneのudidが表示されているので伏せさせて頂く

裏側の仕組みとしては、やはりxcrun instrumens -sの結果を頑張って正規表現でパースしてインスタンスを作っている。
先人の努力に感謝しつつ、ありがたく使わせて頂きます。

まとめ

  • xcrun instruments -s
    • 実行するのは最も手軽。ただし、自力で正規表現を頑張る必要はある
  • mobiledevice
    • macOS 10.2までならhomebrewでインストールできるかも?High Sierraの人は自分でmakeする必要がある
    • 正規表現を頑張る必要はないのでインストールさえできれば簡単
  • FastlaneCore::DeviceManager
    • 自分と同じくrubyから使いたい人は最も手軽

自分の用途に合った方法をどうぞ。

SRE-SET Automation Night #2にお邪魔してきました

2018/03/06に開催されたSRE-SET Automation Night #2へブログ枠として参加させて頂きました。

SRE-SET Automation NightはCIやテスト、Seleniumなどを使った自動化についてが主なテーマの勉強会です。
第1回目のときにも参加させて頂いたので、今回は2回目の参加でした。

会場に着いたのが早めだったので、まだ皆さんが集まっていないうちにケータリングの写真を撮らせて頂きました。 定番のお寿司もいいですが、たまにはこういう違ったおしゃれメニューもいいですね!

f:id:Kesin:20180306191120j:plain

今回の発表の中で特に自分が興味持った2つを紹介します。

AWS Lambdaで作る GitHub bot @siroken3

GitHubでもイベントに反応するbotAWS Lambdaに構築するという発表でした。
おそらく公式ドキュメントだとこのあたりのことなのではないかと思います
https://developer.github.com/apps/getting-started-with-building-apps/

GitHub botができることの例として紹介されていたもの。

  • PRへのコメント
  • Issueの自動ラベリング
  • PRの内容チェック

Issueの自動ラベリングとか、自動クローズとかはOSSで最近見かけることがありますね。

botをLambda上に構築する仕組みは、GitHubのwebhookでAWS Lambdaを起動してLambdaに置いたスクリプトからGitHubAPIを叩いて実現しているようです。 つまりこういう流れ。

Github(webhook) -> Amazon SNS(Topicsを設定) -> Lambda -> Github API

帰宅してからちょっと調べたら、似たような構成でGitHub -> Lambda -> Slackの連携を構築している方がいました。
https://qiita.com/ooharabucyou/items/2a3dca643f6b7783d665

注意点としては、GitHubと連携するためのpersonal access tokenはAWS KMSを使って暗号化しておき、Lambdaから使うときに復号化する必要があるようです。

今回の発表では、サンプルとしてissueにコメントすると単純に同じコメントをbotが返すecho botの作り方を紹介していました。
リポジトリはこちらのようです https://github.com/siroken3/SreSetAutomationNightVol2

echo botはissueにコメントされたタイミングでwebhookを受け取る必要があるのですが、webhookのデフォルトの設定だとpushしたタイミングでしか発火してくれないとのことです。
発表では、コメントされたタイミングでもwebhookが発火するようにcurlでリクエストを投げて設定を変更されていました。 調べてみたところ、リポジトリのsettingsからwebhookを設定するUIにもそれっぽいものがあったので、おそらくこれでもいけるのではないかと思います。

2018/03/08 追記
webhookの設定ではなく、Integration & servicesからAmazon SNSの設定をされているとのことでした。たしかにこちらだとUIから発火するイベントの選択はできないようでした。
訂正いたします

自動回帰テストフローとGitHub Apps @Quramy

こちらもGitHubと連携するbot(Apps)を使って、回帰テストフローを自動化したという内容でした。 少しややこしいのですが、@siroken3さんの発表とは異なるGitHub Appsという仕組みを使っています。
GitHubと連携するbot(Apps)には@siroken3さんのpersonal access tokenを使う方法とは別のGitHub AppsとOAuth Appsという2つの仕組みがあり、 この2つの違いはこのようになっているとのことです。

GitHub Appsの場合は個人のアカウントに紐付かないためチーム開発においてはOAuthより便利だということです。
公式のドキュメントだと多分このあたり
https://developer.github.com/apps/getting-started-with-building-apps/

@Quramyさんのチームではフロントエンド用の自動回帰テスト環境として、修正前の環境でのスクリーンショットと、コードに修正を加えた後の環境でのスクリーンショットを比較して問題がないかの確認を行うツールを作成して運用しているとのことでした。
今回の発表では触れられていませんでしたが、2017年のNode学園祭で発表されていたREG-SUITというツールだと思われます。 https://speakerdeck.com/quramy/introduction-to-visual-regression-testing

スクリーンショットを比較するテストの場合、画像を比較して差分が出ていたとしてもデザイン変更を伴う正しい修正かもしれないため、機械的にOKかNGかを判断することはできず人間がチェックします。
そして差分が意図したものである場合は、修正後のスクリーンショットを次回テストの正として画像を保存しておく必要があります。

今回作成されたGitHub Appsは以下の作業を自動的にしてくれるようでした。

  • CIで実行してスクリーンショットの差分が存在した際に画像チェック用のページのurlをプルリクにコメントする
  • レビューがApproveされたらそれを検知してマージOKのステータスに変更する
  • 今回のスクリーンショットを次回テストの正とするために画像を保存

実装の詳しい説明までは残念ながらおそらく時間の都合上発表されていませんでしたが、AWS Lambda上で構築されているとのことでした。

感想

他の方の発表も非常におもしろかったのですが、最近自分がDangerというプルリクに対するアクションを簡単に書けるライブラリのプラグインを作っていたので、似たようなことができるGitHub連携ツールの発表が非常に興味深かったです。

プルリクへのコメントや、Issueの自動ラベリングはDangerでも実現できるのですが、DangerはCIサービス上で動くことをウリにしているので手軽に導入できる反面、pushされたタイミングでしか処理が行えないという不便さを感じていました。

botなりGitHub Appsは実装とAWS Lambdaにデプロイする手間はかかるはずですが、GitHub上のあらゆるイベントに対して処理を走らせることができるという点が強みだと思います。 特にApproveされたり、LGTMされたタイミングで何かのアクションをするというのは色々なことに使えそうですね。

便利そうだなーと感じる一方で、残念ながら今のところ自分はまだ便利に使えそうなアイディアが浮かんでいないです。ただGitHub Apps自体が発表されたのは去年のようですので、これからどんどん活用事例が増えていくのではないかなという予感がしています。

ちなみに、自分はまだ触っていないのですがGitHubbotを簡単に作るためのライブラリとしてhttps://probot.github.io/というのもあるようです。
今回の発表やprobotを見ていると、元々便利なGitHubをさらに便利にしていくという流れを何となく感じますね。

textlintの結果をプルリクにコメントしてもらう(danger-textlintの紹介)

danger-textlintというdangerプラグインを公開しました!今回はその紹介エントリです。

dangerについて

そもそもdanger自体の知名度がまだまだだと思うので簡単に紹介したいと思います。

dangerはpull requestのコードレビューを助けるためのツールです。
dangerはpull requestのデータにrubyから簡単にアクセスするためのインターフェースと、pull requestにコメントするためのインターフェースを提供してくれます。
例えばdangerを使うとタイトルに[WIP]が含まれる場合は警告のコメントを出してマージできないようにする、ということが簡単に行えます。

textlintについて

textlintはコードの代わりに文章を対象としたlintツールです。最近、紹介エントリを見る機会が増えたのでだいぶ知名度が上がったと思います。

textlintのルールはeslintのようにそれぞれプラグインとなっており、自由にカスタムできることが特長です。 日本語でミスしやすいルールが数多く用意されており、出自からかエンジニアが書く文章のためのルールも充実しています。

個人的にはブログを書くときにtextlintはもはや手放せない存在です。また会社で社外向けブログを書くときはチームメンバーにレビューをしてもらうのですが、日本語のミスが残っていると肝心の内容よりも表現の指摘に終止しがちです。 自動赤ペン先生として事前にtextlintでチェックしておくことでレビューが生産的になります。

danger-textlintについて

dangerのプラグインは既にたくさんあり、その中にはlintの結果をプルリクの中でコメントとして付けてくれるものがあります。danger-textlintはそれらと同じようにtextlintのlint結果をコメントしてくれます。

実際にこのブログを書いているプルリクに対して実行した様子です。こんな感じでtextlintによる日本語のチェック結果をコメントしてくれます。

f:id:Kesin:20180217213341p:plain

導入方法

導入方法はREADMEにまとまめました。他のDangerプラグインと同様にgemをインストールしてDangerfileに以下を追加します。

# デフォルトだとtextlintの結果が1つでも違反しているとマージできなくなる
# max_severity = "warn" を設定するとDangerはコメントするだけに留めてマージまでは禁止しない
textlint.max_severity = "warn"

# 最低限これが必要
textlint.lint

加えて、そもそもtextlintを動かすためには当然nodejsが動く環境とtextlint自体のインストールも必要です。 danger-textlintはnpm i --globalオプションでインストールされても動くように作ってありますが、dangerはCIで動作させる前提のツールなので普通にnpm install textlintでローカルに保存した方が無難です。
node_modulesを.gitignoreに追加するのも忘れないようにしましょう。

CIの設定は普通にdangerを実行するだけですが、textlintを動かすのにnodejsが必要なのでruby + nodeの環境が必要です。自分はCircleCIを使うことが多いのでCircleCIのサンプルを載せておきます。

version: 2
jobs:
  build:
    docker:
       - image: circleci/ruby:2.4.1-node-browsers
    working_directory: ~/repo
    steps:
      - checkout

      # ruby dependencies
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "Gemfile.lock" }}
          # fallback to using the latest cache if no exact match is found
          - v1-dependencies-

      - run:
          name: bundle install
          command: |
            bundle install --jobs=4 --retry=3 --path vendor/bundle

      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

      # npm dependencies
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}

      - run:
          name: npm install
          command: npm install

      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules

      - run:
          name: danger
          command: |
            bundle exec danger

なぜ作ったのか

自分が働いているチームではブログを書く環境がめちゃめちゃ整備されていて、そこら辺のプロダクトよりもPR周りの自動化が整備されているんじゃないかというほどです。 その中でDangerも使われているのですが、最近導入されたtextlintはDangerプラグインが存在しなかったのでフローにうまく統合できておらず、textlintに怒られたときは直接CIのコンソールから内容を確認しているという状況でした。

lintの結果はエディタで見るなら分かりやすいのですが、コンソールだと非常に確認しにくいです。CIが落ちたらローカルで再実行して確認すれば済む話ではあるのですが、プルリク上でそのままコメントしてくれた方が楽だし、他のレビュアーもただlintが落ちたという結果だけではなくてどの行でどのような間違えがあったのかを確認できるというメリットがあります。

textlintと同じnodejsで動くeslintのプラグインであるdanger-eslintが既に存在していたので参考にしつつ、サクッと作ってみました。

danger-checkstyle_formatについて

実はdanger-textlintを作る前にそもそも既に存在するdangerプラグインで実現できないか調べていて、その中でdanger-checkstyle_formatというプラグインを見つけました。 これはcheckstyleという形式のxmlを読み込んでDangerにコメントしてもらう汎用的に使えるプラグインです。textlintは--formatオプションで豊富な出力フォーマットを選択可能になっていて、checkstyle形式で出力することも可能です。

なので最初はtextlintが出力したcheckstyle形式のxmlをdanger-checkstyle_formatに食わせるという方法を試してみて、一応自分がやりたかったことは実現できました。ただ、textlintは1つでも違反があると終了ステータスが1になってしまうという仕様になっていて、CircleCIがtextlintで止まってしまうのでDangerの実行までステップが進まないという問題がありました。

その問題は一応こんな感じでtextlintの終了ステータスを握りつぶしてしまうことで回避することはできました。

run:
  name: textlint
  command: |
    set +e
    npm run textlint -- -f checkstyle -o textlint-result.xml
    echo 0

が、ちょっとハックな感じだったのでそれならと自分でDangerプラグインを作ってみました。 プラグイン化することで中間ファイルであるxmlも生成されなくなったし、Dangerfileだけみれば何をしているか一目瞭然になったので、なかなかよかったと思います。

まとめ

というわけでdanger-textlintの紹介でした。
既にGithubのプルリクでレビューをもらいながらブログを書ける環境の方はDangerとdanger-textlintを、まだプルリク運用できていない場合はまずは次の記事からGithubにでもリポジトリを用意して快適な執筆フローを整えてみてはいかがでしょうか。