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") || { 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