zed/.github/workflows/docs_suggestions.yml
morgankrey 04bdd17de2
Fix JS syntax error in docs_suggestions cherry-pick job (#49643)
## Summary

Fixes the `SyntaxError: Unexpected identifier 'gemini'` error in the
cherry-pick documentation suggestions workflow.

## Problem

The 'Post suggestions as PR comment' step was directly interpolating
markdown content into a JavaScript template literal:

```javascript
const suggestions = `${{ steps.analyze.outputs.suggestions }}`;
```

When the suggestions contained backticks, `${}` sequences, or other
special characters (like the `gemini-3.1-pro-preview` model name in
markdown code blocks), it broke the JavaScript syntax.

## Solution

Write suggestions to a file in `$RUNNER_TEMP` and read it using
`fs.readFileSync()` in the script step. This avoids all GitHub Actions
template interpolation and JavaScript string parsing issues.

## Testing

This should fix the failed run:
https://github.com/zed-industries/zed/actions/runs/22194396124/job/64190762087

Release Notes:

- N/A
2026-02-19 13:10:25 -06:00

419 lines
15 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
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: |
# 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
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: |
# 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 "${{ steps.pr.outputs.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
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'
env:
PR_NUM: ${{ steps.pr.outputs.number }}
PR_TITLE: ${{ steps.pr.outputs.title }}
OUTPUT_FILE: ${{ steps.analyze.outputs.output_file }}
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/${{ 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: |
# 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
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: |
# 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 "${{ steps.pr.outputs.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
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
env:
SUGGESTIONS_FILE: ${{ steps.analyze.outputs.suggestions_file }}
with:
script: |
const fs = require('fs');
const suggestions = fs.readFileSync(process.env.SUGGESTIONS_FILE, 'utf8');
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 ""
cat "${{ steps.analyze.outputs.suggestions_file }}"
else
echo "No documentation suggestions for this cherry-pick."
fi
} >> "$GITHUB_STEP_SUMMARY"