From dc41f71f57d3810725fbe6d5336cc6b101d5aa5b Mon Sep 17 00:00:00 2001 From: morgankrey Date: Wed, 18 Feb 2026 06:39:09 -0600 Subject: [PATCH] Add documentation suggestion automation (#49194) Adds scripts and a GitHub Action workflow for automatically suggesting documentation updates when PRs modify user-facing code. ## Scripts - **`script/docs-suggest`**: Analyze PRs/commits for documentation needs using AI - **`script/docs-suggest-publish`**: Create a PR from batched suggestions - **`script/docs-strip-preview-callouts`**: Remove Preview callouts when shipping to stable - **`script/test-docs-suggest-batch`**: Testing utility for batch analysis ## Workflow The GitHub Action (`.github/workflows/docs_suggestions.yml`) handles two scenarios: 1. **PRs merged to main**: Suggestions are batched to `docs/suggestions-pending` branch for the next Preview release 2. **Cherry-picks to release branches**: Suggestions are posted as PR comments for immediate review ## Callout Types The system distinguishes between: - **Additive features** (new commands, settings, UI): ```markdown > **Preview:** This feature is available in Zed Preview. It will be included in the next Stable release. ``` - **Behavior modifications** (changed defaults, altered existing behavior): ```markdown > **Changed in Preview (v0.XXX).** See [release notes](/releases#0.XXX). ``` Both callout types are stripped by `docs-strip-preview-callouts` when features ship to stable. ## Example Output See PR #49190 for example documentation suggestions generated by running this on PRs from the v0.224 preview window. ## Usage ```bash # Analyze a PR (auto-detects batch vs immediate mode) script/docs-suggest --pr 49100 # Dry run to see assembled context script/docs-suggest --pr 49100 --dry-run # Create PR from batched suggestions script/docs-suggest-publish # Strip callouts for stable release script/docs-strip-preview-callouts ``` Release Notes: - N/A --- .github/workflows/docs_suggestions.yml | 358 ++++++++++++++ docs/.conventions/CONVENTIONS.md | 30 ++ script/docs-strip-preview-callouts | 228 +++++++++ script/docs-suggest | 616 +++++++++++++++++++++++++ script/docs-suggest-publish | 378 +++++++++++++++ script/test-docs-suggest-batch | 139 ++++++ 6 files changed, 1749 insertions(+) create mode 100644 .github/workflows/docs_suggestions.yml create mode 100755 script/docs-strip-preview-callouts create mode 100755 script/docs-suggest create mode 100755 script/docs-suggest-publish create mode 100755 script/test-docs-suggest-batch diff --git a/.github/workflows/docs_suggestions.yml b/.github/workflows/docs_suggestions.yml new file mode 100644 index 0000000000000000000000000000000000000000..fc55617edc7973a41b8dc80a852162dd0fb0b7a1 --- /dev/null +++ b/.github/workflows/docs_suggestions.yml @@ -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<> "$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} + + --- +
+ About this comment + + 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. + +
`; + + // 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" diff --git a/docs/.conventions/CONVENTIONS.md b/docs/.conventions/CONVENTIONS.md index f090e7dbe6d1b712b1c9eb110d1128a1e874b1e9..585971f8fb4b15609f7339c3baab6828964e7f94 100644 --- a/docs/.conventions/CONVENTIONS.md +++ b/docs/.conventions/CONVENTIONS.md @@ -80,6 +80,18 @@ description: One sentence describing what this page covers. Used in search resul - `title`: Feature name, optionally with "- Zed" suffix for SEO - `description`: Concise summary for search engines and link previews +- Keep frontmatter values as simple single-line `key: value` entries (no + multiline values, no quotes) for compatibility with the docs postprocessor + +#### Frontmatter SEO Guidelines + +- Choose one primary keyword/intent phrase for each page +- Write unique `title` values that clearly state the page topic and target user + intent; aim for ~50-60 characters +- Write `description` values that summarize what the reader can do on the page; + aim for ~140-160 characters +- Use the primary keyword naturally in the `title` and page body at least once + (usually in the opening paragraph); avoid keyword stuffing ### Section Ordering @@ -235,6 +247,20 @@ End pages with related links when helpful: - [Inline Assistant](./inline-assistant.md): Prompt-driven code transformations ``` +### SEO Linking Guidelines + +- Ensure each page is reachable from at least one other docs page (no orphan + pages) +- For non-reference pages, include at least 3 internal links to related docs + when possible +- Reference pages (for example, `docs/src/reference/*`) can use fewer links when + extra links would add noise +- Add links to closely related docs where they help users complete the next task +- Use descriptive link text that tells users what they will get on the linked + page +- For main feature pages with a matching marketing page, include a relevant + `zed.dev` marketing link in addition to docs links + --- ## Language-Specific Documentation @@ -327,6 +353,8 @@ Before any documentation change is considered complete: Before finalizing documentation: - [ ] Frontmatter includes `title` and `description` +- [ ] Page has a clear primary keyword/intent phrase +- [ ] Primary keyword appears naturally in the page body (no keyword stuffing) - [ ] Opening paragraph explains what and why - [ ] Settings show UI first, then JSON examples - [ ] Actions use `{#action ...}` and `{#kb ...}` syntax @@ -334,6 +362,8 @@ Before finalizing documentation: - [ ] Anchor IDs on sections likely to be linked - [ ] Version callouts where behavior differs by release - [ ] No orphan pages (linked from somewhere) +- [ ] Non-reference pages include at least 3 useful internal docs links +- [ ] Main feature pages include a relevant `zed.dev` marketing link - [ ] Passes Prettier formatting check - [ ] Passes brand voice rubric (see `brand-voice/rubric.md`) diff --git a/script/docs-strip-preview-callouts b/script/docs-strip-preview-callouts new file mode 100755 index 0000000000000000000000000000000000000000..dc4d1eb1764a88a89b7406b8b02577bb7471ceb7 --- /dev/null +++ b/script/docs-strip-preview-callouts @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +# +# Remove Preview callouts from documentation for stable release. +# +# Usage: +# script/docs-strip-preview-callouts [--dry-run] +# +# This script finds and removes all Preview-related callouts from docs: +# > **Preview:** This feature is available in Zed Preview... +# > **Changed in Preview (v0.XXX).** See [release notes]... +# +# Then creates a PR with the changes. +# +# Options: +# --dry-run Show what would be changed without modifying files or creating PR +# --verbose Show detailed progress +# +# Run this as part of the stable release workflow. + +set -euo pipefail + +DRY_RUN=false +VERBOSE=false + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { + if [[ "$VERBOSE" == "true" ]]; then + echo -e "${BLUE}[strip-preview]${NC} $*" >&2 + fi +} + +error() { + echo -e "${RED}Error:${NC} $*" >&2 + exit 1 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + --verbose) + VERBOSE=true + shift + ;; + -h|--help) + head -18 "$0" | tail -16 + exit 0 + ;; + *) + error "Unknown option: $1" + ;; + esac +done + +# Get repo root +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +DOCS_DIR="$REPO_ROOT/docs/src" + +echo "Searching for Preview callouts in $DOCS_DIR..." + +# Find files with either type of preview callout: +# - > **Preview:** ... +# - > **Changed in Preview ... +files_with_callouts=$(grep -rlE "> \*\*(Preview:|Changed in Preview)" "$DOCS_DIR" 2>/dev/null || true) + +if [[ -z "$files_with_callouts" ]]; then + echo "No Preview callouts found. Nothing to do." + exit 0 +fi + +file_count=$(echo "$files_with_callouts" | wc -l | tr -d ' ') +echo "Found $file_count file(s) with Preview callouts:" +echo "" + +for file in $files_with_callouts; do + relative_path="${file#$REPO_ROOT/}" + echo " $relative_path" + + if [[ "$VERBOSE" == "true" ]]; then + grep -nE "> \*\*(Preview:|Changed in Preview)" "$file" | while read -r line; do + echo " $line" + done + fi +done + +echo "" + +if [[ "$DRY_RUN" == "true" ]]; then + echo -e "${YELLOW}=== DRY RUN ===${NC}" + echo "" + echo "Would remove Preview callouts from the files above and create a PR." + echo "" + echo "Lines to be removed:" + echo "" + + for file in $files_with_callouts; do + relative_path="${file#$REPO_ROOT/}" + echo "--- $relative_path ---" + grep -nE "> \*\*(Preview:|Changed in Preview)" "$file" || true + echo "" + done + + echo -e "${YELLOW}=== END DRY RUN ===${NC}" + echo "" + echo "Run without --dry-run to apply changes and create PR." + exit 0 +fi + +# Check for clean working state +if [[ -n "$(git status --porcelain docs/)" ]]; then + error "docs/ directory has uncommitted changes. Please commit or stash first." +fi + +# Apply changes +echo "Removing Preview callouts..." + +for file in $files_with_callouts; do + log "Processing: $file" + + tmp_file=$(mktemp) + + # Remove preview callout lines and their continuations + # Handles both: + # > **Preview:** This feature is available... + # > **Changed in Preview (v0.XXX).** See [release notes]... + awk ' + BEGIN { in_callout = 0 } + /^> \*\*Preview:\*\*/ { + in_callout = 1 + next + } + /^> \*\*Changed in Preview/ { + in_callout = 1 + next + } + in_callout && /^>/ && !/^> \*\*/ { + next + } + in_callout && /^$/ { + in_callout = 0 + next + } + { + in_callout = 0 + print + } + ' "$file" > "$tmp_file" + + mv "$tmp_file" "$file" + echo " Updated: ${file#$REPO_ROOT/}" +done + +echo "" +echo -e "${GREEN}Preview callouts removed from $file_count file(s).${NC}" + +# Check if there are actual changes (in case callouts were in comments or something) +if [[ -z "$(git status --porcelain docs/)" ]]; then + echo "" + echo "No effective changes to commit (callouts may have been in non-rendered content)." + exit 0 +fi + +# Create branch and PR +echo "" +echo "Creating PR..." + +BRANCH_NAME="docs/stable-release-$(date +%Y-%m-%d)" +log "Branch: $BRANCH_NAME" + +git checkout -b "$BRANCH_NAME" +git add docs/src/ + +# Build file list for commit message +FILE_LIST=$(echo "$files_with_callouts" | sed "s|$REPO_ROOT/||" | sed 's/^/- /') + +git commit -m "docs: Remove Preview callouts for stable release + +Features documented with Preview callouts are now in Stable. + +Files updated: +$FILE_LIST" + +git push -u origin "$BRANCH_NAME" + +gh pr create \ + --title "docs: Remove Preview callouts for stable release" \ + --body "This PR removes Preview callouts from documentation for features that are now in Stable. + +## Files Updated + +$(echo "$files_with_callouts" | sed "s|$REPO_ROOT/|• |") + +## What This Does + +Removes callouts like: +\`\`\`markdown +> **Preview:** This feature is available in Zed Preview. It will be included in the next Stable release. +\`\`\` + +And: +\`\`\`markdown +> **Changed in Preview (v0.XXX).** See [release notes](/releases#0.XXX). +\`\`\` + +These features are now in Stable, so the callouts are no longer needed." \ + --label "documentation" + +PR_URL=$(gh pr view --json url --jq '.url') + +echo "" +echo -e "${GREEN}Done!${NC}" +echo "" +echo "PR created: $PR_URL" +echo "" +echo "Next steps:" +echo "1. Review the PR to ensure callouts were removed correctly" +echo "2. Merge the PR as part of the stable release" diff --git a/script/docs-suggest b/script/docs-suggest new file mode 100755 index 0000000000000000000000000000000000000000..a33184cf04d6edb7310156f47e806121975f2753 --- /dev/null +++ b/script/docs-suggest @@ -0,0 +1,616 @@ +#!/usr/bin/env bash +# +# Analyze code changes and suggest documentation updates. +# +# Usage: +# script/docs-suggest --pr 49177 # Analyze a PR (immediate mode) +# script/docs-suggest --commit abc123 # Analyze a commit +# script/docs-suggest --diff file.patch # Analyze a diff file +# script/docs-suggest --staged # Analyze staged changes +# +# Modes: +# --batch Append suggestions to batch file for later PR (default for main) +# --immediate Output suggestions directly (default for cherry-picks) +# +# Options: +# --dry-run Show assembled context without calling LLM +# --output FILE Write suggestions to file instead of stdout (immediate mode) +# --verbose Show detailed progress +# --model NAME Override default model +# --preview Add preview callout to suggested docs (auto-detected) +# +# Batch mode: +# Suggestions are appended to docs/.suggestions/pending.md +# Use script/docs-suggest-publish to create a PR from batched suggestions +# +# Examples: +# # Analyze a PR to main (batches by default) +# script/docs-suggest --pr 49100 +# +# # Analyze a cherry-pick PR (immediate by default) +# script/docs-suggest --pr 49152 +# +# # Force immediate output for testing +# script/docs-suggest --pr 49100 --immediate +# +# # Dry run to see context +# script/docs-suggest --pr 49100 --dry-run + +set -euo pipefail + +# Defaults +MODE="" +TARGET="" +DRY_RUN=false +VERBOSE=false +OUTPUT="" +MODEL="${DROID_MODEL:-claude-sonnet-4-5-20250929}" +OUTPUT_MODE="" # batch or immediate, auto-detected if not set +ADD_PREVIEW_CALLOUT="" # auto-detected if not set + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log() { + if [[ "$VERBOSE" == "true" ]]; then + echo -e "${BLUE}[docs-suggest]${NC} $*" >&2 + fi +} + +error() { + echo -e "${RED}Error:${NC} $*" >&2 + exit 1 +} + +warn() { + echo -e "${YELLOW}Warning:${NC} $*" >&2 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --pr) + MODE="pr" + TARGET="$2" + shift 2 + ;; + --commit) + MODE="commit" + TARGET="$2" + shift 2 + ;; + --diff) + MODE="diff" + TARGET="$2" + shift 2 + ;; + --staged) + MODE="staged" + shift + ;; + --batch) + OUTPUT_MODE="batch" + shift + ;; + --immediate) + OUTPUT_MODE="immediate" + shift + ;; + --preview) + ADD_PREVIEW_CALLOUT="true" + shift + ;; + --no-preview) + ADD_PREVIEW_CALLOUT="false" + shift + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --verbose) + VERBOSE=true + shift + ;; + --output) + OUTPUT="$2" + shift 2 + ;; + --model) + MODEL="$2" + shift 2 + ;; + -h|--help) + head -42 "$0" | tail -40 + exit 0 + ;; + *) + error "Unknown option: $1" + ;; + esac +done + +# Validate mode +if [[ -z "$MODE" ]]; then + error "Must specify one of: --pr, --commit, --diff, or --staged" +fi + +# Get repo root +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +# Batch file location +BATCH_DIR="$REPO_ROOT/docs/.suggestions" +BATCH_FILE="$BATCH_DIR/pending.md" + +# Temp directory for context assembly +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +log "Mode: $MODE, Target: ${TARGET:-staged changes}" +log "Temp dir: $TMPDIR" + +# ============================================================================ +# Step 1: Get the diff and detect context +# ============================================================================ + +get_diff() { + case $MODE in + pr) + if ! command -v gh &> /dev/null; then + error "gh CLI required for --pr mode. Install: https://cli.github.com" + fi + log "Fetching PR #$TARGET info..." + + # Get PR metadata for auto-detection + PR_JSON=$(gh pr view "$TARGET" --json baseRefName,title,number) + PR_BASE=$(echo "$PR_JSON" | grep -o '"baseRefName":"[^"]*"' | cut -d'"' -f4) + PR_TITLE=$(echo "$PR_JSON" | grep -o '"title":"[^"]*"' | cut -d'"' -f4) + PR_NUMBER=$(echo "$PR_JSON" | grep -o '"number":[0-9]*' | cut -d':' -f2) + + log "PR #$PR_NUMBER: $PR_TITLE (base: $PR_BASE)" + + # Auto-detect output mode based on target branch + if [[ -z "$OUTPUT_MODE" ]]; then + if [[ "$PR_BASE" == "main" ]]; then + OUTPUT_MODE="batch" + log "Auto-detected: batch mode (PR targets main)" + else + OUTPUT_MODE="immediate" + log "Auto-detected: immediate mode (PR targets $PR_BASE)" + fi + fi + + # Auto-detect preview callout + if [[ -z "$ADD_PREVIEW_CALLOUT" ]]; then + if [[ "$PR_BASE" == "main" ]]; then + ADD_PREVIEW_CALLOUT="true" + log "Auto-detected: will add preview callout (new feature going to main)" + else + # Cherry-pick to release branch - check if it's preview or stable + ADD_PREVIEW_CALLOUT="false" + log "Auto-detected: no preview callout (cherry-pick)" + fi + fi + + # Store metadata for batch mode + echo "$PR_NUMBER" > "$TMPDIR/pr_number" + echo "$PR_TITLE" > "$TMPDIR/pr_title" + echo "$PR_BASE" > "$TMPDIR/pr_base" + + log "Fetching PR #$TARGET diff..." + gh pr diff "$TARGET" > "$TMPDIR/changes.diff" + gh pr diff "$TARGET" --name-only > "$TMPDIR/changed_files.txt" + ;; + commit) + log "Getting commit $TARGET diff..." + git show "$TARGET" --format="" > "$TMPDIR/changes.diff" + git show "$TARGET" --format="" --name-only > "$TMPDIR/changed_files.txt" + + # Default to immediate for commits + OUTPUT_MODE="${OUTPUT_MODE:-immediate}" + ADD_PREVIEW_CALLOUT="${ADD_PREVIEW_CALLOUT:-false}" + ;; + diff) + if [[ ! -f "$TARGET" ]]; then + error "Diff file not found: $TARGET" + fi + log "Using provided diff file..." + cp "$TARGET" "$TMPDIR/changes.diff" + grep -E '^\+\+\+ b/' "$TARGET" | sed 's|^+++ b/||' > "$TMPDIR/changed_files.txt" || true + + OUTPUT_MODE="${OUTPUT_MODE:-immediate}" + ADD_PREVIEW_CALLOUT="${ADD_PREVIEW_CALLOUT:-false}" + ;; + staged) + log "Getting staged changes..." + git diff --cached > "$TMPDIR/changes.diff" + git diff --cached --name-only > "$TMPDIR/changed_files.txt" + + OUTPUT_MODE="${OUTPUT_MODE:-immediate}" + ADD_PREVIEW_CALLOUT="${ADD_PREVIEW_CALLOUT:-false}" + ;; + esac + + if [[ ! -s "$TMPDIR/changes.diff" ]]; then + error "No changes found" + fi + + log "Found $(wc -l < "$TMPDIR/changed_files.txt" | tr -d ' ') changed files" + log "Output mode: $OUTPUT_MODE, Preview callout: $ADD_PREVIEW_CALLOUT" +} + +# ============================================================================ +# Step 2: Filter to relevant changes +# ============================================================================ + +filter_changes() { + log "Filtering to documentation-relevant changes..." + + # Keep only source code changes (not tests, not CI, not docs themselves) + grep -E '^crates/.*\.rs$' "$TMPDIR/changed_files.txt" | \ + grep -v '_test\.rs$' | \ + grep -v '/tests/' | \ + grep -v '/test_' > "$TMPDIR/source_files.txt" || true + + # Also track if settings/keybindings changed + grep -E '(settings|keymap|actions)' "$TMPDIR/changed_files.txt" > "$TMPDIR/config_files.txt" || true + + local source_count=$(wc -l < "$TMPDIR/source_files.txt" | tr -d ' ') + local config_count=$(wc -l < "$TMPDIR/config_files.txt" | tr -d ' ') + + log "Relevant files: $source_count source, $config_count config" + + if [[ "$source_count" -eq 0 && "$config_count" -eq 0 ]]; then + echo "No documentation-relevant changes detected (only tests, CI, or docs modified)." + exit 0 + fi +} + +# ============================================================================ +# Step 3: Assemble context +# ============================================================================ + +assemble_context() { + log "Assembling context..." + + # Start the prompt + cat > "$TMPDIR/prompt.md" << 'PROMPT_HEADER' +# Documentation Suggestion Request + +You are analyzing code changes to determine if documentation updates are needed. + +## Your Task + +1. Analyze the diff below for user-facing changes +2. Determine if any documentation updates are warranted +3. If yes, provide specific, actionable suggestions +4. If no, explain why no updates are needed + +## Guidelines + +PROMPT_HEADER + + # Add conventions + log "Adding documentation conventions..." + cat >> "$TMPDIR/prompt.md" << 'CONVENTIONS' +### Documentation Conventions + +- **Voice**: Second person ("you"), present tense, direct and concise +- **No hedging**: Avoid "simply", "just", "easily" +- **Settings pattern**: Show Settings Editor UI first, then JSON as alternative +- **Keybindings**: Use `{#kb action::Name}` syntax, not hardcoded keys +- **Terminology**: "folder" not "directory", "project" not "workspace", "Settings Editor" not "settings UI" +- **SEO keyword targeting**: For each docs page you suggest updating, choose one + primary keyword/intent phrase using the page's user intent +- **SEO metadata**: Every updated/new docs page should include frontmatter with + `title` and `description` (single-line `key: value` entries) +- **Metadata quality**: Titles should clearly state page intent (~50-60 chars), + descriptions should summarize the reader outcome (~140-160 chars) +- **Keyword usage**: Use the primary keyword naturally in frontmatter and in page + body at least once; never keyword-stuff +- **SEO structure**: Keep exactly one H1 and preserve logical H1→H2→H3 + hierarchy +- **Internal links minimum**: Non-reference pages should include at least 3 + useful internal docs links; reference pages can include fewer when extra links + would be noise +- **Marketing links**: For main feature pages, include a relevant `zed.dev` + marketing link alongside docs links + +### Brand Voice Rubric (Required) + +For suggested doc text, apply the brand rubric scoring exactly and only pass text +that scores 4+ on every criterion: + +| Criterion | +| -------------------- | +| Technical Grounding | +| Natural Syntax | +| Quiet Confidence | +| Developer Respect | +| Information Priority | +| Specificity | +| Voice Consistency | +| Earned Claims | + +Pass threshold: all criteria 4+ (minimum 32/40 total). + +Also reject suggestions containing obvious taboo phrasing (hype, emotional +manipulation, or marketing-style superlatives). + +For every docs file you suggest changing, treat the entire file as in scope for +brand review (not only the edited section). Include any additional full-file +voice fixes needed to reach passing rubric scores. + +### What Requires Documentation + +- New user-facing features or commands +- Changed keybindings or default behaviors +- New or modified settings +- Deprecated or removed functionality + +### What Does NOT Require Documentation + +- Internal refactoring without behavioral changes +- Performance optimizations (unless user-visible) +- Bug fixes that restore documented behavior +- Test changes, CI changes +CONVENTIONS + + # Add preview callout instructions if needed + if [[ "$ADD_PREVIEW_CALLOUT" == "true" ]]; then + # Get current preview version for modification callouts + local preview_version + preview_version=$(gh release list --limit 5 2>/dev/null | grep -E '\-pre\s' | head -1 | grep -oE 'v[0-9]+\.[0-9]+' | head -1 || echo "v0.XXX") + preview_version="${preview_version#v}" # Remove leading 'v' + + cat >> "$TMPDIR/prompt.md" << PREVIEW_INSTRUCTIONS + +### Preview Release Callouts + +This change is going into Zed Preview first. Use the appropriate callout based on the type of change: + +#### For NEW/ADDITIVE features (new commands, new settings, new UI elements): + +\`\`\`markdown +> **Preview:** This feature is available in Zed Preview. It will be included in the next Stable release. +\`\`\` + +#### For BEHAVIOR MODIFICATIONS (changed defaults, altered behavior of existing features): + +\`\`\`markdown +> **Changed in Preview (v${preview_version}).** See [release notes](/releases#${preview_version}). +\`\`\` + +**Guidelines:** +- Use the "Preview" callout for entirely new features or sections +- Use the "Changed in Preview" callout when modifying documentation of existing behavior +- Place callouts immediately after the section heading, before any content +- Both callout types will be stripped when the feature ships to Stable +PREVIEW_INSTRUCTIONS + fi + + echo "" >> "$TMPDIR/prompt.md" + echo "## Changed Files" >> "$TMPDIR/prompt.md" + echo "" >> "$TMPDIR/prompt.md" + echo '```' >> "$TMPDIR/prompt.md" + cat "$TMPDIR/changed_files.txt" >> "$TMPDIR/prompt.md" + echo '```' >> "$TMPDIR/prompt.md" + echo "" >> "$TMPDIR/prompt.md" + + # Add the diff (truncated if huge) + echo "## Code Diff" >> "$TMPDIR/prompt.md" + echo "" >> "$TMPDIR/prompt.md" + + local diff_lines=$(wc -l < "$TMPDIR/changes.diff" | tr -d ' ') + if [[ "$diff_lines" -gt 2000 ]]; then + warn "Diff is large ($diff_lines lines), truncating to 2000 lines" + echo '```diff' >> "$TMPDIR/prompt.md" + head -2000 "$TMPDIR/changes.diff" >> "$TMPDIR/prompt.md" + echo "" >> "$TMPDIR/prompt.md" + echo "[... truncated, $((diff_lines - 2000)) more lines ...]" >> "$TMPDIR/prompt.md" + echo '```' >> "$TMPDIR/prompt.md" + else + echo '```diff' >> "$TMPDIR/prompt.md" + cat "$TMPDIR/changes.diff" >> "$TMPDIR/prompt.md" + echo '```' >> "$TMPDIR/prompt.md" + fi + + # Add output format instructions + cat >> "$TMPDIR/prompt.md" << 'OUTPUT_FORMAT' + +## Output Format + +Respond with ONE of these formats: + +### If documentation updates ARE needed: + +```markdown +## Documentation Suggestions + +### Summary +[1-2 sentence summary of what changed and why docs need updating] + +### Suggested Changes + +#### 1. [docs/src/path/to/file.md] +- **Section**: [existing section to update, or "New section"] +- **Change**: [Add/Update/Remove] +- **Target keyword**: [single keyword/intent phrase for this page] +- **Frontmatter**: + ```yaml + --- + title: ... + description: ... + --- + ``` +- **Links**: [List at least 3 internal docs links for non-reference pages; if + this is a main feature page, include one relevant `zed.dev` marketing link] +- **Suggestion**: [Specific text or description of what to add/change] +- **Full-file brand pass**: [Required: yes. Note any additional voice edits + elsewhere in the same file needed to pass rubric across the entire file.] +- **Brand voice scorecard**: + + | Criterion | Score | Notes | + | -------------------- | ----- | ----- | + | Technical Grounding | /5 | | + | Natural Syntax | /5 | | + | Quiet Confidence | /5 | | + | Developer Respect | /5 | | + | Information Priority | /5 | | + | Specificity | /5 | | + | Voice Consistency | /5 | | + | Earned Claims | /5 | | + | **TOTAL** | /40 | | + + Pass threshold: all criteria 4+. + +#### 2. [docs/src/another/file.md] +... + +### Notes for Reviewer +[Any context or uncertainty worth flagging] +``` + +### If NO documentation updates are needed: + +```markdown +## No Documentation Updates Needed + +**Reason**: [Brief explanation - e.g., "Internal refactoring only", "Test changes", "Bug fix restoring existing behavior"] + +**Changes reviewed**: +- [Brief summary of what the code changes do] +- [Why they don't affect user-facing documentation] +``` + +Be conservative. Only suggest documentation changes when there's a clear user-facing impact. +OUTPUT_FORMAT + + log "Context assembled: $(wc -l < "$TMPDIR/prompt.md" | tr -d ' ') lines" +} + +# ============================================================================ +# Step 4: Run the analysis +# ============================================================================ + +run_analysis() { + if [[ "$DRY_RUN" == "true" ]]; then + echo -e "${GREEN}=== DRY RUN: Assembled Context ===${NC}" + echo "" + echo "Output mode: $OUTPUT_MODE" + echo "Preview callout: $ADD_PREVIEW_CALLOUT" + if [[ "$OUTPUT_MODE" == "batch" ]]; then + echo "Batch file: $BATCH_FILE" + fi + echo "" + cat "$TMPDIR/prompt.md" + echo "" + echo -e "${GREEN}=== End Context ===${NC}" + echo "" + echo "To run for real, remove --dry-run flag" + return + fi + + # Check for droid CLI + if ! command -v droid &> /dev/null; then + error "droid CLI required. Install from: https://app.factory.ai/cli" + fi + + log "Running analysis with model: $MODEL" + + # Run the LLM + local suggestions + suggestions=$(droid exec -m "$MODEL" -f "$TMPDIR/prompt.md") + + # Handle output based on mode + if [[ "$OUTPUT_MODE" == "batch" ]]; then + append_to_batch "$suggestions" + else + output_immediate "$suggestions" + fi +} + +# ============================================================================ +# Output handlers +# ============================================================================ + +append_to_batch() { + local suggestions="$1" + + # Check if suggestions indicate no updates needed + if echo "$suggestions" | grep -q "No Documentation Updates Needed"; then + log "No documentation updates needed, skipping batch" + echo "$suggestions" + return + fi + + # Create batch directory if needed + mkdir -p "$BATCH_DIR" + + # Get PR info if available + local pr_number="" + local pr_title="" + if [[ -f "$TMPDIR/pr_number" ]]; then + pr_number=$(cat "$TMPDIR/pr_number") + pr_title=$(cat "$TMPDIR/pr_title") + fi + + # Initialize batch file if it doesn't exist + if [[ ! -f "$BATCH_FILE" ]]; then + cat > "$BATCH_FILE" << 'BATCH_HEADER' +# Pending Documentation Suggestions + +This file contains batched documentation suggestions for the next Preview release. +Run `script/docs-suggest-publish` to create a PR from these suggestions. + +--- + +BATCH_HEADER + fi + + # Append suggestions with metadata + { + echo "" + echo "## PR #$pr_number: $pr_title" + echo "" + echo "_Added: $(date -u +%Y-%m-%dT%H:%M:%SZ)_" + echo "" + echo "$suggestions" + echo "" + echo "---" + } >> "$BATCH_FILE" + + echo -e "${GREEN}Suggestions batched to:${NC} $BATCH_FILE" + echo "" + echo "Batched suggestions for PR #$pr_number" + echo "Run 'script/docs-suggest-publish' when ready to create the docs PR." +} + +output_immediate() { + local suggestions="$1" + + if [[ -n "$OUTPUT" ]]; then + echo "$suggestions" > "$OUTPUT" + echo "Suggestions written to: $OUTPUT" + else + echo "$suggestions" + fi +} + +# ============================================================================ +# Main +# ============================================================================ + +main() { + get_diff + filter_changes + assemble_context + run_analysis +} + +main diff --git a/script/docs-suggest-publish b/script/docs-suggest-publish new file mode 100755 index 0000000000000000000000000000000000000000..f65b597a6e0c34f88e58532062c59424c5d5a764 --- /dev/null +++ b/script/docs-suggest-publish @@ -0,0 +1,378 @@ +#!/usr/bin/env bash +# +# Create a draft documentation PR by auto-applying batched suggestions. +# +# Usage: +# script/docs-suggest-publish [--dry-run] [--model MODEL] +# +# This script: +# 1. Reads pending suggestions from the docs/suggestions-pending branch +# 2. Uses Droid to apply all suggestions directly to docs files +# 3. Runs docs formatting +# 4. Creates a draft PR for human review/merge +# 5. Optionally resets the suggestions branch after successful PR creation +# +# Options: +# --dry-run Show what would be done without creating PR +# --keep-queue Don't reset the suggestions branch after PR creation +# --model MODEL Override Droid model used for auto-apply +# --verbose Show detailed progress +# +# Run this as part of the preview release workflow. + +set -euo pipefail + +DRY_RUN=false +KEEP_QUEUE=false +VERBOSE=false +MODEL="${DROID_MODEL:-claude-sonnet-4-5-20250929}" + +SUGGESTIONS_BRANCH="docs/suggestions-pending" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { + if [[ "$VERBOSE" == "true" ]]; then + echo -e "${BLUE}[docs-publish]${NC} $*" >&2 + fi +} + +error() { + echo -e "${RED}Error:${NC} $*" >&2 + exit 1 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + --keep-queue) + KEEP_QUEUE=true + shift + ;; + --verbose) + VERBOSE=true + shift + ;; + --model) + MODEL="$2" + shift 2 + ;; + -h|--help) + head -26 "$0" | tail -24 + exit 0 + ;; + *) + error "Unknown option: $1" + ;; + esac +done + +# Get repo root +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +# Check if suggestions branch exists +log "Checking for suggestions branch..." +if ! git ls-remote --exit-code --heads origin "$SUGGESTIONS_BRANCH" > /dev/null 2>&1; then + echo "No pending suggestions found (branch $SUGGESTIONS_BRANCH doesn't exist)." + echo "Suggestions are queued automatically when PRs are merged to main." + exit 0 +fi + +# Fetch the suggestions branch +log "Fetching suggestions branch..." +git fetch origin "$SUGGESTIONS_BRANCH" + +# Check for manifest +if ! git show "origin/$SUGGESTIONS_BRANCH:manifest.json" > /dev/null 2>&1; then + echo "No manifest found on suggestions branch." + exit 0 +fi + +# Read manifest +MANIFEST=$(git show "origin/$SUGGESTIONS_BRANCH:manifest.json") +SUGGESTION_COUNT=$(echo "$MANIFEST" | jq '.suggestions | length') + +if [[ "$SUGGESTION_COUNT" -eq 0 ]]; then + echo "No pending suggestions in queue." + exit 0 +fi + +echo "Found $SUGGESTION_COUNT pending suggestion(s):" +echo "" +echo "$MANIFEST" | jq -r '.suggestions[] | " PR #\(.pr): \(.title)"' +echo "" + +if [[ "$DRY_RUN" == "true" ]]; then + echo -e "${YELLOW}=== DRY RUN ===${NC}" + echo "" + echo "Would auto-apply suggestions to docs via Droid and create a draft PR." + echo "Model: $MODEL" + echo "" + + # Show each suggestion file + for file in $(echo "$MANIFEST" | jq -r '.suggestions[].file'); do + echo "--- $file ---" + git show "origin/$SUGGESTIONS_BRANCH:$file" 2>/dev/null || echo "(file not found)" + echo "" + done + + echo -e "${YELLOW}=== END DRY RUN ===${NC}" + echo "" + echo "Run without --dry-run to create the PR." + exit 0 +fi + +# Ensure clean working state +if [[ -n "$(git status --porcelain)" ]]; then + error "Working directory has uncommitted changes. Please commit or stash first." +fi + +for command in git gh jq droid; do + if ! command -v "$command" > /dev/null 2>&1; then + error "Required command not found: $command" + fi +done + +# Remember current branch +ORIGINAL_BRANCH=$(git branch --show-current) +log "Current branch: $ORIGINAL_BRANCH" + +# Create new branch for docs PR from latest main +git fetch origin main +DOCS_BRANCH="docs/preview-auto-$(date +%Y-%m-%d-%H%M%S)" +log "Creating docs branch: $DOCS_BRANCH" + +git checkout -b "$DOCS_BRANCH" origin/main + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +SUGGESTIONS_FILE="$TMPDIR/suggestions.md" +APPLY_PROMPT_FILE="$TMPDIR/apply-prompt.md" +APPLY_SUMMARY_FILE="$TMPDIR/apply-summary.md" + +# Combine queued suggestion files into one input +for file in $(echo "$MANIFEST" | jq -r '.suggestions[].file'); do + { + echo "## Source: $file" + echo "" + git show "origin/$SUGGESTIONS_BRANCH:$file" 2>/dev/null || error "Suggestion file missing: $file" + echo "" + echo "---" + echo "" + } >> "$SUGGESTIONS_FILE" +done + +# Build auto-apply prompt +cat > "$APPLY_PROMPT_FILE" << 'EOF' +# Documentation Auto-Apply Request (Preview Release) + +Apply all queued documentation suggestions below directly to docs files in this repository. + +## Required behavior + +1. Apply concrete documentation edits (not suggestion text) to the appropriate files. +2. Edit only docs content files under `docs/src/` unless a suggestion explicitly requires another docs path. +3. For every docs file you modify, run a full-file brand voice pass (entire file, not only edited sections). +4. Enforce the brand rubric exactly; final file content must score 4+ on every criterion: + - Technical Grounding + - Natural Syntax + - Quiet Confidence + - Developer Respect + - Information Priority + - Specificity + - Voice Consistency + - Earned Claims +5. Keep SEO/frontmatter/linking requirements from the suggestions where applicable. +6. Keep preview callout semantics correct: + - Additive features: `> **Preview:** ...` + - Behavior modifications: `> **Changed in Preview (vX.XXX).** ...` +7. If a suggestion is too ambiguous to apply safely, skip it and explain why in the summary. + +## Output format (after making edits) + +Return markdown with: + +- `## Applied Suggestions` +- `## Skipped Suggestions` +- `## Files Updated` +- `## Brand Voice Verification` (one line per updated file confirming full-file pass) + +Do not include a patch in the response; apply edits directly to files. + +## Queued Suggestions + +EOF + +cat "$SUGGESTIONS_FILE" >> "$APPLY_PROMPT_FILE" + +log "Running Droid auto-apply with model: $MODEL" +droid exec -m "$MODEL" -f "$APPLY_PROMPT_FILE" > "$APPLY_SUMMARY_FILE" + +if [[ -n "$(git status --porcelain | grep -vE '^.. docs/' || true)" ]]; then + error "Auto-apply modified non-doc files. Revert and re-run." +fi + +if [[ -z "$(git status --porcelain docs/ | grep '^.. docs/src/' || true)" ]]; then + error "Auto-apply produced no docs/src changes." +fi + +log "Running docs formatter" +./script/prettier + +if [[ -z "$(git status --porcelain docs/ | grep '^.. docs/src/' || true)" ]]; then + error "No docs/src changes remain after formatting; aborting PR creation." +fi + +# Build PR body from suggestions +PR_BODY_FILE="$TMPDIR/pr-body.md" +cat > "$PR_BODY_FILE" << 'EOF' +# Documentation Updates for Preview Release + +This draft PR auto-applies queued documentation suggestions collected from +recently merged PRs. + +## How to Use This PR + +1. Review the applied changes file-by-file. +2. Verify brand voice on each touched file as a full-file pass. +3. Ensure docs use the correct callout type: + - Additive features: Preview callout + - Behavior modifications: Changed in Preview callout +4. Merge when ready. + +## Auto-Apply Summary + +EOF + +cat "$APPLY_SUMMARY_FILE" >> "$PR_BODY_FILE" + +cat >> "$PR_BODY_FILE" << 'EOF' + +## Preview Callouts + +Use the Preview callout for new/additive features: + +```markdown +> **Preview:** This feature is available in Zed Preview. It will be included in the next Stable release. +``` + +Use this callout for behavior modifications to existing functionality: + +```markdown +> **Changed in Preview (v0.XXX).** See [release notes](/releases#0.XXX). +``` + +--- + +## Pending Suggestions + +EOF + +# Append each suggestion to PR body +for file in $(echo "$MANIFEST" | jq -r '.suggestions[].file'); do + log "Adding $file to PR body..." + echo "" >> "$PR_BODY_FILE" + git show "origin/$SUGGESTIONS_BRANCH:$file" >> "$PR_BODY_FILE" 2>/dev/null || true + echo "" >> "$PR_BODY_FILE" + echo "---" >> "$PR_BODY_FILE" +done + +# Add tracking info +cat >> "$PR_BODY_FILE" << EOF + +## PRs Included + +EOF + +echo "$MANIFEST" | jq -r '.suggestions[] | "- [PR #\(.pr)](\(.file)): \(.title)"' >> "$PR_BODY_FILE" + +git add docs/ +git commit -m "docs: auto-apply preview release suggestions + +Auto-applied queued documentation suggestions from: +$(echo "$MANIFEST" | jq -r '.suggestions[] | "- PR #\(.pr)"') + +Generated with script/docs-suggest-publish for human review in draft PR." + +# Push and create PR +log "Pushing branch..." +git push -u origin "$DOCS_BRANCH" + +log "Creating PR..." +PR_URL=$(gh pr create \ + --draft \ + --title "docs: auto-apply preview release suggestions" \ + --body-file "$PR_BODY_FILE" \ + --label "documentation") + +echo "" +echo -e "${GREEN}PR created:${NC} $PR_URL" + +# Reset suggestions branch if not keeping +if [[ "$KEEP_QUEUE" != "true" ]]; then + echo "" + echo "Resetting suggestions queue..." + + git checkout --orphan "${SUGGESTIONS_BRANCH}-reset" + git rm -rf . > /dev/null 2>&1 || true + + 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 "Reset documentation suggestions queue + +Previous suggestions published in: $PR_URL" + + git push -f origin "${SUGGESTIONS_BRANCH}-reset:$SUGGESTIONS_BRANCH" + git checkout "$ORIGINAL_BRANCH" + git branch -D "${SUGGESTIONS_BRANCH}-reset" + + echo "Suggestions queue reset." +else + git checkout "$ORIGINAL_BRANCH" + echo "" + echo "Suggestions queue kept (--keep-queue). Remember to reset manually after PR is merged." +fi + +# Cleanup + + +echo "" +echo -e "${GREEN}Done!${NC}" +echo "" +echo "Next steps:" +echo "1. Review the draft PR and verify all auto-applied docs changes" +echo "2. Confirm full-file brand voice pass for each touched docs file" +echo "3. Mark ready for review and merge" diff --git a/script/test-docs-suggest-batch b/script/test-docs-suggest-batch new file mode 100755 index 0000000000000000000000000000000000000000..3bd6c9811a4391a11155fecf34c3520b8c1e9860 --- /dev/null +++ b/script/test-docs-suggest-batch @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# +# Test docs-suggest on multiple PRs and generate a summary report. +# +# Usage: +# script/test-docs-suggest-batch [--limit N] [--output FILE] +# +# This script runs docs-suggest in dry-run mode on recent merged PRs +# to validate the context assembly and help tune the prompt. + +set -euo pipefail + +LIMIT=50 +OUTPUT="docs-suggest-batch-results.md" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +while [[ $# -gt 0 ]]; do + case $1 in + --limit) + LIMIT="$2" + shift 2 + ;; + --output) + OUTPUT="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +echo "Testing docs-suggest on $LIMIT recent merged PRs..." +echo "Output: $OUTPUT" +echo "" + +# Get list of PRs +PRS=$(gh pr list --state merged --limit "$LIMIT" --json number,title --jq '.[] | "\(.number)|\(.title)"') + +# Initialize output file +cat > "$OUTPUT" << HEADER +# docs-suggest Batch Test Results + +**Date**: $(date +%Y-%m-%d) +**PRs tested**: $LIMIT + +## Summary + +| PR | Title | Result | Source Files | Notes | +|----|-------|--------|--------------|-------| +HEADER + +# Track stats +total=0 +has_source=0 +no_source=0 +errors=0 + +while IFS='|' read -r pr_num title; do + total=$((total + 1)) + echo -n "[$total/$LIMIT] PR #$pr_num: " + + # Run dry-run and capture output + tmpfile=$(mktemp) + if "$SCRIPT_DIR/docs-suggest" --pr "$pr_num" --dry-run 2>"$tmpfile.err" >"$tmpfile.out"; then + # Check if it found source files + if grep -q "No documentation-relevant changes" "$tmpfile.out"; then + result="No source changes" + no_source=$((no_source + 1)) + source_count="0" + echo "skipped (no source)" + else + # Extract source file count from verbose output + source_count=$(grep -oE '[0-9]+ source' "$tmpfile.err" 2>/dev/null | grep -oE '[0-9]+' || echo "?") + result="Has source changes" + has_source=$((has_source + 1)) + echo "has $source_count source files" + fi + notes="" + else + result="Error" + errors=$((errors + 1)) + source_count="-" + notes=$(head -1 "$tmpfile.err" 2>/dev/null || echo "unknown error") + echo "error" + fi + + # Escape title for markdown table + title_escaped=$(echo "$title" | sed 's/|/\\|/g' | cut -c1-60) + + # Add row to table + echo "| [#$pr_num](https://github.com/zed-industries/zed/pull/$pr_num) | $title_escaped | $result | $source_count | $notes |" >> "$OUTPUT" + + rm -f "$tmpfile" "$tmpfile.out" "$tmpfile.err" +done <<< "$PRS" + +# Add summary stats +cat >> "$OUTPUT" << STATS + +## Statistics + +- **Total PRs**: $total +- **With source changes**: $has_source ($(( has_source * 100 / total ))%) +- **No source changes**: $no_source ($(( no_source * 100 / total ))%) +- **Errors**: $errors + +## Observations + +_Add manual observations here after reviewing results._ + +## Sample Contexts + +STATS + +# Add 3 sample contexts from PRs with source changes +echo "" >> "$OUTPUT" +echo "### Sample 1: PR with source changes" >> "$OUTPUT" +echo "" >> "$OUTPUT" + +sample_pr=$(gh pr list --state merged --limit 20 --json number --jq '.[].number' | while read pr; do + if "$SCRIPT_DIR/docs-suggest" --pr "$pr" --dry-run 2>/dev/null | grep -q "## Code Diff"; then + echo "$pr" + break + fi +done) + +if [[ -n "$sample_pr" ]]; then + echo "PR #$sample_pr:" >> "$OUTPUT" + echo "" >> "$OUTPUT" + echo '```' >> "$OUTPUT" + "$SCRIPT_DIR/docs-suggest" --pr "$sample_pr" --dry-run 2>/dev/null | head -100 >> "$OUTPUT" + echo '```' >> "$OUTPUT" +fi + +echo "" +echo "Done! Results written to: $OUTPUT" +echo "" +echo "Stats: $has_source with source changes, $no_source without, $errors errors"