4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Cluster,Inc.Advent Calendar 2017

Day 11

SlackにいるZenHubさんにIn Progressにイシューがないと怒られる話

Last updated at Posted at 2017-12-10

はじめに

弊社ではチャットにSlack、タスク管理にZenHubを使っています。

タスク管理ツールを使ってると、タスクの状態を更新し忘れることが多いですよね。

弊社でも、イシューをIn Progressにするのを忘れて、今何やってるんでしたっけ…? と聞かれるという事案が発生していました。

「〜することを忘れないようにする」というTryはエンジニアとしての敗北なので、ボットにお知らせしてもらうようにしましょう。

Kotlin製ボットフレームワークBotlin

Kotlinが大好きなので、ノリでボットフレームワークをつくりました。

これをしれっと業務で使っていきます。

Featureをつくる

Speaker Deckのスライドにもある通り、Ktorに影響を受けていますので、なにかやりたいことが生まれたらFeatureをつくっていきます。

GitHubクライアントにはkohsuke/github-api: Java API for GitHub、ZenHubのAPIはkittinunf/Fuel: The easiest HTTP networking library for Kotlin/Androidで直接叩きます。

今回はZenHubの特定のパイプライン(今回はIn Progress)にイシューがない人を探して通知するというFeatureを作っていきます。

Botlinには組み込みでCronというFeatureがあり定期的にコマンドを実行できるので、このFeatureが平日の定時内に30分に1回呼ばれるようにします。

実装の主要なところをダイジェストでご紹介します。

ZenHub.kt
// ...

class ZenHub(internal val configuration: Configuration) : BotFeature {
    // ...

    override fun install(context: BotFeatureContext) {
        FuelManager.instance.baseHeaders = mapOf("X-Authentication-Token" to configuration.authenticationToken)

        context.pipelineOf<BotMessageCommand>().intercept {
            if (it.command != "zenhub") {
                return@intercept
            }

            if (it.args.startsWith("in-progress")) {
                executeCheckIssuesInProgress(context, it, it.args.contains("--silent"))
                return@intercept
            }

            postMessage(context, it, """|使い方
                |```
                |zenhub in-progress
                |    パイプラインIn Progressにイシューがない人を怒る
                |```
            """.trimMargin())
        }
    }

    internal fun getBoardPipelines(context: BotFeatureContext, command: BotMessageCommand, callback: (Pipelines) -> Unit) {

        "https://api.zenhub.io/p1/repositories/${configuration.repositoryId}/board"
                .httpGet()
                .responseObject<Pipelines> { _, _, result ->
                    val (pipelines, error) = result

                    // エラーハンドリング...

                    callback.invoke(pipelines)
                }
    }

    internal fun postMessage(context: BotFeatureContext, command: BotMessageCommand, message: String) {
        val message = BotMessageRequest(BotEngineId("Slack"), command.channelId, message)
        context.pipelineOf<BotMessageRequest>().execute(message)
    }

    class Configuration {
        lateinit var authenticationToken: String
        // ...
    }

    companion object ZenHubFactory : BotFeatureFactory<Configuration> {
        override fun create(configure: Configuration.() -> Unit): BotFeature {
            val conf = Configuration().apply(configure)
            return ZenHub(conf)
        }
    }
}
InProgress.kt
// ...

fun ZenHub.executeCheckIssuesInProgress(context: BotFeatureContext, command: BotMessageCommand, shouldBeSilent: Boolean) {
    getBoardPipelines(context, command) { pipelines ->
        val issueNumbers = pipelines.issueIdsInPipeline(configuration.pipelineName)
        val repository = getRepository()
        val issues = issueNumbers.mapToGitHubIssues(repository)
        val assignedUsers = issues.distinctBy { it.number }.mapNotNull {
            if (it.assignee == null) {
                val message = "In Progressにアサインされてないイシュー(#${it.number})があるよ (#^ω^)ビキビキ\nhttps://github.com/${configuration.gitHubOrganization}/${configuration.gitHubRepositoryName}/issues/${it.number}"
                postMessage(context, command, message)
                null
            } else {
                it.assignees
            }
        }.flatten().map { it.login }

        val usersNotAssigned = configuration.usernamePairs.usersNotIncluded(assignedUsers)

        if (usersNotAssigned.count() > 0) {
            usersNotAssigned.forEach {
                val message = "@${configuration.usernamePairs.slackUsername(it)} In Progressにイシューがないよ〜〜 :innocent: :innocent: :innocent:"
                postMessage(context, command, message)
            }
        } else {
            if (!shouldBeSilent) {
                postMessage(context, command, "【朗報】全員In Progressのイシューがある【:tada::tada::tada:】")
            }
        }
    }
}
Main.kt
fun main(args: Array<String>) {
    botlin {
        install(SlackEngine) {
            token = System.getenv("SLACK_TOKEN")
        }

        install(RedisStorage) {
            uri = URI(System.getenv("REDIS_URL"))
        }

        install(MessageCommand)
        install(Cron)
        install(Echo)
        install(ZenHub) {
            authenticationToken = System.getenv("ZENHUB_TOKEN")
            repositoryId = System.getenv("ZENHUB_REPOSITORY_ID")
            pipelineName = System.getenv("ZENHUB_PIPELINE_NAME")
            gitHubUsername = System.getenv("GITHUB_USERNAME")
            gitHubToken = System.getenv("GITHUB_TOKEN")
            gitHubOrganization = System.getenv("GITHUB_ORGANIZATION")
            gitHubRepositoryName = System.getenv("GITHUB_REPOSITORY_NAME")

             // GitHubとSlackのユーザー名が違う人のためのマッピング
            usernamePairs = setOf(
                    UsernamePair(github = "mizoguche", slack = "mizoguche"),
                    // ...
            )
        }
    }.start()
}

結果

怒られの発生

怒られが発生しました😇

まとめ

  • ちゃんとイシューのステータスを更新していきましょう🎉
  • Tryは気持ちではなく仕組みにしていきましょう🤖
  • Kotlinかわいい🐣
4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?