@kabran-tecnologia/kabran-config 1.5.0 → 1.7.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/README.md +283 -0
- package/package.json +67 -9
- package/src/schemas/ci-result.v2.schema.json +125 -0
- package/src/scripts/ci/ci-core.sh +61 -0
- package/src/scripts/ci/ci-runner.sh +89 -0
- package/src/scripts/ci-result-history.mjs +245 -0
- package/src/scripts/ci-result-trends.mjs +296 -0
- package/src/scripts/ci-result-utils.mjs +223 -0
- package/src/scripts/generate-ci-result.mjs +79 -11
- package/src/scripts/pr-quality-comment.mjs +326 -0
- package/src/scripts/setup.mjs +91 -4
- package/src/telemetry/config/defaults.mjs +421 -0
- package/src/telemetry/config/index.mjs +132 -0
- package/src/telemetry/edge/index.mjs +446 -0
- package/src/telemetry/frontend/index.mjs +366 -0
- package/src/telemetry/logger/index.mjs +236 -0
- package/src/telemetry/node/index.mjs +386 -0
- package/src/telemetry/shared/helpers.mjs +133 -0
- package/src/telemetry/shared/index.mjs +15 -0
- package/src/telemetry/shared/types.d.ts +123 -0
- package/templates/.github/workflows/ci-quality.yml +111 -0
- package/templates/telemetry/.env.telemetry.example +118 -0
|
@@ -305,6 +305,125 @@ export function extractComponents(steps) {
|
|
|
305
305
|
return Array.from(components).sort()
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Aggregate coverage data from multiple test steps
|
|
310
|
+
*
|
|
311
|
+
* @param {Array} steps - Array of step results with coverage data
|
|
312
|
+
* @returns {Object|null} Aggregated coverage or null if no coverage data
|
|
313
|
+
*/
|
|
314
|
+
export function aggregateCoverage(steps) {
|
|
315
|
+
const coverageData = []
|
|
316
|
+
|
|
317
|
+
for (const step of steps) {
|
|
318
|
+
if (step.output?.coverage) {
|
|
319
|
+
coverageData.push({
|
|
320
|
+
component: step.component || 'default',
|
|
321
|
+
coverage: step.output.coverage,
|
|
322
|
+
tests: {
|
|
323
|
+
passed: step.output.passed || 0,
|
|
324
|
+
failed: step.output.failed || 0,
|
|
325
|
+
skipped: step.output.skipped || 0,
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (coverageData.length === 0) {
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Calculate weighted average based on test count
|
|
336
|
+
let totalTests = 0
|
|
337
|
+
let weightedLines = 0
|
|
338
|
+
let weightedBranches = 0
|
|
339
|
+
let weightedFunctions = 0
|
|
340
|
+
let weightedStatements = 0
|
|
341
|
+
|
|
342
|
+
const byComponent = {}
|
|
343
|
+
|
|
344
|
+
for (const data of coverageData) {
|
|
345
|
+
const testCount = data.tests.passed + data.tests.failed
|
|
346
|
+
totalTests += testCount
|
|
347
|
+
|
|
348
|
+
const cov = data.coverage
|
|
349
|
+
if (cov.lines !== undefined) weightedLines += cov.lines * testCount
|
|
350
|
+
if (cov.branches !== undefined) weightedBranches += cov.branches * testCount
|
|
351
|
+
if (cov.functions !== undefined) weightedFunctions += cov.functions * testCount
|
|
352
|
+
if (cov.statements !== undefined) weightedStatements += cov.statements * testCount
|
|
353
|
+
|
|
354
|
+
byComponent[data.component] = {
|
|
355
|
+
...data.coverage,
|
|
356
|
+
tests: data.tests,
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Calculate averages
|
|
361
|
+
const avgLines = totalTests > 0 ? Math.round((weightedLines / totalTests) * 10) / 10 : 0
|
|
362
|
+
const avgBranches = totalTests > 0 ? Math.round((weightedBranches / totalTests) * 10) / 10 : 0
|
|
363
|
+
const avgFunctions = totalTests > 0 ? Math.round((weightedFunctions / totalTests) * 10) / 10 : 0
|
|
364
|
+
const avgStatements = totalTests > 0 ? Math.round((weightedStatements / totalTests) * 10) / 10 : 0
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
lines: avgLines,
|
|
368
|
+
branches: avgBranches,
|
|
369
|
+
functions: avgFunctions,
|
|
370
|
+
statements: avgStatements,
|
|
371
|
+
by_component: byComponent,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Compare two CI results and calculate diff
|
|
377
|
+
*
|
|
378
|
+
* @param {Object} current - Current CI result
|
|
379
|
+
* @param {Object} baseline - Baseline CI result (e.g., from main branch)
|
|
380
|
+
* @returns {Object} Comparison result with diffs
|
|
381
|
+
*/
|
|
382
|
+
export function compareCiResults(current, baseline) {
|
|
383
|
+
const scoreDiff = current.summary.score - baseline.summary.score
|
|
384
|
+
const issuesDiff = current.summary.total_issues - baseline.summary.total_issues
|
|
385
|
+
const blockingDiff = current.summary.blocking - baseline.summary.blocking
|
|
386
|
+
|
|
387
|
+
// Determine trend
|
|
388
|
+
let trend = 'stable'
|
|
389
|
+
if (scoreDiff > 5) trend = 'improving'
|
|
390
|
+
else if (scoreDiff < -5) trend = 'degrading'
|
|
391
|
+
|
|
392
|
+
// Compare coverage if available
|
|
393
|
+
let coverageDiff = null
|
|
394
|
+
if (current.checks?.test?.coverage && baseline.checks?.test?.coverage) {
|
|
395
|
+
coverageDiff = {
|
|
396
|
+
lines: (current.checks.test.coverage.lines || 0) - (baseline.checks.test.coverage.lines || 0),
|
|
397
|
+
branches: (current.checks.test.coverage.branches || 0) - (baseline.checks.test.coverage.branches || 0),
|
|
398
|
+
functions: (current.checks.test.coverage.functions || 0) - (baseline.checks.test.coverage.functions || 0),
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
trend,
|
|
404
|
+
score: {
|
|
405
|
+
current: current.summary.score,
|
|
406
|
+
baseline: baseline.summary.score,
|
|
407
|
+
diff: scoreDiff,
|
|
408
|
+
},
|
|
409
|
+
issues: {
|
|
410
|
+
current: current.summary.total_issues,
|
|
411
|
+
baseline: baseline.summary.total_issues,
|
|
412
|
+
diff: issuesDiff,
|
|
413
|
+
},
|
|
414
|
+
blocking: {
|
|
415
|
+
current: current.summary.blocking,
|
|
416
|
+
baseline: baseline.summary.blocking,
|
|
417
|
+
diff: blockingDiff,
|
|
418
|
+
},
|
|
419
|
+
coverage: coverageDiff,
|
|
420
|
+
status: {
|
|
421
|
+
current: current.summary.status,
|
|
422
|
+
baseline: baseline.summary.status,
|
|
423
|
+
},
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
308
427
|
/**
|
|
309
428
|
* Create a minimal valid CI result object
|
|
310
429
|
*
|
|
@@ -363,3 +482,107 @@ export function createMinimalResult({ projectName, passed = true }) {
|
|
|
363
482
|
extensions: {},
|
|
364
483
|
}
|
|
365
484
|
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Get trace ID from environment variables
|
|
488
|
+
*
|
|
489
|
+
* Checks common OpenTelemetry and CI environment variables for trace IDs.
|
|
490
|
+
*
|
|
491
|
+
* @returns {string|null} Trace ID or null if not available
|
|
492
|
+
*/
|
|
493
|
+
export function getTraceId() {
|
|
494
|
+
// Direct trace ID from environment
|
|
495
|
+
if (process.env.OTEL_TRACE_ID) {
|
|
496
|
+
return process.env.OTEL_TRACE_ID
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// W3C traceparent header format: 00-{trace_id}-{span_id}-{flags}
|
|
500
|
+
if (process.env.TRACEPARENT) {
|
|
501
|
+
const parts = process.env.TRACEPARENT.split('-')
|
|
502
|
+
if (parts.length >= 2) {
|
|
503
|
+
return parts[1]
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// GitHub Actions run ID as fallback trace correlation
|
|
508
|
+
if (process.env.GITHUB_RUN_ID) {
|
|
509
|
+
return `gh-${process.env.GITHUB_RUN_ID}`
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return null
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Build trace URL from trace ID and endpoint
|
|
517
|
+
*
|
|
518
|
+
* @param {string} traceId - The trace ID
|
|
519
|
+
* @param {string} [endpoint] - OTel endpoint (default: from env)
|
|
520
|
+
* @param {string} [template] - URL template with {trace_id} placeholder
|
|
521
|
+
* @returns {string|null} Trace URL or null
|
|
522
|
+
*/
|
|
523
|
+
export function buildTraceUrl(traceId, endpoint = null, template = null) {
|
|
524
|
+
if (!traceId) return null
|
|
525
|
+
|
|
526
|
+
const otelEndpoint = endpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_ENDPOINT
|
|
527
|
+
|
|
528
|
+
// Use custom template if provided
|
|
529
|
+
if (template) {
|
|
530
|
+
return template.replace('{trace_id}', traceId).replace('{endpoint}', otelEndpoint || '')
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Use env template if available
|
|
534
|
+
if (process.env.OTEL_TRACE_URL_TEMPLATE) {
|
|
535
|
+
return process.env.OTEL_TRACE_URL_TEMPLATE.replace('{trace_id}', traceId).replace(
|
|
536
|
+
'{endpoint}',
|
|
537
|
+
otelEndpoint || ''
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Default Jaeger-style URL if endpoint is known
|
|
542
|
+
if (otelEndpoint) {
|
|
543
|
+
// Extract base URL from endpoint (remove /v1/traces if present)
|
|
544
|
+
const baseUrl = otelEndpoint.replace(/\/v1\/traces\/?$/, '').replace(/\/$/, '')
|
|
545
|
+
return `${baseUrl}/trace/${traceId}`
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return null
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Build telemetry extension object for ci-result.json
|
|
553
|
+
*
|
|
554
|
+
* @param {string} traceId - The trace ID
|
|
555
|
+
* @param {Object} [options] - Additional options
|
|
556
|
+
* @param {number} [options.spansExported] - Number of spans exported
|
|
557
|
+
* @param {number} [options.errorsRecorded] - Number of errors recorded
|
|
558
|
+
* @param {string} [options.endpoint] - OTel endpoint
|
|
559
|
+
* @returns {Object} Telemetry extension object
|
|
560
|
+
*/
|
|
561
|
+
export function buildTelemetryExtension(traceId, options = {}) {
|
|
562
|
+
const { spansExported = 0, errorsRecorded = 0, endpoint = null } = options
|
|
563
|
+
|
|
564
|
+
const extension = {
|
|
565
|
+
trace_id: traceId,
|
|
566
|
+
enabled: true,
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Add trace URL if we can build one
|
|
570
|
+
const traceUrl = buildTraceUrl(traceId, endpoint)
|
|
571
|
+
if (traceUrl) {
|
|
572
|
+
extension.trace_url = traceUrl
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Add metrics if provided
|
|
576
|
+
if (spansExported > 0 || errorsRecorded > 0) {
|
|
577
|
+
extension.spans_exported = spansExported
|
|
578
|
+
extension.errors_recorded = errorsRecorded
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Add endpoint info
|
|
582
|
+
const otelEndpoint = endpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_ENDPOINT
|
|
583
|
+
if (otelEndpoint) {
|
|
584
|
+
extension.collector_endpoint = otelEndpoint
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return extension
|
|
588
|
+
}
|
|
@@ -34,8 +34,14 @@ import {
|
|
|
34
34
|
calculateExecutionStats,
|
|
35
35
|
countIssues,
|
|
36
36
|
extractComponents,
|
|
37
|
+
getTraceId,
|
|
38
|
+
buildTelemetryExtension,
|
|
37
39
|
} from './ci-result-utils.mjs'
|
|
38
40
|
|
|
41
|
+
// History and trends imports
|
|
42
|
+
import { loadHistory, addToHistory, saveHistory } from './ci-result-history.mjs'
|
|
43
|
+
import { calculateTrends } from './ci-result-trends.mjs'
|
|
44
|
+
|
|
39
45
|
// Validator imports
|
|
40
46
|
import { getLicenseCheckResult } from './license-check.mjs'
|
|
41
47
|
import { getReadmeCheckResult } from './readme-validator.mjs'
|
|
@@ -132,6 +138,7 @@ async function runValidators(projectRoot, options = {}) {
|
|
|
132
138
|
* @param {Object} input.project - Project information
|
|
133
139
|
* @param {Object} input.metadata - Additional metadata
|
|
134
140
|
* @param {Object} input.validators - Validator results (from runValidators)
|
|
141
|
+
* @param {Object} input.trends - Pre-calculated trends (optional)
|
|
135
142
|
* @returns {Object} CI result object
|
|
136
143
|
*/
|
|
137
144
|
export function generateCiResult(input) {
|
|
@@ -143,6 +150,7 @@ export function generateCiResult(input) {
|
|
|
143
150
|
metadata = {},
|
|
144
151
|
issues = [],
|
|
145
152
|
validators = {},
|
|
153
|
+
trends = null,
|
|
146
154
|
} = input
|
|
147
155
|
|
|
148
156
|
const now = new Date().toISOString()
|
|
@@ -169,19 +177,36 @@ export function generateCiResult(input) {
|
|
|
169
177
|
// Determine exit code
|
|
170
178
|
const exitCode = executionStats.steps_failed > 0 ? 1 : 0
|
|
171
179
|
|
|
180
|
+
// Get trace ID if available
|
|
181
|
+
const traceId = getTraceId()
|
|
182
|
+
|
|
183
|
+
// Build meta object
|
|
184
|
+
const meta = {
|
|
185
|
+
generated_at: now,
|
|
186
|
+
generator: getGeneratorVersion(),
|
|
187
|
+
run_id: generateRunId(),
|
|
188
|
+
trigger: detectTrigger(),
|
|
189
|
+
branch: getGitBranch(),
|
|
190
|
+
commit: getGitCommit(),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add trace_id if available
|
|
194
|
+
if (traceId) {
|
|
195
|
+
meta.trace_id = traceId
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Build extensions with telemetry if trace_id exists
|
|
199
|
+
const extensions = { ...(metadata.extensions || {}) }
|
|
200
|
+
if (traceId) {
|
|
201
|
+
extensions.telemetry = buildTelemetryExtension(traceId)
|
|
202
|
+
}
|
|
203
|
+
|
|
172
204
|
// Build result object
|
|
173
205
|
const result = {
|
|
174
206
|
$schema: 'https://kabran.dev/schemas/ci-result.v2.json',
|
|
175
207
|
version: '1.0.0',
|
|
176
208
|
|
|
177
|
-
meta
|
|
178
|
-
generated_at: now,
|
|
179
|
-
generator: getGeneratorVersion(),
|
|
180
|
-
run_id: generateRunId(),
|
|
181
|
-
trigger: detectTrigger(),
|
|
182
|
-
branch: getGitBranch(),
|
|
183
|
-
commit: getGitCommit(),
|
|
184
|
-
},
|
|
209
|
+
meta,
|
|
185
210
|
|
|
186
211
|
project: {
|
|
187
212
|
name: project.name || 'unknown',
|
|
@@ -216,7 +241,12 @@ export function generateCiResult(input) {
|
|
|
216
241
|
checks,
|
|
217
242
|
issues,
|
|
218
243
|
errors,
|
|
219
|
-
extensions
|
|
244
|
+
extensions,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Add trends if provided
|
|
248
|
+
if (trends) {
|
|
249
|
+
result.trends = trends
|
|
220
250
|
}
|
|
221
251
|
|
|
222
252
|
return result
|
|
@@ -236,6 +266,10 @@ function parseArgs(args) {
|
|
|
236
266
|
skipReadme: false,
|
|
237
267
|
skipEnv: false,
|
|
238
268
|
skipQualityStandard: false,
|
|
269
|
+
trackHistory: false,
|
|
270
|
+
historyFile: null,
|
|
271
|
+
maxHistoryEntries: 30,
|
|
272
|
+
calculateTrends: false,
|
|
239
273
|
}
|
|
240
274
|
|
|
241
275
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -259,6 +293,16 @@ function parseArgs(args) {
|
|
|
259
293
|
options.skipEnv = true
|
|
260
294
|
} else if (arg === '--skip-quality-standard') {
|
|
261
295
|
options.skipQualityStandard = true
|
|
296
|
+
} else if (arg === '--track-history') {
|
|
297
|
+
options.trackHistory = true
|
|
298
|
+
} else if (arg === '--history-file') {
|
|
299
|
+
options.historyFile = args[++i]
|
|
300
|
+
options.trackHistory = true
|
|
301
|
+
} else if (arg === '--max-history') {
|
|
302
|
+
options.maxHistoryEntries = parseInt(args[++i], 10) || 30
|
|
303
|
+
} else if (arg === '--calculate-trends') {
|
|
304
|
+
options.calculateTrends = true
|
|
305
|
+
options.trackHistory = true
|
|
262
306
|
} else if (arg === '--help' || arg === '-h') {
|
|
263
307
|
console.log(`
|
|
264
308
|
Usage: generate-ci-result.mjs [options]
|
|
@@ -273,6 +317,10 @@ Options:
|
|
|
273
317
|
--skip-readme Skip README check when running validators
|
|
274
318
|
--skip-env Skip env check when running validators
|
|
275
319
|
--skip-quality-standard Skip quality-standard check when running validators
|
|
320
|
+
--track-history Track result in history file (max 30 entries)
|
|
321
|
+
--history-file <file> Custom history file path (default: docs/quality/ci-result-history.json)
|
|
322
|
+
--max-history <n> Maximum history entries to keep (default: 30)
|
|
323
|
+
--calculate-trends Calculate and include trends from history
|
|
276
324
|
-h, --help Show this help message
|
|
277
325
|
|
|
278
326
|
Input format:
|
|
@@ -362,15 +410,35 @@ async function main() {
|
|
|
362
410
|
})
|
|
363
411
|
}
|
|
364
412
|
|
|
413
|
+
// Determine paths
|
|
414
|
+
const outputPath = options.output || resolve(options.projectRoot, 'docs/quality/ci-result.json')
|
|
415
|
+
const historyPath = options.historyFile || resolve(options.projectRoot, 'docs/quality/ci-result-history.json')
|
|
416
|
+
|
|
417
|
+
// Load history and calculate trends if requested
|
|
418
|
+
let history = null
|
|
419
|
+
if (options.trackHistory || options.calculateTrends) {
|
|
420
|
+
history = loadHistory(historyPath)
|
|
421
|
+
|
|
422
|
+
// Calculate trends if requested
|
|
423
|
+
if (options.calculateTrends && history.entries.length > 0) {
|
|
424
|
+
input.trends = calculateTrends(history.entries)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
365
428
|
// Generate result
|
|
366
429
|
const result = generateCiResult(input)
|
|
367
430
|
|
|
431
|
+
// Update history with new result
|
|
432
|
+
if (options.trackHistory && history) {
|
|
433
|
+
addToHistory(result, history)
|
|
434
|
+
saveHistory(history, historyPath, options.maxHistoryEntries)
|
|
435
|
+
console.log(`History updated: ${historyPath} (${history.entries.length} entries)`)
|
|
436
|
+
}
|
|
437
|
+
|
|
368
438
|
// Output
|
|
369
439
|
if (options.stdout) {
|
|
370
440
|
console.log(JSON.stringify(result, null, 2))
|
|
371
441
|
} else {
|
|
372
|
-
const outputPath = options.output || resolve(options.projectRoot, 'docs/quality/ci-result.json')
|
|
373
|
-
|
|
374
442
|
// Ensure directory exists
|
|
375
443
|
mkdirSync(dirname(outputPath), { recursive: true })
|
|
376
444
|
|