From c175519d7064645c52f4cc24861c52e11822bc92 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 8 Apr 2023 16:01:58 +0300 Subject: [PATCH] Add support for syncing to Vercel preview branches --- .../v1/integrationAuthController.ts | 65 ++++++++++--- backend/src/integrations/apps.ts | 1 + backend/src/integrations/sync.ts | 71 ++++++-------- backend/src/models/integration.ts | 1 + backend/src/routes/v1/integrationAuth.ts | 15 +++ .../src/hooks/api/integrationAuth/index.tsx | 4 +- .../src/hooks/api/integrationAuth/queries.tsx | 51 +++++++++- .../src/pages/integrations/gitlab/create.tsx | 4 - .../src/pages/integrations/vercel/create.tsx | 92 +++++++++++++------ 9 files changed, 211 insertions(+), 93 deletions(-) diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index da8fc5570..3f28787de 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -12,6 +12,10 @@ import { getTeams, revokeAccess } from '../../integrations'; +import { + INTEGRATION_VERCEL_API_URL +} from '../../variables'; +import request from '../../config/request'; /*** * Return integration authorization with id [integrationAuthId] @@ -188,25 +192,60 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => { * @returns */ export const getIntegrationAuthTeams = async (req: Request, res: Response) => { - let teams; - try { - teams = await getTeams({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get integration authorization teams" - }); - } + const teams = await getTeams({ + integrationAuth: req.integrationAuth, + accessToken: req.accessToken + }); return res.status(200).send({ teams }); } +/** + * Return list of available Vercel (preview) branches + * @param req + * @param res + */ +export const getIntegrationAuthVercelBranches = async (req: Request, res: Response) => { + const { integrationAuthId } = req.params; + const appId = req.query.appId as string; + + interface VercelBranch { + ref: string; + lastCommit: string; + isProtected: boolean; + } + + const params = new URLSearchParams({ + projectId: appId, + ...(req.integrationAuth.teamId ? { + teamId: req.integrationAuth.teamId + } : {}) + }); + + let branches: string[] = []; + + if (appId && appId !== '') { + const { data }: { data: VercelBranch[] } = await request.get( + `${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`, + { + params, + headers: { + Authorization: `Bearer ${req.accessToken}`, + 'Accept-Encoding': 'application/json' + } + } + ); + + branches = data.map((b) => b.ref); + } + + return res.status(200).send({ + branches + }); +} + /** * Delete integration authorization with id [integrationAuthId] * @param req diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 1bd138a37..e09946530 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -184,6 +184,7 @@ const getAppsVercel = async ({ apps = res.projects.map((a: any) => ({ name: a.name, + appId: a.id })); } catch (err) { Sentry.setUser(null); diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 53af1ac2c..ac2b27043 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -608,6 +608,7 @@ const syncSecretsVercel = async ({ key: string; value: string; target: string[]; + gitBranch?: string; } try { @@ -621,46 +622,7 @@ const syncSecretsVercel = async ({ } : {}), }; - - // const res = ( - // await Promise.all( - // ( - // await request.get( - // `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, - // { - // params, - // headers: { - // Authorization: `Bearer ${accessToken}`, - // 'Accept-Encoding': 'application/json' - // } - // } - // )) - // .data - // .envs - // .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment)) - // .map(async (secret: VercelSecret) => { - // if (secret.type === 'encrypted') { - // // case: secret is encrypted -> need to decrypt - // const decryptedSecret = (await request.get( - // `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - // { - // params, - // headers: { - // Authorization: `Bearer ${accessToken}`, - // 'Accept-Encoding': 'application/json' - // } - // } - // )).data; - - // return decryptedSecret; - // } - - // return secret; - // }))).reduce((obj: any, secret: any) => ({ - // ...obj, - // [secret.key]: secret - // }), {}); - + const vercelSecrets: VercelSecret[] = (await request.get( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, { @@ -673,7 +635,21 @@ const syncSecretsVercel = async ({ )) .data .envs - .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment)); + .filter((secret: VercelSecret) => { + if (!secret.target.includes(integration.targetEnvironment)) { + // case: secret does not have the same target environment + return false; + } + + if (integration.targetEnvironment === 'preview' && integration.path && integration.path !== secret.gitBranch) { + // case: secret on preview environment does not have same target git branch + return false; + } + + return true; + }); + + // return secret.target.includes(integration.targetEnvironment); const res: { [key: string]: VercelSecret } = {}; @@ -696,7 +672,7 @@ const syncSecretsVercel = async ({ res[vercelSecret.key] = vercelSecret; } } - + const updateSecrets: VercelSecret[] = []; const deleteSecrets: VercelSecret[] = []; const newSecrets: VercelSecret[] = []; @@ -710,6 +686,9 @@ const syncSecretsVercel = async ({ value: secrets[key], type: "encrypted", target: [integration.targetEnvironment], + ...(integration.path ? { + gitBranch: integration.path + } : {}) }); } }); @@ -726,7 +705,10 @@ const syncSecretsVercel = async ({ type: res[key].type, target: res[key].target.includes(integration.targetEnvironment) ? [...res[key].target] - : [...res[key].target, integration.targetEnvironment] + : [...res[key].target, integration.targetEnvironment], + ...(integration.path ? { + gitBranch: integration.path + } : {}) }); } } else { @@ -737,6 +719,9 @@ const syncSecretsVercel = async ({ value: res[key].value, type: "encrypted", // value doesn't matter target: [integration.targetEnvironment], + ...(integration.path ? { + gitBranch: integration.path + } : {}) }); } }); diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 698846ea7..33cd848bb 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -78,6 +78,7 @@ const integrationSchema = new Schema( }, path: { // aws-parameter-store-specific path + // (also) vercel preview-branch type: String, default: null }, diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index 2c1f5f045..a5a2d2882 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -91,6 +91,21 @@ router.get( integrationAuthController.getIntegrationAuthTeams ); +router.get( + '/:integrationAuthId/vercel/branches', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER] + }), + param('integrationAuthId').exists().isString(), + query('appId').exists().isString(), + query('teamId').optional().isString(), + validateRequest, + integrationAuthController.getIntegrationAuthVercelBranches +); + router.delete( '/:integrationAuthId', requireAuth({ diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index c02938985..443316c19 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -1,4 +1,6 @@ export { useGetIntegrationAuthApps, useGetIntegrationAuthById, - useGetIntegrationAuthTeams} from './queries'; \ No newline at end of file + useGetIntegrationAuthTeams, + useGetIntegrationAuthVercelBranches +} from './queries'; \ No newline at end of file diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 56a6ee45e..167f39a7d 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -5,12 +5,20 @@ import { apiRequest } from "@app/config/request"; import { App, IntegrationAuth, - Team} from './types'; + Team +} from './types'; const integrationAuthKeys = { getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuth'] as const, getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => [{ integrationAuthId, teamId }, 'integrationAuthApps'] as const, - getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthTeams'] as const + getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthTeams'] as const, + getIntegrationAuthVercelBranches: ({ + integrationAuthId, + appId, + }: { + integrationAuthId: string; + appId: string; + }) => [{ integrationAuthId, appId }, 'integrationAuthVercelBranches'] } const fetchIntegrationAuthById = async (integrationAuthId: string) => { @@ -38,6 +46,22 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => { return data.teams; } +const fetchIntegrationAuthVercelBranches = async ({ + integrationAuthId, + appId +}: { + integrationAuthId: string; + appId: string; +}) => { + const { data: { branches } } = await apiRequest.get<{ branches: string[] }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`, { + params: { + appId + } + }); + + return branches; +}; + export const useGetIntegrationAuthById = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), @@ -46,7 +70,6 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => { }); } -// TODO: fix to teamId export const useGetIntegrationAuthApps = ({ integrationAuthId, teamId @@ -70,4 +93,24 @@ export const useGetIntegrationAuthTeams = (integrationAuthId: string) => { queryFn: () => fetchIntegrationAuthTeams(integrationAuthId), enabled: true }); -} \ No newline at end of file +} + +export const useGetIntegrationAuthVercelBranches = ({ + integrationAuthId, + appId, +}: { + integrationAuthId: string; + appId: string; +}) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({ + integrationAuthId, + appId, + }), + queryFn: () => fetchIntegrationAuthVercelBranches({ + integrationAuthId, + appId, + }), + enabled: true + }); +} diff --git a/frontend/src/pages/integrations/gitlab/create.tsx b/frontend/src/pages/integrations/gitlab/create.tsx index 259285109..f4da8c9ff 100644 --- a/frontend/src/pages/integrations/gitlab/create.tsx +++ b/frontend/src/pages/integrations/gitlab/create.tsx @@ -109,7 +109,6 @@ export default function GitLabCreateIntegrationPage() { GitLab Integration branchName !== 'main') + .concat(''); + useEffect(() => { if (workspace) { setSelectedSourceEnvironment(workspace.environments[0].slug); @@ -45,15 +57,15 @@ export default function VercelCreateIntegrationPage() { }, [workspace]); useEffect(() => { - if (integrationAuthApps) { - if (integrationAuthApps.length > 0) { - setTargetApp(integrationAuthApps[0].name); - setTargetEnvironment(vercelEnvironments[0].slug); - } else { - setTargetApp('none'); - setTargetEnvironment(vercelEnvironments[0].slug); - } + if (integrationAuthApps) { + if (integrationAuthApps.length > 0) { + setTargetAppId(integrationAuthApps[0].appId as string); + setTargetEnvironment(vercelEnvironments[0].slug); + } else { + setTargetAppId('none'); + setTargetEnvironment(vercelEnvironments[0].slug); } + } }, [integrationAuthApps]); const handleButtonClick = async () => { @@ -61,15 +73,22 @@ export default function VercelCreateIntegrationPage() { if (!integrationAuth?._id) return; setIsLoading(true); + + const targetApp = integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId); + + if (!targetApp || !targetApp.appId) return; + + const path = (targetEnvironment === 'preview' && targetBranch !== '') ? targetBranch : null; + await createIntegration({ integrationAuthId: integrationAuth?._id, isActive: true, - app: targetApp, - appId: null, + app: targetApp.name, + appId: targetApp.appId, sourceEnvironment: selectedSourceEnvironment, targetEnvironment, owner: null, - path: null, + path, region: null }); @@ -82,7 +101,7 @@ export default function VercelCreateIntegrationPage() { } } - return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? ( + return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetAppId && targetEnvironment) ? (
Vercel Integration @@ -106,14 +125,14 @@ export default function VercelCreateIntegrationPage() { label="Vercel App" > + {targetEnvironment === 'preview' && filteredBranches && ( + + + + )}