feat(integrations): New CircleCI Context Sync

This commit is contained in:
McPizza
2024-12-10 20:07:23 +01:00
parent c648235390
commit cc3d132f5d
16 changed files with 633 additions and 2 deletions
@@ -8,6 +8,7 @@ import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { NotFoundError } from "@app/lib/errors";
import { TCircleCIContext } from "./integration-app-types";
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
import { Integrations, IntegrationUrls } from "./integration-list";
@@ -489,6 +490,47 @@ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
return apps;
};
/**
* Return list of contexts for CircleCI_Context integration
*/
const getAppsCircleCIContexts = async ({ accessToken, orgSlug }: { accessToken: string; orgSlug: string }) => {
type NextPageToken = string | null | undefined;
type CircleCIContextResponse = {
items: TCircleCIContext[];
next_page_token: NextPageToken;
};
const contexts: TCircleCIContext[] = [];
let nextPageToken: NextPageToken;
while (nextPageToken !== null) {
const res = (
await request.get<CircleCIContextResponse>(`${IntegrationUrls.CIRCLECI_CONTEXT_API_URL}/v2/context`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
},
params: new URLSearchParams({
"owner-slug": orgSlug,
...(nextPageToken ? { "page-token": nextPageToken } : {})
})
})
).data;
contexts.push(...res.items);
nextPageToken = res.next_page_token;
}
const apps = contexts?.map((context) => ({
name: context.name,
appId: context.id
}));
return apps;
};
/**
* Return list of projects for Databricks integration
*/
@@ -1195,6 +1237,12 @@ export const getApps = async ({
accessToken
});
case Integrations.CIRCLECI_CONTEXT:
return getAppsCircleCIContexts({
accessToken,
orgSlug: workspaceSlug as string
});
case Integrations.DATABRICKS:
return getAppsDatabricks({
url,
@@ -0,0 +1,5 @@
export type TCircleCIContext = {
id: string;
name: string;
created_at: string;
};
@@ -25,6 +25,7 @@ import {
TBitbucketEnvironment,
TBitbucketWorkspace,
TChecklyGroups,
TCircleCIOrganization,
TDeleteIntegrationAuthByIdDTO,
TDeleteIntegrationAuthsDTO,
TDuplicateGithubIntegrationAuthDTO,
@@ -36,6 +37,7 @@ import {
TIntegrationAuthBitbucketEnvironmentsDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthCircleCIOrganizationDTO,
TIntegrationAuthGithubEnvsDTO,
TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthHerokuPipelinesDTO,
@@ -1427,6 +1429,40 @@ export const integrationAuthServiceFactory = ({
return [];
};
const getCircleCIOrganizations = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TIntegrationAuthCircleCIOrganizationDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
const { data }: { data: TCircleCIOrganization[] } = await request.get(
`${IntegrationUrls.CIRCLECI_CONTEXT_API_URL}/v2/me/collaborations`,
{
headers: {
"Circle-Token": `${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
return data;
};
const deleteIntegrationAuths = async ({
projectId,
integration,
@@ -1638,6 +1674,7 @@ export const integrationAuthServiceFactory = ({
getTeamcityBuildConfigs,
getBitbucketWorkspaces,
getBitbucketEnvironments,
getCircleCIOrganizations,
getIntegrationAccessToken,
duplicateIntegrationAuth,
getOctopusDeploySpaces,
@@ -123,6 +123,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
appId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthCircleCIOrganizationDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TVercelBranches = {
ref: string;
lastCommit: string;
@@ -184,6 +188,14 @@ export type TTeamCityBuildConfig = {
webUrl: string;
};
export type TCircleCIOrganization = {
id: string;
vcsType: string;
name: string;
avatarUrl: string;
slug: string;
};
export type TIntegrationsWithEnvironment = TIntegrations & {
environment?:
| {
@@ -15,6 +15,7 @@ export enum Integrations {
FLYIO = "flyio",
LARAVELFORGE = "laravel-forge",
CIRCLECI = "circleci",
CIRCLECI_CONTEXT = "circleci-context",
DATABRICKS = "databricks",
TRAVISCI = "travisci",
TEAMCITY = "teamcity",
@@ -76,6 +77,8 @@ export enum IntegrationUrls {
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
FLYIO_API_URL = "https://api.fly.io/graphql",
CIRCLECI_API_URL = "https://circleci.com/api",
// eslint-disable-next-line
CIRCLECI_CONTEXT_API_URL = "https://circleci.com/api",
DATABRICKS_API_URL = "https:/xxxx.com/api",
TRAVISCI_API_URL = "https://api.travis-ci.com",
SUPABASE_API_URL = "https://api.supabase.com",
@@ -226,6 +229,15 @@ export const getIntegrationOptions = async () => {
clientId: "",
docsLink: ""
},
{
name: "Circle CI Contexts",
slug: "circleci-context",
image: "Circle CI.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Databricks",
slug: "databricks",
@@ -2343,6 +2343,89 @@ const syncSecretsCircleCI = async ({
);
};
/**
* Sync/push [secrets] to CircleCI Context
*/
const syncSecretsCircleCIContext = async ({
integration,
secrets,
accessToken
}: {
integration: TIntegrations;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
// sync secrets to CircleCI
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.put(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${key}`,
{
value: secrets[key].value
},
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
}
)
)
);
// get secrets from CircleCI
const getSecretsRes = async () => {
type EnvVars = {
variable: string;
created_at: string;
updated_at: string;
context_id: string;
};
type ResponseSchema = {
items: EnvVars[];
next_page_token: string | null;
};
let nextPageToken: string | null | undefined;
const envVars: EnvVars[] = [];
while (nextPageToken !== null) {
const res = await request.get<ResponseSchema>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
}
);
envVars.push(...res.data.items);
nextPageToken = res.data.next_page_token;
}
return envVars;
};
// delete secrets from CircleCI
await Promise.all(
(await getSecretsRes()).map(async (sec) => {
if (!(sec.variable in secrets)) {
return request.delete(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${sec.variable}`,
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
}
);
}
})
);
};
/**
* Sync/push [secrets] to Databricks project
*/
@@ -4433,6 +4516,13 @@ export const syncIntegrationSecrets = async ({
accessToken
});
break;
case Integrations.CIRCLECI_CONTEXT:
await syncSecretsCircleCIContext({
integration,
secrets,
accessToken
});
break;
case Integrations.DATABRICKS:
await syncSecretsDatabricks({
integration,