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,706 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright release — Release train automation ║
4
+ # ║ Bump versions, generate changelog, create tags and GitHub releases ║
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
+ # ─── Helpers ─────────────────────────────────────────────────────────────────
29
+
30
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
31
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
32
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
33
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
34
+
35
+ emit_event() {
36
+ local event_type="$1"; shift
37
+ local events_file="${HOME}/.shipwright/events.jsonl"
38
+ mkdir -p "$(dirname "$events_file")"
39
+ local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
40
+ while [[ $# -gt 0 ]]; do
41
+ local key="${1%%=*}" val="${1#*=}"
42
+ payload="${payload},\"${key}\":\"${val}\""
43
+ shift
44
+ done
45
+ payload="${payload}}"
46
+ echo "$payload" >> "$events_file"
47
+ }
48
+
49
+ # ─── Parse flags ───────────────────────────────────────────────────────────
50
+ DRY_RUN=false
51
+ VERSION_TYPE=""
52
+ FROM_TAG=""
53
+ TO_TAG="HEAD"
54
+
55
+ for arg in "$@"; do
56
+ case "$arg" in
57
+ --dry-run) DRY_RUN=true ;;
58
+ --major) VERSION_TYPE="major" ;;
59
+ --minor) VERSION_TYPE="minor" ;;
60
+ --patch) VERSION_TYPE="patch" ;;
61
+ --from) shift_next=true ;;
62
+ --from=*) FROM_TAG="${arg#--from=}" ;;
63
+ --to) shift_next=true ;;
64
+ --to=*) TO_TAG="${arg#--to=}" ;;
65
+ *)
66
+ if [[ "${shift_next:-false}" == "true" ]]; then
67
+ case "${prev_arg:-}" in
68
+ --from) FROM_TAG="$arg" ;;
69
+ --to) TO_TAG="$arg" ;;
70
+ esac
71
+ shift_next=false
72
+ fi
73
+ prev_arg="$arg"
74
+ ;;
75
+ esac
76
+ done
77
+
78
+ # ─── Git helpers ───────────────────────────────────────────────────────────
79
+
80
+ # Get latest tag
81
+ get_latest_tag() {
82
+ git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"
83
+ }
84
+
85
+ # Parse semantic version (e.g., "v1.2.3" -> major=1, minor=2, patch=3)
86
+ parse_version() {
87
+ local version="$1"
88
+ # Strip leading 'v' if present
89
+ version="${version#v}"
90
+
91
+ IFS='.' read -r major minor patch <<< "$version"
92
+
93
+ echo "$major|$minor|${patch:-0}"
94
+ }
95
+
96
+ # Compare semantic versions
97
+ # Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
98
+ compare_versions() {
99
+ local v1="$1" v2="$2"
100
+
101
+ IFS='|' read -r m1 mi1 p1 <<< "$(parse_version "$v1")"
102
+ IFS='|' read -r m2 mi2 p2 <<< "$(parse_version "$v2")"
103
+
104
+ if [[ $m1 -lt $m2 ]]; then echo -1; return; fi
105
+ if [[ $m1 -gt $m2 ]]; then echo 1; return; fi
106
+ if [[ $mi1 -lt $mi2 ]]; then echo -1; return; fi
107
+ if [[ $mi1 -gt $mi2 ]]; then echo 1; return; fi
108
+ if [[ $p1 -lt $p2 ]]; then echo -1; return; fi
109
+ if [[ $p1 -gt $p2 ]]; then echo 1; return; fi
110
+ echo 0
111
+ }
112
+
113
+ # Bump version
114
+ bump_version() {
115
+ local current="$1" bump_type="$2"
116
+
117
+ IFS='|' read -r major minor patch <<< "$(parse_version "$current")"
118
+
119
+ case "$bump_type" in
120
+ major)
121
+ major=$((major + 1))
122
+ minor=0
123
+ patch=0
124
+ ;;
125
+ minor)
126
+ minor=$((minor + 1))
127
+ patch=0
128
+ ;;
129
+ patch)
130
+ patch=$((patch + 1))
131
+ ;;
132
+ esac
133
+
134
+ echo "v${major}.${minor}.${patch}"
135
+ }
136
+
137
+ # Get conventional commit type from message
138
+ get_commit_type() {
139
+ local msg="$1"
140
+ # Extract type from "type: description" or "type(scope): description"
141
+ if [[ $msg =~ ^([a-z]+) ]]; then
142
+ echo "${BASH_REMATCH[1]}"
143
+ fi
144
+ }
145
+
146
+ # Check if commit contains BREAKING CHANGE
147
+ has_breaking_change() {
148
+ local commit_hash="$1"
149
+ git show "$commit_hash" | grep -q "BREAKING CHANGE:" && echo "yes" || echo ""
150
+ }
151
+
152
+ # ─── Changelog generation ─────────────────────────────────────────────────
153
+
154
+ # Get commits between two revisions
155
+ get_commits() {
156
+ local from="$1" to="$2"
157
+
158
+ if [[ "$from" == "v0.0.0" ]]; then
159
+ git log "$to" --oneline --format="%h|%s|%b" 2>/dev/null || true
160
+ else
161
+ git log "${from}..${to}" --oneline --format="%h|%s|%b" 2>/dev/null || true
162
+ fi
163
+ }
164
+
165
+ # Parse commits and categorize them (using parallel arrays for bash 3.2 compatibility)
166
+ categorize_commits() {
167
+ local from="$1" to="$2"
168
+
169
+ # Use parallel arrays instead of associative arrays
170
+ local breaking_hashes=() breaking_subjects=()
171
+ local features_hashes=() features_subjects=()
172
+ local fixes_hashes=() fixes_subjects=()
173
+ local chores_hashes=() chores_subjects=()
174
+ local docs_hashes=() docs_subjects=()
175
+
176
+ # Read commits from git log
177
+ while IFS='|' read -r hash subject body; do
178
+ [[ -z "$hash" ]] && continue
179
+
180
+ local type
181
+ type="$(get_commit_type "$subject")"
182
+
183
+ # Check for breaking change
184
+ local breaking_flag=""
185
+ if echo "$body" | grep -q "BREAKING CHANGE:"; then
186
+ breaking_flag="yes"
187
+ fi
188
+
189
+ # Categorize using parallel arrays
190
+ case "$type" in
191
+ feat)
192
+ if [[ "$breaking_flag" == "yes" ]]; then
193
+ breaking_hashes+=("$hash")
194
+ breaking_subjects+=("$subject")
195
+ else
196
+ features_hashes+=("$hash")
197
+ features_subjects+=("$subject")
198
+ fi
199
+ ;;
200
+ fix)
201
+ fixes_hashes+=("$hash")
202
+ fixes_subjects+=("$subject")
203
+ ;;
204
+ chore|ci|test)
205
+ chores_hashes+=("$hash")
206
+ chores_subjects+=("$subject")
207
+ ;;
208
+ docs)
209
+ docs_hashes+=("$hash")
210
+ docs_subjects+=("$subject")
211
+ ;;
212
+ esac
213
+ done < <(get_commits "$from" "$to")
214
+
215
+ # Output data (not used by default, kept for future use)
216
+ true
217
+ }
218
+
219
+ # Generate markdown changelog (bash 3.2 compatible - no associative arrays)
220
+ generate_changelog_md() {
221
+ local version="$1" from="$2" to="$3"
222
+
223
+ echo "## [$version] — $(date +%Y-%m-%d)"
224
+ echo ""
225
+
226
+ # Use grep filtering to categorize commits
227
+ local breaking_commits docs_commits features_commits fixes_commits
228
+
229
+ breaking_commits="$(get_commits "$from" "$to" | while IFS='|' read -r hash subject body; do
230
+ [[ -z "$hash" ]] && continue
231
+ type="$(get_commit_type "$subject")"
232
+ if [[ "$type" == "feat" ]] && echo "$body" | grep -q "BREAKING CHANGE:"; then
233
+ echo "$hash|$subject"
234
+ fi
235
+ done)"
236
+
237
+ features_commits="$(get_commits "$from" "$to" | while IFS='|' read -r hash subject body; do
238
+ [[ -z "$hash" ]] && continue
239
+ type="$(get_commit_type "$subject")"
240
+ if [[ "$type" == "feat" ]] && ! echo "$body" | grep -q "BREAKING CHANGE:"; then
241
+ echo "$hash|$subject"
242
+ fi
243
+ done)"
244
+
245
+ fixes_commits="$(get_commits "$from" "$to" | while IFS='|' read -r hash subject body; do
246
+ [[ -z "$hash" ]] && continue
247
+ type="$(get_commit_type "$subject")"
248
+ [[ "$type" == "fix" ]] && echo "$hash|$subject"
249
+ done)"
250
+
251
+ docs_commits="$(get_commits "$from" "$to" | while IFS='|' read -r hash subject body; do
252
+ [[ -z "$hash" ]] && continue
253
+ type="$(get_commit_type "$subject")"
254
+ [[ "$type" == "docs" ]] && echo "$hash|$subject"
255
+ done)"
256
+
257
+ # Breaking changes section
258
+ if [[ -n "$breaking_commits" ]]; then
259
+ echo "### Breaking Changes"
260
+ echo ""
261
+ echo "$breaking_commits" | while IFS='|' read -r hash subject; do
262
+ [[ -z "$hash" ]] && continue
263
+ echo "- $subject ([\`$hash\`](https://github.com/sethdford/shipwright/commit/$hash))"
264
+ done
265
+ echo ""
266
+ fi
267
+
268
+ # Features section
269
+ if [[ -n "$features_commits" ]]; then
270
+ echo "### Features"
271
+ echo ""
272
+ echo "$features_commits" | while IFS='|' read -r hash subject; do
273
+ [[ -z "$hash" ]] && continue
274
+ echo "- $subject ([\`$hash\`](https://github.com/sethdford/shipwright/commit/$hash))"
275
+ done
276
+ echo ""
277
+ fi
278
+
279
+ # Fixes section
280
+ if [[ -n "$fixes_commits" ]]; then
281
+ echo "### Bug Fixes"
282
+ echo ""
283
+ echo "$fixes_commits" | while IFS='|' read -r hash subject; do
284
+ [[ -z "$hash" ]] && continue
285
+ echo "- $subject ([\`$hash\`](https://github.com/sethdford/shipwright/commit/$hash))"
286
+ done
287
+ echo ""
288
+ fi
289
+
290
+ # Docs section
291
+ if [[ -n "$docs_commits" ]]; then
292
+ echo "### Documentation"
293
+ echo ""
294
+ echo "$docs_commits" | while IFS='|' read -r hash subject; do
295
+ [[ -z "$hash" ]] && continue
296
+ echo "- $subject ([\`$hash\`](https://github.com/sethdford/shipwright/commit/$hash))"
297
+ done
298
+ echo ""
299
+ fi
300
+ }
301
+
302
+ # ─── Version detection ─────────────────────────────────────────────────────
303
+
304
+ # Detect next version based on commit types
305
+ detect_next_version() {
306
+ local current="$1" from="$2" to="$3"
307
+
308
+ local has_breaking=false
309
+ local has_feature=false
310
+
311
+ while IFS='|' read -r hash subject body; do
312
+ [[ -z "$hash" ]] && continue
313
+
314
+ local type
315
+ type="$(get_commit_type "$subject")"
316
+
317
+ case "$type" in
318
+ feat)
319
+ has_feature=true
320
+ if echo "$body" | grep -q "BREAKING CHANGE:"; then
321
+ has_breaking=true
322
+ fi
323
+ ;;
324
+ esac
325
+ done < <(get_commits "$from" "$to")
326
+
327
+ if $has_breaking; then
328
+ bump_version "$current" "major"
329
+ elif $has_feature; then
330
+ bump_version "$current" "minor"
331
+ else
332
+ bump_version "$current" "patch"
333
+ fi
334
+ }
335
+
336
+ # ─── Update VERSION in scripts ─────────────────────────────────────────────
337
+
338
+ update_version_in_files() {
339
+ local new_version="$1"
340
+ local version_num="${new_version#v}" # Strip 'v' prefix
341
+
342
+ info "Updating VERSION variable in scripts..."
343
+
344
+ # Find all shell scripts with VERSION variable
345
+ while IFS= read -r file; do
346
+ if grep -q '^VERSION=' "$file"; then
347
+ # Use sed to update VERSION="x.y.z" pattern
348
+ # This is shell-safe: VERSION="1.11.0" → VERSION="1.12.0"
349
+ local tmp_file
350
+ tmp_file=$(mktemp)
351
+ sed 's/^VERSION="[^"]*"$/VERSION="'"$version_num"'"/' "$file" > "$tmp_file"
352
+ mv "$tmp_file" "$file"
353
+ success "Updated VERSION in $(basename "$file")"
354
+ fi
355
+ done < <(find "$REPO_DIR/scripts" -name "sw*.sh" -o -name "lib/*.sh" 2>/dev/null)
356
+ }
357
+
358
+ # ─── Command implementations ──────────────────────────────────────────────
359
+
360
+ cmd_prepare() {
361
+ local current_version
362
+ current_version="$(get_latest_tag)"
363
+
364
+ if [[ -z "$VERSION_TYPE" ]]; then
365
+ # Auto-detect from commits
366
+ info "Auto-detecting version bump from commits..."
367
+ local next_version
368
+ next_version="$(detect_next_version "$current_version" "$current_version" "HEAD")"
369
+ VERSION_TYPE="detected"
370
+ else
371
+ local next_version
372
+ next_version="$(bump_version "$current_version" "$VERSION_TYPE")"
373
+ fi
374
+
375
+ echo ""
376
+ info "Release Preparation"
377
+ echo ""
378
+ echo -e " Current version: ${CYAN}${current_version}${RESET}"
379
+ echo -e " Next version: ${CYAN}${next_version}${RESET}"
380
+ echo -e " Bump type: ${CYAN}${VERSION_TYPE}${RESET}"
381
+
382
+ if $DRY_RUN; then
383
+ echo -e " Mode: ${YELLOW}DRY RUN${RESET}"
384
+ fi
385
+
386
+ echo ""
387
+
388
+ if ! $DRY_RUN; then
389
+ echo -e "This will:"
390
+ echo -e " 1. Update VERSION variable in all scripts"
391
+ echo -e " 2. Generate changelog"
392
+ echo -e " 3. Create git tag: ${CYAN}${next_version}${RESET}"
393
+ echo ""
394
+
395
+ read -p "Continue? (y/n) " -n 1 -r
396
+ echo
397
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
398
+ warn "Aborted"
399
+ return 1
400
+ fi
401
+
402
+ update_version_in_files "$next_version"
403
+ fi
404
+
405
+ success "Preparation complete"
406
+ }
407
+
408
+ cmd_changelog() {
409
+ local from_tag="$FROM_TAG"
410
+ local to_tag="$TO_TAG"
411
+
412
+ if [[ -z "$from_tag" ]]; then
413
+ from_tag="$(get_latest_tag)"
414
+ fi
415
+
416
+ info "Generating changelog"
417
+ echo ""
418
+
419
+ echo -e "From: ${CYAN}${from_tag}${RESET}"
420
+ echo -e "To: ${CYAN}${to_tag}${RESET}"
421
+ echo ""
422
+
423
+ if $DRY_RUN; then
424
+ echo -e "${YELLOW}DRY RUN${RESET} — showing preview only"
425
+ echo ""
426
+ fi
427
+
428
+ local changelog
429
+ changelog="$(generate_changelog_md "$to_tag" "$from_tag" "$to_tag")"
430
+
431
+ echo "$changelog"
432
+
433
+ echo ""
434
+ success "Changelog generated"
435
+ }
436
+
437
+ cmd_tag() {
438
+ local tag_version="$1"
439
+
440
+ if [[ -z "$tag_version" ]]; then
441
+ error "Version required: shipwright release tag v1.2.3"
442
+ return 1
443
+ fi
444
+
445
+ info "Creating git tag: ${CYAN}${tag_version}${RESET}"
446
+ echo ""
447
+
448
+ # Validate format
449
+ if ! [[ $tag_version =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
450
+ error "Invalid version format. Expected: v1.2.3"
451
+ return 1
452
+ fi
453
+
454
+ if $DRY_RUN; then
455
+ echo -e "${YELLOW}DRY RUN${RESET} — tag would be created"
456
+ echo ""
457
+ success "Tag validation passed"
458
+ return
459
+ fi
460
+
461
+ # Create annotated tag with message
462
+ local tag_msg="Release $tag_version"
463
+ git tag -a "$tag_version" -m "$tag_msg"
464
+
465
+ success "Tag created: ${CYAN}${tag_version}${RESET}"
466
+
467
+ # Show next steps
468
+ echo ""
469
+ info "Push tag with: ${DIM}git push origin $tag_version${RESET}"
470
+ }
471
+
472
+ cmd_publish() {
473
+ local current_version
474
+ current_version="$(get_latest_tag)"
475
+
476
+ local next_version
477
+ next_version="$(detect_next_version "$current_version" "$current_version" "HEAD")"
478
+
479
+ info "Full Release: ${CYAN}${next_version}${RESET}"
480
+ echo ""
481
+
482
+ if $DRY_RUN; then
483
+ echo -e "${YELLOW}DRY RUN${RESET} — showing what would happen:"
484
+ echo ""
485
+ echo " 1. Update all VERSION variables to ${CYAN}${next_version#v}${RESET}"
486
+ echo " 2. Generate changelog"
487
+ echo " 3. Create git tag: ${CYAN}${next_version}${RESET}"
488
+ echo " 4. Push tag to origin"
489
+ echo " 5. Create GitHub release"
490
+ echo ""
491
+ success "Dry run complete — no changes made"
492
+ return
493
+ fi
494
+
495
+ read -p "Release ${next_version}? (y/n) " -n 1 -r
496
+ echo
497
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
498
+ warn "Aborted"
499
+ return 1
500
+ fi
501
+
502
+ echo ""
503
+
504
+ # Step 1: Update versions
505
+ info "Step 1/5: Updating VERSION variables..."
506
+ update_version_in_files "$next_version"
507
+ echo ""
508
+
509
+ # Step 2: Generate changelog
510
+ info "Step 2/5: Generating changelog..."
511
+ local changelog
512
+ changelog="$(generate_changelog_md "$next_version" "$current_version" "HEAD")"
513
+ echo "$changelog" > /tmp/release-changelog.md
514
+ success "Changelog generated"
515
+ echo ""
516
+
517
+ # Step 3: Create git tag
518
+ info "Step 3/5: Creating git tag..."
519
+ git tag -a "$next_version" -m "Release $next_version"
520
+ success "Tag created"
521
+ echo ""
522
+
523
+ # Step 4: Push tag
524
+ info "Step 4/5: Pushing tag to origin..."
525
+ if git push origin "$next_version"; then
526
+ success "Tag pushed"
527
+ else
528
+ warn "Failed to push tag (may already exist)"
529
+ fi
530
+ echo ""
531
+
532
+ # Step 5: Create GitHub release
533
+ info "Step 5/5: Creating GitHub release..."
534
+ if command -v gh &>/dev/null; then
535
+ if gh release create "$next_version" --title "$next_version" --notes-file /tmp/release-changelog.md; then
536
+ success "GitHub release created"
537
+ else
538
+ warn "Failed to create GitHub release (may already exist)"
539
+ fi
540
+ else
541
+ warn "gh CLI not installed — skipping GitHub release creation"
542
+ echo -e " Manual create: ${DIM}gh release create $next_version --title '$next_version' --notes-file /tmp/release-changelog.md${RESET}"
543
+ fi
544
+ echo ""
545
+
546
+ success "Release ${next_version} complete!"
547
+
548
+ emit_event "release" "version=$next_version" "type=publish"
549
+ }
550
+
551
+ cmd_status() {
552
+ local current_version
553
+ current_version="$(get_latest_tag)"
554
+
555
+ info "Release Status"
556
+ echo ""
557
+ echo -e " Current version: ${CYAN}${current_version}${RESET}"
558
+
559
+ # Check for unreleased commits
560
+ local unreleased_count
561
+ unreleased_count="$(git rev-list "${current_version}..HEAD" --count 2>/dev/null || echo 0)"
562
+
563
+ if [[ $unreleased_count -gt 0 ]]; then
564
+ echo -e " Unreleased commits: ${YELLOW}${unreleased_count}${RESET}"
565
+
566
+ # Predict next version
567
+ local next_version
568
+ next_version="$(detect_next_version "$current_version" "$current_version" "HEAD")"
569
+
570
+ echo -e " Predicted next: ${CYAN}${next_version}${RESET}"
571
+
572
+ echo ""
573
+ info "Recent commits:"
574
+ git log "${current_version}..HEAD" --oneline | head -5
575
+ else
576
+ echo -e " Unreleased commits: ${GREEN}0${RESET}"
577
+ echo ""
578
+ success "All commits have been released"
579
+ fi
580
+
581
+ echo ""
582
+ }
583
+
584
+ cmd_help() {
585
+ cat << 'EOF'
586
+ shipwright release — Release train automation
587
+
588
+ USAGE
589
+ shipwright release <command> [options]
590
+
591
+ COMMANDS
592
+ prepare [--major|--minor|--patch]
593
+ Prepare a release: bump version, generate changelog
594
+ Auto-detects version type from conventional commits if not specified
595
+
596
+ changelog [--from TAG --to HEAD]
597
+ Generate markdown changelog from conventional commits
598
+ Default: from last tag to HEAD
599
+
600
+ tag [version]
601
+ Create and push a git tag (e.g., v1.2.3)
602
+ Validates semantic version format
603
+
604
+ publish
605
+ Full automated release: prepare + changelog + tag + GitHub release
606
+ Updates all VERSION variables, commits, tags, and creates release
607
+
608
+ status
609
+ Show current version, unreleased commits, and predicted next version
610
+
611
+ help
612
+ Show this help message
613
+
614
+ OPTIONS
615
+ --dry-run
616
+ Show what would happen without making changes
617
+
618
+ --major
619
+ Release with major version bump (for breaking changes)
620
+
621
+ --minor
622
+ Release with minor version bump (for new features)
623
+
624
+ --patch
625
+ Release with patch version bump (for bug fixes)
626
+
627
+ --from TAG
628
+ Start changelog from specific tag (changelog only)
629
+
630
+ --to REV
631
+ End changelog at specific revision (default: HEAD)
632
+
633
+ EXAMPLES
634
+ # Check current release status
635
+ shipwright release status
636
+
637
+ # Auto-detect and prepare next release
638
+ shipwright release prepare
639
+
640
+ # Force a minor version bump
641
+ shipwright release prepare --minor
642
+
643
+ # Generate changelog from v1.0.0 to HEAD
644
+ shipwright release changelog --from v1.0.0
645
+
646
+ # Dry run of full release
647
+ shipwright release publish --dry-run
648
+
649
+ # Publish full release
650
+ shipwright release publish
651
+
652
+ CONVENTIONAL COMMITS
653
+
654
+ The release tool parses commits to auto-detect version bumps:
655
+
656
+ feat: New feature → minor version bump
657
+ fix: Bug fix → patch version bump
658
+ BREAKING CHANGE: in message → major version bump
659
+ chore, docs, ci: Other → no bump (patch only)
660
+
661
+ Example commit messages:
662
+ - "feat: add authentication system"
663
+ - "fix: prevent data loss in sync"
664
+ - "feat: new API endpoint\n\nBREAKING CHANGE: old endpoint removed"
665
+
666
+ EOF
667
+ }
668
+
669
+ # ─── Main router ──────────────────────────────────────────────────────────
670
+
671
+ main() {
672
+ local cmd="${1:-status}"
673
+ shift 2>/dev/null || true
674
+
675
+ case "$cmd" in
676
+ prepare)
677
+ cmd_prepare "$@"
678
+ ;;
679
+ changelog)
680
+ cmd_changelog "$@"
681
+ ;;
682
+ tag)
683
+ cmd_tag "$@"
684
+ ;;
685
+ publish)
686
+ cmd_publish "$@"
687
+ ;;
688
+ status)
689
+ cmd_status "$@"
690
+ ;;
691
+ help|--help|-h)
692
+ cmd_help
693
+ ;;
694
+ *)
695
+ error "Unknown command: $cmd"
696
+ echo ""
697
+ cmd_help
698
+ exit 1
699
+ ;;
700
+ esac
701
+ }
702
+
703
+ # Only run main if this script is executed directly (not sourced)
704
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
705
+ main "$@"
706
+ fi