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.
- package/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +208 -1
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +432 -130
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +64 -3
- package/scripts/sw-memory.sh +1 -1
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +687 -24
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
|
@@ -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
|