infra/scripts/parse-postmortem-todos.sh
Viktor Barzin 8badb8181a feat: post-mortem automation pipeline
E2E workflow for incident post-mortems:
1. /post-mortem skill generates structured post-mortem markdown
2. Woodpecker pipeline triggers on docs/post-mortems/*.md changes
3. parse-postmortem-todos.sh extracts safe TODOs (Alert/Config/Monitor)
4. postmortem-todo-resolver agent implements TODOs headlessly
5. Agent updates post-mortem with Follow-up Implementation table

Components:
- .claude/skills/post-mortem/ — writer skill + template
- .claude/agents/postmortem-todo-resolver.md — headless agent
- .woodpecker/postmortem-todos.yml — CI pipeline
- scripts/parse-postmortem-todos.sh — TODO extractor
- cluster-health skill — auto-suggest post-mortem after recovery

Safety: only auto-implements Alert/Config/Monitor types.
Architecture/Migration/Investigation items are skipped.

[ci skip]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 15:34:42 +00:00

89 lines
3.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# parse-postmortem-todos.sh — Extract auto-implementable TODOs from a post-mortem markdown file
# Usage: bash scripts/parse-postmortem-todos.sh docs/post-mortems/2026-04-14-foo.md
# Output: JSON with file path and list of TODOs
#
# Supports two table formats:
# New: | Priority | Action | Type | Details | Status |
# Old: | Action | Status | Details | (infers type from action text)
set -euo pipefail
PM_FILE="${1:?Usage: $0 <post-mortem.md>}"
if [ ! -f "$PM_FILE" ]; then
echo '{"file": "", "todos": [], "error": "File not found"}' >&2
exit 1
fi
python3 -c "
import re, json, sys
pm_file = sys.argv[1]
with open(pm_file) as f:
content = f.read()
safe_types = {'Alert', 'Config', 'Monitor'}
todos = []
# Format 1 (new template): | Priority | Action | Type | Details | Status |
pattern_new = r'\|\s*(P[0-3])\s*\|\s*(.+?)\s*\|\s*(\w+)\s*\|\s*(.+?)\s*\|\s*TODO\s*\|'
for priority, action, todo_type, details in re.findall(pattern_new, content):
todos.append({
'priority': priority.strip(),
'action': action.strip(),
'type': todo_type.strip(),
'details': details.strip(),
'safe': todo_type.strip() in safe_types
})
# Format 2 (old): | Action | TODO/Done | Details | or | Action | Owner | Status |
# Look for rows with TODO in any column
if not todos:
pattern_old = r'\|\s*(.+?)\s*\|\s*TODO\s*\|\s*(.+?)\s*\|'
for action, details in re.findall(pattern_old, content):
action = action.strip()
details = details.strip()
# Skip header rows and clean up leading pipes
if action.startswith('--') or action.lower() == 'action':
continue
action = action.lstrip('| ').strip()
# Infer type from action text
action_lower = action.lower()
if any(kw in action_lower for kw in ['prometheusrule', 'alert', 'alerting']):
todo_type = 'Alert'
elif any(kw in action_lower for kw in ['uptime kuma', 'monitor', 'ping', 'tcp check']):
todo_type = 'Monitor'
elif any(kw in action_lower for kw in ['config', 'manage', 'add.*option', 'document', 'nfs.conf']):
todo_type = 'Config'
elif any(kw in action_lower for kw in ['migrate', 'move']):
todo_type = 'Migration'
elif any(kw in action_lower for kw in ['review', 'investigate', 'verify']):
todo_type = 'Investigation'
else:
todo_type = 'Config' # default to Config for ambiguous items
# Infer priority from section header context
priority = 'P2' # default
todos.append({
'priority': priority,
'action': action,
'type': todo_type,
'details': details,
'safe': todo_type in safe_types
})
safe_todos = [t for t in todos if t['safe']]
unsafe_todos = [t for t in todos if not t['safe']]
result = {
'file': pm_file,
'todos': safe_todos,
'skipped': unsafe_todos,
'total_todos_in_doc': len(todos),
'safe_todos': len(safe_todos),
'skipped_todos': len(unsafe_todos)
}
print(json.dumps(result, indent=2))
" "$PM_FILE"