Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const { URL } = require('node:url')

const {
EmailAlertChannel,
SlackAppAlertChannel,
WebhookAlertChannel,
} = require('checkly/constructs')

Expand All @@ -25,10 +24,13 @@ const emailChannel = new EmailAlertChannel('email-channel-1', {
...sendDefaults,
})

const slackChannel = new SlackAppAlertChannel('slack-channel-1', {
slackChannels: ['#ops'],
...sendDefaults,
})
// A Slack channel needs the Checkly Slack app connected to your account first
// (https://www.checklyhq.com/docs/integrations/alerts/slack/). Once it is, import
// SlackAppAlertChannel from 'checkly/constructs' and uncomment:
// const slackChannel = new SlackAppAlertChannel('slack-channel-1', {
// slackChannels: ['#ops'],
// ...sendDefaults,
// })

const webhookChannel = new WebhookAlertChannel('webhook-channel-1', {
name: 'Pushover webhook',
Expand All @@ -49,6 +51,5 @@ const webhookChannel = new WebhookAlertChannel('webhook-channel-1', {

module.exports = {
emailChannel,
slackChannel,
webhookChannel,
}
12 changes: 7 additions & 5 deletions examples/advanced-project/src/__checks__/utils/alert-channels.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { URL } from 'node:url'
import {
EmailAlertChannel,
SlackAppAlertChannel,
WebhookAlertChannel
} from 'checkly/constructs'

Expand All @@ -25,10 +24,13 @@ export const emailChannel = new EmailAlertChannel('email-channel-1', {
...sendDefaults
})

export const slackChannel = new SlackAppAlertChannel('slack-channel-1', {
slackChannels: ['#ops'],
...sendDefaults
})
// A Slack channel needs the Checkly Slack app connected to your account first
// (https://www.checklyhq.com/docs/integrations/alerts/slack/). Once it is, import
// SlackAppAlertChannel from 'checkly/constructs' and uncomment:
// export const slackChannel = new SlackAppAlertChannel('slack-channel-1', {
// slackChannels: ['#ops'],
// ...sendDefaults
// })

export const webhookChannel = new WebhookAlertChannel('webhook-channel-1', {
name: 'Pushover webhook',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ All available alerts are described in the [Checkly docs](https://www.checklyhq.c

*Important*: Don't make up email addresses, phone numbers, Slack channel names or similar static values. Scan the project to discover a valid configuration or ask what the values should be.

For new Slack notifications, prefer `SlackAppAlertChannel`. It uses the Checkly Slack App and only needs Slack `#channel` names or `@user` handles in `slackChannels`.
For new Slack notifications, prefer `SlackAppAlertChannel`. It uses the Checkly Slack App and only needs Slack `#channel` names or `@user` handles in `slackChannels`. It needs the Checkly Slack App connected to the Checkly account: without it, `npx checkly validate`, `test` and `deploy` fail with "Checkly Slack app not connected". If you can't confirm the app is connected, ask before adding one.

## Email Alert Channel

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/authCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export abstract class AuthCommand extends BaseCommand {
// Constructs validate against account-specific limits and have no access to
// the command instance.
Session.accountFeatures = this.#account?.features ?? []
Session.accountId = this.#account?.id
Session.accountConnectedIntegrations = this.#account?.connectedIntegrations
}

protected async validateProject (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, it, expect, afterEach, beforeEach } from 'vitest'

import { SlackAppAlertChannel } from '../index.js'
import { Diagnostics } from '../diagnostics.js'
import { Project } from '../project.js'
import { Session } from '../session.js'

const validate = async (slackChannels: string[]) => {
const diagnostics = new Diagnostics()
await new SlackAppAlertChannel('ops-slack', { slackChannels }).validate(diagnostics)
return diagnostics
}

describe('SlackAppAlertChannel', () => {
beforeEach(() => {
Session.project = new Project('project-id', {
name: 'Test Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
})
Session.accountId = 'acc-1'
})

afterEach(() => {
Session.accountId = undefined
Session.accountConnectedIntegrations = undefined
})

it('refuses the channel when the account has no Checkly Slack app', async () => {
Session.accountConnectedIntegrations = []

const diagnostics = await validate(['#ops'])

expect(diagnostics.isFatal()).toBe(true)
expect(diagnostics.observations).toEqual([
expect.objectContaining({
title: 'Checkly Slack app not connected',
message: expect.stringContaining(
`SlackAppAlertChannel 'ops-slack' needs the Checkly Slack app, which is not connected to this account, so its alerts cannot be delivered.`,
),
}),
])
expect(diagnostics.observations[0].message).toContain(
'https://app.checklyhq.com/accounts/acc-1/settings/account/integrations',
)
})

it('accepts the channel when the Checkly Slack app is connected', async () => {
Session.accountConnectedIntegrations = ['SLACK']

const diagnostics = await validate(['#ops'])

expect(diagnostics.isFatal()).toBe(false)
})

it('skips the check when the API does not report connected integrations', async () => {
Session.accountConnectedIntegrations = undefined

const diagnostics = await validate(['#ops'])

expect(diagnostics.isFatal()).toBe(false)
})

it('accepts a channel without Slack destinations, which needs no Slack app', async () => {
Session.accountConnectedIntegrations = []

const diagnostics = await validate([])

expect(diagnostics.isFatal()).toBe(false)
})
})
8 changes: 8 additions & 0 deletions packages/cli/src/constructs/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ export class Session {
static loadingChecklyConfigFile: boolean
static checklyConfigFileConstructs?: Construct[]
static accountFeatures: string[] = []
static accountId?: string
/**
* The integrations the authenticated account has connected, or undefined
* when unknown (not authenticated, or an API that does not report them).
*/
static accountConnectedIntegrations?: string[]
static privateLocations: PrivateLocationApi[]
static parsers = new Map<string, Parser>()
static playwrightProjectBundler?: PlaywrightProjectBundler
Expand Down Expand Up @@ -124,6 +130,8 @@ export class Session {
this.loadingChecklyConfigFile = false
this.checklyConfigFileConstructs = undefined
this.accountFeatures = []
this.accountId = undefined
this.accountConnectedIntegrations = undefined
this.privateLocations = []
this.parsers = new Map<string, Parser>()
this.playwrightProjectBundler = undefined
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/constructs/slack-app-alert-channel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { AlertChannel, AlertChannelProps } from './alert-channel.js'
import { Diagnostics, ErrorDiagnostic } from './diagnostics.js'
import { Session } from './session.js'

const SLACK_INTEGRATION = 'SLACK'

export interface SlackAppAlertChannelProps extends AlertChannelProps {
/**
* List of Slack #channels or @handles to notify.
Expand Down Expand Up @@ -38,6 +41,33 @@ export class SlackAppAlertChannel extends AlertChannel {
return `SlackAppAlertChannel:${this.logicalId}`
}

/**
* Refuses the channel when the account has no Checkly Slack app: its alerts
* cannot be delivered, whether the channel is new or was deployed before the
* app was disconnected. Skipped when the account's integrations are unknown.
*/
async validate (diagnostics: Diagnostics): Promise<void> {
await super.validate(diagnostics)

const connected = Session.accountConnectedIntegrations
if (connected === undefined || connected.includes(SLACK_INTEGRATION) || !this.slackChannels?.length) {
return
}
const error = new Error(
`SlackAppAlertChannel '${this.logicalId}' needs the Checkly Slack app, which is not connected to this account,`
+ ` so its alerts cannot be delivered.`,
)
diagnostics.add(new ErrorDiagnostic({
title: 'Checkly Slack app not connected',
message:
`${error.message}\n\n`
+ `Hint: Connect the Checkly Slack app at`
+ ` https://app.checklyhq.com/accounts/${Session.accountId}/settings/account/integrations,`
+ ` or remove the channel.`,
error,
}))
}

synthesize () {
return {
...super.synthesize(),
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/rest/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export interface Account {
* in which case standard limits apply.
*/
features?: string[]
/**
* The integrations the account has connected, e.g. `SLACK` for the Checkly
* Slack app. Absent when the API predates it.
*/
connectedIntegrations?: string[]
}

class Accounts {
Expand Down
Loading