Skip to content

AI Issue Planning

AI Issue Planning #77

Workflow file for this run

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