mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-24 05:25:18 +00:00
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
488 lines
18 KiB
YAML
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"
|