mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-24 21:59:04 +00:00
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/github-script](https://redirect.github.com/actions/github-script) | action | pinDigest | → `f28e40c` | | [actions/setup-python](https://redirect.github.com/actions/setup-python) | action | pinDigest | → `a26af69` | | [namespacelabs/nscloud-cache-action](https://redirect.github.com/namespacelabs/nscloud-cache-action) | action | pinDigest | → `a90bb5d` | | [taiki-e/install-action](https://redirect.github.com/taiki-e/install-action) | action | pinDigest | → `921e2c9` | | [taiki-e/install-action](https://redirect.github.com/taiki-e/install-action) | action | pinDigest | → `b4f2d5c` | | [withastro/automation](https://redirect.github.com/withastro/automation) | action | pinDigest | → `a5bd0c5` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/15138) for more information. --- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- Release Notes: - N/A <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My45MS41IiwidXBkYXRlZEluVmVyIjoiNDMuOTEuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Marshall Bowers <git@maxdeviant.com>
331 lines
13 KiB
YAML
331 lines
13 KiB
YAML
name: background_agent_mvp
|
|
|
|
# NOTE: Scheduled runs disabled as of 2026-02-24. The workflow can still be
|
|
# triggered manually via workflow_dispatch. See Notion doc "Background Agent
|
|
# for Zed" for current status and contact info to resume this work.
|
|
on:
|
|
# schedule:
|
|
# - cron: "0 16 * * 1-5"
|
|
workflow_dispatch:
|
|
inputs:
|
|
crash_ids:
|
|
description: "Optional comma-separated Sentry issue IDs (e.g. ZED-4VS,ZED-123)"
|
|
required: false
|
|
type: string
|
|
reviewers:
|
|
description: "Optional comma-separated GitHub reviewer handles"
|
|
required: false
|
|
type: string
|
|
top:
|
|
description: "Top N candidates when crash_ids is empty"
|
|
required: false
|
|
type: string
|
|
default: "3"
|
|
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
|
|
env:
|
|
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
|
DROID_MODEL: claude-opus-4-5-20251101
|
|
SENTRY_ORG: zed-dev
|
|
|
|
jobs:
|
|
run-mvp:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 180
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Install Droid CLI
|
|
run: |
|
|
curl -fsSL https://app.factory.ai/cli | sh
|
|
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
|
|
echo "DROID_BIN=${HOME}/.local/bin/droid" >> "$GITHUB_ENV"
|
|
"${HOME}/.local/bin/droid" --version
|
|
|
|
- name: Setup Python
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Resolve reviewers
|
|
id: reviewers
|
|
env:
|
|
INPUT_REVIEWERS: ${{ inputs.reviewers }}
|
|
DEFAULT_REVIEWERS: ${{ vars.BACKGROUND_AGENT_REVIEWERS }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "$DEFAULT_REVIEWERS" ]; then
|
|
DEFAULT_REVIEWERS="eholk,morgankrey,osiewicz,bennetbo"
|
|
fi
|
|
REVIEWERS="${INPUT_REVIEWERS:-$DEFAULT_REVIEWERS}"
|
|
REVIEWERS="$(echo "$REVIEWERS" | tr -d '[:space:]')"
|
|
echo "reviewers=$REVIEWERS" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Select crash candidates
|
|
id: candidates
|
|
env:
|
|
INPUT_CRASH_IDS: ${{ inputs.crash_ids }}
|
|
INPUT_TOP: ${{ inputs.top }}
|
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_BACKGROUND_AGENT_MVP_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
PREFETCH_DIR="/tmp/crash-data"
|
|
ARGS=(--select-only --prefetch-dir "$PREFETCH_DIR" --org "$SENTRY_ORG")
|
|
if [ -n "$INPUT_CRASH_IDS" ]; then
|
|
ARGS+=(--crash-ids "$INPUT_CRASH_IDS")
|
|
else
|
|
TARGET_DRAFT_PRS="${INPUT_TOP:-3}"
|
|
if ! [[ "$TARGET_DRAFT_PRS" =~ ^[0-9]+$ ]] || [ "$TARGET_DRAFT_PRS" -lt 1 ]; then
|
|
TARGET_DRAFT_PRS="3"
|
|
fi
|
|
CANDIDATE_TOP=$((TARGET_DRAFT_PRS * 5))
|
|
if [ "$CANDIDATE_TOP" -gt 100 ]; then
|
|
CANDIDATE_TOP=100
|
|
fi
|
|
ARGS+=(--top "$CANDIDATE_TOP" --sample-size 100)
|
|
fi
|
|
|
|
IDS="$(python3 script/run-background-agent-mvp-local "${ARGS[@]}")"
|
|
|
|
if [ -z "$IDS" ]; then
|
|
echo "No candidates selected"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Using crash IDs: $IDS"
|
|
echo "ids=$IDS" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Run background agent pipeline per crash
|
|
id: pipeline
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REVIEWERS: ${{ steps.reviewers.outputs.reviewers }}
|
|
CRASH_IDS: ${{ steps.candidates.outputs.ids }}
|
|
TARGET_DRAFT_PRS_INPUT: ${{ inputs.top }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
git config user.name "factory-droid[bot]"
|
|
git config user.email "138933559+factory-droid[bot]@users.noreply.github.com"
|
|
|
|
# Crash ID format validation regex
|
|
CRASH_ID_PATTERN='^[A-Za-z0-9]+-[A-Za-z0-9]+$'
|
|
TARGET_DRAFT_PRS="${TARGET_DRAFT_PRS_INPUT:-3}"
|
|
if ! [[ "$TARGET_DRAFT_PRS" =~ ^[0-9]+$ ]] || [ "$TARGET_DRAFT_PRS" -lt 1 ]; then
|
|
TARGET_DRAFT_PRS="3"
|
|
fi
|
|
CREATED_DRAFT_PRS=0
|
|
|
|
IFS=',' read -r -a CRASH_ID_ARRAY <<< "$CRASH_IDS"
|
|
|
|
for CRASH_ID in "${CRASH_ID_ARRAY[@]}"; do
|
|
if [ "$CREATED_DRAFT_PRS" -ge "$TARGET_DRAFT_PRS" ]; then
|
|
echo "Reached target draft PR count ($TARGET_DRAFT_PRS), stopping candidate processing"
|
|
break
|
|
fi
|
|
|
|
CRASH_ID="$(echo "$CRASH_ID" | xargs)"
|
|
[ -z "$CRASH_ID" ] && continue
|
|
|
|
# Validate crash ID format to prevent injection via branch names or prompts
|
|
if ! [[ "$CRASH_ID" =~ $CRASH_ID_PATTERN ]]; then
|
|
echo "ERROR: Invalid crash ID format: '$CRASH_ID' — skipping"
|
|
continue
|
|
fi
|
|
|
|
BRANCH="background-agent/mvp-${CRASH_ID,,}-$(date +%Y%m%d)"
|
|
echo "Running crash pipeline for $CRASH_ID on $BRANCH"
|
|
|
|
# Deduplication: skip if a draft PR already exists for this crash
|
|
EXISTING_BRANCH_PR="$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number' || echo "")"
|
|
if [ -n "$EXISTING_BRANCH_PR" ]; then
|
|
echo "Draft PR #$EXISTING_BRANCH_PR already exists for $CRASH_ID — skipping"
|
|
continue
|
|
fi
|
|
|
|
if ! git fetch origin main; then
|
|
echo "WARNING: Failed to fetch origin/main for $CRASH_ID — skipping"
|
|
continue
|
|
fi
|
|
|
|
if ! git checkout -B "$BRANCH" origin/main; then
|
|
echo "WARNING: Failed to create checkout branch $BRANCH for $CRASH_ID — skipping"
|
|
continue
|
|
fi
|
|
|
|
CRASH_DATA_FILE="/tmp/crash-data/crash-${CRASH_ID}.md"
|
|
if [ ! -f "$CRASH_DATA_FILE" ]; then
|
|
echo "WARNING: No pre-fetched crash data for $CRASH_ID at $CRASH_DATA_FILE — skipping"
|
|
continue
|
|
fi
|
|
|
|
python3 -c "
|
|
import sys
|
|
crash_id, data_file = sys.argv[1], sys.argv[2]
|
|
prompt = f'''You are running the weekly background crash-fix MVP pipeline for crash {crash_id}.
|
|
|
|
The crash report has been pre-fetched and is available at: {data_file}
|
|
Read this file to get the crash data. Do not call script/sentry-fetch.
|
|
|
|
Required workflow:
|
|
1. Read the crash report from {data_file}
|
|
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; format as Release Notes:, then a blank line, then one bullet like - N/A)
|
|
|
|
Constraints:
|
|
- Do not merge or auto-approve.
|
|
- Keep changes narrowly scoped to this crash.
|
|
- Do not modify files in .github/, .factory/, or script/ directories.
|
|
- When investigating git history, limit your search to the last 2 weeks of commits. Do not traverse older history.
|
|
- If the crash is not solvable with available context, write a clear blocker summary to PR_BODY.md.
|
|
'''
|
|
import textwrap
|
|
with open('/tmp/background-agent-prompt.md', 'w') as f:
|
|
f.write(textwrap.dedent(prompt))
|
|
" "$CRASH_ID" "$CRASH_DATA_FILE"
|
|
|
|
if ! "$DROID_BIN" exec --auto medium -m "$DROID_MODEL" -f /tmp/background-agent-prompt.md; then
|
|
echo "Droid execution failed for $CRASH_ID, continuing to next candidate"
|
|
continue
|
|
fi
|
|
|
|
for REPORT_FILE in ANALYSIS.md LINKED_ISSUES.md PR_BODY.md; do
|
|
if [ -f "$REPORT_FILE" ]; then
|
|
echo "::group::${CRASH_ID} ${REPORT_FILE}"
|
|
cat "$REPORT_FILE"
|
|
echo "::endgroup::"
|
|
fi
|
|
done
|
|
|
|
if git diff --quiet; then
|
|
echo "No code changes produced for $CRASH_ID"
|
|
continue
|
|
fi
|
|
|
|
# Stage only expected file types — not git add -A
|
|
git add -- '*.rs' '*.toml' 'Cargo.lock' 'ANALYSIS.md' 'LINKED_ISSUES.md' 'PR_BODY.md'
|
|
|
|
# Reject changes to protected paths
|
|
PROTECTED_CHANGES="$(git diff --cached --name-only | grep -E '^(\.github/|\.factory/|script/)' || true)"
|
|
if [ -n "$PROTECTED_CHANGES" ]; then
|
|
echo "ERROR: Agent modified protected paths — aborting commit for $CRASH_ID:"
|
|
echo "$PROTECTED_CHANGES"
|
|
git reset HEAD -- .
|
|
continue
|
|
fi
|
|
|
|
if ! git diff --cached --quiet; then
|
|
git commit -m "Fix crash ${CRASH_ID}"
|
|
fi
|
|
|
|
git push -u origin "$BRANCH"
|
|
|
|
CRATE_PREFIX=""
|
|
CHANGED_CRATES="$(git diff --cached --name-only | awk -F/ '/^crates\/[^/]+\// {print $2}' | sort -u)"
|
|
if [ -n "$CHANGED_CRATES" ] && [ "$(printf "%s\n" "$CHANGED_CRATES" | wc -l | tr -d ' ')" -eq 1 ]; then
|
|
CRATE_PREFIX="${CHANGED_CRATES}: "
|
|
fi
|
|
|
|
TITLE="${CRATE_PREFIX}Fix crash ${CRASH_ID}"
|
|
BODY_FILE="PR_BODY.md"
|
|
if [ ! -f "$BODY_FILE" ]; then
|
|
BODY_FILE="/tmp/pr-body-${CRASH_ID}.md"
|
|
printf "Automated draft crash-fix pipeline output for %s.\n\nNo PR_BODY.md was generated by the agent; please review commit and linked artifacts manually.\n" "$CRASH_ID" > "$BODY_FILE"
|
|
fi
|
|
|
|
python3 -c '
|
|
import re
|
|
import sys
|
|
|
|
path = sys.argv[1]
|
|
body = open(path, encoding="utf-8").read()
|
|
pattern = re.compile(r"(^|\n)Release Notes:\r?\n(?:\r?\n)*(?P<bullets>(?:\s*-\s+.*(?:\r?\n|$))+)", re.MULTILINE)
|
|
match = pattern.search(body)
|
|
|
|
if match:
|
|
bullets = [
|
|
re.sub(r"^\s*", "", bullet)
|
|
for bullet in re.findall(r"^\s*-\s+.*$", match.group("bullets"), re.MULTILINE)
|
|
]
|
|
if not bullets:
|
|
bullets = ["- N/A"]
|
|
section = "Release Notes:\n\n" + "\n".join(bullets)
|
|
body_without_release_notes = (body[: match.start()] + body[match.end() :]).rstrip()
|
|
if body_without_release_notes:
|
|
normalized_body = f"{body_without_release_notes}\n\n{section}\n"
|
|
else:
|
|
normalized_body = f"{section}\n"
|
|
else:
|
|
normalized_body = body.rstrip() + "\n\nRelease Notes:\n\n- N/A\n"
|
|
|
|
with open(path, "w", encoding="utf-8") as file:
|
|
file.write(normalized_body)
|
|
' "$BODY_FILE"
|
|
|
|
EXISTING_PR="$(gh pr list --head "$BRANCH" --json number --jq '.[0].number')"
|
|
if [ -n "$EXISTING_PR" ]; then
|
|
gh pr edit "$EXISTING_PR" --title "$TITLE" --body-file "$BODY_FILE"
|
|
PR_NUMBER="$EXISTING_PR"
|
|
else
|
|
PR_URL="$(gh pr create --draft --base main --head "$BRANCH" --title "$TITLE" --body-file "$BODY_FILE")"
|
|
PR_NUMBER="$(basename "$PR_URL")"
|
|
fi
|
|
|
|
if [ -n "$REVIEWERS" ]; then
|
|
IFS=',' read -r -a REVIEWER_ARRAY <<< "$REVIEWERS"
|
|
for REVIEWER in "${REVIEWER_ARRAY[@]}"; do
|
|
[ -z "$REVIEWER" ] && continue
|
|
gh pr edit "$PR_NUMBER" --add-reviewer "$REVIEWER" || true
|
|
done
|
|
fi
|
|
|
|
CREATED_DRAFT_PRS=$((CREATED_DRAFT_PRS + 1))
|
|
echo "Created/updated draft PRs this run: $CREATED_DRAFT_PRS/$TARGET_DRAFT_PRS"
|
|
done
|
|
|
|
echo "created_draft_prs=$CREATED_DRAFT_PRS" >> "$GITHUB_OUTPUT"
|
|
echo "target_draft_prs=$TARGET_DRAFT_PRS" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Cleanup pre-fetched crash data
|
|
if: always()
|
|
run: rm -rf /tmp/crash-data
|
|
|
|
- name: Workflow summary
|
|
if: always()
|
|
env:
|
|
SUMMARY_CRASH_IDS: ${{ steps.candidates.outputs.ids }}
|
|
SUMMARY_REVIEWERS: ${{ steps.reviewers.outputs.reviewers }}
|
|
SUMMARY_CREATED_DRAFT_PRS: ${{ steps.pipeline.outputs.created_draft_prs }}
|
|
SUMMARY_TARGET_DRAFT_PRS: ${{ steps.pipeline.outputs.target_draft_prs }}
|
|
run: |
|
|
{
|
|
echo "## Background Agent MVP"
|
|
echo ""
|
|
echo "- Crash IDs: ${SUMMARY_CRASH_IDS:-none}"
|
|
echo "- Reviewer routing: ${SUMMARY_REVIEWERS:-NOT CONFIGURED}"
|
|
echo "- Draft PRs created: ${SUMMARY_CREATED_DRAFT_PRS:-0}/${SUMMARY_TARGET_DRAFT_PRS:-3}"
|
|
echo "- Pipeline: investigate -> link-issues -> fix -> draft PR"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
concurrency:
|
|
group: background-agent-mvp
|
|
cancel-in-progress: false
|