AI Implementation #44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: AI Implementation | |
| on: | |
| schedule: | |
| - cron: '0 16 * * *' | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - 'ai/**' | |
| concurrency: | |
| group: ai-impl-${{ github.ref_name }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: read | |
| id-token: write | |
| jobs: | |
| ai-implement: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.ref_name }} | |
| fetch-depth: 1 | |
| - name: Setup bun | |
| uses: oven-sh/setup-bun@v1 | |
| with: | |
| bun-version: latest | |
| - name: Get opencode version | |
| id: version | |
| run: | | |
| VERSION=$(curl -sf https://api.github.com/repos/anomalyco/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4) | |
| echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT | |
| - name: Cache opencode | |
| id: cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.opencode/bin | |
| key: opencode-${{ runner.os }}-${{ runner.arch }}-${{ steps.version.outputs.version }} | |
| - name: Install opencode | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| run: curl -fsSL https://opencode.ai/install | bash | |
| - name: Add opencode to PATH | |
| run: echo "$HOME/.opencode/bin" >> $GITHUB_PATH | |
| - name: Install oh-my-opencode | |
| run: | | |
| bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=no | |
| - name: Copy oh-my-opencode config | |
| env: | |
| MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} | |
| run: | | |
| mkdir -p ~/.config/opencode | |
| sed "s/MINIMAX_API_KEY/${MINIMAX_API_KEY}/g" .github/workflows/opencode.json > ~/.config/opencode/opencode.json | |
| sed "s/MINIMAX_API_KEY/${MINIMAX_API_KEY}/g" .github/workflows/oh-my-opencode.json > ~/.config/opencode/oh-my-opencode.json | |
| - name: Determine trigger type and get issue info | |
| id: determine | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| script: | | |
| const isSchedule = context.eventName === 'schedule'; | |
| const isDispatch = context.eventName === 'workflow_dispatch'; | |
| const isPush = context.eventName === 'push'; | |
| let issueNumber = null; | |
| let branchName = null; | |
| if (isPush && '${{ github.ref_name }}'.startsWith('ai/issue-')) { | |
| const match = '${{ github.ref_name }}'.match(/ai\/issue-(\d+)/); | |
| if (match) { | |
| issueNumber = parseInt(match[1]); | |
| branchName = '${{ github.ref_name }}'; | |
| } | |
| } else if (isDispatch) { | |
| const inputs = context.payload.inputs; | |
| if (inputs && inputs.issue_number) { | |
| issueNumber = parseInt(inputs.issue_number); | |
| branchName = 'ai/issue-' + issueNumber; | |
| } | |
| } else if (isSchedule) { | |
| const { data: issues } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'ai-task', | |
| per_page: 10 | |
| }); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| issue_number: issues[0]?.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| const hasPlan = comments.some(c => c.body.includes('## AI Implementation Plan')); | |
| if (issues.length > 0 && hasPlan) { | |
| issueNumber = issues[0].number; | |
| branchName = 'ai/issue-' + issueNumber; | |
| } | |
| } | |
| return JSON.stringify({ issueNumber, branchName }); | |
| - name: Set issue number | |
| id: extract | |
| run: | | |
| node -e "const d=JSON.parse(process.env.RESULT); console.log('issue_number='+(d.issueNumber||'')); console.log('branch_name='+(d.branchName||''))" >> $GITHUB_OUTPUT | |
| env: | |
| RESULT: ${{ steps.determine.outputs.result }} | |
| - name: Skip if no issues to process | |
| if: steps.extract.outputs.issue_number == '' | |
| run: | | |
| echo "SKIP=true" >> $GITHUB_OUTPUT | |
| echo "No issues with plan found" | |
| - name: Checkout branch | |
| if: steps.extract.outputs.branch_name != '' | |
| env: | |
| BRANCH: ${{ steps.extract.outputs.branch_name }} | |
| run: | | |
| git fetch origin $BRANCH || git checkout -b $BRANCH || true | |
| git checkout $BRANCH || true | |
| - name: Set environment variables | |
| if: steps.extract.outputs.branch_name != '' | |
| run: | | |
| echo "ISSUE_NUM=${{ steps.extract.outputs.issue_number }}" >> $GITHUB_ENV | |
| echo "PR_NUMBER=${{ steps.extract.outputs.issue_number }}" >> $GITHUB_ENV | |
| - name: Get issue and plan | |
| id: issue | |
| if: steps.extract.outputs.branch_name != '' | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| script: | | |
| const num = '${{ steps.extract.outputs.issue_number }}'; | |
| const { data: issue } = await github.rest.issues.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: parseInt(num) | |
| }); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| issue_number: parseInt(num), | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| const plan = comments.find(c => c.body.includes('## AI Implementation Plan')); | |
| return JSON.stringify({ number: issue.number, title: issue.title, plan: plan ? plan.body : '' }); | |
| - name: Prepare prompt | |
| if: steps.extract.outputs.branch_name != '' | |
| id: prepare_prompt | |
| env: | |
| ISSUE_DATA: ${{ steps.issue.outputs.result }} | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const data = JSON.parse(process.env.ISSUE_DATA || '{}'); | |
| let p = fs.readFileSync('.github/workflows/prompts/ai-implement-prompt.txt','utf8'); | |
| p = p.replace(/{{issue_number}}/g, data.number || '') | |
| .replace(/{{issue_title}}/g, data.title || '') | |
| .replace(/{{plan}}/g, data.plan || 'No plan found'); | |
| console.log('prompt_content=' + p); | |
| " >> $GITHUB_OUTPUT | |
| - name: Run OpenCode | |
| id: opencode | |
| if: steps.extract.outputs.branch_name != '' | |
| run: opencode github run | |
| env: | |
| MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} | |
| MODEL: minimax-cn-coding-plan/MiniMax-M2.5 | |
| AGENT: sisyphus | |
| PROMPT: ${{ steps.prepare_prompt.outputs.prompt_content }} | |
| - name: Check changes | |
| id: changes | |
| run: | | |
| echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT | |
| - name: Commit changes | |
| if: steps.changes.outputs.changed != '0' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git config user.name "AI Agent" | |
| git config user.email "ai@github.local" | |
| git add -A | |
| git commit -m "AI: Implement issue-${{ env.ISSUE_NUM }}" || true | |
| git push origin "ai/issue-${{ env.ISSUE_NUM }}" || true | |
| - name: Build | |
| id: build | |
| run: | | |
| dotnet restore TelegramSearchBot.sln | |
| dotnet build TelegramSearchBot.sln --configuration Release --no-restore | |
| continue-on-error: true | |
| - name: Prepare build fix prompt | |
| if: steps.build.outcome == 'failure' | |
| id: prepare_build_fix | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| let p = fs.readFileSync('.github/workflows/prompts/ai-fix-build-prompt.txt','utf8'); | |
| p = p.replace(/{{issue_number}}/g, '${{ env.ISSUE_NUM }}'); | |
| console.log('prompt_content=' + p); | |
| " >> $GITHUB_OUTPUT | |
| - name: Fix build | |
| if: steps.build.outcome == 'failure' | |
| run: opencode github run | |
| env: | |
| MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} | |
| MODEL: minimax-cn-coding-plan/MiniMax-M2.5 | |
| AGENT: sisyphus | |
| PROMPT: ${{ steps.prepare_build_fix.outputs.prompt_content }} | |
| - name: Rebuild after fix | |
| if: steps.build.outcome == 'failure' | |
| run: dotnet build TelegramSearchBot.sln --configuration Release | |
| - name: Commit build fixes | |
| if: steps.build.outcome == 'failure' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git config user.name "AI Agent" | |
| git config user.email "ai@github.local" | |
| git add -A | |
| git commit -m "AI: Fix build issue-${{ env.ISSUE_NUM }}" || true | |
| git push origin "ai/issue-${{ env.ISSUE_NUM }}" || true | |
| - name: Run tests | |
| id: tests | |
| run: dotnet test TelegramSearchBot.sln --configuration Release --no-build | |
| continue-on-error: true | |
| - name: Prepare test fix prompt | |
| if: steps.tests.outcome == 'failure' | |
| id: prepare_test_fix | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| let p = fs.readFileSync('.github/workflows/prompts/ai-fix-tests-prompt.txt','utf8'); | |
| p = p.replace(/{{issue_number}}/g, '${{ env.ISSUE_NUM }}'); | |
| console.log('prompt_content=' + p); | |
| " >> $GITHUB_OUTPUT | |
| - name: Fix tests | |
| if: steps.tests.outcome == 'failure' | |
| run: opencode github run | |
| env: | |
| MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} | |
| MODEL: minimax-cn-coding-plan/MiniMax-M2.5 | |
| AGENT: sisyphus | |
| PROMPT: ${{ steps.prepare_test_fix.outputs.prompt_content }} | |
| - name: Rebuild after test fix | |
| if: steps.tests.outcome == 'failure' | |
| run: | | |
| dotnet build TelegramSearchBot.sln --configuration Release | |
| dotnet test TelegramSearchBot.sln --configuration Release --no-build || true | |
| - name: Commit fixes | |
| if: steps.tests.outcome == 'failure' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git config user.name "AI Agent" | |
| git config user.email "ai@github.local" | |
| git add -A | |
| git commit -m "AI: Fix tests issue-${{ env.ISSUE_NUM }}" || true | |
| git push origin "ai/issue-${{ env.ISSUE_NUM }}" || true | |
| - name: Create PR | |
| if: always() | |
| id: create_pr | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh pr create --base master --head "ai/issue-${{ env.ISSUE_NUM }}" --title "AI: Issue #${{ env.ISSUE_NUM }}" --body "AI implementation" || echo "PR exists" |