import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { EnvironmentPauseSource } from "@trigger.dev/database"; import { z } from "zod"; import { prisma } from "~/db.server"; import { requireAdminApiRequest } from "~/services/personalAccessToken.server"; import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server"; import { PauseEnvironmentService } from "~/v3/services/pauseEnvironment.server"; const ParamsSchema = z.object({ organizationId: z.string(), }); const BodySchema = z.object({ enable: z.boolean(), }); /** * Enable or disable runs for an organization and pause/resume its non-dev environments. * * Billing-limit-paused environments are left unchanged when enabling or disabling runs; * they are reported in `skipped`, not counted in the update total. Other per-environment * failures are returned in `failures` (HTTP 409 when every environment fails, otherwise * HTTP 200). */ export async function action({ request, params }: ActionFunctionArgs) { await requireAdminApiRequest(request); const { organizationId } = ParamsSchema.parse(params); const body = BodySchema.safeParse(await request.json()); if (!body.success) { return json({ error: "Invalid request body", details: body.error }, { status: 400 }); } const organization = await prisma.organization.update({ where: { id: organizationId, }, data: { runsEnabled: body.data.enable, }, }); if (!organization) { return json({ error: "Organization not found" }, { status: 404 }); } // `runsEnabled` is embedded in every env of the org; drop all its cached env rows. The // per-env pause writes below invalidate their own envs via PauseEnvironmentService. controlPlaneResolver.invalidateOrganization(organizationId); const environments = await prisma.runtimeEnvironment.findMany({ where: { organizationId, type: { not: "DEVELOPMENT", }, }, include: { organization: true, project: true, }, }); const pauseEnvironmentService = new PauseEnvironmentService(); const pauseAction = body.data.enable ? "resumed" : "paused"; const failures: Array<{ environmentId: string; error: string }> = []; const skipped: Array<{ environmentId: string; reason: string }> = []; let updatedCount = 0; for (const environment of environments) { if (environment.pauseSource === EnvironmentPauseSource.BILLING_LIMIT) { if (!body.data.enable) { skipped.push({ environmentId: environment.id, reason: "Environment is already paused due to billing limit and was left unchanged.", }); continue; } skipped.push({ environmentId: environment.id, reason: "Environment is paused due to billing limit and was left unchanged. Resolve the billing limit to resume.", }); continue; } const result = await pauseEnvironmentService.call( { ...environment, organization }, pauseAction ); if (result.success) { updatedCount++; } else { failures.push({ environmentId: environment.id, error: result.error }); } } const stateLabel = body.data.enable ? "enabled" : "disabled"; const message = `${updatedCount} of ${environments.length} environments updated to ${stateLabel}`; if (failures.length > 0) { return json( { success: false, message, failures, ...(skipped.length > 0 ? { skipped } : {}), }, { status: updatedCount === 0 ? 409 : 200 } ); } return json({ success: true, message, ...(skipped.length > 0 ? { skipped } : {}), }); }