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          echo "$GH_TOKEN" | gh auth login --with-token
 99          
100          OUTPUT_FILE=$(mktemp)
101          
102          ./script/docs-suggest \
103            --pr "${{ steps.pr.outputs.number }}" \
104            --immediate \
105            --preview \
106            --output "$OUTPUT_FILE" \
107            --verbose
108          
109          # Check if we got actionable suggestions (not "no updates needed")
110          if grep -q "Documentation Suggestions" "$OUTPUT_FILE" && \
111             ! grep -q "No Documentation Updates Needed" "$OUTPUT_FILE"; then
112            echo "has_suggestions=true" >> "$GITHUB_OUTPUT"
113            echo "output_file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT"
114          else
115            echo "has_suggestions=false" >> "$GITHUB_OUTPUT"
116            echo "No actionable documentation suggestions for this PR"
117            cat "$OUTPUT_FILE"
118          fi
119        env:
120          GH_TOKEN: ${{ github.token }}
121          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
122
123      - name: Commit suggestions to queue branch
124        if: steps.analyze.outputs.has_suggestions == 'true'
125        env:
126          PR_NUM: ${{ steps.pr.outputs.number }}
127          PR_TITLE: ${{ steps.pr.outputs.title }}
128          OUTPUT_FILE: ${{ steps.analyze.outputs.output_file }}
129        run: |
130          set -euo pipefail
131          
132          # Configure git
133          git config user.name "github-actions[bot]"
134          git config user.email "github-actions[bot]@users.noreply.github.com"
135          
136          # Retry loop for handling concurrent pushes
137          MAX_RETRIES=3
138          for i in $(seq 1 "$MAX_RETRIES"); do
139            echo "Attempt $i of $MAX_RETRIES"
140            
141            # Fetch and checkout suggestions branch (create if doesn't exist)
142            if git ls-remote --exit-code --heads origin "$SUGGESTIONS_BRANCH" > /dev/null 2>&1; then
143              git fetch origin "$SUGGESTIONS_BRANCH"
144              git checkout -B "$SUGGESTIONS_BRANCH" "origin/$SUGGESTIONS_BRANCH"
145            else
146              # Create orphan branch for clean history
147              git checkout --orphan "$SUGGESTIONS_BRANCH"
148              git rm -rf . > /dev/null 2>&1 || true
149              
150              # Initialize with README
151              cat > README.md << 'EOF'
152          # Documentation Suggestions Queue
153
154          This branch contains batched documentation suggestions for the next Preview release.
155
156          Each file represents suggestions from a merged PR. At preview branch cut time,
157          run `script/docs-suggest-publish` to create a documentation PR from these suggestions.
158
159          ## Structure
160
161          - `suggestions/PR-XXXXX.md` - Suggestions for PR #XXXXX
162          - `manifest.json` - Index of all pending suggestions
163
164          ## Workflow
165
166          1. PRs merged to main trigger documentation analysis
167          2. Suggestions are committed here as individual files
168          3. At preview release, suggestions are collected into a docs PR
169          4. After docs PR is created, this branch is reset
170          EOF
171              
172              mkdir -p suggestions
173              echo '{"suggestions":[]}' > manifest.json
174              git add README.md suggestions manifest.json
175              git commit -m "Initialize documentation suggestions queue"
176            fi
177            
178            # Create suggestion file
179            SUGGESTION_FILE="suggestions/PR-${PR_NUM}.md"
180            
181            {
182              echo "# PR #${PR_NUM}: ${PR_TITLE}"
183              echo ""
184              echo "_Merged: $(date -u +%Y-%m-%dT%H:%M:%SZ)_"
185              echo "_PR: https://github.com/${{ github.repository }}/pull/${PR_NUM}_"
186              echo ""
187              cat "$OUTPUT_FILE"
188            } > "$SUGGESTION_FILE"
189            
190            # Update manifest
191            MANIFEST=$(cat manifest.json)
192            NEW_ENTRY="{\"pr\":${PR_NUM},\"title\":$(echo "$PR_TITLE" | jq -R .),\"file\":\"$SUGGESTION_FILE\",\"date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
193            
194            # Add to manifest if not already present
195            if ! echo "$MANIFEST" | jq -e ".suggestions[] | select(.pr == $PR_NUM)" > /dev/null 2>&1; then
196              echo "$MANIFEST" | jq ".suggestions += [$NEW_ENTRY]" > manifest.json
197            fi
198            
199            # Commit
200            git add "$SUGGESTION_FILE" manifest.json
201            git commit -m "docs: Add suggestions for PR #${PR_NUM}
202
203          ${PR_TITLE}
204
205          Auto-generated documentation suggestions for review at next preview release."
206            
207            # Try to push
208            if git push origin "$SUGGESTIONS_BRANCH"; then
209              echo "Successfully pushed suggestions"
210              break
211            else
212              echo "Push failed, retrying..."
213              if [ "$i" -eq "$MAX_RETRIES" ]; then
214                echo "Failed after $MAX_RETRIES attempts"
215                exit 1
216              fi
217              sleep $((i * 2))
218            fi
219          done
220
221      - name: Summary
222        if: always()
223        run: |
224          {
225            echo "## Documentation Suggestions"
226            echo ""
227            if [ "${{ steps.analyze.outputs.has_suggestions }}" == "true" ]; then
228              echo "✅ Suggestions queued for PR #${{ steps.pr.outputs.number }}"
229              echo ""
230              echo "View pending suggestions: [docs/suggestions-pending branch](https://github.com/${{ github.repository }}/tree/${{ env.SUGGESTIONS_BRANCH }})"
231            else
232              echo "No documentation updates needed for this PR."
233            fi
234          } >> "$GITHUB_STEP_SUMMARY"
235
236  # Job for cherry-picks to release branches - immediate output to step summary
237  cherry-pick-suggestions:
238    runs-on: ubuntu-latest
239    timeout-minutes: 10
240    if: |
241      (github.event_name == 'pull_request_target' && 
242       startsWith(github.event.pull_request.base.ref, 'v0.')) ||
243      (github.event_name == 'workflow_dispatch' && inputs.mode == 'immediate')
244    
245    steps:
246      - name: Checkout repository
247        uses: actions/checkout@v4
248        with:
249          fetch-depth: 0
250
251      - name: Install Droid CLI
252        run: |
253          curl -fsSL https://app.factory.ai/cli | sh
254          echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
255        env:
256          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
257
258      - name: Get PR number
259        id: pr
260        run: |
261          if [ -n "${{ inputs.pr_number }}" ]; then
262            echo "number=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT"
263          else
264            echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
265          fi
266
267      - name: Analyze PR for documentation needs
268        id: analyze
269        run: |
270          # Ensure gh CLI is authenticated (GH_TOKEN may not be auto-detected)
271          echo "$GH_TOKEN" | gh auth login --with-token
272          
273          OUTPUT_FILE=$(mktemp)
274          
275          # Cherry-picks don't get preview callout
276          ./script/docs-suggest \
277            --pr "${{ steps.pr.outputs.number }}" \
278            --immediate \
279            --no-preview \
280            --output "$OUTPUT_FILE" \
281            --verbose
282          
283          # Check if we got actionable suggestions
284          if [ -s "$OUTPUT_FILE" ] && \
285             grep -q "Documentation Suggestions" "$OUTPUT_FILE" && \
286             ! grep -q "No Documentation Updates Needed" "$OUTPUT_FILE"; then
287            echo "has_suggestions=true" >> "$GITHUB_OUTPUT"
288            {
289              echo 'suggestions<<EOF'
290              cat "$OUTPUT_FILE"
291              echo 'EOF'
292            } >> "$GITHUB_OUTPUT"
293          else
294            echo "has_suggestions=false" >> "$GITHUB_OUTPUT"
295          fi
296        env:
297          GH_TOKEN: ${{ github.token }}
298          FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
299
300      - name: Post suggestions as PR comment
301        if: steps.analyze.outputs.has_suggestions == 'true'
302        uses: actions/github-script@v7
303        with:
304          script: |
305            const suggestions = `${{ steps.analyze.outputs.suggestions }}`;
306            
307            const body = `## 📚 Documentation Suggestions
308
309            This cherry-pick contains changes that may need documentation updates.
310
311            ${suggestions}
312
313            ---
314            <details>
315            <summary>About this comment</summary>
316
317            This comment was generated automatically by analyzing code changes in this cherry-pick.
318            Cherry-picks typically don't need new documentation since the feature was already 
319            documented when merged to main, but please verify.
320
321            </details>`;
322            
323            // Find existing comment to update (avoid spam)
324            const { data: comments } = await github.rest.issues.listComments({
325              owner: context.repo.owner,
326              repo: context.repo.repo,
327              issue_number: ${{ steps.pr.outputs.number }}
328            });
329            
330            const botComment = comments.find(c => 
331              c.user.type === 'Bot' && 
332              c.body.includes('Documentation Suggestions')
333            );
334            
335            if (botComment) {
336              await github.rest.issues.updateComment({
337                owner: context.repo.owner,
338                repo: context.repo.repo,
339                comment_id: botComment.id,
340                body: body
341              });
342            } else {
343              await github.rest.issues.createComment({
344                owner: context.repo.owner,
345                repo: context.repo.repo,
346                issue_number: ${{ steps.pr.outputs.number }},
347                body: body
348              });
349            }
350
351      - name: Summary
352        if: always()
353        run: |
354          {
355            echo "## 📚 Documentation Suggestions (Cherry-pick)"
356            echo ""
357            if [ "${{ steps.analyze.outputs.has_suggestions }}" == "true" ]; then
358              echo "Suggestions posted as PR comment."
359              echo ""
360              echo "${{ steps.analyze.outputs.suggestions }}"
361            else
362              echo "No documentation suggestions for this cherry-pick."
363            fi
364          } >> "$GITHUB_STEP_SUMMARY"