@kabran-tecnologia/kabran-config 1.9.0 → 1.12.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/package.json +7 -1
- package/src/cli/commands/build.mjs +54 -0
- package/src/cli/commands/check.mjs +107 -0
- package/src/cli/commands/ci.mjs +109 -0
- package/src/cli/commands/test.mjs +130 -0
- package/src/cli/kabran.mjs +144 -0
- package/src/core/config-loader.mjs +154 -0
- package/src/schemas/ci-result.v2.schema.json +1 -1
- package/src/scripts/ci/ci-core.sh +274 -1
- package/src/scripts/ci/ci-runner.sh +9 -0
- package/src/scripts/ci-result-history.mjs +2 -2
- package/src/scripts/ci-result-utils.mjs +1 -1
- package/src/scripts/deploy/deploy-core.sh +1 -1
- package/src/scripts/env-validator.mjs +12 -11
- package/src/scripts/generate-ci-result.mjs +3 -3
- package/src/scripts/quality-standard-validator.mjs +24 -13
- package/src/scripts/readme-validator.mjs +35 -19
- package/src/scripts/traceability/coverage-report.sh +1 -1
- package/src/scripts/traceability/traceability-core.sh +1 -1
- package/src/scripts/traceability/validate-traceability.sh +1 -1
- package/src/telemetry/README.md +11 -11
- package/src/telemetry/config/defaults.mjs +5 -7
- package/src/telemetry/shared/types.d.ts +1 -1
- package/templates/config/kabran.config.mjs +53 -0
- package/CI-CD-MIGRATION.md +0 -388
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kabran Config Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads project configuration from kabran.config.mjs/js/json with fallback to defaults.
|
|
5
|
+
* Enables projects to customize validator behavior without modifying kabran-config.
|
|
6
|
+
*
|
|
7
|
+
* @module config-loader
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {existsSync} from 'node:fs';
|
|
11
|
+
import {readFile} from 'node:fs/promises';
|
|
12
|
+
import {join} from 'node:path';
|
|
13
|
+
import {pathToFileURL} from 'node:url';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default configuration values.
|
|
17
|
+
* These match the current hardcoded values in validators to ensure backwards compatibility.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULTS = {
|
|
20
|
+
readme: {
|
|
21
|
+
required: ['Installation', 'Usage', 'License'],
|
|
22
|
+
recommended: ['Development', 'Testing', 'Contributing'],
|
|
23
|
+
},
|
|
24
|
+
env: {
|
|
25
|
+
requireExample: true,
|
|
26
|
+
detectPatterns: ['process.env', 'import.meta.env', 'Deno.env', 'os.getenv', '$_ENV'],
|
|
27
|
+
},
|
|
28
|
+
quality: {
|
|
29
|
+
standardPath: 'docs/quality/001-quality-standard.md',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Configuration file names to search for, in priority order.
|
|
35
|
+
*/
|
|
36
|
+
const CONFIG_FILES = ['kabran.config.mjs', 'kabran.config.js', 'kabran.config.json'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Deep merge two objects. Arrays are replaced, not merged.
|
|
40
|
+
* @param {object} target - Base object
|
|
41
|
+
* @param {object} source - Object to merge in
|
|
42
|
+
* @returns {object} Merged object
|
|
43
|
+
*/
|
|
44
|
+
function deepMerge(target, source) {
|
|
45
|
+
const result = {...target};
|
|
46
|
+
|
|
47
|
+
for (const key of Object.keys(source)) {
|
|
48
|
+
const sourceValue = source[key];
|
|
49
|
+
const targetValue = target[key];
|
|
50
|
+
|
|
51
|
+
if (sourceValue === null || sourceValue === undefined) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
typeof sourceValue === 'object' &&
|
|
57
|
+
!Array.isArray(sourceValue) &&
|
|
58
|
+
typeof targetValue === 'object' &&
|
|
59
|
+
!Array.isArray(targetValue)
|
|
60
|
+
) {
|
|
61
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
62
|
+
} else {
|
|
63
|
+
result[key] = sourceValue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Load JSON configuration file.
|
|
72
|
+
* @param {string} configPath - Path to JSON config file
|
|
73
|
+
* @returns {Promise<object>} Parsed configuration
|
|
74
|
+
*/
|
|
75
|
+
async function loadJsonConfig(configPath) {
|
|
76
|
+
const content = await readFile(configPath, 'utf-8');
|
|
77
|
+
return JSON.parse(content);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Load JavaScript/ESM configuration file.
|
|
82
|
+
* @param {string} configPath - Path to JS/MJS config file
|
|
83
|
+
* @returns {Promise<object>} Parsed configuration
|
|
84
|
+
*/
|
|
85
|
+
async function loadJsConfig(configPath) {
|
|
86
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
87
|
+
const module = await import(configUrl);
|
|
88
|
+
return module.default || module;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load project configuration with fallback to defaults.
|
|
93
|
+
*
|
|
94
|
+
* Searches for configuration files in order:
|
|
95
|
+
* 1. kabran.config.mjs
|
|
96
|
+
* 2. kabran.config.js
|
|
97
|
+
* 3. kabran.config.json
|
|
98
|
+
*
|
|
99
|
+
* If no config file is found, returns DEFAULTS.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} [cwd=process.cwd()] - Directory to search for config
|
|
102
|
+
* @returns {Promise<object>} Merged configuration
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* const config = await loadConfig();
|
|
106
|
+
* console.log(config.readme.required); // ['Installation', 'Usage', 'License']
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* // With custom config in kabran.config.mjs:
|
|
110
|
+
* // export default { readme: { required: ['Setup', 'API'] } }
|
|
111
|
+
* const config = await loadConfig('/path/to/project');
|
|
112
|
+
* console.log(config.readme.required); // ['Setup', 'API']
|
|
113
|
+
*/
|
|
114
|
+
export async function loadConfig(cwd = process.cwd()) {
|
|
115
|
+
for (const fileName of CONFIG_FILES) {
|
|
116
|
+
const configPath = join(cwd, fileName);
|
|
117
|
+
|
|
118
|
+
if (existsSync(configPath)) {
|
|
119
|
+
try {
|
|
120
|
+
let projectConfig;
|
|
121
|
+
|
|
122
|
+
if (fileName.endsWith('.json')) {
|
|
123
|
+
projectConfig = await loadJsonConfig(configPath);
|
|
124
|
+
} else {
|
|
125
|
+
projectConfig = await loadJsConfig(configPath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return deepMerge(DEFAULTS, projectConfig);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Log warning but continue with defaults
|
|
131
|
+
console.warn(`Warning: Failed to load ${fileName}: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return DEFAULTS;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a config file exists in the given directory.
|
|
141
|
+
* @param {string} [cwd=process.cwd()] - Directory to check
|
|
142
|
+
* @returns {{exists: boolean, path?: string, name?: string}} Config file info
|
|
143
|
+
*/
|
|
144
|
+
export function findConfigFile(cwd = process.cwd()) {
|
|
145
|
+
for (const fileName of CONFIG_FILES) {
|
|
146
|
+
const configPath = join(cwd, fileName);
|
|
147
|
+
|
|
148
|
+
if (existsSync(configPath)) {
|
|
149
|
+
return {exists: true, path: configPath, name: fileName};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {exists: false};
|
|
154
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "
|
|
3
|
+
"$id": "ci-result.v2.json",
|
|
4
4
|
"title": "CI Result Schema v2",
|
|
5
5
|
"description": "Unified schema for CI/CD results with quality metrics, timing, and observability data",
|
|
6
6
|
"type": "object",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# ==============================================================================
|
|
3
3
|
# Kabran CI Core - Shared Functions
|
|
4
|
-
# Part of @kabran-
|
|
4
|
+
# Part of @kabran-tecnologia/kabran-config
|
|
5
5
|
# ==============================================================================
|
|
6
6
|
|
|
7
7
|
# Version
|
|
@@ -590,6 +590,279 @@ export_ci_data() {
|
|
|
590
590
|
log_debug "CI data exported to: $output_file"
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
# ==============================================================================
|
|
594
|
+
# OpenTelemetry Metrics Export
|
|
595
|
+
# ==============================================================================
|
|
596
|
+
|
|
597
|
+
# Export CI metrics to OTel Collector via OTLP HTTP
|
|
598
|
+
# Usage: export_ci_metrics_to_otel "$CI_DATA_FILE"
|
|
599
|
+
# Environment: OTEL_ENDPOINT must be set (e.g., http://localhost:4318)
|
|
600
|
+
# Returns: 0 on success, 1 on failure (but never fails the build due to || true usage)
|
|
601
|
+
export_ci_metrics_to_otel() {
|
|
602
|
+
local ci_data_file="${1:-}"
|
|
603
|
+
|
|
604
|
+
# Check if OTEL_ENDPOINT is configured
|
|
605
|
+
if [ -z "${OTEL_ENDPOINT:-}" ]; then
|
|
606
|
+
log_debug "OTEL_ENDPOINT not set, skipping metrics export"
|
|
607
|
+
return 0
|
|
608
|
+
fi
|
|
609
|
+
|
|
610
|
+
# Validate input file
|
|
611
|
+
if [ -z "$ci_data_file" ] || [ ! -f "$ci_data_file" ]; then
|
|
612
|
+
log_warn "CI data file not found: $ci_data_file"
|
|
613
|
+
return 1
|
|
614
|
+
fi
|
|
615
|
+
|
|
616
|
+
# Check for curl
|
|
617
|
+
if ! command -v curl &>/dev/null; then
|
|
618
|
+
log_warn "curl not available, skipping OTel metrics export"
|
|
619
|
+
return 1
|
|
620
|
+
fi
|
|
621
|
+
|
|
622
|
+
log_info "Exporting CI metrics to OTel Collector..."
|
|
623
|
+
log_debug " Endpoint: $OTEL_ENDPOINT"
|
|
624
|
+
log_debug " Data file: $ci_data_file"
|
|
625
|
+
|
|
626
|
+
# Extract metrics from CI data
|
|
627
|
+
local total_ms steps_json project_name trace_id ci_passed
|
|
628
|
+
total_ms=$(jq -r '.timing.total_ms // 0' "$ci_data_file" 2>/dev/null)
|
|
629
|
+
steps_json=$(jq -c '.steps // []' "$ci_data_file" 2>/dev/null)
|
|
630
|
+
project_name=$(jq -r '.project.name // "unknown"' "$ci_data_file" 2>/dev/null)
|
|
631
|
+
trace_id=$(jq -r '.trace_context.trace_id // ""' "$ci_data_file" 2>/dev/null)
|
|
632
|
+
|
|
633
|
+
# Determine overall status
|
|
634
|
+
local failed_count
|
|
635
|
+
failed_count=$(echo "$steps_json" | jq '[.[] | select(.status == "fail")] | length' 2>/dev/null || echo "0")
|
|
636
|
+
if [ "$failed_count" -gt 0 ]; then
|
|
637
|
+
ci_passed="false"
|
|
638
|
+
else
|
|
639
|
+
ci_passed="true"
|
|
640
|
+
fi
|
|
641
|
+
|
|
642
|
+
# Get timestamp in nanoseconds (Unix epoch)
|
|
643
|
+
local timestamp_ns
|
|
644
|
+
timestamp_ns=$(date +%s)000000000
|
|
645
|
+
|
|
646
|
+
# Build OTLP metrics payload
|
|
647
|
+
local otlp_payload
|
|
648
|
+
otlp_payload=$(build_otlp_metrics_payload \
|
|
649
|
+
"$project_name" \
|
|
650
|
+
"$total_ms" \
|
|
651
|
+
"$ci_passed" \
|
|
652
|
+
"$steps_json" \
|
|
653
|
+
"$timestamp_ns" \
|
|
654
|
+
"$trace_id")
|
|
655
|
+
|
|
656
|
+
if [ -z "$otlp_payload" ] || [ "$otlp_payload" = "null" ]; then
|
|
657
|
+
log_warn "Failed to build OTLP payload"
|
|
658
|
+
return 1
|
|
659
|
+
fi
|
|
660
|
+
|
|
661
|
+
# Send to OTel Collector with aggressive timeouts
|
|
662
|
+
# --connect-timeout 1: max 1 second to establish connection
|
|
663
|
+
# --max-time 5: max 5 seconds total for the request
|
|
664
|
+
# -f: fail silently on HTTP errors
|
|
665
|
+
local otel_metrics_endpoint="${OTEL_ENDPOINT}/v1/metrics"
|
|
666
|
+
|
|
667
|
+
log_debug "Sending metrics to: $otel_metrics_endpoint"
|
|
668
|
+
|
|
669
|
+
local http_code
|
|
670
|
+
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
671
|
+
--connect-timeout 1 \
|
|
672
|
+
--max-time 5 \
|
|
673
|
+
-X POST \
|
|
674
|
+
-H "Content-Type: application/json" \
|
|
675
|
+
-d "$otlp_payload" \
|
|
676
|
+
"$otel_metrics_endpoint" 2>/dev/null) || {
|
|
677
|
+
log_warn "Failed to send metrics to OTel Collector (connection error)"
|
|
678
|
+
return 1
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
|
682
|
+
log_success "CI metrics exported to OTel Collector (HTTP $http_code)"
|
|
683
|
+
return 0
|
|
684
|
+
else
|
|
685
|
+
log_warn "OTel Collector returned HTTP $http_code"
|
|
686
|
+
return 1
|
|
687
|
+
fi
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
# Build OTLP JSON payload for metrics
|
|
691
|
+
# Usage: build_otlp_metrics_payload "$project" "$duration_ms" "$passed" "$steps_json" "$timestamp_ns" "$trace_id"
|
|
692
|
+
build_otlp_metrics_payload() {
|
|
693
|
+
local project="$1"
|
|
694
|
+
local duration_ms="$2"
|
|
695
|
+
local passed="$3"
|
|
696
|
+
local steps_json="$4"
|
|
697
|
+
local timestamp_ns="$5"
|
|
698
|
+
local trace_id="${6:-}"
|
|
699
|
+
|
|
700
|
+
# Service attributes
|
|
701
|
+
local service_name="ci-runner"
|
|
702
|
+
local service_version="${CI_CORE_VERSION:-unknown}"
|
|
703
|
+
|
|
704
|
+
# Count steps by status
|
|
705
|
+
local pass_count fail_count skip_count
|
|
706
|
+
pass_count=$(echo "$steps_json" | jq '[.[] | select(.status == "pass")] | length' 2>/dev/null || echo "0")
|
|
707
|
+
fail_count=$(echo "$steps_json" | jq '[.[] | select(.status == "fail")] | length' 2>/dev/null || echo "0")
|
|
708
|
+
skip_count=$(echo "$steps_json" | jq '[.[] | select(.status == "skip")] | length' 2>/dev/null || echo "0")
|
|
709
|
+
|
|
710
|
+
# Build step duration data points
|
|
711
|
+
local step_duration_points
|
|
712
|
+
step_duration_points=$(echo "$steps_json" | jq -c --arg ts "$timestamp_ns" '
|
|
713
|
+
[.[] | select(.status != "skip") | {
|
|
714
|
+
attributes: ([
|
|
715
|
+
{key: "step.name", value: {stringValue: .name}},
|
|
716
|
+
{key: "step.category", value: {stringValue: (.category // "custom")}},
|
|
717
|
+
{key: "step.status", value: {stringValue: .status}}
|
|
718
|
+
] + (if (.component // "") != "" then [{key: "step.component", value: {stringValue: .component}}] else [] end)),
|
|
719
|
+
startTimeUnixNano: $ts,
|
|
720
|
+
timeUnixNano: $ts,
|
|
721
|
+
asDouble: .duration_ms
|
|
722
|
+
}]
|
|
723
|
+
' 2>/dev/null || echo "[]")
|
|
724
|
+
|
|
725
|
+
# Build resource attributes (conditionally include trace_id)
|
|
726
|
+
local resource_attributes
|
|
727
|
+
if [ -n "$trace_id" ]; then
|
|
728
|
+
resource_attributes=$(jq -n \
|
|
729
|
+
--arg service_name "$service_name" \
|
|
730
|
+
--arg service_version "$service_version" \
|
|
731
|
+
--arg project "$project" \
|
|
732
|
+
--arg trace_id "$trace_id" \
|
|
733
|
+
'[
|
|
734
|
+
{key: "service.name", value: {stringValue: $service_name}},
|
|
735
|
+
{key: "service.version", value: {stringValue: $service_version}},
|
|
736
|
+
{key: "project.name", value: {stringValue: $project}},
|
|
737
|
+
{key: "trace.id", value: {stringValue: $trace_id}}
|
|
738
|
+
]')
|
|
739
|
+
else
|
|
740
|
+
resource_attributes=$(jq -n \
|
|
741
|
+
--arg service_name "$service_name" \
|
|
742
|
+
--arg service_version "$service_version" \
|
|
743
|
+
--arg project "$project" \
|
|
744
|
+
'[
|
|
745
|
+
{key: "service.name", value: {stringValue: $service_name}},
|
|
746
|
+
{key: "service.version", value: {stringValue: $service_version}},
|
|
747
|
+
{key: "project.name", value: {stringValue: $project}}
|
|
748
|
+
]')
|
|
749
|
+
fi
|
|
750
|
+
|
|
751
|
+
# Determine status string
|
|
752
|
+
local status_str="fail"
|
|
753
|
+
if [ "$passed" = "true" ]; then
|
|
754
|
+
status_str="pass"
|
|
755
|
+
fi
|
|
756
|
+
|
|
757
|
+
# Build the full OTLP payload
|
|
758
|
+
jq -n \
|
|
759
|
+
--arg service_version "$service_version" \
|
|
760
|
+
--arg project "$project" \
|
|
761
|
+
--arg timestamp_ns "$timestamp_ns" \
|
|
762
|
+
--arg status_str "$status_str" \
|
|
763
|
+
--argjson duration_ms "$duration_ms" \
|
|
764
|
+
--argjson pass_count "$pass_count" \
|
|
765
|
+
--argjson fail_count "$fail_count" \
|
|
766
|
+
--argjson skip_count "$skip_count" \
|
|
767
|
+
--argjson step_duration_points "$step_duration_points" \
|
|
768
|
+
--argjson resource_attributes "$resource_attributes" \
|
|
769
|
+
'{
|
|
770
|
+
resourceMetrics: [{
|
|
771
|
+
resource: {
|
|
772
|
+
attributes: $resource_attributes
|
|
773
|
+
},
|
|
774
|
+
scopeMetrics: [{
|
|
775
|
+
scope: {
|
|
776
|
+
name: "kabran-config/ci-runner",
|
|
777
|
+
version: $service_version
|
|
778
|
+
},
|
|
779
|
+
metrics: [
|
|
780
|
+
{
|
|
781
|
+
name: "ci.build.duration",
|
|
782
|
+
description: "Total duration of CI build in milliseconds",
|
|
783
|
+
unit: "ms",
|
|
784
|
+
gauge: {
|
|
785
|
+
dataPoints: [{
|
|
786
|
+
attributes: [
|
|
787
|
+
{key: "project", value: {stringValue: $project}},
|
|
788
|
+
{key: "status", value: {stringValue: $status_str}}
|
|
789
|
+
],
|
|
790
|
+
startTimeUnixNano: $timestamp_ns,
|
|
791
|
+
timeUnixNano: $timestamp_ns,
|
|
792
|
+
asDouble: $duration_ms
|
|
793
|
+
}]
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: "ci.build.status",
|
|
798
|
+
description: "CI build status counter (1 = occurrence)",
|
|
799
|
+
unit: "1",
|
|
800
|
+
sum: {
|
|
801
|
+
dataPoints: [{
|
|
802
|
+
attributes: [
|
|
803
|
+
{key: "project", value: {stringValue: $project}},
|
|
804
|
+
{key: "status", value: {stringValue: $status_str}}
|
|
805
|
+
],
|
|
806
|
+
startTimeUnixNano: $timestamp_ns,
|
|
807
|
+
timeUnixNano: $timestamp_ns,
|
|
808
|
+
asInt: "1"
|
|
809
|
+
}],
|
|
810
|
+
aggregationTemporality: 2,
|
|
811
|
+
isMonotonic: true
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
name: "ci.step.count",
|
|
816
|
+
description: "Count of CI steps by status",
|
|
817
|
+
unit: "1",
|
|
818
|
+
sum: {
|
|
819
|
+
dataPoints: [
|
|
820
|
+
{
|
|
821
|
+
attributes: [
|
|
822
|
+
{key: "project", value: {stringValue: $project}},
|
|
823
|
+
{key: "status", value: {stringValue: "pass"}}
|
|
824
|
+
],
|
|
825
|
+
startTimeUnixNano: $timestamp_ns,
|
|
826
|
+
timeUnixNano: $timestamp_ns,
|
|
827
|
+
asInt: ($pass_count | tostring)
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
attributes: [
|
|
831
|
+
{key: "project", value: {stringValue: $project}},
|
|
832
|
+
{key: "status", value: {stringValue: "fail"}}
|
|
833
|
+
],
|
|
834
|
+
startTimeUnixNano: $timestamp_ns,
|
|
835
|
+
timeUnixNano: $timestamp_ns,
|
|
836
|
+
asInt: ($fail_count | tostring)
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
attributes: [
|
|
840
|
+
{key: "project", value: {stringValue: $project}},
|
|
841
|
+
{key: "status", value: {stringValue: "skip"}}
|
|
842
|
+
],
|
|
843
|
+
startTimeUnixNano: $timestamp_ns,
|
|
844
|
+
timeUnixNano: $timestamp_ns,
|
|
845
|
+
asInt: ($skip_count | tostring)
|
|
846
|
+
}
|
|
847
|
+
],
|
|
848
|
+
aggregationTemporality: 2,
|
|
849
|
+
isMonotonic: false
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: "ci.step.duration",
|
|
854
|
+
description: "Duration of individual CI steps in milliseconds",
|
|
855
|
+
unit: "ms",
|
|
856
|
+
gauge: {
|
|
857
|
+
dataPoints: $step_duration_points
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
]
|
|
861
|
+
}]
|
|
862
|
+
}]
|
|
863
|
+
}'
|
|
864
|
+
}
|
|
865
|
+
|
|
593
866
|
# ==============================================================================
|
|
594
867
|
# JSON Output Generation
|
|
595
868
|
# ==============================================================================
|
|
@@ -31,6 +31,7 @@ Environment Variables:
|
|
|
31
31
|
CI_OUTPUT_FILE Output file for legacy v1 format
|
|
32
32
|
CI_OUTPUT_FILE_V2 Output file for v2 format (default: docs/quality/ci-result.json)
|
|
33
33
|
CI_CONFIG_FILE Path to project ci-config.sh
|
|
34
|
+
OTEL_ENDPOINT OTel Collector endpoint for metrics export (e.g., http://localhost:4318)
|
|
34
35
|
|
|
35
36
|
Examples:
|
|
36
37
|
# Run all steps
|
|
@@ -232,6 +233,14 @@ if [ "$USE_V2" = "true" ]; then
|
|
|
232
233
|
generate_ci_json "$OUTPUT_FILE" "$CI_PASSED" "$FAILED" "$PROJECT_NAME" "$METADATA"
|
|
233
234
|
fi
|
|
234
235
|
|
|
236
|
+
# ==============================================================================
|
|
237
|
+
# Export Metrics to OTel Collector (if configured)
|
|
238
|
+
# ==============================================================================
|
|
239
|
+
# Fail-safe: telemetry failures NEVER fail the build (RN-01)
|
|
240
|
+
if [ -n "${OTEL_ENDPOINT:-}" ]; then
|
|
241
|
+
export_ci_metrics_to_otel "$INTERMEDIATE_FILE" || true
|
|
242
|
+
fi
|
|
243
|
+
|
|
235
244
|
# Cleanup
|
|
236
245
|
rm -f "$INTERMEDIATE_FILE"
|
|
237
246
|
else
|
|
@@ -51,7 +51,7 @@ export function extractHistoryEntry(result) {
|
|
|
51
51
|
export function loadHistory(filePath) {
|
|
52
52
|
if (!existsSync(filePath)) {
|
|
53
53
|
return {
|
|
54
|
-
$schema: '
|
|
54
|
+
$schema: 'ci-result-history.json',
|
|
55
55
|
version: '1.0.0',
|
|
56
56
|
meta: {
|
|
57
57
|
created_at: new Date().toISOString(),
|
|
@@ -75,7 +75,7 @@ export function loadHistory(filePath) {
|
|
|
75
75
|
} catch (error) {
|
|
76
76
|
// If file is corrupted, start fresh
|
|
77
77
|
return {
|
|
78
|
-
$schema: '
|
|
78
|
+
$schema: 'ci-result-history.json',
|
|
79
79
|
version: '1.0.0',
|
|
80
80
|
meta: {
|
|
81
81
|
created_at: new Date().toISOString(),
|
|
@@ -436,7 +436,7 @@ export function createMinimalResult({ projectName, passed = true }) {
|
|
|
436
436
|
const now = new Date().toISOString()
|
|
437
437
|
|
|
438
438
|
return {
|
|
439
|
-
$schema: '
|
|
439
|
+
$schema: 'ci-result.v2.json',
|
|
440
440
|
version: '1.0.0',
|
|
441
441
|
meta: {
|
|
442
442
|
generated_at: now,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# ==============================================================================
|
|
3
3
|
# Kabran Deploy Core - Shared Functions
|
|
4
|
-
# Part of @kabran-
|
|
4
|
+
# Part of @kabran-tecnologia/kabran-config
|
|
5
5
|
# ==============================================================================
|
|
6
6
|
|
|
7
7
|
# ==============================================================================
|
|
@@ -18,9 +18,13 @@ import {exec} from 'node:child_process';
|
|
|
18
18
|
import {promisify} from 'node:util';
|
|
19
19
|
import fs from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
|
+
import {loadConfig, DEFAULTS} from '../core/config-loader.mjs';
|
|
21
22
|
|
|
22
23
|
const execAsync = promisify(exec);
|
|
23
24
|
|
|
25
|
+
// Default patterns for backwards compatibility
|
|
26
|
+
export const DEFAULT_DETECT_PATTERNS = DEFAULTS.env.detectPatterns;
|
|
27
|
+
|
|
24
28
|
/**
|
|
25
29
|
* Check if .env file is tracked in git (CRITICAL SECURITY ISSUE)
|
|
26
30
|
* @param {string} [cwd] - Directory to check (defaults to process.cwd())
|
|
@@ -40,17 +44,10 @@ export async function checkEnvInGit(cwd = process.cwd()) {
|
|
|
40
44
|
/**
|
|
41
45
|
* Detect if project uses environment variables
|
|
42
46
|
* @param {string} [cwd] - Directory to check (defaults to process.cwd())
|
|
47
|
+
* @param {string[]} [patterns] - Patterns to search for (defaults to config patterns)
|
|
43
48
|
* @returns {Promise<{usesEnv: boolean, files: string[]}>}
|
|
44
49
|
*/
|
|
45
|
-
export async function detectEnvUsage(cwd = process.cwd()) {
|
|
46
|
-
const patterns = [
|
|
47
|
-
'process.env', // Node.js
|
|
48
|
-
'os.getenv', // Python
|
|
49
|
-
'import.meta.env', // Vite/ESM
|
|
50
|
-
'Deno.env', // Deno
|
|
51
|
-
'$_ENV', // PHP
|
|
52
|
-
];
|
|
53
|
-
|
|
50
|
+
export async function detectEnvUsage(cwd = process.cwd(), patterns = DEFAULT_DETECT_PATTERNS) {
|
|
54
51
|
const extensions = ['js', 'ts', 'jsx', 'tsx', 'mjs', 'cjs', 'py', 'php'];
|
|
55
52
|
const extensionPattern = extensions.join(',');
|
|
56
53
|
|
|
@@ -157,6 +154,9 @@ export async function validateEnv(cwd = process.cwd(), silent = false) {
|
|
|
157
154
|
const errors = [];
|
|
158
155
|
const warnings = [];
|
|
159
156
|
|
|
157
|
+
// Load project config
|
|
158
|
+
const config = await loadConfig(cwd);
|
|
159
|
+
|
|
160
160
|
// CRITICAL: Check if .env is committed to git
|
|
161
161
|
log('Checking for .env in git...');
|
|
162
162
|
const envInGit = await checkEnvInGit(cwd);
|
|
@@ -171,7 +171,7 @@ export async function validateEnv(cwd = process.cwd(), silent = false) {
|
|
|
171
171
|
|
|
172
172
|
// Detect if project uses environment variables
|
|
173
173
|
log('Detecting environment variable usage...');
|
|
174
|
-
const {usesEnv, files} = await detectEnvUsage(cwd);
|
|
174
|
+
const {usesEnv, files} = await detectEnvUsage(cwd, config.env.detectPatterns);
|
|
175
175
|
|
|
176
176
|
if (!usesEnv) {
|
|
177
177
|
log(' No environment variable usage detected');
|
|
@@ -236,9 +236,10 @@ export async function validateEnv(cwd = process.cwd(), silent = false) {
|
|
|
236
236
|
* @returns {Promise<Object>} Check result for ci-result.json
|
|
237
237
|
*/
|
|
238
238
|
export async function getEnvCheckResult(cwd = process.cwd()) {
|
|
239
|
+
const config = await loadConfig(cwd);
|
|
239
240
|
const envInGit = await checkEnvInGit(cwd);
|
|
240
241
|
const envExample = checkEnvExampleExists(cwd);
|
|
241
|
-
const {usesEnv} = await detectEnvUsage(cwd);
|
|
242
|
+
const {usesEnv} = await detectEnvUsage(cwd, config.env.detectPatterns);
|
|
242
243
|
|
|
243
244
|
let undocumented = [];
|
|
244
245
|
if (envExample.exists) {
|
|
@@ -103,7 +103,7 @@ async function runValidators(projectRoot, options = {}) {
|
|
|
103
103
|
|
|
104
104
|
if (!options.skipReadme) {
|
|
105
105
|
try {
|
|
106
|
-
results.readme = getReadmeCheckResult(projectRoot)
|
|
106
|
+
results.readme = await getReadmeCheckResult(projectRoot)
|
|
107
107
|
} catch (err) {
|
|
108
108
|
results.readme = { status: 'fail', error: err.message }
|
|
109
109
|
}
|
|
@@ -119,7 +119,7 @@ async function runValidators(projectRoot, options = {}) {
|
|
|
119
119
|
|
|
120
120
|
if (!options.skipQualityStandard) {
|
|
121
121
|
try {
|
|
122
|
-
results.quality_standard = getQualityStandardCheckResult(projectRoot)
|
|
122
|
+
results.quality_standard = await getQualityStandardCheckResult(projectRoot)
|
|
123
123
|
} catch (err) {
|
|
124
124
|
results.quality_standard = { status: 'fail', error: err.message }
|
|
125
125
|
}
|
|
@@ -216,7 +216,7 @@ export function generateCiResult(input) {
|
|
|
216
216
|
|
|
217
217
|
// Build result object
|
|
218
218
|
const result = {
|
|
219
|
-
$schema: '
|
|
219
|
+
$schema: 'ci-result.v2.json',
|
|
220
220
|
version: '1.0.0',
|
|
221
221
|
|
|
222
222
|
meta,
|