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. 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"]' rules_files: description: "Rules files for Aider" required: false type: string 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 }} DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_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: 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.11" - 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: Install Aider and Dependencies run: | echo "Installing Aider..." python -m pip install uv python -m venv ~/uv-env source ~/uv-env/bin/activate uv pip install configargparse==1.7 uv pip install aider-chat==0.83.1 uv pip install -U google-generativeai sudo apt-get update && sudo apt-get install -y jq echo "$HOME/.local/bin" >> $GITHUB_PATH echo "VIRTUAL_ENV_PATH=$HOME/uv-env" >> $GITHUB_ENV - 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=$(printf "%s\nINSTRUCTION:\n%s" "$BASE_PROMPT_ENV" "$INSTRUCTION_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 }} PROBE_PROMPT: ${{ inputs.probe_prompt }} run: | echo "Running probe-chat to find relevant files..." MESSAGE_FOR_PROBE=$(printf "%s\nREQUEST:\n%s" "$PROBE_PROMPT" "$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: Prepare branch for Aider id: prepare_branch 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 "BRANCH_NAME=$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 "Using PR branch $PR_HEAD_REF for PR #$PR_NUMBER" echo "BRANCH_NAME=$PR_HEAD_REF" >> $GITHUB_OUTPUT fi - 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 }} RULES_FILES: ${{ inputs.rules_files }} run: | source $VIRTUAL_ENV_PATH/bin/activate echo "$FINAL_PROMPT" > .aider_final_prompt.txt echo "FILES_TO_EDIT: $FILES_TO_EDIT" RULES="" if [ -n "$RULES_FILES" ]; then for rule in $RULES_FILES; do RULES="$RULES --read $rule" done fi aider \ $RULES \ $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 }} BRANCH_NAME: ${{ steps.prepare_branch.outputs.BRANCH_NAME }} run: | if [[ "$ISSUE_ID" != "" ]]; then # Check if there are any uncommitted changes if [[ -n $(git status --porcelain) ]]; then echo "Found uncommitted changes, committing them" git add . git commit -m "Aider changes" fi # Push changes to the branch if git push origin $BRANCH_NAME; then 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 echo "CHANGES_APPLIED=true" >> $GITHUB_OUTPUT else echo "::warning::Push to PR branch $BRANCH_NAME failed." echo "CHANGES_APPLIED_MESSAGE=Aider ran, but failed to push changes to PR branch $BRANCH_NAME." >> $GITHUB_OUTPUT echo "CHANGES_APPLIED=false" >> $GITHUB_OUTPUT fi else # We're in a pull_request_review event PR_HEAD_REF="${{ github.event.pull_request.head.ref }}" echo "Attempting to push changes to PR branch $PR_HEAD_REF" 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 }} GITHUB_EVENT_NAME: ${{ github.event_name }} run: | # Create PR description in a temporary file to avoid command line length limits and ensure it stays under 40k chars HEADER="This PR was created automatically by Aider to fix issue #${ISSUE_NUM}." # if event is repository_dispatch, add the issue title to the header if [ "$GITHUB_EVENT_NAME" == "repository_dispatch" ]; then if [[ "${{ github.event.client_payload.source }}" == "linear" ]]; then HEADER="This PR was created automatically by Aider to fix issue #linear:${ISSUE_NUM}." elif [[ "${{ github.event.client_payload.source }}" == "discord" ]]; then HEADER="This PR was created automatically by Aider to fix issue #discord:${ISSUE_NUM}." fi fi cat > /tmp/pr-description.md << EOL | head -c 40000 $HEADER ## 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 PR_TITLE="[Aider PR] Fix: ${ISSUE_TITLE}" if [ -z "$ISSUE_TITLE" ]; then PR_TITLE="[Aider PR] AI changes after request" fi gh pr create \ --title "$PR_TITLE" \ --body-file /tmp/pr-description.md \ --head "$PR_BRANCH" \ --base main \ --draft 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 }} PR_URL: ${{ steps.create_pr.outputs.PR_URL }} 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. $PR_URL" 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 }} PR_URL: ${{ steps.create_pr.outputs.PR_URL }} DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_AI_BOT_TOKEN }} SOURCE: ${{ github.event.client_payload.source }} run: | echo "Notifying user about Aider completion status for $SOURCE request #${{ github.event.client_payload.issue_id }}" if [[ "$JOB_STATUS" == "success" ]]; then if [[ "$PR_CREATED" == "true" ]]; then COMMENT_BODY="🤖 Aider has finished working on your request. A PR has been created. $PR_URL" 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 if [[ "$SOURCE" == "discord" ]]; then curl -X POST \ -H "Authorization: Bot $DISCORD_BOT_TOKEN" \ -H "Content-Type: application/json" \ "https://discord.com/api/v10/channels/${{ github.event.client_payload.channel_id }}/messages" \ -d "{\"content\":\"${COMMENT_BODY}\"}" else 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 } }\"}" fi