drbh commited on
Commit
e06b71c
·
1 Parent(s): ec788f0

feat: trigger on issue

Browse files
app/lib/github-app.server.ts CHANGED
@@ -188,6 +188,24 @@ export class GitHubAppAuth {
188
  async getInstallationOctokit(installationId: number) {
189
  return await this.app.getInstallationOctokit(installationId);
190
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  /**
193
  * Verify webhook signature
 
188
  async getInstallationOctokit(installationId: number) {
189
  return await this.app.getInstallationOctokit(installationId);
190
  }
191
+
192
+ /**
193
+ * Get installation by repository owner and name
194
+ */
195
+ async getInstallationByRepo(owner: string, repo: string) {
196
+ try {
197
+ const appOctokit = await this.app.getOctokit();
198
+ const { data } = await appOctokit.rest.apps.getRepoInstallation({
199
+ owner,
200
+ repo,
201
+ });
202
+
203
+ return data;
204
+ } catch (error) {
205
+ console.error(`Failed to get installation for repository ${owner}/${repo}:`, error);
206
+ return null;
207
+ }
208
+ }
209
 
210
  /**
211
  * Verify webhook signature
app/lib/hugex-service.server.ts ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { accountLinkingService } from './account-linking.server';
2
+ import { githubApp } from './github-app.server';
3
+
4
+ // Default API URL
5
+ const DEFAULT_API_URL = 'https://huggingface-hugex-explore.hf.space/api';
6
+
7
+ interface HugExRepository {
8
+ url: string;
9
+ }
10
+
11
+ interface HugExEnvironment {
12
+ LLM_MODEL: string;
13
+ LLM_PROVIDER: string;
14
+ }
15
+
16
+ interface HugExSecrets {
17
+ [key: string]: string;
18
+ }
19
+
20
+ export interface HugExJobCreationParams {
21
+ title: string;
22
+ description: string;
23
+ repository: HugExRepository;
24
+ branch: string;
25
+ environment: HugExEnvironment;
26
+ secrets: HugExSecrets;
27
+ }
28
+
29
+ export interface HugExJob {
30
+ id: string;
31
+ status: string;
32
+ createdAt: string;
33
+ title: string;
34
+ repository: HugExRepository;
35
+ }
36
+
37
+ export class HugExService {
38
+ private apiUrl: string;
39
+
40
+ constructor() {
41
+ this.apiUrl = process.env.HUGEX_API_URL || DEFAULT_API_URL;
42
+
43
+ // Log configuration status
44
+ if (!process.env.HUGEX_API_URL) {
45
+ console.log(`ℹ️ Using default HugEx API URL: ${this.apiUrl}`);
46
+ } else {
47
+ console.log(`✅ HugEx API configured: ${this.apiUrl}`);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Process a GitHub issue to check for @hugex mentions and create jobs
53
+ */
54
+ async processGitHubIssue(issue: any, repository: any): Promise<boolean> {
55
+ try {
56
+ // Check if issue body contains @hugex mention
57
+ if (!issue.body || !issue.body.includes('@hugex')) {
58
+ return false;
59
+ }
60
+
61
+ console.log(`🤖 Found @hugex mention in issue #${issue.number}: ${issue.title}`);
62
+
63
+ // Get GitHub user's linked HF account
64
+ const githubUserId = issue.user.id.toString();
65
+ const accountLink = accountLinkingService.findByGitHubUser(githubUserId);
66
+
67
+ if (!accountLink) {
68
+ console.log(`⚠️ No linked HuggingFace account found for GitHub user: ${issue.user.login}`);
69
+
70
+ // Comment on the issue to inform the user they need to link their account
71
+ await this.commentOnIssue(
72
+ repository.owner.login,
73
+ repository.name,
74
+ issue.number,
75
+ `⚠️ @${issue.user.login}, to use @hugex, you need to link your GitHub account with your HuggingFace account. Please visit the dashboard to complete this process.`
76
+ );
77
+ return false;
78
+ }
79
+
80
+ // Extract job parameters from issue body
81
+ const jobParams = await this.extractJobParamsFromIssue(issue, repository);
82
+
83
+ // Create HugEx job
84
+ const jobId = await this.createJob(accountLink.huggingfaceUsername, jobParams);
85
+
86
+ if (jobId) {
87
+ console.log(`✅ Created HugEx job ${jobId} for issue #${issue.number}`);
88
+
89
+ // Comment on the issue with job info
90
+ await this.commentOnIssue(
91
+ repository.owner.login,
92
+ repository.name,
93
+ issue.number,
94
+ `🚀 Created HugEx job for this issue. You can track progress at ${this.apiUrl}/jobs/${jobId}`
95
+ );
96
+ return true;
97
+ }
98
+
99
+ return false;
100
+ } catch (error) {
101
+ console.error('Error processing GitHub issue for HugEx:', error);
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Extract job parameters from issue body
108
+ */
109
+ private async extractJobParamsFromIssue(issue: any, repository: any): Promise<HugExJobCreationParams> {
110
+ // Default values
111
+ const params: HugExJobCreationParams = {
112
+ title: issue.title,
113
+ description: issue.body,
114
+ repository: {
115
+ url: repository.html_url
116
+ },
117
+ branch: repository.default_branch || 'main',
118
+ environment: {
119
+ LLM_MODEL: 'gpt-4',
120
+ LLM_PROVIDER: 'openai'
121
+ },
122
+ secrets: {}
123
+ };
124
+
125
+ // Extract custom parameters if available
126
+ try {
127
+ // Look for JSON block in the issue body
128
+ const jsonMatch = issue.body.match(/```json\s*([\s\S]*?)\s*```/);
129
+ if (jsonMatch && jsonMatch[1]) {
130
+ const customParams = JSON.parse(jsonMatch[1]);
131
+
132
+ // Merge with defaults, keeping required structure
133
+ if (customParams.title) params.title = customParams.title;
134
+ if (customParams.description) params.description = customParams.description;
135
+ if (customParams.repository && customParams.repository.url) {
136
+ params.repository.url = customParams.repository.url;
137
+ }
138
+ if (customParams.branch) params.branch = customParams.branch;
139
+ if (customParams.environment) params.environment = {
140
+ ...params.environment,
141
+ ...customParams.environment
142
+ };
143
+ if (customParams.secrets) params.secrets = {
144
+ ...params.secrets,
145
+ ...customParams.secrets
146
+ };
147
+ }
148
+ } catch (error) {
149
+ console.warn('Failed to parse custom parameters from issue body:', error);
150
+ // Continue with default parameters
151
+ }
152
+
153
+ return params;
154
+ }
155
+
156
+ /**
157
+ * Create a new HugEx job
158
+ */
159
+ async createJob(huggingfaceUsername: string, params: HugExJobCreationParams): Promise<string | null> {
160
+ try {
161
+ // Get HuggingFace token from persistent storage
162
+ // Note: In a real implementation, you'd need a secure way to store and retrieve HF tokens
163
+ // This is a placeholder - you need to implement token storage
164
+ const hfToken = await this.getHuggingFaceToken(huggingfaceUsername);
165
+
166
+ if (!hfToken) {
167
+ console.error(`No HuggingFace token available for user: ${huggingfaceUsername}`);
168
+ return null;
169
+ }
170
+
171
+ const response = await fetch(`${this.apiUrl}/jobs/create-with-key`, {
172
+ method: 'POST',
173
+ headers: {
174
+ 'Content-Type': 'application/json',
175
+ 'Authorization': `Bearer ${hfToken}`
176
+ },
177
+ body: JSON.stringify(params)
178
+ });
179
+
180
+ if (!response.ok) {
181
+ const errorText = await response.text();
182
+ throw new Error(`Failed to create HugEx job: ${response.status} - ${errorText}`);
183
+ }
184
+
185
+ const responseData = await response.json();
186
+ return responseData.id || null;
187
+ } catch (error) {
188
+ console.error('Error creating HugEx job:', error);
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Comment on a GitHub issue
195
+ */
196
+ private async commentOnIssue(owner: string, repo: string, issueNumber: number, body: string): Promise<void> {
197
+ try {
198
+ // Get installation Octokit instance for the repository
199
+ const installation = await githubApp.getInstallationByRepo(owner, repo);
200
+ if (!installation) {
201
+ throw new Error(`No GitHub App installation found for repository: ${owner}/${repo}`);
202
+ }
203
+
204
+ const octokit = await githubApp.getInstallationOctokit(installation.id);
205
+
206
+ await octokit.rest.issues.createComment({
207
+ owner,
208
+ repo,
209
+ issue_number: issueNumber,
210
+ body
211
+ });
212
+
213
+ console.log(`✅ Posted comment on ${owner}/${repo}#${issueNumber}`);
214
+ } catch (error) {
215
+ console.error('Error commenting on GitHub issue:', error);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Get HuggingFace token for a user
221
+ * Note: This is a placeholder. In a production system, you'd need a secure way to store and retrieve tokens.
222
+ */
223
+ private async getHuggingFaceToken(username: string): Promise<string | null> {
224
+ // In a real implementation, you'd have a secure storage mechanism for tokens
225
+ // This is a placeholder - you need to implement secure token storage
226
+
227
+ // For now, return null to indicate we don't have a token
228
+ // This would be replaced with your token retrieval logic
229
+ console.warn(`⚠️ HuggingFace token storage not implemented. Cannot retrieve token for: ${username}`);
230
+ return process.env.HF_DEFAULT_TOKEN || null;
231
+ }
232
+ }
233
+
234
+ // Singleton instance
235
+ export const hugexService = new HugExService();
app/routes/_index.tsx CHANGED
@@ -311,30 +311,6 @@ export default function Index() {
311
  </div>
312
  )}
313
  </div>
314
-
315
- {/* Quick Info */}
316
- <div className="grid md:grid-cols-3 gap-6 w-full">
317
- <div className="bg-white rounded-lg shadow p-6">
318
- <h3 className="text-lg font-semibold text-gray-900 mb-2">Why Link Accounts?</h3>
319
- <p className="text-gray-600 text-sm">
320
- Linking your GitHub and HuggingFace accounts enables seamless integration between the two platforms.
321
- </p>
322
- </div>
323
-
324
- <div className="bg-white rounded-lg shadow p-6">
325
- <h3 className="text-lg font-semibold text-gray-900 mb-2">How It Works</h3>
326
- <p className="text-gray-600 text-sm">
327
- Simply connect both accounts and we'll automatically link them for you. Your data is stored securely.
328
- </p>
329
- </div>
330
-
331
- <div className="bg-white rounded-lg shadow p-6">
332
- <h3 className="text-lg font-semibold text-gray-900 mb-2">Privacy</h3>
333
- <p className="text-gray-600 text-sm">
334
- We only store the minimum required information to link your accounts. No sensitive data is kept.
335
- </p>
336
- </div>
337
- </div>
338
  </div>
339
  </div>
340
  </div>
 
311
  </div>
312
  )}
313
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  </div>
315
  </div>
316
  </div>
app/routes/webhook.github.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import { json } from "@remix-run/node";
2
  import type { ActionFunctionArgs } from "@remix-run/node";
3
  import { githubApp } from "~/lib/github-app.server";
 
4
 
5
  export async function action({ request }: ActionFunctionArgs) {
6
  if (request.method !== "POST") {
@@ -154,6 +155,19 @@ async function handleIssuesEvent(data: any) {
154
  author: issue.user.login,
155
  });
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  // Example: Use the issue author's authenticated session
158
  try {
159
  const userAuth = githubApp.getUserAuth(issue.user.login);
 
1
  import { json } from "@remix-run/node";
2
  import type { ActionFunctionArgs } from "@remix-run/node";
3
  import { githubApp } from "~/lib/github-app.server";
4
+ import { hugexService } from "~/lib/hugex-service.server";
5
 
6
  export async function action({ request }: ActionFunctionArgs) {
7
  if (request.method !== "POST") {
 
155
  author: issue.user.login,
156
  });
157
 
158
+ // Process issue for HugEx if action is 'opened' or 'edited'
159
+ if (['opened', 'edited'].includes(action)) {
160
+ try {
161
+ // Check for @hugex mention and create job if found
162
+ const jobCreated = await hugexService.processGitHubIssue(issue, repository);
163
+ if (jobCreated) {
164
+ console.log(`✅ Created HugEx job for issue #${issue.number}`);
165
+ }
166
+ } catch (error) {
167
+ console.error("Error processing issue for HugEx:", error);
168
+ }
169
+ }
170
+
171
  // Example: Use the issue author's authenticated session
172
  try {
173
  const userAuth = githubApp.getUserAuth(issue.user.login);