AI Issue Planning #77
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 Issue Planning | |
| on: | |
| schedule: | |
| - cron: '0 16 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| ai-plan: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Find issues with ai-task label | |
| id: find-issues | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| script: | | |
| 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 fs = require('fs'); | |
| fs.writeFileSync('/tmp/issues.json', JSON.stringify(issues)); | |
| console.log('Found issues:', issues.length); | |
| return JSON.stringify(issues.map(i => i.number)); | |
| - name: Check issues | |
| id: check-issues | |
| run: | | |
| echo "issue_numbers=${{ steps.find-issues.outputs.result }}" >> $GITHUB_OUTPUT | |
| echo "Found issues: ${{ steps.find-issues.outputs.result }}" | |
| - name: Skip if no issues | |
| if: steps.check-issues.outputs.issue_numbers == '[]' | |
| run: | | |
| echo "No issues with ai-task label found" | |
| echo "SKIP=true" >> $GITHUB_OUTPUT | |
| - name: Setup bun | |
| if: steps.check-issues.outputs.issue_numbers != '[]' | |
| uses: oven-sh/setup-bun@v1 | |
| with: | |
| bun-version: latest | |
| - name: Install OpenCode and oh-my-opencode | |
| if: steps.check-issues.outputs.issue_numbers != '[]' | |
| run: | | |
| bun add -g opencode-ai | |
| bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=no | |
| - name: Copy configs and setup | |
| if: steps.check-issues.outputs.issue_numbers != '[]' | |
| 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: Get issue details | |
| if: steps.check-issues.outputs.issue_numbers != '[]' | |
| id: issue | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| script: | | |
| const fs = require('fs'); | |
| const issues = JSON.parse(fs.readFileSync('/tmp/issues.json', 'utf8')); | |
| if (issues.length === 0) { | |
| console.log('No issues to process'); | |
| return JSON.stringify({}); | |
| } | |
| const issue = issues[0]; | |
| const { data: issueData } = await github.rest.issues.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number | |
| }); | |
| const data = { | |
| number: issue.number, | |
| title: issue.title, | |
| body: issueData.body, | |
| url: issueData.html_url | |
| }; | |
| fs.writeFileSync('/tmp/issue_data.json', JSON.stringify(data)); | |
| return JSON.stringify(data); | |
| - name: Skip if no issues to process | |
| if: steps.issue.outputs.result == '{}' | |
| run: | | |
| echo "No issues to process - exiting" | |
| echo "SKIP=true" >> $GITHUB_OUTPUT | |
| - name: Check existing plan | |
| if: steps.issue.outputs.result != '{}' | |
| id: find-comment | |
| uses: actions/github-script@v7 | |
| with: | |
| result-encoding: string | |
| script: | | |
| const fs = require('fs'); | |
| const data = JSON.parse(fs.readFileSync('/tmp/issue_data.json', 'utf8')); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| issue_number: data.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| const aiComment = comments.find(c => c.body.includes('## AI Implementation Plan')); | |
| return aiComment ? aiComment.id : 'none'; | |
| - name: Remove label if plan exists | |
| if: steps.issue.outputs.result != '{}' && steps.find-comment.outputs.result != 'none' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const data = JSON.parse(fs.readFileSync('/tmp/issue_data.json', 'utf8')); | |
| await github.rest.issues.removeLabel({ | |
| issue_number: data.number, | |
| name: 'ai-task', | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| - name: Generate plan | |
| if: steps.issue.outputs.result != '{}' && steps.find-comment.outputs.result == 'none' | |
| id: opencode | |
| env: | |
| MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const data = JSON.parse(fs.readFileSync('/tmp/issue_data.json', 'utf8')); | |
| let p = fs.readFileSync('.github/workflows/prompts/ai-plan-prompt.txt','utf8'); | |
| p = p.replace(/{{issue_number}}/g, data.number) | |
| .replace(/{{issue_title}}/g, data.title) | |
| .replace(/{{issue_body}}/g, data.body || '(No description)') | |
| .replace(/{{issue_url}}/g, data.url); | |
| fs.writeFileSync('/tmp/prompt.txt', p); | |
| " | |
| opencode run "$(cat /tmp/prompt.txt)" | |
| - name: Post plan comment | |
| if: steps.issue.outputs.result != '{}' && steps.find-comment.outputs.result == 'none' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let plan = ''; | |
| try { | |
| plan = fs.readFileSync('plan.md', 'utf8'); | |
| } catch (e) { | |
| try { | |
| plan = fs.readFileSync('/tmp/plan.md', 'utf8'); | |
| } catch (e2) { | |
| plan = ''; | |
| } | |
| } | |
| if (!plan || plan.trim() === '' || plan.includes('No plan generated')) { | |
| throw new Error('AI failed to generate a plan. Check opencode configuration and API key.'); | |
| } | |
| const data = JSON.parse(fs.readFileSync('/tmp/issue_data.json', 'utf8')); | |
| await github.rest.issues.createComment({ | |
| issue_number: data.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: plan | |
| }); | |
| - name: Create branch | |
| if: steps.issue.outputs.result != '{}' && steps.find-comment.outputs.result == 'none' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| ISSUE_NUM=$(node -e "const fs=require('fs');console.log(JSON.parse(fs.readFileSync('/tmp/issue_data.json','utf8')).number)") | |
| BRANCH_NAME="ai/issue-${ISSUE_NUM}" | |
| git config user.name "AI Agent" | |
| git config user.email "ai@github.local" | |
| git checkout -b "$BRANCH_NAME" main 2>/dev/null || git checkout -b "$BRANCH_NAME" master 2>/dev/null || git checkout -b "$BRANCH_NAME" | |
| git push -u origin "$BRANCH_NAME" || echo "Branch created locally" | |
| - name: Remove ai-task label after planning | |
| if: steps.issue.outputs.result != '{}' && steps.find-comment.outputs.result == 'none' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const data = JSON.parse(fs.readFileSync('/tmp/issue_data.json', 'utf8')); | |
| await github.rest.issues.removeLabel({ | |
| issue_number: data.number, | |
| name: 'ai-task', | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| - name: Cleanup temp files | |
| if: always() | |
| run: | | |
| rm -f /tmp/prompt.txt /tmp/plan.md plan.md /tmp/issue_data.json 2>/dev/null || true | |
| - name: Cleanup label on failure | |
| if: failure() && steps.issue.outputs.result != '{}' && steps.find-comment.outputs.result == 'none' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| try { | |
| const data = JSON.parse(fs.readFileSync('/tmp/issue_data.json', 'utf8')); | |
| await github.rest.issues.removeLabel({ | |
| issue_number: data.number, | |
| name: 'ai-task', | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| } catch (e) { | |
| console.log('Could not remove label: ' + e.message); | |
| } |