From 6ccef97cd18aa70fc5479da18d3094f509c56d9f Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 7 Apr 2026 15:09:49 -0400 Subject: [PATCH 1/5] feat(vercel): add App Availability check for deployment health monitoring --- .../vercel/checks/app-availability.ts | 125 ++++++++++++++++++ .../src/manifests/vercel/checks/index.ts | 1 + .../src/manifests/vercel/index.ts | 4 +- 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 packages/integration-platform/src/manifests/vercel/checks/app-availability.ts diff --git a/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts b/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts new file mode 100644 index 0000000000..d111af6605 --- /dev/null +++ b/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts @@ -0,0 +1,125 @@ +import { TASK_TEMPLATES } from '../../../task-mappings'; +import type { CheckContext, IntegrationCheck } from '../../../types'; +import type { + VercelDeployment, + VercelDeploymentsResponse, + VercelProject, + VercelProjectsResponse, +} from '../types'; + +/** + * Vercel App Availability Check + * + * Verifies that Vercel projects have active, healthy deployments + * indicating the applications are available and running. + * Maps to: App Availability task + */ +export const appAvailabilityCheck: IntegrationCheck = { + id: 'app-availability', + name: 'App Availability', + description: 'Verify Vercel projects have active, healthy deployments', + taskMapping: TASK_TEMPLATES.appAvailability, + variables: [], + + run: async (ctx: CheckContext) => { + ctx.log('Starting Vercel App Availability check'); + + const oauthMeta = (ctx.metadata?.oauth || {}) as { + team?: { id?: string; name?: string }; + user?: { id?: string; username?: string }; + }; + const teamId = oauthMeta.team?.id; + + if (teamId) { + ctx.log(`Operating in team context: ${teamId}`); + } + + // Fetch projects + let projects: VercelProject[] = []; + try { + const response = await ctx.fetch( + teamId ? `/v9/projects?teamId=${teamId}` : '/v9/projects', + ); + projects = response.projects || []; + ctx.log(`Found ${projects.length} projects`); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + ctx.fail({ + title: 'Failed to fetch Vercel projects', + resourceType: 'vercel', + resourceId: 'projects', + severity: 'high', + description: `Could not fetch projects: ${msg}`, + remediation: 'Ensure the OAuth connection has access to your projects.', + }); + return; + } + + if (projects.length === 0) { + ctx.fail({ + title: 'No Vercel projects found', + resourceType: 'vercel', + resourceId: 'projects', + severity: 'medium', + description: 'No projects found in this account.', + remediation: 'Verify the connection has access to your Vercel projects.', + }); + return; + } + + for (const project of projects) { + try { + const params = new URLSearchParams({ projectId: project.id, limit: '1', target: 'production' }); + if (teamId) params.set('teamId', teamId); + + const response = await ctx.fetch( + `/v6/deployments?${params.toString()}`, + ); + const deployments = response.deployments || []; + const latestDeploy = deployments[0]; + + if (latestDeploy && latestDeploy.state === 'READY') { + ctx.pass({ + title: `Available: ${project.name}`, + resourceType: 'project', + resourceId: project.id, + description: `Latest production deployment is READY.`, + evidence: { + project: project.name, + deploymentState: latestDeploy.state, + deploymentUrl: latestDeploy.url, + deployedAt: new Date(latestDeploy.created).toISOString(), + }, + }); + } else if (latestDeploy) { + ctx.fail({ + title: `Unhealthy: ${project.name}`, + resourceType: 'project', + resourceId: project.id, + severity: 'high', + description: `Latest production deployment state: ${latestDeploy.state}.`, + remediation: `Check deployment status in Vercel Dashboard > ${project.name} > Deployments.`, + evidence: { + project: project.name, + deploymentState: latestDeploy.state, + deploymentUrl: latestDeploy.url, + }, + }); + } else { + ctx.fail({ + title: `No production deployment: ${project.name}`, + resourceType: 'project', + resourceId: project.id, + severity: 'medium', + description: 'No production deployments found for this project.', + remediation: `Deploy to production via Vercel Dashboard or CLI.`, + }); + } + } catch (error) { + ctx.log(`Could not check deployments for ${project.name}: ${error}`); + } + } + + ctx.log('Vercel App Availability check complete'); + }, +}; diff --git a/packages/integration-platform/src/manifests/vercel/checks/index.ts b/packages/integration-platform/src/manifests/vercel/checks/index.ts index 9fb6ed1eba..4adb6362d3 100644 --- a/packages/integration-platform/src/manifests/vercel/checks/index.ts +++ b/packages/integration-platform/src/manifests/vercel/checks/index.ts @@ -1 +1,2 @@ export { monitoringAlertingCheck } from './monitoring-alerting'; +export { appAvailabilityCheck } from './app-availability'; diff --git a/packages/integration-platform/src/manifests/vercel/index.ts b/packages/integration-platform/src/manifests/vercel/index.ts index bbad6f040a..b3fec1ff2a 100644 --- a/packages/integration-platform/src/manifests/vercel/index.ts +++ b/packages/integration-platform/src/manifests/vercel/index.ts @@ -1,5 +1,5 @@ import type { IntegrationManifest } from '../../types'; -import { monitoringAlertingCheck } from './checks'; +import { appAvailabilityCheck, monitoringAlertingCheck } from './checks'; export const vercelManifest: IntegrationManifest = { id: 'vercel', @@ -54,5 +54,5 @@ Enter the Client ID, Secret, and the integration slug (from \`vercel.com/integra capabilities: ['checks'], - checks: [monitoringAlertingCheck], + checks: [monitoringAlertingCheck, appAvailabilityCheck], }; From 4ed79d0168d1d701cc07189cf9b7e6a5e63ecb5d Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 7 Apr 2026 15:28:51 -0400 Subject: [PATCH 2/5] fix(vercel): fix false positives, silent errors, and missing project cap - Treat BUILDING/QUEUED/INITIALIZING as transitional (pass, not fail) since Vercel keeps the previous READY deployment serving traffic - Emit ctx.fail() in per-project catch blocks instead of just logging, so failures are never silently swallowed as success - Add post-loop fallback for zero checked projects - Cap iteration to first 10 projects to match monitoring-alerting check and avoid Vercel rate limits - Remove unused VercelDeployment import Co-Authored-By: Claude Opus 4.6 (1M context) --- .../vercel/checks/app-availability.ts | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts b/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts index d111af6605..f5327c3e73 100644 --- a/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts +++ b/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts @@ -1,7 +1,6 @@ import { TASK_TEMPLATES } from '../../../task-mappings'; import type { CheckContext, IntegrationCheck } from '../../../types'; import type { - VercelDeployment, VercelDeploymentsResponse, VercelProject, VercelProjectsResponse, @@ -67,7 +66,11 @@ export const appAvailabilityCheck: IntegrationCheck = { return; } - for (const project of projects) { + // Transient states where Vercel keeps the previous READY deployment serving traffic + const transitionalStates = new Set(['BUILDING', 'QUEUED', 'INITIALIZING']); + let checkedCount = 0; + + for (const project of projects.slice(0, 10)) { try { const params = new URLSearchParams({ projectId: project.id, limit: '1', target: 'production' }); if (teamId) params.set('teamId', teamId); @@ -79,6 +82,7 @@ export const appAvailabilityCheck: IntegrationCheck = { const latestDeploy = deployments[0]; if (latestDeploy && latestDeploy.state === 'READY') { + checkedCount++; ctx.pass({ title: `Available: ${project.name}`, resourceType: 'project', @@ -91,7 +95,21 @@ export const appAvailabilityCheck: IntegrationCheck = { deployedAt: new Date(latestDeploy.created).toISOString(), }, }); + } else if (latestDeploy && transitionalStates.has(latestDeploy.state)) { + checkedCount++; + ctx.pass({ + title: `Deploying: ${project.name}`, + resourceType: 'project', + resourceId: project.id, + description: `Deployment in progress (${latestDeploy.state}). Previous deployment continues serving traffic.`, + evidence: { + project: project.name, + deploymentState: latestDeploy.state, + deploymentUrl: latestDeploy.url, + }, + }); } else if (latestDeploy) { + checkedCount++; ctx.fail({ title: `Unhealthy: ${project.name}`, resourceType: 'project', @@ -106,6 +124,7 @@ export const appAvailabilityCheck: IntegrationCheck = { }, }); } else { + checkedCount++; ctx.fail({ title: `No production deployment: ${project.name}`, resourceType: 'project', @@ -116,10 +135,29 @@ export const appAvailabilityCheck: IntegrationCheck = { }); } } catch (error) { - ctx.log(`Could not check deployments for ${project.name}: ${error}`); + checkedCount++; + ctx.fail({ + title: `Failed to check: ${project.name}`, + resourceType: 'project', + resourceId: project.id, + severity: 'medium', + description: `Could not fetch deployments: ${error instanceof Error ? error.message : String(error)}`, + remediation: 'Verify the OAuth connection has access to this project.', + }); } } + if (checkedCount === 0) { + ctx.fail({ + title: 'No projects could be checked', + resourceType: 'vercel', + resourceId: 'projects', + severity: 'high', + description: 'All project deployment checks failed.', + remediation: 'Check Vercel API access and try again.', + }); + } + ctx.log('Vercel App Availability check complete'); }, }; From 2a8ac25f3e6a72395e9f1b70cdb123a75bd5bbbb Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 7 Apr 2026 15:36:20 -0400 Subject: [PATCH 3/5] fix(vercel): treat CANCELED as transitional, remove dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CANCELED deployments don't take down the app — Vercel keeps serving the previous READY deployment. Also removed unreachable checkedCount guard since the loop always executes at least once (projects is guaranteed non-empty at that point). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../vercel/checks/app-availability.ts | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts b/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts index f5327c3e73..1be96fad56 100644 --- a/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts +++ b/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts @@ -67,8 +67,7 @@ export const appAvailabilityCheck: IntegrationCheck = { } // Transient states where Vercel keeps the previous READY deployment serving traffic - const transitionalStates = new Set(['BUILDING', 'QUEUED', 'INITIALIZING']); - let checkedCount = 0; + const transitionalStates = new Set(['BUILDING', 'QUEUED', 'INITIALIZING', 'CANCELED']); for (const project of projects.slice(0, 10)) { try { @@ -82,7 +81,7 @@ export const appAvailabilityCheck: IntegrationCheck = { const latestDeploy = deployments[0]; if (latestDeploy && latestDeploy.state === 'READY') { - checkedCount++; + ctx.pass({ title: `Available: ${project.name}`, resourceType: 'project', @@ -96,7 +95,7 @@ export const appAvailabilityCheck: IntegrationCheck = { }, }); } else if (latestDeploy && transitionalStates.has(latestDeploy.state)) { - checkedCount++; + ctx.pass({ title: `Deploying: ${project.name}`, resourceType: 'project', @@ -109,7 +108,7 @@ export const appAvailabilityCheck: IntegrationCheck = { }, }); } else if (latestDeploy) { - checkedCount++; + ctx.fail({ title: `Unhealthy: ${project.name}`, resourceType: 'project', @@ -124,7 +123,7 @@ export const appAvailabilityCheck: IntegrationCheck = { }, }); } else { - checkedCount++; + ctx.fail({ title: `No production deployment: ${project.name}`, resourceType: 'project', @@ -147,17 +146,6 @@ export const appAvailabilityCheck: IntegrationCheck = { } } - if (checkedCount === 0) { - ctx.fail({ - title: 'No projects could be checked', - resourceType: 'vercel', - resourceId: 'projects', - severity: 'high', - description: 'All project deployment checks failed.', - remediation: 'Check Vercel API access and try again.', - }); - } - ctx.log('Vercel App Availability check complete'); }, }; From 265a5e42b9c5177567000545f5c1743369366407 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 7 Apr 2026 15:38:20 -0400 Subject: [PATCH 4/5] fix(vercel): remove remaining checkedCount reference in catch block Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/manifests/vercel/checks/app-availability.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts b/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts index 1be96fad56..944b6ea4f7 100644 --- a/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts +++ b/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts @@ -134,7 +134,6 @@ export const appAvailabilityCheck: IntegrationCheck = { }); } } catch (error) { - checkedCount++; ctx.fail({ title: `Failed to check: ${project.name}`, resourceType: 'project', From 662ad720b642f44b38c907e81b9b30114a760c5f Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 7 Apr 2026 15:46:27 -0400 Subject: [PATCH 5/5] fix(vercel): handle CANCELED as medium-severity failure, not transitional CANCELED is a terminal state, not in-progress. Remove from transitionalStates and handle as its own branch with medium severity, consistent with monitoring-alerting.ts treating it as a failure. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/manifests/vercel/checks/app-availability.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts b/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts index 944b6ea4f7..ad26fd1935 100644 --- a/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts +++ b/packages/integration-platform/src/manifests/vercel/checks/app-availability.ts @@ -67,7 +67,7 @@ export const appAvailabilityCheck: IntegrationCheck = { } // Transient states where Vercel keeps the previous READY deployment serving traffic - const transitionalStates = new Set(['BUILDING', 'QUEUED', 'INITIALIZING', 'CANCELED']); + const transitionalStates = new Set(['BUILDING', 'QUEUED', 'INITIALIZING']); for (const project of projects.slice(0, 10)) { try { @@ -107,8 +107,16 @@ export const appAvailabilityCheck: IntegrationCheck = { deploymentUrl: latestDeploy.url, }, }); + } else if (latestDeploy && latestDeploy.state === 'CANCELED') { + ctx.fail({ + title: `Canceled deployment: ${project.name}`, + resourceType: 'project', + resourceId: project.id, + severity: 'medium', + description: `Latest production deployment was canceled. Previous deployment may still be serving traffic.`, + remediation: `Review canceled deployment and redeploy via Vercel Dashboard > ${project.name} > Deployments.`, + }); } else if (latestDeploy) { - ctx.fail({ title: `Unhealthy: ${project.name}`, resourceType: 'project',