shipwright-cli 1.9.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/.claude/hooks/post-tool-use.sh +12 -5
- 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 +217 -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 +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +79 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +192 -7
- 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 +812 -138
- 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 +366 -31
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +507 -51
- package/scripts/sw-memory.sh +198 -3
- 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 +8 -8
- package/scripts/sw-pipeline-vitals.sh +1096 -0
- package/scripts/sw-pipeline.sh +2451 -180
- 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 +4 -3
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +5 -3
- 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 +109 -8
- package/scripts/sw-session.sh +31 -9
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +192 -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
- package/templates/pipelines/autonomous.json +8 -1
- package/templates/pipelines/cost-aware.json +21 -0
- package/templates/pipelines/deployed.json +40 -6
- package/templates/pipelines/enterprise.json +16 -2
- package/templates/pipelines/fast.json +19 -0
- package/templates/pipelines/full.json +16 -2
- package/templates/pipelines/hotfix.json +19 -0
- package/templates/pipelines/standard.json +19 -0
|
@@ -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.0.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
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright tracker: GitHub Provider ║
|
|
4
|
+
# ║ Sourced by sw-tracker.sh — do not call directly ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
# This file is sourced by sw-tracker.sh.
|
|
7
|
+
# It defines provider_* functions used by the tracker router.
|
|
8
|
+
# Do NOT add set -euo pipefail or a main() function here.
|
|
9
|
+
|
|
10
|
+
# ─── Discovery & CRUD Interface ────────────────────────────────────────────
|
|
11
|
+
# All functions output normalized JSON (or plain text where specified).
|
|
12
|
+
# Input: normalized arguments (label, state, issue_id, etc.)
|
|
13
|
+
# Output: JSON matching common schema across all providers
|
|
14
|
+
|
|
15
|
+
# Discover issues from GitHub using gh CLI
|
|
16
|
+
# Input: label, state, limit
|
|
17
|
+
# Output: JSON array of {id, title, labels[], state, body}
|
|
18
|
+
provider_discover_issues() {
|
|
19
|
+
local label="$1"
|
|
20
|
+
local state="${2:-open}"
|
|
21
|
+
local limit="${3:-50}"
|
|
22
|
+
|
|
23
|
+
# Check $NO_GITHUB env var
|
|
24
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
25
|
+
|
|
26
|
+
local gh_args=()
|
|
27
|
+
gh_args+=(issue list)
|
|
28
|
+
gh_args+=(--state "$state")
|
|
29
|
+
gh_args+=(--limit "$limit")
|
|
30
|
+
|
|
31
|
+
if [[ -n "$label" ]]; then
|
|
32
|
+
gh_args+=(--label "$label")
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Fetch as JSON: number, title, labels, state
|
|
36
|
+
gh_args+=(--json number,title,labels,state)
|
|
37
|
+
|
|
38
|
+
local response
|
|
39
|
+
response=$(gh "${gh_args[@]}" 2>/dev/null) || {
|
|
40
|
+
echo "[]"
|
|
41
|
+
return 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Normalize to {id, title, labels[], state}
|
|
45
|
+
echo "$response" | jq '[.[] | {id: .number, title: .title, labels: [.labels[].name], state: .state}]' 2>/dev/null || echo "[]"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Fetch single issue details
|
|
49
|
+
# Input: issue_id (number or identifier like "123" or "OWNER/REPO#123")
|
|
50
|
+
# Output: JSON {id, title, body, labels[], state}
|
|
51
|
+
provider_get_issue() {
|
|
52
|
+
local issue_id="$1"
|
|
53
|
+
|
|
54
|
+
[[ -z "$issue_id" ]] && return 1
|
|
55
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
56
|
+
|
|
57
|
+
local response
|
|
58
|
+
response=$(gh issue view "$issue_id" --json number,title,body,labels,state 2>/dev/null) || {
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Normalize output
|
|
63
|
+
echo "$response" | jq '{id: .number, title: .title, body: .body, labels: [.labels[].name], state: .state}' 2>/dev/null || return 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Fetch issue body text only
|
|
67
|
+
# Input: issue_id
|
|
68
|
+
# Output: plain text body
|
|
69
|
+
provider_get_issue_body() {
|
|
70
|
+
local issue_id="$1"
|
|
71
|
+
|
|
72
|
+
[[ -z "$issue_id" ]] && return 1
|
|
73
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
74
|
+
|
|
75
|
+
gh issue view "$issue_id" --json body --jq '.body' 2>/dev/null || return 1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Add label to issue
|
|
79
|
+
# Input: issue_id, label
|
|
80
|
+
# Output: none (stdout on success, nothing on failure)
|
|
81
|
+
provider_add_label() {
|
|
82
|
+
local issue_id="$1"
|
|
83
|
+
local label="$2"
|
|
84
|
+
|
|
85
|
+
[[ -z "$issue_id" || -z "$label" ]] && return 1
|
|
86
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
87
|
+
|
|
88
|
+
gh issue edit "$issue_id" --add-label "$label" 2>/dev/null || return 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Remove label from issue
|
|
92
|
+
# Input: issue_id, label
|
|
93
|
+
# Output: none
|
|
94
|
+
provider_remove_label() {
|
|
95
|
+
local issue_id="$1"
|
|
96
|
+
local label="$2"
|
|
97
|
+
|
|
98
|
+
[[ -z "$issue_id" || -z "$label" ]] && return 1
|
|
99
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
100
|
+
|
|
101
|
+
gh issue edit "$issue_id" --remove-label "$label" 2>/dev/null || return 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Add comment to issue
|
|
105
|
+
# Input: issue_id, body
|
|
106
|
+
# Output: none
|
|
107
|
+
provider_comment() {
|
|
108
|
+
local issue_id="$1"
|
|
109
|
+
local body="$2"
|
|
110
|
+
|
|
111
|
+
[[ -z "$issue_id" || -z "$body" ]] && return 1
|
|
112
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
113
|
+
|
|
114
|
+
gh issue comment "$issue_id" --body "$body" 2>/dev/null || return 1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Close/resolve issue
|
|
118
|
+
# Input: issue_id
|
|
119
|
+
# Output: none
|
|
120
|
+
provider_close_issue() {
|
|
121
|
+
local issue_id="$1"
|
|
122
|
+
|
|
123
|
+
[[ -z "$issue_id" ]] && return 1
|
|
124
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
125
|
+
|
|
126
|
+
gh issue close "$issue_id" 2>/dev/null || return 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Create new issue
|
|
130
|
+
# Input: title, body, labels (comma-separated or space-separated)
|
|
131
|
+
# Output: JSON {id, title}
|
|
132
|
+
provider_create_issue() {
|
|
133
|
+
local title="$1"
|
|
134
|
+
local body="$2"
|
|
135
|
+
local labels="${3:-}"
|
|
136
|
+
|
|
137
|
+
[[ -z "$title" ]] && return 1
|
|
138
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
139
|
+
|
|
140
|
+
local gh_args=(issue create)
|
|
141
|
+
gh_args+=(--title "$title")
|
|
142
|
+
|
|
143
|
+
if [[ -n "$body" ]]; then
|
|
144
|
+
gh_args+=(--body "$body")
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
if [[ -n "$labels" ]]; then
|
|
148
|
+
# Convert space-separated to gh format (--label multiple times)
|
|
149
|
+
# Handle both "label1,label2" and "label1 label2"
|
|
150
|
+
local label_list
|
|
151
|
+
label_list=$(echo "$labels" | tr ',' '\n' | tr ' ' '\n' | grep -v '^$' || true)
|
|
152
|
+
while IFS= read -r label; do
|
|
153
|
+
[[ -n "$label" ]] && gh_args+=(--label "$label")
|
|
154
|
+
done <<< "$label_list"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
local response
|
|
158
|
+
response=$(gh "${gh_args[@]}" 2>/dev/null) || {
|
|
159
|
+
return 1
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Extract issue number from response or return error
|
|
163
|
+
# GitHub response is typically: "Created issue <repo>#<number>"
|
|
164
|
+
local issue_num
|
|
165
|
+
issue_num=$(echo "$response" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || true)
|
|
166
|
+
|
|
167
|
+
if [[ -z "$issue_num" ]]; then
|
|
168
|
+
return 1
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
echo "{\"id\": $issue_num, \"title\": \"$title\"}"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# ─── Main Provider Entry Point (Notification) ──────────────────────────────
|
|
175
|
+
# Called by tracker_notify() in sw-tracker.sh
|
|
176
|
+
|
|
177
|
+
provider_notify() {
|
|
178
|
+
local event="$1"
|
|
179
|
+
local gh_issue="${2:-}"
|
|
180
|
+
local detail="${3:-}"
|
|
181
|
+
|
|
182
|
+
# GitHub is the native provider — no external sync needed
|
|
183
|
+
# This function exists for consistency with Linear/Jira but is minimal
|
|
184
|
+
# Real integration happens through pipeline stages calling provider_* functions
|
|
185
|
+
|
|
186
|
+
# For now, just log the event
|
|
187
|
+
emit_event "tracker.notify" "provider=github" "event=$event" "issue=$gh_issue"
|
|
188
|
+
}
|