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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
DISCORD_TOKEN=your_discord_bot_token_here
DISCORD_CHANNEL_ID=your_channel_id_where_bot_speaks

# GitHub Webhook Secret (set this in GitHub repo Settings > Webhooks)
GITHUB_WEBHOOK_SECRET=your_webhook_secret_here

# LM Studio Configuration (Local LLM)
LM_STUDIO_BASE_URL=http://localhost:1234/v1
LM_STUDIO_MODEL=google/gemma-3-4b
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/gitme-notify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: GitMe Discord Notify

on:
push:
branches: ['**']
pull_request:
types: [opened, closed, reopened]
issues:
types: [opened, closed, reopened]

jobs:
notify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Send Discord notification
uses: ./
with:
discord-webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,5 @@ ehthumbs.db
Thumbs.db

# Optional: if you have a build output
dist/
build/
coverage/
102 changes: 102 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What This Project Does

**GitMe** is a GitHub-to-Discord notification bot. It receives GitHub webhook events (push, pull request, issues), optionally summarizes commit messages via a local LLM (LM Studio + Gemma), and sends rich Discord embeds to a configured channel.

## Commands

```bash
# Install dependencies (uses pnpm)
npm install

# Start production server
npm start

# Start development server (Node --watch auto-reload)
npm run dev

# Bundle the GitHub Action (must re-run after editing src/action/index.js)
npm run build:action

# Test AI summarization (requires LM Studio running locally)
npm run test:gemma

# Simulate GitHub webhook events against the local server
node tests/test-manual-events.js push
node tests/test-manual-events.js pr
node tests/test-manual-events.js issue

# Test LM Studio connectivity
node tests/test-ngrok.js
```

There is no automated test suite. `npm test` is a placeholder. All testing is done via the manual integration scripts above.

## Architecture

### Data Flow

```
GitHub Webhook → POST /webhook
→ webhookController (parse & route by event type)
→ aiService.summarizeCommit() ← LM Studio (push only)
→ discordService.send*Notification()
→ Discord channel (rich embeds)
```

### Key Modules

| File | Role |
|------|------|
| `src/index.js` | Entry point — Express setup, middleware, route registration, Discord client start |
| `src/config/env.js` | Loads `.env`, validates required vars (`DISCORD_TOKEN`, `DISCORD_CHANNEL_ID`), exposes AI feature flag |
| `src/middleware/verifySignature.js` | Validates `X-Hub-Signature-256` HMAC; skipped if `GITHUB_WEBHOOK_SECRET` is unset |
| `src/controllers/webhookController.js` | Parses GitHub payloads (JSON + form-encoded), routes `push`/`pull_request`/`issues` events, calls services |
| `src/services/aiService.js` | Calls LM Studio `/chat/completions`; falls back to raw commit message if unavailable |
| `src/services/discordService.js` | Initializes Discord.js client, builds and sends color-coded embeds |
| `action/index.js` | GitHub Action entrypoint — reads event context, posts Discord embed via webhook URL |
| `dist/index.js` | Bundled Action output (generated by `npm run build:action`; commit this file) |
| `action.yml` | Action definition — declares `discord-webhook-url` input, points to `dist/index.js` |

### Event Support

| Event | AI Summary | Embed Color |
|-------|:----------:|-------------|
| Push | Yes (Gemma via LM Studio) | Green |
| Pull Request | No | Blue (open) / Purple (merged) |
| Issue | No | Green (open) / Red (closed) |

### Architecture Notes

- **CommonJS** (`require`/`module.exports`) throughout — not ES modules.
- AI summarization is **optional** — the app works without LM Studio; it degrades gracefully.
- **Stateless** — no database; all history lives in Discord.
- Uses **ngrok** to expose `localhost:3000` to GitHub during local development.
- Deployed on Render with UptimeRobot pinging to keep the free tier alive.

## Environment Variables

```
DISCORD_TOKEN= # Required: Discord bot token
DISCORD_CHANNEL_ID= # Required: Discord channel to post in
GITHUB_WEBHOOK_SECRET= # Recommended: secret set in GitHub repo > Settings > Webhooks
LM_STUDIO_BASE_URL= # Optional: e.g. http://localhost:1234/v1
LM_STUDIO_MODEL= # Optional: e.g. google/gemma-3-4b
PORT=3000 # Optional: defaults to 3000
```

Copy `.env.example` to `.env` and fill in values before running.

## GitHub Action

The repo also ships as a reusable GitHub Action (`action.yml`). It requires **no server** — it runs inside GitHub's CI and posts to Discord via a Discord Webhook URL (not a bot token).

**Setup for users of the action:**
1. In Discord: Server Settings → Integrations → Webhooks → New Webhook → copy URL.
2. In the target GitHub repo: Settings → Secrets → `DISCORD_WEBHOOK_URL` = the copied URL.
3. Add `.github/workflows/gitme-notify.yml` (see example in this repo's own workflows).

**After editing `action/index.js`**, always re-run `npm run build:action` and commit the updated `dist/index.js` — GitHub Actions executes the bundled file, not the source.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,17 @@ Update your GitHub Webhook URL to:

## 🤝 Contributing

Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on how to get started.
Contributions are welcome! Please read our [Contributing Guide](docs/CONTRIBUTING.md) for details on how to get started.

Please note that this project has a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you agree to abide by its terms.
Please note that this project has a [Code of Conduct](docs/CODE_OF_CONDUCT.md). By participating, you agree to abide by its terms.

## 🔒 Security

To report a security vulnerability, please see our [Security Policy](SECURITY.md).
To report a security vulnerability, please see our [Security Policy](docs/SECURITY.md).

## 📝 Changelog

See [CHANGELOG.md](CHANGELOG.md) for a list of notable changes.
See [docs/CHANGELOG.md](docs/CHANGELOG.md) for a list of notable changes.

## License

Expand Down
16 changes: 16 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: 'GitMe Discord Notify'
description: 'Send GitHub event notifications to a Discord channel via webhook'
author: 'Kailash (https://github.com/gubbysbyte)'

branding:
icon: 'bell'
color: 'blue'

inputs:
discord-webhook-url:
description: 'Discord webhook URL (from Server Settings > Integrations > Webhooks)'
required: true

runs:
using: 'node24'
main: 'dist/index.js'
111 changes: 111 additions & 0 deletions action/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const core = require('@actions/core');
const github = require('@actions/github');

async function sendToDiscord(webhookUrl, embed) {
const res = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ embeds: [embed] })
});
if (!res.ok) {
throw new Error(`Discord responded with ${res.status}: ${await res.text()}`);
}
}

function buildPushEmbed(payload) {
const repo = payload.repository.full_name;
const branch = payload.ref.replace('refs/heads/', '');
const commits = (payload.commits || []).slice(0, 5);

const description = commits.map(c => {
const hash = c.id.substring(0, 7);
const msg = c.message.split('\n')[0].substring(0, 72);
return `[\`${hash}\`](${c.url}) ${msg}`;
}).join('\n') || 'No commits';

return {
title: `Push to \`${branch}\``,
url: `https://github.com/${repo}/commits/${branch}`,
color: 0x28a745,
description,
author: { name: payload.pusher.name },
footer: { text: repo },
timestamp: new Date().toISOString()
};
}

function buildPREmbed(payload) {
const pr = payload.pull_request;
const action = payload.action;
const repo = payload.repository.full_name;

let color = 0x0075ca;
if (action === 'closed' && pr.merged) color = 0x6f42c1;
else if (action === 'closed') color = 0xcb2431;

const actionLabel = action === 'closed' && pr.merged ? 'merged' : action;

return {
title: `PR ${actionLabel}: ${pr.title}`,
url: pr.html_url,
color,
description: (pr.body || '').substring(0, 300) || null,
author: { name: pr.user.login, icon_url: pr.user.avatar_url },
fields: [{ name: 'Branch', value: `\`${pr.head.ref}\` → \`${pr.base.ref}\``, inline: true }],
footer: { text: repo },
timestamp: pr.updated_at
};
}

function buildIssueEmbed(payload) {
const issue = payload.issue;
const action = payload.action;
const repo = payload.repository.full_name;

return {
title: `Issue ${action}: ${issue.title}`,
url: issue.html_url,
color: action === 'closed' ? 0xcb2431 : 0x28a745,
description: (issue.body || '').substring(0, 300) || null,
author: { name: issue.user.login, icon_url: issue.user.avatar_url },
footer: { text: repo },
timestamp: issue.updated_at
};
}

async function run() {
try {
const webhookUrl = core.getInput('discord-webhook-url', { required: true });
const { eventName, payload } = github.context;

core.info(`Processing event: ${eventName}`);

let embed;

if (eventName === 'push') {
embed = buildPushEmbed(payload);
} else if (eventName === 'pull_request') {
if (!['opened', 'closed', 'reopened'].includes(payload.action)) {
core.info(`Skipping pull_request action: ${payload.action}`);
return;
}
embed = buildPREmbed(payload);
} else if (eventName === 'issues') {
if (!['opened', 'closed', 'reopened'].includes(payload.action)) {
core.info(`Skipping issues action: ${payload.action}`);
return;
}
embed = buildIssueEmbed(payload);
} else {
core.info(`Unsupported event: ${eventName}`);
return;
}

await sendToDiscord(webhookUrl, embed);
core.info('Discord notification sent');
} catch (err) {
core.setFailed(err.message);
}
}

run();
Loading
Loading