@almadar/integrations 2.0.2 → 2.0.3
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/dist/{factory-Bi5eLpB0.d.ts → BaseIntegration-DNSuiUnG.d.ts} +1 -35
- package/dist/factory-B1QtCI_c.d.ts +37 -0
- package/dist/index.d.ts +5 -59
- package/dist/integrations/github/index.d.ts +326 -0
- package/dist/integrations/github/index.js +756 -0
- package/dist/integrations/github/index.js.map +1 -0
- package/dist/mocks/index.d.ts +2 -1
- package/dist/runtime/index.d.ts +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { integratorsRegistry } from '@almadar/patterns';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { promises } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
// src/core/logger.ts
|
|
7
|
+
var ConsoleLogger = class {
|
|
8
|
+
constructor(level = "info") {
|
|
9
|
+
this.level = level;
|
|
10
|
+
}
|
|
11
|
+
debug(message, meta) {
|
|
12
|
+
if (this.shouldLog("debug")) {
|
|
13
|
+
console.debug(`[DEBUG] ${message}`, meta || "");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
info(message, meta) {
|
|
17
|
+
if (this.shouldLog("info")) {
|
|
18
|
+
console.log(`[INFO] ${message}`, meta || "");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
warn(message, meta) {
|
|
22
|
+
if (this.shouldLog("warn")) {
|
|
23
|
+
console.warn(`[WARN] ${message}`, meta || "");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
error(message, meta) {
|
|
27
|
+
if (this.shouldLog("error")) {
|
|
28
|
+
console.error(`[ERROR] ${message}`, meta || "");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
shouldLog(level) {
|
|
32
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
33
|
+
return levels.indexOf(level) >= levels.indexOf(this.level);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function validateParams(integration, action, params) {
|
|
37
|
+
const registry = integratorsRegistry.integrators[integration];
|
|
38
|
+
if (!registry) {
|
|
39
|
+
return {
|
|
40
|
+
valid: false,
|
|
41
|
+
errors: [
|
|
42
|
+
{
|
|
43
|
+
param: "integration",
|
|
44
|
+
message: `Unknown integration: ${integration}`
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const actionDef = registry.actions.find((a) => a.name === action);
|
|
50
|
+
if (!actionDef) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
errors: [{ param: "action", message: `Unknown action: ${action}` }]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const errors = [];
|
|
57
|
+
for (const paramDef of actionDef.params) {
|
|
58
|
+
if (paramDef.required && !(paramDef.name in params)) {
|
|
59
|
+
errors.push({
|
|
60
|
+
param: paramDef.name,
|
|
61
|
+
message: `Missing required parameter: ${paramDef.name}`
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (paramDef.name in params) {
|
|
65
|
+
const value = params[paramDef.name];
|
|
66
|
+
const expectedType = paramDef.type;
|
|
67
|
+
const actualType = typeof value;
|
|
68
|
+
if (expectedType === "number" && actualType !== "number") {
|
|
69
|
+
errors.push({
|
|
70
|
+
param: paramDef.name,
|
|
71
|
+
message: `Expected ${expectedType}, got ${actualType}`
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (expectedType === "string" && actualType !== "string") {
|
|
75
|
+
errors.push({
|
|
76
|
+
param: paramDef.name,
|
|
77
|
+
message: `Expected ${expectedType}, got ${actualType}`
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (expectedType === "array" && !Array.isArray(value)) {
|
|
81
|
+
errors.push({
|
|
82
|
+
param: paramDef.name,
|
|
83
|
+
message: `Expected array, got ${actualType}`
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (expectedType === "object" && (actualType !== "object" || Array.isArray(value) || value === null)) {
|
|
87
|
+
errors.push({
|
|
88
|
+
param: paramDef.name,
|
|
89
|
+
message: `Expected object, got ${actualType}`
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
valid: errors.length === 0,
|
|
96
|
+
errors
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/core/retry.ts
|
|
101
|
+
async function withRetry(fn, config) {
|
|
102
|
+
const {
|
|
103
|
+
maxAttempts,
|
|
104
|
+
backoffMs,
|
|
105
|
+
maxBackoffMs = 3e4,
|
|
106
|
+
retryableErrors
|
|
107
|
+
} = config;
|
|
108
|
+
let lastError;
|
|
109
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
110
|
+
try {
|
|
111
|
+
return await fn();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
lastError = error;
|
|
114
|
+
if (error && typeof error === "object" && "code" in error && retryableErrors) {
|
|
115
|
+
const integrationError = error;
|
|
116
|
+
if (!retryableErrors.includes(integrationError.code)) {
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (attempt === maxAttempts) {
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
const delay = Math.min(
|
|
124
|
+
backoffMs * Math.pow(2, attempt - 1),
|
|
125
|
+
maxBackoffMs
|
|
126
|
+
);
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw lastError;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/core/BaseIntegration.ts
|
|
134
|
+
var BaseIntegration = class {
|
|
135
|
+
constructor(config) {
|
|
136
|
+
this.config = config;
|
|
137
|
+
this.logger = config.logger || new ConsoleLogger();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Validate action params against registry
|
|
141
|
+
*/
|
|
142
|
+
validateParams(action, params) {
|
|
143
|
+
return validateParams(this.config.name, action, params);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Handle errors uniformly
|
|
147
|
+
*/
|
|
148
|
+
handleError(action, error) {
|
|
149
|
+
this.logger.error(`Integration error in ${this.config.name}.${action}`, {
|
|
150
|
+
error
|
|
151
|
+
});
|
|
152
|
+
const integrationError = error instanceof Error ? error : new Error(String(error));
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: integrationError,
|
|
156
|
+
metadata: this.createMetadata(action, 0, 0)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Create metadata for result
|
|
161
|
+
*/
|
|
162
|
+
createMetadata(action, duration, retries = 0) {
|
|
163
|
+
return {
|
|
164
|
+
integration: this.config.name,
|
|
165
|
+
action,
|
|
166
|
+
duration,
|
|
167
|
+
retries,
|
|
168
|
+
timestamp: Date.now()
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Execute with retry logic
|
|
173
|
+
*/
|
|
174
|
+
async executeWithRetry(fn) {
|
|
175
|
+
if (!this.config.retry) {
|
|
176
|
+
return fn();
|
|
177
|
+
}
|
|
178
|
+
return withRetry(fn, {
|
|
179
|
+
maxAttempts: this.config.retry.maxAttempts,
|
|
180
|
+
backoffMs: this.config.retry.backoffMs,
|
|
181
|
+
maxBackoffMs: this.config.retry.maxBackoffMs,
|
|
182
|
+
retryableErrors: [
|
|
183
|
+
"TIMEOUT_ERROR",
|
|
184
|
+
"NETWORK_ERROR",
|
|
185
|
+
"RATE_LIMIT_ERROR"
|
|
186
|
+
]
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/types.ts
|
|
192
|
+
var IntegrationError = class extends Error {
|
|
193
|
+
constructor(message, code = "UNKNOWN_ERROR", details) {
|
|
194
|
+
super(message);
|
|
195
|
+
this.name = "IntegrationError";
|
|
196
|
+
this.code = code;
|
|
197
|
+
this.details = details;
|
|
198
|
+
}
|
|
199
|
+
toJSON() {
|
|
200
|
+
return {
|
|
201
|
+
name: this.name,
|
|
202
|
+
message: this.message,
|
|
203
|
+
code: this.code,
|
|
204
|
+
integration: this.integration,
|
|
205
|
+
action: this.action,
|
|
206
|
+
details: this.details
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// src/integrations/github/github-git.ts
|
|
212
|
+
async function execGit(args, cwd, env) {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const proc = spawn("git", args, {
|
|
215
|
+
cwd,
|
|
216
|
+
env: { ...process.env, ...env },
|
|
217
|
+
stdio: "pipe"
|
|
218
|
+
});
|
|
219
|
+
let stdout = "";
|
|
220
|
+
let stderr = "";
|
|
221
|
+
proc.stdout?.on("data", (data) => {
|
|
222
|
+
stdout += data.toString();
|
|
223
|
+
});
|
|
224
|
+
proc.stderr?.on("data", (data) => {
|
|
225
|
+
stderr += data.toString();
|
|
226
|
+
});
|
|
227
|
+
proc.on("close", (code) => {
|
|
228
|
+
if (code === 0) {
|
|
229
|
+
resolve({ stdout, stderr });
|
|
230
|
+
} else {
|
|
231
|
+
reject(
|
|
232
|
+
new IntegrationError(
|
|
233
|
+
`Git command failed: ${args.join(" ")}
|
|
234
|
+
${stderr}`,
|
|
235
|
+
"SERVICE_ERROR",
|
|
236
|
+
{ code, stderr }
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
proc.on("error", (error) => {
|
|
242
|
+
reject(
|
|
243
|
+
new IntegrationError(
|
|
244
|
+
`Failed to execute git: ${error.message}`,
|
|
245
|
+
"SERVICE_ERROR",
|
|
246
|
+
{ error }
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async function cloneRepo(params, token) {
|
|
253
|
+
const { repoUrl, targetDir, branch, depth = 1 } = params;
|
|
254
|
+
const authUrl = injectTokenIntoUrl(repoUrl, token);
|
|
255
|
+
const args = ["clone"];
|
|
256
|
+
if (depth > 0) {
|
|
257
|
+
args.push("--depth", depth.toString());
|
|
258
|
+
}
|
|
259
|
+
if (branch) {
|
|
260
|
+
args.push("--branch", branch);
|
|
261
|
+
}
|
|
262
|
+
args.push(authUrl, targetDir);
|
|
263
|
+
try {
|
|
264
|
+
await execGit(args, process.cwd());
|
|
265
|
+
await scrubTokenFromRemote(targetDir, repoUrl);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new IntegrationError(
|
|
268
|
+
`Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`,
|
|
269
|
+
"SERVICE_ERROR",
|
|
270
|
+
{ repoUrl, error }
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function createBranch(params, workDir) {
|
|
275
|
+
const { branchName, baseBranch } = params;
|
|
276
|
+
const cwd = params.workDir || workDir;
|
|
277
|
+
try {
|
|
278
|
+
if (baseBranch) {
|
|
279
|
+
await execGit(["checkout", baseBranch], cwd);
|
|
280
|
+
}
|
|
281
|
+
await execGit(["checkout", "-b", branchName], cwd);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
throw new IntegrationError(
|
|
284
|
+
`Failed to create branch: ${error instanceof Error ? error.message : String(error)}`,
|
|
285
|
+
"SERVICE_ERROR",
|
|
286
|
+
{ branchName, baseBranch, error }
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function commit(params, workDir) {
|
|
291
|
+
const { message, files } = params;
|
|
292
|
+
const cwd = params.workDir || workDir;
|
|
293
|
+
try {
|
|
294
|
+
if (files && files.length > 0) {
|
|
295
|
+
await execGit(["add", ...files], cwd);
|
|
296
|
+
} else {
|
|
297
|
+
await execGit(["add", "."], cwd);
|
|
298
|
+
}
|
|
299
|
+
await execGit(["commit", "-m", message], cwd);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
throw new IntegrationError(
|
|
302
|
+
`Failed to commit: ${error instanceof Error ? error.message : String(error)}`,
|
|
303
|
+
"SERVICE_ERROR",
|
|
304
|
+
{ message, error }
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function push(params, workDir, token) {
|
|
309
|
+
const { branchName, force = false } = params;
|
|
310
|
+
const cwd = params.workDir || workDir;
|
|
311
|
+
const protectedBranches = ["main", "master", "production", "prod"];
|
|
312
|
+
if (force && protectedBranches.includes(branchName.toLowerCase())) {
|
|
313
|
+
throw new IntegrationError(
|
|
314
|
+
`Force push to protected branch '${branchName}' is not allowed`,
|
|
315
|
+
"VALIDATION_ERROR"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const credHelper = await createTempCredentialHelper(token);
|
|
320
|
+
const args = ["push"];
|
|
321
|
+
if (force) {
|
|
322
|
+
args.push("--force");
|
|
323
|
+
}
|
|
324
|
+
args.push("-u", "origin", branchName);
|
|
325
|
+
await execGit(args, cwd, {
|
|
326
|
+
GIT_ASKPASS: credHelper,
|
|
327
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
328
|
+
});
|
|
329
|
+
await promises.unlink(credHelper);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
throw new IntegrationError(
|
|
332
|
+
`Failed to push: ${error instanceof Error ? error.message : String(error)}`,
|
|
333
|
+
"SERVICE_ERROR",
|
|
334
|
+
{ branchName, error }
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function injectTokenIntoUrl(repoUrl, token) {
|
|
339
|
+
const url = new URL(repoUrl);
|
|
340
|
+
url.username = "x-access-token";
|
|
341
|
+
url.password = token;
|
|
342
|
+
return url.toString();
|
|
343
|
+
}
|
|
344
|
+
async function scrubTokenFromRemote(workDir, originalUrl) {
|
|
345
|
+
try {
|
|
346
|
+
const url = new URL(originalUrl);
|
|
347
|
+
url.username = "";
|
|
348
|
+
url.password = "";
|
|
349
|
+
const cleanUrl = url.toString();
|
|
350
|
+
await execGit(["remote", "set-url", "origin", cleanUrl], workDir);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error("Warning: Failed to scrub token from remote URL:", error);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function createTempCredentialHelper(token) {
|
|
356
|
+
const tmpDir = process.env.TMPDIR || "/tmp";
|
|
357
|
+
const helperPath = join(tmpDir, `git-cred-${Date.now()}.sh`);
|
|
358
|
+
const script = `#!/bin/sh
|
|
359
|
+
echo "${token}"`;
|
|
360
|
+
await promises.writeFile(helperPath, script, { mode: 448 });
|
|
361
|
+
return helperPath;
|
|
362
|
+
}
|
|
363
|
+
async function getCurrentBranch(workDir) {
|
|
364
|
+
try {
|
|
365
|
+
const { stdout } = await execGit(["branch", "--show-current"], workDir);
|
|
366
|
+
return stdout.trim();
|
|
367
|
+
} catch (error) {
|
|
368
|
+
throw new IntegrationError(
|
|
369
|
+
`Failed to get current branch: ${error instanceof Error ? error.message : String(error)}`,
|
|
370
|
+
"SERVICE_ERROR",
|
|
371
|
+
{ error }
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function hasUncommittedChanges(workDir) {
|
|
376
|
+
try {
|
|
377
|
+
const { stdout } = await execGit(["status", "--porcelain"], workDir);
|
|
378
|
+
return stdout.trim().length > 0;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
throw new IntegrationError(
|
|
381
|
+
`Failed to check git status: ${error instanceof Error ? error.message : String(error)}`,
|
|
382
|
+
"SERVICE_ERROR",
|
|
383
|
+
{ error }
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/integrations/github/github-api.ts
|
|
389
|
+
async function githubFetch(endpoint, config, options = {}) {
|
|
390
|
+
const url = `https://api.github.com${endpoint}`;
|
|
391
|
+
const headers = {
|
|
392
|
+
"Authorization": `Bearer ${config.token}`,
|
|
393
|
+
"Accept": "application/vnd.github+json",
|
|
394
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
395
|
+
...options.headers
|
|
396
|
+
};
|
|
397
|
+
try {
|
|
398
|
+
const response = await fetch(url, {
|
|
399
|
+
...options,
|
|
400
|
+
headers
|
|
401
|
+
});
|
|
402
|
+
const rateLimit = {
|
|
403
|
+
remaining: parseInt(response.headers.get("x-ratelimit-remaining") || "0", 10),
|
|
404
|
+
limit: parseInt(response.headers.get("x-ratelimit-limit") || "5000", 10),
|
|
405
|
+
reset: parseInt(response.headers.get("x-ratelimit-reset") || "0", 10)
|
|
406
|
+
};
|
|
407
|
+
if (!response.ok) {
|
|
408
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
409
|
+
if (response.status === 429) {
|
|
410
|
+
throw new IntegrationError(
|
|
411
|
+
"GitHub API rate limit exceeded",
|
|
412
|
+
"RATE_LIMIT_ERROR",
|
|
413
|
+
{ rateLimit, error }
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
if (response.status === 401 || response.status === 403) {
|
|
417
|
+
throw new IntegrationError(
|
|
418
|
+
`GitHub API authentication failed: ${error.message || response.statusText}`,
|
|
419
|
+
"AUTH_ERROR",
|
|
420
|
+
{ status: response.status, error }
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
throw new IntegrationError(
|
|
424
|
+
`GitHub API request failed: ${error.message || response.statusText}`,
|
|
425
|
+
"SERVICE_ERROR",
|
|
426
|
+
{ status: response.status, error }
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const data = await response.json();
|
|
430
|
+
return { data, rateLimit };
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (error instanceof IntegrationError) {
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
throw new IntegrationError(
|
|
436
|
+
`GitHub API request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
437
|
+
"NETWORK_ERROR",
|
|
438
|
+
{ error }
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function createPR(params, config) {
|
|
443
|
+
const { title, body, baseBranch, headBranch, draft = false } = params;
|
|
444
|
+
const endpoint = `/repos/${config.owner}/${config.repo}/pulls`;
|
|
445
|
+
const { data } = await githubFetch(endpoint, config, {
|
|
446
|
+
method: "POST",
|
|
447
|
+
body: JSON.stringify({
|
|
448
|
+
title,
|
|
449
|
+
body,
|
|
450
|
+
base: baseBranch,
|
|
451
|
+
head: headBranch,
|
|
452
|
+
draft
|
|
453
|
+
})
|
|
454
|
+
});
|
|
455
|
+
return data;
|
|
456
|
+
}
|
|
457
|
+
async function getPRComments(params, config) {
|
|
458
|
+
const { prNumber } = params;
|
|
459
|
+
const endpoint = `/repos/${config.owner}/${config.repo}/pulls/${prNumber}/comments`;
|
|
460
|
+
const { data } = await githubFetch(endpoint, config);
|
|
461
|
+
return data;
|
|
462
|
+
}
|
|
463
|
+
async function listIssues(params, config) {
|
|
464
|
+
const { state = "open", labels = [], limit = 30 } = params;
|
|
465
|
+
const queryParams = new URLSearchParams({
|
|
466
|
+
state,
|
|
467
|
+
per_page: Math.min(limit, 100).toString()
|
|
468
|
+
});
|
|
469
|
+
if (labels.length > 0) {
|
|
470
|
+
queryParams.append("labels", labels.join(","));
|
|
471
|
+
}
|
|
472
|
+
const endpoint = `/repos/${config.owner}/${config.repo}/issues?${queryParams}`;
|
|
473
|
+
const { data } = await githubFetch(endpoint, config);
|
|
474
|
+
return data;
|
|
475
|
+
}
|
|
476
|
+
async function getIssue(params, config) {
|
|
477
|
+
const { issueNumber } = params;
|
|
478
|
+
const issueEndpoint = `/repos/${config.owner}/${config.repo}/issues/${issueNumber}`;
|
|
479
|
+
const { data: issue } = await githubFetch(issueEndpoint, config);
|
|
480
|
+
const commentsEndpoint = `/repos/${config.owner}/${config.repo}/issues/${issueNumber}/comments`;
|
|
481
|
+
const { data: comments } = await githubFetch(commentsEndpoint, config);
|
|
482
|
+
return { issue, comments };
|
|
483
|
+
}
|
|
484
|
+
async function getRateLimit(config) {
|
|
485
|
+
const endpoint = "/rate_limit";
|
|
486
|
+
const { data } = await githubFetch(endpoint, config);
|
|
487
|
+
return data.rate;
|
|
488
|
+
}
|
|
489
|
+
async function listCommits(config, options) {
|
|
490
|
+
const params = new URLSearchParams();
|
|
491
|
+
if (options?.path) params.set("path", options.path);
|
|
492
|
+
if (options?.perPage) params.set("per_page", options.perPage.toString());
|
|
493
|
+
if (options?.page) params.set("page", options.page.toString());
|
|
494
|
+
const queryStr = params.toString();
|
|
495
|
+
const endpoint = `/repos/${config.owner}/${config.repo}/commits${queryStr ? `?${queryStr}` : ""}`;
|
|
496
|
+
const { data } = await githubFetch(endpoint, config);
|
|
497
|
+
return data.map((item) => ({
|
|
498
|
+
sha: item.sha,
|
|
499
|
+
message: item.commit.message,
|
|
500
|
+
author: item.commit.author,
|
|
501
|
+
stats: item.stats
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
async function getCommitDiff(config, sha) {
|
|
505
|
+
const url = `https://api.github.com/repos/${config.owner}/${config.repo}/commits/${sha}`;
|
|
506
|
+
const response = await fetch(url, {
|
|
507
|
+
headers: {
|
|
508
|
+
Authorization: `Bearer ${config.token}`,
|
|
509
|
+
Accept: "application/vnd.github.diff",
|
|
510
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
if (!response.ok) {
|
|
514
|
+
throw new IntegrationError(
|
|
515
|
+
`GitHub API request failed: ${response.statusText}`,
|
|
516
|
+
"SERVICE_ERROR",
|
|
517
|
+
{ status: response.status }
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
return response.text();
|
|
521
|
+
}
|
|
522
|
+
async function getFileAtCommit(config, path, sha) {
|
|
523
|
+
const endpoint = `/repos/${config.owner}/${config.repo}/contents/${path}?ref=${sha}`;
|
|
524
|
+
try {
|
|
525
|
+
const response = await fetch(`https://api.github.com${endpoint}`, {
|
|
526
|
+
headers: {
|
|
527
|
+
Authorization: `Bearer ${config.token}`,
|
|
528
|
+
Accept: "application/vnd.github.raw+json",
|
|
529
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
if (!response.ok) {
|
|
533
|
+
if (response.status === 404) return null;
|
|
534
|
+
throw new Error(`${response.status}: ${response.statusText}`);
|
|
535
|
+
}
|
|
536
|
+
return response.text();
|
|
537
|
+
} catch (error) {
|
|
538
|
+
if (error instanceof IntegrationError) throw error;
|
|
539
|
+
throw new IntegrationError(
|
|
540
|
+
`Failed to get file at commit: ${error instanceof Error ? error.message : String(error)}`,
|
|
541
|
+
"SERVICE_ERROR",
|
|
542
|
+
{ path, sha, error }
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function parseRepoUrl(repoUrl) {
|
|
547
|
+
try {
|
|
548
|
+
const url = new URL(repoUrl);
|
|
549
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
550
|
+
if (pathParts.length < 2) {
|
|
551
|
+
throw new Error("Invalid repository URL format");
|
|
552
|
+
}
|
|
553
|
+
const owner = pathParts[0];
|
|
554
|
+
const repo = pathParts[1].replace(/\.git$/, "");
|
|
555
|
+
return { owner, repo };
|
|
556
|
+
} catch (error) {
|
|
557
|
+
throw new IntegrationError(
|
|
558
|
+
`Failed to parse repository URL: ${repoUrl}`,
|
|
559
|
+
"VALIDATION_ERROR",
|
|
560
|
+
{ error }
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/integrations/github/index.ts
|
|
566
|
+
var GitHubIntegration = class extends BaseIntegration {
|
|
567
|
+
constructor(config) {
|
|
568
|
+
super(config);
|
|
569
|
+
this.token = config.env.GITHUB_TOKEN;
|
|
570
|
+
if (!this.token) {
|
|
571
|
+
throw new Error("GITHUB_TOKEN not configured");
|
|
572
|
+
}
|
|
573
|
+
this.owner = config.env.GITHUB_OWNER || "";
|
|
574
|
+
this.repo = config.env.GITHUB_REPO || "";
|
|
575
|
+
this.workDir = config.env.GITHUB_WORK_DIR || process.cwd();
|
|
576
|
+
this.logger.info("GitHub integration initialized", {
|
|
577
|
+
owner: this.owner,
|
|
578
|
+
repo: this.repo
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Execute a GitHub action
|
|
583
|
+
*/
|
|
584
|
+
async execute(action, params) {
|
|
585
|
+
const validation = this.validateParams(action, params);
|
|
586
|
+
if (!validation.valid) {
|
|
587
|
+
return {
|
|
588
|
+
success: false,
|
|
589
|
+
error: {
|
|
590
|
+
name: "IntegrationError",
|
|
591
|
+
message: "Validation failed",
|
|
592
|
+
code: "VALIDATION_ERROR",
|
|
593
|
+
details: validation.errors
|
|
594
|
+
},
|
|
595
|
+
metadata: this.createMetadata(action, 0)
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const startTime = Date.now();
|
|
599
|
+
let retries = 0;
|
|
600
|
+
try {
|
|
601
|
+
let data;
|
|
602
|
+
switch (action) {
|
|
603
|
+
case "cloneRepo":
|
|
604
|
+
data = await this.executeWithRetry(
|
|
605
|
+
() => this.cloneRepo(params)
|
|
606
|
+
);
|
|
607
|
+
break;
|
|
608
|
+
case "createBranch":
|
|
609
|
+
data = await this.executeWithRetry(
|
|
610
|
+
() => this.createBranch(params)
|
|
611
|
+
);
|
|
612
|
+
break;
|
|
613
|
+
case "commit":
|
|
614
|
+
data = await this.executeWithRetry(
|
|
615
|
+
() => this.commit(params)
|
|
616
|
+
);
|
|
617
|
+
break;
|
|
618
|
+
case "push":
|
|
619
|
+
data = await this.executeWithRetry(
|
|
620
|
+
() => this.push(params)
|
|
621
|
+
);
|
|
622
|
+
break;
|
|
623
|
+
case "createPR":
|
|
624
|
+
data = await this.executeWithRetry(
|
|
625
|
+
() => this.createPR(params)
|
|
626
|
+
);
|
|
627
|
+
break;
|
|
628
|
+
case "getPRComments":
|
|
629
|
+
data = await this.executeWithRetry(
|
|
630
|
+
() => this.getPRComments(params)
|
|
631
|
+
);
|
|
632
|
+
break;
|
|
633
|
+
case "listIssues":
|
|
634
|
+
data = await this.executeWithRetry(
|
|
635
|
+
() => this.listIssues(params)
|
|
636
|
+
);
|
|
637
|
+
break;
|
|
638
|
+
case "getIssue":
|
|
639
|
+
data = await this.executeWithRetry(
|
|
640
|
+
() => this.getIssue(params)
|
|
641
|
+
);
|
|
642
|
+
break;
|
|
643
|
+
default:
|
|
644
|
+
throw new Error(`Unknown GitHub action: ${action}`);
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
success: true,
|
|
648
|
+
data,
|
|
649
|
+
metadata: this.createMetadata(action, Date.now() - startTime, retries)
|
|
650
|
+
};
|
|
651
|
+
} catch (error) {
|
|
652
|
+
return this.handleError(action, error);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Clone a repository
|
|
657
|
+
*/
|
|
658
|
+
async cloneRepo(params) {
|
|
659
|
+
this.logger.debug("Cloning repository", { repoUrl: params.repoUrl });
|
|
660
|
+
if (!this.owner || !this.repo) {
|
|
661
|
+
const parsed = parseRepoUrl(params.repoUrl);
|
|
662
|
+
this.owner = parsed.owner;
|
|
663
|
+
this.repo = parsed.repo;
|
|
664
|
+
}
|
|
665
|
+
await cloneRepo(params, this.token);
|
|
666
|
+
return {
|
|
667
|
+
message: `Successfully cloned ${params.repoUrl} to ${params.targetDir}`
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Create a branch
|
|
672
|
+
*/
|
|
673
|
+
async createBranch(params) {
|
|
674
|
+
this.logger.debug("Creating branch", { branchName: params.branchName });
|
|
675
|
+
await createBranch(params, this.workDir);
|
|
676
|
+
return {
|
|
677
|
+
message: `Successfully created branch: ${params.branchName}`
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Commit changes
|
|
682
|
+
*/
|
|
683
|
+
async commit(params) {
|
|
684
|
+
this.logger.debug("Committing changes", { message: params.message });
|
|
685
|
+
await commit(params, this.workDir);
|
|
686
|
+
return {
|
|
687
|
+
message: `Successfully committed changes: ${params.message}`
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Push branch
|
|
692
|
+
*/
|
|
693
|
+
async push(params) {
|
|
694
|
+
this.logger.debug("Pushing branch", { branchName: params.branchName });
|
|
695
|
+
await push(params, this.workDir, this.token);
|
|
696
|
+
return {
|
|
697
|
+
message: `Successfully pushed branch: ${params.branchName}`
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Create a pull request
|
|
702
|
+
*/
|
|
703
|
+
async createPR(params) {
|
|
704
|
+
this.logger.debug("Creating pull request", { title: params.title });
|
|
705
|
+
const apiConfig = this.getAPIConfig();
|
|
706
|
+
const pr = await createPR(params, apiConfig);
|
|
707
|
+
this.logger.info("Pull request created", { number: pr.number, url: pr.url });
|
|
708
|
+
return pr;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Get PR comments
|
|
712
|
+
*/
|
|
713
|
+
async getPRComments(params) {
|
|
714
|
+
this.logger.debug("Getting PR comments", { prNumber: params.prNumber });
|
|
715
|
+
const apiConfig = this.getAPIConfig();
|
|
716
|
+
const comments = await getPRComments(params, apiConfig);
|
|
717
|
+
return { comments };
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* List issues
|
|
721
|
+
*/
|
|
722
|
+
async listIssues(params) {
|
|
723
|
+
this.logger.debug("Listing issues", params);
|
|
724
|
+
const apiConfig = this.getAPIConfig();
|
|
725
|
+
const issues = await listIssues(params, apiConfig);
|
|
726
|
+
return { issues };
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Get issue details
|
|
730
|
+
*/
|
|
731
|
+
async getIssue(params) {
|
|
732
|
+
this.logger.debug("Getting issue", { issueNumber: params.issueNumber });
|
|
733
|
+
const apiConfig = this.getAPIConfig();
|
|
734
|
+
const result = await getIssue(params, apiConfig);
|
|
735
|
+
return result;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Get API config for GitHub API calls
|
|
739
|
+
*/
|
|
740
|
+
getAPIConfig() {
|
|
741
|
+
if (!this.owner || !this.repo) {
|
|
742
|
+
throw new Error(
|
|
743
|
+
"GitHub owner and repo must be configured. Either set GITHUB_OWNER/GITHUB_REPO or clone a repository first."
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
token: this.token,
|
|
748
|
+
owner: this.owner,
|
|
749
|
+
repo: this.repo
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
export { GitHubIntegration, cloneRepo, commit, createBranch, createPR, getCommitDiff, getCurrentBranch, getFileAtCommit, getIssue, getPRComments, getRateLimit, hasUncommittedChanges, listCommits, listIssues, parseRepoUrl, push };
|
|
755
|
+
//# sourceMappingURL=index.js.map
|
|
756
|
+
//# sourceMappingURL=index.js.map
|