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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion src/main/kotlin/com/wire/github/EventsHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
18 changes: 18 additions & 0 deletions src/main/kotlin/com/wire/github/response/model/Conclusion.kt
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
25 changes: 25 additions & 0 deletions src/main/kotlin/com/wire/github/response/model/WorkflowJob.kt
Original file line number Diff line number Diff line change
@@ -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? = null,
val conclusion: String? = null,
@SerialName("html_url")
val htmlUrl: String? = null,
@SerialName("workflow_name")
val workflowName: String? = null,
@SerialName("head_branch")
val headBranch: String? = null
) {
val emoji: String
get() = conclusionPresentation(conclusion).emoji

val conclusionText: String
get() = conclusionPresentation(conclusion).text

val displayName: String
get() = name?.takeIf { it.isNotBlank() } ?: "Workflow job"
}
27 changes: 27 additions & 0 deletions src/main/kotlin/com/wire/github/response/model/WorkflowRun.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.wire.github.response.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class WorkflowRun(
val name: String? = null,
val conclusion: String? = null,
val event: String? = null,
val actor: User? = null,
@SerialName("head_branch")
val headBranch: String? = null,
@SerialName("html_url")
val htmlUrl: String? = null,
@SerialName("run_number")
val runNumber: Int? = null
) {
val emoji: String
get() = conclusionPresentation(conclusion).emoji

val conclusionText: String
get() = conclusionPresentation(conclusion).text

val displayName: String
get() = name?.takeIf { it.isNotBlank() } ?: "Workflow run"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{{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}}`
{{/workflowJob.headBranch}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{{workflowRun.emoji}} **Workflow {{workflowRun.conclusionText}}:** {{{workflowRun.displayName}}}{{#workflowRun.runNumber}} #{{workflowRun.runNumber}}{{/workflowRun.runNumber}}{{#workflowRun.htmlUrl}} — [view run]({{workflowRun.htmlUrl}}){{/workflowRun.htmlUrl}}
**Repository:** {{repository.fullName}}
{{#workflowRun.headBranch}}**Branch:** `{{workflowRun.headBranch}}`
{{/workflowRun.headBranch}}{{#workflowRun.event}}**Trigger:** {{workflowRun.event}}
{{/workflowRun.event}}{{#workflowRun.actor}}**Started by:** {{workflowRun.actor.login}}
{{/workflowRun.actor}}
18 changes: 18 additions & 0 deletions src/test/fixtures/events/workflow_job.completed.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
14 changes: 14 additions & 0 deletions src/test/fixtures/events/workflow_job.completed.minimal.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
23 changes: 23 additions & 0 deletions src/test/fixtures/events/workflow_run.completed.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
14 changes: 14 additions & 0 deletions src/test/fixtures/events/workflow_run.completed.minimal.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ℹ️ **Job completed:** Workflow job
**Repository:** wireapp/example
4 changes: 4 additions & 0 deletions src/test/fixtures/messages/workflow_job.completed.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
❌ **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`
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ℹ️ **Workflow completed:** Workflow run
**Repository:** wireapp/example
5 changes: 5 additions & 0 deletions src/test/fixtures/messages/workflow_run.completed.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
✅ **Workflow succeeded:** Build and test #42 — [view run](https://github.com/wireapp/example/actions/runs/123456789)
**Repository:** wireapp/example
**Branch:** `main`
**Trigger:** push
**Started by:** octocat
40 changes: 40 additions & 0 deletions src/test/kotlin/com/wire/github/ApplicationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,46 @@ class ApplicationTest {
}
}

@Test
fun `given completed GitHub Actions events, when received, then messages are sent`() {
val signatureValidator = mockk<SignatureValidator>()
every { signatureValidator.isValid(any(), any(), any(), any()) } returns true

val wireAppSdk = mockk<WireAppSdk>()
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(TestFixtures.event("$event.completed"))
}

assertEquals(HttpStatusCode.OK, response.status)
}

verify(exactly = 2) {
wireAppSdk.getApplicationManager().sendMessage(message = any())
}
}
}

private companion object {
val CONVERSATION_ID = QualifiedId(
id = UUID.randomUUID(),
Expand Down
13 changes: 13 additions & 0 deletions src/test/kotlin/com/wire/github/TestFixtures.kt
Original file line number Diff line number Diff line change
@@ -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()
}
Loading