Skip to content

feat: one-way Discord to Linear bridge for #help threads - #61

Merged
phorcys420 merged 62 commits into
mainfrom
phorcys/linear-bridge
Aug 20, 2026
Merged

feat: one-way Discord to Linear bridge for #help threads#61
phorcys420 merged 62 commits into
mainfrom
phorcys/linear-bridge

Conversation

@phorcys420

Copy link
Copy Markdown
Member

What

One-way Discord → Linear bridge for the #help forum (MVP for DEVREL-329).

  • New help thread → new Linear issue
  • New message → issue comment
  • Status change (closed/reopened, waiting-for-user/team) → workflow state + labels
  • One-way only (no Linear → Discord, no two-way)
  • Off by default, toggled via config.linearBridge.enabled

How it's wired

discord.js has no middleware, so the help flow now emits enriched domain events on an internal typed bus (src/lib/bus.ts, no new dep) and the bridge subscribes. The bridge never re-runs isHelpPost.

  • helpThreadCreated, helpMessagePosted, helpThreadStatusChanged emitted from events/channels.ts, events/messages.ts, and lib/discord/help.ts.
  • Bridge lib under src/bridge/linear/ (subfolder so future bridges sit beside it), using @linear/sdk.

Mapping & metadata (stateless)

  • Thread↔issue link lives on Linear as an attachment: url = thread URL (also the issue's Link), title "Discord thread", metadata { threadId, tagIds, tagNames, status, waiting }, readable subtitle. Lookup/dedup via attachmentsForURL.

Labels

  • Single team-scoped group Discord (#help) (created with teamId, never global).
  • One label per applied tag (except open/closed); label description = the Discord tag ID, and find-or-create matches on that (rename-safe, no dupes). Diffed on status change.
  • config.linearBridge.labels.enabled = false → attachment metadata only.

Config

linearBridge { enabled, apiKey, teamId, labels{ enabled, groupName } }, default off. apiKey is env-only (Codercord_linearBridge__apiKey); validateLinearBridgeConfig() exits at startup if enabled without apiKey/teamId.

Backfill

scripts/discord-linear-sync.ts (bun run sync:linear) reuses the same lib to seed existing threads.

Notes / follow-ups

  • Adds the privileged MessageContent intent in code; it must be enabled in the Discord dev portal or comment bodies come through empty.
  • closed/reopened is emitted only from the ThreadUpdate handler (not close.ts) since /close triggers ThreadUpdate too, avoiding duplicate status comments.
  • Out of scope (future issues): Linear → Discord, two-way sync, mention/issue-link resolution, cross-restart message dedup.

Verification

tsc --noEmit clean (incl. script), bun lint/format clean, runtime smoke tests for disabled/enabled/fail-fast paths pass. Bot behavior unchanged when disabled.

Implementation plan

Plan: Discord to Linear bridge (codercord)

Repo coder/codercord · branch phorcys/linear-bridge · one-way MVP for DEVREL-329.

What it does

  • Mirrors #help forum threads into Linear: new thread -> new issue; new message -> issue comment; status change -> workflow state + labels.
  • One-way only (no Linear -> Discord, no two-way).
  • Off by default, toggled via config.linearBridge.enabled.

How it's wired: internal event bus

discord.js has no middleware, so the help flow emits enriched domain events and the bridge subscribes. Bridge never re-runs isHelpPost.

src/lib/bus.ts (typed EventEmitter, no new dep) with:

  • helpThreadCreated(ctx)
  • helpMessagePosted(ctx)
  • helpThreadStatusChanged(ctx)

Emitted from the existing handlers (events/channels.ts, events/messages.ts, lib/discord/help.ts applyWaitingTag, commands/util/close.ts). Emits are best-effort so a consumer error can't break the help flow.

Context resolver in lib/discord/help.ts:

getHelpThreadContext(thread) -> {
  thread, url, title,
  status: "open" | "closed",
  waiting: "user" | "team" | null,
  tags: { id, name }[]   // applied tags minus open/closed
}

Linear side

New dep @linear/sdk. Code under src/bridge/linear/ (subfolder so future bridges live beside it):

  • client.tsLinearClient from config.linearBridge.apiKey.
  • api.ts — thin wrappers: find/create issue, comment, set state, attachment upsert, label group + label ensure.
  • index.ts — orchestration: mirrorThreadCreated, mirrorMessage, mirrorStatus, ensureIssueForThread.
  • src/events/bridge.tsregisterEvents(client); if enabled, subscribe to the bus. Wired into src/index.ts.

Mapping & metadata (stateless)

  • Thread<->issue link lives on Linear: an attachment with url = thread URL (also the issue's Link), title "Discord thread", plus metadata { threadId, tagIds, tagNames, status, waiting } and a readable subtitle.
  • Lookup/dedup via attachmentsForURL; in-run Map is just a cache.
  • Status -> Linear workflow state (closed -> Done, etc.), best-effort.

Labels

  • Single team-scoped group Discord (#help) (created with teamId, never global).
  • One label per applied tag (except open/closed), name kept verbatim.
  • Label description = the Discord tag ID; find-or-create matches on that (rename-safe, no dupes).
  • On status change, recompute desired labels from ctx.tags and diff.
  • config.linearBridge.labels.enabled = false -> metadata only.

Config (config.ts + config.json.example)

linearBridge: {
  enabled: boolean;   // default false
  apiKey: string;     // env only
  teamId: string;     // required when enabled
  labels?: { enabled: boolean; groupName: string }; // default on, "Discord (#help)"
}

configmasher mandatory is static, so validateLinearBridgeConfig() throws at startup if enabled but apiKey/teamId missing.

Backfill script

scripts/discord-linear-sync.ts (bun scripts/discord-linear-sync.ts, plus a sync:linear package script): logs in, walks active help threads like catchUpHelpPosts, and runs the same src/bridge/linear lib. Keeps the always-on bot lean.

Also

  • Add GatewayIntentBits.MessageContent in index.ts (privileged; needs the Discord dev-portal toggle or comment bodies are empty).
  • Update AGENTS.md layout + config example.

Generated by Coder Agents on behalf of @phorcys420.

@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 15:28 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 15:49 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 16:51 Inactive
@phorcys420
phorcys420 force-pushed the phorcys/linear-bridge branch from 6fd36e5 to 5c6a8fa Compare August 19, 2026 17:02
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 17:02 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 18:49 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 19:05 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 19:08 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 19:17 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 19:22 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 20:06 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 20:16 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 20:23 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 20:26 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 20:30 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 20:33 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 20:39 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 20:43 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 20:48 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 21:20 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 21:32 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 21:44 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 21:45 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 21:46 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 21:51 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 21:52 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 21:53 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 22:08 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 22:14 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 19, 2026 22:15 Inactive
@phorcys420
phorcys420 force-pushed the phorcys/linear-bridge branch from ac72642 to 81528ee Compare August 20, 2026 08:29
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 08:29 Inactive
Split the Discord-baked one-way mirror into a source-agnostic core, a Discord
connector, and a Linear hub connector, making way for a future GitHub
Discussions bridge without changing behavior or the on-the-wire format.

- core/: canonical model, Source/Target capability interfaces, orchestrating
  Mirror, reconciler, references, rate-limit retry.
- discord/: DiscordConnector (listeners + backfill + announce) and model mapping.
- linear/: hub split into client/issues/comments/reactions/labels/emojis/
  attachments/state/assets behind LinearConnector.
- Add src/bridge/ARCHITECTURE.md documenting the source-authoritative,
  Linear-as-relay-hub reconciliation model and the planned bidirectional path.
- Delete the redundant one-shot sync script (superseded by startup backfill).
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 09:05 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 10:11 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 10:17 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 10:20 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 10:21 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 10:28 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 10:43 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 10:48 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 10:50 Inactive
@railway-app
railway-app Bot temporarily deployed to Discord bot (Codercord) / production August 20, 2026 10:54 Inactive
@phorcys420
phorcys420 marked this pull request as ready for review August 20, 2026 16:23
@phorcys420
phorcys420 merged commit 653e41a into main Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant