Addressed PR suggestions

This commit is contained in:
Carlos Monastyrski
2025-07-21 11:01:22 -03:00
parent d44b3293b6
commit ce877cd352
6 changed files with 126 additions and 241 deletions

View File

@@ -51,7 +51,7 @@ export type TBitbucketEnvironment = {
name: string; name: string;
}; };
export type BitbucketEnvironmentsResponse = { export type TBitbucketEnvironmentsResponse = {
values: TBitbucketEnvironment[]; values: TBitbucketEnvironment[];
next?: string; next?: string;
}; };

View File

@@ -11,145 +11,85 @@ import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
const listBitbucketSecrets = async ({ email, apiToken, workspace, repository }: TBitbucketListVariables) => { const createAuthHeader = (email: string, apiToken: string): string => {
const { data } = await request.get<{ values: TBitbucketVariable[] }>( return `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`;
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/pipelines_config/variables/`,
{
headers: {
Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`,
Accept: "application/json"
}
}
);
return data.values;
}; };
const listBitbucketEnvironmentSecrets = async ({ const buildVariablesUrl = (workspace: string, repository: string, environment?: string, uuid?: string): string => {
const baseUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}`;
if (environment) {
return `${baseUrl}/deployments_config/environments/${environment}/variables${uuid ? `/${uuid}` : ""}`;
}
return `${baseUrl}/pipelines_config/variables/${uuid || ""}`;
};
const listVariables = async ({
email, email,
apiToken, apiToken,
workspace, workspace,
repository, repository,
environment environment
}: TBitbucketListVariables & { environment: string }) => { }: TBitbucketListVariables & { environment?: string }): Promise<TBitbucketVariable[]> => {
const { data } = await request.get<{ values: TBitbucketVariable[] }>( const url = buildVariablesUrl(workspace, repository, environment);
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/deployments_config/environments/${environment}/variables`, const authHeader = createAuthHeader(email, apiToken);
{
headers: { const { data } = await request.get<{ values: TBitbucketVariable[] }>(url, {
Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`, headers: {
Accept: "application/json" Authorization: authHeader,
} Accept: "application/json"
} }
); });
return data.values; return data.values;
}; };
// Helper function to upsert a single variable const upsertVariable = async ({
const upsertBitbucketVariable = async ({
email, email,
apiToken, apiToken,
workspace, workspace,
repository, repository,
environment,
key, key,
value, value,
existingVariables, existingVariables
isEnvironment = false,
environment
}: { }: {
email: string; email: string;
apiToken: string; apiToken: string;
workspace: string; workspace: string;
repository: string; repository: string;
environment?: string;
key: string; key: string;
value: string; value: string;
existingVariables: TBitbucketVariable[]; existingVariables: TBitbucketVariable[];
isEnvironment?: boolean;
environment?: string;
}) => { }) => {
const existingVariable = existingVariables.find((variable) => variable.key === key); const existingVariable = existingVariables.find((variable) => variable.key === key);
const auth = `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`; const authHeader = createAuthHeader(email, apiToken);
const requestData = { key, value, secured: true };
const headers = {
Authorization: authHeader,
"Content-Type": "application/json"
};
if (existingVariable) { if (existingVariable) {
// Variable exists, use PUT to update it const url = buildVariablesUrl(workspace, repository, environment, existingVariable.uuid);
const baseUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}`; return request.put(url, requestData, { headers });
const url = isEnvironment
? `${baseUrl}/deployments_config/environments/${environment}/variables/${existingVariable.uuid}`
: `${baseUrl}/pipelines_config/variables/${existingVariable.uuid}`;
return request.put(
url,
{
key,
value,
secured: true
},
{
headers: {
Authorization: auth,
"Content-Type": "application/json"
}
}
);
} }
const baseUrl = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}`; const url = buildVariablesUrl(workspace, repository, environment);
const url = isEnvironment return request.post(url, requestData, { headers });
? `${baseUrl}/deployments_config/environments/${environment}/variables`
: `${baseUrl}/pipelines_config/variables/`;
return request.post(
url,
{
key,
value,
secured: true
},
{
headers: {
Authorization: auth,
"Content-Type": "application/json"
}
}
);
}; };
const putBitbucketSecrets = async ({ const putVariables = async ({
email,
apiToken,
workspace,
repository,
secretMap
}: TPutBitbucketVariable & { secretMap: TSecretMap }) => {
// Get existing variables first
const existingVariables = await listBitbucketSecrets({ email, apiToken, workspace, repository });
const promises = Object.entries(secretMap).map(([key, { value }]) => {
return upsertBitbucketVariable({
email,
apiToken,
workspace,
repository,
key,
value,
existingVariables,
isEnvironment: false
});
});
return Promise.all(promises);
};
const putBitbucketEnvironmentSecrets = async ({
email, email,
apiToken, apiToken,
workspace, workspace,
repository, repository,
environment, environment,
secretMap secretMap
}: TPutBitbucketVariable & { environment: string; secretMap: TSecretMap }) => { }: TPutBitbucketVariable & { environment?: string; secretMap: TSecretMap }) => {
// Get existing variables first const existingVariables = await listVariables({
const existingVariables = await listBitbucketEnvironmentSecrets({
email, email,
apiToken, apiToken,
workspace, workspace,
@@ -157,69 +97,46 @@ const putBitbucketEnvironmentSecrets = async ({
environment environment
}); });
const promises = Object.entries(secretMap).map(([key, { value }]) => { const promises = Object.entries(secretMap).map(([key, { value }]) =>
return upsertBitbucketVariable({ upsertVariable({
email, email,
apiToken, apiToken,
workspace, workspace,
repository, repository,
environment,
key, key,
value, value,
existingVariables, existingVariables
isEnvironment: true, })
environment );
});
});
return Promise.all(promises); return Promise.all(promises);
}; };
const deleteBitbucketSecrets = async ({ email, apiToken, workspace, repository, keys }: TDeleteBitbucketVariable) => { const deleteVariables = async ({
// First, we need to get the variable UUIDs since Bitbucket requires UUIDs for deletion
const existingVariables = await listBitbucketSecrets({ email, apiToken, workspace, repository });
const variablesToDelete = existingVariables.filter((variable) => keys.includes(variable.key));
const promises = variablesToDelete.map((variable) => {
return request.delete(
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/pipelines_config/variables/${variable.uuid}`,
{
headers: {
Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`
}
}
);
});
return Promise.all(promises);
};
const deleteBitbucketEnvironmentSecrets = async ({
email, email,
apiToken, apiToken,
workspace, workspace,
repository, repository,
environment, environment,
keys keys
}: TDeleteBitbucketVariable & { environment: string }) => { }: TDeleteBitbucketVariable & { environment?: string }) => {
// Get the variable UUIDs since Bitbucket requires UUIDs for deletion const existingVariables = await listVariables({
const existingVariables = await listBitbucketEnvironmentSecrets({
email, email,
apiToken, apiToken,
workspace, workspace,
repository, repository,
environment environment
}); });
const variablesToDelete = existingVariables.filter((variable) => keys.includes(variable.key)); const variablesToDelete = existingVariables.filter((variable) => keys.includes(variable.key));
const authHeader = createAuthHeader(email, apiToken);
const promises = variablesToDelete.map((variable) => { const promises = variablesToDelete.map((variable) => {
return request.delete( const url = buildVariablesUrl(workspace, repository, environment, variable.uuid);
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/deployments_config/environments/${environment}/variables/${variable.uuid}`, return request.delete(url, {
{ headers: { Authorization: authHeader }
headers: { });
Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`
}
}
);
}); });
return Promise.all(promises); return Promise.all(promises);
@@ -236,67 +153,51 @@ export const BitbucketSyncFns = {
const { email, apiToken } = connection.credentials; const { email, apiToken } = connection.credentials;
try { try {
// If environment is specified in destinationConfig, use environment variables await putVariables({
if (configEnvironment) { email,
await putBitbucketEnvironmentSecrets({ apiToken,
workspace,
repository,
environment: configEnvironment,
secretMap
});
} catch (error) {
throw new SecretSyncError({ error });
}
if (secretSync.syncOptions.disableSecretDeletion) return;
try {
const existingVariables = await listVariables({
email,
apiToken,
workspace,
repository,
environment: configEnvironment
});
const keysToDelete = existingVariables
.map((variable) => variable.key)
.filter(
(secret) =>
matchesSchema(secret, environment?.slug || "", secretSync.syncOptions.keySchema) && !(secret in secretMap)
);
if (keysToDelete.length > 0) {
await deleteVariables({
email, email,
apiToken, apiToken,
workspace, workspace,
repository, repository,
environment: configEnvironment, environment: configEnvironment,
secretMap keys: keysToDelete
}); });
} else {
// Otherwise, use repository variables (original behavior)
await putBitbucketSecrets({ email, apiToken, workspace, repository, secretMap });
} }
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({ error });
error
});
}
if (secretSync.syncOptions.disableSecretDeletion) return;
// Get existing secrets based on whether we're using environment or repository variables
const existingVariables = configEnvironment
? await listBitbucketEnvironmentSecrets({
email,
apiToken,
workspace,
repository,
environment: configEnvironment
})
: await listBitbucketSecrets({ email, apiToken, workspace, repository });
const keys = existingVariables
.map((variable) => variable.key)
.filter(
(secret) =>
matchesSchema(secret, environment?.slug || "", secretSync.syncOptions.keySchema) && !(secret in secretMap)
);
if (keys.length > 0) {
try {
if (configEnvironment) {
await deleteBitbucketEnvironmentSecrets({
email,
apiToken,
workspace,
repository,
environment: configEnvironment,
keys
});
} else {
await deleteBitbucketSecrets({ email, apiToken, workspace, repository, keys });
}
} catch (error) {
throw new SecretSyncError({
error
});
}
} }
}, },
removeSecrets: async (secretSync: TBitbucketSyncWithCredentials, secretMap: TSecretMap) => { removeSecrets: async (secretSync: TBitbucketSyncWithCredentials, secretMap: TSecretMap) => {
const { const {
connection, connection,
@@ -305,39 +206,32 @@ export const BitbucketSyncFns = {
const { email, apiToken } = connection.credentials; const { email, apiToken } = connection.credentials;
const existingVariables = configEnvironment try {
? await listBitbucketEnvironmentSecrets({ const existingVariables = await listVariables({
email,
apiToken,
workspace,
repository,
environment: configEnvironment
});
const keysToRemove = existingVariables.map((variable) => variable.key).filter((secret) => secret in secretMap);
if (keysToRemove.length > 0) {
await deleteVariables({
email, email,
apiToken, apiToken,
workspace, workspace,
repository, repository,
environment: configEnvironment environment: configEnvironment,
}) keys: keysToRemove
: await listBitbucketSecrets({ email, apiToken, workspace, repository });
const keys = existingVariables.map((variable) => variable.key).filter((secret) => secret in secretMap);
if (keys.length > 0) {
try {
if (configEnvironment) {
await deleteBitbucketEnvironmentSecrets({
email,
apiToken,
workspace,
repository,
environment: configEnvironment,
keys
});
} else {
await deleteBitbucketSecrets({ email, apiToken, workspace, repository, keys });
}
} catch (error) {
throw new SecretSyncError({
error
}); });
} }
} catch (error) {
throw new SecretSyncError({ error });
} }
}, },
getSecrets: async (secretSync: TBitbucketSyncWithCredentials) => { getSecrets: async (secretSync: TBitbucketSyncWithCredentials) => {
const { const {
connection, connection,
@@ -347,23 +241,16 @@ export const BitbucketSyncFns = {
const { email, apiToken } = connection.credentials; const { email, apiToken } = connection.credentials;
try { try {
let url: string; const variables = await listVariables({
email,
if (environment) { apiToken,
url = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/deployments_config/environments/${environment}/variables`; workspace,
} else { repository,
url = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/pipelines_config/variables/`; environment
}
const { data } = await request.get<{ values: TBitbucketVariable[] }>(url, {
headers: {
Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`,
Accept: "application/json"
}
}); });
const secretMap: TSecretMap = {}; const secretMap: TSecretMap = {};
data.values.forEach((variable) => { variables.forEach((variable) => {
secretMap[variable.key] = { secretMap[variable.key] = {
value: variable.secured ? "[SECURED]" : variable.value || "", value: variable.secured ? "[SECURED]" : variable.value || "",
comment: "" comment: ""
@@ -372,9 +259,7 @@ export const BitbucketSyncFns = {
return secretMap; return secretMap;
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({ error });
error
});
} }
} }
}; };

View File

@@ -96,8 +96,8 @@ description: "Learn how to configure a Bitbucket Sync for Infisical."
"initialSyncBehavior": "overwrite-destination" "initialSyncBehavior": "overwrite-destination"
}, },
"destinationConfig": { "destinationConfig": {
"workspace": "...", "workspace": "my-bitbucket-workspace",
"repository": "..." "repository": "my-bitbucket-repository"
} }
}' }'
``` ```
@@ -148,8 +148,8 @@ description: "Learn how to configure a Bitbucket Sync for Infisical."
}, },
"destination": "bitbucket", "destination": "bitbucket",
"destinationConfig": { "destinationConfig": {
"workspace": "...", "workspace": "my-bitbucket-workspace",
"repository": "..." "repository": "my-bitbucket-repository"
} }
} }
} }

View File

@@ -4,7 +4,7 @@ import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2"; import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
export const BitBucketSyncReviewFields = () => { export const BitbucketSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Bitbucket }>(); const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Bitbucket }>();
const repository = watch("destinationConfig.repository"); const repository = watch("destinationConfig.repository");
const environment = watch("destinationConfig.environment"); const environment = watch("destinationConfig.environment");

View File

@@ -18,7 +18,7 @@ import {
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields"; import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields"; import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields";
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
import { BitBucketSyncReviewFields } from "./BitBucketSyncReviewFields"; import { BitbucketSyncReviewFields } from "./BitbucketSyncReviewFields";
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields"; import { ChecklySyncReviewFields } from "./ChecklySyncReviewFields";
import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields";
@@ -146,7 +146,7 @@ export const SecretSyncReviewFields = () => {
DestinationFieldsComponent = <SupabaseSyncReviewFields />; DestinationFieldsComponent = <SupabaseSyncReviewFields />;
break; break;
case SecretSync.Bitbucket: case SecretSync.Bitbucket:
DestinationFieldsComponent = <BitBucketSyncReviewFields />; DestinationFieldsComponent = <BitbucketSyncReviewFields />;
break; break;
default: default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`); throw new Error(`Unhandled Destination Review Fields: ${destination}`);

View File

@@ -7,9 +7,9 @@ export const BitbucketSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({ z.object({
destination: z.literal(SecretSync.Bitbucket), destination: z.literal(SecretSync.Bitbucket),
destinationConfig: z.object({ destinationConfig: z.object({
repository: z.string().trim().describe("Repository Name"), repository: z.string().trim().min(1, "Repository slug required").describe("Repository slug"),
environment: z.string().trim().optional().describe("Environment Name"), environment: z.string().trim().optional().describe("Deployment environment uuid"),
workspace: z.string().trim().describe("Workspace Name") workspace: z.string().trim().min(1, "Workspace slug required").describe("Workspace slug")
}) })
}) })
); );