Skip to content

Commit a387bd2

Browse files
authored
feat: GitHub Action, webhook signature verification, and repo restructure (#9)
* docs: add workflow documentation and system diagram image * feat: add GitHub Action, webhook signature verification, and repo restructure - Add GitHub Action (action/index.js + action.yml + dist/) for zero-infrastructure Discord notifications - Add webhook signature verification middleware (X-Hub-Signature-256 HMAC validation) - Restructure: move src/action/ → action/, move docs to docs/ folder - Add act test event fixtures (tests/events/) - Switch from npm to pnpm, remove package-lock.json - Add CLAUDE.md and docs/explanation.md
1 parent 77aadee commit a387bd2

23 files changed

Lines changed: 33623 additions & 10 deletions

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
DISCORD_TOKEN=your_discord_bot_token_here
33
DISCORD_CHANNEL_ID=your_channel_id_where_bot_speaks
44

5+
# GitHub Webhook Secret (set this in GitHub repo Settings > Webhooks)
6+
GITHUB_WEBHOOK_SECRET=your_webhook_secret_here
7+
58
# LM Studio Configuration (Local LLM)
69
LM_STUDIO_BASE_URL=http://localhost:1234/v1
710
LM_STUDIO_MODEL=google/gemma-3-4b

.github/workflows/gitme-notify.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: GitMe Discord Notify
2+
3+
on:
4+
push:
5+
branches: ['**']
6+
pull_request:
7+
types: [opened, closed, reopened]
8+
issues:
9+
types: [opened, closed, reopened]
10+
11+
jobs:
12+
notify:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Send Discord notification
18+
uses: ./
19+
with:
20+
discord-webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,5 @@ ehthumbs.db
3434
Thumbs.db
3535

3636
# Optional: if you have a build output
37-
dist/
3837
build/
3938
coverage/

CLAUDE.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## What This Project Does
6+
7+
**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.
8+
9+
## Commands
10+
11+
```bash
12+
# Install dependencies (uses pnpm)
13+
npm install
14+
15+
# Start production server
16+
npm start
17+
18+
# Start development server (Node --watch auto-reload)
19+
npm run dev
20+
21+
# Bundle the GitHub Action (must re-run after editing src/action/index.js)
22+
npm run build:action
23+
24+
# Test AI summarization (requires LM Studio running locally)
25+
npm run test:gemma
26+
27+
# Simulate GitHub webhook events against the local server
28+
node tests/test-manual-events.js push
29+
node tests/test-manual-events.js pr
30+
node tests/test-manual-events.js issue
31+
32+
# Test LM Studio connectivity
33+
node tests/test-ngrok.js
34+
```
35+
36+
There is no automated test suite. `npm test` is a placeholder. All testing is done via the manual integration scripts above.
37+
38+
## Architecture
39+
40+
### Data Flow
41+
42+
```
43+
GitHub Webhook → POST /webhook
44+
→ webhookController (parse & route by event type)
45+
→ aiService.summarizeCommit() ← LM Studio (push only)
46+
→ discordService.send*Notification()
47+
→ Discord channel (rich embeds)
48+
```
49+
50+
### Key Modules
51+
52+
| File | Role |
53+
|------|------|
54+
| `src/index.js` | Entry point — Express setup, middleware, route registration, Discord client start |
55+
| `src/config/env.js` | Loads `.env`, validates required vars (`DISCORD_TOKEN`, `DISCORD_CHANNEL_ID`), exposes AI feature flag |
56+
| `src/middleware/verifySignature.js` | Validates `X-Hub-Signature-256` HMAC; skipped if `GITHUB_WEBHOOK_SECRET` is unset |
57+
| `src/controllers/webhookController.js` | Parses GitHub payloads (JSON + form-encoded), routes `push`/`pull_request`/`issues` events, calls services |
58+
| `src/services/aiService.js` | Calls LM Studio `/chat/completions`; falls back to raw commit message if unavailable |
59+
| `src/services/discordService.js` | Initializes Discord.js client, builds and sends color-coded embeds |
60+
| `action/index.js` | GitHub Action entrypoint — reads event context, posts Discord embed via webhook URL |
61+
| `dist/index.js` | Bundled Action output (generated by `npm run build:action`; commit this file) |
62+
| `action.yml` | Action definition — declares `discord-webhook-url` input, points to `dist/index.js` |
63+
64+
### Event Support
65+
66+
| Event | AI Summary | Embed Color |
67+
|-------|:----------:|-------------|
68+
| Push | Yes (Gemma via LM Studio) | Green |
69+
| Pull Request | No | Blue (open) / Purple (merged) |
70+
| Issue | No | Green (open) / Red (closed) |
71+
72+
### Architecture Notes
73+
74+
- **CommonJS** (`require`/`module.exports`) throughout — not ES modules.
75+
- AI summarization is **optional** — the app works without LM Studio; it degrades gracefully.
76+
- **Stateless** — no database; all history lives in Discord.
77+
- Uses **ngrok** to expose `localhost:3000` to GitHub during local development.
78+
- Deployed on Render with UptimeRobot pinging to keep the free tier alive.
79+
80+
## Environment Variables
81+
82+
```
83+
DISCORD_TOKEN= # Required: Discord bot token
84+
DISCORD_CHANNEL_ID= # Required: Discord channel to post in
85+
GITHUB_WEBHOOK_SECRET= # Recommended: secret set in GitHub repo > Settings > Webhooks
86+
LM_STUDIO_BASE_URL= # Optional: e.g. http://localhost:1234/v1
87+
LM_STUDIO_MODEL= # Optional: e.g. google/gemma-3-4b
88+
PORT=3000 # Optional: defaults to 3000
89+
```
90+
91+
Copy `.env.example` to `.env` and fill in values before running.
92+
93+
## GitHub Action
94+
95+
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).
96+
97+
**Setup for users of the action:**
98+
1. In Discord: Server Settings → Integrations → Webhooks → New Webhook → copy URL.
99+
2. In the target GitHub repo: Settings → Secrets → `DISCORD_WEBHOOK_URL` = the copied URL.
100+
3. Add `.github/workflows/gitme-notify.yml` (see example in this repo's own workflows).
101+
102+
**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.

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,17 +163,17 @@ Update your GitHub Webhook URL to:
163163

164164
## 🤝 Contributing
165165

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

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

170170
## 🔒 Security
171171

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

174174
## 📝 Changelog
175175

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

178178
## License
179179

action.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: 'GitMe Discord Notify'
2+
description: 'Send GitHub event notifications to a Discord channel via webhook'
3+
author: 'Kailash (https://github.com/gubbysbyte)'
4+
5+
branding:
6+
icon: 'bell'
7+
color: 'blue'
8+
9+
inputs:
10+
discord-webhook-url:
11+
description: 'Discord webhook URL (from Server Settings > Integrations > Webhooks)'
12+
required: true
13+
14+
runs:
15+
using: 'node24'
16+
main: 'dist/index.js'

action/index.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
const core = require('@actions/core');
2+
const github = require('@actions/github');
3+
4+
async function sendToDiscord(webhookUrl, embed) {
5+
const res = await fetch(webhookUrl, {
6+
method: 'POST',
7+
headers: { 'Content-Type': 'application/json' },
8+
body: JSON.stringify({ embeds: [embed] })
9+
});
10+
if (!res.ok) {
11+
throw new Error(`Discord responded with ${res.status}: ${await res.text()}`);
12+
}
13+
}
14+
15+
function buildPushEmbed(payload) {
16+
const repo = payload.repository.full_name;
17+
const branch = payload.ref.replace('refs/heads/', '');
18+
const commits = (payload.commits || []).slice(0, 5);
19+
20+
const description = commits.map(c => {
21+
const hash = c.id.substring(0, 7);
22+
const msg = c.message.split('\n')[0].substring(0, 72);
23+
return `[\`${hash}\`](${c.url}) ${msg}`;
24+
}).join('\n') || 'No commits';
25+
26+
return {
27+
title: `Push to \`${branch}\``,
28+
url: `https://github.com/${repo}/commits/${branch}`,
29+
color: 0x28a745,
30+
description,
31+
author: { name: payload.pusher.name },
32+
footer: { text: repo },
33+
timestamp: new Date().toISOString()
34+
};
35+
}
36+
37+
function buildPREmbed(payload) {
38+
const pr = payload.pull_request;
39+
const action = payload.action;
40+
const repo = payload.repository.full_name;
41+
42+
let color = 0x0075ca;
43+
if (action === 'closed' && pr.merged) color = 0x6f42c1;
44+
else if (action === 'closed') color = 0xcb2431;
45+
46+
const actionLabel = action === 'closed' && pr.merged ? 'merged' : action;
47+
48+
return {
49+
title: `PR ${actionLabel}: ${pr.title}`,
50+
url: pr.html_url,
51+
color,
52+
description: (pr.body || '').substring(0, 300) || null,
53+
author: { name: pr.user.login, icon_url: pr.user.avatar_url },
54+
fields: [{ name: 'Branch', value: `\`${pr.head.ref}\` → \`${pr.base.ref}\``, inline: true }],
55+
footer: { text: repo },
56+
timestamp: pr.updated_at
57+
};
58+
}
59+
60+
function buildIssueEmbed(payload) {
61+
const issue = payload.issue;
62+
const action = payload.action;
63+
const repo = payload.repository.full_name;
64+
65+
return {
66+
title: `Issue ${action}: ${issue.title}`,
67+
url: issue.html_url,
68+
color: action === 'closed' ? 0xcb2431 : 0x28a745,
69+
description: (issue.body || '').substring(0, 300) || null,
70+
author: { name: issue.user.login, icon_url: issue.user.avatar_url },
71+
footer: { text: repo },
72+
timestamp: issue.updated_at
73+
};
74+
}
75+
76+
async function run() {
77+
try {
78+
const webhookUrl = core.getInput('discord-webhook-url', { required: true });
79+
const { eventName, payload } = github.context;
80+
81+
core.info(`Processing event: ${eventName}`);
82+
83+
let embed;
84+
85+
if (eventName === 'push') {
86+
embed = buildPushEmbed(payload);
87+
} else if (eventName === 'pull_request') {
88+
if (!['opened', 'closed', 'reopened'].includes(payload.action)) {
89+
core.info(`Skipping pull_request action: ${payload.action}`);
90+
return;
91+
}
92+
embed = buildPREmbed(payload);
93+
} else if (eventName === 'issues') {
94+
if (!['opened', 'closed', 'reopened'].includes(payload.action)) {
95+
core.info(`Skipping issues action: ${payload.action}`);
96+
return;
97+
}
98+
embed = buildIssueEmbed(payload);
99+
} else {
100+
core.info(`Unsupported event: ${eventName}`);
101+
return;
102+
}
103+
104+
await sendToDiscord(webhookUrl, embed);
105+
core.info('Discord notification sent');
106+
} catch (err) {
107+
core.setFailed(err.message);
108+
}
109+
}
110+
111+
run();

0 commit comments

Comments
 (0)