test-docs-automation

  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/"