Files
blackroad-os-archive/scripts/slack-to-github.js
Alexa Louise d3c9e82850 Add BlackRoad Completion Framework
- GitHub Actions workflows (auto-merge, branch-tracker, issue-to-board, stale-cleanup)
- Issue templates (agent-task, bug, task)
- PR template
- Automation scripts (slack-to-github, create-issue)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 07:40:23 -06:00

184 lines
5.3 KiB
JavaScript

/**
* slack-to-github.js
*
* Webhook handler that creates GitHub issues from Slack messages.
* Deploy as: Cloudflare Worker, Vercel Function, or any serverless platform.
*
* Trigger: Slack slash command or bot mention
* Example: /issue Fix the login redirect bug
* Example: @blackroad-bot create issue: Add dark mode toggle
*
* Setup:
* 1. Create a Slack App with slash commands or bot
* 2. Set the request URL to your deployed function
* 3. Set environment variables: GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO, SLACK_SIGNING_SECRET
*/
// For Cloudflare Workers
export default {
async fetch(request, env) {
return handleRequest(request, env);
}
};
// For Vercel/other platforms, use:
// export default async function handler(req, res) { ... }
async function handleRequest(request, env) {
// Verify this is a POST request
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
try {
// Parse the Slack payload
const formData = await request.formData();
const payload = Object.fromEntries(formData);
// For slash commands, the text is in payload.text
// For bot mentions, you'd parse payload differently
const text = payload.text || '';
const userId = payload.user_id || 'unknown';
const userName = payload.user_name || 'unknown';
const channelId = payload.channel_id || '';
if (!text.trim()) {
return slackResponse('Please provide an issue title. Usage: `/issue Fix the login bug`');
}
// Parse priority from text if included
let priority = 'P2';
let title = text;
const priorityMatch = text.match(/\b(p[0-3])\b/i);
if (priorityMatch) {
priority = priorityMatch[1].toUpperCase();
title = text.replace(priorityMatch[0], '').trim();
}
// Detect if this should be an agent task
const isAgentTask = text.toLowerCase().includes('[agent]') ||
text.toLowerCase().includes('--agent');
title = title.replace(/\[agent\]/gi, '').replace(/--agent/gi, '').trim();
// Detect if this is a bug
const isBug = text.toLowerCase().includes('[bug]') ||
text.toLowerCase().includes('--bug');
title = title.replace(/\[bug\]/gi, '').replace(/--bug/gi, '').trim();
// Build labels
const labels = [];
if (isBug) {
labels.push('bug');
} else if (isAgentTask) {
labels.push('agent-task', 'automated');
} else {
labels.push('task');
}
// Add priority label
switch (priority) {
case 'P0': labels.push('p0-now'); break;
case 'P1': labels.push('p1-today'); break;
case 'P2': labels.push('p2-week'); break;
case 'P3': labels.push('p3-backlog'); break;
}
// Build title prefix
let fullTitle = title;
if (isBug) {
fullTitle = `[BUG] ${title}`;
} else if (isAgentTask) {
fullTitle = `[AGENT] ${title}`;
} else {
fullTitle = `[TASK] ${title}`;
}
// Create the GitHub issue
const issue = await createGitHubIssue({
title: fullTitle,
body: `Created from Slack by @${userName}\n\nChannel: <#${channelId}>`,
labels: labels,
env: env
});
// Send success response back to Slack
return slackResponse(
`✅ Issue created: <${issue.html_url}|#${issue.number} ${title}>\n` +
`Priority: ${priority} | Labels: ${labels.join(', ')}`
);
} catch (error) {
console.error('Error:', error);
return slackResponse(`❌ Failed to create issue: ${error.message}`);
}
}
async function createGitHubIssue({ title, body, labels, env }) {
const response = await fetch(
`https://api.github.com/repos/${env.GITHUB_OWNER}/${env.GITHUB_REPO}/issues`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${env.GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
'User-Agent': 'BlackRoad-Slack-Bot'
},
body: JSON.stringify({
title,
body,
labels
})
}
);
if (!response.ok) {
const error = await response.text();
throw new Error(`GitHub API error: ${response.status} - ${error}`);
}
return response.json();
}
function slackResponse(text) {
return new Response(
JSON.stringify({
response_type: 'in_channel', // visible to everyone, or 'ephemeral' for private
text: text
}),
{
headers: {
'Content-Type': 'application/json'
}
}
);
}
/**
* Environment variables needed:
*
* GITHUB_TOKEN - Personal access token with repo scope
* GITHUB_OWNER - Your GitHub username or org
* GITHUB_REPO - Repository name
* SLACK_SIGNING_SECRET - (optional) For verifying Slack requests
*
* To verify Slack requests (recommended for production):
*
* async function verifySlackRequest(request, signingSecret) {
* const timestamp = request.headers.get('x-slack-request-timestamp');
* const signature = request.headers.get('x-slack-signature');
* const body = await request.text();
*
* const baseString = `v0:${timestamp}:${body}`;
* const hmac = crypto.createHmac('sha256', signingSecret);
* hmac.update(baseString);
* const computed = `v0=${hmac.digest('hex')}`;
*
* return crypto.timingSafeEqual(
* Buffer.from(signature),
* Buffer.from(computed)
* );
* }
*/