import { getLinearApi } from "@automate.ax/integration-contracts/linear" import { LINEAR_CYCLE_SCHEMA, LINEAR_PAGE_INFO_SCHEMA, LINEAR_PAGE_INPUT_SCHEMA, linearConnectionSchema, } from "@automate.ax/integration-contracts/linear" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { LINEAR_CYCLE_FIELDS } from "../lib/fragments" import { LINEAR_READ_ACCOUNT_OPTIONS, LINEAR_WRITE_ACCOUNT_OPTIONS, } from "../lib/scopes" const CYCLE_RESPONSE_SCHEMA = z.object({ cycle: LINEAR_CYCLE_SCHEMA }) const CYCLE_CONNECTION_SCHEMA = linearConnectionSchema(LINEAR_CYCLE_SCHEMA) const CYCLE_PAGE_SCHEMA = z.object({ cycles: LINEAR_CYCLE_SCHEMA.array(), pageInfo: LINEAR_PAGE_INFO_SCHEMA, }) const CYCLE_MUTATION_SCHEMA = z.object({ cycleMutation: z.object({ cycle: LINEAR_CYCLE_SCHEMA, success: z.literal(true), }), }) const CYCLE_ARCHIVE_SCHEMA = z.object({ cycleMutation: z.object({ entity: LINEAR_CYCLE_SCHEMA.nullable(), success: z.literal(true), }), }) const CYCLE_FIELDS_INPUT_SCHEMA = z.object({ /** When the cycle completed, or `null` to reopen it. */ completedAt: z.iso.datetime({ offset: true }).nullable().optional(), /** Cycle description, or `null` to clear it. */ description: z.string().nullable().optional(), /** Scheduled cycle end. */ endsAt: z.iso.datetime({ offset: true }).optional(), /** Custom cycle name, or `null` to clear it. */ name: z.string().min(1).nullable().optional(), /** Scheduled cycle start. */ startsAt: z.iso.datetime({ offset: true }).optional(), }) /** Lists Linear cycles, optionally limited to one team. */ export const listLinearCycles = defineAction("List Linear cycles") .describe("Lists a cursor-paginated page of Linear cycles.") .account("linear", LINEAR_READ_ACCOUNT_OPTIONS) .input( LINEAR_PAGE_INPUT_SCHEMA.extend({ /** Sort by creation time or most recent update. */ orderBy: z.enum(["createdAt", "updatedAt"]).prefault("updatedAt"), /** Team whose cycles should be returned. */ teamId: z.string().min(1).optional(), }), ) .output(CYCLE_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { cycles } = await getLinearApi(account.secret).graphql( `query LinearCycles( $first: Int! $after: String $includeArchived: Boolean $orderBy: PaginationOrderBy! $filter: CycleFilter ) { cycles( first: $first after: $after includeArchived: $includeArchived orderBy: $orderBy filter: $filter ) { nodes { ${LINEAR_CYCLE_FIELDS} } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } }`, { responseSchema: z.object({ cycles: CYCLE_CONNECTION_SCHEMA }), variables: { first: input.limit, orderBy: input.orderBy, ...(input.after && { after: input.after }), ...(input.includeArchived !== undefined && { includeArchived: input.includeArchived, }), ...(input.teamId && { filter: { team: { id: { eq: input.teamId } } }, }), }, }, ) return { cycles: cycles.nodes, pageInfo: cycles.pageInfo! } }) /** Gets one Linear cycle by ID. */ export const getLinearCycle = defineAction("Get Linear cycle") .describe("Gets one Linear cycle by its stable ID.") .account("linear", LINEAR_READ_ACCOUNT_OPTIONS) .input(z.object({ cycleId: z.string().min(1) })) .output(LINEAR_CYCLE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => getLinearApi(account.secret) .graphql( `query LinearCycle($id: String!) { cycle(id: $id) { ${LINEAR_CYCLE_FIELDS} } }`, { responseSchema: CYCLE_RESPONSE_SCHEMA, variables: { id: input.cycleId }, }, ) .then(({ cycle }) => cycle), ) /** Creates a dated Linear cycle for one team. */ export const createLinearCycle = defineAction("Create Linear cycle") .describe("Creates a Linear cycle with explicit start and end times.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input( CYCLE_FIELDS_INPUT_SCHEMA.extend({ endsAt: z.iso.datetime({ offset: true }), startsAt: z.iso.datetime({ offset: true }), teamId: z.string().min(1), }), ) .output(LINEAR_CYCLE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { cycleMutation } = await getLinearApi(account.secret).graphql( `mutation LinearCycleCreate($input: CycleCreateInput!) { cycleMutation: cycleCreate(input: $input) { success cycle { ${LINEAR_CYCLE_FIELDS} } } }`, { responseSchema: CYCLE_MUTATION_SCHEMA, variables: { input } }, ) return cycleMutation.cycle }) /** Updates the dates, completion, name, or description of a Linear cycle. */ export const updateLinearCycle = defineAction("Update Linear cycle") .describe("Updates a Linear cycle's editable planning fields.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input(CYCLE_FIELDS_INPUT_SCHEMA.extend({ cycleId: z.string().min(1) })) .output(LINEAR_CYCLE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { cycleId, ...update } = input const { cycleMutation } = await getLinearApi(account.secret).graphql( `mutation LinearCycleUpdate($id: String!, $input: CycleUpdateInput!) { cycleMutation: cycleUpdate(id: $id, input: $input) { success cycle { ${LINEAR_CYCLE_FIELDS} } } }`, { responseSchema: CYCLE_MUTATION_SCHEMA, variables: { id: cycleId, input: update }, }, ) return cycleMutation.cycle }) /** Archives a Linear cycle. */ export const archiveLinearCycle = defineAction("Archive Linear cycle") .describe("Archives a Linear cycle by ID.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input(z.object({ cycleId: z.string().min(1) })) .output(LINEAR_CYCLE_SCHEMA.nullable()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { cycleMutation } = await getLinearApi(account.secret).graphql( `mutation LinearCycleArchive($id: String!) { cycleMutation: cycleArchive(id: $id) { success entity { ${LINEAR_CYCLE_FIELDS} } } }`, { responseSchema: CYCLE_ARCHIVE_SCHEMA, variables: { id: input.cycleId }, }, ) return cycleMutation.entity })