import { useEffect, useMemo, useState } from "react"; import { Helmet } from "react-helmet"; import { Controller, useForm } from "react-hook-form"; import { faAngleDown, faArrowUpRightFromSquare, faBookOpen, faBugs, faCheckCircle, faCircleInfo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { useNavigate, useSearch } from "@tanstack/react-router"; import axios from "axios"; import { motion } from "framer-motion"; import { twMerge } from "tailwind-merge"; import { z, ZodIssueCode } from "zod"; import { createNotification } from "@app/components/notifications"; import { Button, Card, CardTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, FormControl, Input, Select, SelectItem, Switch, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { useWorkspace } from "@app/context"; import { useCreateIntegration, useGetIntegrationAuthApps, useGetIntegrationAuthById, useGetIntegrationAuthGithubEnvs, useGetIntegrationAuthGithubOrgs } from "@app/hooks/api"; enum TabSections { Connection = "connection", Options = "options" } const secretsVisibility = [ { value: "selected", label: "Select repositories" }, { value: "all", label: "All public repositories" }, { value: "private", label: "All private repositories" } ] as const; const targetEnv = ["github-repo", "github-org", "github-env"] as const; const schema = z .object({ selectedSourceEnvironment: z.string().trim(), secretPath: z.string().trim(), secretSuffix: z.string().trim().optional(), shouldEnableDelete: z.boolean().optional(), scope: z.enum(targetEnv), // Explanation: If scope is (github-repo) OR (github-org AND visibility is set to selected), then repoIds is required repoId: z.string().optional(), repoIds: z.string().array().optional(), repoName: z.string().optional(), repoOwner: z.string().optional(), envId: z.string().optional(), orgId: z.string().optional(), visibility: z.string().optional() }) .superRefine((data, ctx) => { if (data.scope === "github-env") { if (!data.repoId) { ctx.addIssue({ code: ZodIssueCode.custom, path: ["repoId"], message: "Repository is required" }); } if (!data.repoName) { ctx.addIssue({ code: ZodIssueCode.custom, path: ["repoName"], message: "Repository is required" }); } if (!data.repoOwner) { ctx.addIssue({ code: ZodIssueCode.custom, path: ["repoOwner"], message: "Repository is required" }); } if (!data.envId) { ctx.addIssue({ code: ZodIssueCode.custom, path: ["envId"], message: "Environment is required" }); } } if (data.scope === "github-org") { if (!data.orgId) { ctx.addIssue({ code: ZodIssueCode.custom, path: ["orgId"], message: "Organization is required" }); } if (!data.visibility) { ctx.addIssue({ code: ZodIssueCode.custom, path: ["visibility"], message: "Visibility is required" }); } } }); type FormData = z.infer; export const GithubConfigurePage = () => { const navigate = useNavigate(); const { mutateAsync } = useCreateIntegration(); const { currentWorkspace } = useWorkspace(); const integrationAuthId = useSearch({ from: ROUTE_PATHS.SecretManager.Integratons.GithubConfigurePage.id, select: (el) => el.integrationAuthId }); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId); const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = useGetIntegrationAuthApps({ integrationAuthId }); const { data: integrationAuthOrgs } = useGetIntegrationAuthGithubOrgs( integrationAuthId as string ); const { control, handleSubmit, watch, setValue } = useForm({ resolver: zodResolver(schema), defaultValues: { secretPath: "/", scope: "github-repo", repoIds: [], visibility: "all", shouldEnableDelete: false, selectedSourceEnvironment: currentWorkspace.environments[0].slug } }); const scope = watch("scope"); const repoId = watch("repoId"); const repoIds = watch("repoIds") || []; const repoName = watch("repoName"); const repoOwner = watch("repoOwner"); const selectedOrgId = watch("orgId"); const { data: integrationAuthGithubEnvs } = useGetIntegrationAuthGithubEnvs( integrationAuthId as string, repoName || "", repoOwner || "" ); const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (integrationAuthGithubEnvs && integrationAuthGithubEnvs?.length > 0) { setValue("envId", integrationAuthGithubEnvs[0].envId); } else { setValue("envId", undefined); } }, [integrationAuthGithubEnvs]); const onFormSubmit = async (data: FormData) => { try { setIsLoading(true); if (!integrationAuth?.id) return; switch (data.scope) { case "github-repo": { const targetApps = integrationAuthApps?.filter((integrationAuthApp) => data.repoIds?.includes(String(integrationAuthApp.appId)) ); if (!targetApps) return; 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, shouldEnableDelete: data.shouldEnableDelete } }); }) ); break; } case "github-org": await mutateAsync({ integrationAuthId: integrationAuth?.id, isActive: true, secretPath: data.secretPath, sourceEnvironment: data.selectedSourceEnvironment, scope: data.scope, owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name, metadata: { githubVisibility: data.visibility, githubVisibilityRepoIds: data.repoIds, secretSuffix: data.secretSuffix, shouldEnableDelete: data.shouldEnableDelete } }); 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, shouldEnableDelete: data.shouldEnableDelete } }); break; default: throw new Error("Invalid scope"); } setIsLoading(false); navigate({ to: "/secret-manager/$projectId/integrations", params: { projectId: currentWorkspace.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); } }; const selectedOrganization = useMemo(() => { if (!integrationAuthApps) return null; return integrationAuthApps.filter( (authApp) => integrationAuthOrgs?.find((e) => e.orgId === selectedOrgId)?.name === authApp.owner ); }, [selectedOrgId, integrationAuthApps]); return integrationAuth && integrationAuthApps ? (
Set Up GitHub Integration
GitHub logo
GitHub Integration
Docs
Connection Options
( )} /> ( )} /> ( )} /> {scope === "github-repo" && ( ( {integrationAuthApps.length > 0 ? (
{repoIds?.length === 1 ? integrationAuthApps?.reduce( (acc, { appId, name, owner }) => repoIds?.[0] === appId ? `${owner}/${name}` : acc, "" ) : `${repoIds?.length} repositories selected`}
) : (
No repositories found
)}
{integrationAuthApps.length > 0 ? ( integrationAuthApps.map((integrationAuthApp) => { const isSelected = repoIds?.includes( String(integrationAuthApp.appId) ); return ( { 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 ? ( ) : (
) } iconPos="left" className="w-[28.4rem] text-sm" > {integrationAuthApp.owner}/{integrationAuthApp.name} ); }) ) : (
)} )} /> )} {scope === "github-org" && ( <> ( )} /> ( )} /> {watch("visibility") === "selected" && ( ( {integrationAuthApps.length > 0 ? (
{repoIds?.length === 1 ? integrationAuthApps?.reduce( (acc, { appId, name, owner }) => repoIds?.[0] === appId ? `${owner}/${name}` : acc, "" ) : `${repoIds?.length} repositories selected`}
) : (
No repositories found
)}
{selectedOrganization ? ( selectedOrganization.map((integrationAuthApp) => { const isSelected = repoIds?.includes( String(integrationAuthApp.appId) ); return ( { 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 ? ( ) : (
) } iconPos="left" className="w-[28.4rem] text-sm" > {integrationAuthApp.owner}/{integrationAuthApp.name} ); }) ) : (
)} )} /> )} )} {scope === "github-env" && ( ( )} /> )} {scope === "github-env" && ( ( )} /> )}
( onChange(isChecked)} isChecked={value} > Delete secrets in Github that are not in Infisical )} />
( )} />
{" "} Pro Tip
After creating an integration, your secrets will start syncing immediately. This might cause an unexpected override of current secrets in GitHub with secrets from Infisical.
) : (
Set Up GitHub Integration {isIntegrationAuthAppsLoading ? ( infisical loading indicator ) : (

Something went wrong. Please contact{" "} support@infisical.com {" "} if the issue persists.

)}
); };