import type { Encodable } from "@automate.ax/codec" import type { Vercel } from "@vercel/sdk" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { getVercelApi, getVercelTeamId, vercelAccessTokenAccountOptions, vercelAccountOptions, } from "../lib/api" import { CANCELED_VERCEL_DEPLOYMENT_SCHEMA, CREATED_VERCEL_DEPLOYMENT_SCHEMA, VERCEL_DEPLOYMENT_PAGE_SCHEMA, VERCEL_DEPLOYMENT_SCHEMA, vercelArraySchema, vercelObjectSchema, } from "../lib/schemas" import { VERCEL_DEPLOYMENT_READ_SCOPE, VERCEL_DEPLOYMENT_WRITE_SCOPE, } from "../lib/scopes" const VERCEL_DEPLOYMENT_STATE_SCHEMA = z.enum([ "BLOCKED", "BUILDING", "CANCELED", "ERROR", "INITIALIZING", "QUEUED", "READY", ]) const VERCEL_GIT_SOURCE_SCHEMA = z.discriminatedUnion("type", [ z.object({ /** Git branch or tag. */ ref: z.string().min(1), /** GitHub repository ID. */ repoId: z.union([z.string(), z.number()]), /** Optional exact commit SHA. */ sha: z.string().min(1).optional(), type: z.literal("github"), }), z.object({ /** Git branch or tag. */ ref: z.string().min(1), /** GitLab project ID. */ projectId: z.union([z.string(), z.number()]), /** Optional exact commit SHA. */ sha: z.string().min(1).optional(), type: z.literal("gitlab"), }), z.object({ /** Git branch or tag. */ ref: z.string().min(1), /** Bitbucket repository UUID. */ repoUuid: z.string().min(1), /** Optional exact commit SHA. */ sha: z.string().min(1).optional(), type: z.literal("bitbucket"), /** Bitbucket workspace UUID. */ workspaceUuid: z.string().min(1).optional(), }), ]) const VERCEL_DEPLOYMENT_SCOPE_SCHEMA = z.object({ /** Team ID. OAuth accounts default to the installed team. */ teamId: z.string().min(1).optional(), }) const VERCEL_DEPLOYMENT_EVENTS_SCHEMA = z.custom< Awaited> & Encodable >( (value) => z.union([z.looseObject({}), z.array(z.unknown())]).safeParse(value).success, ) const VERCEL_DEPLOYMENT_FILES_SCHEMA = vercelArraySchema< Awaited> & Encodable >(z.looseObject({ name: z.string(), type: z.string() })) const VERCEL_DEPLOYMENT_ALIASES_SCHEMA = vercelObjectSchema< Awaited> & Encodable >( z.looseObject({ aliases: z.looseObject({ alias: z.string(), uid: z.string() }).array(), }), ) const DELETED_VERCEL_DEPLOYMENT_SCHEMA = vercelObjectSchema< Awaited> & Encodable >(z.looseObject({ state: z.literal("DELETED"), uid: z.string() })) /** Lists Vercel deployments with project, state, branch, and time filters. */ export const listVercelDeployments = defineAction("List Vercel deployments") .describe("Lists a page of deployments in the selected account or team.") .account("vercel", vercelAccountOptions(VERCEL_DEPLOYMENT_READ_SCOPE)) .input( VERCEL_DEPLOYMENT_SCOPE_SCHEMA.extend({ /** Filter by Git branch. */ branch: z.string().min(1).optional(), /** Maximum deployments to return. */ limit: z.number().int().min(1).max(100).prefault(20), /** Filter by project ID. */ projectId: z.string().min(1).optional(), /** Filter by exact commit SHA. */ sha: z.string().min(1).optional(), /** Filter by deployment state. */ state: VERCEL_DEPLOYMENT_STATE_SCHEMA.optional(), /** Filter by target environment such as `production` or `preview`. */ target: z.string().min(1).optional(), /** Include deployments created before this millisecond timestamp. */ until: z.number().int().nonnegative().optional(), }), ) .output(VERCEL_DEPLOYMENT_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getVercelApi(account).deployments.getDeployments({ limit: input.limit, ...(input.branch && { branch: input.branch }), ...(input.projectId && { projectId: input.projectId }), ...(input.sha && { sha: input.sha }), ...(input.state && { state: input.state }), ...(input.target && { target: input.target }), ...(input.until !== undefined && { until: input.until }), ...(getVercelTeamId(account, input.teamId) && { teamId: getVercelTeamId(account, input.teamId), }), }), ) /** Gets one Vercel deployment by ID or hostname. */ export const getVercelDeployment = defineAction("Get Vercel deployment") .describe("Gets complete information for one deployment by ID or hostname.") .account("vercel", vercelAccountOptions(VERCEL_DEPLOYMENT_READ_SCOPE)) .input( VERCEL_DEPLOYMENT_SCOPE_SCHEMA.extend({ /** Deployment ID or hostname. */ deployment: z.string().min(1), /** Include connected Git repository metadata. */ includeGitRepository: z.boolean().prefault(false), }), ) .output(VERCEL_DEPLOYMENT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getVercelApi(account).deployments.getDeployment({ idOrUrl: input.deployment, withGitRepoInfo: input.includeGitRepository ? "true" : "false", ...(getVercelTeamId(account, input.teamId) && { teamId: getVercelTeamId(account, input.teamId), }), }), ) /** Creates a Git-backed deployment or redeploys an existing deployment. */ export const createVercelDeployment = defineAction("Create Vercel deployment") .describe("Creates a Git-backed deployment or redeploys an existing build.") .account("vercel", vercelAccountOptions(VERCEL_DEPLOYMENT_WRITE_SCOPE)) .input( VERCEL_DEPLOYMENT_SCOPE_SCHEMA.extend({ /** Custom environment ID or slug. */ customEnvironment: z.string().min(1).optional(), /** Existing deployment ID to rebuild. */ deploymentId: z.string().min(1).optional(), /** Always build even when Vercel finds an equivalent deployment. */ forceNew: z.boolean().prefault(false), /** Git source for a new deployment. */ gitSource: VERCEL_GIT_SOURCE_SCHEMA.optional(), /** Provider-native string metadata. */ metadata: z.record(z.string(), z.string()).optional(), /** Project name used in the deployment URL. */ name: z.string().min(1), /** Target project ID, overriding `name` for project selection. */ project: z.string().min(1).optional(), /** Skip framework auto-detection confirmation. */ skipAutoDetectionConfirmation: z.boolean().prefault(false), /** `production`, `staging`, or a custom environment identifier. */ target: z.string().min(1).optional(), /** Redeploy the latest commit instead of the original commit. */ withLatestCommit: z.boolean().prefault(false), }).refine((input) => input.deploymentId || input.gitSource, { message: "Provide deploymentId or gitSource.", path: ["deploymentId"], }), ) .output(CREATED_VERCEL_DEPLOYMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => getVercelApi(account).deployments.createDeployment({ forceNew: input.forceNew ? "1" : undefined, skipAutoDetectionConfirmation: input.skipAutoDetectionConfirmation ? "1" : undefined, requestBody: { name: input.name, ...(input.customEnvironment && { customEnvironmentSlugOrId: input.customEnvironment, }), ...(input.deploymentId && { deploymentId: input.deploymentId }), ...(input.gitSource && { gitSource: input.gitSource }), ...(input.metadata && { meta: input.metadata }), ...(input.project && { project: input.project }), ...(input.target && { target: input.target }), ...(input.withLatestCommit && { withLatestCommit: true }), }, ...(getVercelTeamId(account, input.teamId) && { teamId: getVercelTeamId(account, input.teamId), }), }), ) /** Cancels a Vercel deployment that is still building. */ export const cancelVercelDeployment = defineAction("Cancel Vercel deployment") .describe("Stops a deployment that has not reached a terminal state.") .account("vercel", vercelAccountOptions(VERCEL_DEPLOYMENT_WRITE_SCOPE)) .input( VERCEL_DEPLOYMENT_SCOPE_SCHEMA.extend({ /** Deployment ID. */ deploymentId: z.string().min(1), }), ) .output(CANCELED_VERCEL_DEPLOYMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => getVercelApi(account).deployments.cancelDeployment({ id: input.deploymentId, ...(getVercelTeamId(account, input.teamId) && { teamId: getVercelTeamId(account, input.teamId), }), }), ) /** Promotes an existing Vercel deployment to production without rebuilding. */ export const promoteVercelDeployment = defineAction("Promote Vercel deployment") .describe("Points production traffic to an existing deployment.") .account("vercel", vercelAccessTokenAccountOptions()) .input( VERCEL_DEPLOYMENT_SCOPE_SCHEMA.extend({ /** Deployment ID to promote. */ deploymentId: z.string().min(1), /** Owning project ID. */ projectId: z.string().min(1), }), ) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => getVercelApi(account).projects.requestPromote({ deploymentId: input.deploymentId, projectId: input.projectId, ...(getVercelTeamId(account, input.teamId) && { teamId: getVercelTeamId(account, input.teamId), }), }), ) /** Gets build events and logs for one Vercel deployment. */ export const getVercelDeploymentEvents = defineAction( "Get Vercel deployment events", ) .describe("Gets a bounded page of build events and logs for one deployment.") .account("vercel", vercelAccountOptions(VERCEL_DEPLOYMENT_READ_SCOPE)) .input( VERCEL_DEPLOYMENT_SCOPE_SCHEMA.extend({ /** Deployment ID or hostname. */ deployment: z.string().min(1), /** Sort direction by timestamp. */ direction: z.enum(["backward", "forward"]).prefault("forward"), /** Maximum events to return. */ limit: z.number().int().min(1).max(1000).prefault(100), /** Include events at or after this millisecond timestamp. */ since: z.number().int().nonnegative().optional(), /** Filter by one status code or range such as `500-599`. */ statusCode: z.union([z.number().int(), z.string().min(1)]).optional(), /** Include events at or before this millisecond timestamp. */ until: z.number().int().nonnegative().optional(), }), ) .output(VERCEL_DEPLOYMENT_EVENTS_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getVercelApi(account).deployments.getDeploymentEvents({ direction: input.direction, idOrUrl: input.deployment, limit: input.limit, ...(input.since !== undefined && { since: input.since }), ...(input.statusCode !== undefined && { statusCode: input.statusCode }), ...(input.until !== undefined && { until: input.until }), ...(getVercelTeamId(account, input.teamId) && { teamId: getVercelTeamId(account, input.teamId), }), }), ) /** Lists files in one Vercel deployment. */ export const listVercelDeploymentFiles = defineAction( "List Vercel deployment files", ) .describe("Lists file-tree metadata for one deployment.") .account("vercel", vercelAccountOptions(VERCEL_DEPLOYMENT_READ_SCOPE)) .input( VERCEL_DEPLOYMENT_SCOPE_SCHEMA.extend({ /** Deployment ID. */ deploymentId: z.string().min(1), }), ) .output(VERCEL_DEPLOYMENT_FILES_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getVercelApi(account).deployments.listDeploymentFiles({ id: input.deploymentId, ...(getVercelTeamId(account, input.teamId) && { teamId: getVercelTeamId(account, input.teamId), }), }), ) /** Lists aliases assigned to one Vercel deployment. */ export const listVercelDeploymentAliases = defineAction( "List Vercel deployment aliases", ) .describe("Lists aliases currently assigned to one deployment.") .account("vercel", vercelAccountOptions(VERCEL_DEPLOYMENT_READ_SCOPE)) .input( VERCEL_DEPLOYMENT_SCOPE_SCHEMA.extend({ /** Deployment ID. */ deploymentId: z.string().min(1), }), ) .output(VERCEL_DEPLOYMENT_ALIASES_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getVercelApi(account).aliases.listDeploymentAliases({ id: input.deploymentId, ...(getVercelTeamId(account, input.teamId) && { teamId: getVercelTeamId(account, input.teamId), }), }), ) /** Permanently deletes one Vercel deployment. */ export const deleteVercelDeployment = defineAction("Delete Vercel deployment") .describe("Permanently deletes one deployment by ID.") .account("vercel", vercelAccountOptions(VERCEL_DEPLOYMENT_WRITE_SCOPE)) .input( VERCEL_DEPLOYMENT_SCOPE_SCHEMA.extend({ /** Deployment ID. */ deploymentId: z.string().min(1), }), ) .output(DELETED_VERCEL_DEPLOYMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => getVercelApi(account).deployments.deleteDeployment({ id: input.deploymentId, ...(getVercelTeamId(account, input.teamId) && { teamId: getVercelTeamId(account, input.teamId), }), }), ) /** Rolls one Vercel project back to an earlier deployment. */ export const rollbackVercelDeployment = defineAction( "Rollback Vercel deployment", ) .describe("Rolls one project back to an earlier deployment.") .account("vercel", vercelAccessTokenAccountOptions()) .input( z.object({ /** Target deployment ID. */ deploymentId: z.string().min(1), /** Human-readable reason for the rollback. */ description: z.string().optional(), /** Project ID. */ projectId: z.string().min(1), /** Team ID for a team-owned project. */ teamId: z.string().min(1).optional(), }), ) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => getVercelApi(account).projects.requestRollback({ deploymentId: input.deploymentId, projectId: input.projectId, ...(input.description !== undefined && { description: input.description, }), ...(input.teamId && { teamId: input.teamId }), }), )