From 8745c9a82a792212d3bd68b190a4773a970dac30 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Mon, 13 Jan 2025 05:22:13 +0900 Subject: [PATCH] feat: check constructs against account default runtime if not given The CLI had a hardcoded default runtime value of 2024.02 which became the effective value if no runtime was set at check or project level. Now, the account's default runtime is used instead. Additionally, if the account's default runtime is used, then we leave `runtimeId` undefined when synthesizing resources. This allows the user to have their checks always use the current account default runtime if they wish. If they do not wish to have such behavior, they should set a default runtime at project level as usual. --- packages/cli/src/commands/deploy.ts | 7 +- packages/cli/src/commands/test.ts | 2 + .../constructs/__tests__/api-check.spec.ts | 78 +++++++++++++++++++ .../__tests__/browser-check.spec.ts | 78 +++++++++++++++++++ .../__tests__/multi-step-check.spec.ts | 69 ++++++++++++++++ packages/cli/src/constructs/api-check.ts | 8 +- packages/cli/src/constructs/browser-check.ts | 7 +- .../cli/src/constructs/multi-step-check.ts | 8 +- packages/cli/src/constructs/project.ts | 11 ++- packages/cli/src/rest/accounts.ts | 1 + .../services/__tests__/project-parser.spec.ts | 7 ++ packages/cli/src/services/project-parser.ts | 4 +- 12 files changed, 262 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 166209a3..7a229b4d 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -6,7 +6,6 @@ import { Flags, ux } from '@oclif/core' import { AuthCommand } from './authCommand' import { parseProject } from '../services/project-parser' import { loadChecklyConfig } from '../services/checkly-config-loader' -import { runtimes } from '../rest/api' import type { Runtime } from '../rest/runtimes' import { Check, AlertChannelSubscription, AlertChannel, CheckGroup, Dashboard, @@ -92,7 +91,8 @@ export default class Deploy extends AuthCommand { config: checklyConfig, constructs: checklyConfigConstructs, } = await loadChecklyConfig(configDirectory, configFilenames) - const { data: avilableRuntimes } = await runtimes.getAll() + const { data: account } = await api.accounts.get(config.getAccountId()) + const { data: avilableRuntimes } = await api.runtimes.getAll() const project = await parseProject({ directory: configDirectory, projectLogicalId: checklyConfig.logicalId, @@ -108,6 +108,7 @@ export default class Deploy extends AuthCommand { acc[runtime.name] = runtime return acc }, > {}), + defaultRuntimeId: account.runtimeId, verifyRuntimeDependencies, checklyConfigConstructs, }) @@ -140,8 +141,6 @@ export default class Deploy extends AuthCommand { return } - const { data: account } = await api.accounts.get(config.getAccountId()) - if (!force && !preview) { const { confirm } = await prompts({ name: 'confirm', diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index f6405373..087f9a36 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -159,6 +159,7 @@ export default class Test extends AuthCommand { }) const verbose = this.prepareVerboseFlag(verboseFlag, checklyConfig.cli?.verbose) const reporterTypes = this.prepareReportersTypes(reporterFlag as ReporterType, checklyConfig.cli?.reporters) + const { data: account } = await api.accounts.get(config.getAccountId()) const { data: availableRuntimes } = await api.runtimes.getAll() const project = await parseProject({ @@ -176,6 +177,7 @@ export default class Test extends AuthCommand { acc[runtime.name] = runtime return acc }, > {}), + defaultRuntimeId: account.runtimeId, verifyRuntimeDependencies, checklyConfigConstructs, }) diff --git a/packages/cli/src/constructs/__tests__/api-check.spec.ts b/packages/cli/src/constructs/__tests__/api-check.spec.ts index 487db3e4..b32136f2 100644 --- a/packages/cli/src/constructs/__tests__/api-check.spec.ts +++ b/packages/cli/src/constructs/__tests__/api-check.spec.ts @@ -36,6 +36,84 @@ describe('ApiCheck', () => { }) }) + it('should fail to bundle if runtime is not specified and default runtime is not set', () => { + const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) + const bundle = () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _bundle = ApiCheck.bundle(getFilePath('entrypoint.js'), undefined) + } + + Session.basePath = __dirname + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = undefined + expect(bundle).toThrowError('runtime is not set') + delete Session.basePath + delete Session.defaultRuntimeId + }) + + it('should successfully bundle if runtime is not specified but default runtime is set', () => { + const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) + const bundle = () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _bundle = ApiCheck.bundle(getFilePath('entrypoint.js'), undefined) + } + + Session.basePath = __dirname + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = '2022.10' + expect(bundle).not.toThrowError('is not supported') + delete Session.basePath + delete Session.defaultRuntimeId + }) + + it('should fail to bundle if runtime is not supported even if default runtime is set', () => { + const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) + const bundle = () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _bundle = ApiCheck.bundle(getFilePath('entrypoint.js'), '9999.99') + } + + Session.basePath = __dirname + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = '2022.02' + expect(bundle).toThrowError('9999.99 is not supported') + delete Session.basePath + delete Session.defaultRuntimeId + }) + + it('should not synthesize runtime if not specified even if default runtime is set', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = '2022.02' + const apiCheck = new ApiCheck('test-check', { + name: 'Test Check', + request, + }) + const payload = apiCheck.synthesize() + expect(payload.runtimeId).toBeUndefined() + delete Session.defaultRuntimeId + }) + + it('should synthesize runtime if specified', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = '2022.02' + const apiCheck = new ApiCheck('test-check', { + name: 'Test Check', + runtimeId: '2022.02', + request, + }) + const payload = apiCheck.synthesize() + expect(payload.runtimeId).toEqual('2022.02') + delete Session.defaultRuntimeId + }) + it('should apply default check settings', () => { Session.project = new Project('project-id', { name: 'Test Project', diff --git a/packages/cli/src/constructs/__tests__/browser-check.spec.ts b/packages/cli/src/constructs/__tests__/browser-check.spec.ts index dc782333..05017ff8 100644 --- a/packages/cli/src/constructs/__tests__/browser-check.spec.ts +++ b/packages/cli/src/constructs/__tests__/browser-check.spec.ts @@ -31,6 +31,84 @@ describe('BrowserCheck', () => { }) }) + it('should fail to bundle if runtime is not specified and default runtime is not set', () => { + const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) + const bundle = () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _bundle = BrowserCheck.bundle(getFilePath('entrypoint.js'), undefined) + } + + Session.basePath = __dirname + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = undefined + expect(bundle).toThrowError('runtime is not set') + delete Session.basePath + delete Session.defaultRuntimeId + }) + + it('should successfully bundle if runtime is not specified but default runtime is set', () => { + const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) + const bundle = () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _bundle = BrowserCheck.bundle(getFilePath('entrypoint.js'), undefined) + } + + Session.basePath = __dirname + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = '2022.10' + expect(bundle).not.toThrowError('is not supported') + delete Session.basePath + delete Session.defaultRuntimeId + }) + + it('should fail to bundle if runtime is not supported even if default runtime is set', () => { + const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) + const bundle = () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _bundle = BrowserCheck.bundle(getFilePath('entrypoint.js'), '9999.99') + } + + Session.basePath = __dirname + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = '2022.02' + expect(bundle).toThrowError('9999.99 is not supported') + delete Session.basePath + delete Session.defaultRuntimeId + }) + + it('should not synthesize runtime if not specified even if default runtime is set', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = '2022.02' + const browserCheck = new BrowserCheck('test-check', { + name: 'Test Check', + code: { content: 'console.log("test check")' }, + }) + const payload = browserCheck.synthesize() + expect(payload.runtimeId).toBeUndefined() + delete Session.defaultRuntimeId + }) + + it('should synthesize runtime if specified', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = runtimes + Session.defaultRuntimeId = '2022.02' + const browserCheck = new BrowserCheck('test-check', { + name: 'Test Check', + runtimeId: '2022.02', + code: { content: 'console.log("test check")' }, + }) + const payload = browserCheck.synthesize() + expect(payload.runtimeId).toEqual('2022.02') + delete Session.defaultRuntimeId + }) + it('should apply default check settings', () => { Session.project = new Project('project-id', { name: 'Test Project', diff --git a/packages/cli/src/constructs/__tests__/multi-step-check.spec.ts b/packages/cli/src/constructs/__tests__/multi-step-check.spec.ts index e24513d9..2087d0c0 100644 --- a/packages/cli/src/constructs/__tests__/multi-step-check.spec.ts +++ b/packages/cli/src/constructs/__tests__/multi-step-check.spec.ts @@ -19,6 +19,7 @@ describe('MultistepCheck', () => { }) expect(check.synthesize()).toMatchObject({ checkType: 'MULTI_STEP' }) }) + it('should throw if runtime does not support multi step check type', () => { Session.project = new Project('project-id', { name: 'Test Project', @@ -33,7 +34,75 @@ describe('MultistepCheck', () => { } expect(() => new MultiStepCheck('main-check', { name: 'Main Check', + runtimeId: '2023.09', code: { content: '' }, })).toThrowError('This runtime does not support multi step checks.') }) + + it('should throw if runtime is not set and default runtime does not support multi step check type', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = { + ...runtimesWithMultiStepSupport, + 9999.99: { + ...runtimesWithMultiStepSupport['2023.09'], + multiStepSupport: false, + }, + } + Session.defaultRuntimeId = '9999.99' + expect(() => new MultiStepCheck('main-check', { + name: 'Main Check', + code: { content: '' }, + })).toThrowError('This runtime does not support multi step checks.') + delete Session.defaultRuntimeId + }) + + it('should succeed if runtime is not set but default runtime supports multi step check type', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = runtimesWithMultiStepSupport + Session.defaultRuntimeId = '2023.09' + expect(() => new MultiStepCheck('main-check', { + name: 'Main Check', + code: { content: '' }, + })).not.toThrowError() + delete Session.defaultRuntimeId + }) + + it('should not synthesize runtime if not specified even if default runtime is set', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = runtimesWithMultiStepSupport + Session.defaultRuntimeId = '2023.09' + const multiCheck = new MultiStepCheck('main-check', { + name: 'Main Check', + code: { content: '' }, + }) + const payload = multiCheck.synthesize() + expect(payload.runtimeId).toBeUndefined() + delete Session.defaultRuntimeId + }) + + it('should synthesize runtime if specified', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = runtimesWithMultiStepSupport + Session.defaultRuntimeId = '2023.09' + const multiCheck = new MultiStepCheck('main-check', { + name: 'Main Check', + runtimeId: '2023.09', + code: { content: '' }, + }) + const payload = multiCheck.synthesize() + expect(payload.runtimeId).toEqual('2023.09') + delete Session.defaultRuntimeId + }) }) diff --git a/packages/cli/src/constructs/api-check.ts b/packages/cli/src/constructs/api-check.ts index 07eeeab7..5f3a7224 100644 --- a/packages/cli/src/constructs/api-check.ts +++ b/packages/cli/src/constructs/api-check.ts @@ -295,7 +295,7 @@ export class ApiCheck extends Check { if (props.setupScript) { if ('entrypoint' in props.setupScript) { - const { script, scriptPath, dependencies } = ApiCheck.bundle(props.setupScript.entrypoint, this.runtimeId!) + const { script, scriptPath, dependencies } = ApiCheck.bundle(props.setupScript.entrypoint, this.runtimeId) this.localSetupScript = script this.setupScriptPath = scriptPath this.setupScriptDependencies = dependencies @@ -314,7 +314,7 @@ export class ApiCheck extends Check { if (props.tearDownScript) { if ('entrypoint' in props.tearDownScript) { - const { script, scriptPath, dependencies } = ApiCheck.bundle(props.tearDownScript.entrypoint, this.runtimeId!) + const { script, scriptPath, dependencies } = ApiCheck.bundle(props.tearDownScript.entrypoint, this.runtimeId) this.localTearDownScript = script this.tearDownScriptPath = scriptPath this.tearDownScriptDependencies = dependencies @@ -337,7 +337,7 @@ export class ApiCheck extends Check { this.addPrivateLocationCheckAssignments() } - static bundle (entrypoint: string, runtimeId: string) { + static bundle (entrypoint: string, runtimeId?: string) { let absoluteEntrypoint = null if (path.isAbsolute(entrypoint)) { absoluteEntrypoint = entrypoint @@ -348,7 +348,7 @@ export class ApiCheck extends Check { absoluteEntrypoint = path.join(path.dirname(Session.checkFileAbsolutePath), entrypoint) } - const runtime = Session.availableRuntimes[runtimeId] + const runtime = Session.getRuntime(runtimeId) if (!runtime) { throw new Error(`${runtimeId} is not supported`) } diff --git a/packages/cli/src/constructs/browser-check.ts b/packages/cli/src/constructs/browser-check.ts index fe13f7e1..ce139d16 100644 --- a/packages/cli/src/constructs/browser-check.ts +++ b/packages/cli/src/constructs/browser-check.ts @@ -79,8 +79,7 @@ export class BrowserCheck extends Check { } absoluteEntrypoint = path.join(path.dirname(Session.checkFileAbsolutePath), entrypoint) } - // runtimeId will always be set by check or browser check defaults so it is safe to use ! operator - const bundle = BrowserCheck.bundle(absoluteEntrypoint, this.runtimeId!) + const bundle = BrowserCheck.bundle(absoluteEntrypoint, this.runtimeId) if (!bundle.script) { throw new Error(`Browser check "${logicalId}" is not allowed to be empty`) } @@ -115,8 +114,8 @@ export class BrowserCheck extends Check { } } - static bundle (entry: string, runtimeId: string) { - const runtime = Session.availableRuntimes[runtimeId] + static bundle (entry: string, runtimeId?: string) { + const runtime = Session.getRuntime(runtimeId) if (!runtime) { throw new Error(`${runtimeId} is not supported`) } diff --git a/packages/cli/src/constructs/multi-step-check.ts b/packages/cli/src/constructs/multi-step-check.ts index 89abc267..8e1500e9 100644 --- a/packages/cli/src/constructs/multi-step-check.ts +++ b/packages/cli/src/constructs/multi-step-check.ts @@ -47,7 +47,7 @@ export class MultiStepCheck extends Check { this.playwrightConfig = props.playwrightConfig - if (!Session.availableRuntimes[this.runtimeId!]?.multiStepSupport) { + if (!Session.getRuntime(this.runtimeId)?.multiStepSupport) { throw new Error('This runtime does not support multi step checks.') } if ('content' in props.code) { @@ -65,7 +65,7 @@ export class MultiStepCheck extends Check { absoluteEntrypoint = path.join(path.dirname(Session.checkFileAbsolutePath), entrypoint) } // runtimeId will always be set by check or multi-step check defaults so it is safe to use ! operator - const bundle = MultiStepCheck.bundle(absoluteEntrypoint, this.runtimeId!) + const bundle = MultiStepCheck.bundle(absoluteEntrypoint, this.runtimeId) if (!bundle.script) { throw new Error(`Multi-Step check "${logicalId}" is not allowed to be empty`) } @@ -99,8 +99,8 @@ export class MultiStepCheck extends Check { } } - static bundle (entry: string, runtimeId: string) { - const runtime = Session.availableRuntimes[runtimeId] + static bundle (entry: string, runtimeId?: string) { + const runtime = Session.getRuntime(runtimeId) if (!runtime) { throw new Error(`${runtimeId} is not supported`) } diff --git a/packages/cli/src/constructs/project.ts b/packages/cli/src/constructs/project.ts index 8d9d798a..169c96b1 100644 --- a/packages/cli/src/constructs/project.ts +++ b/packages/cli/src/constructs/project.ts @@ -142,6 +142,7 @@ export class Session { static checkFilePath?: string static checkFileAbsolutePath?: string static availableRuntimes: Record + static defaultRuntimeId?: string static verifyRuntimeDependencies = true static loadingChecklyConfigFile: boolean static checklyConfigFileConstructs?: Construct[] @@ -153,7 +154,7 @@ export class Session { } else if (Session.loadingChecklyConfigFile && construct.allowInChecklyConfig()) { Session.checklyConfigFileConstructs!.push(construct) } else { - throw new Error('Internal Error: Session is not properly configured for using a construct. Please contact Checkly support on support@checklyhq.com') + throw new Error('Internal Error: Session is not properly configured for using a construct. Please contact Checkly support at support@checklyhq.com.') } } @@ -182,4 +183,12 @@ export class Session { } return Session.privateLocations } + + static getRuntime (runtimeId?: string): Runtime | undefined { + const effectiveRuntimeId = runtimeId ?? Session.defaultRuntimeId + if (effectiveRuntimeId === undefined) { + throw new Error('Internal Error: Account default runtime is not set. Please contact Checkly support at support@checklyhq.com.') + } + return Session.availableRuntimes[effectiveRuntimeId] + } } diff --git a/packages/cli/src/rest/accounts.ts b/packages/cli/src/rest/accounts.ts index 2a23b722..c7fdeb63 100644 --- a/packages/cli/src/rest/accounts.ts +++ b/packages/cli/src/rest/accounts.ts @@ -3,6 +3,7 @@ import type { AxiosInstance } from 'axios' export interface Account { id: string name: string + runtimeId: string } class Accounts { diff --git a/packages/cli/src/services/__tests__/project-parser.spec.ts b/packages/cli/src/services/__tests__/project-parser.spec.ts index e057c77c..4cf828b9 100644 --- a/packages/cli/src/services/__tests__/project-parser.spec.ts +++ b/packages/cli/src/services/__tests__/project-parser.spec.ts @@ -33,6 +33,7 @@ describe('parseProject()', () => { projectName: 'project name', repoUrl: 'https://github.com/checkly/checkly-cli', availableRuntimes: runtimes, + defaultRuntimeId: '2024.02', }) const synthesizedProject = project.synthesize() expect(synthesizedProject).toMatchObject({ @@ -67,6 +68,7 @@ describe('parseProject()', () => { projectName: 'project name', repoUrl: 'https://github.com/checkly/checkly-cli', availableRuntimes: runtimes, + defaultRuntimeId: '2024.02', }) const synthesizedProject = project.synthesize() expect(synthesizedProject).toMatchObject({ @@ -123,6 +125,7 @@ describe('parseProject()', () => { projectName: 'ts project', repoUrl: 'https://github.com/checkly/checkly-cli', availableRuntimes: runtimes, + defaultRuntimeId: '2024.02', }) expect(project.synthesize()).toMatchObject({ project: { @@ -143,6 +146,7 @@ describe('parseProject()', () => { availableRuntimes: runtimes, checkMatch: ['**/__checks1__/*.check.js', '**/__checks2__/*.check.js', '**/__nested-checks__/*.check.js'], browserCheckMatch: ['**/__checks1__/*.spec.js', '**/__checks2__/*.spec.js', '**/__nested-checks__/*.spec.js'], + defaultRuntimeId: '2024.02', }) expect(project.synthesize()).toMatchObject({ project: { @@ -170,6 +174,7 @@ describe('parseProject()', () => { availableRuntimes: runtimes, checkMatch: '**/*.foobar.js', // don't match .check.js files used for a different test browserCheckMatch: '**/*.spec.js', + defaultRuntimeId: '2024.02', }) // shouldn't reach this point expect(true).toBe(false) @@ -187,6 +192,7 @@ describe('parseProject()', () => { projectName: 'empty script project', repoUrl: 'https://github.com/checkly/checkly-cli', availableRuntimes: runtimes, + defaultRuntimeId: '2024.02', browserCheckMatch: '**/*.foobar.js', // don't match .spec.js files used for a different test }) // shouldn't reach this point @@ -203,6 +209,7 @@ describe('parseProject()', () => { projectLogicalId: 'glob-project-id', projectName: 'glob project', availableRuntimes: runtimes, + defaultRuntimeId: '2024.02', checkMatch: [], browserCheckMatch: ['**/__checks__/browser/*.spec.js'], multiStepCheckMatch: ['**/__checks__/multistep/*.spec.js'], diff --git a/packages/cli/src/services/project-parser.ts b/packages/cli/src/services/project-parser.ts index 1be67ee2..98845ba8 100644 --- a/packages/cli/src/services/project-parser.ts +++ b/packages/cli/src/services/project-parser.ts @@ -23,12 +23,12 @@ type ProjectParseOpts = { checkDefaults?: CheckConfigDefaults, browserCheckDefaults?: CheckConfigDefaults, availableRuntimes: Record, + defaultRuntimeId: string, verifyRuntimeDependencies?: boolean, checklyConfigConstructs?: Construct[], } const BASE_CHECK_DEFAULTS = { - runtimeId: '2024.02', } export async function parseProject (opts: ProjectParseOpts): Promise { @@ -44,6 +44,7 @@ export async function parseProject (opts: ProjectParseOpts): Promise { checkDefaults = {}, browserCheckDefaults = {}, availableRuntimes, + defaultRuntimeId, verifyRuntimeDependencies, checklyConfigConstructs, } = opts @@ -59,6 +60,7 @@ export async function parseProject (opts: ProjectParseOpts): Promise { Session.checkDefaults = Object.assign({}, BASE_CHECK_DEFAULTS, checkDefaults) Session.browserCheckDefaults = browserCheckDefaults Session.availableRuntimes = availableRuntimes + Session.defaultRuntimeId = defaultRuntimeId Session.verifyRuntimeDependencies = verifyRuntimeDependencies ?? true // TODO: Do we really need all of the ** globs, or could we just put node_modules?