From 93cf1272bba9e482231ba5be740e0d5e7db182cb Mon Sep 17 00:00:00 2001 From: ponderingdemocritus Date: Wed, 29 Jan 2025 15:08:07 +1100 Subject: [PATCH] openhands --- .github/workflows/openhands.yaml | 406 +++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 .github/workflows/openhands.yaml diff --git a/.github/workflows/openhands.yaml b/.github/workflows/openhands.yaml new file mode 100644 index 00000000..f0fed3ac --- /dev/null +++ b/.github/workflows/openhands.yaml @@ -0,0 +1,406 @@ +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" + LLM_MODEL: + required: false + type: string + default: "anthropic/claude-3-5-sonnet-20241022" + base_container_image: + required: false + type: string + default: "" + description: "Custom sandbox env" + 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: 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: 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 '()') + # Ensure requirements.txt ends with newline before appending + if [ -f requirements.txt ] && [ -s requirements.txt ]; then + sed -i -e '$a\' requirements.txt + fi + echo "openhands-ai==${OPENHANDS_VERSION}" >> requirements.txt + cat 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('requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('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 }} + 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 + 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 "${{ github.event.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 "${{ github.event.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_ENV_BASE_CONTAINER_IMAGE=${{ inputs.base_container_image }}" >> $GITHUB_ENV + + # Set branch variables + echo "TARGET_BRANCH=${{ inputs.target_branch }}" >> $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/All-Hands-AI/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("python -m pip install --upgrade pip"); + await exec.exec("pip install git+https://github.com/all-hands-ai/openhands.git"); + } else { + console.log("Installing from requirements.txt..."); + await exec.exec("python -m pip install --upgrade pip"); + await exec.exec("pip install -r requirements.txt"); + } + + - name: Attempt to resolve issue + env: + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }} + GITHUB_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 }} + PYTHONPATH: "" + run: | + cd /tmp && python -m openhands.resolver.resolve_issue \ + --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' }} + LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + 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 }} \ + --pr-type draft \ + --reviewer ${{ github.actor }} | tee pr_result.txt && \ + grep "draft 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' }} + with: + github-token: ${{ secrets.PAT_TOKEN || github.token }} + script: | + const fs = require('fs'); + const issueNumber = ${{ 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' }} + with: + github-token: ${{ secrets.PAT_TOKEN || github.token }} + script: | + const fs = require('fs'); + const path = require('path'); + const issueNumber = ${{ env.ISSUE_NUMBER }}; + const success = ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}; + + 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 + with: + github-token: ${{ secrets.PAT_TOKEN || github.token }} + script: | + const issueNumber = ${{ 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.` + });