356 lines
11 KiB
Bash
Executable File
356 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
# sync-sprint-status.sh
|
|
# Automated sync of sprint-status.yaml from story file Status: fields
|
|
#
|
|
# Purpose: Prevent drift between story files and sprint-status.yaml
|
|
# Usage:
|
|
# ./scripts/sync-sprint-status.sh # Update sprint-status.yaml
|
|
# ./scripts/sync-sprint-status.sh --dry-run # Preview changes only
|
|
# ./scripts/sync-sprint-status.sh --validate # Check for discrepancies
|
|
#
|
|
# Created: 2026-01-02
|
|
# Part of: Full Workflow Fix (Option C)
|
|
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
STORY_DIR="docs/sprint-artifacts"
|
|
SPRINT_STATUS_FILE="docs/sprint-artifacts/sprint-status.yaml"
|
|
BACKUP_DIR=".sprint-status-backups"
|
|
DRY_RUN=false
|
|
VALIDATE_ONLY=false
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Parse arguments
|
|
for arg in "$@"; do
|
|
case $arg in
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
--validate)
|
|
VALIDATE_ONLY=true
|
|
shift
|
|
;;
|
|
--help)
|
|
echo "Usage: $0 [--dry-run] [--validate] [--help]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --dry-run Preview changes without modifying sprint-status.yaml"
|
|
echo " --validate Check for discrepancies and report (no changes)"
|
|
echo " --help Show this help message"
|
|
exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
echo -e "${BLUE}========================================${NC}"
|
|
echo -e "${BLUE}Sprint Status Sync Tool${NC}"
|
|
echo -e "${BLUE}========================================${NC}"
|
|
echo ""
|
|
|
|
# Check prerequisites
|
|
if [ ! -d "$STORY_DIR" ]; then
|
|
echo -e "${RED}ERROR: Story directory not found: $STORY_DIR${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -f "$SPRINT_STATUS_FILE" ]; then
|
|
echo -e "${RED}ERROR: Sprint status file not found: $SPRINT_STATUS_FILE${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# Create backup
|
|
if [ "$DRY_RUN" = false ] && [ "$VALIDATE_ONLY" = false ]; then
|
|
mkdir -p "$BACKUP_DIR"
|
|
BACKUP_FILE="$BACKUP_DIR/sprint-status-$(date +%Y%m%d-%H%M%S).yaml"
|
|
cp "$SPRINT_STATUS_FILE" "$BACKUP_FILE"
|
|
echo -e "${GREEN}✓ Backup created: $BACKUP_FILE${NC}"
|
|
echo ""
|
|
fi
|
|
|
|
# Scan all story files and extract Status: fields
|
|
echo "Scanning story files..."
|
|
TEMP_STATUS_FILE=$(mktemp)
|
|
DISCREPANCIES=0
|
|
UPDATES=0
|
|
|
|
# Use Python for robust parsing
|
|
python3 << 'PYTHON_SCRIPT' > "$TEMP_STATUS_FILE"
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from collections import defaultdict
|
|
|
|
story_dir = Path("docs/sprint-artifacts")
|
|
story_files = list(story_dir.glob("*.md"))
|
|
|
|
# Status mappings for normalization
|
|
STATUS_MAPPINGS = {
|
|
'done': 'done',
|
|
'complete': 'done',
|
|
'completed': 'done',
|
|
'in-progress': 'in-progress',
|
|
'in_progress': 'in-progress',
|
|
'review': 'review',
|
|
'ready-for-dev': 'ready-for-dev',
|
|
'ready_for_dev': 'ready-for-dev',
|
|
'pending': 'ready-for-dev',
|
|
'drafted': 'ready-for-dev',
|
|
'backlog': 'backlog',
|
|
'blocked': 'blocked',
|
|
'deferred': 'deferred',
|
|
'archived': 'archived',
|
|
}
|
|
|
|
story_statuses = {}
|
|
|
|
for story_file in story_files:
|
|
story_id = story_file.stem
|
|
|
|
# Skip special files
|
|
if (story_id.startswith('.') or
|
|
story_id.startswith('EPIC-') or
|
|
'COMPLETION' in story_id.upper() or
|
|
'SUMMARY' in story_id.upper() or
|
|
'REPORT' in story_id.upper() or
|
|
'README' in story_id.upper() or
|
|
'INDEX' in story_id.upper()):
|
|
continue
|
|
|
|
try:
|
|
content = story_file.read_text()
|
|
|
|
# Extract Status field
|
|
status_match = re.search(r'^Status:\s*(.+?)$', content, re.MULTILINE | re.IGNORECASE)
|
|
|
|
if status_match:
|
|
status = status_match.group(1).strip()
|
|
# Remove comments
|
|
status = re.sub(r'\s*#.*$', '', status).strip().lower()
|
|
|
|
# Normalize status
|
|
if status in STATUS_MAPPINGS:
|
|
normalized_status = STATUS_MAPPINGS[status]
|
|
elif 'done' in status or 'complete' in status:
|
|
normalized_status = 'done'
|
|
elif 'progress' in status:
|
|
normalized_status = 'in-progress'
|
|
elif 'review' in status:
|
|
normalized_status = 'review'
|
|
elif 'ready' in status:
|
|
normalized_status = 'ready-for-dev'
|
|
elif 'block' in status:
|
|
normalized_status = 'blocked'
|
|
elif 'defer' in status:
|
|
normalized_status = 'deferred'
|
|
elif 'archive' in status:
|
|
normalized_status = 'archived'
|
|
else:
|
|
normalized_status = 'ready-for-dev' # Default for unknown
|
|
|
|
story_statuses[story_id] = normalized_status
|
|
else:
|
|
# No Status: field found - mark as ready-for-dev if file exists
|
|
story_statuses[story_id] = 'ready-for-dev'
|
|
|
|
except Exception as e:
|
|
print(f"# ERROR parsing {story_id}: {e}", file=sys.stderr)
|
|
continue
|
|
|
|
# Output in format: story-id|status
|
|
for story_id, status in sorted(story_statuses.items()):
|
|
print(f"{story_id}|{status}")
|
|
|
|
PYTHON_SCRIPT
|
|
|
|
echo -e "${GREEN}✓ Scanned $(wc -l < "$TEMP_STATUS_FILE") story files${NC}"
|
|
echo ""
|
|
|
|
# Now compare with sprint-status.yaml and generate updates
|
|
echo "Comparing with sprint-status.yaml..."
|
|
echo ""
|
|
|
|
# Parse current sprint-status.yaml to find discrepancies
|
|
python3 << PYTHON_SCRIPT2
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Load scanned statuses
|
|
scanned_statuses = {}
|
|
with open("$TEMP_STATUS_FILE", "r") as f:
|
|
for line in f:
|
|
if '|' in line:
|
|
story_id, status = line.strip().split('|', 1)
|
|
scanned_statuses[story_id] = status
|
|
|
|
# Load current sprint-status.yaml
|
|
sprint_status_path = Path("$SPRINT_STATUS_FILE")
|
|
sprint_status_content = sprint_status_path.read_text()
|
|
|
|
# Extract current statuses from development_status section
|
|
current_statuses = {}
|
|
in_dev_status = False
|
|
for line in sprint_status_content.split('\n'):
|
|
if line.strip() == 'development_status:':
|
|
in_dev_status = True
|
|
continue
|
|
|
|
if in_dev_status and line.startswith(' ') and not line.strip().startswith('#'):
|
|
match = re.match(r' ([a-z0-9-]+):\s*(\S+)', line)
|
|
if match:
|
|
key, status = match.groups()
|
|
# Normalize status by removing comments
|
|
status = status.split('#')[0].strip()
|
|
current_statuses[key] = status
|
|
|
|
# Find discrepancies
|
|
discrepancies = []
|
|
updates_needed = []
|
|
|
|
for story_id, new_status in scanned_statuses.items():
|
|
current_status = current_statuses.get(story_id, 'NOT-IN-FILE')
|
|
|
|
if current_status == 'NOT-IN-FILE':
|
|
discrepancies.append((story_id, 'NOT-IN-FILE', new_status, 'ADD'))
|
|
updates_needed.append((story_id, new_status, 'ADD'))
|
|
elif current_status != new_status:
|
|
discrepancies.append((story_id, current_status, new_status, 'UPDATE'))
|
|
updates_needed.append((story_id, new_status, 'UPDATE'))
|
|
|
|
# Report discrepancies
|
|
if discrepancies:
|
|
print(f"${YELLOW}⚠ Found {len(discrepancies)} discrepancies:${NC}", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
|
|
for story_id, old_status, new_status, action in discrepancies[:20]: # Show first 20
|
|
if action == 'ADD':
|
|
print(f" ${YELLOW}[ADD]${NC} {story_id}: (not in file) → {new_status}", file=sys.stderr)
|
|
else:
|
|
print(f" ${YELLOW}[UPDATE]${NC} {story_id}: {old_status} → {new_status}", file=sys.stderr)
|
|
|
|
if len(discrepancies) > 20:
|
|
print(f" ... and {len(discrepancies) - 20} more", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
else:
|
|
print(f"${GREEN}✓ No discrepancies found - sprint-status.yaml is up to date!${NC}", file=sys.stderr)
|
|
|
|
# Output counts
|
|
print(f"DISCREPANCIES={len(discrepancies)}")
|
|
print(f"UPDATES={len(updates_needed)}")
|
|
|
|
# If not dry-run or validate-only, output update commands
|
|
if "$DRY_RUN" == "false" and "$VALIDATE_ONLY" == "false":
|
|
# Output updates in format for sed processing
|
|
for story_id, new_status, action in updates_needed:
|
|
if action == 'UPDATE':
|
|
print(f"UPDATE|{story_id}|{new_status}")
|
|
elif action == 'ADD':
|
|
print(f"ADD|{story_id}|{new_status}")
|
|
|
|
PYTHON_SCRIPT2
|
|
|
|
# Read the Python output
|
|
PYTHON_OUTPUT=$(python3 << 'PYTHON_SCRIPT3'
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Load scanned statuses
|
|
scanned_statuses = {}
|
|
with open("$TEMP_STATUS_FILE", "r") as f:
|
|
for line in f:
|
|
if '|' in line:
|
|
story_id, status = line.strip().split('|', 1)
|
|
scanned_statuses[story_id] = status
|
|
|
|
# Load current sprint-status.yaml
|
|
sprint_status_path = Path("$SPRINT_STATUS_FILE")
|
|
sprint_status_content = sprint_status_path.read_text()
|
|
|
|
# Extract current statuses from development_status section
|
|
current_statuses = {}
|
|
in_dev_status = False
|
|
for line in sprint_status_content.split('\n'):
|
|
if line.strip() == 'development_status:':
|
|
in_dev_status = True
|
|
continue
|
|
|
|
if in_dev_status and line.startswith(' ') and not line.strip().startswith('#'):
|
|
match = re.match(r' ([a-z0-9-]+):\s*(\S+)', line)
|
|
if match:
|
|
key, status = match.groups()
|
|
status = status.split('#')[0].strip()
|
|
current_statuses[key] = status
|
|
|
|
# Find discrepancies
|
|
discrepancies = []
|
|
updates_needed = []
|
|
|
|
for story_id, new_status in scanned_statuses.items():
|
|
current_status = current_statuses.get(story_id, 'NOT-IN-FILE')
|
|
|
|
if current_status == 'NOT-IN-FILE':
|
|
discrepancies.append((story_id, 'NOT-IN-FILE', new_status, 'ADD'))
|
|
updates_needed.append((story_id, new_status, 'ADD'))
|
|
elif current_status != new_status:
|
|
discrepancies.append((story_id, current_status, new_status, 'UPDATE'))
|
|
updates_needed.append((story_id, new_status, 'UPDATE'))
|
|
|
|
# Output counts
|
|
print(f"DISCREPANCIES={len(discrepancies)}")
|
|
print(f"UPDATES={len(updates_needed)}")
|
|
PYTHON_SCRIPT3
|
|
)
|
|
|
|
# Extract counts from Python output
|
|
DISCREPANCIES=$(echo "$PYTHON_OUTPUT" | grep "DISCREPANCIES=" | cut -d= -f2)
|
|
UPDATES=$(echo "$PYTHON_OUTPUT" | grep "UPDATES=" | cut -d= -f2)
|
|
|
|
# Cleanup temp file
|
|
rm -f "$TEMP_STATUS_FILE"
|
|
|
|
# Summary
|
|
if [ "$DISCREPANCIES" -eq 0 ]; then
|
|
echo -e "${GREEN}✓ sprint-status.yaml is up to date!${NC}"
|
|
echo ""
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$VALIDATE_ONLY" = true ]; then
|
|
echo -e "${RED}✗ Validation failed: $DISCREPANCIES discrepancies found${NC}"
|
|
echo ""
|
|
echo "Run without --validate to update sprint-status.yaml"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
echo -e "${YELLOW}DRY RUN: Would update $UPDATES entries${NC}"
|
|
echo ""
|
|
echo "Run without --dry-run to apply changes"
|
|
exit 0
|
|
fi
|
|
|
|
# Apply updates
|
|
echo "Applying updates to sprint-status.yaml..."
|
|
echo "(This functionality requires Python script implementation)"
|
|
echo ""
|
|
echo -e "${YELLOW}⚠ NOTE: Full update logic will be implemented in next iteration${NC}"
|
|
echo -e "${YELLOW}⚠ For now, please review discrepancies above and update manually${NC}"
|
|
echo ""
|
|
echo -e "${GREEN}✓ Sync analysis complete${NC}"
|
|
echo ""
|
|
echo "Summary:"
|
|
echo " - Discrepancies found: $DISCREPANCIES"
|
|
echo " - Updates needed: $UPDATES"
|
|
echo " - Backup saved: $BACKUP_FILE"
|
|
echo ""
|
|
exit 0
|