name: Auto-Fix Tagged Issue with OpenHands on: workflow_call: inputs: max_iterations: required: false type: number default: 50 macro: required: false type: string default: "@openhands-agent" target_branch: required: false type: string default: "main" description: "Target branch to pull and create PR against" pr_type: required: false type: string default: "draft" description: "The PR type that is going to be created (draft, ready)" LLM_MODEL: required: false type: string default: "anthropic/claude-sonnet-4-20250514" LLM_API_VERSION: required: false type: string default: "" base_container_image: required: false type: string default: "" description: "Custom sandbox env" runner: required: false type: string default: "ubuntu-latest" secrets: LLM_MODEL: required: false LLM_API_KEY: required: true LLM_BASE_URL: required: false PAT_TOKEN: required: false PAT_USERNAME: required: false issues: types: [labeled] pull_request: types: [labeled] issue_comment: types: [created] pull_request_review_comment: types: [created] pull_request_review: types: [submitted] permissions: contents: write pull-requests: write issues: write jobs: auto-fix: if: | github.event_name == 'workflow_call' || github.event.label.name == 'fix-me' || github.event.label.name == 'fix-me-experimental' || ( ((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && contains(github.event.comment.body, inputs.macro || '@openhands-agent') && (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER') ) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, inputs.macro || '@openhands-agent') && (github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER') ) ) runs-on: "${{ inputs.runner || 'ubuntu-latest' }}" steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Upgrade pip run: | python -m pip install --upgrade pip - name: Get latest versions and create requirements.txt run: | python -m pip index versions openhands-ai > openhands_versions.txt OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()') # Create a new requirements.txt locally within the workflow, ensuring no reference to the repo's file echo "openhands-ai==${OPENHANDS_VERSION}" > /tmp/requirements.txt cat /tmp/requirements.txt - name: Cache pip dependencies if: | !( github.event.label.name == 'fix-me-experimental' || ( (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && contains(github.event.comment.body, '@openhands-agent-exp') ) || ( github.event_name == 'pull_request_review' && contains(github.event.review.body, '@openhands-agent-exp') ) ) uses: actions/cache@v4 with: path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/* key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }} - name: Check required environment variables env: LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }} LLM_API_KEY: ${{ secrets.LLM_API_KEY }} LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }} PAT_TOKEN: ${{ secrets.PAT_TOKEN }} PAT_USERNAME: ${{ secrets.PAT_USERNAME }} GITHUB_TOKEN: ${{ github.token }} run: | required_vars=("LLM_API_KEY") for var in "${required_vars[@]}"; do if [ -z "${!var}" ]; then echo "Error: Required environment variable $var is not set." exit 1 fi done # Check optional variables and warn about fallbacks if [ -z "$LLM_BASE_URL" ]; then echo "Warning: LLM_BASE_URL is not set, will use default API endpoint" fi if [ -z "$PAT_TOKEN" ]; then echo "Warning: PAT_TOKEN is not set, falling back to GITHUB_TOKEN" fi if [ -z "$PAT_USERNAME" ]; then echo "Warning: PAT_USERNAME is not set, will use openhands-agent" fi - name: Set environment variables env: REVIEW_BODY: ${{ github.event.review.body || '' }} run: | # Handle pull request events first if [ -n "${{ github.event.pull_request.number }}" ]; then echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV echo "ISSUE_TYPE=pr" >> $GITHUB_ENV # Handle pull request review events elif [ -n "$REVIEW_BODY" ]; then echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV echo "ISSUE_TYPE=pr" >> $GITHUB_ENV # Handle issue comment events that reference a PR elif [ -n "${{ github.event.issue.pull_request }}" ]; then echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV echo "ISSUE_TYPE=pr" >> $GITHUB_ENV # Handle regular issue events else echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV echo "ISSUE_TYPE=issue" >> $GITHUB_ENV fi if [ -n "$REVIEW_BODY" ]; then echo "COMMENT_ID=${{ github.event.review.id || 'None' }}" >> $GITHUB_ENV else echo "COMMENT_ID=${{ github.event.comment.id || 'None' }}" >> $GITHUB_ENV fi echo "MAX_ITERATIONS=${{ inputs.max_iterations || 50 }}" >> $GITHUB_ENV echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.PAT_TOKEN || github.token }}" >> $GITHUB_ENV echo "SANDBOX_BASE_CONTAINER_IMAGE=${{ inputs.base_container_image }}" >> $GITHUB_ENV # Set branch variables echo "TARGET_BRANCH=${{ inputs.target_branch || 'main' }}" >> $GITHUB_ENV - name: Comment on issue with start message uses: actions/github-script@v7 with: github-token: ${{ secrets.PAT_TOKEN || github.token }} script: | const issueType = process.env.ISSUE_TYPE; github.rest.issues.createComment({ issue_number: ${{ env.ISSUE_NUMBER }}, owner: context.repo.owner, repo: context.repo.repo, body: `[OpenHands](https://github.com/OpenHands/OpenHands) started fixing the ${issueType}! You can monitor the progress [here](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).` }); - name: Install OpenHands id: install_openhands uses: actions/github-script@v7 env: COMMENT_BODY: ${{ github.event.comment.body || '' }} REVIEW_BODY: ${{ github.event.review.body || '' }} LABEL_NAME: ${{ github.event.label.name || '' }} EVENT_NAME: ${{ github.event_name }} with: script: | const commentBody = process.env.COMMENT_BODY.trim(); const reviewBody = process.env.REVIEW_BODY.trim(); const labelName = process.env.LABEL_NAME.trim(); const eventName = process.env.EVENT_NAME.trim(); // Check conditions const isExperimentalLabel = labelName === "fix-me-experimental"; const isIssueCommentExperimental = (eventName === "issue_comment" || eventName === "pull_request_review_comment") && commentBody.includes("@openhands-agent-exp"); const isReviewCommentExperimental = eventName === "pull_request_review" && reviewBody.includes("@openhands-agent-exp"); // Set output variable core.setOutput('isExperimental', isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental); // Perform package installation if (isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental) { console.log("Installing experimental OpenHands..."); await exec.exec("pip install git+https://github.com/openhands/openhands.git"); } else { console.log("Installing from requirements.txt..."); await exec.exec("pip install -r /tmp/requirements.txt"); } - name: Attempt to resolve issue env: GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }} GITHUB_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }} GIT_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }} LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }} LLM_API_KEY: ${{ secrets.LLM_API_KEY }} LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }} PYTHONPATH: "" run: | cd /tmp && python -m openhands.resolver.resolve_issue \ --selected-repo ${{ github.repository }} \ --issue-number ${{ env.ISSUE_NUMBER }} \ --issue-type ${{ env.ISSUE_TYPE }} \ --max-iterations ${{ env.MAX_ITERATIONS }} \ --comment-id ${{ env.COMMENT_ID }} \ --is-experimental ${{ steps.install_openhands.outputs.isExperimental }} - name: Check resolution result id: check_result run: | if cd /tmp && grep -q '"success":true' output/output.jsonl; then echo "RESOLUTION_SUCCESS=true" >> $GITHUB_OUTPUT else echo "RESOLUTION_SUCCESS=false" >> $GITHUB_OUTPUT fi - name: Upload output.jsonl as artifact uses: actions/upload-artifact@v4 if: always() # Upload even if the previous steps fail with: name: resolver-output path: /tmp/output/output.jsonl retention-days: 30 # Keep the artifact for 30 days - name: Create draft PR or push branch if: always() # Create PR or branch even if the previous steps fail env: GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }} GITHUB_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }} GIT_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }} LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }} LLM_API_KEY: ${{ secrets.LLM_API_KEY }} LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }} PYTHONPATH: "" run: | if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then cd /tmp && python -m openhands.resolver.send_pull_request \ --issue-number ${{ env.ISSUE_NUMBER }} \ --target-branch ${{ env.TARGET_BRANCH }} \ --pr-type ${{ inputs.pr_type || 'draft' }} \ --reviewer ${{ github.actor }} | tee pr_result.txt && \ grep "PR created" pr_result.txt | sed 's/.*\///g' > pr_number.txt else cd /tmp && python -m openhands.resolver.send_pull_request \ --issue-number ${{ env.ISSUE_NUMBER }} \ --pr-type branch \ --send-on-failure | tee branch_result.txt && \ grep "branch created" branch_result.txt | sed 's/.*\///g; s/.expand=1//g' > branch_name.txt fi # Step leaves comment for when agent is invoked on PR - name: Analyze Push Logs (Updated PR or No Changes) # Skip comment if PR update was successful OR leave comment if the agent made no code changes uses: actions/github-script@v7 if: always() env: AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }} ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }} with: github-token: ${{ secrets.PAT_TOKEN || github.token }} script: | const fs = require('fs'); const issueNumber = process.env.ISSUE_NUMBER; let logContent = ''; try { logContent = fs.readFileSync('/tmp/pr_result.txt', 'utf8').trim(); } catch (error) { console.error('Error reading pr_result.txt file:', error); } const noChangesMessage = `No changes to commit for issue #${issueNumber}. Skipping commit.`; // Check logs from send_pull_request.py (pushes code to GitHub) if (logContent.includes("Updated pull request")) { console.log("Updated pull request found. Skipping comment."); process.env.AGENT_RESPONDED = 'true'; } else if (logContent.includes(noChangesMessage)) { github.rest.issues.createComment({ issue_number: issueNumber, owner: context.repo.owner, repo: context.repo.repo, body: `The workflow to fix this issue encountered an error. Openhands failed to create any code changes.` }); process.env.AGENT_RESPONDED = 'true'; } # Step leaves comment for when agent is invoked on issue - name: Comment on issue # Comment link to either PR or branch created by agent uses: actions/github-script@v7 if: always() # Comment on issue even if the previous steps fail env: AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }} ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }} RESOLUTION_SUCCESS: ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }} with: github-token: ${{ secrets.PAT_TOKEN || github.token }} script: | const fs = require('fs'); const path = require('path'); const issueNumber = process.env.ISSUE_NUMBER; const success = process.env.RESOLUTION_SUCCESS === 'true'; let prNumber = ''; let branchName = ''; let resultExplanation = ''; try { if (success) { prNumber = fs.readFileSync('/tmp/pr_number.txt', 'utf8').trim(); } else { branchName = fs.readFileSync('/tmp/branch_name.txt', 'utf8').trim(); } } catch (error) { console.error('Error reading file:', error); } try { if (!success){ // Read result_explanation from JSON file for failed resolution const outputFilePath = path.resolve('/tmp/output/output.jsonl'); if (fs.existsSync(outputFilePath)) { const outputContent = fs.readFileSync(outputFilePath, 'utf8'); const jsonLines = outputContent.split('\n').filter(line => line.trim() !== ''); if (jsonLines.length > 0) { // First entry in JSON lines has the key 'result_explanation' const firstEntry = JSON.parse(jsonLines[0]); resultExplanation = firstEntry.result_explanation || ''; } } } } catch (error){ console.error('Error reading file:', error); } // Check "success" log from resolver output if (success && prNumber) { github.rest.issues.createComment({ issue_number: issueNumber, owner: context.repo.owner, repo: context.repo.repo, body: `A potential fix has been generated and a draft PR #${prNumber} has been created. Please review the changes.` }); process.env.AGENT_RESPONDED = 'true'; } else if (!success && branchName) { let commentBody = `An attempt was made to automatically fix this issue, but it was unsuccessful. A branch named '${branchName}' has been created with the attempted changes. You can view the branch [here](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${branchName}). Manual intervention may be required.`; if (resultExplanation) { commentBody += `\n\nAdditional details about the failure:\n${resultExplanation}`; } github.rest.issues.createComment({ issue_number: issueNumber, owner: context.repo.owner, repo: context.repo.repo, body: commentBody }); process.env.AGENT_RESPONDED = 'true'; } # Leave error comment when both PR/Issue comment handling fail - name: Fallback Error Comment uses: actions/github-script@v7 if: ${{ env.AGENT_RESPONDED == 'false' }} # Only run if no conditions were met in previous steps env: ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }} with: github-token: ${{ secrets.PAT_TOKEN || github.token }} script: | const issueNumber = process.env.ISSUE_NUMBER; github.rest.issues.createComment({ issue_number: issueNumber, owner: context.repo.owner, repo: context.repo.repo, body: `The workflow to fix this issue encountered an error. Please check the [workflow logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for more information.` });