Compare commits

...

5 Commits

Author SHA1 Message Date
Mauricio Siu
75fc030984 Merge pull request #1508 from Dokploy/feat/add-invalidation-cache
feat(application): add cleanCache feature to application management
2025-03-16 03:21:42 -06:00
Mauricio Siu
060a170aee chore(package): bump version to v0.20.4 2025-03-16 03:21:08 -06:00
Mauricio Siu
40718293a1 feat(application): add cleanCache feature to application management
- Introduced a new boolean column `cleanCache` in the application schema to manage cache cleaning behavior.
- Updated the application form to include a toggle for `cleanCache`, allowing users to enable or disable cache cleaning.
- Enhanced application deployment logic to utilize the `cleanCache` setting, affecting build commands across various builders (Docker, Heroku, Nixpacks, Paketo, Railpack).
- Implemented success and error notifications for cache updates in the UI.
2025-03-16 03:20:47 -06:00
Mauricio Siu
9ac68985e0 Merge pull request #1506 from Dokploy/feat/add-swarm-to-remote-servers
feat(cluster-nodes): enhance node management by adding serverId prop …
2025-03-16 00:43:35 -06:00
Mauricio Siu
35ff8dcfe6 feat(cluster-nodes): enhance node management by adding serverId prop to components and implementing ShowNodesModal 2025-03-16 00:42:19 -06:00
21 changed files with 5428 additions and 114 deletions

View File

@@ -267,6 +267,28 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
className="flex flex-row gap-2 items-center" className="flex flex-row gap-2 items-center"
/> />
</div> </div>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Clean Cache</span>
<Switch
aria-label="Toggle italic"
checked={data?.cleanCache || false}
onCheckedChange={async (enabled) => {
await update({
applicationId,
cleanCache: enabled,
})
.then(async () => {
toast.success("Clean Cache Updated");
await refetch();
})
.catch(() => {
toast.error("Error updating Clean Cache");
});
}}
className="flex flex-row gap-2 items-center"
/>
</div>
</CardContent> </CardContent>
</Card> </Card>
<ShowProviderForm applicationId={applicationId} /> <ShowProviderForm applicationId={applicationId} />

View File

@@ -13,7 +13,11 @@ import Link from "next/link";
import { AddManager } from "./manager/add-manager"; import { AddManager } from "./manager/add-manager";
import { AddWorker } from "./workers/add-worker"; import { AddWorker } from "./workers/add-worker";
export const AddNode = () => { interface Props {
serverId?: string;
}
export const AddNode = ({ serverId }: Props) => {
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -53,10 +57,10 @@ export const AddNode = () => {
<TabsTrigger value="manager">Manager</TabsTrigger> <TabsTrigger value="manager">Manager</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="worker" className="pt-4"> <TabsContent value="worker" className="pt-4">
<AddWorker /> <AddWorker serverId={serverId} />
</TabsContent> </TabsContent>
<TabsContent value="manager" className="pt-4"> <TabsContent value="manager" className="pt-4">
<AddManager /> <AddManager serverId={serverId} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>

View File

@@ -9,8 +9,12 @@ import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react"; import { CopyIcon } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
export const AddManager = () => { interface Props {
const { data } = api.cluster.addManager.useQuery(); serverId?: string;
}
export const AddManager = ({ serverId }: Props) => {
const { data } = api.cluster.addManager.useQuery({ serverId });
return ( return (
<> <>

View File

@@ -0,0 +1,30 @@
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useState } from "react";
import { ShowNodes } from "./show-nodes";
interface Props {
serverId: string;
}
export const ShowNodesModal = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Nodes
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
<div className="grid w-full gap-1">
<ShowNodes serverId={serverId} />
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -32,13 +32,25 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Boxes, HelpCircle, LockIcon, MoreHorizontal } from "lucide-react"; import {
Boxes,
HelpCircle,
LockIcon,
MoreHorizontal,
Loader2,
} from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { AddNode } from "./add-node"; import { AddNode } from "./add-node";
import { ShowNodeData } from "./show-node-data"; import { ShowNodeData } from "./show-node-data";
export const ShowNodes = () => { interface Props {
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery(); serverId?: string;
}
export const ShowNodes = ({ serverId }: Props) => {
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery({
serverId,
});
const { data: registry } = api.registry.all.useQuery(); const { data: registry } = api.registry.all.useQuery();
const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation(); const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation();
@@ -58,14 +70,17 @@ export const ShowNodes = () => {
</div> </div>
{haveAtLeastOneRegistry && ( {haveAtLeastOneRegistry && (
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<AddNode /> <AddNode serverId={serverId} />
</div> </div>
)} )}
</CardHeader> </CardHeader>
<CardContent className="space-y-2 py-8 border-t min-h-[35vh]"> <CardContent className="space-y-2 py-8 border-t min-h-[35vh]">
{haveAtLeastOneRegistry ? ( {isLoading ? (
<div className="flex items-center justify-center w-full h-[40vh]">
<Loader2 className="size-8 animate-spin text-muted-foreground" />
</div>
) : haveAtLeastOneRegistry ? (
<div className="grid md:grid-cols-1 gap-4"> <div className="grid md:grid-cols-1 gap-4">
{isLoading && <div>Loading...</div>}
<Table> <Table>
<TableCaption> <TableCaption>
A list of your managers / workers. A list of your managers / workers.
@@ -137,6 +152,7 @@ export const ShowNodes = () => {
onClick={async () => { onClick={async () => {
await deleteNode({ await deleteNode({
nodeId: node.ID, nodeId: node.ID,
serverId,
}) })
.then(() => { .then(() => {
refetch(); refetch();

View File

@@ -9,8 +9,12 @@ import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react"; import { CopyIcon } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
export const AddWorker = () => { interface Props {
const { data } = api.cluster.addWorker.useQuery(); serverId?: string;
}
export const AddWorker = ({ serverId }: Props) => {
const { data } = api.cluster.addWorker.useQuery({ serverId });
return ( return (
<div> <div>

View File

@@ -42,6 +42,7 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal"; import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal"; import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription"; import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
export const ShowServers = () => { export const ShowServers = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
@@ -328,6 +329,9 @@ export const ShowServers = () => {
<ShowSwarmOverviewModal <ShowSwarmOverviewModal
serverId={server.serverId} serverId={server.serverId}
/> />
<ShowNodesModal
serverId={server.serverId}
/>
</> </>
)} )}
</DropdownMenuContent> </DropdownMenuContent>

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "cleanCache" boolean DEFAULT false;

File diff suppressed because it is too large Load Diff

View File

@@ -547,6 +547,13 @@
"when": 1741510086231, "when": 1741510086231,
"tag": "0077_chemical_dreadnoughts", "tag": "0077_chemical_dreadnoughts",
"breakpoints": true "breakpoints": true
},
{
"idx": 78,
"version": "7",
"when": 1742112194375,
"tag": "0078_uneven_omega_sentinel",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.20.3", "version": "v0.20.4",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@@ -1,22 +1,35 @@
import { getPublicIpWithFallback } from "@/server/wss/terminal"; import { getPublicIpWithFallback } from "@/server/wss/terminal";
import { type DockerNode, IS_CLOUD, docker, execAsync } from "@dokploy/server"; import {
type DockerNode,
IS_CLOUD,
execAsync,
getRemoteDocker,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
export const clusterRouter = createTRPCRouter({ export const clusterRouter = createTRPCRouter({
getNodes: protectedProcedure.query(async () => { getNodes: protectedProcedure
if (IS_CLOUD) { .input(
return []; z.object({
} serverId: z.string().optional(),
const workers: DockerNode[] = await docker.listNodes(); }),
)
.query(async ({ input }) => {
if (IS_CLOUD) {
return [];
}
return workers; const docker = await getRemoteDocker(input.serverId);
}), const workers: DockerNode[] = await docker.listNodes();
return workers;
}),
removeWorker: protectedProcedure removeWorker: protectedProcedure
.input( .input(
z.object({ z.object({
nodeId: z.string(), nodeId: z.string(),
serverId: z.string().optional(),
}), }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
@@ -40,37 +53,51 @@ export const clusterRouter = createTRPCRouter({
}); });
} }
}), }),
addWorker: protectedProcedure.query(async () => { addWorker: protectedProcedure
if (IS_CLOUD) { .input(
return { z.object({
command: "", serverId: z.string().optional(),
version: "", }),
}; )
} .query(async ({ input }) => {
const result = await docker.swarmInspect(); if (IS_CLOUD) {
const docker_version = await docker.version(); return {
command: "",
version: "",
};
}
const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect();
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
addManager: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return { return {
command: "", command: `docker swarm join --token ${
version: "", result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
}; };
} }),
const result = await docker.swarmInspect(); addManager: protectedProcedure
const docker_version = await docker.version(); .input(
return { z.object({
command: `docker swarm join --token ${ serverId: z.string().optional(),
result.JoinTokens.Manager }),
} ${await getPublicIpWithFallback()}:2377`, )
version: docker_version.Version, .query(async ({ input }) => {
}; if (IS_CLOUD) {
}), return {
command: "",
version: "",
};
}
const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect();
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Manager
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
}); });

View File

@@ -141,6 +141,7 @@ export const applications = pgTable("application", {
command: text("command"), command: text("command"),
refreshToken: text("refreshToken").$defaultFn(() => nanoid()), refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
sourceType: sourceType("sourceType").notNull().default("github"), sourceType: sourceType("sourceType").notNull().default("github"),
cleanCache: boolean("cleanCache").default(false),
// Github // Github
repository: text("repository"), repository: text("repository"),
owner: text("owner"), owner: text("owner"),
@@ -408,6 +409,7 @@ const createSchema = createInsertSchema(applications, {
previewPath: z.string().optional(), previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(),
}); });
export const apiCreateApplication = createSchema.pick({ export const apiCreateApplication = createSchema.pick({

View File

@@ -159,6 +159,7 @@ table application {
command text command text
refreshToken text refreshToken text
sourceType sourceType [not null, default: 'github'] sourceType sourceType [not null, default: 'github']
cleanCache boolean [default: false]
repository text repository text
owner text owner text
branch text branch text

View File

@@ -182,12 +182,6 @@ export const deployApplication = async ({
}); });
try { try {
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") { if (application.sourceType === "github") {
await cloneGithubRepository({ await cloneGithubRepository({
...application, ...application,
@@ -257,11 +251,6 @@ export const rebuildApplication = async ({
}); });
try { try {
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") { if (application.sourceType === "github") {
await buildApplication(application, deployment.logPath); await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "gitlab") { } else if (application.sourceType === "gitlab") {
@@ -306,11 +295,6 @@ export const deployRemoteApplication = async ({
try { try {
if (application.serverId) { if (application.serverId) {
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
let command = "set -e;"; let command = "set -e;";
if (application.sourceType === "github") { if (application.sourceType === "github") {
command += await getGithubCloneCommand({ command += await getGithubCloneCommand({
@@ -451,12 +435,6 @@ export const deployPreviewApplication = async ({
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = application.previewBuildArgs; application.buildArgs = application.previewBuildArgs;
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheOnPreviews) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") { if (application.sourceType === "github") {
await cloneGithubRepository({ await cloneGithubRepository({
...application, ...application,
@@ -565,11 +543,6 @@ export const deployRemotePreviewApplication = async ({
application.buildArgs = application.previewBuildArgs; application.buildArgs = application.previewBuildArgs;
if (application.serverId) { if (application.serverId) {
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheOnPreviews) {
// await cleanupFullDocker(application?.serverId);
// }
let command = "set -e;"; let command = "set -e;";
if (application.sourceType === "github") { if (application.sourceType === "github") {
command += await getGithubCloneCommand({ command += await getGithubCloneCommand({
@@ -634,11 +607,6 @@ export const rebuildRemoteApplication = async ({
try { try {
if (application.serverId) { if (application.serverId) {
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType !== "docker") { if (application.sourceType !== "docker") {
let command = "set -e;"; let command = "set -e;";
command += getBuildCommand(application, deployment.logPath); command += getBuildCommand(application, deployment.logPath);

View File

@@ -216,10 +216,6 @@ export const deployCompose = async ({
}); });
try { try {
// const admin = await findUserById(compose.project.userId);
// if (admin.cleanupCacheOnCompose) {
// await cleanupFullDocker(compose?.serverId);
// }
if (compose.sourceType === "github") { if (compose.sourceType === "github") {
await cloneGithubRepository({ await cloneGithubRepository({
...compose, ...compose,
@@ -285,11 +281,6 @@ export const rebuildCompose = async ({
}); });
try { try {
// const admin = await findUserById(compose.project.userId);
// if (admin.cleanupCacheOnCompose) {
// await cleanupFullDocker(compose?.serverId);
// }
if (compose.sourceType === "raw") { if (compose.sourceType === "raw") {
await createComposeFile(compose, deployment.logPath); await createComposeFile(compose, deployment.logPath);
} }
@@ -331,10 +322,6 @@ export const deployRemoteCompose = async ({
}); });
try { try {
if (compose.serverId) { if (compose.serverId) {
// const admin = await findUserById(compose.project.userId);
// if (admin.cleanupCacheOnCompose) {
// await cleanupFullDocker(compose?.serverId);
// }
let command = "set -e;"; let command = "set -e;";
if (compose.sourceType === "github") { if (compose.sourceType === "github") {
@@ -429,10 +416,6 @@ export const rebuildRemoteCompose = async ({
}); });
try { try {
// const admin = await findUserById(compose.project.userId);
// if (admin.cleanupCacheOnCompose) {
// await cleanupFullDocker(compose?.serverId);
// }
if (compose.sourceType === "raw") { if (compose.sourceType === "raw") {
const command = getCreateComposeFileCommand(compose, deployment.logPath); const command = getCreateComposeFileCommand(compose, deployment.logPath);
await execAsyncRemote(compose.serverId, command); await execAsyncRemote(compose.serverId, command);

View File

@@ -12,8 +12,14 @@ export const buildCustomDocker = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } = const {
application; appName,
env,
publishDirectory,
buildArgs,
dockerBuildStage,
cleanCache,
} = application;
const dockerFilePath = getBuildAppDirectory(application); const dockerFilePath = getBuildAppDirectory(application);
try { try {
const image = `${appName}`; const image = `${appName}`;
@@ -29,6 +35,10 @@ export const buildCustomDocker = async (
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."]; const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
if (cleanCache) {
commandArgs.push("--no-cache");
}
if (dockerBuildStage) { if (dockerBuildStage) {
commandArgs.push("--target", dockerBuildStage); commandArgs.push("--target", dockerBuildStage);
} }
@@ -65,8 +75,14 @@ export const getDockerCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } = const {
application; appName,
env,
publishDirectory,
buildArgs,
dockerBuildStage,
cleanCache,
} = application;
const dockerFilePath = getBuildAppDirectory(application); const dockerFilePath = getBuildAppDirectory(application);
try { try {
@@ -88,6 +104,10 @@ export const getDockerCommand = (
commandArgs.push("--target", dockerBuildStage); commandArgs.push("--target", dockerBuildStage);
} }
if (cleanCache) {
commandArgs.push("--no-cache");
}
for (const arg of args) { for (const arg of args) {
commandArgs.push("--build-arg", arg); commandArgs.push("--build-arg", arg);
} }

View File

@@ -9,7 +9,7 @@ export const buildHeroku = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
env, env,
@@ -29,6 +29,10 @@ export const buildHeroku = async (
args.push("--env", env); args.push("--env", env);
} }
if (cleanCache) {
args.push("--clear-cache");
}
await spawnAsync("pack", args, (data) => { await spawnAsync("pack", args, (data) => {
if (writeStream.writable) { if (writeStream.writable) {
writeStream.write(data); writeStream.write(data);
@@ -44,7 +48,7 @@ export const getHerokuCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
@@ -61,6 +65,10 @@ export const getHerokuCommand = (
`heroku/builder:${application.herokuVersion || "24"}`, `heroku/builder:${application.herokuVersion || "24"}`,
]; ];
if (cleanCache) {
args.push("--clear-cache");
}
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", `'${env}'`); args.push("--env", `'${env}'`);
} }

View File

@@ -14,7 +14,7 @@ export const buildNixpacks = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { env, appName, publishDirectory } = application; const { env, appName, publishDirectory, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const buildContainerId = `${appName}-${nanoid(10)}`; const buildContainerId = `${appName}-${nanoid(10)}`;
@@ -32,6 +32,10 @@ export const buildNixpacks = async (
try { try {
const args = ["build", buildAppDirectory, "--name", appName]; const args = ["build", buildAppDirectory, "--name", appName];
if (cleanCache) {
args.push("--no-cache");
}
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", env); args.push("--env", env);
} }
@@ -91,7 +95,7 @@ export const getNixpacksCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { env, appName, publishDirectory } = application; const { env, appName, publishDirectory, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const buildContainerId = `${appName}-${nanoid(10)}`; const buildContainerId = `${appName}-${nanoid(10)}`;
@@ -102,6 +106,10 @@ export const getNixpacksCommand = (
const args = ["build", buildAppDirectory, "--name", appName]; const args = ["build", buildAppDirectory, "--name", appName];
if (cleanCache) {
args.push("--no-cache");
}
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", `'${env}'`); args.push("--env", `'${env}'`);
} }

View File

@@ -8,7 +8,7 @@ export const buildPaketo = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
env, env,
@@ -24,6 +24,10 @@ export const buildPaketo = async (
"paketobuildpacks/builder-jammy-full", "paketobuildpacks/builder-jammy-full",
]; ];
if (cleanCache) {
args.push("--clear-cache");
}
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", env); args.push("--env", env);
} }
@@ -43,7 +47,7 @@ export const getPaketoCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
@@ -60,6 +64,10 @@ export const getPaketoCommand = (
"paketobuildpacks/builder-jammy-full", "paketobuildpacks/builder-jammy-full",
]; ];
if (cleanCache) {
args.push("--clear-cache");
}
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", `'${env}'`); args.push("--env", `'${env}'`);
} }

View File

@@ -4,12 +4,22 @@ import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { nanoid } from "nanoid";
import { createHash } from "node:crypto";
const calculateSecretsHash = (envVariables: string[]): string => {
const hash = createHash("sha256");
for (const env of envVariables.sort()) {
hash.update(env);
}
return hash.digest("hex");
};
export const buildRailpack = async ( export const buildRailpack = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
env, env,
@@ -45,10 +55,22 @@ export const buildRailpack = async (
} }
}); });
// Calculate secrets hash for layer invalidation
const secretsHash = calculateSecretsHash(envVariables);
// Build with BuildKit using the Railpack frontend // Build with BuildKit using the Railpack frontend
const cacheKey = cleanCache ? nanoid(10) : undefined;
const buildArgs = [ const buildArgs = [
"buildx", "buildx",
"build", "build",
...(cacheKey
? [
"--build-arg",
`secrets-hash=${secretsHash}`,
"--build-arg",
`cache-key=${cacheKey}`,
]
: []),
"--build-arg", "--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55", "BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55",
"-f", "-f",
@@ -92,7 +114,7 @@ export const getRailpackCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
env, env,
@@ -113,10 +135,22 @@ export const getRailpackCommand = (
prepareArgs.push("--env", env); prepareArgs.push("--env", env);
} }
// Calculate secrets hash for layer invalidation
const secretsHash = calculateSecretsHash(envVariables);
const cacheKey = cleanCache ? nanoid(10) : undefined;
// Build command // Build command
const buildArgs = [ const buildArgs = [
"buildx", "buildx",
"build", "build",
...(cacheKey
? [
"--build-arg",
`secrets-hash=${secretsHash}`,
"--build-arg",
`cache-key=${cacheKey}`,
]
: []),
"--build-arg", "--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55", "BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55",
"-f", "-f",