Slack에 있는 ZenHub씨에게 In Progress에 이슈가 없으면 화가 난 이야기
소개
우리는 채팅에 슬랙, 작업 관리에 ZenHub을 사용합니다.
작업 관리 도구를 사용하면 작업 상태를 업데이트하는 것을 잊는 경우가 많지요.
폐사에서도 이슈를 In Progress로 만드는 것을 잊고 지금 무엇을 하고 있었는데… 라고 묻는 사안이 발생하고 있었습니다.
"~하는 것을 잊지 않게 한다"라는 Try는 엔지니어로서의 패배이므로, 봇에게 알려 주도록 합시다.
Kotlin 봇 프레임워크 Botlin
Kotlin을 사랑하기 때문에 김으로 봇 프레임 워크를 만들었습니다.
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.ktfun 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()
}
결과
화가 났어요 😇
요약
// ...
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)
}
}
}
// ...
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:】")
}
}
}
}
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()
}
화가 났어요 😇
요약
Reference
이 문제에 관하여(Slack에 있는 ZenHub씨에게 In Progress에 이슈가 없으면 화가 난 이야기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://qiita.com/mizoguche/items/5ec246aca78c47635c21텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)