docs-suggest-publish

  1#!/usr/bin/env bash
  2#
  3# Create a draft documentation PR by auto-applying batched suggestions.
  4#
  5# Usage:
  6#   script/docs-suggest-publish [--dry-run] [--model MODEL]
  7#
  8# This script:
  9# 1. Reads pending suggestions from the docs/suggestions-pending branch
 10# 2. Uses Droid to apply suggestions in batches (default 10 per batch)
 11# 3. Runs docs formatting
 12# 4. Validates docs build (action references, JSON schemas, links)
 13# 5. Creates a draft PR for human review/merge
 14# 6. Optionally resets the suggestions branch after successful PR creation
 15#
 16# Options:
 17#   --dry-run          Show what would be done without creating PR
 18#   --keep-queue       Don't reset the suggestions branch after PR creation
 19#   --model MODEL      Override Droid model used for auto-apply
 20#   --batch-size N     Suggestions per Droid invocation (default: 10)
 21#   --skip-validation  Skip the docs build validation step
 22#   --verbose          Show detailed progress
 23#
 24# Run this as part of the preview release workflow.
 25
 26set -euo pipefail
 27
 28DRY_RUN=false
 29KEEP_QUEUE=false
 30VERBOSE=false
 31SKIP_VALIDATION=false
 32BATCH_SIZE=10
 33MODEL="${DROID_MODEL:-claude-sonnet-4-5-latest}"
 34
 35SUGGESTIONS_BRANCH="docs/suggestions-pending"
 36
 37# Colors
 38RED='\033[0;31m'
 39GREEN='\033[0;32m'
 40YELLOW='\033[0;33m'
 41BLUE='\033[0;34m'
 42NC='\033[0m'
 43
 44log() {
 45    if [[ "$VERBOSE" == "true" ]]; then
 46        echo -e "${BLUE}[docs-publish]${NC} $*" >&2
 47    fi
 48}
 49
 50error() {
 51    echo -e "${RED}Error:${NC} $*" >&2
 52    exit 1
 53}
 54
 55# Parse arguments
 56while [[ $# -gt 0 ]]; do
 57    case $1 in
 58        --dry-run)
 59            DRY_RUN=true
 60            shift
 61            ;;
 62        --keep-queue)
 63            KEEP_QUEUE=true
 64            shift
 65            ;;
 66        --verbose)
 67            VERBOSE=true
 68            shift
 69            ;;
 70        --model)
 71            MODEL="$2"
 72            shift 2
 73            ;;
 74        --batch-size)
 75            BATCH_SIZE="$2"
 76            shift 2
 77            ;;
 78        --skip-validation)
 79            SKIP_VALIDATION=true
 80            shift
 81            ;;
 82        -h|--help)
 83            head -30 "$0" | tail -28
 84            exit 0
 85            ;;
 86        *)
 87            error "Unknown option: $1"
 88            ;;
 89    esac
 90done
 91
 92# Get repo root
 93REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
 94cd "$REPO_ROOT"
 95
 96# Check if suggestions branch exists
 97log "Checking for suggestions branch..."
 98if ! git ls-remote --exit-code --heads origin "$SUGGESTIONS_BRANCH" > /dev/null 2>&1; then
 99    echo "No pending suggestions found (branch $SUGGESTIONS_BRANCH doesn't exist)."
100    echo "Suggestions are queued automatically when PRs are merged to main."
101    exit 0
102fi
103
104# Fetch the suggestions branch
105log "Fetching suggestions branch..."
106git fetch origin "$SUGGESTIONS_BRANCH"
107
108# Check for manifest
109if ! git show "origin/$SUGGESTIONS_BRANCH:manifest.json" > /dev/null 2>&1; then
110    echo "No manifest found on suggestions branch."
111    exit 0
112fi
113
114# Read manifest
115MANIFEST=$(git show "origin/$SUGGESTIONS_BRANCH:manifest.json")
116SUGGESTION_COUNT=$(echo "$MANIFEST" | jq '.suggestions | length')
117
118if [[ "$SUGGESTION_COUNT" -eq 0 ]]; then
119    echo "No pending suggestions in queue."
120    exit 0
121fi
122
123echo "Found $SUGGESTION_COUNT pending suggestion(s):"
124echo ""
125echo "$MANIFEST" | jq -r '.suggestions[] | "  PR #\(.pr): \(.title)"'
126echo ""
127
128if [[ "$DRY_RUN" == "true" ]]; then
129    echo -e "${YELLOW}=== DRY RUN ===${NC}"
130    echo ""
131    echo "Would auto-apply suggestions to docs via Droid and create a draft PR."
132    echo "Model: $MODEL"
133    echo ""
134    
135    # Show each suggestion file
136    for file in $(echo "$MANIFEST" | jq -r '.suggestions[].file'); do
137        echo "--- $file ---"
138        git show "origin/$SUGGESTIONS_BRANCH:$file" 2>/dev/null || echo "(file not found)"
139        echo ""
140    done
141    
142    echo -e "${YELLOW}=== END DRY RUN ===${NC}"
143    echo ""
144    echo "Run without --dry-run to create the PR."
145    exit 0
146fi
147
148# Ensure clean working state (ignore untracked files with grep -v '??')
149if [[ -n "$(git status --porcelain | grep -v '^??' || true)" ]]; then
150    error "Working directory has uncommitted changes. Please commit or stash first."
151fi
152
153REQUIRED_COMMANDS=(git gh jq droid)
154if [[ "$SKIP_VALIDATION" != "true" ]]; then
155    REQUIRED_COMMANDS+=(mdbook)
156fi
157for command in "${REQUIRED_COMMANDS[@]}"; do
158    if ! command -v "$command" > /dev/null 2>&1; then
159        error "Required command not found: $command"
160    fi
161done
162
163# Remember current branch
164ORIGINAL_BRANCH=$(git branch --show-current)
165log "Current branch: $ORIGINAL_BRANCH"
166
167# Create new branch for docs PR from latest main
168git fetch origin main
169DOCS_BRANCH="docs/preview-auto-$(date +%Y-%m-%d-%H%M%S)"
170log "Creating docs branch: $DOCS_BRANCH"
171
172git checkout -b "$DOCS_BRANCH" origin/main
173
174TMPDIR=$(mktemp -d)
175trap 'rm -rf "$TMPDIR"' EXIT
176
177APPLY_SUMMARY_FILE="$TMPDIR/apply-summary.md"
178touch "$APPLY_SUMMARY_FILE"
179
180# Collect suggestion files into an array
181SUGGESTION_FILES=()
182while IFS= read -r file; do
183    SUGGESTION_FILES+=("$file")
184done < <(echo "$MANIFEST" | jq -r '.suggestions[].file')
185
186# Determine which PRs are already in the latest stable release.
187# Suggestions queued with --preview may reference features that shipped in stable
188# by the time this script runs, so their Preview callouts should be stripped.
189STABLE_PRS=()
190STABLE_TAG=$(git tag -l 'v*' --sort=-v:refname | grep -v 'pre' | head -1 || true)
191if [[ -n "$STABLE_TAG" ]]; then
192    log "Latest stable release tag: $STABLE_TAG"
193    for file in "${SUGGESTION_FILES[@]}"; do
194        pr_num=$(echo "$MANIFEST" | jq -r --arg f "$file" '.suggestions[] | select(.file == $f) | .pr')
195        # Find the merge commit for this PR
196        merge_sha=$(gh pr view "$pr_num" --json mergeCommit --jq '.mergeCommit.oid' 2>/dev/null || true)
197        if [[ -n "$merge_sha" ]] && git merge-base --is-ancestor "$merge_sha" "$STABLE_TAG" 2>/dev/null; then
198            STABLE_PRS+=("$pr_num")
199            log "PR #$pr_num is in stable ($STABLE_TAG)"
200        fi
201    done
202    if [[ ${#STABLE_PRS[@]} -gt 0 ]]; then
203        echo -e "${YELLOW}Note:${NC} ${#STABLE_PRS[@]} suggestion(s) are for PRs already in stable ($STABLE_TAG)."
204        echo "  Preview callouts will be stripped for: ${STABLE_PRS[*]}"
205        echo ""
206    fi
207else
208    log "No stable release tag found, treating all suggestions as preview-only"
209fi
210
211# Determine which PRs touch code gated behind feature flags.
212# Features behind flags aren't generally available and shouldn't be documented yet.
213FLAGGED_PRS=()
214FLAGS_FILE="$REPO_ROOT/crates/feature_flags/src/flags.rs"
215if [[ -f "$FLAGS_FILE" ]]; then
216    # Extract feature flag struct names (e.g. SubagentsFeatureFlag, GitGraphFeatureFlag)
217    FLAG_NAMES=$(grep -oE 'pub struct \w+FeatureFlag' "$FLAGS_FILE" | awk '{print $3}')
218    if [[ -n "$FLAG_NAMES" ]]; then
219        FLAG_PATTERN=$(echo "$FLAG_NAMES" | tr '\n' '|' | sed 's/|$//')
220        log "Feature flags found: $(echo "$FLAG_NAMES" | tr '\n' ' ')"
221        for file in "${SUGGESTION_FILES[@]}"; do
222            pr_num=$(echo "$MANIFEST" | jq -r --arg f "$file" '.suggestions[] | select(.file == $f) | .pr')
223            # Skip PRs already marked as stable (no need to double-check)
224            is_already_stable=false
225            for stable_pr in "${STABLE_PRS[@]+"${STABLE_PRS[@]}"}"; do
226                if [[ "$stable_pr" == "$pr_num" ]]; then
227                    is_already_stable=true
228                    break
229                fi
230            done
231            if [[ "$is_already_stable" == "true" ]]; then
232                continue
233            fi
234            # Check if the PR diff references any feature flag
235            pr_diff=$(gh pr diff "$pr_num" 2>/dev/null || true)
236            if [[ -n "$pr_diff" ]] && echo "$pr_diff" | grep -qE "$FLAG_PATTERN"; then
237                matched_flags=$(echo "$pr_diff" | grep -oE "$FLAG_PATTERN" | sort -u | tr '\n' ', ' | sed 's/,$//')
238                FLAGGED_PRS+=("$pr_num")
239                log "PR #$pr_num is behind feature flag(s): $matched_flags"
240            fi
241        done
242        if [[ ${#FLAGGED_PRS[@]} -gt 0 ]]; then
243            echo -e "${YELLOW}Note:${NC} ${#FLAGGED_PRS[@]} suggestion(s) are for features behind feature flags."
244            echo "  These will be skipped: ${FLAGGED_PRS[*]}"
245            echo ""
246        fi
247    fi
248else
249    log "Feature flags file not found, skipping flag detection"
250fi
251
252# Split into batches
253TOTAL=${#SUGGESTION_FILES[@]}
254BATCH_COUNT=$(( (TOTAL + BATCH_SIZE - 1) / BATCH_SIZE ))
255
256if [[ "$BATCH_COUNT" -gt 1 ]]; then
257    echo "Processing $TOTAL suggestions in $BATCH_COUNT batches of up to $BATCH_SIZE..."
258else
259    echo "Processing $TOTAL suggestions..."
260fi
261echo ""
262
263for (( batch=0; batch<BATCH_COUNT; batch++ )); do
264    START=$(( batch * BATCH_SIZE ))
265    END=$(( START + BATCH_SIZE ))
266    if [[ "$END" -gt "$TOTAL" ]]; then
267        END=$TOTAL
268    fi
269
270    BATCH_NUM=$(( batch + 1 ))
271    BATCH_SUGGESTIONS_FILE="$TMPDIR/batch-${BATCH_NUM}-suggestions.md"
272    BATCH_PROMPT_FILE="$TMPDIR/batch-${BATCH_NUM}-prompt.md"
273    BATCH_SUMMARY_FILE="$TMPDIR/batch-${BATCH_NUM}-summary.md"
274
275    echo -e "${BLUE}Batch $BATCH_NUM/$BATCH_COUNT${NC} (suggestions $(( START + 1 ))-$END of $TOTAL)"
276
277    # Combine suggestion files for this batch, skipping flagged PRs and annotating stable PRs
278    BATCH_HAS_SUGGESTIONS=false
279    for (( i=START; i<END; i++ )); do
280        file="${SUGGESTION_FILES[$i]}"
281        pr_num=$(echo "$MANIFEST" | jq -r --arg f "$file" '.suggestions[] | select(.file == $f) | .pr')
282
283        # Skip PRs behind feature flags entirely
284        is_flagged=false
285        for flagged_pr in "${FLAGGED_PRS[@]+"${FLAGGED_PRS[@]}"}"; do
286            if [[ "$flagged_pr" == "$pr_num" ]]; then
287                is_flagged=true
288                break
289            fi
290        done
291        if [[ "$is_flagged" == "true" ]]; then
292            log "Skipping PR #$pr_num (behind feature flag)"
293            {
294                echo "### Skipped: PR #$pr_num"
295                echo ""
296                echo "This PR is behind a feature flag and was not applied."
297                echo ""
298            } >> "$APPLY_SUMMARY_FILE"
299            continue
300        fi
301
302        BATCH_HAS_SUGGESTIONS=true
303
304        # Check if PR is already in stable
305        is_stable=false
306        for stable_pr in "${STABLE_PRS[@]+"${STABLE_PRS[@]}"}"; do
307            if [[ "$stable_pr" == "$pr_num" ]]; then
308                is_stable=true
309                break
310            fi
311        done
312        {
313            echo "## Source: $file"
314            if [[ "$is_stable" == "true" ]]; then
315                echo ""
316                echo "> **ALREADY IN STABLE**: PR #$pr_num shipped in $STABLE_TAG."
317                echo "> Do NOT add Preview or Changed-in-Preview callouts for this suggestion."
318                echo "> Apply the documentation content only, without any preview-related callouts."
319            fi
320            echo ""
321            git show "origin/$SUGGESTIONS_BRANCH:$file" 2>/dev/null || error "Suggestion file missing: $file"
322            echo ""
323            echo "---"
324            echo ""
325        } >> "$BATCH_SUGGESTIONS_FILE"
326    done
327
328    # Skip this batch if all its suggestions were flagged
329    if [[ "$BATCH_HAS_SUGGESTIONS" == "false" ]]; then
330        echo -e "  ${YELLOW}Batch $BATCH_NUM skipped (all suggestions behind feature flags)${NC}"
331        continue
332    fi
333
334    # Build auto-apply prompt for this batch
335    cat > "$BATCH_PROMPT_FILE" << 'EOF'
336# Documentation Auto-Apply Request (Preview Release)
337
338Apply all queued documentation suggestions below directly to docs files in this repository.
339
340Before making edits, read and follow these rule files:
341- `.rules`
342- `docs/.rules`
343
344## Required behavior
345
3461. Apply concrete documentation edits (not suggestion text) to the appropriate files.
3472. Edit only docs content files under `docs/src/` unless a suggestion explicitly requires another docs path.
3483. For every docs file you modify, run a full-file brand voice pass (entire file, not only edited sections).
3494. Enforce the brand rubric exactly; final file content must score 4+ on every criterion:
350   - Technical Grounding
351   - Natural Syntax
352   - Quiet Confidence
353   - Developer Respect
354   - Information Priority
355   - Specificity
356   - Voice Consistency
357   - Earned Claims
3585. Keep SEO/frontmatter/linking requirements from the suggestions where applicable.
3596. Keep preview callout semantics correct:
360   - Additive features: `> **Preview:** ...`
361   - Behavior modifications: `> **Changed in Preview (vX.XXX).** ...`
362   - **Exception**: Suggestions marked "ALREADY IN STABLE" must NOT get any preview callouts.
363     These features already shipped in a stable release. Apply the content changes only.
364   - Suggestions for features behind feature flags have been pre-filtered and excluded.
365     If you encounter references to feature-flagged functionality, do not document it.
3667. If a suggestion is too ambiguous to apply safely, skip it and explain why in the summary.
3678. **Do not invent `{#kb}` or `{#action}` references.** Only use action names that already
368   appear in the existing docs files you are editing. If unsure whether an action name is
369   valid, use plain text instead. The docs build validates all action references against
370   the compiled binary and will reject unknown names.
371
372## Output format (after making edits)
373
374Return markdown with:
375
376- `## Applied Suggestions`
377- `## Skipped Suggestions`
378- `## Files Updated`
379- `## Brand Voice Verification` (one line per updated file confirming full-file pass)
380
381Do not include a patch in the response; apply edits directly to files.
382
383## Queued Suggestions
384
385EOF
386
387    cat "$BATCH_SUGGESTIONS_FILE" >> "$BATCH_PROMPT_FILE"
388
389    log "Running Droid auto-apply (batch $BATCH_NUM) with model: $MODEL"
390    if ! droid exec -m "$MODEL" -f "$BATCH_PROMPT_FILE" --auto high > "$BATCH_SUMMARY_FILE" 2>&1; then
391        echo "Droid exec output (batch $BATCH_NUM):"
392        cat "$BATCH_SUMMARY_FILE"
393        error "Droid exec failed on batch $BATCH_NUM. See output above."
394    fi
395
396    # Append batch summary
397    {
398        echo "### Batch $BATCH_NUM"
399        echo ""
400        cat "$BATCH_SUMMARY_FILE"
401        echo ""
402    } >> "$APPLY_SUMMARY_FILE"
403
404    echo -e "  ${GREEN}Batch $BATCH_NUM complete${NC}"
405done
406echo ""
407
408log "All batches completed, checking results..."
409
410if [[ -n "$(git status --porcelain | grep -v '^??' | grep -vE '^.. docs/' || true)" ]]; then
411    error "Auto-apply modified non-doc files. Revert and re-run."
412fi
413
414if [[ -z "$(git status --porcelain docs/ | grep '^.. docs/src/' || true)" ]]; then
415    error "Auto-apply produced no docs/src changes."
416fi
417
418log "Running docs formatter"
419./script/prettier --write
420
421if [[ -z "$(git status --porcelain docs/ | grep '^.. docs/src/' || true)" ]]; then
422    error "No docs/src changes remain after formatting; aborting PR creation."
423fi
424
425# Validate docs build before creating PR
426if [[ "$SKIP_VALIDATION" != "true" ]]; then
427    echo "Validating docs build..."
428    log "Generating action metadata..."
429    if ! ./script/generate-action-metadata > /dev/null 2>&1; then
430        echo -e "${YELLOW}Warning:${NC} Could not generate action metadata (cargo build may have failed)."
431        echo "Skipping docs build validation. CI will still catch errors."
432    else
433        VALIDATION_DIR="$TMPDIR/docs-validation"
434        if ! mdbook build ./docs --dest-dir="$VALIDATION_DIR" 2>"$TMPDIR/validation-errors.txt"; then
435            echo ""
436            echo -e "${RED}Docs build validation failed:${NC}"
437            cat "$TMPDIR/validation-errors.txt"
438            echo ""
439            error "Fix the errors above and re-run, or use --skip-validation to bypass."
440        fi
441        echo -e "${GREEN}Docs build validation passed.${NC}"
442    fi
443    echo ""
444fi
445
446# Build PR body from suggestions
447PR_BODY_FILE="$TMPDIR/pr-body.md"
448cat > "$PR_BODY_FILE" << 'EOF'
449# Documentation Updates for Preview Release
450
451This draft PR auto-applies queued documentation suggestions collected from
452recently merged PRs.
453
454## How to Use This PR
455
4561. Review the applied changes file-by-file.
4572. Verify brand voice on each touched file as a full-file pass.
4583. Ensure docs use the correct callout type:
459   - Additive features: Preview callout
460   - Behavior modifications: Changed in Preview callout
4614. Merge when ready.
462
463## Auto-Apply Summary
464
465EOF
466
467cat "$APPLY_SUMMARY_FILE" >> "$PR_BODY_FILE"
468
469cat >> "$PR_BODY_FILE" << 'EOF'
470
471## Preview Callouts
472
473Use the Preview callout for new/additive features:
474
475```markdown
476> **Preview:** This feature is available in Zed Preview. It will be included in the next Stable release.
477```
478
479Use this callout for behavior modifications to existing functionality:
480
481```markdown
482> **Changed in Preview (v0.XXX).** See [release notes](/releases#0.XXX).
483```
484
485---
486
487## Pending Suggestions
488
489EOF
490
491# Append each suggestion to PR body
492for file in $(echo "$MANIFEST" | jq -r '.suggestions[].file'); do
493    log "Adding $file to PR body..."
494    echo "" >> "$PR_BODY_FILE"
495    git show "origin/$SUGGESTIONS_BRANCH:$file" >> "$PR_BODY_FILE" 2>/dev/null || true
496    echo "" >> "$PR_BODY_FILE"
497    echo "---" >> "$PR_BODY_FILE"
498done
499
500# Add tracking info
501cat >> "$PR_BODY_FILE" << EOF
502
503## PRs Included
504
505EOF
506
507echo "$MANIFEST" | jq -r '.suggestions[] | "- [PR #\(.pr)](\(.file)): \(.title)"' >> "$PR_BODY_FILE"
508
509cat >> "$PR_BODY_FILE" << 'EOF'
510
511Release Notes:
512
513- N/A
514EOF
515
516git add docs/
517git commit -m "docs: Auto-apply preview release suggestions
518
519Auto-applied queued documentation suggestions from:
520$(echo "$MANIFEST" | jq -r '.suggestions[] | "- PR #\(.pr)"')
521
522Generated with script/docs-suggest-publish for human review in draft PR."
523
524# Push and create PR
525log "Pushing branch..."
526git push -u origin "$DOCS_BRANCH"
527
528log "Creating PR..."
529PR_URL=$(gh pr create \
530    --draft \
531    --title "docs: Apply preview release suggestions" \
532    --body-file "$PR_BODY_FILE")
533
534echo ""
535echo -e "${GREEN}PR created:${NC} $PR_URL"
536
537# Reset suggestions branch if not keeping
538if [[ "$KEEP_QUEUE" != "true" ]]; then
539    echo ""
540    echo "Resetting suggestions queue..."
541    
542    git checkout --orphan "${SUGGESTIONS_BRANCH}-reset"
543    git rm -rf . > /dev/null 2>&1 || true
544    
545    cat > README.md << 'EOF'
546# Documentation Suggestions Queue
547
548This branch contains batched documentation suggestions for the next Preview release.
549
550Each file represents suggestions from a merged PR. At preview branch cut time,
551run `script/docs-suggest-publish` to create a documentation PR from these suggestions.
552
553## Structure
554
555- `suggestions/PR-XXXXX.md` - Suggestions for PR #XXXXX
556- `manifest.json` - Index of all pending suggestions
557
558## Workflow
559
5601. PRs merged to main trigger documentation analysis
5612. Suggestions are committed here as individual files
5623. At preview release, suggestions are collected into a docs PR
5634. After docs PR is created, this branch is reset
564EOF
565    
566    mkdir -p suggestions
567    echo '{"suggestions":[]}' > manifest.json
568    git add README.md suggestions manifest.json
569    git commit -m "Reset documentation suggestions queue
570
571Previous suggestions published in: $PR_URL"
572    
573    # Force push required: replacing the orphan suggestions branch with a clean slate
574    git push -f origin "${SUGGESTIONS_BRANCH}-reset:$SUGGESTIONS_BRANCH"
575    git checkout "$ORIGINAL_BRANCH"
576    git branch -D "${SUGGESTIONS_BRANCH}-reset"
577    
578    echo "Suggestions queue reset."
579else
580    git checkout "$ORIGINAL_BRANCH"
581    echo ""
582    echo "Suggestions queue kept (--keep-queue). Remember to reset manually after PR is merged."
583fi
584
585echo ""
586echo -e "${GREEN}Done!${NC}"
587echo ""
588echo "Next steps:"
589echo "1. Review the draft PR and verify all auto-applied docs changes"
590echo "2. Confirm full-file brand voice pass for each touched docs file"
591echo "3. Mark ready for review and merge"