diff --git a/.github/workflows/aider-after-review.yaml b/.github/workflows/aider-after-review.yaml index 649ef52724..2ba3483a6c 100644 --- a/.github/workflows/aider-after-review.yaml +++ b/.github/workflows/aider-after-review.yaml @@ -5,12 +5,14 @@ on: types: [submitted] jobs: - auto-fix-review: + check-and-prepare: if: github.event.review.state == 'changes_requested' && contains(github.event.pull_request.title, '[Aider PR]') - runs-on: ubicloud-standard-8 + runs-on: ubicloud-standard-2 permissions: contents: write pull-requests: write + outputs: + prompt_content: ${{ steps.prepare_prompt.outputs.prompt_content }} env: GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} @@ -19,66 +21,24 @@ jobs: 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 + - name: Acknowledge Request env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} run: | - echo "PR review trigger: Checking out PR branch..." - PR_NUMBER=${{ github.event.pull_request.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)" + echo "Commenting on PR #${{ github.event.pull_request.number }} to acknowledge the /aider command." + gh pr comment ${{ github.event.pull_request.number }} --body "🤖 Aider is starting to work on your request. Please be patient, this might take a few minutes." --repo $GITHUB_REPOSITORY - - 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: Generate Prompt from Review - id: generate_prompt + - name: Prepare prompt for Aider + id: prepare_prompt shell: bash run: | - mkdir -p .github/aider - PROMPT_FILE_PATH=".github/aider/review-prompt.txt" - # Get PR review body REVIEW_BODY="${{ github.event.review.body }}" REVIEW_BODY_Q=$(printf '%q' "$REVIEW_BODY") PR_NUMBER="${{ github.event.pull_request.number }}" - # Get PR description for context NOT USED FOR NOW - # PR_DETAILS=$(gh pr view $PR_NUMBER --json title,body --repo $GITHUB_REPOSITORY) - # PR_TITLE=$(echo "$PR_DETAILS" | jq -r .title) - # PR_BODY=$(echo "$PR_DETAILS" | jq -r .body) - # Get all PR review comments ALL_REVIEW_COMMENTS=$(gh api \ -H "Accept: application/vnd.github+json" \ @@ -87,132 +47,20 @@ jobs: | jq '[.[] | {diff_hunk: .diff_hunk, path: .path, body: .body}]') BASE_PROMPT="Fix the following issues in the PR based on the review feedback. The review body is prepended with REVIEW. The review comments are prepended with REVIEW_COMMENTS. The review body and comments are separated by a blank line." - COMPLETE_PROMPT=$(printf "%s\nREVIEW:\n%s\nREVIEW_COMMENTS:\n%s" \ - "$BASE_PROMPT" "$REVIEW_BODY_Q" "$ALL_REVIEW_COMMENTS") - echo "$COMPLETE_PROMPT" > "$PROMPT_FILE_PATH" - echo "PROMPT_FILE_PATH=$PROMPT_FILE_PATH" >> $GITHUB_OUTPUT + printf -v COMPLETE_PROMPT "%s\nREVIEW:\n%s\nREVIEW_COMMENTS:\n%s" \ + "$BASE_PROMPT" "$REVIEW_BODY_Q" "$ALL_REVIEW_COMMENTS" - - name: Probe Chat for Relevant Files - id: probe_files - env: - PROMPT_CONTENT_FILE: ${{ steps.generate_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 + echo "$COMPLETE_PROMPT" - PROMPT_ESCAPED=$(jq -Rs . <<< "$PROMPT_CONTENT") + # Use the proper multi-line output format + echo "prompt_content<> $GITHUB_OUTPUT + echo "$COMPLETE_PROMPT" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT - 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 review prompt - run: | - aider \ - --read CLAUDE.md \ - --read backend/CLAUDE.md \ - --read frontend/CLAUDE.md \ - ${{ env.FILES_TO_EDIT }} \ - --model gemini/gemini-2.5-pro-preview-05-06 \ - --message-file .github/aider/review-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" - # Check if there are any changes to commit - if [[ -z "$(git status --porcelain)" ]]; then - echo "No changes detected after running Aider." - echo "HAS_CHANGES=false" >> $GITHUB_OUTPUT - exit 0 - fi - - - name: Clean up prompt file - if: always() - run: rm -f .github/aider/review-prompt.txt - - - name: Commit and Push Changes - id: commit_and_push - if: ${{ success() }} - run: | - CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) - echo "Attempting to push changes to PR branch $CURRENT_BRANCH_NAME for PR #${{ github.event.pull_request.number }}" - - # Pull latest changes to avoid rejection due to non-fast-forward - git config pull.rebase true - git pull origin $CURRENT_BRANCH_NAME - - if git push origin $CURRENT_BRANCH_NAME; then - echo "Push to $CURRENT_BRANCH_NAME successful." - echo "CHANGES_APPLIED=true" >> $GITHUB_OUTPUT - else - echo "::warning::Push to PR branch $CURRENT_BRANCH_NAME failed." - echo "CHANGES_APPLIED=false" >> $GITHUB_OUTPUT - fi - - - name: Comment on PR - if: success() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUM: ${{ github.event.pull_request.number }} - run: | - # Create comment body in a temporary file to avoid command line length limits - if [[ "${{ steps.commit_and_push.outputs.CHANGES_APPLIED }}" == "true" ]]; then - cat > /tmp/pr-comment.md << EOL - 🤖 I've automatically addressed the feedback based on the review. - - ## Aider Output - \`\`\` - $(cat .github/aider/aider-output.txt || echo 'No output available') - \`\`\` - - Please review the changes and let me know if further adjustments are needed. - EOL - else - cat > /tmp/pr-comment.md << EOL - 🤖 I attempted to address the review feedback, but no modifications were made. - - ## Aider Output - \`\`\` - $(cat .github/aider/aider-output.txt || echo 'No output available') - \`\`\` - - Please review the output and provide additional guidance if needed. - EOL - fi - - # Use the file for comment body - gh pr comment $PR_NUM --body-file /tmp/pr-comment.md + run-aider: + needs: check-and-prepare + uses: ./.github/workflows/aider-common.yml + with: + needs_processing: false + base_prompt: ${{ needs.check-and-prepare.outputs.prompt_content }} + secrets: inherit diff --git a/.github/workflows/aider-common.yml b/.github/workflows/aider-common.yml new file mode 100644 index 0000000000..2e4e99c716 --- /dev/null +++ b/.github/workflows/aider-common.yml @@ -0,0 +1,480 @@ +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 "create a test file in backend/test.txt with hello world in it" \ + --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 } }\"}" diff --git a/.github/workflows/aider.yaml b/.github/workflows/aider.yaml index e97b75bf22..fdfd7b4d39 100644 --- a/.github/workflows/aider.yaml +++ b/.github/workflows/aider.yaml @@ -5,8 +5,8 @@ on: types: [created] jobs: - auto-fix: - runs-on: ubicloud-standard-8 + check-and-prepare: + runs-on: ubicloud-standard-2 if: | github.event_name == 'issue_comment' && contains(github.event.comment.body, '/aider') && @@ -21,323 +21,95 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} WINDMILL_TOKEN: ${{ secrets.WINDMILL_TOKEN }} + outputs: + issue_title: ${{ steps.determine_inputs.outputs.ISSUE_TITLE }} + issue_body: ${{ steps.determine_inputs.outputs.ISSUE_BODY }} + comment_content: ${{ steps.determine_inputs.outputs.COMMENT_CONTENT }} + pr_branch: ${{ steps.checkout_pr.outputs.PR_BRANCH }} 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 + - name: Acknowledge Request env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} 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)" + echo "Commenting on issue/PR #${{ github.event.issue.number }} to acknowledge the /aider command." + gh issue comment ${{ github.event.issue.number }} --body "🤖 Aider is starting to work on your request. I'll update you here once I have a PR ready. Please be patient, this might take a few minutes." --repo $GITHUB_REPOSITORY - - 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 + - name: Determine inputs for Aider + id: determine_inputs shell: bash run: | - PROMPT_FILE_PATH=".github/aider/issue-prompt.txt" - mkdir -p .github/aider + echo "Determining inputs for Aider..." + ISSUE_TITLE_VAL="" + ISSUE_BODY_VAL="" - # 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 + PR_BODY_JSON=$(GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh pr view "$PR_NUMBER" --json body --repo "$GITHUB_REPOSITORY") + if [[ $? -ne 0 ]]; then + echo "Error fetching PR body for PR #$PR_NUMBER" + PR_BODY_VAL="" 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:]]*$//') + PR_BODY_VAL=$(echo "$PR_BODY_JSON" | jq -r .body) + fi + + if [[ ! -z "$PR_BODY_VAL" ]]; then + REFERENCED_ISSUE=$(echo "$PR_BODY_VAL" | grep -oE "#[0-9]+" | grep -oE "[0-9]+" | head -1) - 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" + if [[ ! -z "$REFERENCED_ISSUE" ]]; then + echo "Found referenced issue #$REFERENCED_ISSUE in PR description" + + ISSUE_DETAILS_JSON=$(GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh issue view "$REFERENCED_ISSUE" --json title,body --repo "$GITHUB_REPOSITORY") + if [[ $? -ne 0 ]]; then + echo "Error fetching issue details for #$REFERENCED_ISSUE" + else + ISSUE_TITLE_VAL=$(echo "$ISSUE_DETAILS_JSON" | jq -r .title) + ISSUE_BODY_VAL=$(echo "$ISSUE_DETAILS_JSON" | jq -r .body) + fi fi + else + echo "PR body is empty or could not be fetched." 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 + ISSUE_DETAILS_JSON=$(GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh issue view "$ISSUE_NUMBER" --json title,body --repo "$GITHUB_REPOSITORY") + if [[ $? -ne 0 ]]; then + echo "Error fetching issue details for #$ISSUE_NUMBER" 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 + ISSUE_TITLE_VAL=$(echo "$ISSUE_DETAILS_JSON" | jq -r .title) + ISSUE_BODY_VAL=$(echo "$ISSUE_DETAILS_JSON" | jq -r .body) 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 + echo "Setting GITHUB_OUTPUT for ISSUE_TITLE..." + echo "ISSUE_TITLE<> "$GITHUB_OUTPUT" + echo "$ISSUE_TITLE_VAL" >> "$GITHUB_OUTPUT" + echo "EOF_AIDER_TITLE" >> "$GITHUB_OUTPUT" + + echo "Setting GITHUB_OUTPUT for ISSUE_BODY..." + echo "ISSUE_BODY<> "$GITHUB_OUTPUT" + echo "$ISSUE_BODY_VAL" >> "$GITHUB_OUTPUT" + echo "EOF_AIDER_BODY" >> "$GITHUB_OUTPUT" + + # Process COMMENT_CONTENT + printf -v COMMENT_CONTENT_VAL "%s" "$(echo "${{ github.event.comment.body }}" | sed 's|^/aider||' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + echo "COMMENT_CONTENT<> "$GITHUB_OUTPUT" + echo "$COMMENT_CONTENT_VAL" >> "$GITHUB_OUTPUT" + echo "EOF_AIDER_COMMENT" >> "$GITHUB_OUTPUT" + echo "Finished determining inputs." 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 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Make sure gh cli has a token - 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 CLAUDE.md \ - --read backend/CLAUDE.md \ - --read frontend/CLAUDE.md \ - ${{ 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 + run-aider: + needs: check-and-prepare + uses: ./.github/workflows/aider-common.yml + with: + issue_title: ${{ needs.check-and-prepare.outputs.issue_title }} + issue_body: ${{ needs.check-and-prepare.outputs.issue_body }} + instruction: ${{ needs.check-and-prepare.outputs.comment_content }} + issue_id: ${{ github.event.issue.number }} + secrets: inherit diff --git a/.github/workflows/linear-issue.yaml b/.github/workflows/linear-issue.yaml index 7dd217e141..1dce5b6c87 100644 --- a/.github/workflows/linear-issue.yaml +++ b/.github/workflows/linear-issue.yaml @@ -5,219 +5,65 @@ on: types: [external_issue_fix] jobs: - auto-fix: - runs-on: ubicloud-standard-8 + check-and-prepare: + runs-on: ubicloud-standard-2 permissions: contents: write pull-requests: write - issues: write + outputs: + issue_title: ${{ steps.determine_inputs.outputs.ISSUE_TITLE }} + issue_body: ${{ steps.determine_inputs.outputs.ISSUE_BODY }} + instruction: ${{ steps.determine_inputs.outputs.INSTRUCTION }} 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: Configure Git User + - name: Acknowledge Request + env: + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" + echo "Commenting on Linear issue #${{ github.event.client_payload.issue_id }} to acknowledge the request." + 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: \\\"🤖 Aider is starting to work on your request. I'll update you here once I have a PR ready. Please be patient, this might take a few minutes.\\\" }) { success } }\"}" - - 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: Create Prompt for Aider - id: create_prompt + - name: Determine inputs for Aider + id: determine_inputs shell: bash run: | - PROMPT_FILE_PATH=".github/aider/issue-prompt.txt" - mkdir -p .github/aider + echo "Determining inputs for Aider..." + ISSUE_TITLE_VAL="${{ github.event.client_payload.issue_title }}" + INSTRUCTION_VAL="${{ github.event.client_payload.instruction }}" + ISSUE_BODY_VAL=$(printf '%q' "${{ github.event.client_payload.issue_body }}") + echo "Setting GITHUB_OUTPUT for ISSUE_TITLE..." + echo "ISSUE_TITLE<> "$GITHUB_OUTPUT" + echo "$ISSUE_TITLE_VAL" >> "$GITHUB_OUTPUT" + echo "EOF_AIDER_TITLE" >> "$GITHUB_OUTPUT" - ISSUE_TITLE="${{ github.event.client_payload.issue_title }}" - INSTRUCTION="${{ github.event.client_payload.instruction }}" - ISSUE_BODY=$(printf '%q' "${{ github.event.client_payload.issue_body }}") + echo "Setting GITHUB_OUTPUT for ISSUE_BODY..." + echo "ISSUE_BODY<> "$GITHUB_OUTPUT" + echo "$ISSUE_BODY_VAL" >> "$GITHUB_OUTPUT" + echo "EOF_AIDER_BODY" >> "$GITHUB_OUTPUT" - echo "Processing issue with title: $ISSUE_TITLE" + echo "Setting GITHUB_OUTPUT for INSTRUCTION..." + echo "INSTRUCTION<> "$GITHUB_OUTPUT" + echo "$INSTRUCTION_VAL" >> "$GITHUB_OUTPUT" + echo "EOF_AIDER_INSTRUCTION" >> "$GITHUB_OUTPUT" + echo "Finished determining inputs." - JSON_PAYLOAD=$(jq -n \ - --arg title "$ISSUE_TITLE" \ - --arg body "$ISSUE_BODY" \ - '{"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. 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" "$INSTRUCTION" > "$PROMPT_FILE_PATH" - else - echo "::warning::API call failed (HTTP $HTTP_CODE). Using raw issue content." - printf "%s\nISSUE:\n%s\nINSTRUCTION:\n%s" \ - "$BASE_PROMPT" "$ISSUE_BODY" "$INSTRUCTION" > "$PROMPT_FILE_PATH" - fi - rm -f /tmp/api_response.txt - - echo "Prompt created and written to $PROMPT_FILE_PATH" - echo "PROMPT_FILE_PATH=$PROMPT_FILE_PATH" >> $GITHUB_OUTPUT - - # Store the issue title for PR creation - ISSUE_TITLE_SAFE=$(echo "$ISSUE_TITLE" | tr -d '\n' | sed 's/"/\\"/g') - echo "ISSUE_TITLE=$ISSUE_TITLE_SAFE" >> $GITHUB_OUTPUT - - # Generate unique branch name using timestamp and issue info - ISSUE_ID="${{ github.event.client_payload.issue_id }}" - BRANCH_NAME="aider-fix-linear-issue-$ISSUE_ID" - echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT - - - name: Probe Chat for Relevant Files - id: probe_files - env: - PROMPT_CONTENT_FILE: ${{ steps.create_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 CLAUDE.md \ - --read backend/CLAUDE.md \ - --read frontend/CLAUDE.md \ - ${{ 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: | - BRANCH_NAME="${{ steps.create_prompt.outputs.BRANCH_NAME }}" - - # 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 - - # Check if there are any changes to commit - if git diff --quiet && git diff --staged --quiet; then - echo "No changes to commit" - else - git commit -am "Auto-fix using Aider for external issue [skip ci]" || echo "No changes to commit" - fi - - git push origin $BRANCH_NAME - echo "Pushed to branch $BRANCH_NAME" - echo "PR_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT - - - name: Create Pull Request - if: success() && 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_TITLE: ${{ steps.create_prompt.outputs.ISSUE_TITLE }} - ISSUE_ID: ${{ github.event.client_payload.issue_id }} - 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 an external issue: ${ISSUE_TITLE} - - ## 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] Fix: ${ISSUE_TITLE}" \ - --body-file /tmp/pr-description.md \ - --head "$PR_BRANCH" \ - --base main || echo "PR already exists or couldn't be created" + run-aider: + needs: check-and-prepare + uses: ./.github/workflows/aider-common.yml + with: + issue_title: ${{ needs.check-and-prepare.outputs.issue_title }} + issue_body: ${{ needs.check-and-prepare.outputs.issue_body }} + instruction: ${{ needs.check-and-prepare.outputs.instruction }} + issue_id: ${{ github.event.client_payload.issue_id }} + secrets: inherit