diff --git a/.github/workflows/discord-events.yml b/.github/workflows/discord-events.yml new file mode 100644 index 0000000..9bb3e2a --- /dev/null +++ b/.github/workflows/discord-events.yml @@ -0,0 +1,20 @@ +name: Discord Events + +on: + issues: + types: + - opened + pull_request: + types: + - opened + release: + types: + - published + +jobs: + notify-discord: + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} + permissions: {} + uses: ./.github/workflows/discord-notify.yml + secrets: + discord_webhook: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.github/workflows/discord-notify.yml b/.github/workflows/discord-notify.yml new file mode 100644 index 0000000..0f0f2a9 --- /dev/null +++ b/.github/workflows/discord-notify.yml @@ -0,0 +1,160 @@ +name: Discord Notify + +on: + workflow_call: + inputs: + username: + description: Discord webhook username + required: false + default: DEVtheOPS Bot + type: string + avatar_url: + description: Discord webhook avatar URL + required: false + default: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + type: string + color: + description: Decimal embed color override + required: false + default: "5793266" + type: string + title_prefix: + description: Optional prefix added to the embed title + required: false + default: "" + type: string + include_body: + description: Include issue, PR, or release body in the embed description + required: false + default: true + type: boolean + secrets: + discord_webhook: + description: Discord webhook URL + required: true + +jobs: + notify: + name: Send Discord notification + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + steps: + - name: Build payload + id: payload + uses: actions/github-script@v7 + env: + DISCORD_USERNAME: ${{ inputs.username }} + DISCORD_AVATAR_URL: ${{ inputs.avatar_url }} + DISCORD_COLOR: ${{ inputs.color }} + DISCORD_TITLE_PREFIX: ${{ inputs.title_prefix }} + DISCORD_INCLUDE_BODY: ${{ inputs.include_body }} + with: + script: | + const eventName = context.eventName; + const action = context.payload.action || ""; + const repo = context.repo; + const prefix = process.env.DISCORD_TITLE_PREFIX || ""; + const defaultColor = Number.parseInt(process.env.DISCORD_COLOR || "5793266", 10) || 5793266; + const includeBody = (process.env.DISCORD_INCLUDE_BODY || "true") === "true"; + + const truncate = (value, limit) => { + if (!value) return ""; + const normalized = String(value).replace(/\r\n/g, "\n").trim(); + if (!normalized) return ""; + return normalized.length > limit ? `${normalized.slice(0, limit - 1)}…` : normalized; + }; + + const author = { + name: `${repo.owner}/${repo.repo}`, + url: `https://github.com/${repo.owner}/${repo.repo}`, + icon_url: `https://github.com/${repo.owner}.png` + }; + + const fields = [ + { name: "Repository", value: `[${repo.owner}/${repo.repo}](https://github.com/${repo.owner}/${repo.repo})`, inline: true }, + { name: "Actor", value: `[${context.actor}](https://github.com/${context.actor})`, inline: true }, + { name: "Event", value: `${eventName}${action ? `.${action}` : ""}`, inline: true } + ]; + + let title = `GitHub event in ${repo.repo}`; + let url = `https://github.com/${repo.owner}/${repo.repo}`; + let description = ""; + let color = defaultColor; + + if (eventName === "issues" && context.payload.issue) { + const issue = context.payload.issue; + title = `Issue #${issue.number} opened: ${issue.title}`; + url = issue.html_url; + description = includeBody ? truncate(issue.body, 4000) : ""; + color = 16098851; + fields.push( + { name: "Author", value: `[${issue.user.login}](${issue.user.html_url})`, inline: true }, + { name: "Labels", value: issue.labels.length ? issue.labels.map((label) => label.name).join(", ") : "None", inline: true } + ); + } + + if (eventName === "pull_request" && context.payload.pull_request) { + const pr = context.payload.pull_request; + title = `PR #${pr.number} opened: ${pr.title}`; + url = pr.html_url; + description = includeBody ? truncate(pr.body, 4000) : ""; + color = 3447003; + fields.push( + { name: "Author", value: `[${pr.user.login}](${pr.user.html_url})`, inline: true }, + { name: "Branch", value: `\`${pr.head.ref}\` -> \`${pr.base.ref}\``, inline: true } + ); + } + + if (eventName === "release" && context.payload.release) { + const release = context.payload.release; + title = `Release published: ${release.name || release.tag_name}`; + url = release.html_url; + description = includeBody ? truncate(release.body, 4000) : ""; + color = 10181046; + fields.push( + { name: "Tag", value: `\`${release.tag_name}\``, inline: true }, + { name: "Prerelease", value: release.prerelease ? "Yes" : "No", inline: true } + ); + } + + if (prefix) { + title = `${prefix} ${title}`; + } + + const payload = { + username: process.env.DISCORD_USERNAME || "DEVtheOPS Bot", + avatar_url: process.env.DISCORD_AVATAR_URL || "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png", + embeds: [ + { + title, + url, + description, + color, + author, + fields, + timestamp: new Date().toISOString(), + footer: { + text: "GitHub Actions" + } + } + ] + }; + + core.setOutput("json", JSON.stringify(payload)); + + - name: Post to Discord + env: + DISCORD_WEBHOOK: ${{ secrets.discord_webhook }} + DISCORD_PAYLOAD: ${{ steps.payload.outputs.json }} + run: | + curl --fail --silent --show-error \ + --connect-timeout 10 \ + --max-time 30 \ + --retry 3 \ + --retry-delay 2 \ + -H "Content-Type: application/json" \ + -d "$DISCORD_PAYLOAD" \ + "$DISCORD_WEBHOOK" diff --git a/README.md b/README.md index 5595abe..7172a92 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![npm downloads](https://img.shields.io/npm/dm/@devtheops/opencode-plugin-otel.svg)](https://www.npmjs.com/package/@devtheops/opencode-plugin-otel) [![GitHub stars](https://img.shields.io/github/stars/DEVtheOPS/opencode-plugin-otel.svg)](https://github.com/DEVtheOPS/opencode-plugin-otel/stargazers) [![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) +[![Discord notifications](https://img.shields.io/badge/discord-notifications-5865F2?logo=discord&logoColor=white)](https://discord.gg/zavuskz8xB) [![License](https://img.shields.io/npm/l/@devtheops/opencode-plugin-otel.svg)](https://github.com/DEVtheOPS/opencode-plugin-otel/blob/main/LICENSE) 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 - [Honeycomb example](#honeycomb-example) - [Claude Code dashboard compatibility](#claude-code-dashboard-compatibility) - [Local development](#local-development) +- [GitHub Discord notifications](#github-discord-notifications) ## What it instruments @@ -83,17 +85,17 @@ All configuration is via environment variables. Set them in your shell profile ( | Variable | Default | Description | |----------|---------|-------------| -| `OPENCODE_ENABLE_TELEMETRY` | _(unset)_ | Set to any non-empty value to enable the plugin | +| `OPENCODE_ENABLE_TELEMETRY` | *(unset)* | Set to any non-empty value to enable the plugin | | `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`. | | `OPENCODE_OTLP_PROTOCOL` | `grpc` | OTLP transport protocol: `grpc` or `http/protobuf` | | `OPENCODE_OTLP_METRICS_INTERVAL` | `60000` | Metrics export interval in milliseconds | | `OPENCODE_OTLP_LOGS_INTERVAL` | `5000` | Logs export interval in milliseconds | | `OPENCODE_METRIC_PREFIX` | `opencode.` | Prefix for all metric names (e.g. set to `claude_code.` for Claude Code dashboard compatibility) | -| `OPENCODE_DISABLE_METRICS` | _(unset)_ | Comma-separated list of metric name suffixes to disable (e.g. `cache.count,session.duration`) | -| `OPENCODE_OTLP_HEADERS` | _(unset)_ | Comma-separated `key=value` headers added to all OTLP exports. **Keep out of version control — may contain sensitive auth tokens.** | -| `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`. | -| `OPENCODE_RESOURCE_ATTRIBUTES` | _(unset)_ | Comma-separated `key=value` pairs merged into the OTel resource. Example: `service.version=1.2.3,deployment.environment=production` | -| `OPENCODE_OTLP_METRICS_TEMPORALITY` | _(unset)_ | Metrics aggregation temporality: `delta`, `cumulative`, or `lowmemory`. Required for Datadog (`delta`). Copied to `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`. | +| `OPENCODE_DISABLE_METRICS` | *(unset)* | Comma-separated list of metric name suffixes to disable (e.g. `cache.count,session.duration`) | +| `OPENCODE_OTLP_HEADERS` | *(unset)* | Comma-separated `key=value` headers added to all OTLP exports. **Keep out of version control — may contain sensitive auth tokens.** | +| `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`. | +| `OPENCODE_RESOURCE_ATTRIBUTES` | *(unset)* | Comma-separated `key=value` pairs merged into the OTel resource. Example: `service.version=1.2.3,deployment.environment=production` | +| `OPENCODE_OTLP_METRICS_TEMPORALITY` | *(unset)* | Metrics aggregation temporality: `delta`, `cumulative`, or `lowmemory`. Required for Datadog (`delta`). Copied to `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`. | ### Quick start @@ -187,7 +189,7 @@ export OPENCODE_OTLP_HEADERS="signoz-ingestion-key=" ``` > Use `https://ingest.in.signoz.cloud:443` for India, `https://ingest.eu2.signoz.cloud:443` for EU2, etc. -> See [SigNoz setup docs](https://signoz.io/docs/cloud/) for all regions. +> See [SigNoz setup docs](https://signoz.io/docs/cloud/) for all regions. ### Datadog example @@ -231,3 +233,45 @@ export OPENCODE_METRIC_PREFIX=claude_code. ## Local development See [CONTRIBUTING.md](./CONTRIBUTING.md). + +## GitHub Discord notifications + +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: + +- `issues.opened` +- `pull_request.opened` +- `release.published` + +Set an org or repo secret named `DISCORD_WEBHOOK` and the workflow will post to that webhook automatically. + +To reuse it from another repository in the `DEVtheOPS` org: + +```yaml +name: Discord Events + +on: + issues: + types: [opened] + pull_request: + types: [opened] + release: + types: [published] + +jobs: + notify-discord: + uses: DEVtheOPS/opencode-plugin-otel/.github/workflows/discord-notify.yml@main + with: + username: DEVtheOPS Bot + title_prefix: "[DEVtheOPS]" + include_body: true + secrets: + discord_webhook: ${{ secrets.DISCORD_WEBHOOK }} +``` + +Available workflow inputs: + +- `username`: webhook display name +- `avatar_url`: webhook avatar image URL +- `title_prefix`: optional title prefix for the embed +- `include_body`: include the issue, PR, or release body in the card +- `color`: fallback embed color for unsupported events