From b212012cf9e2d0d15d025326d9b9fcda202e5750 Mon Sep 17 00:00:00 2001 From: "checkly-rca-bot-angie[bot]" <266841500+checkly-rca-bot-angie[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 02:02:29 +0000 Subject: [PATCH] feat: support private StatusPageV3 constructs --- packages/cli/src/commands/deploy.ts | 18 +++++++++++++++++- .../__tests__/status-page-v3-codegen.spec.ts | 2 ++ .../__tests__/status-page-v3.spec.ts | 2 ++ .../src/constructs/status-page-v3-codegen.ts | 5 +++++ packages/cli/src/constructs/status-page-v3.ts | 8 ++++++++ packages/cli/src/rest/projects.ts | 1 + packages/cli/src/rest/status-pages.ts | 8 ++++++++ 7 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index fd3dd7425..9b3c081d4 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -10,7 +10,7 @@ import { MaintenanceWindow, PrivateLocation, PrivateLocationCheckAssignment, PrivateLocationGroupAssignment, Project, ProjectData, Session, StatusPage, StatusPageService, - StatusPageV3Component, StatusPageV3AutomationRule, + StatusPageV3, StatusPageV3Component, StatusPageV3AutomationRule, } from '../constructs/index.js' import chalk from 'chalk' import { splitConfigFilePath, getGitInformation } from '../services/util.js' @@ -334,6 +334,7 @@ export default class Deploy extends AuthCommand { ) if (!preview) { this.style.actionSuccess() + await this.printSensitiveOutputs(data, project) } if (preview || output) { this.log(this.formatPreview(data, project, verbose)) @@ -373,6 +374,21 @@ export default class Deploy extends AuthCommand { } } + private async printSensitiveOutputs (data: ProjectDeployResponse, project: Project): Promise { + for (const change of data.diff.filter(({ sensitiveOutput }) => sensitiveOutput === 'status-page-password')) { + const construct = project.data[change.type as keyof ProjectData][change.logicalId] + if (!(construct instanceof StatusPageV3) || !change.physicalId) continue + + try { + const { data: generated } = await api.statusPages.regeneratePasswordV3(String(change.physicalId)) + this.log(chalk.yellow.bold(`Password for status page "${construct.name}" (shown once): ${generated.password}`)) + this.log(chalk.yellow('Store this password securely. It cannot be retrieved again.')) + } catch { + this.warn(`Password protection was enabled for status page "${construct.name}", but the generated password could not be retrieved. Regenerate it in the Checkly web app.`) + } + } + } + private collectDeletions (previewData: ProjectDeployResponse): Array<{ resourceType: string, logicalId: string }> { return (previewData?.diff ?? []) .filter(change => diff --git a/packages/cli/src/constructs/__tests__/status-page-v3-codegen.spec.ts b/packages/cli/src/constructs/__tests__/status-page-v3-codegen.spec.ts index c3027381f..0ff725fdf 100644 --- a/packages/cli/src/constructs/__tests__/status-page-v3-codegen.spec.ts +++ b/packages/cli/src/constructs/__tests__/status-page-v3-codegen.spec.ts @@ -26,6 +26,7 @@ const page: StatusPageV3Resource = { description: 'All systems', defaultTheme: 'DARK', allowIndexing: false, + isPrivate: true, } const group: StatusPageV3ComponentResource = { @@ -114,6 +115,7 @@ describe('StatusPageV3 codegen', () => { expect(pageSource).toContain('description: \'All systems\'') expect(pageSource).toContain('defaultTheme: \'DARK\'') expect(pageSource).toContain('allowIndexing: false') + expect(pageSource).toContain('isPrivate: true') expect(pageSource).not.toContain('cards') const groupSource = sources['resources/status-pages/components/platform.check.ts'] diff --git a/packages/cli/src/constructs/__tests__/status-page-v3.spec.ts b/packages/cli/src/constructs/__tests__/status-page-v3.spec.ts index c69c9443a..84cab1104 100644 --- a/packages/cli/src/constructs/__tests__/status-page-v3.spec.ts +++ b/packages/cli/src/constructs/__tests__/status-page-v3.spec.ts @@ -25,6 +25,7 @@ describe('StatusPageV3', () => { defaultTheme: 'DARK', termsOfServiceLink: 'https://acme.example/terms', allowIndexing: false, + isPrivate: true, }) expect(page.type).toBe('status-page') @@ -34,6 +35,7 @@ describe('StatusPageV3', () => { defaultTheme: 'DARK', termsOfServiceLink: 'https://acme.example/terms', allowIndexing: false, + isPrivate: true, version: 3, })) expect(page.synthesize()).not.toHaveProperty('cards') diff --git a/packages/cli/src/constructs/status-page-v3-codegen.ts b/packages/cli/src/constructs/status-page-v3-codegen.ts index ceae9fd8a..bfb9197d9 100644 --- a/packages/cli/src/constructs/status-page-v3-codegen.ts +++ b/packages/cli/src/constructs/status-page-v3-codegen.ts @@ -19,6 +19,7 @@ export interface StatusPageV3Resource { footerText?: string | null googleAnalyticsTag?: string | null allowIndexing?: boolean | null + isPrivate?: boolean | null } const construct = 'StatusPageV3' @@ -138,6 +139,10 @@ export class StatusPageV3Codegen extends Codegen { if (resource.allowIndexing === false) { builder.boolean('allowIndexing', false) } + + if (resource.isPrivate === true) { + builder.boolean('isPrivate', true) + } }) }) })) diff --git a/packages/cli/src/constructs/status-page-v3.ts b/packages/cli/src/constructs/status-page-v3.ts index af2b64ed3..beb65f5c4 100644 --- a/packages/cli/src/constructs/status-page-v3.ts +++ b/packages/cli/src/constructs/status-page-v3.ts @@ -61,6 +61,11 @@ export interface StatusPageV3Props { * Whether search engines may index the public page. Defaults to true. */ allowIndexing?: boolean + /** + * Protect the status page with a generated password. The password is shown + * once after the deployment that enables protection. + */ + isPrivate?: boolean } /** @@ -117,6 +122,7 @@ export class StatusPageV3 extends Construct { footerText?: string googleAnalyticsTag?: string allowIndexing?: boolean + isPrivate?: boolean // Same resource type as the v2 page: both live in one table and are told // apart by the `version` discriminator synthesized below. @@ -146,6 +152,7 @@ export class StatusPageV3 extends Construct { this.footerText = props.footerText this.googleAnalyticsTag = props.googleAnalyticsTag this.allowIndexing = props.allowIndexing + this.isPrivate = props.isPrivate Session.registerConstruct(this) } @@ -177,6 +184,7 @@ export class StatusPageV3 extends Construct { footerText: this.footerText, googleAnalyticsTag: this.googleAnalyticsTag, allowIndexing: this.allowIndexing, + isPrivate: this.isPrivate, version: 3, } } diff --git a/packages/cli/src/rest/projects.ts b/packages/cli/src/rest/projects.ts index abb7b1cb6..19b050807 100644 --- a/packages/cli/src/rest/projects.ts +++ b/packages/cli/src/rest/projects.ts @@ -18,6 +18,7 @@ export interface Change { physicalId?: string | number type: string action: string + sensitiveOutput?: 'status-page-password' } export interface ResourceSync { diff --git a/packages/cli/src/rest/status-pages.ts b/packages/cli/src/rest/status-pages.ts index 2e24462ae..3aae8237f 100644 --- a/packages/cli/src/rest/status-pages.ts +++ b/packages/cli/src/rest/status-pages.ts @@ -37,6 +37,10 @@ export interface PaginatedStatusPages { length: number } +export interface GeneratedStatusPagePassword { + password: string +} + class StatusPages { api: AxiosInstance constructor (api: AxiosInstance) { @@ -52,6 +56,10 @@ class StatusPages { const response = await this.api.get(`/v1/status-pages/${id}`) return response.data } + + regeneratePasswordV3 (id: string) { + return this.api.post(`/v3/status-pages/${id}/password`) + } } export default StatusPages