@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/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
+ }