@vercel/board 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ ---
2
+ title: Linear
3
+ description: Turn each Linear issue into a board thread, with its comments as replies and its state as a label.
4
+ ---
5
+
6
+ ## Configure it
7
+
8
+ ```package-install
9
+ npm i chat @chat-adapter/linear @vercel/connect
10
+ ```
11
+
12
+ `linear(app, sync)` takes a [Vercel Connect](./01-overview.md#set-up-webhooks)
13
+ connector and a [sync function](./02-functions.md). `teams` makes one from a map
14
+ of Linear team keys to board channels. board builds the Chat SDK adapter from the
15
+ connector name:
16
+
17
+ ```ts app/api/board/[[...path]]/route.ts
18
+ import { board } from '@vercel/board'
19
+ import { a2 } from '@vercel/board/backend-a2'
20
+ import { vercelQueues } from '@vercel/board/backend-a2/scheduler-vercel'
21
+ import { postgres } from '@vercel/board/backend-a2/store-postgres'
22
+ import { linear, teams } from '@vercel/board/sync-linear'
23
+
24
+ const backend = a2({
25
+ store: postgres({ connectionString: process.env.DATABASE_URL! }),
26
+ scheduler: vercelQueues({ topic: 'board' }),
27
+ })
28
+
29
+ const team = board({
30
+ backend,
31
+ sync: {
32
+ linear: linear('linear/acme', teams({
33
+ ENG: 'dev', // ENG-123 and the rest of the team's issues, both ways
34
+ SUP: 'support',
35
+ })),
36
+ },
37
+ })
38
+ export const GET = team.fetch, POST = team.fetch
39
+ export const { scheduler } = backend
40
+ ```
41
+
42
+ A key is a team's key, the prefix of its issue IDs (`ENG` in `ENG-123`). A value is
43
+ a board channel to mirror, or a [function](./02-functions.md). A team that isn't
44
+ in the map stays off the board, except threads someone syncs by hand.
45
+
46
+ ## An issue's life in one thread
47
+
48
+ 1. **An issue opens.** The board creates a root in the team's channel,
49
+ `ENG-123: <title>` with its link, labeled `issue=ENG-123` and `state` with its
50
+ workflow state's type.
51
+ 2. **Comments** arrive as replies. A reply to a comment stays in that comment's
52
+ thread on Linear, and a board reply to one goes back into it. Edits and
53
+ deletes sync too.
54
+ 3. **Moving the issue** to another state updates `state`. A new title edits a
55
+ root that Linear opened to say it.
56
+ 4. **A root posted on the board** in a mirrored channel opens an issue in that
57
+ team. Its first line is the title, and the whole message is the description.
58
+ Editing the root edits the description. Deleting it on the board leaves the
59
+ issue open on Linear, and a new title on Linear leaves the root as written.
60
+
61
+ | Thing | ID |
62
+ | --- | --- |
63
+ | A team, the channel | `linear:ENG` |
64
+ | An issue | `linear:ENG:<issueId>` |
65
+ | A comment thread, grouped under its issue | `linear:ENG:<issueId>:c:<commentId>` |
66
+
67
+ `<issueId>` is the issue's UUID, which stays the same when the issue is renamed.
68
+ To sync a thread by hand, name the issue by its identifier, its UUID, or its ID:
69
+ `vb sync 812 linear:ENG-123`. A comment thread isn't a sync target: sync its
70
+ issue.
71
+
72
+ An issue that moves to another team starts a new thread in that team's channel,
73
+ with a root fetched from Linear: the team is part of the ID, so what's said on
74
+ the board follows the team it's in now. The old thread stays as it was.
75
+
76
+ ## Events
77
+
78
+ | Event | What the mirror does |
79
+ | --- | --- |
80
+ | `issue.created` | A root labeled `issue=ENG-123` and `state=<type>` |
81
+ | `issue.updated` | A new state sets `state`; a new title edits a root Linear opened. Other changes write nothing |
82
+ | `comment` | A reply. Edits and deletes sync too |
83
+
84
+ `state` is the workflow state's type: `triage`, `backlog`, `unstarted`,
85
+ `started`, `completed`, or `canceled`, the same whatever a team names its states.
86
+
87
+ ```bash
88
+ # in a terminal:
89
+ vb read dev --label state=started # what's in progress
90
+ vb wait dev --label state=completed --timeout 1h
91
+ ```
92
+
93
+ A comment on an issue that opened before you mapped its team still lands: the
94
+ board fetches the issue through Linear's API, creates its root, then posts the
95
+ comment as a reply.
96
+
97
+ ## Reactions
98
+
99
+ A reaction on the board goes to its comment on Linear, while it's added: Linear
100
+ reacts to comments, not issues, and the adapter can't take one back. Reactions
101
+ on Linear stay there, since Linear's reaction webhooks don't say which issue
102
+ they're on.
103
+
104
+ ## Setup
105
+
106
+ 1. Create a Linear app (**Settings > API > Applications**), through Vercel
107
+ Connect or by hand, with the `read`, `write`, `comments:create`, and
108
+ `issues:create` scopes, installed as the app (`actor=app`).
109
+ 2. Subscribe its webhook to **Issues** and **Comments**, for the teams you map.
110
+ 3. Point the webhook at `https://<host>/api/board/webhooks/linear`: Connect's
111
+ trigger path, or the app's webhook URL with its signing secret.
112
+
113
+ Without Connect, pass your own adapter, in its default `comments` mode, with the
114
+ app's own credentials:
115
+
116
+ ```ts
117
+ // in board({ sync: { … } }):
118
+ // import { createLinearAdapter } from '@chat-adapter/linear'
119
+ linear({
120
+ adapter: createLinearAdapter({
121
+ clientCredentials: { clientId: process.env.LINEAR_CLIENT_ID!, clientSecret: process.env.LINEAR_CLIENT_SECRET! },
122
+ webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,
123
+ }),
124
+ }, teams({ ENG: 'dev' })),
125
+ ```
126
+
127
+ > [!WARNING]
128
+ > The adapter takes the token's user for itself and skips what they write. With a
129
+ > personal API key, that's you: your own comments never reach the board.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/board",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "A message board you mount in one route: one durable log, verified authors, live tail, and two-way sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -100,6 +100,10 @@
100
100
  "types": "./dist/sync-kapso.d.mts",
101
101
  "default": "./dist/sync-kapso.mjs"
102
102
  },
103
+ "./sync-linear": {
104
+ "types": "./dist/sync-linear.d.mts",
105
+ "default": "./dist/sync-linear.mjs"
106
+ },
103
107
  "./package.json": "./package.json"
104
108
  },
105
109
  "dependencies": {
@@ -109,6 +113,7 @@
109
113
  },
110
114
  "peerDependencies": {
111
115
  "@chat-adapter/github": "^4.41.1",
116
+ "@chat-adapter/linear": "^4.41.1",
112
117
  "@chat-adapter/slack": "^4.41.1",
113
118
  "@upstash/qstash": "^2.11.0",
114
119
  "@vercel/blob": "^2.8.0",
@@ -121,6 +126,7 @@
121
126
  "devDependencies": {
122
127
  "@base-ui/react": "^1.8.0",
123
128
  "@chat-adapter/github": "^4.41.1",
129
+ "@chat-adapter/linear": "^4.41.1",
124
130
  "@chat-adapter/slack": "^4.41.1",
125
131
  "@chat-adapter/tests": "^4.41.1",
126
132
  "@electric-sql/pglite": "0.3.15",
@@ -150,6 +156,9 @@
150
156
  "@chat-adapter/github": {
151
157
  "optional": true
152
158
  },
159
+ "@chat-adapter/linear": {
160
+ "optional": true
161
+ },
153
162
  "@chat-adapter/slack": {
154
163
  "optional": true
155
164
  },