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"
11DROID_MODEL="${DROID_MODEL:-claude-opus-4-5-20251101}"
12DRY_RUN=false
13VERBOSE=false
14PR_NUMBER=""
15SOURCE_BRANCH=""
16
17usage() {
18 cat << EOF
19Usage: $(basename "$0") [OPTIONS]
20
21Test the documentation automation workflow locally.
22
23OPTIONS:
24 -p, --pr NUMBER PR number to analyze (uses gh pr diff)
25 -r, --branch BRANCH Remote branch to compare (e.g., origin/feature-branch)
26 -b, --base BRANCH Base branch to compare against (default: main)
27 -m, --model MODEL Droid model to use (default: $DROID_MODEL)
28 -d, --dry-run Run phases 2-4 only (no file modifications)
29 -s, --skip-apply Alias for --dry-run
30 -v, --verbose Show full output from each phase
31 -o, --output DIR Output directory for phase artifacts (default: $OUTPUT_DIR)
32 -h, --help Show this help message
33
34EXAMPLES:
35 # Analyze a PR (most common use case)
36 $(basename "$0") --pr 12345
37
38 # Analyze a PR with dry run (no file changes)
39 $(basename "$0") --pr 12345 --dry-run
40
41 # Analyze a remote branch against main
42 $(basename "$0") --branch origin/feature-branch
43
44 # Test current branch against main
45 $(basename "$0")
46
47 # Use a different model
48 $(basename "$0") --pr 12345 --model claude-sonnet-4-20250514
49
50ENVIRONMENT:
51 FACTORY_API_KEY Required: Your Factory API key
52 DROID_MODEL Override default model
53 GH_TOKEN Required for --pr option (or gh auth login)
54
55EOF
56 exit 0
57}
58
59while [[ $# -gt 0 ]]; do
60 case $1 in
61 -p|--pr)
62 PR_NUMBER="$2"
63 shift 2
64 ;;
65 -r|--branch)
66 SOURCE_BRANCH="$2"
67 shift 2
68 ;;
69 -b|--base)
70 BASE_BRANCH="$2"
71 shift 2
72 ;;
73 -m|--model)
74 DROID_MODEL="$2"
75 shift 2
76 ;;
77 -d|--dry-run|-s|--skip-apply)
78 DRY_RUN=true
79 shift
80 ;;
81 -v|--verbose)
82 VERBOSE=true
83 shift
84 ;;
85 -o|--output)
86 OUTPUT_DIR="$2"
87 shift 2
88 ;;
89 -h|--help)
90 usage
91 ;;
92 *)
93 echo "Unknown option: $1"
94 usage
95 ;;
96 esac
97done
98
99# Cleanup function for restoring original branch
100cleanup_on_exit() {
101 if [[ -f "$OUTPUT_DIR/original-branch.txt" ]]; then
102 ORIGINAL_BRANCH=$(cat "$OUTPUT_DIR/original-branch.txt")
103 CURRENT=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
104 if [[ "$CURRENT" != "$ORIGINAL_BRANCH" && -n "$ORIGINAL_BRANCH" ]]; then
105 echo ""
106 echo "Restoring original branch: $ORIGINAL_BRANCH"
107 git -C "$REPO_ROOT" checkout "$ORIGINAL_BRANCH" 2>/dev/null || true
108 if [[ "$CURRENT" == temp-analysis-* ]]; then
109 git -C "$REPO_ROOT" branch -D "$CURRENT" 2>/dev/null || true
110 fi
111 fi
112 fi
113}
114trap cleanup_on_exit EXIT
115
116# Check for required tools
117if ! command -v droid &> /dev/null; then
118 echo "Error: droid CLI not found. Install from https://app.factory.ai/cli"
119 exit 1
120fi
121
122if [[ -z "${FACTORY_API_KEY:-}" ]]; then
123 echo "Error: FACTORY_API_KEY environment variable is not set"
124 exit 1
125fi
126
127# Check gh CLI if PR mode
128if [[ -n "$PR_NUMBER" ]]; then
129 if ! command -v gh &> /dev/null; then
130 echo "Error: gh CLI not found. Install from https://cli.github.com/"
131 echo "Required for --pr option"
132 exit 1
133 fi
134fi
135
136# Create output directory
137mkdir -p "$OUTPUT_DIR"
138echo "Output directory: $OUTPUT_DIR"
139echo "Model: $DROID_MODEL"
140echo ""
141
142cd "$REPO_ROOT"
143
144# Copy prompts to output directory BEFORE any branch checkout
145# This ensures we have access to prompts even if PR branch doesn't have them
146if [[ -d "$PROMPTS_DIR" ]]; then
147 cp -r "$PROMPTS_DIR" "$OUTPUT_DIR/prompts"
148 PROMPTS_DIR="$OUTPUT_DIR/prompts"
149 echo "Prompts copied to: $PROMPTS_DIR"
150else
151 echo "Error: Prompts directory not found: $PROMPTS_DIR"
152 exit 1
153fi
154echo ""
155
156# Get changed files based on mode
157echo "=== Getting changed files ==="
158
159if [[ -n "$PR_NUMBER" ]]; then
160 # PR mode: use gh pr diff like the workflow does
161 echo "Analyzing PR #$PR_NUMBER"
162
163 # Get PR info for context
164 echo "Fetching PR details..."
165 gh pr view "$PR_NUMBER" --json title,headRefName,baseRefName,state > "$OUTPUT_DIR/pr-info.json" 2>/dev/null || true
166 if [[ -f "$OUTPUT_DIR/pr-info.json" ]]; then
167 PR_TITLE=$(jq -r '.title // "Unknown"' "$OUTPUT_DIR/pr-info.json")
168 PR_HEAD=$(jq -r '.headRefName // "Unknown"' "$OUTPUT_DIR/pr-info.json")
169 PR_BASE=$(jq -r '.baseRefName // "Unknown"' "$OUTPUT_DIR/pr-info.json")
170 PR_STATE=$(jq -r '.state // "Unknown"' "$OUTPUT_DIR/pr-info.json")
171 echo " Title: $PR_TITLE"
172 echo " Branch: $PR_HEAD -> $PR_BASE"
173 echo " State: $PR_STATE"
174 fi
175 echo ""
176
177 # Get the list of changed files
178 gh pr diff "$PR_NUMBER" --name-only > "$OUTPUT_DIR/changed_files.txt"
179
180 # Also save the full diff for analysis
181 gh pr diff "$PR_NUMBER" > "$OUTPUT_DIR/pr-diff.patch" 2>/dev/null || true
182
183 # Checkout the PR branch to have the code available for analysis
184 echo "Checking out PR branch for analysis..."
185 ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
186 echo "$ORIGINAL_BRANCH" > "$OUTPUT_DIR/original-branch.txt"
187
188 gh pr checkout "$PR_NUMBER" --force 2>/dev/null || {
189 echo "Warning: Could not checkout PR branch. Analysis will use current branch state."
190 }
191
192elif [[ -n "$SOURCE_BRANCH" ]]; then
193 # Remote branch mode
194 echo "Analyzing branch: $SOURCE_BRANCH"
195 echo "Base branch: $BASE_BRANCH"
196
197 # Fetch the branches
198 git fetch origin 2>/dev/null || true
199
200 # Resolve branch refs
201 SOURCE_REF="$SOURCE_BRANCH"
202 BASE_REF="origin/$BASE_BRANCH"
203
204 # Get merge base
205 MERGE_BASE=$(git merge-base "$BASE_REF" "$SOURCE_REF" 2>/dev/null) || {
206 echo "Error: Could not find merge base between $BASE_REF and $SOURCE_REF"
207 exit 1
208 }
209 echo "Merge base: $MERGE_BASE"
210
211 # Get changed files
212 git diff --name-only "$MERGE_BASE" "$SOURCE_REF" > "$OUTPUT_DIR/changed_files.txt"
213
214 # Checkout the source branch for analysis
215 echo "Checking out $SOURCE_BRANCH for analysis..."
216 ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
217 echo "$ORIGINAL_BRANCH" > "$OUTPUT_DIR/original-branch.txt"
218
219 git checkout "$SOURCE_BRANCH" 2>/dev/null || git checkout -b "temp-analysis-$$" "$SOURCE_REF" || {
220 echo "Warning: Could not checkout branch. Analysis will use current branch state."
221 }
222
223else
224 # Current branch mode (original behavior)
225 CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
226 echo "Analyzing current branch: $CURRENT_BRANCH"
227 echo "Base branch: $BASE_BRANCH"
228
229 # Fetch the base branch
230 git fetch origin "$BASE_BRANCH" 2>/dev/null || true
231
232 # Get merge base
233 MERGE_BASE=$(git merge-base "origin/$BASE_BRANCH" HEAD 2>/dev/null || git merge-base "$BASE_BRANCH" HEAD)
234 echo "Merge base: $MERGE_BASE"
235
236 git diff --name-only "$MERGE_BASE" HEAD > "$OUTPUT_DIR/changed_files.txt"
237fi
238
239if [[ ! -s "$OUTPUT_DIR/changed_files.txt" ]]; then
240 echo "No changed files found."
241 exit 0
242fi
243
244echo ""
245echo "Changed files ($(wc -l < "$OUTPUT_DIR/changed_files.txt" | tr -d ' ') files):"
246cat "$OUTPUT_DIR/changed_files.txt"
247echo ""
248
249# Phase 3: Analyze Changes
250echo "=== Phase 3: Analyze Changes ==="
251CHANGED_FILES=$(tr '\n' ' ' < "$OUTPUT_DIR/changed_files.txt")
252
253droid exec \
254 -m "$DROID_MODEL" \
255 --auto low \
256 "$(cat "$PROMPTS_DIR/phase3-analyze.md")
257
258 Changed files: $CHANGED_FILES" \
259 > "$OUTPUT_DIR/phase3-output.md" 2>&1 || true
260
261if [[ "$VERBOSE" == "true" ]]; then
262 cat "$OUTPUT_DIR/phase3-output.md"
263else
264 echo "Output saved to: $OUTPUT_DIR/phase3-output.md"
265fi
266echo ""
267
268# Phase 4: Plan Documentation Impact
269echo "=== Phase 4: Plan Documentation Impact ==="
270PHASE3_OUTPUT=$(cat "$OUTPUT_DIR/phase3-output.md")
271
272droid exec \
273 -m "$DROID_MODEL" \
274 --auto low \
275 "$(cat "$PROMPTS_DIR/phase4-plan.md")
276
277## Context from Phase 3
278
279### Changed Files
280$CHANGED_FILES
281
282### Phase 3 Analysis
283$PHASE3_OUTPUT" \
284 > "$OUTPUT_DIR/phase4-plan.md" 2>&1 || true
285
286if [[ "$VERBOSE" == "true" ]]; then
287 cat "$OUTPUT_DIR/phase4-plan.md"
288else
289 echo "Output saved to: $OUTPUT_DIR/phase4-plan.md"
290fi
291echo ""
292
293# Check if updates are required
294if grep -q "NO_UPDATES_REQUIRED" "$OUTPUT_DIR/phase4-plan.md" || \
295 grep -qi "Documentation Updates Required: No" "$OUTPUT_DIR/phase4-plan.md"; then
296 echo "=== No documentation updates required ==="
297 echo "Phase 4 determined no documentation changes are needed."
298 exit 0
299fi
300
301if [[ "$DRY_RUN" == "true" ]]; then
302 echo "=== Phase 5 (Preview): Generate Proposed Changes ==="
303 PHASE4_PLAN=$(cat "$OUTPUT_DIR/phase4-plan.md")
304
305 droid exec \
306 -m "$DROID_MODEL" \
307 --auto low \
308 "CRITICAL INSTRUCTION: You MUST output the ACTUAL markdown content, not a summary.
309
310Your task: Generate a preview showing EXACTLY what documentation changes would be made.
311
312REQUIRED STEPS:
3131. Read the file docs/src/ai/edit-prediction.md using the Read tool
3142. Find the Codestral section (### Codestral {#codestral})
3153. Output the EXACT current text and your EXACT proposed replacement
316
317YOUR OUTPUT MUST FOLLOW THIS EXACT FORMAT (no deviations, no summaries):
318
319---
320## File: docs/src/ai/edit-prediction.md
321
322### CURRENT CONTENT (copy exactly from file):
323\`\`\`markdown
324### Codestral {#codestral}
325
326[PASTE THE EXACT CURRENT CONTENT OF THIS SECTION HERE - EVERY LINE]
327\`\`\`
328
329### PROPOSED CONTENT (your changes):
330\`\`\`markdown
331### Codestral {#codestral}
332
333[WRITE YOUR COMPLETE REPLACEMENT FOR THIS SECTION HERE - EVERY LINE]
334\`\`\`
335
336### UNIFIED DIFF:
337\`\`\`diff
338[Generate a unified diff showing - lines removed and + lines added]
339\`\`\`
340
341### Why this change:
342[One sentence explanation]
343---
344
345DO NOT:
346- Summarize the changes
347- Skip reading the actual file
348- Provide abbreviated content
349- Say 'the section that...' - show the ACTUAL text
350
351Context for what changed in the codebase:
352$PHASE3_OUTPUT
353
354Documentation plan:
355$PHASE4_PLAN" \
356 > "$OUTPUT_DIR/phase5-preview.md" 2>&1 || true
357
358 echo "Preview saved to: $OUTPUT_DIR/phase5-preview.md"
359 echo ""
360 echo "=== Preview Content ==="
361 cat "$OUTPUT_DIR/phase5-preview.md"
362
363 echo ""
364 echo "=== Dry run complete ==="
365 echo "To apply changes for real, run without --dry-run flag."
366 echo ""
367 echo "All outputs saved to: $OUTPUT_DIR/"
368 exit 0
369fi
370
371# Phase 5: Apply Documentation Plan
372echo "=== Phase 5: Apply Documentation Plan ==="
373PHASE4_PLAN=$(cat "$OUTPUT_DIR/phase4-plan.md")
374
375droid exec \
376 -m "$DROID_MODEL" \
377 --auto medium \
378 "$(cat "$PROMPTS_DIR/phase5-apply.md")
379
380## Documentation Plan from Phase 4
381$PHASE4_PLAN" \
382 > "$OUTPUT_DIR/phase5-report.md" 2>&1 || true
383
384if [[ "$VERBOSE" == "true" ]]; then
385 cat "$OUTPUT_DIR/phase5-report.md"
386else
387 echo "Output saved to: $OUTPUT_DIR/phase5-report.md"
388fi
389echo ""
390
391# Phase 5b: Format with Prettier (only changed files)
392echo "=== Phase 5b: Format with Prettier ==="
393cd "$REPO_ROOT"
394
395# Get list of changed doc files (relative to docs/ directory)
396CHANGED_DOCS=$(git diff --name-only docs/src/ 2>/dev/null | sed 's|^docs/||' | tr '\n' ' ')
397
398if [[ -n "$CHANGED_DOCS" ]]; then
399 echo "Formatting changed files: $CHANGED_DOCS"
400 # Use pnpm dlx like the project's prettier script does
401 if command -v pnpm &> /dev/null; then
402 (cd docs && pnpm dlx prettier@3.5.0 $CHANGED_DOCS --write) 2>/dev/null || true
403 elif command -v prettier &> /dev/null; then
404 (cd docs && prettier --write $CHANGED_DOCS) 2>/dev/null || true
405 else
406 echo "Warning: neither pnpm nor prettier found, skipping formatting"
407 fi
408 echo "Prettier formatting applied"
409else
410 echo "No changed docs files to format"
411fi
412echo ""
413
414# Phase 6: Summarize Changes
415echo "=== Phase 6: Summarize Changes ==="
416git -C "$REPO_ROOT" diff docs/src/ > "$OUTPUT_DIR/docs-diff.txt" 2>/dev/null || true
417
418PHASE5_REPORT=$(cat "$OUTPUT_DIR/phase5-report.md")
419DOCS_DIFF=$(cat "$OUTPUT_DIR/docs-diff.txt")
420
421droid exec \
422 -m "$DROID_MODEL" \
423 --auto low \
424 "$(cat "$PROMPTS_DIR/phase6-summarize.md")
425
426## Context
427
428### Phase 5 Report
429$PHASE5_REPORT
430
431### Documentation Diff
432\`\`\`diff
433$DOCS_DIFF
434\`\`\`" \
435 > "$OUTPUT_DIR/phase6-summary.md" 2>&1 || true
436
437if [[ "$VERBOSE" == "true" ]]; then
438 cat "$OUTPUT_DIR/phase6-summary.md"
439else
440 echo "Output saved to: $OUTPUT_DIR/phase6-summary.md"
441fi
442echo ""
443
444# Phase 7: Create Branch and PR
445echo "=== Phase 7: Create Branch and PR ==="
446
447# Check if there are actual changes
448if git -C "$REPO_ROOT" diff --quiet docs/src/; then
449 echo "No documentation changes detected after Phase 5"
450 echo ""
451 echo "=== Test Complete (no changes to commit) ==="
452 exit 0
453fi
454
455# Check if gh CLI is available
456if ! command -v gh &> /dev/null; then
457 echo "Warning: gh CLI not found. Skipping PR creation."
458 echo "Install from https://cli.github.com/ to enable automatic PR creation."
459 echo ""
460 echo "Documentation changes (git status):"
461 git -C "$REPO_ROOT" status --short docs/src/
462 echo ""
463 echo "To review the diff:"
464 echo " git diff docs/src/"
465 echo ""
466 echo "To discard changes:"
467 echo " git checkout docs/src/"
468 exit 0
469fi
470
471cd "$REPO_ROOT"
472
473# Daily batch branch - one branch per day, multiple commits accumulate
474BRANCH_NAME="docs/auto-update-$(date +%Y-%m-%d)"
475
476# Stash local changes from phase 5
477echo "Stashing documentation changes..."
478git stash push -m "docs-automation-changes" -- docs/src/
479
480# Check if branch already exists on remote
481if git ls-remote --exit-code --heads origin "$BRANCH_NAME" > /dev/null 2>&1; then
482 echo "Branch $BRANCH_NAME exists, checking out and updating..."
483 git fetch origin "$BRANCH_NAME"
484 git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME"
485else
486 echo "Creating new branch $BRANCH_NAME from main..."
487 git fetch origin main
488 git checkout -B "$BRANCH_NAME" origin/main
489fi
490
491# Apply stashed changes
492echo "Applying documentation changes..."
493git stash pop || true
494
495# Stage and commit
496git add docs/src/
497
498# Get source PR info for attribution
499SOURCE_PR_INFO=""
500TRIGGER_INFO=""
501if [[ -n "$PR_NUMBER" ]]; then
502 # Fetch PR details: title, author, url
503 PR_DETAILS=$(gh pr view "$PR_NUMBER" --json title,author,url 2>/dev/null || echo "{}")
504 SOURCE_TITLE=$(echo "$PR_DETAILS" | jq -r '.title // "Unknown"')
505 SOURCE_AUTHOR=$(echo "$PR_DETAILS" | jq -r '.author.login // "Unknown"')
506 SOURCE_URL=$(echo "$PR_DETAILS" | jq -r '.url // ""')
507
508 TRIGGER_INFO="Triggered by: PR #$PR_NUMBER"
509 SOURCE_PR_INFO="
510---
511**Source**: [#$PR_NUMBER]($SOURCE_URL) - $SOURCE_TITLE
512**Author**: @$SOURCE_AUTHOR
513"
514elif [[ -n "$SOURCE_BRANCH" ]]; then
515 TRIGGER_INFO="Triggered by: branch $SOURCE_BRANCH"
516 SOURCE_PR_INFO="
517---
518**Source**: Branch \`$SOURCE_BRANCH\`
519"
520fi
521
522# Build commit message
523SUMMARY=$(head -50 < "$OUTPUT_DIR/phase6-summary.md" 2>/dev/null || echo "Automated documentation update")
524
525git commit -m "docs: auto-update documentation
526
527${SUMMARY}
528
529${TRIGGER_INFO}
530
531Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>" || {
532 echo "Nothing to commit"
533 exit 0
534}
535
536# Push
537echo "Pushing to origin/$BRANCH_NAME..."
538git push -u origin "$BRANCH_NAME"
539
540# Build the PR body section for this update
541PR_BODY_SECTION="## Update from $(date '+%Y-%m-%d %H:%M')
542$SOURCE_PR_INFO
543$(cat "$OUTPUT_DIR/phase6-summary.md")
544"
545
546# Check if PR already exists for this branch
547EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number,url,body --jq '.[0]' 2>/dev/null || echo "")
548
549if [[ -n "$EXISTING_PR" && "$EXISTING_PR" != "null" ]]; then
550 PR_NUM=$(echo "$EXISTING_PR" | jq -r '.number')
551 PR_URL=$(echo "$EXISTING_PR" | jq -r '.url')
552 EXISTING_BODY=$(echo "$EXISTING_PR" | jq -r '.body // ""')
553
554 # Append new summary to existing PR body
555 echo "Updating PR body with new summary..."
556 NEW_BODY="${EXISTING_BODY}
557
558---
559
560${PR_BODY_SECTION}"
561
562 echo "$NEW_BODY" > "$OUTPUT_DIR/updated-pr-body.md"
563 gh pr edit "$PR_NUM" --body-file "$OUTPUT_DIR/updated-pr-body.md"
564
565 echo ""
566 echo "=== Updated existing PR ==="
567 echo "PR #$PR_NUM: $PR_URL"
568 echo "New commit added and PR description updated."
569else
570 # Create new PR with full body
571 echo "Creating new PR..."
572 echo "$PR_BODY_SECTION" > "$OUTPUT_DIR/new-pr-body.md"
573
574 PR_URL=$(gh pr create \
575 --title "docs: automated documentation update ($(date +%Y-%m-%d))" \
576 --body-file "$OUTPUT_DIR/new-pr-body.md" \
577 --base main 2>&1) || {
578 echo "Failed to create PR: $PR_URL"
579 exit 1
580 }
581 echo ""
582 echo "=== PR Created ==="
583 echo "$PR_URL"
584fi
585
586echo ""
587echo "=== Test Complete ==="
588echo "All phase outputs saved to: $OUTPUT_DIR/"