@monodog/ci-status 1.0.0
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/index.d.ts +187 -0
- package/dist/index.js +634 -0
- package/dist/index.js.map +1 -0
- package/dist/libs/utils/helpers.d.ts +93 -0
- package/dist/libs/utils/helpers.js +478 -0
- package/dist/libs/utils/helpers.js.map +1 -0
- package/dist/packages/ci-status/index.d.ts +187 -0
- package/dist/packages/ci-status/index.js +634 -0
- package/dist/packages/ci-status/index.js.map +1 -0
- package/index.ts +639 -0
- package/package.json +30 -0
- package/tsconfig.json +24 -0
package/index.ts
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import { PackageInfo } from '@monodog/utils/helpers';
|
|
2
|
+
|
|
3
|
+
export interface CIProvider {
|
|
4
|
+
name: string;
|
|
5
|
+
type:
|
|
6
|
+
| 'github'
|
|
7
|
+
| 'gitlab'
|
|
8
|
+
| 'jenkins'
|
|
9
|
+
| 'circleci'
|
|
10
|
+
| 'travis'
|
|
11
|
+
| 'azure'
|
|
12
|
+
| 'custom';
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
apiToken?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CIBuild {
|
|
18
|
+
id: string;
|
|
19
|
+
status: 'success' | 'failed' | 'running' | 'pending' | 'cancelled';
|
|
20
|
+
branch: string;
|
|
21
|
+
commit: string;
|
|
22
|
+
commitMessage: string;
|
|
23
|
+
author: string;
|
|
24
|
+
startTime: Date;
|
|
25
|
+
endTime?: Date;
|
|
26
|
+
duration?: number;
|
|
27
|
+
url: string;
|
|
28
|
+
packageName?: string;
|
|
29
|
+
workflowName?: string;
|
|
30
|
+
jobName?: string;
|
|
31
|
+
steps: CIBuildStep[];
|
|
32
|
+
artifacts?: CIArtifact[];
|
|
33
|
+
coverage?: CICoverage;
|
|
34
|
+
tests?: CITestResults;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CIBuildStep {
|
|
38
|
+
name: string;
|
|
39
|
+
status: 'success' | 'failed' | 'running' | 'skipped';
|
|
40
|
+
duration: number;
|
|
41
|
+
logs?: string;
|
|
42
|
+
error?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CIArtifact {
|
|
46
|
+
name: string;
|
|
47
|
+
type: 'build' | 'test' | 'coverage' | 'documentation';
|
|
48
|
+
size: number;
|
|
49
|
+
url: string;
|
|
50
|
+
expiresAt?: Date;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CICoverage {
|
|
54
|
+
percentage: number;
|
|
55
|
+
lines: number;
|
|
56
|
+
functions: number;
|
|
57
|
+
branches: number;
|
|
58
|
+
statements: number;
|
|
59
|
+
uncoveredLines?: number[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CITestResults {
|
|
63
|
+
total: number;
|
|
64
|
+
passed: number;
|
|
65
|
+
failed: number;
|
|
66
|
+
skipped: number;
|
|
67
|
+
duration: number;
|
|
68
|
+
suites: CITestSuite[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface CITestSuite {
|
|
72
|
+
name: string;
|
|
73
|
+
status: 'pass' | 'fail' | 'skip';
|
|
74
|
+
tests: CITest[];
|
|
75
|
+
duration: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CITest {
|
|
79
|
+
name: string;
|
|
80
|
+
status: 'pass' | 'fail' | 'skip';
|
|
81
|
+
duration: number;
|
|
82
|
+
error?: string;
|
|
83
|
+
output?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CIPackageStatus {
|
|
87
|
+
packageName: string;
|
|
88
|
+
lastBuild?: CIBuild;
|
|
89
|
+
buildHistory: CIBuild[];
|
|
90
|
+
successRate: number;
|
|
91
|
+
averageDuration: number;
|
|
92
|
+
lastCommit: string;
|
|
93
|
+
lastCommitDate: Date;
|
|
94
|
+
branch: string;
|
|
95
|
+
isHealthy: boolean;
|
|
96
|
+
issues: string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CIMonorepoStatus {
|
|
100
|
+
totalPackages: number;
|
|
101
|
+
healthyPackages: number;
|
|
102
|
+
warningPackages: number;
|
|
103
|
+
errorPackages: number;
|
|
104
|
+
overallHealth: number;
|
|
105
|
+
packages: CIPackageStatus[];
|
|
106
|
+
recentBuilds: CIBuild[];
|
|
107
|
+
failedBuilds: CIBuild[];
|
|
108
|
+
coverage: {
|
|
109
|
+
overall: number;
|
|
110
|
+
packages: Record<string, number>;
|
|
111
|
+
};
|
|
112
|
+
tests: {
|
|
113
|
+
total: number;
|
|
114
|
+
passed: number;
|
|
115
|
+
failed: number;
|
|
116
|
+
successRate: number;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class CIStatusManager {
|
|
121
|
+
private providers: Map<string, CIProvider> = new Map();
|
|
122
|
+
private cache: Map<string, any> = new Map();
|
|
123
|
+
private cacheExpiry = 2 * 60 * 1000; // 2 minutes
|
|
124
|
+
|
|
125
|
+
constructor() {
|
|
126
|
+
this.initializeDefaultProviders();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Initialize default CI providers
|
|
131
|
+
*/
|
|
132
|
+
private initializeDefaultProviders(): void {
|
|
133
|
+
// GitHub Actions
|
|
134
|
+
this.addProvider({
|
|
135
|
+
name: 'GitHub Actions',
|
|
136
|
+
type: 'github',
|
|
137
|
+
baseUrl: 'https://api.github.com',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// GitLab CI
|
|
141
|
+
this.addProvider({
|
|
142
|
+
name: 'GitLab CI',
|
|
143
|
+
type: 'gitlab',
|
|
144
|
+
baseUrl: 'https://gitlab.com/api/v4',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// CircleCI
|
|
148
|
+
this.addProvider({
|
|
149
|
+
name: 'CircleCI',
|
|
150
|
+
type: 'circleci',
|
|
151
|
+
baseUrl: 'https://circleci.com/api/v2',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Add a CI provider
|
|
157
|
+
*/
|
|
158
|
+
addProvider(provider: CIProvider): void {
|
|
159
|
+
this.providers.set(provider.name, provider);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Remove a CI provider
|
|
164
|
+
*/
|
|
165
|
+
removeProvider(name: string): void {
|
|
166
|
+
this.providers.delete(name);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get all registered providers
|
|
171
|
+
*/
|
|
172
|
+
getProviders(): CIProvider[] {
|
|
173
|
+
return Array.from(this.providers.values());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fetch CI status for a specific package
|
|
178
|
+
*/
|
|
179
|
+
async getPackageStatus(
|
|
180
|
+
packageName: string,
|
|
181
|
+
providerName?: string
|
|
182
|
+
): Promise<CIPackageStatus | null> {
|
|
183
|
+
const cacheKey = `package-status-${packageName}-${providerName || 'all'}`;
|
|
184
|
+
const cached = this.getFromCache(cacheKey);
|
|
185
|
+
if (cached) {
|
|
186
|
+
return cached;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
let builds: CIBuild[] = [];
|
|
191
|
+
|
|
192
|
+
if (providerName) {
|
|
193
|
+
const provider = this.providers.get(providerName);
|
|
194
|
+
if (provider) {
|
|
195
|
+
builds = await this.fetchBuildsFromProvider(provider, packageName);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// Fetch from all providers
|
|
199
|
+
for (const provider of this.providers.values()) {
|
|
200
|
+
const providerBuilds = await this.fetchBuildsFromProvider(
|
|
201
|
+
provider,
|
|
202
|
+
packageName
|
|
203
|
+
);
|
|
204
|
+
builds.push(...providerBuilds);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (builds.length === 0) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Sort builds by start time (newest first)
|
|
213
|
+
builds.sort((a, b) => b.startTime.getTime() - a.startTime.getTime());
|
|
214
|
+
|
|
215
|
+
const lastBuild = builds[0];
|
|
216
|
+
const buildHistory = builds.slice(0, 10); // Last 10 builds
|
|
217
|
+
const successRate = this.calculateSuccessRate(builds);
|
|
218
|
+
const averageDuration = this.calculateAverageDuration(builds);
|
|
219
|
+
const isHealthy = this.determinePackageHealth(builds);
|
|
220
|
+
const issues = this.identifyIssues(builds);
|
|
221
|
+
|
|
222
|
+
const status: CIPackageStatus = {
|
|
223
|
+
packageName,
|
|
224
|
+
lastBuild,
|
|
225
|
+
buildHistory,
|
|
226
|
+
successRate,
|
|
227
|
+
averageDuration,
|
|
228
|
+
lastCommit: lastBuild.commit,
|
|
229
|
+
lastCommitDate: lastBuild.startTime,
|
|
230
|
+
branch: lastBuild.branch,
|
|
231
|
+
isHealthy,
|
|
232
|
+
issues,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
this.setCache(cacheKey, status);
|
|
236
|
+
return status;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error(`Error fetching CI status for ${packageName}:`, error);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get overall monorepo CI status
|
|
245
|
+
*/
|
|
246
|
+
async getMonorepoStatus(packages: PackageInfo[]): Promise<CIMonorepoStatus> {
|
|
247
|
+
const cacheKey = 'monorepo-ci-status';
|
|
248
|
+
const cached = this.getFromCache(cacheKey);
|
|
249
|
+
if (cached) {
|
|
250
|
+
return cached;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const packageStatuses: CIPackageStatus[] = [];
|
|
254
|
+
const allBuilds: CIBuild[] = [];
|
|
255
|
+
let totalTests = 0;
|
|
256
|
+
let passedTests = 0;
|
|
257
|
+
let failedTests = 0;
|
|
258
|
+
const packageCoverage: Record<string, number> = {};
|
|
259
|
+
|
|
260
|
+
// Get status for each package
|
|
261
|
+
for (const pkg of packages) {
|
|
262
|
+
const status = await this.getPackageStatus(pkg.name);
|
|
263
|
+
if (status) {
|
|
264
|
+
packageStatuses.push(status);
|
|
265
|
+
allBuilds.push(...status.buildHistory);
|
|
266
|
+
|
|
267
|
+
// Aggregate test results
|
|
268
|
+
if (status.lastBuild?.tests) {
|
|
269
|
+
totalTests += status.lastBuild.tests.total;
|
|
270
|
+
passedTests += status.lastBuild.tests.passed;
|
|
271
|
+
failedTests += status.lastBuild.tests.failed;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Aggregate coverage
|
|
275
|
+
if (status.lastBuild?.coverage) {
|
|
276
|
+
packageCoverage[pkg.name] = status.lastBuild.coverage.percentage;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Calculate overall metrics
|
|
282
|
+
const totalPackages = packages.length;
|
|
283
|
+
const healthyPackages = packageStatuses.filter(s => s.isHealthy).length;
|
|
284
|
+
const warningPackages = packageStatuses.filter(
|
|
285
|
+
s => !s.isHealthy && s.issues.length < 3
|
|
286
|
+
).length;
|
|
287
|
+
const errorPackages = packageStatuses.filter(
|
|
288
|
+
s => !s.isHealthy && s.issues.length >= 3
|
|
289
|
+
).length;
|
|
290
|
+
const overallHealth = (healthyPackages / totalPackages) * 100;
|
|
291
|
+
|
|
292
|
+
// Sort builds by time
|
|
293
|
+
allBuilds.sort((a, b) => b.startTime.getTime() - a.startTime.getTime());
|
|
294
|
+
const recentBuilds = allBuilds.slice(0, 20);
|
|
295
|
+
const failedBuilds = allBuilds.filter(b => b.status === 'failed');
|
|
296
|
+
|
|
297
|
+
// Calculate overall coverage
|
|
298
|
+
const coverageValues = Object.values(packageCoverage);
|
|
299
|
+
const overallCoverage =
|
|
300
|
+
coverageValues.length > 0
|
|
301
|
+
? coverageValues.reduce((sum, val) => sum + val, 0) /
|
|
302
|
+
coverageValues.length
|
|
303
|
+
: 0;
|
|
304
|
+
|
|
305
|
+
const status: CIMonorepoStatus = {
|
|
306
|
+
totalPackages,
|
|
307
|
+
healthyPackages,
|
|
308
|
+
warningPackages,
|
|
309
|
+
errorPackages,
|
|
310
|
+
overallHealth,
|
|
311
|
+
packages: packageStatuses,
|
|
312
|
+
recentBuilds,
|
|
313
|
+
failedBuilds,
|
|
314
|
+
coverage: {
|
|
315
|
+
overall: overallCoverage,
|
|
316
|
+
packages: packageCoverage,
|
|
317
|
+
},
|
|
318
|
+
tests: {
|
|
319
|
+
total: totalTests,
|
|
320
|
+
passed: passedTests,
|
|
321
|
+
failed: failedTests,
|
|
322
|
+
successRate: totalTests > 0 ? (passedTests / totalTests) * 100 : 0,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
this.setCache(cacheKey, status);
|
|
327
|
+
return status;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Fetch builds from a specific CI provider
|
|
332
|
+
*/
|
|
333
|
+
private async fetchBuildsFromProvider(
|
|
334
|
+
_provider: CIProvider,
|
|
335
|
+
packageName: string
|
|
336
|
+
): Promise<CIBuild[]> {
|
|
337
|
+
// This is a mock implementation
|
|
338
|
+
// In a real implementation, you would make API calls to the CI provider
|
|
339
|
+
|
|
340
|
+
const mockBuilds: CIBuild[] = [
|
|
341
|
+
{
|
|
342
|
+
id: `build-${Date.now()}-1`,
|
|
343
|
+
status: 'success',
|
|
344
|
+
branch: 'main',
|
|
345
|
+
commit: 'abc1234',
|
|
346
|
+
commitMessage: `feat: update ${packageName}`,
|
|
347
|
+
author: 'developer@example.com',
|
|
348
|
+
startTime: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago
|
|
349
|
+
endTime: new Date(Date.now() - 1000 * 60 * 25), // 25 minutes ago
|
|
350
|
+
duration: 5 * 60 * 1000, // 5 minutes
|
|
351
|
+
url: `https://ci.example.com/builds/build-${Date.now()}-1`,
|
|
352
|
+
packageName,
|
|
353
|
+
workflowName: 'Build and Test',
|
|
354
|
+
jobName: 'test',
|
|
355
|
+
steps: [
|
|
356
|
+
{
|
|
357
|
+
name: 'Install dependencies',
|
|
358
|
+
status: 'success',
|
|
359
|
+
duration: 30000,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'Run tests',
|
|
363
|
+
status: 'success',
|
|
364
|
+
duration: 120000,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'Build package',
|
|
368
|
+
status: 'success',
|
|
369
|
+
duration: 60000,
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
coverage: {
|
|
373
|
+
percentage: 85,
|
|
374
|
+
lines: 1000,
|
|
375
|
+
functions: 50,
|
|
376
|
+
branches: 200,
|
|
377
|
+
statements: 1200,
|
|
378
|
+
},
|
|
379
|
+
tests: {
|
|
380
|
+
total: 150,
|
|
381
|
+
passed: 145,
|
|
382
|
+
failed: 0,
|
|
383
|
+
skipped: 5,
|
|
384
|
+
duration: 120000,
|
|
385
|
+
suites: [
|
|
386
|
+
{
|
|
387
|
+
name: 'Unit Tests',
|
|
388
|
+
status: 'pass',
|
|
389
|
+
tests: [],
|
|
390
|
+
duration: 80000,
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: 'Integration Tests',
|
|
394
|
+
status: 'pass',
|
|
395
|
+
tests: [],
|
|
396
|
+
duration: 40000,
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: `build-${Date.now()}-2`,
|
|
403
|
+
status: 'failed',
|
|
404
|
+
branch: 'feature/new-feature',
|
|
405
|
+
commit: 'def5678',
|
|
406
|
+
commitMessage: `fix: resolve issue in ${packageName}`,
|
|
407
|
+
author: 'developer@example.com',
|
|
408
|
+
startTime: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
|
|
409
|
+
endTime: new Date(Date.now() - 1000 * 60 * 60 * 1.5), // 1.5 hours ago
|
|
410
|
+
duration: 30 * 60 * 1000, // 30 minutes
|
|
411
|
+
url: `https://ci.example.com/builds/build-${Date.now()}-2`,
|
|
412
|
+
packageName,
|
|
413
|
+
workflowName: 'Build and Test',
|
|
414
|
+
jobName: 'test',
|
|
415
|
+
steps: [
|
|
416
|
+
{
|
|
417
|
+
name: 'Install dependencies',
|
|
418
|
+
status: 'success',
|
|
419
|
+
duration: 30000,
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'Run tests',
|
|
423
|
+
status: 'failed',
|
|
424
|
+
duration: 120000,
|
|
425
|
+
error: 'Test suite failed with 3 failing tests',
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
tests: {
|
|
429
|
+
total: 150,
|
|
430
|
+
passed: 147,
|
|
431
|
+
failed: 3,
|
|
432
|
+
skipped: 0,
|
|
433
|
+
duration: 120000,
|
|
434
|
+
suites: [
|
|
435
|
+
{
|
|
436
|
+
name: 'Unit Tests',
|
|
437
|
+
status: 'pass',
|
|
438
|
+
tests: [],
|
|
439
|
+
duration: 80000,
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: 'Integration Tests',
|
|
443
|
+
status: 'fail',
|
|
444
|
+
tests: [],
|
|
445
|
+
duration: 40000,
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
return mockBuilds;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Calculate success rate from builds
|
|
457
|
+
*/
|
|
458
|
+
private calculateSuccessRate(builds: CIBuild[]): number {
|
|
459
|
+
if (builds.length === 0) return 0;
|
|
460
|
+
|
|
461
|
+
const successfulBuilds = builds.filter(b => b.status === 'success').length;
|
|
462
|
+
return (successfulBuilds / builds.length) * 100;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Calculate average build duration
|
|
467
|
+
*/
|
|
468
|
+
private calculateAverageDuration(builds: CIBuild[]): number {
|
|
469
|
+
if (builds.length === 0) return 0;
|
|
470
|
+
|
|
471
|
+
const completedBuilds = builds.filter(b => b.duration !== undefined);
|
|
472
|
+
if (completedBuilds.length === 0) return 0;
|
|
473
|
+
|
|
474
|
+
const totalDuration = completedBuilds.reduce(
|
|
475
|
+
(sum, b) => sum + (b.duration || 0),
|
|
476
|
+
0
|
|
477
|
+
);
|
|
478
|
+
return totalDuration / completedBuilds.length;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Determine if a package is healthy based on CI results
|
|
483
|
+
*/
|
|
484
|
+
private determinePackageHealth(builds: CIBuild[]): boolean {
|
|
485
|
+
if (builds.length === 0) return true;
|
|
486
|
+
|
|
487
|
+
const recentBuilds = builds.slice(0, 5); // Last 5 builds
|
|
488
|
+
const successRate = this.calculateSuccessRate(recentBuilds);
|
|
489
|
+
|
|
490
|
+
return successRate >= 80; // 80% success rate threshold
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Identify issues from build results
|
|
495
|
+
*/
|
|
496
|
+
private identifyIssues(builds: CIBuild[]): string[] {
|
|
497
|
+
const issues: string[] = [];
|
|
498
|
+
|
|
499
|
+
if (builds.length === 0) return issues;
|
|
500
|
+
|
|
501
|
+
const recentBuilds = builds.slice(0, 3); // Last 3 builds
|
|
502
|
+
const successRate = this.calculateSuccessRate(recentBuilds);
|
|
503
|
+
|
|
504
|
+
if (successRate < 50) {
|
|
505
|
+
issues.push('High failure rate in recent builds');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const failedBuilds = recentBuilds.filter(b => b.status === 'failed');
|
|
509
|
+
for (const build of failedBuilds) {
|
|
510
|
+
const failedSteps = build.steps.filter(s => s.status === 'failed');
|
|
511
|
+
for (const step of failedSteps) {
|
|
512
|
+
if (step.error) {
|
|
513
|
+
issues.push(`Build step '${step.name}' failed: ${step.error}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Check for long build times
|
|
519
|
+
const avgDuration = this.calculateAverageDuration(recentBuilds);
|
|
520
|
+
if (avgDuration > 10 * 60 * 1000) {
|
|
521
|
+
// 10 minutes
|
|
522
|
+
issues.push('Builds are taking longer than expected');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return issues;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get cache value if not expired
|
|
530
|
+
*/
|
|
531
|
+
private getFromCache(key: string): any {
|
|
532
|
+
const cached = this.cache.get(key);
|
|
533
|
+
if (cached && Date.now() - cached.timestamp < this.cacheExpiry) {
|
|
534
|
+
return cached.data;
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Set cache value with timestamp
|
|
541
|
+
*/
|
|
542
|
+
private setCache(key: string, data: any): void {
|
|
543
|
+
this.cache.set(key, {
|
|
544
|
+
data,
|
|
545
|
+
timestamp: Date.now(),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Clear the cache
|
|
551
|
+
*/
|
|
552
|
+
clearCache(): void {
|
|
553
|
+
this.cache.clear();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Get build logs for a specific build
|
|
558
|
+
*/
|
|
559
|
+
async getBuildLogs(buildId: string, providerName: string): Promise<string> {
|
|
560
|
+
// Mock implementation
|
|
561
|
+
return `Build logs for ${buildId} from ${providerName}\n\nStep 1: Install dependencies\n✓ Dependencies installed successfully\n\nStep 2: Run tests\n✓ All tests passed\n\nStep 3: Build package\n✓ Package built successfully`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Trigger a new build for a package
|
|
566
|
+
*/
|
|
567
|
+
async triggerBuild(
|
|
568
|
+
packageName: string,
|
|
569
|
+
providerName: string,
|
|
570
|
+
branch: string = 'main'
|
|
571
|
+
): Promise<{ success: boolean; buildId?: string; error?: string }> {
|
|
572
|
+
try {
|
|
573
|
+
// Mock implementation
|
|
574
|
+
const buildId = `build-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
575
|
+
|
|
576
|
+
console.log(
|
|
577
|
+
`Triggering build for ${packageName} on ${branch} via ${providerName}`
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
success: true,
|
|
582
|
+
buildId,
|
|
583
|
+
};
|
|
584
|
+
} catch (error) {
|
|
585
|
+
return {
|
|
586
|
+
success: false,
|
|
587
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Get build artifacts
|
|
594
|
+
*/
|
|
595
|
+
async getBuildArtifacts(
|
|
596
|
+
buildId: string,
|
|
597
|
+
_providerName: string
|
|
598
|
+
): Promise<CIArtifact[]> {
|
|
599
|
+
// Mock implementation
|
|
600
|
+
return [
|
|
601
|
+
{
|
|
602
|
+
name: 'coverage-report.html',
|
|
603
|
+
type: 'coverage',
|
|
604
|
+
size: 1024 * 50, // 50KB
|
|
605
|
+
url: `https://ci.example.com/artifacts/${buildId}/coverage-report.html`,
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
name: 'test-results.xml',
|
|
609
|
+
type: 'test',
|
|
610
|
+
size: 1024 * 10, // 10KB
|
|
611
|
+
url: `https://ci.example.com/artifacts/${buildId}/test-results.xml`,
|
|
612
|
+
},
|
|
613
|
+
];
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Export default instance
|
|
618
|
+
export const ciStatusManager = new CIStatusManager();
|
|
619
|
+
|
|
620
|
+
// Export convenience functions
|
|
621
|
+
export async function getPackageCIStatus(
|
|
622
|
+
packageName: string
|
|
623
|
+
): Promise<CIPackageStatus | null> {
|
|
624
|
+
return ciStatusManager.getPackageStatus(packageName);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export async function getMonorepoCIStatus(
|
|
628
|
+
packages: PackageInfo[]
|
|
629
|
+
): Promise<CIMonorepoStatus> {
|
|
630
|
+
return ciStatusManager.getMonorepoStatus(packages);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export async function triggerPackageBuild(
|
|
634
|
+
packageName: string,
|
|
635
|
+
providerName: string,
|
|
636
|
+
branch?: string
|
|
637
|
+
): Promise<{ success: boolean; buildId?: string; error?: string }> {
|
|
638
|
+
return ciStatusManager.triggerBuild(packageName, providerName, branch);
|
|
639
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@monodog/ci-status",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CI/CD status manager for monorepo packages",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "tsx watch index.ts",
|
|
10
|
+
"start": "tsx index.ts",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"clean": "rm -rf dist node_modules/.cache",
|
|
14
|
+
"test:coverage": "jest --coverage",
|
|
15
|
+
"lint": "eslint .",
|
|
16
|
+
"lint:fix": "eslint . --fix"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@prisma/client": "^5.7.0",
|
|
20
|
+
"@monodog/utils": "workspace:*"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.10.0",
|
|
24
|
+
"tsx": "^4.6.0",
|
|
25
|
+
"typescript": "^5.3.0",
|
|
26
|
+
"prisma": "^5.7.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
}
|
|
30
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Extends the shared Node configuration for utility/service packages
|
|
3
|
+
// "extends": "@monorepo-dashboard/tsconfig/node.json",
|
|
4
|
+
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
// Output compilation results to the 'dist' directory
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.ci-status.tsbuildinfo",
|
|
9
|
+
// "rootDir": "./",
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"downlevelIteration": true,
|
|
14
|
+
|
|
15
|
+
// Since your package.json defines "main": "index.ts",
|
|
16
|
+
// we set the 'root' to the package root and 'outDir' to 'dist'.
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
// Include the main entry point and any other source files
|
|
20
|
+
"include": ["index.ts", "src/**/*"],
|
|
21
|
+
|
|
22
|
+
// Exclude node_modules and the output directory
|
|
23
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
24
|
+
}
|