1#!/usr/bin/env bash
2set -euo pipefail
3
4SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
6PROMPTS_DIR="$REPO_ROOT/.factory/prompts/docs-automation"
7OUTPUT_DIR="${TMPDIR:-/tmp}/docs-automation-test"
8
9# Default values
10BASE_BRANCH="main"
11# Use fast model for analysis, powerful model for writing
12ANALYSIS_MODEL="${ANALYSIS_MODEL:-gemini-3-flash-preview}"
13WRITING_MODEL="${WRITING_MODEL:-claude-opus-4-5-20251101}"
14DRY_RUN=false
15VERBOSE=false
16PR_NUMBER=""
17SOURCE_BRANCH=""
18
19# Patterns for files that could affect documentation
20DOCS_RELEVANT_PATTERNS=(
21 "crates/.*/src/.*\.rs" # Rust source files
22 "assets/settings/.*" # Settings schemas
23 "assets/keymaps/.*" # Keymaps
24 "extensions/.*" # Extensions
25 "docs/.*" # Docs themselves
26)
27
28usage() {
29 cat << EOF
30Usage: $(basename "$0") [OPTIONS]
31
32Test the documentation automation workflow locally.
33
34OPTIONS:
35 -p, --pr NUMBER PR number to analyze (uses gh pr diff)
36 -r, --branch BRANCH Remote branch to compare (e.g., origin/feature-branch)
37 -b, --base BRANCH Base branch to compare against (default: main)
38 -d, --dry-run Preview changes without modifying files
39 -s, --skip-apply Alias for --dry-run
40 -v, --verbose Show full output from each phase
41 -o, --output DIR Output directory for phase artifacts (default: $OUTPUT_DIR)
42 -h, --help Show this help message
43
44EXAMPLES:
45 # Analyze a PR (most common use case)
46 $(basename "$0") --pr 12345
47
48 # Analyze a PR with dry run (no file changes)
49 $(basename "$0") --pr 12345 --dry-run
50
51 # Analyze a remote branch against main
52 $(basename "$0") --branch origin/feature-branch
53
54ENVIRONMENT:
55 FACTORY_API_KEY Required: Your Factory API key
56 ANALYSIS_MODEL Model for analysis (default: gemini-2.0-flash)
57 WRITING_MODEL Model for writing (default: claude-opus-4-5-20251101)
58 GH_TOKEN Required for --pr option (or gh auth login)
59
60EOF
61 exit 0
62}
63
64while [[ $# -gt 0 ]]; do
65 case $1 in
66 -p|--pr)
67 PR_NUMBER="$2"
68 shift 2
69 ;;
70 -r|--branch)
71 SOURCE_BRANCH="$2"
72 shift 2
73 ;;
74 -b|--base)
75 BASE_BRANCH="$2"
76 shift 2
77 ;;
78 -d|--dry-run|-s|--skip-apply)
79 DRY_RUN=true
80 shift
81 ;;
82 -v|--verbose)
83 VERBOSE=true
84 shift
85 ;;
86 -o|--output)
87 OUTPUT_DIR="$2"
88 shift 2
89 ;;
90 -h|--help)
91 usage
92 ;;
93 *)
94 echo "Unknown option: $1"
95 usage
96 ;;
97 esac
98done
99
100# Cleanup function for restoring original branch
101cleanup_on_exit() {
102 if [[ -f "$OUTPUT_DIR/original-branch.txt" ]]; then
103 ORIGINAL_BRANCH=$(cat "$OUTPUT_DIR/original-branch.txt")
104 CURRENT=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
105 if [[ "$CURRENT" != "$ORIGINAL_BRANCH" && -n "$ORIGINAL_BRANCH" ]]; then
106 echo ""
107 echo "Restoring original branch: $ORIGINAL_BRANCH"
108 git -C "$REPO_ROOT" checkout "$ORIGINAL_BRANCH" 2>/dev/null || true
109 if [[ "$CURRENT" == temp-analysis-* ]]; then
110 git -C "$REPO_ROOT" branch -D "$CURRENT" 2>/dev/null || true
111 fi
112 fi
113 fi
114}
115trap cleanup_on_exit EXIT
116
117# Check for required tools
118if ! command -v droid &> /dev/null; then
119 echo "Error: droid CLI not found. Install from https://app.factory.ai/cli"
120 exit 1
121fi
122
123if [[ -z "${FACTORY_API_KEY:-}" ]]; then
124 echo "Error: FACTORY_API_KEY environment variable is not set"
125 exit 1
126fi
127
128# Check gh CLI if PR mode
129if [[ -n "$PR_NUMBER" ]]; then
130 if ! command -v gh &> /dev/null; then
131 echo "Error: gh CLI not found. Install from https://cli.github.com/"
132 echo "Required for --pr option"
133 exit 1
134 fi
135fi
136
137# Create output directory
138mkdir -p "$OUTPUT_DIR"
139echo "Output directory: $OUTPUT_DIR"
140echo "Analysis model: $ANALYSIS_MODEL"
141echo "Writing model: $WRITING_MODEL"
142echo ""
143
144cd "$REPO_ROOT"
145
146# Get changed files based on mode
147echo "=== Getting changed files ==="
148
149if [[ -n "$PR_NUMBER" ]]; then
150 # PR mode: use gh pr diff like the workflow does
151 echo "Analyzing PR #$PR_NUMBER"
152
153 # Get PR info for context
154 echo "Fetching PR details..."
155 gh pr view "$PR_NUMBER" --json title,headRefName,baseRefName,state > "$OUTPUT_DIR/pr-info.json" 2>/dev/null || true
156 if [[ -f "$OUTPUT_DIR/pr-info.json" ]]; then
157 PR_TITLE=$(jq -r '.title // "Unknown"' "$OUTPUT_DIR/pr-info.json")
158 PR_HEAD=$(jq -r '.headRefName // "Unknown"' "$OUTPUT_DIR/pr-info.json")
159 PR_BASE=$(jq -r '.baseRefName // "Unknown"' "$OUTPUT_DIR/pr-info.json")
160 PR_STATE=$(jq -r '.state // "Unknown"' "$OUTPUT_DIR/pr-info.json")
161 echo " Title: $PR_TITLE"
162 echo " Branch: $PR_HEAD -> $PR_BASE"
163 echo " State: $PR_STATE"
164 fi
165 echo ""
166
167 # Get the list of changed files
168 gh pr diff "$PR_NUMBER" --name-only > "$OUTPUT_DIR/changed_files.txt"
169
170 # Also save the full diff for analysis
171 gh pr diff "$PR_NUMBER" > "$OUTPUT_DIR/pr-diff.patch" 2>/dev/null || true
172
173 # Checkout the PR branch to have the code available for analysis
174 echo "Checking out PR branch for analysis..."
175 ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
176 echo "$ORIGINAL_BRANCH" > "$OUTPUT_DIR/original-branch.txt"
177
178 gh pr checkout "$PR_NUMBER" --force 2>/dev/null || {
179 echo "Warning: Could not checkout PR branch. Analysis will use current branch state."
180 }
181
182elif [[ -n "$SOURCE_BRANCH" ]]; then
183 # Remote branch mode
184 echo "Analyzing branch: $SOURCE_BRANCH"
185 echo "Base branch: $BASE_BRANCH"
186
187 # Fetch the branches
188 git fetch origin 2>/dev/null || true
189
190 # Resolve branch refs
191 SOURCE_REF="$SOURCE_BRANCH"
192 BASE_REF="origin/$BASE_BRANCH"
193
194 # Get merge base
195 MERGE_BASE=$(git merge-base "$BASE_REF" "$SOURCE_REF" 2>/dev/null) || {
196 echo "Error: Could not find merge base between $BASE_REF and $SOURCE_REF"
197 exit 1
198 }
199 echo "Merge base: $MERGE_BASE"
200
201 # Get changed files
202 git diff --name-only "$MERGE_BASE" "$SOURCE_REF" > "$OUTPUT_DIR/changed_files.txt"
203
204 # Checkout the source branch for analysis
205 echo "Checking out $SOURCE_BRANCH for analysis..."
206 ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
207 echo "$ORIGINAL_BRANCH" > "$OUTPUT_DIR/original-branch.txt"
208
209 git checkout "$SOURCE_BRANCH" 2>/dev/null || git checkout -b "temp-analysis-$$" "$SOURCE_REF" || {
210 echo "Warning: Could not checkout branch. Analysis will use current branch state."
211 }
212
213else
214 # Current branch mode (original behavior)
215 CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
216 echo "Analyzing current branch: $CURRENT_BRANCH"
217 echo "Base branch: $BASE_BRANCH"
218
219 # Fetch the base branch
220 git fetch origin "$BASE_BRANCH" 2>/dev/null || true
221
222 # Get merge base
223 MERGE_BASE=$(git merge-base "origin/$BASE_BRANCH" HEAD 2>/dev/null || git merge-base "$BASE_BRANCH" HEAD)
224 echo "Merge base: $MERGE_BASE"
225
226 git diff --name-only "$MERGE_BASE" HEAD > "$OUTPUT_DIR/changed_files.txt"
227fi
228
229if [[ ! -s "$OUTPUT_DIR/changed_files.txt" ]]; then
230 echo "No changed files found."
231 exit 0
232fi
233
234echo ""
235echo "Changed files ($(wc -l < "$OUTPUT_DIR/changed_files.txt" | tr -d ' ') files):"
236cat "$OUTPUT_DIR/changed_files.txt"
237echo ""
238
239# Early exit: Filter for docs-relevant files only
240echo "=== Filtering for docs-relevant files ==="
241DOCS_RELEVANT_FILES=""
242while IFS= read -r file; do
243 for pattern in "${DOCS_RELEVANT_PATTERNS[@]}"; do
244 if [[ "$file" =~ $pattern ]]; then
245 DOCS_RELEVANT_FILES="$DOCS_RELEVANT_FILES $file"
246 break
247 fi
248 done
249done < "$OUTPUT_DIR/changed_files.txt"
250
251# Trim leading space
252DOCS_RELEVANT_FILES="${DOCS_RELEVANT_FILES# }"
253
254if [[ -z "$DOCS_RELEVANT_FILES" ]]; then
255 echo "No docs-relevant files changed (only tests, configs, CI, etc.)"
256 echo "Skipping documentation analysis."
257 exit 0
258fi
259
260echo "Docs-relevant files: $(echo "$DOCS_RELEVANT_FILES" | wc -w | tr -d ' ')"
261echo "$DOCS_RELEVANT_FILES" | tr ' ' '\n' | head -20
262echo ""
263
264# Combined Phase: Analyze + Plan (using fast model)
265echo "=== Analyzing Changes & Planning Documentation Impact ==="
266echo "Using model: $ANALYSIS_MODEL"
267
268CHANGED_FILES=$(tr '\n' ' ' < "$OUTPUT_DIR/changed_files.txt")
269
270# Embed AGENTS.md guidelines directly to avoid file read
271AGENTS_GUIDELINES='## Documentation Guidelines (from AGENTS.md)
272
273### Requires Documentation Update
274- New user-facing features or commands
275- Changed keybindings or default behaviors
276- Modified settings schema or options
277- Deprecated or removed functionality
278
279### Does NOT Require Documentation Update
280- Internal refactoring without behavioral changes
281- Performance optimizations (unless user-visible)
282- Bug fixes that restore documented behavior
283- Test changes, CI/CD changes
284
285### In-Scope: docs/src/**/*.md
286### Out-of-Scope: CHANGELOG.md, README.md, code comments, rustdoc
287
288### Output Format Required
289You MUST output a JSON object with this exact structure:
290```json
291{
292 "updates_required": true/false,
293 "summary": "Brief description of changes",
294 "planned_changes": [
295 {
296 "file": "docs/src/path/to/file.md",
297 "section": "Section name",
298 "change_type": "update|add|deprecate",
299 "description": "What to change"
300 }
301 ],
302 "skipped_files": ["reason1", "reason2"]
303}
304```
305'
306
307droid exec \
308 -m "$ANALYSIS_MODEL" \
309 --auto low \
310 "Analyze these code changes and determine if documentation updates are needed.
311
312$AGENTS_GUIDELINES
313
314## Changed Files
315$CHANGED_FILES
316
317## Task
3181. Review the changed files (read them if needed to understand the changes)
3192. Determine if any user-facing behavior, settings, or features changed
3203. Output the JSON structure above with your assessment
321
322Be conservative - only flag documentation updates for user-visible changes." \
323 > "$OUTPUT_DIR/analysis.json" 2>&1 || true
324
325if [[ "$VERBOSE" == "true" ]]; then
326 cat "$OUTPUT_DIR/analysis.json"
327else
328 echo "Output saved to: $OUTPUT_DIR/analysis.json"
329fi
330echo ""
331
332# Check if updates are required (parse JSON output)
333UPDATES_REQUIRED=$(grep -o '"updates_required":\s*true' "$OUTPUT_DIR/analysis.json" || echo "")
334
335if [[ -z "$UPDATES_REQUIRED" ]]; then
336 echo "=== No documentation updates required ==="
337 echo "Analysis determined no documentation changes are needed."
338 cat "$OUTPUT_DIR/analysis.json"
339 exit 0
340fi
341
342echo "Documentation updates ARE required."
343echo ""
344
345# Extract planned changes for the next phase
346ANALYSIS_OUTPUT=$(cat "$OUTPUT_DIR/analysis.json")
347
348if [[ "$DRY_RUN" == "true" ]]; then
349 # Combined Preview Phase (dry-run): Show what would change
350 echo "=== Preview: Generating Proposed Changes ==="
351 echo "Using model: $WRITING_MODEL"
352
353 droid exec \
354 -m "$WRITING_MODEL" \
355 --auto low \
356 "Generate a PREVIEW of the documentation changes. Do NOT modify any files.
357
358Based on this analysis:
359$ANALYSIS_OUTPUT
360
361For each planned change:
3621. Read the current file
3632. Show the CURRENT section that would be modified
3643. Show the PROPOSED new content
3654. Generate a unified diff
366
367Output format:
368---
369## File: [path]
370
371### Current:
372\`\`\`markdown
373[exact current content]
374\`\`\`
375
376### Proposed:
377\`\`\`markdown
378[proposed new content]
379\`\`\`
380
381### Diff:
382\`\`\`diff
383[unified diff]
384\`\`\`
385---
386
387Show the ACTUAL content, not summaries." \
388 > "$OUTPUT_DIR/preview.md" 2>&1 || true
389
390 echo ""
391 echo "=== Preview ==="
392 cat "$OUTPUT_DIR/preview.md"
393
394 echo ""
395 echo "=== Dry run complete ==="
396 echo "To apply changes, run without --dry-run flag."
397 echo "Output saved to: $OUTPUT_DIR/"
398 exit 0
399fi
400
401# Combined Phase: Apply Changes + Generate Summary (using writing model)
402echo "=== Applying Documentation Changes ==="
403echo "Using model: $WRITING_MODEL"
404
405droid exec \
406 -m "$WRITING_MODEL" \
407 --auto medium \
408 "Apply the documentation changes specified in this analysis:
409
410$ANALYSIS_OUTPUT
411
412Instructions:
4131. For each planned change, edit the specified file
4142. Follow the mdBook format and style from docs/AGENTS.md
4153. Use {#kb action::Name} syntax for keybindings
4164. After making changes, output a brief summary
417
418Output format:
419## Changes Applied
420- [file]: [what was changed]
421
422## Summary for PR
423[2-3 sentence summary suitable for a PR description]" \
424 > "$OUTPUT_DIR/apply-report.md" 2>&1 || true
425
426if [[ "$VERBOSE" == "true" ]]; then
427 cat "$OUTPUT_DIR/apply-report.md"
428else
429 echo "Output saved to: $OUTPUT_DIR/apply-report.md"
430fi
431echo ""
432
433# Format with Prettier (only changed files)
434echo "=== Formatting with Prettier ==="
435cd "$REPO_ROOT"
436
437CHANGED_DOCS=$(git diff --name-only docs/src/ 2>/dev/null | sed 's|^docs/||' | tr '\n' ' ')
438
439if [[ -n "$CHANGED_DOCS" ]]; then
440 echo "Formatting: $CHANGED_DOCS"
441 if command -v pnpm &> /dev/null; then
442 (cd docs && pnpm dlx prettier@3.5.0 $CHANGED_DOCS --write) 2>/dev/null || true
443 elif command -v prettier &> /dev/null; then
444 (cd docs && prettier --write $CHANGED_DOCS) 2>/dev/null || true
445 fi
446 echo "Done"
447else
448 echo "No changed docs files to format"
449fi
450echo ""
451
452# Generate summary from the apply report
453cp "$OUTPUT_DIR/apply-report.md" "$OUTPUT_DIR/phase6-summary.md"
454
455# Phase 7: Create Branch and PR
456echo "=== Phase 7: Create Branch and PR ==="
457
458# Check if there are actual changes
459if git -C "$REPO_ROOT" diff --quiet docs/src/; then
460 echo "No documentation changes detected after Phase 5"
461 echo ""
462 echo "=== Test Complete (no changes to commit) ==="
463 exit 0
464fi
465
466# Check if gh CLI is available
467if ! command -v gh &> /dev/null; then
468 echo "Warning: gh CLI not found. Skipping PR creation."
469 echo "Install from https://cli.github.com/ to enable automatic PR creation."
470 echo ""
471 echo "Documentation changes (git status):"
472 git -C "$REPO_ROOT" status --short docs/src/
473 echo ""
474 echo "To review the diff:"
475 echo " git diff docs/src/"
476 echo ""
477 echo "To discard changes:"
478 echo " git checkout docs/src/"
479 exit 0
480fi
481
482cd "$REPO_ROOT"
483
484# Daily batch branch - one branch per day, multiple commits accumulate
485BRANCH_NAME="docs/auto-update-$(date +%Y-%m-%d)"
486
487# Stash local changes from phase 5
488echo "Stashing documentation changes..."
489git stash push -m "docs-automation-changes" -- docs/src/
490
491# Check if branch already exists on remote
492if git ls-remote --exit-code --heads origin "$BRANCH_NAME" > /dev/null 2>&1; then
493 echo "Branch $BRANCH_NAME exists, checking out and updating..."
494 git fetch origin "$BRANCH_NAME"
495 git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME"
496else
497 echo "Creating new branch $BRANCH_NAME from main..."
498 git fetch origin main
499 git checkout -B "$BRANCH_NAME" origin/main
500fi
501
502# Apply stashed changes
503echo "Applying documentation changes..."
504git stash pop || true
505
506# Stage and commit
507git add docs/src/
508
509# Get source PR info for attribution
510SOURCE_PR_INFO=""
511TRIGGER_INFO=""
512if [[ -n "$PR_NUMBER" ]]; then
513 # Fetch PR details: title, author, url
514 PR_DETAILS=$(gh pr view "$PR_NUMBER" --json title,author,url 2>/dev/null || echo "{}")
515 SOURCE_TITLE=$(echo "$PR_DETAILS" | jq -r '.title // "Unknown"')
516 SOURCE_AUTHOR=$(echo "$PR_DETAILS" | jq -r '.author.login // "Unknown"')
517 SOURCE_URL=$(echo "$PR_DETAILS" | jq -r '.url // ""')
518
519 TRIGGER_INFO="Triggered by: PR #$PR_NUMBER"
520 SOURCE_PR_INFO="
521---
522**Source**: [#$PR_NUMBER]($SOURCE_URL) - $SOURCE_TITLE
523**Author**: @$SOURCE_AUTHOR
524"
525elif [[ -n "$SOURCE_BRANCH" ]]; then
526 TRIGGER_INFO="Triggered by: branch $SOURCE_BRANCH"
527 SOURCE_PR_INFO="
528---
529**Source**: Branch \`$SOURCE_BRANCH\`
530"
531fi
532
533# Build commit message
534SUMMARY=$(head -50 < "$OUTPUT_DIR/phase6-summary.md" 2>/dev/null || echo "Automated documentation update")
535
536git commit -m "docs: auto-update documentation
537
538${SUMMARY}
539
540${TRIGGER_INFO}
541
542Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>" || {
543 echo "Nothing to commit"
544 exit 0
545}
546
547# Push
548echo "Pushing to origin/$BRANCH_NAME..."
549git push -u origin "$BRANCH_NAME"
550
551# Build the PR body section for this update
552PR_BODY_SECTION="## Update from $(date '+%Y-%m-%d %H:%M')
553$SOURCE_PR_INFO
554$(cat "$OUTPUT_DIR/phase6-summary.md")
555"
556
557# Check if PR already exists for this branch
558EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number,url,body --jq '.[0]' 2>/dev/null || echo "")
559
560if [[ -n "$EXISTING_PR" && "$EXISTING_PR" != "null" ]]; then
561 PR_NUM=$(echo "$EXISTING_PR" | jq -r '.number')
562 PR_URL=$(echo "$EXISTING_PR" | jq -r '.url')
563 EXISTING_BODY=$(echo "$EXISTING_PR" | jq -r '.body // ""')
564
565 # Append new summary to existing PR body
566 echo "Updating PR body with new summary..."
567 NEW_BODY="${EXISTING_BODY}
568
569---
570
571${PR_BODY_SECTION}"
572
573 echo "$NEW_BODY" > "$OUTPUT_DIR/updated-pr-body.md"
574 gh pr edit "$PR_NUM" --body-file "$OUTPUT_DIR/updated-pr-body.md"
575
576 echo ""
577 echo "=== Updated existing PR ==="
578 echo "PR #$PR_NUM: $PR_URL"
579 echo "New commit added and PR description updated."
580else
581 # Create new PR with full body
582 echo "Creating new PR..."
583 echo "$PR_BODY_SECTION" > "$OUTPUT_DIR/new-pr-body.md"
584
585 PR_URL=$(gh pr create \
586 --title "docs: automated documentation update ($(date +%Y-%m-%d))" \
587 --body-file "$OUTPUT_DIR/new-pr-body.md" \
588 --base main 2>&1) || {
589 echo "Failed to create PR: $PR_URL"
590 exit 1
591 }
592 echo ""
593 echo "=== PR Created ==="
594 echo "$PR_URL"
595fi
596
597echo ""
598echo "=== Test Complete ==="
599echo "All phase outputs saved to: $OUTPUT_DIR/"