diff --git a/.factory/prompts/crash/fix.md b/.factory/prompts/crash/fix.md deleted file mode 100644 index 0899c55c837..00000000000 --- a/.factory/prompts/crash/fix.md +++ /dev/null @@ -1,84 +0,0 @@ -# Crash Fix - -You are fixing a crash that has been analyzed and has a reproduction test case. Your goal is to implement a minimal, correct fix that resolves the root cause and makes the reproduction test pass. - -## Inputs - -Before starting, you should have: - -1. **ANALYSIS.md** — the crash analysis from the investigation phase. Read it thoroughly. -2. **A failing test** — a reproduction test that triggers the crash. Run it first to confirm it fails as expected. - -If either is missing, ask the user to provide them or run the investigation phase first (`/prompt crash/investigate`). - -## Workflow - -### Step 1: Confirm the Failing Test - -Run the reproduction test and verify it fails with the expected crash: - -``` -cargo test -p -``` - -Read the failure output. Confirm the panic message and stack trace match what ANALYSIS.md describes. If the test doesn't fail, or fails differently than expected, stop and reassess before proceeding. - -### Step 2: Understand the Fix - -Read the "Suggested Fix" section of ANALYSIS.md and the relevant source code. Before writing any code, be clear on: - -1. **What invariant is being violated** — what property of the data does the crashing code assume? -2. **Where the invariant breaks** — which function produces the bad state? - -### Step 3: Implement the Fix - -Apply the minimal change needed to resolve the root cause. Guidelines: - -- **Fix the root cause, not the symptom.** Don't just catch the panic with a bounds check if the real problem is an incorrect offset calculation. Fix the calculation. -- **Preserve existing behavior** for all non-crashing cases. The fix should only change what happens in the scenario that was previously crashing. -- **Don't add unnecessary changes.** No drive-by improvements, keep the diff focused. -- **Add a comment only if the fix is non-obvious.** If a reader might wonder "why is this check here?", a brief comment explaining the crash scenario is appropriate. -- **Consider long term maintainability** Please make a targeted fix while being sure to consider the long term maintainability and reliability of the codebase - -### Step 4: Verify the Fix - -Run the reproduction test and confirm it passes: - -``` -cargo test -p -``` - -Then run the full test suite for the affected crate to check for regressions: - -``` -cargo test -p -``` - -If any tests fail, determine whether the fix introduced a regression. Fix regressions before proceeding. - -### Step 5: Run Clippy - -``` -./script/clippy -``` - -Address any new warnings introduced by your change. - -### Step 6: Summarize - -Write a brief summary of the fix for use in a PR description. Include: - -- **What was the bug** — one sentence on the root cause. -- **What the fix does** — one sentence on the change. -- **How it was verified** — note that the reproduction test now passes. -- **Sentry issue link** — if available from ANALYSIS.md. - -We use the following template for pull request descriptions. Please add information to answer the relevant sections, especially for release notes. - -``` - - -Release Notes: - -- N/A *or* Added/Fixed/Improved ... -``` diff --git a/.factory/prompts/crash/investigate.md b/.factory/prompts/crash/investigate.md deleted file mode 100644 index 93d35a0f0b9..00000000000 --- a/.factory/prompts/crash/investigate.md +++ /dev/null @@ -1,89 +0,0 @@ -# Crash Investigation - -You are investigating a crash that was observed in the wild. Your goal is to understand the root cause and produce a minimal reproduction test case that triggers the same crash. This test will be used to verify a fix and prevent regressions. - -## Workflow - -### Step 1: Get the Crash Report - -If given a Sentry issue ID (like `ZED-4VS` or a numeric ID), there are several ways to fetch the crash data: - -**Option A: Sentry MCP server (preferred if available)** -If the Sentry MCP server is configured as a context server, use its tools directly (e.g., `get_sentry_issue`) to fetch the issue details and stack trace. This is the simplest path — no tokens or scripts needed. - -**Option B: Fetch script** -Run the fetch script from the terminal: - -``` -script/sentry-fetch -``` - -This reads authentication from `~/.sentryclirc` (set up via `sentry-cli login`) or the `SENTRY_AUTH_TOKEN` environment variable. - -**Option C: Crash report provided directly** -If the crash report was provided inline or as a file, read it carefully before proceeding. - -### Step 2: Analyze the Stack Trace - -Read the stack trace bottom-to-top (from crash site upward) and identify: - -1. **The crash site** — the exact function and line where the panic/abort occurs. -2. **The immediate cause** — what operation failed (e.g., slice indexing on a non-char-boundary, out-of-bounds access, unwrap on None). -3. **The relevant application frames** — filter out crash handler, signal handler, parking_lot, and stdlib frames. Focus on frames marked "(In app)". -4. **The data flow** — trace how the invalid data reached the crash site. What computed the bad index, the None value, etc.? - -Find the relevant source files in the repository and read them. Pay close attention to: -- The crashing function and its callers -- How inputs to the crashing operation are computed -- Any assumptions the code makes about its inputs (string encoding, array lengths, option values) - -### Step 3: Identify the Root Cause - -Work backwards from the crash site to determine **what sequence of events or data conditions** produces the invalid state. - -Ask yourself: *What user action or sequence of actions could lead to this state?* The crash came from a real user, so there is some natural usage pattern that triggers it. - -### Step 4: Write a Reproduction Test - -Write a minimal test case that: - -1. **Mimics user actions** rather than constructing corrupt state directly. Work from the top down: what does the user do (open a file, type text, trigger a completion, etc.) that eventually causes the internal state to become invalid? -2. **Exercises the same code path** as the crash. The test should fail in the same function with the same kind of error (e.g., same panic message pattern). -3. **Is minimal** — include only what's necessary to trigger the crash. Remove anything that isn't load-bearing. -4. **Lives in the right place** — add the test to the existing test module of the crate where the bug lives. Follow the existing test patterns in that module. -5. **Avoid overly verbose comments** - the test should be self-explanatory and concise. More detailed descriptions of the test can go in ANALYSIS.md (see the next section). - -When the test fails, its stack trace should share the key application frames from the original crash report. The outermost frames (crash handler, signal handling) will differ since we're in a test environment — that's expected. - -If you can't reproduce the exact crash but can demonstrate the same class of bug (e.g., same function panicking with a similar invalid input), that is still valuable. Note the difference in your analysis. - -### Step 5: Write the Analysis - -Create an `ANALYSIS.md` file (in the working directory root, or wherever instructed) with these sections: - -```markdown -# Crash Analysis: - -## Crash Summary -- **Sentry Issue:** -- **Error:** -- **Crash Site:** - -## Root Cause - - -## Reproduction - `> - -## Suggested Fix - -``` - -## Guidelines - -- **Don't guess.** If you're unsure about a code path, read the source. Use `grep` to find relevant functions, types, and call sites. -- **Check the git history.** If the crash appeared in a specific version, `git log` on the relevant files may reveal a recent change that introduced the bug. -- **Look at existing tests.** The crate likely has tests that show how to set up the relevant subsystem. Follow those patterns rather than inventing new test infrastructure. diff --git a/.factory/prompts/crash/link-issues.md b/.factory/prompts/crash/link-issues.md deleted file mode 100644 index 0413c791ed6..00000000000 --- a/.factory/prompts/crash/link-issues.md +++ /dev/null @@ -1,89 +0,0 @@ -# Crash Issue Linking - -You are linking a crash to potentially related GitHub issues so human reviewers can quickly validate whether a fix may resolve multiple reports. - -## Inputs - -Before starting, you should have: - -1. **Crash report** (from `script/sentry-fetch ` or Sentry MCP) -2. **ANALYSIS.md** from investigation phase, including root cause and crash site - -If either is missing, stop and report what is missing. - -## Goal - -Search GitHub issues and produce a reviewer-ready shortlist grouped by confidence: - -- **High confidence** -- **Medium confidence** -- **Low confidence** - -The output is advisory only. Humans must confirm before adding closing keywords or making release claims. - -## Workflow - -### Step 1: Build Search Signals - -Extract concrete signals from the crash + analysis: - -1. Crash site function, file, and crate -2. Error message / panic text -3. Key stack frames (especially in-app) -4. Reproduction trigger phrasing (user actions) -5. Affected platform/version tags if available - -### Step 2: Search GitHub Issues - -Search **only** issues in `zed-industries/zed` (prefer `gh issue list` / `gh issue view` / GraphQL if available) by: - -1. Panic/error text -2. Function/file names -3. Crate/module names + symptom keywords -4. Similar reproduction patterns - -Check both open and recently closed issues in `zed-industries/zed`. - -### Step 3: Score Confidence - -Assign confidence based on evidence quality: - -- **High:** direct technical overlap (same crash site or same invariant violation with matching repro language) -- **Medium:** partial overlap (same subsystem and symptom, but indirect stack/repro match) -- **Low:** thematic similarity only (same area/keywords without solid technical match) - -Avoid inflated confidence. If uncertain, downgrade. - -### Step 4: Produce Structured Output - -Write `LINKED_ISSUES.md` using this exact structure: - -```markdown -# Potentially Related GitHub Issues - -## High Confidence -- [#12345](https://github.com/zed-industries/zed/issues/12345) — - - Why: <1-2 sentence evidence-backed rationale> - - Evidence: <stack frame / error text / repro alignment> - -## Medium Confidence -- ... - -## Low Confidence -- ... - -## Reviewer Checklist -- [ ] Confirm High confidence issues should be referenced in PR body -- [ ] Confirm any issue should receive closing keywords (`Fixes #...`) -- [ ] Reject false positives before merge -``` - -If no credible matches are found, keep sections present and write `- None found` under each. - -## Rules - -- Do not fabricate issues or URLs. -- Do not include issues from any repository other than `zed-industries/zed`. -- Do not add closing keywords automatically. -- Keep rationale short and evidence-based. -- Favor precision over recall. diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml index c9624eaa652..0266262c0ea 100644 --- a/.github/workflows/deploy_collab.yml +++ b/.github/workflows/deploy_collab.yml @@ -7,6 +7,7 @@ on: push: tags: - collab-production + - collab-staging permissions: contents: read jobs: diff --git a/.github/workflows/docs_suggestions.yml b/.github/workflows/docs_suggestions.yml deleted file mode 100644 index 1509e79ad92..00000000000 --- a/.github/workflows/docs_suggestions.yml +++ /dev/null @@ -1,489 +0,0 @@ -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: - # Analyze merged PRs on main and cherry-picks to release branches. Each job - # further restricts the event actions and target branch that it handles. - pull_request: - types: [closed, opened, synchronize] - branches: - - main - - "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 - -# Both jobs below declare their own `permissions:` blocks, so no job uses this -# top-level default. Least privilege therefore grants nothing here. -permissions: {} - -env: - DROID_MODEL: claude-sonnet-4-5-20250929 - SUGGESTIONS_BRANCH: docs/suggestions-pending - -jobs: - # Job for PRs merged to main - batch suggestions to branch - # Only runs for PRs from the same repo (not forks) since secrets aren't available for fork PRs - batch-suggestions: - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: write - pull-requests: read - if: | - (github.event_name == 'pull_request' && - github.event.pull_request.merged == true && - github.event.pull_request.base.ref == 'main' && - github.event.pull_request.head.repo.full_name == github.repository) || - (github.event_name == 'workflow_dispatch' && inputs.mode == 'batch') - - steps: - - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Droid CLI - run: | - # Retry with exponential backoff for transient network/auth issues - MAX_RETRIES=3 - for i in $(seq 1 "$MAX_RETRIES"); do - echo "Attempt $i of $MAX_RETRIES to install Droid CLI..." - if curl -fsSL https://app.factory.ai/cli | sh; then - echo "Droid CLI installed successfully" - break - fi - if [ "$i" -eq "$MAX_RETRIES" ]; then - echo "Failed to install Droid CLI after $MAX_RETRIES attempts" - exit 1 - fi - sleep $((i * 5)) - done - env: - FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }} - - - name: Get PR info - id: pr - env: - INPUT_PR_NUMBER: ${{ inputs.pr_number }} - EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} - GH_TOKEN: ${{ github.token }} - run: | - if [ -n "$INPUT_PR_NUMBER" ]; then - PR_NUM="$INPUT_PR_NUMBER" - else - PR_NUM="$EVENT_PR_NUMBER" - fi - if ! [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then - echo "::error::Invalid PR number: $PR_NUM" - exit 1 - fi - echo "number=$PR_NUM" >> "$GITHUB_OUTPUT" - PR_TITLE=$(gh pr view "$PR_NUM" --json title --jq '.title' | tr -d '\n\r' | head -c 200) - EOF_MARKER="EOF_$(openssl rand -hex 8)" - { - echo "title<<$EOF_MARKER" - echo "$PR_TITLE" - echo "$EOF_MARKER" - } >> "$GITHUB_OUTPUT" - - - name: Analyze PR for documentation needs - id: analyze - env: - GH_TOKEN: ${{ github.token }} - FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }} - PR_NUMBER: ${{ steps.pr.outputs.number }} - run: | - export PATH="${HOME}/.local/bin:${PATH}" - - # Ensure gh CLI is authenticated (GH_TOKEN may not be auto-detected) - # Unset GH_TOKEN first to allow gh auth login to store credentials - echo "$GH_TOKEN" | (unset GH_TOKEN && gh auth login --with-token) - - OUTPUT_FILE=$(mktemp) - - # Retry with exponential backoff for transient Factory API failures - MAX_RETRIES=3 - for i in $(seq 1 "$MAX_RETRIES"); do - echo "Attempt $i of $MAX_RETRIES to analyze PR..." - if ./script/docs-suggest \ - --pr "$PR_NUMBER" \ - --immediate \ - --preview \ - --output "$OUTPUT_FILE" \ - --verbose; then - echo "Analysis completed successfully" - break - fi - if [ "$i" -eq "$MAX_RETRIES" ]; then - echo "Analysis failed after $MAX_RETRIES attempts" - exit 1 - fi - echo "Retrying in $((i * 5)) seconds..." - sleep $((i * 5)) - done - - # Check if we got actionable suggestions (not "no updates needed") - if grep -q "Documentation Suggestions" "$OUTPUT_FILE" && \ - ! grep -q "No Documentation Updates Needed" "$OUTPUT_FILE"; then - echo "has_suggestions=true" >> "$GITHUB_OUTPUT" - echo "output_file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT" - else - echo "has_suggestions=false" >> "$GITHUB_OUTPUT" - echo "No actionable documentation suggestions for this PR" - cat "$OUTPUT_FILE" - fi - - - name: Commit suggestions to queue branch - if: steps.analyze.outputs.has_suggestions == 'true' - env: - PR_NUM: ${{ steps.pr.outputs.number }} - PR_TITLE: ${{ steps.pr.outputs.title }} - OUTPUT_FILE: ${{ steps.analyze.outputs.output_file }} - REPO: ${{ github.repository }} - run: | - set -euo pipefail - - # Configure git - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # Retry loop for handling concurrent pushes - MAX_RETRIES=3 - for i in $(seq 1 "$MAX_RETRIES"); do - echo "Attempt $i of $MAX_RETRIES" - - # Fetch and checkout suggestions branch (create if doesn't exist) - if git ls-remote --exit-code --heads origin "$SUGGESTIONS_BRANCH" > /dev/null 2>&1; then - git fetch origin "$SUGGESTIONS_BRANCH" - git checkout -B "$SUGGESTIONS_BRANCH" "origin/$SUGGESTIONS_BRANCH" - else - # Create orphan branch for clean history - git checkout --orphan "$SUGGESTIONS_BRANCH" - git rm -rf . > /dev/null 2>&1 || true - - # Initialize with README - cat > README.md << 'EOF' - # Documentation Suggestions Queue - - This branch contains batched documentation suggestions for the next Preview release. - - Each file represents suggestions from a merged PR. At preview branch cut time, - run `script/docs-suggest-publish` to create a documentation PR from these suggestions. - - ## Structure - - - `suggestions/PR-XXXXX.md` - Suggestions for PR #XXXXX - - `manifest.json` - Index of all pending suggestions - - ## Workflow - - 1. PRs merged to main trigger documentation analysis - 2. Suggestions are committed here as individual files - 3. At preview release, suggestions are collected into a docs PR - 4. After docs PR is created, this branch is reset - EOF - - mkdir -p suggestions - echo '{"suggestions":[]}' > manifest.json - git add README.md suggestions manifest.json - git commit -m "Initialize documentation suggestions queue" - fi - - # Create suggestion file - SUGGESTION_FILE="suggestions/PR-${PR_NUM}.md" - - { - echo "# PR #${PR_NUM}: ${PR_TITLE}" - echo "" - echo "_Merged: $(date -u +%Y-%m-%dT%H:%M:%SZ)_" - echo "_PR: https://github.com/${REPO}/pull/${PR_NUM}_" - echo "" - cat "$OUTPUT_FILE" - } > "$SUGGESTION_FILE" - - # Update manifest - MANIFEST=$(cat manifest.json) - NEW_ENTRY="{\"pr\":${PR_NUM},\"title\":$(echo "$PR_TITLE" | jq -R .),\"file\":\"$SUGGESTION_FILE\",\"date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" - - # Add to manifest if not already present - if ! echo "$MANIFEST" | jq -e ".suggestions[] | select(.pr == $PR_NUM)" > /dev/null 2>&1; then - echo "$MANIFEST" | jq ".suggestions += [$NEW_ENTRY]" > manifest.json - fi - - # Commit - git add "$SUGGESTION_FILE" manifest.json - git commit -m "docs: Add suggestions for PR #${PR_NUM} - - ${PR_TITLE} - - Auto-generated documentation suggestions for review at next preview release." - - # Try to push - if git push origin "$SUGGESTIONS_BRANCH"; then - echo "Successfully pushed suggestions" - break - else - echo "Push failed, retrying..." - if [ "$i" -eq "$MAX_RETRIES" ]; then - echo "Failed after $MAX_RETRIES attempts" - exit 1 - fi - sleep $((i * 2)) - fi - done - - - name: Summary - if: always() - env: - HAS_SUGGESTIONS: ${{ steps.analyze.outputs.has_suggestions }} - PR_NUM: ${{ steps.pr.outputs.number }} - REPO: ${{ github.repository }} - run: | - { - echo "## Documentation Suggestions" - echo "" - if [ "$HAS_SUGGESTIONS" == "true" ]; then - echo "✅ Suggestions queued for PR #${PR_NUM}" - echo "" - echo "View pending suggestions: [docs/suggestions-pending branch](https://github.com/${REPO}/tree/${SUGGESTIONS_BRANCH})" - else - echo "No documentation updates needed for this PR." - fi - } >> "$GITHUB_STEP_SUMMARY" - - # Job for cherry-picks to release branches - immediate output as PR comment - cherry-pick-suggestions: - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - pull-requests: write - concurrency: - group: docs-suggestions-${{ github.event.pull_request.number || inputs.pr_number || 'manual' }} - cancel-in-progress: true - if: | - (github.event_name == 'pull_request' && - contains(fromJSON('["opened","synchronize"]'), github.event.action) && - startsWith(github.event.pull_request.base.ref, 'v0.') && - github.event.pull_request.head.repo.full_name == github.repository && - contains(fromJSON('["MEMBER","OWNER"]'), - github.event.pull_request.author_association)) || - (github.event_name == 'workflow_dispatch' && inputs.mode == 'immediate') - - steps: - - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - with: - fetch-depth: 0 - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || '' }} - persist-credentials: false - - - name: Install Droid CLI - run: | - # Retry with exponential backoff for transient network/auth issues - MAX_RETRIES=3 - for i in $(seq 1 "$MAX_RETRIES"); do - echo "Attempt $i of $MAX_RETRIES to install Droid CLI..." - if curl -fsSL https://app.factory.ai/cli | sh; then - echo "Droid CLI installed successfully" - break - fi - if [ "$i" -eq "$MAX_RETRIES" ]; then - echo "Failed to install Droid CLI after $MAX_RETRIES attempts" - exit 1 - fi - sleep $((i * 5)) - done - env: - FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }} - - - name: Get PR number - id: pr - env: - INPUT_PR_NUMBER: ${{ inputs.pr_number }} - EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - if [ -n "$INPUT_PR_NUMBER" ]; then - PR_NUM="$INPUT_PR_NUMBER" - else - PR_NUM="$EVENT_PR_NUMBER" - fi - if ! [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then - echo "::error::Invalid PR number: $PR_NUM" - exit 1 - fi - echo "number=$PR_NUM" >> "$GITHUB_OUTPUT" - - - name: Analyze PR for documentation needs - id: analyze - env: - GH_TOKEN: ${{ github.token }} - FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }} - PR_NUMBER: ${{ steps.pr.outputs.number }} - run: | - export PATH="${HOME}/.local/bin:${PATH}" - - # Ensure gh CLI is authenticated (GH_TOKEN may not be auto-detected) - # Unset GH_TOKEN first to allow gh auth login to store credentials - echo "$GH_TOKEN" | (unset GH_TOKEN && gh auth login --with-token) - - OUTPUT_FILE="${RUNNER_TEMP}/suggestions.md" - - # Cherry-picks don't get preview callout - # Retry with exponential backoff for transient Factory API failures - MAX_RETRIES=3 - for i in $(seq 1 "$MAX_RETRIES"); do - echo "Attempt $i of $MAX_RETRIES to analyze PR..." - if ./script/docs-suggest \ - --pr "$PR_NUMBER" \ - --immediate \ - --no-preview \ - --output "$OUTPUT_FILE" \ - --verbose; then - echo "Analysis completed successfully" - break - fi - if [ "$i" -eq "$MAX_RETRIES" ]; then - echo "Analysis failed after $MAX_RETRIES attempts" - exit 1 - fi - echo "Retrying in $((i * 5)) seconds..." - sleep $((i * 5)) - done - - # Check if we got actionable suggestions - if [ -s "$OUTPUT_FILE" ] && \ - grep -q "Documentation Suggestions" "$OUTPUT_FILE" && \ - ! grep -q "No Documentation Updates Needed" "$OUTPUT_FILE"; then - echo "has_suggestions=true" >> "$GITHUB_OUTPUT" - echo "suggestions_file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT" - else - echo "has_suggestions=false" >> "$GITHUB_OUTPUT" - fi - - - name: Post suggestions as PR comment - if: steps.analyze.outputs.has_suggestions == 'true' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 - env: - SUGGESTIONS_FILE: ${{ steps.analyze.outputs.suggestions_file }} - PR_NUMBER: ${{ steps.pr.outputs.number }} - with: - script: | - const fs = require('fs'); - - // Read suggestions from file - const suggestionsRaw = fs.readFileSync(process.env.SUGGESTIONS_FILE, 'utf8'); - - // Sanitize AI-generated content - let sanitized = suggestionsRaw - // Strip HTML tags - .replace(/<[^>]*>/g, '') - // Strip markdown links but keep display text - .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') - // Strip raw URLs - .replace(/https?:\/\/[^\s)>\]]+/g, '[link removed]') - // Strip protocol-relative URLs - .replace(/\/\/[^\s)>\]]+\.[^\s)>\]]+/g, '[link removed]') - // Neutralize @-mentions (preserve JSDoc-style annotations) - .replace(/@(?!param\b|returns?\b|throws?\b|typedef\b|type\b|see\b|example\b|since\b|deprecated\b|default\b)(\w+)/g, '`@$1`') - // Strip cross-repo references that could be confused with real links - .replace(/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+#\d+/g, '[ref removed]'); - - // Truncate to 20,000 characters - if (sanitized.length > 20000) { - sanitized = sanitized.substring(0, 20000) + '\n\n…(truncated)'; - } - - // Parse and validate PR number - const prNumber = parseInt(process.env.PR_NUMBER, 10); - if (isNaN(prNumber) || prNumber <= 0) { - core.setFailed(`Invalid PR number: ${process.env.PR_NUMBER}`); - return; - } - - const body = `## 📚 Documentation Suggestions - - This cherry-pick contains changes that may need documentation updates. - - ${sanitized} - - --- - > **Note:** This comment was generated automatically by an AI model analyzing - > code changes. Suggestions may contain inaccuracies — please verify before acting. - - <details> - <summary>About this comment</summary> - - This comment was generated automatically by analyzing code changes in this cherry-pick. - Cherry-picks typically don't need new documentation since the feature was already - documented when merged to main, but please verify. - - </details>`; - - // Find existing comment to update (avoid spam) - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber - }); - - const botComment = comments.find(c => - c.user.type === 'Bot' && - c.body.includes('Documentation Suggestions') - ); - - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: body - }); - } - - - name: Summary - if: always() - env: - HAS_SUGGESTIONS: ${{ steps.analyze.outputs.has_suggestions }} - PR_NUM: ${{ steps.pr.outputs.number }} - run: | - { - echo "## 📚 Documentation Suggestions (Cherry-pick)" - echo "" - if [ "$HAS_SUGGESTIONS" == "true" ]; then - echo "Suggestions posted as PR comment on #${PR_NUM}." - else - echo "No documentation suggestions for this cherry-pick." - fi - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/hotfix-review-monitor.yml b/.github/workflows/hotfix-review-monitor.yml deleted file mode 100644 index c6e4bfe6e53..00000000000 --- a/.github/workflows/hotfix-review-monitor.yml +++ /dev/null @@ -1,115 +0,0 @@ -# Hotfix Review Monitor -# -# Runs daily and checks for merged PRs with the 'hotfix' label that have not -# received a post-merge review approval within one business day. Posts a summary to -# Slack if any are found. This is a SOC2 compensating control for the -# emergency hotfix fast path. -# -# Security note: No untrusted input (PR titles, bodies, etc.) is interpolated -# into shell commands. All PR metadata is read via gh API + jq, not via -# github.event context expressions. -# -# Required secrets: -# SLACK_WEBHOOK_PR_REVIEW_BOT - Incoming webhook URL for the #pr-review-ops channel - -name: Hotfix Review Monitor - -on: - schedule: - - cron: "30 13 * * 1-5" # 1:30 PM UTC weekdays - workflow_dispatch: {} - -permissions: - contents: read - -jobs: - check-hotfix-reviews: - if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest - permissions: - pull-requests: read - timeout-minutes: 5 - env: - REPO: ${{ github.repository }} - steps: - - name: Find unreviewed hotfixes - id: check - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # 80h lookback covers the Friday-to-Monday gap (72h) with buffer. - # Overlap on weekdays is harmless — reviewed PRs are filtered out below. - SINCE=$(date -u -v-80H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ - || date -u -d '80 hours ago' +%Y-%m-%dT%H:%M:%SZ) - SINCE_DATE=$(echo "$SINCE" | cut -dT -f1) - - # Use the Search API to find hotfix PRs merged in the lookback window. - # The Pulls API with state=closed paginates through all closed PRs in - # the repo, which times out on large repos. The Search API supports - # merged:>DATE natively so GitHub does the filtering server-side. - gh api --paginate \ - "search/issues?q=repo:${REPO}+is:pr+is:merged+label:hotfix+merged:>${SINCE_DATE}&per_page=100" \ - --jq '[.items[] | {number, title, merged_at: .pull_request.merged_at}]' \ - > /tmp/hotfix_prs.json - - # Check each hotfix PR for a post-merge approving review - jq -r '.[].number' /tmp/hotfix_prs.json | while read -r PR_NUMBER; do - APPROVALS=$(gh api \ - "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \ - --jq "[.[] | select(.state == \"APPROVED\")] | length") - - if [ "$APPROVALS" -eq 0 ]; then - jq ".[] | select(.number == ${PR_NUMBER})" /tmp/hotfix_prs.json - fi - done | jq -s '.' > /tmp/unreviewed.json - - COUNT=$(jq 'length' /tmp/unreviewed.json) - echo "count=$COUNT" >> "$GITHUB_OUTPUT" - - - name: Notify Slack - if: steps.check.outputs.count != '0' - env: - SLACK_WEBHOOK_PR_REVIEW_BOT: ${{ secrets.SLACK_WEBHOOK_PR_REVIEW_BOT }} - COUNT: ${{ steps.check.outputs.count }} - run: | - # Build Block Kit payload from JSON — no shell interpolation of PR titles. - # Why jq? PR titles are attacker-controllable input. By reading them - # through jq -r from the JSON file and passing the result to jq --arg, - # the content stays safely JSON-encoded in the final payload. Block Kit - # doesn't change this — the same jq pipeline feeds into the blocks - # structure instead of plain text. - PRS=$(jq -r '.[] | "• <https://github.com/'"${REPO}"'/pull/\(.number)|#\(.number)> — \(.title) (merged \(.merged_at | split("T")[0]))"' /tmp/unreviewed.json) - - jq -n \ - --arg count "$COUNT" \ - --arg prs "$PRS" \ - '{ - text: ($count + " hotfix PR(s) still need post-merge review"), - blocks: [ - { - type: "section", - text: { - type: "mrkdwn", - text: (":rotating_light: *" + $count + " Hotfix PR(s) Need Post-Merge Review*") - } - }, - { - type: "section", - text: { type: "mrkdwn", text: $prs } - }, - { type: "divider" }, - { - type: "context", - elements: [{ - type: "mrkdwn", - text: "Hotfix PRs require review within one business day of merge." - }] - } - ] - }' | \ - curl -s -X POST "$SLACK_WEBHOOK_PR_REVIEW_BOT" \ - -H 'Content-Type: application/json' \ - -d @- -defaults: - run: - shell: bash -euxo pipefail {0} diff --git a/.github/workflows/stale-pr-reminder.yml b/.github/workflows/stale-pr-reminder.yml deleted file mode 100644 index a35430857ec..00000000000 --- a/.github/workflows/stale-pr-reminder.yml +++ /dev/null @@ -1,116 +0,0 @@ -# Stale PR Review Reminder -# -# Runs daily on weekdays (second run at 8 PM UTC disabled during rollout) and posts a Slack summary of open PRs that -# have been awaiting review for more than 72 hours. Team-level signal only — -# no individual shaming. -# -# Security note: No untrusted input is interpolated into shell commands. -# All PR metadata is read via gh API + jq. -# -# Required secrets: -# SLACK_WEBHOOK_PR_REVIEW_BOT - Incoming webhook URL for the #pr-review-ops channel - -name: Stale PR Review Reminder - -on: - schedule: - - cron: "0 14 * * 1-5" # 2 PM UTC weekdays - # - cron: "0 20 * * 1-5" # 8 PM UTC weekdays — enable after initial rollout - workflow_dispatch: {} - -permissions: - contents: read - -jobs: - check-stale-prs: - if: github.repository_owner == 'zed-industries' - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - pull-requests: read - env: - REPO: ${{ github.repository }} - # Only surface PRs created on or after this date. Update this if the - # review process enforcement date changes. - PROCESS_START_DATE: "2026-03-19T00:00:00Z" - steps: - - name: Find PRs awaiting review longer than 72h - id: stale - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - CUTOFF=$(date -u -v-72H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ - || date -u -d '72 hours ago' +%Y-%m-%dT%H:%M:%SZ) - - # Get open, non-draft PRs with pending review requests, created before cutoff - # but after the review process start date (to exclude pre-existing backlog) - gh api --paginate \ - "repos/${REPO}/pulls?state=open&sort=updated&direction=asc&per_page=100" \ - --jq "[ - .[] | - select(.draft == false) | - select(.created_at > \"$PROCESS_START_DATE\") | - select(.created_at < \"$CUTOFF\") | - select((.requested_reviewers | length > 0) or (.requested_teams | length > 0)) - ]" > /tmp/candidates.json - - # Filter to PRs with zero approving reviews - jq -r '.[].number' /tmp/candidates.json | while read -r PR_NUMBER; do - APPROVALS=$(gh api \ - "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \ - --jq "[.[] | select(.state == \"APPROVED\")] | length" 2>/dev/null || echo "0") - - if [ "$APPROVALS" -eq 0 ]; then - jq ".[] | select(.number == ${PR_NUMBER}) | {number, title, author: .user.login, created_at}" \ - /tmp/candidates.json - fi - done | jq -s '.' > /tmp/awaiting.json - - COUNT=$(jq 'length' /tmp/awaiting.json) - echo "count=$COUNT" >> "$GITHUB_OUTPUT" - - - name: Notify Slack - if: steps.stale.outputs.count != '0' - env: - SLACK_WEBHOOK_PR_REVIEW_BOT: ${{ secrets.SLACK_WEBHOOK_PR_REVIEW_BOT }} - COUNT: ${{ steps.stale.outputs.count }} - run: | - # Build Block Kit payload from JSON — no shell interpolation of PR titles. - # Why jq? PR titles are attacker-controllable input. By reading them - # through jq -r from the JSON file and passing the result to jq --arg, - # the content stays safely JSON-encoded in the final payload. - PRS=$(jq -r '.[] | "• <https://github.com/'"${REPO}"'/pull/\(.number)|#\(.number)> — \(.title) (by \(.author), opened \(.created_at | split("T")[0]))"' /tmp/awaiting.json) - - jq -n \ - --arg count "$COUNT" \ - --arg prs "$PRS" \ - '{ - text: ($count + " PR(s) awaiting review for >72 hours"), - blocks: [ - { - type: "section", - text: { - type: "mrkdwn", - text: (":hourglass_flowing_sand: *" + $count + " PR(s) Awaiting Review >72 Hours*") - } - }, - { - type: "section", - text: { type: "mrkdwn", text: $prs } - }, - { type: "divider" }, - { - type: "context", - elements: [{ - type: "mrkdwn", - text: "PRs awaiting review are surfaced daily. Reviewers: pick one up or reassign." - }] - } - ] - }' | \ - curl -s -X POST "$SLACK_WEBHOOK_PR_REVIEW_BOT" \ - -H 'Content-Type: application/json' \ - -d @- -defaults: - run: - shell: bash -euxo pipefail {0} diff --git a/script/analyze_highlights.py b/script/analyze_highlights.py deleted file mode 100644 index aaf7386be6d..00000000000 --- a/script/analyze_highlights.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -""" -This script analyzes all the highlights.scm files in our embedded languages and extensions. -It counts the number of unique instances of @{name} and the languages in which they are used. - -This is useful to help avoid accidentally introducing new tags when appropriate ones already exist when adding new languages. - -Flags: --v, --verbose: Include a detailed list of languages for each tag found in the highlights.scm files. -""" - -import argparse -import re -from collections import defaultdict -from pathlib import Path -from typing import Any - -pattern = re.compile(r'@(?!_)[a-zA-Z_.]+') - -def parse_arguments(): - parser = argparse.ArgumentParser(description='Analyze highlights.scm files for unique instances and their languages.') - parser.add_argument('-v', '--verbose', action='store_true', help='Include a list of languages for each tag.') - return parser.parse_args() - -def find_highlight_files(root_dir): - for path in Path(root_dir).rglob('highlights.scm'): - yield path - -def count_instances(files): - instances: defaultdict[list[Any], dict[str, Any]] = defaultdict(lambda: {'count': 0, 'languages': set()}) - for file_path in files: - language = file_path.parent.name - with open(file_path, "r") as file: - text = file.read() - matches = pattern.findall(text) - for match in matches: - instances[match]['count'] += 1 - instances[match]['languages'].add(language) - return instances - -def print_instances(instances, verbose=False): - for item, details in sorted(instances.items(), key=lambda x: x[0]): - languages = ', '.join(sorted(details['languages'])) - if verbose: - print(f"{item} ({details['count']}) - [{languages}]") - else: - print(f"{item} ({details['count']})") - -def main(): - args = parse_arguments() - - base_dir = Path(__file__).parent.parent - core_path = base_dir / 'crates/languages/src' - extension_path = base_dir / 'extensions/' - - core_instances = count_instances(find_highlight_files(core_path)) - extension_instances = count_instances(find_highlight_files(extension_path)) - - unique_extension_instances = {k: v for k, v in extension_instances.items() if k not in core_instances} - - print('Shared:\n') - print_instances(core_instances, args.verbose) - - if unique_extension_instances: - print('\nExtension-only:\n') - print_instances(unique_extension_instances, args.verbose) - -if __name__ == '__main__': - main() diff --git a/script/docs-strip-preview-callouts b/script/docs-strip-preview-callouts deleted file mode 100755 index ca88fa22b5e..00000000000 --- a/script/docs-strip-preview-callouts +++ /dev/null @@ -1,231 +0,0 @@ -#!/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 (ignore untracked files) -if [[ -n "$(git status --porcelain docs/ | grep -v '^??' || true)" ]]; 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. - -Release Notes: - -- N/A" - -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 deleted file mode 100755 index a24f2a1fa8f..00000000000 --- a/script/docs-suggest +++ /dev/null @@ -1,620 +0,0 @@ -#!/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. - -Before analysis, read and follow these rule files: -- `.rules` -- `docs/.rules` - -## 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 deleted file mode 100755 index fc420f3fbc7..00000000000 --- a/script/docs-suggest-publish +++ /dev/null @@ -1,591 +0,0 @@ -#!/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) - 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<BATCH_COUNT; batch++ )); do - START=$(( batch * BATCH_SIZE )) - END=$(( START + BATCH_SIZE )) - if [[ "$END" -gt "$TOTAL" ]]; then - END=$TOTAL - fi - - BATCH_NUM=$(( batch + 1 )) - BATCH_SUGGESTIONS_FILE="$TMPDIR/batch-${BATCH_NUM}-suggestions.md" - BATCH_PROMPT_FILE="$TMPDIR/batch-${BATCH_NUM}-prompt.md" - BATCH_SUMMARY_FILE="$TMPDIR/batch-${BATCH_NUM}-summary.md" - - echo -e "${BLUE}Batch $BATCH_NUM/$BATCH_COUNT${NC} (suggestions $(( START + 1 ))-$END of $TOTAL)" - - # Combine suggestion files for this batch, skipping flagged PRs and annotating stable PRs - BATCH_HAS_SUGGESTIONS=false - for (( i=START; i<END; i++ )); do - file="${SUGGESTION_FILES[$i]}" - pr_num=$(echo "$MANIFEST" | jq -r --arg f "$file" '.suggestions[] | select(.file == $f) | .pr') - - # Skip PRs behind feature flags entirely - is_flagged=false - for flagged_pr in "${FLAGGED_PRS[@]+"${FLAGGED_PRS[@]}"}"; do - if [[ "$flagged_pr" == "$pr_num" ]]; then - is_flagged=true - break - fi - done - if [[ "$is_flagged" == "true" ]]; then - log "Skipping PR #$pr_num (behind feature flag)" - { - echo "### Skipped: PR #$pr_num" - echo "" - echo "This PR is behind a feature flag and was not applied." - echo "" - } >> "$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" diff --git a/script/prompts b/script/prompts deleted file mode 100755 index 5486600c1ee..00000000000 --- a/script/prompts +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash - -# This script manages prompt overrides for the Zed editor. -# -# It provides functionality to: -# 1. Link the current repository's prompt templates to Zed's configuration. -# 2. Create and link a separate Git worktree for prompt management. -# 3. Unlink previously linked prompt overrides. -# -# Usage: -# ./script_name.sh link # Link current repo's prompts -# ./script_name.sh link --worktree # Create and link a separate worktree -# ./script_name.sh unlink # Remove existing prompt override link -# -# The script ensures proper Git branch and worktree setup when using the -# --worktree option. It also provides informative output and error handling. - -if [ "$1" = "link" ]; then - # Remove existing link (or directory) - rm -rf ~/.config/zed/prompt_overrides - if [ "$2" = "--worktree" ]; then - # Check if 'prompts' branch exists, create if not - if ! git show-ref --quiet refs/heads/prompts; then - git branch prompts - fi - # Check if 'prompts' worktree exists - if git worktree list | grep -q "../zed_prompts"; then - echo "Worktree already exists at ../zed_prompts." - else - # Create worktree if it doesn't exist - git worktree add ../zed_prompts prompts || git worktree add ../zed_prompts -b prompts - fi - ln -sf "$(realpath "$(pwd)/../zed_prompts/assets/prompts")" ~/.config/zed/prompt_overrides - echo "Linked $(realpath "$(pwd)/../zed_prompts/assets/prompts") to ~/.config/zed/prompt_overrides" - echo -e "\033[0;33mDon't forget you have it linked, or your prompts will go stale\033[0m" - else - ln -sf "$(pwd)/assets/prompts" ~/.config/zed/prompt_overrides - echo "Linked $(pwd)/assets/prompts to ~/.config/zed/prompt_overrides" - fi -elif [ "$1" = "unlink" ]; then - if [ -e ~/.config/zed/prompt_overrides ]; then - # Remove symbolic link - rm -rf ~/.config/zed/prompt_overrides - echo "Unlinked ~/.config/zed/prompt_overrides" - else - echo -e "\033[33mWarning: No file exists at ~/.config/zed/prompt_overrides\033[0m" - fi -else - echo "This script helps you manage prompt overrides for Zed." - echo "You can link this directory to have Zed use the contents of your current repo templates as your active prompts," - echo "or store your modifications in a separate Git worktree." - echo - echo "Usage: $0 [link [--worktree]|unlink]" - echo - echo "Options:" - echo " link Create a symbolic link from ./assets/prompts to ~/.config/zed/prompt_overrides" - echo " link --worktree Create a 'prompts' Git worktree in ../prompts, then link ../prompts/assets/prompts" - echo " to ~/.config/zed/prompt_overrides" - echo " unlink Remove the symbolic link at ~/.config/zed/prompt_overrides" - exit 1 -fi diff --git a/script/randomized-test-ci b/script/randomized-test-ci deleted file mode 100755 index 36eab40a7a8..00000000000 --- a/script/randomized-test-ci +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env node --redirect-warnings=/dev/null - -const fs = require("fs"); -const { randomBytes } = require("crypto"); -const { execFileSync } = require("child_process"); -const { - minimizeTestPlan, - buildTests, - runTests, -} = require("./randomized-test-minimize"); - -const { ZED_SERVER_URL } = process.env; -if (!ZED_SERVER_URL) throw new Error("Missing env var `ZED_SERVER_URL`"); - -main(); - -async function main() { - buildTests(); - - const seed = randomU64(); - const commit = execFileSync("git", ["rev-parse", "HEAD"], { - encoding: "utf8", - }).trim(); - - console.log("commit:", commit); - console.log("starting seed:", seed); - - const planPath = "target/test-plan.json"; - const minPlanPath = "target/test-plan.min.json"; - const failingSeed = runTests({ - SEED: seed, - SAVE_PLAN: planPath, - ITERATIONS: 50000, - OPERATIONS: 200, - }); - - if (!failingSeed) { - console.log("tests passed"); - return; - } - - console.log("found failure at seed", failingSeed); - const minimizedSeed = minimizeTestPlan(planPath, minPlanPath); - const minimizedPlan = fs.readFileSync(minPlanPath, "utf8"); - - console.log("minimized plan:\n", minimizedPlan); - - const url = `${ZED_SERVER_URL}/api/randomized_test_failure`; - const body = { - seed: minimizedSeed, - plan: JSON.parse(minimizedPlan), - commit: commit, - }; - await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - - process.exit(1); -} - -function randomU64() { - const bytes = randomBytes(8); - const hexString = bytes.reduce( - (string, byte) => string + byte.toString(16), - "", - ); - return BigInt("0x" + hexString).toString(10); -} diff --git a/script/randomized-test-minimize b/script/randomized-test-minimize deleted file mode 100755 index efed3ee5018..00000000000 --- a/script/randomized-test-minimize +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env node --redirect-warnings=/dev/null - -const fs = require("fs"); -const path = require("path"); -const { spawnSync } = require("child_process"); - -const FAILING_SEED_REGEX = /failing seed: (\d+)/gi; -const CARGO_TEST_ARGS = ["--release", "--lib", "--package", "collab"]; - -if (require.main === module) { - if (process.argv.length < 4) { - process.stderr.write( - "usage: script/randomized-test-minimize <input-plan> <output-plan> [start-index]\n", - ); - process.exit(1); - } - - minimizeTestPlan( - process.argv[2], - process.argv[3], - parseInt(process.argv[4]) || 0, - ); -} - -function minimizeTestPlan(inputPlanPath, outputPlanPath, startIndex = 0) { - const tempPlanPath = inputPlanPath + ".try"; - - fs.copyFileSync(inputPlanPath, outputPlanPath); - let testPlan = JSON.parse(fs.readFileSync(outputPlanPath, "utf8")); - - process.stderr.write("minimizing failing test plan...\n"); - for (let ix = startIndex; ix < testPlan.length; ix++) { - // Skip 'MutateClients' entries, since they themselves are not single operations. - if (testPlan[ix].MutateClients) { - continue; - } - - // Remove a row from the test plan - const newTestPlan = testPlan.slice(); - newTestPlan.splice(ix, 1); - fs.writeFileSync(tempPlanPath, serializeTestPlan(newTestPlan), "utf8"); - - process.stderr.write( - `${ix}/${testPlan.length}: ${JSON.stringify(testPlan[ix])}`, - ); - const failingSeed = runTests({ - SEED: "0", - LOAD_PLAN: tempPlanPath, - SAVE_PLAN: tempPlanPath, - ITERATIONS: "500", - }); - - // If the test failed, keep the test plan with the removed row. Reload the test - // plan from the JSON file, since the test itself will remove any operations - // which are no longer valid before saving the test plan. - if (failingSeed != null) { - process.stderr.write(` - remove. failing seed: ${failingSeed}.\n`); - fs.copyFileSync(tempPlanPath, outputPlanPath); - testPlan = JSON.parse(fs.readFileSync(outputPlanPath, "utf8")); - ix--; - } else { - process.stderr.write(` - keep.\n`); - } - } - - fs.unlinkSync(tempPlanPath); - - // Re-run the final minimized plan to get the correct failing seed. - // This is a workaround for the fact that the execution order can - // slightly change when replaying a test plan after it has been - // saved and loaded. - const failingSeed = runTests({ - SEED: "0", - ITERATIONS: "5000", - LOAD_PLAN: outputPlanPath, - }); - - process.stderr.write(`final test plan: ${outputPlanPath}\n`); - process.stderr.write(`final seed: ${failingSeed}\n`); - return failingSeed; -} - -function buildTests() { - const { status } = spawnSync( - "cargo", - ["test", "--no-run", ...CARGO_TEST_ARGS], - { - stdio: "inherit", - encoding: "utf8", - env: { - ...process.env, - }, - }, - ); - if (status !== 0) { - throw new Error("build failed"); - } -} - -function runTests(env) { - const { status, stdout } = spawnSync( - "cargo", - ["test", ...CARGO_TEST_ARGS, "random_project_collaboration"], - { - stdio: "pipe", - encoding: "utf8", - env: { - ...process.env, - ...env, - }, - }, - ); - - if (status !== 0) { - FAILING_SEED_REGEX.lastIndex = 0; - const match = FAILING_SEED_REGEX.exec(stdout); - if (!match) { - process.stderr.write("test failed, but no failing seed found:\n"); - process.stderr.write(stdout); - process.stderr.write("\n"); - process.exit(1); - } - return match[1]; - } else { - return null; - } -} - -function serializeTestPlan(plan) { - return "[\n" + plan.map((row) => JSON.stringify(row)).join(",\n") + "\n]\n"; -} - -exports.buildTests = buildTests; -exports.runTests = runTests; -exports.minimizeTestPlan = minimizeTestPlan; diff --git a/script/remote-server b/script/remote-server deleted file mode 100755 index 1ab2d943e16..00000000000 --- a/script/remote-server +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -xeuo pipefail - -# if root or if sudo/unavailable, define an empty variable -if [ "$(id -u)" -eq 0 ] -then maysudo='' -else maysudo="$(command -v sudo || command -v doas || true)" -fi - -deps=( - clang -) - -$maysudo apt-get update -$maysudo apt-get install -y "${deps[@]}" -exit 0 diff --git a/script/run-background-agent-mvp-local b/script/run-background-agent-mvp-local deleted file mode 100755 index f4f7bf7b8e3..00000000000 --- a/script/run-background-agent-mvp-local +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env python3 -"""Run the background crash-agent MVP pipeline locally. - -Default mode is dry-run: generate branch + agent output without commit/push/PR. -""" - -import argparse -import json -import os -import re -import subprocess -import sys -import tempfile -from datetime import datetime -from pathlib import Path - - -CRASH_ID_PATTERN = re.compile(r"^[A-Za-z0-9]+-[A-Za-z0-9]+$") - - -REPO_ROOT = Path(__file__).resolve().parents[1] - - -def run(command: list[str], check: bool = True, capture_output: bool = False) -> subprocess.CompletedProcess: - return subprocess.run( - command, - cwd=REPO_ROOT, - text=True, - check=check, - capture_output=capture_output, - ) - - -def validate_crash_ids(ids: list[str]) -> list[str]: - valid = [] - for crash_id in ids: - if not CRASH_ID_PATTERN.match(crash_id): - print(f"WARNING: Skipping invalid crash ID format: '{crash_id}'", file=sys.stderr) - continue - valid.append(crash_id) - return valid - - -MAX_TOP = 100 - - -def prefetch_crash_data(crashes: list[dict[str, str]], output_dir: str) -> None: - """Fetch crash reports and save to output_dir/crash-{ID}.md. - - Each crash item must contain: - - crash_id: short ID used by the pipeline (e.g. ZED-202) - - fetch_id: identifier passed to script/sentry-fetch (short or numeric) - """ - os.makedirs(output_dir, exist_ok=True) - for crash in crashes: - crash_id = crash["crash_id"] - fetch_id = crash["fetch_id"] - output_path = os.path.join(output_dir, f"crash-{crash_id}.md") - result = run( - ["python3", "script/sentry-fetch", fetch_id], - check=False, - capture_output=True, - ) - if result.returncode != 0: - print( - f"WARNING: Failed to fetch crash data for {crash_id} " - f"(fetch id: {fetch_id}): {result.stderr.strip()}", - file=sys.stderr, - ) - continue - with open(output_path, "w", encoding="utf-8") as f: - f.write(result.stdout) - print( - f"Fetched crash data for {crash_id} (fetch id: {fetch_id}) -> {output_path}", - file=sys.stderr, - ) - - -def resolve_crashes(args) -> list[dict[str, str]]: - if args.crash_ids: - raw = [item.strip() for item in args.crash_ids.split(",") if item.strip()] - crash_ids = validate_crash_ids(raw) - return [{"crash_id": crash_id, "fetch_id": crash_id} for crash_id in crash_ids] - - top = min(args.top, MAX_TOP) - if args.top > MAX_TOP: - print(f"Capping --top from {args.top} to {MAX_TOP}", file=sys.stderr) - - with tempfile.NamedTemporaryFile(mode="w+", suffix=".json", delete=False) as file: - output_path = file.name - - try: - run( - [ - "python3", - "script/select-sentry-crash-candidates", - "--org", - args.org, - "--top", - str(top), - "--sample-size", - str(args.sample_size), - "--output", - output_path, - ], - capture_output=True, - ) - with open(output_path, "r", encoding="utf-8") as file: - payload = json.load(file) - crashes = [] - for item in payload.get("selected", []): - crash_id = item.get("short_id") - issue_id = item.get("issue_id") - if not crash_id: - continue - crashes.append( - { - "crash_id": crash_id, - "fetch_id": str(issue_id) if issue_id else crash_id, - } - ) - - valid_crash_ids = set(validate_crash_ids([item["crash_id"] for item in crashes])) - return [item for item in crashes if item["crash_id"] in valid_crash_ids] - finally: - try: - os.remove(output_path) - except OSError: - pass - - -def write_prompt(crash_id: str, crash_data_file: str | None = None) -> Path: - if crash_data_file: - fetch_step = ( - f"The crash report has been pre-fetched and is available at: {crash_data_file}\n" - f"Read this file to get the crash data. Do not call script/sentry-fetch.\n" - f"\n" - f"1. Read the crash report from {crash_data_file}" - ) - else: - fetch_step = f"1. Fetch crash data with: script/sentry-fetch {crash_id}" - - prompt = f"""You are running the weekly background crash-fix MVP pipeline for crash {crash_id}. - -Required workflow: -{fetch_step} -2. Read and follow `.rules`. -3. Follow .factory/prompts/crash/investigate.md and write ANALYSIS.md -4. Follow .factory/prompts/crash/link-issues.md and write LINKED_ISSUES.md -5. Follow .factory/prompts/crash/fix.md to implement a minimal fix with tests -6. Run validators required by the fix prompt for the affected code paths -7. Write PR_BODY.md with sections: - - Crash Summary - - Root Cause - - Fix - - Validation - - Potentially Related Issues (High/Medium/Low from LINKED_ISSUES.md) - - Reviewer Checklist - - Release Notes (final section, formatted as "Release Notes:", blank line, then one bullet like "- N/A" when not user-facing) - -Constraints: -- Keep changes narrowly scoped to this crash. -- If the crash is not solvable with available context, write a clear blocker summary to PR_BODY.md. -""" - - file_path = Path(tempfile.gettempdir()) / f"background-agent-{crash_id}.md" - file_path.write_text(prompt, encoding="utf-8") - return file_path - - -def run_pipeline_for_crash(args, crash_id: str) -> dict: - branch = f"background-agent/mvp-{crash_id.lower()}-{datetime.utcnow().strftime('%Y%m%d')}" - - if args.reset_branch: - run(["git", "fetch", "origin", "main"], check=False) - run(["git", "checkout", "-B", branch, "origin/main"]) - - prompt_file = write_prompt(crash_id) - try: - droid_command = [ - args.droid_bin, - "exec", - "--auto", - "medium", - "-m", - args.model, - "-f", - str(prompt_file), - ] - completed = run(droid_command, check=False) - - has_changes = run(["git", "diff", "--quiet"], check=False).returncode != 0 - return { - "crash_id": crash_id, - "branch": branch, - "droid_exit_code": completed.returncode, - "has_changes": has_changes, - } - finally: - try: - prompt_file.unlink() - except OSError: - pass - - -def main() -> int: - parser = argparse.ArgumentParser(description="Run local background-agent MVP dry-run workflow") - parser.add_argument("--crash-ids", help="Comma-separated crash IDs (e.g. ZED-4VS,ZED-123)") - parser.add_argument("--top", type=int, default=3, help="Top N crashes when --crash-ids is omitted (max 100)") - parser.add_argument( - "--sample-size", - type=int, - default=100, - help="Number of unresolved issues to consider for candidate selection", - ) - parser.add_argument("--org", default="zed-dev", help="Sentry org slug") - parser.add_argument( - "--select-only", - action="store_true", - help="Resolve crash IDs and print them comma-separated, then exit. No agent execution.", - ) - parser.add_argument( - "--prefetch-dir", - help="When used with --select-only, also fetch crash data via script/sentry-fetch " - "and save reports to DIR/crash-{ID}.md for each resolved ID.", - ) - parser.add_argument("--model", default=os.environ.get("DROID_MODEL", "claude-opus-4-5-20251101")) - parser.add_argument("--droid-bin", default=os.environ.get("DROID_BIN", "droid")) - parser.add_argument( - "--reset-branch", - action="store_true", - help="For each crash, checkout a fresh local branch from origin/main", - ) - args = parser.parse_args() - - crashes = resolve_crashes(args) - if not crashes: - print("No crash IDs were selected.", file=sys.stderr) - return 1 - - crash_ids = [item["crash_id"] for item in crashes] - - if args.select_only: - if args.prefetch_dir: - prefetch_crash_data(crashes, args.prefetch_dir) - print(",".join(crash_ids)) - return 0 - - print(f"Running local dry-run for crashes: {', '.join(crash_ids)}") - results = [run_pipeline_for_crash(args, crash_id) for crash_id in crash_ids] - - print("\nRun summary:") - for result in results: - print( - f"- {result['crash_id']}: droid_exit={result['droid_exit_code']} " - f"changes={result['has_changes']} branch={result['branch']}" - ) - - failures = [result for result in results if result["droid_exit_code"] != 0] - return 1 if failures else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/script/test-docs-suggest-batch b/script/test-docs-suggest-batch deleted file mode 100755 index 3bd6c9811a4..00000000000 --- a/script/test-docs-suggest-batch +++ /dev/null @@ -1,139 +0,0 @@ -#!/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" diff --git a/script/triage_watcher.jl b/script/triage_watcher.jl deleted file mode 100644 index 905d2fdd4eb..00000000000 --- a/script/triage_watcher.jl +++ /dev/null @@ -1,38 +0,0 @@ -## Triage Watcher v0.1 -# This is a small script to watch for new issues on the Zed repository and open them in a new browser tab interactively. -# -## Installing Julia -# -# You need Julia installed on your system: -# curl -fsSL https://install.julialang.org | sh -# -## Running this script: -# 1. It only works on Macos/Linux -# Open a new Julia repl with `julia` inside the `zed` repo -# 2. Paste the following code -# 3. Whenever you close your computer, just type the Up arrow on the REPL + enter to rerun the loop again to resume -function get_issues() - entries = filter(x -> occursin("state:needs triage", x), split(read(`gh issue list -L 10`, String), '\n')) - top = findfirst.('\t', entries) .- 1 - [entries[i][begin:top[i]] for i in eachindex(entries)] -end - -nums = get_issues(); -while true - new_nums = get_issues() - # Open each new issue in a new browser tab - for issue_num in setdiff(new_nums, nums) - url = "https://github.com/zed-industries/zed/issues/" * issue_num - println("\nOpening $url") - open_tab = `/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome $url` - try - sound_file = "/Users/mrg/Downloads/mario_coin_sound.mp3" - run(`afplay -v 0.02 $sound_file`) - finally - end - run(open_tab) - end - nums = new_nums - print("🧘🏼") - sleep(60) -end diff --git a/tooling/xtask/src/tasks/workflows/deploy_collab.rs b/tooling/xtask/src/tasks/workflows/deploy_collab.rs index 5302bc2f3ff..4bd0e8ff1f4 100644 --- a/tooling/xtask/src/tasks/workflows/deploy_collab.rs +++ b/tooling/xtask/src/tasks/workflows/deploy_collab.rs @@ -16,7 +16,11 @@ pub(crate) fn deploy_collab() -> Workflow { named::workflow() .with_minimal_permissions() - .on(Event::default().push(Push::default().add_tag("collab-production"))) + .on(Event::default().push( + Push::default() + .add_tag("collab-production") + .add_tag("collab-staging"), + )) .add_env(("DOCKER_BUILDKIT", "1")) .add_job(style.name, style.job) .add_job(tests.name, tests.job)