n8n/.github/scripts/slack/notify.mjs
Declan Carroll 6eb4b4c1a5
ci: Unify Slack notifications across workflows (#30483)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 09:21:42 +00:00

98 lines
2.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Send a Slack chat.postMessage from a GitHub Actions step.
*
* Usage:
* node .github/scripts/slack/notify.mjs \
* --channel '#alerts-build' \
* --text 'CI failed on master'
*
* Rich blocks via a builder module under .github/scripts/slack/:
* node .github/scripts/slack/notify.mjs \
* --channel C0AHNJU9XFA \
* --text 'Trivy: 14 high vulns in nightly' \
* --blocks trivy \
* --results trivy-results.json \
* --image-ref $IMAGE_REF
*
* --blocks <name> resolves to ./build-<name>-blocks.mjs (default export receives the args).
* Builders read repo / run context from $GITHUB_* runner env vars.
*
* Bot token: $SLACK_TOKEN (required).
*
* Exits non-zero on any Slack API error so the step fails loudly.
*/
import { pathToFileURL } from 'node:url';
export async function sendSlackMessage({ token, channel, text, blocks }) {
const payload = { channel, text };
if (blocks) payload.blocks = blocks;
const res = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
const body = await res.json();
if (!body.ok) {
console.error('Slack chat.postMessage failed:');
console.error(JSON.stringify(body, null, 2));
process.exit(1);
}
console.log(`Slack message posted to ${body.channel} at ts=${body.ts}`);
return body;
}
function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (!a.startsWith('--')) continue;
const key = a.slice(2);
const next = argv[i + 1];
const value = next && !next.startsWith('--') ? next : 'true';
out[key] = value;
if (value !== 'true') i++;
}
return out;
}
function kebabToCamel(s) {
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const channel = args.channel;
const text = args.text;
const blocksName = args.blocks;
if (!channel) throw new Error('--channel is required');
if (!text) throw new Error('--text is required');
const token = process.env.SLACK_TOKEN;
if (!token) throw new Error('SLACK_TOKEN env var is required');
let blocks;
if (blocksName) {
const { default: builder } = await import(`./build-${blocksName}-blocks.mjs`);
const builderArgs = Object.fromEntries(
Object.entries(args)
.filter(([k]) => k !== 'channel' && k !== 'text' && k !== 'blocks')
.map(([k, v]) => [kebabToCamel(k), v]),
);
blocks = builder(builderArgs);
}
await sendSlackMessage({ token, channel, text, blocks });
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
await main();
}