docs_suggestions.yml

  1name: Documentation Suggestions
  2
  3# Stable release callout stripping plan (not wired yet):
  4# 1. Add a separate stable-only workflow trigger on `release.published`
  5#    with `github.event.release.prerelease == false`.
  6# 2. In that workflow, run `script/docs-strip-preview-callouts` on `main`.
  7# 3. Open a PR with stripped preview callouts for human review.
  8# 4. Fail loudly on script errors or when no callout changes are produced.
  9# 5. Keep this workflow focused on suggestions only until that stable workflow is added.
 10
 11on:
 12  # Run when PRs are merged to main
 13  pull_request:
 14    types: [closed]
 15    branches: [main]
 16    paths:
 17      - 'crates/**/*.rs'
 18      - '!crates/**/*_test.rs'
 19      - '!crates/**/tests/**'
 20  
 21  # Run on cherry-picks to release branches
 22  pull_request_target:
 23    types: [opened, synchronize]
 24    branches:
 25      - 'v0.*'
 26    paths:
 27      - 'crates/**/*.rs'
 28  
 29  # Manual trigger for testing
 30  workflow_dispatch:
 31    inputs:
 32      pr_number:
 33        description: 'PR number to analyze'
 34        required: true
 35        type: string
 36      mode:
 37        description: 'Output mode'
 38        required: true
 39        type: choice
 40        options:
 41          - batch
 42          - immediate
 43        default: batch
 44
 45permissions:
 46  contents: write
 47  pull-requests: write
 48
 49env:
 50  DROID_MODEL: claude-sonnet-4-5-20250929
 51  SUGGESTIONS_BRANCH: docs/suggestions-pending
 52
 53jobs:
 54  # Job for PRs merged to main - batch suggestions to branch
 55  batch-suggestions:
 56    runs-on: ubuntu-latest
 57    timeout-minutes: 10
 58    if: |
 59      (github.event_name == 'pull_request' && 
 60       github.event.pull_request.merged == true &&
 61       github.event.pull_request.base.ref == 'main') ||
 62      (github.event_name == 'workflow_dispatch' && inputs.mode == 'batch')
 63    
 64    steps:
 65      - name: Checkout repository
 66        uses: actions/checkout@v4
 67        with:
 68          fetch-depth: 0
 69          token: ${{ secrets.GITHUB_TOKEN }}
 70
 71      - name: Install Droid CLI
 72        run: |
 73          curl -fsSL https://app.factory.ai/cli | sh
 74          echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
 75        env:
 76          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
 77
 78      - name: Get PR info
 79        id: pr
 80        run: |
 81          if [ -n "${{ inputs.pr_number }}" ]; then
 82            PR_NUM="${{ inputs.pr_number }}"
 83          else
 84            PR_NUM="${{ github.event.pull_request.number }}"
 85          fi
 86          echo "number=$PR_NUM" >> "$GITHUB_OUTPUT"
 87          
 88          # Get PR title
 89          PR_TITLE=$(gh pr view "$PR_NUM" --json title --jq '.title')
 90          echo "title=$PR_TITLE" >> "$GITHUB_OUTPUT"
 91        env:
 92          GH_TOKEN: ${{ github.token }}
 93
 94      - name: Analyze PR for documentation needs
 95        id: analyze
 96        run: |
 97          # Ensure gh CLI is authenticated (GH_TOKEN may not be auto-detected)
 98          # Unset GH_TOKEN first to allow gh auth login to store credentials
 99          echo "$GH_TOKEN" | (unset GH_TOKEN && gh auth login --with-token)
100          
101          OUTPUT_FILE=$(mktemp)
102          
103          ./script/docs-suggest \
104            --pr "${{ steps.pr.outputs.number }}" \
105            --immediate \
106            --preview \
107            --output "$OUTPUT_FILE" \
108            --verbose
109          
110          # Check if we got actionable suggestions (not "no updates needed")
111          if grep -q "Documentation Suggestions" "$OUTPUT_FILE" && \
112             ! grep -q "No Documentation Updates Needed" "$OUTPUT_FILE"; then
113            echo "has_suggestions=true" >> "$GITHUB_OUTPUT"
114            echo "output_file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT"
115          else
116            echo "has_suggestions=false" >> "$GITHUB_OUTPUT"
117            echo "No actionable documentation suggestions for this PR"
118            cat "$OUTPUT_FILE"
119          fi
120        env:
121          GH_TOKEN: ${{ github.token }}
122          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
123
124      - name: Commit suggestions to queue branch
125        if: steps.analyze.outputs.has_suggestions == 'true'
126        env:
127          PR_NUM: ${{ steps.pr.outputs.number }}
128          PR_TITLE: ${{ steps.pr.outputs.title }}
129          OUTPUT_FILE: ${{ steps.analyze.outputs.output_file }}
130        run: |
131          set -euo pipefail
132          
133          # Configure git
134          git config user.name "github-actions[bot]"
135          git config user.email "github-actions[bot]@users.noreply.github.com"
136          
137          # Retry loop for handling concurrent pushes
138          MAX_RETRIES=3
139          for i in $(seq 1 "$MAX_RETRIES"); do
140            echo "Attempt $i of $MAX_RETRIES"
141            
142            # Fetch and checkout suggestions branch (create if doesn't exist)
143            if git ls-remote --exit-code --heads origin "$SUGGESTIONS_BRANCH" > /dev/null 2>&1; then
144              git fetch origin "$SUGGESTIONS_BRANCH"
145              git checkout -B "$SUGGESTIONS_BRANCH" "origin/$SUGGESTIONS_BRANCH"
146            else
147              # Create orphan branch for clean history
148              git checkout --orphan "$SUGGESTIONS_BRANCH"
149              git rm -rf . > /dev/null 2>&1 || true
150              
151              # Initialize with README
152              cat > README.md << 'EOF'
153          # Documentation Suggestions Queue
154
155          This branch contains batched documentation suggestions for the next Preview release.
156
157          Each file represents suggestions from a merged PR. At preview branch cut time,
158          run `script/docs-suggest-publish` to create a documentation PR from these suggestions.
159
160          ## Structure
161
162          - `suggestions/PR-XXXXX.md` - Suggestions for PR #XXXXX
163          - `manifest.json` - Index of all pending suggestions
164
165          ## Workflow
166
167          1. PRs merged to main trigger documentation analysis
168          2. Suggestions are committed here as individual files
169          3. At preview release, suggestions are collected into a docs PR
170          4. After docs PR is created, this branch is reset
171          EOF
172              
173              mkdir -p suggestions
174              echo '{"suggestions":[]}' > manifest.json
175              git add README.md suggestions manifest.json
176              git commit -m "Initialize documentation suggestions queue"
177            fi
178            
179            # Create suggestion file
180            SUGGESTION_FILE="suggestions/PR-${PR_NUM}.md"
181            
182            {
183              echo "# PR #${PR_NUM}: ${PR_TITLE}"
184              echo ""
185              echo "_Merged: $(date -u +%Y-%m-%dT%H:%M:%SZ)_"
186              echo "_PR: https://github.com/${{ github.repository }}/pull/${PR_NUM}_"
187              echo ""
188              cat "$OUTPUT_FILE"
189            } > "$SUGGESTION_FILE"
190            
191            # Update manifest
192            MANIFEST=$(cat manifest.json)
193            NEW_ENTRY="{\"pr\":${PR_NUM},\"title\":$(echo "$PR_TITLE" | jq -R .),\"file\":\"$SUGGESTION_FILE\",\"date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
194            
195            # Add to manifest if not already present
196            if ! echo "$MANIFEST" | jq -e ".suggestions[] | select(.pr == $PR_NUM)" > /dev/null 2>&1; then
197              echo "$MANIFEST" | jq ".suggestions += [$NEW_ENTRY]" > manifest.json
198            fi
199            
200            # Commit
201            git add "$SUGGESTION_FILE" manifest.json
202            git commit -m "docs: Add suggestions for PR #${PR_NUM}
203
204          ${PR_TITLE}
205
206          Auto-generated documentation suggestions for review at next preview release."
207            
208            # Try to push
209            if git push origin "$SUGGESTIONS_BRANCH"; then
210              echo "Successfully pushed suggestions"
211              break
212            else
213              echo "Push failed, retrying..."
214              if [ "$i" -eq "$MAX_RETRIES" ]; then
215                echo "Failed after $MAX_RETRIES attempts"
216                exit 1
217              fi
218              sleep $((i * 2))
219            fi
220          done
221
222      - name: Summary
223        if: always()
224        run: |
225          {
226            echo "## Documentation Suggestions"
227            echo ""
228            if [ "${{ steps.analyze.outputs.has_suggestions }}" == "true" ]; then
229              echo "✅ Suggestions queued for PR #${{ steps.pr.outputs.number }}"
230              echo ""
231              echo "View pending suggestions: [docs/suggestions-pending branch](https://github.com/${{ github.repository }}/tree/${{ env.SUGGESTIONS_BRANCH }})"
232            else
233              echo "No documentation updates needed for this PR."
234            fi
235          } >> "$GITHUB_STEP_SUMMARY"
236
237  # Job for cherry-picks to release branches - immediate output to step summary
238  cherry-pick-suggestions:
239    runs-on: ubuntu-latest
240    timeout-minutes: 10
241    if: |
242      (github.event_name == 'pull_request_target' && 
243       startsWith(github.event.pull_request.base.ref, 'v0.')) ||
244      (github.event_name == 'workflow_dispatch' && inputs.mode == 'immediate')
245    
246    steps:
247      - name: Checkout repository
248        uses: actions/checkout@v4
249        with:
250          fetch-depth: 0
251
252      - name: Install Droid CLI
253        run: |
254          curl -fsSL https://app.factory.ai/cli | sh
255          echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
256        env:
257          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
258
259      - name: Get PR number
260        id: pr
261        run: |
262          if [ -n "${{ inputs.pr_number }}" ]; then
263            echo "number=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT"
264          else
265            echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
266          fi
267
268      - name: Analyze PR for documentation needs
269        id: analyze
270        run: |
271          # Ensure gh CLI is authenticated (GH_TOKEN may not be auto-detected)
272          # Unset GH_TOKEN first to allow gh auth login to store credentials
273          echo "$GH_TOKEN" | (unset GH_TOKEN && gh auth login --with-token)
274          
275          OUTPUT_FILE=$(mktemp)
276          
277          # Cherry-picks don't get preview callout
278          ./script/docs-suggest \
279            --pr "${{ steps.pr.outputs.number }}" \
280            --immediate \
281            --no-preview \
282            --output "$OUTPUT_FILE" \
283            --verbose
284          
285          # Check if we got actionable suggestions
286          if [ -s "$OUTPUT_FILE" ] && \
287             grep -q "Documentation Suggestions" "$OUTPUT_FILE" && \
288             ! grep -q "No Documentation Updates Needed" "$OUTPUT_FILE"; then
289            echo "has_suggestions=true" >> "$GITHUB_OUTPUT"
290            {
291              echo 'suggestions<<EOF'
292              cat "$OUTPUT_FILE"
293              echo 'EOF'
294            } >> "$GITHUB_OUTPUT"
295          else
296            echo "has_suggestions=false" >> "$GITHUB_OUTPUT"
297          fi
298        env:
299          GH_TOKEN: ${{ github.token }}
300          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
301
302      - name: Post suggestions as PR comment
303        if: steps.analyze.outputs.has_suggestions == 'true'
304        uses: actions/github-script@v7
305        with:
306          script: |
307            const suggestions = `${{ steps.analyze.outputs.suggestions }}`;
308            
309            const body = `## 📚 Documentation Suggestions
310
311            This cherry-pick contains changes that may need documentation updates.
312
313            ${suggestions}
314
315            ---
316            <details>
317            <summary>About this comment</summary>
318
319            This comment was generated automatically by analyzing code changes in this cherry-pick.
320            Cherry-picks typically don't need new documentation since the feature was already 
321            documented when merged to main, but please verify.
322
323            </details>`;
324            
325            // Find existing comment to update (avoid spam)
326            const { data: comments } = await github.rest.issues.listComments({
327              owner: context.repo.owner,
328              repo: context.repo.repo,
329              issue_number: ${{ steps.pr.outputs.number }}
330            });
331            
332            const botComment = comments.find(c => 
333              c.user.type === 'Bot' && 
334              c.body.includes('Documentation Suggestions')
335            );
336            
337            if (botComment) {
338              await github.rest.issues.updateComment({
339                owner: context.repo.owner,
340                repo: context.repo.repo,
341                comment_id: botComment.id,
342                body: body
343              });
344            } else {
345              await github.rest.issues.createComment({
346                owner: context.repo.owner,
347                repo: context.repo.repo,
348                issue_number: ${{ steps.pr.outputs.number }},
349                body: body
350              });
351            }
352
353      - name: Summary
354        if: always()
355        run: |
356          {
357            echo "## 📚 Documentation Suggestions (Cherry-pick)"
358            echo ""
359            if [ "${{ steps.analyze.outputs.has_suggestions }}" == "true" ]; then
360              echo "Suggestions posted as PR comment."
361              echo ""
362              echo "${{ steps.analyze.outputs.suggestions }}"
363            else
364              echo "No documentation suggestions for this cherry-pick."
365            fi
366          } >> "$GITHUB_STEP_SUMMARY"