- 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>
184 lines
5.3 KiB
JavaScript
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)
|
|
* );
|
|
* }
|
|
*/
|