Skip to content

🤖 Auto Issue Fix Pipeline #31

🤖 Auto Issue Fix Pipeline

🤖 Auto Issue Fix Pipeline #31

name: 🤖 Auto Issue Fix Pipeline
# Multi-agent pipeline that:
# 1. Scans and triages recent GitHub issues (issue-scanner agent)
# 2. Fixes the selected issue and pushes a fix branch (issue-fixer agent)
# 3. Verifies the fix branch against the DTS emulator (pr-verification agent)
#
# Agents are chained sequentially via file-based handoff:
# issue-scanner → /tmp/selected-issue.json → issue-fixer → /tmp/fix-branch-info.json → pr-verification
#
# Note: GitHub Copilot CLI agents do not support built-in handoffs in CI.
# Chaining is achieved by running agents as sequential workflow steps and
# injecting the previous agent's output into the next agent's prompt.
#
# The fixer agent pushes a branch (not a PR) because the workflow only has
# pull-requests: read permission. A human opens the PR from the branch.
on:
# Run every day at 09:00 UTC
schedule:
- cron: "0 9 * * *"
# Allow manual trigger for testing
workflow_dispatch:
permissions:
contents: write
issues: write
pull-requests: read
jobs:
auto-issue-fix:
runs-on: ubuntu-latest
timeout-minutes: 150
env:
DOTNET_VER_6: "6.0.x"
DOTNET_VER_8: "8.0.x"
DOTNET_VER_10: "10.0.x"
SOLUTION: "Microsoft.DurableTask.sln"
steps:
# ─── Setup ───────────────────────────────────────────────────────
- name: 📥 Checkout code (full history for analysis)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: ⚙️ Setup .NET 6.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VER_6 }}
- name: ⚙️ Setup .NET 8.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VER_8 }}
- name: ⚙️ Setup .NET 10.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VER_10 }}
- name: ⚙️ Setup .NET from global.json
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: 🔨 Restore and build
run: |
dotnet restore $SOLUTION
dotnet build $SOLUTION --configuration Release --no-restore
- name: 🧪 Run tests (non-blocking baseline before analysis)
continue-on-error: true
run: |
dotnet test $SOLUTION --configuration Release --no-build --verbosity normal
# ─── Deduplication Context ───────────────────────────────────────
- name: 🔍 Collect existing work to avoid duplicates
id: dedup
run: |
echo "Fetching open PRs and issues with copilot-finds label..."
# Get open PRs with copilot-finds label (include file paths for targeted dedup)
OPEN_PRS=$(gh pr list \
--label "copilot-finds" \
--state open \
--limit 50 \
--json title,url,headRefName,files \
--jq '[.[] | {title: .title, url: .url, branch: .headRefName, files: [.files[].path]}]' \
2>/dev/null || echo "[]")
# Get open issues with copilot-finds label
OPEN_ISSUES=$(gh issue list \
--label "copilot-finds" \
--state open \
--limit 50 \
--json title,url \
--jq '[.[] | {title: .title, url: .url}]' \
2>/dev/null || echo "[]")
# Get recently merged PRs (last 14 days) — title/url only to avoid prompt bloat
RECENT_MERGED=$(gh pr list \
--label "copilot-finds" \
--state merged \
--limit 50 \
--json title,url,mergedAt \
--jq '[.[] | select((.mergedAt | fromdateiso8601) > (now - 14*86400)) | {title: .title, url: .url}]' \
2>/dev/null || echo "[]")
# Write dedup context
cat <<DEDUP_EOF > /tmp/exclusion-context.txt
=== EXISTING WORK (DO NOT DUPLICATE) ===
## Open PRs with copilot-finds label:
$OPEN_PRS
## Open issues with copilot-finds label:
$OPEN_ISSUES
## Recently merged copilot-finds PRs (last 14 days):
$RECENT_MERGED
=== END EXISTING WORK ===
DEDUP_EOF
echo "Dedup context collected."
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
# ─── Labels ─────────────────────────────────────────────────────
- name: 🏷️ Ensure required labels exist
run: |
set -euo pipefail
ensure_label() {
local name="$1"
local description="$2"
local color="$3"
if gh label list --limit 200 --json name --jq '.[].name' | grep -Fxq "$name"; then
echo "Label '$name' already exists; skipping."
else
echo "Creating label '$name'."
gh label create "$name" --description "$description" --color "$color"
fi
}
# Pipeline labels
ensure_label "copilot-finds" \
"Findings from automated code review agents" "7057ff"
ensure_label "pending-verification" \
"PR awaiting automated verification" "fbca04"
ensure_label "sample-verification-added" \
"PR has been verified by the PR verification agent" "0e8a16"
# Triage labels (used by issue-scanner agent)
ensure_label "triage/actionable" \
"Issue is ready for automated fix — clear scope, no blockers" "1d76db"
ensure_label "triage/needs-human-verification" \
"Requires human judgment or domain expertise to verify" "c5def5"
ensure_label "triage/known-blocker" \
"Has a known dependency or blocker preventing fix" "e4e669"
ensure_label "triage/requires-redesign" \
"Needs architectural changes or design discussion" "d4c5f9"
ensure_label "triage/needs-info" \
"Missing reproduction steps or unclear description" "d93f0b"
ensure_label "triage/already-fixed" \
"Already resolved by a merged PR or closed" "bfdadc"
ensure_label "triage/too-large" \
"Scope too large for a single automated fix" "f9d0c4"
ensure_label "triage/external-dependency" \
"Depends on changes in another repo or service" "c2e0c6"
ensure_label "triage/duplicate" \
"Duplicate of another issue" "cfd3d7"
ensure_label "triage/feature-request" \
"Feature request, not a bug fix" "a2eeef"
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
# ─── Agent 1: Issue Scanner ─────────────────────────────────────
- name: 🤖 Install GitHub Copilot CLI
run: npm install -g @github/copilot
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GH_TOKEN: ${{ github.token }}
- name: 🔍 Agent 1 — Issue Scanner
id: issue_scanner
run: |
EXCLUSION_CONTEXT=$(cat /tmp/exclusion-context.txt)
AGENT_PROMPT=$(cat .github/agents/issue-scanner.agent.md)
FULL_PROMPT=$(cat <<PROMPT_EOF
$AGENT_PROMPT
---
## Pre-loaded Deduplication Context
The following items are already tracked. DO NOT select issues that overlap
with any of these:
$EXCLUSION_CONTEXT
---
## Execution Instructions
You are running in CI. Today's date is $(date +%Y-%m-%d).
Repository: ${{ github.repository }}
Execute the full workflow described above:
1. Fetch the 20 most recent GitHub issues
2. Triage and classify each one
3. Select the single best actionable issue (if any)
4. Write the handoff context to /tmp/selected-issue.json
Remember:
- Be conservative — only select issues with high confidence
- Write the handoff file to /tmp/selected-issue.json (MANDATORY)
- If no actionable issue is found, write {"found": false, ...} and stop
PROMPT_EOF
)
EXIT_CODE=0
timeout --foreground --signal=TERM --kill-after=30s 1200s \
copilot \
--prompt "$FULL_PROMPT" \
--model "claude-opus-4.6" \
--allow-all-tools \
--allow-all-paths \
< /dev/null 2>&1 || EXIT_CODE=$?
if [ $EXIT_CODE -eq 124 ]; then
echo "::warning::Issue scanner agent timed out after 20 minutes"
fi
# Check if an issue was found
if [ -f /tmp/selected-issue.json ]; then
FOUND=$(cat /tmp/selected-issue.json | jq -r '.found // false')
echo "issue_found=$FOUND" >> $GITHUB_OUTPUT
if [ "$FOUND" = "true" ]; then
ISSUE_NUM=$(cat /tmp/selected-issue.json | jq -r '.issueNumber')
echo "issue_number=$ISSUE_NUM" >> $GITHUB_OUTPUT
echo "Issue #$ISSUE_NUM selected for fixing."
else
echo "No actionable issue found. Pipeline will stop."
fi
else
echo "issue_found=false" >> $GITHUB_OUTPUT
echo "::warning::Handoff file not created — no issue found."
fi
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GH_TOKEN: ${{ github.token }}
CI: "true"
NO_COLOR: "1"
TERM: "dumb"
# ─── Agent 2: Issue Fixer ───────────────────────────────────────
- name: 🔧 Agent 2 — Issue Fixer
id: issue_fixer
if: steps.issue_scanner.outputs.issue_found == 'true'
run: |
ISSUE_CONTEXT=$(cat /tmp/selected-issue.json)
AGENT_PROMPT=$(cat .github/agents/issue-fixer.agent.md)
FULL_PROMPT=$(cat <<PROMPT_EOF
$AGENT_PROMPT
---
## Injected Issue Context (from Issue Scanner Agent)
The issue-scanner agent has selected the following issue for you to fix:
\`\`\`json
$ISSUE_CONTEXT
\`\`\`
---
## Execution Instructions
You are running in CI. Today's date is $(date +%Y-%m-%d).
Repository: ${{ github.repository }}
Execute the full workflow described above:
1. Read the injected issue context above
2. Deep-analyze the codebase to understand the problem
3. Implement the fix following ALL repository conventions
4. Add comprehensive unit tests (and integration tests if applicable)
5. Run the full test suite: dotnet test Microsoft.DurableTask.sln --configuration Release
6. Push a fix branch and post a comment on the issue with the branch link
7. Write the handoff context to /tmp/fix-branch-info.json
IMPORTANT: Do NOT open a PR. You do not have permission to create PRs.
Instead, push a branch and comment on the issue so a human can open the PR.
Remember:
- Follow all C# conventions from .github/copilot-instructions.md
- Copyright headers, XML docs, this., Async suffix, sealed private classes
- All tests must pass
- Write the handoff file to /tmp/fix-branch-info.json (MANDATORY)
PROMPT_EOF
)
EXIT_CODE=0
timeout --foreground --signal=TERM --kill-after=30s 2400s \
copilot \
--prompt "$FULL_PROMPT" \
--model "claude-opus-4.6" \
--allow-all-tools \
--allow-all-paths \
< /dev/null 2>&1 || EXIT_CODE=$?
if [ $EXIT_CODE -eq 124 ]; then
echo "::warning::Issue fixer agent timed out after 40 minutes"
fi
# Check if a fix branch was pushed
if [ -f /tmp/fix-branch-info.json ]; then
CREATED=$(cat /tmp/fix-branch-info.json | jq -r '.created // false')
echo "branch_created=$CREATED" >> $GITHUB_OUTPUT
if [ "$CREATED" = "true" ]; then
BRANCH_NAME=$(cat /tmp/fix-branch-info.json | jq -r '.branchName')
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "Fix branch '$BRANCH_NAME' pushed successfully."
else
echo "No fix branch was pushed. Pipeline will stop before verification."
fi
else
echo "branch_created=false" >> $GITHUB_OUTPUT
echo "::warning::Handoff file not created — no fix branch pushed."
fi
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GH_TOKEN: ${{ github.token }}
CI: "true"
NO_COLOR: "1"
TERM: "dumb"
# ─── Agent 3: Branch Verification ──────────────────────────────
- name: 🐳 Start DTS Emulator
if: steps.issue_fixer.outputs.branch_created == 'true'
run: |
docker run --name dts-emulator -d --rm -p 4001:8080 \
mcr.microsoft.com/dts/dts-emulator:latest
echo "Waiting for emulator to be ready..."
for i in $(seq 1 30); do
if nc -z localhost 4001 2>/dev/null; then
echo "Emulator is ready!"
break
fi
if [ "$i" -eq 30 ]; then
echo "Emulator failed to start within 30 seconds"
exit 1
fi
sleep 1
done
- name: 🔎 Agent 3 — Branch Verification
if: steps.issue_fixer.outputs.branch_created == 'true'
run: |
BRANCH_CONTEXT=$(cat /tmp/fix-branch-info.json)
AGENT_PROMPT=$(cat .github/agents/pr-verification.agent.md)
FULL_PROMPT=$(cat <<PROMPT_EOF
$AGENT_PROMPT
---
## Injected Branch Context (from Issue Fixer Agent)
The issue-fixer agent has pushed the following fix branch for you to verify:
\`\`\`json
$BRANCH_CONTEXT
\`\`\`
---
## Execution Instructions
You are running in CI. Today's date is $(date +%Y-%m-%d).
Repository: ${{ github.repository }}
The DTS emulator is running at localhost:4001.
Execute the full workflow described above:
1. Read the injected branch context above
2. Understand the fix from the branch diff
3. Extract the verification scenario
4. Checkout the fix branch and rebuild
5. Create a standalone C# verification sample
6. Run it against the emulator
7. Post verification results to the linked issue
Remember:
- DTS_ENDPOINT=localhost:4001
- DTS_TASKHUB=default
- Always checkout the fix branch before building/running
- Retry up to 2 times on failure
- Maximum timeout per verification: 5 minutes
PROMPT_EOF
)
EXIT_CODE=0
timeout --foreground --signal=TERM --kill-after=30s 1800s \
copilot \
--prompt "$FULL_PROMPT" \
--model "claude-opus-4.6" \
--allow-all-tools \
--allow-all-paths \
< /dev/null 2>&1 || EXIT_CODE=$?
if [ $EXIT_CODE -eq 124 ]; then
echo "::warning::Branch verification agent timed out after 30 minutes"
elif [ $EXIT_CODE -ne 0 ]; then
echo "::warning::Branch verification agent exited with code $EXIT_CODE"
fi
echo "Branch verification agent completed."
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GH_TOKEN: ${{ github.token }}
DTS_ENDPOINT: localhost:4001
DTS_TASKHUB: default
CI: "true"
NO_COLOR: "1"
TERM: "dumb"
- name: 🧹 Stop DTS Emulator
if: always()
run: docker stop dts-emulator 2>/dev/null || true
# ─── Summary ────────────────────────────────────────────────────
- name: 📊 Pipeline Summary
if: always()
run: |
echo "## 🤖 Auto Issue Fix Pipeline Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Date:** $(date +%Y-%m-%d)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Agent 1 results
echo "### Agent 1: Issue Scanner" >> $GITHUB_STEP_SUMMARY
if [ -f /tmp/selected-issue.json ]; then
FOUND=$(cat /tmp/selected-issue.json | jq -r '.found // false')
if [ "$FOUND" = "true" ]; then
ISSUE_NUM=$(cat /tmp/selected-issue.json | jq -r '.issueNumber')
ISSUE_TITLE=$(cat /tmp/selected-issue.json | jq -r '.issueTitle')
echo "- ✅ Selected issue: #$ISSUE_NUM — $ISSUE_TITLE" >> $GITHUB_STEP_SUMMARY
else
REASON=$(cat /tmp/selected-issue.json | jq -r '.reason // "Unknown"')
echo "- ⏭️ No actionable issue found: $REASON" >> $GITHUB_STEP_SUMMARY
fi
else
echo "- ❌ Handoff file not created" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
# Agent 2 results
echo "### Agent 2: Issue Fixer" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.issue_scanner.outputs.issue_found }}" != "true" ]; then
echo "- ⏭️ Skipped (no issue selected)" >> $GITHUB_STEP_SUMMARY
elif [ -f /tmp/fix-branch-info.json ]; then
CREATED=$(cat /tmp/fix-branch-info.json | jq -r '.created // false')
if [ "$CREATED" = "true" ]; then
BRANCH_NAME=$(cat /tmp/fix-branch-info.json | jq -r '.branchName')
BRANCH_URL=$(cat /tmp/fix-branch-info.json | jq -r '.branchUrl')
echo "- ✅ Fix branch pushed: [$BRANCH_NAME]($BRANCH_URL)" >> $GITHUB_STEP_SUMMARY
else
echo "- ⏭️ No fix branch pushed" >> $GITHUB_STEP_SUMMARY
fi
else
echo "- ❌ Fixer agent ran but handoff file was not created (agent may have crashed or timed out)" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
# Agent 3 results
echo "### Agent 3: Branch Verification" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.issue_fixer.outputs.branch_created }}" = "true" ]; then
echo "- ✅ Verification completed (check issue comments for results)" >> $GITHUB_STEP_SUMMARY
else
echo "- ⏭️ Skipped (no fix branch to verify)" >> $GITHUB_STEP_SUMMARY
fi
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}