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"