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"