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
29 changes: 29 additions & 0 deletions .github/actions/notify-wire/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Notify Wire

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if just by placing the action here it's somehow reachable by all the other repositories in wireapp org:

https://docs.github.com/en/actions/how-tos/reuse-automations/share-with-your-organization#sharing-actions-and-workflows-with-your-organization

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this repository is public, it is open. The configurations/changes are only needed if the repository is set to private.

Also, after this PR is approved and merged, I will be creating a tag so the actions can be reachable by (or something like):

uses: wireapp/github-app/.github/actions/notify-wire@v1

description: Send a custom message to a Wire conversation through github-app
inputs:
webhook-url:
description: Full github-app Actions endpoint URL for the Wire conversation
required: true
token:
description: GitHub Actions token configured for the Wire conversation
required: true
text:
description: Message text to send
required: true
runs:
using: composite
steps:
- name: Send message to Wire
shell: bash
env:
WIRE_WEBHOOK_URL: ${{ inputs.webhook-url }}
WIRE_ACTIONS_TOKEN: ${{ inputs.token }}
MESSAGE_TEXT: ${{ inputs.text }}
run: |
set -euo pipefail
python3 -c 'import json, os; print(json.dumps({"text": os.environ["MESSAGE_TEXT"]}))' \
| curl --fail-with-body --silent --show-error \
-X POST "$WIRE_WEBHOOK_URL" \
-H "Authorization: Bearer ${WIRE_ACTIONS_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @-
2 changes: 1 addition & 1 deletion .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 1

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 1

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
tests:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c
with:
distribution: 'zulu'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
if: github.actor != 'github-actions[bot]'
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
Expand Down
52 changes: 45 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ No license is granted to the Wire trademark and its associated logos, all of whi

Here's a list of features included in this project:

| Name | Description |
|------------------------------------------|--------------------------------------------|
| /health | Healthcheck endpoint returning HTTP OK 200. |
| /{conversation_id}/{conversation_domain} | Webhook endpoint. |
| Name | Description |
|--------------------------------------------------------|--------------------------------------------------|
| `/health` | Healthcheck endpoint returning HTTP OK 200. |
| `/{conversation_id}/{conversation_domain}` | Receive signed GitHub repository webhooks. |
| `/actions/{conversation_id}/{conversation_domain}` | Receive workflow-authored GitHub Actions text. |

### GitHub Actions notifications
### Repository webhook notifications

The existing webhook endpoint supports centralized notifications for these GitHub Actions events:
The existing webhook endpoint provides centralized notifications rendered by this service. It supports
these GitHub Actions-related repository webhook events:

| GitHub event | Supported action | Notification |
|--------------|------------------|--------------|
Expand All @@ -41,6 +43,43 @@ All completed conclusions are reported, including successful, failed, cancelled,
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.

Use `/github webhook help` in the Wire conversation for its URL, secret, and setup instructions.

### Workflow-authored GitHub Actions notifications

Use `/github actions help` in the Wire conversation to obtain the complete endpoint URL and Actions
token. Store them as `WIRE_WEBHOOK_URL` and `WIRE_ACTIONS_TOKEN` respectively.
Comment on lines +46 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too much for the README and too specific, just keep this part and the yaml example of the action IMHO

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed most of the explanation parts and kept the inputs table/yaml configuration and all commands at the bottom


The action accepts these inputs:

| Input | Required | Description |
|-------|----------|-------------|
| `webhook-url` | Yes | Complete `/actions/{conversation_id}/{conversation_domain}` URL. |
| `token` | Yes | Dedicated Actions bearer token. |
| `text` | Yes | Complete message text to send without service-side formatting. |

```yaml
- name: Notify Wire on failure
if: failure() || cancelled()
uses: wireapp/github-app/.github/actions/notify-wire@v1
with:
webhook-url: ${{ secrets.WIRE_WEBHOOK_URL }}
token: ${{ secrets.WIRE_ACTIONS_TOKEN }}
text: |
CI failed for ${{ github.repository }} on `${{ github.ref_name }}`.
Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
```

## Wire commands

| Command | Description |
|---------|-------------|
| `/github help` | List available commands and notification approaches. |
| `/github tokens` | Show or create the webhook secret and Actions token. |
| `/github webhook help` | Show repository webhook setup instructions. |
| `/github actions help` | Show the Actions endpoint, token usage, and reusable action example. |


## Building & Running

To build or run the project, you can use the IDE Run configuration with environment variables.
Expand All @@ -57,7 +96,6 @@ GHAPP_SERVER_PORT=8083
GHAPP_REDIS_URL=redis://username:password@host:port
WIRE_SDK_API_HOST=https://nginz-https.chala.wire.link
WIRE_SDK_API_TOKEN=myApiToken
WIRE_SDK_APP_ID=f562e146-dec2-4d85-93c7-7132746b5cca
WIRE_SDK_CRYPTOGRAPHY_STORAGE_PASSWORD=myDummyPasswordmyDummyPassword01
```

Expand Down
1 change: 0 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ tasks {
environment("GHAPP_REDIS_URL", "redis://localhost:6379")
environment("WIRE_SDK_API_HOST", "https://nginz-https.chala.wire.link")
environment("WIRE_SDK_API_TOKEN", "myApiToken")
environment("WIRE_SDK_APP_ID", "f562e146-dec2-4d85-93c7-7132746b5cca")
environment("WIRE_SDK_CRYPTOGRAPHY_STORAGE_PASSWORD", "myDummyPasswordmyDummyPassword01")
}
}
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ services:
- GHAPP_REDIS_URL=${GHAPP_REDIS_URL}
- WIRE_SDK_API_HOST=${WIRE_SDK_API_HOST}
- WIRE_SDK_API_TOKEN=${WIRE_SDK_API_TOKEN}
- WIRE_SDK_APP_ID=${WIRE_SDK_APP_ID}
- WIRE_SDK_CRYPTOGRAPHY_STORAGE_PASSWORD=${WIRE_SDK_CRYPTOGRAPHY_STORAGE_PASSWORD}
ports:
- "${GHAPP_SERVER_PORT:-8083}:${GHAPP_SERVER_PORT:-8083}"
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ktlint-version = "14.2.0"
detekt-version = "1.23.8"
kotlin-serialization = "1.11.0"
koin-version = "4.2.2"
wire-sdk-version = "0.2.1"
wire-sdk-version = "0.3.0"
redis-version = "6.7.1.RELEASE"
mustache-version = "0.9.14"
mockk-version = "1.14.11"
Expand Down
2 changes: 0 additions & 2 deletions helm/githubapp/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ ingress:
- github-app.apps-prod.wire.com
secretName: github-app-tls
env:
- name: WIRE_SDK_APP_ID
value: "61c2f1c4-ca8a-4d67-8f05-ca9bb37d85cc"
- name: WIRE_SDK_API_HOST
value: "https://prod-nginz-https.wire.com"
- name: GHAPP_API_HOST
Expand Down
2 changes: 0 additions & 2 deletions helm/githubapp/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ affinity: {}

# Environment variables for the application
env:
- name: WIRE_SDK_APP_ID
value: ""
- name: WIRE_SDK_API_HOST
value: ""
- name: GHAPP_API_HOST
Expand Down
91 changes: 25 additions & 66 deletions src/main/kotlin/com/wire/github/EventsHandler.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package com.wire.github

import com.wire.github.metrics.UsageMetrics
import com.wire.github.util.ENV_VAR_HOST
import com.wire.github.util.SessionIdentifierGenerator
import com.wire.github.util.toStorageKey
import com.wire.sdk.WireEventsHandlerSuspending
import com.wire.sdk.model.Conversation
import com.wire.sdk.model.ConversationMember
import com.wire.sdk.model.QualifiedId
import com.wire.sdk.model.WireMessage
import io.lettuce.core.api.StatefulRedisConnection
import org.koin.core.context.GlobalContext
Expand All @@ -16,33 +12,33 @@ import org.slf4j.LoggerFactory
class EventsHandler : WireEventsHandlerSuspending() {
private val logger = LoggerFactory.getLogger(this::class.java)
private val redisConnection = GlobalContext.get().get<StatefulRedisConnection<String, String>>()
private val storage = redisConnection.sync()
private val usageMetrics = GlobalContext.get().get<UsageMetrics>()
private val commandHandler = GitHubCommandHandler(
storage = redisConnection.sync(),
usageMetrics = usageMetrics
)

override suspend fun onTextMessageReceived(wireMessage: WireMessage.Text) {
if (wireMessage.text.equals(HELP_COMMAND, ignoreCase = true)) {
logger.info(
"Event received. Event: TextMessageReceived (HELP command), " +
"conversationId: ${wireMessage.conversationId}, " +
"senderId: ${wireMessage.sender}"
)
usageMetrics.onHelpCommand()
val message = formatSetupInstructions(
conversationId = wireMessage.conversationId,
secret = storage.get(wireMessage.conversationId.toStorageKey())
)
val response = commandHandler.response(
command = wireMessage.text,
conversationId = wireMessage.conversationId
) ?: return

manager.sendMessage(
message = WireMessage.Text.create(
conversationId = wireMessage.conversationId,
text = message
)
)
logger.info(
"Event is processed successfully. Event: TextMessageReceived (HELP command), " +
"conversationId: ${wireMessage.conversationId}"
logger.info(
"Event received. Event: TextMessageReceived (GitHub command), " +
"conversationId: ${wireMessage.conversationId}, " +
"senderId: ${wireMessage.sender}"
)
manager.sendMessage(
message = WireMessage.Text.create(
conversationId = wireMessage.conversationId,
text = response
)
}
)
logger.info(
"Event is processed successfully. Event: TextMessageReceived (GitHub command), " +
"conversationId: ${wireMessage.conversationId}"
)
}

override suspend fun onAppAddedToConversation(
Expand All @@ -54,11 +50,12 @@ class EventsHandler : WireEventsHandlerSuspending() {
"conversationId: ${conversation.id}"
)
usageMetrics.onAppAddedToConversation()
val webhookSetup = commandHandler.webhookHelp(conversationId = conversation.id)
val message = buildString {
appendLine(WELCOME_TEXT)
appendLine(formatSetupInstructions(conversationId = conversation.id))
appendLine(webhookSetup)
appendLine()
append("Use the `$HELP_COMMAND` command to see the usage again.")
append("Use `${GitHubCommandHandler.HELP_COMMAND}` to see all available commands.")
}

manager.sendMessage(
Expand All @@ -73,48 +70,10 @@ class EventsHandler : WireEventsHandlerSuspending() {
)
}

private fun formatSetupInstructions(
conversationId: QualifiedId,
secret: String? = null
): String {
val generatedSecret = secret ?: run {
val generated = SessionIdentifierGenerator.generate()
storage.set(conversationId.toStorageKey(), generated)
generated
}

val url = String.format(
HOST_URL_PATTERN,
ENV_VAR_HOST,
conversationId.id,
conversationId.domain
)

val setupInstructions = String.format(
"Here is how to set me up:\n\n" +
"1. Go to the repository that you would like to connect to\n" +
"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\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
)

return setupInstructions
}

private companion object {
const val HOST_URL_PATTERN = "%s/%s/%s"

const val WELCOME_TEXT =
"👋 Hi, I'm GitHub App. Thanks for adding me to the conversation.\n" +
"You can use me to receive GitHub notifications in Wire.\n" +
"I'm here to help make everyday work a little easier."

const val HELP_COMMAND = "/github help"
}
}
Loading