shipwright-cli 1.10.0 → 2.1.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 +221 -55
- package/completions/_shipwright +264 -32
- package/completions/shipwright.bash +118 -26
- package/completions/shipwright.fish +80 -2
- package/dashboard/server.ts +208 -0
- 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/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
- package/docs/tmux-research/TMUX-AUDIT.md +925 -0
- package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
- package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
- package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
- package/package.json +4 -2
- package/scripts/lib/helpers.sh +7 -0
- package/scripts/sw +323 -2
- 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 +754 -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 +698 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +44 -3
- package/scripts/sw-daemon.sh +568 -138
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +1380 -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 +107 -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 +479 -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 +4 -4
- 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 +656 -0
- package/scripts/sw-init.sh +237 -24
- 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 +267 -21
- package/scripts/sw-memory.sh +18 -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 +764 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +947 -35
- package/scripts/sw-pm.sh +758 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +8 -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 +2248 -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 +263 -127
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +44 -2
- package/scripts/sw-strategic.sh +806 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +620 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +4 -4
- package/scripts/sw-testgen.sh +566 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux-role-color.sh +58 -0
- package/scripts/sw-tmux-status.sh +128 -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 +627 -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
- package/templates/pipelines/autonomous.json +2 -2
- package/tmux/shipwright-overlay.conf +35 -17
- package/tmux/tmux.conf +23 -21
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ sw-tmux-status.sh — Status bar widgets for tmux ║
|
|
4
|
+
# ║ ║
|
|
5
|
+
# ║ Called by tmux via #() in status-right. Must be FAST (<100ms). ║
|
|
6
|
+
# ║ Reads pipeline state from .claude/pipeline-state.md and heartbeats ║
|
|
7
|
+
# ║ from ~/.shipwright/heartbeats/. Outputs styled tmux format strings. ║
|
|
8
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
# ─── Stage colors (match Shipwright brand palette) ────────────────────────
|
|
12
|
+
# Each pipeline stage gets a distinct color for instant visual recognition
|
|
13
|
+
stage_color() {
|
|
14
|
+
case "${1:-}" in
|
|
15
|
+
intake) echo "#71717a" ;; # muted — gathering
|
|
16
|
+
plan) echo "#7c3aed" ;; # purple — thinking
|
|
17
|
+
design) echo "#7c3aed" ;; # purple — thinking
|
|
18
|
+
build) echo "#0066ff" ;; # blue — working
|
|
19
|
+
test) echo "#facc15" ;; # yellow — validating
|
|
20
|
+
review) echo "#f97316" ;; # orange — scrutinizing
|
|
21
|
+
compound_quality) echo "#f97316" ;; # orange — scrutinizing
|
|
22
|
+
pr) echo "#00d4ff" ;; # cyan — shipping
|
|
23
|
+
merge) echo "#00d4ff" ;; # cyan — shipping
|
|
24
|
+
deploy) echo "#4ade80" ;; # green — deploying
|
|
25
|
+
validate) echo "#4ade80" ;; # green — verifying
|
|
26
|
+
monitor) echo "#4ade80" ;; # green — watching
|
|
27
|
+
*) echo "#71717a" ;; # muted fallback
|
|
28
|
+
esac
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# ─── Stage icons ──────────────────────────────────────────────────────────
|
|
32
|
+
stage_icon() {
|
|
33
|
+
case "${1:-}" in
|
|
34
|
+
intake) echo "◇" ;;
|
|
35
|
+
plan) echo "◆" ;;
|
|
36
|
+
design) echo "△" ;;
|
|
37
|
+
build) echo "⚙" ;;
|
|
38
|
+
test) echo "⚡" ;;
|
|
39
|
+
review) echo "◎" ;;
|
|
40
|
+
compound_quality) echo "◎" ;;
|
|
41
|
+
pr) echo "↑" ;;
|
|
42
|
+
merge) echo "⊕" ;;
|
|
43
|
+
deploy) echo "▲" ;;
|
|
44
|
+
validate) echo "✦" ;;
|
|
45
|
+
monitor) echo "◉" ;;
|
|
46
|
+
*) echo "·" ;;
|
|
47
|
+
esac
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# ─── Pipeline stage widget ────────────────────────────────────────────────
|
|
51
|
+
# Reads current pipeline stage from state file, outputs tmux format string
|
|
52
|
+
pipeline_widget() {
|
|
53
|
+
local state_file=".claude/pipeline-state.md"
|
|
54
|
+
|
|
55
|
+
# Try current directory, then walk up to find repo root
|
|
56
|
+
if [[ ! -f "$state_file" ]]; then
|
|
57
|
+
local dir
|
|
58
|
+
dir="$(pwd)"
|
|
59
|
+
while [[ "$dir" != "/" ]]; do
|
|
60
|
+
if [[ -f "$dir/$state_file" ]]; then
|
|
61
|
+
state_file="$dir/$state_file"
|
|
62
|
+
break
|
|
63
|
+
fi
|
|
64
|
+
dir="$(dirname "$dir")"
|
|
65
|
+
done
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
[[ -f "$state_file" ]] || return 0
|
|
69
|
+
|
|
70
|
+
# Extract current stage — look for "Stage:" or "## Stage:" pattern
|
|
71
|
+
local stage=""
|
|
72
|
+
stage="$(grep -iE '^\*?\*?(current )?stage:?\*?\*?' "$state_file" 2>/dev/null | head -1 | sed 's/.*: *//' | tr -d '*' | tr '[:upper:]' '[:lower:]' | tr -d ' ')" || true
|
|
73
|
+
|
|
74
|
+
[[ -n "$stage" ]] || return 0
|
|
75
|
+
|
|
76
|
+
local color icon
|
|
77
|
+
color="$(stage_color "$stage")"
|
|
78
|
+
icon="$(stage_icon "$stage")"
|
|
79
|
+
local label
|
|
80
|
+
label="$(echo "$stage" | tr '[:lower:]' '[:upper:]')"
|
|
81
|
+
|
|
82
|
+
# Output: colored badge with icon
|
|
83
|
+
echo "#[fg=#1e1e32,bg=${color},bold] ${icon} ${label} #[fg=${color},bg=#1a1a2e]"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ─── Agent count widget ──────────────────────────────────────────────────
|
|
87
|
+
# Shows number of active agents from heartbeat files
|
|
88
|
+
agent_widget() {
|
|
89
|
+
local hb_dir="${HOME}/.shipwright/heartbeats"
|
|
90
|
+
[[ -d "$hb_dir" ]] || return 0
|
|
91
|
+
|
|
92
|
+
local now count=0
|
|
93
|
+
now="$(date +%s)"
|
|
94
|
+
|
|
95
|
+
for hb in "$hb_dir"/*.json; do
|
|
96
|
+
[[ -f "$hb" ]] || continue
|
|
97
|
+
# Heartbeat is alive if updated within last 60 seconds
|
|
98
|
+
local mtime
|
|
99
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
100
|
+
mtime="$(stat -f %m "$hb" 2>/dev/null || echo 0)"
|
|
101
|
+
else
|
|
102
|
+
mtime="$(stat -c %Y "$hb" 2>/dev/null || echo 0)"
|
|
103
|
+
fi
|
|
104
|
+
if (( now - mtime < 60 )); then
|
|
105
|
+
count=$((count + 1))
|
|
106
|
+
fi
|
|
107
|
+
done
|
|
108
|
+
|
|
109
|
+
if [[ $count -gt 0 ]]; then
|
|
110
|
+
echo "#[fg=#1e1e32,bg=#7c3aed,bold] λ${count} #[fg=#7c3aed,bg=#1a1a2e]"
|
|
111
|
+
fi
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# ─── Dispatch ─────────────────────────────────────────────────────────────
|
|
115
|
+
case "${1:-pipeline}" in
|
|
116
|
+
pipeline) pipeline_widget ;;
|
|
117
|
+
agents) agent_widget ;;
|
|
118
|
+
all)
|
|
119
|
+
# Combine both widgets
|
|
120
|
+
local p a
|
|
121
|
+
p="$(pipeline_widget)"
|
|
122
|
+
a="$(agent_widget)"
|
|
123
|
+
echo "${a}${p}"
|
|
124
|
+
;;
|
|
125
|
+
*)
|
|
126
|
+
echo ""
|
|
127
|
+
;;
|
|
128
|
+
esac
|
package/scripts/sw-tmux.sh
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# ║ shipwright tmux fix — Auto-fix common issues ║
|
|
12
12
|
# ║ shipwright tmux reload — Reload tmux config ║
|
|
13
13
|
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
14
|
-
VERSION="1.
|
|
14
|
+
VERSION="2.1.0"
|
|
15
15
|
set -euo pipefail
|
|
16
16
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
17
17
|
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright trace — E2E Traceability (Issue → Commit → PR → Deploy) ║
|
|
4
|
+
# ║ Query and link the full chain from GitHub issue to production ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="2.1.0"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
14
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
15
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
16
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
17
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
18
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
19
|
+
RED='\033[38;2;248;113;113m' # error
|
|
20
|
+
DIM='\033[2m'
|
|
21
|
+
BOLD='\033[1m'
|
|
22
|
+
RESET='\033[0m'
|
|
23
|
+
|
|
24
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
25
|
+
# shellcheck source=lib/compat.sh
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
|
|
28
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
29
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
30
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
31
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
32
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
33
|
+
|
|
34
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
35
|
+
|
|
36
|
+
# ─── Data Paths ─────────────────────────────────────────────────────────────
|
|
37
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
38
|
+
SHIPWRIGHT_DIR="${REPO_DIR}/.claude/pipeline-artifacts"
|
|
39
|
+
|
|
40
|
+
# ─── Helper: Extract GitHub repo owner/name ──────────────────────────────────
|
|
41
|
+
get_gh_repo() {
|
|
42
|
+
gh repo view --json nameWithOwner -q 2>/dev/null || echo ""
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# ─── Helper: Extract issue number from branch name ──────────────────────────
|
|
46
|
+
issue_from_branch() {
|
|
47
|
+
local branch="$1"
|
|
48
|
+
# Handle feat/...-N, fix/...-N, issue-N patterns
|
|
49
|
+
if [[ "$branch" =~ -([0-9]+)$ ]]; then
|
|
50
|
+
echo "${BASH_REMATCH[1]}"
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ─── trace_show: Full chain for a single issue ──────────────────────────────
|
|
55
|
+
trace_show() {
|
|
56
|
+
local issue="$1"
|
|
57
|
+
|
|
58
|
+
if [[ ! "$issue" =~ ^[0-9]+$ ]]; then
|
|
59
|
+
error "Issue must be a number"
|
|
60
|
+
return 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
info "Tracing issue #${issue}..."
|
|
64
|
+
echo ""
|
|
65
|
+
|
|
66
|
+
# Get issue details from GitHub
|
|
67
|
+
local issue_data
|
|
68
|
+
if ! issue_data=$(gh issue view "$issue" --json "title,state,assignees,labels,url,createdAt,closedAt" 2>/dev/null); then
|
|
69
|
+
error "Could not fetch issue #${issue}. Check permissions or issue number."
|
|
70
|
+
return 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
local title state url
|
|
74
|
+
title=$(echo "$issue_data" | jq -r '.title')
|
|
75
|
+
state=$(echo "$issue_data" | jq -r '.state')
|
|
76
|
+
url=$(echo "$issue_data" | jq -r '.url')
|
|
77
|
+
|
|
78
|
+
# ─── Issue Section ─────────────────────────────────────────────────────
|
|
79
|
+
echo -e "${BOLD}ISSUE${RESET}"
|
|
80
|
+
echo -e " ${CYAN}#${issue}${RESET} ${BOLD}${title}${RESET}"
|
|
81
|
+
echo -e " State: ${CYAN}${state}${RESET} • URL: ${BLUE}${url}${RESET}"
|
|
82
|
+
echo ""
|
|
83
|
+
|
|
84
|
+
# ─── Pipeline Section ──────────────────────────────────────────────────
|
|
85
|
+
echo -e "${BOLD}PIPELINE${RESET}"
|
|
86
|
+
|
|
87
|
+
# Find pipeline events for this issue
|
|
88
|
+
if [[ ! -f "$EVENTS_FILE" ]]; then
|
|
89
|
+
warn "No events log found at $EVENTS_FILE"
|
|
90
|
+
else
|
|
91
|
+
local pipeline_started
|
|
92
|
+
pipeline_started=$(grep "\"issue\":${issue}" "$EVENTS_FILE" | head -1)
|
|
93
|
+
|
|
94
|
+
if [[ -z "$pipeline_started" ]]; then
|
|
95
|
+
echo -e " ${DIM}No pipeline run found for this issue${RESET}"
|
|
96
|
+
else
|
|
97
|
+
local ts job_id stage
|
|
98
|
+
ts=$(echo "$pipeline_started" | jq -r '.ts // "unknown"')
|
|
99
|
+
job_id=$(echo "$pipeline_started" | jq -r '.job_id // "unknown"')
|
|
100
|
+
stage=$(echo "$pipeline_started" | jq -r '.stage // "intake"')
|
|
101
|
+
|
|
102
|
+
echo -e " Job ID: ${CYAN}${job_id}${RESET}"
|
|
103
|
+
echo -e " Started: ${DIM}${ts}${RESET}"
|
|
104
|
+
|
|
105
|
+
# Find max stage reached
|
|
106
|
+
local max_stage
|
|
107
|
+
max_stage=$(grep "\"job_id\":\"${job_id}\"" "$EVENTS_FILE" \
|
|
108
|
+
| jq -r '.stage // ""' 2>/dev/null \
|
|
109
|
+
| grep -v '^$' | tail -1)
|
|
110
|
+
|
|
111
|
+
if [[ -n "$max_stage" ]]; then
|
|
112
|
+
echo -e " Last Stage: ${GREEN}${max_stage}${RESET}"
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
echo ""
|
|
117
|
+
|
|
118
|
+
# ─── Feature Branch Section ────────────────────────────────────────────
|
|
119
|
+
echo -e "${BOLD}FEATURE BRANCH${RESET}"
|
|
120
|
+
|
|
121
|
+
local feature_branch
|
|
122
|
+
feature_branch="feat/issue-${issue}"
|
|
123
|
+
|
|
124
|
+
# Check if worktree exists
|
|
125
|
+
local worktree_path="${REPO_DIR}/.worktrees/issue-${issue}"
|
|
126
|
+
if [[ -d "$worktree_path" ]]; then
|
|
127
|
+
echo -e " Worktree: ${GREEN}${worktree_path}${RESET}"
|
|
128
|
+
|
|
129
|
+
# Get commits from worktree
|
|
130
|
+
cd "$worktree_path" 2>/dev/null || true
|
|
131
|
+
local commit_count
|
|
132
|
+
commit_count=$(git rev-list --count main..HEAD 2>/dev/null || echo "0")
|
|
133
|
+
echo -e " Commits: ${CYAN}${commit_count}${RESET}"
|
|
134
|
+
|
|
135
|
+
# Show recent commits
|
|
136
|
+
if [[ "$commit_count" -gt 0 ]]; then
|
|
137
|
+
echo -e " ${DIM}Recent commits:${RESET}"
|
|
138
|
+
git log --oneline -5 main..HEAD 2>/dev/null | while read -r sha msg; do
|
|
139
|
+
echo -e " ${CYAN}${sha:0:7}${RESET} ${DIM}${msg}${RESET}"
|
|
140
|
+
done
|
|
141
|
+
fi
|
|
142
|
+
cd - >/dev/null 2>&1 || true
|
|
143
|
+
else
|
|
144
|
+
# Try to find any branch matching the issue
|
|
145
|
+
local branches
|
|
146
|
+
branches=$(git branch -r --list "*issue-${issue}*" 2>/dev/null || echo "")
|
|
147
|
+
if [[ -z "$branches" ]]; then
|
|
148
|
+
echo -e " ${DIM}No branch found${RESET}"
|
|
149
|
+
else
|
|
150
|
+
echo "$branches" | while read -r branch; do
|
|
151
|
+
branch=$(echo "$branch" | xargs)
|
|
152
|
+
echo -e " ${CYAN}${branch}${RESET}"
|
|
153
|
+
done
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
echo ""
|
|
157
|
+
|
|
158
|
+
# ─── Pull Request Section ──────────────────────────────────────────────
|
|
159
|
+
echo -e "${BOLD}PULL REQUEST${RESET}"
|
|
160
|
+
|
|
161
|
+
# Search for PR linked to this issue
|
|
162
|
+
local pr_data
|
|
163
|
+
pr_data=$(gh pr list --state all --search "issue:${issue}" --json "number,title,state,mergedAt,url" -L 1 2>/dev/null || echo "")
|
|
164
|
+
|
|
165
|
+
if [[ -z "$pr_data" ]] || [[ "$pr_data" == "[]" ]]; then
|
|
166
|
+
# Fallback: look for PR with matching branch
|
|
167
|
+
pr_data=$(gh pr list --state all --search "head:feat/issue-${issue}" --json "number,title,state,mergedAt,url" -L 1 2>/dev/null || echo "")
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
if [[ -z "$pr_data" ]] || [[ "$pr_data" == "[]" ]]; then
|
|
171
|
+
echo -e " ${DIM}No PR found${RESET}"
|
|
172
|
+
else
|
|
173
|
+
local pr_num pr_title pr_state merged_at pr_url
|
|
174
|
+
pr_num=$(echo "$pr_data" | jq -r '.[0].number // "unknown"')
|
|
175
|
+
pr_title=$(echo "$pr_data" | jq -r '.[0].title // "unknown"')
|
|
176
|
+
pr_state=$(echo "$pr_data" | jq -r '.[0].state // "unknown"')
|
|
177
|
+
merged_at=$(echo "$pr_data" | jq -r '.[0].mergedAt // ""')
|
|
178
|
+
pr_url=$(echo "$pr_data" | jq -r '.[0].url // ""')
|
|
179
|
+
|
|
180
|
+
echo -e " ${CYAN}#${pr_num}${RESET} ${pr_title}"
|
|
181
|
+
echo -e " State: ${CYAN}${pr_state}${RESET} • URL: ${BLUE}${pr_url}${RESET}"
|
|
182
|
+
|
|
183
|
+
if [[ -n "$merged_at" && "$merged_at" != "null" ]]; then
|
|
184
|
+
echo -e " Merged: ${GREEN}${merged_at}${RESET}"
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
echo ""
|
|
188
|
+
|
|
189
|
+
# ─── Deployment Section ────────────────────────────────────────────────
|
|
190
|
+
echo -e "${BOLD}DEPLOYMENT${RESET}"
|
|
191
|
+
|
|
192
|
+
# Check if deployment tracking exists
|
|
193
|
+
if [[ -f "${SHIPWRIGHT_DIR}/deployment.json" ]]; then
|
|
194
|
+
local deploy_env deploy_status
|
|
195
|
+
deploy_env=$(jq -r '.environment // "unknown"' "${SHIPWRIGHT_DIR}/deployment.json" 2>/dev/null || echo "")
|
|
196
|
+
deploy_status=$(jq -r '.status // "unknown"' "${SHIPWRIGHT_DIR}/deployment.json" 2>/dev/null || echo "")
|
|
197
|
+
|
|
198
|
+
if [[ -n "$deploy_env" ]] && [[ "$deploy_env" != "null" ]]; then
|
|
199
|
+
echo -e " Environment: ${CYAN}${deploy_env}${RESET}"
|
|
200
|
+
echo -e " Status: ${GREEN}${deploy_status}${RESET}"
|
|
201
|
+
else
|
|
202
|
+
echo -e " ${DIM}No deployment tracked${RESET}"
|
|
203
|
+
fi
|
|
204
|
+
else
|
|
205
|
+
echo -e " ${DIM}No deployment tracked${RESET}"
|
|
206
|
+
fi
|
|
207
|
+
echo ""
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# ─── trace_list: Recent pipeline activity ──────────────────────────────────
|
|
211
|
+
trace_list() {
|
|
212
|
+
local limit="${1:-10}"
|
|
213
|
+
|
|
214
|
+
if [[ ! -f "$EVENTS_FILE" ]]; then
|
|
215
|
+
warn "No events log found"
|
|
216
|
+
return 1
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
info "Recent pipeline runs (last ${limit})..."
|
|
220
|
+
echo ""
|
|
221
|
+
|
|
222
|
+
# Extract unique job_ids with their issues
|
|
223
|
+
local jobs
|
|
224
|
+
jobs=$(grep '"job_id"' "$EVENTS_FILE" | jq -r '.job_id' | sort -u | tail -n "$limit")
|
|
225
|
+
|
|
226
|
+
if [[ -z "$jobs" ]]; then
|
|
227
|
+
echo " ${DIM}No pipeline runs found${RESET}"
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
local count=0
|
|
232
|
+
echo -e "${BOLD}JOB_ID ISSUE STAGE STATUS DURATION${RESET}"
|
|
233
|
+
echo -e "${DIM}──────────────────────────────────────────────────────────────────────────────${RESET}"
|
|
234
|
+
|
|
235
|
+
while read -r job_id; do
|
|
236
|
+
((count++ <= limit)) || break
|
|
237
|
+
|
|
238
|
+
# Get first and last event for this job
|
|
239
|
+
local first_event last_event issue stage status duration_s
|
|
240
|
+
first_event=$(grep "\"job_id\":\"${job_id}\"" "$EVENTS_FILE" | head -1)
|
|
241
|
+
last_event=$(grep "\"job_id\":\"${job_id}\"" "$EVENTS_FILE" | tail -1)
|
|
242
|
+
|
|
243
|
+
issue=$(echo "$first_event" | jq -r '.issue // ""')
|
|
244
|
+
stage=$(echo "$last_event" | jq -r '.stage // "?"')
|
|
245
|
+
status=$(echo "$last_event" | jq -r '.status // "?"')
|
|
246
|
+
duration_s=$(echo "$last_event" | jq -r '.duration_secs // 0')
|
|
247
|
+
|
|
248
|
+
# Format duration
|
|
249
|
+
local duration_fmt
|
|
250
|
+
if [[ "$duration_s" -gt 3600 ]]; then
|
|
251
|
+
duration_fmt="$(( duration_s / 3600 ))h $(( (duration_s % 3600) / 60 ))m"
|
|
252
|
+
elif [[ "$duration_s" -gt 60 ]]; then
|
|
253
|
+
duration_fmt="$(( duration_s / 60 ))m"
|
|
254
|
+
else
|
|
255
|
+
duration_fmt="${duration_s}s"
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
# Color status
|
|
259
|
+
local status_color
|
|
260
|
+
case "$status" in
|
|
261
|
+
completed|success) status_color="${GREEN}${status}${RESET}" ;;
|
|
262
|
+
failed|error) status_color="${RED}${status}${RESET}" ;;
|
|
263
|
+
running|in_progress) status_color="${CYAN}${status}${RESET}" ;;
|
|
264
|
+
*) status_color="$status" ;;
|
|
265
|
+
esac
|
|
266
|
+
|
|
267
|
+
printf "%-32s #%-4s %-15s %-12s %s\n" \
|
|
268
|
+
"${job_id:0:30}" \
|
|
269
|
+
"${issue:-?}" \
|
|
270
|
+
"$stage" \
|
|
271
|
+
"$status_color" \
|
|
272
|
+
"$duration_fmt"
|
|
273
|
+
done <<< "$jobs"
|
|
274
|
+
|
|
275
|
+
echo ""
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# ─── trace_search: Find issue/pipeline by commit ─────────────────────────────
|
|
279
|
+
trace_search() {
|
|
280
|
+
local commit_sha="$1"
|
|
281
|
+
|
|
282
|
+
if [[ ! "$commit_sha" =~ ^[a-f0-9]{6,40}$ ]]; then
|
|
283
|
+
error "Commit SHA must be 6-40 hex characters"
|
|
284
|
+
return 1
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
info "Searching for commit ${CYAN}${commit_sha:0:8}${RESET}..."
|
|
288
|
+
echo ""
|
|
289
|
+
|
|
290
|
+
# Find which branch contains this commit
|
|
291
|
+
local branch
|
|
292
|
+
branch=$(git branch -r --contains "$commit_sha" 2>/dev/null | head -1 | xargs || echo "")
|
|
293
|
+
|
|
294
|
+
if [[ -z "$branch" ]]; then
|
|
295
|
+
warn "Commit not found in any tracked branch"
|
|
296
|
+
return 1
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
# Try to extract issue number from branch name
|
|
300
|
+
local issue
|
|
301
|
+
issue=$(issue_from_branch "$branch")
|
|
302
|
+
|
|
303
|
+
echo -e "${BOLD}COMMIT${RESET}"
|
|
304
|
+
echo -e " SHA: ${CYAN}${commit_sha:0:8}${RESET}"
|
|
305
|
+
echo -e " Branch: ${CYAN}${branch}${RESET}"
|
|
306
|
+
echo ""
|
|
307
|
+
|
|
308
|
+
if [[ -n "$issue" ]]; then
|
|
309
|
+
echo -e "${BOLD}LINKED ISSUE${RESET}"
|
|
310
|
+
echo -e " Issue: ${CYAN}#${issue}${RESET}"
|
|
311
|
+
echo ""
|
|
312
|
+
|
|
313
|
+
# Show full trace for this issue
|
|
314
|
+
trace_show "$issue" || true
|
|
315
|
+
else
|
|
316
|
+
warn "Could not extract issue number from branch name"
|
|
317
|
+
echo " Branch: ${CYAN}${branch}${RESET}"
|
|
318
|
+
echo ""
|
|
319
|
+
fi
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# ─── trace_export: Generate markdown report ──────────────────────────────────
|
|
323
|
+
trace_export() {
|
|
324
|
+
local issue="$1"
|
|
325
|
+
local output_file="${2:-trace-issue-${issue}.md}"
|
|
326
|
+
|
|
327
|
+
if [[ ! "$issue" =~ ^[0-9]+$ ]]; then
|
|
328
|
+
error "Issue must be a number"
|
|
329
|
+
return 1
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
info "Exporting trace for issue #${issue} to ${CYAN}${output_file}${RESET}..."
|
|
333
|
+
|
|
334
|
+
# Get issue details
|
|
335
|
+
local issue_data title state url created_at
|
|
336
|
+
if ! issue_data=$(gh issue view "$issue" --json "title,state,url,createdAt,closedAt" 2>/dev/null); then
|
|
337
|
+
error "Could not fetch issue #${issue}"
|
|
338
|
+
return 1
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
title=$(echo "$issue_data" | jq -r '.title')
|
|
342
|
+
state=$(echo "$issue_data" | jq -r '.state')
|
|
343
|
+
url=$(echo "$issue_data" | jq -r '.url')
|
|
344
|
+
created_at=$(echo "$issue_data" | jq -r '.createdAt')
|
|
345
|
+
|
|
346
|
+
# Build markdown report
|
|
347
|
+
local report=""
|
|
348
|
+
report+="# Traceability Report: Issue #${issue}\n\n"
|
|
349
|
+
report+="## Issue\n\n"
|
|
350
|
+
report+="- **Title**: ${title}\n"
|
|
351
|
+
report+="- **State**: ${state}\n"
|
|
352
|
+
report+="- **URL**: [${url}](${url})\n"
|
|
353
|
+
report+="- **Created**: ${created_at}\n"
|
|
354
|
+
report+="- **Report Generated**: $(now_iso)\n\n"
|
|
355
|
+
|
|
356
|
+
# Pipeline section
|
|
357
|
+
report+="## Pipeline\n\n"
|
|
358
|
+
if [[ -f "$EVENTS_FILE" ]]; then
|
|
359
|
+
local job_data job_id ts stage max_stage
|
|
360
|
+
job_data=$(grep "\"issue\":${issue}" "$EVENTS_FILE" 2>/dev/null | head -1)
|
|
361
|
+
|
|
362
|
+
if [[ -n "$job_data" ]]; then
|
|
363
|
+
job_id=$(echo "$job_data" | jq -r '.job_id // "unknown"')
|
|
364
|
+
ts=$(echo "$job_data" | jq -r '.ts // "unknown"')
|
|
365
|
+
stage=$(echo "$job_data" | jq -r '.stage // "unknown"')
|
|
366
|
+
|
|
367
|
+
report+="- **Job ID**: \`${job_id}\`\n"
|
|
368
|
+
report+="- **Started**: ${ts}\n"
|
|
369
|
+
|
|
370
|
+
# Find final stage
|
|
371
|
+
max_stage=$(grep "\"job_id\":\"${job_id}\"" "$EVENTS_FILE" 2>/dev/null \
|
|
372
|
+
| jq -r '.stage // ""' | tail -1)
|
|
373
|
+
report+="- **Final Stage**: ${max_stage}\n\n"
|
|
374
|
+
else
|
|
375
|
+
report+="No pipeline run found.\n\n"
|
|
376
|
+
fi
|
|
377
|
+
else
|
|
378
|
+
report+="No events log available.\n\n"
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
# Commits section
|
|
382
|
+
report+="## Commits\n\n"
|
|
383
|
+
local commit_count
|
|
384
|
+
commit_count=$(git rev-list --count main..feat/issue-"${issue}" 2>/dev/null || echo "0")
|
|
385
|
+
|
|
386
|
+
if [[ "$commit_count" -gt 0 ]]; then
|
|
387
|
+
report+="Found ${commit_count} commits on feature branch:\n\n"
|
|
388
|
+
report+="\`\`\`\n"
|
|
389
|
+
report+=$(git log --oneline main..feat/issue-"${issue}" 2>/dev/null || echo "(no commits)")
|
|
390
|
+
report+="\n\`\`\`\n\n"
|
|
391
|
+
else
|
|
392
|
+
report+="No commits on feature branch.\n\n"
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
# PR section
|
|
396
|
+
report+="## Pull Request\n\n"
|
|
397
|
+
local pr_data
|
|
398
|
+
pr_data=$(gh pr list --state all --search "issue:${issue}" --json "number,title,state,url" -L 1 2>/dev/null || echo "")
|
|
399
|
+
|
|
400
|
+
if [[ -n "$pr_data" ]] && [[ "$pr_data" != "[]" ]]; then
|
|
401
|
+
local pr_num pr_title pr_state pr_url
|
|
402
|
+
pr_num=$(echo "$pr_data" | jq -r '.[0].number // "unknown"')
|
|
403
|
+
pr_title=$(echo "$pr_data" | jq -r '.[0].title // "unknown"')
|
|
404
|
+
pr_state=$(echo "$pr_data" | jq -r '.[0].state // "unknown"')
|
|
405
|
+
pr_url=$(echo "$pr_data" | jq -r '.[0].url // "unknown"')
|
|
406
|
+
|
|
407
|
+
report+="- **Number**: [#${pr_num}](${pr_url})\n"
|
|
408
|
+
report+="- **Title**: ${pr_title}\n"
|
|
409
|
+
report+="- **State**: ${pr_state}\n"
|
|
410
|
+
else
|
|
411
|
+
report+="No PR found.\n"
|
|
412
|
+
fi
|
|
413
|
+
report+="\n"
|
|
414
|
+
|
|
415
|
+
# Write report
|
|
416
|
+
echo -e "$report" > "$output_file"
|
|
417
|
+
success "Exported to ${CYAN}${output_file}${RESET}"
|
|
418
|
+
echo ""
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# ─── Show help ──────────────────────────────────────────────────────────────
|
|
422
|
+
show_help() {
|
|
423
|
+
echo -e "${BOLD}shipwright trace${RESET} — E2E Traceability (Issue → Commit → PR → Deploy)"
|
|
424
|
+
echo ""
|
|
425
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
426
|
+
echo -e " ${CYAN}shipwright trace${RESET} <command> [options]"
|
|
427
|
+
echo ""
|
|
428
|
+
echo -e "${BOLD}COMMANDS${RESET}"
|
|
429
|
+
echo -e " ${CYAN}show <issue>${RESET} Show full traceability chain for an issue"
|
|
430
|
+
echo -e " ${CYAN}list [limit]${RESET} Show recent pipeline runs (default: 10)"
|
|
431
|
+
echo -e " ${CYAN}search --commit <sha>${RESET} Find issue/pipeline for a commit"
|
|
432
|
+
echo -e " ${CYAN}export <issue> [file]${RESET} Export traceability as markdown"
|
|
433
|
+
echo ""
|
|
434
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
435
|
+
echo -e " ${DIM}shipwright trace show 42${RESET}"
|
|
436
|
+
echo -e " ${DIM}shipwright trace list 20${RESET}"
|
|
437
|
+
echo -e " ${DIM}shipwright trace search --commit abc1234${RESET}"
|
|
438
|
+
echo -e " ${DIM}shipwright trace export 42 trace-issue-42.md${RESET}"
|
|
439
|
+
echo ""
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
# ─── Main ───────────────────────────────────────────────────────────────────
|
|
443
|
+
main() {
|
|
444
|
+
local cmd="${1:-help}"
|
|
445
|
+
shift 2>/dev/null || true
|
|
446
|
+
|
|
447
|
+
case "$cmd" in
|
|
448
|
+
show)
|
|
449
|
+
if [[ -z "${1:-}" ]]; then
|
|
450
|
+
error "Issue number required"
|
|
451
|
+
return 1
|
|
452
|
+
fi
|
|
453
|
+
trace_show "$1"
|
|
454
|
+
;;
|
|
455
|
+
list)
|
|
456
|
+
trace_list "${1:-10}"
|
|
457
|
+
;;
|
|
458
|
+
search)
|
|
459
|
+
if [[ "${1:-}" != "--commit" ]] || [[ -z "${2:-}" ]]; then
|
|
460
|
+
error "Usage: shipwright trace search --commit <sha>"
|
|
461
|
+
return 1
|
|
462
|
+
fi
|
|
463
|
+
trace_search "$2"
|
|
464
|
+
;;
|
|
465
|
+
export)
|
|
466
|
+
if [[ -z "${1:-}" ]]; then
|
|
467
|
+
error "Issue number required"
|
|
468
|
+
return 1
|
|
469
|
+
fi
|
|
470
|
+
trace_export "$1" "${2:-}"
|
|
471
|
+
;;
|
|
472
|
+
help|--help|-h)
|
|
473
|
+
show_help
|
|
474
|
+
;;
|
|
475
|
+
*)
|
|
476
|
+
error "Unknown command: ${cmd}"
|
|
477
|
+
show_help
|
|
478
|
+
return 1
|
|
479
|
+
;;
|
|
480
|
+
esac
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
484
|
+
main "$@"
|
|
485
|
+
fi
|