- Add setup instructions for system-wide and org/repo webhooks - Fix sidebar to sort log file groups by most recent file timestamp - Maintains chronological order when viewing logs across different git commits Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
5.1 KiB
Spike
Discord-Gitea bridge bot. When someone opens a PR or leaves a comment on git.nose.space, Spike posts it to a Discord channel. When someone replies in the Discord thread, Spike posts it back to Gitea.
Setup
- Install bun and clone this repo.
bun installfrom the repo root.cp .env.example .envand fill in the values (see below).bun run subdomain:devto start the server.- Visit
localhost:3000/discord/authto authorize the bot to your Discord server. - Use Tailscale Funnel or ngrok to expose your local server to the internet.
- Add a Gitea webhook (see below).
Gitea webhook
Spike needs a webhook so Gitea sends PR and comment events to it. You have two options:
System webhook (covers all repos) — Requires Gitea admin access. Go to Site Administration > System Webhooks > Add Webhook > Gitea. Set the target URL to https://spike.theworkshop.cc/gitea/webhook. Under "Trigger On", select Custom Events and enable Pull Request, Issue Comment, and Pull Request Comment. This fires for every repo on the instance.
Org/repo webhook (covers one org or repo) — Go to the org or repo settings, then Webhooks > Add Webhook > Gitea. Same target URL and event configuration as above. Only fires for that org or repo.
Environment variables
Copy .env.example and fill in:
| Variable | What it is |
|---|---|
DISCORD_TOKEN |
Bot token from Discord Developer Portal |
DISCORD_CLIENT_ID |
Application ID from the same portal |
GITEA_API_TOKEN |
Personal access token from git.nose.space (Settings > Applications) |
PORT |
Server port (default 3000) |
NODE_ENV |
Set to production for prod, leave blank for dev |
DATA_DIR |
Where to store the SQLite DB in prod |
For running integration tests, you also need TEST_GITEA_API_TOKEN_COREY, TEST_GITEA_API_TOKEN_SPIKE, TEST_REPO_COREY, and TEST_REPO_SPIKE — see .env.example for defaults.
Username mappings
Spike converts between Gitea and Discord usernames so @mentions work across platforms. These mappings live in src/config.ts under giteaToDiscordUserMappings. Add your Gitea username → Discord username pair there.
Architecture
src/
├── server.tsx — HTTP server (webhook endpoint, error log)
├── config.ts — Dev/prod config (DB paths, channel IDs, username mappings)
├── gitea/ — Pure Gitea API client and types
├── discord/ — Discord bot client, events, slash commands
└── bridge/ — Wiring between Gitea and Discord (webhook handler, DB, helpers)
Dependencies flow one way: bridge/ → gitea/ and bridge/ → discord/. The gitea and discord modules don't know about each other — bridge is the only thing that connects them.
How the code is organized
Each directory with an index.ts is a standalone lib. The index.ts is a barrel — it re-exports the lib's public API. Everything else inside the directory is internal.
The rule: external code imports from the barrel only. Never reach into a lib's internal files.
// Good
import { handleGiteaWebhook } from "./bridge"
// Bad — reaches into internals
import { handleGiteaWebhook } from "./bridge/webhook-handler"
Each lib has a README.md that documents its barrel exports. This is the contract — if you want to know what a lib does, read its README. You don't need to read every internal file.
This matters for AI-assisted development: when Claude works on code that uses a lib, it only needs the README to understand the API. When it works on code inside a lib, it reads the internals. This keeps context small and focused.
The three libs
gitea/ — Pure Gitea API. Fetches PRs and review comments, converts usernames, formats thread names. No side effects, no Discord, no database. Unit-testable in ~15ms.
discord/ — Discord bot setup. Creates and logs in the client, registers slash commands, listens for messages. When someone types in a PR thread, it hands off to bridge to relay the message to Gitea.
bridge/ — The glue. Receives Gitea webhooks and creates Discord threads/messages. Receives Discord messages and posts Gitea comments. Owns the SQLite database that maps Gitea PR IDs ↔ Discord thread IDs and Gitea comment IDs ↔ Discord message IDs.
Data flow
Gitea webhook POST → server.tsx → bridge/handleGiteaWebhook → Discord thread/message
Discord message → discord/events → bridge/createPRComment → Gitea comment
Running
bun run subdomain:dev— Start with hot reloadbun run subdomain:start— Production modebun test— Run integration tests (requires Tailscale funnel + test env vars)
Tests
Unit tests for pure functions (username conversion, thread naming) run in ~15ms with no external dependencies.
Integration tests for webhooks require a live Gitea instance and Tailscale funnel. They open real PRs, post real comments, and verify the correct webhooks arrive. See src/gitea/test/ for details.