zed/.github/workflows/docs_suggestions.yml
Finn Evers c3d1f7981b
ci: Update workflows to prepare for Node.js 20 deprecation (#52443)
The workflow run at
https://github.com/zed-industries/zed/actions/runs/23557683707 succeeded
but threw some warnings for a rather-soon Node.js 20 deprecation (June
2nd).

Hence, this PR updates in that context mentioned workflows to newer
versions from which on the actions will use Node.js 24.

Namely, this updates
- `actions/checkout`
- `actions/create-github-app-token` and
- `peter-evans/create-pull-request`

to their latest version which includes said updates. As for their most
recent versions, all of these actions just updated their versions to
account for said deprecation.

Release Notes:

- N/A
2026-03-26 10:08:06 +01:00

488 lines
18 KiB
YAML

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
env:
DROID_MODEL: claude-sonnet-4-5-20250929
SUGGESTIONS_BRANCH: docs/suggestions-pending
jobs:
# Job for PRs merged to main - batch suggestions to branch
# Only runs for PRs from the same repo (not forks) since secrets aren't available for fork PRs
batch-suggestions:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: read
if: |
(github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' && inputs.mode == 'batch')
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Droid CLI
run: |
# Retry with exponential backoff for transient network/auth issues
MAX_RETRIES=3
for i in $(seq 1 "$MAX_RETRIES"); do
echo "Attempt $i of $MAX_RETRIES to install Droid CLI..."
if curl -fsSL https://app.factory.ai/cli | sh; then
echo "Droid CLI installed successfully"
break
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "Failed to install Droid CLI after $MAX_RETRIES attempts"
exit 1
fi
sleep $((i * 5))
done
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
env:
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
- name: Get PR info
id: pr
env:
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ github.token }}
run: |
if [ -n "$INPUT_PR_NUMBER" ]; then
PR_NUM="$INPUT_PR_NUMBER"
else
PR_NUM="$EVENT_PR_NUMBER"
fi
if ! [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number: $PR_NUM"
exit 1
fi
echo "number=$PR_NUM" >> "$GITHUB_OUTPUT"
PR_TITLE=$(gh pr view "$PR_NUM" --json title --jq '.title' | tr -d '\n\r' | head -c 200)
EOF_MARKER="EOF_$(openssl rand -hex 8)"
{
echo "title<<$EOF_MARKER"
echo "$PR_TITLE"
echo "$EOF_MARKER"
} >> "$GITHUB_OUTPUT"
- name: Analyze PR for documentation needs
id: analyze
env:
GH_TOKEN: ${{ github.token }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
run: |
# Ensure gh CLI is authenticated (GH_TOKEN may not be auto-detected)
# Unset GH_TOKEN first to allow gh auth login to store credentials
echo "$GH_TOKEN" | (unset GH_TOKEN && gh auth login --with-token)
OUTPUT_FILE=$(mktemp)
# Retry with exponential backoff for transient Factory API failures
MAX_RETRIES=3
for i in $(seq 1 "$MAX_RETRIES"); do
echo "Attempt $i of $MAX_RETRIES to analyze PR..."
if ./script/docs-suggest \
--pr "$PR_NUMBER" \
--immediate \
--preview \
--output "$OUTPUT_FILE" \
--verbose; then
echo "Analysis completed successfully"
break
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "Analysis failed after $MAX_RETRIES attempts"
exit 1
fi
echo "Retrying in $((i * 5)) seconds..."
sleep $((i * 5))
done
# 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
- name: Commit suggestions to queue branch
if: steps.analyze.outputs.has_suggestions == 'true'
env:
PR_NUM: ${{ steps.pr.outputs.number }}
PR_TITLE: ${{ steps.pr.outputs.title }}
OUTPUT_FILE: ${{ steps.analyze.outputs.output_file }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# 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/${REPO}/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()
env:
HAS_SUGGESTIONS: ${{ steps.analyze.outputs.has_suggestions }}
PR_NUM: ${{ steps.pr.outputs.number }}
REPO: ${{ github.repository }}
run: |
{
echo "## Documentation Suggestions"
echo ""
if [ "$HAS_SUGGESTIONS" == "true" ]; then
echo "✅ Suggestions queued for PR #${PR_NUM}"
echo ""
echo "View pending suggestions: [docs/suggestions-pending branch](https://github.com/${REPO}/tree/${SUGGESTIONS_BRANCH})"
else
echo "No documentation updates needed for this PR."
fi
} >> "$GITHUB_STEP_SUMMARY"
# Job for cherry-picks to release branches - immediate output as PR comment
cherry-pick-suggestions:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
concurrency:
group: docs-suggestions-${{ github.event.pull_request.number || inputs.pr_number || 'manual' }}
cancel-in-progress: true
if: |
(github.event_name == 'pull_request_target' &&
startsWith(github.event.pull_request.base.ref, 'v0.') &&
contains(fromJSON('["MEMBER","OWNER"]'),
github.event.pull_request.author_association)) ||
(github.event_name == 'workflow_dispatch' && inputs.mode == 'immediate')
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
fetch-depth: 0
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.ref || '' }}
persist-credentials: false
- name: Install Droid CLI
run: |
# Retry with exponential backoff for transient network/auth issues
MAX_RETRIES=3
for i in $(seq 1 "$MAX_RETRIES"); do
echo "Attempt $i of $MAX_RETRIES to install Droid CLI..."
if curl -fsSL https://app.factory.ai/cli | sh; then
echo "Droid CLI installed successfully"
break
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "Failed to install Droid CLI after $MAX_RETRIES attempts"
exit 1
fi
sleep $((i * 5))
done
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
env:
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
- name: Get PR number
id: pr
env:
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [ -n "$INPUT_PR_NUMBER" ]; then
PR_NUM="$INPUT_PR_NUMBER"
else
PR_NUM="$EVENT_PR_NUMBER"
fi
if ! [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number: $PR_NUM"
exit 1
fi
echo "number=$PR_NUM" >> "$GITHUB_OUTPUT"
- name: Analyze PR for documentation needs
id: analyze
env:
GH_TOKEN: ${{ github.token }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
run: |
# Ensure gh CLI is authenticated (GH_TOKEN may not be auto-detected)
# Unset GH_TOKEN first to allow gh auth login to store credentials
echo "$GH_TOKEN" | (unset GH_TOKEN && gh auth login --with-token)
OUTPUT_FILE="${RUNNER_TEMP}/suggestions.md"
# Cherry-picks don't get preview callout
# Retry with exponential backoff for transient Factory API failures
MAX_RETRIES=3
for i in $(seq 1 "$MAX_RETRIES"); do
echo "Attempt $i of $MAX_RETRIES to analyze PR..."
if ./script/docs-suggest \
--pr "$PR_NUMBER" \
--immediate \
--no-preview \
--output "$OUTPUT_FILE" \
--verbose; then
echo "Analysis completed successfully"
break
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "Analysis failed after $MAX_RETRIES attempts"
exit 1
fi
echo "Retrying in $((i * 5)) seconds..."
sleep $((i * 5))
done
# 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_file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT"
else
echo "has_suggestions=false" >> "$GITHUB_OUTPUT"
fi
- name: Post suggestions as PR comment
if: steps.analyze.outputs.has_suggestions == 'true'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
env:
SUGGESTIONS_FILE: ${{ steps.analyze.outputs.suggestions_file }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
with:
script: |
const fs = require('fs');
// Read suggestions from file
const suggestionsRaw = fs.readFileSync(process.env.SUGGESTIONS_FILE, 'utf8');
// Sanitize AI-generated content
let sanitized = suggestionsRaw
// Strip HTML tags
.replace(/<[^>]*>/g, '')
// Strip markdown links but keep display text
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
// Strip raw URLs
.replace(/https?:\/\/[^\s)>\]]+/g, '[link removed]')
// Strip protocol-relative URLs
.replace(/\/\/[^\s)>\]]+\.[^\s)>\]]+/g, '[link removed]')
// Neutralize @-mentions (preserve JSDoc-style annotations)
.replace(/@(?!param\b|returns?\b|throws?\b|typedef\b|type\b|see\b|example\b|since\b|deprecated\b|default\b)(\w+)/g, '`@$1`')
// Strip cross-repo references that could be confused with real links
.replace(/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+#\d+/g, '[ref removed]');
// Truncate to 20,000 characters
if (sanitized.length > 20000) {
sanitized = sanitized.substring(0, 20000) + '\n\n…(truncated)';
}
// Parse and validate PR number
const prNumber = parseInt(process.env.PR_NUMBER, 10);
if (isNaN(prNumber) || prNumber <= 0) {
core.setFailed(`Invalid PR number: ${process.env.PR_NUMBER}`);
return;
}
const body = `## 📚 Documentation Suggestions
This cherry-pick contains changes that may need documentation updates.
${sanitized}
---
> **Note:** This comment was generated automatically by an AI model analyzing
> code changes. Suggestions may contain inaccuracies — please verify before acting.
<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: prNumber
});
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: prNumber,
body: body
});
}
- name: Summary
if: always()
env:
HAS_SUGGESTIONS: ${{ steps.analyze.outputs.has_suggestions }}
PR_NUM: ${{ steps.pr.outputs.number }}
run: |
{
echo "## 📚 Documentation Suggestions (Cherry-pick)"
echo ""
if [ "$HAS_SUGGESTIONS" == "true" ]; then
echo "Suggestions posted as PR comment on #${PR_NUM}."
else
echo "No documentation suggestions for this cherry-pick."
fi
} >> "$GITHUB_STEP_SUMMARY"