Skip to content

Commit 277626c

Browse files
committed
feat(trogon-sink-github): add webhook receiver that sinks events to NATS JetStream
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 97a900c commit 277626c

File tree

16 files changed

+1482
-5
lines changed

16 files changed

+1482
-5
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Receiving GitHub Webhooks Locally
2+
3+
## Prerequisites
4+
5+
- Docker Compose
6+
- A GitHub App (org or personal) — the app's webhook delivers events from every
7+
repo where it's installed, so you configure the URL once
8+
9+
## 1. Generate a webhook secret
10+
11+
```bash
12+
openssl rand -hex 32
13+
```
14+
15+
Save the output — you'll use it in both GitHub and the local stack.
16+
17+
## 2. Create a smee.io channel
18+
19+
Go to [smee.io](https://smee.io) and click **Start a new channel**. Copy the
20+
channel URL (e.g. `https://smee.io/abc123`).
21+
22+
## 3. Configure your GitHub App
23+
24+
1. Go to **Settings → Developer settings → GitHub Apps**
25+
2. Select your app (or create a new one)
26+
3. Under **Webhook**:
27+
- **Webhook URL**: your smee.io channel URL
28+
- **Webhook secret**: the secret you generated in step 1
29+
4. Under **Permissions & events**, subscribe to the events you need
30+
5. Install the app on the repositories or organization you want to receive
31+
events from
32+
33+
## 4. Start the stack
34+
35+
```bash
36+
SMEE_URL=https://smee.io/abc123 \
37+
GITHUB_WEBHOOK_SECRET=<secret-from-step-1> \
38+
docker compose --profile dev up
39+
```
40+
41+
This starts NATS, the webhook receiver, and the smee client. The smee client
42+
connects to your channel and forwards events to the webhook receiver.
43+
44+
Without `--profile dev`, the smee client is excluded and only the core services
45+
start.
46+
47+
## 5. Verify
48+
49+
Trigger an event in a repository where the app is installed (e.g. push a
50+
commit). You should see:
51+
52+
- The event appear on your smee.io channel page
53+
- The smee client forward it to the webhook receiver
54+
- The webhook receiver publish it to NATS on `github.{event}`
55+
56+
## Environment variables
57+
58+
| Variable | Required | Default | Description |
59+
|---|---|---|---|
60+
| `GITHUB_WEBHOOK_SECRET` | yes || HMAC-SHA256 secret (must match GitHub App) |
61+
| `SMEE_URL` | yes (dev profile) || smee.io channel URL |
62+
| `GITHUB_WEBHOOK_PORT` | no | `8080` | HTTP port for the webhook receiver |
63+
| `GITHUB_SUBJECT_PREFIX` | no | `github` | NATS subject prefix |
64+
| `GITHUB_STREAM_NAME` | no | `GITHUB` | JetStream stream name |
65+
| `RUST_LOG` | no | `info` | Log level |

devops/docker/compose/compose.yml

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,66 @@ name: trogonai
22

33
services:
44
nats:
5-
image: nats:latest
6-
container_name: nats
7-
command:
5+
image: nats:alpine
6+
command:
87
- "--jetstream"
98
- "--store_dir=/data"
9+
- "--http_port=8222"
10+
ports:
11+
- "4222:4222"
12+
- "8222:8222"
1013
volumes:
1114
- nats_data:/data
15+
restart: unless-stopped
16+
healthcheck:
17+
test: ["CMD", "wget", "-qO-", "http://localhost:8222/healthz"]
18+
interval: 5s
19+
timeout: 3s
20+
start_period: 5s
21+
retries: 3
22+
23+
trogon-sink-github:
24+
build:
25+
context: ../../../rsworkspace
26+
dockerfile: crates/trogon-sink-github/Dockerfile
27+
ports:
28+
- "${GITHUB_WEBHOOK_PORT:-8080}:${GITHUB_WEBHOOK_PORT:-8080}"
29+
environment:
30+
NATS_URL: "nats:4222"
31+
GITHUB_WEBHOOK_SECRET: "${GITHUB_WEBHOOK_SECRET:?GITHUB_WEBHOOK_SECRET is required}"
32+
GITHUB_WEBHOOK_PORT: "${GITHUB_WEBHOOK_PORT:-8080}"
33+
GITHUB_SUBJECT_PREFIX: "${GITHUB_SUBJECT_PREFIX:-github}"
34+
GITHUB_STREAM_NAME: "${GITHUB_STREAM_NAME:-GITHUB}"
35+
GITHUB_STREAM_MAX_AGE_SECS: "${GITHUB_STREAM_MAX_AGE_SECS:-604800}"
36+
GITHUB_NATS_ACK_TIMEOUT_SECS: "${GITHUB_NATS_ACK_TIMEOUT_SECS:-10}"
37+
GITHUB_MAX_BODY_SIZE: "${GITHUB_MAX_BODY_SIZE:-26214400}"
38+
RUST_LOG: "${RUST_LOG:-info}"
39+
depends_on:
40+
nats:
41+
condition: service_healthy
42+
restart: unless-stopped
43+
healthcheck:
44+
test: ["CMD", "curl", "-sf", "http://localhost:${GITHUB_WEBHOOK_PORT:-8080}/health"]
45+
interval: 10s
46+
timeout: 3s
47+
start_period: 10s
48+
retries: 3
49+
50+
smee:
51+
image: node:alpine
52+
command:
53+
- npx
54+
- smee-client
55+
- --url
56+
- "${SMEE_URL:?SMEE_URL is required — create a channel at https://smee.io}"
57+
- --target
58+
- "http://trogon-sink-github:${GITHUB_WEBHOOK_PORT:-8080}/webhook"
59+
depends_on:
60+
trogon-sink-github:
61+
condition: service_healthy
62+
restart: unless-stopped
63+
profiles:
64+
- dev
1265

1366
volumes:
1467
nats_data:

rsworkspace/.dockerignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Rust build artifacts — largest offender, can be many GBs
2+
target/
3+
4+
# Dev tooling
5+
.git/
6+
.github/
7+
.cargo/
8+
9+
# Test/CI artifacts
10+
lcov.info
11+
coverage.xml
12+
*.profraw
13+
*.profdata
14+
mutants.out*/
15+
16+
# IDE / OS
17+
.idea/
18+
.vscode/
19+
.DS_Store
20+
Thumbs.db
21+
*.swp
22+
*.swo
23+
24+
# Environment files with secrets
25+
.env
26+
.env.*
27+
!.env.example

rsworkspace/Cargo.lock

Lines changed: 92 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rsworkspace/crates/acp-telemetry/src/service_name.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
pub enum ServiceName {
66
AcpNatsStdio,
77
AcpNatsWs,
8+
TrogonSinkGithub,
89
}
910

1011
impl ServiceName {
1112
pub fn as_str(self) -> &'static str {
1213
match self {
1314
Self::AcpNatsStdio => "acp-nats-stdio",
1415
Self::AcpNatsWs => "acp-nats-ws",
16+
Self::TrogonSinkGithub => "trogon-sink-github",
1517
}
1618
}
1719
}
@@ -30,11 +32,16 @@ mod tests {
3032
fn as_str_returns_expected_values() {
3133
assert_eq!(ServiceName::AcpNatsStdio.as_str(), "acp-nats-stdio");
3234
assert_eq!(ServiceName::AcpNatsWs.as_str(), "acp-nats-ws");
35+
assert_eq!(ServiceName::TrogonSinkGithub.as_str(), "trogon-sink-github");
3336
}
3437

3538
#[test]
3639
fn display_delegates_to_as_str() {
3740
assert_eq!(format!("{}", ServiceName::AcpNatsStdio), "acp-nats-stdio");
3841
assert_eq!(format!("{}", ServiceName::AcpNatsWs), "acp-nats-ws");
42+
assert_eq!(
43+
format!("{}", ServiceName::TrogonSinkGithub),
44+
"trogon-sink-github"
45+
);
3946
}
4047
}

rsworkspace/crates/trogon-nats/src/jetstream/client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ impl JetStreamPublisher for NatsJetStreamClient {
5151
subject: S,
5252
headers: HeaderMap,
5353
payload: Bytes,
54-
) -> Result<PublishAckFuture, PublishError> {
54+
) -> Result<Self::AckFuture, Self::PublishError> {
5555
self.context
5656
.publish_with_headers(subject, headers, payload)
5757
.await

rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ impl MockJetStreamPublisher {
246246
.map(|m| m.payload.clone())
247247
.collect()
248248
}
249+
250+
pub fn published_messages(&self) -> Vec<MockPublishedJsMessage> {
251+
self.published.lock().unwrap().clone()
252+
}
249253
}
250254

251255
impl Default for MockJetStreamPublisher {

rsworkspace/crates/trogon-nats/src/jetstream/traits.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ pub trait JetStreamContext: Send + Sync + Clone + 'static {
2121

2222
pub trait JetStreamPublisher: Send + Sync + Clone + 'static {
2323
type PublishError: Error + Send + Sync;
24-
type AckFuture: IntoFuture<Output = Result<PublishAck, Self::PublishError>> + Send;
24+
type AckFuture: IntoFuture<Output = Result<PublishAck, Self::PublishError>, IntoFuture: Send>
25+
+ Send;
2526

2627
fn publish_with_headers<S: ToSubject + Send>(
2728
&self,

0 commit comments

Comments
 (0)