feat: Digital Ocean App connection and App Platform secret sync (#4203)

* fix: save wip

* feat: final impl

* feat: docs

* Update backend/src/services/app-connection/digital-ocean/digital-ocean-connection-service.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* chore: remove empty conflict files

* Update backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/schemas/digital-ocean-app-platform-sync-destination-schema.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/DigitalOceanAppPlatformSyncFields.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update backend/src/services/secret-sync/digital-ocean-app-platform/digital-ocean-app-platform-sync-schemas.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: lint

* fix: api client

* fix: lint and types

* fix: typecheck lint

* fix: docs

* fix: docs

* fix: linting

---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Sid
2025-07-23 19:59:29 +05:30
committed by GitHub
co-authored by greptile-apps[bot]
parent 95b997c100
commit ab56a69d59
90 changed files with 1363 additions and 8 deletions
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Digital Ocean App Platform" as const,
destination: SecretSync.DigitalOceanAppPlatform,
connection: AppConnection.DigitalOcean,
canImportSecrets: false
};
@@ -0,0 +1,83 @@
/* eslint-disable no-continue */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { TDigitalOceanVariable } from "@app/services/app-connection/digital-ocean";
import { DigitalOceanAppPlatformPublicAPI } from "@app/services/app-connection/digital-ocean/digital-ocean-connection-public-client";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SecretSyncError } from "../secret-sync-errors";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
import { TSecretMap } from "../secret-sync-types";
import { TDigitalOceanAppPlatformSyncWithCredentials } from "./digital-ocean-app-platform-sync-types";
export const DigitalOceanAppPlatformSyncFns = {
async getSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials) {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
},
async syncSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials, secretMap: TSecretMap) {
const {
environment,
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const config = secretSync.destinationConfig;
const existing = await DigitalOceanAppPlatformPublicAPI.getVariables(secretSync.connection, config.appId);
const variables: Record<string, TDigitalOceanVariable> = Object.fromEntries(existing.map((v) => [v.key, v]));
for (const [key, value] of Object.entries(secretMap)) {
variables[key] = {
key,
value: value.value,
type: "SECRET"
} as TDigitalOceanVariable;
}
if (!disableSecretDeletion) {
for (const v of existing) {
if (!matchesSchema(v.key, environment?.slug || "", keySchema)) continue;
if (!(v.key in secretMap)) {
delete variables[v.key];
}
}
}
try {
const vars = Object.values(variables);
await DigitalOceanAppPlatformPublicAPI.putVariables(secretSync.connection, config.appId, ...vars);
} catch (error) {
throw new SecretSyncError({
error
});
}
},
async removeSecrets(secretSync: TDigitalOceanAppPlatformSyncWithCredentials, secretMap: TSecretMap) {
const config = secretSync.destinationConfig;
try {
const existingSecrets = await DigitalOceanAppPlatformPublicAPI.getVariables(secretSync.connection, config.appId);
const vars = Object.entries(existingSecrets)
.map(([key, v]) => {
if (!(key in secretMap)) return;
return {
key,
value: v.value,
type: "SECRET"
} as TDigitalOceanVariable;
})
.filter(Boolean) as TDigitalOceanVariable[];
await DigitalOceanAppPlatformPublicAPI.deleteVariables(secretSync.connection, config.appId, ...vars);
} catch (error) {
throw new SecretSyncError({
error
});
}
}
};
@@ -0,0 +1,46 @@
import { z } from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const DigitalOceanAppPlatformSyncDestinationConfigSchema = z.object({
appId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"),
appName: z.string().min(1, "Account Name is required").max(255, "Account Name must be less than 255 characters")
});
const DigitalOceanAppPlatformSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const DigitalOceanAppPlatformSyncSchema = BaseSecretSyncSchema(
SecretSync.DigitalOceanAppPlatform,
DigitalOceanAppPlatformSyncOptionsConfig
).extend({
destination: z.literal(SecretSync.DigitalOceanAppPlatform),
destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema
});
export const CreateDigitalOceanAppPlatformSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.DigitalOceanAppPlatform,
DigitalOceanAppPlatformSyncOptionsConfig
).extend({
destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema
});
export const UpdateDigitalOceanAppPlatformSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.DigitalOceanAppPlatform,
DigitalOceanAppPlatformSyncOptionsConfig
).extend({
destinationConfig: DigitalOceanAppPlatformSyncDestinationConfigSchema.optional()
});
export const DigitalOceanAppPlatformSyncListItemSchema = z.object({
name: z.literal("Digital Ocean App Platform"),
connection: z.literal(AppConnection.DigitalOcean),
destination: z.literal(SecretSync.DigitalOceanAppPlatform),
canImportSecrets: z.literal(false)
});
@@ -0,0 +1,23 @@
import z from "zod";
import { TDigitalOceanConnection, TDigitalOceanVariable } from "@app/services/app-connection/digital-ocean";
import {
CreateDigitalOceanAppPlatformSyncSchema,
DigitalOceanAppPlatformSyncListItemSchema,
DigitalOceanAppPlatformSyncSchema
} from "./digital-ocean-app-platform-sync-schemas";
export type TDigitalOceanAppPlatformSyncListItem = z.infer<typeof DigitalOceanAppPlatformSyncListItemSchema>;
export type TDigitalOceanAppPlatformSync = z.infer<typeof DigitalOceanAppPlatformSyncSchema>;
export type TDigitalOceanAppPlatformSyncInput = z.infer<typeof CreateDigitalOceanAppPlatformSyncSchema>;
export type TDigitalOceanAppPlatformSyncWithCredentials = TDigitalOceanAppPlatformSync & {
connection: TDigitalOceanConnection;
};
export type TDigitalOceanAppPlatformSecret = TDigitalOceanVariable & {
type: "SECRET";
};
@@ -0,0 +1,4 @@
export * from "./digital-ocean-app-platform-sync-constants";
export * from "./digital-ocean-app-platform-sync-fns";
export * from "./digital-ocean-app-platform-sync-schemas";
export * from "./digital-ocean-app-platform-sync-types";