From 6eb4b4c1a5e37f0b6d55d85b42551f6e02768729 Mon Sep 17 00:00:00 2001 From: Declan Carroll Date: Fri, 15 May 2026 10:21:42 +0100 Subject: [PATCH] ci: Unify Slack notifications across workflows (#30483) Co-authored-by: Claude Opus 4.7 --- .github/WORKFLOWS.md | 51 ++++++++- .github/scripts/package.json | 2 +- .github/scripts/slack/build-trivy-blocks.mjs | 103 ++++++++++++++++++ .../scripts/slack/build-trivy-blocks.test.mjs | 96 ++++++++++++++++ .github/scripts/slack/notify.mjs | 97 +++++++++++++++++ .github/scripts/slack/notify.test.mjs | 55 ++++++++++ .github/workflows/build-windows.yml | 46 ++------ .github/workflows/ci-detect-new-packages.yml | 23 ++-- .github/workflows/ci-master.yml | 18 +-- .github/workflows/docker-build-push.yml | 15 ++- .github/workflows/docker-build-smoke.yml | 16 ++- .github/workflows/release-create-minor-pr.yml | 17 +-- .github/workflows/release-create-patch-pr.yml | 17 +-- .../workflows/release-merge-tag-to-branch.yml | 25 +++-- .../workflows/sbom-generation-callable.yml | 15 ++- .github/workflows/sec-publish-fix-1x.yml | 24 ++-- .github/workflows/sec-publish-fix.yml | 12 +- .../security-trivy-scan-callable.yml | 100 ++--------------- .github/workflows/test-benchmark-nightly.yml | 17 ++- .../test-e2e-vm-expressions-nightly.yml | 17 ++- 20 files changed, 547 insertions(+), 219 deletions(-) create mode 100644 .github/scripts/slack/build-trivy-blocks.mjs create mode 100644 .github/scripts/slack/build-trivy-blocks.test.mjs create mode 100644 .github/scripts/slack/notify.mjs create mode 100644 .github/scripts/slack/notify.test.mjs diff --git a/.github/WORKFLOWS.md b/.github/WORKFLOWS.md index 8b93297f4a1..67021996f5d 100644 --- a/.github/WORKFLOWS.md +++ b/.github/WORKFLOWS.md @@ -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--blocks.mjs` whose default export returns a blocks array, then pass `--blocks ` 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` | diff --git a/.github/scripts/package.json b/.github/scripts/package.json index 2851b783238..5a631def70b 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -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", diff --git a/.github/scripts/slack/build-trivy-blocks.mjs b/.github/scripts/slack/build-trivy-blocks.mjs new file mode 100644 index 00000000000..66f7d977873 --- /dev/null +++ b/.github/scripts/slack/build-trivy-blocks.mjs @@ -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:'} ** (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, + }, + ], + }, + ]; +} diff --git a/.github/scripts/slack/build-trivy-blocks.test.mjs b/.github/scripts/slack/build-trivy-blocks.test.mjs new file mode 100644 index 00000000000..e5e5ca4fcd2 --- /dev/null +++ b/.github/scripts/slack/build-trivy-blocks.test.mjs @@ -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'); +}); diff --git a/.github/scripts/slack/notify.mjs b/.github/scripts/slack/notify.mjs new file mode 100644 index 00000000000..450a38df7c2 --- /dev/null +++ b/.github/scripts/slack/notify.mjs @@ -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 resolves to ./build--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(); +} diff --git a/.github/scripts/slack/notify.test.mjs b/.github/scripts/slack/notify.test.mjs new file mode 100644 index 00000000000..01d515688bc --- /dev/null +++ b/.github/scripts/slack/notify.test.mjs @@ -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); +}); diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 76e6271f3fa..4404601c065 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -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}" diff --git a/.github/workflows/ci-detect-new-packages.yml b/.github/workflows/ci-detect-new-packages.yml index a2eaa67b788..cb0501f3630 100644 --- a/.github/workflows/ci-detect-new-packages.yml +++ b/.github/workflows/ci-detect-new-packages.yml @@ -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" diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index f82ecea42fc..5369a0de78b 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -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 }})' diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index de43c3971fb..e1e7a64e8d7 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -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 }}' diff --git a/.github/workflows/docker-build-smoke.yml b/.github/workflows/docker-build-smoke.yml index 33fa0e6a1f4..ee054e533ff 100644 --- a/.github/workflows/docker-build-smoke.yml +++ b/.github/workflows/docker-build-smoke.yml @@ -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 }}' diff --git a/.github/workflows/release-create-minor-pr.yml b/.github/workflows/release-create-minor-pr.yml index 043296c2f79..938848c72bc 100644 --- a/.github/workflows/release-create-minor-pr.yml +++ b/.github/workflows/release-create-minor-pr.yml @@ -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.' diff --git a/.github/workflows/release-create-patch-pr.yml b/.github/workflows/release-create-patch-pr.yml index 40b4d318c6b..63bfef0d006 100644 --- a/.github/workflows/release-create-patch-pr.yml +++ b/.github/workflows/release-create-patch-pr.yml @@ -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.' diff --git a/.github/workflows/release-merge-tag-to-branch.yml b/.github/workflows/release-merge-tag-to-branch.yml index 528a5303f1d..6617ad6d143 100644 --- a/.github/workflows/release-merge-tag-to-branch.yml +++ b/.github/workflows/release-merge-tag-to-branch.yml @@ -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 }}>' diff --git a/.github/workflows/sbom-generation-callable.yml b/.github/workflows/sbom-generation-callable.yml index 0840ea31fb7..dab1177fe60 100644 --- a/.github/workflows/sbom-generation-callable.yml +++ b/.github/workflows/sbom-generation-callable.yml @@ -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 }}>' diff --git a/.github/workflows/sec-publish-fix-1x.yml b/.github/workflows/sec-publish-fix-1x.yml index 07ab5b8b586..31a2e7de40e 100644 --- a/.github/workflows/sec-publish-fix-1x.yml +++ b/.github/workflows/sec-publish-fix-1x.yml @@ -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 }})' diff --git a/.github/workflows/sec-publish-fix.yml b/.github/workflows/sec-publish-fix.yml index e18c142c75e..e7ba6bda5be 100644 --- a/.github/workflows/sec-publish-fix.yml +++ b/.github/workflows/sec-publish-fix.yml @@ -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 }})' diff --git a/.github/workflows/security-trivy-scan-callable.yml b/.github/workflows/security-trivy-scan-callable.yml index 3bf970924c0..369f47e5cc8 100644 --- a/.github/workflows/security-trivy-scan-callable.yml +++ b/.github/workflows/security-trivy-scan-callable.yml @@ -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) ** (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" diff --git a/.github/workflows/test-benchmark-nightly.yml b/.github/workflows/test-benchmark-nightly.yml index 54666993991..59184eda939 100644 --- a/.github/workflows/test-benchmark-nightly.yml +++ b/.github/workflows/test-benchmark-nightly.yml @@ -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 }}" diff --git a/.github/workflows/test-e2e-vm-expressions-nightly.yml b/.github/workflows/test-e2e-vm-expressions-nightly.yml index 50467d51bbb..d7786b56be2 100644 --- a/.github/workflows/test-e2e-vm-expressions-nightly.yml +++ b/.github/workflows/test-e2e-vm-expressions-nightly.yml @@ -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 }}'