mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-02 19:25:51 +00:00
Merge pull request #1398 from Salman2301/feat-github-integration
Github Integrations
This commit is contained in:
@@ -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({
|
server.route({
|
||||||
url: "/:integrationAuthId/qovery/orgs",
|
url: "/:integrationAuthId/qovery/orgs",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
|
||||||
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
@@ -24,6 +25,8 @@ import {
|
|||||||
TIntegrationAuthAppsDTO,
|
TIntegrationAuthAppsDTO,
|
||||||
TIntegrationAuthBitbucketWorkspaceDTO,
|
TIntegrationAuthBitbucketWorkspaceDTO,
|
||||||
TIntegrationAuthChecklyGroupsDTO,
|
TIntegrationAuthChecklyGroupsDTO,
|
||||||
|
TIntegrationAuthGithubEnvsDTO,
|
||||||
|
TIntegrationAuthGithubOrgsDTO,
|
||||||
TIntegrationAuthHerokuPipelinesDTO,
|
TIntegrationAuthHerokuPipelinesDTO,
|
||||||
TIntegrationAuthNorthflankSecretGroupDTO,
|
TIntegrationAuthNorthflankSecretGroupDTO,
|
||||||
TIntegrationAuthQoveryEnvironmentsDTO,
|
TIntegrationAuthQoveryEnvironmentsDTO,
|
||||||
@@ -383,6 +386,72 @@ export const integrationAuthServiceFactory = ({
|
|||||||
return [];
|
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 getQoveryOrgs = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthQoveryOrgsDTO) => {
|
||||||
const integrationAuth = await integrationAuthDAL.findById(id);
|
const integrationAuth = await integrationAuthDAL.findById(id);
|
||||||
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
|
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
|
||||||
@@ -756,9 +825,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
|
|
||||||
while (hasNextPage) {
|
while (hasNextPage) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const { data }: { data: { values: TBitbucketWorkspace[]; next: string } } = await request.get(
|
const { data }: { data: { values: TBitbucketWorkspace[]; next: string } } = await request.get(workspaceUrl, {
|
||||||
workspaceUrl,
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Accept-Encoding": "application/json"
|
"Accept-Encoding": "application/json"
|
||||||
@@ -934,6 +1001,8 @@ export const integrationAuthServiceFactory = ({
|
|||||||
getIntegrationApps,
|
getIntegrationApps,
|
||||||
getVercelBranches,
|
getVercelBranches,
|
||||||
getApps,
|
getApps,
|
||||||
|
getGithubOrgs,
|
||||||
|
getGithubEnvs,
|
||||||
getChecklyGroups,
|
getChecklyGroups,
|
||||||
getQoveryApps,
|
getQoveryApps,
|
||||||
getQoveryEnvs,
|
getQoveryEnvs,
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ export type TIntegrationAuthChecklyGroupsDTO = {
|
|||||||
accountId: string;
|
accountId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & 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 = {
|
export type TIntegrationAuthQoveryOrgsDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|||||||
@@ -1110,98 +1110,176 @@ const syncSecretsGitHub = async ({
|
|||||||
interface GitHubRepoKey {
|
interface GitHubRepoKey {
|
||||||
key_id: string;
|
key_id: string;
|
||||||
key: string;
|
key: string;
|
||||||
|
id?: number | undefined;
|
||||||
|
url?: string | undefined;
|
||||||
|
title?: string | undefined;
|
||||||
|
created_at?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GitHubSecret {
|
interface GitHubSecret {
|
||||||
name: string;
|
name: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
visibility?: "all" | "private" | "selected";
|
||||||
|
selected_repositories_url?: string | undefined;
|
||||||
interface GitHubSecretRes {
|
|
||||||
[index: string]: GitHubSecret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const octokit = new Octokit({
|
const octokit = new Octokit({
|
||||||
auth: accessToken
|
auth: accessToken
|
||||||
});
|
});
|
||||||
|
|
||||||
// const user = (await octokit.request('GET /user', {})).data;
|
enum GithubScope {
|
||||||
const repoPublicKey: GitHubRepoKey = (
|
Repo = "github-repo",
|
||||||
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
|
Org = "github-org",
|
||||||
owner: integration.owner as string,
|
Env = "github-env"
|
||||||
repo: integration.app as string
|
}
|
||||||
})
|
|
||||||
).data;
|
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
|
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
|
||||||
let encryptedSecrets: GitHubSecretRes = (
|
let encryptedSecrets: GitHubSecret[];
|
||||||
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
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
encryptedSecrets = Object.keys(encryptedSecrets).reduce(
|
switch (integration.scope) {
|
||||||
(
|
case GithubScope.Org: {
|
||||||
result: {
|
encryptedSecrets = (
|
||||||
[key: string]: GitHubSecret;
|
await octokit.request("GET /orgs/{org}/actions/secrets", {
|
||||||
},
|
org: integration.owner as string
|
||||||
key
|
})
|
||||||
) => {
|
).data.secrets;
|
||||||
if (
|
break;
|
||||||
(appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) &&
|
}
|
||||||
(appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true)
|
case GithubScope.Env: {
|
||||||
) {
|
encryptedSecrets = (
|
||||||
result[key] = encryptedSecrets[key];
|
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
|
||||||
}
|
repository_id: Number(integration.appId),
|
||||||
return result;
|
environment_name: integration.targetEnvironmentId as string
|
||||||
},
|
})
|
||||||
{}
|
).data.secrets;
|
||||||
);
|
break;
|
||||||
|
}
|
||||||
await Promise.all(
|
default: {
|
||||||
Object.keys(encryptedSecrets).map(async (key) => {
|
encryptedSecrets = (
|
||||||
if (!(key in secrets)) {
|
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
|
||||||
return octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
|
|
||||||
owner: integration.owner as string,
|
owner: integration.owner as string,
|
||||||
repo: integration.app as string,
|
repo: integration.app as string
|
||||||
secret_name: key
|
})
|
||||||
});
|
).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(
|
await sodium.ready.then(async () => {
|
||||||
Object.keys(secrets).map((key) => {
|
for await (const key of Object.keys(secrets)) {
|
||||||
// let encryptedSecret;
|
// convert secret & base64 key to Uint8Array.
|
||||||
return sodium.ready.then(async () => {
|
const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL);
|
||||||
// convert secret & base64 key to Uint8Array.
|
const binsec = sodium.from_string(secrets[key].value);
|
||||||
const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL);
|
|
||||||
const binsec = sodium.from_string(secrets[key].value);
|
|
||||||
|
|
||||||
// encrypt secret using libsodium
|
// encrypt secret using libsodium
|
||||||
const encBytes = sodium.crypto_box_seal(binsec, binkey);
|
const encBytes = sodium.crypto_box_seal(binsec, binkey);
|
||||||
|
|
||||||
// convert encrypted Uint8Array to base64
|
// convert encrypted Uint8Array to base64
|
||||||
const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
|
const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
|
||||||
|
|
||||||
await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
|
switch (integration.scope) {
|
||||||
owner: integration.owner as string,
|
case GithubScope.Org:
|
||||||
repo: integration.app as string,
|
await octokit.request("PUT /orgs/{org}/actions/secrets/{secret_name}", {
|
||||||
secret_name: key,
|
org: integration.owner as string,
|
||||||
encrypted_value: encryptedSecret,
|
secret_name: key,
|
||||||
key_id: repoPublicKey.key_id
|
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 |
@@ -3,17 +3,14 @@ title: "GitHub Actions"
|
|||||||
description: "How to sync secrets from Infisical to 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>
|
<Tabs>
|
||||||
<Tab title="Usage">
|
<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>
|
<Steps>
|
||||||
<Step title="Authorize Infisical for GitHub">
|
<Step title="Authorize Infisical for GitHub">
|
||||||
Navigate to your project's integrations tab in Infisical.
|
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.
|
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
|
||||||
</Info>
|
</Info>
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Start integration">
|
<Step title="Configure Infisical GitHub 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.
|
Select which Infisical environment secrets you want to sync to which GitHub organization, repository, or repository environment.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Repository">
|
||||||
|

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

|
||||||
|
</Tab>
|
||||||
|
<Tab title="Repository Environment">
|
||||||
|

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

|

|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Self-Hosted Setup">
|
<Tab title="Self-Hosted Setup">
|
||||||
Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub
|
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"
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
Create the OAuth application. As part of the form, set the **Homepage URL** to your self-hosted domain `https://your-domain.com`
|
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`.
|
and the **Authorization callback URL** to `https://your-domain.com/integrations/github/oauth2/callback`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
If you have a GitHub organization, you can create an OAuth application under it
|
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.
|
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>
|
||||||
<Step title="Add your OAuth application credentials to Infisical">
|
<Step title="Add your OAuth application credentials to Infisical">
|
||||||
Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application.
|
Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application:
|
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_ID_GITHUB`: The **Client ID** of your GitHub OAuth application.
|
||||||
- `CLIENT_SECRET_GITHUB`: The **Client Secret** 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.
|
Once added, restart your Infisical instance and use the GitHub integration.
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export {
|
|||||||
useGetIntegrationAuthBitBucketWorkspaces,
|
useGetIntegrationAuthBitBucketWorkspaces,
|
||||||
useGetIntegrationAuthById,
|
useGetIntegrationAuthById,
|
||||||
useGetIntegrationAuthChecklyGroups,
|
useGetIntegrationAuthChecklyGroups,
|
||||||
|
useGetIntegrationAuthGithubEnvs,
|
||||||
|
useGetIntegrationAuthGithubOrgs,
|
||||||
useGetIntegrationAuthNorthflankSecretGroups,
|
useGetIntegrationAuthNorthflankSecretGroups,
|
||||||
useGetIntegrationAuthRailwayEnvironments,
|
useGetIntegrationAuthRailwayEnvironments,
|
||||||
useGetIntegrationAuthRailwayServices,
|
useGetIntegrationAuthRailwayServices,
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const integrationAuthKeys = {
|
|||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
}) => [{ integrationAuthId, accountId }, "integrationAuthChecklyGroups"] as const,
|
}) => [{ 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) =>
|
getIntegrationAuthQoveryOrgs: (integrationAuthId: string) =>
|
||||||
[{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const,
|
[{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const,
|
||||||
getIntegrationAuthQoveryProjects: ({
|
getIntegrationAuthQoveryProjects: ({
|
||||||
@@ -177,6 +181,32 @@ const fetchIntegrationAuthVercelBranches = async ({
|
|||||||
return branches;
|
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 fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => {
|
||||||
const {
|
const {
|
||||||
data: { orgs }
|
data: { orgs }
|
||||||
@@ -301,8 +331,6 @@ const fetchIntegrationAuthHerokuPipelines = async ({ integrationAuthId }: {
|
|||||||
`/api/v1/integration-auth/${integrationAuthId}/heroku/pipelines`
|
`/api/v1/integration-auth/${integrationAuthId}/heroku/pipelines`
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(99999, pipelines)
|
|
||||||
|
|
||||||
return 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) => {
|
export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId),
|
queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId),
|
||||||
|
|||||||
@@ -11,22 +11,21 @@ export type TCloudIntegration = {
|
|||||||
|
|
||||||
export type TIntegration = {
|
export type TIntegration = {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
|
||||||
envId: string;
|
|
||||||
environment: { slug: string; name: string; id: string };
|
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
url: any;
|
url?: string;
|
||||||
app: string;
|
app?: string;
|
||||||
appId: string;
|
appId?: string;
|
||||||
targetEnvironment: string;
|
targetEnvironment?: string;
|
||||||
targetEnvironmentId: string;
|
targetEnvironmentId?: string;
|
||||||
targetService: string;
|
targetService?: string;
|
||||||
targetServiceId: string;
|
targetServiceId?: string;
|
||||||
owner: string;
|
owner?: string;
|
||||||
path: string;
|
path?: string;
|
||||||
region: string;
|
region?: string;
|
||||||
|
scope?: string;
|
||||||
integration: string;
|
integration: string;
|
||||||
integrationAuth: string;
|
integrationAuthId: string;
|
||||||
|
envId: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -45,4 +44,4 @@ export enum IntegrationSyncBehavior {
|
|||||||
OVERWRITE_TARGET = "overwrite-target",
|
OVERWRITE_TARGET = "overwrite-target",
|
||||||
PREFER_TARGET = "prefer-target",
|
PREFER_TARGET = "prefer-target",
|
||||||
PREFER_SOURCE = "prefer-source"
|
PREFER_SOURCE = "prefer-source"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -12,11 +13,14 @@ import {
|
|||||||
faCircleInfo
|
faCircleInfo
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import axios from "axios";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import queryString from "query-string";
|
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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -33,263 +37,547 @@ import {
|
|||||||
TabList,
|
TabList,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
Tabs
|
Tabs
|
||||||
} from "../../../components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
|
useCreateIntegration,
|
||||||
useGetIntegrationAuthApps,
|
useGetIntegrationAuthApps,
|
||||||
useGetIntegrationAuthById
|
useGetIntegrationAuthById,
|
||||||
} from "../../../hooks/api/integrationAuth";
|
useGetIntegrationAuthGithubEnvs,
|
||||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
useGetIntegrationAuthGithubOrgs,
|
||||||
|
useGetWorkspaceById
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
Connection = "connection",
|
Connection = "connection",
|
||||||
Options = "options"
|
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() {
|
export default function GitHubCreateIntegrationPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync } = useCreateIntegration();
|
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: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
||||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId);
|
||||||
|
|
||||||
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
||||||
useGetIntegrationAuthApps({
|
useGetIntegrationAuthApps({
|
||||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
integrationAuthId
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
const { data: integrationAuthOrgs } = useGetIntegrationAuthGithubOrgs(
|
||||||
const [secretPath, setSecretPath] = useState("/");
|
integrationAuthId as string
|
||||||
const [targetAppIds, setTargetAppIds] = useState<string[]>([]);
|
);
|
||||||
const [secretSuffix, setSecretSuffix] = useState("");
|
|
||||||
|
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);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
setValue("selectedSourceEnvironment", workspace.environments[0].slug);
|
||||||
}
|
}
|
||||||
}, [workspace]);
|
}, [workspace]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (integrationAuthApps) {
|
if (integrationAuthGithubEnvs && integrationAuthGithubEnvs?.length > 0) {
|
||||||
if (integrationAuthApps.length > 0) {
|
setValue("envId", integrationAuthGithubEnvs[0].envId);
|
||||||
setTargetAppIds([String(integrationAuthApps[0].appId)]);
|
} else {
|
||||||
} else {
|
setValue("envId", undefined);
|
||||||
setTargetAppIds(["none"]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [integrationAuthApps]);
|
}, [integrationAuthGithubEnvs]);
|
||||||
|
|
||||||
const handleButtonClick = async () => {
|
const onFormSubmit = async (data: FormData) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
if (!integrationAuth?.id) return;
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
const targetApps = integrationAuthApps?.filter((integrationAuthApp) =>
|
switch (data.scope) {
|
||||||
targetAppIds.includes(String(integrationAuthApp.appId))
|
case "github-repo": {
|
||||||
);
|
const targetApps = integrationAuthApps?.filter((integrationAuthApp) =>
|
||||||
|
data.repoIds?.includes(String(integrationAuthApp.appId))
|
||||||
|
);
|
||||||
|
|
||||||
if (!targetApps) return;
|
if (!targetApps) return;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
targetApps.map(async (targetApp) => {
|
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({
|
await mutateAsync({
|
||||||
integrationAuthId: integrationAuth?.id,
|
integrationAuthId: integrationAuth?.id,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
app: targetApp.name,
|
secretPath: data.secretPath,
|
||||||
sourceEnvironment: selectedSourceEnvironment,
|
sourceEnvironment: data.selectedSourceEnvironment,
|
||||||
owner: targetApp.owner,
|
scope: data.scope,
|
||||||
secretPath,
|
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
|
||||||
metadata: {
|
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);
|
setIsLoading(false);
|
||||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(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 &&
|
return integrationAuth && workspace && integrationAuthApps ? (
|
||||||
workspace &&
|
<div className="flex h-full w-full flex-col items-center justify-center py-4">
|
||||||
selectedSourceEnvironment &&
|
|
||||||
integrationAuthApps &&
|
|
||||||
targetAppIds ? (
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>Set Up GitHub Integration</title>
|
<title>Set Up GitHub Integration</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
|
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
|
||||||
<CardTitle
|
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6">
|
||||||
className="px-6 text-left text-xl"
|
<CardTitle
|
||||||
subTitle="Choose which environment in Infisical you want to sync to environment variables in GitHub."
|
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="inline flex items-center rounded-full bg-mineshaft-200">
|
<div className="flex flex-row items-center">
|
||||||
<Image
|
<div className="flex items-center rounded-full bg-mineshaft-200">
|
||||||
src="/images/integrations/GitHub.png"
|
<Image
|
||||||
height={30}
|
src="/images/integrations/GitHub.png"
|
||||||
width={30}
|
height={30}
|
||||||
alt="GitHub logo"
|
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>
|
</div>
|
||||||
<span className="ml-2.5">GitHub Integration </span>
|
</CardTitle>
|
||||||
<Link href="https://infisical.com/docs/integrations/cicd/githubactions" passHref>
|
<Tabs defaultValue={TabSections.Connection}>
|
||||||
<a target="_blank" rel="noopener noreferrer">
|
<TabList>
|
||||||
<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">
|
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
||||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
<Tab value={TabSections.Connection}>Connection</Tab>
|
||||||
Docs
|
<Tab value={TabSections.Options}>Options</Tab>
|
||||||
<FontAwesomeIcon
|
</div>
|
||||||
icon={faArrowUpRightFromSquare}
|
</TabList>
|
||||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
<TabPanel value={TabSections.Connection}>
|
||||||
/>
|
<motion.div
|
||||||
</div>
|
key="panel-1"
|
||||||
</a>
|
transition={{ duration: 0.15 }}
|
||||||
</Link>
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
</div>
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
</CardTitle>
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
<Tabs defaultValue={TabSections.Connection} className="px-6">
|
>
|
||||||
<TabList>
|
<Controller
|
||||||
<div className="flex w-full flex-row border-b border-mineshaft-600">
|
control={control}
|
||||||
<Tab value={TabSections.Connection}>Connection</Tab>
|
name="selectedSourceEnvironment"
|
||||||
<Tab value={TabSections.Options}>Options</Tab>
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
</div>
|
<FormControl
|
||||||
</TabList>
|
label="Project Environment"
|
||||||
<TabPanel value={TabSections.Connection}>
|
errorText={error?.message}
|
||||||
<motion.div
|
isError={Boolean(error)}
|
||||||
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}`}
|
|
||||||
>
|
>
|
||||||
{sourceEnvironment.name}
|
<Select
|
||||||
</SelectItem>
|
defaultValue={field.value}
|
||||||
))}
|
onValueChange={onChange}
|
||||||
</Select>
|
className="w-full border border-mineshaft-500"
|
||||||
</FormControl>
|
>
|
||||||
<FormControl label="Secrets Path">
|
{workspace?.environments.map((sourceEnvironment) => (
|
||||||
<Input
|
<SelectItem
|
||||||
value={secretPath}
|
value={sourceEnvironment.slug}
|
||||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{integrationAuthApp.name}
|
{sourceEnvironment.name}
|
||||||
</DropdownMenuItem>
|
</SelectItem>
|
||||||
);
|
))}
|
||||||
})
|
</Select>
|
||||||
) : (
|
</FormControl>
|
||||||
<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"
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
<Controller
|
||||||
</motion.div>
|
control={control}
|
||||||
</TabPanel>
|
name="secretPath"
|
||||||
</Tabs>
|
render={({ field, fieldState: { error } }) => (
|
||||||
<Button
|
<FormControl
|
||||||
onClick={handleButtonClick}
|
label="Secrets Path"
|
||||||
color="mineshaft"
|
errorText={error?.message}
|
||||||
variant="outline_bg"
|
isError={Boolean(error)}
|
||||||
className="mb-6 ml-auto mr-6"
|
>
|
||||||
isLoading={isLoading}
|
<Input {...field} placeholder="Provide a path, default is /" />
|
||||||
isDisabled={integrationAuthApps.length === 0 || targetAppIds.length === 0}
|
</FormControl>
|
||||||
>
|
)}
|
||||||
Create Integration
|
/>
|
||||||
</Button>
|
<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>
|
</Card>
|
||||||
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
<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="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">
|
<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>
|
<p>
|
||||||
Something went wrong. Please contact{" "}
|
Something went wrong. Please contact{" "}
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -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`;
|
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;
|
break;
|
||||||
case "github":
|
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;
|
break;
|
||||||
case "gitlab":
|
case "gitlab":
|
||||||
link = `${window.location.origin}/integrations/gitlab/authorize`;
|
link = `${window.location.origin}/integrations/gitlab/authorize`;
|
||||||
|
|||||||
+40
-47
@@ -9,11 +9,8 @@ import {
|
|||||||
AlertDescription,
|
AlertDescription,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
FormLabel,
|
||||||
IconButton,
|
IconButton,
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
@@ -22,7 +19,7 @@ import { usePopUp } from "@app/hooks";
|
|||||||
import { TIntegration } from "@app/hooks/api/types";
|
import { TIntegration } from "@app/hooks/api/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
environments: Array<{ name: string; slug: string }>;
|
environments: Array<{ name: string; slug: string; id: string }>;
|
||||||
integrations?: TIntegration[];
|
integrations?: TIntegration[];
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
|
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">
|
<div className="flex flex-col space-y-4 p-6 pt-0">
|
||||||
{integrations?.map((integration) => (
|
{integrations?.map((integration) => (
|
||||||
<div
|
<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()}`}
|
key={`integration-${integration?.id.toString()}`}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div>
|
<div className="ml-2 flex flex-col">
|
||||||
<FormControl label="Environment">
|
<FormLabel label="Environment" />
|
||||||
<Select
|
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||||
value={integration.environment.slug}
|
{environments.find((e) => e.id === integration.envId)?.name || "-"}
|
||||||
isDisabled={integration.isActive}
|
</div>
|
||||||
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>
|
</div>
|
||||||
<div className="ml-2 flex flex-col">
|
<div className="ml-2 flex flex-col">
|
||||||
<FormLabel label="Secret Path" />
|
<FormLabel label="Secret Path" />
|
||||||
@@ -142,11 +125,22 @@ export const IntegrationsSection = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="ml-2 flex flex-col">
|
<div className="ml-2 flex flex-col">
|
||||||
<FormLabel label={integration?.metadata?.scope || "App"} />
|
<FormLabel
|
||||||
<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">
|
label={
|
||||||
{integration.integration === "hashicorp-vault"
|
(integration.integration === "qovery" && integration?.scope) ||
|
||||||
? `${integration.app} - path: ${integration.path}`
|
(integration?.scope === "github-org" && "Organization") ||
|
||||||
: integration.app}
|
(["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>
|
||||||
</div>
|
</div>
|
||||||
{(integration.integration === "vercel" ||
|
{(integration.integration === "vercel" ||
|
||||||
@@ -154,32 +148,31 @@ export const IntegrationsSection = ({
|
|||||||
integration.integration === "railway" ||
|
integration.integration === "railway" ||
|
||||||
integration.integration === "gitlab" ||
|
integration.integration === "gitlab" ||
|
||||||
integration.integration === "teamcity" ||
|
integration.integration === "teamcity" ||
|
||||||
integration.integration === "bitbucket") && (
|
integration.integration === "bitbucket" ||
|
||||||
|
(integration.integration === "github" && integration.scope === "github-env")) && (
|
||||||
<div className="ml-4 flex flex-col">
|
<div className="ml-4 flex flex-col">
|
||||||
<FormLabel label="Target Environment" />
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(integration.integration === "checkly" ||
|
{(integration.integration === "checkly" ||
|
||||||
integration.integration === "github") && (
|
integration.integration === "github") && (
|
||||||
<>
|
<div className="ml-2">
|
||||||
{integration.targetService && (
|
<FormLabel label="Secret Suffix" />
|
||||||
<div className="ml-2">
|
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
|
||||||
<FormLabel label="Group" />
|
{integration?.metadata?.secretSuffix || "-"}
|
||||||
<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>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex cursor-default items-center">
|
<div className="flex cursor-default items-center">
|
||||||
@@ -214,7 +207,7 @@ export const IntegrationsSection = ({
|
|||||||
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
|
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
|
||||||
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || " "}?`}
|
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || " "}?`}
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
|
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 () =>
|
onDeleteApproved={async () =>
|
||||||
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
|
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
|
||||||
handlePopUpClose("deleteConfirmation")
|
handlePopUpClose("deleteConfirmation")
|
||||||
|
|||||||
Reference in New Issue
Block a user