import { getLinearApi } from "@automate.ax/integration-contracts/linear" import { LINEAR_ISSUE_LABEL_SCHEMA } from "@automate.ax/integration-contracts/linear" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { LINEAR_ISSUE_LABEL_FIELDS } from "../lib/fragments" import { LINEAR_WRITE_ACCOUNT_OPTIONS } from "../lib/scopes" const ISSUE_LABEL_MUTATION_SCHEMA = z.object({ issueLabelMutation: z.object({ issueLabel: LINEAR_ISSUE_LABEL_SCHEMA, success: z.literal(true), }), }) const ISSUE_LABEL_DELETE_SCHEMA = z.object({ issueLabelMutation: z.object({ entityId: z.string(), success: z.literal(true), }), }) const ISSUE_LABEL_FIELDS_INPUT_SCHEMA = z.object({ /** Label display color. */ color: z.string().min(1).optional(), /** Label description, or `null` to clear it. */ description: z.string().nullable().optional(), /** Whether the label groups child labels. */ isGroup: z.boolean().optional(), /** Human-readable label name. */ name: z.string().min(1).optional(), /** Parent label ID, or `null` to make this a root label. */ parentId: z.string().min(1).nullable().optional(), }) /** Creates a workspace-wide or team-specific Linear issue label. */ export const createLinearIssueLabel = defineAction("Create Linear issue label") .describe("Creates a workspace-wide or team-specific issue label.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input( ISSUE_LABEL_FIELDS_INPUT_SCHEMA.extend({ name: z.string().min(1), /** Replace same-named team labels with this workspace label. */ replaceTeamLabels: z.boolean().optional(), /** Owning team, or omit for a workspace-wide label. */ teamId: z.string().min(1).optional(), }), ) .output(LINEAR_ISSUE_LABEL_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { replaceTeamLabels, ...label } = input const { issueLabelMutation } = await getLinearApi(account.secret).graphql( `mutation LinearIssueLabelCreate( $input: IssueLabelCreateInput! $replaceTeamLabels: Boolean ) { issueLabelMutation: issueLabelCreate( input: $input replaceTeamLabels: $replaceTeamLabels ) { success issueLabel { ${LINEAR_ISSUE_LABEL_FIELDS} } } }`, { responseSchema: ISSUE_LABEL_MUTATION_SCHEMA, variables: { input: label, ...(replaceTeamLabels !== undefined && { replaceTeamLabels }), }, }, ) return issueLabelMutation.issueLabel }) /** Updates a Linear issue label. */ export const updateLinearIssueLabel = defineAction("Update Linear issue label") .describe("Updates a Linear issue label's name, color, or hierarchy.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input( ISSUE_LABEL_FIELDS_INPUT_SCHEMA.extend({ issueLabelId: z.string().min(1), /** Replace same-named team labels with this workspace label. */ replaceTeamLabels: z.boolean().optional(), }), ) .output(LINEAR_ISSUE_LABEL_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { issueLabelId, replaceTeamLabels, ...update } = input const { issueLabelMutation } = await getLinearApi(account.secret).graphql( `mutation LinearIssueLabelUpdate( $id: String! $input: IssueLabelUpdateInput! $replaceTeamLabels: Boolean ) { issueLabelMutation: issueLabelUpdate( id: $id input: $input replaceTeamLabels: $replaceTeamLabels ) { success issueLabel { ${LINEAR_ISSUE_LABEL_FIELDS} } } }`, { responseSchema: ISSUE_LABEL_MUTATION_SCHEMA, variables: { id: issueLabelId, input: update, ...(replaceTeamLabels !== undefined && { replaceTeamLabels }), }, }, ) return issueLabelMutation.issueLabel }) /** Retires a Linear issue label without removing it from existing issues. */ export const retireLinearIssueLabel = defineAction("Retire Linear issue label") .describe("Retires a label so it cannot be added to new issues.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input(z.object({ issueLabelId: z.string().min(1) })) .output(LINEAR_ISSUE_LABEL_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateIssueLabel(account.secret, "issueLabelRetire", input.issueLabelId), ) /** Restores a retired Linear issue label. */ export const restoreLinearIssueLabel = defineAction( "Restore Linear issue label", ) .describe("Restores a retired Linear issue label for new use.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input(z.object({ issueLabelId: z.string().min(1) })) .output(LINEAR_ISSUE_LABEL_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateIssueLabel(account.secret, "issueLabelRestore", input.issueLabelId), ) /** Permanently deletes a Linear issue label. */ export const deleteLinearIssueLabel = defineAction("Delete Linear issue label") .describe("Permanently deletes a Linear issue label by ID.") .account("linear", LINEAR_WRITE_ACCOUNT_OPTIONS) .input(z.object({ issueLabelId: z.string().min(1) })) .output(z.object({ issueLabelId: z.string() })) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { issueLabelMutation } = await getLinearApi(account.secret).graphql( `mutation LinearIssueLabelDelete($id: String!) { issueLabelMutation: issueLabelDelete(id: $id) { success entityId } }`, { responseSchema: ISSUE_LABEL_DELETE_SCHEMA, variables: { id: input.issueLabelId }, }, ) return { issueLabelId: issueLabelMutation.entityId } }) /** * Executes a Linear issue-label lifecycle mutation. * * @param secret - Resolved Linear credential. * @param mutation - Provider label lifecycle mutation. * @param issueLabelId - Label to mutate. */ async function mutateIssueLabel( secret: Record, mutation: "issueLabelRestore" | "issueLabelRetire", issueLabelId: string, ) { const { issueLabelMutation } = await getLinearApi(secret).graphql( mutation === "issueLabelRetire" ? `mutation LinearIssueLabelRetire($id: String!) { issueLabelMutation: issueLabelRetire(id: $id) { success issueLabel { ${LINEAR_ISSUE_LABEL_FIELDS} } } }` : `mutation LinearIssueLabelRestore($id: String!) { issueLabelMutation: issueLabelRestore(id: $id) { success issueLabel { ${LINEAR_ISSUE_LABEL_FIELDS} } } }`, { responseSchema: ISSUE_LABEL_MUTATION_SCHEMA, variables: { id: issueLabelId }, }, ) return issueLabelMutation.issueLabel }