Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 358 additions & 0 deletions .github/workflows/docs_suggestions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
name: Documentation Suggestions

# Stable release callout stripping plan (not wired yet):
# 1. Add a separate stable-only workflow trigger on `release.published`
# with `github.event.release.prerelease == false`.
# 2. In that workflow, run `script/docs-strip-preview-callouts` on `main`.
# 3. Open a PR with stripped preview callouts for human review.
# 4. Fail loudly on script errors or when no callout changes are produced.
# 5. Keep this workflow focused on suggestions only until that stable workflow is added.

on:
# Run when PRs are merged to main
pull_request:
types: [closed]
branches: [main]
paths:
- 'crates/**/*.rs'
- '!crates/**/*_test.rs'
- '!crates/**/tests/**'

# Run on cherry-picks to release branches
pull_request_target:
types: [opened, synchronize]
branches:
- 'v0.*'
paths:
- 'crates/**/*.rs'

# Manual trigger for testing
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to analyze'
required: true
type: string
mode:
description: 'Output mode'
required: true
type: choice
options:
- batch
- immediate
default: batch

permissions:
contents: write
pull-requests: write

env:
DROID_MODEL: claude-sonnet-4-5-20250929
SUGGESTIONS_BRANCH: docs/suggestions-pending

jobs:
# Job for PRs merged to main - batch suggestions to branch
batch-suggestions:
runs-on: ubuntu-latest
timeout-minutes: 10
if: |
(github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main') ||
(github.event_name == 'workflow_dispatch' && inputs.mode == 'batch')

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Install Droid CLI
run: |
curl -fsSL https://app.factory.ai/cli | sh
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
env:
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}

- name: Get PR info
id: pr
run: |
if [ -n "${{ inputs.pr_number }}" ]; then
PR_NUM="${{ inputs.pr_number }}"
else
PR_NUM="${{ github.event.pull_request.number }}"
fi
echo "number=$PR_NUM" >> "$GITHUB_OUTPUT"

# Get PR title
PR_TITLE=$(gh pr view "$PR_NUM" --json title --jq '.title')
echo "title=$PR_TITLE" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}

- name: Analyze PR for documentation needs
id: analyze
run: |
OUTPUT_FILE=$(mktemp)

./script/docs-suggest \
--pr "${{ steps.pr.outputs.number }}" \
--immediate \
--preview \
--output "$OUTPUT_FILE" \
--verbose

# Check if we got actionable suggestions (not "no updates needed")
if grep -q "Documentation Suggestions" "$OUTPUT_FILE" && \
! grep -q "No Documentation Updates Needed" "$OUTPUT_FILE"; then
echo "has_suggestions=true" >> "$GITHUB_OUTPUT"
echo "output_file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT"
else
echo "has_suggestions=false" >> "$GITHUB_OUTPUT"
echo "No actionable documentation suggestions for this PR"
cat "$OUTPUT_FILE"
fi
env:
GH_TOKEN: ${{ github.token }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}

- name: Commit suggestions to queue branch
if: steps.analyze.outputs.has_suggestions == 'true'
run: |
set -euo pipefail

PR_NUM="${{ steps.pr.outputs.number }}"
PR_TITLE="${{ steps.pr.outputs.title }}"
OUTPUT_FILE="${{ steps.analyze.outputs.output_file }}"

# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

# Retry loop for handling concurrent pushes
MAX_RETRIES=3
for i in $(seq 1 "$MAX_RETRIES"); do
echo "Attempt $i of $MAX_RETRIES"

# Fetch and checkout suggestions branch (create if doesn't exist)
if git ls-remote --exit-code --heads origin "$SUGGESTIONS_BRANCH" > /dev/null 2>&1; then
git fetch origin "$SUGGESTIONS_BRANCH"
git checkout -B "$SUGGESTIONS_BRANCH" "origin/$SUGGESTIONS_BRANCH"
else
# Create orphan branch for clean history
git checkout --orphan "$SUGGESTIONS_BRANCH"
git rm -rf . > /dev/null 2>&1 || true

# Initialize with README
cat > README.md << 'EOF'
# Documentation Suggestions Queue

This branch contains batched documentation suggestions for the next Preview release.

Each file represents suggestions from a merged PR. At preview branch cut time,
run `script/docs-suggest-publish` to create a documentation PR from these suggestions.

## Structure

- `suggestions/PR-XXXXX.md` - Suggestions for PR #XXXXX
- `manifest.json` - Index of all pending suggestions

## Workflow

1. PRs merged to main trigger documentation analysis
2. Suggestions are committed here as individual files
3. At preview release, suggestions are collected into a docs PR
4. After docs PR is created, this branch is reset
EOF

mkdir -p suggestions
echo '{"suggestions":[]}' > manifest.json
git add README.md suggestions manifest.json
git commit -m "Initialize documentation suggestions queue"
fi

# Create suggestion file
SUGGESTION_FILE="suggestions/PR-${PR_NUM}.md"

{
echo "# PR #${PR_NUM}: ${PR_TITLE}"
echo ""
echo "_Merged: $(date -u +%Y-%m-%dT%H:%M:%SZ)_"
echo "_PR: https://github.com/${{ github.repository }}/pull/${PR_NUM}_"
echo ""
cat "$OUTPUT_FILE"
} > "$SUGGESTION_FILE"

# Update manifest
MANIFEST=$(cat manifest.json)
NEW_ENTRY="{\"pr\":${PR_NUM},\"title\":$(echo "$PR_TITLE" | jq -R .),\"file\":\"$SUGGESTION_FILE\",\"date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"

# Add to manifest if not already present
if ! echo "$MANIFEST" | jq -e ".suggestions[] | select(.pr == $PR_NUM)" > /dev/null 2>&1; then
echo "$MANIFEST" | jq ".suggestions += [$NEW_ENTRY]" > manifest.json
fi

# Commit
git add "$SUGGESTION_FILE" manifest.json
git commit -m "docs: Add suggestions for PR #${PR_NUM}

${PR_TITLE}

Auto-generated documentation suggestions for review at next preview release."

# Try to push
if git push origin "$SUGGESTIONS_BRANCH"; then
echo "Successfully pushed suggestions"
break
else
echo "Push failed, retrying..."
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "Failed after $MAX_RETRIES attempts"
exit 1
fi
sleep $((i * 2))
fi
done

- name: Summary
if: always()
run: |
{
echo "## Documentation Suggestions"
echo ""
if [ "${{ steps.analyze.outputs.has_suggestions }}" == "true" ]; then
echo "✅ Suggestions queued for PR #${{ steps.pr.outputs.number }}"
echo ""
echo "View pending suggestions: [docs/suggestions-pending branch](https://github.com/${{ github.repository }}/tree/${{ env.SUGGESTIONS_BRANCH }})"
else
echo "No documentation updates needed for this PR."
fi
} >> "$GITHUB_STEP_SUMMARY"

# Job for cherry-picks to release branches - immediate output to step summary
cherry-pick-suggestions:
runs-on: ubuntu-latest
timeout-minutes: 10
if: |
(github.event_name == 'pull_request_target' &&
startsWith(github.event.pull_request.base.ref, 'v0.')) ||
(github.event_name == 'workflow_dispatch' && inputs.mode == 'immediate')

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Droid CLI
run: |
curl -fsSL https://app.factory.ai/cli | sh
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
env:
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}

- name: Get PR number
id: pr
run: |
if [ -n "${{ inputs.pr_number }}" ]; then
echo "number=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT"
else
echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
fi

- name: Analyze PR for documentation needs
id: analyze
run: |
OUTPUT_FILE=$(mktemp)

# Cherry-picks don't get preview callout
./script/docs-suggest \
--pr "${{ steps.pr.outputs.number }}" \
--immediate \
--no-preview \
--output "$OUTPUT_FILE" \
--verbose

# Check if we got actionable suggestions
if [ -s "$OUTPUT_FILE" ] && \
grep -q "Documentation Suggestions" "$OUTPUT_FILE" && \
! grep -q "No Documentation Updates Needed" "$OUTPUT_FILE"; then
echo "has_suggestions=true" >> "$GITHUB_OUTPUT"
{
echo 'suggestions<<EOF'
cat "$OUTPUT_FILE"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
else
echo "has_suggestions=false" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ github.token }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}

- name: Post suggestions as PR comment
if: steps.analyze.outputs.has_suggestions == 'true'
uses: actions/github-script@v7
with:
script: |
const suggestions = `${{ steps.analyze.outputs.suggestions }}`;

const body = `## 📚 Documentation Suggestions

This cherry-pick contains changes that may need documentation updates.

${suggestions}

---
<details>
<summary>About this comment</summary>

This comment was generated automatically by analyzing code changes in this cherry-pick.
Cherry-picks typically don't need new documentation since the feature was already
documented when merged to main, but please verify.

</details>`;

// Find existing comment to update (avoid spam)
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ steps.pr.outputs.number }}
});

const botComment = comments.find(c =>
c.user.type === 'Bot' &&
c.body.includes('Documentation Suggestions')
);

if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ steps.pr.outputs.number }},
body: body
});
}

- name: Summary
if: always()
run: |
{
echo "## 📚 Documentation Suggestions (Cherry-pick)"
echo ""
if [ "${{ steps.analyze.outputs.has_suggestions }}" == "true" ]; then
echo "Suggestions posted as PR comment."
echo ""
echo "${{ steps.analyze.outputs.suggestions }}"
else
echo "No documentation suggestions for this cherry-pick."
fi
} >> "$GITHUB_STEP_SUMMARY"
Loading