Skip to content

feat(netlify): add Netlify block with deploys, env vars, and deploy triggers#4518

Open
stylessh wants to merge 5 commits intosimstudioai:stagingfrom
stylessh:feat/netlify-integration
Open

feat(netlify): add Netlify block with deploys, env vars, and deploy triggers#4518
stylessh wants to merge 5 commits intosimstudioai:stagingfrom
stylessh:feat/netlify-integration

Conversation

@stylessh
Copy link
Copy Markdown
Contributor

@stylessh stylessh commented May 8, 2026

Summary

  • Adds a Netlify block with nine tool operations: list sites, list/get/cancel/create deploys, and list/create/update/delete environment variables. Authentication is a Personal Access Token sent as Authorization: Bearer <token>.
  • Adds six deploy webhook triggers (deploy_created, deploy_building, deploy_succeeded, deploy_failed, deploy_locked, deploy_unlocked). Sim auto-registers an outgoing webhook on the chosen site at deploy time and removes it on undeploy.
  • Inbound webhook signatures are verified as JWTs (HS256, iss=netlify, with the sha256 claim matching sha256(rawBody)) using the crypto standard library — no jsonwebtoken dependency added.

Notes on Netlify specifics

  • "Deployment" in Vercel terms maps to a deploy in Netlify. Creating a deploy is implemented by triggering a build from a branch (POST /sites/{site_id}/builds?branch=...), which produces a deploy.
  • Netlify hooks are one event per hook, so each Sim trigger maps to exactly one Netlify hook. There's no combined "common events" trigger like Vercel.
  • The signing secret is generated client-side and passed via data.signature_secret when creating the hook. If Netlify ever changes that field, the signature check is the canary.

Test plan

  • List sites with a valid nfp_ PAT
  • Trigger a deploy via the Create Deploy operation against a real site (with and without branch)
  • List, get, and cancel a running deploy
  • CRUD environment variables (account-scoped, then with siteId for site override)
  • Deploy a workflow with the Deploy Succeeded trigger; confirm the hook appears in Netlify under Site settings → Build & deploy → Deploy notifications
  • Push a commit (or hit create_deploy) and verify the workflow fires with a normalized deploy payload
  • Confirm X-Webhook-Signature JWT is verified (tamper with the body locally to confirm a 401)
  • Undeploy the workflow; confirm the hook is removed in Netlify

…riggers

Adds the Netlify integration with nine tool operations (list sites, list/get/cancel/create deploys, and list/create/update/delete env vars) plus six webhook triggers (deploy created, building, succeeded, failed, locked, unlocked).

Tools authenticate via a Netlify Personal Access Token (Bearer). Triggers automatically register an outgoing webhook on the chosen site at deploy time and clean it up on undeploy. Inbound webhook signatures are verified as JWTs (HS256, iss=netlify, sha256 claim of the raw body) without adding a jsonwebtoken dependency.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

@stylessh is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@cursor
Copy link
Copy Markdown

cursor Bot commented May 8, 2026

PR Summary

Medium Risk
Adds a new external integration with authenticated API calls and a new webhook signature-verification path; incorrect parameter mapping or signature handling could break workflows or reject/accept deliveries unexpectedly.

Overview
Adds a new Netlify integration across Sim and Docs, including a NetlifyBlock with 9 tool operations for listing sites/deploys, triggering/canceling deploys, and CRUD for environment variables (PAT via Authorization: Bearer).

Introduces 6 deploy-related webhook triggers and a new netlify webhook provider that validates X-Webhook-Signature as an HS256 JWT (checks signature, iss, and sha256(rawBody)), adds idempotency extraction/normalized payload formatting, and registers the provider/trigger/tool entries plus the new NetlifyIcon and generated docs/integration metadata.

Reviewed by Cursor Bugbot for commit a5a8100. Bugbot is set up for automated code reviews on this repo. Configure here.

Removes auto-registration of Netlify outgoing webhooks because the data.signature_secret field doesn't reliably round-trip through the hooks API. The user now configures the webhook in Netlify's dashboard and pastes the JWS secret token into the trigger; Sim only verifies inbound signatures with that secret. Drops apiKey and siteId from the trigger config (still required on the block for tool operations).
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 8, 2026

Greptile Summary

This PR adds a Netlify integration with nine tool operations (sites, deploys, env vars) and six deploy webhook triggers (deploy_created, deploy_building, deploy_succeeded, deploy_failed, deploy_locked, deploy_unlocked). Webhook signatures are verified as HS256 JWTs using the standard crypto library, and Netlify hooks are auto-registered/deregistered during workflow deploy/undeploy.

  • Nine Netlify tools covering site listing, deploy lifecycle (list/get/create/cancel), and full env var CRUD; all follow existing tool patterns with visibility: 'user-only' on the API key.
  • Six webhook triggers, each creating a dedicated Netlify hook (one event per hook, matching Netlify's model); the signing secret is generated per-hook and stored as webhookSecret.
  • JWT verification covers HMAC-SHA256 signature, iss: netlify claim, and body SHA-256 binding, but does not validate the exp claim — Netlify JWTs are short-lived and include exp, so skipping this check allows indefinitely-valid replays of captured tokens."

Confidence Score: 4/5

Safe to merge with one recommended follow-up: add JWT exp claim validation before this goes to production webhook traffic.

The integration is well-structured and follows existing patterns throughout. The webhook JWT verification correctly handles HMAC-SHA256, issuer, and body-hash checks, but omits the exp claim — Netlify JWTs are short-lived and include exp, so a captured token remains indefinitely replayable without that guard. The idempotency check on deploy ID reduces the practical impact, but it is not a complete substitute. Everything else — tool params, env var CRUD, hook registration/deletion, event routing — looks correct.

apps/sim/lib/webhooks/providers/netlify.ts — the JWT verification function needs an exp claim check before reaching production webhook traffic.

Security Review

  • Missing JWT exp validation (apps/sim/lib/webhooks/providers/netlify.ts): verifyNetlifyJwt checks the HMAC signature and body hash but never reads the exp claim. Netlify webhook JWTs are short-lived; without an expiry check, a previously-captured valid JWT can be replayed indefinitely to trigger workflows (mitigated in practice by the extractIdempotencyId idempotency guard, but not a complete substitute for proper JWT expiry enforcement).

Important Files Changed

Filename Overview
apps/sim/lib/webhooks/providers/netlify.ts Core webhook provider: JWT verification is solid (HMAC-SHA256, iss check, body-hash) but missing exp claim validation; subscription/deletion logic is well-structured.
apps/sim/triggers/netlify/utils.ts Event matching, setup instructions, and output schema helpers; state-based filter for deploy_created/locked/unlocked falls to a permissive default that is safe in practice but lacks explicit guards.
apps/sim/blocks/blocks/netlify.ts Block definition with nine operations, trigger integration, and conditional field display; correctly structured following existing block patterns.
apps/sim/tools/netlify/get_deploy.ts Get deploy tool; NetlifyApiDeploy interface is duplicated verbatim in cancel_deploy.ts and list_deploys.ts — should be extracted to a shared location.
apps/sim/tools/netlify/create_deploy.ts Triggers a build via POST /sites/{id}/builds with branch/title/clear_cache as query params; correct Netlify API usage.
apps/sim/tools/netlify/create_env_var.ts Correctly wraps the env var body in an array as required by Netlify's POST /accounts/{id}/env endpoint.
apps/sim/tools/netlify/delete_env_var.ts Delete env var tool; transformResponse unconditionally returns deleted:true, which is correct since the framework handles non-2xx errors upstream and DELETE 204 carries no body.
apps/sim/tools/netlify/utils.ts Shared normalizeEnvVar and buildEnvBody helpers used by create/update env var tools; clean and correct.

Sequence Diagram

sequenceDiagram
    participant User
    participant SimBlock as Sim Netlify Block
    participant NetlifyAPI as Netlify API
    participant SimWebhook as Sim Webhook Endpoint
    participant Workflow as Sim Workflow

    Note over User,Workflow: Tool Operations (e.g. Create Deploy)
    User->>SimBlock: Configure operation + PAT
    SimBlock->>NetlifyAPI: "POST /sites/{id}/builds?branch=..."
    NetlifyAPI-->>SimBlock: Build object (id, deploy_id, ...)
    SimBlock-->>User: Normalised output

    Note over User,Workflow: Trigger Registration (Deploy)
    User->>SimBlock: Deploy workflow with Netlify trigger
    SimBlock->>NetlifyAPI: "POST /hooks?site_id=... with event + signature_secret"
    NetlifyAPI-->>SimBlock: Hook ID
    SimBlock->>SimBlock: Store externalId + webhookSecret

    Note over User,Workflow: Inbound Webhook
    NetlifyAPI->>SimWebhook: POST deploy payload + X-Webhook-Signature JWT
    SimWebhook->>SimWebhook: verifyNetlifyJwt (sig + body-hash, no exp check)
    SimWebhook->>SimWebhook: matchEvent — triggerId + state
    SimWebhook->>Workflow: formatInput and run workflow
    Workflow-->>SimWebhook: 200 OK

    Note over User,Workflow: Trigger Deletion (Undeploy)
    User->>SimBlock: Undeploy workflow
    SimBlock->>NetlifyAPI: "DELETE /hooks/{externalId}"
    NetlifyAPI-->>SimBlock: 204 No Content
Loading

Reviews (1): Last reviewed commit: "refactor(netlify): switch trigger to man..." | Re-trigger Greptile

Comment on lines +23 to +52

const expectedSignature = crypto
.createHmac('sha256', secret)
.update(`${headerB64}.${payloadB64}`)
.digest('base64url')

if (!safeCompare(expectedSignature, signatureB64)) {
return false
}

let payload: Record<string, unknown>
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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security Missing JWT exp claim validation

verifyNetlifyJwt validates the HMAC signature and the sha256 body-hash but never checks the exp claim. Netlify JWTs are short-lived (typically valid for ~30 seconds after delivery). Without an expiry check, a previously-captured valid token can be replayed indefinitely — the signature and body-hash will still pass because neither changes between replays. The extractIdempotencyId guard limits duplicate workflow executions for the same deploy ID, but a replay with the same JWT + body would bypass signature freshness entirely.

Comment on lines +4 to +21
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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicated NetlifyApiDeploy interface

The same NetlifyApiDeploy interface is defined independently in get_deploy.ts, cancel_deploy.ts, and list_deploys.ts. Moving this to types.ts (or utils.ts) would eliminate the duplication and make future field additions consistent across all three files.

Suggested change
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
}
// Consider extracting this interface to @/tools/netlify/types.ts or @/tools/netlify/utils.ts
// to avoid the identical definition in get_deploy.ts, cancel_deploy.ts, and list_deploys.ts.
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
}

Comment on lines +35 to +52
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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 isNetlifyEventMatch returns true for all states on deploy_created, deploy_locked, and deploy_unlocked

The default: return true branch allows any state value to pass for the three unlisted event types. In practice this is safe because Netlify fires each event-specific hook in isolation. However, if two triggers for the same site share a notification endpoint (or if a future refactor coalesces URLs), a payload with an unexpected state would still match. Adding explicit state guards for deploy_created (enqueued | new), deploy_locked, and deploy_unlocked would make the check fully authoritative.

'Content-Type': 'application/json',
}),
body: () => ({}),
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create deploy sends branch in query params instead of body

High Severity

The create_deploy tool passes branch, title, and clear_cache as URL query parameters while sending an empty body (body: () => ({})). The Netlify POST /api/v1/sites/{site_id}/builds endpoint expects these parameters in the JSON request body (e.g. {"branch": "main"}). This means the branch selection, title, and cache-clearing options are silently ignored, and every build triggers on the default production branch regardless of user input.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 36c8db9. Configure here.

created_at?: string
updated_at?: string
published_at?: string
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated NetlifyApiDeploy interface across three tool files

Low Severity

The identical NetlifyApiDeploy interface (18 lines) is copy-pasted across cancel_deploy.ts, get_deploy.ts, and list_deploys.ts. This triples the maintenance surface — a field change or addition needs updating in all three files. It naturally belongs in utils.ts alongside the already-shared NetlifyApiEnvVar interface.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 36c8db9. Configure here.

stylessh added 2 commits May 8, 2026 15:31
Multiple Netlify deploy events (created, building, succeeded) share the same deploy id, so keying dedup only on id collapsed distinct events into one. Including state plus the locked flag differentiates each event for the same deploy.
Runs generate-docs.ts to produce the tool and trigger MDX pages, the integrations.json entry, and icon mappings for the docs app. Adds Netlify to the trigger provider display-name map so the docs render the proper label. Reorders the webhook provider registry so netlify sits alphabetically between monday and notion instead of being parked next to vercel.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 287fbb1. Configure here.

default:
return base
}
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy siteId leaks into env var operations via ...rest

Medium Severity

The siteId sub-block (used for deploy operations) is not destructured from params, so it flows into ...rest and then into base. For env var operations like list_env_vars, create_env_var, update_env_var, and delete_env_var, envSiteId is conditionally spread as siteId only when truthy. When envSiteId is empty (it's optional), the deploy-scoped siteId already present in base leaks through and is sent to the env var tool, which uses it to scope the query to that site — silently filtering results to an unintended site.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 287fbb1. Configure here.

The list_sites and list_deploys tools support page and per_page parameters but the block didn't surface them, so users couldn't paginate from the UI. Adds Limit and Page advanced fields with numeric coercion in tools.config.params.

Expands the block outputs for get_deploy, cancel_deploy, and create_deploy so the tag dropdown surfaces siteId, branch, commitRef, errorMessage, sha, and done in addition to id and state. The tools already returned these, but only a subset was discoverable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant