From d6b44f048859670abac28dfe363f763872534e76 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 8 May 2026 15:13:58 -0400 Subject: [PATCH 1/5] feat(netlify): add Netlify block with deploys, env vars, and deploy triggers Adds the Netlify integration with nine tool operations (list sites, list/get/cancel/create deploys, and list/create/update/delete env vars) plus six webhook triggers (deploy created, building, succeeded, failed, locked, unlocked). Tools authenticate via a Netlify Personal Access Token (Bearer). Triggers automatically register an outgoing webhook on the chosen site at deploy time and clean it up on undeploy. Inbound webhook signatures are verified as JWTs (HS256, iss=netlify, sha256 claim of the raw body) without adding a jsonwebtoken dependency. --- apps/sim/blocks/blocks/netlify.ts | 428 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 24 + apps/sim/lib/webhooks/providers/netlify.ts | 282 ++++++++++++ apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/tools/netlify/cancel_deploy.ts | 101 +++++ apps/sim/tools/netlify/create_deploy.ts | 104 +++++ apps/sim/tools/netlify/create_env_var.ts | 127 ++++++ apps/sim/tools/netlify/delete_env_var.ts | 69 +++ apps/sim/tools/netlify/get_deploy.ts | 98 ++++ apps/sim/tools/netlify/index.ts | 23 + apps/sim/tools/netlify/list_deploys.ts | 162 +++++++ apps/sim/tools/netlify/list_env_vars.ts | 113 +++++ apps/sim/tools/netlify/list_sites.ts | 119 +++++ apps/sim/tools/netlify/types.ts | 180 ++++++++ apps/sim/tools/netlify/update_env_var.ts | 125 +++++ apps/sim/tools/netlify/utils.ts | 59 +++ apps/sim/tools/registry.ts | 21 + apps/sim/triggers/netlify/deploy_building.ts | 34 ++ apps/sim/triggers/netlify/deploy_created.ts | 38 ++ apps/sim/triggers/netlify/deploy_failed.ts | 34 ++ apps/sim/triggers/netlify/deploy_locked.ts | 34 ++ apps/sim/triggers/netlify/deploy_succeeded.ts | 34 ++ apps/sim/triggers/netlify/deploy_unlocked.ts | 34 ++ apps/sim/triggers/netlify/index.ts | 6 + apps/sim/triggers/netlify/utils.ts | 137 ++++++ apps/sim/triggers/registry.ts | 14 + 27 files changed, 2404 insertions(+) create mode 100644 apps/sim/blocks/blocks/netlify.ts create mode 100644 apps/sim/lib/webhooks/providers/netlify.ts create mode 100644 apps/sim/tools/netlify/cancel_deploy.ts create mode 100644 apps/sim/tools/netlify/create_deploy.ts create mode 100644 apps/sim/tools/netlify/create_env_var.ts create mode 100644 apps/sim/tools/netlify/delete_env_var.ts create mode 100644 apps/sim/tools/netlify/get_deploy.ts create mode 100644 apps/sim/tools/netlify/index.ts create mode 100644 apps/sim/tools/netlify/list_deploys.ts create mode 100644 apps/sim/tools/netlify/list_env_vars.ts create mode 100644 apps/sim/tools/netlify/list_sites.ts create mode 100644 apps/sim/tools/netlify/types.ts create mode 100644 apps/sim/tools/netlify/update_env_var.ts create mode 100644 apps/sim/tools/netlify/utils.ts create mode 100644 apps/sim/triggers/netlify/deploy_building.ts create mode 100644 apps/sim/triggers/netlify/deploy_created.ts create mode 100644 apps/sim/triggers/netlify/deploy_failed.ts create mode 100644 apps/sim/triggers/netlify/deploy_locked.ts create mode 100644 apps/sim/triggers/netlify/deploy_succeeded.ts create mode 100644 apps/sim/triggers/netlify/deploy_unlocked.ts create mode 100644 apps/sim/triggers/netlify/index.ts create mode 100644 apps/sim/triggers/netlify/utils.ts diff --git a/apps/sim/blocks/blocks/netlify.ts b/apps/sim/blocks/blocks/netlify.ts new file mode 100644 index 0000000000..5469c63f10 --- /dev/null +++ b/apps/sim/blocks/blocks/netlify.ts @@ -0,0 +1,428 @@ +import { NetlifyIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import { getTrigger } from '@/triggers' + +export const NetlifyBlock: BlockConfig = { + type: 'netlify', + name: 'Netlify', + description: 'Manage Netlify sites, deploys, and environment variables', + longDescription: + 'Trigger and inspect Netlify deploys (builds), and manage account or site-scoped environment variables. Generate a Personal Access Token at https://app.netlify.com/user/applications#personal-access-tokens.', + docsLink: 'https://docs.sim.ai/tools/netlify', + category: 'tools', + integrationType: IntegrationType.DeveloperTools, + tags: ['cloud', 'ci-cd'], + bgColor: '#00C7B7', + icon: NetlifyIcon, + authMode: AuthMode.ApiKey, + triggers: { + enabled: true, + available: [ + 'netlify_deploy_created', + 'netlify_deploy_building', + 'netlify_deploy_succeeded', + 'netlify_deploy_failed', + 'netlify_deploy_locked', + 'netlify_deploy_unlocked', + ], + }, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Sites + { label: 'List Sites', id: 'list_sites' }, + // Deploys + { label: 'List Deploys', id: 'list_deploys' }, + { label: 'Get Deploy', id: 'get_deploy' }, + { label: 'Create Deploy', id: 'create_deploy' }, + { label: 'Cancel Deploy', id: 'cancel_deploy' }, + // Environment Variables + { label: 'List Environment Variables', id: 'list_env_vars' }, + { label: 'Create Environment Variable', id: 'create_env_var' }, + { label: 'Update Environment Variable', id: 'update_env_var' }, + { label: 'Delete Environment Variable', id: 'delete_env_var' }, + ], + value: () => 'list_sites', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter Netlify Personal Access Token', + required: true, + password: true, + }, + { + id: 'setupInstructions', + title: 'Setup Instructions', + type: 'text', + hideFromPreview: true, + defaultValue: [ + 'Sign in to Netlify.', + 'Open User settings → Applications → Personal access tokens (direct link).', + 'Click "New access token", give it a description, and choose an expiration.', + 'Copy the token and paste it into the API Key field above.', + ] + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join(''), + }, + + // === Sites filters === + { + id: 'siteName', + title: 'Name Filter', + type: 'short-input', + placeholder: 'Filter sites by name (optional)', + condition: { field: 'operation', value: 'list_sites' }, + mode: 'advanced', + }, + { + id: 'sitesFilter', + title: 'Scope', + type: 'dropdown', + options: [ + { label: 'All', id: 'all' }, + { label: 'Owner', id: 'owner' }, + { label: 'Guest', id: 'guest' }, + ], + condition: { field: 'operation', value: 'list_sites' }, + mode: 'advanced', + }, + + // === Deploy fields === + { + id: 'siteId', + title: 'Site ID', + type: 'short-input', + placeholder: 'Site ID or primary domain', + condition: { field: 'operation', value: ['list_deploys', 'create_deploy'] }, + required: { field: 'operation', value: ['list_deploys', 'create_deploy'] }, + }, + { + id: 'deployId', + title: 'Deploy ID', + type: 'short-input', + placeholder: 'Deploy ID', + condition: { field: 'operation', value: ['get_deploy', 'cancel_deploy'] }, + required: { field: 'operation', value: ['get_deploy', 'cancel_deploy'] }, + }, + { + id: 'branchFilter', + title: 'Branch Filter', + type: 'short-input', + placeholder: 'Filter by branch (optional)', + condition: { field: 'operation', value: 'list_deploys' }, + mode: 'advanced', + }, + { + id: 'stateFilter', + title: 'State Filter', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: 'Ready', id: 'ready' }, + { label: 'Building', id: 'building' }, + { label: 'Enqueued', id: 'enqueued' }, + { label: 'Processing', id: 'processing' }, + { label: 'Uploading', id: 'uploading' }, + { label: 'Error', id: 'error' }, + { label: 'New', id: 'new' }, + ], + condition: { field: 'operation', value: 'list_deploys' }, + mode: 'advanced', + }, + { + id: 'productionFilter', + title: 'Production Only', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + condition: { field: 'operation', value: 'list_deploys' }, + mode: 'advanced', + }, + { + id: 'deployBranch', + title: 'Branch', + type: 'short-input', + placeholder: 'Git branch to deploy (defaults to production branch)', + condition: { field: 'operation', value: 'create_deploy' }, + }, + { + id: 'deployTitle', + title: 'Title', + type: 'short-input', + placeholder: 'Deploy label shown in logs (optional)', + condition: { field: 'operation', value: 'create_deploy' }, + mode: 'advanced', + }, + { + id: 'clearCache', + title: 'Clear Cache', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + condition: { field: 'operation', value: 'create_deploy' }, + mode: 'advanced', + }, + + // === Env var fields === + { + id: 'accountId', + title: 'Account ID', + type: 'short-input', + placeholder: 'Account ID or slug', + condition: { + field: 'operation', + value: ['list_env_vars', 'create_env_var', 'update_env_var', 'delete_env_var'], + }, + required: { + field: 'operation', + value: ['list_env_vars', 'create_env_var', 'update_env_var', 'delete_env_var'], + }, + }, + { + id: 'envSiteId', + title: 'Site ID', + type: 'short-input', + placeholder: 'Optional site ID (omit for account-level)', + condition: { + field: 'operation', + value: ['list_env_vars', 'create_env_var', 'update_env_var', 'delete_env_var'], + }, + mode: 'advanced', + }, + { + id: 'envKey', + title: 'Key', + type: 'short-input', + placeholder: 'Variable name (e.g., DATABASE_URL)', + condition: { + field: 'operation', + value: ['create_env_var', 'update_env_var', 'delete_env_var'], + }, + required: { + field: 'operation', + value: ['create_env_var', 'update_env_var', 'delete_env_var'], + }, + }, + { + id: 'envValue', + title: 'Value', + type: 'short-input', + placeholder: 'Variable value', + condition: { field: 'operation', value: ['create_env_var', 'update_env_var'] }, + required: { field: 'operation', value: ['create_env_var', 'update_env_var'] }, + }, + { + id: 'envContext', + title: 'Context', + type: 'dropdown', + options: [ + { label: 'All Contexts', id: 'all' }, + { label: 'Production', id: 'production' }, + { label: 'Deploy Preview', id: 'deploy-preview' }, + { label: 'Branch Deploy', id: 'branch-deploy' }, + { label: 'Dev', id: 'dev' }, + ], + condition: { field: 'operation', value: ['create_env_var', 'update_env_var'] }, + mode: 'advanced', + }, + { + id: 'envScopes', + title: 'Scopes', + type: 'short-input', + placeholder: 'builds,functions,runtime,post_processing', + condition: { field: 'operation', value: ['create_env_var', 'update_env_var'] }, + mode: 'advanced', + }, + { + id: 'envIsSecret', + title: 'Secret', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + condition: { field: 'operation', value: ['create_env_var', 'update_env_var'] }, + mode: 'advanced', + }, + + // === Trigger subBlocks === + ...getTrigger('netlify_deploy_created').subBlocks, + ...getTrigger('netlify_deploy_building').subBlocks, + ...getTrigger('netlify_deploy_succeeded').subBlocks, + ...getTrigger('netlify_deploy_failed').subBlocks, + ...getTrigger('netlify_deploy_locked').subBlocks, + ...getTrigger('netlify_deploy_unlocked').subBlocks, + ], + tools: { + access: [ + 'netlify_list_sites', + 'netlify_list_deploys', + 'netlify_get_deploy', + 'netlify_create_deploy', + 'netlify_cancel_deploy', + 'netlify_list_env_vars', + 'netlify_create_env_var', + 'netlify_update_env_var', + 'netlify_delete_env_var', + ], + config: { + tool: (params) => `netlify_${params.operation}`, + params: (params) => { + const { + apiKey, + operation, + siteName, + sitesFilter, + branchFilter, + stateFilter, + productionFilter, + deployBranch, + deployTitle, + clearCache, + envSiteId, + envKey, + envValue, + envContext, + envScopes, + envIsSecret, + ...rest + } = params + + const base = { ...rest, apiKey } + + switch (operation) { + case 'list_sites': + return { + ...base, + ...(siteName ? { name: siteName } : {}), + ...(sitesFilter ? { filter: sitesFilter } : {}), + } + case 'list_deploys': + return { + ...base, + ...(branchFilter ? { branch: branchFilter } : {}), + ...(stateFilter ? { state: stateFilter } : {}), + ...(productionFilter ? { production: productionFilter } : {}), + } + case 'create_deploy': + return { + ...base, + ...(deployBranch ? { branch: deployBranch } : {}), + ...(deployTitle ? { title: deployTitle } : {}), + ...(clearCache ? { clearCache } : {}), + } + case 'list_env_vars': + return { + ...base, + ...(envSiteId ? { siteId: envSiteId } : {}), + } + case 'create_env_var': + case 'update_env_var': + return { + ...base, + ...(envSiteId ? { siteId: envSiteId } : {}), + key: envKey, + value: envValue, + ...(envContext ? { context: envContext } : {}), + ...(envScopes ? { scopes: envScopes } : {}), + ...(envIsSecret ? { isSecret: envIsSecret } : {}), + } + case 'delete_env_var': + return { + ...base, + ...(envSiteId ? { siteId: envSiteId } : {}), + key: envKey, + } + default: + return base + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Netlify Personal Access Token' }, + siteId: { type: 'string', description: 'Site ID or primary domain' }, + siteName: { type: 'string', description: 'Site name filter' }, + sitesFilter: { type: 'string', description: 'Site scope filter' }, + deployId: { type: 'string', description: 'Deploy ID' }, + branchFilter: { type: 'string', description: 'Branch filter for list deploys' }, + stateFilter: { type: 'string', description: 'State filter for list deploys' }, + productionFilter: { type: 'string', description: 'Production-only filter for list deploys' }, + deployBranch: { type: 'string', description: 'Branch to build for create deploy' }, + deployTitle: { type: 'string', description: 'Deploy title shown in logs' }, + clearCache: { type: 'string', description: 'Clear build cache before deploying' }, + accountId: { type: 'string', description: 'Account ID or slug' }, + envSiteId: { type: 'string', description: 'Site ID scope for env var operations' }, + envKey: { type: 'string', description: 'Environment variable key' }, + envValue: { type: 'string', description: 'Environment variable value' }, + envContext: { type: 'string', description: 'Deploy context for the value' }, + envScopes: { type: 'string', description: 'Comma-separated scopes' }, + envIsSecret: { type: 'string', description: 'Mark the value as secret' }, + }, + outputs: { + sites: { + type: 'array', + description: 'List of sites', + condition: { field: 'operation', value: 'list_sites' }, + }, + deploys: { + type: 'array', + description: 'List of deploys', + condition: { field: 'operation', value: 'list_deploys' }, + }, + envVars: { + type: 'array', + description: 'List of environment variables', + condition: { field: 'operation', value: 'list_env_vars' }, + }, + envVar: { + type: 'json', + description: 'Environment variable', + condition: { field: 'operation', value: ['create_env_var', 'update_env_var'] }, + }, + id: { + type: 'string', + description: 'Resource ID', + condition: { + field: 'operation', + value: ['get_deploy', 'cancel_deploy', 'create_deploy'], + }, + }, + state: { + type: 'string', + description: 'Deploy state', + condition: { field: 'operation', value: ['get_deploy', 'cancel_deploy'] }, + }, + deployUrl: { + type: 'string', + description: 'Unique deploy URL', + condition: { field: 'operation', value: ['get_deploy', 'cancel_deploy'] }, + }, + deployId: { + type: 'string', + description: 'Deploy ID produced by a build', + condition: { field: 'operation', value: 'create_deploy' }, + }, + deleted: { + type: 'boolean', + description: 'Whether the resource was deleted', + condition: { field: 'operation', value: 'delete_env_var' }, + }, + count: { type: 'number', description: 'Number of items returned' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index f5d9b40fd3..8cc8351a4b 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -138,6 +138,7 @@ import { MongoDBBlock } from '@/blocks/blocks/mongodb' import { MothershipBlock } from '@/blocks/blocks/mothership' import { MySQLBlock } from '@/blocks/blocks/mysql' import { Neo4jBlock } from '@/blocks/blocks/neo4j' +import { NetlifyBlock } from '@/blocks/blocks/netlify' import { NoteBlock } from '@/blocks/blocks/note' import { NotionBlock, NotionV2Block } from '@/blocks/blocks/notion' import { ObsidianBlock } from '@/blocks/blocks/obsidian' @@ -388,6 +389,7 @@ export const registry: Record = { mothership: MothershipBlock, mysql: MySQLBlock, neo4j: Neo4jBlock, + netlify: NetlifyBlock, note: NoteBlock, notion: NotionBlock, notion_v2: NotionV2Block, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 4092f8c10a..8763ee093f 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -6727,6 +6727,30 @@ export function VercelIcon(props: SVGProps) { ) } +export function NetlifyIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function CloudflareIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/webhooks/providers/netlify.ts b/apps/sim/lib/webhooks/providers/netlify.ts new file mode 100644 index 0000000000..72ca1edd15 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/netlify.ts @@ -0,0 +1,282 @@ +import crypto from 'crypto' +import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { NextResponse } from 'next/server' +import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import type { + AuthContext, + DeleteSubscriptionContext, + EventMatchContext, + FormatInputContext, + FormatInputResult, + SubscriptionContext, + SubscriptionResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' + +const logger = createLogger('WebhookProvider:Netlify') + +/** + * Verifies a Netlify outgoing webhook JWT signature (HS256, iss=netlify). + * The token's `sha256` claim must equal the SHA-256 hex digest of the raw body. + */ +function verifyNetlifyJwt(token: string, secret: string, rawBody: string): boolean { + const parts = token.split('.') + if (parts.length !== 3) return false + const [headerB64, payloadB64, signatureB64] = parts + + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(`${headerB64}.${payloadB64}`) + .digest('base64url') + + if (!safeCompare(expectedSignature, signatureB64)) { + return false + } + + let payload: Record + try { + payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8')) as Record< + string, + unknown + > + } catch { + return false + } + + if (payload.iss !== 'netlify') return false + + const bodyHash = crypto.createHash('sha256').update(rawBody, 'utf8').digest('hex') + if (typeof payload.sha256 !== 'string') return false + return safeCompare(payload.sha256, bodyHash) +} + +export const netlifyHandler: WebhookProviderHandler = { + verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext): NextResponse | null { + const secret = (providerConfig.webhookSecret as string | undefined)?.trim() + if (!secret) { + logger.warn(`[${requestId}] Netlify webhook secret missing; rejecting delivery`) + return new NextResponse( + 'Unauthorized - Netlify webhook signing secret is not configured. Re-save the trigger so a webhook can be registered.', + { status: 401 } + ) + } + + const signature = request.headers.get('x-webhook-signature') + if (!signature) { + logger.warn(`[${requestId}] Netlify webhook missing X-Webhook-Signature header`) + return new NextResponse('Unauthorized - Missing Netlify signature', { status: 401 }) + } + + if (!verifyNetlifyJwt(signature, secret, rawBody)) { + logger.warn(`[${requestId}] Netlify signature verification failed`) + return new NextResponse('Unauthorized - Invalid Netlify signature', { status: 401 }) + } + + return null + }, + + async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) { + const triggerId = providerConfig.triggerId as string | undefined + if (!triggerId) return true + + const { isNetlifyEventMatch } = await import('@/triggers/netlify/utils') + const obj = body as Record + const state = typeof obj.state === 'string' ? obj.state : undefined + + if (!isNetlifyEventMatch(triggerId, state)) { + logger.debug(`[${requestId}] Netlify event mismatch for trigger ${triggerId}. Skipping.`, { + webhookId: webhook.id, + workflowId: workflow.id, + triggerId, + state, + }) + return false + } + + return true + }, + + extractIdempotencyId(body: unknown) { + const id = (body as Record)?.id + if (id === undefined || id === null || id === '') { + return null + } + return `netlify:${String(id)}` + }, + + async createSubscription(ctx: SubscriptionContext): Promise { + const { webhook, requestId } = ctx + try { + const providerConfig = getProviderConfig(webhook) + const apiKey = providerConfig.apiKey as string | undefined + const triggerId = providerConfig.triggerId as string | undefined + const siteId = (providerConfig.siteId as string | undefined)?.trim() + + if (!apiKey) { + throw new Error( + 'Netlify Personal Access Token is required. Provide your access token in the trigger configuration.' + ) + } + if (!siteId) { + throw new Error('Netlify Site ID is required to register a deploy webhook.') + } + if (!triggerId) { + throw new Error('Missing trigger ID — re-save the Netlify trigger.') + } + + const { NETLIFY_TRIGGER_EVENT_TYPES } = await import('@/triggers/netlify/utils') + const event = NETLIFY_TRIGGER_EVENT_TYPES[triggerId] + if (!event) { + throw new Error( + `Unknown Netlify trigger "${triggerId}". Remove and re-add the Netlify trigger, then save again.` + ) + } + + const notificationUrl = getNotificationUrl(webhook) + const signingSecret = crypto.randomBytes(32).toString('base64url') + + logger.info(`[${requestId}] Creating Netlify webhook`, { + triggerId, + event, + siteId, + webhookId: webhook.id, + }) + + const apiUrl = `https://api.netlify.com/api/v1/hooks?site_id=${encodeURIComponent(siteId)}` + const requestBody = { + type: 'url', + event, + data: { + url: notificationUrl, + signature_secret: signingSecret, + }, + } + + const netlifyResponse = await fetch(apiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = (await netlifyResponse.json().catch(() => ({}))) as Record< + string, + unknown + > + + if (!netlifyResponse.ok) { + const errorMessage = + (responseBody.message as string) || + (responseBody.error as string) || + 'Unknown Netlify API error' + + let userFriendlyMessage = 'Failed to create webhook subscription in Netlify' + if (netlifyResponse.status === 401 || netlifyResponse.status === 403) { + userFriendlyMessage = + 'Invalid or insufficient Netlify Personal Access Token. Verify the token has access to this site.' + } else if (netlifyResponse.status === 404) { + userFriendlyMessage = `Netlify site "${siteId}" not found or not accessible with this token.` + } else if (errorMessage && errorMessage !== 'Unknown Netlify API error') { + userFriendlyMessage = `Netlify error: ${errorMessage}` + } + + throw new Error(userFriendlyMessage) + } + + const externalId = (responseBody.id as string | undefined) ?? undefined + if (!externalId) { + throw new Error('Netlify webhook creation succeeded but no hook ID was returned') + } + + logger.info(`[${requestId}] Successfully created Netlify hook ${externalId}`, { + webhookId: webhook.id, + event, + }) + + return { + providerConfigUpdates: { + externalId, + webhookSecret: signingSecret, + }, + } + } catch (error: unknown) { + const err = error as Error + logger.error(`[${requestId}] Exception during Netlify webhook creation`, { + message: err.message, + webhookId: webhook.id, + }) + throw error + } + }, + + async deleteSubscription(ctx: DeleteSubscriptionContext): Promise { + const { webhook, requestId } = ctx + try { + const config = getProviderConfig(webhook) + const apiKey = config.apiKey as string | undefined + const externalId = config.externalId as string | undefined + + if (!apiKey || !externalId) { + logger.warn( + `[${requestId}] Missing apiKey or externalId for Netlify webhook deletion ${webhook.id}, skipping cleanup` + ) + if (ctx.strict) throw new Error('Missing Netlify webhook deletion credentials') + return + } + + const apiUrl = `https://api.netlify.com/api/v1/hooks/${encodeURIComponent(externalId)}` + + const response = await fetch(apiUrl, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }) + + if (!response.ok && response.status !== 404) { + logger.warn( + `[${requestId}] Failed to delete Netlify webhook (non-fatal): ${response.status}` + ) + if (ctx.strict) throw new Error(`Failed to delete Netlify webhook: ${response.status}`) + } else { + await response.body?.cancel() + logger.info(`[${requestId}] Successfully deleted Netlify hook ${externalId}`) + } + } catch (error) { + logger.warn(`[${requestId}] Error deleting Netlify webhook (non-fatal)`, error) + if (ctx.strict) throw error + } + }, + + async formatInput(ctx: FormatInputContext): Promise { + const body = ctx.body as Record + + const str = (v: unknown): string => (v == null ? '' : String(v)) + + return { + input: { + id: str(body.id), + siteId: str(body.site_id), + state: str(body.state), + name: str(body.name), + url: str(body.url), + deployUrl: str(body.deploy_url), + deploySslUrl: str(body.deploy_ssl_url), + adminUrl: str(body.admin_url), + branch: str(body.branch), + context: str(body.context), + commitRef: str(body.commit_ref), + commitUrl: str(body.commit_url), + title: str(body.title), + errorMessage: str(body.error_message), + createdAt: str(body.created_at), + updatedAt: str(body.updated_at), + publishedAt: str(body.published_at), + payload: body, + }, + } + }, +} diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index ce8e9c6af7..571ee1a54c 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -26,6 +26,7 @@ import { lemlistHandler } from '@/lib/webhooks/providers/lemlist' import { linearHandler } from '@/lib/webhooks/providers/linear' import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams' import { mondayHandler } from '@/lib/webhooks/providers/monday' +import { netlifyHandler } from '@/lib/webhooks/providers/netlify' import { notionHandler } from '@/lib/webhooks/providers/notion' import { outlookHandler } from '@/lib/webhooks/providers/outlook' import { resendHandler } from '@/lib/webhooks/providers/resend' @@ -88,6 +89,7 @@ const PROVIDER_HANDLERS: Record = { twilio: twilioHandler, twilio_voice: twilioVoiceHandler, typeform: typeformHandler, + netlify: netlifyHandler, vercel: vercelHandler, webflow: webflowHandler, whatsapp: whatsappHandler, diff --git a/apps/sim/tools/netlify/cancel_deploy.ts b/apps/sim/tools/netlify/cancel_deploy.ts new file mode 100644 index 0000000000..ac5954a921 --- /dev/null +++ b/apps/sim/tools/netlify/cancel_deploy.ts @@ -0,0 +1,101 @@ +import type { NetlifyCancelDeployParams, NetlifyCancelDeployResponse } from '@/tools/netlify/types' +import type { ToolConfig } from '@/tools/types' + +interface NetlifyApiDeploy { + id?: string + site_id?: string + state?: string + name?: string + url?: string + deploy_url?: string + deploy_ssl_url?: string + admin_url?: string + branch?: string + context?: string + commit_ref?: string + commit_url?: string + error_message?: string + created_at?: string + updated_at?: string + published_at?: string +} + +export const netlifyCancelDeployTool: ToolConfig< + NetlifyCancelDeployParams, + NetlifyCancelDeployResponse +> = { + id: 'netlify_cancel_deploy', + name: 'Netlify Cancel Deploy', + description: 'Cancel an in-progress Netlify deploy', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Netlify Personal Access Token', + }, + deployId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Deploy ID to cancel', + }, + }, + + request: { + url: (params: NetlifyCancelDeployParams) => + `https://api.netlify.com/api/v1/deploys/${encodeURIComponent(params.deployId.trim())}/cancel`, + method: 'POST', + headers: (params: NetlifyCancelDeployParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const d = (await response.json()) as NetlifyApiDeploy + + return { + success: true, + output: { + id: d.id ?? '', + siteId: d.site_id ?? null, + state: d.state ?? 'error', + name: d.name ?? null, + url: d.url ?? null, + deployUrl: d.deploy_url ?? null, + deploySslUrl: d.deploy_ssl_url ?? null, + adminUrl: d.admin_url ?? null, + branch: d.branch ?? null, + context: d.context ?? null, + commitRef: d.commit_ref ?? null, + commitUrl: d.commit_url ?? null, + errorMessage: d.error_message ?? null, + createdAt: d.created_at ?? null, + updatedAt: d.updated_at ?? null, + publishedAt: d.published_at ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Deploy ID' }, + siteId: { type: 'string', description: 'Site ID', optional: true }, + state: { type: 'string', description: 'Deploy state after cancellation' }, + name: { type: 'string', description: 'Site name', optional: true }, + url: { type: 'string', description: 'Site URL', optional: true }, + deployUrl: { type: 'string', description: 'Unique deploy URL', optional: true }, + deploySslUrl: { type: 'string', description: 'Unique deploy HTTPS URL', optional: true }, + adminUrl: { type: 'string', description: 'Netlify admin URL', optional: true }, + branch: { type: 'string', description: 'Git branch', optional: true }, + context: { type: 'string', description: 'Deploy context', optional: true }, + commitRef: { type: 'string', description: 'Git commit SHA', optional: true }, + commitUrl: { type: 'string', description: 'Git commit URL', optional: true }, + errorMessage: { type: 'string', description: 'Error message if failed', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + publishedAt: { type: 'string', description: 'Publish timestamp', optional: true }, + }, +} diff --git a/apps/sim/tools/netlify/create_deploy.ts b/apps/sim/tools/netlify/create_deploy.ts new file mode 100644 index 0000000000..6d0e0d9cb7 --- /dev/null +++ b/apps/sim/tools/netlify/create_deploy.ts @@ -0,0 +1,104 @@ +import type { NetlifyCreateDeployParams, NetlifyCreateDeployResponse } from '@/tools/netlify/types' +import type { ToolConfig } from '@/tools/types' + +interface NetlifyApiBuild { + id?: string + deploy_id?: string + site_id?: string + sha?: string + done?: boolean + error?: string + created_at?: string +} + +export const netlifyCreateDeployTool: ToolConfig< + NetlifyCreateDeployParams, + NetlifyCreateDeployResponse +> = { + id: 'netlify_create_deploy', + name: 'Netlify Create Deploy', + description: + 'Trigger a new Netlify deploy by starting a build for a site (optionally from a specific branch)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Netlify Personal Access Token', + }, + siteId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Site ID or primary domain to deploy', + }, + branch: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Git branch to build from (defaults to the site’s configured production branch)', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional human-readable label shown in the deploy log', + }, + clearCache: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Clear the build cache before deploying ("true" or "false")', + }, + }, + + request: { + url: (params: NetlifyCreateDeployParams) => { + const query = new URLSearchParams() + if (params.branch) query.set('branch', params.branch.trim()) + if (params.title) query.set('title', params.title.trim()) + if (params.clearCache === 'true') query.set('clear_cache', 'true') + const qs = query.toString() + return `https://api.netlify.com/api/v1/sites/${encodeURIComponent(params.siteId.trim())}/builds${qs ? `?${qs}` : ''}` + }, + method: 'POST', + headers: (params: NetlifyCreateDeployParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: () => ({}), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as NetlifyApiBuild + + return { + success: true, + output: { + id: data.id ?? '', + deployId: data.deploy_id ?? null, + siteId: data.site_id ?? null, + sha: data.sha ?? null, + done: data.done ?? false, + error: data.error ?? null, + createdAt: data.created_at ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Build ID' }, + deployId: { + type: 'string', + description: 'Deploy ID produced by this build (use to poll status)', + optional: true, + }, + siteId: { type: 'string', description: 'Site ID', optional: true }, + sha: { type: 'string', description: 'Git commit SHA being built', optional: true }, + done: { type: 'boolean', description: 'Whether the build has completed' }, + error: { type: 'string', description: 'Build error if any', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + }, +} diff --git a/apps/sim/tools/netlify/create_env_var.ts b/apps/sim/tools/netlify/create_env_var.ts new file mode 100644 index 0000000000..2805840a28 --- /dev/null +++ b/apps/sim/tools/netlify/create_env_var.ts @@ -0,0 +1,127 @@ +import type { NetlifyCreateEnvVarParams, NetlifyCreateEnvVarResponse } from '@/tools/netlify/types' +import { buildEnvBody, type NetlifyApiEnvVar, normalizeEnvVar } from '@/tools/netlify/utils' +import type { ToolConfig } from '@/tools/types' + +export const netlifyCreateEnvVarTool: ToolConfig< + NetlifyCreateEnvVarParams, + NetlifyCreateEnvVarResponse +> = { + id: 'netlify_create_env_var', + name: 'Netlify Create Environment Variable', + description: 'Create a new environment variable for an account, optionally scoped to a site', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Netlify Personal Access Token', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Account ID or slug that owns the variable', + }, + siteId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional site ID to scope the variable to a specific site', + }, + key: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Variable name (e.g., DATABASE_URL)', + }, + value: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Variable value', + }, + context: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Deploy context this value applies to: all, production, deploy-preview, branch-deploy, dev (default: all)', + }, + scopes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated scopes (builds, functions, runtime, post_processing). Defaults to all scopes', + }, + isSecret: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Mark the value as secret ("true" or "false")', + }, + }, + + request: { + url: (params: NetlifyCreateEnvVarParams) => { + const query = new URLSearchParams() + if (params.siteId) query.set('site_id', params.siteId.trim()) + const qs = query.toString() + return `https://api.netlify.com/api/v1/accounts/${encodeURIComponent(params.accountId.trim())}/env${qs ? `?${qs}` : ''}` + }, + method: 'POST', + headers: (params: NetlifyCreateEnvVarParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params: NetlifyCreateEnvVarParams) => [buildEnvBody(params)], + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as NetlifyApiEnvVar | NetlifyApiEnvVar[] + const created = Array.isArray(data) ? data[0] : data + + return { + success: true, + output: { + envVar: normalizeEnvVar(created ?? {}), + }, + } + }, + + outputs: { + envVar: { + type: 'object', + description: 'Created environment variable', + properties: { + key: { type: 'string', description: 'Variable name' }, + scopes: { + type: 'array', + description: 'Scopes', + items: { type: 'string', description: 'Scope name' }, + }, + values: { + type: 'array', + description: 'Per-context values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Value ID', optional: true }, + context: { type: 'string', description: 'Context name', optional: true }, + contextParameter: { + type: 'string', + description: 'Branch name for branch-deploy context', + optional: true, + }, + value: { type: 'string', description: 'Variable value' }, + }, + }, + }, + isSecret: { type: 'boolean', description: 'Whether the value is secret' }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/netlify/delete_env_var.ts b/apps/sim/tools/netlify/delete_env_var.ts new file mode 100644 index 0000000000..1d87019440 --- /dev/null +++ b/apps/sim/tools/netlify/delete_env_var.ts @@ -0,0 +1,69 @@ +import type { NetlifyDeleteEnvVarParams, NetlifyDeleteEnvVarResponse } from '@/tools/netlify/types' +import type { ToolConfig } from '@/tools/types' + +export const netlifyDeleteEnvVarTool: ToolConfig< + NetlifyDeleteEnvVarParams, + NetlifyDeleteEnvVarResponse +> = { + id: 'netlify_delete_env_var', + name: 'Netlify Delete Environment Variable', + description: 'Delete an environment variable from an account, optionally scoped to a site', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Netlify Personal Access Token', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Account ID or slug that owns the variable', + }, + siteId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional site ID to scope deletion to a specific site', + }, + key: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Variable name to delete', + }, + }, + + request: { + url: (params: NetlifyDeleteEnvVarParams) => { + const query = new URLSearchParams() + if (params.siteId) query.set('site_id', params.siteId.trim()) + const qs = query.toString() + return `https://api.netlify.com/api/v1/accounts/${encodeURIComponent(params.accountId.trim())}/env/${encodeURIComponent(params.key.trim())}${qs ? `?${qs}` : ''}` + }, + method: 'DELETE', + headers: (params: NetlifyDeleteEnvVarParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async () => { + return { + success: true, + output: { + deleted: true, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the environment variable was deleted', + }, + }, +} diff --git a/apps/sim/tools/netlify/get_deploy.ts b/apps/sim/tools/netlify/get_deploy.ts new file mode 100644 index 0000000000..0a663e03cc --- /dev/null +++ b/apps/sim/tools/netlify/get_deploy.ts @@ -0,0 +1,98 @@ +import type { NetlifyGetDeployParams, NetlifyGetDeployResponse } from '@/tools/netlify/types' +import type { ToolConfig } from '@/tools/types' + +interface NetlifyApiDeploy { + id?: string + site_id?: string + state?: string + name?: string + url?: string + deploy_url?: string + deploy_ssl_url?: string + admin_url?: string + branch?: string + context?: string + commit_ref?: string + commit_url?: string + error_message?: string + created_at?: string + updated_at?: string + published_at?: string +} + +export const netlifyGetDeployTool: ToolConfig = { + id: 'netlify_get_deploy', + name: 'Netlify Get Deploy', + description: 'Get details of a specific Netlify deploy', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Netlify Personal Access Token', + }, + deployId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Deploy ID', + }, + }, + + request: { + url: (params: NetlifyGetDeployParams) => + `https://api.netlify.com/api/v1/deploys/${encodeURIComponent(params.deployId.trim())}`, + method: 'GET', + headers: (params: NetlifyGetDeployParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const d = (await response.json()) as NetlifyApiDeploy + + return { + success: true, + output: { + id: d.id ?? '', + siteId: d.site_id ?? null, + state: d.state ?? 'unknown', + name: d.name ?? null, + url: d.url ?? null, + deployUrl: d.deploy_url ?? null, + deploySslUrl: d.deploy_ssl_url ?? null, + adminUrl: d.admin_url ?? null, + branch: d.branch ?? null, + context: d.context ?? null, + commitRef: d.commit_ref ?? null, + commitUrl: d.commit_url ?? null, + errorMessage: d.error_message ?? null, + createdAt: d.created_at ?? null, + updatedAt: d.updated_at ?? null, + publishedAt: d.published_at ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Deploy ID' }, + siteId: { type: 'string', description: 'Site ID', optional: true }, + state: { type: 'string', description: 'Deploy state' }, + name: { type: 'string', description: 'Site name', optional: true }, + url: { type: 'string', description: 'Site URL', optional: true }, + deployUrl: { type: 'string', description: 'Unique deploy URL', optional: true }, + deploySslUrl: { type: 'string', description: 'Unique deploy HTTPS URL', optional: true }, + adminUrl: { type: 'string', description: 'Netlify admin URL', optional: true }, + branch: { type: 'string', description: 'Git branch', optional: true }, + context: { type: 'string', description: 'Deploy context', optional: true }, + commitRef: { type: 'string', description: 'Git commit SHA', optional: true }, + commitUrl: { type: 'string', description: 'Git commit URL', optional: true }, + errorMessage: { type: 'string', description: 'Error message if failed', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + publishedAt: { type: 'string', description: 'Publish timestamp', optional: true }, + }, +} diff --git a/apps/sim/tools/netlify/index.ts b/apps/sim/tools/netlify/index.ts new file mode 100644 index 0000000000..85cbcd5002 --- /dev/null +++ b/apps/sim/tools/netlify/index.ts @@ -0,0 +1,23 @@ +import { netlifyCancelDeployTool } from '@/tools/netlify/cancel_deploy' +import { netlifyCreateDeployTool } from '@/tools/netlify/create_deploy' +import { netlifyCreateEnvVarTool } from '@/tools/netlify/create_env_var' +import { netlifyDeleteEnvVarTool } from '@/tools/netlify/delete_env_var' +import { netlifyGetDeployTool } from '@/tools/netlify/get_deploy' +import { netlifyListDeploysTool } from '@/tools/netlify/list_deploys' +import { netlifyListEnvVarsTool } from '@/tools/netlify/list_env_vars' +import { netlifyListSitesTool } from '@/tools/netlify/list_sites' +import { netlifyUpdateEnvVarTool } from '@/tools/netlify/update_env_var' + +export { + netlifyListSitesTool, + netlifyListDeploysTool, + netlifyGetDeployTool, + netlifyCancelDeployTool, + netlifyCreateDeployTool, + netlifyListEnvVarsTool, + netlifyCreateEnvVarTool, + netlifyUpdateEnvVarTool, + netlifyDeleteEnvVarTool, +} + +export * from './types' diff --git a/apps/sim/tools/netlify/list_deploys.ts b/apps/sim/tools/netlify/list_deploys.ts new file mode 100644 index 0000000000..6ae4f02256 --- /dev/null +++ b/apps/sim/tools/netlify/list_deploys.ts @@ -0,0 +1,162 @@ +import type { NetlifyListDeploysParams, NetlifyListDeploysResponse } from '@/tools/netlify/types' +import type { ToolConfig } from '@/tools/types' + +interface NetlifyApiDeploy { + id?: string + site_id?: string + state?: string + name?: string + url?: string + deploy_url?: string + deploy_ssl_url?: string + admin_url?: string + branch?: string + context?: string + commit_ref?: string + commit_url?: string + error_message?: string + created_at?: string + updated_at?: string + published_at?: string +} + +export const netlifyListDeploysTool: ToolConfig< + NetlifyListDeploysParams, + NetlifyListDeploysResponse +> = { + id: 'netlify_list_deploys', + name: 'Netlify List Deploys', + description: 'List deploys for a Netlify site', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Netlify Personal Access Token', + }, + siteId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Site ID or primary domain', + }, + state: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filter by deploy state: ready, error, building, enqueued, processing, uploading, new', + }, + branch: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by git branch', + }, + production: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to production deploys only ("true" or "false")', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (1-indexed)', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100)', + }, + }, + + request: { + url: (params: NetlifyListDeploysParams) => { + const query = new URLSearchParams() + if (params.state) query.set('state', params.state) + if (params.branch) query.set('branch', params.branch.trim()) + if (params.production) query.set('production', params.production) + if (params.page) query.set('page', String(params.page)) + if (params.perPage) query.set('per_page', String(params.perPage)) + const qs = query.toString() + return `https://api.netlify.com/api/v1/sites/${encodeURIComponent(params.siteId.trim())}/deploys${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params: NetlifyListDeploysParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as NetlifyApiDeploy[] + const deploys = (Array.isArray(data) ? data : []).map((d) => ({ + id: d.id ?? '', + siteId: d.site_id ?? null, + state: d.state ?? 'unknown', + name: d.name ?? null, + url: d.url ?? null, + deployUrl: d.deploy_url ?? null, + deploySslUrl: d.deploy_ssl_url ?? null, + adminUrl: d.admin_url ?? null, + branch: d.branch ?? null, + context: d.context ?? null, + commitRef: d.commit_ref ?? null, + commitUrl: d.commit_url ?? null, + errorMessage: d.error_message ?? null, + createdAt: d.created_at ?? null, + updatedAt: d.updated_at ?? null, + publishedAt: d.published_at ?? null, + })) + + return { + success: true, + output: { + deploys, + count: deploys.length, + }, + } + }, + + outputs: { + deploys: { + type: 'array', + description: 'List of deploys', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Deploy ID' }, + siteId: { type: 'string', description: 'Site ID', optional: true }, + state: { + type: 'string', + description: + 'Deploy state: new, enqueued, building, uploading, processing, ready, error, retrying', + }, + name: { type: 'string', description: 'Site name', optional: true }, + url: { type: 'string', description: 'Site URL', optional: true }, + deployUrl: { type: 'string', description: 'Unique deploy URL', optional: true }, + deploySslUrl: { type: 'string', description: 'Unique deploy HTTPS URL', optional: true }, + adminUrl: { type: 'string', description: 'Netlify admin URL', optional: true }, + branch: { type: 'string', description: 'Git branch', optional: true }, + context: { + type: 'string', + description: 'Deploy context: production, deploy-preview, branch-deploy', + optional: true, + }, + commitRef: { type: 'string', description: 'Git commit SHA', optional: true }, + commitUrl: { type: 'string', description: 'Git commit URL', optional: true }, + errorMessage: { type: 'string', description: 'Error message if failed', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + publishedAt: { type: 'string', description: 'Publish timestamp', optional: true }, + }, + }, + }, + count: { type: 'number', description: 'Number of deploys returned' }, + }, +} diff --git a/apps/sim/tools/netlify/list_env_vars.ts b/apps/sim/tools/netlify/list_env_vars.ts new file mode 100644 index 0000000000..8ba3cdcd35 --- /dev/null +++ b/apps/sim/tools/netlify/list_env_vars.ts @@ -0,0 +1,113 @@ +import type { NetlifyListEnvVarsParams, NetlifyListEnvVarsResponse } from '@/tools/netlify/types' +import { type NetlifyApiEnvVar, normalizeEnvVar } from '@/tools/netlify/utils' +import type { ToolConfig } from '@/tools/types' + +export const netlifyListEnvVarsTool: ToolConfig< + NetlifyListEnvVarsParams, + NetlifyListEnvVarsResponse +> = { + id: 'netlify_list_env_vars', + name: 'Netlify List Environment Variables', + description: 'List environment variables for an account, optionally scoped to a site', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Netlify Personal Access Token', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Account ID or slug that owns the environment variables', + }, + siteId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional site ID to scope variables to a specific site', + }, + contextName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by deploy context (production, deploy-preview, branch-deploy, dev)', + }, + scope: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by scope (builds, functions, runtime, post_processing)', + }, + }, + + request: { + url: (params: NetlifyListEnvVarsParams) => { + const query = new URLSearchParams() + if (params.siteId) query.set('site_id', params.siteId.trim()) + if (params.contextName) query.set('context_name', params.contextName.trim()) + if (params.scope) query.set('scope', params.scope.trim()) + const qs = query.toString() + return `https://api.netlify.com/api/v1/accounts/${encodeURIComponent(params.accountId.trim())}/env${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params: NetlifyListEnvVarsParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as NetlifyApiEnvVar[] + const envVars = (Array.isArray(data) ? data : []).map(normalizeEnvVar) + + return { + success: true, + output: { + envVars, + count: envVars.length, + }, + } + }, + + outputs: { + envVars: { + type: 'array', + description: 'List of environment variables', + items: { + type: 'object', + properties: { + key: { type: 'string', description: 'Variable name' }, + scopes: { + type: 'array', + description: 'Where the variable applies (builds, functions, runtime, post_processing)', + items: { type: 'string', description: 'Scope name' }, + }, + values: { + type: 'array', + description: 'Per-context values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Value ID', optional: true }, + context: { type: 'string', description: 'Context name', optional: true }, + contextParameter: { + type: 'string', + description: 'Branch name when context is branch-deploy', + optional: true, + }, + value: { type: 'string', description: 'Variable value' }, + }, + }, + }, + isSecret: { type: 'boolean', description: 'Whether the value is secret' }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + }, + }, + }, + count: { type: 'number', description: 'Number of variables returned' }, + }, +} diff --git a/apps/sim/tools/netlify/list_sites.ts b/apps/sim/tools/netlify/list_sites.ts new file mode 100644 index 0000000000..861df0526c --- /dev/null +++ b/apps/sim/tools/netlify/list_sites.ts @@ -0,0 +1,119 @@ +import type { NetlifyListSitesParams, NetlifyListSitesResponse } from '@/tools/netlify/types' +import type { ToolConfig } from '@/tools/types' + +interface NetlifyApiSite { + id?: string + name?: string + url?: string + ssl_url?: string + admin_url?: string + custom_domain?: string + account_id?: string + account_slug?: string + created_at?: string + updated_at?: string +} + +export const netlifyListSitesTool: ToolConfig = { + id: 'netlify_list_sites', + name: 'Netlify List Sites', + description: 'List Netlify sites accessible to the authenticated user', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Netlify Personal Access Token', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter sites by name', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter scope: all, owner, or guest', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (1-indexed)', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100)', + }, + }, + + request: { + url: (params: NetlifyListSitesParams) => { + const query = new URLSearchParams() + if (params.name) query.set('name', params.name.trim()) + if (params.filter) query.set('filter', params.filter) + if (params.page) query.set('page', String(params.page)) + if (params.perPage) query.set('per_page', String(params.perPage)) + const qs = query.toString() + return `https://api.netlify.com/api/v1/sites${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params: NetlifyListSitesParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as NetlifyApiSite[] + const sites = (Array.isArray(data) ? data : []).map((s) => ({ + id: s.id ?? '', + name: s.name ?? null, + url: s.url ?? null, + sslUrl: s.ssl_url ?? null, + adminUrl: s.admin_url ?? null, + customDomain: s.custom_domain ?? null, + accountId: s.account_id ?? null, + accountSlug: s.account_slug ?? null, + createdAt: s.created_at ?? null, + updatedAt: s.updated_at ?? null, + })) + + return { + success: true, + output: { + sites, + count: sites.length, + }, + } + }, + + outputs: { + sites: { + type: 'array', + description: 'List of Netlify sites', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Site ID' }, + name: { type: 'string', description: 'Site name', optional: true }, + url: { type: 'string', description: 'Primary site URL', optional: true }, + sslUrl: { type: 'string', description: 'HTTPS site URL', optional: true }, + adminUrl: { type: 'string', description: 'Netlify admin URL', optional: true }, + customDomain: { type: 'string', description: 'Custom domain', optional: true }, + accountId: { type: 'string', description: 'Owning account ID', optional: true }, + accountSlug: { type: 'string', description: 'Owning account slug', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + }, + }, + }, + count: { type: 'number', description: 'Number of sites returned' }, + }, +} diff --git a/apps/sim/tools/netlify/types.ts b/apps/sim/tools/netlify/types.ts new file mode 100644 index 0000000000..c29665480b --- /dev/null +++ b/apps/sim/tools/netlify/types.ts @@ -0,0 +1,180 @@ +import type { ToolResponse } from '@/tools/types' + +export interface NetlifySite { + id: string + name: string | null + url: string | null + sslUrl: string | null + adminUrl: string | null + customDomain: string | null + accountId: string | null + accountSlug: string | null + createdAt: string | null + updatedAt: string | null +} + +export interface NetlifyDeploy { + id: string + siteId: string | null + state: string + name: string | null + url: string | null + deployUrl: string | null + deploySslUrl: string | null + adminUrl: string | null + branch: string | null + context: string | null + commitRef: string | null + commitUrl: string | null + errorMessage: string | null + createdAt: string | null + updatedAt: string | null + publishedAt: string | null +} + +export interface NetlifyEnvValue { + id: string | null + context: string | null + contextParameter: string | null + value: string +} + +export interface NetlifyEnvVar { + key: string + scopes: string[] + values: NetlifyEnvValue[] + isSecret: boolean + updatedAt: string | null +} + +export interface NetlifyListSitesParams { + apiKey: string + name?: string + filter?: string + page?: number + perPage?: number +} + +export interface NetlifyListSitesResponse extends ToolResponse { + output: { + sites: NetlifySite[] + count: number + } +} + +export interface NetlifyListDeploysParams { + apiKey: string + siteId: string + state?: string + branch?: string + production?: string + page?: number + perPage?: number +} + +export interface NetlifyListDeploysResponse extends ToolResponse { + output: { + deploys: NetlifyDeploy[] + count: number + } +} + +export interface NetlifyGetDeployParams { + apiKey: string + deployId: string +} + +export interface NetlifyGetDeployResponse extends ToolResponse { + output: NetlifyDeploy +} + +export interface NetlifyCancelDeployParams { + apiKey: string + deployId: string +} + +export interface NetlifyCancelDeployResponse extends ToolResponse { + output: NetlifyDeploy +} + +export interface NetlifyCreateDeployParams { + apiKey: string + siteId: string + branch?: string + title?: string + clearCache?: string +} + +export interface NetlifyCreateDeployResponse extends ToolResponse { + output: { + id: string + deployId: string | null + siteId: string | null + sha: string | null + done: boolean + error: string | null + createdAt: string | null + } +} + +export interface NetlifyListEnvVarsParams { + apiKey: string + accountId: string + siteId?: string + contextName?: string + scope?: string +} + +export interface NetlifyListEnvVarsResponse extends ToolResponse { + output: { + envVars: NetlifyEnvVar[] + count: number + } +} + +export interface NetlifyCreateEnvVarParams { + apiKey: string + accountId: string + siteId?: string + key: string + value: string + context?: string + scopes?: string + isSecret?: string +} + +export interface NetlifyCreateEnvVarResponse extends ToolResponse { + output: { + envVar: NetlifyEnvVar + } +} + +export interface NetlifyUpdateEnvVarParams { + apiKey: string + accountId: string + siteId?: string + key: string + value: string + context?: string + scopes?: string + isSecret?: string +} + +export interface NetlifyUpdateEnvVarResponse extends ToolResponse { + output: { + envVar: NetlifyEnvVar + } +} + +export interface NetlifyDeleteEnvVarParams { + apiKey: string + accountId: string + siteId?: string + key: string +} + +export interface NetlifyDeleteEnvVarResponse extends ToolResponse { + output: { + deleted: boolean + } +} diff --git a/apps/sim/tools/netlify/update_env_var.ts b/apps/sim/tools/netlify/update_env_var.ts new file mode 100644 index 0000000000..4f26589260 --- /dev/null +++ b/apps/sim/tools/netlify/update_env_var.ts @@ -0,0 +1,125 @@ +import type { NetlifyUpdateEnvVarParams, NetlifyUpdateEnvVarResponse } from '@/tools/netlify/types' +import { buildEnvBody, type NetlifyApiEnvVar, normalizeEnvVar } from '@/tools/netlify/utils' +import type { ToolConfig } from '@/tools/types' + +export const netlifyUpdateEnvVarTool: ToolConfig< + NetlifyUpdateEnvVarParams, + NetlifyUpdateEnvVarResponse +> = { + id: 'netlify_update_env_var', + name: 'Netlify Update Environment Variable', + description: 'Replace an environment variable for an account, optionally scoped to a site', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Netlify Personal Access Token', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Account ID or slug that owns the variable', + }, + siteId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional site ID to scope the variable to a specific site', + }, + key: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Variable name to update', + }, + value: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New variable value (replaces all existing values)', + }, + context: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Deploy context: all, production, deploy-preview, branch-deploy, dev (default: all)', + }, + scopes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated scopes (builds, functions, runtime, post_processing)', + }, + isSecret: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Mark the value as secret ("true" or "false")', + }, + }, + + request: { + url: (params: NetlifyUpdateEnvVarParams) => { + const query = new URLSearchParams() + if (params.siteId) query.set('site_id', params.siteId.trim()) + const qs = query.toString() + return `https://api.netlify.com/api/v1/accounts/${encodeURIComponent(params.accountId.trim())}/env/${encodeURIComponent(params.key.trim())}${qs ? `?${qs}` : ''}` + }, + method: 'PUT', + headers: (params: NetlifyUpdateEnvVarParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params: NetlifyUpdateEnvVarParams) => buildEnvBody(params), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as NetlifyApiEnvVar + + return { + success: true, + output: { + envVar: normalizeEnvVar(data ?? {}), + }, + } + }, + + outputs: { + envVar: { + type: 'object', + description: 'Updated environment variable', + properties: { + key: { type: 'string', description: 'Variable name' }, + scopes: { + type: 'array', + description: 'Scopes', + items: { type: 'string', description: 'Scope name' }, + }, + values: { + type: 'array', + description: 'Per-context values', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Value ID', optional: true }, + context: { type: 'string', description: 'Context name', optional: true }, + contextParameter: { + type: 'string', + description: 'Branch name for branch-deploy context', + optional: true, + }, + value: { type: 'string', description: 'Variable value' }, + }, + }, + }, + isSecret: { type: 'boolean', description: 'Whether the value is secret' }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/netlify/utils.ts b/apps/sim/tools/netlify/utils.ts new file mode 100644 index 0000000000..348be35a40 --- /dev/null +++ b/apps/sim/tools/netlify/utils.ts @@ -0,0 +1,59 @@ +import type { NetlifyEnvVar } from '@/tools/netlify/types' + +export interface NetlifyApiEnvValue { + id?: string + context?: string + context_parameter?: string + value?: string +} + +export interface NetlifyApiEnvVar { + key?: string + scopes?: string[] + values?: NetlifyApiEnvValue[] + is_secret?: boolean + updated_at?: string +} + +export function normalizeEnvVar(v: NetlifyApiEnvVar): NetlifyEnvVar { + return { + key: v.key ?? '', + scopes: v.scopes ?? [], + values: (v.values ?? []).map((val) => ({ + id: val.id ?? null, + context: val.context ?? null, + contextParameter: val.context_parameter ?? null, + value: val.value ?? '', + })), + isSecret: v.is_secret ?? false, + updatedAt: v.updated_at ?? null, + } +} + +export function buildEnvBody(params: { + key: string + value: string + context?: string + scopes?: string + isSecret?: string +}): { + key: string + scopes: string[] + values: Array<{ context: string; value: string }> + is_secret: boolean +} { + const scopes = params.scopes + ? params.scopes + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + : ['builds', 'functions', 'runtime', 'post_processing'] + const context = params.context?.trim() ? params.context.trim() : 'all' + + return { + key: params.key.trim(), + scopes, + values: [{ context, value: params.value }], + is_secret: params.isSecret === 'true', + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 8da147da4e..3eb688595a 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1806,6 +1806,17 @@ import { neo4jQueryTool, neo4jUpdateTool, } from '@/tools/neo4j' +import { + netlifyCancelDeployTool, + netlifyCreateDeployTool, + netlifyCreateEnvVarTool, + netlifyDeleteEnvVarTool, + netlifyGetDeployTool, + netlifyListDeploysTool, + netlifyListEnvVarsTool, + netlifyListSitesTool, + netlifyUpdateEnvVarTool, +} from '@/tools/netlify' import { notionAddDatabaseRowTool, notionAddDatabaseRowV2Tool, @@ -4567,6 +4578,16 @@ export const tools: Record = { trello_update_card: trelloUpdateCardTool, trello_get_actions: trelloGetActionsTool, trello_add_comment: trelloAddCommentTool, + // Netlify + netlify_list_sites: netlifyListSitesTool, + netlify_list_deploys: netlifyListDeploysTool, + netlify_get_deploy: netlifyGetDeployTool, + netlify_create_deploy: netlifyCreateDeployTool, + netlify_cancel_deploy: netlifyCancelDeployTool, + netlify_list_env_vars: netlifyListEnvVarsTool, + netlify_create_env_var: netlifyCreateEnvVarTool, + netlify_update_env_var: netlifyUpdateEnvVarTool, + netlify_delete_env_var: netlifyDeleteEnvVarTool, // Vercel - Deployments vercel_list_deployments: vercelListDeploymentsTool, vercel_get_deployment: vercelGetDeploymentTool, diff --git a/apps/sim/triggers/netlify/deploy_building.ts b/apps/sim/triggers/netlify/deploy_building.ts new file mode 100644 index 0000000000..5e2156ac6a --- /dev/null +++ b/apps/sim/triggers/netlify/deploy_building.ts @@ -0,0 +1,34 @@ +import { NetlifyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNetlifyDeployOutputs, + buildNetlifyExtraFields, + netlifySetupInstructions, + netlifyTriggerOptions, +} from '@/triggers/netlify/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const netlifyDeployBuildingTrigger: TriggerConfig = { + id: 'netlify_deploy_building', + name: 'Netlify Deploy Building', + provider: 'netlify', + description: 'Trigger workflow when Netlify starts building a deploy', + version: '1.0.0', + icon: NetlifyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'netlify_deploy_building', + triggerOptions: netlifyTriggerOptions, + setupInstructions: netlifySetupInstructions('Deploy Building'), + extraFields: buildNetlifyExtraFields('netlify_deploy_building'), + }), + + outputs: buildNetlifyDeployOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/netlify/deploy_created.ts b/apps/sim/triggers/netlify/deploy_created.ts new file mode 100644 index 0000000000..ea22477546 --- /dev/null +++ b/apps/sim/triggers/netlify/deploy_created.ts @@ -0,0 +1,38 @@ +import { NetlifyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNetlifyDeployOutputs, + buildNetlifyExtraFields, + netlifySetupInstructions, + netlifyTriggerOptions, +} from '@/triggers/netlify/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Primary Netlify trigger — includes the dropdown for picking which event to listen for. + */ +export const netlifyDeployCreatedTrigger: TriggerConfig = { + id: 'netlify_deploy_created', + name: 'Netlify Deploy Created', + provider: 'netlify', + description: 'Trigger workflow when a new Netlify deploy is created (build queued)', + version: '1.0.0', + icon: NetlifyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'netlify_deploy_created', + triggerOptions: netlifyTriggerOptions, + includeDropdown: true, + setupInstructions: netlifySetupInstructions('Deploy Created'), + extraFields: buildNetlifyExtraFields('netlify_deploy_created'), + }), + + outputs: buildNetlifyDeployOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/netlify/deploy_failed.ts b/apps/sim/triggers/netlify/deploy_failed.ts new file mode 100644 index 0000000000..c7ae9b18ab --- /dev/null +++ b/apps/sim/triggers/netlify/deploy_failed.ts @@ -0,0 +1,34 @@ +import { NetlifyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNetlifyDeployOutputs, + buildNetlifyExtraFields, + netlifySetupInstructions, + netlifyTriggerOptions, +} from '@/triggers/netlify/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const netlifyDeployFailedTrigger: TriggerConfig = { + id: 'netlify_deploy_failed', + name: 'Netlify Deploy Failed', + provider: 'netlify', + description: 'Trigger workflow when a Netlify deploy fails', + version: '1.0.0', + icon: NetlifyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'netlify_deploy_failed', + triggerOptions: netlifyTriggerOptions, + setupInstructions: netlifySetupInstructions('Deploy Failed'), + extraFields: buildNetlifyExtraFields('netlify_deploy_failed'), + }), + + outputs: buildNetlifyDeployOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/netlify/deploy_locked.ts b/apps/sim/triggers/netlify/deploy_locked.ts new file mode 100644 index 0000000000..77a1bf18bd --- /dev/null +++ b/apps/sim/triggers/netlify/deploy_locked.ts @@ -0,0 +1,34 @@ +import { NetlifyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNetlifyDeployOutputs, + buildNetlifyExtraFields, + netlifySetupInstructions, + netlifyTriggerOptions, +} from '@/triggers/netlify/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const netlifyDeployLockedTrigger: TriggerConfig = { + id: 'netlify_deploy_locked', + name: 'Netlify Deploy Locked', + provider: 'netlify', + description: 'Trigger workflow when a Netlify deploy is locked to a published version', + version: '1.0.0', + icon: NetlifyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'netlify_deploy_locked', + triggerOptions: netlifyTriggerOptions, + setupInstructions: netlifySetupInstructions('Deploy Locked'), + extraFields: buildNetlifyExtraFields('netlify_deploy_locked'), + }), + + outputs: buildNetlifyDeployOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/netlify/deploy_succeeded.ts b/apps/sim/triggers/netlify/deploy_succeeded.ts new file mode 100644 index 0000000000..1d1fc6f66f --- /dev/null +++ b/apps/sim/triggers/netlify/deploy_succeeded.ts @@ -0,0 +1,34 @@ +import { NetlifyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNetlifyDeployOutputs, + buildNetlifyExtraFields, + netlifySetupInstructions, + netlifyTriggerOptions, +} from '@/triggers/netlify/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const netlifyDeploySucceededTrigger: TriggerConfig = { + id: 'netlify_deploy_succeeded', + name: 'Netlify Deploy Succeeded', + provider: 'netlify', + description: 'Trigger workflow when a Netlify deploy completes successfully', + version: '1.0.0', + icon: NetlifyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'netlify_deploy_succeeded', + triggerOptions: netlifyTriggerOptions, + setupInstructions: netlifySetupInstructions('Deploy Succeeded'), + extraFields: buildNetlifyExtraFields('netlify_deploy_succeeded'), + }), + + outputs: buildNetlifyDeployOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/netlify/deploy_unlocked.ts b/apps/sim/triggers/netlify/deploy_unlocked.ts new file mode 100644 index 0000000000..535a4a1c84 --- /dev/null +++ b/apps/sim/triggers/netlify/deploy_unlocked.ts @@ -0,0 +1,34 @@ +import { NetlifyIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildNetlifyDeployOutputs, + buildNetlifyExtraFields, + netlifySetupInstructions, + netlifyTriggerOptions, +} from '@/triggers/netlify/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const netlifyDeployUnlockedTrigger: TriggerConfig = { + id: 'netlify_deploy_unlocked', + name: 'Netlify Deploy Unlocked', + provider: 'netlify', + description: 'Trigger workflow when a Netlify deploy is unlocked (auto-publish resumed)', + version: '1.0.0', + icon: NetlifyIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'netlify_deploy_unlocked', + triggerOptions: netlifyTriggerOptions, + setupInstructions: netlifySetupInstructions('Deploy Unlocked'), + extraFields: buildNetlifyExtraFields('netlify_deploy_unlocked'), + }), + + outputs: buildNetlifyDeployOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/netlify/index.ts b/apps/sim/triggers/netlify/index.ts new file mode 100644 index 0000000000..c703c58c57 --- /dev/null +++ b/apps/sim/triggers/netlify/index.ts @@ -0,0 +1,6 @@ +export { netlifyDeployBuildingTrigger } from './deploy_building' +export { netlifyDeployCreatedTrigger } from './deploy_created' +export { netlifyDeployFailedTrigger } from './deploy_failed' +export { netlifyDeployLockedTrigger } from './deploy_locked' +export { netlifyDeploySucceededTrigger } from './deploy_succeeded' +export { netlifyDeployUnlockedTrigger } from './deploy_unlocked' diff --git a/apps/sim/triggers/netlify/utils.ts b/apps/sim/triggers/netlify/utils.ts new file mode 100644 index 0000000000..8b88dcbc85 --- /dev/null +++ b/apps/sim/triggers/netlify/utils.ts @@ -0,0 +1,137 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +/** + * Dropdown options for the Netlify trigger type selector. + */ +export const netlifyTriggerOptions = [ + { label: 'Deploy Created', id: 'netlify_deploy_created' }, + { label: 'Deploy Building', id: 'netlify_deploy_building' }, + { label: 'Deploy Succeeded', id: 'netlify_deploy_succeeded' }, + { label: 'Deploy Failed', id: 'netlify_deploy_failed' }, + { label: 'Deploy Locked', id: 'netlify_deploy_locked' }, + { label: 'Deploy Unlocked', id: 'netlify_deploy_unlocked' }, +] + +/** + * Maps Sim trigger IDs to Netlify hook `event` names. Netlify outgoing webhooks + * require exactly one event per hook, so each trigger creates a single hook. + */ +export const NETLIFY_TRIGGER_EVENT_TYPES: Record = { + netlify_deploy_created: 'deploy_created', + netlify_deploy_building: 'deploy_building', + netlify_deploy_succeeded: 'deploy_succeeded', + netlify_deploy_failed: 'deploy_failed', + netlify_deploy_locked: 'deploy_locked', + netlify_deploy_unlocked: 'deploy_unlocked', +} + +/** + * Returns whether the incoming Netlify event matches the configured trigger. + * Netlify deploy webhooks deliver the deploy object directly (no `type` field), + * so we rely on the event configured at subscription time and rarely need to + * cross-check, but we expose the helper for symmetry with other providers. + */ +export function isNetlifyEventMatch(triggerId: string, state: string | undefined): boolean { + const expected = NETLIFY_TRIGGER_EVENT_TYPES[triggerId] + if (!expected) { + return false + } + if (!state) { + return true + } + switch (expected) { + case 'deploy_succeeded': + return state === 'ready' + case 'deploy_failed': + return state === 'error' || state === 'rejected' + case 'deploy_building': + return state === 'building' + default: + return true + } +} + +/** + * Generates HTML setup instructions shown inside the trigger config panel. + */ +export function netlifySetupInstructions(eventLabel: string): string { + const instructions = [ + 'Generate a Personal Access Token at User settings → Applications → Personal access tokens (direct link) and paste it above.', + 'Enter the target Site ID (or primary domain) of the Netlify site to listen on.', + `Deploy the workflow — Sim will automatically register an outgoing webhook in Netlify for ${eventLabel} events on the chosen site.`, + 'The webhook is automatically removed from Netlify when you delete this trigger or undeploy the workflow.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * Netlify-specific extra fields exposed in trigger configuration. + */ +export function buildNetlifyExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'apiKey', + title: 'Access Token', + type: 'short-input' as const, + placeholder: 'Enter your Netlify Personal Access Token', + description: 'Required to register and remove the webhook in Netlify.', + password: true, + required: true, + paramVisibility: 'user-only', + mode: 'trigger' as const, + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'siteId', + title: 'Site ID', + type: 'short-input' as const, + placeholder: 'Site ID or primary domain (e.g., 0d3a9d2f-... or my-site.netlify.app)', + description: 'The Netlify site whose deploys will trigger this workflow.', + required: true, + mode: 'trigger' as const, + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +const coreOutputs = { + id: { type: 'string', description: 'Deploy ID' }, + siteId: { type: 'string', description: 'Site ID' }, + state: { + type: 'string', + description: 'Deploy state at the time of the event (e.g., ready, error, building)', + }, + name: { type: 'string', description: 'Site name' }, + url: { type: 'string', description: 'Site URL' }, + deployUrl: { type: 'string', description: 'Unique deploy URL' }, + deploySslUrl: { type: 'string', description: 'Unique deploy HTTPS URL' }, + adminUrl: { type: 'string', description: 'Netlify admin URL' }, + branch: { type: 'string', description: 'Git branch' }, + context: { + type: 'string', + description: 'Deploy context: production, deploy-preview, branch-deploy', + }, + commitRef: { type: 'string', description: 'Git commit SHA' }, + commitUrl: { type: 'string', description: 'Git commit URL' }, + title: { type: 'string', description: 'Commit message / deploy title' }, + errorMessage: { type: 'string', description: 'Error message when the deploy failed' }, + createdAt: { type: 'string', description: 'Deploy creation timestamp' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + publishedAt: { type: 'string', description: 'Publish timestamp' }, + payload: { type: 'json', description: 'Raw deploy payload from Netlify' }, +} as const + +/** + * Build outputs for any Netlify deploy event. The shape of the deploy object + * is identical across event types — only the `state` field differs. + */ +export function buildNetlifyDeployOutputs(): Record { + return { ...coreOutputs } as Record +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index b1d747f529..2798b8425d 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -260,6 +260,14 @@ import { mondaySubitemCreatedTrigger, mondayUpdateCreatedTrigger, } from '@/triggers/monday' +import { + netlifyDeployBuildingTrigger, + netlifyDeployCreatedTrigger, + netlifyDeployFailedTrigger, + netlifyDeployLockedTrigger, + netlifyDeploySucceededTrigger, + netlifyDeployUnlockedTrigger, +} from '@/triggers/netlify' import { notionCommentCreatedTrigger, notionDatabaseCreatedTrigger, @@ -563,6 +571,12 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { whatsapp_webhook: whatsappWebhookTrigger, google_forms_webhook: googleFormsWebhookTrigger, twilio_voice_webhook: twilioVoiceWebhookTrigger, + netlify_deploy_created: netlifyDeployCreatedTrigger, + netlify_deploy_building: netlifyDeployBuildingTrigger, + netlify_deploy_succeeded: netlifyDeploySucceededTrigger, + netlify_deploy_failed: netlifyDeployFailedTrigger, + netlify_deploy_locked: netlifyDeployLockedTrigger, + netlify_deploy_unlocked: netlifyDeployUnlockedTrigger, vercel_deployment_created: vercelDeploymentCreatedTrigger, vercel_deployment_ready: vercelDeploymentReadyTrigger, vercel_deployment_error: vercelDeploymentErrorTrigger, From 36c8db91c028068a5b309314c14e0bde49be91ec Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 8 May 2026 15:18:52 -0400 Subject: [PATCH 2/5] refactor(netlify): switch trigger to manual webhook setup Removes auto-registration of Netlify outgoing webhooks because the data.signature_secret field doesn't reliably round-trip through the hooks API. The user now configures the webhook in Netlify's dashboard and pastes the JWS secret token into the trigger; Sim only verifies inbound signatures with that secret. Drops apiKey and siteId from the trigger config (still required on the block for tool operations). --- apps/sim/lib/webhooks/providers/netlify.ts | 156 +-------------------- apps/sim/triggers/netlify/utils.ts | 32 ++--- 2 files changed, 17 insertions(+), 171 deletions(-) diff --git a/apps/sim/lib/webhooks/providers/netlify.ts b/apps/sim/lib/webhooks/providers/netlify.ts index 72ca1edd15..5238e075a2 100644 --- a/apps/sim/lib/webhooks/providers/netlify.ts +++ b/apps/sim/lib/webhooks/providers/netlify.ts @@ -2,15 +2,11 @@ import crypto from 'crypto' import { createLogger } from '@sim/logger' import { safeCompare } from '@sim/security/compare' import { NextResponse } from 'next/server' -import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { AuthContext, - DeleteSubscriptionContext, EventMatchContext, FormatInputContext, FormatInputResult, - SubscriptionContext, - SubscriptionResult, WebhookProviderHandler, } from '@/lib/webhooks/providers/types' @@ -53,11 +49,11 @@ function verifyNetlifyJwt(token: string, secret: string, rawBody: string): boole export const netlifyHandler: WebhookProviderHandler = { verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext): NextResponse | null { - const secret = (providerConfig.webhookSecret as string | undefined)?.trim() + const secret = (providerConfig.signatureSecret as string | undefined)?.trim() if (!secret) { - logger.warn(`[${requestId}] Netlify webhook secret missing; rejecting delivery`) + logger.warn(`[${requestId}] Netlify signature secret missing; rejecting delivery`) return new NextResponse( - 'Unauthorized - Netlify webhook signing secret is not configured. Re-save the trigger so a webhook can be registered.', + 'Unauthorized - Netlify signature secret is not configured. Set the JWS secret token on this trigger.', { status: 401 } ) } @@ -105,152 +101,6 @@ export const netlifyHandler: WebhookProviderHandler = { return `netlify:${String(id)}` }, - async createSubscription(ctx: SubscriptionContext): Promise { - const { webhook, requestId } = ctx - try { - const providerConfig = getProviderConfig(webhook) - const apiKey = providerConfig.apiKey as string | undefined - const triggerId = providerConfig.triggerId as string | undefined - const siteId = (providerConfig.siteId as string | undefined)?.trim() - - if (!apiKey) { - throw new Error( - 'Netlify Personal Access Token is required. Provide your access token in the trigger configuration.' - ) - } - if (!siteId) { - throw new Error('Netlify Site ID is required to register a deploy webhook.') - } - if (!triggerId) { - throw new Error('Missing trigger ID — re-save the Netlify trigger.') - } - - const { NETLIFY_TRIGGER_EVENT_TYPES } = await import('@/triggers/netlify/utils') - const event = NETLIFY_TRIGGER_EVENT_TYPES[triggerId] - if (!event) { - throw new Error( - `Unknown Netlify trigger "${triggerId}". Remove and re-add the Netlify trigger, then save again.` - ) - } - - const notificationUrl = getNotificationUrl(webhook) - const signingSecret = crypto.randomBytes(32).toString('base64url') - - logger.info(`[${requestId}] Creating Netlify webhook`, { - triggerId, - event, - siteId, - webhookId: webhook.id, - }) - - const apiUrl = `https://api.netlify.com/api/v1/hooks?site_id=${encodeURIComponent(siteId)}` - const requestBody = { - type: 'url', - event, - data: { - url: notificationUrl, - signature_secret: signingSecret, - }, - } - - const netlifyResponse = await fetch(apiUrl, { - method: 'POST', - headers: { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }) - - const responseBody = (await netlifyResponse.json().catch(() => ({}))) as Record< - string, - unknown - > - - if (!netlifyResponse.ok) { - const errorMessage = - (responseBody.message as string) || - (responseBody.error as string) || - 'Unknown Netlify API error' - - let userFriendlyMessage = 'Failed to create webhook subscription in Netlify' - if (netlifyResponse.status === 401 || netlifyResponse.status === 403) { - userFriendlyMessage = - 'Invalid or insufficient Netlify Personal Access Token. Verify the token has access to this site.' - } else if (netlifyResponse.status === 404) { - userFriendlyMessage = `Netlify site "${siteId}" not found or not accessible with this token.` - } else if (errorMessage && errorMessage !== 'Unknown Netlify API error') { - userFriendlyMessage = `Netlify error: ${errorMessage}` - } - - throw new Error(userFriendlyMessage) - } - - const externalId = (responseBody.id as string | undefined) ?? undefined - if (!externalId) { - throw new Error('Netlify webhook creation succeeded but no hook ID was returned') - } - - logger.info(`[${requestId}] Successfully created Netlify hook ${externalId}`, { - webhookId: webhook.id, - event, - }) - - return { - providerConfigUpdates: { - externalId, - webhookSecret: signingSecret, - }, - } - } catch (error: unknown) { - const err = error as Error - logger.error(`[${requestId}] Exception during Netlify webhook creation`, { - message: err.message, - webhookId: webhook.id, - }) - throw error - } - }, - - async deleteSubscription(ctx: DeleteSubscriptionContext): Promise { - const { webhook, requestId } = ctx - try { - const config = getProviderConfig(webhook) - const apiKey = config.apiKey as string | undefined - const externalId = config.externalId as string | undefined - - if (!apiKey || !externalId) { - logger.warn( - `[${requestId}] Missing apiKey or externalId for Netlify webhook deletion ${webhook.id}, skipping cleanup` - ) - if (ctx.strict) throw new Error('Missing Netlify webhook deletion credentials') - return - } - - const apiUrl = `https://api.netlify.com/api/v1/hooks/${encodeURIComponent(externalId)}` - - const response = await fetch(apiUrl, { - method: 'DELETE', - headers: { - Authorization: `Bearer ${apiKey}`, - }, - }) - - if (!response.ok && response.status !== 404) { - logger.warn( - `[${requestId}] Failed to delete Netlify webhook (non-fatal): ${response.status}` - ) - if (ctx.strict) throw new Error(`Failed to delete Netlify webhook: ${response.status}`) - } else { - await response.body?.cancel() - logger.info(`[${requestId}] Successfully deleted Netlify hook ${externalId}`) - } - } catch (error) { - logger.warn(`[${requestId}] Error deleting Netlify webhook (non-fatal)`, error) - if (ctx.strict) throw error - } - }, - async formatInput(ctx: FormatInputContext): Promise { const body = ctx.body as Record diff --git a/apps/sim/triggers/netlify/utils.ts b/apps/sim/triggers/netlify/utils.ts index 8b88dcbc85..2080c33b18 100644 --- a/apps/sim/triggers/netlify/utils.ts +++ b/apps/sim/triggers/netlify/utils.ts @@ -54,13 +54,16 @@ export function isNetlifyEventMatch(triggerId: string, state: string | undefined /** * Generates HTML setup instructions shown inside the trigger config panel. + * Netlify outgoing webhooks are configured manually in the Netlify dashboard; + * Sim verifies inbound deliveries using the JWS secret token the user provides. */ export function netlifySetupInstructions(eventLabel: string): string { const instructions = [ - 'Generate a Personal Access Token at User settings → Applications → Personal access tokens (direct link) and paste it above.', - 'Enter the target Site ID (or primary domain) of the Netlify site to listen on.', - `Deploy the workflow — Sim will automatically register an outgoing webhook in Netlify for ${eventLabel} events on the chosen site.`, - 'The webhook is automatically removed from Netlify when you delete this trigger or undeploy the workflow.', + 'Copy the Webhook URL above.', + 'In Netlify open Site settings → Build & deploy → Deploy notifications → Add notification → Outgoing webhook.', + `Paste the URL, choose ${eventLabel} as the event to listen for, generate a JWS secret token, and save the notification.`, + 'Paste that same JWS secret into the Signature Secret field above, then Deploy the workflow.', + 'Remove the notification in Netlify when you delete this trigger — Sim does not manage the webhook on your behalf.', ] return instructions @@ -73,31 +76,24 @@ export function netlifySetupInstructions(eventLabel: string): string { /** * Netlify-specific extra fields exposed in trigger configuration. + * The signature secret is the JWS secret token the user configures on the + * outgoing webhook in Netlify; we use it to verify inbound deliveries. */ export function buildNetlifyExtraFields(triggerId: string): SubBlockConfig[] { return [ { - id: 'apiKey', - title: 'Access Token', + id: 'signatureSecret', + title: 'Signature Secret', type: 'short-input' as const, - placeholder: 'Enter your Netlify Personal Access Token', - description: 'Required to register and remove the webhook in Netlify.', + placeholder: 'Paste the JWS secret token configured in Netlify', + description: + 'The JWS secret token set on the outgoing webhook in Netlify. Used to verify the signature of every incoming delivery.', password: true, required: true, paramVisibility: 'user-only', mode: 'trigger' as const, condition: { field: 'selectedTriggerId', value: triggerId }, }, - { - id: 'siteId', - title: 'Site ID', - type: 'short-input' as const, - placeholder: 'Site ID or primary domain (e.g., 0d3a9d2f-... or my-site.netlify.app)', - description: 'The Netlify site whose deploys will trigger this workflow.', - required: true, - mode: 'trigger' as const, - condition: { field: 'selectedTriggerId', value: triggerId }, - }, ] } From 287fbb18bb6d40a79aa6095d2288e3ccf38f3114 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 8 May 2026 15:31:03 -0400 Subject: [PATCH 3/5] fix(netlify): include deploy state in idempotency key Multiple Netlify deploy events (created, building, succeeded) share the same deploy id, so keying dedup only on id collapsed distinct events into one. Including state plus the locked flag differentiates each event for the same deploy. --- apps/sim/lib/webhooks/providers/netlify.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/webhooks/providers/netlify.ts b/apps/sim/lib/webhooks/providers/netlify.ts index 5238e075a2..fbc990a892 100644 --- a/apps/sim/lib/webhooks/providers/netlify.ts +++ b/apps/sim/lib/webhooks/providers/netlify.ts @@ -94,11 +94,14 @@ export const netlifyHandler: WebhookProviderHandler = { }, extractIdempotencyId(body: unknown) { - const id = (body as Record)?.id + const obj = body as Record | undefined + const id = obj?.id if (id === undefined || id === null || id === '') { return null } - return `netlify:${String(id)}` + const state = typeof obj?.state === 'string' ? obj.state : '' + const locked = obj?.locked === true ? '1' : '0' + return `netlify:${String(id)}:${state}:${locked}` }, async formatInput(ctx: FormatInputContext): Promise { From 34cb2745ec5abe3b9e6f5474ef9369d781b10a22 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 8 May 2026 15:36:28 -0400 Subject: [PATCH 4/5] chore(netlify): generate docs and align registry with skill checklist Runs generate-docs.ts to produce the tool and trigger MDX pages, the integrations.json entry, and icon mappings for the docs app. Adds Netlify to the trigger provider display-name map so the docs render the proper label. Reorders the webhook provider registry so netlify sits alphabetically between monday and notion instead of being parked next to vercel. --- apps/docs/components/icons.tsx | 24 ++ apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/meta.json | 1 + apps/docs/content/docs/en/tools/netlify.mdx | 294 ++++++++++++++++++ apps/docs/content/docs/en/triggers/meta.json | 1 + .../docs/content/docs/en/triggers/netlify.mdx | 229 ++++++++++++++ .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 86 +++++ apps/sim/lib/webhooks/providers/registry.ts | 2 +- scripts/generate-docs.ts | 1 + 10 files changed, 641 insertions(+), 1 deletion(-) create mode 100644 apps/docs/content/docs/en/tools/netlify.mdx create mode 100644 apps/docs/content/docs/en/triggers/netlify.mdx diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 4092f8c10a..8763ee093f 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -6727,6 +6727,30 @@ export function VercelIcon(props: SVGProps) { ) } +export function NetlifyIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function CloudflareIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index b515c6ccdd..ca76723f84 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -126,6 +126,7 @@ import { MongoDBIcon, MySQLIcon, Neo4jIcon, + NetlifyIcon, NotionIcon, ObsidianIcon, OktaIcon, @@ -341,6 +342,7 @@ export const blockTypeToIconMap: Record = { mongodb: MongoDBIcon, mysql: MySQLIcon, neo4j: Neo4jIcon, + netlify: NetlifyIcon, notion: NotionIcon, notion_v2: NotionIcon, obsidian: ObsidianIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index af967f765a..227c65c5c0 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -122,6 +122,7 @@ "mongodb", "mysql", "neo4j", + "netlify", "notion", "obsidian", "okta", diff --git a/apps/docs/content/docs/en/tools/netlify.mdx b/apps/docs/content/docs/en/tools/netlify.mdx new file mode 100644 index 0000000000..faf5068cfe --- /dev/null +++ b/apps/docs/content/docs/en/tools/netlify.mdx @@ -0,0 +1,294 @@ +--- +title: Netlify +description: Manage Netlify sites, deploys, and environment variables +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Trigger and inspect Netlify deploys (builds), and manage account or site-scoped environment variables. Generate a Personal Access Token at https://app.netlify.com/user/applications#personal-access-tokens. + + + +## Tools + +### `netlify_list_sites` + +List Netlify sites accessible to the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Netlify Personal Access Token | +| `name` | string | No | Filter sites by name | +| `filter` | string | No | Filter scope: all, owner, or guest | +| `page` | number | No | Page number \(1-indexed\) | +| `perPage` | number | No | Results per page \(max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sites` | array | List of Netlify sites | +| ↳ `id` | string | Site ID | +| ↳ `name` | string | Site name | +| ↳ `url` | string | Primary site URL | +| ↳ `sslUrl` | string | HTTPS site URL | +| ↳ `adminUrl` | string | Netlify admin URL | +| ↳ `customDomain` | string | Custom domain | +| ↳ `accountId` | string | Owning account ID | +| ↳ `accountSlug` | string | Owning account slug | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last update timestamp | +| `count` | number | Number of sites returned | + +### `netlify_list_deploys` + +List deploys for a Netlify site + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Netlify Personal Access Token | +| `siteId` | string | Yes | Site ID or primary domain | +| `state` | string | No | Filter by deploy state: ready, error, building, enqueued, processing, uploading, new | +| `branch` | string | No | Filter by git branch | +| `production` | string | No | Filter to production deploys only \("true" or "false"\) | +| `page` | number | No | Page number \(1-indexed\) | +| `perPage` | number | No | Results per page \(max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deploys` | array | List of deploys | +| ↳ `id` | string | Deploy ID | +| ↳ `siteId` | string | Site ID | +| ↳ `state` | string | Deploy state: new, enqueued, building, uploading, processing, ready, error, retrying | +| ↳ `name` | string | Site name | +| ↳ `url` | string | Site URL | +| ↳ `deployUrl` | string | Unique deploy URL | +| ↳ `deploySslUrl` | string | Unique deploy HTTPS URL | +| ↳ `adminUrl` | string | Netlify admin URL | +| ↳ `branch` | string | Git branch | +| ↳ `context` | string | Deploy context: production, deploy-preview, branch-deploy | +| ↳ `commitRef` | string | Git commit SHA | +| ↳ `commitUrl` | string | Git commit URL | +| ↳ `errorMessage` | string | Error message if failed | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last update timestamp | +| ↳ `publishedAt` | string | Publish timestamp | +| `count` | number | Number of deploys returned | + +### `netlify_get_deploy` + +Get details of a specific Netlify deploy + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Netlify Personal Access Token | +| `deployId` | string | Yes | Deploy ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Deploy ID | +| `siteId` | string | Site ID | +| `state` | string | Deploy state | +| `name` | string | Site name | +| `url` | string | Site URL | +| `deployUrl` | string | Unique deploy URL | +| `deploySslUrl` | string | Unique deploy HTTPS URL | +| `adminUrl` | string | Netlify admin URL | +| `branch` | string | Git branch | +| `context` | string | Deploy context | +| `commitRef` | string | Git commit SHA | +| `commitUrl` | string | Git commit URL | +| `errorMessage` | string | Error message if failed | +| `createdAt` | string | Creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `publishedAt` | string | Publish timestamp | + +### `netlify_create_deploy` + +Trigger a new Netlify deploy by starting a build for a site (optionally from a specific branch) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Netlify Personal Access Token | +| `siteId` | string | Yes | Site ID or primary domain to deploy | +| `branch` | string | No | Git branch to build from \(defaults to the site’s configured production branch\) | +| `title` | string | No | Optional human-readable label shown in the deploy log | +| `clearCache` | string | No | Clear the build cache before deploying \("true" or "false"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Build ID | +| `deployId` | string | Deploy ID produced by this build \(use to poll status\) | +| `siteId` | string | Site ID | +| `sha` | string | Git commit SHA being built | +| `done` | boolean | Whether the build has completed | +| `error` | string | Build error if any | +| `createdAt` | string | Creation timestamp | + +### `netlify_cancel_deploy` + +Cancel an in-progress Netlify deploy + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Netlify Personal Access Token | +| `deployId` | string | Yes | Deploy ID to cancel | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Deploy ID | +| `siteId` | string | Site ID | +| `state` | string | Deploy state after cancellation | +| `name` | string | Site name | +| `url` | string | Site URL | +| `deployUrl` | string | Unique deploy URL | +| `deploySslUrl` | string | Unique deploy HTTPS URL | +| `adminUrl` | string | Netlify admin URL | +| `branch` | string | Git branch | +| `context` | string | Deploy context | +| `commitRef` | string | Git commit SHA | +| `commitUrl` | string | Git commit URL | +| `errorMessage` | string | Error message if failed | +| `createdAt` | string | Creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `publishedAt` | string | Publish timestamp | + +### `netlify_list_env_vars` + +List environment variables for an account, optionally scoped to a site + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Netlify Personal Access Token | +| `accountId` | string | Yes | Account ID or slug that owns the environment variables | +| `siteId` | string | No | Optional site ID to scope variables to a specific site | +| `contextName` | string | No | Filter by deploy context \(production, deploy-preview, branch-deploy, dev\) | +| `scope` | string | No | Filter by scope \(builds, functions, runtime, post_processing\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `envVars` | array | List of environment variables | +| ↳ `key` | string | Variable name | +| ↳ `scopes` | array | Where the variable applies \(builds, functions, runtime, post_processing\) | +| ↳ `values` | array | Per-context values | +| ↳ `id` | string | Value ID | +| ↳ `context` | string | Context name | +| ↳ `contextParameter` | string | Branch name when context is branch-deploy | +| ↳ `value` | string | Variable value | +| ↳ `isSecret` | boolean | Whether the value is secret | +| ↳ `updatedAt` | string | Last update timestamp | +| `count` | number | Number of variables returned | + +### `netlify_create_env_var` + +Create a new environment variable for an account, optionally scoped to a site + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Netlify Personal Access Token | +| `accountId` | string | Yes | Account ID or slug that owns the variable | +| `siteId` | string | No | Optional site ID to scope the variable to a specific site | +| `key` | string | Yes | Variable name \(e.g., DATABASE_URL\) | +| `value` | string | Yes | Variable value | +| `context` | string | No | Deploy context this value applies to: all, production, deploy-preview, branch-deploy, dev \(default: all\) | +| `scopes` | string | No | Comma-separated scopes \(builds, functions, runtime, post_processing\). Defaults to all scopes | +| `isSecret` | string | No | Mark the value as secret \("true" or "false"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `envVar` | object | Created environment variable | +| ↳ `key` | string | Variable name | +| ↳ `scopes` | array | Scopes | +| ↳ `values` | array | Per-context values | +| ↳ `id` | string | Value ID | +| ↳ `context` | string | Context name | +| ↳ `contextParameter` | string | Branch name for branch-deploy context | +| ↳ `value` | string | Variable value | +| ↳ `isSecret` | boolean | Whether the value is secret | +| ↳ `updatedAt` | string | Last update timestamp | + +### `netlify_update_env_var` + +Replace an environment variable for an account, optionally scoped to a site + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Netlify Personal Access Token | +| `accountId` | string | Yes | Account ID or slug that owns the variable | +| `siteId` | string | No | Optional site ID to scope the variable to a specific site | +| `key` | string | Yes | Variable name to update | +| `value` | string | Yes | New variable value \(replaces all existing values\) | +| `context` | string | No | Deploy context: all, production, deploy-preview, branch-deploy, dev \(default: all\) | +| `scopes` | string | No | Comma-separated scopes \(builds, functions, runtime, post_processing\) | +| `isSecret` | string | No | Mark the value as secret \("true" or "false"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `envVar` | object | Updated environment variable | +| ↳ `key` | string | Variable name | +| ↳ `scopes` | array | Scopes | +| ↳ `values` | array | Per-context values | +| ↳ `id` | string | Value ID | +| ↳ `context` | string | Context name | +| ↳ `contextParameter` | string | Branch name for branch-deploy context | +| ↳ `value` | string | Variable value | +| ↳ `isSecret` | boolean | Whether the value is secret | +| ↳ `updatedAt` | string | Last update timestamp | + +### `netlify_delete_env_var` + +Delete an environment variable from an account, optionally scoped to a site + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Netlify Personal Access Token | +| `accountId` | string | Yes | Account ID or slug that owns the variable | +| `siteId` | string | No | Optional site ID to scope deletion to a specific site | +| `key` | string | Yes | Variable name to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the environment variable was deleted | + + diff --git a/apps/docs/content/docs/en/triggers/meta.json b/apps/docs/content/docs/en/triggers/meta.json index 70d13afd92..6410cbff1b 100644 --- a/apps/docs/content/docs/en/triggers/meta.json +++ b/apps/docs/content/docs/en/triggers/meta.json @@ -33,6 +33,7 @@ "linear", "microsoft-teams", "monday", + "netlify", "notion", "outlook", "resend", diff --git a/apps/docs/content/docs/en/triggers/netlify.mdx b/apps/docs/content/docs/en/triggers/netlify.mdx new file mode 100644 index 0000000000..127426972b --- /dev/null +++ b/apps/docs/content/docs/en/triggers/netlify.mdx @@ -0,0 +1,229 @@ +--- +title: Netlify +description: Available Netlify triggers for automating workflows +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +Netlify provides 6 triggers for automating workflows based on events. + +## Triggers + +### Netlify Deploy Building + +Trigger workflow when Netlify starts building a deploy + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signatureSecret` | string | Yes | The JWS secret token set on the outgoing webhook in Netlify. Used to verify the signature of every incoming delivery. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Deploy ID | +| `siteId` | string | Site ID | +| `state` | string | Deploy state at the time of the event \(e.g., ready, error, building\) | +| `name` | string | Site name | +| `url` | string | Site URL | +| `deployUrl` | string | Unique deploy URL | +| `deploySslUrl` | string | Unique deploy HTTPS URL | +| `adminUrl` | string | Netlify admin URL | +| `branch` | string | Git branch | +| `context` | string | Deploy context: production, deploy-preview, branch-deploy | +| `commitRef` | string | Git commit SHA | +| `commitUrl` | string | Git commit URL | +| `title` | string | Commit message / deploy title | +| `errorMessage` | string | Error message when the deploy failed | +| `createdAt` | string | Deploy creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `publishedAt` | string | Publish timestamp | +| `payload` | json | Raw deploy payload from Netlify | + + +--- + +### Netlify Deploy Created + +Trigger workflow when a new Netlify deploy is created (build queued) + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signatureSecret` | string | Yes | The JWS secret token set on the outgoing webhook in Netlify. Used to verify the signature of every incoming delivery. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Deploy ID | +| `siteId` | string | Site ID | +| `state` | string | Deploy state at the time of the event \(e.g., ready, error, building\) | +| `name` | string | Site name | +| `url` | string | Site URL | +| `deployUrl` | string | Unique deploy URL | +| `deploySslUrl` | string | Unique deploy HTTPS URL | +| `adminUrl` | string | Netlify admin URL | +| `branch` | string | Git branch | +| `context` | string | Deploy context: production, deploy-preview, branch-deploy | +| `commitRef` | string | Git commit SHA | +| `commitUrl` | string | Git commit URL | +| `title` | string | Commit message / deploy title | +| `errorMessage` | string | Error message when the deploy failed | +| `createdAt` | string | Deploy creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `publishedAt` | string | Publish timestamp | +| `payload` | json | Raw deploy payload from Netlify | + + +--- + +### Netlify Deploy Failed + +Trigger workflow when a Netlify deploy fails + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signatureSecret` | string | Yes | The JWS secret token set on the outgoing webhook in Netlify. Used to verify the signature of every incoming delivery. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Deploy ID | +| `siteId` | string | Site ID | +| `state` | string | Deploy state at the time of the event \(e.g., ready, error, building\) | +| `name` | string | Site name | +| `url` | string | Site URL | +| `deployUrl` | string | Unique deploy URL | +| `deploySslUrl` | string | Unique deploy HTTPS URL | +| `adminUrl` | string | Netlify admin URL | +| `branch` | string | Git branch | +| `context` | string | Deploy context: production, deploy-preview, branch-deploy | +| `commitRef` | string | Git commit SHA | +| `commitUrl` | string | Git commit URL | +| `title` | string | Commit message / deploy title | +| `errorMessage` | string | Error message when the deploy failed | +| `createdAt` | string | Deploy creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `publishedAt` | string | Publish timestamp | +| `payload` | json | Raw deploy payload from Netlify | + + +--- + +### Netlify Deploy Locked + +Trigger workflow when a Netlify deploy is locked to a published version + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signatureSecret` | string | Yes | The JWS secret token set on the outgoing webhook in Netlify. Used to verify the signature of every incoming delivery. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Deploy ID | +| `siteId` | string | Site ID | +| `state` | string | Deploy state at the time of the event \(e.g., ready, error, building\) | +| `name` | string | Site name | +| `url` | string | Site URL | +| `deployUrl` | string | Unique deploy URL | +| `deploySslUrl` | string | Unique deploy HTTPS URL | +| `adminUrl` | string | Netlify admin URL | +| `branch` | string | Git branch | +| `context` | string | Deploy context: production, deploy-preview, branch-deploy | +| `commitRef` | string | Git commit SHA | +| `commitUrl` | string | Git commit URL | +| `title` | string | Commit message / deploy title | +| `errorMessage` | string | Error message when the deploy failed | +| `createdAt` | string | Deploy creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `publishedAt` | string | Publish timestamp | +| `payload` | json | Raw deploy payload from Netlify | + + +--- + +### Netlify Deploy Succeeded + +Trigger workflow when a Netlify deploy completes successfully + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signatureSecret` | string | Yes | The JWS secret token set on the outgoing webhook in Netlify. Used to verify the signature of every incoming delivery. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Deploy ID | +| `siteId` | string | Site ID | +| `state` | string | Deploy state at the time of the event \(e.g., ready, error, building\) | +| `name` | string | Site name | +| `url` | string | Site URL | +| `deployUrl` | string | Unique deploy URL | +| `deploySslUrl` | string | Unique deploy HTTPS URL | +| `adminUrl` | string | Netlify admin URL | +| `branch` | string | Git branch | +| `context` | string | Deploy context: production, deploy-preview, branch-deploy | +| `commitRef` | string | Git commit SHA | +| `commitUrl` | string | Git commit URL | +| `title` | string | Commit message / deploy title | +| `errorMessage` | string | Error message when the deploy failed | +| `createdAt` | string | Deploy creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `publishedAt` | string | Publish timestamp | +| `payload` | json | Raw deploy payload from Netlify | + + +--- + +### Netlify Deploy Unlocked + +Trigger workflow when a Netlify deploy is unlocked (auto-publish resumed) + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `signatureSecret` | string | Yes | The JWS secret token set on the outgoing webhook in Netlify. Used to verify the signature of every incoming delivery. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Deploy ID | +| `siteId` | string | Site ID | +| `state` | string | Deploy state at the time of the event \(e.g., ready, error, building\) | +| `name` | string | Site name | +| `url` | string | Site URL | +| `deployUrl` | string | Unique deploy URL | +| `deploySslUrl` | string | Unique deploy HTTPS URL | +| `adminUrl` | string | Netlify admin URL | +| `branch` | string | Git branch | +| `context` | string | Deploy context: production, deploy-preview, branch-deploy | +| `commitRef` | string | Git commit SHA | +| `commitUrl` | string | Git commit URL | +| `title` | string | Commit message / deploy title | +| `errorMessage` | string | Error message when the deploy failed | +| `createdAt` | string | Deploy creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `publishedAt` | string | Publish timestamp | +| `payload` | json | Raw deploy payload from Netlify | + diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index e19d3d267b..98b59cd613 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -126,6 +126,7 @@ import { MongoDBIcon, MySQLIcon, Neo4jIcon, + NetlifyIcon, NotionIcon, ObsidianIcon, OktaIcon, @@ -326,6 +327,7 @@ export const blockTypeToIconMap: Record = { mongodb: MongoDBIcon, mysql: MySQLIcon, neo4j: Neo4jIcon, + netlify: NetlifyIcon, notion_v2: NotionIcon, obsidian: ObsidianIcon, okta: OktaIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 91d27d0e15..6fcd926a56 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -9303,6 +9303,92 @@ "integrationTypes": ["databases", "analytics"], "tags": ["data-warehouse", "data-analytics"] }, + { + "type": "netlify", + "slug": "netlify", + "name": "Netlify", + "description": "Manage Netlify sites, deploys, and environment variables", + "longDescription": "Trigger and inspect Netlify deploys (builds), and manage account or site-scoped environment variables. Generate a Personal Access Token at https://app.netlify.com/user/applications#personal-access-tokens.", + "bgColor": "#00C7B7", + "iconName": "NetlifyIcon", + "docsUrl": "https://docs.sim.ai/tools/netlify", + "operations": [ + { + "name": "List Sites", + "description": "List Netlify sites accessible to the authenticated user" + }, + { + "name": "List Deploys", + "description": "List deploys for a Netlify site" + }, + { + "name": "Get Deploy", + "description": "Get details of a specific Netlify deploy" + }, + { + "name": "Create Deploy", + "description": "Trigger a new Netlify deploy by starting a build for a site (optionally from a specific branch)" + }, + { + "name": "Cancel Deploy", + "description": "Cancel an in-progress Netlify deploy" + }, + { + "name": "List Environment Variables", + "description": "List environment variables for an account, optionally scoped to a site" + }, + { + "name": "Create Environment Variable", + "description": "Create a new environment variable for an account, optionally scoped to a site" + }, + { + "name": "Update Environment Variable", + "description": "Replace an environment variable for an account, optionally scoped to a site" + }, + { + "name": "Delete Environment Variable", + "description": "Delete an environment variable from an account, optionally scoped to a site" + } + ], + "operationCount": 9, + "triggers": [ + { + "id": "netlify_deploy_created", + "name": "Netlify Deploy Created", + "description": "Trigger workflow when a new Netlify deploy is created (build queued)" + }, + { + "id": "netlify_deploy_building", + "name": "Netlify Deploy Building", + "description": "Trigger workflow when Netlify starts building a deploy" + }, + { + "id": "netlify_deploy_succeeded", + "name": "Netlify Deploy Succeeded", + "description": "Trigger workflow when a Netlify deploy completes successfully" + }, + { + "id": "netlify_deploy_failed", + "name": "Netlify Deploy Failed", + "description": "Trigger workflow when a Netlify deploy fails" + }, + { + "id": "netlify_deploy_locked", + "name": "Netlify Deploy Locked", + "description": "Trigger workflow when a Netlify deploy is locked to a published version" + }, + { + "id": "netlify_deploy_unlocked", + "name": "Netlify Deploy Unlocked", + "description": "Trigger workflow when a Netlify deploy is unlocked (auto-publish resumed)" + } + ], + "triggerCount": 6, + "authType": "api-key", + "category": "tools", + "integrationTypes": ["developer-tools"], + "tags": ["cloud", "ci-cd"] + }, { "type": "notion_v2", "slug": "notion", diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 571ee1a54c..b3eea79255 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -75,6 +75,7 @@ const PROVIDER_HANDLERS: Record = { lemlist: lemlistHandler, linear: linearHandler, monday: mondayHandler, + netlify: netlifyHandler, resend: resendHandler, 'microsoft-teams': microsoftTeamsHandler, notion: notionHandler, @@ -89,7 +90,6 @@ const PROVIDER_HANDLERS: Record = { twilio: twilioHandler, twilio_voice: twilioVoiceHandler, typeform: typeformHandler, - netlify: netlifyHandler, vercel: vercelHandler, webflow: webflowHandler, whatsapp: whatsappHandler, diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 65d93e3e39..8a3e5e444e 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -73,6 +73,7 @@ const TRIGGER_PROVIDER_DISPLAY_NAMES: Record = { lemlist: 'Lemlist', linear: 'Linear', 'microsoft-teams': 'Microsoft Teams', + netlify: 'Netlify', notion: 'Notion', outlook: 'Outlook', resend: 'Resend', From a5a8100cab1d0935d2886820edb8b72029234240 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 8 May 2026 15:40:47 -0400 Subject: [PATCH 5/5] fix(netlify): expose pagination + expand block outputs The list_sites and list_deploys tools support page and per_page parameters but the block didn't surface them, so users couldn't paginate from the UI. Adds Limit and Page advanced fields with numeric coercion in tools.config.params. Expands the block outputs for get_deploy, cancel_deploy, and create_deploy so the tag dropdown surfaces siteId, branch, commitRef, errorMessage, sha, and done in addition to id and state. The tools already returned these, but only a subset was discoverable. --- apps/sim/blocks/blocks/netlify.ts | 69 ++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/netlify.ts b/apps/sim/blocks/blocks/netlify.ts index 5469c63f10..9578d25b53 100644 --- a/apps/sim/blocks/blocks/netlify.ts +++ b/apps/sim/blocks/blocks/netlify.ts @@ -150,6 +150,22 @@ export const NetlifyBlock: BlockConfig = { condition: { field: 'operation', value: 'list_deploys' }, mode: 'advanced', }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Results per page (max 100)', + condition: { field: 'operation', value: ['list_sites', 'list_deploys'] }, + mode: 'advanced', + }, + { + id: 'page', + title: 'Page', + type: 'short-input', + placeholder: 'Page number (1-indexed)', + condition: { field: 'operation', value: ['list_sites', 'list_deploys'] }, + mode: 'advanced', + }, { id: 'deployBranch', title: 'Branch', @@ -299,9 +315,18 @@ export const NetlifyBlock: BlockConfig = { envContext, envScopes, envIsSecret, + limit, + page, ...rest } = params + const limitNum = limit ? Number(limit) : undefined + const pageNum = page ? Number(page) : undefined + const paginationParams = { + ...(limitNum && !Number.isNaN(limitNum) ? { perPage: limitNum } : {}), + ...(pageNum && !Number.isNaN(pageNum) ? { page: pageNum } : {}), + } + const base = { ...rest, apiKey } switch (operation) { @@ -310,6 +335,7 @@ export const NetlifyBlock: BlockConfig = { ...base, ...(siteName ? { name: siteName } : {}), ...(sitesFilter ? { filter: sitesFilter } : {}), + ...paginationParams, } case 'list_deploys': return { @@ -317,6 +343,7 @@ export const NetlifyBlock: BlockConfig = { ...(branchFilter ? { branch: branchFilter } : {}), ...(stateFilter ? { state: stateFilter } : {}), ...(productionFilter ? { production: productionFilter } : {}), + ...paginationParams, } case 'create_deploy': return { @@ -373,6 +400,8 @@ export const NetlifyBlock: BlockConfig = { envContext: { type: 'string', description: 'Deploy context for the value' }, envScopes: { type: 'string', description: 'Comma-separated scopes' }, envIsSecret: { type: 'string', description: 'Mark the value as secret' }, + limit: { type: 'string', description: 'Results per page for list operations' }, + page: { type: 'string', description: 'Page number for list operations' }, }, outputs: { sites: { @@ -397,7 +426,15 @@ export const NetlifyBlock: BlockConfig = { }, id: { type: 'string', - description: 'Resource ID', + description: 'Resource ID (deploy ID for deploy ops; build ID for create_deploy)', + condition: { + field: 'operation', + value: ['get_deploy', 'cancel_deploy', 'create_deploy'], + }, + }, + siteId: { + type: 'string', + description: 'Site ID', condition: { field: 'operation', value: ['get_deploy', 'cancel_deploy', 'create_deploy'], @@ -413,11 +450,41 @@ export const NetlifyBlock: BlockConfig = { description: 'Unique deploy URL', condition: { field: 'operation', value: ['get_deploy', 'cancel_deploy'] }, }, + deploySslUrl: { + type: 'string', + description: 'Unique deploy HTTPS URL', + condition: { field: 'operation', value: ['get_deploy', 'cancel_deploy'] }, + }, + branch: { + type: 'string', + description: 'Git branch the deploy was built from', + condition: { field: 'operation', value: ['get_deploy', 'cancel_deploy'] }, + }, + commitRef: { + type: 'string', + description: 'Git commit SHA', + condition: { field: 'operation', value: ['get_deploy', 'cancel_deploy'] }, + }, + errorMessage: { + type: 'string', + description: 'Error message when the deploy failed', + condition: { field: 'operation', value: ['get_deploy', 'cancel_deploy'] }, + }, deployId: { type: 'string', description: 'Deploy ID produced by a build', condition: { field: 'operation', value: 'create_deploy' }, }, + sha: { + type: 'string', + description: 'Git commit SHA being built', + condition: { field: 'operation', value: 'create_deploy' }, + }, + done: { + type: 'boolean', + description: 'Whether the build has completed', + condition: { field: 'operation', value: 'create_deploy' }, + }, deleted: { type: 'boolean', description: 'Whether the resource was deleted',