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
@@ -51,7 +51,7 @@ export type TBitbucketEnvironment = {
name: string;
};
export type BitbucketEnvironmentsResponse = {
export type TBitbucketEnvironmentsResponse = {
values: TBitbucketEnvironment[];
next?: string;
};
@@ -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 });
}
}
};