Skip to content

Commit 12591a0

Browse files
Merge pull request #54 from DEVtheOPS/feat/discord-webhook-workflow
2 parents c0688a5 + 213fe51 commit 12591a0

3 files changed

Lines changed: 231 additions & 7 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Discord Events
2+
3+
on:
4+
issues:
5+
types:
6+
- opened
7+
pull_request:
8+
types:
9+
- opened
10+
release:
11+
types:
12+
- published
13+
14+
jobs:
15+
notify-discord:
16+
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
17+
permissions: {}
18+
uses: ./.github/workflows/discord-notify.yml
19+
secrets:
20+
discord_webhook: ${{ secrets.DISCORD_WEBHOOK }}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
name: Discord Notify
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
username:
7+
description: Discord webhook username
8+
required: false
9+
default: DEVtheOPS Bot
10+
type: string
11+
avatar_url:
12+
description: Discord webhook avatar URL
13+
required: false
14+
default: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
15+
type: string
16+
color:
17+
description: Decimal embed color override
18+
required: false
19+
default: "5793266"
20+
type: string
21+
title_prefix:
22+
description: Optional prefix added to the embed title
23+
required: false
24+
default: ""
25+
type: string
26+
include_body:
27+
description: Include issue, PR, or release body in the embed description
28+
required: false
29+
default: true
30+
type: boolean
31+
secrets:
32+
discord_webhook:
33+
description: Discord webhook URL
34+
required: true
35+
36+
jobs:
37+
notify:
38+
name: Send Discord notification
39+
runs-on: ubuntu-latest
40+
permissions:
41+
contents: read
42+
issues: read
43+
pull-requests: read
44+
steps:
45+
- name: Build payload
46+
id: payload
47+
uses: actions/github-script@v7
48+
env:
49+
DISCORD_USERNAME: ${{ inputs.username }}
50+
DISCORD_AVATAR_URL: ${{ inputs.avatar_url }}
51+
DISCORD_COLOR: ${{ inputs.color }}
52+
DISCORD_TITLE_PREFIX: ${{ inputs.title_prefix }}
53+
DISCORD_INCLUDE_BODY: ${{ inputs.include_body }}
54+
with:
55+
script: |
56+
const eventName = context.eventName;
57+
const action = context.payload.action || "";
58+
const repo = context.repo;
59+
const prefix = process.env.DISCORD_TITLE_PREFIX || "";
60+
const defaultColor = Number.parseInt(process.env.DISCORD_COLOR || "5793266", 10) || 5793266;
61+
const includeBody = (process.env.DISCORD_INCLUDE_BODY || "true") === "true";
62+
63+
const truncate = (value, limit) => {
64+
if (!value) return "";
65+
const normalized = String(value).replace(/\r\n/g, "\n").trim();
66+
if (!normalized) return "";
67+
return normalized.length > limit ? `${normalized.slice(0, limit - 1)}…` : normalized;
68+
};
69+
70+
const author = {
71+
name: `${repo.owner}/${repo.repo}`,
72+
url: `https://github.com/${repo.owner}/${repo.repo}`,
73+
icon_url: `https://github.com/${repo.owner}.png`
74+
};
75+
76+
const fields = [
77+
{ name: "Repository", value: `[${repo.owner}/${repo.repo}](https://github.com/${repo.owner}/${repo.repo})`, inline: true },
78+
{ name: "Actor", value: `[${context.actor}](https://github.com/${context.actor})`, inline: true },
79+
{ name: "Event", value: `${eventName}${action ? `.${action}` : ""}`, inline: true }
80+
];
81+
82+
let title = `GitHub event in ${repo.repo}`;
83+
let url = `https://github.com/${repo.owner}/${repo.repo}`;
84+
let description = "";
85+
let color = defaultColor;
86+
87+
if (eventName === "issues" && context.payload.issue) {
88+
const issue = context.payload.issue;
89+
title = `Issue #${issue.number} opened: ${issue.title}`;
90+
url = issue.html_url;
91+
description = includeBody ? truncate(issue.body, 4000) : "";
92+
color = 16098851;
93+
fields.push(
94+
{ name: "Author", value: `[${issue.user.login}](${issue.user.html_url})`, inline: true },
95+
{ name: "Labels", value: issue.labels.length ? issue.labels.map((label) => label.name).join(", ") : "None", inline: true }
96+
);
97+
}
98+
99+
if (eventName === "pull_request" && context.payload.pull_request) {
100+
const pr = context.payload.pull_request;
101+
title = `PR #${pr.number} opened: ${pr.title}`;
102+
url = pr.html_url;
103+
description = includeBody ? truncate(pr.body, 4000) : "";
104+
color = 3447003;
105+
fields.push(
106+
{ name: "Author", value: `[${pr.user.login}](${pr.user.html_url})`, inline: true },
107+
{ name: "Branch", value: `\`${pr.head.ref}\` -> \`${pr.base.ref}\``, inline: true }
108+
);
109+
}
110+
111+
if (eventName === "release" && context.payload.release) {
112+
const release = context.payload.release;
113+
title = `Release published: ${release.name || release.tag_name}`;
114+
url = release.html_url;
115+
description = includeBody ? truncate(release.body, 4000) : "";
116+
color = 10181046;
117+
fields.push(
118+
{ name: "Tag", value: `\`${release.tag_name}\``, inline: true },
119+
{ name: "Prerelease", value: release.prerelease ? "Yes" : "No", inline: true }
120+
);
121+
}
122+
123+
if (prefix) {
124+
title = `${prefix} ${title}`;
125+
}
126+
127+
const payload = {
128+
username: process.env.DISCORD_USERNAME || "DEVtheOPS Bot",
129+
avatar_url: process.env.DISCORD_AVATAR_URL || "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
130+
embeds: [
131+
{
132+
title,
133+
url,
134+
description,
135+
color,
136+
author,
137+
fields,
138+
timestamp: new Date().toISOString(),
139+
footer: {
140+
text: "GitHub Actions"
141+
}
142+
}
143+
]
144+
};
145+
146+
core.setOutput("json", JSON.stringify(payload));
147+
148+
- name: Post to Discord
149+
env:
150+
DISCORD_WEBHOOK: ${{ secrets.discord_webhook }}
151+
DISCORD_PAYLOAD: ${{ steps.payload.outputs.json }}
152+
run: |
153+
curl --fail --silent --show-error \
154+
--connect-timeout 10 \
155+
--max-time 30 \
156+
--retry 3 \
157+
--retry-delay 2 \
158+
-H "Content-Type: application/json" \
159+
-d "$DISCORD_PAYLOAD" \
160+
"$DISCORD_WEBHOOK"

README.md

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[![npm downloads](https://img.shields.io/npm/dm/@devtheops/opencode-plugin-otel.svg)](https://www.npmjs.com/package/@devtheops/opencode-plugin-otel)
55
[![GitHub stars](https://img.shields.io/github/stars/DEVtheOPS/opencode-plugin-otel.svg)](https://github.com/DEVtheOPS/opencode-plugin-otel/stargazers)
66
[![Build status](https://img.shields.io/github/actions/workflow/status/DEVtheOPS/opencode-plugin-otel/release-please.yml?branch=main)](https://github.com/DEVtheOPS/opencode-plugin-otel/actions/workflows/release-please.yml)
7+
[![Discord notifications](https://img.shields.io/badge/discord-notifications-5865F2?logo=discord&logoColor=white)](https://discord.gg/zavuskz8xB)
78
[![License](https://img.shields.io/npm/l/@devtheops/opencode-plugin-otel.svg)](https://github.com/DEVtheOPS/opencode-plugin-otel/blob/main/LICENSE)
89

910
An [opencode](https://opencode.ai) plugin that exports telemetry via OpenTelemetry (OTLP over gRPC or HTTP/protobuf), mirroring the same signals as [Claude Code's monitoring](https://code.claude.com/docs/en/monitoring-usage).
@@ -21,6 +22,7 @@ An [opencode](https://opencode.ai) plugin that exports telemetry via OpenTelemet
2122
- [Honeycomb example](#honeycomb-example)
2223
- [Claude Code dashboard compatibility](#claude-code-dashboard-compatibility)
2324
- [Local development](#local-development)
25+
- [GitHub Discord notifications](#github-discord-notifications)
2426

2527
## What it instruments
2628

@@ -83,17 +85,17 @@ All configuration is via environment variables. Set them in your shell profile (
8385

8486
| Variable | Default | Description |
8587
|----------|---------|-------------|
86-
| `OPENCODE_ENABLE_TELEMETRY` | _(unset)_ | Set to any non-empty value to enable the plugin |
88+
| `OPENCODE_ENABLE_TELEMETRY` | *(unset)* | Set to any non-empty value to enable the plugin |
8789
| `OPENCODE_OTLP_ENDPOINT` | `http://localhost:4317` | OTLP collector endpoint. For `grpc`, use the collector host/port. For `http/protobuf`, use the base URL and the plugin will append `/v1/traces`, `/v1/metrics`, and `/v1/logs`. |
8890
| `OPENCODE_OTLP_PROTOCOL` | `grpc` | OTLP transport protocol: `grpc` or `http/protobuf` |
8991
| `OPENCODE_OTLP_METRICS_INTERVAL` | `60000` | Metrics export interval in milliseconds |
9092
| `OPENCODE_OTLP_LOGS_INTERVAL` | `5000` | Logs export interval in milliseconds |
9193
| `OPENCODE_METRIC_PREFIX` | `opencode.` | Prefix for all metric names (e.g. set to `claude_code.` for Claude Code dashboard compatibility) |
92-
| `OPENCODE_DISABLE_METRICS` | _(unset)_ | Comma-separated list of metric name suffixes to disable (e.g. `cache.count,session.duration`) |
93-
| `OPENCODE_OTLP_HEADERS` | _(unset)_ | Comma-separated `key=value` headers added to all OTLP exports. **Keep out of version control — may contain sensitive auth tokens.** |
94-
| `OPENCODE_OTLP_HEADERS_HELPER` | _(unset)_ | Executable script/binary that returns dynamic OTLP headers as JSON after an auth failure. Helper headers override `OPENCODE_OTLP_HEADERS`. |
95-
| `OPENCODE_RESOURCE_ATTRIBUTES` | _(unset)_ | Comma-separated `key=value` pairs merged into the OTel resource. Example: `service.version=1.2.3,deployment.environment=production` |
96-
| `OPENCODE_OTLP_METRICS_TEMPORALITY` | _(unset)_ | Metrics aggregation temporality: `delta`, `cumulative`, or `lowmemory`. Required for Datadog (`delta`). Copied to `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`. |
94+
| `OPENCODE_DISABLE_METRICS` | *(unset)* | Comma-separated list of metric name suffixes to disable (e.g. `cache.count,session.duration`) |
95+
| `OPENCODE_OTLP_HEADERS` | *(unset)* | Comma-separated `key=value` headers added to all OTLP exports. **Keep out of version control — may contain sensitive auth tokens.** |
96+
| `OPENCODE_OTLP_HEADERS_HELPER` | *(unset)* | Executable script/binary that returns dynamic OTLP headers as JSON after an auth failure. Helper headers override `OPENCODE_OTLP_HEADERS`. |
97+
| `OPENCODE_RESOURCE_ATTRIBUTES` | *(unset)* | Comma-separated `key=value` pairs merged into the OTel resource. Example: `service.version=1.2.3,deployment.environment=production` |
98+
| `OPENCODE_OTLP_METRICS_TEMPORALITY` | *(unset)* | Metrics aggregation temporality: `delta`, `cumulative`, or `lowmemory`. Required for Datadog (`delta`). Copied to `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`. |
9799

98100
### Quick start
99101

@@ -187,7 +189,7 @@ export OPENCODE_OTLP_HEADERS="signoz-ingestion-key=<SIGNOZ_INGESTION_KEY>"
187189
```
188190

189191
> Use `https://ingest.in.signoz.cloud:443` for India, `https://ingest.eu2.signoz.cloud:443` for EU2, etc.
190-
> See [SigNoz setup docs](https://signoz.io/docs/cloud/) for all regions.
192+
> See [SigNoz setup docs](https://signoz.io/docs/cloud/) for all regions.
191193
192194
### Datadog example
193195

@@ -231,3 +233,45 @@ export OPENCODE_METRIC_PREFIX=claude_code.
231233
## Local development
232234

233235
See [CONTRIBUTING.md](./CONTRIBUTING.md).
236+
237+
## GitHub Discord notifications
238+
239+
This repo includes a reusable workflow at `.github/workflows/discord-notify.yml` that posts a Discord embed for supported GitHub events. The included `.github/workflows/discord-events.yml` file wires it up for:
240+
241+
- `issues.opened`
242+
- `pull_request.opened`
243+
- `release.published`
244+
245+
Set an org or repo secret named `DISCORD_WEBHOOK` and the workflow will post to that webhook automatically.
246+
247+
To reuse it from another repository in the `DEVtheOPS` org:
248+
249+
```yaml
250+
name: Discord Events
251+
252+
on:
253+
issues:
254+
types: [opened]
255+
pull_request:
256+
types: [opened]
257+
release:
258+
types: [published]
259+
260+
jobs:
261+
notify-discord:
262+
uses: DEVtheOPS/opencode-plugin-otel/.github/workflows/discord-notify.yml@main
263+
with:
264+
username: DEVtheOPS Bot
265+
title_prefix: "[DEVtheOPS]"
266+
include_body: true
267+
secrets:
268+
discord_webhook: ${{ secrets.DISCORD_WEBHOOK }}
269+
```
270+
271+
Available workflow inputs:
272+
273+
- `username`: webhook display name
274+
- `avatar_url`: webhook avatar image URL
275+
- `title_prefix`: optional title prefix for the embed
276+
- `include_body`: include the issue, PR, or release body in the card
277+
- `color`: fallback embed color for unsupported events

0 commit comments

Comments
 (0)