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.
Files changed (108) hide show
  1. package/README.md +114 -36
  2. package/completions/_shipwright +212 -32
  3. package/completions/shipwright.bash +97 -25
  4. package/docs/strategy/01-market-research.md +619 -0
  5. package/docs/strategy/02-mission-and-brand.md +587 -0
  6. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  7. package/docs/strategy/QUICK-START.txt +289 -0
  8. package/docs/strategy/README.md +172 -0
  9. package/package.json +4 -2
  10. package/scripts/sw +208 -1
  11. package/scripts/sw-activity.sh +500 -0
  12. package/scripts/sw-adaptive.sh +925 -0
  13. package/scripts/sw-adversarial.sh +1 -1
  14. package/scripts/sw-architecture-enforcer.sh +1 -1
  15. package/scripts/sw-auth.sh +613 -0
  16. package/scripts/sw-autonomous.sh +664 -0
  17. package/scripts/sw-changelog.sh +704 -0
  18. package/scripts/sw-checkpoint.sh +1 -1
  19. package/scripts/sw-ci.sh +602 -0
  20. package/scripts/sw-cleanup.sh +1 -1
  21. package/scripts/sw-code-review.sh +637 -0
  22. package/scripts/sw-connect.sh +1 -1
  23. package/scripts/sw-context.sh +605 -0
  24. package/scripts/sw-cost.sh +1 -1
  25. package/scripts/sw-daemon.sh +432 -130
  26. package/scripts/sw-dashboard.sh +1 -1
  27. package/scripts/sw-db.sh +540 -0
  28. package/scripts/sw-decompose.sh +539 -0
  29. package/scripts/sw-deps.sh +551 -0
  30. package/scripts/sw-developer-simulation.sh +1 -1
  31. package/scripts/sw-discovery.sh +412 -0
  32. package/scripts/sw-docs-agent.sh +539 -0
  33. package/scripts/sw-docs.sh +1 -1
  34. package/scripts/sw-doctor.sh +59 -1
  35. package/scripts/sw-dora.sh +615 -0
  36. package/scripts/sw-durable.sh +710 -0
  37. package/scripts/sw-e2e-orchestrator.sh +535 -0
  38. package/scripts/sw-eventbus.sh +393 -0
  39. package/scripts/sw-feedback.sh +471 -0
  40. package/scripts/sw-fix.sh +1 -1
  41. package/scripts/sw-fleet-discover.sh +567 -0
  42. package/scripts/sw-fleet-viz.sh +404 -0
  43. package/scripts/sw-fleet.sh +8 -1
  44. package/scripts/sw-github-app.sh +596 -0
  45. package/scripts/sw-github-checks.sh +1 -1
  46. package/scripts/sw-github-deploy.sh +1 -1
  47. package/scripts/sw-github-graphql.sh +1 -1
  48. package/scripts/sw-guild.sh +569 -0
  49. package/scripts/sw-heartbeat.sh +1 -1
  50. package/scripts/sw-hygiene.sh +559 -0
  51. package/scripts/sw-incident.sh +617 -0
  52. package/scripts/sw-init.sh +88 -1
  53. package/scripts/sw-instrument.sh +699 -0
  54. package/scripts/sw-intelligence.sh +1 -1
  55. package/scripts/sw-jira.sh +1 -1
  56. package/scripts/sw-launchd.sh +363 -28
  57. package/scripts/sw-linear.sh +1 -1
  58. package/scripts/sw-logs.sh +1 -1
  59. package/scripts/sw-loop.sh +64 -3
  60. package/scripts/sw-memory.sh +1 -1
  61. package/scripts/sw-mission-control.sh +487 -0
  62. package/scripts/sw-model-router.sh +545 -0
  63. package/scripts/sw-otel.sh +596 -0
  64. package/scripts/sw-oversight.sh +689 -0
  65. package/scripts/sw-pipeline-composer.sh +1 -1
  66. package/scripts/sw-pipeline-vitals.sh +1 -1
  67. package/scripts/sw-pipeline.sh +687 -24
  68. package/scripts/sw-pm.sh +693 -0
  69. package/scripts/sw-pr-lifecycle.sh +522 -0
  70. package/scripts/sw-predictive.sh +1 -1
  71. package/scripts/sw-prep.sh +1 -1
  72. package/scripts/sw-ps.sh +1 -1
  73. package/scripts/sw-public-dashboard.sh +798 -0
  74. package/scripts/sw-quality.sh +595 -0
  75. package/scripts/sw-reaper.sh +1 -1
  76. package/scripts/sw-recruit.sh +573 -0
  77. package/scripts/sw-regression.sh +642 -0
  78. package/scripts/sw-release-manager.sh +736 -0
  79. package/scripts/sw-release.sh +706 -0
  80. package/scripts/sw-remote.sh +1 -1
  81. package/scripts/sw-replay.sh +520 -0
  82. package/scripts/sw-retro.sh +691 -0
  83. package/scripts/sw-scale.sh +444 -0
  84. package/scripts/sw-security-audit.sh +505 -0
  85. package/scripts/sw-self-optimize.sh +1 -1
  86. package/scripts/sw-session.sh +1 -1
  87. package/scripts/sw-setup.sh +1 -1
  88. package/scripts/sw-standup.sh +712 -0
  89. package/scripts/sw-status.sh +1 -1
  90. package/scripts/sw-strategic.sh +658 -0
  91. package/scripts/sw-stream.sh +450 -0
  92. package/scripts/sw-swarm.sh +583 -0
  93. package/scripts/sw-team-stages.sh +511 -0
  94. package/scripts/sw-templates.sh +1 -1
  95. package/scripts/sw-testgen.sh +515 -0
  96. package/scripts/sw-tmux-pipeline.sh +554 -0
  97. package/scripts/sw-tmux.sh +1 -1
  98. package/scripts/sw-trace.sh +485 -0
  99. package/scripts/sw-tracker-github.sh +188 -0
  100. package/scripts/sw-tracker-jira.sh +172 -0
  101. package/scripts/sw-tracker-linear.sh +251 -0
  102. package/scripts/sw-tracker.sh +117 -2
  103. package/scripts/sw-triage.sh +603 -0
  104. package/scripts/sw-upgrade.sh +1 -1
  105. package/scripts/sw-ux.sh +677 -0
  106. package/scripts/sw-webhook.sh +627 -0
  107. package/scripts/sw-widgets.sh +530 -0
  108. package/scripts/sw-worktree.sh +1 -1
@@ -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
+ }