Compare commits
15 Commits
developmen
...
misc/remov
Author | SHA1 | Date | |
---|---|---|---|
b99b98b6a4 | |||
379e526200 | |||
6b2eb9c6c9 | |||
b669b0a9f8 | |||
9e768640cd | |||
e3d29b637d | |||
9cd0dc8970 | |||
f8f5000bad | |||
40919ccf59 | |||
44303aca6a | |||
4bd50c3548 | |||
1cbf030e6c | |||
7c8f2e5548 | |||
a730b16318 | |||
cc3d132f5d |
@ -1185,4 +1185,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
||||
return { spaces };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:integrationAuthId/circleci/organizations",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
params: z.object({
|
||||
integrationAuthId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organizations: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
projects: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array(),
|
||||
contexts: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const organizations = await server.services.integrationAuth.getCircleCIOrganizations({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.integrationAuthId
|
||||
});
|
||||
return { organizations };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -16,6 +16,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||
|
||||
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
||||
|
||||
@ -29,9 +30,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
organizations: OrganizationsSchema.extend({
|
||||
orgAuthMethod: z.string()
|
||||
}).array()
|
||||
organizations: sanitizedOrganizationSchema
|
||||
.extend({
|
||||
orgAuthMethod: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||
|
||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -134,7 +135,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
description: "Return organizations that current user is part of",
|
||||
response: {
|
||||
200: z.object({
|
||||
organizations: OrganizationsSchema.array()
|
||||
organizations: sanitizedOrganizationSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@ -0,0 +1,5 @@
|
||||
export type TCircleCIContext = {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
};
|
@ -17,6 +17,8 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
@ -24,6 +26,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { getApps } from "./integration-app-list";
|
||||
import { TCircleCIContext } from "./integration-app-types";
|
||||
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||
import {
|
||||
@ -31,6 +34,7 @@ import {
|
||||
TBitbucketEnvironment,
|
||||
TBitbucketWorkspace,
|
||||
TChecklyGroups,
|
||||
TCircleCIOrganization,
|
||||
TDeleteIntegrationAuthByIdDTO,
|
||||
TDeleteIntegrationAuthsDTO,
|
||||
TDuplicateGithubIntegrationAuthDTO,
|
||||
@ -42,6 +46,7 @@ import {
|
||||
TIntegrationAuthBitbucketEnvironmentsDTO,
|
||||
TIntegrationAuthBitbucketWorkspaceDTO,
|
||||
TIntegrationAuthChecklyGroupsDTO,
|
||||
TIntegrationAuthCircleCIOrganizationDTO,
|
||||
TIntegrationAuthGithubEnvsDTO,
|
||||
TIntegrationAuthGithubOrgsDTO,
|
||||
TIntegrationAuthHerokuPipelinesDTO,
|
||||
@ -1578,6 +1583,120 @@ 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: organizations }: { data: TCircleCIOrganization[] } = await request.get(
|
||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
|
||||
{
|
||||
headers: {
|
||||
"Circle-Token": `${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let projects: {
|
||||
orgName: string;
|
||||
projectName: string;
|
||||
projectId?: string;
|
||||
}[] = [];
|
||||
|
||||
try {
|
||||
const projectRes = (
|
||||
await request.get<{ reponame: string; username: string; vcs_url: string }[]>(
|
||||
`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`,
|
||||
{
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
).data;
|
||||
|
||||
projects = projectRes.map((a) => ({
|
||||
orgName: a.username, // username maps to unique organization name in CircleCI
|
||||
projectName: a.reponame, // reponame maps to project name within an organization in CircleCI
|
||||
projectId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
const projectsByOrg = groupBy(
|
||||
projects.map((p) => ({
|
||||
orgName: p.orgName,
|
||||
name: p.projectName,
|
||||
id: p.projectId as string
|
||||
})),
|
||||
(p) => p.orgName
|
||||
);
|
||||
|
||||
const getOrgContexts = async (orgSlug: string) => {
|
||||
type NextPageToken = string | null | undefined;
|
||||
|
||||
try {
|
||||
const contexts: TCircleCIContext[] = [];
|
||||
let nextPageToken: NextPageToken;
|
||||
|
||||
while (nextPageToken !== null) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { data } = await request.get<{
|
||||
items: TCircleCIContext[];
|
||||
next_page_token: NextPageToken;
|
||||
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context`, {
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Accept-Encoding": "application/json"
|
||||
},
|
||||
params: new URLSearchParams({
|
||||
"owner-slug": orgSlug,
|
||||
...(nextPageToken ? { "page-token": nextPageToken } : {})
|
||||
})
|
||||
});
|
||||
|
||||
contexts.push(...data.items);
|
||||
nextPageToken = data.next_page_token;
|
||||
}
|
||||
|
||||
return contexts?.map((context) => ({
|
||||
name: context.name,
|
||||
id: context.id
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return Promise.all(
|
||||
organizations.map(async (org) => ({
|
||||
name: org.name,
|
||||
slug: org.slug,
|
||||
projects: projectsByOrg[org.name] ?? [],
|
||||
contexts: (await getOrgContexts(org.slug)) ?? []
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const deleteIntegrationAuths = async ({
|
||||
projectId,
|
||||
integration,
|
||||
@ -1790,6 +1909,7 @@ export const integrationAuthServiceFactory = ({
|
||||
getTeamcityBuildConfigs,
|
||||
getBitbucketWorkspaces,
|
||||
getBitbucketEnvironments,
|
||||
getCircleCIOrganizations,
|
||||
getIntegrationAccessToken,
|
||||
duplicateIntegrationAuth,
|
||||
getOctopusDeploySpaces,
|
||||
|
@ -128,6 +128,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
|
||||
appId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIntegrationAuthCircleCIOrganizationDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TVercelBranches = {
|
||||
ref: string;
|
||||
lastCommit: string;
|
||||
@ -189,6 +193,14 @@ export type TTeamCityBuildConfig = {
|
||||
webUrl: string;
|
||||
};
|
||||
|
||||
export type TCircleCIOrganization = {
|
||||
id: string;
|
||||
vcsType: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type TIntegrationsWithEnvironment = TIntegrations & {
|
||||
environment?:
|
||||
| {
|
||||
@ -215,6 +227,11 @@ export enum OctopusDeployScope {
|
||||
// add tenant, variable set, etc.
|
||||
}
|
||||
|
||||
export enum CircleCiScope {
|
||||
Project = "project",
|
||||
Context = "context"
|
||||
}
|
||||
|
||||
export type TOctopusDeployVariableSet = {
|
||||
Id: string;
|
||||
OwnerId: string;
|
||||
|
@ -76,7 +76,6 @@ 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",
|
||||
DATABRICKS_API_URL = "https:/xxxx.com/api",
|
||||
TRAVISCI_API_URL = "https://api.travis-ci.com",
|
||||
SUPABASE_API_URL = "https://api.supabase.com",
|
||||
LARAVELFORGE_API_URL = "https://forge.laravel.com",
|
||||
@ -218,9 +217,9 @@ export const getIntegrationOptions = async () => {
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Circle CI",
|
||||
name: "CircleCI",
|
||||
slug: "circleci",
|
||||
image: "Circle CI.png",
|
||||
image: "CircleCI.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
|
@ -39,7 +39,12 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
|
||||
import { OctopusDeployScope, TIntegrationsWithEnvironment, TOctopusDeployVariableSet } from "./integration-auth-types";
|
||||
import {
|
||||
CircleCiScope,
|
||||
OctopusDeployScope,
|
||||
TIntegrationsWithEnvironment,
|
||||
TOctopusDeployVariableSet
|
||||
} from "./integration-auth-types";
|
||||
import {
|
||||
IntegrationInitialSyncBehavior,
|
||||
IntegrationMappingBehavior,
|
||||
@ -2245,102 +2250,174 @@ const syncSecretsCircleCI = async ({
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
const getProjectSlug = async () => {
|
||||
const requestConfig = {
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const projectDetails = (
|
||||
await request.get<{ slug: string }>(
|
||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
|
||||
requestConfig
|
||||
if (integration.scope === CircleCiScope.Context) {
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
)
|
||||
).data;
|
||||
)
|
||||
);
|
||||
|
||||
return projectDetails.slug;
|
||||
} catch (err) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err.response?.data?.message !== "Not Found") {
|
||||
throw new Error("Failed to get project slug from CircleCI during first attempt.");
|
||||
}
|
||||
}
|
||||
}
|
||||
// get secrets from CircleCI
|
||||
const getSecretsRes = async () => {
|
||||
type EnvVars = {
|
||||
variable: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
context_id: string;
|
||||
};
|
||||
|
||||
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
|
||||
try {
|
||||
const circleCiOrganization = (
|
||||
await request.get<{ slug: string; name: string }[]>(
|
||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
|
||||
requestConfig
|
||||
)
|
||||
).data;
|
||||
let nextPageToken: string | null | undefined;
|
||||
const envVars: EnvVars[] = [];
|
||||
|
||||
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
|
||||
if (integration.owner) {
|
||||
const org = circleCiOrganization.find((o) => o.name === integration.owner);
|
||||
if (org) {
|
||||
return `${org.slug}/${integration.app}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
|
||||
return `${circleCiOrganization[0].slug}/${integration.app}`;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to get project slug from CircleCI during second attempt.");
|
||||
}
|
||||
};
|
||||
|
||||
const projectSlug = await getProjectSlug();
|
||||
|
||||
// sync secrets to CircleCI
|
||||
await Promise.all(
|
||||
Object.keys(secrets).map(async (key) =>
|
||||
request.post(
|
||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
|
||||
{
|
||||
name: key,
|
||||
value: secrets[key].value
|
||||
},
|
||||
{
|
||||
while (nextPageToken !== null) {
|
||||
const res = await request.get<{
|
||||
items: EnvVars[];
|
||||
next_page_token: string | null;
|
||||
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable`, {
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
"Accept-Encoding": "application/json"
|
||||
},
|
||||
params: nextPageToken
|
||||
? new URLSearchParams({
|
||||
"page-token": nextPageToken
|
||||
})
|
||||
: undefined
|
||||
});
|
||||
|
||||
// get secrets from CircleCI
|
||||
const getSecretsRes = (
|
||||
await request.get<{ items: { name: string }[] }>(
|
||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
|
||||
{
|
||||
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"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const getProjectSlug = async () => {
|
||||
const requestConfig = {
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
).data?.items;
|
||||
};
|
||||
|
||||
// delete secrets from CircleCI
|
||||
await Promise.all(
|
||||
getSecretsRes.map(async (sec) => {
|
||||
if (!(sec.name in secrets)) {
|
||||
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
|
||||
try {
|
||||
const projectDetails = (
|
||||
await request.get<{ slug: string }>(
|
||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
|
||||
requestConfig
|
||||
)
|
||||
).data;
|
||||
|
||||
return projectDetails.slug;
|
||||
} catch (err) {
|
||||
if (err instanceof AxiosError) {
|
||||
if (err.response?.data?.message !== "Not Found") {
|
||||
throw new Error("Failed to get project slug from CircleCI during first attempt.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
|
||||
try {
|
||||
const circleCiOrganization = (
|
||||
await request.get<{ slug: string; name: string }[]>(
|
||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
|
||||
requestConfig
|
||||
)
|
||||
).data;
|
||||
|
||||
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
|
||||
if (integration.owner) {
|
||||
const org = circleCiOrganization.find((o) => o.name === integration.owner);
|
||||
if (org) {
|
||||
return `${org.slug}/${integration.app}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
|
||||
return `${circleCiOrganization[0].slug}/${integration.app}`;
|
||||
} catch (err) {
|
||||
throw new Error("Failed to get project slug from CircleCI during second attempt.");
|
||||
}
|
||||
};
|
||||
|
||||
const projectSlug = await getProjectSlug();
|
||||
|
||||
// sync secrets to CircleCI
|
||||
await Promise.all(
|
||||
Object.keys(secrets).map(async (key) =>
|
||||
request.post(
|
||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
|
||||
{
|
||||
name: key,
|
||||
value: secrets[key].value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// get secrets from CircleCI
|
||||
const getSecretsRes = (
|
||||
await request.get<{ items: { name: string }[] }>(
|
||||
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
|
||||
{
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Content-Type": "application/json"
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
).data?.items;
|
||||
|
||||
// delete secrets from CircleCI
|
||||
await Promise.all(
|
||||
getSecretsRes.map(async (sec) => {
|
||||
if (!(sec.name in secrets)) {
|
||||
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
|
||||
headers: {
|
||||
"Circle-Token": accessToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
16
backend/src/services/org/org-schema.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { OrganizationsSchema } from "@app/db/schemas";
|
||||
|
||||
export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
customerId: true,
|
||||
slug: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
authEnforced: true,
|
||||
scimEnabled: true,
|
||||
kmsDefaultKeyId: true,
|
||||
defaultMembershipRole: true,
|
||||
enforceMfa: true,
|
||||
selectedMfaMethod: true
|
||||
});
|
@ -51,7 +51,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.where(`${TableName.Project}.orgId`, orgId)
|
||||
.andWhere((qb) => {
|
||||
if (projectType) {
|
||||
if (projectType !== "all") {
|
||||
void qb.where(`${TableName.Project}.type`, projectType);
|
||||
}
|
||||
})
|
||||
|
@ -441,7 +441,12 @@ export const projectServiceFactory = ({
|
||||
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
|
||||
|
||||
if (includeRoles) {
|
||||
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
|
||||
const { permission } = await permissionService.getUserOrgPermission(
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
// `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls.
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 494 KiB |
After Width: | Height: | Size: 537 KiB |
After Width: | Height: | Size: 538 KiB |
Before Width: | Height: | Size: 339 KiB After Width: | Height: | Size: 555 KiB |
@ -11,21 +11,30 @@ Prerequisites:
|
||||
<Step title="Authorize Infisical for CircleCI">
|
||||
Obtain an API token in User Settings > Personal API Tokens
|
||||
|
||||

|
||||

|
||||
|
||||
Navigate to your project's integrations tab in Infisical.
|
||||
|
||||

|
||||

|
||||
|
||||
Press on the CircleCI tile and input your CircleCI API token to grant Infisical access to your CircleCI account.
|
||||
|
||||

|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Start integration">
|
||||
Select which Infisical environment secrets you want to sync to which CircleCI project and press create integration to start syncing secrets to CircleCI.
|
||||
Select which Infisical environment secrets you want to sync to which CircleCI project or context.
|
||||
<Tabs>
|
||||
<Tab title="Project">
|
||||

|
||||
</Tab>
|
||||
<Tab title="Context">
|
||||

|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Finally, press create integration to start syncing secrets to CircleCI.
|
||||

|
||||
|
||||

|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
</Steps>
|
||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
@ -7,6 +7,7 @@ export {
|
||||
useGetIntegrationAuthBitBucketWorkspaces,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthChecklyGroups,
|
||||
useGetIntegrationAuthCircleCIOrganizations,
|
||||
useGetIntegrationAuthGithubEnvs,
|
||||
useGetIntegrationAuthGithubOrgs,
|
||||
useGetIntegrationAuthNorthflankSecretGroups,
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
BitBucketEnvironment,
|
||||
BitBucketWorkspace,
|
||||
ChecklyGroup,
|
||||
CircleCIOrganization,
|
||||
Environment,
|
||||
HerokuPipelineCoupling,
|
||||
IntegrationAuth,
|
||||
@ -128,7 +129,9 @@ const integrationAuthKeys = {
|
||||
integrationAuthId,
|
||||
...params
|
||||
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const
|
||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const,
|
||||
getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||
@ -510,6 +513,15 @@ const fetchIntegrationAuthOctopusDeployScopeValues = async ({
|
||||
return data;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthCircleCIOrganizations = async (integrationAuthId: string) => {
|
||||
const {
|
||||
data: { organizations }
|
||||
} = await apiRequest.get<{
|
||||
organizations: CircleCIOrganization[];
|
||||
}>(`/api/v1/integration-auth/${integrationAuthId}/circleci/organizations`);
|
||||
return organizations;
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||
@ -884,6 +896,13 @@ export const useGetIntegrationAuthTeamCityBuildConfigs = ({
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthCircleCIOrganizations = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthCircleCIOrganizations(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthCircleCIOrganizations(integrationAuthId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useAuthorizeIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
@ -105,6 +105,19 @@ export enum OctopusDeployScope {
|
||||
// tenant, variable set
|
||||
}
|
||||
|
||||
export type CircleCIOrganization = {
|
||||
name: string;
|
||||
slug: string;
|
||||
projects: {
|
||||
name: string;
|
||||
id: string;
|
||||
}[];
|
||||
contexts: {
|
||||
name: string;
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = {
|
||||
integrationAuthId: string;
|
||||
spaceId: string;
|
||||
@ -125,3 +138,8 @@ export type TOctopusDeployVariableSetScopeValues = {
|
||||
Name: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export enum CircleCiScope {
|
||||
Context = "context",
|
||||
Project = "project"
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ export default function CircleCICreateIntegrationPage() {
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="inline flex items-center pb-0.5">
|
||||
<Image
|
||||
src="/images/integrations/Circle CI.png"
|
||||
src="/images/integrations/CircleCI.png"
|
||||
height={30}
|
||||
width={30}
|
||||
alt="CircleCI logo"
|
||||
|
@ -1,293 +1,309 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faBugs,
|
||||
faCircleInfo
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "../../../components/v2";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
} from "../../../hooks/api/integrationAuth";
|
||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||
SelectItem,
|
||||
Spinner
|
||||
} from "@app/components/v2";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
import { useGetIntegrationAuthCircleCIOrganizations } from "@app/hooks/api/integrationAuth";
|
||||
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||
|
||||
const formSchema = z.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(CircleCiScope.Context),
|
||||
secretPath: z.string().default("/"),
|
||||
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||
targetOrg: z.object({ name: z.string().min(1), slug: z.string().min(1) }),
|
||||
targetContext: z.object({ name: z.string().min(1), id: z.string().min(1) })
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(CircleCiScope.Project),
|
||||
secretPath: z.string().default("/"),
|
||||
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||
targetOrg: z.object({ name: z.string().min(1), slug: z.string().min(1) }),
|
||||
targetProject: z.object({ name: z.string().min(1), id: z.string().min(1) })
|
||||
})
|
||||
]);
|
||||
|
||||
type TFormData = z.infer<typeof formSchema>;
|
||||
|
||||
export default function CircleCICreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
const { mutateAsync, isLoading: isCreatingIntegration } = useCreateIntegration();
|
||||
const { currentWorkspace, isLoading: isProjectLoading } = useWorkspace();
|
||||
|
||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
||||
const integrationAuthId = router.query.integrationAuthId as string;
|
||||
|
||||
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
||||
const { data: integrationAuth, isLoading: isintegrationAuthLoading } = useGetIntegrationAuthById(
|
||||
(integrationAuthId as string) ?? ""
|
||||
);
|
||||
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
||||
useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||
const [targetOrganization, setTargetOrganization] = useState("");
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
|
||||
const [targetProjectId, setTargetProjectId] = useState("");
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
const { control, watch, handleSubmit, setValue } = useForm<TFormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
secretPath: "/",
|
||||
sourceEnvironment: currentWorkspace?.environments[0],
|
||||
scope: CircleCiScope.Project
|
||||
}
|
||||
}, [workspace]);
|
||||
});
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
const selectedScope = watch("scope");
|
||||
const selectedOrg = watch("targetOrg");
|
||||
|
||||
const { data: circleCIOrganizations, isLoading: isCircleCIOrganizationsLoading } =
|
||||
useGetIntegrationAuthCircleCIOrganizations(integrationAuthId);
|
||||
|
||||
const selectedOrganizationEntry = selectedOrg
|
||||
? circleCIOrganizations?.find((org) => org.slug === selectedOrg.slug)
|
||||
: undefined;
|
||||
|
||||
const onSubmit = async (data: TFormData) => {
|
||||
try {
|
||||
if (!integrationAuth?.id) return;
|
||||
|
||||
if (!targetProjectId || targetOrganization === "none") {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Please select a project"
|
||||
if (data.scope === CircleCiScope.Context) {
|
||||
await mutateAsync({
|
||||
scope: data.scope,
|
||||
integrationAuthId,
|
||||
isActive: true,
|
||||
sourceEnvironment: data.sourceEnvironment.slug,
|
||||
app: data.targetContext.name,
|
||||
appId: data.targetContext.id,
|
||||
owner: data.targetOrg.name,
|
||||
secretPath: data.secretPath
|
||||
});
|
||||
} else {
|
||||
await mutateAsync({
|
||||
scope: data.scope,
|
||||
integrationAuthId,
|
||||
isActive: true,
|
||||
app: data.targetProject.name, // project name
|
||||
owner: data.targetOrg.name, // organization name
|
||||
appId: data.targetProject.id, // project id (used for syncing)
|
||||
sourceEnvironment: data.sourceEnvironment.slug,
|
||||
secretPath: data.secretPath
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const selectedApp = integrationAuthApps?.find(
|
||||
(integrationAuthApp) => integrationAuthApp.appId === targetProjectId
|
||||
);
|
||||
|
||||
if (!selectedApp) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Invalid project selected"
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
integrationAuthId: integrationAuth?.id,
|
||||
isActive: true,
|
||||
app: selectedApp.name, // project name
|
||||
owner: selectedApp.owner, // organization name
|
||||
appId: selectedApp.appId, // project id (used for syncing)
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
secretPath
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created integration"
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||
router.push(`/integrations/${currentWorkspace?.id}`);
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create integration"
|
||||
});
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredProjects = useMemo(() => {
|
||||
if (!integrationAuthApps) return [];
|
||||
if (isProjectLoading || isCircleCIOrganizationsLoading)
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center p-24">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
return integrationAuthApps.filter((integrationAuthApp) => {
|
||||
return integrationAuthApp.owner === targetOrganization;
|
||||
});
|
||||
}, [integrationAuthApps, targetOrganization]);
|
||||
|
||||
const filteredOrganizations = useMemo(() => {
|
||||
const organizations = new Set<string>();
|
||||
|
||||
if (integrationAuthApps) {
|
||||
integrationAuthApps.forEach((integrationAuthApp) => {
|
||||
if (!integrationAuthApp.owner) return;
|
||||
organizations.add(integrationAuthApp.owner);
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(organizations);
|
||||
}, [integrationAuthApps]);
|
||||
|
||||
return integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps ? (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<Head>
|
||||
<title>Set Up CircleCI Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
>
|
||||
<Card className="max-w-lg rounded-md p-8 pt-4">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI environment variables."
|
||||
className="w-full px-0 text-left text-xl"
|
||||
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex items-center pb-0.5">
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-1.5">
|
||||
<Image
|
||||
src="/images/integrations/Circle CI.png"
|
||||
src="/images/integrations/CircleCI.png"
|
||||
height={30}
|
||||
width={30}
|
||||
alt="CircleCI logo"
|
||||
/>
|
||||
|
||||
<span className="">CircleCI Context Integration </span>
|
||||
</div>
|
||||
<span className="ml-1.5">CircleCI Integration </span>
|
||||
<Link href="https://infisical.com/docs/integrations/cicd/circleci" passHref>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<Link
|
||||
href="https://infisical.com/docs/integrations/cicd/circleci"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
passHref
|
||||
>
|
||||
<div className="ml-2 mb-1 flex cursor-pointer flex-row items-center gap-0.5 rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>
|
||||
|
||||
<FormControl label="Project Environment" className="px-6">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path" className="px-6">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="CircleCI Organization" className="px-6">
|
||||
<Select
|
||||
value={targetOrganization}
|
||||
onValueChange={(val) => {
|
||||
setTargetOrganization(val);
|
||||
setTargetProjectId("none");
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
isDisabled={filteredOrganizations.length === 0}
|
||||
>
|
||||
{filteredOrganizations.length > 0 ? (
|
||||
filteredOrganizations.map((org) => (
|
||||
<SelectItem value={org} key={`target-org-${org}`}>
|
||||
{org}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No organizations found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{targetOrganization && (
|
||||
<FormControl label="CircleCI Project ID" className="px-6">
|
||||
<Select
|
||||
value={targetProjectId}
|
||||
onValueChange={(val) => {
|
||||
setTargetProjectId(val);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
isDisabled={filteredProjects.length === 0}
|
||||
<Controller
|
||||
control={control}
|
||||
name="sourceEnvironment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="Project Environment"
|
||||
>
|
||||
{filteredProjects.length > 0 ? (
|
||||
filteredProjects.map((project) => (
|
||||
<SelectItem value={project.appId!} key={`target-project-${project.owner}`}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No projects found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.slug}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={currentWorkspace?.environments}
|
||||
placeholder="Select a project environment"
|
||||
isDisabled={!currentWorkspace?.environments.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
|
||||
<SecretPathInput {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetOrg"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="CircleCI Organization"
|
||||
>
|
||||
<FilterableSelect
|
||||
getOptionValue={(option) => option.slug}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={(e) => {
|
||||
setValue("targetProject", {
|
||||
name: "",
|
||||
id: ""
|
||||
});
|
||||
setValue("targetContext", {
|
||||
name: "",
|
||||
id: ""
|
||||
});
|
||||
|
||||
onChange(e);
|
||||
}}
|
||||
options={circleCIOrganizations}
|
||||
placeholder={
|
||||
circleCIOrganizations?.length
|
||||
? "Select an organization..."
|
||||
: "No organizations found..."
|
||||
}
|
||||
isDisabled={!circleCIOrganizations?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="scope"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
onValueChange={(e) => {
|
||||
onChange(e);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
<SelectItem value={CircleCiScope.Project}>Project</SelectItem>
|
||||
<SelectItem value={CircleCiScope.Context}>Context</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{selectedScope === CircleCiScope.Context && selectedOrganizationEntry && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetContext"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="CircleCI Context"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
getOptionValue={(option) => option.id!}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={selectedOrganizationEntry?.contexts}
|
||||
placeholder={
|
||||
selectedOrganizationEntry.contexts?.length
|
||||
? "Select a context..."
|
||||
: "No contexts found..."
|
||||
}
|
||||
isDisabled={!selectedOrganizationEntry.contexts?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectedScope === CircleCiScope.Project && selectedOrganizationEntry && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="targetProject"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="CircleCI Project"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
getOptionValue={(option) => option.id!}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
options={selectedOrganizationEntry?.projects}
|
||||
placeholder={
|
||||
selectedOrganizationEntry.projects?.length
|
||||
? "Select a project..."
|
||||
: "No projects found..."
|
||||
}
|
||||
isDisabled={!selectedOrganizationEntry.projects?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
className="mt-4"
|
||||
isLoading={isCreatingIntegration}
|
||||
isDisabled={isCreatingIntegration}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
||||
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
||||
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
||||
</div>
|
||||
<span className="mt-4 text-sm text-mineshaft-300">
|
||||
After creating an integration, your secrets will start syncing immediately. This might
|
||||
cause an unexpected override of current secrets in CircleCI with secrets from Infisical.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Head>
|
||||
<title>Set Up CircleCI Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
{isIntegrationAuthAppsLoading || isintegrationAuthLoading ? (
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
||||
<p>
|
||||
Something went wrong. Please contact{" "}
|
||||
<a
|
||||
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="mailto:support@infisical.com"
|
||||
>
|
||||
support@infisical.com
|
||||
</a>{" "}
|
||||
if the issue persists.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||
|
||||
import { FormLabel } from "@app/components/v2";
|
||||
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||
import { IntegrationMappingBehavior, TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||
|
||||
type Props = {
|
||||
@ -46,6 +47,11 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
||||
case "qovery":
|
||||
return integration.scope;
|
||||
case "circleci":
|
||||
if (integration.scope === CircleCiScope.Context) {
|
||||
return "Context";
|
||||
}
|
||||
|
||||
return "Project";
|
||||
case "terraform-cloud":
|
||||
return "Project";
|
||||
case "aws-secret-manager":
|
||||
@ -77,7 +83,6 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
||||
return `${integration.owner}`;
|
||||
}
|
||||
return `${integration.owner}/${integration.app}`;
|
||||
|
||||
case "aws-parameter-store":
|
||||
case "rundeck":
|
||||
return `${integration.path}`;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FormLabel } from "@app/components/v2";
|
||||
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||
import { IntegrationMappingBehavior, TIntegration } from "@app/hooks/api/integrations/types";
|
||||
|
||||
type Props = {
|
||||
@ -52,7 +53,8 @@ export const IntegrationDetails = ({ integration }: Props) => {
|
||||
<FormLabel
|
||||
label={
|
||||
(integration.integration === "qovery" && integration?.scope) ||
|
||||
(integration.integration === "circleci" && "Project") ||
|
||||
(integration.integration === "circleci" &&
|
||||
(integration.scope === CircleCiScope.Context ? "Context" : "Project")) ||
|
||||
(integration.integration === "bitbucket" && "Repository") ||
|
||||
(integration.integration === "octopus-deploy" && "Project") ||
|
||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||
|