Slack에 있는 ZenHub씨에게 In Progress에 이슈가 없으면 화가 난 이야기

19278 단어 슬랙zenhubKotlinbot

소개



우리는 채팅에 슬랙, 작업 관리에 ZenHub을 사용합니다.

작업 관리 도구를 사용하면 작업 상태를 업데이트하는 것을 잊는 경우가 많지요.

폐사에서도 이슈를 In Progress로 만드는 것을 잊고 지금 무엇을 하고 있었는데… 라고 묻는 사안이 발생하고 있었습니다.

"~하는 것을 잊지 않게 한다"라는 Try는 엔지니어로서의 패배이므로, 봇에게 알려 주도록 합시다.

Kotlin 봇 프레임워크 Botlin



Kotlin을 사랑하기 때문에 김으로 봇 프레임 워크를 만들었습니다.
  • mizoguche/botlin: Bot framework built with Kotlin
  • Ktor가 재미 있었기 때문에 영감을 받아 봇 프레임 워크를 만든 이야기 // Speaker Deck

  • 이것을 확실히 업무로 사용해 갑니다.

    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 귀여운 🐣
  • 좋은 웹페이지 즐겨찾기