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