Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions src/main/kotlin/com/wire/github/GitHubMessageSender.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package com.wire.github

import com.wire.github.github.GitHubWebhookManager
import com.wire.github.response.model.GitHubResponse
import com.wire.github.response.model.WorkflowRun
import com.wire.sdk.WireAppSdk
import com.wire.sdk.model.QualifiedId
import com.wire.sdk.model.WireMessage
import com.wire.sdk.service.WireApplicationManager
import java.util.Locale
import java.util.UUID

internal fun WireAppSdk.sendGitHubMessage(
gitHubWebhookManager: GitHubWebhookManager,
response: GitHubResponse,
text: String
) {
gitHubWebhookManager
.conversationsForRepository(response.repository.fullName)
.forEach { conversationId ->
sendGitHubMessage(
conversationId = conversationId,
text = text
)
}
}

private fun WireAppSdk.sendGitHubMessage(
conversationId: QualifiedId,
text: String
) {
getApplicationManager().sendTextMessage(
conversationId = conversationId,
text = text
)
}

internal fun WireAppSdk.sendCommitCiMessage(
gitHubWebhookManager: GitHubWebhookManager,
response: GitHubResponse,
workflowRun: WorkflowRun?
) {
val headSha = workflowRun?.headSha ?: return
val workflowRunId = workflowRun.id ?: return
val workflow = GitHubWebhookManager.CommitCiWorkflow(
runId = workflowRunId,
name = workflowRun.name ?: "Workflow $workflowRunId",
status = workflowRun.status,
conclusion = workflowRun.conclusion,
htmlUrl = workflowRun.htmlUrl
)

gitHubWebhookManager
.conversationsForRepository(response.repository.fullName)
.forEach { conversationId ->
sendCommitCiMessage(
gitHubWebhookManager = gitHubWebhookManager,
context = CommitCiMessageContext(
response = response,
workflowRun = workflowRun,
workflow = workflow,
headSha = headSha,
conversationId = conversationId
)
)
}
}

private fun WireAppSdk.sendCommitCiMessage(
gitHubWebhookManager: GitHubWebhookManager,
context: CommitCiMessageContext
): UUID {
val summary = gitHubWebhookManager.updateCommitCiWorkflow(
fullName = context.response.repository.fullName,
headSha = context.headSha,
conversationId = context.conversationId,
workflow = context.workflow
)
val text = CommitCiMessageFormatter.toMessage(
summary = summary,
context = context
)

val messageId = getApplicationManager().sendTextMessage(
replacingMessageId = summary.messageId,
conversationId = context.conversationId,
text = text
)
gitHubWebhookManager.rememberCommitCiMessageId(
fullName = context.response.repository.fullName,
headSha = context.headSha,
conversationId = context.conversationId,
messageId = messageId
)

return messageId
}

private fun WireApplicationManager.sendTextMessage(
replacingMessageId: UUID? = null,
conversationId: QualifiedId,
text: String
): UUID {
val message = replacingMessageId
?.let {
WireMessage.TextEdited.create(
replacingMessageId = it,
conversationId = conversationId,
text = text
)
}
?: WireMessage.Text.create(
conversationId = conversationId,
text = text
)

return sendMessage(message = message)
}

private data class CommitCiMessageContext(
val response: GitHubResponse,
val workflowRun: WorkflowRun,
val workflow: GitHubWebhookManager.CommitCiWorkflow,
val headSha: String,
val conversationId: QualifiedId
)

private object CommitCiMessageFormatter {
fun toMessage(
summary: GitHubWebhookManager.CommitCiSummary,
context: CommitCiMessageContext
): String =
buildString {
val icon = headlineIcon(summary)
val status = headlineStatus(summary)
val headline = "$icon **Check suite status:** $status"

appendLine(headline)
appendLine()
appendLine("📦 **Repository:** ${context.response.repository.fullName}")
context.workflowRun.headBranch?.let { appendLine("🌿 **Branch:** $it") }
appendLine("🔖 **Commit:** `${context.headSha.take(COMMIT_SHA_DISPLAY_LENGTH)}`")
appendLine()
summary.workflows.forEach { workflow ->
appendLine(
"${workflow.icon()} **${workflow.name}:** " +
"${workflow.displayStatus()}${workflow.link()}"
)
}
}.trim()

private fun headlineIcon(summary: GitHubWebhookManager.CommitCiSummary): String =
when {
summary.workflows.any { it.failed() } -> "❌"
summary.workflows.isNotEmpty() && summary.workflows.all { it.successful() } -> "✅"
summary.workflows.all { it.completed() } -> "⚪"
else -> "⏳"
}

private fun headlineStatus(summary: GitHubWebhookManager.CommitCiSummary): String =
when {
summary.workflows.any { it.failed() } -> "Failed"
summary.workflows.isNotEmpty() && summary.workflows.all { it.successful() } -> "Passed"
summary.workflows.all { it.completed() } -> "Completed"
else -> "Running"
}

private fun GitHubWebhookManager.CommitCiWorkflow.displayStatus(): String =
(conclusion ?: status ?: "unknown").humanizeStatus()

private fun GitHubWebhookManager.CommitCiWorkflow.icon(): String =
when {
failed() -> "❌"
successful() -> "✅"
completed() -> "⚪"
else -> "⏳"
}

private fun GitHubWebhookManager.CommitCiWorkflow.link(): String =
htmlUrl?.let { " - $it" } ?: ""

private fun GitHubWebhookManager.CommitCiWorkflow.successful(): Boolean =
conclusion == WORKFLOW_CONCLUSION_SUCCESS

private fun GitHubWebhookManager.CommitCiWorkflow.failed(): Boolean =
conclusion in WORKFLOW_FAILURE_CONCLUSIONS

private fun GitHubWebhookManager.CommitCiWorkflow.completed(): Boolean =
status == WORKFLOW_STATUS_COMPLETED || conclusion != null

private fun String.humanizeStatus(): String =
split("_")
.joinToString(" ") { word ->
word.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
}
}

private const val COMMIT_SHA_DISPLAY_LENGTH = 12
private const val WORKFLOW_CONCLUSION_SUCCESS = "success"
private const val WORKFLOW_STATUS_COMPLETED = "completed"

private val WORKFLOW_FAILURE_CONCLUSIONS = setOf(
"failure",
"error",
"timed_out",
"action_required",
"startup_failure"
)
139 changes: 27 additions & 112 deletions src/main/kotlin/com/wire/github/Routing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import com.wire.github.util.KtxSerializer
import com.wire.github.util.SignatureValidator
import com.wire.github.util.TemplateHandler
import com.wire.sdk.WireAppSdk
import com.wire.sdk.model.QualifiedId
import com.wire.sdk.model.WireMessage
import com.wire.sdk.service.WireApplicationManager
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
Expand All @@ -23,7 +20,6 @@ import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.routing
import kotlinx.serialization.ExperimentalSerializationApi
import java.util.UUID
import org.koin.core.context.GlobalContext

@OptIn(ExperimentalSerializationApi::class)
Expand Down Expand Up @@ -59,15 +55,7 @@ fun Application.configureRouting() {
// Payload
val payload = call.receiveText()

// Validation of received signature
val isSignatureValid = signatureValidator.isValid(
signature = signature,
payload = payload,
secret = requireNotNull(ENV_VAR_GITHUB_WEBHOOK_SECRET) {
"GHAPP_GITHUB_WEBHOOK_SECRET must be set to validate GitHub webhooks"
}
)
if (!isSignatureValid) {
if (!signatureValidator.isValidGithubWebhookSignature(signature, payload)) {
return@post call.respond(
status = HttpStatusCode.Forbidden,
message = "Invalid GitHub webhook signature"
Expand All @@ -77,116 +65,43 @@ fun Application.configureRouting() {
val response = KtxSerializer.json.decodeFromString<GitHubResponse>(payload)
gitHubWebhookManager.markRepositoryActive(response.repository.fullName)

// Handle event response and send message
val messageTemplate = templateHandler.handleEvent(
event = event,
response = response
)

messageTemplate?.let { message ->
wireAppSdk.sendGitHubMessage(
if (event == EVENT_WORKFLOW_RUN) {
wireAppSdk.sendCommitCiMessage(
gitHubWebhookManager = gitHubWebhookManager,
event = event,
response = response,
text = message
workflowRun = response.workflowRun
)
} else {
// Handle event response and send message
val messageTemplate = templateHandler.handleEvent(
event = event,
response = response
)

messageTemplate?.let { message ->
wireAppSdk.sendGitHubMessage(
gitHubWebhookManager = gitHubWebhookManager,
response = response,
text = message
)
}
}

return@post call.response.status(HttpStatusCode.OK)
}
}
}

private fun WireAppSdk.sendGitHubMessage(
gitHubWebhookManager: GitHubWebhookManager,
event: String,
response: GitHubResponse,
text: String
) {
gitHubWebhookManager
.conversationsForRepository(response.repository.fullName)
.forEach { conversationId ->
sendGitHubMessage(
gitHubWebhookManager = gitHubWebhookManager,
event = event,
response = response,
conversationId = conversationId,
text = text
)
}
}

private fun WireAppSdk.sendGitHubMessage(
gitHubWebhookManager: GitHubWebhookManager,
event: String,
response: GitHubResponse,
conversationId: QualifiedId,
text: String
) {
val workflowRunId = response.workflowRun?.id
val messageId = if (event == EVENT_WORKFLOW_RUN && workflowRunId != null) {
getApplicationManager().sendWorkflowRunMessage(
gitHubWebhookManager = gitHubWebhookManager,
repositoryFullName = response.repository.fullName,
workflowRunId = workflowRunId,
conversationId = conversationId,
text = text
)
} else {
getApplicationManager().sendTextMessage(
conversationId = conversationId,
text = text
)
}

if (event == EVENT_WORKFLOW_RUN && workflowRunId != null) {
gitHubWebhookManager.rememberWorkflowRunMessageId(
fullName = response.repository.fullName,
workflowRunId = workflowRunId,
conversationId = conversationId,
messageId = messageId
)
}
}

private fun WireApplicationManager.sendWorkflowRunMessage(
gitHubWebhookManager: GitHubWebhookManager,
repositoryFullName: String,
workflowRunId: Long,
conversationId: QualifiedId,
text: String
): UUID {
val replacingMessageId = gitHubWebhookManager.workflowRunMessageId(
fullName = repositoryFullName,
workflowRunId = workflowRunId,
conversationId = conversationId
)

val message = replacingMessageId
?.let {
WireMessage.TextEdited.create(
replacingMessageId = it,
conversationId = conversationId,
text = text
)
private fun SignatureValidator.isValidGithubWebhookSignature(
signature: String,
payload: String
): Boolean =
isValid(
signature = signature,
payload = payload,
secret = requireNotNull(ENV_VAR_GITHUB_WEBHOOK_SECRET) {
"GHAPP_GITHUB_WEBHOOK_SECRET must be set to validate GitHub webhooks"
}
?: WireMessage.Text.create(
conversationId = conversationId,
text = text
)

return sendMessage(message = message)
}

private fun WireApplicationManager.sendTextMessage(
conversationId: QualifiedId,
text: String
): UUID =
sendMessage(
message = WireMessage.Text.create(
conversationId = conversationId,
text = text
)
)

private const val EVENT_WORKFLOW_RUN = "workflow_run"
Loading