Merge pull request #4743 from Infisical/feat/adds-PAT-to-github-integration

feat: adds PAT to GitHub integration
This commit is contained in:
Piyush Gupta
2025-10-25 02:37:10 +05:30
committed by GitHub
15 changed files with 288 additions and 36 deletions

View File

@@ -345,6 +345,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case GitHubConnectionMethod.App:
case GitHubRadarConnectionMethod.App:
return "GitHub App";
case GitHubConnectionMethod.Pat:
return "Personal Access Token";
case AzureKeyVaultConnectionMethod.OAuth:
case AzureAppConfigurationConnectionMethod.OAuth:
case AzureClientSecretsConnectionMethod.OAuth:

View File

@@ -1,4 +1,5 @@
export enum GitHubConnectionMethod {
OAuth = "oauth",
App = "github-app"
App = "github-app",
Pat = "pat"
}

View File

@@ -248,10 +248,18 @@ export const makePaginatedGitHubRequest = async <T, R = T[]>(
): Promise<T[]> => {
const { credentials, method } = appConnection;
const token =
method === GitHubConnectionMethod.OAuth
? credentials.accessToken
: await getGitHubAppAuthToken(appConnection, gatewayService, gatewayV2Service);
let token: string;
switch (method) {
case GitHubConnectionMethod.OAuth:
token = credentials.accessToken;
break;
case GitHubConnectionMethod.Pat:
token = credentials.personalAccessToken;
break;
default:
token = await getGitHubAppAuthToken(appConnection, gatewayService, gatewayV2Service);
}
const baseUrl = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
const initialUrlObj = new URL(baseUrl);
@@ -460,6 +468,35 @@ export const validateGitHubConnectionCredentials = async (
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
) => {
const { credentials, method } = config;
// PAT validation
if (method === GitHubConnectionMethod.Pat) {
try {
const apiUrl = await getGitHubInstanceApiUrl(config);
await requestWithGitHubGateway(config, gatewayService, gatewayV2Service, {
url: `https://${apiUrl}/user`,
method: "GET",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${credentials.personalAccessToken}`,
"X-GitHub-Api-Version": "2022-11-28"
}
});
return {
personalAccessToken: credentials.personalAccessToken,
instanceType: credentials.instanceType,
host: credentials.host
};
} catch (e: unknown) {
logger.error(e, "Unable to verify GitHub PAT connection");
throw new BadRequestError({
message: "Unable to validate Personal Access Token: verify token has proper permissions"
});
}
}
const {
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,

View File

@@ -38,6 +38,19 @@ export const GitHubConnectionAppInputCredentialsSchema = z.union([
})
]);
export const GitHubConnectionPatInputCredentialsSchema = z.union([
z.object({
personalAccessToken: z.string().trim().min(1, "Personal Access Token required"),
instanceType: z.literal("server"),
host: z.string().trim().min(1, "Host is required for server instance type")
}),
z.object({
personalAccessToken: z.string().trim().min(1, "Personal Access Token required"),
instanceType: z.literal("cloud").optional(),
host: z.string().trim().optional()
})
]);
export const GitHubConnectionOAuthOutputCredentialsSchema = z.union([
z.object({
accessToken: z.string(),
@@ -64,6 +77,19 @@ export const GitHubConnectionAppOutputCredentialsSchema = z.union([
})
]);
export const GitHubConnectionPatOutputCredentialsSchema = z.union([
z.object({
personalAccessToken: z.string(),
instanceType: z.literal("server"),
host: z.string().trim().min(1)
}),
z.object({
personalAccessToken: z.string(),
instanceType: z.literal("cloud").optional(),
host: z.string().trim().optional()
})
]);
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
@@ -76,6 +102,12 @@ export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("m
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GitHub).credentials
)
}),
z.object({
method: z.literal(GitHubConnectionMethod.Pat).describe(AppConnections.CREATE(AppConnection.GitHub).method),
credentials: GitHubConnectionPatInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GitHub).credentials
)
})
]);
@@ -88,7 +120,11 @@ export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsS
export const UpdateGitHubConnectionSchema = z
.object({
credentials: z
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
.union([
GitHubConnectionAppInputCredentialsSchema,
GitHubConnectionOAuthInputCredentialsSchema,
GitHubConnectionPatInputCredentialsSchema
])
.optional()
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
})
@@ -110,6 +146,10 @@ export const GitHubConnectionSchema = z.intersection(
z.object({
method: z.literal(GitHubConnectionMethod.OAuth),
credentials: GitHubConnectionOAuthOutputCredentialsSchema
}),
z.object({
method: z.literal(GitHubConnectionMethod.Pat),
credentials: GitHubConnectionPatOutputCredentialsSchema
})
])
);
@@ -128,6 +168,13 @@ export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
host: z.string().optional()
})
}),
BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.Pat),
credentials: z.object({
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
host: z.string().optional()
})
})
]);

View File

@@ -211,10 +211,18 @@ export const GithubSyncFns = {
}
const { connection } = secretSync;
const token =
connection.method === GitHubConnectionMethod.OAuth
? connection.credentials.accessToken
: await getGitHubAppAuthToken(connection, gatewayService, gatewayV2Service);
let token: string;
switch (connection.method) {
case GitHubConnectionMethod.OAuth:
token = connection.credentials.accessToken;
break;
case GitHubConnectionMethod.Pat:
token = connection.credentials.personalAccessToken;
break;
default:
token = await getGitHubAppAuthToken(connection, gatewayService, gatewayV2Service);
}
const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService, gatewayV2Service);
const publicKey = await getPublicKey(secretSync, gatewayService, gatewayV2Service, token);
@@ -269,10 +277,18 @@ export const GithubSyncFns = {
const secretMap = Object.fromEntries(Object.entries(ogSecretMap).map(([i, v]) => [i.toUpperCase(), v]));
const { connection } = secretSync;
const token =
connection.method === GitHubConnectionMethod.OAuth
? connection.credentials.accessToken
: await getGitHubAppAuthToken(connection, gatewayService, gatewayV2Service);
let token: string;
switch (connection.method) {
case GitHubConnectionMethod.OAuth:
token = connection.credentials.accessToken;
break;
case GitHubConnectionMethod.Pat:
token = connection.credentials.personalAccessToken;
break;
default:
token = await getGitHubAppAuthToken(connection, gatewayService, gatewayV2Service);
}
const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService, gatewayV2Service);

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -3,7 +3,7 @@ title: "GitHub Connection"
description: "Learn how to configure a GitHub Connection for Infisical."
---
Infisical supports two methods for connecting to GitHub.
Infisical supports three methods for connecting to GitHub.
<Tabs>
<Tab title="GitHub App (Recommended)">
@@ -178,5 +178,72 @@ Infisical supports two methods for connecting to GitHub.
</Step>
</Steps>
</Tab>
<Tab title="Personal Access Token">
Infisical will use a Personal Access Token to connect to GitHub.
## Create a Personal Access Token
<Steps>
<Step title="Create a Personal Access Token in GitHub">
Navigate to your user Settings > Developer settings > Personal Access Tokens to create a new Personal Access Token.
![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-pat.png)
Click **Generate new token** to create the token.
</Step>
<Step title="Fill in the Personal Access Token details">
Fill in the Personal Access Token details:
- **Token name:** A descriptive name for the token (e.g., "infisical-connection-token")
- **Repository access:** Select the repositories you want to grant access to
- Select `All repositories` or `Only selected repositories` to be able to manage the secrets in the selected repositories.
- **Select scopes:** Add the following scopes:
- **Metadata**: Read-only
- **Environments**: Read and write
- **Secrets**: Read and write
![Personal Access Token Form](/images/app-connections/github/create-pat-form.png)
Click **Generate token** to create the token.
</Step>
<Step title="Copy the Personal Access Token">
Copy the generated token immediately as it won't be shown again.
![Personal Access Token Generated](/images/app-connections/github/create-pat-token-generated.png)
<Warning>
Keep your Personal Access Token secure and do not share it. Anyone with access to this token can access your GitHub account and repositories.
</Warning>
</Step>
</Steps>
## Setup GitHub Connection in Infisical
<Steps>
<Step title="Navigate to App Connections">
Navigate to the **App Connections** page in the desired project.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Add Connection">
Select the **GitHub Connection** option from the connection options modal.
![Select GitHub Connection](/images/app-connections/github/select-github-connection.png)
</Step>
<Step title="Authorize Connection">
Select the **Personal Access Token** method and fill in the **Personal Access Token** field with your Personal Access Token.
You may optionally configure GitHub Enterprise options:
- **Gateway:** The gateway connected to your private network
- **Hostname:** The hostname at which to access your GitHub Enterprise instance
Click **Create Connection**.
![Personal Access Token](/images/app-connections/github/create-pat-method.png)
</Step>
<Step title="Connection Created">
Your **GitHub Connection** is now available for use.
![Personal Access Token Connection](/images/app-connections/github/pat-connection.png)
</Step>
</Steps>
</Tab>
</Tabs>

View File

@@ -135,6 +135,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case GitHubConnectionMethod.App:
case GitHubRadarConnectionMethod.App:
return { name: "GitHub App", icon: faGithub };
case GitHubConnectionMethod.Pat:
return { name: "Personal Access Token", icon: faKey };
case AzureKeyVaultConnectionMethod.OAuth:
case AzureAppConfigurationConnectionMethod.OAuth:
case AzureClientSecretsConnectionMethod.OAuth:

View File

@@ -3,7 +3,8 @@ import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-con
export enum GitHubConnectionMethod {
App = "github-app",
OAuth = "oauth"
OAuth = "oauth",
Pat = "pat"
}
export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub } & (
@@ -24,4 +25,12 @@ export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub
host?: string;
};
}
| {
method: GitHubConnectionMethod.Pat;
credentials: {
personalAccessToken: string;
instanceType?: "cloud" | "server";
host?: string;
};
}
);

View File

@@ -96,7 +96,7 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
case AppConnection.AWS:
return <AwsConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm projectId={projectId} />;
return <GitHubConnectionForm projectId={projectId} onSubmit={onSubmit} />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm projectId={projectId} />;
case AppConnection.GCP:
@@ -213,7 +213,11 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.GitHub:
return (
<GitHubConnectionForm appConnection={appConnection} projectId={appConnection.projectId} />
<GitHubConnectionForm
appConnection={appConnection}
projectId={appConnection.projectId}
onSubmit={onSubmit}
/>
);
case AppConnection.GitHubRadar:
return (

View File

@@ -16,6 +16,7 @@ import {
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem,
Tooltip
@@ -48,28 +49,58 @@ import {
type Props = {
appConnection?: TGitHubConnection;
projectId: string | undefined | null;
onSubmit: (formData: PatSchemaForm) => Promise<void>;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.GitHub),
method: z.nativeEnum(GitHubConnectionMethod),
credentials: z
.union([
method: z.nativeEnum(GitHubConnectionMethod)
});
const baseCredentialsSchema = z.union([
z.object({
instanceType: z.literal("server"),
host: z.string().min(1, "Host is required for server instance type")
}),
z.object({
instanceType: z.literal("cloud").optional(),
host: z.string().optional()
}),
})
]);
const appSchema = rootSchema.extend({
method: z.literal(GitHubConnectionMethod.App),
credentials: baseCredentialsSchema
});
const oauthSchema = rootSchema.extend({
method: z.literal(GitHubConnectionMethod.OAuth),
credentials: baseCredentialsSchema
});
const patSchema = rootSchema.extend({
method: z.literal(GitHubConnectionMethod.Pat),
credentials: z.union([
z.object({
instanceType: z.literal("server"),
host: z.string().min(1, "Required")
host: z.string().min(1, "Host is required for server instance type"),
personalAccessToken: z.string().min(1, "Personal Access Token is required")
}),
z.object({
instanceType: z.literal("cloud").optional(),
host: z.string().optional(),
personalAccessToken: z.string().min(1, "Personal Access Token is required")
})
])
.optional()
});
type PatSchemaForm = z.infer<typeof patSchema>;
const formSchema = z.discriminatedUnion("method", [appSchema, oauthSchema, patSchema]);
type FormData = z.infer<typeof formSchema>;
export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
export const GitHubConnectionForm = ({ appConnection, projectId, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -106,7 +137,12 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
const returnUrl = useGetAppConnectionOauthReturnUrl();
const onSubmit = (formData: FormData) => {
const submitHandler = async (formData: FormData) => {
if (formData.method === GitHubConnectionMethod.Pat) {
await onSubmit(formData);
return;
}
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
@@ -151,15 +187,26 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
case GitHubConnectionMethod.App:
isMissingConfig = !appClientSlug;
break;
case GitHubConnectionMethod.Pat:
isMissingConfig = false;
break;
default:
throw new Error(`Unhandled GitHub Connection method: ${selectedMethod}`);
}
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
const getButtonText = () => {
if (selectedMethod === GitHubConnectionMethod.Pat) {
return isUpdate ? "Update Connection" : "Create Connection";
}
return isUpdate ? "Reconnect to GitHub" : "Connect to GitHub";
};
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(submitHandler)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
@@ -201,6 +248,26 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
</FormControl>
)}
/>
{selectedMethod === GitHubConnectionMethod.Pat && (
<Controller
name="credentials.personalAccessToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Personal Access Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
)}
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="enterprise-options" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
@@ -310,7 +377,7 @@ export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
isLoading={isSubmitting || isRedirecting}
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
>
{isUpdate ? "Reconnect to GitHub" : "Connect to GitHub"}
{getButtonText()}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">