Merge pull request #1398 from Salman2301/feat-github-integration

Github Integrations
This commit is contained in:
BlackMagiq
2024-03-15 20:44:09 -07:00
committed by GitHub
15 changed files with 922 additions and 359 deletions

View File

@ -343,6 +343,66 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
}
});
server.route({
url: "/:integrationAuthId/github/orgs",
method: "GET",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
response: {
200: z.object({
orgs: z.object({ name: z.string(), orgId: z.string() }).array()
})
}
},
handler: async (req) => {
const orgs = await server.services.integrationAuth.getGithubOrgs({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId
});
if (!orgs) throw new Error("No organization found.");
return { orgs };
}
});
server.route({
url: "/:integrationAuthId/github/envs",
method: "GET",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
querystring: z.object({
repoOwner: z.string().trim(),
repoName: z.string().trim()
}),
response: {
200: z.object({
envs: z.object({ name: z.string(), envId: z.string() }).array()
})
}
},
handler: async (req) => {
const envs = await server.services.integrationAuth.getGithubEnvs({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
repoName: req.query.repoName,
repoOwner: req.query.repoOwner
});
if (!envs) throw new Error("No organization found.");
return { envs };
}
});
server.route({
url: "/:integrationAuthId/qovery/orgs",
method: "GET",

View File

@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { Octokit } from "@octokit/rest";
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@ -24,6 +25,8 @@ import {
TIntegrationAuthAppsDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthGithubEnvsDTO,
TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthHerokuPipelinesDTO,
TIntegrationAuthNorthflankSecretGroupDTO,
TIntegrationAuthQoveryEnvironmentsDTO,
@ -383,6 +386,72 @@ export const integrationAuthServiceFactory = ({
return [];
};
const getGithubOrgs = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthGithubOrgsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
const octokit = new Octokit({
auth: accessToken
});
const { data } = await octokit.request("GET /user/orgs", {
headers: {
"X-GitHub-Api-Version": "2022-11-28"
}
});
if (!data) return [];
return data.map(({ login: name, id: orgId }) => ({ name, orgId: String(orgId) }));
};
const getGithubEnvs = async ({
actorId,
actor,
actorOrgId,
id,
repoOwner,
repoName
}: TIntegrationAuthGithubEnvsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
const octokit = new Octokit({
auth: accessToken
});
const {
data: { environments }
} = await octokit.request("GET /repos/{owner}/{repo}/environments", {
headers: {
"X-GitHub-Api-Version": "2022-11-28"
},
owner: repoOwner,
repo: repoName
});
if (!environments) return [];
return environments.map(({ id: envId, name }) => ({ name, envId: String(envId) }));
};
const getQoveryOrgs = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthQoveryOrgsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@ -756,9 +825,7 @@ export const integrationAuthServiceFactory = ({
while (hasNextPage) {
// eslint-disable-next-line
const { data }: { data: { values: TBitbucketWorkspace[]; next: string } } = await request.get(
workspaceUrl,
{
const { data }: { data: { values: TBitbucketWorkspace[]; next: string } } = await request.get(workspaceUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
@ -934,6 +1001,8 @@ export const integrationAuthServiceFactory = ({
getIntegrationApps,
getVercelBranches,
getApps,
getGithubOrgs,
getGithubEnvs,
getChecklyGroups,
getQoveryApps,
getQoveryEnvs,

View File

@ -44,6 +44,16 @@ export type TIntegrationAuthChecklyGroupsDTO = {
accountId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthGithubOrgsDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthGithubEnvsDTO = {
id: string;
repoName: string;
repoOwner: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthQoveryOrgsDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;

View File

@ -1110,98 +1110,176 @@ const syncSecretsGitHub = async ({
interface GitHubRepoKey {
key_id: string;
key: string;
id?: number | undefined;
url?: string | undefined;
title?: string | undefined;
created_at?: string | undefined;
}
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
}
interface GitHubSecretRes {
[index: string]: GitHubSecret;
visibility?: "all" | "private" | "selected";
selected_repositories_url?: string | undefined;
}
const octokit = new Octokit({
auth: accessToken
});
// const user = (await octokit.request('GET /user', {})).data;
const repoPublicKey: GitHubRepoKey = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
owner: integration.owner as string,
repo: integration.app as string
})
).data;
enum GithubScope {
Repo = "github-repo",
Org = "github-org",
Env = "github-env"
}
let repoPublicKey: GitHubRepoKey;
switch (integration.scope) {
case GithubScope.Org: {
const { data } = await octokit.request("GET /orgs/{org}/actions/secrets/public-key", {
org: integration.owner as string
});
repoPublicKey = data;
break;
}
case GithubScope.Env: {
const { data } = await octokit.request(
"GET /repositories/{repository_id}/environments/{environment_name}/secrets/public-key",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string
}
);
repoPublicKey = data;
break;
}
default: {
const { data } = await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
owner: integration.owner as string,
repo: integration.app as string
});
repoPublicKey = data;
break;
}
}
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
let encryptedSecrets: GitHubSecretRes = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner as string,
repo: integration.app as string
})
).data.secrets.reduce(
(obj, secret) => ({
...obj,
[secret.name]: secret
}),
{}
);
let encryptedSecrets: GitHubSecret[];
encryptedSecrets = Object.keys(encryptedSecrets).reduce(
(
result: {
[key: string]: GitHubSecret;
},
key
) => {
if (
(appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) &&
(appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)
) {
result[key] = encryptedSecrets[key];
}
return result;
},
{}
);
await Promise.all(
Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) {
return octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
switch (integration.scope) {
case GithubScope.Org: {
encryptedSecrets = (
await octokit.request("GET /orgs/{org}/actions/secrets", {
org: integration.owner as string
})
).data.secrets;
break;
}
case GithubScope.Env: {
encryptedSecrets = (
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string
})
).data.secrets;
break;
}
default: {
encryptedSecrets = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: key
});
repo: integration.app as string
})
).data.secrets;
break;
}
}
for await (const encryptedSecret of encryptedSecrets) {
if (
!(encryptedSecret.name in secrets) &&
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
) {
switch (integration.scope) {
case GithubScope.Org: {
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: encryptedSecret.name
});
break;
}
case GithubScope.Env: {
await octokit.request(
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
secret_name: encryptedSecret.name
}
);
break;
}
default: {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: encryptedSecret.name
});
break;
}
}
})
);
}
}
await Promise.all(
Object.keys(secrets).map((key) => {
// let encryptedSecret;
return sodium.ready.then(async () => {
// convert secret & base64 key to Uint8Array.
const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL);
const binsec = sodium.from_string(secrets[key].value);
await sodium.ready.then(async () => {
for await (const key of Object.keys(secrets)) {
// convert secret & base64 key to Uint8Array.
const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL);
const binsec = sodium.from_string(secrets[key].value);
// encrypt secret using libsodium
const encBytes = sodium.crypto_box_seal(binsec, binkey);
// encrypt secret using libsodium
const encBytes = sodium.crypto_box_seal(binsec, binkey);
// convert encrypted Uint8Array to base64
const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
// convert encrypted Uint8Array to base64
const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
});
});
})
);
switch (integration.scope) {
case GithubScope.Org:
await octokit.request("PUT /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: key,
visibility: "all",
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
});
break;
case GithubScope.Env:
await octokit.request(
"PUT /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
}
);
break;
default:
await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
});
break;
}
}
});
};
/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 649 KiB

View File

@ -3,17 +3,14 @@ title: "GitHub Actions"
description: "How to sync secrets from Infisical to GitHub Actions"
---
Infisical lets you sync secrets to GitHub at the organization-level, repository-level, and repository environment-level.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Ensure that you have admin privileges to the repository you want to sync secrets to.
<Tabs>
<Tab title="Usage">
<Warning>
Infisical can sync secrets to GitHub repo secrets only. If your repo uses environment secrets, then stay tuned with this [issue](https://github.com/Infisical/infisical/issues/54).
</Warning>
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Ensure you have admin privileges to the repo you want to sync secrets to.
<Steps>
<Step title="Authorize Infisical for GitHub">
Navigate to your project's integrations tab in Infisical.
@ -29,12 +26,27 @@ description: "How to sync secrets from Infisical to GitHub Actions"
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
</Info>
</Step>
<Step title="Start integration">
Select which Infisical environment secrets you want to sync to which GitHub repo and press start integration to start syncing secrets to the repo.
<Step title="Configure Infisical GitHub integration">
Select which Infisical environment secrets you want to sync to which GitHub organization, repository, or repository environment.
<Tabs>
<Tab title="Repository">
![integrations github](../../images/integrations/github/integrations-github-scope-repo.png)
</Tab>
<Tab title="Organization">
![integrations github](../../images/integrations/github/integrations-github-scope-org.png)
</Tab>
<Tab title="Repository Environment">
![integrations github](../../images/integrations/github/integrations-github-scope-env.png)
</Tab>
</Tabs>
Finally, press create integration to start syncing secrets to GitHub.
![integrations github](../../images/integrations/github/integrations-github.png)
</Step>
</Steps>
</Tab>
<Tab title="Self-Hosted Setup">
Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub
@ -45,13 +57,13 @@ description: "How to sync secrets from Infisical to GitHub Actions"
![integrations github config](../../images/integrations/github/integrations-github-config-settings.png)
![integrations github config](../../images/integrations/github/integrations-github-config-dev-settings.png)
![integrations github config](../../images/integrations/github/integrations-github-config-new-app.png)
![integrations github config](../../images/integrations/github/integrations-github-config-new-app.png)
Create the OAuth application. As part of the form, set the **Homepage URL** to your self-hosted domain `https://your-domain.com`
and the **Authorization callback URL** to `https://your-domain.com/integrations/github/oauth2/callback`.
![integrations github config](../../images/integrations/github/integrations-github-config-new-app-form.png)
![integrations github config](../../images/integrations/github/integrations-github-config-new-app-form.png)
<Note>
If you have a GitHub organization, you can create an OAuth application under it
in your organization Settings > Developer settings > OAuth Apps > New Org OAuth App.
@ -59,17 +71,17 @@ description: "How to sync secrets from Infisical to GitHub Actions"
</Step>
<Step title="Add your OAuth application credentials to Infisical">
Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application.
![integrations github config](../../images/integrations/github/integrations-github-config-credentials.png)
![integrations github config](../../images/integrations/github/integrations-github-config-credentials.png)
Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application:
- `CLIENT_ID_GITHUB`: The **Client ID** of your GitHub OAuth application.
- `CLIENT_SECRET_GITHUB`: The **Client Secret** of your GitHub OAuth application.
Once added, restart your Infisical instance and use the GitHub integration.
</Step>
</Steps>
</Tab>
</Tabs>

View File

@ -6,6 +6,8 @@ export {
useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById,
useGetIntegrationAuthChecklyGroups,
useGetIntegrationAuthGithubEnvs,
useGetIntegrationAuthGithubOrgs,
useGetIntegrationAuthNorthflankSecretGroups,
useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices,

View File

@ -39,6 +39,10 @@ const integrationAuthKeys = {
integrationAuthId: string;
accountId: string;
}) => [{ integrationAuthId, accountId }, "integrationAuthChecklyGroups"] as const,
getIntegrationAuthGithubOrgs: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthGithubOrgs"] as const,
getIntegrationAuthGithubEnvs: (integrationAuthId: string, repoName: string, repoOwner: string) =>
[{ integrationAuthId, repoName, repoOwner }, "integrationAuthGithubOrgs"] as const,
getIntegrationAuthQoveryOrgs: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const,
getIntegrationAuthQoveryProjects: ({
@ -177,6 +181,32 @@ const fetchIntegrationAuthVercelBranches = async ({
return branches;
};
const fetchIntegrationAuthGithubOrgs = async (integrationAuthId: string) => {
const {
data: { orgs }
} = await apiRequest.get<{ orgs: Org[] }>(
`/api/v1/integration-auth/${integrationAuthId}/github/orgs`
);
return orgs;
};
const fetchIntegrationAuthGithubEnvs = async (
integrationAuthId: string,
repoName: string,
repoOwner: string
) => {
if (!repoName || !repoOwner) return [];
const {
data: { envs }
} = await apiRequest.get<{ envs: Array<{ name: string; envId: string }> }>(
`/api/v1/integration-auth/${integrationAuthId}/github/envs?repoName=${repoName}&repoOwner=${repoOwner}`
);
return envs;
};
const fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => {
const {
data: { orgs }
@ -301,8 +331,6 @@ const fetchIntegrationAuthHerokuPipelines = async ({ integrationAuthId }: {
`/api/v1/integration-auth/${integrationAuthId}/heroku/pipelines`
);
console.log(99999, pipelines)
return pipelines;
};
@ -482,6 +510,30 @@ export const useGetIntegrationAuthChecklyGroups = ({
});
};
export const useGetIntegrationAuthGithubOrgs = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthGithubOrgs(integrationAuthId),
queryFn: () => fetchIntegrationAuthGithubOrgs(integrationAuthId),
enabled: true
});
};
export const useGetIntegrationAuthGithubEnvs = (
integrationAuthId: string,
repoName: string,
repoOwner: string
) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthGithubEnvs(
integrationAuthId,
repoName,
repoOwner
),
queryFn: () => fetchIntegrationAuthGithubEnvs(integrationAuthId, repoName, repoOwner),
enabled: true
});
};
export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId),

View File

@ -11,22 +11,21 @@ export type TCloudIntegration = {
export type TIntegration = {
id: string;
projectId: string;
envId: string;
environment: { slug: string; name: string; id: string };
isActive: boolean;
url: any;
app: string;
appId: string;
targetEnvironment: string;
targetEnvironmentId: string;
targetService: string;
targetServiceId: string;
owner: string;
path: string;
region: string;
url?: string;
app?: string;
appId?: string;
targetEnvironment?: string;
targetEnvironmentId?: string;
targetService?: string;
targetServiceId?: string;
owner?: string;
path?: string;
region?: string;
scope?: string;
integration: string;
integrationAuth: string;
integrationAuthId: string;
envId: string;
secretPath: string;
createdAt: string;
updatedAt: string;
@ -45,4 +44,4 @@ export enum IntegrationSyncBehavior {
OVERWRITE_TARGET = "overwrite-target",
PREFER_TARGET = "prefer-target",
PREFER_SOURCE = "prefer-source"
}
}

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
@ -12,11 +13,14 @@ import {
faCircleInfo
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import axios from "axios";
import { motion } from "framer-motion";
import queryString from "query-string";
import { twMerge } from "tailwind-merge";
import * as yup from "yup";
import { useCreateIntegration } from "@app/hooks/api";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
Card,
@ -33,263 +37,547 @@ import {
TabList,
TabPanel,
Tabs
} from "../../../components/v2";
} from "@app/components/v2";
import {
useCreateIntegration,
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
useGetIntegrationAuthById,
useGetIntegrationAuthGithubEnvs,
useGetIntegrationAuthGithubOrgs,
useGetWorkspaceById
} from "@app/hooks/api";
enum TabSections {
Connection = "connection",
Options = "options"
}
const targetEnv = ["github-repo", "github-org", "github-env"] as const;
type TargetEnv = (typeof targetEnv)[number];
const schema = yup.object({
selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"),
secretPath: yup.string().trim().required("Secrets Path is required"),
secretSuffix: yup.string().trim().optional(),
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
repoIds: yup.mixed().when("scope", {
is: "github-repo",
then: yup.array(yup.string().required()).min(1, "Select at least one repositories")
}),
repoId: yup.mixed().when("scope", {
is: "github-env",
then: yup.string().required("Repository is required")
}),
repoName: yup.mixed().when("scope", {
is: "github-env",
then: yup.string().required("Repository is required")
}),
repoOwner: yup.mixed().when("scope", {
is: "github-env",
then: yup.string().required("Repository is required")
}),
envId: yup.mixed().when("scope", {
is: "github-env",
then: yup.string().required("Environment is required")
}),
orgId: yup.mixed().when("scope", {
is: "github-org",
then: yup.string().required("Organization is required")
})
});
type FormData = yup.InferType<typeof schema>;
export default function GitHubCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { createNotification } = useNotificationContext();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const integrationAuthId =
(queryString.parse(router.asPath.split("?")[1]).integrationAuthId as string) ?? "";
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId);
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
integrationAuthId
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [targetAppIds, setTargetAppIds] = useState<string[]>([]);
const [secretSuffix, setSecretSuffix] = useState("");
const { data: integrationAuthOrgs } = useGetIntegrationAuthGithubOrgs(
integrationAuthId as string
);
const { control, handleSubmit, watch, setValue } = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
secretPath: "/",
scope: "github-repo",
repoIds: []
}
});
const scope = watch("scope");
const repoId = watch("repoId");
const repoIds = watch("repoIds");
const repoName = watch("repoName");
const repoOwner = watch("repoOwner");
const { data: integrationAuthGithubEnvs } = useGetIntegrationAuthGithubEnvs(
integrationAuthId as string,
repoName,
repoOwner
);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setValue("selectedSourceEnvironment", workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetAppIds([String(integrationAuthApps[0].appId)]);
} else {
setTargetAppIds(["none"]);
}
if (integrationAuthGithubEnvs && integrationAuthGithubEnvs?.length > 0) {
setValue("envId", integrationAuthGithubEnvs[0].envId);
} else {
setValue("envId", undefined);
}
}, [integrationAuthApps]);
}, [integrationAuthGithubEnvs]);
const handleButtonClick = async () => {
const onFormSubmit = async (data: FormData) => {
try {
setIsLoading(true);
if (!integrationAuth?.id) return;
const targetApps = integrationAuthApps?.filter((integrationAuthApp) =>
targetAppIds.includes(String(integrationAuthApp.appId))
);
switch (data.scope) {
case "github-repo": {
const targetApps = integrationAuthApps?.filter((integrationAuthApp) =>
data.repoIds?.includes(String(integrationAuthApp.appId))
);
if (!targetApps) return;
if (!targetApps) return;
await Promise.all(
targetApps.map(async (targetApp) => {
await Promise.all(
targetApps.map(async (targetApp) => {
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
scope: data.scope,
secretPath: data.secretPath,
sourceEnvironment: data.selectedSourceEnvironment,
app: targetApp.name, // repo name
owner: targetApp.owner, // repo owner
metadata: {
secretSuffix: data.secretSuffix
}
});
})
);
break;
}
case "github-org":
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
app: targetApp.name,
sourceEnvironment: selectedSourceEnvironment,
owner: targetApp.owner,
secretPath,
secretPath: data.secretPath,
sourceEnvironment: data.selectedSourceEnvironment,
scope: data.scope,
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
metadata: {
secretSuffix
secretSuffix: data.secretSuffix
}
});
})
);
break;
case "github-env":
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
secretPath: data.secretPath,
sourceEnvironment: data.selectedSourceEnvironment,
scope: data.scope,
app: repoName,
appId: data.repoId,
owner: repoOwner,
targetEnvironmentId: data.envId,
metadata: {
secretSuffix: data.secretSuffix
}
});
break;
default:
throw new Error("Invalid scope");
}
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
let errorMessage: string = "Something went wrong!";
if (axios.isAxiosError(err)) {
const { message } = err?.response?.data as { message: string };
errorMessage = message;
}
createNotification({
text: errorMessage,
type: "error"
});
setIsLoading(false);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetAppIds ? (
<div className="flex h-full w-full flex-col items-center justify-center">
return integrationAuth && workspace && integrationAuthApps ? (
<div className="flex h-full w-full flex-col items-center justify-center py-4">
<Head>
<title>Set Up GitHub Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
<CardTitle
className="px-6 text-left text-xl"
subTitle="Choose which environment in Infisical you want to sync to environment variables in GitHub."
>
<div className="flex flex-row items-center">
<div className="inline flex items-center rounded-full bg-mineshaft-200">
<Image
src="/images/integrations/GitHub.png"
height={30}
width={30}
alt="GitHub logo"
/>
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6">
<CardTitle
className="px-0 text-left text-xl"
subTitle="Choose which environment in Infisical you want to sync to environment variables in GitHub."
>
<div className="flex flex-row items-center">
<div className="flex items-center rounded-full bg-mineshaft-200">
<Image
src="/images/integrations/GitHub.png"
height={30}
width={30}
alt="GitHub logo"
/>
</div>
<span className="ml-2.5">GitHub Integration </span>
<Link href="https://infisical.com/docs/integrations/cicd/githubactions" 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>
</div>
<span className="ml-2.5">GitHub Integration </span>
<Link href="https://infisical.com/docs/integrations/cicd/githubactions" 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>
</div>
</CardTitle>
<Tabs defaultValue={TabSections.Connection} className="px-6">
<TabList>
<div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Connection}>Connection</Tab>
<Tab value={TabSections.Options}>Options</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Connection}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<FormControl label="Project Environment">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
</CardTitle>
<Tabs defaultValue={TabSections.Connection}>
<TabList>
<div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Connection}>Connection</Tab>
<Tab value={TabSections.Options}>Options</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Connection}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<Controller
control={control}
name="selectedSourceEnvironment"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Project Environment"
errorText={error?.message}
isError={Boolean(error)}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GitHub Repo">
<DropdownMenu>
<DropdownMenuTrigger asChild>
{integrationAuthApps.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{targetAppIds.length === 1
? integrationAuthApps?.find(
(integrationAuthApp) =>
targetAppIds[0] === String(integrationAuthApp.appId)
)?.name
: `${targetAppIds.length} repositories selected`}
<FontAwesomeIcon icon={faAngleDown} className="text-xs" />
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No repositories found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80 overflow-y-scroll"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => {
const isSelected = targetAppIds.includes(String(integrationAuthApp.appId));
return (
<DropdownMenuItem
onClick={() => {
if (targetAppIds.includes(String(integrationAuthApp.appId))) {
setTargetAppIds(
targetAppIds.filter(
(appId) => appId !== String(integrationAuthApp.appId)
)
);
} else {
setTargetAppIds([
...targetAppIds,
String(integrationAuthApp.appId)
]);
}
}}
key={integrationAuthApp.appId}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
<Select
defaultValue={field.value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{integrationAuthApp.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: -30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<FormControl label="Append Secret Names with..." className="pb-[9.75rem]">
<Input
value={secretSuffix}
onChange={(evt) => setSecretSuffix(evt.target.value)}
placeholder="Provide a suffix for secret names, default is no suffix"
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</FormControl>
</motion.div>
</TabPanel>
</Tabs>
<Button
onClick={handleButtonClick}
color="mineshaft"
variant="outline_bg"
className="mb-6 ml-auto mr-6"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0 || targetAppIds.length === 0}
>
Create Integration
</Button>
<Controller
control={control}
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secrets Path"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="Provide a path, default is /" />
</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={onChange}
className="w-full border border-mineshaft-500"
>
<SelectItem value="github-org">Organization</SelectItem>
<SelectItem value="github-repo">Repository</SelectItem>
<SelectItem value="github-env">Repository Environment</SelectItem>
</Select>
</FormControl>
)}
/>
{scope === "github-repo" && (
<Controller
control={control}
name="repoIds"
render={({ field: { onChange }, fieldState: { error } }) => (
<FormControl
label="Repositories"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{integrationAuthApps.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{repoIds.length === 1
? integrationAuthApps?.reduce(
(acc, { appId, name, owner }) =>
repoIds[0] === appId ? `${owner}/${name}` : acc,
""
)
: `${repoIds.length} repositories selected`}
<FontAwesomeIcon icon={faAngleDown} className="text-xs" />
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No repositories found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80 overflow-y-scroll"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => {
const isSelected = repoIds.includes(
String(integrationAuthApp.appId)
);
return (
<DropdownMenuItem
onClick={() => {
if (repoIds.includes(String(integrationAuthApp.appId))) {
onChange(
repoIds.filter(
(appId: string) =>
appId !== String(integrationAuthApp.appId)
)
);
} else {
onChange([...repoIds, String(integrationAuthApp.appId)]);
}
}}
key={`repos-id-${integrationAuthApp.appId}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{integrationAuthApp.owner}/{integrationAuthApp.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
)}
{scope === "github-org" && (
<Controller
control={control}
name="orgId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Organization"
errorText={
integrationAuthOrgs?.length ? error?.message : "No organizations found"
}
isError={Boolean(integrationAuthOrgs?.length && error?.message)}
>
<Select
value={field.value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
>
{integrationAuthOrgs &&
integrationAuthOrgs.map(({ name, orgId }) => (
<SelectItem key={`github-organization-${orgId}`} value={orgId}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
{scope === "github-env" && (
<Controller
control={control}
name="repoId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Repository"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
value={field.value}
onValueChange={(e) => {
const selectedRepo = integrationAuthApps.find((app) => app.appId === e);
setValue("repoName", selectedRepo?.name);
setValue("repoOwner", selectedRepo?.owner);
onChange(e);
}}
className="w-full border border-mineshaft-500"
>
{integrationAuthApps?.length ? (
integrationAuthApps.map((app) => {
return (
<SelectItem
value={app.appId as string}
key={`repo-id-${app.appId}`}
className="w-[28.4rem] text-sm"
>
{app.owner}/{app.name}
</SelectItem>
);
})
) : (
<div />
)}
</Select>
</FormControl>
)}
/>
)}
{scope === "github-env" && (
<Controller
control={control}
name="envId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Environment"
errorText={
integrationAuthGithubEnvs?.length
? error?.message
: "No Environment found"
}
isError={Boolean(integrationAuthGithubEnvs?.length || error?.message)}
>
<Select
value={field.value}
onValueChange={onChange}
isDisabled={!repoId}
className={twMerge(
"w-full border border-mineshaft-500",
!repoId && "h-10 cursor-not-allowed"
)}
>
{integrationAuthGithubEnvs?.length ? (
integrationAuthGithubEnvs.map((githubEnv) => {
return (
<SelectItem
value={githubEnv.name as string}
key={`env-id-${githubEnv.envId}`}
className="w-[28.4rem] text-sm"
>
{githubEnv.name}
</SelectItem>
);
})
) : (
<div />
)}
</Select>
</FormControl>
)}
/>
)}
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: -30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<Controller
control={control}
name="secretSuffix"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Append Secret Names with..."
className="pb-[9.75rem]"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="Provide a suffix for secret names, default is no suffix"
/>
</FormControl>
)}
/>
</motion.div>
</TabPanel>
</Tabs>
<div className="flex w-full justify-end">
<Button
type="submit"
color="mineshaft"
variant="outline_bg"
className="mb-6"
isLoading={isLoading}
>
Create Integration
</Button>
</div>
</form>
</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">
@ -318,7 +606,7 @@ export default function GitHubCreateIntegrationPage() {
/>
) : (
<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" />
<FontAwesomeIcon icon={faBugs} className="li my-2 inline text-6xl" />
<p>
Something went wrong. Please contact{" "}
<a

View File

@ -60,7 +60,7 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case "github":
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case "gitlab":
link = `${window.location.origin}/integrations/gitlab/authorize`;

View File

@ -9,11 +9,8 @@ import {
AlertDescription,
DeleteActionModal,
EmptyState,
FormControl,
FormLabel,
IconButton,
Select,
SelectItem,
Skeleton,
Tooltip
} from "@app/components/v2";
@ -22,7 +19,7 @@ import { usePopUp } from "@app/hooks";
import { TIntegration } from "@app/hooks/api/types";
type Props = {
environments: Array<{ name: string; slug: string }>;
environments: Array<{ name: string; slug: string; id: string }>;
integrations?: TIntegration[];
isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
@ -80,29 +77,15 @@ export const IntegrationsSection = ({
<div className="flex flex-col space-y-4 p-6 pt-0">
{integrations?.map((integration) => (
<div
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3 pb-0"
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3"
key={`integration-${integration?.id.toString()}`}
>
<div className="flex">
<div>
<FormControl label="Environment">
<Select
value={integration.environment.slug}
isDisabled={integration.isActive}
className="min-w-[8rem] border border-mineshaft-700"
>
{environments.map((environment) => {
return (
<SelectItem
value={environment.slug}
key={`environment-${environment.slug}`}
>
{environment.name}
</SelectItem>
);
})}
</Select>
</FormControl>
<div className="ml-2 flex flex-col">
<FormLabel label="Environment" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{environments.find((e) => e.id === integration.envId)?.name || "-"}
</div>
</div>
<div className="ml-2 flex flex-col">
<FormLabel label="Secret Path" />
@ -142,11 +125,22 @@ export const IntegrationsSection = ({
</div>
)}
<div className="ml-2 flex flex-col">
<FormLabel label={integration?.metadata?.scope || "App"} />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.integration === "hashicorp-vault"
? `${integration.app} - path: ${integration.path}`
: integration.app}
<FormLabel
label={
(integration.integration === "qovery" && integration?.scope) ||
(integration?.scope === "github-org" && "Organization") ||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
"Repository") ||
"App"
}
/>
<div className="min-w-[8rem] max-w-[12rem] overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{(integration.integration === "hashicorp-vault" &&
`${integration.app} - path: ${integration.path}`) ||
(integration.scope === "github-org" && `${integration.owner}`) ||
(integration.scope?.startsWith("github-") &&
`${integration.owner}/${integration.app}`) ||
integration.app}
</div>
</div>
{(integration.integration === "vercel" ||
@ -154,32 +148,31 @@ export const IntegrationsSection = ({
integration.integration === "railway" ||
integration.integration === "gitlab" ||
integration.integration === "teamcity" ||
integration.integration === "bitbucket") && (
integration.integration === "bitbucket" ||
(integration.integration === "github" && integration.scope === "github-env")) && (
<div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" />
<div className="overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetEnvironment || integration.targetEnvironmentId}
</div>
</div>
)}
{integration.integration === "checkly" && integration.targetService && (
<div className="ml-2">
<FormLabel label="Group" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetEnvironment}
{integration.targetService}
</div>
</div>
)}
{(integration.integration === "checkly" ||
integration.integration === "github") && (
<>
{integration.targetService && (
<div className="ml-2">
<FormLabel label="Group" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetService}
</div>
</div>
)}
<div className="ml-2">
<FormLabel label="Secret Suffix" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration?.metadata?.secretSuffix || "-"}
</div>
<div className="ml-2">
<FormLabel label="Secret Suffix" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration?.metadata?.secretSuffix || "-"}
</div>
</>
</div>
)}
</div>
<div className="flex cursor-default items-center">
@ -214,7 +207,7 @@ export const IntegrationsSection = ({
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || " "}?`}
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={(popUp?.deleteConfirmation?.data as TIntegration)?.app || ""}
deleteKey={(popUp?.deleteConfirmation?.data as TIntegration)?.app || (popUp?.deleteConfirmation?.data as TIntegration)?.owner || ""}
onDeleteApproved={async () =>
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
handlePopUpClose("deleteConfirmation")