diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index 7a828f332..f18734805 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -13,12 +13,14 @@ import { INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, + INTEGRATION_TRAVISCI, INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_TRAVISCI_API_URL, } from "../variables"; /** @@ -42,7 +44,7 @@ const getApps = async ({ owner?: string; } - let apps: App[]; + let apps: App[] = []; try { switch (integrationAuth.integration) { case INTEGRATION_AZURE_KEY_VAULT: @@ -90,6 +92,11 @@ const getApps = async ({ accessToken, }); break; + case INTEGRATION_TRAVISCI: + apps = await getAppsTravisCI({ + accessToken, + }) + break; } } catch (err) { Sentry.setUser(null); @@ -369,4 +376,34 @@ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { return apps; }; +const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { + let apps: any; + try { + const res = ( + await axios.get( + `${INTEGRATION_TRAVISCI_API_URL}/repos`, + { + headers: { + "Authorization": `token ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ) + ).data; + + apps = res?.map((a: any) => { + return { + name: a?.slug?.split("/")[1], + appId: a?.id, + } + }); + }catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to get TravisCI projects"); + } + + return apps; +} + export { getApps }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 3309e5813..61d345c38 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -23,12 +23,14 @@ import { INTEGRATION_RENDER, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, + INTEGRATION_TRAVISCI, INTEGRATION_HEROKU_API_URL, INTEGRATION_VERCEL_API_URL, INTEGRATION_NETLIFY_API_URL, INTEGRATION_RENDER_API_URL, INTEGRATION_FLYIO_API_URL, INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_TRAVISCI_API_URL, } from "../variables"; /** @@ -128,6 +130,14 @@ const syncSecrets = async ({ secrets, accessToken, }); + break; + case INTEGRATION_TRAVISCI: + await syncSecretsTravisCI({ + integration, + secrets, + accessToken, + }); + break; } } catch (err) { Sentry.setUser(null); @@ -1283,4 +1293,96 @@ const syncSecretsCircleCI = async ({ } }; +/** + * Sync/push [secrets] to TravisCI project + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - access token for TravisCI integration + */ +const syncSecretsTravisCI = async ({ + integration, + secrets, + accessToken, +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + try { + // get secrets from travis-ci + const getSecretsRes = ( + await axios.get( + `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, + { + headers: { + "Authorization": `token ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ) + ).data?.env_vars; + + // add secrets + for (const key of Object.keys(secrets)) { + const existingSecret = getSecretsRes.find((s: any) => s.name == key); + if(!existingSecret){ + await axios.post( + `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, + { + env_var: { + name: key, + value: secrets[key], + } + }, + { + headers: { + "Authorization": `token ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", + }, + } + ) + }else { // update secret + await axios.patch( + `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${existingSecret.id}?repository_id=${existingSecret.repository_id}`, + { + env_var: { + name: key, + value: secrets[key], + } + }, + { + headers: { + "Authorization": `token ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", + }, + } + ) + } + } + + // delete secret + for (const sec of getSecretsRes) { + if (!(sec.name in secrets)){ + await axios.delete( + `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${sec.id}?repository_id=${sec.repository_id}`, + { + headers: { + "Authorization": `token ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", + }, + } + ); + } + } + }catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to CircleCI"); + } +} + export { syncSecrets }; diff --git a/frontend/src/pages/integrations/travisci/authorize.tsx b/frontend/src/pages/integrations/travisci/authorize.tsx index 3ae7e9def..e55b82f1a 100644 --- a/frontend/src/pages/integrations/travisci/authorize.tsx +++ b/frontend/src/pages/integrations/travisci/authorize.tsx @@ -11,7 +11,7 @@ import { } from '../../../components/v2'; import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; -export default function CircleCICreateIntegrationPage() { +export default function TravisCICreateIntegrationPage() { const router = useRouter(); const [apiKey, setApiKey] = useState(''); const [apiKeyErrorText, setApiKeyErrorText] = useState(''); @@ -49,7 +49,7 @@ export default function CircleCICreateIntegrationPage() { TravisCI Integration @@ -72,6 +72,6 @@ export default function CircleCICreateIntegrationPage() { ) } -CircleCICreateIntegrationPage.requireAuth = true; +TravisCICreateIntegrationPage.requireAuth = true; export const getServerSideProps = getTranslatedServerSideProps(['integrations']); \ No newline at end of file diff --git a/frontend/src/pages/integrations/travisci/create.tsx b/frontend/src/pages/integrations/travisci/create.tsx index 6d42f5b0c..4a06c838a 100644 --- a/frontend/src/pages/integrations/travisci/create.tsx +++ b/frontend/src/pages/integrations/travisci/create.tsx @@ -15,7 +15,7 @@ import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../ho import { useGetWorkspaceById } from '../../../hooks/api/workspace'; import createIntegration from "../../api/integrations/createIntegration"; -export default function CircleCICreateIntegrationPage() { +export default function TravisCICreateIntegrationPage() { const router = useRouter(); const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]); @@ -119,6 +119,6 @@ export default function CircleCICreateIntegrationPage() { ) :
} -CircleCICreateIntegrationPage.requireAuth = true; +TravisCICreateIntegrationPage.requireAuth = true; export const getServerSideProps = getTranslatedServerSideProps(['integrations']); \ No newline at end of file