workshop/packages/spike
Corey Johnson 04aa7c1c91 Add typed event logging system and restructure server
Implement a centralized logging system with typed events (webhook, PR, comment, review, Discord relay, errors, etc.) that feeds a pub/sub pattern. Events are logged to console (with ANSI colors: green=normal, yellow=skipped, red=errors, cyan=PR#) and persisted to JSONL files organized by git commit SHA.

Restructure server code into src/server/ directory with separate route handlers. Add a new /logs endpoint with an HTML viewer that displays events in a searchable table with type/repo filtering and browser timezone conversion. Remove the old /errors endpoint.

Improvements:
- All webhook handlers, Discord events, and errors now use log() instead of console.* calls
- Error objects are properly serialized to JSONL (message + stack extracted)
- Path traversal vulnerability fixed in readLogFile()
- Failed API calls and caught errors are now always logged
- Graceful error handling with fallbacks where appropriate (e.g., missing Discord users)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-10 08:58:47 -07:00
..
src Add typed event logging system and restructure server 2026-03-10 08:58:47 -07:00
.env.example Untangle Spike architecture with bridge pattern 2026-03-09 15:23:11 -07:00
.gitignore spike, you are alive 2025-11-07 14:58:20 -08:00
CLAUDE.md Add typed event logging system and restructure server 2026-03-10 08:58:47 -07:00
package.json Add typed event logging system and restructure server 2026-03-10 08:58:47 -07:00
README.md Untangle Spike architecture with bridge pattern 2026-03-09 15:23:11 -07:00
tsconfig.json I went a little crazy 2025-07-11 13:05:50 -07:00

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

  1. Install bun and clone this repo.
  2. bun install from the repo root.
  3. cp .env.example .env and fill in the values (see below).
  4. bun run subdomain:dev to start the server.
  5. Visit localhost:3000/discord/auth to authorize the bot to your Discord server.
  6. Use Tailscale Funnel or ngrok to expose your local server to the internet.
  7. Add a Gitea webhook at https://git.nose.space/<org>/settings/hooks pointing to https://<your-tunnel>/gitea/webhook.

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 reload
  • bun run subdomain:start — Production mode
  • bun 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.