Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/fly

src/smoke-test.ts

123 lines · 3.8 KB · TypeScript
import { kody } from 'kody:runtime'
import { listApps } from './apps.ts'
import {
	API_TOKEN_SETUP_URL,
	resolveApiTokenSecretName,
	secretSetupUrl,
	type FlyAuthOptions,
} from './fly-core.ts'
import { destroyMachine, startMachine, stopMachine } from './machines.ts'
import { listOrganizations } from './organizations.ts'
import machineCount from './machine-count.ts'
import type { FlySmokeTestInput } from './types.ts'

function secretEntries(result: unknown): Array<{ name?: string; scope?: string }> {
	if (result && typeof result === 'object' && Array.isArray((result as { secrets?: unknown }).secrets)) {
		return (result as { secrets: Array<{ name?: string; scope?: string }> }).secrets
	}
	return Array.isArray(result) ? result : []
}

async function listUserSecretNames(): Promise<Set<string>> {
	try {
		const listed = await kody.secret_list({ scope: 'user' })
		const names = new Set<string>()
		for (const entry of secretEntries(listed)) {
			if (entry?.name && (entry.scope === 'user' || !entry.scope)) names.add(entry.name)
		}
		return names
	} catch {
		return new Set()
	}
}

async function mutationSelfCheck() {
	const target = { appName: 'my-app', machineId: 'machine_smoke' }
	const start = await startMachine({ ...target, dryRun: true })
	const stop = await stopMachine({ ...target, dryRun: true, timeout: 30 })
	const destroy = await destroyMachine({ ...target, dryRun: true, force: true })
	if (!('dryRun' in start) || start.dryRun !== true) {
		throw new Error('startMachine dryRun self-check failed.')
	}
	if (!('dryRun' in stop) || stop.dryRun !== true) {
		throw new Error('stopMachine dryRun self-check failed.')
	}
	if (!('dryRun' in destroy) || destroy.dryRun !== true) {
		throw new Error('destroyMachine dryRun self-check failed.')
	}
	return { start: true, stop: true, destroy: true }
}

/**
 * Local dry-run checks plus an optional live org/apps read.
 *
 * Never starts, stops, or destroys Machines.
 *
 * @example
 * import smokeTest from 'kody:@kody/fly/smoke-test'
 * const result = await smokeTest()
 */
export async function smokeTest(input: FlySmokeTestInput = {}) {
	const dryRunMutations = await mutationSelfCheck()
	const secretName = resolveApiTokenSecretName(input)
	const names = await listUserSecretNames()
	const hasToken = names.has(secretName)

	if (!hasToken) {
		return {
			ok: true,
			live: false,
			selfCheck: { dryRunMutations },
			secretName,
			setup: {
				auth: 'api-token',
				hosts: ['api.machines.dev', 'api.fly.io'],
				nextSteps: [
					'Save a Fly API token as ' + secretName + '.',
					secretSetupUrl(secretName),
					'Approve hosts api.machines.dev and api.fly.io.',
				],
			},
		}
	}

	const organizations = await listOrganizations({ ...input, first: 10 })
	const orgSlug =
		typeof input.organizationSlug === 'string' && input.organizationSlug.trim()
			? input.organizationSlug.trim()
			: organizations.organizations.length === 1
				? organizations.organizations[0]?.slug
				: undefined
	const apps = orgSlug
		? await listApps({ ...input, organizationSlug: orgSlug })
		: { count: 0, apps: [], organization: { slug: null } }
	const machines =
		input.includeMachines === false || !orgSlug
			? null
			: await machineCount({ ...input, organizationSlug: orgSlug, maxPages: 1 })

	return {
		ok: true,
		live: true,
		selfCheck: { dryRunMutations },
		secretName,
		organizationCount: organizations.organizations.length,
		organizations: organizations.organizations.map((org: any) => ({
			id: org.id,
			name: org.name,
			slug: org.slug,
		})),
		organizationSlug: orgSlug ?? null,
		appCount: apps.count,
		appsSample: apps.apps.slice(0, 5).map((app: any) => ({ name: app.name, status: app.status })),
		machineCount: machines?.count ?? null,
		byState: machines?.byState ?? null,
		setup: {
			auth: 'api-token',
			hosts: ['api.machines.dev', 'api.fly.io'],
			apiTokenUrl: hasToken ? null : API_TOKEN_SETUP_URL,
		},
	}
}

export default smokeTest