shipwright-cli 1.10.0 → 2.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/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +208 -1
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +432 -130
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +64 -3
- package/scripts/sw-memory.sh +1 -1
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +687 -24
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ sw-dora.sh — DORA Metrics Dashboard with Engineering Intelligence ║
|
|
4
|
+
# ║ ║
|
|
5
|
+
# ║ Computes Lead Time, Deploy Frequency, Change Failure Rate, MTTR, ║
|
|
6
|
+
# ║ DX metrics, AI intelligence metrics, trends, and comparative analysis ║
|
|
7
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
10
|
+
|
|
11
|
+
VERSION="2.0.0"
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
|
|
14
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
15
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
16
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
17
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
18
|
+
GREEN='\033[38;2;74;222;128m' # #4ade80 — success
|
|
19
|
+
YELLOW='\033[38;2;250;204;21m' # #faca15 — warning
|
|
20
|
+
RED='\033[38;2;248;113;113m' # #f87171 — error
|
|
21
|
+
DIM='\033[2m'
|
|
22
|
+
BOLD='\033[1m'
|
|
23
|
+
RESET='\033[0m'
|
|
24
|
+
|
|
25
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
26
|
+
_COMPAT="$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
[[ -f "$_COMPAT" ]] && source "$_COMPAT"
|
|
28
|
+
|
|
29
|
+
# ─── Helpers ────────────────────────────────────────────────────────────────
|
|
30
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
31
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
32
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
33
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
34
|
+
|
|
35
|
+
emit_event() {
|
|
36
|
+
local event_type="$1"; shift
|
|
37
|
+
local events_file="${HOME}/.shipwright/events.jsonl"
|
|
38
|
+
mkdir -p "$(dirname "$events_file")"
|
|
39
|
+
local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
|
|
40
|
+
while [[ $# -gt 0 ]]; do
|
|
41
|
+
local key="${1%%=*}" val="${1#*=}"
|
|
42
|
+
payload="${payload},\"${key}\":\"${val}\""
|
|
43
|
+
shift
|
|
44
|
+
done
|
|
45
|
+
payload="${payload}}"
|
|
46
|
+
echo "$payload" >> "$events_file"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
now_epoch() {
|
|
50
|
+
date +%s
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# ─── DORA Metrics Calculation ────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
# Classify performance band per DORA standards
|
|
56
|
+
classify_band() {
|
|
57
|
+
local metric="$1"
|
|
58
|
+
local value="$2"
|
|
59
|
+
|
|
60
|
+
case "$metric" in
|
|
61
|
+
lead_time)
|
|
62
|
+
if (( $(echo "$value <= 1" | bc -l) )); then echo "Elite"
|
|
63
|
+
elif (( $(echo "$value <= 7" | bc -l) )); then echo "High"
|
|
64
|
+
elif (( $(echo "$value <= 30" | bc -l) )); then echo "Medium"
|
|
65
|
+
else echo "Low"; fi
|
|
66
|
+
;;
|
|
67
|
+
deploy_frequency)
|
|
68
|
+
if (( $(echo "$value >= 7" | bc -l) )); then echo "Elite"
|
|
69
|
+
elif (( $(echo "$value >= 1" | bc -l) )); then echo "High"
|
|
70
|
+
elif (( $(echo "$value >= 0.3" | bc -l) )); then echo "Medium"
|
|
71
|
+
else echo "Low"; fi
|
|
72
|
+
;;
|
|
73
|
+
cfr)
|
|
74
|
+
if (( $(echo "$value <= 15" | bc -l) )); then echo "Elite"
|
|
75
|
+
elif (( $(echo "$value <= 30" | bc -l) )); then echo "High"
|
|
76
|
+
elif (( $(echo "$value <= 45" | bc -l) )); then echo "Medium"
|
|
77
|
+
else echo "Low"; fi
|
|
78
|
+
;;
|
|
79
|
+
mttr)
|
|
80
|
+
if (( $(echo "$value <= 1" | bc -l) )); then echo "Elite"
|
|
81
|
+
elif (( $(echo "$value <= 24" | bc -l) )); then echo "High"
|
|
82
|
+
elif (( $(echo "$value <= 168" | bc -l) )); then echo "Medium"
|
|
83
|
+
else echo "Low"; fi
|
|
84
|
+
;;
|
|
85
|
+
*)
|
|
86
|
+
echo "Unknown"
|
|
87
|
+
;;
|
|
88
|
+
esac
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Determine trend arrow
|
|
92
|
+
trend_arrow() {
|
|
93
|
+
local current="$1"
|
|
94
|
+
local previous="$2"
|
|
95
|
+
local metric="$3"
|
|
96
|
+
|
|
97
|
+
# Handle division by zero
|
|
98
|
+
if (( $(echo "$previous == 0" | bc -l) )); then
|
|
99
|
+
echo "→"
|
|
100
|
+
return
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
local threshold=0.05 # 5% change threshold
|
|
104
|
+
local pct_change
|
|
105
|
+
pct_change=$(echo "scale=4; ($current - $previous) / $previous" | bc)
|
|
106
|
+
|
|
107
|
+
# For metrics where lower is better (lead_time, cfr, mttr)
|
|
108
|
+
case "$metric" in
|
|
109
|
+
lead_time|cfr|mttr)
|
|
110
|
+
if (( $(echo "$pct_change < -$threshold" | bc -l) )); then echo "↓"
|
|
111
|
+
elif (( $(echo "$pct_change > $threshold" | bc -l) )); then echo "↑"
|
|
112
|
+
else echo "→"; fi
|
|
113
|
+
;;
|
|
114
|
+
*)
|
|
115
|
+
# For metrics where higher is better (deploy_frequency)
|
|
116
|
+
if (( $(echo "$pct_change > $threshold" | bc -l) )); then echo "↑"
|
|
117
|
+
elif (( $(echo "$pct_change < -$threshold" | bc -l) )); then echo "↓"
|
|
118
|
+
else echo "→"; fi
|
|
119
|
+
;;
|
|
120
|
+
esac
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Calculate DORA metrics for time window
|
|
124
|
+
calculate_dora() {
|
|
125
|
+
local window_days="${1:-7}"
|
|
126
|
+
local offset_days="${2:-0}"
|
|
127
|
+
|
|
128
|
+
local events_file="${HOME}/.shipwright/events.jsonl"
|
|
129
|
+
if [[ ! -f "$events_file" ]]; then
|
|
130
|
+
echo '{"deploy_freq":0,"cycle_time":0,"cfr":0,"mttr":0,"total":0}'
|
|
131
|
+
return 0
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
if ! command -v jq &>/dev/null; then
|
|
135
|
+
echo '{"deploy_freq":0,"cycle_time":0,"cfr":0,"mttr":0,"total":0}'
|
|
136
|
+
return 0
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
local now_e
|
|
140
|
+
now_e=$(now_epoch)
|
|
141
|
+
local window_end=$((now_e - offset_days * 86400))
|
|
142
|
+
local window_start=$((window_end - window_days * 86400))
|
|
143
|
+
|
|
144
|
+
jq -s --argjson start "$window_start" --argjson end "$window_end" '
|
|
145
|
+
[.[] | select(.ts_epoch >= $start and .ts_epoch < $end)] as $events |
|
|
146
|
+
[$events[] | select(.type == "pipeline.completed")] as $completed |
|
|
147
|
+
($completed | length) as $total |
|
|
148
|
+
[$completed[] | select(.result == "success")] as $successes |
|
|
149
|
+
[$completed[] | select(.result == "failure")] as $failures |
|
|
150
|
+
($successes | length) as $success_count |
|
|
151
|
+
($failures | length) as $failure_count |
|
|
152
|
+
(if $total > 0 then ($success_count * 7 / '"$window_days"') else 0 end) as $deploy_freq |
|
|
153
|
+
([$successes[] | .duration_s] | sort |
|
|
154
|
+
if length > 0 then .[length/2 | floor] else 0 end) as $cycle_time |
|
|
155
|
+
(if $total > 0 then ($failure_count / $total * 100) else 0 end) as $cfr |
|
|
156
|
+
($completed | sort_by(.ts_epoch // 0) |
|
|
157
|
+
[range(length) as $i |
|
|
158
|
+
if .[$i].result == "failure" then
|
|
159
|
+
[.[$i+1:][] | select(.result == "success")][0] as $next |
|
|
160
|
+
if $next and $next.ts_epoch and .[$i].ts_epoch then
|
|
161
|
+
($next.ts_epoch - .[$i].ts_epoch)
|
|
162
|
+
else null end
|
|
163
|
+
else null end
|
|
164
|
+
] | map(select(. != null)) |
|
|
165
|
+
if length > 0 then (add / length | floor) else 0 end
|
|
166
|
+
) as $mttr |
|
|
167
|
+
{
|
|
168
|
+
deploy_freq: ($deploy_freq * 100 | floor / 100),
|
|
169
|
+
cycle_time: ($cycle_time / 3600),
|
|
170
|
+
cfr: ($cfr * 10 | floor / 10),
|
|
171
|
+
mttr: ($mttr / 3600),
|
|
172
|
+
total: $total
|
|
173
|
+
}
|
|
174
|
+
' "$events_file" 2>/dev/null || echo '{"deploy_freq":0,"cycle_time":0,"cfr":0,"mttr":0,"total":0}'
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# ─── Dashboard Display ────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
show_dora_dashboard() {
|
|
180
|
+
info "Shipwright DORA Metrics Dashboard"
|
|
181
|
+
echo ""
|
|
182
|
+
|
|
183
|
+
local current previous
|
|
184
|
+
current=$(calculate_dora 7 0)
|
|
185
|
+
previous=$(calculate_dora 7 7)
|
|
186
|
+
|
|
187
|
+
if ! command -v jq &>/dev/null; then
|
|
188
|
+
error "jq is required for dashboard display"
|
|
189
|
+
exit 1
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# Extract metrics
|
|
193
|
+
local curr_deploy_freq curr_cycle_time curr_cfr curr_mttr curr_total
|
|
194
|
+
local prev_deploy_freq prev_cycle_time prev_cfr prev_mttr
|
|
195
|
+
|
|
196
|
+
curr_deploy_freq=$(echo "$current" | jq -r '.deploy_freq // 0')
|
|
197
|
+
curr_cycle_time=$(echo "$current" | jq -r '.cycle_time // 0')
|
|
198
|
+
curr_cfr=$(echo "$current" | jq -r '.cfr // 0')
|
|
199
|
+
curr_mttr=$(echo "$current" | jq -r '.mttr // 0')
|
|
200
|
+
curr_total=$(echo "$current" | jq -r '.total // 0')
|
|
201
|
+
|
|
202
|
+
prev_deploy_freq=$(echo "$previous" | jq -r '.deploy_freq // 0')
|
|
203
|
+
prev_cycle_time=$(echo "$previous" | jq -r '.cycle_time // 0')
|
|
204
|
+
prev_cfr=$(echo "$previous" | jq -r '.cfr // 0')
|
|
205
|
+
prev_mttr=$(echo "$previous" | jq -r '.mttr // 0')
|
|
206
|
+
|
|
207
|
+
# Trends
|
|
208
|
+
local trend_df trend_ct trend_cfr trend_mttr
|
|
209
|
+
trend_df=$(trend_arrow "$curr_deploy_freq" "$prev_deploy_freq" "deploy_frequency")
|
|
210
|
+
trend_ct=$(trend_arrow "$curr_cycle_time" "$prev_cycle_time" "lead_time")
|
|
211
|
+
trend_cfr=$(trend_arrow "$curr_cfr" "$prev_cfr" "cfr")
|
|
212
|
+
trend_mttr=$(trend_arrow "$curr_mttr" "$prev_mttr" "mttr")
|
|
213
|
+
|
|
214
|
+
# Bands
|
|
215
|
+
local band_df band_ct band_cfr band_mttr
|
|
216
|
+
band_df=$(classify_band "deploy_frequency" "$curr_deploy_freq")
|
|
217
|
+
band_ct=$(classify_band "lead_time" "$curr_cycle_time")
|
|
218
|
+
band_cfr=$(classify_band "cfr" "$curr_cfr")
|
|
219
|
+
band_mttr=$(classify_band "mttr" "$curr_mttr")
|
|
220
|
+
|
|
221
|
+
# Color-code bands
|
|
222
|
+
local color_df color_ct color_cfr color_mttr
|
|
223
|
+
case "$band_df" in
|
|
224
|
+
Elite) color_df="$GREEN" ;;
|
|
225
|
+
High) color_df="$CYAN" ;;
|
|
226
|
+
Medium) color_df="$YELLOW" ;;
|
|
227
|
+
Low) color_df="$RED" ;;
|
|
228
|
+
esac
|
|
229
|
+
case "$band_ct" in
|
|
230
|
+
Elite) color_ct="$GREEN" ;;
|
|
231
|
+
High) color_ct="$CYAN" ;;
|
|
232
|
+
Medium) color_ct="$YELLOW" ;;
|
|
233
|
+
Low) color_ct="$RED" ;;
|
|
234
|
+
esac
|
|
235
|
+
case "$band_cfr" in
|
|
236
|
+
Elite) color_cfr="$GREEN" ;;
|
|
237
|
+
High) color_cfr="$CYAN" ;;
|
|
238
|
+
Medium) color_cfr="$YELLOW" ;;
|
|
239
|
+
Low) color_cfr="$RED" ;;
|
|
240
|
+
esac
|
|
241
|
+
case "$band_mttr" in
|
|
242
|
+
Elite) color_mttr="$GREEN" ;;
|
|
243
|
+
High) color_mttr="$CYAN" ;;
|
|
244
|
+
Medium) color_mttr="$YELLOW" ;;
|
|
245
|
+
Low) color_mttr="$RED" ;;
|
|
246
|
+
esac
|
|
247
|
+
|
|
248
|
+
# Display 4 core metrics
|
|
249
|
+
echo -e "${BOLD}CORE DORA METRICS${RESET} ${DIM}(Last 7 days vs previous 7 days)${RESET}"
|
|
250
|
+
echo ""
|
|
251
|
+
|
|
252
|
+
printf " ${BOLD}Deploy Frequency${RESET} %6.2f /week ${color_df}${BOLD}%s${RESET}%-8s ${CYAN}%s${RESET} (Band: ${color_df}${band_df}${RESET})\n" \
|
|
253
|
+
"$curr_deploy_freq" "$trend_df" "" "[$(echo "scale=1; ($curr_deploy_freq - $prev_deploy_freq)" | bc)%]"
|
|
254
|
+
|
|
255
|
+
printf " ${BOLD}Lead Time${RESET} %6.2f hours ${color_ct}${BOLD}%s${RESET}%-8s ${CYAN}%s${RESET} (Band: ${color_ct}${band_ct}${RESET})\n" \
|
|
256
|
+
"$curr_cycle_time" "$trend_ct" "" "[$(echo "scale=1; ($curr_cycle_time - $prev_cycle_time)" | bc)h]"
|
|
257
|
+
|
|
258
|
+
printf " ${BOLD}Change Failure Rate${RESET} %6.1f %% ${color_cfr}${BOLD}%s${RESET}%-8s ${CYAN}%s${RESET} (Band: ${color_cfr}${band_cfr}${RESET})\n" \
|
|
259
|
+
"$curr_cfr" "$trend_cfr" "" "[$(echo "scale=1; ($curr_cfr - $prev_cfr)" | bc)pp]"
|
|
260
|
+
|
|
261
|
+
local mttr_hours mttr_str
|
|
262
|
+
mttr_hours=$(echo "scale=1; $curr_mttr" | bc)
|
|
263
|
+
if (( $(echo "$curr_mttr >= 24" | bc -l) )); then
|
|
264
|
+
mttr_str=$(echo "scale=1; $curr_mttr / 24" | bc)
|
|
265
|
+
mttr_str="${mttr_str} days"
|
|
266
|
+
else
|
|
267
|
+
mttr_str="${mttr_hours} hours"
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
printf " ${BOLD}MTTR${RESET} %6s ${color_mttr}${BOLD}%s${RESET}%-8s ${CYAN}%s${RESET} (Band: ${color_mttr}${band_mttr}${RESET})\n" \
|
|
271
|
+
"$mttr_str" "$trend_mttr" "" "[prev: $(echo "scale=1; $prev_mttr" | bc)h]"
|
|
272
|
+
|
|
273
|
+
echo ""
|
|
274
|
+
success "Computed from $(printf '%d' "$curr_total") pipeline runs in the last 7 days"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# ─── DX Metrics Display ────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
show_dx_metrics() {
|
|
280
|
+
info "Developer Experience (DX) Metrics"
|
|
281
|
+
echo ""
|
|
282
|
+
|
|
283
|
+
local events_file="${HOME}/.shipwright/events.jsonl"
|
|
284
|
+
if [[ ! -f "$events_file" ]]; then
|
|
285
|
+
warn "No events found. Run pipelines to generate metrics."
|
|
286
|
+
return 0
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
if ! command -v jq &>/dev/null; then
|
|
290
|
+
error "jq is required"
|
|
291
|
+
exit 1
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
# Calculate DX metrics
|
|
295
|
+
local dx_metrics
|
|
296
|
+
dx_metrics=$(jq -s '
|
|
297
|
+
[.[] | select(.type == "pipeline.completed")] as $completed |
|
|
298
|
+
[.[] | select(.type == "build.iteration")] as $iterations |
|
|
299
|
+
[$completed[] | select(.result == "success")] as $successes |
|
|
300
|
+
($successes | length) as $success_count |
|
|
301
|
+
($completed | length) as $total_runs |
|
|
302
|
+
|
|
303
|
+
# First-time pass rate
|
|
304
|
+
(if $total_runs > 0 then
|
|
305
|
+
($completed | group_by(.issue_id) |
|
|
306
|
+
map(if length == 1 then 1 else 0 end) | add) / $total_runs * 100
|
|
307
|
+
else 0 end) as $ftp_rate |
|
|
308
|
+
|
|
309
|
+
# Avg iterations to pass per issue
|
|
310
|
+
($iterations | map(.iteration_num) |
|
|
311
|
+
if length > 0 then (add / length) else 1 end) as $avg_iterations |
|
|
312
|
+
|
|
313
|
+
# Cost per issue
|
|
314
|
+
([$completed[] | .cost_usd] | if length > 0 then add else 0 end) as $total_cost |
|
|
315
|
+
(if $total_runs > 0 then ($total_cost / $total_runs) else 0 end) as $cost_per_issue |
|
|
316
|
+
|
|
317
|
+
{
|
|
318
|
+
ftp_rate: ($ftp_rate * 10 | floor / 10),
|
|
319
|
+
avg_iterations: ($avg_iterations * 100 | floor / 100),
|
|
320
|
+
total_runs: $total_runs,
|
|
321
|
+
cost_per_issue: ($cost_per_issue * 100 | floor / 100)
|
|
322
|
+
}
|
|
323
|
+
' "$events_file" 2>/dev/null || echo '{"ftp_rate":0,"avg_iterations":0,"total_runs":0,"cost_per_issue":0}')
|
|
324
|
+
|
|
325
|
+
local ftp_rate avg_iterations total_runs cost_per_issue
|
|
326
|
+
ftp_rate=$(echo "$dx_metrics" | jq -r '.ftp_rate // 0')
|
|
327
|
+
avg_iterations=$(echo "$dx_metrics" | jq -r '.avg_iterations // 0')
|
|
328
|
+
total_runs=$(echo "$dx_metrics" | jq -r '.total_runs // 0')
|
|
329
|
+
cost_per_issue=$(echo "$dx_metrics" | jq -r '.cost_per_issue // 0')
|
|
330
|
+
|
|
331
|
+
printf " ${BOLD}First-Time Pass Rate${RESET} %6.1f %%\n" "$ftp_rate"
|
|
332
|
+
printf " ${BOLD}Avg Iterations to Pass${RESET} %6.2f\n" "$avg_iterations"
|
|
333
|
+
printf " ${BOLD}Cost per Issue${RESET} ${GREEN}\$${RESET}%-6.2f\n" "$cost_per_issue"
|
|
334
|
+
printf " ${BOLD}Total Pipeline Runs${RESET} %6d\n" "$total_runs"
|
|
335
|
+
echo ""
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# ─── AI Metrics Display ────────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
show_ai_metrics() {
|
|
341
|
+
info "AI Performance Metrics"
|
|
342
|
+
echo ""
|
|
343
|
+
|
|
344
|
+
local events_file="${HOME}/.shipwright/events.jsonl"
|
|
345
|
+
if [[ ! -f "$events_file" ]]; then
|
|
346
|
+
warn "No events found. Run pipelines to generate metrics."
|
|
347
|
+
return 0
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
if ! command -v jq &>/dev/null; then
|
|
351
|
+
error "jq is required"
|
|
352
|
+
exit 1
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
# Calculate AI metrics
|
|
356
|
+
local ai_metrics
|
|
357
|
+
ai_metrics=$(jq -s '
|
|
358
|
+
[.[] | select(.type == "intelligence.cache_hit")] as $cache_hits |
|
|
359
|
+
[.[] | select(.type == "intelligence.cache_miss")] as $cache_misses |
|
|
360
|
+
[.[] | select(.type == "intelligence.prediction")] as $predictions |
|
|
361
|
+
[.[] | select(.result == "accurate")] as $accurate |
|
|
362
|
+
|
|
363
|
+
($cache_hits | length) as $hits |
|
|
364
|
+
($cache_misses | length) as $misses |
|
|
365
|
+
($hits + $misses) as $total_cache |
|
|
366
|
+
(if $total_cache > 0 then ($hits / $total_cache * 100) else 0 end) as $cache_rate |
|
|
367
|
+
|
|
368
|
+
($accurate | length) as $accurate_count |
|
|
369
|
+
($predictions | length) as $total_predictions |
|
|
370
|
+
(if $total_predictions > 0 then ($accurate_count / $total_predictions * 100) else 0 end) as $pred_accuracy |
|
|
371
|
+
|
|
372
|
+
# Model routing efficiency (cost savings from routing optimization)
|
|
373
|
+
([.[] | select(.type == "cost.savings")] | map(.amount_usd) | add) as $total_savings |
|
|
374
|
+
(if $total_savings then $total_savings else 0 end) as $savings |
|
|
375
|
+
|
|
376
|
+
{
|
|
377
|
+
cache_hit_rate: ($cache_rate * 10 | floor / 10),
|
|
378
|
+
cache_total: $total_cache,
|
|
379
|
+
prediction_accuracy: ($pred_accuracy * 10 | floor / 10),
|
|
380
|
+
total_predictions: $total_predictions,
|
|
381
|
+
model_routing_savings: ($savings * 100 | floor / 100)
|
|
382
|
+
}
|
|
383
|
+
' "$events_file" 2>/dev/null || echo '{"cache_hit_rate":0,"cache_total":0,"prediction_accuracy":0,"total_predictions":0,"model_routing_savings":0}')
|
|
384
|
+
|
|
385
|
+
local cache_rate cache_total pred_accuracy total_pred savings
|
|
386
|
+
cache_rate=$(echo "$ai_metrics" | jq -r '.cache_hit_rate // 0')
|
|
387
|
+
cache_total=$(echo "$ai_metrics" | jq -r '.cache_total // 0')
|
|
388
|
+
pred_accuracy=$(echo "$ai_metrics" | jq -r '.prediction_accuracy // 0')
|
|
389
|
+
total_pred=$(echo "$ai_metrics" | jq -r '.total_predictions // 0')
|
|
390
|
+
savings=$(echo "$ai_metrics" | jq -r '.model_routing_savings // 0')
|
|
391
|
+
|
|
392
|
+
printf " ${BOLD}Cache Hit Rate${RESET} %6.1f %% (${DIM}%d total${RESET})\n" "$cache_rate" "$cache_total"
|
|
393
|
+
printf " ${BOLD}Prediction Accuracy${RESET} %6.1f %% (${DIM}%d predictions${RESET})\n" "$pred_accuracy" "$total_pred"
|
|
394
|
+
printf " ${BOLD}Model Routing Savings${RESET} ${GREEN}\$${RESET}%-6.2f\n" "$savings"
|
|
395
|
+
echo ""
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
# ─── Trends Display ────────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
show_trends() {
|
|
401
|
+
local period="${1:-7}"
|
|
402
|
+
|
|
403
|
+
info "Shipwright Metrics Trends (Last ${period} Days)"
|
|
404
|
+
echo ""
|
|
405
|
+
|
|
406
|
+
local events_file="${HOME}/.shipwright/events.jsonl"
|
|
407
|
+
if [[ ! -f "$events_file" ]]; then
|
|
408
|
+
warn "No events found. Run pipelines to generate metrics."
|
|
409
|
+
return 0
|
|
410
|
+
fi
|
|
411
|
+
|
|
412
|
+
if ! command -v jq &>/dev/null; then
|
|
413
|
+
error "jq is required"
|
|
414
|
+
exit 1
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
# Show day-by-day trends
|
|
418
|
+
local day=0
|
|
419
|
+
printf " ${BOLD}Day${RESET} ${BOLD}Deployments${RESET} ${BOLD}Cycle Time${RESET} ${BOLD}CFR${RESET} ${BOLD}MTTR${RESET}\n"
|
|
420
|
+
printf " ${DIM}─────────────────────────────────────────────${RESET}\n"
|
|
421
|
+
|
|
422
|
+
while [[ $day -lt $period ]]; do
|
|
423
|
+
local metrics
|
|
424
|
+
metrics=$(calculate_dora 1 "$day")
|
|
425
|
+
|
|
426
|
+
local deploys ct cfr mttr
|
|
427
|
+
deploys=$(echo "$metrics" | jq -r '.total // 0')
|
|
428
|
+
ct=$(echo "$metrics" | jq -r '.cycle_time // 0')
|
|
429
|
+
cfr=$(echo "$metrics" | jq -r '.cfr // 0')
|
|
430
|
+
mttr=$(echo "$metrics" | jq -r '.mttr // 0')
|
|
431
|
+
|
|
432
|
+
local date_str
|
|
433
|
+
date_str=$(date -u -v-${day}d +"%a" 2>/dev/null || date -u -d "${day} days ago" +"%a" 2>/dev/null || echo "Day")
|
|
434
|
+
|
|
435
|
+
printf " %-3s %d %.1fh %.1f%% %.1fh\n" \
|
|
436
|
+
"$date_str" "$deploys" "$ct" "$cfr" "$mttr"
|
|
437
|
+
|
|
438
|
+
((day++))
|
|
439
|
+
done
|
|
440
|
+
|
|
441
|
+
echo ""
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
# ─── Comparison Display ────────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
show_comparison() {
|
|
447
|
+
local current_period="${1:-7}"
|
|
448
|
+
local previous_period="${2:-7}"
|
|
449
|
+
|
|
450
|
+
info "Period Comparison Analysis"
|
|
451
|
+
echo ""
|
|
452
|
+
printf " ${BOLD}Current (last %d days) vs Previous (%d days)${RESET}\n" "$current_period" "$previous_period"
|
|
453
|
+
echo ""
|
|
454
|
+
|
|
455
|
+
local curr prev
|
|
456
|
+
curr=$(calculate_dora "$current_period" 0)
|
|
457
|
+
prev=$(calculate_dora "$previous_period" "$current_period")
|
|
458
|
+
|
|
459
|
+
if ! command -v jq &>/dev/null; then
|
|
460
|
+
error "jq is required"
|
|
461
|
+
exit 1
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
local curr_df curr_ct curr_cfr curr_mttr
|
|
465
|
+
local prev_df prev_ct prev_cfr prev_mttr
|
|
466
|
+
|
|
467
|
+
curr_df=$(echo "$curr" | jq -r '.deploy_freq // 0')
|
|
468
|
+
curr_ct=$(echo "$curr" | jq -r '.cycle_time // 0')
|
|
469
|
+
curr_cfr=$(echo "$curr" | jq -r '.cfr // 0')
|
|
470
|
+
curr_mttr=$(echo "$curr" | jq -r '.mttr // 0')
|
|
471
|
+
|
|
472
|
+
prev_df=$(echo "$prev" | jq -r '.deploy_freq // 0')
|
|
473
|
+
prev_ct=$(echo "$prev" | jq -r '.cycle_time // 0')
|
|
474
|
+
prev_cfr=$(echo "$prev" | jq -r '.cfr // 0')
|
|
475
|
+
prev_mttr=$(echo "$prev" | jq -r '.mttr // 0')
|
|
476
|
+
|
|
477
|
+
# Calculate percent changes
|
|
478
|
+
local pct_df pct_ct pct_cfr pct_mttr
|
|
479
|
+
pct_df=$(echo "scale=1; (($curr_df - $prev_df) / $prev_df * 100)" | bc 2>/dev/null || echo "0")
|
|
480
|
+
pct_ct=$(echo "scale=1; (($curr_ct - $prev_ct) / $prev_ct * 100)" | bc 2>/dev/null || echo "0")
|
|
481
|
+
pct_cfr=$(echo "scale=1; (($curr_cfr - $prev_cfr) / $prev_cfr * 100)" | bc 2>/dev/null || echo "0")
|
|
482
|
+
pct_mttr=$(echo "scale=1; (($curr_mttr - $prev_mttr) / $prev_mttr * 100)" | bc 2>/dev/null || echo "0")
|
|
483
|
+
|
|
484
|
+
printf " ${BOLD}Deploy Frequency${RESET} %6.2f → %6.2f /week ${CYAN}(%+.1f%%)${RESET}\n" \
|
|
485
|
+
"$prev_df" "$curr_df" "$pct_df"
|
|
486
|
+
|
|
487
|
+
printf " ${BOLD}Lead Time${RESET} %6.2f → %6.2f hours ${CYAN}(%+.1f%%)${RESET}\n" \
|
|
488
|
+
"$prev_ct" "$curr_ct" "$pct_ct"
|
|
489
|
+
|
|
490
|
+
printf " ${BOLD}Change Failure Rate${RESET} %6.1f%% → %6.1f%% ${CYAN}(%+.1f pp)${RESET}\n" \
|
|
491
|
+
"$prev_cfr" "$curr_cfr" "$pct_cfr"
|
|
492
|
+
|
|
493
|
+
printf " ${BOLD}MTTR${RESET} %6.1f → %6.1f hours ${CYAN}(%+.1f%%)${RESET}\n" \
|
|
494
|
+
"$prev_mttr" "$curr_mttr" "$pct_mttr"
|
|
495
|
+
|
|
496
|
+
echo ""
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# ─── Export to JSON ────────────────────────────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
export_metrics() {
|
|
502
|
+
local current previous
|
|
503
|
+
current=$(calculate_dora 7 0)
|
|
504
|
+
previous=$(calculate_dora 7 7)
|
|
505
|
+
|
|
506
|
+
if ! command -v jq &>/dev/null; then
|
|
507
|
+
error "jq is required for JSON export"
|
|
508
|
+
exit 1
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
# Build comprehensive metrics object
|
|
512
|
+
jq -n \
|
|
513
|
+
--argjson current "$current" \
|
|
514
|
+
--argjson previous "$previous" \
|
|
515
|
+
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
516
|
+
'{
|
|
517
|
+
timestamp: $timestamp,
|
|
518
|
+
current_period: ($current | {
|
|
519
|
+
deploy_freq: .deploy_freq,
|
|
520
|
+
cycle_time: .cycle_time,
|
|
521
|
+
cfr: .cfr,
|
|
522
|
+
mttr: .mttr,
|
|
523
|
+
total_runs: .total
|
|
524
|
+
}),
|
|
525
|
+
previous_period: ($previous | {
|
|
526
|
+
deploy_freq: .deploy_freq,
|
|
527
|
+
cycle_time: .cycle_time,
|
|
528
|
+
cfr: .cfr,
|
|
529
|
+
mttr: .mttr,
|
|
530
|
+
total_runs: .total
|
|
531
|
+
})
|
|
532
|
+
}'
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
# ─── Help Display ────────────────────────────────────────────────────────────
|
|
536
|
+
|
|
537
|
+
show_help() {
|
|
538
|
+
cat << EOF
|
|
539
|
+
${BOLD}shipwright dora${RESET} — DORA Metrics Dashboard & Engineering Intelligence
|
|
540
|
+
|
|
541
|
+
${BOLD}USAGE${RESET}
|
|
542
|
+
shipwright dora <subcommand> [options]
|
|
543
|
+
|
|
544
|
+
${BOLD}SUBCOMMANDS${RESET}
|
|
545
|
+
show Display DORA dashboard (4 core metrics)
|
|
546
|
+
dx Developer Experience metrics (FTP, iterations, cost)
|
|
547
|
+
ai AI performance metrics (cache, predictions, routing savings)
|
|
548
|
+
trends [days] Trend analysis over time (default: 7 days)
|
|
549
|
+
compare [c] [p] Compare current vs previous period (default: 7 vs 7 days)
|
|
550
|
+
export Export all metrics as JSON
|
|
551
|
+
help Show this help message
|
|
552
|
+
|
|
553
|
+
${BOLD}EXAMPLES${RESET}
|
|
554
|
+
${DIM}shipwright dora show${RESET} # Display DORA dashboard
|
|
555
|
+
${DIM}shipwright dora trends 30${RESET} # Show 30-day trends
|
|
556
|
+
${DIM}shipwright dora compare 7 14${RESET} # Compare last 7 days vs previous 14
|
|
557
|
+
${DIM}shipwright dora export | jq .${RESET} # Export metrics as JSON
|
|
558
|
+
|
|
559
|
+
${BOLD}DORA BANDS${RESET} (per DORA standards)
|
|
560
|
+
${GREEN}Elite${RESET} — Highest performance tier
|
|
561
|
+
${CYAN}High${RESET} — Above average
|
|
562
|
+
${YELLOW}Medium${RESET} — Average performance
|
|
563
|
+
${RED}Low${RESET} — Below average
|
|
564
|
+
|
|
565
|
+
${BOLD}METRICS REFERENCE${RESET}
|
|
566
|
+
Deploy Frequency — Deployments per week (higher is better)
|
|
567
|
+
Lead Time — Hours from commit to production (lower is better)
|
|
568
|
+
Change Failure Rate — % of deployments requiring hotfix (lower is better)
|
|
569
|
+
MTTR — Hours to restore after failure (lower is better)
|
|
570
|
+
|
|
571
|
+
EOF
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
# ─── Main Entry Point ────────────────────────────────────────────────────────
|
|
575
|
+
|
|
576
|
+
main() {
|
|
577
|
+
local cmd="${1:-show}"
|
|
578
|
+
|
|
579
|
+
case "$cmd" in
|
|
580
|
+
show)
|
|
581
|
+
show_dora_dashboard
|
|
582
|
+
;;
|
|
583
|
+
dx)
|
|
584
|
+
show_dx_metrics
|
|
585
|
+
;;
|
|
586
|
+
ai)
|
|
587
|
+
show_ai_metrics
|
|
588
|
+
;;
|
|
589
|
+
trends)
|
|
590
|
+
local days="${2:-7}"
|
|
591
|
+
show_trends "$days"
|
|
592
|
+
;;
|
|
593
|
+
compare)
|
|
594
|
+
local curr="${2:-7}"
|
|
595
|
+
local prev="${3:-7}"
|
|
596
|
+
show_comparison "$curr" "$prev"
|
|
597
|
+
;;
|
|
598
|
+
export)
|
|
599
|
+
export_metrics
|
|
600
|
+
;;
|
|
601
|
+
help|--help|-h)
|
|
602
|
+
show_help
|
|
603
|
+
;;
|
|
604
|
+
*)
|
|
605
|
+
error "Unknown command: $cmd"
|
|
606
|
+
echo ""
|
|
607
|
+
show_help
|
|
608
|
+
exit 1
|
|
609
|
+
;;
|
|
610
|
+
esac
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
614
|
+
main "$@"
|
|
615
|
+
fi
|