diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index f4c25d54b..b91319a2f 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -547,6 +547,57 @@ export const getIntegrationAuthNorthflankSecretGroups = async (req: Request, res }); } +/** + * Return list of build configs for TeamCity project with id [appId] + * @param req + * @param res + * @returns + */ +export const getIntegrationAuthTeamCityBuildConfigs = async (req: Request, res: Response) => { + const appId = req.query.appId as string; + + interface TeamCityBuildConfig { + id: string; + name: string; + projectName: string; + projectId: string; + href: string; + webUrl: string; + } + + interface GetTeamCityBuildConfigsRes { + count: number; + href: string; + buildType: TeamCityBuildConfig[]; + } + + + if (appId && appId !== "") { + const { data: { buildType } } = ( + await standardRequest.get(`${req.integrationAuth.url}/app/rest/buildTypes`, { + params: { + locator: `project:${appId}` + }, + headers: { + Authorization: `Bearer ${req.accessToken}`, + Accept: "application/json", + }, + }) + ); + + return res.status(200).send({ + buildConfigs: buildType.map((buildConfig) => ({ + name: buildConfig.name, + buildConfigId: buildConfig.id + })) + }); + } + + return res.status(200).send({ + buildConfigs: [] + }); +} + /** * Delete integration authorization with id [integrationAuthId] * @param req diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index bf6cd9fc1..b22bd63fe 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -236,7 +236,7 @@ export const batchSecrets = async (req: Request, res: Response) => { version: 1 }, $unset: { - 'metadata.source': true as true + "metadata.source": true as const }, ...u, _id: new Types.ObjectId(u._id) diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 5e2887aa6..4b1604d50 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -9,7 +9,6 @@ import { INTEGRATION_VERCEL } from "../variables"; import { UnauthorizedRequestError } from "../utils/errors"; -import { syncSecretsToActiveIntegrationsQueue } from "../queues/integrations/syncSecretsToThirdPartyServices" interface Update { workspace: string; diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index af16e29bc..1eef252b1 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -850,7 +850,7 @@ const getAppsTeamCity = async ({ }, }) ).data.project.slice(1); - + const apps = res.map((a: any) => { return { name: a.name, diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index 5054df832..3201e6ca1 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -4,6 +4,8 @@ import { INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_BITBUCKET, INTEGRATION_BITBUCKET_TOKEN_URL, + INTEGRATION_GCP_SECRET_MANAGER, + INTEGRATION_GCP_TOKEN_URL, INTEGRATION_GITHUB, INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITLAB, @@ -13,21 +15,19 @@ import { INTEGRATION_NETLIFY, INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_VERCEL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_GCP_SECRET_MANAGER, - INTEGRATION_GCP_TOKEN_URL + INTEGRATION_VERCEL_TOKEN_URL } from "../variables"; import { - getClientIdGCPSecretManager, - getClientSecretGCPSecretManager, getClientIdAzure, getClientIdBitBucket, + getClientIdGCPSecretManager, getClientIdGitHub, getClientIdGitLab, getClientIdNetlify, getClientIdVercel, getClientSecretAzure, getClientSecretBitBucket, + getClientSecretGCPSecretManager, getClientSecretGitHub, getClientSecretGitLab, getClientSecretHeroku, diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 1950edd8d..1803a37bb 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -2185,7 +2185,7 @@ const syncSecretsTerraformCloud = async ({ }; /** - * Sync/push [secrets] to TeamCity project + * Sync/push [secrets] to TeamCity project (and optionally build config) * @param {Object} obj * @param {IIntegration} obj.integration - integration details * @param {Object} obj.secrets - secrets to push to integration @@ -2207,57 +2207,124 @@ const syncSecretsTeamCity = async ({ value: string; } - // get secrets from Teamcity - const res = ( - await standardRequest.get( - `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, + interface TeamCityBuildConfigParameter { + name: string; + value: string; + inherited: boolean; + } + interface GetTeamCityBuildConfigParametersRes { + href: string; + count: number; + property: TeamCityBuildConfigParameter[]; + } + + if (integration.targetEnvironment && integration.targetEnvironmentId) { + // case: sync to specific build-config in TeamCity project + const res = (await standardRequest.get( + `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, { headers: { Authorization: `Bearer ${accessToken}`, - Accept: "application/json" - } + Accept: "application/json", + }, } - ) - ).data.property.reduce((obj: any, secret: TeamCitySecret) => { - const secretName = secret.name.replace(/^env\./, ""); - return { - ...obj, - [secretName]: secret.value - }; - }, {}); - - for await (const key of Object.keys(secrets)) { - if (!(key in res) || (key in res && secrets[key] !== res[key])) { - // case: secret does not exist in TeamCity or secret value has changed - // -> create/update secret - await standardRequest.post( - `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, + )) + .data + .property + .filter((parameter) => !parameter.inherited) + .reduce((obj: any, secret: TeamCitySecret) => { + const secretName = secret.name.replace(/^env\./, ""); + return { + ...obj, + [secretName]: secret.value + }; + }, {}); + + for await (const key of Object.keys(secrets)) { + if (!(key in res) || (key in res && secrets[key].value !== res[key])) { + // case: secret does not exist in TeamCity or secret value has changed + // -> create/update secret + await standardRequest.post(`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, { - name: `env.${key}`, + name:`env.${key}`, value: secrets[key].value }, { headers: { Authorization: `Bearer ${accessToken}`, - Accept: "application/json" - } - } - ); + Accept: "application/json", + }, + }); + } } - } - for await (const key of Object.keys(res)) { - if (!(key in secrets)) { - // delete secret - await standardRequest.delete( - `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`, + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // delete secret + await standardRequest.delete( + `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters/env.${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } + } else { + // case: sync to TeamCity project + const res = ( + await standardRequest.get( + `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" } } - ); + ) + ).data.property.reduce((obj: any, secret: TeamCitySecret) => { + const secretName = secret.name.replace(/^env\./, ""); + return { + ...obj, + [secretName]: secret.value + }; + }, {}); + + for await (const key of Object.keys(secrets)) { + if (!(key in res) || (key in res && secrets[key] !== res[key])) { + // case: secret does not exist in TeamCity or secret value has changed + // -> create/update secret + await standardRequest.post( + `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters`, + { + name: `env.${key}`, + value: secrets[key].value + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } + } + + for await (const key of Object.keys(res)) { + if (!(key in secrets)) { + // delete secret + await standardRequest.delete( + `${integrationAuth.url}/app/rest/projects/id:${integration.appId}/parameters/env.${key}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } + } + ); + } } } }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 6b22c51b8..b5c467458 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -10,6 +10,7 @@ import { INTEGRATION_CODEFRESH, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, + INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, @@ -24,8 +25,7 @@ import { INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, - INTEGRATION_WINDMILL, - INTEGRATION_GCP_SECRET_MANAGER + INTEGRATION_WINDMILL } from "../variables"; import { Schema, Types, model } from "mongoose"; diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 1ad843f74..5b05c2242 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -12,6 +12,7 @@ import { INTEGRATION_CODEFRESH, INTEGRATION_DIGITAL_OCEAN_APP_PLATFORM, INTEGRATION_FLYIO, + INTEGRATION_GCP_SECRET_MANAGER, INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_HASHICORP_VAULT, @@ -26,8 +27,7 @@ import { INTEGRATION_TERRAFORM_CLOUD, INTEGRATION_TRAVISCI, INTEGRATION_VERCEL, - INTEGRATION_WINDMILL, - INTEGRATION_GCP_SECRET_MANAGER + INTEGRATION_WINDMILL } from "../variables"; import { Document, Schema, Types, model } from "mongoose"; diff --git a/backend/src/queues/secret-scanning/githubScanPushEvent.ts b/backend/src/queues/secret-scanning/githubScanPushEvent.ts index ec7e9e650..71a7e92d4 100644 --- a/backend/src/queues/secret-scanning/githubScanPushEvent.ts +++ b/backend/src/queues/secret-scanning/githubScanPushEvent.ts @@ -1,16 +1,16 @@ import Queue, { Job } from "bull"; import { ProbotOctokit } from "probot" -import { Commit, Committer, Repository } from "@octokit/webhooks-types"; +import { Commit } from "@octokit/webhooks-types"; import TelemetryService from "../../services/TelemetryService"; import { sendMail } from "../../helpers"; import GitRisks from "../../ee/models/gitRisks"; import { MembershipOrg, User } from "../../models"; -import { OWNER, ADMIN } from "../../variables"; +import { ADMIN, OWNER } from "../../variables"; import { convertKeysToLowercase, scanContentAndGetFindings } from "../../ee/services/GithubSecretScanning/helper"; import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config"; import { SecretMatch } from "../../ee/services/GithubSecretScanning/types"; -export const githubPushEventSecretScan = new Queue('github-push-event-secret-scanning', 'redis://redis:6379'); +export const githubPushEventSecretScan = new Queue("github-push-event-secret-scanning", "redis://redis:6379"); type TScanPushEventQueueDetails = { organizationId: string, diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index c3de5c3b0..dc97e66c0 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -89,4 +89,4 @@ router.post( integrationController.manualSync ); -export default router; +export default router; \ No newline at end of file diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index b8b7f348d..edc7314cc 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -168,6 +168,20 @@ router.get( integrationAuthController.getIntegrationAuthNorthflankSecretGroups ); +router.get( + "/:integrationAuthId/teamcity/build-configs", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT], + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param("integrationAuthId").exists().isString(), + query("appId").exists().isString(), + validateRequest, + integrationAuthController.getIntegrationAuthTeamCityBuildConfigs +); + router.delete( "/:integrationAuthId", requireAuth({ diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 89f4162b2..8f8fac9c4 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -1,4 +1,4 @@ -import express, { Request, Response } from "express"; +import express from "express"; const router = express.Router(); import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; import { body, param, query } from "express-validator"; diff --git a/docs/images/integrations/teamcity/integrations-teamcity-auth.png b/docs/images/integrations/teamcity/integrations-teamcity-auth.png new file mode 100644 index 000000000..6f555a052 Binary files /dev/null and b/docs/images/integrations/teamcity/integrations-teamcity-auth.png differ diff --git a/docs/images/integrations/teamcity/integrations-teamcity-create.png b/docs/images/integrations/teamcity/integrations-teamcity-create.png new file mode 100644 index 000000000..d851863f2 Binary files /dev/null and b/docs/images/integrations/teamcity/integrations-teamcity-create.png differ diff --git a/docs/images/integrations/teamcity/integrations-teamcity-dashboard.png b/docs/images/integrations/teamcity/integrations-teamcity-dashboard.png new file mode 100644 index 000000000..e519be110 Binary files /dev/null and b/docs/images/integrations/teamcity/integrations-teamcity-dashboard.png differ diff --git a/docs/images/integrations/teamcity/integrations-teamcity-token.png b/docs/images/integrations/teamcity/integrations-teamcity-token.png new file mode 100644 index 000000000..caf3896d6 Binary files /dev/null and b/docs/images/integrations/teamcity/integrations-teamcity-token.png differ diff --git a/docs/images/integrations/teamcity/integrations-teamcity.png b/docs/images/integrations/teamcity/integrations-teamcity.png new file mode 100644 index 000000000..5caa2110c Binary files /dev/null and b/docs/images/integrations/teamcity/integrations-teamcity.png differ diff --git a/docs/integrations/cloud/teamcity.mdx b/docs/integrations/cloud/teamcity.mdx index 90661a790..2ade11a66 100644 --- a/docs/integrations/cloud/teamcity.mdx +++ b/docs/integrations/cloud/teamcity.mdx @@ -11,21 +11,18 @@ Prerequisites: ![integrations](../../images/integrations.png) -## Enter your TeamCity API Token and Server URL +## Enter your TeamCity Access Token and Server URL -Obtain a TeamCity API Token in Profile > Access Tokens +Obtain a TeamCity Access Token in Profile > Access Tokens -![integrations teamcity dashboard](../../images/integrations-teamcity-dashboard.png) -![integrations teamcity tokens](../../images/integrations-teamcity-tokens.png) +![integrations teamcity dashboard](../../images/integrations/teamcity/integrations-teamcity-dashboard.png) +![integrations teamcity token](../../images/integrations/teamcity/integrations-teamcity-token.png) -Obtain your TeamCity Server URL in Administration > Cloud Server Settings > Server URL - -![integrations teamcity projects](../../images/integrations-teamcity-projects.png) -![integrations teamcity server url](../../images/integrations-teamcity-serverurl.png) - -Press on the TeamCity tile and input your TeamCity API Token and Server URL to grant Infisical access to your TeamCity account. - -![integrations teamcity authorization](../../images/integrations-teamcity-auth.png) + + For this integration to work, the TeamCity Access Token must either have the + **Same as current user** account-wide permission enabled or, if **Limit per project** + is selected, then it must at minimum have the **View build configuration settings** and **Edit project** permissions enabled. + If this is your project's first cloud integration, then you'll have to grant @@ -34,9 +31,20 @@ Press on the TeamCity tile and input your TeamCity API Token and Server URL to g the cloud platform. +Press on the TeamCity tile and input your TeamCity Access Token and Server URL to grant Infisical access to your TeamCity account. + +![integrations teamcity authorization](../../images/integrations/teamcity/integrations-teamcity-auth.png) + ## Start integration -Select which Infisical environment secrets, you want to sync to which TeamCity project and press create integration to start syncing secrets to TeamCity. +Select which Infisical environment secrets you want to sync to which TeamCity project (and optionally build configuration) and press create integration to start syncing secrets to TeamCity. -![integrations teamcity](../../images/integrations-teamcity-create.png) -![integrations teamcity](../../images/integrations-teamcity.png) +![integrations teamcity](../../images/integrations/teamcity/integrations-teamcity-create.png) + + + Infisical integrates with both TeamCity's project-level and build configuration-level environment variables. + + To sync secrets to a specific build configuration in a TeamCity project, you can select a build configuration from the **TeamCity Build Config** dropdown; otherwise, leaving it empty will sync secrets to TeamCity at the project-level. + + +![integrations teamcity](../../images/integrations/teamcity/integrations-teamcity.png) diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index d6e1b11e3..7e7c355ca 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -7,6 +7,7 @@ export { useGetIntegrationAuthNorthflankSecretGroups, useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayServices, + useGetIntegrationAuthTeamCityBuildConfigs, useGetIntegrationAuthTeams, useGetIntegrationAuthVercelBranches, useSaveIntegrationAccessToken diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 1ec08d839..ec0c785f2 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -10,8 +10,8 @@ import { IntegrationAuth, NorthflankSecretGroup, Service, - Team -} from "./types"; + Team, + TeamCityBuildConfig} from "./types"; const integrationAuthKeys = { getIntegrationAuthById: (integrationAuthId: string) => @@ -49,7 +49,14 @@ const integrationAuthKeys = { }: { integrationAuthId: string; appId: string; - }) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const, + }) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const, + getIntegrationAuthTeamCityBuildConfigs: ({ + integrationAuthId, + appId + }: { + integrationAuthId: string; + appId: string; + }) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const, }; const fetchIntegrationAuthById = async (integrationAuthId: string) => { @@ -183,6 +190,27 @@ const fetchIntegrationAuthNorthflankSecretGroups = async ({ return secretGroups; }; +const fetchIntegrationAuthTeamCityBuildConfigs = async ({ + integrationAuthId, + appId +}: { + integrationAuthId: string; + appId: string; +}) => { + const { + data: { buildConfigs } + } = await apiRequest.get<{ buildConfigs: TeamCityBuildConfig[] }>( + `/api/v1/integration-auth/${integrationAuthId}/teamcity/build-configs`, + { + params: { + appId + } + } + ); + + return buildConfigs; +}; + export const useGetIntegrationAuthById = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), @@ -312,6 +340,26 @@ export const useGetIntegrationAuthNorthflankSecretGroups = ({ }); }; +export const useGetIntegrationAuthTeamCityBuildConfigs = ({ + integrationAuthId, + appId +}: { + integrationAuthId: string; + appId: string; +}) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthTeamCityBuildConfigs({ + integrationAuthId, + appId + }), + queryFn: () => fetchIntegrationAuthTeamCityBuildConfigs({ + integrationAuthId, + appId + }), + enabled: true + }); +}; + export const useAuthorizeIntegration = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index 1214600e8..47f0fcfc9 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -40,4 +40,9 @@ export type BitBucketWorkspace = { export type NorthflankSecretGroup = { name: string; groupId: string; +} + +export type TeamCityBuildConfig = { + name: string; + buildConfigId: string; } \ No newline at end of file diff --git a/frontend/src/pages/integrations/teamcity/create.tsx b/frontend/src/pages/integrations/teamcity/create.tsx index a6282af92..e2a2b2d58 100644 --- a/frontend/src/pages/integrations/teamcity/create.tsx +++ b/frontend/src/pages/integrations/teamcity/create.tsx @@ -17,13 +17,20 @@ import { } from "../../../components/v2"; import { useGetIntegrationAuthApps, - useGetIntegrationAuthById + useGetIntegrationAuthById, + useGetIntegrationAuthTeamCityBuildConfigs } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; export default function TeamCityCreateIntegrationPage() { const router = useRouter(); const { mutateAsync } = useCreateIntegration(); + + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); + const [targetAppId, setTargetAppId] = useState(""); + const [targetBuildConfigId, setTargetBuildConfigId] = useState(""); + const [secretPath, setSecretPath] = useState("/"); + const [isLoading, setIsLoading] = useState(false); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); @@ -32,11 +39,11 @@ export default function TeamCityCreateIntegrationPage() { const { data: integrationAuthApps } = useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "" }); - - const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); - const [targetApp, setTargetApp] = useState(""); - const [secretPath, setSecretPath] = useState("/"); - const [isLoading, setIsLoading] = useState(false); + + const { data: targetBuildConfigs } = useGetIntegrationAuthTeamCityBuildConfigs({ + integrationAuthId: (integrationAuthId as string) ?? "", + appId: targetAppId + }); useEffect(() => { if (workspace) { @@ -47,29 +54,31 @@ export default function TeamCityCreateIntegrationPage() { useEffect(() => { if (integrationAuthApps) { if (integrationAuthApps.length > 0) { - setTargetApp(integrationAuthApps[0].name); + setTargetAppId(integrationAuthApps[0].appId as string); } else { - setTargetApp("none"); + setTargetAppId("none"); } } }, [integrationAuthApps]); - + const handleButtonClick = async () => { try { if (!integrationAuth?._id) return; setIsLoading(true); - + + const targetEnvironment = targetBuildConfigs?.find( + (buildConfig) => buildConfig.buildConfigId === targetBuildConfigId + ); + await mutateAsync({ integrationAuthId: integrationAuth?._id, isActive: true, - app: targetApp, - appId: - integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp) - ?.appId ?? null, + app: integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId)?.name ?? null, + appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, - targetEnvironment: null, - targetEnvironmentId: null, + targetEnvironment: targetEnvironment ? targetEnvironment.name : null, + targetEnvironmentId: targetEnvironment ? targetEnvironment.buildConfigId : null, targetService: null, targetServiceId: null, owner: null, @@ -86,12 +95,17 @@ export default function TeamCityCreateIntegrationPage() { } }; + const filteredBuildConfigs = targetBuildConfigs?.concat({ + name: "", + buildConfigId: "" + }); return integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && - targetApp ? ( + filteredBuildConfigs && + targetAppId ? (
TeamCity Integration @@ -120,16 +134,16 @@ export default function TeamCityCreateIntegrationPage() { + + +