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"