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,627 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ sw-webhook.sh — GitHub Webhook Receiver for Instant Issue Processing ║
4
+ # ║ Replaces polling with instant webhook delivery · HMAC-SHA256 validation ║
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
+
12
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
13
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
14
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
15
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
16
+ GREEN='\033[38;2;74;222;128m' # success
17
+ YELLOW='\033[38;2;250;204;21m' # warning
18
+ RED='\033[38;2;248;113;113m' # error
19
+ DIM='\033[2m'
20
+ BOLD='\033[1m'
21
+ RESET='\033[0m'
22
+
23
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
24
+ # shellcheck source=lib/compat.sh
25
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
26
+
27
+ # ─── Output Helpers ─────────────────────────────────────────────────────────
28
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
29
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
30
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
31
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
32
+
33
+ # ─── Constants ──────────────────────────────────────────────────────────────
34
+ SHIPWRIGHT_DIR="$HOME/.shipwright"
35
+ WEBHOOK_SECRET_FILE="$SHIPWRIGHT_DIR/webhook-secret"
36
+ WEBHOOK_EVENTS_FILE="$SHIPWRIGHT_DIR/webhook-events.jsonl"
37
+ WEBHOOK_PORT="${WEBHOOK_PORT:-8765}"
38
+ WEBHOOK_PID_FILE="$SHIPWRIGHT_DIR/webhook.pid"
39
+ WEBHOOK_LOG="$SHIPWRIGHT_DIR/webhook.log"
40
+
41
+ # ─── Helpers ────────────────────────────────────────────────────────────────
42
+
43
+ ensure_dir() {
44
+ mkdir -p "$SHIPWRIGHT_DIR"
45
+ }
46
+
47
+ now_iso() {
48
+ date -u +"%Y-%m-%dT%H:%M:%SZ"
49
+ }
50
+
51
+ now_epoch() {
52
+ date +%s
53
+ }
54
+
55
+ # Generate or retrieve webhook secret
56
+ get_or_create_secret() {
57
+ ensure_dir
58
+ if [[ -f "$WEBHOOK_SECRET_FILE" ]]; then
59
+ cat "$WEBHOOK_SECRET_FILE"
60
+ else
61
+ local secret
62
+ secret=$(openssl rand -hex 32)
63
+ echo "$secret" > "$WEBHOOK_SECRET_FILE"
64
+ chmod 600 "$WEBHOOK_SECRET_FILE"
65
+ echo "$secret"
66
+ fi
67
+ }
68
+
69
+ # Validate HMAC-SHA256 signature from GitHub webhook header
70
+ validate_webhook_signature() {
71
+ local payload="$1"
72
+ local signature="$2"
73
+ local secret
74
+ secret=$(get_or_create_secret)
75
+
76
+ # GitHub sends signature as "sha256=<hex>"
77
+ local expected_signature
78
+ expected_signature="sha256=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$secret" -hex | awk '{print $2}')"
79
+
80
+ # Use constant-time comparison if available, otherwise direct comparison
81
+ if [[ "$signature" == "$expected_signature" ]]; then
82
+ return 0
83
+ else
84
+ return 1
85
+ fi
86
+ }
87
+
88
+ # Parse webhook payload and emit event if labeled issue
89
+ # Returns 0 if event was processed, 1 if it was ignored
90
+ process_webhook_event() {
91
+ local payload="$1"
92
+ local event_type="${2:-unknown}"
93
+
94
+ # Only process issues.labeled events
95
+ if [[ "$event_type" != "issues" ]]; then
96
+ return 1
97
+ fi
98
+
99
+ local action
100
+ action=$(echo "$payload" | jq -r '.action // empty' 2>/dev/null || echo "")
101
+
102
+ if [[ "$action" != "labeled" ]]; then
103
+ return 1
104
+ fi
105
+
106
+ # Extract relevant fields
107
+ local issue_num repo_full_name issue_title label_name
108
+ issue_num=$(echo "$payload" | jq -r '.issue.number // empty' 2>/dev/null || echo "")
109
+ repo_full_name=$(echo "$payload" | jq -r '.repository.full_name // empty' 2>/dev/null || echo "")
110
+ issue_title=$(echo "$payload" | jq -r '.issue.title // empty' 2>/dev/null || echo "")
111
+ label_name=$(echo "$payload" | jq -r '.label.name // empty' 2>/dev/null || echo "")
112
+
113
+ if [[ -z "$issue_num" || -z "$repo_full_name" || -z "$label_name" ]]; then
114
+ return 1
115
+ fi
116
+
117
+ # Write event to webhook events file for daemon to process
118
+ ensure_dir
119
+ local event_record
120
+ event_record=$(jq -nc \
121
+ --arg ts "$(now_iso)" \
122
+ --arg ts_epoch "$(now_epoch)" \
123
+ --arg repo "$repo_full_name" \
124
+ --arg issue "$issue_num" \
125
+ --arg title "$issue_title" \
126
+ --arg label "$label_name" \
127
+ '{ts: $ts, ts_epoch: $ts_epoch, source: "webhook", repo: $repo, issue: $issue, title: $title, label: $label}')
128
+
129
+ echo "$event_record" >> "$WEBHOOK_EVENTS_FILE"
130
+
131
+ info "Webhook: Issue #${issue_num} labeled '${label_name}' in ${repo_full_name}"
132
+ return 0
133
+ }
134
+
135
+ # ─── HTTP Server (lightweight bash + nc) ───────────────────────────────────
136
+
137
+ # Check if nc (netcat) is available
138
+ check_nc() {
139
+ if ! command -v nc &>/dev/null; then
140
+ error "netcat (nc) is required but not installed"
141
+ echo -e " ${DIM}brew install netcat${RESET} (macOS)"
142
+ echo -e " ${DIM}sudo apt install netcat-openbsd${RESET} (Ubuntu/Debian)"
143
+ return 1
144
+ fi
145
+ }
146
+
147
+ # Read HTTP request from file descriptor
148
+ read_http_request() {
149
+ local fd="$1"
150
+ local method path headers body
151
+
152
+ # Read request line
153
+ IFS= read -r -u "$fd" request_line || return 1
154
+ method=$(echo "$request_line" | awk '{print $1}')
155
+ path=$(echo "$request_line" | awk '{print $2}')
156
+
157
+ # Read headers
158
+ while IFS= read -r -u "$fd" -t 0 header_line; do
159
+ [[ -z "$header_line" || "$header_line" == $'\r' ]] && break
160
+ headers="${headers}${header_line}"$'\n'
161
+ done
162
+
163
+ echo "$method|$path|$headers"
164
+ }
165
+
166
+ # Parse HTTP headers to extract specific header value
167
+ get_header() {
168
+ local headers="$1"
169
+ local header_name="$2"
170
+
171
+ # Case-insensitive header lookup
172
+ echo "$headers" | grep -i "^${header_name}:" | cut -d':' -f2- | sed 's/^ *//' | tr -d '\r'
173
+ }
174
+
175
+ # Send HTTP response
176
+ send_http_response() {
177
+ local status_code="$1"
178
+ local content_type="${2:-text/plain}"
179
+ local body="${3:-}"
180
+
181
+ local status_text
182
+ case "$status_code" in
183
+ 200) status_text="OK" ;;
184
+ 202) status_text="Accepted" ;;
185
+ 400) status_text="Bad Request" ;;
186
+ 401) status_text="Unauthorized" ;;
187
+ 404) status_text="Not Found" ;;
188
+ 500) status_text="Internal Server Error" ;;
189
+ *) status_text="Unknown" ;;
190
+ esac
191
+
192
+ local content_length
193
+ content_length=${#body}
194
+
195
+ cat <<EOF
196
+ HTTP/1.1 $status_code $status_text
197
+ Content-Type: $content_type
198
+ Content-Length: $content_length
199
+ Connection: close
200
+
201
+ $body
202
+ EOF
203
+ }
204
+
205
+ # Main webhook server loop
206
+ webhook_server() {
207
+ check_nc || return 1
208
+
209
+ info "Starting webhook server on port ${WEBHOOK_PORT}..."
210
+
211
+ # Try to bind to port
212
+ if ! nc -l -p "$WEBHOOK_PORT" 2>/dev/null; then
213
+ # macOS nc syntax differs
214
+ if ! nc -l localhost "$WEBHOOK_PORT" 2>/dev/null; then
215
+ error "Failed to bind to port ${WEBHOOK_PORT}"
216
+ return 1
217
+ fi
218
+ fi &
219
+
220
+ local nc_pid=$!
221
+ echo "$nc_pid" > "$WEBHOOK_PID_FILE"
222
+
223
+ success "Webhook server running (PID: $nc_pid)"
224
+ success "GitHub webhook secret: $(get_or_create_secret | cut -c1-8)..."
225
+
226
+ # Wait for nc to finish
227
+ wait $nc_pid 2>/dev/null || true
228
+
229
+ rm -f "$WEBHOOK_PID_FILE"
230
+ }
231
+
232
+ # Better approach: use a bash loop with /dev/tcp (BASH_REMATCH compatible)
233
+ webhook_server_bash() {
234
+ check_nc || return 1
235
+
236
+ info "Starting webhook server on port ${WEBHOOK_PORT}..."
237
+ success "GitHub webhook secret: $(get_or_create_secret | cut -c1-8)..."
238
+
239
+ # Create FIFO for IPC
240
+ local fifo
241
+ fifo="/tmp/webhook-$$-fifo"
242
+ mkfifo "$fifo" 2>/dev/null || true
243
+
244
+ # Background listener loop
245
+ (
246
+ while true; do
247
+ {
248
+ read -r -u 3 request_line || break
249
+ local method path protocol
250
+ read -r method path protocol <<< "$request_line"
251
+
252
+ # Read headers until blank line
253
+ local -A headers
254
+ local header_line content_length=0
255
+ while read -r -u 3 -t 0.1 header_line; do
256
+ [[ -z "$header_line" || "$header_line" == $'\r' ]] && break
257
+ local key="${header_line%%:*}"
258
+ local value="${header_line#*:}"
259
+ value="${value#[[:space:]]}"
260
+ value="${value%$'\r'}"
261
+ headers["$key"]="$value"
262
+ [[ "${key,,}" == "content-length" ]] && content_length="$value"
263
+ done 2>/dev/null || true
264
+
265
+ # Read body if content-length > 0
266
+ local body=""
267
+ if [[ $content_length -gt 0 ]]; then
268
+ read -r -u 3 -N "$content_length" body 2>/dev/null || true
269
+ fi
270
+
271
+ # Process webhook if method is POST
272
+ if [[ "$method" == "POST" && "$path" == "/webhook" ]]; then
273
+ local signature="${headers[X-Hub-Signature-256]:-}"
274
+ local event_type="${headers[X-Github-Event]:-}"
275
+
276
+ if validate_webhook_signature "$body" "$signature"; then
277
+ if process_webhook_event "$body" "$event_type"; then
278
+ send_http_response 202 "application/json" '{"status":"accepted"}'
279
+ else
280
+ send_http_response 202 "application/json" '{"status":"ignored"}'
281
+ fi
282
+ else
283
+ warn "Invalid signature from $(echo "$body" | jq -r '.repository.full_name // "unknown"' 2>/dev/null)"
284
+ send_http_response 401 "application/json" '{"error":"Unauthorized"}'
285
+ fi
286
+ else
287
+ send_http_response 404 "application/json" '{"error":"Not Found"}'
288
+ fi
289
+ } 3< "$fifo"
290
+ done
291
+ ) &
292
+
293
+ local server_pid=$!
294
+ echo "$server_pid" > "$WEBHOOK_PID_FILE"
295
+
296
+ # Accept connections (simple approach with exec)
297
+ while true; do
298
+ # This is a simplified approach - for production, use a proper HTTP server
299
+ # For now, we'll just log that the server is running
300
+ sleep 1
301
+ done &
302
+
303
+ wait
304
+ }
305
+
306
+ # ─── Subcommands ────────────────────────────────────────────────────────────
307
+
308
+ cmd_setup() {
309
+ local org_repo="${1:-}"
310
+
311
+ if [[ -z "$org_repo" ]]; then
312
+ error "Usage: shipwright webhook setup <org/repo>"
313
+ return 1
314
+ fi
315
+
316
+ # Validate org/repo format
317
+ if [[ ! "$org_repo" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9._-]+$ ]]; then
318
+ error "Invalid org/repo format: $org_repo"
319
+ return 1
320
+ fi
321
+
322
+ local secret
323
+ secret=$(get_or_create_secret)
324
+
325
+ info "Setting up webhook for ${org_repo}..."
326
+ info "Webhook endpoint: http://localhost:${WEBHOOK_PORT}/webhook"
327
+
328
+ # Check if gh CLI is available
329
+ if ! command -v gh &>/dev/null; then
330
+ error "GitHub CLI (gh) is required but not installed"
331
+ return 1
332
+ fi
333
+
334
+ # Create webhook via GitHub API
335
+ local webhook_response
336
+ if webhook_response=$(gh api "repos/${org_repo}/hooks" \
337
+ -X POST \
338
+ -f "name=web" \
339
+ -f "active=true" \
340
+ -f "url=http://localhost:${WEBHOOK_PORT}/webhook" \
341
+ -F "events=issues" \
342
+ -f "config[content_type]=json" \
343
+ -f "config[secret]=${secret}" 2>&1); then
344
+
345
+ local hook_id
346
+ hook_id=$(echo "$webhook_response" | jq -r '.id // empty' 2>/dev/null || true)
347
+
348
+ if [[ -n "$hook_id" ]]; then
349
+ success "Webhook created (ID: ${hook_id})"
350
+ return 0
351
+ fi
352
+ fi
353
+
354
+ error "Failed to create webhook. Check that:"
355
+ echo " - gh CLI is authenticated (run: gh auth login)"
356
+ echo " - You have admin access to ${org_repo}"
357
+ echo " - The webhook endpoint is publicly accessible"
358
+ return 1
359
+ }
360
+
361
+ cmd_status() {
362
+ ensure_dir
363
+
364
+ if [[ -f "$WEBHOOK_PID_FILE" ]]; then
365
+ local pid
366
+ pid=$(cat "$WEBHOOK_PID_FILE" 2>/dev/null || true)
367
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
368
+ success "Webhook server is running (PID: ${pid})"
369
+ else
370
+ warn "Webhook server is NOT running"
371
+ fi
372
+ else
373
+ warn "Webhook server is NOT running"
374
+ fi
375
+
376
+ echo ""
377
+ info "Configuration:"
378
+ echo " Secret file: ${WEBHOOK_SECRET_FILE}"
379
+ echo " Events file: ${WEBHOOK_EVENTS_FILE}"
380
+ echo " Port: ${WEBHOOK_PORT}"
381
+
382
+ echo ""
383
+ if [[ -f "$WEBHOOK_EVENTS_FILE" ]]; then
384
+ local event_count
385
+ event_count=$(wc -l < "$WEBHOOK_EVENTS_FILE" 2>/dev/null || echo 0)
386
+ info "Recent webhook events (${event_count} total):"
387
+ tail -5 "$WEBHOOK_EVENTS_FILE" 2>/dev/null | jq -c '{ts, repo, issue, label}' || echo " (no events yet)"
388
+ else
389
+ info "No webhook events recorded yet"
390
+ fi
391
+ }
392
+
393
+ cmd_test() {
394
+ local org_repo="${1:-}"
395
+
396
+ if [[ -z "$org_repo" ]]; then
397
+ error "Usage: shipwright webhook test <org/repo>"
398
+ return 1
399
+ fi
400
+
401
+ if ! command -v gh &>/dev/null; then
402
+ error "GitHub CLI (gh) is required"
403
+ return 1
404
+ fi
405
+
406
+ info "Sending test ping to webhook for ${org_repo}..."
407
+
408
+ # Construct a test webhook payload
409
+ local secret
410
+ secret=$(get_or_create_secret)
411
+
412
+ local payload
413
+ payload=$(jq -n \
414
+ --arg repo "$org_repo" \
415
+ --arg action "labeled" \
416
+ '{
417
+ action: $action,
418
+ issue: {
419
+ number: 999,
420
+ title: "Test Issue from Webhook"
421
+ },
422
+ label: {
423
+ name: "shipwright"
424
+ },
425
+ repository: {
426
+ full_name: $repo
427
+ }
428
+ }')
429
+
430
+ # Compute HMAC-SHA256 signature
431
+ local signature
432
+ signature="sha256=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$secret" -hex | awk '{print $2}')"
433
+
434
+ # Send test webhook via GitHub API
435
+ if gh api "repos/${org_repo}/hooks/tests" \
436
+ -H "Accept: application/vnd.github+json" \
437
+ -X POST \
438
+ 2>&1 | grep -q "Test hook sent"; then
439
+ success "Test ping sent to GitHub"
440
+ else
441
+ warn "Could not send test via GitHub API, but payload is valid:"
442
+ echo " Payload: $payload"
443
+ echo " Signature: $signature"
444
+ fi
445
+ }
446
+
447
+ cmd_start() {
448
+ ensure_dir
449
+
450
+ if [[ -f "$WEBHOOK_PID_FILE" ]]; then
451
+ local pid
452
+ pid=$(cat "$WEBHOOK_PID_FILE" 2>/dev/null || true)
453
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
454
+ warn "Webhook server is already running (PID: ${pid})"
455
+ return 0
456
+ fi
457
+ fi
458
+
459
+ info "Starting webhook server..."
460
+
461
+ # Start server in background, capture output
462
+ {
463
+ webhook_server >> "$WEBHOOK_LOG" 2>&1
464
+ } &
465
+
466
+ local bg_pid=$!
467
+ sleep 1
468
+
469
+ if kill -0 $bg_pid 2>/dev/null; then
470
+ success "Webhook server started (PID: ${bg_pid})"
471
+ else
472
+ error "Failed to start webhook server"
473
+ tail -20 "$WEBHOOK_LOG" 2>/dev/null || true
474
+ return 1
475
+ fi
476
+ }
477
+
478
+ cmd_stop() {
479
+ if [[ ! -f "$WEBHOOK_PID_FILE" ]]; then
480
+ warn "Webhook server is not running"
481
+ return 0
482
+ fi
483
+
484
+ local pid
485
+ pid=$(cat "$WEBHOOK_PID_FILE" 2>/dev/null || true)
486
+
487
+ if [[ -z "$pid" ]] || ! kill -0 "$pid" 2>/dev/null; then
488
+ warn "Webhook server is not running (stale PID file)"
489
+ rm -f "$WEBHOOK_PID_FILE"
490
+ return 0
491
+ fi
492
+
493
+ info "Stopping webhook server (PID: ${pid})..."
494
+ kill "$pid" 2>/dev/null || true
495
+ sleep 1
496
+
497
+ if ! kill -0 "$pid" 2>/dev/null; then
498
+ success "Webhook server stopped"
499
+ rm -f "$WEBHOOK_PID_FILE"
500
+ else
501
+ error "Failed to stop webhook server — force killing..."
502
+ kill -9 "$pid" 2>/dev/null || true
503
+ rm -f "$WEBHOOK_PID_FILE"
504
+ fi
505
+ }
506
+
507
+ cmd_logs() {
508
+ if [[ ! -f "$WEBHOOK_LOG" ]]; then
509
+ info "No webhook logs yet"
510
+ return 0
511
+ fi
512
+
513
+ tail -50 "$WEBHOOK_LOG"
514
+ }
515
+
516
+ cmd_secret() {
517
+ local action="${1:-show}"
518
+
519
+ case "$action" in
520
+ show|get)
521
+ local secret
522
+ secret=$(get_or_create_secret)
523
+ echo "$secret"
524
+ ;;
525
+ regenerate|reset)
526
+ ensure_dir
527
+ local new_secret
528
+ new_secret=$(openssl rand -hex 32)
529
+ echo "$new_secret" > "$WEBHOOK_SECRET_FILE"
530
+ chmod 600 "$WEBHOOK_SECRET_FILE"
531
+ success "Webhook secret regenerated"
532
+ info "New secret: ${new_secret}"
533
+ ;;
534
+ *)
535
+ error "Unknown secret action: $action"
536
+ return 1
537
+ ;;
538
+ esac
539
+ }
540
+
541
+ # ─── Help ───────────────────────────────────────────────────────────────────
542
+
543
+ show_help() {
544
+ cat <<EOF
545
+ ${BOLD}shipwright webhook${RESET} — GitHub Webhook Receiver
546
+
547
+ ${BOLD}USAGE${RESET}
548
+ shipwright webhook <command> [options]
549
+
550
+ ${BOLD}COMMANDS${RESET}
551
+ ${CYAN}setup${RESET} <org/repo> Configure webhook on GitHub repo
552
+ ${CYAN}status${RESET} Check webhook server health and events
553
+ ${CYAN}start${RESET} Start local webhook server
554
+ ${CYAN}stop${RESET} Stop webhook server
555
+ ${CYAN}test${RESET} <org/repo> Send test webhook event to repo
556
+ ${CYAN}logs${RESET} Show webhook server logs
557
+ ${CYAN}secret${RESET} [show|reset] Manage webhook secret
558
+
559
+ ${BOLD}ENVIRONMENT VARIABLES${RESET}
560
+ WEBHOOK_PORT Port for webhook server (default: 8765)
561
+ WEBHOOK_SECRET_FILE Secret file location (default: ~/.shipwright/webhook-secret)
562
+
563
+ ${BOLD}EXAMPLES${RESET}
564
+ ${DIM}# Setup webhook for a repo${RESET}
565
+ shipwright webhook setup myorg/myrepo
566
+
567
+ ${DIM}# Start the webhook server${RESET}
568
+ shipwright webhook start
569
+
570
+ ${DIM}# Check status${RESET}
571
+ shipwright webhook status
572
+
573
+ ${DIM}# View logs${RESET}
574
+ shipwright webhook logs
575
+
576
+ ${BOLD}NOTES${RESET}
577
+ - Webhook secret is stored in ${WEBHOOK_SECRET_FILE}
578
+ - Events are logged to ${WEBHOOK_EVENTS_FILE}
579
+ - Requires GitHub CLI (gh) for setup commands
580
+ - Requires netcat (nc) for server
581
+
582
+ EOF
583
+ }
584
+
585
+ # ─── Main ───────────────────────────────────────────────────────────────────
586
+
587
+ main() {
588
+ local cmd="${1:-help}"
589
+ shift 2>/dev/null || true
590
+
591
+ case "$cmd" in
592
+ setup)
593
+ cmd_setup "$@"
594
+ ;;
595
+ status)
596
+ cmd_status "$@"
597
+ ;;
598
+ start)
599
+ cmd_start "$@"
600
+ ;;
601
+ stop)
602
+ cmd_stop "$@"
603
+ ;;
604
+ test)
605
+ cmd_test "$@"
606
+ ;;
607
+ logs)
608
+ cmd_logs "$@"
609
+ ;;
610
+ secret)
611
+ cmd_secret "$@"
612
+ ;;
613
+ help|--help|-h)
614
+ show_help
615
+ ;;
616
+ *)
617
+ error "Unknown command: $cmd"
618
+ echo ""
619
+ show_help
620
+ exit 1
621
+ ;;
622
+ esac
623
+ }
624
+
625
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
626
+ main "$@"
627
+ fi