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,736 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright release-manager — Autonomous Release Pipeline ║
4
+ # ║ Readiness checks · Version bumping · Changelog · RC flow · Rollback ║
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
+ now_epoch() { date +%s; }
36
+
37
+ # ─── Structured Event Log ──────────────────────────────────────────────────
38
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
39
+
40
+ emit_event() {
41
+ local event_type="$1"
42
+ shift
43
+ local json_fields=""
44
+ for kv in "$@"; do
45
+ local key="${kv%%=*}"
46
+ local val="${kv#*=}"
47
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
48
+ json_fields="${json_fields},\"${key}\":${val}"
49
+ else
50
+ val="${val//\"/\\\"}"
51
+ json_fields="${json_fields},\"${key}\":\"${val}\""
52
+ fi
53
+ done
54
+ mkdir -p "${HOME}/.shipwright"
55
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
56
+ }
57
+
58
+ # ─── Release State Storage ─────────────────────────────────────────────────
59
+ RELEASE_STATE_DIR="${HOME}/.shipwright/releases"
60
+
61
+ ensure_release_dir() {
62
+ mkdir -p "$RELEASE_STATE_DIR"
63
+ }
64
+
65
+ # ─── Git helpers ─────────────────────────────────────────────────────────────
66
+
67
+ get_latest_tag() {
68
+ git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"
69
+ }
70
+
71
+ parse_version() {
72
+ local version="$1"
73
+ version="${version#v}"
74
+ IFS='.' read -r major minor patch <<< "$version"
75
+ echo "$major|$minor|${patch:-0}"
76
+ }
77
+
78
+ bump_version() {
79
+ local current="$1" bump_type="$2"
80
+ IFS='|' read -r major minor patch <<< "$(parse_version "$current")"
81
+
82
+ case "$bump_type" in
83
+ major)
84
+ major=$((major + 1))
85
+ minor=0
86
+ patch=0
87
+ ;;
88
+ minor)
89
+ minor=$((minor + 1))
90
+ patch=0
91
+ ;;
92
+ patch)
93
+ patch=$((patch + 1))
94
+ ;;
95
+ esac
96
+
97
+ echo "v${major}.${minor}.${patch}"
98
+ }
99
+
100
+ detect_bump_type() {
101
+ local from="$1" to="$2"
102
+ local has_breaking=false
103
+ local has_feature=false
104
+
105
+ while IFS='|' read -r hash subject body; do
106
+ [[ -z "$hash" ]] && continue
107
+
108
+ local type
109
+ if [[ $subject =~ ^([a-z]+) ]]; then
110
+ type="${BASH_REMATCH[1]}"
111
+ else
112
+ continue
113
+ fi
114
+
115
+ case "$type" in
116
+ feat)
117
+ has_feature=true
118
+ if echo "$body" | grep -q "BREAKING CHANGE:"; then
119
+ has_breaking=true
120
+ fi
121
+ ;;
122
+ esac
123
+ done < <(git log "${from}..${to}" --format="%h|%s|%b" 2>/dev/null || true)
124
+
125
+ if [[ "$has_breaking" == "true" ]]; then
126
+ echo "major"
127
+ elif [[ "$has_feature" == "true" ]]; then
128
+ echo "minor"
129
+ else
130
+ echo "patch"
131
+ fi
132
+ }
133
+
134
+ # ─── Quality Gate Checks ──────────────────────────────────────────────────
135
+
136
+ check_tests_passing() {
137
+ info "Checking test status..."
138
+
139
+ if [[ ! -f "$REPO_DIR/package.json" ]]; then
140
+ warn "No package.json found — skipping test check"
141
+ return 0
142
+ fi
143
+
144
+ if ! npm test 2>&1 | tee /tmp/test-output.log | tail -20; then
145
+ error "Tests are not passing"
146
+ return 1
147
+ fi
148
+
149
+ success "All tests passing"
150
+ return 0
151
+ }
152
+
153
+ check_coverage_threshold() {
154
+ local threshold="${1:-80}"
155
+ info "Checking test coverage (threshold: ${threshold}%)..."
156
+
157
+ if [[ ! -f /tmp/test-output.log ]]; then
158
+ warn "No test output found — skipping coverage check"
159
+ return 0
160
+ fi
161
+
162
+ local coverage=""
163
+ coverage=$(grep -oE '[0-9]+%' /tmp/test-output.log | head -1 | tr -d '%' || echo "")
164
+
165
+ if [[ -z "$coverage" ]]; then
166
+ warn "Could not determine coverage percentage"
167
+ return 0
168
+ fi
169
+
170
+ if [[ $coverage -lt $threshold ]]; then
171
+ error "Coverage ${coverage}% is below threshold ${threshold}%"
172
+ return 1
173
+ fi
174
+
175
+ success "Coverage ${coverage}% meets threshold"
176
+ return 0
177
+ }
178
+
179
+ check_no_open_blockers() {
180
+ info "Checking for open blockers..."
181
+
182
+ if ! command -v gh &>/dev/null; then
183
+ warn "GitHub CLI not available — skipping blocker check"
184
+ return 0
185
+ fi
186
+
187
+ local blockers
188
+ blockers=$(gh issue list --label "blocker" --state "open" --json "number" 2>/dev/null | jq 'length' || echo "0")
189
+
190
+ if [[ $blockers -gt 0 ]]; then
191
+ error "Found $blockers open blocker issues"
192
+ return 1
193
+ fi
194
+
195
+ success "No open blocker issues"
196
+ return 0
197
+ }
198
+
199
+ check_security_scan() {
200
+ info "Checking security scan status..."
201
+
202
+ if ! command -v gh &>/dev/null; then
203
+ warn "GitHub CLI not available — skipping security check"
204
+ return 0
205
+ fi
206
+
207
+ local vulns
208
+ vulns=$(gh api repos/{owner}/{repo}/dependabot/alerts --jq 'length' 2>/dev/null || echo "0")
209
+
210
+ if [[ $vulns -gt 0 ]]; then
211
+ error "Found $vulns security vulnerabilities"
212
+ return 1
213
+ fi
214
+
215
+ success "Security scan clean"
216
+ return 0
217
+ }
218
+
219
+ check_docs_updated() {
220
+ info "Checking documentation status..."
221
+
222
+ # Check if stale documentation markers exist
223
+ if grep -r "AUTO:" "$REPO_DIR/.claude/CLAUDE.md" 2>/dev/null | grep -qv "AUTO.*:"; then
224
+ warn "Documentation contains stale AUTO sections"
225
+ return 1
226
+ fi
227
+
228
+ success "Documentation is up to date"
229
+ return 0
230
+ }
231
+
232
+ # ─── Release Readiness ──────────────────────────────────────────────────────
233
+
234
+ check_release_readiness() {
235
+ local exit_code=0
236
+
237
+ echo ""
238
+ info "Performing release readiness checks..."
239
+ echo ""
240
+
241
+ check_tests_passing || exit_code=1
242
+ echo ""
243
+
244
+ check_coverage_threshold 80 || exit_code=1
245
+ echo ""
246
+
247
+ check_no_open_blockers || exit_code=1
248
+ echo ""
249
+
250
+ check_security_scan || exit_code=1
251
+ echo ""
252
+
253
+ check_docs_updated || exit_code=1
254
+ echo ""
255
+
256
+ if [[ $exit_code -eq 0 ]]; then
257
+ success "All release gates passing ✓"
258
+ return 0
259
+ else
260
+ error "Release readiness check failed"
261
+ return 1
262
+ fi
263
+ }
264
+
265
+ # ─── Prepare Release ──────────────────────────────────────────────────────
266
+
267
+ prepare_release() {
268
+ local current_version
269
+ current_version="$(get_latest_tag)"
270
+
271
+ # Detect bump type from commits
272
+ local bump_type
273
+ bump_type="$(detect_bump_type "$current_version" "HEAD")"
274
+
275
+ local next_version
276
+ next_version="$(bump_version "$current_version" "$bump_type")"
277
+
278
+ echo ""
279
+ info "Release Preparation"
280
+ echo ""
281
+ echo -e " Current version: ${CYAN}${current_version}${RESET}"
282
+ echo -e " Next version: ${CYAN}${next_version}${RESET}"
283
+ echo -e " Bump type: ${CYAN}${bump_type}${RESET}"
284
+ echo ""
285
+
286
+ # Save state to file
287
+ ensure_release_dir
288
+ local state_file="$RELEASE_STATE_DIR/current-release.json"
289
+ jq -n --arg version "$next_version" \
290
+ --arg bump_type "$bump_type" \
291
+ --arg timestamp "$(now_iso)" \
292
+ '{version: $version, bump_type: $bump_type, status: "prepared", timestamp: $timestamp}' > "$state_file"
293
+
294
+ success "Release prepared: $next_version"
295
+ emit_event "release.prepare" "version=$next_version" "bump_type=$bump_type"
296
+ }
297
+
298
+ # ─── Publish Release ──────────────────────────────────────────────────────
299
+
300
+ publish_release() {
301
+ local release_state_file="$RELEASE_STATE_DIR/current-release.json"
302
+
303
+ if [[ ! -f "$release_state_file" ]]; then
304
+ error "No prepared release found — run 'prepare' first"
305
+ return 1
306
+ fi
307
+
308
+ local next_version
309
+ next_version=$(jq -r '.version' "$release_state_file")
310
+
311
+ info "Publishing release: ${CYAN}${next_version}${RESET}"
312
+ echo ""
313
+
314
+ # Create annotated tag
315
+ if git tag -a "$next_version" -m "Release $next_version"; then
316
+ success "Git tag created"
317
+ else
318
+ error "Failed to create git tag"
319
+ return 1
320
+ fi
321
+
322
+ # Push tag to origin
323
+ if git push origin "$next_version"; then
324
+ success "Tag pushed to origin"
325
+ else
326
+ warn "Failed to push tag"
327
+ fi
328
+
329
+ # Create GitHub release
330
+ if command -v gh &>/dev/null; then
331
+ if gh release create "$next_version" --title "$next_version" --generate-notes; then
332
+ success "GitHub release created"
333
+ else
334
+ warn "Failed to create GitHub release"
335
+ fi
336
+ else
337
+ warn "GitHub CLI not available — skipping release creation"
338
+ fi
339
+
340
+ # Update state
341
+ jq '.status = "published"' "$release_state_file" > "${release_state_file}.tmp" && \
342
+ mv "${release_state_file}.tmp" "$release_state_file"
343
+
344
+ success "Release published: ${CYAN}${next_version}${RESET}"
345
+ emit_event "release.publish" "version=$next_version"
346
+ }
347
+
348
+ # ─── Release Candidate Flow ──────────────────────────────────────────────
349
+
350
+ create_rc() {
351
+ local rc_number="${1:-1}"
352
+ local current_version
353
+ current_version="$(get_latest_tag)"
354
+
355
+ local bump_type
356
+ bump_type="$(detect_bump_type "$current_version" "HEAD")"
357
+
358
+ local next_version
359
+ next_version="$(bump_version "$current_version" "$bump_type")"
360
+
361
+ local rc_version="${next_version}-rc.${rc_number}"
362
+
363
+ info "Creating release candidate: ${CYAN}${rc_version}${RESET}"
364
+ echo ""
365
+
366
+ # Create RC tag
367
+ if git tag -a "$rc_version" -m "Release Candidate: $rc_version"; then
368
+ success "RC tag created"
369
+ else
370
+ error "Failed to create RC tag"
371
+ return 1
372
+ fi
373
+
374
+ # Push RC tag
375
+ if git push origin "$rc_version"; then
376
+ success "RC tag pushed"
377
+ else
378
+ warn "Failed to push RC tag"
379
+ fi
380
+
381
+ # Create GitHub pre-release
382
+ if command -v gh &>/dev/null; then
383
+ if gh release create "$rc_version" --title "RC: $rc_version" --prerelease --generate-notes; then
384
+ success "GitHub pre-release created"
385
+ else
386
+ warn "Failed to create GitHub pre-release"
387
+ fi
388
+ fi
389
+
390
+ # Save RC state
391
+ ensure_release_dir
392
+ local rc_state_file="$RELEASE_STATE_DIR/rc-${rc_number}.json"
393
+ jq -n --arg version "$rc_version" \
394
+ --arg timestamp "$(now_iso)" \
395
+ '{version: $version, rc_number: '$rc_number', status: "active", timestamp: $timestamp}' > "$rc_state_file"
396
+
397
+ success "RC created: ${CYAN}${rc_version}${RESET}"
398
+ emit_event "release.rc_create" "version=$rc_version" "rc_number=$rc_number"
399
+ }
400
+
401
+ promote_rc() {
402
+ local rc_number="${1:-1}"
403
+ local rc_state_file="$RELEASE_STATE_DIR/rc-${rc_number}.json"
404
+
405
+ if [[ ! -f "$rc_state_file" ]]; then
406
+ error "RC ${rc_number} not found"
407
+ return 1
408
+ fi
409
+
410
+ local rc_version
411
+ rc_version=$(jq -r '.version' "$rc_state_file")
412
+
413
+ info "Promoting RC to stable: ${CYAN}${rc_version}${RESET}"
414
+ echo ""
415
+
416
+ # Extract stable version (remove -rc.X)
417
+ local stable_version="${rc_version%-rc*}"
418
+
419
+ # Create stable tag from RC
420
+ if git tag -a "$stable_version" -m "Release $stable_version" "$(git rev-list -n 1 "$rc_version")"; then
421
+ success "Stable tag created"
422
+ else
423
+ error "Failed to create stable tag"
424
+ return 1
425
+ fi
426
+
427
+ # Push stable tag
428
+ if git push origin "$stable_version"; then
429
+ success "Stable tag pushed"
430
+ else
431
+ warn "Failed to push stable tag"
432
+ fi
433
+
434
+ # Create GitHub release (not pre-release)
435
+ if command -v gh &>/dev/null; then
436
+ if gh release create "$stable_version" --title "$stable_version" --generate-notes; then
437
+ success "GitHub release created"
438
+ else
439
+ warn "Failed to create GitHub release"
440
+ fi
441
+ fi
442
+
443
+ # Update RC state
444
+ jq '.status = "promoted"' "$rc_state_file" > "${rc_state_file}.tmp" && \
445
+ mv "${rc_state_file}.tmp" "$rc_state_file"
446
+
447
+ success "RC promoted to stable: ${CYAN}${stable_version}${RESET}"
448
+ emit_event "release.rc_promote" "rc_version=$rc_version" "stable_version=$stable_version"
449
+ }
450
+
451
+ # ─── Rollback ──────────────────────────────────────────────────────────
452
+
453
+ rollback_release() {
454
+ local version="${1:-}"
455
+
456
+ if [[ -z "$version" ]]; then
457
+ version="$(git describe --tags --abbrev=0 2>/dev/null || echo "")"
458
+ fi
459
+
460
+ if [[ -z "$version" ]]; then
461
+ error "No version specified and no recent tag found"
462
+ return 1
463
+ fi
464
+
465
+ info "Rolling back release: ${CYAN}${version}${RESET}"
466
+ echo ""
467
+
468
+ # Delete local tag
469
+ if git tag -d "$version"; then
470
+ success "Local tag deleted"
471
+ else
472
+ warn "Failed to delete local tag"
473
+ fi
474
+
475
+ # Delete remote tag
476
+ if git push origin ":refs/tags/$version"; then
477
+ success "Remote tag deleted"
478
+ else
479
+ warn "Failed to delete remote tag"
480
+ fi
481
+
482
+ # Delete GitHub release
483
+ if command -v gh &>/dev/null; then
484
+ if gh release delete "$version" --yes 2>/dev/null; then
485
+ success "GitHub release deleted"
486
+ else
487
+ warn "Failed to delete GitHub release"
488
+ fi
489
+ fi
490
+
491
+ success "Rollback complete for ${CYAN}${version}${RESET}"
492
+ emit_event "release.rollback" "version=$version"
493
+ }
494
+
495
+ # ─── Release Schedule ──────────────────────────────────────────────────────
496
+
497
+ set_release_schedule() {
498
+ local schedule_type="${1:-}"
499
+
500
+ if [[ -z "$schedule_type" ]]; then
501
+ error "Schedule type required: weekly, on-demand, on-green"
502
+ return 1
503
+ fi
504
+
505
+ ensure_release_dir
506
+ local schedule_file="$RELEASE_STATE_DIR/schedule.json"
507
+
508
+ case "$schedule_type" in
509
+ weekly)
510
+ jq -n --arg type "weekly" \
511
+ --arg day "monday" \
512
+ --arg time "09:00 UTC" \
513
+ --arg timestamp "$(now_iso)" \
514
+ '{type: $type, day: $day, time: $time, timestamp: $timestamp}' > "$schedule_file"
515
+ success "Release schedule set to: weekly (Monday 09:00 UTC)"
516
+ ;;
517
+ on-demand)
518
+ jq -n --arg type "on-demand" \
519
+ --arg timestamp "$(now_iso)" \
520
+ '{type: $type, timestamp: $timestamp}' > "$schedule_file"
521
+ success "Release schedule set to: on-demand"
522
+ ;;
523
+ on-green)
524
+ jq -n --arg type "on-green" \
525
+ --arg threshold "all gates passing" \
526
+ --arg timestamp "$(now_iso)" \
527
+ '{type: $type, threshold: $threshold, timestamp: $timestamp}' > "$schedule_file"
528
+ success "Release schedule set to: on-green (all gates passing)"
529
+ ;;
530
+ *)
531
+ error "Unknown schedule type: $schedule_type"
532
+ return 1
533
+ ;;
534
+ esac
535
+
536
+ emit_event "release.schedule" "type=$schedule_type"
537
+ }
538
+
539
+ # ─── History and Stats ──────────────────────────────────────────────────
540
+
541
+ show_history() {
542
+ info "Release History"
543
+ echo ""
544
+
545
+ if ! command -v gh &>/dev/null; then
546
+ error "GitHub CLI required for history"
547
+ return 1
548
+ fi
549
+
550
+ gh release list --limit 10
551
+ echo ""
552
+ }
553
+
554
+ show_stats() {
555
+ info "Release Statistics"
556
+ echo ""
557
+
558
+ local total_releases
559
+ total_releases=$(git tag | wc -l)
560
+
561
+ local releases_this_month
562
+ releases_this_month=$(git log --all --oneline --grep="Release" --since="1 month ago" | wc -l)
563
+
564
+ local days_since_last
565
+ local last_tag
566
+ last_tag="$(get_latest_tag)"
567
+ days_since_last=$(git log "$last_tag"..HEAD --format="%ai" | wc -l)
568
+
569
+ echo -e " Total releases: ${CYAN}${total_releases}${RESET}"
570
+ echo -e " This month: ${CYAN}${releases_this_month}${RESET}"
571
+ echo -e " Commits since last: ${CYAN}${days_since_last}${RESET}"
572
+ echo ""
573
+ }
574
+
575
+ # ─── Help ──────────────────────────────────────────────────────────────────
576
+
577
+ show_help() {
578
+ cat << 'EOF'
579
+ shipwright release-manager — Autonomous Release Pipeline
580
+
581
+ USAGE
582
+ shipwright release-manager <command> [options]
583
+ shipwright rm <command> [options]
584
+
585
+ COMMANDS
586
+ check
587
+ Check release readiness (all quality gates)
588
+ • Tests passing
589
+ • Coverage threshold met (80%)
590
+ • No open blocker issues
591
+ • Security scan clean
592
+ • Documentation up to date
593
+
594
+ prepare
595
+ Prepare a release (determine version, but don't publish)
596
+ Auto-detects version from conventional commits
597
+ Saves state for publication
598
+
599
+ publish
600
+ Publish prepared release (create tag + GitHub release)
601
+ Requires prepared state from 'prepare' command
602
+
603
+ rc [RC_NUMBER]
604
+ Create release candidate (e.g., v1.2.0-rc.1)
605
+ Default RC_NUMBER: 1
606
+
607
+ promote RC_NUMBER
608
+ Promote release candidate to stable release
609
+ Example: shipwright rm promote 1
610
+
611
+ rollback [VERSION]
612
+ Rollback a release (delete tag + GitHub release)
613
+ If no VERSION specified, rolls back latest tag
614
+
615
+ schedule SCHEDULE_TYPE
616
+ Set release schedule
617
+ Types: weekly, on-demand, on-green
618
+
619
+ history
620
+ Show recent releases and stats
621
+
622
+ stats
623
+ Show release statistics
624
+
625
+ help
626
+ Show this help message
627
+
628
+ OPTIONS
629
+ (none at this time)
630
+
631
+ EXAMPLES
632
+ # Check if we can release
633
+ shipwright release-manager check
634
+
635
+ # Prepare a release
636
+ shipwright release-manager prepare
637
+
638
+ # Publish after all checks pass
639
+ shipwright release-manager publish
640
+
641
+ # Create release candidate
642
+ shipwright release-manager rc 1
643
+
644
+ # Promote RC to stable
645
+ shipwright release-manager promote 1
646
+
647
+ # Rollback last release
648
+ shipwright release-manager rollback
649
+
650
+ # Set automatic on-green releases
651
+ shipwright release-manager schedule on-green
652
+
653
+ # View release history
654
+ shipwright release-manager history
655
+
656
+ CONVENTIONAL COMMITS
657
+
658
+ The release manager auto-detects version bumps from commits:
659
+
660
+ feat: New feature → minor version bump
661
+ fix: Bug fix → patch version bump
662
+ BREAKING CHANGE: in message → major version bump
663
+
664
+ QUALITY GATES
665
+
666
+ A release requires all of these checks to pass:
667
+ ✓ Tests passing
668
+ ✓ Coverage >= 80%
669
+ ✓ No open blockers
670
+ ✓ Security scan clean
671
+ ✓ Documentation updated
672
+
673
+ RC FLOW
674
+
675
+ 1. Create RC: shipwright rm rc 1 → v1.2.0-rc.1
676
+ 2. Test RC in staging
677
+ 3. Promote: shipwright rm promote 1 → v1.2.0
678
+ 4. Monitor in production
679
+
680
+ ALIASES
681
+
682
+ shipwright rm = shipwright release-manager
683
+
684
+ EOF
685
+ }
686
+
687
+ # ─── Main Router ──────────────────────────────────────────────────────────
688
+
689
+ main() {
690
+ local cmd="${1:-help}"
691
+ shift 2>/dev/null || true
692
+
693
+ case "$cmd" in
694
+ check)
695
+ check_release_readiness
696
+ ;;
697
+ prepare)
698
+ prepare_release
699
+ ;;
700
+ publish)
701
+ publish_release
702
+ ;;
703
+ rc)
704
+ create_rc "${1:-1}"
705
+ ;;
706
+ promote)
707
+ promote_rc "${1:-}"
708
+ ;;
709
+ rollback)
710
+ rollback_release "${1:-}"
711
+ ;;
712
+ schedule)
713
+ set_release_schedule "${1:-}"
714
+ ;;
715
+ history)
716
+ show_history
717
+ ;;
718
+ stats)
719
+ show_stats
720
+ ;;
721
+ help|--help|-h)
722
+ show_help
723
+ ;;
724
+ *)
725
+ error "Unknown command: $cmd"
726
+ echo ""
727
+ show_help
728
+ exit 1
729
+ ;;
730
+ esac
731
+ }
732
+
733
+ # Source guard: allow sourcing without executing
734
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
735
+ main "$@"
736
+ fi