Skip to content

Commit 9517153

Browse files
authored
Merge pull request #2482 from trycompai/main
[comp] Production Deploy
2 parents a5b93ce + fe3c497 commit 9517153

3 files changed

Lines changed: 161 additions & 2 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { TASK_TEMPLATES } from '../../../task-mappings';
2+
import type { CheckContext, IntegrationCheck } from '../../../types';
3+
import type {
4+
VercelDeploymentsResponse,
5+
VercelProject,
6+
VercelProjectsResponse,
7+
} from '../types';
8+
9+
/**
10+
* Vercel App Availability Check
11+
*
12+
* Verifies that Vercel projects have active, healthy deployments
13+
* indicating the applications are available and running.
14+
* Maps to: App Availability task
15+
*/
16+
export const appAvailabilityCheck: IntegrationCheck = {
17+
id: 'app-availability',
18+
name: 'App Availability',
19+
description: 'Verify Vercel projects have active, healthy deployments',
20+
taskMapping: TASK_TEMPLATES.appAvailability,
21+
variables: [],
22+
23+
run: async (ctx: CheckContext) => {
24+
ctx.log('Starting Vercel App Availability check');
25+
26+
const oauthMeta = (ctx.metadata?.oauth || {}) as {
27+
team?: { id?: string; name?: string };
28+
user?: { id?: string; username?: string };
29+
};
30+
const teamId = oauthMeta.team?.id;
31+
32+
if (teamId) {
33+
ctx.log(`Operating in team context: ${teamId}`);
34+
}
35+
36+
// Fetch projects
37+
let projects: VercelProject[] = [];
38+
try {
39+
const response = await ctx.fetch<VercelProjectsResponse>(
40+
teamId ? `/v9/projects?teamId=${teamId}` : '/v9/projects',
41+
);
42+
projects = response.projects || [];
43+
ctx.log(`Found ${projects.length} projects`);
44+
} catch (error) {
45+
const msg = error instanceof Error ? error.message : String(error);
46+
ctx.fail({
47+
title: 'Failed to fetch Vercel projects',
48+
resourceType: 'vercel',
49+
resourceId: 'projects',
50+
severity: 'high',
51+
description: `Could not fetch projects: ${msg}`,
52+
remediation: 'Ensure the OAuth connection has access to your projects.',
53+
});
54+
return;
55+
}
56+
57+
if (projects.length === 0) {
58+
ctx.fail({
59+
title: 'No Vercel projects found',
60+
resourceType: 'vercel',
61+
resourceId: 'projects',
62+
severity: 'medium',
63+
description: 'No projects found in this account.',
64+
remediation: 'Verify the connection has access to your Vercel projects.',
65+
});
66+
return;
67+
}
68+
69+
// Transient states where Vercel keeps the previous READY deployment serving traffic
70+
const transitionalStates = new Set(['BUILDING', 'QUEUED', 'INITIALIZING']);
71+
72+
for (const project of projects.slice(0, 10)) {
73+
try {
74+
const params = new URLSearchParams({ projectId: project.id, limit: '1', target: 'production' });
75+
if (teamId) params.set('teamId', teamId);
76+
77+
const response = await ctx.fetch<VercelDeploymentsResponse>(
78+
`/v6/deployments?${params.toString()}`,
79+
);
80+
const deployments = response.deployments || [];
81+
const latestDeploy = deployments[0];
82+
83+
if (latestDeploy && latestDeploy.state === 'READY') {
84+
85+
ctx.pass({
86+
title: `Available: ${project.name}`,
87+
resourceType: 'project',
88+
resourceId: project.id,
89+
description: `Latest production deployment is READY.`,
90+
evidence: {
91+
project: project.name,
92+
deploymentState: latestDeploy.state,
93+
deploymentUrl: latestDeploy.url,
94+
deployedAt: new Date(latestDeploy.created).toISOString(),
95+
},
96+
});
97+
} else if (latestDeploy && transitionalStates.has(latestDeploy.state)) {
98+
99+
ctx.pass({
100+
title: `Deploying: ${project.name}`,
101+
resourceType: 'project',
102+
resourceId: project.id,
103+
description: `Deployment in progress (${latestDeploy.state}). Previous deployment continues serving traffic.`,
104+
evidence: {
105+
project: project.name,
106+
deploymentState: latestDeploy.state,
107+
deploymentUrl: latestDeploy.url,
108+
},
109+
});
110+
} else if (latestDeploy && latestDeploy.state === 'CANCELED') {
111+
ctx.fail({
112+
title: `Canceled deployment: ${project.name}`,
113+
resourceType: 'project',
114+
resourceId: project.id,
115+
severity: 'medium',
116+
description: `Latest production deployment was canceled. Previous deployment may still be serving traffic.`,
117+
remediation: `Review canceled deployment and redeploy via Vercel Dashboard > ${project.name} > Deployments.`,
118+
});
119+
} else if (latestDeploy) {
120+
ctx.fail({
121+
title: `Unhealthy: ${project.name}`,
122+
resourceType: 'project',
123+
resourceId: project.id,
124+
severity: 'high',
125+
description: `Latest production deployment state: ${latestDeploy.state}.`,
126+
remediation: `Check deployment status in Vercel Dashboard > ${project.name} > Deployments.`,
127+
evidence: {
128+
project: project.name,
129+
deploymentState: latestDeploy.state,
130+
deploymentUrl: latestDeploy.url,
131+
},
132+
});
133+
} else {
134+
135+
ctx.fail({
136+
title: `No production deployment: ${project.name}`,
137+
resourceType: 'project',
138+
resourceId: project.id,
139+
severity: 'medium',
140+
description: 'No production deployments found for this project.',
141+
remediation: `Deploy to production via Vercel Dashboard or CLI.`,
142+
});
143+
}
144+
} catch (error) {
145+
ctx.fail({
146+
title: `Failed to check: ${project.name}`,
147+
resourceType: 'project',
148+
resourceId: project.id,
149+
severity: 'medium',
150+
description: `Could not fetch deployments: ${error instanceof Error ? error.message : String(error)}`,
151+
remediation: 'Verify the OAuth connection has access to this project.',
152+
});
153+
}
154+
}
155+
156+
ctx.log('Vercel App Availability check complete');
157+
},
158+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { monitoringAlertingCheck } from './monitoring-alerting';
2+
export { appAvailabilityCheck } from './app-availability';

packages/integration-platform/src/manifests/vercel/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { IntegrationManifest } from '../../types';
2-
import { monitoringAlertingCheck } from './checks';
2+
import { appAvailabilityCheck, monitoringAlertingCheck } from './checks';
33

44
export const vercelManifest: IntegrationManifest = {
55
id: 'vercel',
@@ -54,5 +54,5 @@ Enter the Client ID, Secret, and the integration slug (from \`vercel.com/integra
5454

5555
capabilities: ['checks'],
5656

57-
checks: [monitoringAlertingCheck],
57+
checks: [monitoringAlertingCheck, appAvailabilityCheck],
5858
};

0 commit comments

Comments
 (0)