From 188f87811179800a23b3a347b7bd096089ae0492 Mon Sep 17 00:00:00 2001 From: SimonThormeyer Date: Thu, 18 Jun 2026 16:48:35 +0200 Subject: [PATCH] feat: aggregate CI message per commit, not per workflow run --- .../com/wire/github/GitHubMessageSender.kt | 210 ++++++++++++++++++ src/main/kotlin/com/wire/github/Routing.kt | 139 +++--------- .../github/github/GitHubWebhookManager.kt | 122 ++++++++-- .../com/wire/github/util/TemplateHandler.kt | 4 +- .../templates/en/workflow_run.template | 24 -- .../kotlin/com/wire/github/ApplicationTest.kt | 92 +++++--- 6 files changed, 402 insertions(+), 189 deletions(-) create mode 100644 src/main/kotlin/com/wire/github/GitHubMessageSender.kt delete mode 100644 src/main/resources/templates/en/workflow_run.template diff --git a/src/main/kotlin/com/wire/github/GitHubMessageSender.kt b/src/main/kotlin/com/wire/github/GitHubMessageSender.kt new file mode 100644 index 0000000..d3c5b64 --- /dev/null +++ b/src/main/kotlin/com/wire/github/GitHubMessageSender.kt @@ -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" +) diff --git a/src/main/kotlin/com/wire/github/Routing.kt b/src/main/kotlin/com/wire/github/Routing.kt index 539c1c2..4109f6c 100644 --- a/src/main/kotlin/com/wire/github/Routing.kt +++ b/src/main/kotlin/com/wire/github/Routing.kt @@ -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 @@ -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) @@ -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" @@ -77,19 +65,26 @@ fun Application.configureRouting() { val response = KtxSerializer.json.decodeFromString(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) @@ -97,96 +92,16 @@ fun Application.configureRouting() { } } -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" diff --git a/src/main/kotlin/com/wire/github/github/GitHubWebhookManager.kt b/src/main/kotlin/com/wire/github/github/GitHubWebhookManager.kt index 69834bb..2da5760 100644 --- a/src/main/kotlin/com/wire/github/github/GitHubWebhookManager.kt +++ b/src/main/kotlin/com/wire/github/github/GitHubWebhookManager.kt @@ -3,6 +3,7 @@ package com.wire.github.github import com.wire.github.util.ENV_VAR_GITHUB_REPO_INACTIVITY_SECONDS import com.wire.github.util.ENV_VAR_GITHUB_WEBHOOK_SECRET import com.wire.github.util.ENV_VAR_HOST +import com.wire.github.util.KtxSerializer import com.wire.github.util.toStorageKey import com.wire.sdk.model.QualifiedId import io.lettuce.core.api.StatefulRedisConnection @@ -10,6 +11,8 @@ import java.time.Clock import java.util.UUID import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString import org.koin.core.context.GlobalContext import org.slf4j.LoggerFactory @@ -39,6 +42,20 @@ class GitHubWebhookManager( val webhookId: Long ) + data class CommitCiSummary( + val messageId: UUID?, + val workflows: List + ) + + @Serializable + data class CommitCiWorkflow( + val runId: Long, + val name: String, + val status: String?, + val conclusion: String?, + val htmlUrl: String? + ) + fun ensureWebhookForConversation( repository: GitHubRepository, conversationId: QualifiedId @@ -94,24 +111,40 @@ class GitHubWebhookManager( storage.set(lastActivityKey(fullName), clock.millis().toString()) } - fun workflowRunMessageId( + fun updateCommitCiWorkflow( fullName: String, - workflowRunId: Long, - conversationId: QualifiedId - ): UUID? = - storage - .get(workflowRunMessageIdKey(fullName, workflowRunId, conversationId)) - ?.let { runCatching { UUID.fromString(it) }.getOrNull() } + headSha: String, + conversationId: QualifiedId, + workflow: CommitCiWorkflow + ): CommitCiSummary { + val workflowKey = commitCiWorkflowKey(fullName, headSha, conversationId, workflow.runId) + + storage.set(workflowKey, KtxSerializer.json.encodeToString(workflow)) + storage.sadd( + commitCiWorkflowIdsKey(fullName, headSha, conversationId), + workflow.runId.toString() + ) + rememberCommitCiKey(fullName, workflowKey) + rememberCommitCiKey( + fullName, + commitCiWorkflowIdsKey(fullName, headSha, conversationId) + ) + + return CommitCiSummary( + messageId = commitCiMessageId(fullName, headSha, conversationId), + workflows = commitCiWorkflows(fullName, headSha, conversationId) + ) + } - fun rememberWorkflowRunMessageId( + fun rememberCommitCiMessageId( fullName: String, - workflowRunId: Long, + headSha: String, conversationId: QualifiedId, messageId: UUID ) { - val key = workflowRunMessageIdKey(fullName, workflowRunId, conversationId) + val key = commitCiMessageIdKey(fullName, headSha, conversationId) storage.set(key, messageId.toString()) - storage.sadd(workflowRunMessageIdsKey(fullName), key) + rememberCommitCiKey(fullName, key) } fun cleanupInactiveRepositories() { @@ -139,10 +172,10 @@ class GitHubWebhookManager( storage.del(webhookIdKey(fullName)) storage.del(conversationsKey(fullName)) storage.del(lastActivityKey(fullName)) - storage.smembers(workflowRunMessageIdsKey(fullName)).forEach { key -> + storage.smembers(commitCiKeysKey(fullName)).forEach { key -> storage.del(key) } - storage.del(workflowRunMessageIdsKey(fullName)) + storage.del(commitCiKeysKey(fullName)) logger.info("Removed inactive GitHub webhook subscription for repository: $fullName") } @@ -173,16 +206,69 @@ class GitHubWebhookManager( private fun lastActivityKey(fullName: String): String = "$REPOSITORY_KEY_PREFIX:$fullName:last_activity" - private fun workflowRunMessageIdKey( + private fun commitCiMessageId( + fullName: String, + headSha: String, + conversationId: QualifiedId + ): UUID? = + storage + .get(commitCiMessageIdKey(fullName, headSha, conversationId)) + ?.let { runCatching { UUID.fromString(it) }.getOrNull() } + + private fun commitCiWorkflows( + fullName: String, + headSha: String, + conversationId: QualifiedId + ): List { + val workflows = storage + .smembers(commitCiWorkflowIdsKey(fullName, headSha, conversationId)) + .mapNotNull { workflowRunId -> + val runId = workflowRunId.toLongOrNull() ?: return@mapNotNull null + val workflowKey = commitCiWorkflowKey(fullName, headSha, conversationId, runId) + val workflow = storage.get(workflowKey) ?: return@mapNotNull null + + runCatching { + KtxSerializer.json.decodeFromString(workflow) + }.getOrNull() + } + + return workflows.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) + } + + private fun rememberCommitCiKey( + fullName: String, + key: String + ) { + storage.sadd(commitCiKeysKey(fullName), key) + } + + private fun commitCiMessageIdKey( + fullName: String, + headSha: String, + conversationId: QualifiedId + ): String = + "$REPOSITORY_KEY_PREFIX:$fullName:commit_ci:$headSha:message:" + + conversationId.toStorageKey() + + private fun commitCiWorkflowIdsKey( fullName: String, - workflowRunId: Long, + headSha: String, conversationId: QualifiedId ): String = - "$REPOSITORY_KEY_PREFIX:$fullName:workflow_run:$workflowRunId:" + + "$REPOSITORY_KEY_PREFIX:$fullName:commit_ci:$headSha:workflows:" + + conversationId.toStorageKey() + + private fun commitCiWorkflowKey( + fullName: String, + headSha: String, + conversationId: QualifiedId, + workflowRunId: Long + ): String = + "$REPOSITORY_KEY_PREFIX:$fullName:commit_ci:$headSha:workflow:$workflowRunId:" + conversationId.toStorageKey() - private fun workflowRunMessageIdsKey(fullName: String): String = - "$REPOSITORY_KEY_PREFIX:$fullName:workflow_run_messages" + private fun commitCiKeysKey(fullName: String): String = + "$REPOSITORY_KEY_PREFIX:$fullName:commit_ci_keys" companion object { const val GITHUB_WEBHOOK_PATH = "github/webhook" diff --git a/src/main/kotlin/com/wire/github/util/TemplateHandler.kt b/src/main/kotlin/com/wire/github/util/TemplateHandler.kt index ec572fd..ecbc2d3 100644 --- a/src/main/kotlin/com/wire/github/util/TemplateHandler.kt +++ b/src/main/kotlin/com/wire/github/util/TemplateHandler.kt @@ -62,9 +62,7 @@ class TemplateHandler { ): String? = when (event) { EVENT_WORKFLOW_RUN -> - response.workflowRun - ?.takeIf { it.id != null } - ?.let { eventTemplatePath(event) } + null else -> response.action?.let { actionTemplatePath( event = event, diff --git a/src/main/resources/templates/en/workflow_run.template b/src/main/resources/templates/en/workflow_run.template deleted file mode 100644 index bb3d52e..0000000 --- a/src/main/resources/templates/en/workflow_run.template +++ /dev/null @@ -1,24 +0,0 @@ -{{#workflowRun.inProgress}} -⏳ **CI Running**{{#workflowRun.name}} {{workflowRun.name}}{{/workflowRun.name}} -{{/workflowRun.inProgress}} -{{#workflowRun.successful}} -✅ **CI Passed!**{{#workflowRun.name}} {{workflowRun.name}}{{/workflowRun.name}} -{{/workflowRun.successful}} -{{#workflowRun.failed}} -❌ **CI Failed!**{{#workflowRun.name}} {{workflowRun.name}}{{/workflowRun.name}} -{{/workflowRun.failed}} -{{#workflowRun.cancelled}} -⚪ **CI Cancelled**{{#workflowRun.name}} {{workflowRun.name}}{{/workflowRun.name}} -{{/workflowRun.cancelled}} -{{#workflowRun.neutral}} -⚪ **CI Completed**{{#workflowRun.name}} {{workflowRun.name}}{{/workflowRun.name}}{{#workflowRun.conclusion}} ({{workflowRun.conclusion}}){{/workflowRun.conclusion}} -{{/workflowRun.neutral}} - -📦 **Repository:** {{repository.fullName}} -{{#workflowRun.status}}📋 **Status:** {{workflowRun.status}} -{{/workflowRun.status}}{{#workflowRun.conclusion}}🏁 **Conclusion:** {{workflowRun.conclusion}} -{{/workflowRun.conclusion}} -{{#workflowRun.headBranch}}🌿 **Branch:** {{workflowRun.headBranch}} -{{/workflowRun.headBranch}}{{#workflowRun.headSha}}🔖 **Commit:** `{{workflowRun.headSha}}` -{{/workflowRun.headSha}}{{#workflowRun.htmlUrl}}🔗 **Run:** {{workflowRun.htmlUrl}} -{{/workflowRun.htmlUrl}} diff --git a/src/test/kotlin/com/wire/github/ApplicationTest.kt b/src/test/kotlin/com/wire/github/ApplicationTest.kt index 4968946..be29e55 100644 --- a/src/test/kotlin/com/wire/github/ApplicationTest.kt +++ b/src/test/kotlin/com/wire/github/ApplicationTest.kt @@ -33,6 +33,7 @@ import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.koin.dsl.module import kotlin.test.assertIs +import kotlin.test.assertTrue class ApplicationTest { @BeforeTest @@ -161,7 +162,7 @@ class ApplicationTest { } @Test - fun `given workflow run has no stored message, then sends text message and stores id`() { + fun `given commit has no CI message, then sends aggregate status and stores id`() { val signatureValidator = mockk() every { signatureValidator.isValid( @@ -184,12 +185,6 @@ class ApplicationTest { every { wireAppSdk.getApplicationManager() } returns applicationManager val templateHandler = mockk() - every { - templateHandler.handleEvent( - event = EVENT_WORKFLOW_RUN, - response = any() - ) - } returns DUMMY_TEMPLATE val gitHubWebhookManager = mockk() every { @@ -197,16 +192,35 @@ class ApplicationTest { } returns listOf(CONVERSATION_ID) justRun { gitHubWebhookManager.markRepositoryActive(REPOSITORY_FULL_NAME) } every { - gitHubWebhookManager.workflowRunMessageId( + gitHubWebhookManager.updateCommitCiWorkflow( fullName = REPOSITORY_FULL_NAME, - workflowRunId = WORKFLOW_RUN_ID, - conversationId = CONVERSATION_ID + headSha = WORKFLOW_HEAD_SHA, + conversationId = CONVERSATION_ID, + workflow = any() + ) + } returns GitHubWebhookManager.CommitCiSummary( + messageId = null, + workflows = listOf( + GitHubWebhookManager.CommitCiWorkflow( + runId = WORKFLOW_RUN_ID, + name = "Build", + status = "in_progress", + conclusion = null, + htmlUrl = "https://github.com/wireapp/github-app/actions/runs/$WORKFLOW_RUN_ID" + ), + GitHubWebhookManager.CommitCiWorkflow( + runId = SECOND_WORKFLOW_RUN_ID, + name = "Tests", + status = "completed", + conclusion = "success", + htmlUrl = SECOND_WORKFLOW_URL + ) ) - } returns null + ) justRun { - gitHubWebhookManager.rememberWorkflowRunMessageId( + gitHubWebhookManager.rememberCommitCiMessageId( fullName = REPOSITORY_FULL_NAME, - workflowRunId = WORKFLOW_RUN_ID, + headSha = WORKFLOW_HEAD_SHA, conversationId = CONVERSATION_ID, messageId = sentMessageId ) @@ -237,11 +251,14 @@ class ApplicationTest { } assertEquals(HttpStatusCode.OK, response.status) - assertIs(sentMessage.captured) + val textMessage = assertIs(sentMessage.captured) + assertTrue(textMessage.text.contains("**Check suite status:** Running")) + assertTrue(textMessage.text.contains("**Build:** In Progress")) + assertTrue(textMessage.text.contains("**Tests:** Success")) verify(exactly = 1) { - gitHubWebhookManager.rememberWorkflowRunMessageId( + gitHubWebhookManager.rememberCommitCiMessageId( fullName = REPOSITORY_FULL_NAME, - workflowRunId = WORKFLOW_RUN_ID, + headSha = WORKFLOW_HEAD_SHA, conversationId = CONVERSATION_ID, messageId = sentMessageId ) @@ -250,7 +267,7 @@ class ApplicationTest { } @Test - fun `given workflow run has stored message, then edits message and stores new id`() { + fun `given commit has stored CI message, then edits aggregate status and stores new id`() { val signatureValidator = mockk() every { signatureValidator.isValid( @@ -274,12 +291,6 @@ class ApplicationTest { every { wireAppSdk.getApplicationManager() } returns applicationManager val templateHandler = mockk() - every { - templateHandler.handleEvent( - event = EVENT_WORKFLOW_RUN, - response = any() - ) - } returns DUMMY_TEMPLATE val gitHubWebhookManager = mockk() every { @@ -287,16 +298,28 @@ class ApplicationTest { } returns listOf(CONVERSATION_ID) justRun { gitHubWebhookManager.markRepositoryActive(REPOSITORY_FULL_NAME) } every { - gitHubWebhookManager.workflowRunMessageId( + gitHubWebhookManager.updateCommitCiWorkflow( fullName = REPOSITORY_FULL_NAME, - workflowRunId = WORKFLOW_RUN_ID, - conversationId = CONVERSATION_ID + headSha = WORKFLOW_HEAD_SHA, + conversationId = CONVERSATION_ID, + workflow = any() + ) + } returns GitHubWebhookManager.CommitCiSummary( + messageId = storedMessageId, + workflows = listOf( + GitHubWebhookManager.CommitCiWorkflow( + runId = WORKFLOW_RUN_ID, + name = "Build", + status = "completed", + conclusion = "success", + htmlUrl = "https://github.com/wireapp/github-app/actions/runs/$WORKFLOW_RUN_ID" + ) ) - } returns storedMessageId + ) justRun { - gitHubWebhookManager.rememberWorkflowRunMessageId( + gitHubWebhookManager.rememberCommitCiMessageId( fullName = REPOSITORY_FULL_NAME, - workflowRunId = WORKFLOW_RUN_ID, + headSha = WORKFLOW_HEAD_SHA, conversationId = CONVERSATION_ID, messageId = editedMessageId ) @@ -329,10 +352,12 @@ class ApplicationTest { assertEquals(HttpStatusCode.OK, response.status) val editedMessage = assertIs(sentMessage.captured) assertEquals(storedMessageId, editedMessage.replacingMessageId) + assertTrue(editedMessage.newContent.contains("**Check suite status:** Passed")) + assertTrue(editedMessage.newContent.contains("**Build:** Success")) verify(exactly = 1) { - gitHubWebhookManager.rememberWorkflowRunMessageId( + gitHubWebhookManager.rememberCommitCiMessageId( fullName = REPOSITORY_FULL_NAME, - workflowRunId = WORKFLOW_RUN_ID, + headSha = WORKFLOW_HEAD_SHA, conversationId = CONVERSATION_ID, messageId = editedMessageId ) @@ -352,6 +377,9 @@ class ApplicationTest { const val DUMMY_WEBHOOK_SECRET = "dummyWebhookSecret" const val REPOSITORY_FULL_NAME = "dummy_repository_full_name" const val WORKFLOW_RUN_ID = 1234L + const val SECOND_WORKFLOW_RUN_ID = 5678L + const val WORKFLOW_HEAD_SHA = "1234567890abcdef" + const val SECOND_WORKFLOW_URL = "https://example.com/actions/runs/$SECOND_WORKFLOW_RUN_ID" val DUMMY_PAYLOAD = """ { "action": "created", @@ -374,7 +402,7 @@ class ApplicationTest { "name": "Build", "html_url": "https://github.com/wireapp/github-app/actions/runs/$WORKFLOW_RUN_ID", "head_branch": "main", - "head_sha": "1234567890abcdef", + "head_sha": "$WORKFLOW_HEAD_SHA", "status": "in_progress", "conclusion": null },