Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: vercel webhooks integration #706

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/gold-cooks-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/vercel": patch
---

add Vercel integration
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { slack } from "./integrations/slack";
import { stripe } from "./integrations/stripe";
import { supabase, supabaseManagement } from "./integrations/supabase";
import { typeform } from "./integrations/typeform";
import { vercel } from "./integrations/vercel";
import type { Integration } from "./types";

export class IntegrationCatalog {
Expand Down Expand Up @@ -46,4 +47,5 @@ export const integrationCatalog = new IntegrationCatalog({
supabase,
sendgrid,
typeform,
vercel,
});
52 changes: 52 additions & 0 deletions apps/webapp/app/services/externalApis/integrations/vercel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Integration } from "../types";

export const vercel: Integration = {
identifier: "vercel",
name: "Vercel",
packageName: "@trigger.dev/vercel@latest",
authenticationMethods: {
apiKey: {
type: "apikey",
help: {
samples: [
{
title: "Creating the client",
code: `
import { Vercel } from "@trigger.dev/vercel";

const vercel = new Vercel({
id: "__SLUG__",
token: process.env.VERCEL_API_TOKEN!
});
`,
},
{
title: "Using the client",
code: `
import { Vercel } from "@trigger.dev/vercel";

const vercel = new Vercel({
id: "__SLUG__",
token: process.env.VERCEL_API_TOKEN!
});

client.defineJob({
id: "vercel-deployment-created-proj",
name: "Vercel Deployment Created (Project)",
version: "0.1.0",
trigger: vercel.onDeploymentCreated({
teamId: "team_kTDbLdHFZ0x7HU66LRgZCfqh",
projectIds: ["prj_vt57HZEY7iilPaJv71LcOVLcEiPs"],
}),
run: async (payload, io, ctx) => {
io.logger.info("deployment created event received");
},
});
`,
highlight: [[12, 15]],
},
],
},
},
},
};
1 change: 1 addition & 0 deletions integrations/vercel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @trigger.dev/vercel
37 changes: 37 additions & 0 deletions integrations/vercel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@trigger.dev/vercel",
"version": "2.2.0",
"description": "Trigger.dev integration for vercel",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"dist/index.js",
"dist/index.d.ts",
"dist/index.js.map"
],
"devDependencies": {
"@trigger.dev/tsconfig": "workspace:*",
"@trigger.dev/tsup": "workspace:*",
"@types/node": "16.x",
"rimraf": "^3.0.2",
"tsup": "7.1.x",
"typescript": "4.9.4"
},
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && npm run build:tsup",
"build:tsup": "tsup",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/sdk": "workspace:^2.2.0",
"@trigger.dev/integration-kit": "workspace:^2.2.0",
"zod": "3.21.4"
},
"engines": {
"node": ">=18"
}
}
104 changes: 104 additions & 0 deletions integrations/vercel/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { z } from "zod";

const WebhookRegistrationDataSchema = z.object({
id: z.string(),
secret: z.string(),
events: z.array(z.string()),
url: z.string(),
ownerId: z.string(),
projectIds: z.array(z.string()).optional(),
createdAt: z.number(),
updatedAt: z.number(),
});

export type WebhookRegistrationData = z.infer<typeof WebhookRegistrationDataSchema>;

const WebhookListDataSchema = z.array(WebhookRegistrationDataSchema.omit({ secret: true }));

export type WebhookListData = z.infer<typeof WebhookListDataSchema>;

export class VercelClient {
constructor(private apiKey: string) {}

async listWebhooks({ teamId }: { teamId: string }): Promise<WebhookListData> {
const res = await fetch(`https://api.vercel.com/v1/webhooks?teamId=${teamId}`, {
method: "get",
headers: {
Authorization: `Bearer ${this.apiKey}`,
},
});

if (!res.ok) {
throw new Error(`failed to list webhooks: ${res.statusText}`);
}

const webhooks = await res.json();

return WebhookListDataSchema.parse(webhooks);
}

async createWebhook({
teamId,
events,
url,
projectIds,
}: {
teamId: string;
events: string[];
url: string;
projectIds?: string[];
}): Promise<WebhookRegistrationData> {
const body = {
events,
url,
projectIds,
};

const res = await fetch(`https://api.vercel.com/v1/webhooks?teamId=${teamId}`, {
method: "post",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});

if (!res.ok) {
throw new Error(`failed to create webhook: ${res.statusText}`);
}

const webhook = await res.json();

return WebhookRegistrationDataSchema.parse(webhook);
}

async deleteWebhook({ webhookId }: { webhookId: string }): Promise<void> {
const res = await fetch(`https://api.vercel.com/v1/webhooks/${webhookId}`, {
method: "delete",
headers: {
Authorization: `Bearer ${this.apiKey}`,
},
});

if (!res.ok) {
throw new Error(`failed to delete webhook: ${res.statusText}`);
}
}

async updateWebhook({
webhookId,
teamId,
events,
url,
projectIds,
}: {
webhookId: string;
teamId: string;
events: string[];
url: string;
projectIds?: string[];
}): Promise<WebhookRegistrationData> {
await this.deleteWebhook({ webhookId });
return await this.createWebhook({ teamId, events, url, projectIds });
}
}
79 changes: 79 additions & 0 deletions integrations/vercel/src/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { EventSpecification } from "@trigger.dev/sdk";
import {
DeploymentCanceledEventPayload,
DeploymentCreatedEventPayload,
DeploymentErrorEventPayload,
DeploymentSucceededEventPayload,
ProjectCreatedEventPayload,
ProjectRemovedEventPayload,
} from "./schemas";
import {
deploymentCanceled,
deploymentCreated,
deploymentError,
deploymentSucceeded,
projectCreated,
projectRemoved,
} from "./payload-examples";
import { deploymentProperties, projectProperties } from "./utils";
import { WebhookEventTypeSchema } from "./types";

export const onDeploymentCreated: EventSpecification<DeploymentCreatedEventPayload> = {
name: WebhookEventTypeSchema.enum["deployment.created"],
title: "On Deployment Created",
source: "vercel.app",
icon: "vercel",
examples: [deploymentCreated],
parsePayload: (payload) => payload as DeploymentCreatedEventPayload,
runProperties: (event) => deploymentProperties(event),
};

export const onDeploymentSucceeded: EventSpecification<DeploymentSucceededEventPayload> = {
name: WebhookEventTypeSchema.enum["deployment.succeeded"],
title: "On Deployment Succeeded",
source: "vercel.app",
icon: "vercel",
examples: [deploymentSucceeded],
parsePayload: (payload) => payload as DeploymentSucceededEventPayload,
runProperties: (event) => deploymentProperties(event),
};

export const onDeploymentCanceled: EventSpecification<DeploymentCanceledEventPayload> = {
name: WebhookEventTypeSchema.enum["deployment.canceled"],
title: "On Deployment Canceled",
source: "vercel.app",
icon: "vercel",
examples: [deploymentCanceled],
parsePayload: (payload) => payload as DeploymentCanceledEventPayload,
runProperties: (event) => deploymentProperties(event),
};

export const onDeploymentError: EventSpecification<DeploymentErrorEventPayload> = {
name: WebhookEventTypeSchema.enum["deployment.error"],
title: "On Deployment Error",
source: "vercel.app",
icon: "vercel",
examples: [deploymentError],
parsePayload: (payload) => payload as DeploymentErrorEventPayload,
runProperties: (event) => deploymentProperties(event),
};

export const onProjectCreated: EventSpecification<ProjectCreatedEventPayload> = {
name: WebhookEventTypeSchema.enum["project.created"],
title: "On Project Created",
source: "vercel.app",
icon: "vercel",
examples: [projectCreated],
parsePayload: (payload) => payload as ProjectCreatedEventPayload,
runProperties: (event) => projectProperties(event),
};

export const onProjectRemoved: EventSpecification<ProjectRemovedEventPayload> = {
name: WebhookEventTypeSchema.enum["project.removed"],
title: "On Project Removed",
source: "vercel.app",
icon: "vercel",
examples: [projectRemoved],
parsePayload: (payload) => payload as ProjectRemovedEventPayload,
runProperties: (event) => projectProperties(event),
};