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
4 changes: 3 additions & 1 deletion packages/cli/src/ai-context/references/configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ Run `npx checkly skills manage plan` for the full reference.

- Deploy checks using the `npx checkly deploy` command. Use `--output` to see the created, updated, and deleted resources. Use `--verbose` to also include each resource's name and physical ID (UUID), which is useful for programmatically referencing deployed resources (e.g. `npx checkly checks get <id>`).
- Use `--preview` to see which resources a deploy would create, update, delete or keep, without applying it: an overview of every resource the deploy touches and a diff of each updated resource's construct as deployed against as in code. A plain interactive `checkly deploy` prints that same preview before asking the user to apply the changes or cancel. The machine-readable forms (`--dry-run`, and the `confirmation_required` envelope) additionally carry the individual properties that would change.
- When the preview shows a resource that was edited outside the project (in the web app or through the API), an interactive `checkly deploy` offers a third choice next to apply and cancel: update the code with the values from Checkly and deploy nothing. It rewrites literal values (strings, numbers, booleans, and arrays or objects of those, including `runtimeId` on runtime checks and groups, the `request` of every check and monitor type, `sslCheckDomain` and `aiAutoRepairEnabled` on browser checks, `aiAutoRepairEnabled` on multistep checks and `prompt` on agentic checks; a multi-line string over a template literal is written as a template literal) and the helper-spelled properties `frequency` (`Frequency.EVERY_5M`), `retryStrategy` (`RetryStrategyBuilder`), `alertEscalationPolicy` (`AlertEscalationBuilder`) and `assertions` (the class's assertion builder) inside the `new ApiCheck('id', { … })` call of checks and check groups, adds a helper's import when the file lacks it, leaves the rest of the file untouched, and lists everything it could not update with the reason (references to other resources, secrets, scripts, an incident trigger, a helper call holding a variable, `doubleCheck` set beside a retry strategy, a check moved to the global alert policy, which needs `alertEscalationPolicy` removed by hand). It exists only in a terminal; there is no flag for it, and the `confirmation_required` envelope is unchanged.
- When the preview shows a resource that was edited outside the project (in the web app or through the API), an interactive `checkly deploy` offers a third choice next to apply and cancel: update the code with the values from Checkly and deploy nothing. It exists only in a terminal; there is no flag for it, and the `confirmation_required` envelope is unchanged.
- It rewrites literal values (strings, numbers, booleans, and arrays or objects of those; a multi-line string over a template literal is written as a template literal) and the helper-spelled properties `frequency` (`Frequency.EVERY_5M`), `retryStrategy` (`RetryStrategyBuilder`), `alertEscalationPolicy` (`AlertEscalationBuilder`) and `assertions` (the class's assertion builder) inside the `new SomeConstruct('id', { … })` call, adds a helper's import when the file lacks it, and leaves the rest of the file untouched. Covered: checks and check groups (including `runtimeId` on runtime checks and groups, the `request` of every check and monitor type, `sslCheckDomain` and `aiAutoRepairEnabled` on browser checks, `aiAutoRepairEnabled` on multistep checks and `prompt` on agentic checks), alert channels of every type, private locations, dashboards, maintenance windows (`repeatInterval` and `repeatUnit` together), status pages (v2 and v3, each theme colour under an existing `themeColors.light`/`.dark`), status page services, v3 components and automation rules.
- It lists everything it could not update with the reason: references to other resources, secrets (a credential such as a webhook URL or API key is never written), scripts, a dashboard's `customCSS`, an incident trigger, an alert channel's type or a webhook-based channel's fixed type and method, Telegram's packed template, a helper call holding a variable, `doubleCheck` set beside a retry strategy, a check moved to the global alert policy, which needs `alertEscalationPolicy` removed by hand.
- Use `--skip-plan` to deploy without asking Checkly for a plan: nothing is previewed and no plan token is used, so the deploy applies whatever the account looks like when it runs. Resources to delete are still listed before the confirmation, but the code bundle is uploaded before it rather than after. Incompatible with `--preview`, `--dry-run`, `--plan-token` and `--prune-relations`. Prefer a planned deploy unless the plan itself is the problem.
- Use `--prune-relations` to also delete the alert channel subscriptions and private location assignments on this project's checks and groups that the project does not manage. Without it they are only reported.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1180,12 +1180,12 @@ new ApiCheck('api', {
expect(api.projects.deploy).not.toHaveBeenCalled()
})

it('does not offer the choice for a resource whose class it cannot update, or for a secret', async () => {
it('does not offer the choice for a change the table refuses, or for a secret', async () => {
planResolves([
{
...CHANGED,
redactions: [],
changes: [{ path: '/config/address', origin: 'remote', before: 'ops@example.com', after: 'new@example.com' }],
changes: [{ path: '/type', origin: 'remote', before: 'EMAIL', after: 'SLACK' }],
},
{
...remoteEdit,
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ function rejectsPreviewEraField (err: any): boolean {
* outside the project and the code can take the edit: write the account's
* current values into the code and deploy nothing, so the user reviews the
* diff and deploys again rather than overwriting the edit. Only the
* properties of checks and groups the write-back knows the spelling of (a
* literal, or a helper such as `Frequency.EVERY_5M`) can be written;
* everything else is listed with its reason.
* properties the write-back knows the construct's spelling of (a literal,
* or a helper such as `Frequency.EVERY_5M`) can be written; everything
* else is listed with its reason.
*/
function writeBackAlternatives (diff: DiffEntry[], project: Project, command: Deploy): CommandAlternative[] {
if (!hasWritableChanges(diff, project)) {
Expand Down
31 changes: 13 additions & 18 deletions packages/cli/src/constructs/maintenance-window-codegen.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { Codegen, Context } from './internal/codegen/index.js'
import { expr, ident } from '../sourcegen/index.js'
import { expr, ident, Value } from '../sourcegen/index.js'

/** The expression for a timestamp the API reports as an ISO string: `new Date('<iso>')`. */
export function valueForDate (iso: string): Value {
return expr(ident('Date'), builder => {
builder.new(builder => {
builder.string(iso)
})
})
}

export interface MaintenanceWindowResource {
name: string
Expand Down Expand Up @@ -39,17 +48,8 @@ export class MaintenanceWindowCodegen extends Codegen<MaintenanceWindowResource>
}
})

builder.expr('startsAt', ident('Date'), builder => {
builder.new(builder => {
builder.string(resource.startsAt)
})
})

builder.expr('endsAt', ident('Date'), builder => {
builder.new(builder => {
builder.string(resource.endsAt)
})
})
builder.value('startsAt', valueForDate(resource.startsAt))
builder.value('endsAt', valueForDate(resource.endsAt))

if (resource.repeatInterval !== undefined && resource.repeatInterval !== null) {
builder.number('repeatInterval', resource.repeatInterval)
Expand All @@ -60,12 +60,7 @@ export class MaintenanceWindowCodegen extends Codegen<MaintenanceWindowResource>
}

if (resource.repeatEndsAt) {
const repeatEndsAt = resource.repeatEndsAt
builder.expr('repeatEndsAt', ident('Date'), builder => {
builder.new(builder => {
builder.string(repeatEndsAt)
})
})
builder.value('repeatEndsAt', valueForDate(resource.repeatEndsAt))
}
})
})
Expand Down
42 changes: 42 additions & 0 deletions packages/cli/src/services/write-back/__tests__/helper-edit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const escalation = (value: unknown): HelperEdit =>
({ path: ['alertEscalationPolicy'], value, helper: 'alertEscalation' })
const assertions = (value: unknown, builder: AssertionBuilderName = 'AssertionBuilder'): HelperEdit =>
({ path: ['request', 'assertions'], value, helper: 'assertions', builder })
const date = (value: unknown): HelperEdit => ({ path: ['startsAt'], value, helper: 'date' })

const STYLE = { quote: '\'' as const, indentUnit: ' ', lineEnding: '\n' as const }
const LAYOUT = { column: ' ', inline: false, trailingComma: true, locals: new Map<string, string>() }
Expand Down Expand Up @@ -75,6 +76,8 @@ describe('buildHelperValue and renderExpression', () => {
expect(render(frequency(7))).toBe('7')
expect(render(frequency({ frequency: 0, frequencyOffset: 15 }))).toBe('new Frequency(0, 15)')
expect(render(retry(null))).toBe('RetryStrategyBuilder.noRetries()')
expect(render(date('2026-01-01T00:00:00.000Z'))).toBe('new Date(\'2026-01-01T00:00:00.000Z\')')
expect(buildHelperValue(date('2026-01-01T00:00:00.000Z')).imports).toEqual([])
expect(render(retry({ type: 'FIXED', baseBackoffSeconds: 60, maxRetries: 2, maxDurationSeconds: 600, sameRegion: true })))
.toBe('RetryStrategyBuilder.fixedStrategy({})')
expect(render(retry({ type: 'LINEAR', baseBackoffSeconds: 10, maxRetries: 3, sameRegion: false, onlyOn: 'NETWORK_ERROR' })))
Expand Down Expand Up @@ -137,6 +140,8 @@ describe('buildHelperValue and renderExpression', () => {
expect(() => buildHelperValue(assertions([null]))).toThrow('Checkly has no value for request.assertions; edit the property by hand')
expect(() => buildHelperValue(escalation({}))).toThrow('Checkly has no value for alertEscalationPolicy; edit the property by hand')
expect(() => buildHelperValue(retry({}))).toThrow('Checkly has no value for retryStrategy; edit the property by hand')
expect(() => buildHelperValue(date(null))).toThrow('Checkly has no value for startsAt; edit the property by hand')
expect(() => buildHelperValue(date('yesterday'))).toThrow('Checkly has no value for startsAt; edit the property by hand')
// A number the codegen cannot compute is refused before it reaches the file.
const nan = assertions([{ source: 'STATUS_CODE', comparison: 'EQUALS', target: 'abc', property: '', regex: null }])
expect(() => renderExpression(buildHelperValue(nan).value, STYLE, LAYOUT)).toThrow('NaN cannot be written as a literal')
Expand Down Expand Up @@ -208,6 +213,8 @@ describe('isHelperExpression and matchesValue', () => {
[assertions([{ source: 'STATUS_CODE', property: '', comparison: 'EQUALS', target: '200', regex: null }]),
'[AssertionBuilder.statusCode().equals(-200)]', false],
[assertions([]), '[]', true],
[date('2026-01-01T00:00:00.000Z'), 'new Date(\'2026-01-01T00:00:00.000Z\')', true],
[date('2026-01-01T00:00:00.000Z'), 'new Date(\'2026-01-01T00:00:00Z\')', false],
]
const boundAs = new Map<string, string>()
for (const [edit, code, expected] of cases) {
Expand Down Expand Up @@ -499,6 +506,41 @@ new ApiCheck('api', { retryStrategy: RetryStrategyBuilder.fixedStrategy({ sameRe
}
})

it('writes a date over a Date expression or a string, without an import, and not over a variable', () => {
const text = `import { ApiCheck } from 'checkly/constructs'
const when = new Date()
new ApiCheck('api', {
startsAt: new Date('2026-01-01T00:00:00Z'),
endsAt: '2026-01-02T00:00:00.000Z',
repeatEndsAt: when,
})
`
const result = apply('a.ts', text, [
date('2026-01-01T00:00:00.000Z'),
{ path: ['endsAt'], value: '2026-01-03T00:00:00.000Z', helper: 'date' },
{ path: ['repeatEndsAt'], value: '2026-01-04T00:00:00.000Z', helper: 'date' },
{ path: ['name'], value: '2026-01-05T00:00:00.000Z', helper: 'date' },
])
expect(result.applied.map(edit => [edit.path.join('.'), edit.rendered])).toEqual([
['startsAt', 'new Date(\'2026-01-01T00:00:00.000Z\')'],
['endsAt', 'new Date(\'2026-01-03T00:00:00.000Z\')'],
['name', 'new Date(\'2026-01-05T00:00:00.000Z\')'],
])
expect(result.skipped.map(edit => [edit.path.join('.'), edit.reason])).toEqual([
['repeatEndsAt', 'repeatEndsAt is the variable when, not a literal or a Date expression'],
])
expect(result.imports).toEqual([])
expect(result.text).toContain(' startsAt: new Date(\'2026-01-01T00:00:00.000Z\'),\n endsAt: new Date(\'2026-01-03T00:00:00.000Z\'),')
const reparsed = findConstructOptions(parseSource('a.ts', result.text), 'api', NAMES)
expect(result.applied.every(edit => readsBack(reparsed, edit))).toBe(true)
// A file that binds `Date` to something of its own does not hold the global.
const shadowed = apply('a.ts', text.replace('const when = new Date()', 'import { Date } from \'./dates.js\''), [
date('2026-01-01T00:00:00.000Z'),
])
expect(shadowed.applied).toEqual([])
expect(shadowed.skipped.map(edit => edit.reason)).toEqual(['Date is bound to something else in this file'])
})

it('works through acorn for JavaScript files with a require', () => {
const text = `const { ApiCheck } = require('checkly/constructs')\nnew ApiCheck('api', { frequency: 10 })\n`
const result = apply('a.js', text, [frequency({ frequency: 0, frequencyOffset: 20 })])
Expand Down
Loading
Loading