name: Aider Common Steps on: workflow_call: inputs: issue_title: description: "Title of the issue or PR" required: false type: string issue_body: description: "Body of the issue or PR" required: false type: string instruction: description: "Instruction for Aider" required: false type: string issue_id: description: "ID of the issue or PR" required: false type: string needs_processing: description: "Whether the issue needs to be processed by the external API" required: false type: boolean default: true base_prompt: description: "Base prompt for Aider" required: false type: string default: "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." probe_prompt: description: "Prompt for probe-chat" required: false type: string default: '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. REQUEST: $FINAL_PROMPT. 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"]' outputs: files_to_edit: description: "Files identified by probe-chat for editing" value: ${{ jobs.common-steps.outputs.files_to_edit }} final_prompt: description: "Final prompt for Aider" value: ${{ jobs.common-steps.outputs.final_prompt }} pr_branch_name: description: "Name of the branch used for PR" value: ${{ jobs.common-steps.outputs.pr_branch_name }} changes_applied_message: description: "Message indicating changes were applied" value: ${{ jobs.common-steps.outputs.changes_applied_message }} changes_applied: description: "Boolean indicating if changes were successfully applied" value: ${{ jobs.common-steps.outputs.changes_applied }} jobs: common-steps: runs-on: ubicloud-standard-8 outputs: files_to_edit: ${{ steps.probe_files.outputs.files_to_edit }} final_prompt: ${{ steps.create_prompt.outputs.final_prompt }} pr_branch_name: ${{ steps.commit_and_push.outputs.PR_BRANCH_NAME }} changes_applied_message: ${{ steps.commit_and_push.outputs.CHANGES_APPLIED_MESSAGE }} changes_applied: ${{ steps.commit_and_push.outputs.CHANGES_APPLIED }} 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 }} LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} 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: Checkout PR Branch id: checkout_pr if: (github.event_name == 'issue_comment' && github.event.issue.pull_request) || (github.event_name == 'pull_request_review') env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Issue comment trigger: Checking out PR branch..." PR_NUMBER="" if [ -n "${{ github.event.issue.number }}" ]; then PR_NUMBER="${{ github.event.issue.number }}" elif [ -n "${{ github.event.pull_request.number }}" ]; then PR_NUMBER="${{ github.event.pull_request.number }}" else echo "::error::Could not determine PR number." exit 1 fi 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)" echo "PR_BRANCH=$PR_HEAD_REF" >> $GITHUB_OUTPUT - 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: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Cache Python dependencies uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/setup.py') }} restore-keys: | ${{ runner.os }}-pip- - name: Cache Aider installation id: cache-aider uses: actions/cache@v3 with: path: ~/.local/bin/aider key: ${{ runner.os }}-aider-install-${{ hashFiles('**/requirements.txt', '**/setup.py') }} restore-keys: | ${{ runner.os }}-aider-install- - name: Install Aider and Dependencies run: | if [ -f ~/.local/bin/aider ] && [ -x ~/.local/bin/aider ]; then echo "Using cached Aider installation" export PATH="$HOME/.local/bin:$PATH" else echo "Installing Aider..." python -m pip install aider-install; aider-install fi pip install -U google-generativeai sudo apt-get update && sudo apt-get install -y jq echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Create Prompt for Aider id: create_prompt shell: bash env: BASE_PROMPT_ENV: ${{ inputs.base_prompt }} ISSUE_TITLE_ENV: ${{ inputs.issue_title }} ISSUE_BODY_ENV: ${{ inputs.issue_body }} INSTRUCTION_ENV: ${{ inputs.instruction }} NEEDS_PROCESSING_ENV: ${{ inputs.needs_processing }} WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }} run: | set -e FINAL_PROMPT_CONTENT="" if [[ "$ISSUE_TITLE_ENV" != "" && "$ISSUE_BODY_ENV" != "" ]]; then echo "Processing issue with title: $ISSUE_TITLE_ENV" if [[ "$NEEDS_PROCESSING_ENV" == "true" ]]; then echo "Needs processing is true. Calling Windmill API..." JSON_PAYLOAD=$(jq -n \ --arg title "$ISSUE_TITLE_ENV" \ --arg body "$ISSUE_BODY_ENV" \ '{"body":{"issue_title":$title,"issue_body":$body}}') echo "Windmill JSON Payload: $JSON_PAYLOAD" API_RESULT_FILE=$(mktemp) HTTP_CODE=$(curl -s -o "$API_RESULT_FILE" -w "%{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) BODY_CONTENT=$(cat "$API_RESULT_FILE") rm -f "$API_RESULT_FILE" # Clean up temp file echo "Windmill API HTTP Code: $HTTP_CODE" if [[ "$HTTP_CODE" -eq 200 ]]; then PROCESSED_ISSUE_PROMPT=$(echo "$BODY_CONTENT" | jq -r '.effective_body // empty') if [[ -z "$PROCESSED_ISSUE_PROMPT" || "$PROCESSED_ISSUE_PROMPT" == "null" ]]; then echo "::warning::Windmill API returned 200 but effective_body was empty or null." EFFECTIVE_ISSUE_CONTENT_FOR_PROMPT="$ISSUE_BODY_ENV" else echo "Successfully processed issue via Windmill API." EFFECTIVE_ISSUE_CONTENT_FOR_PROMPT="$PROCESSED_ISSUE_PROMPT" fi FINAL_PROMPT_CONTENT=$(printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \ "$BASE_PROMPT_ENV" "$EFFECTIVE_ISSUE_CONTENT_FOR_PROMPT" "$INSTRUCTION_ENV") else echo "::error::Windmill API call failed (HTTP $HTTP_CODE). Using raw issue content for prompt." FINAL_PROMPT_CONTENT=$(printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \ "$BASE_PROMPT_ENV" "$ISSUE_BODY_ENV" "$INSTRUCTION_ENV") fi else echo "Needs processing is false. Using raw issue content for prompt." FINAL_PROMPT_CONTENT=$(printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \ "$BASE_PROMPT_ENV" "$ISSUE_BODY_ENV" "$INSTRUCTION_ENV") fi else echo "No issue title or body given. Using base prompt." FINAL_PROMPT_CONTENT="$BASE_PROMPT_ENV" fi echo "Final prompt: $FINAL_PROMPT_CONTENT" echo "final_prompt<> "$GITHUB_OUTPUT" echo "$FINAL_PROMPT_CONTENT" >> "$GITHUB_OUTPUT" echo "EOF_AIDER_PROMPT" >> "$GITHUB_OUTPUT" - name: Probe Chat for Relevant Files id: probe_files shell: bash env: FINAL_PROMPT: ${{ steps.create_prompt.outputs.final_prompt }} run: | echo "Running probe-chat to find relevant files..." # escape the final prompt printf -v MESSAGE_FOR_PROBE '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: %s. 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"]' "$FINAL_PROMPT" 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))] | join(" ")' || echo "") if [[ -z "$FILES_LIST" ]]; then echo "::warning::probe-chat did not identify any relevant files." fi echo "Formatted files list for aider: $FILES_LIST" echo "files_to_edit=$FILES_LIST" >> $GITHUB_OUTPUT - name: Cache Aider tags uses: actions/cache@v3 with: path: .aider.tags.cache.v4 key: ${{ runner.os }}-aider-${{ github.sha }} restore-keys: | ${{ runner.os }}-aider- - name: Run Aider id: run_aider shell: bash env: FILES_TO_EDIT: ${{ steps.probe_files.outputs.files_to_edit }} FINAL_PROMPT: ${{ steps.create_prompt.outputs.final_prompt }} run: | echo "$FINAL_PROMPT" > .aider_final_prompt.txt echo "FILES_TO_EDIT: $FILES_TO_EDIT" aider \ --read .cursor/rules/rust-best-practices.mdc \ --read .cursor/rules/svelte5-best-practices.mdc \ --read .cursor/rules/windmill-overview.mdc \ $FILES_TO_EDIT \ --model gemini/gemini-2.5-pro-preview-05-06 \ --message-file .aider_final_prompt.txt \ --yes \ --no-check-update \ --auto-commits \ --no-analytics \ --no-gitignore \ | tee .aider_output.txt || true echo "Aider command completed. Output saved to .aider_output.txt" - name: Cache Node.js dependencies uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }} restore-keys: | ${{ runner.os }}-node- - name: Commit and Push Changes id: commit_and_push env: ISSUE_ID: ${{ inputs.issue_id }} run: | if [[ "$ISSUE_ID" != "" ]]; then BRANCH_NAME="aider-fix-issue-${ISSUE_ID}" # 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 #${ISSUE_ID}" 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 # We're in a pull_request_review event PR_NUMBER="${{ github.event.pull_request.number }}" PR_HEAD_REF="${{ github.event.pull_request.head.ref }}" echo "Handling pull_request_review for PR #$PR_NUMBER on branch $PR_HEAD_REF" # Ensure we're on the correct branch git config pull.rebase true git fetch origin $PR_HEAD_REF git checkout $PR_HEAD_REF git pull origin $PR_HEAD_REF echo "Attempting to push changes to PR branch $PR_HEAD_REF for PR #$PR_NUMBER" if git push origin $PR_HEAD_REF; then echo "Push to $PR_HEAD_REF successful (or no new changes to push)." echo "CHANGES_APPLIED_MESSAGE=Aider changes (if any) pushed to PR branch $PR_HEAD_REF." >> $GITHUB_OUTPUT echo "PR_BRANCH_NAME=$PR_HEAD_REF" >> $GITHUB_OUTPUT echo "CHANGES_APPLIED=true" >> $GITHUB_OUTPUT else echo "::warning::Push to PR branch $PR_HEAD_REF failed." echo "CHANGES_APPLIED_MESSAGE=Aider ran, but failed to push changes to PR branch $PR_HEAD_REF." >> $GITHUB_OUTPUT echo "CHANGES_APPLIED=false" >> $GITHUB_OUTPUT fi fi - name: Create Pull Request if: always() && (github.event_name == 'issue_comment' || github.event_name == 'repository_dispatch') && !github.event.issue.pull_request && steps.commit_and_push.outputs.PR_BRANCH_NAME != '' id: create_pr env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_BRANCH: ${{ steps.commit_and_push.outputs.PR_BRANCH_NAME }} ISSUE_NUM: ${{ inputs.issue_id }} ISSUE_TITLE: ${{ inputs.issue_title }} run: | # Debug: Check latest commit and branch status echo "Checking latest commit on branch $PR_BRANCH" git log -1 --pretty=format:"%h - %an, %ar : %s" echo "Changes not yet committed:" git status --porcelain # Check if there are any changes to commit if [[ -n $(git status --porcelain) ]]; then echo "Found uncommitted changes, committing them" git add . git commit -m "Aider changes for issue #${ISSUE_NUM}" git push origin $PR_BRANCH fi # Create PR description in a temporary file to avoid command line length limits and ensure it stays under 40k chars cat > /tmp/pr-description.md << EOL | head -c 40000 This PR was created automatically by Aider to fix issue #${ISSUE_NUM}. ## Aider Output \`\`\` $(cat .aider_output.txt || echo "No output available") \`\`\` EOL # Create PR using the file for the body content, handle errors gracefully set +e # Don't exit on error gh pr create \ --title "[Aider PR] Fix: ${ISSUE_TITLE}" \ --body-file /tmp/pr-description.md \ --head "$PR_BRANCH" \ --base main PR_CREATE_EXIT_CODE=$? set -e # Re-enable exit on error if [ $PR_CREATE_EXIT_CODE -eq 0 ]; then echo "PR created successfully" PR_URL=$(gh pr view $PR_BRANCH --json url --jq .url) echo "PR_URL=$PR_URL" >> $GITHUB_OUTPUT echo "PR_CREATED=true" >> $GITHUB_OUTPUT else echo "Warning: Failed to create PR. Exit code: $PR_CREATE_EXIT_CODE" echo "PR_CREATED=false" >> $GITHUB_OUTPUT # Continue workflow despite PR creation failure fi - name: Comment on PR with Aider Output if: always() && github.event_name == 'pull_request_review' && steps.commit_and_push.outputs.CHANGES_APPLIED != '' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUM: ${{ github.event.pull_request.number }} JOB_STATUS: ${{ job.status }} run: | # Create comment body in a temporary file to avoid command line length limits if [[ "${{ steps.commit_and_push.outputs.CHANGES_APPLIED }}" == "true" ]]; then if [[ "$JOB_STATUS" == "success" ]]; then STATUS_PREFIX="🤖 I've automatically addressed the feedback based on the review." else STATUS_PREFIX="⚠️ I attempted to address the feedback, but encountered some issues." fi else if [[ "$JOB_STATUS" == "success" ]]; then STATUS_PREFIX="🤖 I attempted to address the review feedback, but no modifications were made." else STATUS_PREFIX="⚠️ I encountered issues while attempting to address the feedback, and no modifications were made." fi fi cat > /tmp/pr-comment.md << EOL ${STATUS_PREFIX} ## Aider Output \`\`\` $(cat .aider_output.txt || echo 'No output available') \`\`\` Please review the output and provide additional guidance if needed. EOL # Use the file for comment body gh pr comment $PR_NUM --body-file /tmp/pr-comment.md - name: Comment on issue/PR to let the user know Aider has finished working on the request if: always() && github.event_name == 'issue_comment' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} JOB_STATUS: ${{ job.status }} PR_CREATED: ${{ steps.create_pr.outputs.PR_CREATED }} run: | echo "Commenting on issue/PR #${{ github.event.issue.number }} to let the user know Aider has finished working on the request." if [[ "$JOB_STATUS" == "success" ]]; then if [[ "$PR_CREATED" == "true" ]]; then COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created." else COMMENT_BODY="🤖 Aider has finished working on your request, but was unable to create a PR." fi else COMMENT_BODY="⚠️ Aider encountered issues while working on your request. Please check the workflow logs for details." fi gh issue comment ${{ github.event.issue.number }} --body "$COMMENT_BODY" --repo $GITHUB_REPOSITORY - name: Comment on linear issue to let the user know Aider has finished working on the request if: always() && github.event_name == 'repository_dispatch' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} JOB_STATUS: ${{ job.status }} LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} PR_CREATED: ${{ steps.create_pr.outputs.PR_CREATED }} run: | echo "Commenting on linear issue #${{ github.event.client_payload.issue_id }} to let the user know Aider has finished working on the request." if [[ "$JOB_STATUS" == "success" ]]; then if [[ "$PR_CREATED" == "true" ]]; then COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created." else COMMENT_BODY="🤖 Aider has finished working on your request, but was unable to create a PR." fi else COMMENT_BODY="⚠️ Aider encountered issues while working on your request. Please check the workflow logs for details." fi curl -X POST \ -H "Authorization: $LINEAR_API_KEY" \ -H "Content-Type: application/json" \ "https://api.linear.app/graphql" \ -d "{\"query\":\"mutation { commentCreate(input: { issueId: \\\"${{ github.event.client_payload.issue_id }}\\\", body: \\\"${COMMENT_BODY}\\\" }) { success } }\"}"