@flink-app/github-app-plugin 0.12.1-alpha.38
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/CHANGELOG.md +209 -0
- package/LICENSE +21 -0
- package/README.md +667 -0
- package/SECURITY.md +498 -0
- package/dist/GitHubAppInternalContext.d.ts +44 -0
- package/dist/GitHubAppInternalContext.js +2 -0
- package/dist/GitHubAppPlugin.d.ts +45 -0
- package/dist/GitHubAppPlugin.js +367 -0
- package/dist/GitHubAppPluginContext.d.ts +242 -0
- package/dist/GitHubAppPluginContext.js +2 -0
- package/dist/GitHubAppPluginOptions.d.ts +369 -0
- package/dist/GitHubAppPluginOptions.js +2 -0
- package/dist/handlers/InitiateInstallation.d.ts +32 -0
- package/dist/handlers/InitiateInstallation.js +66 -0
- package/dist/handlers/InstallationCallback.d.ts +42 -0
- package/dist/handlers/InstallationCallback.js +248 -0
- package/dist/handlers/UninstallHandler.d.ts +37 -0
- package/dist/handlers/UninstallHandler.js +153 -0
- package/dist/handlers/WebhookHandler.d.ts +54 -0
- package/dist/handlers/WebhookHandler.js +157 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +23 -0
- package/dist/repos/GitHubAppSessionRepo.d.ts +24 -0
- package/dist/repos/GitHubAppSessionRepo.js +32 -0
- package/dist/repos/GitHubInstallationRepo.d.ts +53 -0
- package/dist/repos/GitHubInstallationRepo.js +83 -0
- package/dist/repos/GitHubWebhookEventRepo.d.ts +29 -0
- package/dist/repos/GitHubWebhookEventRepo.js +42 -0
- package/dist/schemas/GitHubAppSession.d.ts +13 -0
- package/dist/schemas/GitHubAppSession.js +2 -0
- package/dist/schemas/GitHubInstallation.d.ts +28 -0
- package/dist/schemas/GitHubInstallation.js +2 -0
- package/dist/schemas/InstallationCallbackRequest.d.ts +10 -0
- package/dist/schemas/InstallationCallbackRequest.js +2 -0
- package/dist/schemas/WebhookEvent.d.ts +16 -0
- package/dist/schemas/WebhookEvent.js +2 -0
- package/dist/schemas/WebhookPayload.d.ts +35 -0
- package/dist/schemas/WebhookPayload.js +2 -0
- package/dist/services/GitHubAPIClient.d.ts +143 -0
- package/dist/services/GitHubAPIClient.js +167 -0
- package/dist/services/GitHubAuthService.d.ts +85 -0
- package/dist/services/GitHubAuthService.js +160 -0
- package/dist/services/WebhookValidator.d.ts +93 -0
- package/dist/services/WebhookValidator.js +123 -0
- package/dist/utils/error-utils.d.ts +67 -0
- package/dist/utils/error-utils.js +121 -0
- package/dist/utils/jwt-utils.d.ts +35 -0
- package/dist/utils/jwt-utils.js +67 -0
- package/dist/utils/state-utils.d.ts +38 -0
- package/dist/utils/state-utils.js +74 -0
- package/dist/utils/token-cache-utils.d.ts +47 -0
- package/dist/utils/token-cache-utils.js +74 -0
- package/dist/utils/webhook-signature-utils.d.ts +22 -0
- package/dist/utils/webhook-signature-utils.js +57 -0
- package/examples/basic-installation.ts +246 -0
- package/examples/create-issue.ts +392 -0
- package/examples/error-handling.ts +396 -0
- package/examples/multi-event-webhook.ts +367 -0
- package/examples/organization-installation.ts +316 -0
- package/examples/repository-access.ts +480 -0
- package/examples/webhook-handling.ts +343 -0
- package/examples/with-jwt-auth.ts +319 -0
- package/package.json +41 -0
- package/spec/core-utilities.spec.ts +243 -0
- package/spec/handlers.spec.ts +216 -0
- package/spec/helpers/reporter.ts +41 -0
- package/spec/integration-and-security.spec.ts +483 -0
- package/spec/plugin-core.spec.ts +258 -0
- package/spec/project-setup.spec.ts +56 -0
- package/spec/repos-and-schemas.spec.ts +288 -0
- package/spec/services.spec.ts +108 -0
- package/spec/support/jasmine.json +7 -0
- package/src/GitHubAppPlugin.ts +411 -0
- package/src/GitHubAppPluginContext.ts +254 -0
- package/src/GitHubAppPluginOptions.ts +412 -0
- package/src/handlers/InstallationCallback.ts +292 -0
- package/src/handlers/WebhookHandler.ts +179 -0
- package/src/index.ts +29 -0
- package/src/repos/GitHubAppSessionRepo.ts +36 -0
- package/src/repos/GitHubInstallationRepo.ts +95 -0
- package/src/repos/GitHubWebhookEventRepo.ts +48 -0
- package/src/schemas/GitHubAppSession.ts +13 -0
- package/src/schemas/GitHubInstallation.ts +28 -0
- package/src/schemas/InstallationCallbackRequest.ts +10 -0
- package/src/schemas/WebhookEvent.ts +16 -0
- package/src/schemas/WebhookPayload.ts +35 -0
- package/src/services/GitHubAPIClient.ts +244 -0
- package/src/services/GitHubAuthService.ts +188 -0
- package/src/services/WebhookValidator.ts +159 -0
- package/src/utils/error-utils.ts +148 -0
- package/src/utils/jwt-utils.ts +64 -0
- package/src/utils/state-utils.ts +72 -0
- package/src/utils/token-cache-utils.ts +89 -0
- package/src/utils/webhook-signature-utils.ts +57 -0
- package/tsconfig.dist.json +4 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Event Webhook Handling Example
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* - Handling multiple webhook event types
|
|
6
|
+
* - Event filtering and routing
|
|
7
|
+
* - Webhook event logging
|
|
8
|
+
* - Async webhook processing
|
|
9
|
+
* - Event-driven workflows
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { FlinkApp, FlinkContext } from "@flink-app/flink";
|
|
13
|
+
import { githubAppPlugin } from "@flink-app/github-app-plugin";
|
|
14
|
+
|
|
15
|
+
interface AppContext extends FlinkContext {
|
|
16
|
+
plugins: {
|
|
17
|
+
githubApp: any;
|
|
18
|
+
};
|
|
19
|
+
repos: {
|
|
20
|
+
githubWebhookEventRepo: any;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Event processor interface
|
|
25
|
+
interface EventProcessor {
|
|
26
|
+
event: string;
|
|
27
|
+
actions?: string[];
|
|
28
|
+
process: (payload: any, client: any, ctx: AppContext) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Event processors registry
|
|
32
|
+
const eventProcessors: EventProcessor[] = [
|
|
33
|
+
{
|
|
34
|
+
event: "push",
|
|
35
|
+
process: async (payload, client, ctx) => {
|
|
36
|
+
const repo = payload.repository;
|
|
37
|
+
const branch = payload.ref.replace("refs/heads/", "");
|
|
38
|
+
const commits = payload.commits || [];
|
|
39
|
+
|
|
40
|
+
console.log(`\n[PUSH] ${repo.full_name}:${branch} - ${commits.length} commits`);
|
|
41
|
+
|
|
42
|
+
// Auto-create issue for commits with [bug] tag
|
|
43
|
+
for (const commit of commits) {
|
|
44
|
+
if (commit.message.toLowerCase().includes("[bug]")) {
|
|
45
|
+
await client.createIssue(repo.owner.login, repo.name, {
|
|
46
|
+
title: `Bug fix needed: ${commit.message.split("\n")[0]}`,
|
|
47
|
+
body: `Commit ${commit.id.substring(0, 7)} contains a [bug] tag.\n\nMessage: ${commit.message}`,
|
|
48
|
+
});
|
|
49
|
+
console.log(` Created issue for bug commit`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
event: "pull_request",
|
|
56
|
+
actions: ["opened", "closed", "reopened", "synchronize"],
|
|
57
|
+
process: async (payload, client, ctx) => {
|
|
58
|
+
const pr = payload.pull_request;
|
|
59
|
+
const action = payload.action;
|
|
60
|
+
const repo = payload.repository;
|
|
61
|
+
|
|
62
|
+
console.log(`\n[PR] ${repo.full_name}#${pr.number} - ${action}`);
|
|
63
|
+
|
|
64
|
+
if (action === "opened") {
|
|
65
|
+
// Welcome new contributors
|
|
66
|
+
await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
|
|
67
|
+
body: `Thanks for your pull request, @${pr.user.login}! We'll review it soon.`,
|
|
68
|
+
});
|
|
69
|
+
console.log(` Added welcome comment`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (action === "closed" && pr.merged) {
|
|
73
|
+
// Thank for merged PR
|
|
74
|
+
await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
|
|
75
|
+
body: `Thanks for your contribution, @${pr.user.login}! Your changes have been merged.`,
|
|
76
|
+
});
|
|
77
|
+
console.log(` Added thank you comment`);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
event: "issues",
|
|
83
|
+
actions: ["opened", "edited", "closed"],
|
|
84
|
+
process: async (payload, client, ctx) => {
|
|
85
|
+
const issue = payload.issue;
|
|
86
|
+
const action = payload.action;
|
|
87
|
+
const repo = payload.repository;
|
|
88
|
+
|
|
89
|
+
console.log(`\n[ISSUE] ${repo.full_name}#${issue.number} - ${action}`);
|
|
90
|
+
|
|
91
|
+
if (action === "opened") {
|
|
92
|
+
// Auto-label based on title
|
|
93
|
+
const labels: string[] = [];
|
|
94
|
+
|
|
95
|
+
if (issue.title.toLowerCase().includes("bug")) labels.push("bug");
|
|
96
|
+
if (issue.title.toLowerCase().includes("feature")) labels.push("enhancement");
|
|
97
|
+
if (issue.title.toLowerCase().includes("question")) labels.push("question");
|
|
98
|
+
if (issue.title.toLowerCase().includes("urgent")) labels.push("priority: high");
|
|
99
|
+
|
|
100
|
+
if (labels.length > 0) {
|
|
101
|
+
await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${issue.number}/labels`, {
|
|
102
|
+
labels,
|
|
103
|
+
});
|
|
104
|
+
console.log(` Added labels: ${labels.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
event: "issue_comment",
|
|
111
|
+
actions: ["created"],
|
|
112
|
+
process: async (payload, client, ctx) => {
|
|
113
|
+
const comment = payload.comment;
|
|
114
|
+
const issue = payload.issue;
|
|
115
|
+
const repo = payload.repository;
|
|
116
|
+
|
|
117
|
+
console.log(`\n[COMMENT] ${repo.full_name}#${issue.number}`);
|
|
118
|
+
|
|
119
|
+
// Check for commands in comments (e.g., "/label bug")
|
|
120
|
+
const commandMatch = comment.body.match(/^\/(\w+)\s*(.*)$/m);
|
|
121
|
+
|
|
122
|
+
if (commandMatch) {
|
|
123
|
+
const [, command, args] = commandMatch;
|
|
124
|
+
|
|
125
|
+
console.log(` Command detected: /${command} ${args}`);
|
|
126
|
+
|
|
127
|
+
switch (command) {
|
|
128
|
+
case "label":
|
|
129
|
+
const labels = args.split(/\s+/).filter(Boolean);
|
|
130
|
+
if (labels.length > 0) {
|
|
131
|
+
await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${issue.number}/labels`, {
|
|
132
|
+
labels,
|
|
133
|
+
});
|
|
134
|
+
console.log(` Added labels: ${labels.join(", ")}`);
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case "close":
|
|
139
|
+
await client.request("PATCH", `/repos/${repo.owner.login}/${repo.name}/issues/${issue.number}`, {
|
|
140
|
+
state: "closed",
|
|
141
|
+
});
|
|
142
|
+
console.log(` Closed issue`);
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case "assign":
|
|
146
|
+
const assignees = args.split(/\s+/).filter(Boolean);
|
|
147
|
+
if (assignees.length > 0) {
|
|
148
|
+
await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${issue.number}/assignees`, {
|
|
149
|
+
assignees,
|
|
150
|
+
});
|
|
151
|
+
console.log(` Assigned: ${assignees.join(", ")}`);
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
event: "pull_request_review",
|
|
160
|
+
actions: ["submitted"],
|
|
161
|
+
process: async (payload, client, ctx) => {
|
|
162
|
+
const review = payload.review;
|
|
163
|
+
const pr = payload.pull_request;
|
|
164
|
+
const repo = payload.repository;
|
|
165
|
+
|
|
166
|
+
console.log(`\n[PR REVIEW] ${repo.full_name}#${pr.number} - ${review.state}`);
|
|
167
|
+
|
|
168
|
+
if (review.state === "approved") {
|
|
169
|
+
// Thank reviewer
|
|
170
|
+
await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
|
|
171
|
+
body: `Thanks for the review, @${review.user.login}!`,
|
|
172
|
+
});
|
|
173
|
+
console.log(` Thanked reviewer`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (review.state === "changes_requested") {
|
|
177
|
+
// Notify author
|
|
178
|
+
await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
|
|
179
|
+
body: `@${pr.user.login}, please address the requested changes.`,
|
|
180
|
+
});
|
|
181
|
+
console.log(` Notified author of requested changes`);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
event: "release",
|
|
187
|
+
actions: ["published"],
|
|
188
|
+
process: async (payload, client, ctx) => {
|
|
189
|
+
const release = payload.release;
|
|
190
|
+
const repo = payload.repository;
|
|
191
|
+
|
|
192
|
+
console.log(`\n[RELEASE] ${repo.full_name} - ${release.tag_name}`);
|
|
193
|
+
|
|
194
|
+
// Create announcement issue
|
|
195
|
+
await client.createIssue(repo.owner.login, repo.name, {
|
|
196
|
+
title: `Release ${release.tag_name} is now available`,
|
|
197
|
+
body: `A new release has been published!\n\n**${release.name}**\n\n${release.body}\n\n[View Release](${release.html_url})`,
|
|
198
|
+
});
|
|
199
|
+
console.log(` Created announcement issue`);
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
event: "installation",
|
|
204
|
+
actions: ["created", "deleted", "suspend", "unsuspend"],
|
|
205
|
+
process: async (payload, client, ctx) => {
|
|
206
|
+
const installation = payload.installation;
|
|
207
|
+
const action = payload.action;
|
|
208
|
+
|
|
209
|
+
console.log(`\n[INSTALLATION] ${installation.account.login} - ${action}`);
|
|
210
|
+
|
|
211
|
+
if (action === "deleted") {
|
|
212
|
+
// Clean up installation data
|
|
213
|
+
const installations = await ctx.repos.githubInstallationRepo.findByInstallationId(installation.id);
|
|
214
|
+
|
|
215
|
+
for (const inst of installations) {
|
|
216
|
+
await ctx.plugins.githubApp.deleteInstallation(inst.userId, installation.id);
|
|
217
|
+
console.log(` Cleaned up installation for user ${inst.userId}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
event: "installation_repositories",
|
|
224
|
+
actions: ["added", "removed"],
|
|
225
|
+
process: async (payload, client, ctx) => {
|
|
226
|
+
const installation = payload.installation;
|
|
227
|
+
const added = payload.repositories_added || [];
|
|
228
|
+
const removed = payload.repositories_removed || [];
|
|
229
|
+
|
|
230
|
+
console.log(`\n[INSTALLATION REPOS] ${installation.account.login}`);
|
|
231
|
+
|
|
232
|
+
if (added.length > 0) {
|
|
233
|
+
console.log(` Added: ${added.map((r: any) => r.full_name).join(", ")}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (removed.length > 0) {
|
|
237
|
+
console.log(` Removed: ${removed.map((r: any) => r.full_name).join(", ")}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Update installation in database
|
|
241
|
+
// (Plugin handles this automatically, but you can add custom logic)
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
event: "check_suite",
|
|
246
|
+
actions: ["completed"],
|
|
247
|
+
process: async (payload, client, ctx) => {
|
|
248
|
+
const checkSuite = payload.check_suite;
|
|
249
|
+
const repo = payload.repository;
|
|
250
|
+
|
|
251
|
+
console.log(`\n[CHECK SUITE] ${repo.full_name} - ${checkSuite.conclusion}`);
|
|
252
|
+
|
|
253
|
+
if (checkSuite.conclusion === "failure") {
|
|
254
|
+
// Create issue for failed checks
|
|
255
|
+
const prs = checkSuite.pull_requests || [];
|
|
256
|
+
if (prs.length > 0) {
|
|
257
|
+
const pr = prs[0];
|
|
258
|
+
await client.request("POST", `/repos/${repo.owner.login}/${repo.name}/issues/${pr.number}/comments`, {
|
|
259
|
+
body: `Check suite failed. Please review the errors and fix them.`,
|
|
260
|
+
});
|
|
261
|
+
console.log(` Notified PR author of failed checks`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
async function start() {
|
|
269
|
+
const app = new FlinkApp<AppContext>({
|
|
270
|
+
name: "GitHub App Multi-Event Webhook Example",
|
|
271
|
+
port: 3333,
|
|
272
|
+
|
|
273
|
+
db: {
|
|
274
|
+
uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-multi-webhook",
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
plugins: [
|
|
278
|
+
githubAppPlugin({
|
|
279
|
+
appId: process.env.GITHUB_APP_ID!,
|
|
280
|
+
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
|
|
281
|
+
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
282
|
+
clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
283
|
+
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
284
|
+
|
|
285
|
+
onInstallationSuccess: async ({ installationId, account }) => {
|
|
286
|
+
console.log(`Installation: ${installationId} on ${account.login}`);
|
|
287
|
+
return {
|
|
288
|
+
userId: "demo-user",
|
|
289
|
+
redirectUrl: "/dashboard",
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
// Multi-event webhook handler
|
|
294
|
+
onWebhookEvent: async ({ event, action, payload, installationId, deliveryId }, ctx) => {
|
|
295
|
+
console.log(`\n========================================`);
|
|
296
|
+
console.log(`Webhook Delivery: ${deliveryId}`);
|
|
297
|
+
console.log(`Event: ${event}${action ? ` (${action})` : ""}`);
|
|
298
|
+
console.log(`Installation: ${installationId}`);
|
|
299
|
+
console.log(`========================================`);
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
// Find matching processor
|
|
303
|
+
const processor = eventProcessors.find((p) => {
|
|
304
|
+
if (p.event !== event) return false;
|
|
305
|
+
if (p.actions && action && !p.actions.includes(action)) return false;
|
|
306
|
+
return true;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (processor) {
|
|
310
|
+
// Get API client
|
|
311
|
+
const client = await ctx.plugins.githubApp.getClient(installationId);
|
|
312
|
+
|
|
313
|
+
// Process event
|
|
314
|
+
await processor.process(payload, client, ctx);
|
|
315
|
+
|
|
316
|
+
console.log(`\n✅ Event processed successfully`);
|
|
317
|
+
} else {
|
|
318
|
+
console.log(`\n⚠️ No processor registered for ${event}${action ? ` (${action})` : ""}`);
|
|
319
|
+
}
|
|
320
|
+
} catch (error: any) {
|
|
321
|
+
console.error(`\n❌ Error processing webhook:`, error.message);
|
|
322
|
+
// Don't throw - return 200 to GitHub to prevent retries
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.log(`========================================\n`);
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
// Enable webhook event logging
|
|
329
|
+
logWebhookEvents: true,
|
|
330
|
+
}),
|
|
331
|
+
],
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await app.start();
|
|
335
|
+
|
|
336
|
+
console.log(`
|
|
337
|
+
=================================
|
|
338
|
+
Multi-Event Webhook Example
|
|
339
|
+
=================================
|
|
340
|
+
|
|
341
|
+
This example handles multiple webhook event types:
|
|
342
|
+
|
|
343
|
+
✓ push - Create issues for [bug] commits
|
|
344
|
+
✓ pull_request - Welcome contributors, thank for merges
|
|
345
|
+
✓ issues - Auto-label based on title
|
|
346
|
+
✓ issue_comment - Process commands (/label, /close, /assign)
|
|
347
|
+
✓ pull_request_review - Thank reviewers, notify authors
|
|
348
|
+
✓ release - Create announcement issues
|
|
349
|
+
✓ installation - Clean up on deletion
|
|
350
|
+
✓ installation_repositories - Track repo changes
|
|
351
|
+
✓ check_suite - Notify on failed checks
|
|
352
|
+
|
|
353
|
+
Webhook endpoint: /github-app/webhook
|
|
354
|
+
|
|
355
|
+
Configure in GitHub App settings:
|
|
356
|
+
- Webhook URL: https://your-domain.com/github-app/webhook
|
|
357
|
+
- Subscribe to events you want to handle
|
|
358
|
+
|
|
359
|
+
=================================
|
|
360
|
+
`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Start application
|
|
364
|
+
start().catch((error) => {
|
|
365
|
+
console.error("Failed to start application:", error);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
});
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organization-Level Installation Example
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* - Installing GitHub App at organization level
|
|
6
|
+
* - Handling organization installations vs user installations
|
|
7
|
+
* - Managing multiple installations per user (personal + orgs)
|
|
8
|
+
* - Switching between organization contexts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { FlinkApp, FlinkContext, GetHandler } from "@flink-app/flink";
|
|
12
|
+
import { githubAppPlugin } from "@flink-app/github-app-plugin";
|
|
13
|
+
|
|
14
|
+
interface AppContext extends FlinkContext {
|
|
15
|
+
plugins: {
|
|
16
|
+
githubApp: any;
|
|
17
|
+
};
|
|
18
|
+
session?: {
|
|
19
|
+
userId?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function start() {
|
|
24
|
+
const app = new FlinkApp<AppContext>({
|
|
25
|
+
name: "GitHub App Organization Example",
|
|
26
|
+
port: 3333,
|
|
27
|
+
|
|
28
|
+
db: {
|
|
29
|
+
uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-org-example",
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
plugins: [
|
|
33
|
+
githubAppPlugin({
|
|
34
|
+
appId: process.env.GITHUB_APP_ID!,
|
|
35
|
+
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
|
|
36
|
+
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
37
|
+
clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
38
|
+
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
39
|
+
|
|
40
|
+
onInstallationSuccess: async ({ installationId, account, repositories }, ctx) => {
|
|
41
|
+
const userId = ctx.session?.userId || "demo-user";
|
|
42
|
+
|
|
43
|
+
console.log(`\n=== Installation Success ===`);
|
|
44
|
+
console.log(`Installation ID: ${installationId}`);
|
|
45
|
+
console.log(`Account Type: ${account.type}`);
|
|
46
|
+
console.log(`Account: ${account.login}`);
|
|
47
|
+
|
|
48
|
+
if (account.type === "Organization") {
|
|
49
|
+
console.log(`\nOrganization installation detected!`);
|
|
50
|
+
console.log(`Repositories in org: ${repositories.length}`);
|
|
51
|
+
} else {
|
|
52
|
+
console.log(`\nPersonal account installation`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
userId,
|
|
57
|
+
redirectUrl: `/dashboard/installations?account=${account.login}`,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await app.start();
|
|
65
|
+
|
|
66
|
+
console.log(`
|
|
67
|
+
=================================
|
|
68
|
+
Organization Installation Example
|
|
69
|
+
=================================
|
|
70
|
+
|
|
71
|
+
Users can install the GitHub App on:
|
|
72
|
+
1. Their personal account
|
|
73
|
+
2. Organizations they belong to
|
|
74
|
+
|
|
75
|
+
Install on personal account:
|
|
76
|
+
http://localhost:3333/github-app/install?user_id=demo-user
|
|
77
|
+
|
|
78
|
+
Install on organization:
|
|
79
|
+
- Navigate to the install URL above
|
|
80
|
+
- Select "Organization" instead of personal account
|
|
81
|
+
- Choose which organization
|
|
82
|
+
- Select repositories
|
|
83
|
+
|
|
84
|
+
Available endpoints:
|
|
85
|
+
- GET /installations/list - List all installations (personal + orgs)
|
|
86
|
+
- GET /installations/:installationId - Get specific installation details
|
|
87
|
+
- GET /installations/:installationId/repos - List repos for installation
|
|
88
|
+
|
|
89
|
+
=================================
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handler: List all installations for user
|
|
94
|
+
const ListAllInstallations: GetHandler = async ({ ctx }) => {
|
|
95
|
+
const userId = ctx.session?.userId || "demo-user";
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const installations = await ctx.plugins.githubApp.getInstallations(userId);
|
|
99
|
+
|
|
100
|
+
// Group by type
|
|
101
|
+
const personal = installations.filter((inst: any) => inst.accountType === "User");
|
|
102
|
+
const organizations = installations.filter((inst: any) => inst.accountType === "Organization");
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
status: 200,
|
|
106
|
+
data: {
|
|
107
|
+
total: installations.length,
|
|
108
|
+
personal: personal.map((inst: any) => ({
|
|
109
|
+
installationId: inst.installationId,
|
|
110
|
+
account: inst.accountLogin,
|
|
111
|
+
repositoryCount: inst.repositories.length,
|
|
112
|
+
createdAt: inst.createdAt,
|
|
113
|
+
})),
|
|
114
|
+
organizations: organizations.map((inst: any) => ({
|
|
115
|
+
installationId: inst.installationId,
|
|
116
|
+
organization: inst.accountLogin,
|
|
117
|
+
repositoryCount: inst.repositories.length,
|
|
118
|
+
permissions: inst.permissions,
|
|
119
|
+
createdAt: inst.createdAt,
|
|
120
|
+
})),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
} catch (error: any) {
|
|
124
|
+
return {
|
|
125
|
+
status: 500,
|
|
126
|
+
data: {
|
|
127
|
+
error: "failed-to-list-installations",
|
|
128
|
+
message: error.message,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export { ListAllInstallations };
|
|
135
|
+
|
|
136
|
+
// Handler: Get installation details
|
|
137
|
+
const GetInstallationDetails: GetHandler<any, any, { installationId: string }> = async ({ ctx, params }) => {
|
|
138
|
+
const userId = ctx.session?.userId || "demo-user";
|
|
139
|
+
const installationId = parseInt(params.installationId, 10);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const installations = await ctx.plugins.githubApp.getInstallations(userId);
|
|
143
|
+
const installation = installations.find((inst: any) => inst.installationId === installationId);
|
|
144
|
+
|
|
145
|
+
if (!installation) {
|
|
146
|
+
return {
|
|
147
|
+
status: 404,
|
|
148
|
+
data: {
|
|
149
|
+
error: "installation-not-found",
|
|
150
|
+
message: "Installation not found or you do not have access",
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
status: 200,
|
|
157
|
+
data: {
|
|
158
|
+
installationId: installation.installationId,
|
|
159
|
+
account: {
|
|
160
|
+
id: installation.accountId,
|
|
161
|
+
login: installation.accountLogin,
|
|
162
|
+
type: installation.accountType,
|
|
163
|
+
avatarUrl: installation.avatarUrl,
|
|
164
|
+
},
|
|
165
|
+
repositories: installation.repositories.map((repo: any) => ({
|
|
166
|
+
id: repo.id,
|
|
167
|
+
name: repo.name,
|
|
168
|
+
fullName: repo.fullName,
|
|
169
|
+
private: repo.private,
|
|
170
|
+
})),
|
|
171
|
+
permissions: installation.permissions,
|
|
172
|
+
events: installation.events,
|
|
173
|
+
createdAt: installation.createdAt,
|
|
174
|
+
updatedAt: installation.updatedAt,
|
|
175
|
+
suspended: !!installation.suspendedAt,
|
|
176
|
+
suspendedAt: installation.suspendedAt,
|
|
177
|
+
suspendedBy: installation.suspendedBy,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
} catch (error: any) {
|
|
181
|
+
return {
|
|
182
|
+
status: 500,
|
|
183
|
+
data: {
|
|
184
|
+
error: "failed-to-get-installation",
|
|
185
|
+
message: error.message,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export { GetInstallationDetails };
|
|
192
|
+
|
|
193
|
+
// Handler: List repositories for specific installation
|
|
194
|
+
const ListInstallationRepos: GetHandler<any, any, { installationId: string }> = async ({ ctx, params }) => {
|
|
195
|
+
const userId = ctx.session?.userId || "demo-user";
|
|
196
|
+
const installationId = parseInt(params.installationId, 10);
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// Verify user owns this installation
|
|
200
|
+
const installations = await ctx.plugins.githubApp.getInstallations(userId);
|
|
201
|
+
const installation = installations.find((inst: any) => inst.installationId === installationId);
|
|
202
|
+
|
|
203
|
+
if (!installation) {
|
|
204
|
+
return {
|
|
205
|
+
status: 403,
|
|
206
|
+
data: {
|
|
207
|
+
error: "access-denied",
|
|
208
|
+
message: "You do not have access to this installation",
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Get API client for this installation
|
|
214
|
+
const client = await ctx.plugins.githubApp.getClient(installationId);
|
|
215
|
+
|
|
216
|
+
// Fetch repositories from GitHub
|
|
217
|
+
const repos = await client.getRepositories();
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
status: 200,
|
|
221
|
+
data: {
|
|
222
|
+
installation: {
|
|
223
|
+
id: installationId,
|
|
224
|
+
account: installation.accountLogin,
|
|
225
|
+
type: installation.accountType,
|
|
226
|
+
},
|
|
227
|
+
repositories: repos.map((repo: any) => ({
|
|
228
|
+
id: repo.id,
|
|
229
|
+
name: repo.name,
|
|
230
|
+
fullName: repo.full_name,
|
|
231
|
+
private: repo.private,
|
|
232
|
+
description: repo.description,
|
|
233
|
+
url: repo.html_url,
|
|
234
|
+
language: repo.language,
|
|
235
|
+
stargazersCount: repo.stargazers_count,
|
|
236
|
+
defaultBranch: repo.default_branch,
|
|
237
|
+
})),
|
|
238
|
+
total: repos.length,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
} catch (error: any) {
|
|
242
|
+
return {
|
|
243
|
+
status: 500,
|
|
244
|
+
data: {
|
|
245
|
+
error: "failed-to-list-repos",
|
|
246
|
+
message: error.message,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export { ListInstallationRepos };
|
|
253
|
+
|
|
254
|
+
// Handler: Switch organization context
|
|
255
|
+
const SwitchOrganization: GetHandler<any, any, any, { org: string }> = async ({ ctx, query }) => {
|
|
256
|
+
const userId = ctx.session?.userId || "demo-user";
|
|
257
|
+
const orgLogin = query.org;
|
|
258
|
+
|
|
259
|
+
if (!orgLogin) {
|
|
260
|
+
return {
|
|
261
|
+
status: 400,
|
|
262
|
+
data: {
|
|
263
|
+
error: "missing-org",
|
|
264
|
+
message: "org query parameter is required",
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const installations = await ctx.plugins.githubApp.getInstallations(userId);
|
|
271
|
+
const orgInstallation = installations.find(
|
|
272
|
+
(inst: any) => inst.accountType === "Organization" && inst.accountLogin === orgLogin
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
if (!orgInstallation) {
|
|
276
|
+
return {
|
|
277
|
+
status: 404,
|
|
278
|
+
data: {
|
|
279
|
+
error: "org-not-found",
|
|
280
|
+
message: `No installation found for organization ${orgLogin}`,
|
|
281
|
+
availableOrgs: installations
|
|
282
|
+
.filter((inst: any) => inst.accountType === "Organization")
|
|
283
|
+
.map((inst: any) => inst.accountLogin),
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
status: 200,
|
|
290
|
+
data: {
|
|
291
|
+
message: `Switched to organization: ${orgLogin}`,
|
|
292
|
+
installation: {
|
|
293
|
+
installationId: orgInstallation.installationId,
|
|
294
|
+
organization: orgInstallation.accountLogin,
|
|
295
|
+
repositories: orgInstallation.repositories.length,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
} catch (error: any) {
|
|
300
|
+
return {
|
|
301
|
+
status: 500,
|
|
302
|
+
data: {
|
|
303
|
+
error: "switch-org-failed",
|
|
304
|
+
message: error.message,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export { SwitchOrganization };
|
|
311
|
+
|
|
312
|
+
// Start application
|
|
313
|
+
start().catch((error) => {
|
|
314
|
+
console.error("Failed to start application:", error);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
});
|