diff --git a/.github/slack.yml b/.github/slack.yml index 0b4b8db3..703af7e1 100644 --- a/.github/slack.yml +++ b/.github/slack.yml @@ -22,6 +22,9 @@ fields: - title: Job Steps value: "{{#each jobSteps}}{{icon this.outcome}} {{@key}}\n{{/each}}" short: false + - title: Job Matrix + value: "{{#each jobMatrix}}{{@key}}: {{this}}\n{{/each}}" + short: false - title: Workflow value: "<{{{workflowUrl}}}|{{workflow}}>" short: true diff --git a/README.md b/README.md index 2b480dee..5bcc7c55 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,15 @@ message using: **Note: Only steps that have a "step id" will be reported on. See example below.** +#### `matrix` (optional) +Parameters for [matrix jobs](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs) can be included in Slack messages: + + with: + status: ${{ job.status }} + matrix: ${{ toJson(matrix) }} + + + #### `channel` (optional) To override the channel or to send the Slack message to an individual @@ -116,7 +125,7 @@ The following Slack [message fields](https://api.slack.com/reference/messaging/a **Supported Template variables** -`env.*`, `payload.*`, `jobName`, `jobStatus`, `jobSteps`, +`env.*`, `payload.*`, `jobName`, `jobStatus`, `jobSteps`, `jobMatrix`, `eventName`, `workflow`, `workflowUrl`, `workflowRunUrl`, `repositoryName`, `repositoryUrl`, `runId`, `runNumber`, `sha`, `shortSha`, `branch`, `actor`, `action`, `ref`, `refType`, `refUrl`, `diffRef`, `diffUrl`, `description`, `sender` **Helper Functions** @@ -166,6 +175,9 @@ fields: - title: Job Steps value: "{{#each jobSteps}}{{icon this.outcome}} {{@key}}\n{{/each}}" short: false + - title: Job Matrix + value: "{{#each jobMatrix}}{{@key}}: {{this}}\n{{/each}}" + short: false - title: Workflow value: "<{{workflowUrl}}|{{workflow}}>" short: true diff --git a/__tests__/blocks.test.ts b/__tests__/blocks.test.ts index ffb4c236..7953995c 100644 --- a/__tests__/blocks.test.ts +++ b/__tests__/blocks.test.ts @@ -40,6 +40,7 @@ const jobSteps = { conclusion: 'failure' } } +const jobMatrix = {} const channel = '#github-ci' // mock github context @@ -92,7 +93,7 @@ test('custom config of slack action using legacy and blocks', async () => { schema: yaml.FAILSAFE_SCHEMA }) as ConfigOptions - let res = await send(url, jobName, jobStatus, jobSteps, channel, message, config) + let res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message, config) await expect(res).toStrictEqual({text: {status: 'ok'}}) expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ diff --git a/__tests__/config.test.ts b/__tests__/config.test.ts index 78bb45f7..9d517d62 100644 --- a/__tests__/config.test.ts +++ b/__tests__/config.test.ts @@ -40,6 +40,7 @@ const jobSteps = { conclusion: 'failure' } } +const jobMatrix = {} const channel = '#github-ci' // mock github context @@ -92,7 +93,7 @@ test('custom config of slack action using legacy attachments', async () => { schema: yaml.FAILSAFE_SCHEMA }) as ConfigOptions - let res = await send(url, jobName, jobStatus, jobSteps, channel, message, config) + let res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message, config) await expect(res).toStrictEqual({text: {status: 'ok'}}) expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ diff --git a/__tests__/inputs.test.ts b/__tests__/inputs.test.ts index 4cc518cb..fe9a9b52 100644 --- a/__tests__/inputs.test.ts +++ b/__tests__/inputs.test.ts @@ -40,6 +40,7 @@ const jobSteps = { conclusion: 'cancelled' } } +const jobMatrix = {} const channel = '#deploy' let message = 'Successfully deployed to {{ env.ENVIRONMENT }}!' @@ -92,7 +93,7 @@ test('custom config of slack action using inputs for channel and message', async schema: yaml.FAILSAFE_SCHEMA }) as ConfigOptions - let res = await send(url, jobName, jobStatus, jobSteps, channel, message, config) + let res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message, config) await expect(res).toStrictEqual({text: {status: 'ok'}}) expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ diff --git a/__tests__/job_matrix.test.ts b/__tests__/job_matrix.test.ts new file mode 100644 index 00000000..8a4e1b35 --- /dev/null +++ b/__tests__/job_matrix.test.ts @@ -0,0 +1,94 @@ +import * as github from '@actions/github' +import axios from 'axios' +import MockAdapter from 'axios-mock-adapter' +import {send} from '../src/slack' +import {readFileSync} from 'fs' + +const url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX' +const jobName = 'Build and Test' +const jobStatus = 'Success' +const jobSteps = {} +const jobMatrix = { + name1: 'value1', + name2: 'value2' +} +const channel = '@override' +const message = undefined + +// mock github context +const dump = JSON.parse(readFileSync('./__tests__/fixtures/push.json', 'utf-8')) + +github.context.payload = dump.event +github.context.eventName = dump.event_name +github.context.sha = dump.sha +github.context.ref = dump.ref +github.context.workflow = dump.workflow +github.context.action = dump.action +github.context.actor = dump.actor + +process.env.CI = 'true' +process.env.GITHUB_WORKFLOW = 'build-test' +process.env.GITHUB_RUN_ID = '100143423' +process.env.GITHUB_RUN_NUMBER = '8' +process.env.GITHUB_ACTION = 'self2' +process.env.GITHUB_ACTIONS = 'true' +process.env.GITHUB_ACTOR = 'satterly' +process.env.GITHUB_REPOSITORY = 'act10ns/slack' +process.env.GITHUB_EVENT_NAME = 'push' +process.env.GITHUB_EVENT_PATH = '/home/runner/work/_temp/_github_workflow/event.json' +process.env.GITHUB_WORKSPACE = '/home/runner/work/slack/slack' +process.env.GITHUB_SHA = '68d48876e0794fba714cb331a1624af6b20942d8' +process.env.GITHUB_REF = 'refs/heads/master' +process.env.GITHUB_HEAD_REF = '' +process.env.GITHUB_BASE_REF = '' +process.env.GITHUB_SERVER_URL = 'https://github.com' +process.env.GITHUB_API_URL = 'https://github.com' +process.env.GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql' + +test('push event to slack', async () => { + const mockAxios = new MockAdapter(axios, {delayResponse: 200}) + + mockAxios + .onPost() + .reply(config => { + console.log(config.data) + return [200, {status: 'ok'}] + }) + .onAny() + .reply(500) + + const res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message) + await expect(res).toStrictEqual({text: {status: 'ok'}}) + + expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ + username: 'GitHub Actions', + icon_url: 'https://octodex.github.com/images/original.png', + channel: '@override', + attachments: [ + { + fallback: '[GitHub]: [act10ns/slack] build-test push Success', + color: 'good', + author_name: 'satterly', + author_link: 'https://github.com/satterly', + author_icon: 'https://avatars0.githubusercontent.com/u/615057?v=4', + mrkdwn_in: ['pretext', 'text', 'fields'], + pretext: '', + text: '** for \n - 4 commits', + title: '', + fields: [ + { + title: 'Job Matrix', + value: 'name1: value1\nname2: value2\n', + short: false + } + ], + footer: ' #8', + footer_icon: 'https://github.githubassets.com/favicon.ico', + ts: expect.stringMatching(/[0-9]+/) + } + ] + }) + + mockAxios.resetHistory() + mockAxios.reset() +}) diff --git a/__tests__/job_status.test.ts b/__tests__/job_status.test.ts index 0712b123..abf12a5e 100644 --- a/__tests__/job_status.test.ts +++ b/__tests__/job_status.test.ts @@ -39,6 +39,7 @@ const jobSteps = { conclusion: 'skipped' } } +const jobMatrix = {} const channel = '#github-ci' const message = undefined @@ -86,7 +87,7 @@ test('push event to slack', async () => { const config: ConfigOptions = {} - const res = await send(url, jobName, jobStatus, jobSteps, channel, message, config) + const res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message, config) await expect(res).toStrictEqual({text: {status: 'ok'}}) expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ diff --git a/__tests__/pull_request.test.ts b/__tests__/pull_request.test.ts index 7b6e24a1..27c1d5c9 100644 --- a/__tests__/pull_request.test.ts +++ b/__tests__/pull_request.test.ts @@ -8,6 +8,7 @@ const url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXX const jobName = 'Build and Test' const jobStatus = 'Success' const jobSteps = {} +const jobMatrix = {} const channel = '@override' const message = undefined @@ -53,7 +54,7 @@ test('pull request event to slack', async () => { .onAny() .reply(500) - const res = await send(url, jobName, jobStatus, jobSteps, channel, message) + const res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message) await expect(res).toStrictEqual({text: {status: 'ok'}}) expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ diff --git a/__tests__/push.test.ts b/__tests__/push.test.ts index 00c19bbb..2cf1d846 100644 --- a/__tests__/push.test.ts +++ b/__tests__/push.test.ts @@ -8,6 +8,7 @@ const url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXX const jobName = 'Build and Test' const jobStatus = 'Success' const jobSteps = {} +const jobMatrix = {} const channel = '@override' const message = undefined @@ -53,7 +54,7 @@ test('push event to slack', async () => { .onAny() .reply(500) - const res = await send(url, jobName, jobStatus, jobSteps, channel, message) + const res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message) await expect(res).toStrictEqual({text: {status: 'ok'}}) expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ diff --git a/__tests__/release.test.ts b/__tests__/release.test.ts index 8bec9f4c..6e416548 100644 --- a/__tests__/release.test.ts +++ b/__tests__/release.test.ts @@ -8,6 +8,7 @@ const url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXX const jobName = 'Build and Test' const jobStatus = 'Success' const jobSteps = {} +const jobMatrix = {} const channel = '@override' const message = undefined @@ -56,7 +57,7 @@ test('release event to slack', async () => { .onAny() .reply(500) - const res = await send(url, jobName, jobStatus, jobSteps, channel, message) + const res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message) await expect(res).toStrictEqual({text: {status: 'ok'}}) expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ diff --git a/__tests__/schedule.test.ts b/__tests__/schedule.test.ts index 2871af84..a317966e 100644 --- a/__tests__/schedule.test.ts +++ b/__tests__/schedule.test.ts @@ -8,6 +8,7 @@ const url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXX const jobName = 'Build and Test' const jobStatus = 'Success' const jobSteps = {} +const jobMatrix = {} const channel = '@override' const message = undefined @@ -49,7 +50,7 @@ test('schedule event to slack', async () => { .onAny() .reply(500) - const res = await send(url, jobName, jobStatus, jobSteps, channel, message) + const res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message) await expect(res).toStrictEqual({text: {status: 'ok'}}) expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ diff --git a/__tests__/workflow_dispatch.test.ts b/__tests__/workflow_dispatch.test.ts index 1991620a..9ce0a908 100644 --- a/__tests__/workflow_dispatch.test.ts +++ b/__tests__/workflow_dispatch.test.ts @@ -8,6 +8,7 @@ const url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXX const jobName = 'Build and Test' const jobStatus = 'Success' const jobSteps = {} +const jobMatrix = {} const channel = '@override' const message = undefined @@ -54,7 +55,7 @@ test('workflow_dispatch event to slack', async () => { .onAny() .reply(500) - const res = await send(url, jobName, jobStatus, jobSteps, channel, message) + const res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message) await expect(res).toStrictEqual({text: {status: 'ok'}}) expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ diff --git a/__tests__/workflow_run.test.ts b/__tests__/workflow_run.test.ts index f533a589..c2164153 100644 --- a/__tests__/workflow_run.test.ts +++ b/__tests__/workflow_run.test.ts @@ -47,6 +47,7 @@ const url = process.env.SLACK_WEBHOOK_URL as string const jobName = process.env.GITHUB_JOB as string const jobStatus = (process.env.INPUT_STATUS as string).toUpperCase() const jobSteps = process.env.INPUT_STEPS || {} +const jobMatrix = {} const channel = process.env.INPUT_CHANNEL as string const message = process.env.INPUT_MESSAGE as string @@ -66,7 +67,7 @@ test('workflow_run event to slack', async () => { schema: yaml.FAILSAFE_SCHEMA }) as ConfigOptions - const res = await send(url, jobName, jobStatus, jobSteps, channel, message, config) + const res = await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message, config) await expect(res).toStrictEqual({text: {status: 'ok'}}) expect(JSON.parse(mockAxios.history.post[0].data)).toStrictEqual({ diff --git a/action.yml b/action.yml index 96558e72..7601f82b 100644 --- a/action.yml +++ b/action.yml @@ -15,6 +15,9 @@ inputs: steps: description: Report on the status of individual steps required: false + matrix: + description: matrix properties + required: false channel: description: Override default channel with different channel or username required: false diff --git a/docs/images/example4.png b/docs/images/example4.png new file mode 100644 index 00000000..9d463ebc Binary files /dev/null and b/docs/images/example4.png differ diff --git a/src/main.ts b/src/main.ts index 58b475b0..27d1a689 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,13 +29,15 @@ async function run(): Promise { const jobName = process.env.GITHUB_JOB as string const jobStatus = core.getInput('status', {required: true}).toUpperCase() const jobSteps = JSON.parse(core.getInput('steps', {required: false}) || '{}') + const jobMatrix = JSON.parse(core.getInput('matrix', {required: false}) || '{}') const channel = core.getInput('channel', {required: false}) const message = core.getInput('message', {required: false}) core.debug(`jobName: ${jobName}, jobStatus: ${jobStatus}`) core.debug(`channel: ${channel}, message: ${message}`) + core.debug(`jobMatrix: ${JSON.stringify(jobMatrix)}`) if (url) { - await send(url, jobName, jobStatus, jobSteps, channel, message, config) + await send(url, jobName, jobStatus, jobSteps, jobMatrix, channel, message, config) core.info(`Sent ${jobName} status of ${jobStatus} to Slack!`) } else { core.warning('No "SLACK_WEBHOOK_URL"s env or "webhook-url" input configured. Skip.') diff --git a/src/slack.ts b/src/slack.ts index d2d19f71..a7f268d0 100644 --- a/src/slack.ts +++ b/src/slack.ts @@ -132,6 +132,7 @@ export async function send( jobName: string, jobStatus: string, jobSteps: object, + jobMatrix: object, channel?: string, message?: string, opts?: ConfigOptions @@ -241,16 +242,23 @@ export async function send( }{{jobStatus}}` const fallbackTemplate = Handlebars.compile(opts?.fallback || defaultFallback) - const defaultFields = Object.entries(jobSteps).length - ? [ - { - title: 'Job Steps', - value: '{{#each jobSteps}}{{icon this.outcome}} {{@key}}\n{{~/each}}', - short: false, - if: 'always()' - } - ] - : [] + const defaultFields = [] + if (Object.entries(jobSteps).length) { + defaultFields.push({ + title: 'Job Steps', + value: '{{#each jobSteps}}{{icon this.outcome}} {{@key}}\n{{~/each}}', + short: false, + if: 'always()' + }) + } + if (Object.entries(jobMatrix).length) { + defaultFields.push({ + title: 'Job Matrix', + value: '{{#each jobMatrix}}{{@key}}: {{this}}\n{{~/each}}', + short: false, + if: 'always()' + }) + } const filteredFields: object[] = [] for (const field of opts?.fields || defaultFields) { @@ -274,6 +282,7 @@ export async function send( jobName, jobStatus, jobSteps, + jobMatrix, eventName, workflow, workflowUrl,