fix(integrations/circle-ci): Refactored Circle CI integration

The integration seemingly never worked in the first place due to inpropper project slugs. This PR resolves it.
This commit is contained in:
Daniel Hougaard
2024-09-05 13:30:20 +04:00
parent 302e068c74
commit b7d683ee1b
5 changed files with 84 additions and 44 deletions
@@ -459,17 +459,22 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
* Return list of projects for CircleCI integration * Return list of projects for CircleCI integration
*/ */
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
const res = ( // Fetch collaborations (v2 API)
await request.get<{ reponame: string }[]>(`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`, { const collaborations = (
headers: { await request.get<{ id: string; name: string; slug: string }[]>(
"Circle-Token": accessToken, `${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
"Accept-Encoding": "application/json" {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
} }
}) )
).data; ).data;
const apps = res?.map((a) => ({ const apps = collaborations.map((a) => ({
name: a?.reponame name: a.name,
appId: a.id
})); }));
return apps; return apps;
@@ -143,6 +143,12 @@ export type TBitbucketWorkspace = {
updated_on: string; updated_on: string;
}; };
export enum CircleCiVcsType {
GitHub = "GitHub",
CircleCI = "CircleCI",
BitBucket = "BitBucket"
}
export type TNorthflankSecretGroup = { export type TNorthflankSecretGroup = {
id: string; id: string;
name: string; name: string;
@@ -35,7 +35,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema"; import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { TIntegrationsWithEnvironment } from "./integration-auth-types"; import { CircleCiVcsType, TIntegrationsWithEnvironment } from "./integration-auth-types";
import { import {
IntegrationInitialSyncBehavior, IntegrationInitialSyncBehavior,
IntegrationMappingBehavior, IntegrationMappingBehavior,
@@ -1930,21 +1930,48 @@ const syncSecretsCircleCI = async ({
accessToken: string; accessToken: string;
}) => { }) => {
const circleciOrganizationDetail = ( const circleciOrganizationDetail = (
await request.get(`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, { await request.get<{ slug: string; name: string }[]>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, {
headers: { headers: {
"Circle-Token": accessToken, "Circle-Token": accessToken,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
} }
}) })
).data[0]; ).data;
const { slug } = circleciOrganizationDetail; let orgSlug: string | null = null;
if (!integration.owner) {
orgSlug = `${circleciOrganizationDetail[0].slug}/${integration.app}`;
} else {
const projectDetails = (
await request.get<{ vcs_info: { provider: CircleCiVcsType } }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.app}`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
}
)
).data;
const vcsProviderMap: Record<CircleCiVcsType, string> = {
[CircleCiVcsType.GitHub]: "gh",
[CircleCiVcsType.BitBucket]: "bb",
[CircleCiVcsType.CircleCI]: "circleci"
};
orgSlug = `${vcsProviderMap[projectDetails.vcs_info.provider]}/${integration.owner}/${integration.app}`;
if (!orgSlug) {
throw new Error("CircleCI: Organization not found");
}
}
// sync secrets to CircleCI // sync secrets to CircleCI
await Promise.all( await Promise.all(
Object.keys(secrets).map(async (key) => Object.keys(secrets).map(async (key) =>
request.post( request.post(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${orgSlug}/envvar`,
{ {
name: key, name: key,
value: secrets[key].value value: secrets[key].value
@@ -1962,7 +1989,7 @@ const syncSecretsCircleCI = async ({
// get secrets from CircleCI // get secrets from CircleCI
const getSecretsRes = ( const getSecretsRes = (
await request.get<{ items: { name: string }[] }>( await request.get<{ items: { name: string }[] }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, `${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${orgSlug}/envvar`,
{ {
headers: { headers: {
"Circle-Token": accessToken, "Circle-Token": accessToken,
@@ -1976,15 +2003,12 @@ const syncSecretsCircleCI = async ({
await Promise.all( await Promise.all(
getSecretsRes.map(async (sec) => { getSecretsRes.map(async (sec) => {
if (!(sec.name in secrets)) { if (!(sec.name in secrets)) {
return request.delete( return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${orgSlug}/envvar/${sec.name}`, {
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`, headers: {
{ "Circle-Token": accessToken,
headers: { "Content-Type": "application/json"
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
} }
); });
} }
}) })
); );
@@ -45,6 +45,7 @@ export default function CircleCICreateIntegrationPage() {
}); });
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetOrganization, setTargetOrganization] = useState("");
const [secretPath, setSecretPath] = useState("/"); const [secretPath, setSecretPath] = useState("/");
const [targetApp, setTargetApp] = useState(""); const [targetApp, setTargetApp] = useState("");
@@ -57,26 +58,19 @@ export default function CircleCICreateIntegrationPage() {
} }
}, [workspace]); }, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0]?.name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => { const handleButtonClick = async () => {
try { try {
if (!integrationAuth?.id) return; if (!integrationAuth?.id) return;
if (!targetApp || targetOrganization === "none") return;
setIsLoading(true); setIsLoading(true);
await mutateAsync({ await mutateAsync({
integrationAuthId: integrationAuth?.id, integrationAuthId: integrationAuth?.id,
isActive: true, isActive: true,
app: targetApp, app: targetApp,
owner: targetOrganization,
appId: integrationAuthApps?.find( appId: integrationAuthApps?.find(
(integrationAuthApp) => integrationAuthApp.name === targetApp (integrationAuthApp) => integrationAuthApp.name === targetApp
)?.appId, )?.appId,
@@ -92,11 +86,7 @@ export default function CircleCICreateIntegrationPage() {
} }
}; };
return integrationAuth && return integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps ? (
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full flex-col items-center justify-center"> <div className="flex h-full w-full flex-col items-center justify-center">
<Head> <Head>
<title>Set Up CircleCI Integration</title> <title>Set Up CircleCI Integration</title>
@@ -108,7 +98,7 @@ export default function CircleCICreateIntegrationPage() {
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI environment variables." subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI environment variables."
> >
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<div className="inline flex items-center pb-0.5"> <div className="flex items-center pb-0.5">
<Image <Image
src="/images/integrations/Circle CI.png" src="/images/integrations/Circle CI.png"
height={30} height={30}
@@ -131,6 +121,7 @@ export default function CircleCICreateIntegrationPage() {
</Link> </Link>
</div> </div>
</CardTitle> </CardTitle>
<FormControl label="Project Environment" className="px-6"> <FormControl label="Project Environment" className="px-6">
<Select <Select
value={selectedSourceEnvironment} value={selectedSourceEnvironment}
@@ -154,29 +145,42 @@ export default function CircleCICreateIntegrationPage() {
placeholder="Provide a path, default is /" placeholder="Provide a path, default is /"
/> />
</FormControl> </FormControl>
<FormControl label="CircleCI Project" className="px-6">
<FormControl label="CircleCI Organization" className="px-6">
<Select <Select
value={targetApp} value={targetOrganization}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => {
setTargetOrganization(val);
}}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0} isDisabled={integrationAuthApps.length === 0}
> >
{integrationAuthApps.length > 0 ? ( {integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => ( integrationAuthApps.map((integrationAuthApp) => (
<SelectItem <SelectItem
value={integrationAuthApp.name} value={integrationAuthApp.appId!}
key={`target-app-${integrationAuthApp.name}`} key={`target-org-${integrationAuthApp.owner}`}
> >
{integrationAuthApp.name} {integrationAuthApp.name}
</SelectItem> </SelectItem>
)) ))
) : ( ) : (
<SelectItem value="none" key="target-app-none"> <SelectItem value="none" key="target-app-none">
No projects found No organizations found
</SelectItem> </SelectItem>
)} )}
</Select> </Select>
</FormControl> </FormControl>
{targetOrganization && (
<FormControl label="CircleCI Project ID" className="px-6">
<Input
placeholder="55a2071d-97b5-4f71-b285-990d73c41268"
value={targetApp}
onChange={(evt) => setTargetApp(evt.target.value)}
/>
</FormControl>
)}
<Button <Button
onClick={handleButtonClick} onClick={handleButtonClick}
colorSchema="primary" colorSchema="primary"
@@ -99,6 +99,7 @@ export const ConfiguredIntegrationItem = ({
<FormLabel <FormLabel
label={ label={
(integration.integration === "qovery" && integration?.scope) || (integration.integration === "qovery" && integration?.scope) ||
(integration.integration === "circleci" && "Project ID") ||
(integration.integration === "aws-secret-manager" && "Secret") || (integration.integration === "aws-secret-manager" && "Secret") ||
(["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") || (["aws-parameter-store", "rundeck"].includes(integration.integration) && "Path") ||
(integration?.integration === "terraform-cloud" && "Project") || (integration?.integration === "terraform-cloud" && "Project") ||