feat(secret-sync): add Gitlab PR comments suggestions

This commit is contained in:
carlosmonastyrski
2025-06-24 10:05:46 -03:00
parent c305ddd463
commit 43e0d400f9
17 changed files with 243 additions and 153 deletions
+2 -1
View File
@@ -2232,7 +2232,8 @@ export const AppConnections = {
GITLAB: {
instanceUrl: "The GitLab instance URL to connect with.",
accessToken: "The Access Token used to access GitLab.",
code: "The OAuth code to use to connect with GitLab."
code: "The OAuth code to use to connect with GitLab.",
accessTokenType: "The type of token used to connect with GitLab."
}
}
};
@@ -2,3 +2,8 @@ export enum GitLabConnectionMethod {
OAuth = "oauth",
AccessToken = "access-token"
}
export enum GitLabAccessTokenType {
Project = "project",
Personal = "personal"
}
@@ -79,10 +79,13 @@ export const refreshGitLabToken = async (
}
});
const expiresAt = new Date(Date.now() + data.expires_in * 1000 - 60000);
const expiresAt = new Date(Date.now() + data.expires_in * 1000 - 600000);
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: {
instanceUrl,
tokenType: data.token_type,
createdAt: new Date(data.created_at * 1000).toISOString(),
refreshToken: data.refresh_token,
accessToken: data.access_token,
expiresAt
@@ -174,7 +177,7 @@ export const validateGitLabConnectionCredentials = async (config: TGitLabConnect
try {
const url = await getGitLabInstanceUrl(inputCredentials.instanceUrl);
response = await request.get<TGitLabProject[]>(`${url}/api/v4/groups`, {
response = await request.get<TGitLabProject[]>(`${url}/api/v4/user`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
@@ -200,6 +203,7 @@ export const validateGitLabConnectionCredentials = async (config: TGitLabConnect
if (method === GitLabConnectionMethod.OAuth && oauthData) {
return {
accessToken,
instanceUrl: inputCredentials.instanceUrl,
refreshToken: oauthData.refresh_token,
expiresAt: new Date(Date.now() + oauthData.expires_in * 1000 - 60000),
tokenType: oauthData.token_type,
@@ -8,9 +8,8 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { GitLabConnectionMethod } from "./gitlab-connection-enums";
import { GitLabAccessTokenType, GitLabConnectionMethod } from "./gitlab-connection-enums";
// Fixed: Use consistent accessToken naming throughout
export const GitLabConnectionAccessTokenCredentialsSchema = z.object({
accessToken: z
.string()
@@ -22,7 +21,8 @@ export const GitLabConnectionAccessTokenCredentialsSchema = z.object({
.trim()
.url("Invalid Instance URL")
.optional()
.describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl)
.describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl),
accessTokenType: z.nativeEnum(GitLabAccessTokenType).describe(AppConnections.CREDENTIALS.GITLAB.accessTokenType)
});
export const GitLabConnectionOAuthCredentialsSchema = z.object({
@@ -35,7 +35,6 @@ export const GitLabConnectionOAuthCredentialsSchema = z.object({
.describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl)
});
// Fixed: Updated schema to match GitLab's actual OAuth response structure
export const GitLabConnectionOAuthOutputCredentialsSchema = z.object({
accessToken: z.string().trim(),
refreshToken: z.string().trim(),
@@ -50,7 +49,6 @@ export const GitLabConnectionOAuthOutputCredentialsSchema = z.object({
.describe(AppConnections.CREDENTIALS.GITLAB.instanceUrl)
});
// Schema for refresh token input during initial setup
export const GitLabConnectionRefreshTokenCredentialsSchema = z.object({
refreshToken: z.string().trim().min(1, "Refresh token required"),
instanceUrl: z
@@ -83,8 +81,9 @@ export const SanitizedGitLabConnectionSchema = z.discriminatedUnion("method", [
BaseGitLabConnectionSchema.extend({
method: z.literal(GitLabConnectionMethod.AccessToken),
credentials: GitLabConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
}) // Don't expose sensitive data
instanceUrl: true,
accessTokenType: true
})
}),
BaseGitLabConnectionSchema.extend({
method: z.literal(GitLabConnectionMethod.OAuth),
@@ -271,67 +271,84 @@ export const GitLabSyncFns = {
const currentVariableMap = new Map(currentVariables.map((v) => [v.key, v]));
for (const [key, { value }] of Object.entries(secretMap)) {
const existingVariable = currentVariableMap.get(key);
try {
const existingVariable = currentVariableMap.get(key);
if (existingVariable) {
if (existingVariable.value !== value) {
await updateGitLabVariable({
if (existingVariable) {
if (existingVariable.value !== value) {
await updateGitLabVariable({
accessToken,
connection,
projectId,
key,
variable: {
value,
variable_type: existingVariable.variable_type,
environment_scope: targetEnvironment || existingVariable.environment_scope,
protected: destinationConfig.shouldProtectSecrets ?? existingVariable.protected,
...(!existingVariable.masked && destinationConfig.shouldMaskSecrets && { masked: value?.length > 8 }),
...(!existingVariable.hidden &&
destinationConfig.shouldHideSecrets && { masked_and_hidden: value?.length > 8 }),
description: existingVariable.description ?? undefined
},
targetEnvironment
});
}
} else {
await createGitLabVariable({
accessToken,
connection,
projectId,
key,
variable: {
key,
value,
variable_type: existingVariable.variable_type,
environment_scope: targetEnvironment || existingVariable.environment_scope,
protected: destinationConfig.shouldProtectSecrets ?? existingVariable.protected,
...(!existingVariable.masked && destinationConfig.shouldMaskSecrets && { masked: value?.length > 8 }),
...(!existingVariable.hidden &&
destinationConfig.shouldHideSecrets && { masked_and_hidden: value?.length > 8 }),
description: existingVariable.description ?? undefined
},
targetEnvironment
variable_type: "env_var",
environment_scope: targetEnvironment || "*",
protected: destinationConfig.shouldProtectSecrets || false,
masked: value?.length > 8 ? destinationConfig.shouldMaskSecrets || false : false,
masked_and_hidden: value?.length > 8 ? destinationConfig.shouldHideSecrets || false : false
}
});
}
} else {
await createGitLabVariable({
accessToken,
connection,
projectId,
variable: {
key,
value,
variable_type: "env_var",
environment_scope: targetEnvironment || "*",
protected: destinationConfig.shouldProtectSecrets || false,
masked: value?.length > 8 ? destinationConfig.shouldMaskSecrets || false : false,
masked_and_hidden: value?.length > 8 ? destinationConfig.shouldHideSecrets || false : false
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
if (!secretSync.syncOptions.disableSecretDeletion) {
for (const variable of currentVariables) {
const shouldDelete =
matchesSchema(variable.key, environment?.slug || "", secretSync.syncOptions.keySchema) &&
!(variable.key in secretMap);
try {
const shouldDelete =
matchesSchema(variable.key, environment?.slug || "", secretSync.syncOptions.keySchema) &&
!(variable.key in secretMap);
if (shouldDelete) {
await deleteGitLabVariable({
accessToken,
connection,
projectId,
key: variable.key,
targetEnvironment
if (shouldDelete) {
await deleteGitLabVariable({
accessToken,
connection,
projectId,
key: variable.key,
targetEnvironment
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: variable.key
});
}
}
}
} catch (error) {
if (error instanceof SecretSyncError) {
throw error;
}
throw new SecretSyncError({
error,
secretKey: "batch_sync"
message: "Failed to sync secrets",
error
});
}
},
@@ -347,8 +364,8 @@ export const GitLabSyncFns = {
const accessToken = await getValidAccessToken(connection, appConnectionDAL, kmsService);
try {
for (const key of Object.keys(secretMap)) {
for (const key of Object.keys(secretMap)) {
try {
await deleteGitLabVariable({
accessToken,
connection,
@@ -356,12 +373,12 @@ export const GitLabSyncFns = {
key,
targetEnvironment
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: "batch_remove"
});
}
},
@@ -12,7 +12,6 @@ export type TGitLabSyncWithCredentials = TGitLabSync & {
connection: TGitLabConnection;
};
// GitLab CI/CD Variable structure based on API documentation
export type TGitLabVariable = {
key: string;
value: string;
@@ -25,7 +24,6 @@ export type TGitLabVariable = {
description: string | null;
};
// Type for creating a new variable
export type TGitLabVariableCreate = {
key: string;
value: string;
@@ -37,7 +35,6 @@ export type TGitLabVariableCreate = {
description?: string;
};
// Type for updating an existing variable
export type TGitLabVariableUpdate = {
value: string;
variable_type?: "env_var" | "file";