この記事は Slack Advent Calendar 2016 - Qiita の6日目の記事です。
昨日は S_Shimotori さんの HubotとTypeScriptで翻訳Slack botでした。
Hi, I'm John! 今日はエウレカのiOS開発チームが使っている便利なボットを紹介したいです!Swiftで書かれて、Swiftをビルドするボット、 porygon
と呼んでる。
このボットには、Slackでこういうメッセージ(コマンド)を送ると:
@porygon deploy jp-fabric-staging
XcodeのコンパイルからiTunesConnectやCrashlyticsまでデプロイしてくれるボットです!リアルタイムの報告もできる:
そしてporygonの特徴は、ローカルのMacでも実行できる!
porygonみたいなボットが作りたくなってきたかな?
Technologies Used
Porygon is powered by といえば、
1. Xcode 8:iOS+Swift 3アプリのビルド
2. Vapor:サーバーサイドSwiftフレームワーク
[GitHub - vapor/vapor: A web framework and server for Swift that works on macOS and Ubuntu.](https://github.com/vapor/vapor)3. Fabric(特にBeta):ベータテスト用アプリの配信に便利
[Fabric - Twitter's Mobile Development Platform](https://get.fabric.io/)4. Fastlane:デプロイの作業など、全てスクリプト化されてる
[fastlane - iOS and Android Automation for Continuous Delivery](https://fastlane.tools/)5. そして、もちろん、Slack
[Slack: Be less busy](https://slack.com/)Architecture
実はVaporの会社はSlackbotのテンプレートフレームワークまで作ってくれた:
GitHub - vapor/slack-bot
このボットは基本的にSlackのRTM API(Websocket)で動いています。ボットトークンの設定などはテンプレートプロジェクトを参考すればすぐ分かると思います。
(slack botの初期化方法はこちらご参考まで! )
さて、これで全てのツールが揃いましたので、どう連携しているのか紹介します!
① SlackのメッセージをWebsocketで受信する
参考:https://api.slack.com/rtm
返ってきているJSONからは、text
項目をパースする。まず、ボットのメンションがないと無視するべきなので、そのバリデーションを行う。例えば:
@porygon deploy appstore-staging
メッセージを送ると、 @porygon
の部分のメンションは実際のJSONにはこういう形になる:
{
// ...
"channel": "C12345678",
"text": "<@U12345678> deploy appstore-staging",
// ...
}
C12345678
は返事を送るチャンネルのIDです。これを保持してレスポンスを送るときに使う。
U12345678
はボットのIDです。そのIDが見つかるとコマンドをパースして、自由にハンドリングする。私たちの場合、 deploy <Xcode scheme名>
が見つかったら Fastlane のシェルスクリプトをコールしている。
② FastlaneコマンドをSwiftコードから実行する
参考:https://fastlane.tools/
Swiftからシェルコマンドを実行するには、Process (NSProcess)
を使用する。
シェルのoutput streamはPipe (NSPipe)
で取得できる。
先の例を続けると、deploy appstore-staging
の文字列が見つかったら、
let task = Process()
// ...
task.launchPath = "/bin/bash"
task.arguments = ["-l", "-c", "fastlane deploy_appstore_staging"]
// ...
task.launch()
task.waitUntilExit()
を処理している。fastlane
コマンドで deploy_appstore_staging
というlaneを実行している。Fastlaneのlaneというのは、デプロイするときの全てのタスクを実施できるrubyスクリプトです。Fastfileの作り方にはこちらをご参考ください。
Fastfileが出来たら、
- Xcodeプロジェクトのビルド
- Code Sign
- Xcode Archive
- iTunesConnectへのアップロード又はFabricへのアップロード
を一発で fastlane <lane名>
コマンドでデプロイできる。
重要
fastlaneの実行はバックグラウンドスレッドで行わないと、ボットが動くなかったりwebsocketがタイムアウトしたりしてしまうので、Process
をコールするメソッドは必ずバックグラウンドスレッドで実行してください!Grand Central DispatchのSerial Queueはおすすめです。
③ & ④ FastlaneがXcodeプロジェクトをビルドして、出来上がったバイナリをiTunesConnectやFabricへアップロードする
これはfastlaneが全てやってくれる!
が、もちろんビルドの進捗・結果をslackbotに教えて欲しいですよね。そのために先ほどの Process
の初期化には、Pipe (NSPipe)
のオブサーバーを実装して、シェルのoutput文字列を全てパースする:
let task = Process()
// ...
task.arguments = ["-l", "-c", "fastlane deploy_appstore_staging"]
// ...
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
let readHandle = pipe.fileHandleForReading
readHandle.waitForDataInBackgroundAndNotify()
var logObserver: NSObjectProtocol?
logObserver = NotificationCenter.default.addObserver(
forName: NSNotification.Name.NSFileHandleDataAvailable,
object: readHandle,
queue: nil,
using: { (notification) -> Void in
let data = readHandle.availableData
if data.count > 0 {
if let string = String(data: data, encoding: .utf8) {
// stringはシェルの1行出力の文字列になります
print("[log] \(string)", terminator: "")
didReceiveLogString(string, channel: channel) // 後ほど説明する
}
readHandle.waitForDataInBackgroundAndNotify()
}
else {
print("[log] EOF on stdout from process")
NotificationCenter.default.removeObserver(logObserver)
}
}
)
task.launch()
task.waitUntilExit()
didReceiveLogString()
は出力からの文字列をパースするメソッドですが、進捗をどうすれば判定する?と思っているでしょう。そのためには、Fastfileの設定に戻りましょう。私たちの場合は、このようなコードをFastfileに入れている:
lane :deploy_appstore_staging do
# ...
bump_build
puts "--- Hook: Xcodeでコンパイル中..."
ipa_path = gym(scheme: scheme,
output_name: scheme,
export_team_id: team_id,
use_legacy_build_api: true,
include_bitcode: true,
include_symbols: true)
puts "--- Hook: Betaへアップロード中..."
crashlytics(ipa_path: ipa_path)
upload_symbols_to_crashlytics
puts "--- Hook: appstore-stagingの配布が完了しました!"
end
そう、コンソールへわざとputs
(プリント)している!その文字列には --- Hook:
みたいなプレフィックスをつけて、そのあとの文字列をslackbotがパースするべき、という仕組みです。
先ほどのdidReceiveLogString()
メソッドに戻ると、
func didReceiveLogString(_ string: String, channel: String) {
guard let range = string.range(of: "--- Hook:") else{
return // 定義したプレフィックスがないと無視する
}
let response = string.substring(from: range.lowerBound)
// responseをchannelへSlackのWeb APIで送る
}
response
をSlackのRTM APIのwebsocketではなく、Web APIでPOSTする。理由は、websocketのAPIでは送信済みのメッセージを変更できないからです。それと違くてWeb APIでは、
-
Deploying...
を chat.postMessageで送って、タイムスタンプを保存する - 同じタイムスタンプをchat.updateのパラメータで使って、
Xcodeでコンパイル中...
→Betaへアップロード中...
→appstore-stagingの配布が完了しました!
に上書きできる。
私たちの場合は、Slackのカスタム絵文字を使って、この感じで表示しています:
⑤ & ⑥ ビルドとアップロードの成功・失敗を判定してボットで報告する
エラーが発生する場合は --- Hook:
みたいなアウトプットはもちろん出せないです。失敗した時ログが知りたいので、それもファイルに保存してみましょう:
// ...
let pipe = Pipe()
// ...
// ログファイルを作る
FileManager.default.createFile(atPath: outputFilePath, contents: nil, attributes: nil)
let writeHandle = FileHandle(forWritingAtPath: outputFilePath)
var logObserver: NSObjectProtocol?
logObserver = NotificationCenter.default.addObserver(
forName: NSNotification.Name.NSFileHandleDataAvailable,
object: readHandle,
queue: nil,
using: { (notification) -> Void in
let data = readHandle.availableData
if data.count > 0 {
writeHandle?.write(data) // アウトプットをログファイルに保存
// ...
}
else {
// ...
}
}
)
var exitObserver: NSObjectProtocol?
exitObserver = NotificationCenter.default.addObserver(
forName: Process.didTerminateNotification,
object: task,
queue: nil,
using: { (notification) -> Void in
writeHandle?.closeFile()
NotificationCenter.default.removeObserver(exitObserver)
}
)
task.launch()
task.waitUntilExit()
まず、シェル出力をパースしているところには writeHandle?.write(data)
でファイルに書き込む。それで Process.didTerminateNotification
の通知をオブサーブして、 writeHandle?.closeFile()
でファイルを閉じる。
fastlane
のスクリプトが成功したか失敗したかは task.waitUntilExit()
が終わった後判別できる:
// ...
task.launch()
task.waitUntilExit()
if task.terminationStatus == 0 {
// ... 成功
}
else {
// ... 失敗
sendLogFile(outputFilePath, channel: channel)
}
成功した場合はchat.updateで報告する。
失敗した場合は、ログファイルをfiles.upload APIでアップロードする:
Bonus Features
Fastlaneで様々な便利な機能があるので、Slackbotに連携して是非実施してみてください!
- AppStoreの審査状況をSlackbotに教えてもらう
- gitブランチを選択できるようにする
- debugサーバーかproductionサーバーかパラメータ化する
- アプリのデプロイ先はFastlaneかiTunesConnectかパラメータ化する