From 4f862ffbc6efa25303fc843547f60f4fe8929c2f Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 23 Oct 2026 09:30:55 +0000 Subject: [PATCH] Cleanup unused workflows and script files (#66024) Removes a lot of dead code we have floating around here that is no longer used and has not been for ages. If we ever need to check something out here again, we can just revert parts of these. Currently, these just trigger security warnings if anything and thus are dead weight. Hence removing here. Drive-by fix of our collab staging deploys as well. Release Notes: - N/A --- .factory/prompts/crash/fix.md | 84 --- .factory/prompts/crash/investigate.md | 89 --- .factory/prompts/crash/link-issues.md | 89 --- .github/workflows/deploy_collab.yml | 1 + .github/workflows/docs_suggestions.yml | 489 -------------- .github/workflows/hotfix-review-monitor.yml | 115 ---- .github/workflows/stale-pr-reminder.yml | 116 ---- script/analyze_highlights.py | 69 -- script/docs-strip-preview-callouts | 231 ------- script/docs-suggest | 620 ------------------ script/docs-suggest-publish | 591 ----------------- script/prompts | 61 -- script/randomized-test-ci | 70 -- script/randomized-test-minimize | 135 ---- script/remote-server | 17 - script/run-background-agent-mvp-local | 264 -------- script/test-docs-suggest-batch | 139 ---- script/triage_watcher.jl | 38 -- .../src/tasks/workflows/deploy_collab.rs | 6 +- 19 files changed, 6 insertions(+), 3218 deletions(-) delete mode 100644 .factory/prompts/crash/fix.md delete mode 100644 .factory/prompts/crash/investigate.md delete mode 100644 .factory/prompts/crash/link-issues.md delete mode 100644 .github/workflows/docs_suggestions.yml delete mode 100644 .github/workflows/hotfix-review-monitor.yml delete mode 100644 .github/workflows/stale-pr-reminder.yml delete mode 100644 script/analyze_highlights.py delete mode 100755 script/docs-strip-preview-callouts delete mode 100755 script/docs-suggest delete mode 100755 script/docs-suggest-publish delete mode 100755 script/prompts delete mode 100755 script/randomized-test-ci delete mode 100755 script/randomized-test-minimize delete mode 100755 script/remote-server delete mode 100755 script/run-background-agent-mvp-local delete mode 100755 script/test-docs-suggest-batch delete mode 100644 script/triage_watcher.jl 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)