@kabran-tecnologia/kabran-config 1.6.0 → 1.8.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 +63 -8
- package/src/schemas/ci-result.v2.schema.json +125 -0
- package/src/scripts/ci/ci-core.sh +131 -1
- package/src/scripts/ci/ci-runner.sh +88 -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 +104 -0
- package/src/scripts/generate-ci-result.mjs +92 -11
- package/src/scripts/pr-quality-comment.mjs +36 -0
- package/src/scripts/setup.mjs +91 -4
- package/src/telemetry/README.md +407 -0
- 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/telemetry/.env.telemetry.example +118 -0
|
@@ -27,6 +27,8 @@ NC='\033[0m'
|
|
|
27
27
|
declare -a ERRORS=()
|
|
28
28
|
declare -a STEP_RESULTS=()
|
|
29
29
|
CI_START_TIME=""
|
|
30
|
+
CI_TRACE_ID=""
|
|
31
|
+
CI_SPAN_ID=""
|
|
30
32
|
|
|
31
33
|
# ==============================================================================
|
|
32
34
|
# Logging Functions
|
|
@@ -58,6 +60,128 @@ log_debug() {
|
|
|
58
60
|
fi
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
# ==============================================================================
|
|
64
|
+
# Trace Context Functions (OpenTelemetry W3C Trace Context)
|
|
65
|
+
# ==============================================================================
|
|
66
|
+
|
|
67
|
+
# Generate a random hex string of specified length
|
|
68
|
+
# Usage: generate_hex_string 32
|
|
69
|
+
generate_hex_string() {
|
|
70
|
+
local length="${1:-32}"
|
|
71
|
+
# Try multiple methods for generating random hex
|
|
72
|
+
if command -v openssl &>/dev/null; then
|
|
73
|
+
openssl rand -hex "$((length / 2))" 2>/dev/null
|
|
74
|
+
elif [ -r /dev/urandom ]; then
|
|
75
|
+
head -c "$((length / 2))" /dev/urandom | od -An -tx1 | tr -d ' \n' | head -c "$length"
|
|
76
|
+
else
|
|
77
|
+
# Fallback: use date + process ID + random
|
|
78
|
+
local seed="$$$(date +%s%N 2>/dev/null || date +%s)"
|
|
79
|
+
echo "$seed" | md5sum 2>/dev/null | head -c "$length" || echo "$seed" | head -c "$length"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Generate a W3C trace ID (32 hex chars = 128 bits)
|
|
84
|
+
# Usage: generate_trace_id
|
|
85
|
+
generate_trace_id() {
|
|
86
|
+
generate_hex_string 32
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Generate a W3C span ID (16 hex chars = 64 bits)
|
|
90
|
+
# Usage: generate_span_id
|
|
91
|
+
generate_span_id() {
|
|
92
|
+
generate_hex_string 16
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Initialize trace context for the CI run
|
|
96
|
+
# Sets TRACEPARENT env var if not already set
|
|
97
|
+
# Format: 00-{trace_id}-{span_id}-{flags}
|
|
98
|
+
# Usage: setup_trace_context
|
|
99
|
+
setup_trace_context() {
|
|
100
|
+
# Check if trace context already exists from environment
|
|
101
|
+
if [ -n "${TRACEPARENT:-}" ]; then
|
|
102
|
+
log_debug "Using existing TRACEPARENT: $TRACEPARENT"
|
|
103
|
+
# Extract trace_id and span_id from existing TRACEPARENT
|
|
104
|
+
CI_TRACE_ID=$(echo "$TRACEPARENT" | cut -d'-' -f2)
|
|
105
|
+
CI_SPAN_ID=$(echo "$TRACEPARENT" | cut -d'-' -f3)
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Check for direct trace ID from environment
|
|
110
|
+
if [ -n "${OTEL_TRACE_ID:-}" ]; then
|
|
111
|
+
log_debug "Using OTEL_TRACE_ID: $OTEL_TRACE_ID"
|
|
112
|
+
CI_TRACE_ID="$OTEL_TRACE_ID"
|
|
113
|
+
CI_SPAN_ID=$(generate_span_id)
|
|
114
|
+
TRACEPARENT="00-${CI_TRACE_ID}-${CI_SPAN_ID}-01"
|
|
115
|
+
export TRACEPARENT
|
|
116
|
+
return 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# Check for GitHub Actions run ID (use as fallback correlation)
|
|
120
|
+
if [ -n "${GITHUB_RUN_ID:-}" ]; then
|
|
121
|
+
log_debug "Using GitHub run ID for trace correlation"
|
|
122
|
+
# Create deterministic trace_id from GitHub run info
|
|
123
|
+
local gh_seed="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT:-1}"
|
|
124
|
+
CI_TRACE_ID=$(echo "$gh_seed" | md5sum | head -c 32)
|
|
125
|
+
CI_SPAN_ID=$(generate_span_id)
|
|
126
|
+
TRACEPARENT="00-${CI_TRACE_ID}-${CI_SPAN_ID}-01"
|
|
127
|
+
export TRACEPARENT
|
|
128
|
+
log_debug "Generated TRACEPARENT from GitHub: $TRACEPARENT"
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Generate new trace context for local execution
|
|
133
|
+
log_debug "Generating new trace context for local CI run"
|
|
134
|
+
CI_TRACE_ID=$(generate_trace_id)
|
|
135
|
+
CI_SPAN_ID=$(generate_span_id)
|
|
136
|
+
TRACEPARENT="00-${CI_TRACE_ID}-${CI_SPAN_ID}-01"
|
|
137
|
+
export TRACEPARENT
|
|
138
|
+
|
|
139
|
+
log_info "Trace ID: ${CI_TRACE_ID:0:8}... (local)"
|
|
140
|
+
log_debug "Full TRACEPARENT: $TRACEPARENT"
|
|
141
|
+
return 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Get the current trace ID
|
|
145
|
+
# Usage: get_trace_id
|
|
146
|
+
get_trace_id() {
|
|
147
|
+
echo "${CI_TRACE_ID:-}"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Get trace context info for metadata
|
|
151
|
+
# Usage: get_trace_context_json
|
|
152
|
+
get_trace_context_json() {
|
|
153
|
+
local trace_id="${CI_TRACE_ID:-}"
|
|
154
|
+
local span_id="${CI_SPAN_ID:-}"
|
|
155
|
+
local traceparent="${TRACEPARENT:-}"
|
|
156
|
+
|
|
157
|
+
if [ -z "$trace_id" ]; then
|
|
158
|
+
echo "null"
|
|
159
|
+
return
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Determine source of trace context
|
|
163
|
+
local source="local"
|
|
164
|
+
if [ -n "${GITHUB_RUN_ID:-}" ]; then
|
|
165
|
+
source="github"
|
|
166
|
+
elif [ -n "${OTEL_TRACE_ID:-}" ]; then
|
|
167
|
+
source="otel_env"
|
|
168
|
+
elif [ -n "${TRACEPARENT:-}" ] && [ "${CI_TRACE_ID:-}" != "$(echo "$TRACEPARENT" | cut -d'-' -f2)" ]; then
|
|
169
|
+
source="external"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
jq -n \
|
|
173
|
+
--arg trace_id "$trace_id" \
|
|
174
|
+
--arg span_id "$span_id" \
|
|
175
|
+
--arg traceparent "$traceparent" \
|
|
176
|
+
--arg source "$source" \
|
|
177
|
+
'{
|
|
178
|
+
trace_id: $trace_id,
|
|
179
|
+
span_id: $span_id,
|
|
180
|
+
traceparent: $traceparent,
|
|
181
|
+
source: $source
|
|
182
|
+
}'
|
|
183
|
+
}
|
|
184
|
+
|
|
61
185
|
# ==============================================================================
|
|
62
186
|
# Version Compatibility Check
|
|
63
187
|
# ==============================================================================
|
|
@@ -432,6 +556,10 @@ export_ci_data() {
|
|
|
432
556
|
# Get scope
|
|
433
557
|
local scope="${CI_SCOPE:-all}"
|
|
434
558
|
|
|
559
|
+
# Get trace context
|
|
560
|
+
local trace_context
|
|
561
|
+
trace_context=$(get_trace_context_json)
|
|
562
|
+
|
|
435
563
|
# Generate intermediate data file for Node.js generator
|
|
436
564
|
jq -n \
|
|
437
565
|
--argjson steps "$steps_json" \
|
|
@@ -441,6 +569,7 @@ export_ci_data() {
|
|
|
441
569
|
--arg finished_at "$now" \
|
|
442
570
|
--arg project_name "$project_name" \
|
|
443
571
|
--arg scope "$scope" \
|
|
572
|
+
--argjson trace_context "$trace_context" \
|
|
444
573
|
'{
|
|
445
574
|
steps: $steps,
|
|
446
575
|
errors: $errors,
|
|
@@ -454,7 +583,8 @@ export_ci_data() {
|
|
|
454
583
|
},
|
|
455
584
|
metadata: {
|
|
456
585
|
scope: $scope
|
|
457
|
-
}
|
|
586
|
+
},
|
|
587
|
+
trace_context: $trace_context
|
|
458
588
|
}' > "$output_file"
|
|
459
589
|
|
|
460
590
|
log_debug "CI data exported to: $output_file"
|
|
@@ -10,6 +10,64 @@ set -euo pipefail
|
|
|
10
10
|
RUNNER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
CORE_SCRIPT="$RUNNER_DIR/ci-core.sh"
|
|
12
12
|
|
|
13
|
+
# ==============================================================================
|
|
14
|
+
# Command Line Arguments
|
|
15
|
+
# ==============================================================================
|
|
16
|
+
|
|
17
|
+
show_help() {
|
|
18
|
+
cat << EOF
|
|
19
|
+
Usage: $(basename "$0") [options]
|
|
20
|
+
|
|
21
|
+
Kabran CI Runner - Execute project CI pipelines
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--list-scopes List available scopes from ci-config.sh and exit
|
|
25
|
+
--scope <name> Run only the specified scope (component)
|
|
26
|
+
--help Show this help message and exit
|
|
27
|
+
|
|
28
|
+
Environment Variables:
|
|
29
|
+
CI_SCOPE Set the scope filter (default: all)
|
|
30
|
+
CI_VERBOSE Enable verbose output (default: false)
|
|
31
|
+
CI_OUTPUT_FILE Output file for legacy v1 format
|
|
32
|
+
CI_OUTPUT_FILE_V2 Output file for v2 format (default: docs/quality/ci-result.json)
|
|
33
|
+
CI_CONFIG_FILE Path to project ci-config.sh
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
# Run all steps
|
|
37
|
+
$(basename "$0")
|
|
38
|
+
|
|
39
|
+
# Run only specific component
|
|
40
|
+
$(basename "$0") --scope app
|
|
41
|
+
|
|
42
|
+
# List available scopes
|
|
43
|
+
$(basename "$0") --list-scopes
|
|
44
|
+
EOF
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
LIST_SCOPES=false
|
|
48
|
+
|
|
49
|
+
while [[ $# -gt 0 ]]; do
|
|
50
|
+
case $1 in
|
|
51
|
+
--list-scopes)
|
|
52
|
+
LIST_SCOPES=true
|
|
53
|
+
shift
|
|
54
|
+
;;
|
|
55
|
+
--scope)
|
|
56
|
+
CI_SCOPE="$2"
|
|
57
|
+
shift 2
|
|
58
|
+
;;
|
|
59
|
+
--help|-h)
|
|
60
|
+
show_help
|
|
61
|
+
exit 0
|
|
62
|
+
;;
|
|
63
|
+
*)
|
|
64
|
+
echo "Unknown option: $1" >&2
|
|
65
|
+
show_help
|
|
66
|
+
exit 1
|
|
67
|
+
;;
|
|
68
|
+
esac
|
|
69
|
+
done
|
|
70
|
+
|
|
13
71
|
# Load core functions
|
|
14
72
|
if [ ! -f "$CORE_SCRIPT" ]; then
|
|
15
73
|
echo "ERROR: ci-core.sh not found at $CORE_SCRIPT" >&2
|
|
@@ -58,6 +116,33 @@ fi
|
|
|
58
116
|
log_info "Loading project configuration: $CONFIG_FILE"
|
|
59
117
|
source "$CONFIG_FILE"
|
|
60
118
|
|
|
119
|
+
# ==============================================================================
|
|
120
|
+
# List Scopes (if requested)
|
|
121
|
+
# ==============================================================================
|
|
122
|
+
|
|
123
|
+
if [ "$LIST_SCOPES" = "true" ]; then
|
|
124
|
+
echo "Available scopes in $PROJECT_NAME:"
|
|
125
|
+
echo ""
|
|
126
|
+
|
|
127
|
+
# Check if CI_COMPONENTS is defined (common pattern for monorepos)
|
|
128
|
+
if [ -n "${CI_COMPONENTS:-}" ]; then
|
|
129
|
+
echo "Components:"
|
|
130
|
+
for component in $CI_COMPONENTS; do
|
|
131
|
+
echo " - $component"
|
|
132
|
+
done
|
|
133
|
+
elif declare -f list_scopes >/dev/null; then
|
|
134
|
+
# Project can define custom list_scopes function
|
|
135
|
+
list_scopes
|
|
136
|
+
else
|
|
137
|
+
echo " all (single project - no components defined)"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
echo ""
|
|
141
|
+
echo "Usage: CI_SCOPE=<scope> $(basename "$0")"
|
|
142
|
+
echo " or: $(basename "$0") --scope <scope>"
|
|
143
|
+
exit 0
|
|
144
|
+
fi
|
|
145
|
+
|
|
61
146
|
# ==============================================================================
|
|
62
147
|
# Validate Configuration
|
|
63
148
|
# ==============================================================================
|
|
@@ -86,6 +171,9 @@ if [ "$CI_SCOPE" != "all" ]; then
|
|
|
86
171
|
fi
|
|
87
172
|
echo ""
|
|
88
173
|
|
|
174
|
+
# Setup trace context (generates trace_id if not provided externally)
|
|
175
|
+
setup_trace_context
|
|
176
|
+
|
|
89
177
|
# Start timing
|
|
90
178
|
ci_start
|
|
91
179
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI Result History Module
|
|
3
|
+
*
|
|
4
|
+
* Manages historical CI run data for trend analysis.
|
|
5
|
+
* Stores up to 30 recent runs in docs/quality/ci-result-history.json.
|
|
6
|
+
*
|
|
7
|
+
* @module ci-result-history
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
11
|
+
import { dirname } from 'node:path'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default maximum number of history entries to keep
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_MAX_ENTRIES = 30
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract a minimal history entry from a full CI result
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} result - Full CI result object
|
|
22
|
+
* @returns {Object} Minimal history entry
|
|
23
|
+
*/
|
|
24
|
+
export function extractHistoryEntry(result) {
|
|
25
|
+
return {
|
|
26
|
+
run_id: result.meta?.run_id || 'unknown',
|
|
27
|
+
generated_at: result.meta?.generated_at || new Date().toISOString(),
|
|
28
|
+
branch: result.meta?.branch || 'unknown',
|
|
29
|
+
commit: result.meta?.commit || 'unknown',
|
|
30
|
+
score: result.summary?.score ?? 0,
|
|
31
|
+
status: result.summary?.status || 'unknown',
|
|
32
|
+
timing: {
|
|
33
|
+
total_ms: result.timing?.total_ms || 0,
|
|
34
|
+
total_human: result.timing?.total_human || '0ms',
|
|
35
|
+
},
|
|
36
|
+
summary: {
|
|
37
|
+
exit_code: result.summary?.exit_code ?? 0,
|
|
38
|
+
total_issues: result.summary?.total_issues ?? 0,
|
|
39
|
+
blocking: result.summary?.blocking ?? 0,
|
|
40
|
+
warnings: result.summary?.warnings ?? 0,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load history from file
|
|
47
|
+
*
|
|
48
|
+
* @param {string} filePath - Path to history file
|
|
49
|
+
* @returns {Object} History object with entries array and metadata
|
|
50
|
+
*/
|
|
51
|
+
export function loadHistory(filePath) {
|
|
52
|
+
if (!existsSync(filePath)) {
|
|
53
|
+
return {
|
|
54
|
+
$schema: 'https://kabran.dev/schemas/ci-result-history.json',
|
|
55
|
+
version: '1.0.0',
|
|
56
|
+
meta: {
|
|
57
|
+
created_at: new Date().toISOString(),
|
|
58
|
+
updated_at: new Date().toISOString(),
|
|
59
|
+
max_entries: DEFAULT_MAX_ENTRIES,
|
|
60
|
+
},
|
|
61
|
+
entries: [],
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const content = readFileSync(filePath, 'utf8')
|
|
67
|
+
const history = JSON.parse(content)
|
|
68
|
+
|
|
69
|
+
// Ensure entries is an array
|
|
70
|
+
if (!Array.isArray(history.entries)) {
|
|
71
|
+
history.entries = []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return history
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// If file is corrupted, start fresh
|
|
77
|
+
return {
|
|
78
|
+
$schema: 'https://kabran.dev/schemas/ci-result-history.json',
|
|
79
|
+
version: '1.0.0',
|
|
80
|
+
meta: {
|
|
81
|
+
created_at: new Date().toISOString(),
|
|
82
|
+
updated_at: new Date().toISOString(),
|
|
83
|
+
max_entries: DEFAULT_MAX_ENTRIES,
|
|
84
|
+
error: `Failed to load: ${error.message}`,
|
|
85
|
+
},
|
|
86
|
+
entries: [],
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Prune history to maximum number of entries
|
|
93
|
+
*
|
|
94
|
+
* @param {Array} entries - History entries
|
|
95
|
+
* @param {number} maxEntries - Maximum entries to keep
|
|
96
|
+
* @returns {Array} Pruned entries (most recent first)
|
|
97
|
+
*/
|
|
98
|
+
export function pruneHistory(entries, maxEntries = DEFAULT_MAX_ENTRIES) {
|
|
99
|
+
if (!Array.isArray(entries)) {
|
|
100
|
+
return []
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Sort by generated_at descending (most recent first)
|
|
104
|
+
const sorted = [...entries].sort((a, b) => {
|
|
105
|
+
const dateA = new Date(a.generated_at || 0)
|
|
106
|
+
const dateB = new Date(b.generated_at || 0)
|
|
107
|
+
return dateB - dateA
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Keep only the most recent entries
|
|
111
|
+
return sorted.slice(0, maxEntries)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add a CI result to history
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} result - Full CI result object
|
|
118
|
+
* @param {Object} history - Existing history object
|
|
119
|
+
* @returns {Object} Updated history object
|
|
120
|
+
*/
|
|
121
|
+
export function addToHistory(result, history) {
|
|
122
|
+
const entry = extractHistoryEntry(result)
|
|
123
|
+
|
|
124
|
+
// Check for duplicate run_id
|
|
125
|
+
const existingIndex = history.entries.findIndex((e) => e.run_id === entry.run_id)
|
|
126
|
+
|
|
127
|
+
if (existingIndex >= 0) {
|
|
128
|
+
// Update existing entry
|
|
129
|
+
history.entries[existingIndex] = entry
|
|
130
|
+
} else {
|
|
131
|
+
// Add new entry at the beginning
|
|
132
|
+
history.entries.unshift(entry)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Update metadata
|
|
136
|
+
history.meta = {
|
|
137
|
+
...history.meta,
|
|
138
|
+
updated_at: new Date().toISOString(),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return history
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Save history to file
|
|
146
|
+
*
|
|
147
|
+
* @param {Object} history - History object
|
|
148
|
+
* @param {string} filePath - Path to save
|
|
149
|
+
* @param {number} maxEntries - Maximum entries to keep
|
|
150
|
+
*/
|
|
151
|
+
export function saveHistory(history, filePath, maxEntries = DEFAULT_MAX_ENTRIES) {
|
|
152
|
+
// Prune before saving
|
|
153
|
+
history.entries = pruneHistory(history.entries, maxEntries)
|
|
154
|
+
history.meta.max_entries = maxEntries
|
|
155
|
+
history.meta.entry_count = history.entries.length
|
|
156
|
+
|
|
157
|
+
// Ensure directory exists
|
|
158
|
+
const dir = dirname(filePath)
|
|
159
|
+
if (!existsSync(dir)) {
|
|
160
|
+
mkdirSync(dir, { recursive: true })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Write file
|
|
164
|
+
writeFileSync(filePath, JSON.stringify(history, null, 2) + '\n')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get entries for a specific branch
|
|
169
|
+
*
|
|
170
|
+
* @param {Array} entries - History entries
|
|
171
|
+
* @param {string} branch - Branch name to filter
|
|
172
|
+
* @returns {Array} Filtered entries
|
|
173
|
+
*/
|
|
174
|
+
export function filterByBranch(entries, branch) {
|
|
175
|
+
if (!branch) return entries
|
|
176
|
+
return entries.filter((e) => e.branch === branch)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get entries within a date range
|
|
181
|
+
*
|
|
182
|
+
* @param {Array} entries - History entries
|
|
183
|
+
* @param {number} days - Number of days to include
|
|
184
|
+
* @returns {Array} Filtered entries
|
|
185
|
+
*/
|
|
186
|
+
export function filterByDays(entries, days) {
|
|
187
|
+
if (!days || days <= 0) return entries
|
|
188
|
+
|
|
189
|
+
const cutoff = new Date()
|
|
190
|
+
cutoff.setDate(cutoff.getDate() - days)
|
|
191
|
+
|
|
192
|
+
return entries.filter((e) => {
|
|
193
|
+
const date = new Date(e.generated_at)
|
|
194
|
+
return date >= cutoff
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get statistics from history
|
|
200
|
+
*
|
|
201
|
+
* @param {Array} entries - History entries
|
|
202
|
+
* @returns {Object} Statistics object
|
|
203
|
+
*/
|
|
204
|
+
export function getHistoryStats(entries) {
|
|
205
|
+
if (!entries || entries.length === 0) {
|
|
206
|
+
return {
|
|
207
|
+
total_runs: 0,
|
|
208
|
+
oldest_run: null,
|
|
209
|
+
newest_run: null,
|
|
210
|
+
unique_branches: 0,
|
|
211
|
+
branches: [],
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const sorted = [...entries].sort((a, b) => {
|
|
216
|
+
return new Date(a.generated_at) - new Date(b.generated_at)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const branches = [...new Set(entries.map((e) => e.branch).filter(Boolean))]
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
total_runs: entries.length,
|
|
223
|
+
oldest_run: sorted[0]?.generated_at || null,
|
|
224
|
+
newest_run: sorted[sorted.length - 1]?.generated_at || null,
|
|
225
|
+
unique_branches: branches.length,
|
|
226
|
+
branches,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Process a CI result and update history
|
|
232
|
+
*
|
|
233
|
+
* Convenience function that loads, updates, and saves history.
|
|
234
|
+
*
|
|
235
|
+
* @param {Object} result - Full CI result object
|
|
236
|
+
* @param {string} historyPath - Path to history file
|
|
237
|
+
* @param {number} maxEntries - Maximum entries to keep
|
|
238
|
+
* @returns {Object} Updated history object
|
|
239
|
+
*/
|
|
240
|
+
export function processHistory(result, historyPath, maxEntries = DEFAULT_MAX_ENTRIES) {
|
|
241
|
+
const history = loadHistory(historyPath)
|
|
242
|
+
addToHistory(result, history)
|
|
243
|
+
saveHistory(history, historyPath, maxEntries)
|
|
244
|
+
return history
|
|
245
|
+
}
|