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

View File

@@ -96,8 +96,8 @@ description: "Learn how to configure a Bitbucket Sync for Infisical."
"initialSyncBehavior": "overwrite-destination"
},
"destinationConfig": {
"workspace": "...",
"repository": "..."
"workspace": "my-bitbucket-workspace",
"repository": "my-bitbucket-repository"
}
}'
```
@@ -148,8 +148,8 @@ description: "Learn how to configure a Bitbucket Sync for Infisical."
},
"destination": "bitbucket",
"destinationConfig": {
"workspace": "...",
"repository": "..."
"workspace": "my-bitbucket-workspace",
"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 { SecretSync } from "@app/hooks/api/secretSyncs";
export const BitBucketSyncReviewFields = () => {
export const BitbucketSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.Bitbucket }>();
const repository = watch("destinationConfig.repository");
const environment = watch("destinationConfig.environment");

View File

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

View File

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