@vercel/board 0.0.0 → 0.15.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.
- package/README.md +180 -0
- package/dist/a2-5EG8FkZ8.d.mts +3359 -0
- package/dist/a2-DYGYankn.mjs +3045 -0
- package/dist/access-CJU2-vaa.mjs +226 -0
- package/dist/access-CV96SYgr.d.mts +32 -0
- package/dist/access.d.mts +29 -0
- package/dist/access.mjs +4 -0
- package/dist/actors-rdN4dDTU.mjs +223 -0
- package/dist/attempts-BQUp9MKO.mjs +919 -0
- package/dist/auth-COzc09sn.mjs +276 -0
- package/dist/auth-wHFcAte3.d.mts +203 -0
- package/dist/backend-a2/blob-vercel.d.mts +8 -0
- package/dist/backend-a2/blob-vercel.mjs +81 -0
- package/dist/backend-a2/contract.d.mts +18 -0
- package/dist/backend-a2/contract.mjs +5 -0
- package/dist/backend-a2/scheduler-local.d.mts +11 -0
- package/dist/backend-a2/scheduler-local.mjs +2 -0
- package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
- package/dist/backend-a2/scheduler-qstash.mjs +2 -0
- package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
- package/dist/backend-a2/scheduler-vercel.mjs +2 -0
- package/dist/backend-a2/store-memory.d.mts +11 -0
- package/dist/backend-a2/store-memory.mjs +19 -0
- package/dist/backend-a2/store-postgres.d.mts +38 -0
- package/dist/backend-a2/store-postgres.mjs +110 -0
- package/dist/backend-a2/store-redis-http.d.mts +17 -0
- package/dist/backend-a2/store-redis-http.mjs +32 -0
- package/dist/backend-a2/store-redis.d.mts +23 -0
- package/dist/backend-a2/store-redis.mjs +39 -0
- package/dist/backend-a2/store-sqlite.d.mts +12 -0
- package/dist/backend-a2/store-sqlite.mjs +31 -0
- package/dist/backend-a2.d.mts +4 -0
- package/dist/backend-a2.mjs +2 -0
- package/dist/backend-http-8ex5g7Wv.d.mts +630 -0
- package/dist/backend-http-BsDSgmeo.mjs +613 -0
- package/dist/backend-http-Cv6xbEy9.mjs +469 -0
- package/dist/backend-http.d.mts +10 -0
- package/dist/backend-http.mjs +2 -0
- package/dist/backend-http.node.d.mts +10 -0
- package/dist/backend-http.node.mjs +2 -0
- package/dist/blob-C0J3wbGv.d.mts +39 -0
- package/dist/bots-CGkO76qH.mjs +67 -0
- package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
- package/dist/chat-provider-jFSeyNmB.mjs +389 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2252 -0
- package/dist/client-DGDPaN7U.mjs +380 -0
- package/dist/conformance-DRR23YNy.mjs +512 -0
- package/dist/conformance.d.mts +30 -0
- package/dist/conformance.mjs +2 -0
- package/dist/contract-CuJmlrhw.mjs +566 -0
- package/dist/emoji-CTLpvH-A.mjs +445 -0
- package/dist/engine-3EvQ8idk.mjs +1449 -0
- package/dist/errors-DX3hpwWt.mjs +64 -0
- package/dist/exports-BpgyEdh6.d.mts +4 -0
- package/dist/exports-CyY3_FjZ.mjs +1285 -0
- package/dist/files-SniSmeEr.mjs +47 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.default-BZ3lItjH.mjs +12 -0
- package/dist/index.mjs +19 -0
- package/dist/index.node.d.mts +14 -0
- package/dist/index.node.mjs +5 -0
- package/dist/inspect/app.js +416 -0
- package/dist/inspect/engine.js +5 -0
- package/dist/local-Cr_dSjvs.mjs +44 -0
- package/dist/own-OwgPWdmh.mjs +15 -0
- package/dist/project-CFiArraX.mjs +124 -0
- package/dist/provider-BGjz-mDa.d.mts +2473 -0
- package/dist/provider-CuewcDNZ.mjs +205 -0
- package/dist/redis-tables-B35vv4CZ.mjs +842 -0
- package/dist/reducers-B_vUGQRp.d.mts +1172 -0
- package/dist/reducers-mUPFuFJV.mjs +293 -0
- package/dist/schemas-Be08nyfW.mjs +74 -0
- package/dist/schemas-CWxs3ECJ.d.mts +50 -0
- package/dist/serve-Ccbb2LSb.mjs +170 -0
- package/dist/server-Cxey38B-.mjs +17 -0
- package/dist/serving-BNbzc96L.mjs +2 -0
- package/dist/sql-DbiEnqM0.mjs +571 -0
- package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
- package/dist/sync-github.d.mts +473 -0
- package/dist/sync-github.mjs +603 -0
- package/dist/sync-kapso.d.mts +175 -0
- package/dist/sync-kapso.mjs +86 -0
- package/dist/sync-slack.d.mts +364 -0
- package/dist/sync-slack.mjs +728 -0
- package/dist/sync.chat.d.mts +2 -0
- package/dist/sync.chat.mjs +2 -0
- package/dist/sync.d.mts +3 -0
- package/dist/sync.mjs +4 -0
- package/dist/targets-ojFEXEyN.mjs +94 -0
- package/dist/tokens-DKLn5P9B.mjs +256 -0
- package/dist/types-BHCjTZdB.d.mts +104 -0
- package/dist/users-Dls7_ANV.mjs +42 -0
- package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
- package/dist/watchers-hash-D9hbHLKB.mjs +225 -0
- package/docs/01-board/01-mounting.md +506 -0
- package/docs/01-board/02-database.md +258 -0
- package/docs/01-board/03-identity.md +262 -0
- package/docs/01-board/04-messages.md +192 -0
- package/docs/01-board/05-reading.md +177 -0
- package/docs/01-board/06-watchers.md +249 -0
- package/docs/01-board/07-schedules.md +133 -0
- package/docs/01-board/08-middleware.md +139 -0
- package/docs/01-board/09-access.md +499 -0
- package/docs/01-quickstart.md +196 -0
- package/docs/02-sync/01-overview.md +290 -0
- package/docs/02-sync/02-callbacks.md +265 -0
- package/docs/02-sync/03-slack.md +248 -0
- package/docs/02-sync/04-github.md +201 -0
- package/docs/02-sync/05-whatsapp.md +148 -0
- package/docs/02-sync/06-providers.md +228 -0
- package/docs/03-guides/01-coding-agents.md +226 -0
- package/docs/03-guides/02-inspector.md +104 -0
- package/docs/03-guides/03-ci.md +251 -0
- package/docs/03-guides/04-triage.md +233 -0
- package/docs/03-guides/05-agent-factory.md +142 -0
- package/docs/04-reference/01-cli.md +606 -0
- package/docs/04-reference/02-board.md +579 -0
- package/docs/04-reference/03-http.md +489 -0
- package/docs/04-reference/04-log.md +250 -0
- package/docs/04-reference/05-conformance.md +97 -0
- package/docs/index.md +186 -0
- package/package.json +168 -1
- package/sql/postgres/board.sql +145 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +451 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import { c as eventSchema, s as envelope } from "./provider-CuewcDNZ.mjs";
|
|
2
|
+
import { r as withUserTokens, t as connectUserTokens } from "./users-Dls7_ANV.mjs";
|
|
3
|
+
import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-jFSeyNmB.mjs";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { connectGitHubAdapter } from "@vercel/connect/chat";
|
|
6
|
+
import { createGitHubAdapter } from "@chat-adapter/github";
|
|
7
|
+
//#region src/sync/github.ts
|
|
8
|
+
/** `owner/repo:12` (and its review threads) → `owner/repo:pulls`; `owner/repo:issue:3` → `owner/repo:issues`. */
|
|
9
|
+
function channelOf(thread) {
|
|
10
|
+
const match = /^([^:]+):(issue:)?\d+/.exec(thread);
|
|
11
|
+
return match ? `${match[1]}:${match[2] ? "issues" : "pulls"}` : thread;
|
|
12
|
+
}
|
|
13
|
+
const author = (user) => ({
|
|
14
|
+
id: `github:user:${user?.id ?? "unknown"}`,
|
|
15
|
+
handle: user?.login ?? "unknown",
|
|
16
|
+
name: user?.login ?? "unknown",
|
|
17
|
+
avatar: user?.avatar_url ?? null,
|
|
18
|
+
isBot: user?.type === "Bot"
|
|
19
|
+
});
|
|
20
|
+
/** `good first issue` → `good-first-issue`: the part of a label key a name gives. Empty when nothing's left. */
|
|
21
|
+
const slug = (name) => name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^[-_.]+|-+$/g, "");
|
|
22
|
+
/**
|
|
23
|
+
* A GitHub label or assignee as a board label: `bug` → `tag.bug=bug`,
|
|
24
|
+
* `julianbenegas` → `assignee.julianbenegas=julianbenegas`. The keys don't
|
|
25
|
+
* name GitHub, so a filter like `--label tag.bug` holds for any platform.
|
|
26
|
+
*/
|
|
27
|
+
function tagOf(prefix, name) {
|
|
28
|
+
const key = name ? slug(name) : "";
|
|
29
|
+
return key ? [`${prefix}.${key}`.slice(0, 64), name.slice(0, 256)] : null;
|
|
30
|
+
}
|
|
31
|
+
/** A PR's or issue's current labels and assignees, as board labels. */
|
|
32
|
+
function tagsOf(item) {
|
|
33
|
+
const tags = [...(item.labels ?? []).map((label) => tagOf("tag", label.name)), ...(item.assignees ?? []).map((user) => tagOf("assignee", user.login))];
|
|
34
|
+
return Object.fromEntries(tags.filter((tag) => tag !== null));
|
|
35
|
+
}
|
|
36
|
+
/** Actions that change only labels and assignees, which every PR and issue event reconciles. */
|
|
37
|
+
const TAG_ACTIONS = [
|
|
38
|
+
"labeled",
|
|
39
|
+
"unlabeled",
|
|
40
|
+
"assigned",
|
|
41
|
+
"unassigned"
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* The label key prefixes the mirror owns on a GitHub root: keys under them that GitHub doesn't have are unset.
|
|
45
|
+
* A prefix is owned only when the payload carries its array, so a trimmed payload doesn't unset every key.
|
|
46
|
+
*/
|
|
47
|
+
function ownedBy(item) {
|
|
48
|
+
return [...Array.isArray(item.labels) ? ["tag."] : [], ...Array.isArray(item.assignees) ? ["assignee."] : []];
|
|
49
|
+
}
|
|
50
|
+
/** Maps a raw `pull_request`, `pull_request_review`, or `issues` webhook to a lifecycle event. */
|
|
51
|
+
function lifecycle(name, body) {
|
|
52
|
+
const action = String(body.action ?? "");
|
|
53
|
+
const sender = body.sender;
|
|
54
|
+
if (name === "pull_request") {
|
|
55
|
+
const pr = body.pull_request;
|
|
56
|
+
const pr_label = { pr: `${body.repository.full_name}#${pr.number}` };
|
|
57
|
+
const base = {
|
|
58
|
+
title: `PR #${pr.number}: ${pr.title}`,
|
|
59
|
+
url: pr.html_url,
|
|
60
|
+
unset: []
|
|
61
|
+
};
|
|
62
|
+
if (TAG_ACTIONS.includes(action)) return {
|
|
63
|
+
number: pr.number,
|
|
64
|
+
user: sender ?? {},
|
|
65
|
+
lifecycle: {
|
|
66
|
+
...base,
|
|
67
|
+
type: `pr.${action}`,
|
|
68
|
+
labels: {},
|
|
69
|
+
reply: null,
|
|
70
|
+
createRoot: false
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
switch (action) {
|
|
74
|
+
case "opened": return {
|
|
75
|
+
number: pr.number,
|
|
76
|
+
user: sender ?? {},
|
|
77
|
+
lifecycle: {
|
|
78
|
+
...base,
|
|
79
|
+
type: "pr.opened",
|
|
80
|
+
labels: {
|
|
81
|
+
...pr_label,
|
|
82
|
+
state: pr.draft ? "draft" : "open"
|
|
83
|
+
},
|
|
84
|
+
reply: null,
|
|
85
|
+
createRoot: true
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
case "ready_for_review": return {
|
|
89
|
+
number: pr.number,
|
|
90
|
+
user: sender ?? {},
|
|
91
|
+
lifecycle: {
|
|
92
|
+
...base,
|
|
93
|
+
type: "pr.ready",
|
|
94
|
+
labels: {
|
|
95
|
+
...pr_label,
|
|
96
|
+
state: "open"
|
|
97
|
+
},
|
|
98
|
+
reply: null,
|
|
99
|
+
createRoot: true
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
case "converted_to_draft": return {
|
|
103
|
+
number: pr.number,
|
|
104
|
+
user: sender ?? {},
|
|
105
|
+
lifecycle: {
|
|
106
|
+
...base,
|
|
107
|
+
type: "pr.draft",
|
|
108
|
+
labels: { state: "draft" },
|
|
109
|
+
reply: null,
|
|
110
|
+
createRoot: false
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
case "reopened": return {
|
|
114
|
+
number: pr.number,
|
|
115
|
+
user: sender ?? {},
|
|
116
|
+
lifecycle: {
|
|
117
|
+
...base,
|
|
118
|
+
type: "pr.reopened",
|
|
119
|
+
labels: {
|
|
120
|
+
...pr_label,
|
|
121
|
+
state: pr.draft ? "draft" : "open"
|
|
122
|
+
},
|
|
123
|
+
reply: `reopened by ${sender?.login ?? "someone"}`,
|
|
124
|
+
createRoot: true
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
case "closed": return {
|
|
128
|
+
number: pr.number,
|
|
129
|
+
user: sender ?? {},
|
|
130
|
+
lifecycle: {
|
|
131
|
+
...base,
|
|
132
|
+
type: "pr.closed",
|
|
133
|
+
labels: { state: pr.merged ? "merged" : "closed" },
|
|
134
|
+
reply: `${pr.merged ? "merged" : "closed"} by ${sender?.login ?? "someone"}`,
|
|
135
|
+
createRoot: false
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
default: return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (name === "pull_request_review") {
|
|
142
|
+
const pr = body.pull_request;
|
|
143
|
+
const review = body.review;
|
|
144
|
+
const base = {
|
|
145
|
+
title: `PR #${pr.number}: ${pr.title}`,
|
|
146
|
+
url: pr.html_url,
|
|
147
|
+
createRoot: false
|
|
148
|
+
};
|
|
149
|
+
if (action === "dismissed") return {
|
|
150
|
+
number: pr.number,
|
|
151
|
+
user: sender ?? {},
|
|
152
|
+
lifecycle: {
|
|
153
|
+
...base,
|
|
154
|
+
type: "review.dismissed",
|
|
155
|
+
labels: {},
|
|
156
|
+
unset: ["review"],
|
|
157
|
+
reply: null
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
if (action !== "submitted") return null;
|
|
161
|
+
const state = String(review.state ?? "").toLowerCase();
|
|
162
|
+
const verdict = state === "approved" ? "approved" : state === "changes_requested" ? "changes requested" : null;
|
|
163
|
+
const text = [verdict ? `${verdict} by ${sender?.login ?? "someone"}` : null, review.body || null].filter(Boolean).join("\n\n");
|
|
164
|
+
if (!text) return null;
|
|
165
|
+
return {
|
|
166
|
+
number: pr.number,
|
|
167
|
+
user: sender ?? {},
|
|
168
|
+
lifecycle: {
|
|
169
|
+
...base,
|
|
170
|
+
type: "review.submitted",
|
|
171
|
+
labels: verdict ? { review: state } : {},
|
|
172
|
+
unset: [],
|
|
173
|
+
reply: text
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (name === "issues") {
|
|
178
|
+
const issue = body.issue;
|
|
179
|
+
const label = { issue: `${body.repository.full_name}#${issue.number}` };
|
|
180
|
+
const base = {
|
|
181
|
+
title: `Issue #${issue.number}: ${issue.title}`,
|
|
182
|
+
url: issue.html_url,
|
|
183
|
+
unset: []
|
|
184
|
+
};
|
|
185
|
+
if (TAG_ACTIONS.includes(action)) return {
|
|
186
|
+
number: issue.number,
|
|
187
|
+
user: sender ?? {},
|
|
188
|
+
lifecycle: {
|
|
189
|
+
...base,
|
|
190
|
+
type: `issue.${action}`,
|
|
191
|
+
labels: {},
|
|
192
|
+
reply: null,
|
|
193
|
+
createRoot: false
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
if (action === "opened") return {
|
|
197
|
+
number: issue.number,
|
|
198
|
+
user: sender ?? {},
|
|
199
|
+
lifecycle: {
|
|
200
|
+
...base,
|
|
201
|
+
type: "issue.opened",
|
|
202
|
+
labels: {
|
|
203
|
+
...label,
|
|
204
|
+
state: "open"
|
|
205
|
+
},
|
|
206
|
+
reply: null,
|
|
207
|
+
createRoot: true
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
if (action === "closed") return {
|
|
211
|
+
number: issue.number,
|
|
212
|
+
user: sender ?? {},
|
|
213
|
+
lifecycle: {
|
|
214
|
+
...base,
|
|
215
|
+
type: "issue.closed",
|
|
216
|
+
labels: { state: "closed" },
|
|
217
|
+
reply: `closed by ${sender?.login ?? "someone"}`,
|
|
218
|
+
createRoot: false
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
if (action === "reopened") return {
|
|
222
|
+
number: issue.number,
|
|
223
|
+
user: sender ?? {},
|
|
224
|
+
lifecycle: {
|
|
225
|
+
...base,
|
|
226
|
+
type: "issue.reopened",
|
|
227
|
+
labels: {
|
|
228
|
+
...label,
|
|
229
|
+
state: "open"
|
|
230
|
+
},
|
|
231
|
+
reply: `reopened by ${sender?.login ?? "someone"}`,
|
|
232
|
+
createRoot: true
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const GitHubSubject = z.object({
|
|
239
|
+
type: z.enum(["pull_request", "issue"]),
|
|
240
|
+
repo: z.string(),
|
|
241
|
+
number: z.number().int().positive()
|
|
242
|
+
});
|
|
243
|
+
const NativeItem = z.looseObject({
|
|
244
|
+
number: z.number(),
|
|
245
|
+
title: z.string(),
|
|
246
|
+
html_url: z.string(),
|
|
247
|
+
body: z.string().nullable().optional(),
|
|
248
|
+
draft: z.boolean().optional(),
|
|
249
|
+
state: z.string().optional(),
|
|
250
|
+
base: z.looseObject({ ref: z.string() }).optional(),
|
|
251
|
+
head: z.looseObject({ ref: z.string() }).optional()
|
|
252
|
+
});
|
|
253
|
+
const GitHubPayload = z.looseObject({
|
|
254
|
+
pull_request: NativeItem.optional(),
|
|
255
|
+
issue: NativeItem.optional(),
|
|
256
|
+
action: z.string().optional()
|
|
257
|
+
});
|
|
258
|
+
const prTypes = [
|
|
259
|
+
"pr.opened",
|
|
260
|
+
"pr.ready",
|
|
261
|
+
"pr.draft",
|
|
262
|
+
"pr.reopened",
|
|
263
|
+
"pr.closed",
|
|
264
|
+
"pr.labeled",
|
|
265
|
+
"pr.unlabeled",
|
|
266
|
+
"pr.assigned",
|
|
267
|
+
"pr.unassigned",
|
|
268
|
+
"review.submitted",
|
|
269
|
+
"review.dismissed"
|
|
270
|
+
];
|
|
271
|
+
const issueTypes = [
|
|
272
|
+
"issue.opened",
|
|
273
|
+
"issue.closed",
|
|
274
|
+
"issue.reopened",
|
|
275
|
+
"issue.labeled",
|
|
276
|
+
"issue.unlabeled",
|
|
277
|
+
"issue.assigned",
|
|
278
|
+
"issue.unassigned"
|
|
279
|
+
];
|
|
280
|
+
const [messageEvent, editedEvent, deletedEvent, reactionEvent, lifecycleEvent] = eventSchema("github", "comment").options;
|
|
281
|
+
const githubFields = {
|
|
282
|
+
subject: GitHubSubject,
|
|
283
|
+
raw: GitHubPayload
|
|
284
|
+
};
|
|
285
|
+
const prLifecycle = (type) => lifecycleEvent.extend({
|
|
286
|
+
type: z.literal(type),
|
|
287
|
+
subject: GitHubSubject.extend({ type: z.literal("pull_request") }),
|
|
288
|
+
raw: GitHubPayload.extend({ pull_request: NativeItem })
|
|
289
|
+
});
|
|
290
|
+
const issueLifecycle = (type) => lifecycleEvent.extend({
|
|
291
|
+
type: z.literal(type),
|
|
292
|
+
subject: GitHubSubject.extend({ type: z.literal("issue") }),
|
|
293
|
+
raw: GitHubPayload.extend({ issue: NativeItem })
|
|
294
|
+
});
|
|
295
|
+
const GitHubEvent = z.discriminatedUnion("type", [
|
|
296
|
+
messageEvent.extend(githubFields),
|
|
297
|
+
editedEvent.extend(githubFields),
|
|
298
|
+
deletedEvent.extend(githubFields),
|
|
299
|
+
reactionEvent.extend(githubFields),
|
|
300
|
+
...prTypes.map(prLifecycle),
|
|
301
|
+
...issueTypes.map(issueLifecycle)
|
|
302
|
+
]);
|
|
303
|
+
/** `owner/repo:12`, `owner/repo:issue:3`, or `owner/repo:12:rc:345` (a review comment thread). */
|
|
304
|
+
function parseThread(thread) {
|
|
305
|
+
const match = /^([^/]+)\/([^:]+):(?:issue:)?(\d+)(?::rc:(\d+))?$/.exec(thread);
|
|
306
|
+
if (!match) throw new Error(`invalid GitHub thread: ${thread}`);
|
|
307
|
+
return {
|
|
308
|
+
owner: match[1],
|
|
309
|
+
repo: match[2],
|
|
310
|
+
number: Number(match[3]),
|
|
311
|
+
review: match[4] ? Number(match[4]) : null
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/** GitHub's REST API with a person's own token: Chat SDK's adapter only holds the app's. */
|
|
315
|
+
async function asPerson(token, method, path, body) {
|
|
316
|
+
const response = await fetch(`https://api.github.com${path}`, {
|
|
317
|
+
method,
|
|
318
|
+
headers: {
|
|
319
|
+
authorization: `Bearer ${token}`,
|
|
320
|
+
accept: "application/vnd.github+json",
|
|
321
|
+
"x-github-api-version": "2022-11-28",
|
|
322
|
+
...body === void 0 ? {} : { "content-type": "application/json" }
|
|
323
|
+
},
|
|
324
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
325
|
+
});
|
|
326
|
+
if (!response.ok) {
|
|
327
|
+
const detail = await response.text().catch(() => "");
|
|
328
|
+
throw Object.assign(/* @__PURE__ */ new Error(`GitHub ${method} ${path}: ${response.status} ${detail.slice(0, 200)}`), { data: { error: `github_${response.status}` } });
|
|
329
|
+
}
|
|
330
|
+
return response.status === 204 ? null : response.json();
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Syncs GitHub repos with board channels. Keys are repos (`'owner/repo'`);
|
|
334
|
+
* values are a board channel (PRs and issues become threads there), a
|
|
335
|
+
* function, or `{ pulls, issues }` to send each somewhere else. Roots never
|
|
336
|
+
* flow into GitHub: a board thread doesn't open an issue.
|
|
337
|
+
*/
|
|
338
|
+
function github(options) {
|
|
339
|
+
return new GitHubProvider(options);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* A connector's GitHub adapter, finding its own bot user ID. Chat SDK skips detection
|
|
343
|
+
* with Connect's installation token, since REST's `/user` refuses it, and only learns
|
|
344
|
+
* the ID from the first comment it posts, per instance. GraphQL's `viewer` answers with
|
|
345
|
+
* the app's bot user for the same token.
|
|
346
|
+
*/
|
|
347
|
+
function fromConnector(connector, botUserId) {
|
|
348
|
+
const adapter = createGitHubAdapter({
|
|
349
|
+
...connectGitHubAdapter(connector),
|
|
350
|
+
...botUserId !== void 0 ? { botUserId } : {}
|
|
351
|
+
});
|
|
352
|
+
const detecting = adapter;
|
|
353
|
+
detecting.detectBotUserId = async (octokit) => {
|
|
354
|
+
if (detecting._botUserId !== null || !octokit.graphql) return;
|
|
355
|
+
try {
|
|
356
|
+
const { viewer } = await octokit.graphql("query { viewer { databaseId login } }");
|
|
357
|
+
if (typeof viewer?.databaseId !== "number") return;
|
|
358
|
+
detecting._botUserId = viewer.databaseId;
|
|
359
|
+
detecting.logger?.info("GitHub bot user ID detected from the GraphQL viewer", {
|
|
360
|
+
botUserId: viewer.databaseId,
|
|
361
|
+
login: viewer.login
|
|
362
|
+
});
|
|
363
|
+
} catch (error) {
|
|
364
|
+
detecting.logger?.warn("Could not detect the GitHub bot user ID", { error });
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
return withUserTokens(adapter, connectUserTokens(connector));
|
|
368
|
+
}
|
|
369
|
+
var GitHubProvider = class extends ChatSyncProvider {
|
|
370
|
+
body;
|
|
371
|
+
constructor(given) {
|
|
372
|
+
const options = {
|
|
373
|
+
...given,
|
|
374
|
+
adapter: typeof given.adapter === "string" ? fromConnector(given.adapter, given.botUserId) : given.adapter
|
|
375
|
+
};
|
|
376
|
+
super({
|
|
377
|
+
render: options.render,
|
|
378
|
+
platform: "github",
|
|
379
|
+
adapter: options.adapter,
|
|
380
|
+
canStartThreads: false,
|
|
381
|
+
mention: (actor) => actor.id.startsWith("github:user:") ? `@${actor.handle}` : null,
|
|
382
|
+
resolveChannel: (key) => Promise.resolve(key),
|
|
383
|
+
groupThread: (thread) => thread.replace(/:rc:[^:]+$/, ""),
|
|
384
|
+
channelOf,
|
|
385
|
+
isRoot: () => false,
|
|
386
|
+
async fetchRoot(thread) {
|
|
387
|
+
const match = /^([^/]+)\/([^:]+):(issue:)?(\d+)$/.exec(thread);
|
|
388
|
+
if (!match) throw new Error(`invalid GitHub thread: ${thread}`);
|
|
389
|
+
const [, owner, repo, issue, number] = match;
|
|
390
|
+
const client = options.adapter.octokit.rest;
|
|
391
|
+
const { data } = issue ? await client.issues.get({
|
|
392
|
+
owner,
|
|
393
|
+
repo,
|
|
394
|
+
issue_number: Number(number)
|
|
395
|
+
}) : await client.pulls.get({
|
|
396
|
+
owner,
|
|
397
|
+
repo,
|
|
398
|
+
pull_number: Number(number)
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
text: `${issue ? "Issue" : "PR"} #${data.number}: ${data.title}\n${data.html_url}`,
|
|
402
|
+
raw: issue ? { issue: data } : { pull_request: data },
|
|
403
|
+
author: author(data.user),
|
|
404
|
+
labels: {
|
|
405
|
+
...tagsOf(data),
|
|
406
|
+
[issue ? "issue" : "pr"]: `${owner}/${repo}#${data.number}`,
|
|
407
|
+
state: issue ? data.state : data.merged ? "merged" : data.state === "closed" ? "closed" : data.draft ? "draft" : "open"
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
},
|
|
411
|
+
markdown(message) {
|
|
412
|
+
const raw = message.raw;
|
|
413
|
+
const body = raw?.comment?.body ?? raw?.body;
|
|
414
|
+
return typeof body === "string" && body.trim() ? body.trim() : markdownOf(message);
|
|
415
|
+
},
|
|
416
|
+
async avatar(message) {
|
|
417
|
+
const raw = message.raw;
|
|
418
|
+
const url = raw?.comment?.user?.avatar_url ?? raw?.user?.avatar_url;
|
|
419
|
+
return typeof url === "string" ? url : null;
|
|
420
|
+
},
|
|
421
|
+
context(message) {
|
|
422
|
+
const raw = message.raw;
|
|
423
|
+
const path = raw?.path ?? raw?.comment?.path;
|
|
424
|
+
const line = raw?.line ?? raw?.comment?.line;
|
|
425
|
+
return path ? line ? `${path}:${line}` : path : null;
|
|
426
|
+
},
|
|
427
|
+
missed(headers, body) {
|
|
428
|
+
const name = headers.get("x-github-event") ?? "";
|
|
429
|
+
if (name !== "issue_comment" && name !== "pull_request_review_comment") return [];
|
|
430
|
+
let payload;
|
|
431
|
+
try {
|
|
432
|
+
payload = JSON.parse(body);
|
|
433
|
+
} catch {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
if (payload.action !== "edited" && payload.action !== "deleted" || !payload.comment?.id || !payload.repository) return [];
|
|
437
|
+
const { comment, repository } = payload;
|
|
438
|
+
const adapter = options.adapter;
|
|
439
|
+
const review = name === "pull_request_review_comment";
|
|
440
|
+
const number = review ? payload.pull_request?.number : payload.issue?.number;
|
|
441
|
+
if (!number) return [];
|
|
442
|
+
const type = review ? void 0 : payload.issue?.pull_request ? "pr" : "issue";
|
|
443
|
+
const threadId = adapter.encodeThreadId({
|
|
444
|
+
owner: repository.owner.login,
|
|
445
|
+
repo: repository.name,
|
|
446
|
+
prNumber: number,
|
|
447
|
+
...review ? { reviewCommentId: comment.in_reply_to_id ?? comment.id } : { type }
|
|
448
|
+
});
|
|
449
|
+
const message = review ? adapter.parseReviewComment(comment, repository, number, threadId) : adapter.parseIssueComment(comment, repository, number, threadId, type);
|
|
450
|
+
if (message.author.isMe) return [];
|
|
451
|
+
return payload.action === "edited" ? [{
|
|
452
|
+
kind: "edited",
|
|
453
|
+
message
|
|
454
|
+
}] : [{
|
|
455
|
+
kind: "deleted",
|
|
456
|
+
thread: threadId,
|
|
457
|
+
message: message.id,
|
|
458
|
+
raw: payload
|
|
459
|
+
}];
|
|
460
|
+
},
|
|
461
|
+
intercept(key, headers, body) {
|
|
462
|
+
const name = headers.get("x-github-event") ?? "";
|
|
463
|
+
if (![
|
|
464
|
+
"pull_request",
|
|
465
|
+
"pull_request_review",
|
|
466
|
+
"issues"
|
|
467
|
+
].includes(name)) return [];
|
|
468
|
+
let parsed;
|
|
469
|
+
try {
|
|
470
|
+
parsed = JSON.parse(body);
|
|
471
|
+
} catch {
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
const found = lifecycle(name, parsed);
|
|
475
|
+
if (!found) return [];
|
|
476
|
+
const item = parsed.pull_request ?? parsed.issue;
|
|
477
|
+
if (item) Object.assign(found.lifecycle, {
|
|
478
|
+
labels: {
|
|
479
|
+
...tagsOf(item),
|
|
480
|
+
...found.lifecycle.labels
|
|
481
|
+
},
|
|
482
|
+
owns: ownedBy(item)
|
|
483
|
+
});
|
|
484
|
+
const repo = parsed.repository.full_name;
|
|
485
|
+
const thread = name === "issues" ? `${key}:${repo}:issue:${found.number}` : `${key}:${repo}:${found.number}`;
|
|
486
|
+
const delivery = headers.get("x-github-delivery") ?? `${name}:${found.lifecycle.type}:${found.number}`;
|
|
487
|
+
return [{
|
|
488
|
+
event: {
|
|
489
|
+
kind: "lifecycle",
|
|
490
|
+
channel: `${key}:${repo}:${name === "issues" ? "issues" : "pulls"}`,
|
|
491
|
+
thread,
|
|
492
|
+
author: author(found.user),
|
|
493
|
+
...found.lifecycle
|
|
494
|
+
},
|
|
495
|
+
raw: parsed,
|
|
496
|
+
dedupe: `lifecycle:${delivery}`
|
|
497
|
+
}];
|
|
498
|
+
},
|
|
499
|
+
async resolveTarget(target) {
|
|
500
|
+
if (!/^[^/]+\/[^:]+(?::(?:issue:)?\d+(?::rc:[^:]+)?)?$/.test(target)) throw new Error(`invalid GitHub target: ${target}`);
|
|
501
|
+
const at = target.indexOf(":");
|
|
502
|
+
return {
|
|
503
|
+
target,
|
|
504
|
+
channel: at === -1 ? target : channelOf(target),
|
|
505
|
+
kind: at === -1 ? "channel" : "thread"
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
const converter = options.adapter.formatConverter;
|
|
510
|
+
this.body = (post) => {
|
|
511
|
+
try {
|
|
512
|
+
return converter ? converter.renderPostable({ markdown: post.markdown }) : post.markdown;
|
|
513
|
+
} catch {
|
|
514
|
+
return post.markdown;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
for (const [repo, value] of Object.entries(options.repos)) {
|
|
518
|
+
const { pulls, issues } = typeof value === "object" && !("run" in value) ? value : {
|
|
519
|
+
pulls: value,
|
|
520
|
+
issues: value
|
|
521
|
+
};
|
|
522
|
+
if (pulls) this.map(`${repo}:pulls`, pulls);
|
|
523
|
+
if (issues) this.map(`${repo}:issues`, issues);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async whoAmI(token) {
|
|
527
|
+
const user = await asPerson(token, "GET", "/user");
|
|
528
|
+
if (!user.id) throw new Error("GitHub didn't say whose token this is");
|
|
529
|
+
return {
|
|
530
|
+
...author(user),
|
|
531
|
+
isBot: false
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
async postAs(token, target, kind, post) {
|
|
535
|
+
if (kind === "channel") throw new Error("github cannot start threads");
|
|
536
|
+
const { owner, repo, number, review } = parseThread(target);
|
|
537
|
+
const body = this.body(post);
|
|
538
|
+
const comment = await (review !== null ? asPerson(token, "POST", `/repos/${owner}/${repo}/pulls/${number}/comments/${review}/replies`, { body }) : asPerson(token, "POST", `/repos/${owner}/${repo}/issues/${number}/comments`, { body }));
|
|
539
|
+
return {
|
|
540
|
+
thread: target,
|
|
541
|
+
message: String(comment.id)
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
async editAs(token, thread, message, post) {
|
|
545
|
+
const { owner, repo, review } = parseThread(thread);
|
|
546
|
+
await asPerson(token, "PATCH", `/repos/${owner}/${repo}/${review !== null ? "pulls" : "issues"}/comments/${message}`, { body: this.body(post) });
|
|
547
|
+
}
|
|
548
|
+
async deleteAs(token, thread, message) {
|
|
549
|
+
const { owner, repo, review } = parseThread(thread);
|
|
550
|
+
await asPerson(token, "DELETE", `/repos/${owner}/${repo}/${review !== null ? "pulls" : "issues"}/comments/${message}`);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* A thread's root is the PR or issue itself, not a comment: its copy has no comment ID
|
|
554
|
+
* (`root:<event>` or `lifecycle:<event>`), so its reactions go on the PR or issue.
|
|
555
|
+
*/
|
|
556
|
+
async react(thread, message, emoji, added) {
|
|
557
|
+
if (/^\d+$/.test(message)) return super.react(thread, message, emoji, added);
|
|
558
|
+
const match = /^([^/]+)\/([^:]+):(?:issue:)?(\d+)$/.exec(thread);
|
|
559
|
+
if (!match) throw new Error(`invalid GitHub thread: ${thread}`);
|
|
560
|
+
const adapter = await this.ready();
|
|
561
|
+
const content = adapter.emojiToGitHubReaction?.(emoji);
|
|
562
|
+
if (!content || content === "+1" && emoji !== "thumbs_up" && emoji !== "+1") return;
|
|
563
|
+
const [, owner, repo, number] = match;
|
|
564
|
+
const reactions = (await adapter.getOctokitForThread?.(owner, repo) ?? adapter.octokit).rest.reactions;
|
|
565
|
+
if (!reactions) throw new Error("GitHub client has no reactions API");
|
|
566
|
+
const where = {
|
|
567
|
+
owner,
|
|
568
|
+
repo,
|
|
569
|
+
issue_number: Number(number)
|
|
570
|
+
};
|
|
571
|
+
if (added) return void await reactions.createForIssue({
|
|
572
|
+
...where,
|
|
573
|
+
content
|
|
574
|
+
});
|
|
575
|
+
if (adapter.botUserId === void 0) return;
|
|
576
|
+
const { data } = await reactions.listForIssue({
|
|
577
|
+
...where,
|
|
578
|
+
content,
|
|
579
|
+
per_page: 100
|
|
580
|
+
});
|
|
581
|
+
const own = data.find((reaction) => reaction.content === content && String(reaction.user?.id) === adapter.botUserId);
|
|
582
|
+
if (own) await reactions.deleteForIssue({
|
|
583
|
+
...where,
|
|
584
|
+
reaction_id: own.id
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
event(context) {
|
|
588
|
+
const match = /^([^:]+):(issue:)?(\d+)/.exec(context.event.thread.slice(context.key.length + 1));
|
|
589
|
+
if (!match) throw new Error(`invalid GitHub thread: ${context.event.thread}`);
|
|
590
|
+
const base = envelope(this.platform, context.key, context.event, context.raw ?? {}, context.id);
|
|
591
|
+
return GitHubEvent.parse({
|
|
592
|
+
...base,
|
|
593
|
+
type: context.event.kind === "message" ? "comment" : base.type,
|
|
594
|
+
subject: {
|
|
595
|
+
type: match[2] ? "issue" : "pull_request",
|
|
596
|
+
repo: match[1],
|
|
597
|
+
number: Number(match[3])
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
//#endregion
|
|
603
|
+
export { GitHubEvent, GitHubPayload, GitHubProvider, github, withUserTokens };
|