Security compliance - SHA pinning for all actions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
400 lines
16 KiB
YAML
400 lines
16 KiB
YAML
# .github/workflows/autonomous-issue-manager.yml
|
|
# Autonomous issue creation, triage, and management
|
|
|
|
name: "Autonomous Issue Manager"
|
|
|
|
on:
|
|
issues:
|
|
types: [opened, edited, labeled, assigned]
|
|
issue_comment:
|
|
types: [created]
|
|
schedule:
|
|
- cron: '0 9 * * *' # Daily at 9 AM - stale check
|
|
workflow_run:
|
|
workflows: ["Autonomous Orchestrator", "Autonomous Self-Healer"]
|
|
types: [completed]
|
|
conclusions: [failure]
|
|
workflow_dispatch:
|
|
inputs:
|
|
action:
|
|
description: 'Action to perform'
|
|
required: true
|
|
type: choice
|
|
options:
|
|
- triage_all
|
|
- cleanup_stale
|
|
- generate_report
|
|
- create_health_issues
|
|
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
|
|
env:
|
|
BLACKROAD_AGENT_API: https://blackroad-agents.amundsonalexa.workers.dev
|
|
STALE_DAYS: 30
|
|
CLOSE_DAYS: 7
|
|
|
|
jobs:
|
|
# ============================================
|
|
# Smart Issue Triage
|
|
# ============================================
|
|
triage:
|
|
name: "Smart Triage"
|
|
if: github.event_name == 'issues' && github.event.action == 'opened'
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
|
|
|
- name: AI Analysis
|
|
id: ai
|
|
run: |
|
|
TITLE="${{ github.event.issue.title }}"
|
|
BODY="${{ github.event.issue.body }}"
|
|
|
|
# Call AI for smart categorization
|
|
ANALYSIS=$(curl -s -X POST "${{ env.BLACKROAD_AGENT_API }}/analyze-issue" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"title": "'"$TITLE"'",
|
|
"body": "'"$(echo "$BODY" | head -c 2000 | jq -Rs .)"'",
|
|
"repo": "${{ github.repository }}"
|
|
}' 2>/dev/null || echo '{}')
|
|
|
|
echo "analysis=$ANALYSIS" >> $GITHUB_OUTPUT
|
|
|
|
# Parse AI response for labels
|
|
LABELS=$(echo "$ANALYSIS" | jq -r '.labels // [] | join(",")' 2>/dev/null || echo "")
|
|
PRIORITY=$(echo "$ANALYSIS" | jq -r '.priority // "normal"' 2>/dev/null || echo "normal")
|
|
ASSIGNEE=$(echo "$ANALYSIS" | jq -r '.assignee // ""' 2>/dev/null || echo "")
|
|
|
|
echo "labels=$LABELS" >> $GITHUB_OUTPUT
|
|
echo "priority=$PRIORITY" >> $GITHUB_OUTPUT
|
|
echo "assignee=$ASSIGNEE" >> $GITHUB_OUTPUT
|
|
|
|
- name: Keyword-Based Labeling
|
|
id: keywords
|
|
run: |
|
|
TITLE="${{ github.event.issue.title }}"
|
|
BODY="${{ github.event.issue.body }}"
|
|
TEXT="$TITLE $BODY"
|
|
LABELS=""
|
|
|
|
# Type detection
|
|
echo "$TEXT" | grep -qi "bug\|error\|broken\|not working\|crash\|fail" && LABELS="$LABELS,bug"
|
|
echo "$TEXT" | grep -qi "feature\|add\|new\|enhance\|request" && LABELS="$LABELS,enhancement"
|
|
echo "$TEXT" | grep -qi "question\|how\|help\|what\|why" && LABELS="$LABELS,question"
|
|
echo "$TEXT" | grep -qi "doc\|documentation\|readme\|typo" && LABELS="$LABELS,documentation"
|
|
|
|
# Area detection
|
|
echo "$TEXT" | grep -qi "security\|vulnerability\|cve\|auth" && LABELS="$LABELS,security"
|
|
echo "$TEXT" | grep -qi "performance\|slow\|memory\|cpu" && LABELS="$LABELS,performance"
|
|
echo "$TEXT" | grep -qi "ui\|frontend\|css\|style\|design" && LABELS="$LABELS,frontend"
|
|
echo "$TEXT" | grep -qi "api\|backend\|server\|database" && LABELS="$LABELS,backend"
|
|
echo "$TEXT" | grep -qi "ci\|deploy\|workflow\|action" && LABELS="$LABELS,infrastructure"
|
|
|
|
# Priority detection
|
|
echo "$TEXT" | grep -qi "urgent\|critical\|asap\|important\|blocker" && LABELS="$LABELS,priority:high"
|
|
echo "$TEXT" | grep -qi "minor\|low\|when possible" && LABELS="$LABELS,priority:low"
|
|
|
|
# Clean up labels
|
|
LABELS=$(echo "$LABELS" | sed 's/^,//' | sed 's/,,/,/g')
|
|
echo "labels=$LABELS" >> $GITHUB_OUTPUT
|
|
|
|
- name: Apply Labels
|
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
|
|
with:
|
|
script: |
|
|
const aiLabels = '${{ steps.ai.outputs.labels }}'.split(',').filter(l => l);
|
|
const keywordLabels = '${{ steps.keywords.outputs.labels }}'.split(',').filter(l => l);
|
|
|
|
// Merge and dedupe labels
|
|
const allLabels = [...new Set([...aiLabels, ...keywordLabels])].filter(l => l);
|
|
|
|
if (allLabels.length > 0) {
|
|
// Ensure labels exist (create if not)
|
|
for (const label of allLabels) {
|
|
try {
|
|
await github.rest.issues.getLabel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
name: label
|
|
});
|
|
} catch (e) {
|
|
// Label doesn't exist, create it
|
|
const colors = {
|
|
'bug': 'd73a4a',
|
|
'enhancement': 'a2eeef',
|
|
'question': 'd876e3',
|
|
'documentation': '0075ca',
|
|
'security': 'b60205',
|
|
'performance': 'fbca04',
|
|
'frontend': '7057ff',
|
|
'backend': '008672',
|
|
'infrastructure': 'c5def5',
|
|
'priority:high': 'b60205',
|
|
'priority:low': 'c2e0c6'
|
|
};
|
|
|
|
await github.rest.issues.createLabel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
name: label,
|
|
color: colors[label] || '333333'
|
|
}).catch(() => {});
|
|
}
|
|
}
|
|
|
|
await github.rest.issues.addLabels({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.payload.issue.number,
|
|
labels: allLabels
|
|
});
|
|
}
|
|
|
|
- name: Welcome Response
|
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
|
|
with:
|
|
script: |
|
|
const labels = '${{ steps.keywords.outputs.labels }}'.split(',').filter(l => l);
|
|
const priority = '${{ steps.ai.outputs.priority }}';
|
|
|
|
let response = `Thanks for opening this issue! 👋\n\n`;
|
|
|
|
// Add context based on type
|
|
if (labels.includes('bug')) {
|
|
response += `This has been identified as a **bug report**. `;
|
|
response += `To help us investigate:\n`;
|
|
response += `- What version are you using?\n`;
|
|
response += `- Can you provide steps to reproduce?\n`;
|
|
response += `- Any error messages or logs?\n\n`;
|
|
} else if (labels.includes('enhancement')) {
|
|
response += `This has been identified as a **feature request**. `;
|
|
response += `We'll review and prioritize accordingly.\n\n`;
|
|
} else if (labels.includes('question')) {
|
|
response += `This has been identified as a **question**. `;
|
|
response += `Check our [documentation](https://docs.blackroad.io) while you wait for a response.\n\n`;
|
|
}
|
|
|
|
if (priority === 'high') {
|
|
response += `⚠️ **High priority** - This will be reviewed soon.\n\n`;
|
|
}
|
|
|
|
response += `**Automated Labels Applied:** ${labels.length > 0 ? labels.map(l => '`' + l + '`').join(', ') : 'None'}\n\n`;
|
|
response += `---\n*Triaged by BlackRoad Autonomous Agent*`;
|
|
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.payload.issue.number,
|
|
body: response
|
|
});
|
|
|
|
# ============================================
|
|
# Stale Issue Cleanup
|
|
# ============================================
|
|
stale-cleanup:
|
|
name: "Stale Cleanup"
|
|
if: github.event_name == 'schedule' || github.event.inputs.action == 'cleanup_stale'
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Find Stale Issues
|
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
|
|
with:
|
|
script: |
|
|
const staleDays = parseInt('${{ env.STALE_DAYS }}');
|
|
const closeDays = parseInt('${{ env.CLOSE_DAYS }}');
|
|
const now = new Date();
|
|
|
|
// Get open issues
|
|
const issues = await github.rest.issues.listForRepo({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
state: 'open',
|
|
per_page: 100
|
|
});
|
|
|
|
for (const issue of issues.data) {
|
|
// Skip PRs
|
|
if (issue.pull_request) continue;
|
|
|
|
const updatedAt = new Date(issue.updated_at);
|
|
const daysSinceUpdate = Math.floor((now - updatedAt) / (1000 * 60 * 60 * 24));
|
|
|
|
const hasStaleLabel = issue.labels.some(l => l.name === 'stale');
|
|
const isProtected = issue.labels.some(l =>
|
|
['pinned', 'security', 'priority:high', 'in-progress'].includes(l.name)
|
|
);
|
|
|
|
if (isProtected) continue;
|
|
|
|
// Already marked stale - check if should close
|
|
if (hasStaleLabel && daysSinceUpdate >= closeDays) {
|
|
await github.rest.issues.update({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issue.number,
|
|
state: 'closed',
|
|
state_reason: 'not_planned'
|
|
});
|
|
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issue.number,
|
|
body: `This issue has been automatically closed due to inactivity.\n\nIf this is still relevant, please reopen it with additional context.\n\n---\n*Closed by BlackRoad Autonomous Agent*`
|
|
});
|
|
|
|
console.log(`Closed stale issue #${issue.number}`);
|
|
}
|
|
// Mark as stale
|
|
else if (!hasStaleLabel && daysSinceUpdate >= staleDays) {
|
|
await github.rest.issues.addLabels({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issue.number,
|
|
labels: ['stale']
|
|
});
|
|
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issue.number,
|
|
body: `This issue has been automatically marked as **stale** because it has not had recent activity.\n\nIt will be closed in ${closeDays} days if no further activity occurs.\n\n---\n*Marked by BlackRoad Autonomous Agent*`
|
|
});
|
|
|
|
console.log(`Marked issue #${issue.number} as stale`);
|
|
}
|
|
}
|
|
|
|
# ============================================
|
|
# Auto-Create Issues from Failures
|
|
# ============================================
|
|
failure-issue:
|
|
name: "Create Failure Issue"
|
|
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure'
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Check for Existing Issue
|
|
id: check
|
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
|
|
with:
|
|
script: |
|
|
// Search for existing issue about this workflow
|
|
const workflowName = '${{ github.event.workflow_run.name }}';
|
|
const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open "[Automated] ${workflowName}" in:title`;
|
|
|
|
const results = await github.rest.search.issuesAndPullRequests({
|
|
q: searchQuery
|
|
});
|
|
|
|
core.setOutput('exists', results.data.total_count > 0);
|
|
if (results.data.total_count > 0) {
|
|
core.setOutput('issue_number', results.data.items[0].number);
|
|
}
|
|
|
|
- name: Create or Update Issue
|
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
|
|
with:
|
|
script: |
|
|
const workflowName = '${{ github.event.workflow_run.name }}';
|
|
const runId = '${{ github.event.workflow_run.id }}';
|
|
const runUrl = '${{ github.event.workflow_run.html_url }}';
|
|
const exists = '${{ steps.check.outputs.exists }}' === 'true';
|
|
const existingNumber = '${{ steps.check.outputs.issue_number }}';
|
|
|
|
const body = `## Workflow Failure Detected
|
|
|
|
**Workflow:** ${workflowName}
|
|
**Run ID:** ${runId}
|
|
**Run URL:** ${runUrl}
|
|
**Time:** ${new Date().toISOString()}
|
|
|
|
### Details
|
|
The autonomous orchestrator detected a failure in the ${workflowName} workflow.
|
|
|
|
### Suggested Actions
|
|
1. Review the [workflow run logs](${runUrl})
|
|
2. Check recent commits for potential causes
|
|
3. Run the self-healer workflow if appropriate
|
|
|
|
---
|
|
*Created by BlackRoad Autonomous Agent*`;
|
|
|
|
if (exists) {
|
|
// Add comment to existing issue
|
|
await github.rest.issues.createComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: parseInt(existingNumber),
|
|
body: `### New Failure Detected\n\n**Run:** ${runUrl}\n**Time:** ${new Date().toISOString()}`
|
|
});
|
|
} else {
|
|
// Create new issue
|
|
await github.rest.issues.create({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
title: `[Automated] ${workflowName} Workflow Failure`,
|
|
body: body,
|
|
labels: ['bug', 'automated', 'ci-failure']
|
|
});
|
|
}
|
|
|
|
# ============================================
|
|
# Generate Report
|
|
# ============================================
|
|
report:
|
|
name: "Generate Issue Report"
|
|
if: github.event.inputs.action == 'generate_report'
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Generate Statistics
|
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea
|
|
with:
|
|
script: |
|
|
// Get all issues
|
|
const issues = await github.rest.issues.listForRepo({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
state: 'all',
|
|
per_page: 100
|
|
});
|
|
|
|
const stats = {
|
|
total: issues.data.length,
|
|
open: issues.data.filter(i => i.state === 'open' && !i.pull_request).length,
|
|
closed: issues.data.filter(i => i.state === 'closed' && !i.pull_request).length,
|
|
bugs: issues.data.filter(i => i.labels.some(l => l.name === 'bug')).length,
|
|
enhancements: issues.data.filter(i => i.labels.some(l => l.name === 'enhancement')).length,
|
|
stale: issues.data.filter(i => i.labels.some(l => l.name === 'stale')).length
|
|
};
|
|
|
|
console.log('Issue Statistics:', stats);
|
|
|
|
// Create summary issue
|
|
await github.rest.issues.create({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
title: `[Report] Issue Statistics - ${new Date().toISOString().split('T')[0]}`,
|
|
body: `## Issue Statistics Report
|
|
|
|
| Metric | Count |
|
|
|--------|-------|
|
|
| Total Issues | ${stats.total} |
|
|
| Open | ${stats.open} |
|
|
| Closed | ${stats.closed} |
|
|
| Bugs | ${stats.bugs} |
|
|
| Enhancements | ${stats.enhancements} |
|
|
| Stale | ${stats.stale} |
|
|
|
|
---
|
|
*Generated by BlackRoad Autonomous Agent*`,
|
|
labels: ['report', 'automated']
|
|
});
|