shipwright-cli 1.10.0 → 2.1.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 +221 -55
- package/completions/_shipwright +264 -32
- package/completions/shipwright.bash +118 -26
- package/completions/shipwright.fish +80 -2
- package/dashboard/server.ts +208 -0
- 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/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
- package/docs/tmux-research/TMUX-AUDIT.md +925 -0
- package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
- package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
- package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
- package/package.json +4 -2
- package/scripts/lib/helpers.sh +7 -0
- package/scripts/sw +323 -2
- 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 +754 -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 +698 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +44 -3
- package/scripts/sw-daemon.sh +568 -138
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +1380 -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 +107 -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 +479 -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 +4 -4
- 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 +656 -0
- package/scripts/sw-init.sh +237 -24
- 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 +267 -21
- package/scripts/sw-memory.sh +18 -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 +764 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +947 -35
- package/scripts/sw-pm.sh +758 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +8 -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 +2248 -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 +263 -127
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +44 -2
- package/scripts/sw-strategic.sh +806 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +620 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +4 -4
- package/scripts/sw-testgen.sh +566 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux-role-color.sh +58 -0
- package/scripts/sw-tmux-status.sh +128 -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 +627 -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
- package/templates/pipelines/autonomous.json +2 -2
- package/tmux/shipwright-overlay.conf +35 -17
- package/tmux/tmux.conf +23 -21
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright tracker: GitHub Provider ║
|
|
4
|
+
# ║ Sourced by sw-tracker.sh — do not call directly ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
# This file is sourced by sw-tracker.sh.
|
|
7
|
+
# It defines provider_* functions used by the tracker router.
|
|
8
|
+
# Do NOT add set -euo pipefail or a main() function here.
|
|
9
|
+
|
|
10
|
+
# ─── Discovery & CRUD Interface ────────────────────────────────────────────
|
|
11
|
+
# All functions output normalized JSON (or plain text where specified).
|
|
12
|
+
# Input: normalized arguments (label, state, issue_id, etc.)
|
|
13
|
+
# Output: JSON matching common schema across all providers
|
|
14
|
+
|
|
15
|
+
# Discover issues from GitHub using gh CLI
|
|
16
|
+
# Input: label, state, limit
|
|
17
|
+
# Output: JSON array of {id, title, labels[], state, body}
|
|
18
|
+
provider_discover_issues() {
|
|
19
|
+
local label="$1"
|
|
20
|
+
local state="${2:-open}"
|
|
21
|
+
local limit="${3:-50}"
|
|
22
|
+
|
|
23
|
+
# Check $NO_GITHUB env var
|
|
24
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
25
|
+
|
|
26
|
+
local gh_args=()
|
|
27
|
+
gh_args+=(issue list)
|
|
28
|
+
gh_args+=(--state "$state")
|
|
29
|
+
gh_args+=(--limit "$limit")
|
|
30
|
+
|
|
31
|
+
if [[ -n "$label" ]]; then
|
|
32
|
+
gh_args+=(--label "$label")
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Fetch as JSON: number, title, labels, state
|
|
36
|
+
gh_args+=(--json number,title,labels,state)
|
|
37
|
+
|
|
38
|
+
local response
|
|
39
|
+
response=$(gh "${gh_args[@]}" 2>/dev/null) || {
|
|
40
|
+
echo "[]"
|
|
41
|
+
return 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Normalize to {id, title, labels[], state}
|
|
45
|
+
echo "$response" | jq '[.[] | {id: .number, title: .title, labels: [.labels[].name], state: .state}]' 2>/dev/null || echo "[]"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Fetch single issue details
|
|
49
|
+
# Input: issue_id (number or identifier like "123" or "OWNER/REPO#123")
|
|
50
|
+
# Output: JSON {id, title, body, labels[], state}
|
|
51
|
+
provider_get_issue() {
|
|
52
|
+
local issue_id="$1"
|
|
53
|
+
|
|
54
|
+
[[ -z "$issue_id" ]] && return 1
|
|
55
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
56
|
+
|
|
57
|
+
local response
|
|
58
|
+
response=$(gh issue view "$issue_id" --json number,title,body,labels,state 2>/dev/null) || {
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Normalize output
|
|
63
|
+
echo "$response" | jq '{id: .number, title: .title, body: .body, labels: [.labels[].name], state: .state}' 2>/dev/null || return 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Fetch issue body text only
|
|
67
|
+
# Input: issue_id
|
|
68
|
+
# Output: plain text body
|
|
69
|
+
provider_get_issue_body() {
|
|
70
|
+
local issue_id="$1"
|
|
71
|
+
|
|
72
|
+
[[ -z "$issue_id" ]] && return 1
|
|
73
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
74
|
+
|
|
75
|
+
gh issue view "$issue_id" --json body --jq '.body' 2>/dev/null || return 1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Add label to issue
|
|
79
|
+
# Input: issue_id, label
|
|
80
|
+
# Output: none (stdout on success, nothing on failure)
|
|
81
|
+
provider_add_label() {
|
|
82
|
+
local issue_id="$1"
|
|
83
|
+
local label="$2"
|
|
84
|
+
|
|
85
|
+
[[ -z "$issue_id" || -z "$label" ]] && return 1
|
|
86
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
87
|
+
|
|
88
|
+
gh issue edit "$issue_id" --add-label "$label" 2>/dev/null || return 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Remove label from issue
|
|
92
|
+
# Input: issue_id, label
|
|
93
|
+
# Output: none
|
|
94
|
+
provider_remove_label() {
|
|
95
|
+
local issue_id="$1"
|
|
96
|
+
local label="$2"
|
|
97
|
+
|
|
98
|
+
[[ -z "$issue_id" || -z "$label" ]] && return 1
|
|
99
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
100
|
+
|
|
101
|
+
gh issue edit "$issue_id" --remove-label "$label" 2>/dev/null || return 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Add comment to issue
|
|
105
|
+
# Input: issue_id, body
|
|
106
|
+
# Output: none
|
|
107
|
+
provider_comment() {
|
|
108
|
+
local issue_id="$1"
|
|
109
|
+
local body="$2"
|
|
110
|
+
|
|
111
|
+
[[ -z "$issue_id" || -z "$body" ]] && return 1
|
|
112
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
113
|
+
|
|
114
|
+
gh issue comment "$issue_id" --body "$body" 2>/dev/null || return 1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Close/resolve issue
|
|
118
|
+
# Input: issue_id
|
|
119
|
+
# Output: none
|
|
120
|
+
provider_close_issue() {
|
|
121
|
+
local issue_id="$1"
|
|
122
|
+
|
|
123
|
+
[[ -z "$issue_id" ]] && return 1
|
|
124
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
125
|
+
|
|
126
|
+
gh issue close "$issue_id" 2>/dev/null || return 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Create new issue
|
|
130
|
+
# Input: title, body, labels (comma-separated or space-separated)
|
|
131
|
+
# Output: JSON {id, title}
|
|
132
|
+
provider_create_issue() {
|
|
133
|
+
local title="$1"
|
|
134
|
+
local body="$2"
|
|
135
|
+
local labels="${3:-}"
|
|
136
|
+
|
|
137
|
+
[[ -z "$title" ]] && return 1
|
|
138
|
+
[[ "${NO_GITHUB:-}" == "1" ]] && return 0
|
|
139
|
+
|
|
140
|
+
local gh_args=(issue create)
|
|
141
|
+
gh_args+=(--title "$title")
|
|
142
|
+
|
|
143
|
+
if [[ -n "$body" ]]; then
|
|
144
|
+
gh_args+=(--body "$body")
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
if [[ -n "$labels" ]]; then
|
|
148
|
+
# Convert space-separated to gh format (--label multiple times)
|
|
149
|
+
# Handle both "label1,label2" and "label1 label2"
|
|
150
|
+
local label_list
|
|
151
|
+
label_list=$(echo "$labels" | tr ',' '\n' | tr ' ' '\n' | grep -v '^$' || true)
|
|
152
|
+
while IFS= read -r label; do
|
|
153
|
+
[[ -n "$label" ]] && gh_args+=(--label "$label")
|
|
154
|
+
done <<< "$label_list"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
local response
|
|
158
|
+
response=$(gh "${gh_args[@]}" 2>/dev/null) || {
|
|
159
|
+
return 1
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Extract issue number from response or return error
|
|
163
|
+
# GitHub response is typically: "Created issue <repo>#<number>"
|
|
164
|
+
local issue_num
|
|
165
|
+
issue_num=$(echo "$response" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || true)
|
|
166
|
+
|
|
167
|
+
if [[ -z "$issue_num" ]]; then
|
|
168
|
+
return 1
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
echo "{\"id\": $issue_num, \"title\": \"$title\"}"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# ─── Main Provider Entry Point (Notification) ──────────────────────────────
|
|
175
|
+
# Called by tracker_notify() in sw-tracker.sh
|
|
176
|
+
|
|
177
|
+
provider_notify() {
|
|
178
|
+
local event="$1"
|
|
179
|
+
local gh_issue="${2:-}"
|
|
180
|
+
local detail="${3:-}"
|
|
181
|
+
|
|
182
|
+
# GitHub is the native provider — no external sync needed
|
|
183
|
+
# This function exists for consistency with Linear/Jira but is minimal
|
|
184
|
+
# Real integration happens through pipeline stages calling provider_* functions
|
|
185
|
+
|
|
186
|
+
# For now, just log the event
|
|
187
|
+
emit_event "tracker.notify" "provider=github" "event=$event" "issue=$gh_issue"
|
|
188
|
+
}
|
|
@@ -137,6 +137,178 @@ jira_api() {
|
|
|
137
137
|
curl "${args[@]}" "${JIRA_BASE_URL}/rest/api/3/${endpoint}" 2>&1
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
# ─── Discovery & CRUD Interface ───────────────────────────────────────────
|
|
141
|
+
# Implements provider interface for daemon discovery and pipeline CRUD
|
|
142
|
+
|
|
143
|
+
provider_discover_issues() {
|
|
144
|
+
local label="$1"
|
|
145
|
+
local state="${2:-open}"
|
|
146
|
+
local limit="${3:-50}"
|
|
147
|
+
|
|
148
|
+
provider_load_config
|
|
149
|
+
|
|
150
|
+
# Build JQL query
|
|
151
|
+
local jql="project = \"${JIRA_PROJECT_KEY}\""
|
|
152
|
+
|
|
153
|
+
if [[ -n "$label" ]]; then
|
|
154
|
+
jql="${jql} AND labels = \"${label}\""
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Map state parameter to Jira status
|
|
158
|
+
case "$state" in
|
|
159
|
+
open)
|
|
160
|
+
jql="${jql} AND status IN (${JIRA_TRANSITION_IN_PROGRESS:-\"In Progress\"},${JIRA_TRANSITION_IN_REVIEW:-\"In Review\"})"
|
|
161
|
+
;;
|
|
162
|
+
closed)
|
|
163
|
+
jql="${jql} AND status = ${JIRA_TRANSITION_DONE:-\"Done\"}"
|
|
164
|
+
;;
|
|
165
|
+
*)
|
|
166
|
+
# Custom status provided
|
|
167
|
+
jql="${jql} AND status = \"${state}\""
|
|
168
|
+
;;
|
|
169
|
+
esac
|
|
170
|
+
|
|
171
|
+
jql="${jql} ORDER BY created DESC"
|
|
172
|
+
|
|
173
|
+
# Fetch issues
|
|
174
|
+
local response
|
|
175
|
+
response=$(jira_api "GET" "search?jql=$(printf '%s' "$jql" | jq -sRr @uri)&maxResults=${limit}&fields=key,summary,labels,status" 2>/dev/null) || {
|
|
176
|
+
echo "[]"
|
|
177
|
+
return 0
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Normalize to {id, title, labels[], state}
|
|
181
|
+
echo "$response" | jq '[.issues[]? | {id: .key, title: .fields.summary, labels: [.fields.labels[]?.name // empty], state: .fields.status.name}]' 2>/dev/null || echo "[]"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
provider_get_issue() {
|
|
185
|
+
local issue_id="$1"
|
|
186
|
+
|
|
187
|
+
[[ -z "$issue_id" ]] && return 1
|
|
188
|
+
|
|
189
|
+
provider_load_config
|
|
190
|
+
|
|
191
|
+
local response
|
|
192
|
+
response=$(jira_api "GET" "issue/${issue_id}?fields=key,summary,description,labels,status" 2>/dev/null) || {
|
|
193
|
+
return 1
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Normalize output
|
|
197
|
+
echo "$response" | jq '{id: .key, title: .fields.summary, body: .fields.description, labels: [.fields.labels[]?.name // empty], state: .fields.status.name}' 2>/dev/null || return 1
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
provider_get_issue_body() {
|
|
201
|
+
local issue_id="$1"
|
|
202
|
+
|
|
203
|
+
[[ -z "$issue_id" ]] && return 1
|
|
204
|
+
|
|
205
|
+
provider_load_config
|
|
206
|
+
|
|
207
|
+
local response
|
|
208
|
+
response=$(jira_api "GET" "issue/${issue_id}?fields=description" 2>/dev/null) || {
|
|
209
|
+
return 1
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
echo "$response" | jq -r '.fields.description // ""' 2>/dev/null || return 1
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
provider_add_label() {
|
|
216
|
+
local issue_id="$1"
|
|
217
|
+
local label="$2"
|
|
218
|
+
|
|
219
|
+
[[ -z "$issue_id" || -z "$label" ]] && return 1
|
|
220
|
+
|
|
221
|
+
provider_load_config
|
|
222
|
+
|
|
223
|
+
# Get current labels
|
|
224
|
+
local current_labels
|
|
225
|
+
current_labels=$(jira_api "GET" "issue/${issue_id}?fields=labels" 2>/dev/null | jq '.fields.labels[]?.name' 2>/dev/null || echo "[]")
|
|
226
|
+
|
|
227
|
+
# Add new label
|
|
228
|
+
local payload
|
|
229
|
+
payload=$(jq -n --arg label "$label" --argjson labels "$current_labels" '{fields: {labels: ($labels | map(select(. != $label)) + [$label])}}' 2>/dev/null)
|
|
230
|
+
|
|
231
|
+
jira_api "PUT" "issue/${issue_id}" "$payload" 2>/dev/null || return 1
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
provider_remove_label() {
|
|
235
|
+
local issue_id="$1"
|
|
236
|
+
local label="$2"
|
|
237
|
+
|
|
238
|
+
[[ -z "$issue_id" || -z "$label" ]] && return 1
|
|
239
|
+
|
|
240
|
+
provider_load_config
|
|
241
|
+
|
|
242
|
+
# Get current labels and remove the specified one
|
|
243
|
+
local payload
|
|
244
|
+
payload=$(jq -n --arg label "$label" '{fields: {labels: [{name: ""}]}}')
|
|
245
|
+
|
|
246
|
+
jira_api "PUT" "issue/${issue_id}" "$payload" 2>/dev/null || return 1
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
provider_comment() {
|
|
250
|
+
local issue_id="$1"
|
|
251
|
+
local body="$2"
|
|
252
|
+
|
|
253
|
+
[[ -z "$issue_id" || -z "$body" ]] && return 1
|
|
254
|
+
|
|
255
|
+
provider_load_config
|
|
256
|
+
jira_add_comment "$issue_id" "$body"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
provider_close_issue() {
|
|
260
|
+
local issue_id="$1"
|
|
261
|
+
|
|
262
|
+
[[ -z "$issue_id" ]] && return 1
|
|
263
|
+
|
|
264
|
+
provider_load_config
|
|
265
|
+
jira_transition "$issue_id" "$JIRA_TRANSITION_DONE"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
provider_create_issue() {
|
|
269
|
+
local title="$1"
|
|
270
|
+
local body="$2"
|
|
271
|
+
local labels="${3:-}"
|
|
272
|
+
|
|
273
|
+
[[ -z "$title" ]] && return 1
|
|
274
|
+
|
|
275
|
+
provider_load_config
|
|
276
|
+
|
|
277
|
+
# Build payload
|
|
278
|
+
local payload
|
|
279
|
+
payload=$(jq -n --arg summary "$title" --arg description "$body" --arg project "$JIRA_PROJECT_KEY" \
|
|
280
|
+
'{
|
|
281
|
+
fields: {
|
|
282
|
+
project: {key: $project},
|
|
283
|
+
summary: $summary,
|
|
284
|
+
description: $description,
|
|
285
|
+
issuetype: {name: "Task"}
|
|
286
|
+
}
|
|
287
|
+
}')
|
|
288
|
+
|
|
289
|
+
# Add labels if provided
|
|
290
|
+
if [[ -n "$labels" ]]; then
|
|
291
|
+
local label_array
|
|
292
|
+
label_array=$(echo "$labels" | jq -R 'split("[, ]"; "x") | map(select(length > 0))' 2>/dev/null || echo '[]')
|
|
293
|
+
payload=$(echo "$payload" | jq --argjson labels "$label_array" '.fields.labels = $labels' 2>/dev/null)
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
local response
|
|
297
|
+
response=$(jira_api "POST" "issue" "$payload" 2>/dev/null) || {
|
|
298
|
+
return 1
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# Extract issue key from response
|
|
302
|
+
local issue_key
|
|
303
|
+
issue_key=$(echo "$response" | jq -r '.key // empty' 2>/dev/null)
|
|
304
|
+
|
|
305
|
+
if [[ -z "$issue_key" ]]; then
|
|
306
|
+
return 1
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
echo "{\"id\": \"$issue_key\", \"title\": \"$title\"}"
|
|
310
|
+
}
|
|
311
|
+
|
|
140
312
|
# ─── Find Jira Issue Key from GitHub Issue Body ───────────────────────────
|
|
141
313
|
|
|
142
314
|
find_jira_key() {
|
|
@@ -216,6 +216,257 @@ linear_attach_pr() {
|
|
|
216
216
|
linear_add_comment "$issue_id" "$body"
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
# ─── Discovery & CRUD Interface ───────────────────────────────────────────
|
|
220
|
+
# Implements provider interface for daemon discovery and pipeline CRUD
|
|
221
|
+
|
|
222
|
+
provider_discover_issues() {
|
|
223
|
+
local label="$1"
|
|
224
|
+
local state="${2:-open}"
|
|
225
|
+
local limit="${3:-50}"
|
|
226
|
+
|
|
227
|
+
provider_load_config
|
|
228
|
+
|
|
229
|
+
# Build Linear query for issues
|
|
230
|
+
local query='query($teamId: String!, $first: Int, $filter: IssueFilter) {
|
|
231
|
+
team(id: $teamId) {
|
|
232
|
+
issues(first: $first, filter: $filter) {
|
|
233
|
+
nodes {
|
|
234
|
+
id identifier title labels {nodes {name}}
|
|
235
|
+
state {id name type}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}'
|
|
240
|
+
|
|
241
|
+
# Build filter for state
|
|
242
|
+
local state_filter=""
|
|
243
|
+
case "$state" in
|
|
244
|
+
open)
|
|
245
|
+
# Open = unstarted or started
|
|
246
|
+
state_filter='and: [or: [{state: {type: {eq: "unstarted"}}}, {state: {type: {eq: "started"}}}]]'
|
|
247
|
+
;;
|
|
248
|
+
closed)
|
|
249
|
+
state_filter='and: [{state: {type: {eq: "completed"}}}]'
|
|
250
|
+
;;
|
|
251
|
+
*)
|
|
252
|
+
# Custom state provided
|
|
253
|
+
state_filter="and: [{state: {type: {eq: \"${state}\"}}}]"
|
|
254
|
+
;;
|
|
255
|
+
esac
|
|
256
|
+
|
|
257
|
+
# Add label filter if provided
|
|
258
|
+
if [[ -n "$label" ]]; then
|
|
259
|
+
state_filter="${state_filter}, {labels: {some: {name: {eq: \"${label}\"}}}}"
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
local filter
|
|
263
|
+
filter="{${state_filter}}"
|
|
264
|
+
|
|
265
|
+
local vars
|
|
266
|
+
vars=$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg filter "$filter" --arg limit "$limit" \
|
|
267
|
+
"{teamId: \$teamId, first: (\$limit | tonumber), filter: $filter}" 2>/dev/null || \
|
|
268
|
+
jq -n --arg teamId "$LINEAR_TEAM_ID" --arg limit "$limit" \
|
|
269
|
+
'{teamId: $teamId, first: ($limit | tonumber)}')
|
|
270
|
+
|
|
271
|
+
local response
|
|
272
|
+
response=$(linear_graphql "$query" "$vars" 2>/dev/null) || {
|
|
273
|
+
echo "[]"
|
|
274
|
+
return 0
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# Normalize to {id, title, labels[], state}
|
|
278
|
+
echo "$response" | jq '[.data.team.issues.nodes[]? | {id: .id, title: .title, labels: [.labels.nodes[]?.name // empty], state: .state.name}]' 2>/dev/null || echo "[]"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
provider_get_issue() {
|
|
282
|
+
local issue_id="$1"
|
|
283
|
+
|
|
284
|
+
[[ -z "$issue_id" ]] && return 1
|
|
285
|
+
|
|
286
|
+
provider_load_config
|
|
287
|
+
|
|
288
|
+
local query='query($id: String!) {
|
|
289
|
+
issue(id: $id) {
|
|
290
|
+
id title description labels {nodes {name}}
|
|
291
|
+
state {id name}
|
|
292
|
+
}
|
|
293
|
+
}'
|
|
294
|
+
|
|
295
|
+
local vars
|
|
296
|
+
vars=$(jq -n --arg id "$issue_id" '{id: $id}')
|
|
297
|
+
|
|
298
|
+
local response
|
|
299
|
+
response=$(linear_graphql "$query" "$vars" 2>/dev/null) || {
|
|
300
|
+
return 1
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Normalize output
|
|
304
|
+
echo "$response" | jq '{id: .data.issue.id, title: .data.issue.title, body: .data.issue.description, labels: [.data.issue.labels.nodes[]?.name // empty], state: .data.issue.state.name}' 2>/dev/null || return 1
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
provider_get_issue_body() {
|
|
308
|
+
local issue_id="$1"
|
|
309
|
+
|
|
310
|
+
[[ -z "$issue_id" ]] && return 1
|
|
311
|
+
|
|
312
|
+
provider_load_config
|
|
313
|
+
|
|
314
|
+
local query='query($id: String!) {
|
|
315
|
+
issue(id: $id) {
|
|
316
|
+
description
|
|
317
|
+
}
|
|
318
|
+
}'
|
|
319
|
+
|
|
320
|
+
local vars
|
|
321
|
+
vars=$(jq -n --arg id "$issue_id" '{id: $id}')
|
|
322
|
+
|
|
323
|
+
local response
|
|
324
|
+
response=$(linear_graphql "$query" "$vars" 2>/dev/null) || {
|
|
325
|
+
return 1
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
echo "$response" | jq -r '.data.issue.description // ""' 2>/dev/null || return 1
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
provider_add_label() {
|
|
332
|
+
local issue_id="$1"
|
|
333
|
+
local label="$2"
|
|
334
|
+
|
|
335
|
+
[[ -z "$issue_id" || -z "$label" ]] && return 1
|
|
336
|
+
|
|
337
|
+
provider_load_config
|
|
338
|
+
|
|
339
|
+
# Linear label IDs are required — fetch them
|
|
340
|
+
local query='query {
|
|
341
|
+
labels(first: 100) {
|
|
342
|
+
nodes {id name}
|
|
343
|
+
}
|
|
344
|
+
}'
|
|
345
|
+
|
|
346
|
+
local labels_response
|
|
347
|
+
labels_response=$(linear_graphql "$query" "{}" 2>/dev/null) || return 1
|
|
348
|
+
|
|
349
|
+
local label_id
|
|
350
|
+
label_id=$(echo "$labels_response" | jq -r --arg name "$label" '.data.labels.nodes[] | select(.name == $name) | .id' 2>/dev/null || true)
|
|
351
|
+
|
|
352
|
+
if [[ -z "$label_id" ]]; then
|
|
353
|
+
# Label not found — skip
|
|
354
|
+
return 0
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
local update_query='mutation($issueId: String!, $labelIds: [String!]) {
|
|
358
|
+
issueLabelCreate(issueId: $issueId, labelIds: $labelIds) {
|
|
359
|
+
success
|
|
360
|
+
}
|
|
361
|
+
}'
|
|
362
|
+
|
|
363
|
+
local vars
|
|
364
|
+
vars=$(jq -n --arg issueId "$issue_id" --arg labelId "$label_id" \
|
|
365
|
+
'{issueId: $issueId, labelIds: [$labelId]}')
|
|
366
|
+
|
|
367
|
+
linear_graphql "$update_query" "$vars" >/dev/null 2>&1 || return 1
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
provider_remove_label() {
|
|
371
|
+
local issue_id="$1"
|
|
372
|
+
local label="$2"
|
|
373
|
+
|
|
374
|
+
[[ -z "$issue_id" || -z "$label" ]] && return 1
|
|
375
|
+
|
|
376
|
+
provider_load_config
|
|
377
|
+
|
|
378
|
+
# Linear requires label IDs
|
|
379
|
+
local query='query {
|
|
380
|
+
labels(first: 100) {
|
|
381
|
+
nodes {id name}
|
|
382
|
+
}
|
|
383
|
+
}'
|
|
384
|
+
|
|
385
|
+
local labels_response
|
|
386
|
+
labels_response=$(linear_graphql "$query" "{}" 2>/dev/null) || return 1
|
|
387
|
+
|
|
388
|
+
local label_id
|
|
389
|
+
label_id=$(echo "$labels_response" | jq -r --arg name "$label" '.data.labels.nodes[] | select(.name == $name) | .id' 2>/dev/null || true)
|
|
390
|
+
|
|
391
|
+
if [[ -z "$label_id" ]]; then
|
|
392
|
+
return 0
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
local update_query='mutation($issueId: String!, $labelIds: [String!]) {
|
|
396
|
+
issueLabelDelete(issueId: $issueId, labelIds: $labelIds) {
|
|
397
|
+
success
|
|
398
|
+
}
|
|
399
|
+
}'
|
|
400
|
+
|
|
401
|
+
local vars
|
|
402
|
+
vars=$(jq -n --arg issueId "$issue_id" --arg labelId "$label_id" \
|
|
403
|
+
'{issueId: $issueId, labelIds: [$labelId]}')
|
|
404
|
+
|
|
405
|
+
linear_graphql "$update_query" "$vars" >/dev/null 2>&1 || return 1
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
provider_comment() {
|
|
409
|
+
local issue_id="$1"
|
|
410
|
+
local body="$2"
|
|
411
|
+
|
|
412
|
+
[[ -z "$issue_id" || -z "$body" ]] && return 1
|
|
413
|
+
|
|
414
|
+
provider_load_config
|
|
415
|
+
linear_add_comment "$issue_id" "$body"
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
provider_close_issue() {
|
|
419
|
+
local issue_id="$1"
|
|
420
|
+
|
|
421
|
+
[[ -z "$issue_id" ]] && return 1
|
|
422
|
+
|
|
423
|
+
provider_load_config
|
|
424
|
+
linear_update_status "$issue_id" "$STATUS_DONE"
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
provider_create_issue() {
|
|
428
|
+
local title="$1"
|
|
429
|
+
local body="$2"
|
|
430
|
+
local labels="${3:-}"
|
|
431
|
+
|
|
432
|
+
[[ -z "$title" ]] && return 1
|
|
433
|
+
|
|
434
|
+
provider_load_config
|
|
435
|
+
|
|
436
|
+
local query='mutation($title: String!, $description: String, $teamId: String!) {
|
|
437
|
+
issueCreate(input: {title: $title, description: $description, teamId: $teamId}) {
|
|
438
|
+
issue {id}
|
|
439
|
+
}
|
|
440
|
+
}'
|
|
441
|
+
|
|
442
|
+
local vars
|
|
443
|
+
vars=$(jq -n --arg title "$title" --arg description "$body" --arg teamId "$LINEAR_TEAM_ID" \
|
|
444
|
+
'{title: $title, description: $description, teamId: $teamId}')
|
|
445
|
+
|
|
446
|
+
local response
|
|
447
|
+
response=$(linear_graphql "$query" "$vars" 2>/dev/null) || {
|
|
448
|
+
return 1
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
local issue_id
|
|
452
|
+
issue_id=$(echo "$response" | jq -r '.data.issueCreate.issue.id // empty' 2>/dev/null)
|
|
453
|
+
|
|
454
|
+
if [[ -z "$issue_id" ]]; then
|
|
455
|
+
return 1
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
# Add labels if provided
|
|
459
|
+
if [[ -n "$labels" ]]; then
|
|
460
|
+
local label_list
|
|
461
|
+
label_list=$(echo "$labels" | tr ',' '\n' | tr ' ' '\n' | grep -v '^$' || true)
|
|
462
|
+
while IFS= read -r lbl; do
|
|
463
|
+
[[ -n "$lbl" ]] && provider_add_label "$issue_id" "$lbl" || true
|
|
464
|
+
done <<< "$label_list"
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
echo "{\"id\": \"$issue_id\", \"title\": \"$title\"}"
|
|
468
|
+
}
|
|
469
|
+
|
|
219
470
|
# ─── Find Linear Issue ID from GitHub Issue Body ──────────────────────────
|
|
220
471
|
|
|
221
472
|
find_linear_id() {
|