docs_automation.yml

  1name: Documentation Automation
  2
  3on:
  4  # push:
  5  #   branches: [main]
  6  #   paths:
  7  #     - 'crates/**'
  8  #     - 'extensions/**'
  9  workflow_dispatch:
 10    inputs:
 11      pr_number:
 12        description: 'PR number to analyze (gets full PR diff)'
 13        required: false
 14        type: string
 15      trigger_sha:
 16        description: 'Commit SHA to analyze (ignored if pr_number is set)'
 17        required: false
 18        type: string
 19
 20permissions:
 21  contents: write
 22  pull-requests: write
 23
 24env:
 25  FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
 26  ANALYSIS_MODEL: gemini-3-flash-preview
 27  WRITING_MODEL: claude-opus-4-5-20251101
 28
 29jobs:
 30  docs-automation:
 31    runs-on: ubuntu-latest
 32    timeout-minutes: 30
 33
 34    steps:
 35      - name: Checkout repository
 36        uses: actions/checkout@v4
 37        with:
 38          fetch-depth: 0
 39
 40      - name: Install Droid CLI
 41        id: install-droid
 42        run: |
 43          curl -fsSL https://app.factory.ai/cli | sh
 44          echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
 45          echo "DROID_BIN=${HOME}/.local/bin/droid" >> "$GITHUB_ENV"
 46          # Verify installation
 47          "${HOME}/.local/bin/droid" --version
 48
 49      - name: Setup Node.js (for Prettier)
 50        uses: actions/setup-node@v4
 51        with:
 52          node-version: '20'
 53
 54      - name: Install Prettier
 55        run: npm install -g prettier
 56
 57      - name: Get changed files
 58        id: changed
 59        run: |
 60          if [ -n "${{ inputs.pr_number }}" ]; then
 61            # Get full PR diff
 62            echo "Analyzing PR #${{ inputs.pr_number }}"
 63            echo "source=pr" >> "$GITHUB_OUTPUT"
 64            echo "ref=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT"
 65            gh pr diff "${{ inputs.pr_number }}" --name-only > /tmp/changed_files.txt
 66          elif [ -n "${{ inputs.trigger_sha }}" ]; then
 67            # Get single commit diff
 68            SHA="${{ inputs.trigger_sha }}"
 69            echo "Analyzing commit $SHA"
 70            echo "source=commit" >> "$GITHUB_OUTPUT"
 71            echo "ref=$SHA" >> "$GITHUB_OUTPUT"
 72            git diff --name-only "${SHA}^" "$SHA" > /tmp/changed_files.txt
 73          else
 74            # Default to current commit
 75            SHA="${{ github.sha }}"
 76            echo "Analyzing commit $SHA"
 77            echo "source=commit" >> "$GITHUB_OUTPUT"
 78            echo "ref=$SHA" >> "$GITHUB_OUTPUT"
 79            git diff --name-only "${SHA}^" "$SHA" > /tmp/changed_files.txt || git diff --name-only HEAD~1 HEAD > /tmp/changed_files.txt
 80          fi
 81
 82          echo "Changed files:"
 83          cat /tmp/changed_files.txt
 84        env:
 85          GH_TOKEN: ${{ github.token }}
 86
 87      # Filter for docs-relevant files
 88      - name: "Filter docs-relevant files"
 89        id: filter
 90        run: |
 91          # Patterns for files that could affect documentation
 92          PATTERNS="crates/.*/src/.*\.rs|assets/settings/.*|assets/keymaps/.*|extensions/.*|docs/.*"
 93          
 94          RELEVANT=$(grep -E "$PATTERNS" /tmp/changed_files.txt || true)
 95          if [ -z "$RELEVANT" ]; then
 96            echo "No docs-relevant files changed"
 97            echo "has_relevant=false" >> "$GITHUB_OUTPUT"
 98          else
 99            echo "Docs-relevant files found:"
100            echo "$RELEVANT"
101            echo "has_relevant=true" >> "$GITHUB_OUTPUT"
102          fi
103
104      # Combined: Analyze + Plan (using fast model)
105      - name: "Analyze & Plan"
106        id: analyze
107        if: steps.filter.outputs.has_relevant == 'true'
108        run: |
109          CHANGED_FILES=$(tr '\n' ' ' < /tmp/changed_files.txt)
110          
111          GUIDELINES='## Documentation Guidelines
112          ### Requires Update: New features, changed keybindings, modified settings, deprecated functionality
113          ### No Update: Internal refactoring, performance fixes, bug fixes, test/CI changes
114          ### Output JSON: {"updates_required": bool, "summary": str, "planned_changes": [{file, section, change_type, description}]}'
115          
116          "$DROID_BIN" exec \
117            -m "$ANALYSIS_MODEL" \
118            --auto low \
119            "Analyze code changes for documentation impact.
120            
121            $GUIDELINES
122            
123            Changed files: $CHANGED_FILES
124            
125            Output the JSON structure. Be conservative - only flag user-visible changes." \
126            > /tmp/analysis.json 2>&1 || true
127          
128          echo "Analysis complete:"
129          cat /tmp/analysis.json
130          
131          # Check if updates required
132          if grep -q '"updates_required":\s*true' /tmp/analysis.json; then
133            echo "updates_required=true" >> "$GITHUB_OUTPUT"
134          else
135            echo "updates_required=false" >> "$GITHUB_OUTPUT"
136          fi
137
138      # Combined: Apply + Summarize (using writing model)
139      - name: "Apply Documentation Changes"
140        id: apply
141        if: steps.analyze.outputs.updates_required == 'true'
142        run: |
143          ANALYSIS=$(cat /tmp/analysis.json)
144          
145          "$DROID_BIN" exec \
146            -m "$WRITING_MODEL" \
147            --auto medium \
148            "Apply documentation changes from this analysis:
149            
150            $ANALYSIS
151            
152            Instructions:
153            1. Edit each specified file
154            2. Follow mdBook format, use {#kb action::Name} for keybindings
155            3. Output summary:
156            
157            ## Changes Applied
158            - [file]: [change]
159            
160            ## Summary for PR
161            [2-3 sentences]" \
162            > /tmp/apply-report.md 2>&1 || true
163          
164          echo "Changes applied:"
165          cat /tmp/apply-report.md
166          cp /tmp/apply-report.md /tmp/phase6-summary.md
167
168      # Phase 5b: Format with Prettier
169      # Format with Prettier (only changed files)
170      - name: "Format with Prettier"
171        id: format
172        if: steps.analyze.outputs.updates_required == 'true'
173        run: |
174          CHANGED_DOCS=$(git diff --name-only docs/src/ | sed 's|^docs/||' | tr '\n' ' ')
175          if [ -n "$CHANGED_DOCS" ]; then
176            echo "Formatting: $CHANGED_DOCS"
177            cd docs && prettier --write "$CHANGED_DOCS"
178          fi
179
180      # Create PR
181      - name: "Create PR"
182        id: create_pr
183        if: steps.analyze.outputs.updates_required == 'true'
184        run: |
185          # Check if there are actual changes
186          if git diff --quiet docs/src/; then
187            echo "No documentation changes detected"
188            exit 0
189          fi
190
191          # Configure git
192          git config user.name "factory-droid[bot]"
193          git config user.email "138933559+factory-droid[bot]@users.noreply.github.com"
194
195          # Daily batch branch - one branch per day, multiple commits accumulate
196          BRANCH_NAME="docs/auto-update-$(date +%Y-%m-%d)"
197
198          # Get source PR info for attribution
199          SOURCE_PR_INFO=""
200          if [ "${{ steps.changed.outputs.source }}" == "pr" ]; then
201            PR_NUM="${{ steps.changed.outputs.ref }}"
202            PR_DETAILS=$(gh pr view "$PR_NUM" --json title,author,url 2>/dev/null || echo "{}")
203            SOURCE_TITLE=$(echo "$PR_DETAILS" | jq -r '.title // "Unknown"')
204            SOURCE_AUTHOR=$(echo "$PR_DETAILS" | jq -r '.author.login // "Unknown"')
205            SOURCE_URL=$(echo "$PR_DETAILS" | jq -r '.url // ""')
206            SOURCE_PR_INFO="
207          ---
208          **Source**: [#$PR_NUM]($SOURCE_URL) - $SOURCE_TITLE
209          **Author**: @$SOURCE_AUTHOR
210          "
211          fi
212
213          # Stash local changes from phase 5
214          git stash push -m "docs-automation-changes" -- docs/src/
215
216          # Check if branch already exists on remote
217          if git ls-remote --exit-code --heads origin "$BRANCH_NAME" > /dev/null 2>&1; then
218            echo "Branch $BRANCH_NAME exists, checking out and updating..."
219            git fetch origin "$BRANCH_NAME"
220            git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME"
221          else
222            echo "Creating new branch $BRANCH_NAME..."
223            git checkout -b "$BRANCH_NAME"
224          fi
225
226          # Apply stashed changes
227          git stash pop || true
228
229          # Stage and commit
230          git add docs/src/
231          SUMMARY=$(head -50 < /tmp/phase6-summary.md)
232          git commit -m "docs: auto-update documentation
233
234          ${SUMMARY}
235
236          Triggered by: ${{ steps.changed.outputs.source }} ${{ steps.changed.outputs.ref }}
237
238          Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>"
239
240          # Push
241          git push -u origin "$BRANCH_NAME"
242
243          # Build the PR body section for this update
244          PR_BODY_SECTION="## Update from $(date '+%Y-%m-%d %H:%M')
245          $SOURCE_PR_INFO
246          $(cat /tmp/phase6-summary.md)
247          "
248
249          # Check if PR already exists for this branch
250          EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number,url,body --jq '.[0]' || echo "")
251
252          if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then
253            PR_NUM=$(echo "$EXISTING_PR" | jq -r '.number')
254            PR_URL=$(echo "$EXISTING_PR" | jq -r '.url')
255            EXISTING_BODY=$(echo "$EXISTING_PR" | jq -r '.body // ""')
256            
257            # Append new summary to existing PR body
258            NEW_BODY="${EXISTING_BODY}
259
260          ---
261
262          ${PR_BODY_SECTION}"
263            
264            echo "$NEW_BODY" > /tmp/updated-pr-body.md
265            gh pr edit "$PR_NUM" --body-file /tmp/updated-pr-body.md
266            
267            echo "PR #$PR_NUM updated: $PR_URL"
268          else
269            # Create new PR
270            echo "$PR_BODY_SECTION" > /tmp/new-pr-body.md
271            gh pr create \
272              --title "docs: automated documentation update ($(date +%Y-%m-%d))" \
273              --body-file /tmp/new-pr-body.md \
274              --base main || true
275            echo "PR created on branch: $BRANCH_NAME"
276          fi
277        env:
278          GH_TOKEN: ${{ github.token }}
279
280      # Summary output
281      - name: "Summary"
282        if: always()
283        run: |
284          echo "## Documentation Automation Summary" >> "$GITHUB_STEP_SUMMARY"
285          echo "" >> "$GITHUB_STEP_SUMMARY"
286
287          if [ "${{ steps.filter.outputs.has_relevant }}" == "false" ]; then
288            echo "No docs-relevant files changed. Skipped analysis." >> "$GITHUB_STEP_SUMMARY"
289          elif [ "${{ steps.analyze.outputs.updates_required }}" == "false" ]; then
290            echo "No documentation updates required for this change." >> "$GITHUB_STEP_SUMMARY"
291          elif [ -f /tmp/phase6-summary.md ]; then
292            cat /tmp/phase6-summary.md >> "$GITHUB_STEP_SUMMARY"
293          else
294            echo "Workflow completed. Check individual step outputs for details." >> "$GITHUB_STEP_SUMMARY"
295          fi