Skip to content
Merged
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
44 changes: 40 additions & 4 deletions docs/openapi/monitoring-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -22996,7 +22996,20 @@
"items": {
"minLength": 1,
"type": "string",
"description": "Event types to deliver, e.g. monitor.created, incident.resolved"
"description": "Event types to deliver, e.g. monitor.created, incident.resolved",
"enum": [
"monitor.created",
"monitor.updated",
"monitor.deleted",
"incident.created",
"incident.resolved",
"incident.reopened",
"service.status_changed",
"service.component_changed",
"service.incident_created",
"service.incident_updated",
"service.incident_resolved"
]
}
}
}
Expand Down Expand Up @@ -25430,7 +25443,17 @@
"properties": {
"type": {
"type": "string",
"description": "Rule type, e.g. severity_gte, monitor_id_in, region_in"
"description": "Rule type, e.g. severity_gte, monitor_id_in, region_in",
"enum": [
"severity_gte",
"monitor_id_in",
"region_in",
"incident_status",
"monitor_type_in",
"service_id_in",
"resource_group_id_in",
"component_name_in"
]
},
"value": {
"type": "string",
Expand Down Expand Up @@ -32518,7 +32541,20 @@
"nullable": true,
"items": {
"type": "string",
"description": "Replace subscribed events; null preserves current"
"description": "Replace subscribed events; null preserves current",
"enum": [
"monitor.created",
"monitor.updated",
"monitor.deleted",
"incident.created",
"incident.resolved",
"incident.reopened",
"service.status_changed",
"service.component_changed",
"service.incident_created",
"service.incident_updated",
"service.incident_resolved"
]
}
},
"enabled": {
Expand Down Expand Up @@ -32916,4 +32952,4 @@
}
}
}
}
}
27 changes: 22 additions & 5 deletions scripts/generate-zod.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ function extractSchemas(raw) {
if (line.includes('@zodios/core')) continue;
kept.push(line);
}
return '// @ts-nocheck\n// Auto-generated Zod schemas from OpenAPI spec. DO NOT EDIT.\n' +
return '// Auto-generated Zod schemas from OpenAPI spec. DO NOT EDIT.\n' +
'/* eslint-disable */\n' +
kept.join('\n') + '\n';
}

Expand Down Expand Up @@ -81,6 +82,8 @@ function generateSpecFacts(spec) {
SP_INCIDENT_STATUSES: enumsFrom('CreateStatusPageIncidentRequest', 'status'),
AUTH_TYPES: enumsFrom('MonitorAuthDto', 'authType'),
MANAGED_BY: enumsFrom('CreateMonitorRequest', 'managedBy'),
MATCH_RULE_TYPES: enumsFrom('MatchRule', 'type'),
WEBHOOK_EVENT_TYPES: enumsFrom('CreateWebhookEndpointRequest', 'subscribedEvents'),
// ``operator`` is duplicated across StatusCodeAssertion, HeaderValueAssertion,
// JsonPathAssertion, ResponseSizeAssertion, etc. — pull from one
// representative schema. The validator and Zod layer share this single
Expand All @@ -96,11 +99,18 @@ function generateSpecFacts(spec) {
];

for (const [name, values] of Object.entries(facts)) {
// If the spec dropped the underlying enum, fall back to an empty
// tuple instead of skipping the export. Skipping the export breaks
// every downstream consumer that imports the constant, even though
// the runtime semantics are clear: "no values are valid here". The
// empty tuple keeps types/imports compiling and lets Zod's
// `.enum([])` correctly reject all values at validation time.
const items = values && values.length > 0
? values.map(v => `'${v}'`).join(', ')
: '';
if (!values) {
lines.push(`// WARNING: ${name} — enum not found in spec`);
continue;
lines.push(`// NOTE: ${name} — enum missing from spec, exporting empty tuple`);
}
const items = values.map(v => `'${v}'`).join(', ');
lines.push(`export const ${name} = [${items}] as const`);
const typeName = name.split('_').map(w => w[0] + w.slice(1).toLowerCase()).join('');
lines.push(`export type ${typeName} = (typeof ${name})[number]`);
Expand Down Expand Up @@ -142,7 +152,14 @@ async function main() {
openApiDoc: spec,
distPath: OUTPUT_PATH,
options: {
exportSchemas: true,
// Emit a top-level export for every named schema in the spec — without
// this, openapi-zod-client only exports schemas reused across multiple
// endpoints and inlines single-use response DTOs (MonitorDto,
// IncidentDto, …) into the endpoint definitions, which extractSchemas()
// then strips. Matches sdk-js so all surfaces share the same import
// pattern (`schemas.MonitorDto`) instead of forcing the CLI to
// hand-roll DTO Zod schemas that drift from the spec.
shouldExportAllSchemas: true,
// Strict objects everywhere — generated `.passthrough()` calls erase
// type narrowing in consumers (e.g. CLI YAML validators). Also
// required so `z.union([...])` correctly rejects non-matching
Expand Down
52 changes: 52 additions & 0 deletions scripts/regen-from.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
#
# Regenerate generated code from an arbitrary OpenAPI spec file.
#
# Usage: scripts/regen-from.sh <path-to-spec.json>
#
# Per-artifact entry point for the spec-evolution harness
# (`mono/tests/surfaces/evolution/`). The harness handles backup/restore.
#
# Behavior:
# - copies <path-to-spec.json> over docs/openapi/monitoring-api.json
# - runs typegen + zodgen + descgen + tsc to rebuild dist/
# - prints absolute path to src/lib/api.generated.ts on stdout
#
set -euo pipefail

if [[ $# -ne 1 ]]; then
echo "usage: $0 <path-to-spec.json>" >&2
exit 1
fi

INPUT_SPEC="$1"
if [[ ! -f "$INPUT_SPEC" ]]; then
echo "error: spec not found at $INPUT_SPEC" >&2
exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
TARGET_SPEC="$ROOT_DIR/docs/openapi/monitoring-api.json"
OUTPUT="$ROOT_DIR/src/lib/api.generated.ts"

# Skip the copy when the caller passes the vendored spec back in (harness
# post-session teardown re-regens from the restored baseline).
INPUT_ABS="$(cd "$(dirname "$INPUT_SPEC")" && pwd)/$(basename "$INPUT_SPEC")"
TARGET_ABS="$(cd "$(dirname "$TARGET_SPEC")" && pwd)/$(basename "$TARGET_SPEC")"
if [[ "$INPUT_ABS" != "$TARGET_ABS" ]]; then
cp "$INPUT_SPEC" "$TARGET_SPEC"
fi

cd "$ROOT_DIR"
npm run typegen >&2
npm run zodgen >&2
npm run descgen >&2
# Build the dist/ tree so subprocess invocation (bin/run.js) sees the new
# code. We pass `--force` to bypass tsc's incremental cache: the spec-
# evolution harness regenerates from many different specs in one process,
# and a previous failed build can leave .tsbuildinfo in a state that
# convinces tsc to skip emitting files even after the source has changed.
npx tsc -b --force >&2

echo "$OUTPUT"
4 changes: 3 additions & 1 deletion src/commands/alert-channels/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default class AlertChannelsTest extends Command {
async run() {
const {args, flags} = await this.parse(AlertChannelsTest)
const client = buildClient(flags)
const resp = await checkedFetch(client.POST('/api/v1/alert-channels/{id}/test', {params: {path: {id: args.id}}}))
const resp = (await checkedFetch(
client.POST('/api/v1/alert-channels/{id}/test', {params: {path: {id: args.id}}}),
)) as {data?: {success?: boolean}}
this.log(resp.data?.success ? 'Test notification sent successfully.' : 'Test notification failed.')
}
}
8 changes: 7 additions & 1 deletion src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ export default class AuthLogin extends Command {
const client = createApiClient({baseUrl: apiUrl, token})

try {
const resp = await checkedFetch(client.GET('/api/v1/auth/me'))
const resp = (await checkedFetch(client.GET('/api/v1/auth/me'))) as {
data?: {
organization?: {name?: string; id?: string | number}
key?: {name?: string}
plan?: {tier?: string}
}
}
if (!resp.data) {
throw new Error('Empty response')
}
Expand Down
16 changes: 3 additions & 13 deletions src/commands/auth/me.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Command} from '@oclif/core'
import {globalFlags, buildClient} from '../../lib/base-command.js'
import {checkedFetch} from '../../lib/api-client.js'
import {apiGetSingle} from '../../lib/api-client.js'
import {formatOutput, OutputFormat} from '../../lib/output.js'
import {AuthMeResponseSchema} from '../../lib/response-schemas.js'
import {schemas as apiSchemas} from '../../lib/api-zod.generated.js'

export default class AuthMe extends Command {
static description = 'Show current API key identity, organization, plan, and rate limits'
Expand All @@ -12,17 +12,7 @@ export default class AuthMe extends Command {
async run() {
const {flags} = await this.parse(AuthMe)
const client = buildClient(flags)
const resp = await checkedFetch(client.GET('/api/v1/auth/me'))
if (!resp.data) {
this.error('API returned an empty response for /api/v1/auth/me')
}

const parsed = AuthMeResponseSchema.safeParse(resp.data)
if (!parsed.success) {
this.error(`Unexpected auth/me response shape: ${parsed.error.issues.map((i) => i.message).join(', ')}`)
}

const me = resp.data
const me = await apiGetSingle(client, '/api/v1/auth/me', apiSchemas.AuthMeResponse)

const format = flags.output as OutputFormat
if (format === 'json' || format === 'yaml') {
Expand Down
4 changes: 2 additions & 2 deletions src/commands/data/services/status.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, Args} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../../lib/base-command.js'
import {checkedFetch} from '../../../lib/api-client.js'
import {checkedFetch, unwrapData} from '../../../lib/api-client.js'

export default class DataServicesStatus extends Command {
static description = 'Get the current status of a service'
Expand All @@ -12,6 +12,6 @@ export default class DataServicesStatus extends Command {
const {args, flags} = await this.parse(DataServicesStatus)
const client = buildClient(flags)
const resp = await checkedFetch(client.GET('/api/v1/services/{slugOrId}', {params: {path: {slugOrId: args.slug}}}))
display(this, resp.data ?? resp, flags.output)
display(this, unwrapData(resp), flags.output)
}
}
6 changes: 3 additions & 3 deletions src/commands/data/services/uptime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command, Args, Flags} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../../lib/base-command.js'
import {apiGet} from '../../../lib/api-client.js'
import {apiGet, unwrapData} from '../../../lib/api-client.js'

export default class DataServicesUptime extends Command {
static description = 'Get uptime data for a service'
Expand All @@ -20,7 +20,7 @@ export default class DataServicesUptime extends Command {
const client = buildClient(flags)
const query: Record<string, string> = {period: flags.period}
if (flags.granularity) query.granularity = flags.granularity
const resp = await apiGet<{data?: unknown}>(client, `/api/v1/services/${args.slug}/uptime`, {query})
display(this, resp.data ?? resp, flags.output)
const resp = await apiGet(client, `/api/v1/services/${args.slug}/uptime`, {query})
display(this, unwrapData(resp), flags.output)
}
}
4 changes: 3 additions & 1 deletion src/commands/dependencies/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export default class DependenciesTrack extends Command {
async run() {
const {args, flags} = await this.parse(DependenciesTrack)
const client = buildClient(flags)
const resp = await checkedFetch(client.POST('/api/v1/service-subscriptions/{slug}', {params: {path: {slug: args.slug}}}))
const resp = (await checkedFetch(
client.POST('/api/v1/service-subscriptions/{slug}', {params: {path: {slug: args.slug}}}),
)) as {data?: {name?: string}}
this.log(`Now tracking '${resp.data?.name}' as a dependency.`)
}
}
4 changes: 2 additions & 2 deletions src/commands/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,10 @@ export default class Deploy extends Command {

while (true) {
try {
const resp = await apiPost<{data?: {id?: string}}>(
const resp = (await apiPost(
client, '/api/v1/deploy/lock',
{lockedBy: `${process.env.USER ?? 'cli'}@${hostname()}`, ttlMinutes: DEFAULT_LOCK_TTL},
)
)) as {data?: {id?: string}}
const lockId = resp.data?.id
if (!lockId) {
this.warn('Deploy lock acquired but no lock ID returned. Proceeding without lock protection.')
Expand Down
4 changes: 3 additions & 1 deletion src/commands/incidents/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export default class IncidentsResolve extends Command {
const {args, flags} = await this.parse(IncidentsResolve)
const client = buildClient(flags)
const body = flags.message ? {body: flags.message} : undefined
const resp = await checkedFetch(client.POST('/api/v1/incidents/{id}/resolve', {params: {path: {id: args.id}}, body}))
const resp = (await checkedFetch(
client.POST('/api/v1/incidents/{id}/resolve', {params: {path: {id: args.id}}, body}),
)) as {data?: {incident?: {title?: string}}}
this.log(`Incident '${resp.data?.incident?.title}' resolved.`)
}
}
4 changes: 3 additions & 1 deletion src/commands/monitors/pause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default class MonitorsPause extends Command {
async run() {
const {args, flags} = await this.parse(MonitorsPause)
const client = buildClient(flags)
const resp = await checkedFetch(client.POST('/api/v1/monitors/{id}/pause', {params: {path: {id: args.id}}}))
const resp = (await checkedFetch(
client.POST('/api/v1/monitors/{id}/pause', {params: {path: {id: args.id}}}),
)) as {data?: {name?: string}}
this.log(`Monitor '${resp.data?.name}' paused.`)
}
}
4 changes: 3 additions & 1 deletion src/commands/monitors/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default class MonitorsResume extends Command {
async run() {
const {args, flags} = await this.parse(MonitorsResume)
const client = buildClient(flags)
const resp = await checkedFetch(client.POST('/api/v1/monitors/{id}/resume', {params: {path: {id: args.id}}}))
const resp = (await checkedFetch(
client.POST('/api/v1/monitors/{id}/resume', {params: {path: {id: args.id}}}),
)) as {data?: {name?: string}}
this.log(`Monitor '${resp.data?.name}' resumed.`)
}
}
4 changes: 2 additions & 2 deletions src/commands/monitors/test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Command} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../lib/base-command.js'
import {checkedFetch} from '../../lib/api-client.js'
import {checkedFetch, unwrapData} from '../../lib/api-client.js'
import {uuidArg} from '../../lib/validators.js'

export default class MonitorsTest extends Command {
Expand All @@ -14,6 +14,6 @@ export default class MonitorsTest extends Command {
const client = buildClient(flags)
this.log('Running test...')
const resp = await checkedFetch(client.POST('/api/v1/monitors/{id}/test', {params: {path: {id: args.id}}}))
display(this, resp.data ?? resp, flags.output)
display(this, unwrapData(resp), flags.output)
}
}
9 changes: 3 additions & 6 deletions src/commands/monitors/versions/get.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import {Command, Args} from '@oclif/core'
import {globalFlags, buildClient, display} from '../../../lib/base-command.js'
import {apiGet} from '../../../lib/api-client.js'
import type {components} from '../../../lib/api.generated.js'
import {apiGet, unwrapData} from '../../../lib/api-client.js'
import {uuidArg} from '../../../lib/validators.js'

type MonitorVersionDto = components['schemas']['MonitorVersionDto']

export default class MonitorsVersionsGet extends Command {
static description = 'Get a specific version snapshot for a monitor'
static examples = [
Expand All @@ -23,10 +20,10 @@ export default class MonitorsVersionsGet extends Command {
async run() {
const {args, flags} = await this.parse(MonitorsVersionsGet)
const client = buildClient(flags)
const resp = await apiGet<{data?: MonitorVersionDto}>(
const resp = await apiGet(
client,
`/api/v1/monitors/${args.id}/versions/${args.version}`,
)
display(this, resp.data ?? resp, flags.output)
display(this, unwrapData(resp), flags.output)
}
}
7 changes: 2 additions & 5 deletions src/commands/state/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,8 @@ export default class StateShow extends Command {
this.log(`\nResources (${entries.length}):`)
for (const [address, entry] of entries) {
this.log(` ${address} → ${entry.apiId}`)
const childKeys = Object.keys(entry.children)
if (childKeys.length > 0) {
for (const ck of childKeys) {
this.log(` ${ck} → ${entry.children[ck].apiId}`)
}
for (const [ck, child] of Object.entries(entry.children)) {
this.log(` ${ck} → ${child.apiId}`)
}
}
}
Expand Down
Loading
Loading