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/blocks/blocks/netlify.ts b/apps/sim/blocks/blocks/netlify.ts new file mode 100644 index 0000000000..9578d25b53 --- /dev/null +++ b/apps/sim/blocks/blocks/netlify.ts @@ -0,0 +1,495 @@ +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: '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', + 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, + 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) { + case 'list_sites': + return { + ...base, + ...(siteName ? { name: siteName } : {}), + ...(sitesFilter ? { filter: sitesFilter } : {}), + ...paginationParams, + } + case 'list_deploys': + return { + ...base, + ...(branchFilter ? { branch: branchFilter } : {}), + ...(stateFilter ? { state: stateFilter } : {}), + ...(productionFilter ? { production: productionFilter } : {}), + ...paginationParams, + } + 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' }, + limit: { type: 'string', description: 'Results per page for list operations' }, + page: { type: 'string', description: 'Page number for list operations' }, + }, + 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 (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'], + }, + }, + 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'] }, + }, + 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', + 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..fbc990a892 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/netlify.ts @@ -0,0 +1,135 @@ +import crypto from 'crypto' +import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { NextResponse } from 'next/server' +import type { + AuthContext, + EventMatchContext, + FormatInputContext, + FormatInputResult, + 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.signatureSecret as string | undefined)?.trim() + if (!secret) { + logger.warn(`[${requestId}] Netlify signature secret missing; rejecting delivery`) + return new NextResponse( + 'Unauthorized - Netlify signature secret is not configured. Set the JWS secret token on this trigger.', + { 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 obj = body as Record | undefined + const id = obj?.id + if (id === undefined || id === null || id === '') { + return null + } + 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 { + 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..b3eea79255 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' @@ -74,6 +75,7 @@ const PROVIDER_HANDLERS: Record = { lemlist: lemlistHandler, linear: linearHandler, monday: mondayHandler, + netlify: netlifyHandler, resend: resendHandler, 'microsoft-teams': microsoftTeamsHandler, notion: notionHandler, 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..2080c33b18 --- /dev/null +++ b/apps/sim/triggers/netlify/utils.ts @@ -0,0 +1,133 @@ +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. + * 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 = [ + '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 + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * 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: 'signatureSecret', + title: 'Signature Secret', + type: 'short-input' as const, + 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 }, + }, + ] +} + +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, 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',