diff --git a/.agents/TOOLS.md b/.agents/TOOLS.md index 2cbce6c..bed08ea 100644 --- a/.agents/TOOLS.md +++ b/.agents/TOOLS.md @@ -25,7 +25,8 @@ | `publish-all.yml` | tag release | Publicação NuGet | | `code-quality.yml` | PR | SonarCloud + Qodana | | `security-scan.yml` | schedule/PR | Scan de segurança | -| `openhands-resolver.yml` | issue label | Auto-fix via OpenHands | +| `opencode.yml` | issue_comment / pull_request_review_comment | OpenCode com OmniRoute (chat) | +| `opencode-auto-fix.yml` | issue label | OpenCode com OmniRoute (fix-me) | | `auto-pr-from-main.yml` | push main | Sync automático | ## APIs Externas diff --git a/.agents/WORKFLOWS.md b/.agents/WORKFLOWS.md index b4ca3ef..aac6f23 100644 --- a/.agents/WORKFLOWS.md +++ b/.agents/WORKFLOWS.md @@ -83,3 +83,5 @@ dotnet test QRCoder.Core.sln --configuration Release --collect:"XPlat Code Cover | Tag de release | `publish-all.yml` | | Push em `main` | `auto-pr-from-main.yml` | | Schedule | `security-scan.yml` | +| Comentário `/oc` ou `/opencode` | `opencode.yml` | +| Label `fix-me` em issue/PR | `opencode-auto-fix.yml` | diff --git a/.github/workflows/opencode-auto-fix.yml b/.github/workflows/opencode-auto-fix.yml new file mode 100644 index 0000000..90821a7 --- /dev/null +++ b/.github/workflows/opencode-auto-fix.yml @@ -0,0 +1,38 @@ +name: OpenCode Auto-Fix + +on: + issues: + types: [labeled] + pull_request: + types: [labeled] + +jobs: + opencode: + if: github.event.label.name == 'fix-me' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + issues: write + env: + OMNIROUTE_BASE_URL: https://omniroute.afonsoft.dev/v1 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Run OpenCode + uses: anomalyco/opencode/github@b1fc8113948b518835c2a39ece49553cffe9b30c # v1.17.18 + env: + OMNIROUTE_API_KEY: ${{ secrets.OMNIROUTE_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + model: omniroute/auto/claude-sonnet + agent: build + use_github_token: true + prompt: | + Fix the issue or implement the feature described. + Open a pull request with the changes and provide a clear summary. diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml new file mode 100644 index 0000000..1034fb5 --- /dev/null +++ b/.github/workflows/opencode.yml @@ -0,0 +1,42 @@ +name: OpenCode + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +jobs: + opencode: + if: | + contains(fromJSON('["OWNER", "COLLABORATOR", "MEMBER"]'), github.event.comment.author_association) && + ( + github.event.comment.body == '/oc' || + startsWith(github.event.comment.body, '/oc ') || + github.event.comment.body == '/opencode' || + startsWith(github.event.comment.body, '/opencode ') + ) + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + issues: write + env: + OMNIROUTE_BASE_URL: https://omniroute.afonsoft.dev/v1 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Run OpenCode + uses: anomalyco/opencode/github@b1fc8113948b518835c2a39ece49553cffe9b30c # v1.17.18 + env: + OMNIROUTE_API_KEY: ${{ secrets.OMNIROUTE_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + model: omniroute/auto/claude-sonnet + agent: build + use_github_token: true diff --git a/.github/workflows/openhands-resolver.yml b/.github/workflows/openhands-resolver.yml deleted file mode 100644 index 25f1a62..0000000 --- a/.github/workflows/openhands-resolver.yml +++ /dev/null @@ -1,431 +0,0 @@ -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@v7 - - - name: Set up Python - uses: actions/setup-python@v6 - 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@v6 - 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@v9 - 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@v9 - 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@v7 - if: always() # Upload even if previous steps fail - with: - name: resolver-output - path: /tmp/output/output.jsonl - retention-days: 30 # Keep artifact for 30 days - - - name: Create draft PR or push branch - if: always() # Create PR or branch even if 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 agent made no code changes - uses: actions/github-script@v9 - 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@v9 - if: always() # Comment on issue even if 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 been 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 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@v9 - 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.` - }); diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..6de1108 --- /dev/null +++ b/opencode.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://opencode.ai/config.json", + "server": { + "port": 4096, + "hostname": "0.0.0.0", + "mdns": true, + "cors": ["https://opencode.afonsoft.dev"] + }, + "provider": { + "omniroute": { + "npm": "@ai-sdk/openai-compatible", + "name": "OmniRoute", + "options": { + "baseURL": "{env:OMNIROUTE_BASE_URL}", + "apiKey": "{env:OMNIROUTE_API_KEY}" + }, + "models": { + "auto/claude-opus": { "name": "auto/claude-opus" }, + "auto/claude-sonnet": { "name": "auto/claude-sonnet" }, + "auto/pro-coding": { "name": "auto/pro-coding" }, + "auto/best-coding": { "name": "auto/best-coding" } + } + } + } +}