ci: Unify Slack notifications across workflows (#30483)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Declan Carroll
2026-05-15 09:21:42 +00:00
committed by GitHub
co-authored by Claude Opus 4.7
parent dc72a8042c
commit 6eb4b4c1a5
20 changed files with 547 additions and 219 deletions
+49 -2
View File
@@ -455,6 +455,15 @@ Scripts in `.github/scripts/`:
| `validate-docs-links.js`| Check doc URLs | `util-check-docs-urls.yml`|
| `send-build-stats.mjs` | Build telemetry | `setup-nodejs` action |
### Slack Scripts
See [Slack Notifications](#slack-notifications) for the calling pattern.
| Script | Purpose |
|---------------------------------|-------------------------------------------------------------------------------|
| `slack/notify.mjs` | CLI + `sendSlackMessage` export. POSTs `chat.postMessage`, fails on `ok:false`. |
| `slack/build-trivy-blocks.mjs` | `--blocks trivy` — vulnerability digest |
---
## Telemetry
@@ -556,7 +565,7 @@ Supply chain security ensures artifacts haven't been tampered with. We provide t
- **Runs on:** stable/nightly/rc Docker builds
- **Scans:** n8n image, runners image
- **Output:** Slack `#notify-security-scan-outputs` (all), `#mission-security` (critical)
- **Output:** Slack `#updates-security` when vulnerabilities are detected
### SBOM
@@ -634,6 +643,44 @@ cosign verify-attestation --type openvex \
---
## Slack Notifications
All workflows post via `.github/scripts/slack/notify.mjs` — a direct `fetch` to `chat.postMessage` that exits non-zero on any Slack error. No third-party action; no silent swallowing.
```yaml
notify-on-failure:
runs-on: ubuntu-latest
needs: [build]
if: ${{ always() && contains(needs.*.result, 'failure') }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: .github/scripts/slack
sparse-checkout-cone-mode: false
- name: Notify Slack
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel '#alerts-build' \
--text 'Build failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
```
If notify is a step inside an existing checked-out job, skip the `checkout` and gate with step-level `if: failure()` instead.
> `if: failure()` at the **step** level of a dedicated notify job is a no-op when a `needs:` dependency fails (the job is skipped before steps evaluate). Always gate the **job** with `if: ${{ always() && contains(needs.*.result, 'failure') }}`.
**Rich payloads (Block Kit):** add `build-<name>-blocks.mjs` whose default export returns a blocks array, then pass `--blocks <name>` plus any workflow-specific args. Builders read repo / run context from `GITHUB_*` runner env vars. Kebab-case flags become camelCase keys for the builder (`--image-ref``imageRef`).
| Token | Bot | Channels |
|------------------------------|----------------|-------------------------------------------------------------|
| `QBOT_SLACK_TOKEN` | QBot | Default — engineering / build / security |
| `RELEASE_HELPER_SLACK_TOKEN` | Release Helper | `#releases` (C036AELNMV0) |
Adding a new channel requires inviting the bot first; the first run otherwise fails loudly with `not_in_channel`. Private-repo workflows (`sec-publish-fix*.yml`) need `QBOT_SLACK_TOKEN` set in `n8n-io/n8n-private`; the scripts themselves are mirrored by `sec-sync-public-to-private.yml`.
---
## Secrets
### By Category
@@ -641,7 +688,7 @@ cosign verify-attestation --type openvex \
| Category | Secrets |
|---------------------|-------------------------------------------------------------|
| Package Publishing | `NPM_TOKEN`, `DOCKER_USERNAME`, `DOCKER_PASSWORD` |
| Notifications | `SLACK_WEBHOOK_URL`, `QBOT_SLACK_TOKEN` |
| Notifications | `QBOT_SLACK_TOKEN`, `RELEASE_HELPER_SLACK_TOKEN` |
| Code Quality | `CODECOV_TOKEN`, `CHROMATIC_PROJECT_TOKEN`, `CURRENTS_RECORD_KEY` |
| Error Tracking | `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, `SENTRY_*_PROJECT` |
| Cloud/CDN | `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` |
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "workflow-scripts",
"scripts": {
"test": "node --test --experimental-test-module-mocks ./*.test.mjs ./quality/*.test.mjs"
"test": "node --test --experimental-test-module-mocks ./*.test.mjs ./quality/*.test.mjs ./slack/*.test.mjs"
},
"dependencies": {
"@actions/github": "9.0.0",
@@ -0,0 +1,103 @@
/**
* Build Block Kit blocks for the nightly Trivy vulnerability digest.
*
* Workflow-specific inputs (passed via notify.mjs flags):
* results — path to trivy-results.json
* imageRef — full image reference (ghcr.io/n8n-io/n8n:nightly)
*
* Repo / run context is read from the GitHub Actions runner env
* (GITHUB_REPOSITORY, GITHUB_SERVER_URL, GITHUB_RUN_ID). Pass `env` to
* override in tests.
*/
import { readFileSync } from 'node:fs';
const SEVERITY_RANK = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
const SEVERITY_EMOJI = {
CRITICAL: ':red_circle:',
HIGH: ':large_orange_circle:',
MEDIUM: ':large_yellow_circle:',
LOW: ':large_green_circle:',
};
const MAX_CVE_BLOCKS = 8;
export default function buildTrivyBlocks({ results, imageRef, env = process.env }) {
const repoName = env.GITHUB_REPOSITORY;
const repoUrl = `${env.GITHUB_SERVER_URL}/${repoName}`;
const runUrl = `${repoUrl}/actions/runs/${env.GITHUB_RUN_ID}`;
const report = JSON.parse(readFileSync(results, 'utf8'));
const allVulns = (report.Results ?? [])
.flatMap((r) => r.Vulnerabilities ?? [])
.filter((v) => v && v.VulnerabilityID);
const seen = new Set();
const uniqueVulns = [];
for (const v of allVulns) {
if (seen.has(v.VulnerabilityID)) continue;
seen.add(v.VulnerabilityID);
uniqueVulns.push(v);
}
const counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
for (const v of uniqueVulns) {
if (v.Severity in counts) counts[v.Severity]++;
}
const cvssOf = (v) => v?.CVSS?.nvd?.V3Score ?? 0;
uniqueVulns.sort((a, b) => {
const sevDiff = (SEVERITY_RANK[a.Severity] ?? 99) - (SEVERITY_RANK[b.Severity] ?? 99);
if (sevDiff !== 0) return sevDiff;
return cvssOf(b) - cvssOf(a);
});
const cveBlock = (v) => ({
type: 'section',
text: {
type: 'mrkdwn',
text: [
`${SEVERITY_EMOJI[v.Severity] ?? ':white_circle:'} *<https://nvd.nist.gov/vuln/detail/${v.VulnerabilityID}|${v.VulnerabilityID}>* (CVSS: \`${v.CVSS?.nvd?.V3Score ?? 'N/A'}\`)`,
`*Package:* \`${v.PkgName}@${v.InstalledVersion}\`\`${v.FixedVersion ?? 'No fix available'}\``,
].join('\n'),
},
});
return [
{
type: 'header',
text: { type: 'plain_text', text: ':warning: Trivy Scan: Vulnerabilities Detected' },
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Repository:*\n<${repoUrl}|${repoName}>` },
{ type: 'mrkdwn', text: `*Image:*\n\`${imageRef}\`` },
{ type: 'mrkdwn', text: `*Critical:*\n:red_circle: ${counts.CRITICAL}` },
{ type: 'mrkdwn', text: `*High:*\n:large_orange_circle: ${counts.HIGH}` },
{ type: 'mrkdwn', text: `*Medium:*\n:large_yellow_circle: ${counts.MEDIUM}` },
{ type: 'mrkdwn', text: `*Low:*\n:large_green_circle: ${counts.LOW}` },
],
},
{
type: 'context',
elements: [
{ type: 'mrkdwn', text: `:shield: ${uniqueVulns.length} unique CVEs affecting packages` },
],
},
{ type: 'divider' },
...uniqueVulns.slice(0, MAX_CVE_BLOCKS).map(cveBlock),
{ type: 'divider' },
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: ':github: View Full Report' },
style: 'primary',
url: runUrl,
},
],
},
];
}
@@ -0,0 +1,96 @@
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import buildTrivyBlocks from './build-trivy-blocks.mjs';
const ENV = {
GITHUB_REPOSITORY: 'n8n-io/n8n',
GITHUB_SERVER_URL: 'https://github.com',
GITHUB_RUN_ID: '1',
};
function build(report) {
const dir = mkdtempSync(join(tmpdir(), 'trivy-blocks-'));
const path = join(dir, 'trivy-results.json');
writeFileSync(path, JSON.stringify(report));
return buildTrivyBlocks({
results: path,
imageRef: 'ghcr.io/n8n-io/n8n:nightly',
env: ENV,
});
}
test('counts vulnerabilities by severity', () => {
const blocks = build({
Results: [{
Vulnerabilities: [
{ VulnerabilityID: 'CVE-1', Severity: 'CRITICAL', PkgName: 'a', InstalledVersion: '1' },
{ VulnerabilityID: 'CVE-2', Severity: 'HIGH', PkgName: 'b', InstalledVersion: '2' },
{ VulnerabilityID: 'CVE-3', Severity: 'HIGH', PkgName: 'c', InstalledVersion: '3' },
{ VulnerabilityID: 'CVE-4', Severity: 'MEDIUM', PkgName: 'd', InstalledVersion: '4' },
{ VulnerabilityID: 'CVE-5', Severity: 'LOW', PkgName: 'e', InstalledVersion: '5' },
],
}],
});
const counts = blocks.find((b) => b.type === 'section' && b.fields);
const fields = Object.fromEntries(
counts.fields.map((f) => f.text.split('\n')).map(([k, v]) => [k, v]),
);
assert.equal(fields['*Critical:*'], ':red_circle: 1');
assert.equal(fields['*High:*'], ':large_orange_circle: 2');
assert.equal(fields['*Medium:*'], ':large_yellow_circle: 1');
assert.equal(fields['*Low:*'], ':large_green_circle: 1');
});
test('dedupes vulnerabilities by CVE id', () => {
const blocks = build({
Results: [{
Vulnerabilities: [
{ VulnerabilityID: 'CVE-1', Severity: 'HIGH', PkgName: 'a', InstalledVersion: '1' },
{ VulnerabilityID: 'CVE-1', Severity: 'HIGH', PkgName: 'a', InstalledVersion: '1' },
{ VulnerabilityID: 'CVE-2', Severity: 'HIGH', PkgName: 'b', InstalledVersion: '2' },
],
}],
});
const ctx = blocks.find((b) => b.type === 'context');
assert.match(ctx.elements[0].text, /2 unique CVEs/);
});
test('sorts by severity then CVSS', () => {
const blocks = build({
Results: [{
Vulnerabilities: [
{ VulnerabilityID: 'CVE-LOW-1', Severity: 'LOW', PkgName: 'a', InstalledVersion: '1' },
{ VulnerabilityID: 'CVE-HIGH-LOWCVSS', Severity: 'HIGH', PkgName: 'b', InstalledVersion: '2', CVSS: { nvd: { V3Score: 5 } } },
{ VulnerabilityID: 'CVE-HIGH-HIGHCVSS', Severity: 'HIGH', PkgName: 'c', InstalledVersion: '3', CVSS: { nvd: { V3Score: 9 } } },
{ VulnerabilityID: 'CVE-CRIT-1', Severity: 'CRITICAL', PkgName: 'd', InstalledVersion: '4' },
],
}],
});
const cveSections = blocks.filter((b) => b.type === 'section' && b.text?.type === 'mrkdwn');
const ids = cveSections.map((b) => b.text.text.match(/CVE-[A-Z0-9-]+/)?.[0]);
assert.deepEqual(ids, ['CVE-CRIT-1', 'CVE-HIGH-HIGHCVSS', 'CVE-HIGH-LOWCVSS', 'CVE-LOW-1']);
});
test('caps CVE detail blocks at 8', () => {
const vulns = Array.from({ length: 20 }, (_, i) => ({
VulnerabilityID: `CVE-${i}`,
Severity: 'HIGH',
PkgName: `p${i}`,
InstalledVersion: '1',
}));
const blocks = build({ Results: [{ Vulnerabilities: vulns }] });
const cveSections = blocks.filter((b) => b.type === 'section' && b.text?.type === 'mrkdwn');
assert.equal(cveSections.length, 8);
});
test('emits view-report button with run url from GH env', () => {
const blocks = build({ Results: [{ Vulnerabilities: [
{ VulnerabilityID: 'CVE-1', Severity: 'HIGH', PkgName: 'a', InstalledVersion: '1' },
] }] });
const actions = blocks.find((b) => b.type === 'actions');
assert.equal(actions.elements[0].url, 'https://github.com/n8n-io/n8n/actions/runs/1');
});
+97
View File
@@ -0,0 +1,97 @@
#!/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();
}
+55
View File
@@ -0,0 +1,55 @@
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
import { sendSlackMessage } from './notify.mjs';
function mockFetch(responseBody, status = 200) {
return mock.method(globalThis, 'fetch', async () => ({
status,
json: async () => responseBody,
}));
}
test('posts channel + text to chat.postMessage', async (t) => {
const fetchMock = mockFetch({ ok: true, channel: 'C123', ts: '1.0' });
t.after(() => fetchMock.mock.restore());
await sendSlackMessage({ token: 'xoxb-test', channel: '#x', text: 'hi' });
const [url, init] = fetchMock.mock.calls[0].arguments;
assert.equal(url, 'https://slack.com/api/chat.postMessage');
assert.equal(init.method, 'POST');
assert.equal(init.headers.Authorization, 'Bearer xoxb-test');
assert.equal(init.headers['Content-Type'], 'application/json; charset=utf-8');
assert.deepEqual(JSON.parse(init.body), { channel: '#x', text: 'hi' });
});
test('includes blocks when provided', async (t) => {
const fetchMock = mockFetch({ ok: true, channel: 'C123', ts: '1.0' });
t.after(() => fetchMock.mock.restore());
const blocks = [{ type: 'section', text: { type: 'mrkdwn', text: 'x' } }];
await sendSlackMessage({ token: 't', channel: '#x', text: 'hi', blocks });
const [, init] = fetchMock.mock.calls[0].arguments;
assert.deepEqual(JSON.parse(init.body), { channel: '#x', text: 'hi', blocks });
});
test('exits non-zero on Slack ok:false', async (t) => {
const fetchMock = mockFetch({ ok: false, error: 'not_in_channel' });
const exitMock = mock.method(process, 'exit', () => {
throw new Error('exit called');
});
const errMock = mock.method(console, 'error', () => {});
t.after(() => {
fetchMock.mock.restore();
exitMock.mock.restore();
errMock.mock.restore();
});
await assert.rejects(
sendSlackMessage({ token: 't', channel: '#x', text: 'hi' }),
/exit called/,
);
assert.equal(exitMock.mock.calls[0].arguments[0], 1);
});
+7 -39
View File
@@ -55,42 +55,10 @@ jobs:
- name: Send Slack notification on failure
if: failure() && inputs.notify_on_failure == true
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
with:
method: chat.postMessage
token: ${{ secrets.QBOT_SLACK_TOKEN }}
payload: |
{
"channel": "C035KBDA917",
"text": "🚨 Windows build failed for `${{ github.repository }}` on branch `${{ github.ref_name }}`",
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "🚨 Windows Build Failed" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Repository:*\n<${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}>" },
{ "type": "mrkdwn", "text": "*Branch:*\n`${{ github.ref_name }}`" },
{ "type": "mrkdwn", "text": "*Commit:*\n`${{ github.sha }}`" },
{ "type": "mrkdwn", "text": "*Trigger:*\n${{ github.event_name }}" }
]
},
{
"type": "section",
"text": { "type": "mrkdwn", "text": ":warning: *Cross-platform compatibility issue detected*\nThis likely indicates Unix-specific commands in package.json scripts or build configuration that don't work on Windows." }
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": ":github: View Workflow Run" },
"style": "danger",
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
]
}
]
}
shell: bash
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel C035KBDA917 \
--text "🚨 Windows build failed for \`$GITHUB_REPOSITORY\` on \`$GITHUB_REF_NAME\` (\`$GITHUB_SHA\`) — ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
+10 -13
View File
@@ -29,19 +29,16 @@ jobs:
- name: Notify Slack about new packages
if: steps.detect.outcome == 'failure' && steps.detect.outputs.packages != ''
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
with:
method: chat.postMessage
token: ${{ secrets.RELEASE_HELPER_SLACK_TOKEN }}
payload: |
channel: C036AELNMV0
text: |-
:warning: *New unpublished packages detected* after merging <${{ github.event.pull_request.html_url }}|PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}>
env:
SLACK_TOKEN: ${{ secrets.RELEASE_HELPER_SLACK_TOKEN }}
MESSAGE: |-
:warning: *New unpublished packages detected* after merging <${{ github.event.pull_request.html_url }}|PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}>
The following packages do not exist on npm yet: `${{ steps.detect.outputs.packages }}`
The following packages do not exist on npm yet: `${{ steps.detect.outputs.packages }}`
*If a package is not intended for npm*, set `"private": true` in its `package.json` to exclude it from future checks.
*If a package is not intended for npm*, set `"private": true` in its `package.json` to exclude it from future checks.
*Otherwise, to unblock the next release:*
1. Run the <${{ github.server_url }}/${{ github.repository }}/actions/workflows/release-publish-new-package.yml|Release: Publish New Package> workflow for each package
2. Configure Trusted Publishing on npmjs.com (owner: `n8n-io`, repo: `n8n`, workflow: `release-publish.yml`)
*Otherwise, to unblock the next release:*
1. Run the <${{ github.server_url }}/${{ github.repository }}/actions/workflows/release-publish-new-package.yml|Release: Publish New Package> workflow for each package
2. Configure Trusted Publishing on npmjs.com (owner: `n8n-io`, repo: `n8n`, workflow: `release-publish.yml`)
run: node .github/scripts/slack/notify.mjs --channel C036AELNMV0 --text "$MESSAGE"
+11 -7
View File
@@ -51,12 +51,16 @@ jobs:
name: Notify Slack on failure
runs-on: ubuntu-latest
needs: [unit-test, lint, performance, build-github]
if: ${{ always() && contains(needs.*.result, 'failure') }}
steps:
- name: Notify Slack on failure
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: failure()
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
status: ${{ job.status }}
channel: '#alerts-build'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: ${{ github.ref_name }} branch (build or test or lint) failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
sparse-checkout: .github/scripts/slack
sparse-checkout-cone-mode: false
- name: Notify Slack
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel '#alerts-build' \
--text '${{ github.ref_name }} branch (build or test or lint) failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
+10 -5
View File
@@ -453,9 +453,14 @@ jobs:
needs: [build-and-push-docker]
if: needs.build-and-push-docker.result == 'failure' && github.event_name == 'schedule'
steps:
- uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
status: ${{ needs.build-and-push-docker.result }}
channel: '#team-catalysts'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: Nightly Docker build failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
sparse-checkout: .github/scripts/slack
sparse-checkout-cone-mode: false
- name: Notify Slack
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel '#team-catalysts' \
--text 'Nightly Docker build failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
+10 -6
View File
@@ -55,10 +55,14 @@ jobs:
needs: [docker-smoke-test]
if: needs.docker-smoke-test.result == 'failure' && github.event_name == 'schedule'
steps:
- uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
method: chat.postMessage
token: ${{ secrets.QBOT_SLACK_TOKEN }}
payload: |
channel: C0A9RLY8Y20
text: "🚨 Nightly Docker smoke test failed (no-cache build) - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
sparse-checkout: .github/scripts/slack
sparse-checkout-cone-mode: false
- name: Notify Slack
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel C0A9RLY8Y20 \
--text '🚨 Nightly Docker smoke test failed (no-cache build) - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
+10 -7
View File
@@ -20,11 +20,14 @@ jobs:
if: needs.create-release-pr.result == 'success' && needs.create-release-pr.outputs.pull-request-number != ''
runs-on: ubuntu-latest
steps:
- name: Post to Slack
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
method: chat.postMessage
token: ${{ secrets.RELEASE_HELPER_SLACK_TOKEN }}
payload: |
channel: C036AELNMV0
text: ":rocket: Minor release PR created. <${{ github.server_url }}/${{ github.repository }}/pull/${{ needs.create-release-pr.outputs.pull-request-number }}|View PR> — close it to cancel the release."
sparse-checkout: .github/scripts/slack
sparse-checkout-cone-mode: false
- name: Notify Slack
env:
SLACK_TOKEN: ${{ secrets.RELEASE_HELPER_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel C036AELNMV0 \
--text ':rocket: Minor release PR created. <${{ github.server_url }}/${{ github.repository }}/pull/${{ needs.create-release-pr.outputs.pull-request-number }}|View PR> — close it to cancel the release.'
+10 -7
View File
@@ -67,11 +67,14 @@ jobs:
if: needs.create-release-pr.result == 'success' && needs.create-release-pr.outputs.pull-request-number != ''
runs-on: ubuntu-latest
steps:
- name: Post to Slack
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
method: chat.postMessage
token: ${{ secrets.RELEASE_HELPER_SLACK_TOKEN }}
payload: |
channel: C036AELNMV0
text: ":rocket: Patch release PR created for *${{ inputs.track }}* track. <${{ github.server_url }}/${{ github.repository }}/pull/${{ needs.create-release-pr.outputs.pull-request-number }}|View PR> — close it to cancel the release."
sparse-checkout: .github/scripts/slack
sparse-checkout-cone-mode: false
- name: Notify Slack
env:
SLACK_TOKEN: ${{ secrets.RELEASE_HELPER_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel C036AELNMV0 \
--text ':rocket: Patch release PR created for *${{ inputs.track }}* track. <${{ github.server_url }}/${{ github.repository }}/pull/${{ needs.create-release-pr.outputs.pull-request-number }}|View PR> — close it to cancel the release.'
@@ -55,12 +55,21 @@ jobs:
- name: Push to ${{ inputs.target-branch }}
run: git push origin "HEAD:${TARGET_BRANCH}"
- name: Notify Slack on failure
if: failure()
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
notify-on-failure:
name: Notify Slack on failure
needs: [merge-tag-to-branch]
if: ${{ always() && needs.merge-tag-to-branch.result == 'failure' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
status: ${{ job.status }}
channel: '#updates-and-product-releases'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: |
<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}| Release tag merge to ${{ inputs.target-branch }} failed for n8n@${{ inputs.version }} >
ref: master
sparse-checkout: .github/scripts/slack
sparse-checkout-cone-mode: false
- name: Notify Slack
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel '#updates-and-product-releases' \
--text '<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Release tag merge to ${{ inputs.target-branch }} failed for n8n@${{ inputs.version }}>'
@@ -12,7 +12,7 @@ on:
required: true
type: string
secrets:
SLACK_WEBHOOK_URL:
QBOT_SLACK_TOKEN:
required: true
workflow_dispatch:
@@ -80,10 +80,9 @@ jobs:
- name: Notify Slack on failure
if: failure()
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
with:
status: ${{ job.status }}
channel: '#alerts-build'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: |
<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}| SBOM generation and attachment failed for release ${{ inputs.release_tag_ref }} >
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel '#alerts-build' \
--text '<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|SBOM generation and attachment failed for release ${{ inputs.release_tag_ref }}>'
+17 -7
View File
@@ -51,11 +51,21 @@ jobs:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
- name: Notify on failure
if: failure()
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
notify-on-failure:
name: Notify Slack on failure
needs: [sync-security-fix]
if: ${{ always() && needs.sync-security-fix.result == 'failure' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
status: ${{ job.status }}
channel: '#alerts-security'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: 'Security fix PR creation failed (1.x). Run "Security: Sync from Public" workflow, rebase your branch, reopen PR. (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
ref: master
sparse-checkout: .github/scripts/slack
sparse-checkout-cone-mode: false
- name: Notify Slack
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel '#alerts-security' \
--text 'Security fix PR creation failed (1.x). Run "Security: Sync from Public" workflow, rebase your branch, reopen PR. (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
+6 -6
View File
@@ -53,9 +53,9 @@ jobs:
- name: Notify on failure
if: failure()
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
with:
status: ${{ job.status }}
channel: '#alerts-security'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: 'Security fix PR creation failed. Run "Security: Sync from Public" workflow, rebase your branch, reopen PR. (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel '#alerts-security' \
--text 'Security fix PR creation failed. Run "Security: Sync from Public" workflow, rebase your branch, reopen PR. (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
@@ -21,7 +21,6 @@ permissions:
contents: read
env:
QBOT_SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
SLACK_CHANNEL_ID: C0AHNJU9XFA #updates-security
jobs:
@@ -37,6 +36,7 @@ jobs:
security/trivy.yaml
security/trivy-ignore-policy.rego
.github/scripts/retry.mjs
.github/scripts/slack
sparse-checkout-cone-mode: false
- name: Pull Docker image with retry
@@ -166,98 +166,16 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Generate Slack Blocks JSON
- name: Send Slack notification
if: steps.process_results.outputs.vulnerabilities_found == 'true'
id: generate_blocks
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
IMAGE_REF: ${{ inputs.image_ref }}
run: |
BLOCKS_JSON=$(jq -c --arg image_ref "$IMAGE_REF" \
--arg repo_url "${{ github.server_url }}/${{ github.repository }}" \
--arg repo_name "${{ github.repository }}" \
--arg run_url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--arg critical_count "${{ steps.process_results.outputs.critical_count }}" \
--arg high_count "${{ steps.process_results.outputs.high_count }}" \
--arg medium_count "${{ steps.process_results.outputs.medium_count }}" \
--arg low_count "${{ steps.process_results.outputs.low_count }}" \
--arg unique_cves "${{ steps.process_results.outputs.unique_cves }}" \
'
# Function to create a vulnerability block with emoji indicators
def vuln_block: {
"type": "section",
"text": {
"type": "mrkdwn",
"text": "\(if .Severity == "CRITICAL" then ":red_circle:" elif .Severity == "HIGH" then ":large_orange_circle:" elif .Severity == "MEDIUM" then ":large_yellow_circle:" else ":large_green_circle:" end) *<https://nvd.nist.gov/vuln/detail/\(.VulnerabilityID)|\(.VulnerabilityID)>* (CVSS: `\(.CVSS.nvd.V3Score // "N/A")`)\n*Package:* `\(.PkgName)@\(.InstalledVersion)` → `\(.FixedVersion // "No fix available")`"
}
};
# Main structure
[
{
"type": "header",
"text": { "type": "plain_text", "text": ":warning: Trivy Scan: Vulnerabilities Detected" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Repository:*\n<\($repo_url)|\($repo_name)>" },
{ "type": "mrkdwn", "text": "*Image:*\n`\($image_ref)`" },
{ "type": "mrkdwn", "text": "*Critical:*\n:red_circle: \($critical_count)" },
{ "type": "mrkdwn", "text": "*High:*\n:large_orange_circle: \($high_count)" },
{ "type": "mrkdwn", "text": "*Medium:*\n:large_yellow_circle: \($medium_count)" },
{ "type": "mrkdwn", "text": "*Low:*\n:large_green_circle: \($low_count)" }
]
},
{
"type": "context",
"elements": [
{ "type": "mrkdwn", "text": ":shield: \($unique_cves) unique CVEs affecting packages" }
]
},
{ "type": "divider" }
] +
(
# Group vulnerabilities by CVE to avoid duplicates in notification
[.Results[] | select(.Vulnerabilities != null) | .Vulnerabilities[]] |
group_by(.VulnerabilityID) |
map(.[0]) |
sort_by(
(if .Severity == "CRITICAL" then 0
elif .Severity == "HIGH" then 1
elif .Severity == "MEDIUM" then 2
elif .Severity == "LOW" then 3
else 4 end),
-((.CVSS.nvd.V3Score // 0) | tonumber? // 0)
) |
.[:8] |
map(. | vuln_block)
) +
[
{ "type": "divider" },
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": ":github: View Full Report" },
"style": "primary",
"url": $run_url
}
]
}
]
' trivy-results.json)
echo "slack_blocks=$BLOCKS_JSON" >> "$GITHUB_OUTPUT"
- name: Send Slack Notification
if: steps.process_results.outputs.vulnerabilities_found == 'true'
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
with:
method: chat.postMessage
token: ${{ secrets.QBOT_SLACK_TOKEN }}
payload: |
channel: ${{ env.SLACK_CHANNEL_ID }}
text: "🚨 Trivy Scan: ${{ steps.process_results.outputs.critical_count }} Critical, ${{ steps.process_results.outputs.high_count }} High, ${{ steps.process_results.outputs.medium_count }} Medium, ${{ steps.process_results.outputs.low_count }} Low vulnerabilities found in ${{ inputs.image_ref }}"
blocks: ${{ steps.generate_blocks.outputs.slack_blocks }}
node .github/scripts/slack/notify.mjs \
--channel "$SLACK_CHANNEL_ID" \
--text "🚨 Trivy Scan: ${{ steps.process_results.outputs.critical_count }} Critical, ${{ steps.process_results.outputs.high_count }} High, ${{ steps.process_results.outputs.medium_count }} Medium, ${{ steps.process_results.outputs.low_count }} Low vulnerabilities found in $IMAGE_REF" \
--blocks trivy \
--results trivy-results.json \
--image-ref "$IMAGE_REF"
+11 -6
View File
@@ -105,11 +105,16 @@ jobs:
name: Notify Cats on failure
runs-on: ubuntu-latest
needs: [build]
if: failure()
if: ${{ always() && contains(needs.*.result, 'failure') }}
steps:
- uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
status: ${{ job.status }}
channel: '#team-catalysts'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: Benchmark run failed for n8n tag `${{ inputs.n8n_tag || 'nightly' }}` - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
sparse-checkout: .github/scripts/slack
sparse-checkout-cone-mode: false
- name: Notify Slack
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel '#team-catalysts' \
--text "Benchmark run failed for n8n tag \`${{ inputs.n8n_tag || 'nightly' }}\` - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
@@ -36,11 +36,16 @@ jobs:
name: Notify Slack on failure
runs-on: ubuntu-slim
needs: [e2e]
if: failure() && github.event_name == 'schedule'
if: ${{ always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }}
steps:
- uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
status: ${{ job.status }}
channel: '#project-secure-expression-evaluation'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: "VM expression E2E nightly failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
sparse-checkout: .github/scripts/slack
sparse-checkout-cone-mode: false
- name: Notify Slack
env:
SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
run: |
node .github/scripts/slack/notify.mjs \
--channel '#project-secure-expression-evaluation' \
--text 'VM expression E2E nightly failed - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'