@manojkmfsi/monodog 1.1.28 → 1.1.30
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 +64 -0
- package/check-db.js +58 -0
- package/create-test-session.js +50 -0
- package/dev-server.pid +1 -0
- package/dist/controllers/pipeline-controller.js +329 -0
- package/dist/controllers/publish-controller.js +119 -7
- package/dist/middleware/auth-middleware.js +1 -0
- package/dist/middleware/server-startup.js +19 -0
- package/dist/routes/auth-routes.js +2 -0
- package/dist/routes/pipeline-routes.js +76 -0
- package/dist/services/changeset-service.js +2 -13
- package/dist/services/github-actions-service.js +552 -0
- package/dist/services/pipeline-service.js +356 -0
- package/dist/types/github-actions.js +6 -0
- package/monodog-dashboard/dist/assets/index-6NrFUGfK.js +15 -0
- package/monodog-dashboard/dist/assets/index-6NrFUGfK.js.map +1 -0
- package/monodog-dashboard/dist/assets/index-DcvKt8qx.css +1 -0
- package/monodog-dashboard/dist/index.html +2 -2
- package/package.json +2 -2
- package/prisma/migrations/{20260118042615_init_database → 20260224091033_init_db}/migration.sql +58 -0
- package/prisma/schema/pipeline-audit-log.prisma +22 -0
- package/prisma/schema/release-pipeline.prisma +25 -0
- package/monodog-dashboard/dist/assets/index-7G3SCgcz.css +0 -1
- package/monodog-dashboard/dist/assets/index-BMoE990p.js +0 -13
- package/monodog-dashboard/dist/assets/index-BMoE990p.js.map +0 -1
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Actions API Service
|
|
4
|
+
* Handles GitHub Actions workflow management, runs, jobs, and logs
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.listWorkflows = listWorkflows;
|
|
11
|
+
exports.getWorkflowRuns = getWorkflowRuns;
|
|
12
|
+
exports.getWorkflowRun = getWorkflowRun;
|
|
13
|
+
exports.getWorkflowRunJobs = getWorkflowRunJobs;
|
|
14
|
+
exports.getJobLogs = getJobLogs;
|
|
15
|
+
exports.parseJobLogs = parseJobLogs;
|
|
16
|
+
exports.triggerWorkflow = triggerWorkflow;
|
|
17
|
+
exports.cancelWorkflowRun = cancelWorkflowRun;
|
|
18
|
+
exports.rerunWorkflow = rerunWorkflow;
|
|
19
|
+
const https_1 = __importDefault(require("https"));
|
|
20
|
+
const logger_1 = require("../middleware/logger");
|
|
21
|
+
const GITHUB_API_BASE = 'api.github.com';
|
|
22
|
+
/**
|
|
23
|
+
* Make an HTTPS request to GitHub API
|
|
24
|
+
*/
|
|
25
|
+
function makeGitHubRequest(options, data) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const request = https_1.default.request(options, (response) => {
|
|
28
|
+
let body = '';
|
|
29
|
+
const rateLimit = {
|
|
30
|
+
limit: parseInt(response.headers['x-ratelimit-limit']) || 5000,
|
|
31
|
+
remaining: parseInt(response.headers['x-ratelimit-remaining']) || 0,
|
|
32
|
+
reset: parseInt(response.headers['x-ratelimit-reset']) || 0,
|
|
33
|
+
used: parseInt(response.headers['x-ratelimit-used']) || 0,
|
|
34
|
+
};
|
|
35
|
+
response.on('data', (chunk) => {
|
|
36
|
+
body += chunk;
|
|
37
|
+
});
|
|
38
|
+
response.on('end', () => {
|
|
39
|
+
try {
|
|
40
|
+
if (response.statusCode && response.statusCode >= 400) {
|
|
41
|
+
reject(new Error(`GitHub API error: ${response.statusCode} - ${body}`));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const result = body ? JSON.parse(body) : {};
|
|
45
|
+
resolve({ data: result, rateLimit });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
reject(new Error(`Failed to parse GitHub API response: ${error}`));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
request.on('error', (error) => {
|
|
54
|
+
logger_1.AppLogger.error(`GitHub API request failed: ${error.message}`);
|
|
55
|
+
reject(error);
|
|
56
|
+
});
|
|
57
|
+
request.setTimeout(15000, () => {
|
|
58
|
+
request.destroy();
|
|
59
|
+
reject(new Error('GitHub API request timeout'));
|
|
60
|
+
});
|
|
61
|
+
if (data) {
|
|
62
|
+
request.write(data);
|
|
63
|
+
}
|
|
64
|
+
request.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* List available workflows in a repository
|
|
69
|
+
*/
|
|
70
|
+
async function listWorkflows(owner, repo, accessToken) {
|
|
71
|
+
const path = `/repos/${owner}/${repo}/actions/workflows`;
|
|
72
|
+
const requestOptions = {
|
|
73
|
+
hostname: GITHUB_API_BASE,
|
|
74
|
+
path,
|
|
75
|
+
method: 'GET',
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${accessToken}`,
|
|
78
|
+
'User-Agent': 'MonoDog',
|
|
79
|
+
Accept: 'application/vnd.github+json',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
const { data, rateLimit } = await makeGitHubRequest(requestOptions);
|
|
84
|
+
return {
|
|
85
|
+
workflows: data.workflows,
|
|
86
|
+
rateLimit,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger_1.AppLogger.error(`Failed to list workflows: ${error}`);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get workflow runs for a repository
|
|
96
|
+
*/
|
|
97
|
+
async function getWorkflowRuns(owner, repo, accessToken, options) {
|
|
98
|
+
const params = new URLSearchParams();
|
|
99
|
+
// Determine which workflow to filter by
|
|
100
|
+
let workflowToFilter = null;
|
|
101
|
+
// Prioritize numeric workflow ID - it's the most reliable GitHub API parameter
|
|
102
|
+
if (options?.workflowId && Number(options.workflowId) !== 1) {
|
|
103
|
+
params.append('workflow_id', String(options.workflowId));
|
|
104
|
+
logger_1.AppLogger.info(`getWorkflowRuns: Using numeric workflow_id=${options.workflowId} (most reliable)`);
|
|
105
|
+
}
|
|
106
|
+
else if (options?.workflowPath) {
|
|
107
|
+
// Fall back to full workflow path if numeric ID is not valid
|
|
108
|
+
// Use the full path (.github/workflows/release.yml) not just the filename
|
|
109
|
+
workflowToFilter = options.workflowPath;
|
|
110
|
+
logger_1.AppLogger.info(`getWorkflowRuns: Using full workflowPath=${options.workflowPath}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Last resort: default to release.yml workflow
|
|
114
|
+
workflowToFilter = '.github/workflows/release.yml';
|
|
115
|
+
logger_1.AppLogger.warn(`getWorkflowRuns: Using default workflow path (no valid ID or path provided)`);
|
|
116
|
+
}
|
|
117
|
+
// Add the workflow filter if we have one and didn't already use workflow_id
|
|
118
|
+
if (workflowToFilter && !params.has('workflow_id')) {
|
|
119
|
+
params.append('workflow', workflowToFilter);
|
|
120
|
+
logger_1.AppLogger.info(`getWorkflowRuns: Added workflow filter=${workflowToFilter} (using path parameter)`);
|
|
121
|
+
}
|
|
122
|
+
if (options?.status) {
|
|
123
|
+
params.append('status', options.status);
|
|
124
|
+
}
|
|
125
|
+
if (options?.conclusion) {
|
|
126
|
+
params.append('conclusion', options.conclusion);
|
|
127
|
+
}
|
|
128
|
+
params.append('page', String(options?.page || 1));
|
|
129
|
+
params.append('per_page', String(options?.per_page || 30));
|
|
130
|
+
const path = `/repos/${owner}/${repo}/actions/runs?${params.toString()}`;
|
|
131
|
+
const requestOptions = {
|
|
132
|
+
hostname: GITHUB_API_BASE,
|
|
133
|
+
path,
|
|
134
|
+
method: 'GET',
|
|
135
|
+
headers: {
|
|
136
|
+
Authorization: `Bearer ${accessToken}`,
|
|
137
|
+
'User-Agent': 'MonoDog',
|
|
138
|
+
Accept: 'application/vnd.github+json',
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
try {
|
|
142
|
+
const fullUrl = `https://${GITHUB_API_BASE}${path}`;
|
|
143
|
+
logger_1.AppLogger.info(`GitHub API Request: GET ${fullUrl.substring(0, 150)}...`);
|
|
144
|
+
const { data, rateLimit } = await makeGitHubRequest(requestOptions);
|
|
145
|
+
logger_1.AppLogger.info(`GitHub API Response: total_count=${data.total_count}, returned ${data.workflow_runs.length} runs`);
|
|
146
|
+
if (workflowToFilter || (options?.workflowId && Number(options.workflowId) !== 1)) {
|
|
147
|
+
logger_1.AppLogger.info(`Filtered request returned ${data.workflow_runs.length} runs (workflow=${workflowToFilter})`);
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
runs: data.workflow_runs,
|
|
151
|
+
totalCount: data.total_count,
|
|
152
|
+
rateLimit,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
logger_1.AppLogger.error(`Failed to get workflow runs: ${error}`);
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get specific workflow run
|
|
162
|
+
*/
|
|
163
|
+
async function getWorkflowRun(owner, repo, runId, accessToken) {
|
|
164
|
+
const path = `/repos/${owner}/${repo}/actions/runs/${runId}`;
|
|
165
|
+
const requestOptions = {
|
|
166
|
+
hostname: GITHUB_API_BASE,
|
|
167
|
+
path,
|
|
168
|
+
method: 'GET',
|
|
169
|
+
headers: {
|
|
170
|
+
Authorization: `Bearer ${accessToken}`,
|
|
171
|
+
'User-Agent': 'MonoDog',
|
|
172
|
+
Accept: 'application/vnd.github+json',
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
try {
|
|
176
|
+
const { data, rateLimit } = await makeGitHubRequest(requestOptions);
|
|
177
|
+
return { run: data, rateLimit };
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
logger_1.AppLogger.error(`Failed to get workflow run ${runId}: ${error}`);
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get jobs for a workflow run
|
|
186
|
+
*/
|
|
187
|
+
async function getWorkflowRunJobs(owner, repo, runId, accessToken, page = 1, perPage = 30) {
|
|
188
|
+
const params = new URLSearchParams();
|
|
189
|
+
params.append('page', String(page));
|
|
190
|
+
params.append('per_page', String(perPage));
|
|
191
|
+
const path = `/repos/${owner}/${repo}/actions/runs/${runId}/jobs?${params.toString()}`;
|
|
192
|
+
const requestOptions = {
|
|
193
|
+
hostname: GITHUB_API_BASE,
|
|
194
|
+
path,
|
|
195
|
+
method: 'GET',
|
|
196
|
+
headers: {
|
|
197
|
+
Authorization: `Bearer ${accessToken}`,
|
|
198
|
+
'User-Agent': 'MonoDog',
|
|
199
|
+
Accept: 'application/vnd.github+json',
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
try {
|
|
203
|
+
const { data, rateLimit } = await makeGitHubRequest(requestOptions);
|
|
204
|
+
return {
|
|
205
|
+
jobs: data.jobs,
|
|
206
|
+
totalCount: data.total_count,
|
|
207
|
+
rateLimit,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
logger_1.AppLogger.error(`Failed to get jobs for run ${runId}: ${error}`);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get logs for a specific job
|
|
217
|
+
* Returns raw logs that need to be parsed
|
|
218
|
+
*/
|
|
219
|
+
async function getJobLogs(owner, repo, jobId, accessToken) {
|
|
220
|
+
const path = `/repos/${owner}/${repo}/actions/jobs/${jobId}/logs`;
|
|
221
|
+
logger_1.AppLogger.info(`Fetching job logs from GitHub: owner=${owner}, repo=${repo}, jobId=${jobId}`);
|
|
222
|
+
const requestOptions = {
|
|
223
|
+
hostname: GITHUB_API_BASE,
|
|
224
|
+
path,
|
|
225
|
+
method: 'GET',
|
|
226
|
+
headers: {
|
|
227
|
+
Authorization: `Bearer ${accessToken}`,
|
|
228
|
+
'User-Agent': 'MonoDog',
|
|
229
|
+
Accept: 'application/vnd.github.raw',
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const request = https_1.default.request(requestOptions, (response) => {
|
|
234
|
+
let body = '';
|
|
235
|
+
// Handle redirects
|
|
236
|
+
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303) {
|
|
237
|
+
const location = response.headers.location;
|
|
238
|
+
logger_1.AppLogger.info(`GitHub redirected logs request to: ${location}`);
|
|
239
|
+
if (location) {
|
|
240
|
+
// Recursively follow redirect with GET
|
|
241
|
+
const url = new URL(location);
|
|
242
|
+
const redirectOptions = {
|
|
243
|
+
hostname: url.hostname,
|
|
244
|
+
path: url.pathname + url.search,
|
|
245
|
+
method: 'GET',
|
|
246
|
+
headers: {
|
|
247
|
+
'User-Agent': 'MonoDog',
|
|
248
|
+
// Authorization: `Bearer ${accessToken}`,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
const redirectRequest = https_1.default.request(redirectOptions, (redirectResponse) => {
|
|
252
|
+
let redirectBody = '';
|
|
253
|
+
redirectResponse.on('data', (chunk) => {
|
|
254
|
+
redirectBody += chunk;
|
|
255
|
+
});
|
|
256
|
+
redirectResponse.on('end', () => {
|
|
257
|
+
const rateLimit = {
|
|
258
|
+
limit: parseInt(redirectResponse.headers['x-ratelimit-limit']) || 5000,
|
|
259
|
+
remaining: parseInt(redirectResponse.headers['x-ratelimit-remaining']) || 0,
|
|
260
|
+
reset: parseInt(redirectResponse.headers['x-ratelimit-reset']) || 0,
|
|
261
|
+
used: parseInt(redirectResponse.headers['x-ratelimit-used']) || 0,
|
|
262
|
+
};
|
|
263
|
+
// Check for errors in the redirect response too
|
|
264
|
+
if (redirectResponse.statusCode && redirectResponse.statusCode >= 400) {
|
|
265
|
+
logger_1.AppLogger.error(`GitHub API error after redirect ${redirectResponse.statusCode}: ${redirectBody.substring(0, 200)}`);
|
|
266
|
+
reject(new Error(`GitHub API error: ${redirectResponse.statusCode} - ${redirectBody}`));
|
|
267
|
+
}
|
|
268
|
+
else if (!redirectBody || redirectBody.trim().length === 0) {
|
|
269
|
+
logger_1.AppLogger.warn(`Job logs are empty after redirect (status=${redirectResponse.statusCode})`);
|
|
270
|
+
resolve({ logs: '', rateLimit });
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
logger_1.AppLogger.info(`Fetched ${redirectBody.length} characters of job logs from redirect`);
|
|
274
|
+
resolve({ logs: redirectBody, rateLimit });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
redirectRequest.on('error', (error) => {
|
|
279
|
+
logger_1.AppLogger.error(`Failed to follow redirect for job logs: ${error.message}`);
|
|
280
|
+
reject(error);
|
|
281
|
+
});
|
|
282
|
+
redirectRequest.end();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const rateLimit = {
|
|
287
|
+
limit: parseInt(response.headers['x-ratelimit-limit']) || 5000,
|
|
288
|
+
remaining: parseInt(response.headers['x-ratelimit-remaining']) || 0,
|
|
289
|
+
reset: parseInt(response.headers['x-ratelimit-reset']) || 0,
|
|
290
|
+
used: parseInt(response.headers['x-ratelimit-used']) || 0,
|
|
291
|
+
};
|
|
292
|
+
response.on('data', (chunk) => {
|
|
293
|
+
body += chunk;
|
|
294
|
+
});
|
|
295
|
+
response.on('end', () => {
|
|
296
|
+
try {
|
|
297
|
+
logger_1.AppLogger.debug(`Job logs response: status=${response.statusCode}, length=${body.length}`);
|
|
298
|
+
if (response.statusCode && response.statusCode >= 400) {
|
|
299
|
+
logger_1.AppLogger.error(`GitHub API error ${response.statusCode}: ${body.substring(0, 200)}`);
|
|
300
|
+
reject(new Error(`GitHub API error: ${response.statusCode} - ${body}`));
|
|
301
|
+
}
|
|
302
|
+
else if (!body || body.trim().length === 0) {
|
|
303
|
+
logger_1.AppLogger.warn(`Job logs are empty (status=${response.statusCode})`);
|
|
304
|
+
resolve({ logs: '', rateLimit });
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
logger_1.AppLogger.info(`Fetched ${body.length} characters of job logs`);
|
|
308
|
+
resolve({ logs: body, rateLimit });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
reject(new Error(`Failed to parse job logs: ${error}`));
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
request.on('error', (error) => {
|
|
317
|
+
logger_1.AppLogger.error(`Failed to fetch job logs: ${error.message}`);
|
|
318
|
+
reject(error);
|
|
319
|
+
});
|
|
320
|
+
request.setTimeout(30000, () => {
|
|
321
|
+
request.destroy();
|
|
322
|
+
reject(new Error('Job logs request timeout'));
|
|
323
|
+
});
|
|
324
|
+
request.end();
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Parse raw GitHub Actions logs into structured step logs
|
|
329
|
+
* GitHub Actions logs have format:
|
|
330
|
+
* \u001b[36m##[group]Group Name\u001b[0m
|
|
331
|
+
* Log line here
|
|
332
|
+
* \u001b[36m##[endgroup]\u001b[0m
|
|
333
|
+
*/
|
|
334
|
+
function parseJobLogs(rawLogs, job) {
|
|
335
|
+
const steps = [];
|
|
336
|
+
const lines = rawLogs.split('\n');
|
|
337
|
+
let currentStep = null;
|
|
338
|
+
let lineNumber = 0;
|
|
339
|
+
for (const line of lines) {
|
|
340
|
+
lineNumber++;
|
|
341
|
+
// Check for group start (step start)
|
|
342
|
+
if (line.includes('##[group]')) {
|
|
343
|
+
// Save previous step if exists
|
|
344
|
+
if (currentStep) {
|
|
345
|
+
steps.push(currentStep);
|
|
346
|
+
}
|
|
347
|
+
// Extract step name
|
|
348
|
+
// eslint-disable-next-line no-control-regex
|
|
349
|
+
const nameMatch = line.match(/##\[group\](.*?)(\u001b\[0m)?$/);
|
|
350
|
+
const stepName = nameMatch ? nameMatch[1].trim() : `Step ${steps.length + 1}`;
|
|
351
|
+
// Find matching step in workflow
|
|
352
|
+
const workflowStep = job.steps.find((s) => s.name.toLowerCase() === stepName.toLowerCase());
|
|
353
|
+
currentStep = {
|
|
354
|
+
stepNumber: steps.length + 1,
|
|
355
|
+
stepName,
|
|
356
|
+
startedAt: workflowStep?.started_at || null,
|
|
357
|
+
completedAt: workflowStep?.completed_at || null,
|
|
358
|
+
conclusion: workflowStep?.conclusion || null,
|
|
359
|
+
status: workflowStep?.status || 'queued',
|
|
360
|
+
logs: [],
|
|
361
|
+
expanded: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
else if (line.includes('##[endgroup]')) {
|
|
365
|
+
// Step end marker - do nothing, step will be saved on next group or end
|
|
366
|
+
}
|
|
367
|
+
else if (line.trim() && currentStep) {
|
|
368
|
+
// Add log line to current step
|
|
369
|
+
const logLine = {
|
|
370
|
+
lineNumber,
|
|
371
|
+
timestamp: extractTimestamp(line),
|
|
372
|
+
content: stripAnsiCodes(line),
|
|
373
|
+
ansiContent: line,
|
|
374
|
+
};
|
|
375
|
+
currentStep.logs.push(logLine);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Add last step if exists
|
|
379
|
+
if (currentStep) {
|
|
380
|
+
steps.push(currentStep);
|
|
381
|
+
}
|
|
382
|
+
// If no steps were parsed, create one for all logs
|
|
383
|
+
if (steps.length === 0 && lines.length > 0) {
|
|
384
|
+
steps.push({
|
|
385
|
+
stepNumber: 1,
|
|
386
|
+
stepName: 'Output',
|
|
387
|
+
startedAt: job.started_at,
|
|
388
|
+
completedAt: job.completed_at,
|
|
389
|
+
conclusion: job.conclusion,
|
|
390
|
+
status: job.status,
|
|
391
|
+
logs: lines.map((line, idx) => ({
|
|
392
|
+
lineNumber: idx + 1,
|
|
393
|
+
timestamp: extractTimestamp(line),
|
|
394
|
+
content: stripAnsiCodes(line),
|
|
395
|
+
ansiContent: line,
|
|
396
|
+
})),
|
|
397
|
+
expanded: true,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return steps;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Extract timestamp from log line
|
|
404
|
+
* GitHub format: 2024-02-11T12:34:56.123456789Z
|
|
405
|
+
*/
|
|
406
|
+
function extractTimestamp(line) {
|
|
407
|
+
const isoRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/;
|
|
408
|
+
const match = line.match(isoRegex);
|
|
409
|
+
return match ? match[0] : new Date().toISOString();
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Strip ANSI codes from text
|
|
413
|
+
* Keeps the text content but removes styling
|
|
414
|
+
*/
|
|
415
|
+
function stripAnsiCodes(text) {
|
|
416
|
+
// eslint-disable-next-line no-control-regex
|
|
417
|
+
return text.replace(/\u001b\[[0-9;]*m/g, '').replace(/\u001b\[K/g, '');
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Trigger a workflow run
|
|
421
|
+
*/
|
|
422
|
+
async function triggerWorkflow(accessToken, request) {
|
|
423
|
+
// Ensure workflow is a string - convert number if needed
|
|
424
|
+
let workflowIdentifier = String(request.workflow);
|
|
425
|
+
// If it looks like a path (contains slashes), extract filename
|
|
426
|
+
if (workflowIdentifier.includes('/')) {
|
|
427
|
+
// Convert .github/workflows/ci.yml to ci.yml
|
|
428
|
+
workflowIdentifier = workflowIdentifier.split('/').pop() || workflowIdentifier;
|
|
429
|
+
}
|
|
430
|
+
const path = `/repos/${request.owner}/${request.repo}/actions/workflows/${workflowIdentifier}/dispatches`;
|
|
431
|
+
const payload = JSON.stringify({
|
|
432
|
+
ref: request.ref,
|
|
433
|
+
inputs: request.inputs || {},
|
|
434
|
+
});
|
|
435
|
+
const requestOptions = {
|
|
436
|
+
hostname: GITHUB_API_BASE,
|
|
437
|
+
path,
|
|
438
|
+
method: 'POST',
|
|
439
|
+
headers: {
|
|
440
|
+
Authorization: `Bearer ${accessToken}`,
|
|
441
|
+
'User-Agent': 'MonoDog',
|
|
442
|
+
'Content-Type': 'application/json',
|
|
443
|
+
'Content-Length': String(Buffer.byteLength(payload)),
|
|
444
|
+
Accept: 'application/vnd.github+json',
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
try {
|
|
448
|
+
logger_1.AppLogger.debug(`Triggering workflow: ${workflowIdentifier} (original: ${request.workflow}) on ${request.owner}/${request.repo} branch: ${request.ref}`);
|
|
449
|
+
const response = await makeGitHubRequest(requestOptions, payload);
|
|
450
|
+
// GitHub returns 204 No Content on success
|
|
451
|
+
logger_1.AppLogger.info(`Workflow triggered successfully: ${workflowIdentifier}`);
|
|
452
|
+
return {
|
|
453
|
+
response: {
|
|
454
|
+
success: true,
|
|
455
|
+
message: 'Workflow triggered successfully',
|
|
456
|
+
},
|
|
457
|
+
rateLimit: response.rateLimit,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
logger_1.AppLogger.error(`Failed to trigger workflow ${workflowIdentifier}: ${error}`);
|
|
462
|
+
return {
|
|
463
|
+
response: {
|
|
464
|
+
success: false,
|
|
465
|
+
message: `Failed to trigger workflow: ${error}`,
|
|
466
|
+
},
|
|
467
|
+
rateLimit: { limit: 0, remaining: 0, reset: 0, used: 0 },
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Cancel a workflow run
|
|
473
|
+
*/
|
|
474
|
+
async function cancelWorkflowRun(owner, repo, runId, accessToken) {
|
|
475
|
+
const path = `/repos/${owner}/${repo}/actions/runs/${runId}/cancel`;
|
|
476
|
+
const requestOptions = {
|
|
477
|
+
hostname: GITHUB_API_BASE,
|
|
478
|
+
path,
|
|
479
|
+
method: 'POST',
|
|
480
|
+
headers: {
|
|
481
|
+
Authorization: `Bearer ${accessToken}`,
|
|
482
|
+
'User-Agent': 'MonoDog',
|
|
483
|
+
Accept: 'application/vnd.github+json',
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
try {
|
|
487
|
+
const { rateLimit } = await makeGitHubRequest(requestOptions);
|
|
488
|
+
return { success: true, rateLimit };
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
logger_1.AppLogger.error(`Failed to cancel workflow run: ${error}`);
|
|
492
|
+
return {
|
|
493
|
+
success: false,
|
|
494
|
+
rateLimit: { limit: 0, remaining: 0, reset: 0, used: 0 },
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Re-run a workflow run
|
|
500
|
+
*/
|
|
501
|
+
async function rerunWorkflow(owner, repo, runId, accessToken, failedOnly = false) {
|
|
502
|
+
const path = failedOnly
|
|
503
|
+
? `/repos/${owner}/${repo}/actions/runs/${runId}/rerun-failed-jobs`
|
|
504
|
+
: `/repos/${owner}/${repo}/actions/runs/${runId}/rerun`;
|
|
505
|
+
const requestOptions = {
|
|
506
|
+
hostname: GITHUB_API_BASE,
|
|
507
|
+
path,
|
|
508
|
+
method: 'POST',
|
|
509
|
+
headers: {
|
|
510
|
+
Authorization: `Bearer ${accessToken}`,
|
|
511
|
+
'User-Agent': 'MonoDog',
|
|
512
|
+
Accept: 'application/vnd.github+json',
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
try {
|
|
516
|
+
const { rateLimit } = await makeGitHubRequest(requestOptions);
|
|
517
|
+
return { success: true, rateLimit };
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
logger_1.AppLogger.error(`Failed to rerun workflow: ${error}`);
|
|
521
|
+
return {
|
|
522
|
+
success: false,
|
|
523
|
+
rateLimit: { limit: 0, remaining: 0, reset: 0, used: 0 },
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Get rate limit information
|
|
529
|
+
*/
|
|
530
|
+
// export async function getRateLimit(
|
|
531
|
+
// accessToken: string
|
|
532
|
+
// ): Promise<RateLimitInfo> {
|
|
533
|
+
// const requestOptions: GitHubRequestOptions = {
|
|
534
|
+
// hostname: GITHUB_API_BASE,
|
|
535
|
+
// path: '/rate_limit',
|
|
536
|
+
// method: 'GET',
|
|
537
|
+
// headers: {
|
|
538
|
+
// Authorization: `Bearer ${accessToken}`,
|
|
539
|
+
// 'User-Agent': 'MonoDog',
|
|
540
|
+
// Accept: 'application/vnd.github+json',
|
|
541
|
+
// },
|
|
542
|
+
// };
|
|
543
|
+
// try {
|
|
544
|
+
// const { data } = await makeGitHubRequest<{
|
|
545
|
+
// resources: { core: RateLimitInfo };
|
|
546
|
+
// }>(requestOptions);
|
|
547
|
+
// return data.resources.core;
|
|
548
|
+
// } catch (error) {
|
|
549
|
+
// AppLogger.error(`Failed to get rate limit: ${error}`);
|
|
550
|
+
// return { limit: 0, remaining: 0, reset: 0, used: 0 };
|
|
551
|
+
// }
|
|
552
|
+
// }
|