mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 08:01:26 +00:00
0c8caa050f
* add flow for aider review * add aider conventions, use ubicloud, ignore files * better if * use cursor rules * restrict to aider prs * fix
343 lines
16 KiB
YAML
343 lines
16 KiB
YAML
name: Aider Auto-fix issues and PR comments via external prompt
|
|
|
|
on:
|
|
issue_comment:
|
|
types: [created]
|
|
|
|
jobs:
|
|
auto-fix:
|
|
runs-on: ubicloud-standard-8
|
|
if: |
|
|
github.event_name == 'issue_comment' &&
|
|
contains(github.event.comment.body, '/aider') &&
|
|
!contains(github.event.comment.user.login, '[bot]')
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
issues: write
|
|
env:
|
|
GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
|
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }}
|
|
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@v2
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Check out code
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Configure Git User
|
|
run: |
|
|
git config --global user.name "github-actions[bot]"
|
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
|
|
- name: Checkout PR Branch
|
|
if: github.event_name == 'issue_comment' && github.event.issue.pull_request
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
echo "Issue comment trigger: Checking out PR branch..."
|
|
PR_NUMBER=${{ github.event.issue.number }}
|
|
PR_HEAD_REF=$(gh pr view $PR_NUMBER --json headRefName -q .headRefName --repo $GITHUB_REPOSITORY)
|
|
if [[ -z "$PR_HEAD_REF" || "$PR_HEAD_REF" == "null" ]]; then
|
|
echo "::error::Could not determine PR head branch for PR #$PR_NUMBER via gh CLI."
|
|
exit 1
|
|
fi
|
|
echo "Checking out PR head branch: $PR_HEAD_REF for PR #$PR_NUMBER"
|
|
git fetch origin "refs/heads/${PR_HEAD_REF}:refs/remotes/origin/${PR_HEAD_REF}" --no-tags
|
|
git checkout "$PR_HEAD_REF"
|
|
echo "Successfully checked out branch $(git rev-parse --abbrev-ref HEAD)"
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install Aider and Dependencies
|
|
run: |
|
|
python -m pip install aider-install; aider-install
|
|
pip install -U google-generativeai
|
|
sudo apt-get update && sudo apt-get install -y jq
|
|
|
|
- name: Determine Prompt for Aider
|
|
id: determine_prompt
|
|
shell: bash
|
|
run: |
|
|
PROMPT_FILE_PATH=".github/aider/issue-prompt.txt"
|
|
mkdir -p .github/aider
|
|
|
|
# Determine if this is a PR comment or regular issue comment
|
|
if [[ ! -z "${{ github.event.issue.pull_request }}" ]]; then
|
|
echo "This is a comment on a Pull Request"
|
|
PR_NUMBER="${{ github.event.issue.number }}"
|
|
|
|
# Get PR description to check for issue references
|
|
PR_BODY=$(gh pr view $PR_NUMBER --json body -q .body --repo $GITHUB_REPOSITORY)
|
|
|
|
# Extract issue number from PR description (looking for #123 or "fixes #123" patterns)
|
|
REFERENCED_ISSUE=$(echo "$PR_BODY" | grep -oE "#[0-9]+" | grep -oE "[0-9]+" | head -1)
|
|
|
|
if [[ ! -z "$REFERENCED_ISSUE" ]]; then
|
|
echo "Found referenced issue #$REFERENCED_ISSUE in PR description"
|
|
|
|
# Fetch the referenced issue details
|
|
ISSUE_DETAILS=$(gh issue view $REFERENCED_ISSUE --json title,body --repo $GITHUB_REPOSITORY)
|
|
ISSUE_TITLE=$(echo "$ISSUE_DETAILS" | jq -r .title)
|
|
ISSUE_BODY=$(echo "$ISSUE_DETAILS" | jq -r .body)
|
|
|
|
# Store raw comment body in a file first to avoid shell interpretation issues
|
|
echo '${{ github.event.comment.body }}' > /tmp/raw_comment.txt
|
|
RAW_COMMENT_BODY=$(cat /tmp/raw_comment.txt)
|
|
# Remove the /aider prefix and trim whitespace
|
|
COMMENT_CONTENT=$(echo "$RAW_COMMENT_BODY" | sed 's|^/aider||' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
|
|
echo "Sending issue content and PR comment to external API…"
|
|
|
|
ISSUE_TITLE_Q=$(printf '%q' "$ISSUE_TITLE")
|
|
ISSUE_BODY_Q=$(printf '%q' "$ISSUE_BODY")
|
|
|
|
JSON_PAYLOAD=$(jq -n \
|
|
--arg title "$ISSUE_TITLE_Q" \
|
|
--arg body "$ISSUE_BODY_Q" \
|
|
'{"body":{"issue_title":$title,"issue_body":$body}}')
|
|
|
|
API_RESULT=$(curl -s -w "\n%{http_code}" \
|
|
-X POST "https://app.windmill.dev/api/w/windmill-labs/jobs/run_wait_result/p/f/ai/quiet_script" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $WINDMILL_TOKEN" \
|
|
--data-binary "$JSON_PAYLOAD" \
|
|
--max-time 90)
|
|
|
|
HTTP_CODE=$(echo "$API_RESULT" | tail -n1)
|
|
BODY=$(echo "$API_RESULT" | sed '$d')
|
|
|
|
echo "$BODY" > /tmp/api_response.txt
|
|
|
|
BASE_PROMPT="Try to fix the following issue based on the instruction given by the user. The issue is prepended with the word ISSUE. The instruction is prepended with the word INSTRUCTION. The issue and instruction are separated by a blank line."
|
|
if [[ "$HTTP_CODE" -eq 200 ]]; then
|
|
PROCESSED_ISSUE_PROMPT=$(jq -r '.effective_body // empty' /tmp/api_response.txt)
|
|
if [[ -z "$PROCESSED_ISSUE_PROMPT" || "$PROCESSED_ISSUE_PROMPT" == "null" ]]; then
|
|
PROCESSED_ISSUE_PROMPT=""
|
|
fi
|
|
printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \
|
|
"$BASE_PROMPT" "$PROCESSED_ISSUE_PROMPT" "$COMMENT_CONTENT" > "$PROMPT_FILE_PATH"
|
|
else
|
|
echo "::warning::API call failed (HTTP $HTTP_CODE). Using PR comment with issue context."
|
|
printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \
|
|
"$BASE_PROMPT" "$ISSUE_BODY_Q" "$COMMENT_CONTENT" > "$PROMPT_FILE_PATH"
|
|
fi
|
|
rm -f /tmp/api_response.txt
|
|
else
|
|
echo "No referenced issue found in PR description, using comment content only"
|
|
# Use comment content directly as with regular issue comments
|
|
echo '${{ github.event.comment.body }}' > /tmp/raw_comment.txt
|
|
RAW_COMMENT_BODY=$(cat /tmp/raw_comment.txt)
|
|
COMMENT_CONTENT=$(echo "$RAW_COMMENT_BODY" | sed 's|^/aider||' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
|
|
if [[ -z "$COMMENT_CONTENT" ]]; then
|
|
echo "::error::Comment with /aider provided, but no instruction found after it. Cannot proceed."
|
|
printf "Error: /aider command found but no instruction followed." > "$PROMPT_FILE_PATH"
|
|
exit 1
|
|
else
|
|
echo "Using comment content as prompt."
|
|
printf '%s' "$COMMENT_CONTENT" > "$PROMPT_FILE_PATH"
|
|
fi
|
|
fi
|
|
else
|
|
echo "This is a comment on a regular issue"
|
|
|
|
# Fetch the issue details
|
|
ISSUE_NUMBER="${{ github.event.issue.number }}"
|
|
ISSUE_DETAILS=$(gh issue view $ISSUE_NUMBER --json title,body --repo $GITHUB_REPOSITORY)
|
|
ISSUE_TITLE=$(echo "$ISSUE_DETAILS" | jq -r .title)
|
|
ISSUE_BODY=$(echo "$ISSUE_DETAILS" | jq -r .body)
|
|
|
|
# Store raw comment body in a file first to avoid shell interpretation issues
|
|
echo '${{ github.event.comment.body }}' > /tmp/raw_comment.txt
|
|
# Extract the command part safely
|
|
RAW_COMMENT_BODY=$(cat /tmp/raw_comment.txt)
|
|
# Remove the /aider prefix and trim whitespace
|
|
COMMENT_CONTENT=$(echo "$RAW_COMMENT_BODY" | sed 's|^/aider||' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
|
|
if [[ -z "$COMMENT_CONTENT" ]]; then
|
|
echo "::error::Comment with /aider provided, but no instruction found after it. Cannot proceed."
|
|
printf "Error: /aider command found but no instruction followed." > "$PROMPT_FILE_PATH"
|
|
exit 1
|
|
else
|
|
echo "Sending issue content and issue comment to external API…"
|
|
|
|
ISSUE_TITLE_Q=$(printf '%q' "$ISSUE_TITLE")
|
|
ISSUE_BODY_Q=$(printf '%q' "$ISSUE_BODY")
|
|
COMMENT_CONTENT_Q=$(printf '%q' "$COMMENT_CONTENT")
|
|
|
|
JSON_PAYLOAD=$(jq -n \
|
|
--arg title "$ISSUE_TITLE_Q" \
|
|
--arg body "$ISSUE_BODY_Q" \
|
|
--arg comment "$COMMENT_CONTENT_Q" \
|
|
'{"body":{"issue_title":$title,"issue_body":$body,"issue_comment":$comment}}')
|
|
|
|
API_RESULT=$(curl -s -w "\n%{http_code}" \
|
|
-X POST "https://app.windmill.dev/api/w/windmill-labs/jobs/run_wait_result/p/f/ai/quiet_script" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $WINDMILL_TOKEN" \
|
|
--data-binary "$JSON_PAYLOAD" \
|
|
--max-time 90)
|
|
|
|
HTTP_CODE=$(echo "$API_RESULT" | tail -n1)
|
|
BODY=$(echo "$API_RESULT" | sed '$d')
|
|
|
|
echo "$BODY" > /tmp/api_response.txt
|
|
|
|
BASE_PROMPT="Try to fix the following issue based on the instruction given by the user. The issue is prepended with the word ISSUE. The instruction is prepended with the word INSTRUCTION. The issue and instruction are separated by a blank line."
|
|
if [[ "$HTTP_CODE" -eq 200 ]]; then
|
|
PROCESSED_ISSUE_PROMPT=$(jq -r '.effective_body // empty' /tmp/api_response.txt)
|
|
if [[ -z "$PROCESSED_ISSUE_PROMPT" || "$PROCESSED_ISSUE_PROMPT" == "null" ]]; then
|
|
PROCESSED_ISSUE_PROMPT=""
|
|
fi
|
|
printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \
|
|
"$BASE_PROMPT" "$PROCESSED_ISSUE_PROMPT" "$COMMENT_CONTENT" > "$PROMPT_FILE_PATH"
|
|
else
|
|
echo "::warning::API call failed (HTTP $HTTP_CODE). Using PR comment with issue context."
|
|
printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \
|
|
"$BASE_PROMPT" "$ISSUE_BODY_Q" "$COMMENT_CONTENT" > "$PROMPT_FILE_PATH"
|
|
fi
|
|
|
|
rm -f /tmp/api_response.txt
|
|
fi
|
|
fi
|
|
echo "Prompt determined and written to $PROMPT_FILE_PATH"
|
|
echo "PROMPT_FILE_PATH=$PROMPT_FILE_PATH" >> $GITHUB_OUTPUT
|
|
|
|
- name: Probe Chat for Relevant Files
|
|
id: probe_files
|
|
env:
|
|
PROMPT_CONTENT_FILE: ${{ steps.determine_prompt.outputs.PROMPT_FILE_PATH }}
|
|
run: |
|
|
echo "Running probe-chat to find relevant files..."
|
|
if [[ ! -f "$PROMPT_CONTENT_FILE" ]]; then
|
|
echo "::error::Prompt file $PROMPT_CONTENT_FILE not found!"
|
|
exit 1
|
|
fi
|
|
PROMPT_CONTENT=$(cat "$PROMPT_CONTENT_FILE")
|
|
if [ -z "$PROMPT_CONTENT" ]; then
|
|
echo "::error::Prompt content is empty!"
|
|
exit 1
|
|
fi
|
|
|
|
PROMPT_ESCAPED=$(jq -Rs . <<< "$PROMPT_CONTENT")
|
|
|
|
MESSAGE_FOR_PROBE=$(jq -n --arg prompt_escaped "$PROMPT_ESCAPED" \
|
|
'{ "message": "I'\''m giving you a request that needs to be implemented. Your role is ONLY to give me the files that are relevant to the request and nothing else. The request is prepended with the word REQUEST.\\nREQUEST: \($prompt_escaped). Give me all the files relevant to this request. Your output MUST be a single json array that can be parsed with programatic json parsing, with the relevant files. Files can be rust or typescript or javascript files. DO NOT INCLUDE ANY OTHER TEXT IN YOUR OUTPUT. ONLY THE JSON ARRAY. Example of output: [\"file1.py\", \"file2.py\"]" }' | jq -r .message)
|
|
|
|
set -o pipefail
|
|
PROBE_OUTPUT=$(npx --yes @buger/probe-chat@latest --max-iterations 50 --model-name gemini-2.5-pro-preview-05-06 --message "$MESSAGE_FOR_PROBE" 2>&1) || {
|
|
echo "::error::probe-chat command failed. Output:"
|
|
echo "$PROBE_OUTPUT"
|
|
exit 1
|
|
}
|
|
set +o pipefail
|
|
echo "Probe-chat raw output:"
|
|
echo "$PROBE_OUTPUT"
|
|
|
|
JSON_FILES=$(echo "$PROBE_OUTPUT" | sed -n '/^\s*\[/,$p' | sed '/^\s*\]/q')
|
|
echo "Extracted JSON block:"
|
|
echo "$JSON_FILES"
|
|
|
|
FILES_LIST=$(echo "$JSON_FILES" | jq -e -r '[.[] | select(type == "string" and . != "" and . != null and (endswith("/") | not))] | map(@sh) | join(" ")' || echo "")
|
|
|
|
if [[ -z "$FILES_LIST" ]]; then
|
|
echo "::warning::probe-chat did not identify any relevant files."
|
|
exit 1
|
|
fi
|
|
|
|
echo "Formatted files list for aider: $FILES_LIST"
|
|
echo "FILES_TO_EDIT=$FILES_LIST" >> $GITHUB_ENV
|
|
|
|
- name: Run Aider with external prompt
|
|
run: |
|
|
echo "Files identified by probe-chat: ${{ env.FILES_TO_EDIT }}"
|
|
aider \
|
|
--read .cursor/rules/rust-best-practices.mdc \
|
|
--read .cursor/rules/svelte5-best-practices.mdc \
|
|
${{ env.FILES_TO_EDIT }} \
|
|
--model gemini/gemini-2.5-pro-preview-05-06 \
|
|
--message-file .github/aider/issue-prompt.txt \
|
|
--yes \
|
|
--no-check-update \
|
|
--auto-commits \
|
|
--no-analytics \
|
|
--no-gitignore \
|
|
| tee .github/aider/aider-output.txt || true
|
|
echo "Aider command completed. Output saved to .github/aider/aider-output.txt"
|
|
|
|
- name: Clean up prompt file
|
|
if: always()
|
|
run: rm -f .github/aider/issue-prompt.txt
|
|
|
|
- name: Commit and Push Changes
|
|
id: commit_and_push
|
|
if: ${{ success() }}
|
|
run: |
|
|
if [[ -z "${{ github.event.issue.pull_request }}" ]]; then
|
|
BRANCH_NAME="aider-fix-issue-${{ github.event.issue.number }}"
|
|
|
|
# Check if branch exists remotely
|
|
if git ls-remote --heads origin $BRANCH_NAME | grep -q $BRANCH_NAME; then
|
|
echo "Branch $BRANCH_NAME already exists remotely, fetching it"
|
|
git fetch origin $BRANCH_NAME
|
|
git checkout $BRANCH_NAME
|
|
git pull origin $BRANCH_NAME
|
|
else
|
|
echo "Creating new branch $BRANCH_NAME"
|
|
git checkout -b $BRANCH_NAME
|
|
fi
|
|
|
|
echo "Created/checked out branch $BRANCH_NAME for issue #${{ github.event.issue.number }}"
|
|
git push origin $BRANCH_NAME
|
|
echo "Pushed to branch $BRANCH_NAME"
|
|
echo "PR_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
|
echo "CHANGES_APPLIED_MESSAGE=Aider changes pushed to branch $BRANCH_NAME." >> $GITHUB_OUTPUT
|
|
else
|
|
CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
|
|
echo "Attempting to push changes to PR branch $CURRENT_BRANCH_NAME for PR #${{ github.event.issue.number }}"
|
|
if git push origin $CURRENT_BRANCH_NAME; then
|
|
echo "Push to $CURRENT_BRANCH_NAME successful (or no new changes to push)."
|
|
echo "CHANGES_APPLIED_MESSAGE=Aider changes (if any) pushed to PR branch $CURRENT_BRANCH_NAME." >> $GITHUB_OUTPUT
|
|
echo "PR_BRANCH_NAME=$CURRENT_BRANCH_NAME" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "::warning::Push to PR branch $CURRENT_BRANCH_NAME failed."
|
|
echo "CHANGES_APPLIED_MESSAGE=Aider ran, but failed to push changes to PR branch $CURRENT_BRANCH_NAME." >> $GITHUB_OUTPUT
|
|
fi
|
|
fi
|
|
|
|
- name: Create Pull Request
|
|
if: success() && github.event_name == 'issue_comment' && !github.event.issue.pull_request && steps.commit_and_push.outputs.PR_BRANCH_NAME != ''
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PR_BRANCH: ${{ steps.commit_and_push.outputs.PR_BRANCH_NAME }}
|
|
ISSUE_NUM: ${{ github.event.issue.number }}
|
|
run: |
|
|
# Create PR description in a temporary file to avoid command line length limits
|
|
cat > /tmp/pr-description.md << EOL
|
|
This PR was created automatically by Aider to fix issue #${ISSUE_NUM}.
|
|
|
|
## Aider Output
|
|
\`\`\`
|
|
$(cat .github/aider/aider-output.txt || echo "No output available")
|
|
\`\`\`
|
|
EOL
|
|
|
|
# Create PR using the file for the body content
|
|
gh pr create \
|
|
--title "[Aider PR] Add fixes for issue #${ISSUE_NUM}" \
|
|
--body-file /tmp/pr-description.md \
|
|
--head "$PR_BRANCH" \
|
|
--base main
|