specweave 0.16.5 → 0.17.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.
@@ -0,0 +1,353 @@
1
+ #!/bin/bash
2
+
3
+ # ============================================================================
4
+ # Post Living Docs Update Hook - Azure DevOps Sync
5
+ # ============================================================================
6
+ #
7
+ # Triggered after living docs are updated to sync with Azure DevOps.
8
+ # CRITICAL: External tool status ALWAYS wins in conflicts!
9
+ #
10
+ # Triggers:
11
+ # 1. After /specweave:done (increment completion)
12
+ # 2. After /specweave:sync-docs update
13
+ # 3. After manual spec edits
14
+ # 4. After webhook from ADO
15
+ #
16
+ # ============================================================================
17
+
18
+ set -e
19
+
20
+ # Configuration
21
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
22
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
23
+ LIVING_DOCS_DIR="$PROJECT_ROOT/.specweave/docs/internal/specs"
24
+ LOG_FILE="$PROJECT_ROOT/.specweave/logs/ado-sync.log"
25
+ DEBUG=${DEBUG:-0}
26
+
27
+ # Ensure log directory exists
28
+ mkdir -p "$(dirname "$LOG_FILE")"
29
+
30
+ # ============================================================================
31
+ # Logging
32
+ # ============================================================================
33
+
34
+ log() {
35
+ local level=$1
36
+ shift
37
+ local message="$@"
38
+ local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
39
+ echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
40
+ [ "$DEBUG" -eq 1 ] && echo "[$level] $message" >&2
41
+ }
42
+
43
+ log_info() {
44
+ log "INFO" "$@"
45
+ }
46
+
47
+ log_error() {
48
+ log "ERROR" "$@"
49
+ }
50
+
51
+ log_debug() {
52
+ [ "$DEBUG" -eq 1 ] && log "DEBUG" "$@"
53
+ }
54
+
55
+ # ============================================================================
56
+ # External Tool Detection
57
+ # ============================================================================
58
+
59
+ detect_external_tool() {
60
+ local spec_path=$1
61
+
62
+ # Check for external links in spec metadata
63
+ if grep -q "externalLinks:" "$spec_path"; then
64
+ if grep -q "ado:" "$spec_path"; then
65
+ echo "ado"
66
+ elif grep -q "jira:" "$spec_path"; then
67
+ echo "jira"
68
+ elif grep -q "github:" "$spec_path"; then
69
+ echo "github"
70
+ fi
71
+ fi
72
+ }
73
+
74
+ # ============================================================================
75
+ # Status Mapping
76
+ # ============================================================================
77
+
78
+ map_ado_status_to_local() {
79
+ local ado_status=$1
80
+
81
+ case "$ado_status" in
82
+ "New")
83
+ echo "draft"
84
+ ;;
85
+ "Active")
86
+ echo "in-progress"
87
+ ;;
88
+ "Resolved")
89
+ echo "implemented"
90
+ ;;
91
+ "Closed")
92
+ echo "complete"
93
+ ;;
94
+ "In Review"|"In QA")
95
+ echo "in-qa"
96
+ ;;
97
+ *)
98
+ echo "unknown"
99
+ ;;
100
+ esac
101
+ }
102
+
103
+ map_local_status_to_ado() {
104
+ local local_status=$1
105
+
106
+ case "$local_status" in
107
+ "draft")
108
+ echo "New"
109
+ ;;
110
+ "in-progress")
111
+ echo "Active"
112
+ ;;
113
+ "implemented")
114
+ echo "Resolved"
115
+ ;;
116
+ "complete")
117
+ echo "Closed"
118
+ ;;
119
+ "in-qa")
120
+ echo "In Review"
121
+ ;;
122
+ *)
123
+ echo "Active"
124
+ ;;
125
+ esac
126
+ }
127
+
128
+ # ============================================================================
129
+ # ADO API Functions
130
+ # ============================================================================
131
+
132
+ get_ado_work_item_status() {
133
+ local work_item_id=$1
134
+ local org="${AZURE_DEVOPS_ORG}"
135
+ local project="${AZURE_DEVOPS_PROJECT}"
136
+ local pat="${AZURE_DEVOPS_PAT}"
137
+
138
+ if [ -z "$org" ] || [ -z "$pat" ]; then
139
+ log_error "ADO credentials not configured"
140
+ return 1
141
+ fi
142
+
143
+ local api_url="https://dev.azure.com/${org}/${project}/_apis/wit/workitems/${work_item_id}?api-version=7.0"
144
+
145
+ log_debug "Fetching ADO work item $work_item_id status"
146
+
147
+ local response=$(curl -s -u ":${pat}" \
148
+ -H "Content-Type: application/json" \
149
+ "$api_url")
150
+
151
+ if [ $? -ne 0 ]; then
152
+ log_error "Failed to fetch ADO work item status"
153
+ return 1
154
+ fi
155
+
156
+ # Extract status from response
157
+ local status=$(echo "$response" | jq -r '.fields["System.State"]')
158
+
159
+ if [ "$status" = "null" ] || [ -z "$status" ]; then
160
+ log_error "Could not extract status from ADO response"
161
+ return 1
162
+ fi
163
+
164
+ echo "$status"
165
+ }
166
+
167
+ update_ado_work_item() {
168
+ local work_item_id=$1
169
+ local spec_content=$2
170
+ local org="${AZURE_DEVOPS_ORG}"
171
+ local project="${AZURE_DEVOPS_PROJECT}"
172
+ local pat="${AZURE_DEVOPS_PAT}"
173
+
174
+ if [ -z "$org" ] || [ -z "$pat" ]; then
175
+ log_error "ADO credentials not configured"
176
+ return 1
177
+ fi
178
+
179
+ # Extract current status from spec
180
+ local local_status=$(echo "$spec_content" | grep "^status:" | cut -d: -f2 | tr -d ' ')
181
+ local ado_status=$(map_local_status_to_ado "$local_status")
182
+
183
+ local api_url="https://dev.azure.com/${org}/${project}/_apis/wit/workitems/${work_item_id}?api-version=7.0"
184
+
185
+ # Create update payload
186
+ local payload=$(cat <<EOF
187
+ [
188
+ {
189
+ "op": "add",
190
+ "path": "/fields/System.State",
191
+ "value": "$ado_status"
192
+ },
193
+ {
194
+ "op": "add",
195
+ "path": "/fields/System.History",
196
+ "value": "Updated from SpecWeave living docs"
197
+ }
198
+ ]
199
+ EOF
200
+ )
201
+
202
+ log_debug "Updating ADO work item $work_item_id with status: $ado_status"
203
+
204
+ curl -s -X PATCH \
205
+ -u ":${pat}" \
206
+ -H "Content-Type: application/json-patch+json" \
207
+ -d "$payload" \
208
+ "$api_url" > /dev/null
209
+
210
+ if [ $? -ne 0 ]; then
211
+ log_error "Failed to update ADO work item"
212
+ return 1
213
+ fi
214
+
215
+ log_info "Updated ADO work item $work_item_id"
216
+ }
217
+
218
+ # ============================================================================
219
+ # Conflict Resolution - CRITICAL: External Wins!
220
+ # ============================================================================
221
+
222
+ resolve_status_conflict() {
223
+ local spec_path=$1
224
+ local local_status=$2
225
+ local external_status=$3
226
+
227
+ local mapped_external=$(map_ado_status_to_local "$external_status")
228
+
229
+ if [ "$local_status" != "$mapped_external" ]; then
230
+ log_info "Status conflict detected:"
231
+ log_info " Local: $local_status"
232
+ log_info " External: $external_status (mapped: $mapped_external)"
233
+ log_info " Resolution: EXTERNAL WINS - applying $mapped_external"
234
+
235
+ # Update local spec with external status
236
+ sed -i.bak "s/^status: .*/status: $mapped_external/" "$spec_path"
237
+
238
+ # Add sync metadata
239
+ local timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
240
+
241
+ # Check if syncedAt exists, update or add
242
+ if grep -q "syncedAt:" "$spec_path"; then
243
+ sed -i.bak "s/syncedAt: .*/syncedAt: \"$timestamp\"/" "$spec_path"
244
+ else
245
+ # Add after externalLinks section
246
+ sed -i.bak "/externalLinks:/a\\
247
+ syncedAt: \"$timestamp\"" "$spec_path"
248
+ fi
249
+
250
+ # Clean up backup files
251
+ rm -f "${spec_path}.bak"
252
+
253
+ log_info "Local spec updated with external status: $mapped_external"
254
+ return 0
255
+ else
256
+ log_debug "No status conflict - local and external match: $local_status"
257
+ return 0
258
+ fi
259
+ }
260
+
261
+ # ============================================================================
262
+ # Main Sync Function
263
+ # ============================================================================
264
+
265
+ sync_spec_with_ado() {
266
+ local spec_path=$1
267
+
268
+ if [ ! -f "$spec_path" ]; then
269
+ log_error "Spec file not found: $spec_path"
270
+ return 1
271
+ fi
272
+
273
+ local spec_name=$(basename "$spec_path")
274
+ log_info "Syncing spec: $spec_name"
275
+
276
+ # Read spec content
277
+ local spec_content=$(cat "$spec_path")
278
+
279
+ # Extract ADO work item ID from metadata
280
+ local work_item_id=$(echo "$spec_content" | grep -A5 "externalLinks:" | grep -A3 "ado:" | grep "featureId:" | cut -d: -f2 | tr -d ' ')
281
+
282
+ if [ -z "$work_item_id" ]; then
283
+ log_debug "No ADO work item linked to spec, skipping sync"
284
+ return 0
285
+ fi
286
+
287
+ log_info "Found ADO work item ID: $work_item_id"
288
+
289
+ # Step 1: Push updates to ADO (content changes)
290
+ update_ado_work_item "$work_item_id" "$spec_content"
291
+
292
+ # Step 2: CRITICAL - Pull status from ADO (external wins!)
293
+ local external_status=$(get_ado_work_item_status "$work_item_id")
294
+
295
+ if [ -z "$external_status" ]; then
296
+ log_error "Could not fetch ADO status"
297
+ return 1
298
+ fi
299
+
300
+ log_info "ADO status: $external_status"
301
+
302
+ # Step 3: Extract local status
303
+ local local_status=$(echo "$spec_content" | grep "^status:" | cut -d: -f2 | tr -d ' ')
304
+
305
+ log_info "Local status: $local_status"
306
+
307
+ # Step 4: Resolve conflicts - EXTERNAL WINS
308
+ resolve_status_conflict "$spec_path" "$local_status" "$external_status"
309
+
310
+ log_info "Sync completed for $spec_name"
311
+ }
312
+
313
+ # ============================================================================
314
+ # Entry Point
315
+ # ============================================================================
316
+
317
+ main() {
318
+ log_info "=== Post Living Docs Update Hook Started ==="
319
+
320
+ # Get the spec path from arguments or environment
321
+ local spec_path="${1:-$SPECWEAVE_UPDATED_SPEC}"
322
+
323
+ if [ -z "$spec_path" ]; then
324
+ log_error "No spec path provided"
325
+ exit 1
326
+ fi
327
+
328
+ # Detect external tool
329
+ local tool=$(detect_external_tool "$spec_path")
330
+
331
+ if [ "$tool" != "ado" ]; then
332
+ log_debug "Not an ADO-linked spec, skipping"
333
+ exit 0
334
+ fi
335
+
336
+ log_info "Detected ADO integration for spec"
337
+
338
+ # Perform sync
339
+ sync_spec_with_ado "$spec_path"
340
+
341
+ local exit_code=$?
342
+
343
+ if [ $exit_code -eq 0 ]; then
344
+ log_info "=== Sync completed successfully ==="
345
+ else
346
+ log_error "=== Sync failed with exit code: $exit_code ==="
347
+ fi
348
+
349
+ exit $exit_code
350
+ }
351
+
352
+ # Run main function
353
+ main "$@"