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.`
            });
