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