send-slack-triage.ts2.6 KBView on GitHub
/**
 * Send bug triage to #customer-success via Slack webhook.
 *
 * Usage (inline via stdin — no file needed):
 *   echo "<markdown>" | pnpm run send-slack-triage
 *
 * Usage (from file):
 *   pnpm run send-slack-triage -- docs/bug-triage-{call}-{date}.md
 *
 * Requires: SLACK_ALERTS_WEBHOOK_URL or SLACK_CUSTOMER_SUCCESS_WEBHOOK_URL in .env
 */

import { readFile } from 'fs/promises';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

const webhookUrl =
  process.env.SLACK_CUSTOMER_SUCCESS_WEBHOOK_URL || process.env.SLACK_ALERTS_WEBHOOK_URL;

if (!webhookUrl) {
  console.error(
    'Missing webhook URL. Set SLACK_CUSTOMER_SUCCESS_WEBHOOK_URL or SLACK_ALERTS_WEBHOOK_URL in .env'
  );
  process.exit(1);
}

// Read triage content: from file arg, or stdin if no arg given
const pathArg = process.argv.slice(2).find((a) => a !== '--' && !a.startsWith('-'));

let triage: string;
if (pathArg) {
  const triagePath = join(process.cwd(), pathArg);
  triage = await readFile(triagePath, 'utf8');
} else {
  // Read from stdin (piped content — no file creation needed)
  const chunks: Buffer[] = [];
  for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
  triage = Buffer.concat(chunks).toString('utf8').trim();
  if (!triage) {
    console.error('No triage content provided. Pass a file path or pipe content via stdin.');
    process.exit(1);
  }
}

// Format for Slack: convert markdown to Slack mrkdwn (simplified)
const slackText = triage
  .replace(/^# .+$/gm, (m) => `*${m.replace(/^#+ /, '')}*`)
  .replace(/^## .+$/gm, (m) => `\n*${m.replace(/^##+ /, '')}*`)
  .replace(/^### .+$/gm, (m) => `\n*${m.replace(/^###+ /, '')}*`)
  .replace(/^- \[ \] /gm, '• ')
  .replace(/^- \[x\] /gm, '• ✓ ')
  // Convert markdown links [text](url) to Slack mrkdwn <url|text>
  .replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<$2|$1>')
  // Convert bare URLs in ticket lines to Slack links
  .replace(/^- ((?:Isabelle|Jesse): )(https?:\/\/\S+)$/gm, '• $1<$2|$2>')
  .replace(/\*\*(.+?)\*\*/g, '*$1*')
  .replace(/`([^`]+)`/g, '`$1`');

// Use first heading as title, or fallback
const titleMatch = triage.match(/^# (.+)$/m);
const title = titleMatch ? titleMatch[1] : 'Bug Triage';

const payload = {
  channel: '#customer-success',
  text: `*${title}*\n\n${slackText}`,
};

const res = await fetch(webhookUrl, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(payload),
});

if (!res.ok) {
  console.error('Slack webhook failed:', res.status, await res.text());
  process.exit(1);
}

console.log('Triage sent to #customer-success');