shipwright-cli 1.7.1 → 1.9.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/.claude/agents/code-reviewer.md +90 -0
- package/.claude/agents/devops-engineer.md +142 -0
- package/.claude/agents/pipeline-agent.md +80 -0
- package/.claude/agents/shell-script-specialist.md +150 -0
- package/.claude/agents/test-specialist.md +196 -0
- package/.claude/hooks/post-tool-use.sh +38 -0
- package/.claude/hooks/pre-tool-use.sh +25 -0
- package/.claude/hooks/session-started.sh +37 -0
- package/README.md +212 -814
- package/claude-code/CLAUDE.md.shipwright +54 -0
- package/claude-code/hooks/notify-idle.sh +2 -2
- package/claude-code/hooks/session-start.sh +24 -0
- package/claude-code/hooks/task-completed.sh +6 -2
- package/claude-code/settings.json.template +12 -0
- package/dashboard/public/app.js +4422 -0
- package/dashboard/public/index.html +816 -0
- package/dashboard/public/styles.css +4755 -0
- package/dashboard/server.ts +4315 -0
- package/docs/KNOWN-ISSUES.md +18 -10
- package/docs/TIPS.md +38 -26
- package/docs/patterns/README.md +33 -23
- package/package.json +9 -5
- package/scripts/adapters/iterm2-adapter.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +52 -23
- package/scripts/adapters/wezterm-adapter.sh +26 -14
- package/scripts/lib/compat.sh +200 -0
- package/scripts/lib/helpers.sh +72 -0
- package/scripts/postinstall.mjs +72 -13
- package/scripts/{cct → sw} +109 -21
- package/scripts/sw-adversarial.sh +274 -0
- package/scripts/sw-architecture-enforcer.sh +330 -0
- package/scripts/sw-checkpoint.sh +390 -0
- package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
- package/scripts/sw-connect.sh +619 -0
- package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
- package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
- package/scripts/sw-dashboard.sh +477 -0
- package/scripts/sw-developer-simulation.sh +252 -0
- package/scripts/sw-docs.sh +635 -0
- package/scripts/sw-doctor.sh +907 -0
- package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
- package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
- package/scripts/sw-github-checks.sh +521 -0
- package/scripts/sw-github-deploy.sh +533 -0
- package/scripts/sw-github-graphql.sh +972 -0
- package/scripts/sw-heartbeat.sh +293 -0
- package/scripts/{cct-init.sh → sw-init.sh} +144 -11
- package/scripts/sw-intelligence.sh +1196 -0
- package/scripts/sw-jira.sh +643 -0
- package/scripts/sw-launchd.sh +364 -0
- package/scripts/sw-linear.sh +648 -0
- package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
- package/scripts/{cct-loop.sh → sw-loop.sh} +534 -44
- package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
- package/scripts/sw-patrol-meta.sh +417 -0
- package/scripts/sw-pipeline-composer.sh +455 -0
- package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
- package/scripts/sw-predictive.sh +820 -0
- package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
- package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
- package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
- package/scripts/sw-remote.sh +687 -0
- package/scripts/sw-self-optimize.sh +947 -0
- package/scripts/sw-session.sh +519 -0
- package/scripts/sw-setup.sh +234 -0
- package/scripts/sw-status.sh +605 -0
- package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
- package/scripts/sw-tmux.sh +591 -0
- package/scripts/sw-tracker-jira.sh +277 -0
- package/scripts/sw-tracker-linear.sh +292 -0
- package/scripts/sw-tracker.sh +409 -0
- package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
- package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
- package/templates/pipelines/autonomous.json +27 -5
- package/templates/pipelines/full.json +12 -0
- package/templates/pipelines/standard.json +12 -0
- package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
- package/tmux/templates/accessibility.json +34 -0
- package/tmux/templates/api-design.json +35 -0
- package/tmux/templates/architecture.json +1 -0
- package/tmux/templates/bug-fix.json +9 -0
- package/tmux/templates/code-review.json +1 -0
- package/tmux/templates/compliance.json +36 -0
- package/tmux/templates/data-pipeline.json +36 -0
- package/tmux/templates/debt-paydown.json +34 -0
- package/tmux/templates/devops.json +1 -0
- package/tmux/templates/documentation.json +1 -0
- package/tmux/templates/exploration.json +1 -0
- package/tmux/templates/feature-dev.json +1 -0
- package/tmux/templates/full-stack.json +8 -0
- package/tmux/templates/i18n.json +34 -0
- package/tmux/templates/incident-response.json +36 -0
- package/tmux/templates/migration.json +1 -0
- package/tmux/templates/observability.json +35 -0
- package/tmux/templates/onboarding.json +33 -0
- package/tmux/templates/performance.json +35 -0
- package/tmux/templates/refactor.json +1 -0
- package/tmux/templates/release.json +35 -0
- package/tmux/templates/security-audit.json +8 -0
- package/tmux/templates/spike.json +34 -0
- package/tmux/templates/testing.json +1 -0
- package/tmux/tmux.conf +98 -9
- package/scripts/cct-doctor.sh +0 -414
- package/scripts/cct-session.sh +0 -284
- package/scripts/cct-status.sh +0 -169
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright jira — Jira ↔ GitHub Bidirectional Sync ║
|
|
4
|
+
# ║ Sync issues · Update statuses · Link PRs · Pipeline integration ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="1.9.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
|
+
# ─── 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
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
34
|
+
now_epoch() { date +%s; }
|
|
35
|
+
|
|
36
|
+
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
37
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
38
|
+
|
|
39
|
+
emit_event() {
|
|
40
|
+
local event_type="$1"
|
|
41
|
+
shift
|
|
42
|
+
local json_fields=""
|
|
43
|
+
for kv in "$@"; do
|
|
44
|
+
local key="${kv%%=*}"
|
|
45
|
+
local val="${kv#*=}"
|
|
46
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
47
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
48
|
+
else
|
|
49
|
+
val="${val//\"/\\\"}"
|
|
50
|
+
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
51
|
+
fi
|
|
52
|
+
done
|
|
53
|
+
mkdir -p "${HOME}/.shipwright"
|
|
54
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ─── Configuration ─────────────────────────────────────────────────────────
|
|
58
|
+
CONFIG_DIR="${HOME}/.shipwright"
|
|
59
|
+
TRACKER_CONFIG="${CONFIG_DIR}/tracker-config.json"
|
|
60
|
+
|
|
61
|
+
JIRA_BASE_URL=""
|
|
62
|
+
JIRA_EMAIL=""
|
|
63
|
+
JIRA_API_TOKEN=""
|
|
64
|
+
JIRA_PROJECT_KEY=""
|
|
65
|
+
|
|
66
|
+
load_config() {
|
|
67
|
+
if [[ -f "$TRACKER_CONFIG" ]]; then
|
|
68
|
+
JIRA_BASE_URL="${JIRA_BASE_URL:-$(jq -r '.jira_base_url // empty' "$TRACKER_CONFIG" 2>/dev/null || true)}"
|
|
69
|
+
JIRA_EMAIL="${JIRA_EMAIL:-$(jq -r '.jira_email // empty' "$TRACKER_CONFIG" 2>/dev/null || true)}"
|
|
70
|
+
JIRA_API_TOKEN="${JIRA_API_TOKEN:-$(jq -r '.jira_api_token // empty' "$TRACKER_CONFIG" 2>/dev/null || true)}"
|
|
71
|
+
JIRA_PROJECT_KEY="${JIRA_PROJECT_KEY:-$(jq -r '.jira_project_key // empty' "$TRACKER_CONFIG" 2>/dev/null || true)}"
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
JIRA_BASE_URL="${JIRA_BASE_URL:-}"
|
|
75
|
+
JIRA_EMAIL="${JIRA_EMAIL:-}"
|
|
76
|
+
JIRA_API_TOKEN="${JIRA_API_TOKEN:-}"
|
|
77
|
+
JIRA_PROJECT_KEY="${JIRA_PROJECT_KEY:-}"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
check_config() {
|
|
81
|
+
if [[ -z "$JIRA_BASE_URL" ]] || [[ -z "$JIRA_EMAIL" ]] || [[ -z "$JIRA_API_TOKEN" ]]; then
|
|
82
|
+
error "Jira not configured"
|
|
83
|
+
echo ""
|
|
84
|
+
echo -e " Set via environment: ${DIM}export JIRA_BASE_URL=https://your-org.atlassian.net${RESET}"
|
|
85
|
+
echo -e " ${DIM}export JIRA_EMAIL=you@example.com${RESET}"
|
|
86
|
+
echo -e " ${DIM}export JIRA_API_TOKEN=your-token${RESET}"
|
|
87
|
+
echo -e " Or run: ${DIM}shipwright jira init${RESET}"
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# ─── Jira REST API Helper ─────────────────────────────────────────────────
|
|
93
|
+
# Executes a REST request against the Jira API.
|
|
94
|
+
# Uses Basic auth (email:token base64-encoded).
|
|
95
|
+
jira_api() {
|
|
96
|
+
local method="$1" endpoint="$2" data="${3:-}"
|
|
97
|
+
local auth
|
|
98
|
+
auth=$(printf '%s:%s' "$JIRA_EMAIL" "$JIRA_API_TOKEN" | base64)
|
|
99
|
+
local args=(-sf -X "$method" \
|
|
100
|
+
-H "Authorization: Basic $auth" \
|
|
101
|
+
-H "Content-Type: application/json")
|
|
102
|
+
[[ -n "$data" ]] && args+=(-d "$data")
|
|
103
|
+
curl "${args[@]}" "${JIRA_BASE_URL}/rest/api/3/${endpoint}" 2>&1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# ─── Helper: Add ADF Comment to Jira Issue ─────────────────────────────────
|
|
107
|
+
# Jira comments use Atlassian Document Format (ADF).
|
|
108
|
+
jira_add_comment() {
|
|
109
|
+
local issue_key="$1" body_text="$2"
|
|
110
|
+
local payload
|
|
111
|
+
payload=$(jq -n --arg text "$body_text" '{
|
|
112
|
+
body: {
|
|
113
|
+
type: "doc",
|
|
114
|
+
version: 1,
|
|
115
|
+
content: [{
|
|
116
|
+
type: "paragraph",
|
|
117
|
+
content: [{type: "text", text: $text}]
|
|
118
|
+
}]
|
|
119
|
+
}
|
|
120
|
+
}')
|
|
121
|
+
jira_api "POST" "issue/${issue_key}/comment" "$payload"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# ─── Helper: Transition Jira Issue ─────────────────────────────────────────
|
|
125
|
+
# Finds the transition ID by name and applies it.
|
|
126
|
+
jira_transition() {
|
|
127
|
+
local issue_key="$1" target_name="$2"
|
|
128
|
+
local transitions
|
|
129
|
+
transitions=$(jira_api "GET" "issue/${issue_key}/transitions") || return 1
|
|
130
|
+
local tid
|
|
131
|
+
tid=$(echo "$transitions" | jq -r --arg name "$target_name" \
|
|
132
|
+
'.transitions[] | select(.name == $name) | .id' 2>/dev/null || true)
|
|
133
|
+
if [[ -z "$tid" ]]; then
|
|
134
|
+
return 0 # transition not available — silently skip
|
|
135
|
+
fi
|
|
136
|
+
local payload
|
|
137
|
+
payload=$(jq -n --arg id "$tid" '{transition: {id: $id}}')
|
|
138
|
+
jira_api "POST" "issue/${issue_key}/transitions" "$payload"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# ─── Sync: Jira Todo → GitHub Issues ──────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
cmd_sync() {
|
|
144
|
+
check_config
|
|
145
|
+
info "Syncing Jira To Do issues → GitHub..."
|
|
146
|
+
|
|
147
|
+
local dry_run=false
|
|
148
|
+
while [[ $# -gt 0 ]]; do
|
|
149
|
+
case "$1" in
|
|
150
|
+
--dry-run) dry_run=true; shift ;;
|
|
151
|
+
*) shift ;;
|
|
152
|
+
esac
|
|
153
|
+
done
|
|
154
|
+
|
|
155
|
+
# Fetch Jira issues in "To Do" status within the project
|
|
156
|
+
local jql
|
|
157
|
+
jql=$(printf 'project = %s AND status = "To Do"' "$JIRA_PROJECT_KEY")
|
|
158
|
+
local encoded_jql
|
|
159
|
+
encoded_jql=$(printf '%s' "$jql" | jq -sRr @uri)
|
|
160
|
+
|
|
161
|
+
local response
|
|
162
|
+
response=$(jira_api "GET" "search?jql=${encoded_jql}&fields=summary,description,priority,status,labels") || {
|
|
163
|
+
error "Failed to fetch Jira issues"
|
|
164
|
+
return 1
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
local count
|
|
168
|
+
count=$(echo "$response" | jq '.issues | length')
|
|
169
|
+
if [[ "$count" -eq 0 ]]; then
|
|
170
|
+
info "No Jira issues in To Do status"
|
|
171
|
+
return 0
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
info "Found ${count} Jira issue(s) in To Do"
|
|
175
|
+
|
|
176
|
+
local synced=0
|
|
177
|
+
local skipped=0
|
|
178
|
+
|
|
179
|
+
# Process each Jira issue
|
|
180
|
+
local i=0
|
|
181
|
+
while [[ $i -lt $count ]]; do
|
|
182
|
+
local issue
|
|
183
|
+
issue=$(echo "$response" | jq ".issues[$i]")
|
|
184
|
+
local jira_key title description priority_name
|
|
185
|
+
jira_key=$(echo "$issue" | jq -r '.key')
|
|
186
|
+
title=$(echo "$issue" | jq -r '.fields.summary')
|
|
187
|
+
description=$(echo "$issue" | jq -r '.fields.description // ""')
|
|
188
|
+
priority_name=$(echo "$issue" | jq -r '.fields.priority.name // ""')
|
|
189
|
+
|
|
190
|
+
# Extract plain text from ADF description if present
|
|
191
|
+
if echo "$description" | jq -e '.type' >/dev/null 2>&1; then
|
|
192
|
+
description=$(echo "$description" | jq -r '
|
|
193
|
+
[.. | .text? // empty] | join(" ")
|
|
194
|
+
' 2>/dev/null || echo "")
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# Check if GitHub issue already exists for this Jira issue
|
|
198
|
+
local existing_gh
|
|
199
|
+
existing_gh=$(gh issue list --label "ready-to-build" --search "Jira: ${jira_key}" --json number --jq '.[0].number // empty' 2>/dev/null || true)
|
|
200
|
+
|
|
201
|
+
if [[ -n "$existing_gh" ]]; then
|
|
202
|
+
echo -e " ${DIM}Skip${RESET} ${jira_key}: ${title} ${DIM}(GitHub #${existing_gh})${RESET}"
|
|
203
|
+
skipped=$((skipped + 1))
|
|
204
|
+
i=$((i + 1))
|
|
205
|
+
continue
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Map priority to label
|
|
209
|
+
local priority_label=""
|
|
210
|
+
case "$priority_name" in
|
|
211
|
+
Highest|Blocker) priority_label="priority-urgent" ;;
|
|
212
|
+
High) priority_label="priority-high" ;;
|
|
213
|
+
Medium) priority_label="priority-medium" ;;
|
|
214
|
+
Low|Lowest) priority_label="priority-low" ;;
|
|
215
|
+
esac
|
|
216
|
+
|
|
217
|
+
# Build GitHub issue body with Jira back-link
|
|
218
|
+
local jira_url="${JIRA_BASE_URL}/browse/${jira_key}"
|
|
219
|
+
local gh_body
|
|
220
|
+
gh_body=$(printf "## %s\n\n%s\n\n---\n**Jira:** [%s](%s)\n**Jira Key:** %s" \
|
|
221
|
+
"$title" "$description" "$jira_key" "$jira_url" "$jira_key")
|
|
222
|
+
|
|
223
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
224
|
+
echo -e " ${CYAN}Would create${RESET} GitHub issue: ${title} ${DIM}(${jira_key})${RESET}"
|
|
225
|
+
synced=$((synced + 1))
|
|
226
|
+
else
|
|
227
|
+
# Create GitHub issue
|
|
228
|
+
local labels="ready-to-build"
|
|
229
|
+
if [[ -n "$priority_label" ]]; then
|
|
230
|
+
labels="${labels},${priority_label}"
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
local gh_num
|
|
234
|
+
gh_num=$(gh issue create --title "$title" --body "$gh_body" --label "$labels" --json number --jq '.number' 2>&1) || {
|
|
235
|
+
error "Failed to create GitHub issue for ${jira_key}: ${gh_num}"
|
|
236
|
+
i=$((i + 1))
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Add comment on Jira issue linking back to GitHub
|
|
241
|
+
local comment_body
|
|
242
|
+
comment_body=$(printf "Synced to GitHub issue #%s — the daemon will pick this up for autonomous delivery." "$gh_num")
|
|
243
|
+
jira_add_comment "$jira_key" "$comment_body" >/dev/null 2>&1 || true
|
|
244
|
+
|
|
245
|
+
# Move Jira issue to In Progress
|
|
246
|
+
jira_transition "$jira_key" "In Progress" >/dev/null 2>&1 || true
|
|
247
|
+
|
|
248
|
+
success "${jira_key} → GitHub #${gh_num}: ${title}"
|
|
249
|
+
emit_event "jira.sync" "jira_key=$jira_key" "github_issue=$gh_num" "title=$title"
|
|
250
|
+
synced=$((synced + 1))
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
i=$((i + 1))
|
|
254
|
+
done
|
|
255
|
+
|
|
256
|
+
echo ""
|
|
257
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
258
|
+
info "Dry run: ${synced} would be created, ${skipped} already synced"
|
|
259
|
+
else
|
|
260
|
+
success "Synced ${synced} issue(s), ${skipped} already linked"
|
|
261
|
+
fi
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# ─── Update: GitHub → Jira Status ─────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
cmd_update() {
|
|
267
|
+
check_config
|
|
268
|
+
|
|
269
|
+
if [[ $# -lt 2 ]]; then
|
|
270
|
+
error "Usage: shipwright jira update <github-issue-num> <status>"
|
|
271
|
+
echo ""
|
|
272
|
+
echo -e " Statuses: ${CYAN}started${RESET} | ${CYAN}review${RESET} | ${CYAN}done${RESET} | ${CYAN}failed${RESET}"
|
|
273
|
+
echo ""
|
|
274
|
+
echo -e " ${DIM}shipwright jira update 42 started${RESET} # → In Progress"
|
|
275
|
+
echo -e " ${DIM}shipwright jira update 42 review${RESET} # → In Review"
|
|
276
|
+
echo -e " ${DIM}shipwright jira update 42 done${RESET} # → Done"
|
|
277
|
+
echo -e " ${DIM}shipwright jira update 42 failed${RESET} # → adds failure comment"
|
|
278
|
+
exit 1
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
local gh_issue="$1"
|
|
282
|
+
local status="$2"
|
|
283
|
+
local detail="${3:-}"
|
|
284
|
+
|
|
285
|
+
# Find the Jira key from the GitHub issue body
|
|
286
|
+
local jira_key
|
|
287
|
+
jira_key=$(gh issue view "$gh_issue" --json body --jq '.body' 2>/dev/null | \
|
|
288
|
+
grep -o 'Jira Key:.*' | sed 's/.*\*\*Jira Key:\*\* //' | tr -d '[:space:]' || true)
|
|
289
|
+
|
|
290
|
+
if [[ -z "$jira_key" ]]; then
|
|
291
|
+
error "No Jira Key found in GitHub issue #${gh_issue}"
|
|
292
|
+
echo -e " ${DIM}The issue body must contain: **Jira Key:** PROJECT-123${RESET}"
|
|
293
|
+
return 1
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
# Map status to Jira transition
|
|
297
|
+
local target_name="" status_label=""
|
|
298
|
+
case "$status" in
|
|
299
|
+
started|in-progress|in_progress)
|
|
300
|
+
target_name="In Progress"
|
|
301
|
+
status_label="In Progress"
|
|
302
|
+
;;
|
|
303
|
+
review|in-review|in_review|pr)
|
|
304
|
+
target_name="In Review"
|
|
305
|
+
status_label="In Review"
|
|
306
|
+
;;
|
|
307
|
+
done|completed|merged)
|
|
308
|
+
target_name="Done"
|
|
309
|
+
status_label="Done"
|
|
310
|
+
;;
|
|
311
|
+
failed|error)
|
|
312
|
+
# Don't change status, just add a comment
|
|
313
|
+
local comment="Pipeline failed for GitHub issue #${gh_issue}"
|
|
314
|
+
if [[ -n "$detail" ]]; then
|
|
315
|
+
comment="${comment}\n\n${detail}"
|
|
316
|
+
fi
|
|
317
|
+
jira_add_comment "$jira_key" "$comment" >/dev/null 2>&1 || return 1
|
|
318
|
+
warn "Added failure comment to Jira issue ${jira_key}"
|
|
319
|
+
emit_event "jira.update" "github_issue=$gh_issue" "status=failed"
|
|
320
|
+
return 0
|
|
321
|
+
;;
|
|
322
|
+
*)
|
|
323
|
+
error "Unknown status: ${status}"
|
|
324
|
+
echo -e " Valid: ${CYAN}started${RESET} | ${CYAN}review${RESET} | ${CYAN}done${RESET} | ${CYAN}failed${RESET}"
|
|
325
|
+
return 1
|
|
326
|
+
;;
|
|
327
|
+
esac
|
|
328
|
+
|
|
329
|
+
jira_transition "$jira_key" "$target_name" >/dev/null 2>&1 || return 1
|
|
330
|
+
|
|
331
|
+
# Add status transition comment
|
|
332
|
+
local comment="Status updated to ${status_label} (GitHub #${gh_issue})"
|
|
333
|
+
if [[ -n "$detail" ]]; then
|
|
334
|
+
comment="${comment}\n\n${detail}"
|
|
335
|
+
fi
|
|
336
|
+
jira_add_comment "$jira_key" "$comment" >/dev/null 2>&1 || true
|
|
337
|
+
|
|
338
|
+
success "Jira ${jira_key} updated → ${status_label} (GitHub #${gh_issue})"
|
|
339
|
+
emit_event "jira.update" "github_issue=$gh_issue" "jira_key=$jira_key" "status=$status"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
# ─── Status Dashboard ──────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
cmd_status() {
|
|
345
|
+
check_config
|
|
346
|
+
|
|
347
|
+
echo -e "${PURPLE}${BOLD}━━━ Jira Board Status ━━━${RESET}"
|
|
348
|
+
echo ""
|
|
349
|
+
|
|
350
|
+
# Query issues by status
|
|
351
|
+
local statuses="To Do:To Do:YELLOW In Progress:In Progress:CYAN In Review:In Review:BLUE Done:Done:GREEN"
|
|
352
|
+
|
|
353
|
+
for entry in $statuses; do
|
|
354
|
+
local status_name="${entry%%:*}"
|
|
355
|
+
local rest="${entry#*:}"
|
|
356
|
+
local display_name="${rest%%:*}"
|
|
357
|
+
local color_name="${rest#*:}"
|
|
358
|
+
|
|
359
|
+
local color="$DIM"
|
|
360
|
+
case "$color_name" in
|
|
361
|
+
CYAN) color="$CYAN" ;;
|
|
362
|
+
BLUE) color="$BLUE" ;;
|
|
363
|
+
GREEN) color="$GREEN" ;;
|
|
364
|
+
YELLOW) color="$YELLOW" ;;
|
|
365
|
+
esac
|
|
366
|
+
|
|
367
|
+
# URL-encode the status name for JQL
|
|
368
|
+
local jql
|
|
369
|
+
jql=$(printf 'project = %s AND status = "%s"' "$JIRA_PROJECT_KEY" "$status_name")
|
|
370
|
+
local encoded_jql
|
|
371
|
+
encoded_jql=$(printf '%s' "$jql" | jq -sRr @uri)
|
|
372
|
+
|
|
373
|
+
local response
|
|
374
|
+
response=$(jira_api "GET" "search?jql=${encoded_jql}&fields=summary,status&maxResults=50" 2>/dev/null) || {
|
|
375
|
+
echo -e " ${RED}✗${RESET} ${display_name}: ${DIM}(API error)${RESET}"
|
|
376
|
+
continue
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
local count
|
|
380
|
+
count=$(echo "$response" | jq '.total // 0')
|
|
381
|
+
|
|
382
|
+
echo -e " ${color}${BOLD}${display_name}${RESET} ${count}"
|
|
383
|
+
|
|
384
|
+
# Show individual issues for active states
|
|
385
|
+
if [[ "$count" -gt 0 ]] && [[ "$status_name" != "Done" ]]; then
|
|
386
|
+
local issue_count
|
|
387
|
+
issue_count=$(echo "$response" | jq '.issues | length')
|
|
388
|
+
local j=0
|
|
389
|
+
while [[ $j -lt $issue_count ]]; do
|
|
390
|
+
local key title
|
|
391
|
+
key=$(echo "$response" | jq -r ".issues[$j].key")
|
|
392
|
+
title=$(echo "$response" | jq -r ".issues[$j].fields.summary")
|
|
393
|
+
echo -e " ${DIM}${key}${RESET} ${title}"
|
|
394
|
+
j=$((j + 1))
|
|
395
|
+
done
|
|
396
|
+
fi
|
|
397
|
+
done
|
|
398
|
+
|
|
399
|
+
echo ""
|
|
400
|
+
|
|
401
|
+
# Show recent sync events
|
|
402
|
+
if [[ -f "$EVENTS_FILE" ]]; then
|
|
403
|
+
local recent_syncs
|
|
404
|
+
recent_syncs=$(grep '"type":"jira\.' "$EVENTS_FILE" 2>/dev/null | tail -5 || true)
|
|
405
|
+
if [[ -n "$recent_syncs" ]]; then
|
|
406
|
+
echo -e "${BOLD}Recent Activity${RESET}"
|
|
407
|
+
echo "$recent_syncs" | while IFS= read -r line; do
|
|
408
|
+
local ts type
|
|
409
|
+
ts=$(echo "$line" | jq -r '.ts' 2>/dev/null || true)
|
|
410
|
+
type=$(echo "$line" | jq -r '.type' 2>/dev/null || true)
|
|
411
|
+
local short_ts="${ts:-unknown}"
|
|
412
|
+
echo -e " ${DIM}${short_ts}${RESET} ${type}"
|
|
413
|
+
done
|
|
414
|
+
echo ""
|
|
415
|
+
fi
|
|
416
|
+
fi
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
# ─── Init: Save Configuration ──────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
cmd_init() {
|
|
422
|
+
echo -e "${PURPLE}${BOLD}━━━ Jira Integration Setup ━━━${RESET}"
|
|
423
|
+
echo ""
|
|
424
|
+
|
|
425
|
+
mkdir -p "$CONFIG_DIR"
|
|
426
|
+
|
|
427
|
+
# Base URL
|
|
428
|
+
local base_url="${JIRA_BASE_URL:-}"
|
|
429
|
+
if [[ -z "$base_url" ]]; then
|
|
430
|
+
echo -e " ${CYAN}1.${RESET} Enter your Jira base URL (e.g. ${DIM}https://your-org.atlassian.net${RESET})"
|
|
431
|
+
echo ""
|
|
432
|
+
read -rp " Jira Base URL: " base_url
|
|
433
|
+
if [[ -z "$base_url" ]]; then
|
|
434
|
+
error "Base URL is required"
|
|
435
|
+
exit 1
|
|
436
|
+
fi
|
|
437
|
+
# Strip trailing slash
|
|
438
|
+
base_url="${base_url%/}"
|
|
439
|
+
fi
|
|
440
|
+
|
|
441
|
+
# Email
|
|
442
|
+
local email="${JIRA_EMAIL:-}"
|
|
443
|
+
if [[ -z "$email" ]]; then
|
|
444
|
+
echo ""
|
|
445
|
+
echo -e " ${CYAN}2.${RESET} Enter your Jira account email"
|
|
446
|
+
echo ""
|
|
447
|
+
read -rp " Email: " email
|
|
448
|
+
if [[ -z "$email" ]]; then
|
|
449
|
+
error "Email is required"
|
|
450
|
+
exit 1
|
|
451
|
+
fi
|
|
452
|
+
fi
|
|
453
|
+
|
|
454
|
+
# API Token
|
|
455
|
+
local api_token="${JIRA_API_TOKEN:-}"
|
|
456
|
+
if [[ -z "$api_token" ]]; then
|
|
457
|
+
echo ""
|
|
458
|
+
echo -e " ${CYAN}3.${RESET} Create an API token at ${DIM}https://id.atlassian.com/manage-profile/security/api-tokens${RESET}"
|
|
459
|
+
echo -e " Paste it below"
|
|
460
|
+
echo ""
|
|
461
|
+
read -rp " API Token: " api_token
|
|
462
|
+
if [[ -z "$api_token" ]]; then
|
|
463
|
+
error "API token is required"
|
|
464
|
+
exit 1
|
|
465
|
+
fi
|
|
466
|
+
fi
|
|
467
|
+
|
|
468
|
+
# Project Key
|
|
469
|
+
local project_key="${JIRA_PROJECT_KEY:-}"
|
|
470
|
+
if [[ -z "$project_key" ]]; then
|
|
471
|
+
echo ""
|
|
472
|
+
echo -e " ${CYAN}4.${RESET} Enter your Jira project key (e.g. ${DIM}PROJ${RESET})"
|
|
473
|
+
echo ""
|
|
474
|
+
read -rp " Project Key: " project_key
|
|
475
|
+
if [[ -z "$project_key" ]]; then
|
|
476
|
+
error "Project key is required"
|
|
477
|
+
exit 1
|
|
478
|
+
fi
|
|
479
|
+
fi
|
|
480
|
+
|
|
481
|
+
# Merge into existing tracker-config.json if present
|
|
482
|
+
local tmp_config="${TRACKER_CONFIG}.tmp"
|
|
483
|
+
local existing="{}"
|
|
484
|
+
if [[ -f "$TRACKER_CONFIG" ]]; then
|
|
485
|
+
existing=$(cat "$TRACKER_CONFIG" 2>/dev/null || echo "{}")
|
|
486
|
+
fi
|
|
487
|
+
|
|
488
|
+
echo "$existing" | jq \
|
|
489
|
+
--arg base_url "$base_url" \
|
|
490
|
+
--arg email "$email" \
|
|
491
|
+
--arg api_token "$api_token" \
|
|
492
|
+
--arg project_key "$project_key" \
|
|
493
|
+
--arg provider "jira" \
|
|
494
|
+
--arg updated_at "$(now_iso)" \
|
|
495
|
+
'. + {
|
|
496
|
+
provider: $provider,
|
|
497
|
+
jira_base_url: $base_url,
|
|
498
|
+
jira_email: $email,
|
|
499
|
+
jira_api_token: $api_token,
|
|
500
|
+
jira_project_key: $project_key,
|
|
501
|
+
jira_updated_at: $updated_at
|
|
502
|
+
}' > "$tmp_config"
|
|
503
|
+
mv "$tmp_config" "$TRACKER_CONFIG"
|
|
504
|
+
chmod 600 "$TRACKER_CONFIG"
|
|
505
|
+
|
|
506
|
+
success "Configuration saved to ${TRACKER_CONFIG}"
|
|
507
|
+
echo ""
|
|
508
|
+
|
|
509
|
+
# Validate connection
|
|
510
|
+
info "Validating Jira connection..."
|
|
511
|
+
JIRA_BASE_URL="$base_url"
|
|
512
|
+
JIRA_EMAIL="$email"
|
|
513
|
+
JIRA_API_TOKEN="$api_token"
|
|
514
|
+
JIRA_PROJECT_KEY="$project_key"
|
|
515
|
+
|
|
516
|
+
local test_response
|
|
517
|
+
test_response=$(jira_api "GET" "myself") || {
|
|
518
|
+
error "Jira connection failed — check your credentials"
|
|
519
|
+
exit 1
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
local display_name
|
|
523
|
+
display_name=$(echo "$test_response" | jq -r '.displayName // "Unknown"')
|
|
524
|
+
success "Authenticated as: ${display_name}"
|
|
525
|
+
|
|
526
|
+
emit_event "jira.init" "user=$display_name" "project=$project_key"
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# ─── Daemon Integration: Notify Jira on Pipeline Events ───────────────────
|
|
530
|
+
# Called by sw-daemon.sh at spawn, stage transition, completion, and failure.
|
|
531
|
+
# This function is designed to be sourced or called externally.
|
|
532
|
+
|
|
533
|
+
jira_notify() {
|
|
534
|
+
local event="$1"
|
|
535
|
+
local gh_issue="${2:-}"
|
|
536
|
+
local detail="${3:-}"
|
|
537
|
+
|
|
538
|
+
# Only proceed if Jira config exists and credentials are available
|
|
539
|
+
load_config
|
|
540
|
+
if [[ -z "$JIRA_BASE_URL" ]] || [[ -z "$JIRA_EMAIL" ]] || [[ -z "$JIRA_API_TOKEN" ]]; then
|
|
541
|
+
return 0 # silently skip if no Jira integration
|
|
542
|
+
fi
|
|
543
|
+
|
|
544
|
+
# Find the Jira key from GitHub issue
|
|
545
|
+
local jira_key=""
|
|
546
|
+
if [[ -n "$gh_issue" ]]; then
|
|
547
|
+
jira_key=$(gh issue view "$gh_issue" --json body --jq '.body' 2>/dev/null | \
|
|
548
|
+
grep -o 'Jira Key:.*' | sed 's/.*\*\*Jira Key:\*\* //' | tr -d '[:space:]' || true)
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
if [[ -z "$jira_key" ]]; then
|
|
552
|
+
return 0 # no linked Jira issue
|
|
553
|
+
fi
|
|
554
|
+
|
|
555
|
+
case "$event" in
|
|
556
|
+
spawn|started)
|
|
557
|
+
jira_transition "$jira_key" "In Progress" >/dev/null 2>&1 || true
|
|
558
|
+
jira_add_comment "$jira_key" "Pipeline started for GitHub issue #${gh_issue}" >/dev/null 2>&1 || true
|
|
559
|
+
;;
|
|
560
|
+
review|pr-created)
|
|
561
|
+
jira_transition "$jira_key" "In Review" >/dev/null 2>&1 || true
|
|
562
|
+
if [[ -n "$detail" ]]; then
|
|
563
|
+
jira_add_comment "$jira_key" "PR linked: ${detail} (GitHub #${gh_issue})" >/dev/null 2>&1 || true
|
|
564
|
+
fi
|
|
565
|
+
;;
|
|
566
|
+
completed|done)
|
|
567
|
+
jira_transition "$jira_key" "Done" >/dev/null 2>&1 || true
|
|
568
|
+
jira_add_comment "$jira_key" "Pipeline completed successfully for GitHub issue #${gh_issue}" >/dev/null 2>&1 || true
|
|
569
|
+
;;
|
|
570
|
+
failed)
|
|
571
|
+
local msg="Pipeline failed for GitHub issue #${gh_issue}"
|
|
572
|
+
if [[ -n "$detail" ]]; then
|
|
573
|
+
msg="${msg}\n\nDetails:\n${detail}"
|
|
574
|
+
fi
|
|
575
|
+
jira_add_comment "$jira_key" "$msg" >/dev/null 2>&1 || true
|
|
576
|
+
;;
|
|
577
|
+
esac
|
|
578
|
+
|
|
579
|
+
emit_event "jira.notify" "event=$event" "github_issue=$gh_issue" "jira_key=$jira_key"
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
# ─── Help ──────────────────────────────────────────────────────────────────
|
|
583
|
+
|
|
584
|
+
show_help() {
|
|
585
|
+
echo -e "${CYAN}${BOLD}shipwright jira${RESET} — Jira ↔ GitHub Bidirectional Sync"
|
|
586
|
+
echo ""
|
|
587
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
588
|
+
echo -e " ${CYAN}shipwright jira${RESET} <command> [options]"
|
|
589
|
+
echo ""
|
|
590
|
+
echo -e "${BOLD}COMMANDS${RESET}"
|
|
591
|
+
echo -e " ${CYAN}sync${RESET} [--dry-run] Sync Jira To Do issues → GitHub"
|
|
592
|
+
echo -e " ${CYAN}update${RESET} <issue> <status> Update linked Jira ticket status"
|
|
593
|
+
echo -e " ${CYAN}status${RESET} Show Jira board dashboard"
|
|
594
|
+
echo -e " ${CYAN}init${RESET} Configure Jira connection"
|
|
595
|
+
echo -e " ${CYAN}help${RESET} Show this help"
|
|
596
|
+
echo ""
|
|
597
|
+
echo -e "${BOLD}STATUS VALUES${RESET}"
|
|
598
|
+
echo -e " ${CYAN}started${RESET} Pipeline spawned → Jira: In Progress"
|
|
599
|
+
echo -e " ${CYAN}review${RESET} PR created → Jira: In Review"
|
|
600
|
+
echo -e " ${CYAN}done${RESET} Pipeline complete → Jira: Done"
|
|
601
|
+
echo -e " ${CYAN}failed${RESET} Pipeline failed → Jira: adds failure comment"
|
|
602
|
+
echo ""
|
|
603
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
604
|
+
echo -e " ${DIM}shipwright jira init${RESET} # Set up Jira connection"
|
|
605
|
+
echo -e " ${DIM}shipwright jira sync${RESET} # Sync To Do → GitHub"
|
|
606
|
+
echo -e " ${DIM}shipwright jira sync --dry-run${RESET} # Preview what would sync"
|
|
607
|
+
echo -e " ${DIM}shipwright jira update 42 started${RESET} # Mark as In Progress"
|
|
608
|
+
echo -e " ${DIM}shipwright jira update 42 review${RESET} # Mark as In Review"
|
|
609
|
+
echo -e " ${DIM}shipwright jira update 42 done${RESET} # Mark as Done"
|
|
610
|
+
echo -e " ${DIM}shipwright jira status${RESET} # Show board dashboard"
|
|
611
|
+
echo ""
|
|
612
|
+
echo -e "${BOLD}ENVIRONMENT${RESET}"
|
|
613
|
+
echo -e " ${DIM}JIRA_BASE_URL${RESET} Jira instance URL (or use 'jira init' to save)"
|
|
614
|
+
echo -e " ${DIM}JIRA_EMAIL${RESET} Account email for authentication"
|
|
615
|
+
echo -e " ${DIM}JIRA_API_TOKEN${RESET} API token from Atlassian account"
|
|
616
|
+
echo -e " ${DIM}JIRA_PROJECT_KEY${RESET} Jira project key (e.g. PROJ)"
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
# ─── Command Router ─────────────────────────────────────────────────────
|
|
620
|
+
|
|
621
|
+
main() {
|
|
622
|
+
load_config
|
|
623
|
+
|
|
624
|
+
local cmd="${1:-help}"
|
|
625
|
+
shift 2>/dev/null || true
|
|
626
|
+
|
|
627
|
+
case "$cmd" in
|
|
628
|
+
sync) cmd_sync "$@" ;;
|
|
629
|
+
update) cmd_update "$@" ;;
|
|
630
|
+
status) cmd_status "$@" ;;
|
|
631
|
+
init) cmd_init "$@" ;;
|
|
632
|
+
notify) jira_notify "$@" ;;
|
|
633
|
+
help|--help|-h) show_help ;;
|
|
634
|
+
*)
|
|
635
|
+
error "Unknown command: ${cmd}"
|
|
636
|
+
echo ""
|
|
637
|
+
show_help
|
|
638
|
+
exit 1
|
|
639
|
+
;;
|
|
640
|
+
esac
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
main "$@"
|