#!/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 suggestions in batches (default 10 per batch) # 3. Runs docs formatting # 4. Validates docs build (action references, JSON schemas, links) # 5. Creates a draft PR for human review/merge # 6. 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 # --batch-size N Suggestions per Droid invocation (default: 10) # --skip-validation Skip the docs build validation step # --verbose Show detailed progress # # Run this as part of the preview release workflow. set -euo pipefail DRY_RUN=false KEEP_QUEUE=false VERBOSE=false SKIP_VALIDATION=false BATCH_SIZE=10 MODEL="${DROID_MODEL:-claude-sonnet-4-5-latest}" 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 ;; --batch-size) BATCH_SIZE="$2" shift 2 ;; --skip-validation) SKIP_VALIDATION=true shift ;; -h|--help) head -30 "$0" | tail -28 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 (ignore untracked files with grep -v '??') if [[ -n "$(git status --porcelain | grep -v '^??' || true)" ]]; then error "Working directory has uncommitted changes. Please commit or stash first." fi REQUIRED_COMMANDS=(git gh jq droid) if [[ "$SKIP_VALIDATION" != "true" ]]; then REQUIRED_COMMANDS+=(mdbook) fi for command in "${REQUIRED_COMMANDS[@]}"; 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 APPLY_SUMMARY_FILE="$TMPDIR/apply-summary.md" touch "$APPLY_SUMMARY_FILE" # Collect suggestion files into an array SUGGESTION_FILES=() while IFS= read -r file; do SUGGESTION_FILES+=("$file") done < <(echo "$MANIFEST" | jq -r '.suggestions[].file') # Determine which PRs are already in the latest stable release. # Suggestions queued with --preview may reference features that shipped in stable # by the time this script runs, so their Preview callouts should be stripped. STABLE_PRS=() STABLE_TAG=$(git tag -l 'v*' --sort=-v:refname | grep -v 'pre' | head -1 || true) if [[ -n "$STABLE_TAG" ]]; then log "Latest stable release tag: $STABLE_TAG" for file in "${SUGGESTION_FILES[@]}"; do pr_num=$(echo "$MANIFEST" | jq -r --arg f "$file" '.suggestions[] | select(.file == $f) | .pr') # Find the merge commit for this PR merge_sha=$(gh pr view "$pr_num" --json mergeCommit --jq '.mergeCommit.oid' 2>/dev/null || true) if [[ -n "$merge_sha" ]] && git merge-base --is-ancestor "$merge_sha" "$STABLE_TAG" 2>/dev/null; then STABLE_PRS+=("$pr_num") log "PR #$pr_num is in stable ($STABLE_TAG)" fi done if [[ ${#STABLE_PRS[@]} -gt 0 ]]; then echo -e "${YELLOW}Note:${NC} ${#STABLE_PRS[@]} suggestion(s) are for PRs already in stable ($STABLE_TAG)." echo " Preview callouts will be stripped for: ${STABLE_PRS[*]}" echo "" fi else log "No stable release tag found, treating all suggestions as preview-only" fi # Determine which PRs touch code gated behind feature flags. # Features behind flags aren't generally available and shouldn't be documented yet. FLAGGED_PRS=() FLAGS_FILE="$REPO_ROOT/crates/feature_flags/src/flags.rs" if [[ -f "$FLAGS_FILE" ]]; then # Extract feature flag struct names (e.g. SubagentsFeatureFlag, GitGraphFeatureFlag) FLAG_NAMES=$(grep -oE 'pub struct \w+FeatureFlag' "$FLAGS_FILE" | awk '{print $3}') if [[ -n "$FLAG_NAMES" ]]; then FLAG_PATTERN=$(echo "$FLAG_NAMES" | tr '\n' '|' | sed 's/|$//') log "Feature flags found: $(echo "$FLAG_NAMES" | tr '\n' ' ')" for file in "${SUGGESTION_FILES[@]}"; do pr_num=$(echo "$MANIFEST" | jq -r --arg f "$file" '.suggestions[] | select(.file == $f) | .pr') # Skip PRs already marked as stable (no need to double-check) is_already_stable=false for stable_pr in "${STABLE_PRS[@]+"${STABLE_PRS[@]}"}"; do if [[ "$stable_pr" == "$pr_num" ]]; then is_already_stable=true break fi done if [[ "$is_already_stable" == "true" ]]; then continue fi # Check if the PR diff references any feature flag pr_diff=$(gh pr diff "$pr_num" 2>/dev/null || true) if [[ -n "$pr_diff" ]] && echo "$pr_diff" | grep -qE "$FLAG_PATTERN"; then matched_flags=$(echo "$pr_diff" | grep -oE "$FLAG_PATTERN" | sort -u | tr '\n' ', ' | sed 's/,$//') FLAGGED_PRS+=("$pr_num") log "PR #$pr_num is behind feature flag(s): $matched_flags" fi done if [[ ${#FLAGGED_PRS[@]} -gt 0 ]]; then echo -e "${YELLOW}Note:${NC} ${#FLAGGED_PRS[@]} suggestion(s) are for features behind feature flags." echo " These will be skipped: ${FLAGGED_PRS[*]}" echo "" fi fi else log "Feature flags file not found, skipping flag detection" fi # Split into batches TOTAL=${#SUGGESTION_FILES[@]} BATCH_COUNT=$(( (TOTAL + BATCH_SIZE - 1) / BATCH_SIZE )) if [[ "$BATCH_COUNT" -gt 1 ]]; then echo "Processing $TOTAL suggestions in $BATCH_COUNT batches of up to $BATCH_SIZE..." else echo "Processing $TOTAL suggestions..." fi echo "" for (( batch=0; batch> "$APPLY_SUMMARY_FILE" continue fi BATCH_HAS_SUGGESTIONS=true # Check if PR is already in stable is_stable=false for stable_pr in "${STABLE_PRS[@]+"${STABLE_PRS[@]}"}"; do if [[ "$stable_pr" == "$pr_num" ]]; then is_stable=true break fi done { echo "## Source: $file" if [[ "$is_stable" == "true" ]]; then echo "" echo "> **ALREADY IN STABLE**: PR #$pr_num shipped in $STABLE_TAG." echo "> Do NOT add Preview or Changed-in-Preview callouts for this suggestion." echo "> Apply the documentation content only, without any preview-related callouts." fi echo "" git show "origin/$SUGGESTIONS_BRANCH:$file" 2>/dev/null || error "Suggestion file missing: $file" echo "" echo "---" echo "" } >> "$BATCH_SUGGESTIONS_FILE" done # Skip this batch if all its suggestions were flagged if [[ "$BATCH_HAS_SUGGESTIONS" == "false" ]]; then echo -e " ${YELLOW}Batch $BATCH_NUM skipped (all suggestions behind feature flags)${NC}" continue fi # Build auto-apply prompt for this batch cat > "$BATCH_PROMPT_FILE" << 'EOF' # Documentation Auto-Apply Request (Preview Release) Apply all queued documentation suggestions below directly to docs files in this repository. Before making edits, read and follow these rule files: - `.rules` - `docs/.rules` ## 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).** ...` - **Exception**: Suggestions marked "ALREADY IN STABLE" must NOT get any preview callouts. These features already shipped in a stable release. Apply the content changes only. - Suggestions for features behind feature flags have been pre-filtered and excluded. If you encounter references to feature-flagged functionality, do not document it. 7. If a suggestion is too ambiguous to apply safely, skip it and explain why in the summary. 8. **Do not invent `{#kb}` or `{#action}` references.** Only use action names that already appear in the existing docs files you are editing. If unsure whether an action name is valid, use plain text instead. The docs build validates all action references against the compiled binary and will reject unknown names. ## 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 "$BATCH_SUGGESTIONS_FILE" >> "$BATCH_PROMPT_FILE" log "Running Droid auto-apply (batch $BATCH_NUM) with model: $MODEL" if ! droid exec -m "$MODEL" -f "$BATCH_PROMPT_FILE" --auto high > "$BATCH_SUMMARY_FILE" 2>&1; then echo "Droid exec output (batch $BATCH_NUM):" cat "$BATCH_SUMMARY_FILE" error "Droid exec failed on batch $BATCH_NUM. See output above." fi # Append batch summary { echo "### Batch $BATCH_NUM" echo "" cat "$BATCH_SUMMARY_FILE" echo "" } >> "$APPLY_SUMMARY_FILE" echo -e " ${GREEN}Batch $BATCH_NUM complete${NC}" done echo "" log "All batches completed, checking results..." if [[ -n "$(git status --porcelain | grep -v '^??' | 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 --write if [[ -z "$(git status --porcelain docs/ | grep '^.. docs/src/' || true)" ]]; then error "No docs/src changes remain after formatting; aborting PR creation." fi # Validate docs build before creating PR if [[ "$SKIP_VALIDATION" != "true" ]]; then echo "Validating docs build..." log "Generating action metadata..." if ! ./script/generate-action-metadata > /dev/null 2>&1; then echo -e "${YELLOW}Warning:${NC} Could not generate action metadata (cargo build may have failed)." echo "Skipping docs build validation. CI will still catch errors." else VALIDATION_DIR="$TMPDIR/docs-validation" if ! mdbook build ./docs --dest-dir="$VALIDATION_DIR" 2>"$TMPDIR/validation-errors.txt"; then echo "" echo -e "${RED}Docs build validation failed:${NC}" cat "$TMPDIR/validation-errors.txt" echo "" error "Fix the errors above and re-run, or use --skip-validation to bypass." fi echo -e "${GREEN}Docs build validation passed.${NC}" fi echo "" 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" cat >> "$PR_BODY_FILE" << 'EOF' Release Notes: - N/A EOF 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: Apply preview release suggestions" \ --body-file "$PR_BODY_FILE") 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" # Force push required: replacing the orphan suggestions branch with a clean slate 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 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"