From 021bb8c73682e721b0363d31ddbea50354fb3fdc Mon Sep 17 00:00:00 2001 From: alexandreferris Date: Tue, 29 Sep 2026 14:25:29 +0200 Subject: [PATCH 1/2] feat: Add Github Actions workflow in webhook #WPB-28780 * Add actions workflow run notifications * Add actions workflow job completion notifications * Render CI results through Mustache templates * Cover Actions payloads and status rendering --- README.md | 13 ++ .../kotlin/com/wire/github/EventsHandler.kt | 5 +- .../github/response/model/GitHubResponse.kt | 4 + .../wire/github/response/model/WorkflowJob.kt | 25 ++++ .../wire/github/response/model/WorkflowRun.kt | 30 +++++ .../en/workflow_job.completed.template | 5 + .../en/workflow_run.completed.template | 5 + .../events/workflow_job.completed.json | 18 +++ .../events/workflow_run.completed.json | 23 ++++ .../messages/workflow_job.completed.txt | 4 + .../messages/workflow_run.completed.txt | 5 + .../kotlin/com/wire/github/ApplicationTest.kt | 44 +++++++ .../wire/github/util/TemplateHandlerTest.kt | 114 ++++++++++++++++++ 13 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/wire/github/response/model/WorkflowJob.kt create mode 100644 src/main/kotlin/com/wire/github/response/model/WorkflowRun.kt create mode 100644 src/main/resources/templates/en/workflow_job.completed.template create mode 100644 src/main/resources/templates/en/workflow_run.completed.template create mode 100644 src/test/fixtures/events/workflow_job.completed.json create mode 100644 src/test/fixtures/events/workflow_run.completed.json create mode 100644 src/test/fixtures/messages/workflow_job.completed.txt create mode 100644 src/test/fixtures/messages/workflow_run.completed.txt diff --git a/README.md b/README.md index 5953749..33760bf 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,19 @@ Here's a list of features included in this project: | /health | Healthcheck endpoint returning HTTP OK 200. | | /{conversation_id}/{conversation_domain} | Webhook endpoint. | +### GitHub Actions notifications + +The existing webhook endpoint supports centralized notifications for these GitHub Actions events: + +| GitHub event | Supported action | Notification | +|--------------|------------------|--------------| +| Workflow runs (`workflow_run`) | `completed` | One summary for the completed workflow run. | +| Workflow jobs (`workflow_job`) | `completed` | One notification for each completed job in the workflow run. | + +All completed conclusions are reported, including successful, failed, cancelled, skipped, and timed-out +results when GitHub provides them. Other actions, such as `requested`, `queued`, and `in_progress`, are +accepted by the endpoint but do not produce a Wire message. + ## Building & Running To build or run the project, you can use the IDE Run configuration with environment variables. diff --git a/src/main/kotlin/com/wire/github/EventsHandler.kt b/src/main/kotlin/com/wire/github/EventsHandler.kt index 55fcf26..94be414 100644 --- a/src/main/kotlin/com/wire/github/EventsHandler.kt +++ b/src/main/kotlin/com/wire/github/EventsHandler.kt @@ -96,7 +96,10 @@ class EventsHandler : WireEventsHandlerSuspending() { "2. Go to **Settings / Webhooks / Add webhook**\n" + "3. Add **Payload URL**: %s\n" + "4. Set **Content-Type**: application/json\n" + - "5. Set **Secret**: %s", + "5. Set **Secret**: %s\n" + + "6. Select the repository events you want to receive\n" + + "7. For GitHub Actions notifications, select **Workflow runs** and " + + "**Workflow jobs**. Completed results will be sent to this conversation.", url, generatedSecret ) diff --git a/src/main/kotlin/com/wire/github/response/model/GitHubResponse.kt b/src/main/kotlin/com/wire/github/response/model/GitHubResponse.kt index 7be3b07..6887bda 100644 --- a/src/main/kotlin/com/wire/github/response/model/GitHubResponse.kt +++ b/src/main/kotlin/com/wire/github/response/model/GitHubResponse.kt @@ -15,6 +15,10 @@ data class GitHubResponse( val compare: String? = null, val review: Review? = null, val repository: Repository, + @SerialName("workflow_run") + val workflowRun: WorkflowRun? = null, + @SerialName("workflow_job") + val workflowJob: WorkflowJob? = null, val created: Boolean? = null, val deleted: Boolean? = null ) diff --git a/src/main/kotlin/com/wire/github/response/model/WorkflowJob.kt b/src/main/kotlin/com/wire/github/response/model/WorkflowJob.kt new file mode 100644 index 0000000..5bf66af --- /dev/null +++ b/src/main/kotlin/com/wire/github/response/model/WorkflowJob.kt @@ -0,0 +1,25 @@ +package com.wire.github.response.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class WorkflowJob( + val name: String, + val conclusion: String?, + @SerialName("html_url") + val htmlUrl: String, + @SerialName("workflow_name") + val workflowName: String? = null, + @SerialName("head_branch") + val headBranch: String? = null +) { + val emoji: String + get() = when (conclusion) { + "success" -> "✅" + "failure" -> "❌" + "cancelled" -> "đŸšĢ" + "skipped" -> "â­ī¸" + else -> "â„šī¸" + } +} diff --git a/src/main/kotlin/com/wire/github/response/model/WorkflowRun.kt b/src/main/kotlin/com/wire/github/response/model/WorkflowRun.kt new file mode 100644 index 0000000..a75c71a --- /dev/null +++ b/src/main/kotlin/com/wire/github/response/model/WorkflowRun.kt @@ -0,0 +1,30 @@ +package com.wire.github.response.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class WorkflowRun( + val name: String, + val conclusion: String?, + val event: String, + val actor: User, + @SerialName("head_branch") + val headBranch: String, + @SerialName("html_url") + val htmlUrl: String, + @SerialName("run_number") + val runNumber: Int +) { + val emoji: String + get() = when (conclusion) { + "success" -> "✅" + "failure" -> "❌" + "cancelled" -> "đŸšĢ" + "timed_out" -> "âąī¸" + "action_required" -> "âš ī¸" + "skipped" -> "â­ī¸" + "stale" -> "💤" + else -> "â„šī¸" + } +} diff --git a/src/main/resources/templates/en/workflow_job.completed.template b/src/main/resources/templates/en/workflow_job.completed.template new file mode 100644 index 0000000..8ec0e4f --- /dev/null +++ b/src/main/resources/templates/en/workflow_job.completed.template @@ -0,0 +1,5 @@ +{{workflowJob.emoji}} **Job {{workflowJob.conclusion}}:** [{{{workflowJob.name}}}]({{workflowJob.htmlUrl}}) +**Repository:** {{repository.fullName}} +{{#workflowJob.workflowName}}**Workflow:** {{{workflowJob.workflowName}}} +{{/workflowJob.workflowName}}{{#workflowJob.headBranch}}**Branch:** `{{workflowJob.headBranch}}` +{{/workflowJob.headBranch}} diff --git a/src/main/resources/templates/en/workflow_run.completed.template b/src/main/resources/templates/en/workflow_run.completed.template new file mode 100644 index 0000000..99ee72d --- /dev/null +++ b/src/main/resources/templates/en/workflow_run.completed.template @@ -0,0 +1,5 @@ +{{workflowRun.emoji}} **Workflow {{workflowRun.conclusion}}:** [{{{workflowRun.name}}} #{{workflowRun.runNumber}}]({{workflowRun.htmlUrl}}) +**Repository:** {{repository.fullName}} +**Branch:** `{{workflowRun.headBranch}}` +**Trigger:** {{workflowRun.event}} +**Started by:** {{workflowRun.actor.login}} diff --git a/src/test/fixtures/events/workflow_job.completed.json b/src/test/fixtures/events/workflow_job.completed.json new file mode 100644 index 0000000..dd0091f --- /dev/null +++ b/src/test/fixtures/events/workflow_job.completed.json @@ -0,0 +1,18 @@ +{ + "action": "completed", + "workflow_job": { + "name": "unit-tests (JDK 21)", + "conclusion": "failure", + "html_url": "https://github.com/wireapp/example/actions/runs/123456789/job/987654321", + "workflow_name": "Build and test", + "head_branch": "feature/actions-notifications" + }, + "repository": { + "name": "example", + "full_name": "wireapp/example" + }, + "sender": { + "login": "octocat", + "avatar_url": "https://github.com/images/error/octocat_happy.gif" + } +} diff --git a/src/test/fixtures/events/workflow_run.completed.json b/src/test/fixtures/events/workflow_run.completed.json new file mode 100644 index 0000000..8e023d2 --- /dev/null +++ b/src/test/fixtures/events/workflow_run.completed.json @@ -0,0 +1,23 @@ +{ + "action": "completed", + "workflow_run": { + "name": "Build and test", + "conclusion": "success", + "event": "push", + "actor": { + "login": "octocat", + "avatar_url": "https://github.com/images/error/octocat_happy.gif" + }, + "head_branch": "main", + "html_url": "https://github.com/wireapp/example/actions/runs/123456789", + "run_number": 42 + }, + "repository": { + "name": "example", + "full_name": "wireapp/example" + }, + "sender": { + "login": "octocat", + "avatar_url": "https://github.com/images/error/octocat_happy.gif" + } +} diff --git a/src/test/fixtures/messages/workflow_job.completed.txt b/src/test/fixtures/messages/workflow_job.completed.txt new file mode 100644 index 0000000..54167aa --- /dev/null +++ b/src/test/fixtures/messages/workflow_job.completed.txt @@ -0,0 +1,4 @@ +❌ **Job failure:** [unit-tests (JDK 21)](https://github.com/wireapp/example/actions/runs/123456789/job/987654321) +**Repository:** wireapp/example +**Workflow:** Build and test +**Branch:** `feature/actions-notifications` diff --git a/src/test/fixtures/messages/workflow_run.completed.txt b/src/test/fixtures/messages/workflow_run.completed.txt new file mode 100644 index 0000000..bb9fc67 --- /dev/null +++ b/src/test/fixtures/messages/workflow_run.completed.txt @@ -0,0 +1,5 @@ +✅ **Workflow success:** [Build and test #42](https://github.com/wireapp/example/actions/runs/123456789) +**Repository:** wireapp/example +**Branch:** `main` +**Trigger:** push +**Started by:** octocat diff --git a/src/test/kotlin/com/wire/github/ApplicationTest.kt b/src/test/kotlin/com/wire/github/ApplicationTest.kt index 9e7d198..d1bb17e 100644 --- a/src/test/kotlin/com/wire/github/ApplicationTest.kt +++ b/src/test/kotlin/com/wire/github/ApplicationTest.kt @@ -25,6 +25,7 @@ import io.micrometer.prometheusmetrics.PrometheusMeterRegistry import io.mockk.every import io.mockk.mockk import io.mockk.verify +import java.io.File import java.util.UUID import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -290,6 +291,49 @@ class ApplicationTest { } } + @Test + fun `given completed GitHub Actions events, when received, then messages are sent`() { + val signatureValidator = mockk() + every { signatureValidator.isValid(any(), any(), any(), any()) } returns true + + val wireAppSdk = mockk() + every { + wireAppSdk.getApplicationManager().sendMessage(message = any()) + } returns UUID.randomUUID() + + loadKoinModules( + module { + single { signatureValidator } + single { wireAppSdk } + } + ) + + testApplication { + application { + configureRouting() + } + + listOf("workflow_run", "workflow_job").forEach { event -> + val response = client.post("/${CONVERSATION_ID.id}/${CONVERSATION_ID.domain}") { + contentType(ContentType.Application.Json) + header("X-GitHub-Event", event) + header("X-Hub-Signature", "sha1=$DUMMY_SIGNATURE") + header("X-GitHub-Delivery", "delivery-$event") + setBody(eventFixture("$event.completed")) + } + + assertEquals(HttpStatusCode.OK, response.status) + } + + verify(exactly = 2) { + wireAppSdk.getApplicationManager().sendMessage(message = any()) + } + } + } + + private fun eventFixture(name: String): String = + File("src/test/fixtures/events/$name.json").readText() + private companion object { val CONVERSATION_ID = QualifiedId( id = UUID.randomUUID(), diff --git a/src/test/kotlin/com/wire/github/util/TemplateHandlerTest.kt b/src/test/kotlin/com/wire/github/util/TemplateHandlerTest.kt index 33609eb..73100d1 100644 --- a/src/test/kotlin/com/wire/github/util/TemplateHandlerTest.kt +++ b/src/test/kotlin/com/wire/github/util/TemplateHandlerTest.kt @@ -6,8 +6,12 @@ import com.wire.github.response.model.PullRequest import com.wire.github.response.model.Repository import com.wire.github.response.model.Review import com.wire.github.response.model.User +import com.wire.github.response.model.WorkflowJob +import com.wire.github.response.model.WorkflowRun +import java.io.File import kotlin.test.Test import kotlin.test.assertContains +import kotlin.test.assertEquals import kotlin.test.assertNull class TemplateHandlerTest { @@ -84,6 +88,98 @@ class TemplateHandlerTest { assertContains(message.orEmpty(), "Add feature") } + @Test + fun `renders a completed workflow run`() { + val message = renderFixture(event = "workflow_run") + + assertEquals(messageFixture("workflow_run.completed"), message?.trim()) + } + + @Test + fun `renders a completed workflow job`() { + val message = renderFixture(event = "workflow_job") + + assertEquals(messageFixture("workflow_job.completed"), message?.trim()) + } + + @Test + fun `does not render a workflow run before it completes`() { + val response = KtxSerializer.json.decodeFromString( + eventFixture("workflow_run.completed") + .replace("\"action\": \"completed\"", "\"action\": \"in_progress\"") + .replace("\"conclusion\": \"success\"", "\"conclusion\": null") + ) + + val message = templateHandler.handleEvent( + event = "workflow_run", + response = response + ) + + assertNull(message) + } + + @Test + fun `does not render a workflow job before it completes`() { + val response = KtxSerializer.json.decodeFromString( + eventFixture("workflow_job.completed") + .replace("\"action\": \"completed\"", "\"action\": \"in_progress\"") + .replace("\"conclusion\": \"failure\"", "\"conclusion\": null") + ) + + val message = templateHandler.handleEvent( + event = "workflow_job", + response = response + ) + + assertNull(message) + } + + @Test + fun `maps workflow run conclusions to emojis`() { + mapOf( + "success" to "✅", + "failure" to "❌", + "cancelled" to "đŸšĢ", + "timed_out" to "âąī¸", + "action_required" to "âš ī¸", + "skipped" to "â­ī¸", + "stale" to "💤", + "neutral" to "â„šī¸" + ).forEach { (conclusion, emoji) -> + assertEquals(emoji, workflowRun(conclusion).emoji) + } + } + + @Test + fun `maps workflow job conclusions to emojis`() { + mapOf( + "success" to "✅", + "failure" to "❌", + "cancelled" to "đŸšĢ", + "skipped" to "â­ī¸" + ).forEach { (conclusion, emoji) -> + assertEquals(emoji, workflowJob(conclusion).emoji) + } + } + + private fun renderFixture(event: String): String? { + val fixtureName = "$event.completed" + val response = KtxSerializer.json.decodeFromString( + eventFixture(fixtureName) + ) + + return templateHandler.handleEvent( + event = event, + response = response + ) + } + + private fun eventFixture(name: String): String = fixture("events/$name.json") + + private fun messageFixture(name: String): String = fixture("messages/$name.txt").trim() + + private fun fixture(path: String): String = File("src/test/fixtures/$path").readText() + private fun reviewResponse( body: String?, state: String = "approved" @@ -118,6 +214,24 @@ class TemplateHandlerTest { ) ) + private fun workflowRun(conclusion: String) = + WorkflowRun( + name = "Build and test", + conclusion = conclusion, + event = "push", + actor = user, + headBranch = "main", + htmlUrl = "https://github.com/wire/example/actions/runs/1", + runNumber = 1 + ) + + private fun workflowJob(conclusion: String) = + WorkflowJob( + name = "unit-tests", + conclusion = conclusion, + htmlUrl = "https://github.com/wire/example/actions/runs/1/job/1" + ) + private companion object { val user = User( avatarUrl = "https://github.com/wire.png", From 8d86143295b3765fab97ddf41a4024c36d1e045d Mon Sep 17 00:00:00 2001 From: alexandreferris Date: Tue, 29 Sep 2026 15:25:40 +0200 Subject: [PATCH 2/2] feat: Add Github Actions workflow in webhook #WPB-28780 * Handle nullable GitHub Actions payload fields * Unify conclusion status and emoji rendering * Improve templates and fallback test coverage --- .../wire/github/response/model/Conclusion.kt | 18 ++++ .../wire/github/response/model/WorkflowJob.kt | 20 ++--- .../wire/github/response/model/WorkflowRun.kt | 31 +++---- .../en/workflow_job.completed.template | 2 +- .../en/workflow_run.completed.template | 9 +- .../workflow_job.completed.minimal.json | 14 +++ .../workflow_run.completed.minimal.json | 14 +++ .../workflow_job.completed.minimal.txt | 2 + .../messages/workflow_job.completed.txt | 2 +- .../workflow_run.completed.minimal.txt | 2 + .../messages/workflow_run.completed.txt | 2 +- .../kotlin/com/wire/github/ApplicationTest.kt | 6 +- .../kotlin/com/wire/github/TestFixtures.kt | 13 +++ .../wire/github/util/TemplateHandlerTest.kt | 90 ++++++++++++------- 14 files changed, 153 insertions(+), 72 deletions(-) create mode 100644 src/main/kotlin/com/wire/github/response/model/Conclusion.kt create mode 100644 src/test/fixtures/events/workflow_job.completed.minimal.json create mode 100644 src/test/fixtures/events/workflow_run.completed.minimal.json create mode 100644 src/test/fixtures/messages/workflow_job.completed.minimal.txt create mode 100644 src/test/fixtures/messages/workflow_run.completed.minimal.txt create mode 100644 src/test/kotlin/com/wire/github/TestFixtures.kt diff --git a/src/main/kotlin/com/wire/github/response/model/Conclusion.kt b/src/main/kotlin/com/wire/github/response/model/Conclusion.kt new file mode 100644 index 0000000..06e73f7 --- /dev/null +++ b/src/main/kotlin/com/wire/github/response/model/Conclusion.kt @@ -0,0 +1,18 @@ +package com.wire.github.response.model + +internal data class Conclusion( + val emoji: String, + val text: String +) + +internal fun conclusionPresentation(conclusion: String?): Conclusion = + when (conclusion) { + "success" -> Conclusion(emoji = "✅", text = "succeeded") + "failure" -> Conclusion(emoji = "❌", text = "failed") + "cancelled" -> Conclusion(emoji = "đŸšĢ", text = "cancelled") + "timed_out" -> Conclusion(emoji = "âąī¸", text = "timed out") + "action_required" -> Conclusion(emoji = "âš ī¸", text = "requires action") + "skipped" -> Conclusion(emoji = "â­ī¸", text = "skipped") + "stale" -> Conclusion(emoji = "💤", text = "stale") + else -> Conclusion(emoji = "â„šī¸", text = "completed") + } diff --git a/src/main/kotlin/com/wire/github/response/model/WorkflowJob.kt b/src/main/kotlin/com/wire/github/response/model/WorkflowJob.kt index 5bf66af..e7f9fad 100644 --- a/src/main/kotlin/com/wire/github/response/model/WorkflowJob.kt +++ b/src/main/kotlin/com/wire/github/response/model/WorkflowJob.kt @@ -5,21 +5,21 @@ import kotlinx.serialization.Serializable @Serializable data class WorkflowJob( - val name: String, - val conclusion: String?, + val name: String? = null, + val conclusion: String? = null, @SerialName("html_url") - val htmlUrl: String, + val htmlUrl: String? = null, @SerialName("workflow_name") val workflowName: String? = null, @SerialName("head_branch") val headBranch: String? = null ) { val emoji: String - get() = when (conclusion) { - "success" -> "✅" - "failure" -> "❌" - "cancelled" -> "đŸšĢ" - "skipped" -> "â­ī¸" - else -> "â„šī¸" - } + get() = conclusionPresentation(conclusion).emoji + + val conclusionText: String + get() = conclusionPresentation(conclusion).text + + val displayName: String + get() = name?.takeIf { it.isNotBlank() } ?: "Workflow job" } diff --git a/src/main/kotlin/com/wire/github/response/model/WorkflowRun.kt b/src/main/kotlin/com/wire/github/response/model/WorkflowRun.kt index a75c71a..fbfe38c 100644 --- a/src/main/kotlin/com/wire/github/response/model/WorkflowRun.kt +++ b/src/main/kotlin/com/wire/github/response/model/WorkflowRun.kt @@ -5,26 +5,23 @@ import kotlinx.serialization.Serializable @Serializable data class WorkflowRun( - val name: String, - val conclusion: String?, - val event: String, - val actor: User, + val name: String? = null, + val conclusion: String? = null, + val event: String? = null, + val actor: User? = null, @SerialName("head_branch") - val headBranch: String, + val headBranch: String? = null, @SerialName("html_url") - val htmlUrl: String, + val htmlUrl: String? = null, @SerialName("run_number") - val runNumber: Int + val runNumber: Int? = null ) { val emoji: String - get() = when (conclusion) { - "success" -> "✅" - "failure" -> "❌" - "cancelled" -> "đŸšĢ" - "timed_out" -> "âąī¸" - "action_required" -> "âš ī¸" - "skipped" -> "â­ī¸" - "stale" -> "💤" - else -> "â„šī¸" - } + get() = conclusionPresentation(conclusion).emoji + + val conclusionText: String + get() = conclusionPresentation(conclusion).text + + val displayName: String + get() = name?.takeIf { it.isNotBlank() } ?: "Workflow run" } diff --git a/src/main/resources/templates/en/workflow_job.completed.template b/src/main/resources/templates/en/workflow_job.completed.template index 8ec0e4f..297a245 100644 --- a/src/main/resources/templates/en/workflow_job.completed.template +++ b/src/main/resources/templates/en/workflow_job.completed.template @@ -1,4 +1,4 @@ -{{workflowJob.emoji}} **Job {{workflowJob.conclusion}}:** [{{{workflowJob.name}}}]({{workflowJob.htmlUrl}}) +{{workflowJob.emoji}} **Job {{workflowJob.conclusionText}}:** {{{workflowJob.displayName}}}{{#workflowJob.htmlUrl}} — [view job]({{workflowJob.htmlUrl}}){{/workflowJob.htmlUrl}} **Repository:** {{repository.fullName}} {{#workflowJob.workflowName}}**Workflow:** {{{workflowJob.workflowName}}} {{/workflowJob.workflowName}}{{#workflowJob.headBranch}}**Branch:** `{{workflowJob.headBranch}}` diff --git a/src/main/resources/templates/en/workflow_run.completed.template b/src/main/resources/templates/en/workflow_run.completed.template index 99ee72d..ee56c73 100644 --- a/src/main/resources/templates/en/workflow_run.completed.template +++ b/src/main/resources/templates/en/workflow_run.completed.template @@ -1,5 +1,6 @@ -{{workflowRun.emoji}} **Workflow {{workflowRun.conclusion}}:** [{{{workflowRun.name}}} #{{workflowRun.runNumber}}]({{workflowRun.htmlUrl}}) +{{workflowRun.emoji}} **Workflow {{workflowRun.conclusionText}}:** {{{workflowRun.displayName}}}{{#workflowRun.runNumber}} #{{workflowRun.runNumber}}{{/workflowRun.runNumber}}{{#workflowRun.htmlUrl}} — [view run]({{workflowRun.htmlUrl}}){{/workflowRun.htmlUrl}} **Repository:** {{repository.fullName}} -**Branch:** `{{workflowRun.headBranch}}` -**Trigger:** {{workflowRun.event}} -**Started by:** {{workflowRun.actor.login}} +{{#workflowRun.headBranch}}**Branch:** `{{workflowRun.headBranch}}` +{{/workflowRun.headBranch}}{{#workflowRun.event}}**Trigger:** {{workflowRun.event}} +{{/workflowRun.event}}{{#workflowRun.actor}}**Started by:** {{workflowRun.actor.login}} +{{/workflowRun.actor}} diff --git a/src/test/fixtures/events/workflow_job.completed.minimal.json b/src/test/fixtures/events/workflow_job.completed.minimal.json new file mode 100644 index 0000000..19b4b98 --- /dev/null +++ b/src/test/fixtures/events/workflow_job.completed.minimal.json @@ -0,0 +1,14 @@ +{ + "action": "completed", + "workflow_job": { + "conclusion": null + }, + "repository": { + "name": "example", + "full_name": "wireapp/example" + }, + "sender": { + "login": "octocat", + "avatar_url": "https://github.com/images/error/octocat_happy.gif" + } +} diff --git a/src/test/fixtures/events/workflow_run.completed.minimal.json b/src/test/fixtures/events/workflow_run.completed.minimal.json new file mode 100644 index 0000000..e56e403 --- /dev/null +++ b/src/test/fixtures/events/workflow_run.completed.minimal.json @@ -0,0 +1,14 @@ +{ + "action": "completed", + "workflow_run": { + "conclusion": null + }, + "repository": { + "name": "example", + "full_name": "wireapp/example" + }, + "sender": { + "login": "octocat", + "avatar_url": "https://github.com/images/error/octocat_happy.gif" + } +} diff --git a/src/test/fixtures/messages/workflow_job.completed.minimal.txt b/src/test/fixtures/messages/workflow_job.completed.minimal.txt new file mode 100644 index 0000000..df77136 --- /dev/null +++ b/src/test/fixtures/messages/workflow_job.completed.minimal.txt @@ -0,0 +1,2 @@ +â„šī¸ **Job completed:** Workflow job +**Repository:** wireapp/example diff --git a/src/test/fixtures/messages/workflow_job.completed.txt b/src/test/fixtures/messages/workflow_job.completed.txt index 54167aa..54f5a04 100644 --- a/src/test/fixtures/messages/workflow_job.completed.txt +++ b/src/test/fixtures/messages/workflow_job.completed.txt @@ -1,4 +1,4 @@ -❌ **Job failure:** [unit-tests (JDK 21)](https://github.com/wireapp/example/actions/runs/123456789/job/987654321) +❌ **Job failed:** unit-tests (JDK 21) — [view job](https://github.com/wireapp/example/actions/runs/123456789/job/987654321) **Repository:** wireapp/example **Workflow:** Build and test **Branch:** `feature/actions-notifications` diff --git a/src/test/fixtures/messages/workflow_run.completed.minimal.txt b/src/test/fixtures/messages/workflow_run.completed.minimal.txt new file mode 100644 index 0000000..8cf6150 --- /dev/null +++ b/src/test/fixtures/messages/workflow_run.completed.minimal.txt @@ -0,0 +1,2 @@ +â„šī¸ **Workflow completed:** Workflow run +**Repository:** wireapp/example diff --git a/src/test/fixtures/messages/workflow_run.completed.txt b/src/test/fixtures/messages/workflow_run.completed.txt index bb9fc67..78ac3e9 100644 --- a/src/test/fixtures/messages/workflow_run.completed.txt +++ b/src/test/fixtures/messages/workflow_run.completed.txt @@ -1,4 +1,4 @@ -✅ **Workflow success:** [Build and test #42](https://github.com/wireapp/example/actions/runs/123456789) +✅ **Workflow succeeded:** Build and test #42 — [view run](https://github.com/wireapp/example/actions/runs/123456789) **Repository:** wireapp/example **Branch:** `main` **Trigger:** push diff --git a/src/test/kotlin/com/wire/github/ApplicationTest.kt b/src/test/kotlin/com/wire/github/ApplicationTest.kt index d1bb17e..a776ebe 100644 --- a/src/test/kotlin/com/wire/github/ApplicationTest.kt +++ b/src/test/kotlin/com/wire/github/ApplicationTest.kt @@ -25,7 +25,6 @@ import io.micrometer.prometheusmetrics.PrometheusMeterRegistry import io.mockk.every import io.mockk.mockk import io.mockk.verify -import java.io.File import java.util.UUID import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -319,7 +318,7 @@ class ApplicationTest { header("X-GitHub-Event", event) header("X-Hub-Signature", "sha1=$DUMMY_SIGNATURE") header("X-GitHub-Delivery", "delivery-$event") - setBody(eventFixture("$event.completed")) + setBody(TestFixtures.event("$event.completed")) } assertEquals(HttpStatusCode.OK, response.status) @@ -331,9 +330,6 @@ class ApplicationTest { } } - private fun eventFixture(name: String): String = - File("src/test/fixtures/events/$name.json").readText() - private companion object { val CONVERSATION_ID = QualifiedId( id = UUID.randomUUID(), diff --git a/src/test/kotlin/com/wire/github/TestFixtures.kt b/src/test/kotlin/com/wire/github/TestFixtures.kt new file mode 100644 index 0000000..6ff1889 --- /dev/null +++ b/src/test/kotlin/com/wire/github/TestFixtures.kt @@ -0,0 +1,13 @@ +package com.wire.github + +import java.io.File + +internal object TestFixtures { + private const val FIXTURES_DIRECTORY = "src/test/fixtures" + + fun event(name: String): String = read("events/$name.json") + + fun message(name: String): String = read("messages/$name.txt").trim() + + private fun read(path: String): String = File("$FIXTURES_DIRECTORY/$path").readText() +} diff --git a/src/test/kotlin/com/wire/github/util/TemplateHandlerTest.kt b/src/test/kotlin/com/wire/github/util/TemplateHandlerTest.kt index 73100d1..748545c 100644 --- a/src/test/kotlin/com/wire/github/util/TemplateHandlerTest.kt +++ b/src/test/kotlin/com/wire/github/util/TemplateHandlerTest.kt @@ -1,5 +1,6 @@ package com.wire.github.util +import com.wire.github.TestFixtures import com.wire.github.response.model.Commit import com.wire.github.response.model.GitHubResponse import com.wire.github.response.model.PullRequest @@ -8,7 +9,6 @@ import com.wire.github.response.model.Review import com.wire.github.response.model.User import com.wire.github.response.model.WorkflowJob import com.wire.github.response.model.WorkflowRun -import java.io.File import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals @@ -103,11 +103,11 @@ class TemplateHandlerTest { } @Test - fun `does not render a workflow run before it completes`() { + fun `does not render a workflow run for an unsupported action`() { val response = KtxSerializer.json.decodeFromString( - eventFixture("workflow_run.completed") + TestFixtures + .event("workflow_run.completed") .replace("\"action\": \"completed\"", "\"action\": \"in_progress\"") - .replace("\"conclusion\": \"success\"", "\"conclusion\": null") ) val message = templateHandler.handleEvent( @@ -119,11 +119,11 @@ class TemplateHandlerTest { } @Test - fun `does not render a workflow job before it completes`() { + fun `does not render a workflow job for an unsupported action`() { val response = KtxSerializer.json.decodeFromString( - eventFixture("workflow_job.completed") + TestFixtures + .event("workflow_job.completed") .replace("\"action\": \"completed\"", "\"action\": \"in_progress\"") - .replace("\"conclusion\": \"failure\"", "\"conclusion\": null") ) val message = templateHandler.handleEvent( @@ -137,35 +137,63 @@ class TemplateHandlerTest { @Test fun `maps workflow run conclusions to emojis`() { mapOf( - "success" to "✅", - "failure" to "❌", - "cancelled" to "đŸšĢ", - "timed_out" to "âąī¸", - "action_required" to "âš ī¸", - "skipped" to "â­ī¸", - "stale" to "💤", - "neutral" to "â„šī¸" - ).forEach { (conclusion, emoji) -> - assertEquals(emoji, workflowRun(conclusion).emoji) + "success" to ("✅" to "succeeded"), + "failure" to ("❌" to "failed"), + "cancelled" to ("đŸšĢ" to "cancelled"), + "timed_out" to ("âąī¸" to "timed out"), + "action_required" to ("âš ī¸" to "requires action"), + "skipped" to ("â­ī¸" to "skipped"), + "stale" to ("💤" to "stale"), + "neutral" to ("â„šī¸" to "completed") + ).forEach { (conclusion, presentation) -> + assertEquals(presentation.first, workflowRun(conclusion).emoji) + assertEquals(presentation.second, workflowRun(conclusion).conclusionText) } } @Test fun `maps workflow job conclusions to emojis`() { mapOf( - "success" to "✅", - "failure" to "❌", - "cancelled" to "đŸšĢ", - "skipped" to "â­ī¸" - ).forEach { (conclusion, emoji) -> - assertEquals(emoji, workflowJob(conclusion).emoji) + "success" to ("✅" to "succeeded"), + "failure" to ("❌" to "failed"), + "cancelled" to ("đŸšĢ" to "cancelled"), + "timed_out" to ("âąī¸" to "timed out"), + "action_required" to ("âš ī¸" to "requires action"), + "skipped" to ("â­ī¸" to "skipped"), + "stale" to ("💤" to "stale"), + "neutral" to ("â„šī¸" to "completed") + ).forEach { (conclusion, presentation) -> + assertEquals(presentation.first, workflowJob(conclusion).emoji) + assertEquals(presentation.second, workflowJob(conclusion).conclusionText) } } - private fun renderFixture(event: String): String? { - val fixtureName = "$event.completed" + @Test + fun `renders a completed workflow run without optional fields`() { + val message = renderFixture( + event = "workflow_run", + fixtureName = "workflow_run.completed.minimal" + ) + + assertEquals(TestFixtures.message("workflow_run.completed.minimal"), message?.trim()) + } + + @Test + fun `renders a completed workflow job without optional fields`() { + val message = renderFixture( + event = "workflow_job", + fixtureName = "workflow_job.completed.minimal" + ) + + assertEquals(TestFixtures.message("workflow_job.completed.minimal"), message?.trim()) + } + + private fun renderFixture( + event: String, + fixtureName: String = "$event.completed" + ): String? { val response = KtxSerializer.json.decodeFromString( - eventFixture(fixtureName) + TestFixtures.event(fixtureName) ) return templateHandler.handleEvent( @@ -174,11 +202,7 @@ class TemplateHandlerTest { ) } - private fun eventFixture(name: String): String = fixture("events/$name.json") - - private fun messageFixture(name: String): String = fixture("messages/$name.txt").trim() - - private fun fixture(path: String): String = File("src/test/fixtures/$path").readText() + private fun messageFixture(name: String): String = TestFixtures.message(name) private fun reviewResponse( body: String?, @@ -214,7 +238,7 @@ class TemplateHandlerTest { ) ) - private fun workflowRun(conclusion: String) = + private fun workflowRun(conclusion: String?) = WorkflowRun( name = "Build and test", conclusion = conclusion, @@ -225,7 +249,7 @@ class TemplateHandlerTest { runNumber = 1 ) - private fun workflowJob(conclusion: String) = + private fun workflowJob(conclusion: String?) = WorkflowJob( name = "unit-tests", conclusion = conclusion,