workshop/packages/spike
Corey Johnson 238df92888 Untangle Spike architecture with bridge pattern
Split the monolithic gitea/helpers.ts (which had Discord imports and created circular dependencies) into three focused libs:

1. **gitea/** — Pure API client: fetchPR, fetchReviewComments, convertUsername, threadName. No side effects or external deps.

2. **discord/** — Discord client setup: bot login, event listeners, slash commands. Now isolated from Gitea internals.

3. **bridge/** — New integration layer: webhook handler, DB mappings (Gitea PR ↔ Discord thread), Discord helpers, and createPRComment.

Dependencies now flow one direction: bridge → gitea and bridge → discord. No circular imports.

Added:
- Barrel exports (index.ts) for each lib with public API
- README.md for each lib documenting the barrel exports
- Comprehensive spike README.md with setup guide and architecture explanation
- Integration tests for webhooks (callback-based, no race conditions)
- Unit tests for pure API functions
- CLAUDE.md with links to each lib's README

This architecture makes it possible for AI to understand a lib by reading just its README, keeping context focused and small.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-09 15:23:11 -07:00
..
src Untangle Spike architecture with bridge pattern 2026-03-09 15:23:11 -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 Untangle Spike architecture with bridge pattern 2026-03-09 15:23:11 -07:00
package.json Untangle Spike architecture with bridge pattern 2026-03-09 15:23:11 -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.