diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index d1252954e..d36c9c297 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -1,7 +1,7 @@ -import axios from 'axios'; -import * as Sentry from '@sentry/node'; -import { Octokit } from '@octokit/rest'; -import { IIntegrationAuth } from '../models'; +import axios from "axios"; +import * as Sentry from "@sentry/node"; +import { Octokit } from "@octokit/rest"; +import { IIntegrationAuth } from "../models"; import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, @@ -9,12 +9,13 @@ import { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, - INTEGRATION_FLYIO_API_URL -} from '../variables'; + INTEGRATION_FLYIO_API_URL, +} from "../variables"; /** * Return list of names of apps for integration named [integration] @@ -26,7 +27,7 @@ import { */ const getApps = async ({ integrationAuth, - accessToken + accessToken, }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -42,40 +43,45 @@ const getApps = async ({ switch (integrationAuth.integration) { case INTEGRATION_HEROKU: apps = await getAppsHeroku({ - accessToken + accessToken, }); break; case INTEGRATION_VERCEL: apps = await getAppsVercel({ integrationAuth, - accessToken + accessToken, }); break; case INTEGRATION_NETLIFY: apps = await getAppsNetlify({ - accessToken + accessToken, }); break; case INTEGRATION_GITHUB: apps = await getAppsGithub({ - accessToken + accessToken, }); break; case INTEGRATION_RENDER: apps = await getAppsRender({ - accessToken + accessToken, }); break; case INTEGRATION_FLYIO: apps = await getAppsFlyio({ - accessToken + accessToken, + }); + break; + case INTEGRATION_CIRCLECI: + apps = await getAppsCircleci({ + accessToken, }); break; } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get integration apps'); + throw new Error("Failed to get integration apps"); } return apps; @@ -94,19 +100,19 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { const res = ( await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { headers: { - Accept: 'application/vnd.heroku+json; version=3', - Authorization: `Bearer ${accessToken}` - } + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + }, }) ).data; apps = res.map((a: any) => ({ - name: a.name + name: a.name, })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Heroku integration apps'); + throw new Error("Failed to get Heroku integration apps"); } return apps; @@ -119,10 +125,10 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { * @returns {Object[]} apps - names of Vercel apps * @returns {String} apps.name - name of Vercel app */ -const getAppsVercel = async ({ +const getAppsVercel = async ({ integrationAuth, - accessToken -}: { + accessToken, +}: { integrationAuth: IIntegrationAuth; accessToken: string; }) => { @@ -131,23 +137,25 @@ const getAppsVercel = async ({ const res = ( await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { headers: { - Authorization: `Bearer ${accessToken}` + Authorization: `Bearer ${accessToken}`, }, - ...( integrationAuth?.teamId ? { - params: { - teamId: integrationAuth.teamId - } - } : {}) + ...(integrationAuth?.teamId + ? { + params: { + teamId: integrationAuth.teamId, + }, + } + : {}), }) ).data; - + apps = res.projects.map((a: any) => ({ - name: a.name + name: a.name, })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Vercel integration apps'); + throw new Error("Failed to get Vercel integration apps"); } return apps; @@ -160,29 +168,25 @@ const getAppsVercel = async ({ * @returns {Object[]} apps - names of Netlify sites * @returns {String} apps.name - name of Netlify site */ -const getAppsNetlify = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { let apps; try { const res = ( await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, }) ).data; apps = res.map((a: any) => ({ name: a.name, - appId: a.site_id + appId: a.site_id, })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Netlify integration apps'); + throw new Error("Failed to get Netlify integration apps"); } return apps; @@ -195,35 +199,32 @@ const getAppsNetlify = async ({ * @returns {Object[]} apps - names of Netlify sites * @returns {String} apps.name - name of Netlify site */ -const getAppsGithub = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { let apps; try { const octokit = new Octokit({ - auth: accessToken + auth: accessToken, }); - const repos = (await octokit.request( - 'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}', - { - per_page: 100 - } - )).data; + const repos = ( + await octokit.request( + "GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", + { + per_page: 100, + } + ) + ).data; apps = repos - .filter((a:any) => a.permissions.admin === true) + .filter((a: any) => a.permissions.admin === true) .map((a: any) => ({ - name: a.name, - owner: a.owner.login - }) - ); + name: a.name, + owner: a.owner.login, + })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Github repos'); + throw new Error("Failed to get Github repos"); } return apps; @@ -237,34 +238,29 @@ const getAppsGithub = async ({ * @returns {String} apps.name - name of Render service * @returns {String} apps.appId - id of Render service */ -const getAppsRender = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsRender = async ({ accessToken }: { accessToken: string }) => { let apps: any; try { const res = ( await axios.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, }) ).data; - - apps = res - .map((a: any) => ({ - name: a.service.name, - appId: a.service.id - })); + + apps = res.map((a: any) => ({ + name: a.service.name, + appId: a.service.id, + })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Render services'); + throw new Error("Failed to get Render services"); } - + return apps; -} +}; /** * Return list of apps for Fly.io integration @@ -273,11 +269,7 @@ const getAppsRender = async ({ * @returns {Object[]} apps - names and ids of Fly.io apps * @returns {String} apps.name - name of Fly.io apps */ -const getAppsFlyio = async ({ - accessToken -}: { - accessToken: string; -}) => { +const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { let apps; try { const query = ` @@ -291,32 +283,37 @@ const getAppsFlyio = async ({ } } `; - - const res = (await axios({ - url: INTEGRATION_FLYIO_API_URL, - method: 'post', - headers: { - 'Authorization': 'Bearer ' + accessToken - }, - data: { - query, - variables: { - role: null - } - } - })).data.data.apps.nodes; - - apps = res - .map((a: any) => ({ - name: a.name - })); + + const res = ( + await axios({ + url: INTEGRATION_FLYIO_API_URL, + method: "post", + headers: { + Authorization: "Bearer " + accessToken, + }, + data: { + query, + variables: { + role: null, + }, + }, + }) + ).data.data.apps.nodes; + + apps = res.map((a: any) => ({ + name: a.name, + })); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to get Fly.io apps'); + throw new Error("Failed to get Fly.io apps"); } - + return apps; -} +}; + +const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => { + return []; +}; export { getApps }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 01e1d7ee3..d95d490d3 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -1,12 +1,12 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types } from "mongoose"; import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, INTEGRATION_NETLIFY, INTEGRATION_GITHUB, INTEGRATION_RENDER, - INTEGRATION_FLYIO -} from '../variables'; + INTEGRATION_FLYIO, +} from "../variables"; export interface IIntegration { _id: Types.ObjectId; @@ -17,44 +17,53 @@ export interface IIntegration { owner: string; targetEnvironment: string; appId: string; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio'; + integration: + | "heroku" + | "vercel" + | "netlify" + | "github" + | "render" + | "flyio" + | "circleci"; integrationAuth: Types.ObjectId; } const integrationSchema = new Schema( { workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, }, environment: { type: String, - required: true + required: true, }, isActive: { type: Boolean, - required: true + required: true, }, app: { // name of app in provider type: String, - default: null + default: null, }, - appId: { // (new) + appId: { + // (new) // id of app in provider type: String, - default: null + default: null, }, - targetEnvironment: { // (new) - // target environment + targetEnvironment: { + // (new) + // target environment type: String, - default: null + default: null, }, owner: { // github-specific repo owner-login type: String, - default: null + default: null, }, integration: { type: String, @@ -64,21 +73,21 @@ const integrationSchema = new Schema( INTEGRATION_NETLIFY, INTEGRATION_GITHUB, INTEGRATION_RENDER, - INTEGRATION_FLYIO + INTEGRATION_FLYIO, ], - required: true + required: true, }, integrationAuth: { type: Schema.Types.ObjectId, - ref: 'IntegrationAuth', - required: true - } + ref: "IntegrationAuth", + required: true, + }, }, { - timestamps: true + timestamps: true, } ); -const Integration = model('Integration', integrationSchema); +const Integration = model("Integration", integrationSchema); export default Integration; diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index bff56f09a..694ce6a67 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -1,15 +1,22 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types } from "mongoose"; import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, INTEGRATION_NETLIFY, - INTEGRATION_GITHUB -} from '../variables'; + INTEGRATION_GITHUB, +} from "../variables"; export interface IIntegrationAuth { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'render' | 'flyio'; + integration: + | "heroku" + | "vercel" + | "netlify" + | "github" + | "render" + | "flyio" + | "circleci"; teamId: string; accountId: string; refreshCiphertext?: string; @@ -24,9 +31,9 @@ export interface IIntegrationAuth { const integrationAuthSchema = new Schema( { workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, }, integration: { type: String, @@ -34,54 +41,54 @@ const integrationAuthSchema = new Schema( INTEGRATION_HEROKU, INTEGRATION_VERCEL, INTEGRATION_NETLIFY, - INTEGRATION_GITHUB + INTEGRATION_GITHUB, ], - required: true + required: true, }, teamId: { // vercel-specific integration param - type: String + type: String, }, accountId: { // netlify-specific integration param - type: String + type: String, }, refreshCiphertext: { type: String, - select: false + select: false, }, refreshIV: { type: String, - select: false + select: false, }, refreshTag: { type: String, - select: false + select: false, }, accessCiphertext: { type: String, - select: false + select: false, }, accessIV: { type: String, - select: false + select: false, }, accessTag: { type: String, - select: false + select: false, }, accessExpiresAt: { type: Date, - select: false - } + select: false, + }, }, { - timestamps: true + timestamps: true, } ); const IntegrationAuth = model( - 'IntegrationAuth', + "IntegrationAuth", integrationAuthSchema ); diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index dc1ce6f78..ed4be5d16 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -3,8 +3,8 @@ import { ENV_TESTING, ENV_STAGING, ENV_PROD, - ENV_SET -} from './environment'; + ENV_SET, +} from "./environment"; import { INTEGRATION_HEROKU, INTEGRATION_VERCEL, @@ -12,6 +12,7 @@ import { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_HEROKU_TOKEN_URL, @@ -23,25 +24,19 @@ import { INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, - INTEGRATION_OPTIONS -} from './integration'; -import { - OWNER, - ADMIN, - MEMBER, - INVITED, - ACCEPTED, -} from './organization'; -import { SECRET_SHARED, SECRET_PERSONAL } from './secret'; -import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from './event'; + INTEGRATION_OPTIONS, +} from "./integration"; +import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization"; +import { SECRET_SHARED, SECRET_PERSONAL } from "./secret"; +import { EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS } from "./event"; import { ACTION_ADD_SECRETS, ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS, - ACTION_READ_SECRETS -} from './action'; -import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from './smtp'; -import { PLAN_STARTER, PLAN_PRO } from './stripe'; + ACTION_READ_SECRETS, +} from "./action"; +import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from "./smtp"; +import { PLAN_STARTER, PLAN_PRO } from "./stripe"; export { OWNER, @@ -62,6 +57,7 @@ export { INTEGRATION_GITHUB, INTEGRATION_RENDER, INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_HEROKU_TOKEN_URL, diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 7cecb54c2..bcb161f81 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,164 +1,168 @@ import { - CLIENT_ID_HEROKU, - CLIENT_ID_NETLIFY, - CLIENT_ID_GITHUB, - CLIENT_SLUG_VERCEL -} from '../config'; + CLIENT_ID_HEROKU, + CLIENT_ID_NETLIFY, + CLIENT_ID_GITHUB, + CLIENT_SLUG_VERCEL, +} from "../config"; // integrations -const INTEGRATION_HEROKU = 'heroku'; -const INTEGRATION_VERCEL = 'vercel'; -const INTEGRATION_NETLIFY = 'netlify'; -const INTEGRATION_GITHUB = 'github'; -const INTEGRATION_RENDER = 'render'; -const INTEGRATION_FLYIO = 'flyio'; +const INTEGRATION_HEROKU = "heroku"; +const INTEGRATION_VERCEL = "vercel"; +const INTEGRATION_NETLIFY = "netlify"; +const INTEGRATION_GITHUB = "github"; +const INTEGRATION_RENDER = "render"; +const INTEGRATION_FLYIO = "flyio"; +const INTEGRATION_CIRCLECI = "circleci"; const INTEGRATION_SET = new Set([ - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, ]); // integration types -const INTEGRATION_OAUTH2 = 'oauth2'; +const INTEGRATION_OAUTH2 = "oauth2"; // integration oauth endpoints -const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; +const INTEGRATION_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token"; const INTEGRATION_VERCEL_TOKEN_URL = - 'https://api.vercel.com/v2/oauth/access_token'; -const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token'; + "https://api.vercel.com/v2/oauth/access_token"; +const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token"; const INTEGRATION_GITHUB_TOKEN_URL = - 'https://github.com/login/oauth/access_token'; + "https://github.com/login/oauth/access_token"; // integration apps endpoints -const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com'; -const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com'; -const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com'; -const INTEGRATION_RENDER_API_URL = 'https://api.render.com'; -const INTEGRATION_FLYIO_API_URL = 'https://api.fly.io/graphql'; +const INTEGRATION_HEROKU_API_URL = "https://api.heroku.com"; +const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; +const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; +const INTEGRATION_RENDER_API_URL = "https://api.render.com"; +const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; +const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api/v2"; const INTEGRATION_OPTIONS = [ - { - name: 'Heroku', - slug: 'heroku', - image: 'Heroku.png', - isAvailable: true, - type: 'oauth', - clientId: CLIENT_ID_HEROKU, - docsLink: '' - }, - { - name: 'Vercel', - slug: 'vercel', - image: 'Vercel.png', - isAvailable: true, - type: 'oauth', - clientId: '', - clientSlug: CLIENT_SLUG_VERCEL, - docsLink: '' - }, - { - name: 'Netlify', - slug: 'netlify', - image: 'Netlify.png', - isAvailable: true, - type: 'oauth', - clientId: CLIENT_ID_NETLIFY, - docsLink: '' - }, - { - name: 'GitHub', - slug: 'github', - image: 'GitHub.png', - isAvailable: true, - type: 'oauth', - clientId: CLIENT_ID_GITHUB, - docsLink: '' - }, - { - name: 'Render', - slug: 'render', - image: 'Render.png', - isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' - }, - { - name: 'Fly.io', - slug: 'flyio', - image: 'Flyio.svg', - isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' - }, - { - name: 'Google Cloud Platform', - slug: 'gcp', - image: 'Google Cloud Platform.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - }, - { - name: 'Amazon Web Services', - slug: 'aws', - image: 'Amazon Web Services.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - }, - { - name: 'Microsoft Azure', - slug: 'azure', - image: 'Microsoft Azure.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - }, - { - name: 'Travis CI', - slug: 'travisci', - image: 'Travis CI.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - }, - { - name: 'Circle CI', - slug: 'circleci', - image: 'Circle CI.png', - isAvailable: false, - type: '', - clientId: '', - docsLink: '' - } -] + { + name: "Heroku", + slug: "heroku", + image: "Heroku.png", + isAvailable: true, + type: "oauth", + clientId: CLIENT_ID_HEROKU, + docsLink: "", + }, + { + name: "Vercel", + slug: "vercel", + image: "Vercel.png", + isAvailable: true, + type: "oauth", + clientId: "", + clientSlug: CLIENT_SLUG_VERCEL, + docsLink: "", + }, + { + name: "Netlify", + slug: "netlify", + image: "Netlify.png", + isAvailable: true, + type: "oauth", + clientId: CLIENT_ID_NETLIFY, + docsLink: "", + }, + { + name: "GitHub", + slug: "github", + image: "GitHub.png", + isAvailable: true, + type: "oauth", + clientId: CLIENT_ID_GITHUB, + docsLink: "", + }, + { + name: "Render", + slug: "render", + image: "Render.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "", + }, + { + name: "Fly.io", + slug: "flyio", + image: "Flyio.svg", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "", + }, + { + name: "Google Cloud Platform", + slug: "gcp", + image: "Google Cloud Platform.png", + isAvailable: false, + type: "", + clientId: "", + docsLink: "", + }, + { + name: "Amazon Web Services", + slug: "aws", + image: "Amazon Web Services.png", + isAvailable: false, + type: "", + clientId: "", + docsLink: "", + }, + { + name: "Microsoft Azure", + slug: "azure", + image: "Microsoft Azure.png", + isAvailable: false, + type: "", + clientId: "", + docsLink: "", + }, + { + name: "Travis CI", + slug: "travisci", + image: "Travis CI.png", + isAvailable: false, + type: "", + clientId: "", + docsLink: "", + }, + { + name: "Circle CI", + slug: "circleci", + image: "Circle CI.png", + isAvailable: false, + type: "", + clientId: "", + docsLink: "", + }, +]; export { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_RENDER, - INTEGRATION_FLYIO, - INTEGRATION_SET, - INTEGRATION_OAUTH2, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, - INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_RENDER_API_URL, - INTEGRATION_FLYIO_API_URL, - INTEGRATION_OPTIONS + INTEGRATION_HEROKU, + INTEGRATION_VERCEL, + INTEGRATION_NETLIFY, + INTEGRATION_GITHUB, + INTEGRATION_RENDER, + INTEGRATION_FLYIO, + INTEGRATION_CIRCLECI, + INTEGRATION_SET, + INTEGRATION_OAUTH2, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_VERCEL_TOKEN_URL, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_VERCEL_API_URL, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_RENDER_API_URL, + INTEGRATION_FLYIO_API_URL, + INTEGRATION_OPTIONS, };