@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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic GitHub App Installation Example
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* - Setting up GitHub App Plugin with standalone authentication
|
|
6
|
+
* - Initiating GitHub App installation
|
|
7
|
+
* - Handling installation success callback
|
|
8
|
+
* - Linking installation to user account
|
|
9
|
+
* - Accessing user's GitHub repositories
|
|
10
|
+
*
|
|
11
|
+
* Note: This example uses standalone authentication (no JWT Auth Plugin required)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { FlinkApp, FlinkContext, FlinkRepo } from "@flink-app/flink";
|
|
15
|
+
import { githubAppPlugin } from "@flink-app/github-app-plugin";
|
|
16
|
+
|
|
17
|
+
// Define User schema
|
|
18
|
+
interface User {
|
|
19
|
+
_id?: string;
|
|
20
|
+
email: string;
|
|
21
|
+
name: string;
|
|
22
|
+
githubInstallations: number[]; // Array of installation IDs
|
|
23
|
+
createdAt: Date;
|
|
24
|
+
updatedAt: Date;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// User repository
|
|
28
|
+
class UserRepo extends FlinkRepo<AppContext, User> {
|
|
29
|
+
async findByEmail(email: string) {
|
|
30
|
+
return this.getOne({ email });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async addGitHubInstallation(userId: string, installationId: number) {
|
|
34
|
+
const user = await this.getById(userId);
|
|
35
|
+
if (!user) {
|
|
36
|
+
throw new Error("User not found");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const installations = user.githubInstallations || [];
|
|
40
|
+
if (!installations.includes(installationId)) {
|
|
41
|
+
await this.updateOne(userId, {
|
|
42
|
+
githubInstallations: [...installations, installationId],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Application context
|
|
49
|
+
interface AppContext extends FlinkContext {
|
|
50
|
+
repos: {
|
|
51
|
+
userRepo: UserRepo;
|
|
52
|
+
};
|
|
53
|
+
plugins: {
|
|
54
|
+
githubApp: any;
|
|
55
|
+
};
|
|
56
|
+
// Custom session property for this example
|
|
57
|
+
session?: {
|
|
58
|
+
userId?: string;
|
|
59
|
+
email?: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function start() {
|
|
64
|
+
const app = new FlinkApp<AppContext>({
|
|
65
|
+
name: "GitHub App Basic Installation Example",
|
|
66
|
+
port: 3333,
|
|
67
|
+
|
|
68
|
+
db: {
|
|
69
|
+
uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-example",
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
plugins: [
|
|
73
|
+
githubAppPlugin({
|
|
74
|
+
// GitHub App credentials (required)
|
|
75
|
+
appId: process.env.GITHUB_APP_ID!,
|
|
76
|
+
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
|
|
77
|
+
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
78
|
+
clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
79
|
+
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
80
|
+
|
|
81
|
+
// Optional: App slug for installation URL
|
|
82
|
+
appSlug: "my-flink-app",
|
|
83
|
+
|
|
84
|
+
// Optional: Base URL (default: https://api.github.com)
|
|
85
|
+
baseUrl: process.env.GITHUB_API_URL || "https://api.github.com",
|
|
86
|
+
|
|
87
|
+
// Callback after successful installation
|
|
88
|
+
onInstallationSuccess: async ({ installationId, repositories, account, permissions, events }, ctx) => {
|
|
89
|
+
console.log(`\n=== Installation Success ===`);
|
|
90
|
+
console.log(`Installation ID: ${installationId}`);
|
|
91
|
+
console.log(`Account: ${account.login} (${account.type})`);
|
|
92
|
+
console.log(`Repositories: ${repositories.length}`);
|
|
93
|
+
console.log(`Permissions:`, permissions);
|
|
94
|
+
console.log(`Events subscribed:`, events);
|
|
95
|
+
|
|
96
|
+
// Get userId from your authentication system
|
|
97
|
+
// In this example, we use a mock session
|
|
98
|
+
const userId = ctx.session?.userId || "demo-user-123";
|
|
99
|
+
|
|
100
|
+
// Link installation to user in your database
|
|
101
|
+
try {
|
|
102
|
+
await ctx.repos.userRepo.addGitHubInstallation(userId, installationId);
|
|
103
|
+
console.log(`Installation ${installationId} linked to user ${userId}`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error("Failed to link installation:", error);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// List repositories the user granted access to
|
|
109
|
+
console.log("\nRepositories granted access:");
|
|
110
|
+
repositories.forEach((repo) => {
|
|
111
|
+
console.log(` - ${repo.fullName} (${repo.private ? "private" : "public"})`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Redirect user back to dashboard
|
|
115
|
+
return {
|
|
116
|
+
userId,
|
|
117
|
+
redirectUrl: "/dashboard?installation=success",
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Optional: Handle installation errors
|
|
122
|
+
onInstallationError: async ({ error, installationId }) => {
|
|
123
|
+
console.error(`\n=== Installation Error ===`);
|
|
124
|
+
console.error(`Error Code: ${error.code}`);
|
|
125
|
+
console.error(`Message: ${error.message}`);
|
|
126
|
+
if (installationId) {
|
|
127
|
+
console.error(`Installation ID: ${installationId}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
redirectUrl: "/error?message=installation-failed",
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// Optional: Log webhook events for debugging
|
|
136
|
+
logWebhookEvents: false,
|
|
137
|
+
}),
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Create demo user if doesn't exist
|
|
142
|
+
await app.onReady(async () => {
|
|
143
|
+
const demoUser = await app.ctx.repos.userRepo.findByEmail("demo@example.com");
|
|
144
|
+
|
|
145
|
+
if (!demoUser) {
|
|
146
|
+
await app.ctx.repos.userRepo.create({
|
|
147
|
+
email: "demo@example.com",
|
|
148
|
+
name: "Demo User",
|
|
149
|
+
githubInstallations: [],
|
|
150
|
+
createdAt: new Date(),
|
|
151
|
+
updatedAt: new Date(),
|
|
152
|
+
});
|
|
153
|
+
console.log("Demo user created");
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await app.start();
|
|
158
|
+
|
|
159
|
+
console.log(`
|
|
160
|
+
=================================
|
|
161
|
+
GitHub App Basic Example Started
|
|
162
|
+
=================================
|
|
163
|
+
|
|
164
|
+
To install the GitHub App:
|
|
165
|
+
|
|
166
|
+
1. Navigate to: http://localhost:3333/github-app/install?user_id=demo-user-123
|
|
167
|
+
2. Authorize the app on GitHub
|
|
168
|
+
3. Select repositories to grant access
|
|
169
|
+
4. Complete installation
|
|
170
|
+
|
|
171
|
+
To access user's repositories via API:
|
|
172
|
+
|
|
173
|
+
Use the context API in your handlers:
|
|
174
|
+
|
|
175
|
+
const installation = await ctx.plugins.githubApp.getInstallation('demo-user-123');
|
|
176
|
+
if (installation) {
|
|
177
|
+
const client = await ctx.plugins.githubApp.getClient(installation.installationId);
|
|
178
|
+
const repos = await client.getRepositories();
|
|
179
|
+
console.log('User repositories:', repos);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
=================================
|
|
183
|
+
`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Handle errors
|
|
187
|
+
start().catch((error) => {
|
|
188
|
+
console.error("Failed to start application:", error);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Example handler to list user's GitHub repositories
|
|
193
|
+
// Save this in src/handlers/github/GetUserRepos.ts
|
|
194
|
+
|
|
195
|
+
/*
|
|
196
|
+
import { GetHandler } from "@flink-app/flink";
|
|
197
|
+
|
|
198
|
+
const GetUserRepos: GetHandler = async ({ ctx }) => {
|
|
199
|
+
// Get userId from your auth system
|
|
200
|
+
const userId = ctx.session?.userId || 'demo-user-123';
|
|
201
|
+
|
|
202
|
+
// Get user's installation
|
|
203
|
+
const installation = await ctx.plugins.githubApp.getInstallation(userId);
|
|
204
|
+
|
|
205
|
+
if (!installation) {
|
|
206
|
+
return {
|
|
207
|
+
status: 404,
|
|
208
|
+
data: {
|
|
209
|
+
error: 'GitHub App not installed',
|
|
210
|
+
message: 'Please install the GitHub App first'
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Get API client
|
|
216
|
+
const client = await ctx.plugins.githubApp.getClient(installation.installationId);
|
|
217
|
+
|
|
218
|
+
// Fetch repositories
|
|
219
|
+
const repos = await client.getRepositories();
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
status: 200,
|
|
223
|
+
data: {
|
|
224
|
+
installation: {
|
|
225
|
+
account: installation.accountLogin,
|
|
226
|
+
type: installation.accountType,
|
|
227
|
+
installedAt: installation.createdAt
|
|
228
|
+
},
|
|
229
|
+
repositories: repos.map(repo => ({
|
|
230
|
+
id: repo.id,
|
|
231
|
+
name: repo.name,
|
|
232
|
+
fullName: repo.full_name,
|
|
233
|
+
private: repo.private,
|
|
234
|
+
url: repo.html_url,
|
|
235
|
+
description: repo.description
|
|
236
|
+
}))
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export default GetUserRepos;
|
|
242
|
+
|
|
243
|
+
export const Route = {
|
|
244
|
+
path: '/github/repos'
|
|
245
|
+
};
|
|
246
|
+
*/
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create GitHub Issue Example
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* - Creating GitHub issues with permission checks
|
|
6
|
+
* - Using the GitHub API client
|
|
7
|
+
* - Verifying repository access before operations
|
|
8
|
+
* - Adding labels and assignees to issues
|
|
9
|
+
* - Error handling for permission failures
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { FlinkApp, FlinkContext, PostHandler } 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
|
+
session?: {
|
|
20
|
+
userId?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CreateIssueRequest {
|
|
25
|
+
owner: string;
|
|
26
|
+
repo: string;
|
|
27
|
+
title: string;
|
|
28
|
+
body?: string;
|
|
29
|
+
labels?: string[];
|
|
30
|
+
assignees?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function start() {
|
|
34
|
+
const app = new FlinkApp<AppContext>({
|
|
35
|
+
name: "GitHub App Create Issue Example",
|
|
36
|
+
port: 3333,
|
|
37
|
+
|
|
38
|
+
db: {
|
|
39
|
+
uri: process.env.MONGODB_URI || "mongodb://localhost:27017/github-app-issue-example",
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
plugins: [
|
|
43
|
+
githubAppPlugin({
|
|
44
|
+
appId: process.env.GITHUB_APP_ID!,
|
|
45
|
+
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
|
|
46
|
+
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
47
|
+
clientId: process.env.GITHUB_APP_CLIENT_ID!,
|
|
48
|
+
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
|
|
49
|
+
|
|
50
|
+
onInstallationSuccess: async ({ installationId, repositories, account, permissions }) => {
|
|
51
|
+
const userId = "demo-user-123";
|
|
52
|
+
|
|
53
|
+
console.log(`\nInstallation successful!`);
|
|
54
|
+
console.log(`Account: ${account.login}`);
|
|
55
|
+
console.log(`Repositories: ${repositories.length}`);
|
|
56
|
+
console.log(`Permissions:`, permissions);
|
|
57
|
+
|
|
58
|
+
// Check if we have issues permission
|
|
59
|
+
if (permissions.issues !== "write") {
|
|
60
|
+
console.warn(`Warning: Issues permission is ${permissions.issues}, may not be able to create issues`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { userId, redirectUrl: "/dashboard" };
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await app.start();
|
|
70
|
+
|
|
71
|
+
console.log(`
|
|
72
|
+
=================================
|
|
73
|
+
Create Issue Example Started
|
|
74
|
+
=================================
|
|
75
|
+
|
|
76
|
+
Available endpoints:
|
|
77
|
+
|
|
78
|
+
POST /issues/create
|
|
79
|
+
Body: {
|
|
80
|
+
"owner": "owner-name",
|
|
81
|
+
"repo": "repo-name",
|
|
82
|
+
"title": "Issue title",
|
|
83
|
+
"body": "Issue description",
|
|
84
|
+
"labels": ["bug", "help wanted"],
|
|
85
|
+
"assignees": ["username"]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Example curl:
|
|
89
|
+
curl -X POST http://localhost:3333/issues/create \\
|
|
90
|
+
-H "Content-Type: application/json" \\
|
|
91
|
+
-d '{
|
|
92
|
+
"owner": "facebook",
|
|
93
|
+
"repo": "react",
|
|
94
|
+
"title": "Bug Report",
|
|
95
|
+
"body": "Found a bug...",
|
|
96
|
+
"labels": ["bug"]
|
|
97
|
+
}'
|
|
98
|
+
|
|
99
|
+
Install the GitHub App first:
|
|
100
|
+
http://localhost:3333/github-app/install?user_id=demo-user-123
|
|
101
|
+
|
|
102
|
+
=================================
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handler: Create GitHub issue
|
|
107
|
+
const CreateIssue: PostHandler<CreateIssueRequest> = async ({ ctx, body }) => {
|
|
108
|
+
const userId = ctx.session?.userId || "demo-user-123";
|
|
109
|
+
|
|
110
|
+
// Validate request body
|
|
111
|
+
if (!body.owner || !body.repo || !body.title) {
|
|
112
|
+
return {
|
|
113
|
+
status: 400,
|
|
114
|
+
data: {
|
|
115
|
+
error: "invalid-request",
|
|
116
|
+
message: "owner, repo, and title are required",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { owner, repo, title, body: issueBody, labels, assignees } = body;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Step 1: Check if user has access to repository
|
|
125
|
+
console.log(`Checking access to ${owner}/${repo}...`);
|
|
126
|
+
const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(userId, owner, repo);
|
|
127
|
+
|
|
128
|
+
if (!hasAccess) {
|
|
129
|
+
return {
|
|
130
|
+
status: 403,
|
|
131
|
+
data: {
|
|
132
|
+
error: "access-denied",
|
|
133
|
+
message: `You do not have access to repository ${owner}/${repo}`,
|
|
134
|
+
hint: "Install the GitHub App and grant access to this repository",
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Step 2: Get user's installation
|
|
140
|
+
const installation = await ctx.plugins.githubApp.getInstallation(userId);
|
|
141
|
+
|
|
142
|
+
if (!installation) {
|
|
143
|
+
return {
|
|
144
|
+
status: 404,
|
|
145
|
+
data: {
|
|
146
|
+
error: "installation-not-found",
|
|
147
|
+
message: "GitHub App is not installed for your account",
|
|
148
|
+
hint: "Install the GitHub App first",
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Step 3: Get API client
|
|
154
|
+
console.log(`Getting API client for installation ${installation.installationId}...`);
|
|
155
|
+
const client = await ctx.plugins.githubApp.getClient(installation.installationId);
|
|
156
|
+
|
|
157
|
+
// Step 4: Create the issue
|
|
158
|
+
console.log(`Creating issue in ${owner}/${repo}...`);
|
|
159
|
+
const issue = await client.createIssue(owner, repo, {
|
|
160
|
+
title,
|
|
161
|
+
body: issueBody || "",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
console.log(`Issue created: #${issue.number}`);
|
|
165
|
+
|
|
166
|
+
// Step 5: Add labels if provided
|
|
167
|
+
if (labels && labels.length > 0) {
|
|
168
|
+
console.log(`Adding labels: ${labels.join(", ")}`);
|
|
169
|
+
try {
|
|
170
|
+
await client.request("POST", `/repos/${owner}/${repo}/issues/${issue.number}/labels`, {
|
|
171
|
+
labels,
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.warn(`Failed to add labels:`, error);
|
|
175
|
+
// Continue anyway
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Step 6: Add assignees if provided
|
|
180
|
+
if (assignees && assignees.length > 0) {
|
|
181
|
+
console.log(`Adding assignees: ${assignees.join(", ")}`);
|
|
182
|
+
try {
|
|
183
|
+
await client.request("POST", `/repos/${owner}/${repo}/issues/${issue.number}/assignees`, {
|
|
184
|
+
assignees,
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.warn(`Failed to add assignees:`, error);
|
|
188
|
+
// Continue anyway
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Return success response
|
|
193
|
+
return {
|
|
194
|
+
status: 201,
|
|
195
|
+
data: {
|
|
196
|
+
success: true,
|
|
197
|
+
issue: {
|
|
198
|
+
id: issue.id,
|
|
199
|
+
number: issue.number,
|
|
200
|
+
title: issue.title,
|
|
201
|
+
body: issue.body,
|
|
202
|
+
state: issue.state,
|
|
203
|
+
url: issue.html_url,
|
|
204
|
+
createdAt: issue.created_at,
|
|
205
|
+
labels: issue.labels,
|
|
206
|
+
assignees: issue.assignees,
|
|
207
|
+
},
|
|
208
|
+
message: `Issue #${issue.number} created successfully`,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
} catch (error: any) {
|
|
212
|
+
console.error("Failed to create issue:", error);
|
|
213
|
+
|
|
214
|
+
// Handle specific GitHub API errors
|
|
215
|
+
if (error.response?.status === 403) {
|
|
216
|
+
return {
|
|
217
|
+
status: 403,
|
|
218
|
+
data: {
|
|
219
|
+
error: "permission-denied",
|
|
220
|
+
message: "GitHub App does not have permission to create issues in this repository",
|
|
221
|
+
hint: "Check that the GitHub App has 'issues: write' permission",
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (error.response?.status === 404) {
|
|
227
|
+
return {
|
|
228
|
+
status: 404,
|
|
229
|
+
data: {
|
|
230
|
+
error: "repository-not-found",
|
|
231
|
+
message: `Repository ${owner}/${repo} not found or not accessible`,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (error.response?.status === 410) {
|
|
237
|
+
return {
|
|
238
|
+
status: 410,
|
|
239
|
+
data: {
|
|
240
|
+
error: "issues-disabled",
|
|
241
|
+
message: "Issues are disabled for this repository",
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Generic error
|
|
247
|
+
return {
|
|
248
|
+
status: 500,
|
|
249
|
+
data: {
|
|
250
|
+
error: "issue-creation-failed",
|
|
251
|
+
message: error.message || "Failed to create issue",
|
|
252
|
+
details: error.response?.data,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export { CreateIssue };
|
|
259
|
+
|
|
260
|
+
// Handler: Create issue from template
|
|
261
|
+
const CreateIssueFromTemplate: PostHandler<{
|
|
262
|
+
owner: string;
|
|
263
|
+
repo: string;
|
|
264
|
+
template: "bug" | "feature" | "question";
|
|
265
|
+
additionalInfo?: Record<string, any>;
|
|
266
|
+
}> = async ({ ctx, body }) => {
|
|
267
|
+
const userId = ctx.session?.userId || "demo-user-123";
|
|
268
|
+
const { owner, repo, template, additionalInfo = {} } = body;
|
|
269
|
+
|
|
270
|
+
// Define issue templates
|
|
271
|
+
const templates = {
|
|
272
|
+
bug: {
|
|
273
|
+
title: `[BUG] ${additionalInfo.summary || "Issue summary"}`,
|
|
274
|
+
body: `## Bug Description
|
|
275
|
+
${additionalInfo.description || "Describe the bug..."}
|
|
276
|
+
|
|
277
|
+
## Steps to Reproduce
|
|
278
|
+
${additionalInfo.steps || "1. ...\\n2. ...\\n3. ..."}
|
|
279
|
+
|
|
280
|
+
## Expected Behavior
|
|
281
|
+
${additionalInfo.expected || "What should happen..."}
|
|
282
|
+
|
|
283
|
+
## Actual Behavior
|
|
284
|
+
${additionalInfo.actual || "What actually happens..."}
|
|
285
|
+
|
|
286
|
+
## Environment
|
|
287
|
+
${additionalInfo.environment || "- OS: ...\\n- Browser: ...\\n- Version: ..."}`,
|
|
288
|
+
labels: ["bug"],
|
|
289
|
+
},
|
|
290
|
+
feature: {
|
|
291
|
+
title: `[FEATURE] ${additionalInfo.summary || "Feature request"}`,
|
|
292
|
+
body: `## Feature Description
|
|
293
|
+
${additionalInfo.description || "Describe the feature..."}
|
|
294
|
+
|
|
295
|
+
## Use Case
|
|
296
|
+
${additionalInfo.useCase || "Why is this feature needed?"}
|
|
297
|
+
|
|
298
|
+
## Proposed Solution
|
|
299
|
+
${additionalInfo.solution || "How should it work?"}
|
|
300
|
+
|
|
301
|
+
## Alternatives Considered
|
|
302
|
+
${additionalInfo.alternatives || "What alternatives have been considered?"}`,
|
|
303
|
+
labels: ["enhancement"],
|
|
304
|
+
},
|
|
305
|
+
question: {
|
|
306
|
+
title: `[QUESTION] ${additionalInfo.summary || "Question"}`,
|
|
307
|
+
body: `## Question
|
|
308
|
+
${additionalInfo.question || "Your question..."}
|
|
309
|
+
|
|
310
|
+
## Context
|
|
311
|
+
${additionalInfo.context || "Additional context..."}`,
|
|
312
|
+
labels: ["question"],
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const issueTemplate = templates[template];
|
|
317
|
+
|
|
318
|
+
if (!issueTemplate) {
|
|
319
|
+
return {
|
|
320
|
+
status: 400,
|
|
321
|
+
data: {
|
|
322
|
+
error: "invalid-template",
|
|
323
|
+
message: "Template must be one of: bug, feature, question",
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
// Check access
|
|
330
|
+
const hasAccess = await ctx.plugins.githubApp.hasRepositoryAccess(userId, owner, repo);
|
|
331
|
+
if (!hasAccess) {
|
|
332
|
+
return {
|
|
333
|
+
status: 403,
|
|
334
|
+
data: { error: "access-denied" },
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Get installation and client
|
|
339
|
+
const installation = await ctx.plugins.githubApp.getInstallation(userId);
|
|
340
|
+
if (!installation) {
|
|
341
|
+
return {
|
|
342
|
+
status: 404,
|
|
343
|
+
data: { error: "installation-not-found" },
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const client = await ctx.plugins.githubApp.getClient(installation.installationId);
|
|
348
|
+
|
|
349
|
+
// Create issue
|
|
350
|
+
const issue = await client.createIssue(owner, repo, {
|
|
351
|
+
title: issueTemplate.title,
|
|
352
|
+
body: issueTemplate.body,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Add labels
|
|
356
|
+
if (issueTemplate.labels.length > 0) {
|
|
357
|
+
await client.request("POST", `/repos/${owner}/${repo}/issues/${issue.number}/labels`, {
|
|
358
|
+
labels: issueTemplate.labels,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
status: 201,
|
|
364
|
+
data: {
|
|
365
|
+
success: true,
|
|
366
|
+
issue: {
|
|
367
|
+
number: issue.number,
|
|
368
|
+
title: issue.title,
|
|
369
|
+
url: issue.html_url,
|
|
370
|
+
template,
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
} catch (error: any) {
|
|
375
|
+
console.error("Failed to create issue from template:", error);
|
|
376
|
+
return {
|
|
377
|
+
status: 500,
|
|
378
|
+
data: {
|
|
379
|
+
error: "issue-creation-failed",
|
|
380
|
+
message: error.message,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
export { CreateIssueFromTemplate };
|
|
387
|
+
|
|
388
|
+
// Start application
|
|
389
|
+
start().catch((error) => {
|
|
390
|
+
console.error("Failed to start application:", error);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|