feat: implement rollback registry functionality in application settings

- Added a new optional field `rollbackRegistryId` to the application schema to support rollback registry selection.
- Enhanced the form in the ShowRollbackSettings component to include a dropdown for selecting a rollback registry when rollbacks are enabled.
- Updated the application service to handle rollback registry logic during deployment and rollback processes.
- Improved error handling and validation for rollback settings, ensuring a registry is selected when rollbacks are active.
- Adjusted database schema and migration files to accommodate the new rollback registry feature.
This commit is contained in:
Mauricio Siu
2025-12-02 00:48:11 -06:00
parent e052850b87
commit 5a7f55ea63
10 changed files with 7077 additions and 109 deletions

View File

@@ -49,7 +49,6 @@ import {
updatePreviewDeployment,
} from "./preview-deployment";
import { validUniqueServerAppName } from "./project";
import { createRollback } from "./rollbacks";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
@@ -113,6 +112,7 @@ export const findApplicationById = async (applicationId: string) => {
server: true,
previewDeployments: true,
buildRegistry: true,
rollbackRegistry: true,
},
});
if (!application) {
@@ -198,7 +198,7 @@ export const deployApplication = async ({
command += await buildRemoteDocker(application);
}
command += getBuildCommand(application);
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
@@ -211,17 +211,6 @@ export const deployApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
if (application.rollbackActive) {
const tagImage =
application.sourceType === "docker"
? application.dockerImage
: application.appName;
await createRollback({
appName: tagImage || "",
deploymentId: deployment.deploymentId,
});
}
await sendBuildSuccessNotifications({
projectName: application.environment.project.name,
applicationName: application.name,
@@ -299,7 +288,7 @@ export const rebuildApplication = async ({
try {
let command = "set -e;";
// Check case for docker only
command += getBuildCommand(application);
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
@@ -310,17 +299,6 @@ export const rebuildApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
if (application.rollbackActive) {
const tagImage =
application.sourceType === "docker"
? application.dockerImage
: application.appName;
await createRollback({
appName: tagImage || "",
deploymentId: deployment.deploymentId,
});
}
await sendBuildSuccessNotifications({
projectName: application.environment.project.name,
applicationName: application.name,
@@ -423,6 +401,10 @@ export const deployPreviewApplication = async ({
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.rollbackActive = false;
application.buildRegistry = null;
application.rollbackRegistry = null;
application.registry = null;
let command = "set -e;";
if (application.sourceType === "github") {

View File

@@ -7,7 +7,8 @@ import {
deployments as deploymentsSchema,
rollbacks,
} from "../db/schema";
import { type ApplicationNested, getAuthConfig } from "../utils/builders";
import type { ApplicationNested } from "../utils/builders";
import { getRegistryTag } from "../utils/cluster/upload";
import {
calculateResources,
generateBindMounts,
@@ -22,11 +23,12 @@ import { findDeploymentById } from "./deployment";
import type { Mount } from "./mount";
import type { Port } from "./port";
import type { Project } from "./project";
import type { Registry } from "./registry";
export const createRollback = async (
input: z.infer<typeof createRollbackSchema>,
) => {
await db.transaction(async (tx) => {
return await db.transaction(async (tx) => {
const { fullContext, ...other } = input;
const rollback = await tx
.insert(rollbacks)
@@ -70,9 +72,11 @@ export const createRollback = async (
})
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
await createRollbackImage(rest, tagImage);
const updatedRollback = await tx.query.rollbacks.findFirst({
where: eq(rollbacks.rollbackId, rollback.rollbackId),
});
return rollback;
return updatedRollback;
});
};
@@ -103,27 +107,6 @@ export const findRollbackById = async (rollbackId: string) => {
return result;
};
const createRollbackImage = async (
application: ApplicationNested,
tagImage: string,
) => {
const docker = await getRemoteDocker(application.serverId);
const appTagName =
application.sourceType === "docker"
? application.dockerImage
: `${application.appName}:latest`;
const result = docker.getImage(appTagName || "");
const [repo, version] = tagImage.split(":");
await result.tag({
repo,
tag: version,
});
};
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
const command = `docker image rm ${image} --force`;
@@ -179,14 +162,18 @@ export const rollback = async (rollbackId: string) => {
if (!result.fullContext) {
throw new Error("Rollback context not found");
}
// Use the full context for rollback
await rollbackApplication(
application.appName,
result.image || "",
application.serverId,
result.fullContext,
);
try {
// Use the full context for rollback
await rollbackApplication(
application.appName,
result.image || "",
application.serverId,
result.fullContext,
);
} catch (error) {
console.error(error);
throw new Error("Failed to rollback application");
}
};
const rollbackApplication = async (
@@ -199,6 +186,7 @@ const rollbackApplication = async (
};
mounts: Mount[];
ports: Port[];
rollbackRegistry?: Registry;
},
) => {
if (!fullContext) {
@@ -245,16 +233,24 @@ const rollbackApplication = async (
fullContext.environment.project.env,
);
// For rollback, we use the provided image instead of calculating it
const authConfig = getAuthConfig(fullContext as ApplicationNested);
// Build the full registry image path if rollbackRegistry is available
// e.g., "appName:v5" -> "siumauricio/appName:v5" or "registry.com/prefix/appName:v5"
let rollbackImage = image;
if (fullContext.rollbackRegistry) {
rollbackImage = getRegistryTag(fullContext.rollbackRegistry, image);
}
const settings: CreateServiceOptions = {
authconfig: authConfig,
authconfig: {
password: fullContext.rollbackRegistry?.password || "",
username: fullContext.rollbackRegistry?.username || "",
serveraddress: fullContext.rollbackRegistry?.registryUrl || "",
},
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: image,
Image: rollbackImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount],
...(command
@@ -297,7 +293,8 @@ const rollbackApplication = async (
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch {
} catch (error) {
console.error(error);
await docker.createService(settings);
}
};