Merge pull request #1785 from Dokploy/canary
Some checks failed
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled

🚀 Release v0.21.8
This commit is contained in:
Mauricio Siu
2025-04-27 00:18:55 -06:00
committed by GitHub
61 changed files with 11898 additions and 312 deletions

View File

@@ -52,7 +52,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch. Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory. We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
```bash ```bash
git clone https://github.com/dokploy/dokploy.git git clone https://github.com/dokploy/dokploy.git
@@ -147,11 +147,9 @@ curl -sSL https://railpack.com/install.sh | sh
```bash ```bash
# Install Buildpacks # Install Buildpacks
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
``` ```
## Pull Request ## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release. - The `main` branch is the source of truth and should always reflect the latest stable release.
@@ -169,7 +167,6 @@ Thank you for your contribution!
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file. To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
### Recommendations ### Recommendations
- Use the same name of the folder as the id of the template. - Use the same name of the folder as the id of the template.

View File

@@ -49,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx # Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash # | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.29.1 ARG NIXPACKS_VERSION=1.35.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \ && chmod +x install.sh \
&& ./install.sh \ && ./install.sh \

View File

@@ -34,6 +34,7 @@ const baseApp: ApplicationNested = {
giteaRepository: "", giteaRepository: "",
cleanCache: false, cleanCache: false,
watchPaths: [], watchPaths: [],
enableSubmodules: false,
applicationStatus: "done", applicationStatus: "done",
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,

View File

@@ -51,6 +51,35 @@ describe("processTemplate", () => {
expect(result.domains).toHaveLength(0); expect(result.domains).toHaveLength(0);
expect(result.mounts).toHaveLength(0); expect(result.mounts).toHaveLength(0);
}); });
it("should allow creation of real jwt secret", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {
jwt_secret: "cQsdycq1hDLopQonF6jUTqgQc5WEZTwWLL02J6XJ",
anon_payload: JSON.stringify({
role: "tester",
iss: "dockploy",
iat: "${timestamps:2025-01-01T00:00:00Z}",
exp: "${timestamps:2030-01-01T00:00:00Z}",
}),
anon_key: "${jwt:jwt_secret:anon_payload}",
},
config: {
domains: [],
env: {
ANON_KEY: "${anon_key}",
},
},
};
const result = processTemplate(template, mockSchema);
expect(result.envs).toHaveLength(1);
expect(result.envs).toContain(
"ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNzM1Njg5NjAwIiwiZXhwIjoiMTg5MzQ1NjAwMCIsInJvbGUiOiJ0ZXN0ZXIiLCJpc3MiOiJkb2NrcGxveSJ9.BG5JoxL2_NaTFbPgyZdm3kRWenf_O3su_HIRKGCJ_kY",
);
expect(result.mounts).toHaveLength(0);
expect(result.domains).toHaveLength(0);
});
}); });
describe("domains processing", () => { describe("domains processing", () => {

View File

@@ -0,0 +1,232 @@
import type { Schema } from "@dokploy/server/templates";
import { processValue } from "@dokploy/server/templates/processors";
import { describe, expect, it } from "vitest";
describe("helpers functions", () => {
// Mock schema for testing
const mockSchema: Schema = {
projectName: "test",
serverIp: "127.0.0.1",
};
// some helpers to test jwt
type JWTParts = [string, string, string];
const jwtMatchExp = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
const jwtBase64Decode = (str: string) => {
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
const decoded = Buffer.from(base64 + padding, "base64").toString("utf-8");
return JSON.parse(decoded);
};
const jwtCheckHeader = (jwtHeader: string) => {
const decodedHeader = jwtBase64Decode(jwtHeader);
expect(decodedHeader).toHaveProperty("alg");
expect(decodedHeader).toHaveProperty("typ");
expect(decodedHeader.alg).toEqual("HS256");
expect(decodedHeader.typ).toEqual("JWT");
};
describe("${domain}", () => {
it("should generate a random domain", () => {
const domain = processValue("${domain}", {}, mockSchema);
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
expect(
domain.endsWith(
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
),
).toBeTruthy();
});
});
describe("${base64}", () => {
it("should generate a base64 string", () => {
const base64 = processValue("${base64}", {}, mockSchema);
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
});
it.each([
[4, 8],
[8, 12],
[16, 24],
[32, 44],
[64, 88],
[128, 172],
])(
"should generate a base64 string from parameter %d bytes length",
(length, finalLength) => {
const base64 = processValue(`\${base64:${length}}`, {}, mockSchema);
expect(base64).toMatch(/^[A-Za-z0-9+=/]+={0,2}$/);
expect(base64.length).toBe(finalLength);
},
);
});
describe("${password}", () => {
it("should generate a password string", () => {
const password = processValue("${password}", {}, mockSchema);
expect(password).toMatch(/^[A-Za-z0-9]+$/);
});
it.each([6, 8, 12, 16, 32])(
"should generate a password string respecting parameter %d length",
(length) => {
const password = processValue(`\${password:${length}}`, {}, mockSchema);
expect(password).toMatch(/^[A-Za-z0-9]+$/);
expect(password.length).toBe(length);
},
);
});
describe("${hash}", () => {
it("should generate a hash string", () => {
const hash = processValue("${hash}", {}, mockSchema);
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
});
it.each([6, 8, 12, 16, 32])(
"should generate a hash string respecting parameter %d length",
(length) => {
const hash = processValue(`\${hash:${length}}`, {}, mockSchema);
expect(hash).toMatch(/^[A-Za-z0-9]+$/);
expect(hash.length).toBe(length);
},
);
});
describe("${uuid}", () => {
it("should generate a UUID string", () => {
const uuid = processValue("${uuid}", {}, mockSchema);
expect(uuid).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
});
});
describe("${timestamp}", () => {
it("should generate a timestamp string in milliseconds", () => {
const timestamp = processValue("${timestamp}", {}, mockSchema);
const nowLength = Math.floor(Date.now()).toString().length;
expect(timestamp).toMatch(/^\d+$/);
expect(timestamp.length).toBe(nowLength);
});
});
describe("${timestampms}", () => {
it("should generate a timestamp string in milliseconds", () => {
const timestamp = processValue("${timestampms}", {}, mockSchema);
const nowLength = Date.now().toString().length;
expect(timestamp).toMatch(/^\d+$/);
expect(timestamp.length).toBe(nowLength);
});
it("should generate a timestamp string in milliseconds from parameter", () => {
const timestamp = processValue(
"${timestampms:2025-01-01}",
{},
mockSchema,
);
expect(timestamp).toEqual("1735689600000");
});
});
describe("${timestamps}", () => {
it("should generate a timestamp string in seconds", () => {
const timestamps = processValue("${timestamps}", {}, mockSchema);
const nowLength = Math.floor(Date.now() / 1000).toString().length;
expect(timestamps).toMatch(/^\d+$/);
expect(timestamps.length).toBe(nowLength);
});
it("should generate a timestamp string in seconds from parameter", () => {
const timestamps = processValue(
"${timestamps:2025-01-01}",
{},
mockSchema,
);
expect(timestamps).toEqual("1735689600");
});
});
describe("${randomPort}", () => {
it("should generate a random port string", () => {
const randomPort = processValue("${randomPort}", {}, mockSchema);
expect(randomPort).toMatch(/^\d+$/);
expect(Number(randomPort)).toBeLessThan(65536);
});
});
describe("${username}", () => {
it("should generate a username string", () => {
const username = processValue("${username}", {}, mockSchema);
expect(username).toMatch(/^[a-zA-Z0-9._-]{3,}$/);
});
});
describe("${email}", () => {
it("should generate an email string", () => {
const email = processValue("${email}", {}, mockSchema);
expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
});
});
describe("${jwt}", () => {
it("should generate a JWT string", () => {
const jwt = processValue("${jwt}", {}, mockSchema);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
const decodedPayload = jwtBase64Decode(parts[1]);
jwtCheckHeader(parts[0]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.iss).toEqual("dokploy");
});
it.each([6, 8, 12, 16, 32])(
"should generate a random hex string from parameter %d byte length",
(length) => {
const jwt = processValue(`\${jwt:${length}}`, {}, mockSchema);
expect(jwt).toMatch(/^[A-Za-z0-9-_.]+$/);
expect(jwt.length).toBeGreaterThanOrEqual(length); // bytes translated to hex can take up to 2x the length
expect(jwt.length).toBeLessThanOrEqual(length * 2);
},
);
});
describe("${jwt:secret}", () => {
it("should generate a JWT string respecting parameter secret from variable", () => {
const jwt = processValue(
"${jwt:secret}",
{ secret: "mysecret" },
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
const decodedPayload = jwtBase64Decode(parts[1]);
jwtCheckHeader(parts[0]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.iss).toEqual("dokploy");
});
});
describe("${jwt:secret:payload}", () => {
it("should generate a JWT string respecting parameters secret and payload from variables", () => {
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
const expiry = iat + 3600;
const jwt = processValue(
"${jwt:secret:payload}",
{
secret: "mysecret",
payload: `{"iss": "test-issuer", "iat": ${iat}, "exp": ${expiry}, "customprop": "customvalue"}`,
},
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
jwtCheckHeader(parts[0]);
const decodedPayload = jwtBase64Decode(parts[1]);
expect(decodedPayload).toHaveProperty("iat");
expect(decodedPayload.iat).toEqual(iat);
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload.iss).toEqual("test-issuer");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.exp).toEqual(expiry);
expect(decodedPayload).toHaveProperty("customprop");
expect(decodedPayload.customprop).toEqual("customvalue");
expect(jwt).toEqual(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
);
});
});
});

View File

@@ -16,6 +16,7 @@ const baseApp: ApplicationNested = {
applicationStatus: "done", applicationStatus: "done",
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,
enableSubmodules: false,
serverId: "", serverId: "",
branch: null, branch: null,
dockerBuildStage: "", dockerBuildStage: "",

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().optional(),
}); });
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>; type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@@ -84,6 +86,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
bitbucketId: "", bitbucketId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(BitbucketProviderSchema), resolver: zodResolver(BitbucketProviderSchema),
}); });
@@ -130,6 +133,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
buildPath: data.bitbucketBuildPath || "/", buildPath: data.bitbucketBuildPath || "/",
bitbucketId: data.bitbucketId || "", bitbucketId: data.bitbucketId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -143,6 +147,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
bitbucketId: data.bitbucketId, bitbucketId: data.bitbucketId,
applicationId, applicationId,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -467,6 +472,21 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -23,6 +23,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react"; import { KeyRoundIcon, LockIcon, X } from "lucide-react";
@@ -44,6 +45,7 @@ const GitProviderSchema = z.object({
branch: z.string().min(1, "Branch required"), branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(), sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitProvider = z.infer<typeof GitProviderSchema>; type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -67,6 +69,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
repositoryURL: "", repositoryURL: "",
sshKey: undefined, sshKey: undefined,
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GitProviderSchema), resolver: zodResolver(GitProviderSchema),
}); });
@@ -79,6 +82,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
buildPath: data.customGitBuildPath || "/", buildPath: data.customGitBuildPath || "/",
repositoryURL: data.customGitUrl || "", repositoryURL: data.customGitUrl || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -91,6 +95,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey, customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
applicationId, applicationId,
watchPaths: values.watchPaths || [], watchPaths: values.watchPaths || [],
enableSubmodules: values.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Git Provider Saved"); toast.success("Git Provider Saved");
@@ -294,6 +299,22 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -74,6 +75,7 @@ const GiteaProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]), watchPaths: z.array(z.string()).default([]),
enableSubmodules: z.boolean().optional(),
}); });
type GiteaProvider = z.infer<typeof GiteaProviderSchema>; type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@@ -99,6 +101,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
giteaId: "", giteaId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GiteaProviderSchema), resolver: zodResolver(GiteaProviderSchema),
}); });
@@ -152,6 +155,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
buildPath: data.giteaBuildPath || "/", buildPath: data.giteaBuildPath || "/",
giteaId: data.giteaId || "", giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules || false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -165,6 +169,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
giteaId: data.giteaId, giteaId: data.giteaId,
applicationId, applicationId,
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules || false,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provider Saved"); toast.success("Service Provider Saved");
@@ -498,6 +503,21 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -30,6 +30,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -57,6 +58,7 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GithubProvider = z.infer<typeof GithubProviderSchema>; type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -81,6 +83,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
}, },
githubId: "", githubId: "",
branch: "", branch: "",
enableSubmodules: false,
}, },
resolver: zodResolver(GithubProviderSchema), resolver: zodResolver(GithubProviderSchema),
}); });
@@ -124,6 +127,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
buildPath: data.buildPath || "/", buildPath: data.buildPath || "/",
githubId: data.githubId || "", githubId: data.githubId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -137,6 +141,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
buildPath: data.buildPath, buildPath: data.buildPath,
githubId: data.githubId, githubId: data.githubId,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -458,6 +463,22 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -60,6 +61,7 @@ const GitlabProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitlabProvider = z.infer<typeof GitlabProviderSchema>; type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
@@ -86,6 +88,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
}, },
gitlabId: "", gitlabId: "",
branch: "", branch: "",
enableSubmodules: false,
}, },
resolver: zodResolver(GitlabProviderSchema), resolver: zodResolver(GitlabProviderSchema),
}); });
@@ -135,6 +138,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
buildPath: data.gitlabBuildPath || "/", buildPath: data.gitlabBuildPath || "/",
gitlabId: data.gitlabId || "", gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -150,6 +154,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
gitlabProjectId: data.repository.id, gitlabProjectId: data.repository.id,
gitlabPathNamespace: data.repository.gitlabPathNamespace, gitlabPathNamespace: data.repository.gitlabPathNamespace,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -483,6 +488,21 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -65,7 +65,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
setSab(e as TabState); setSab(e as TabState);
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden"> <TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<TabsTrigger <TabsTrigger
value="github" value="github"

View File

@@ -79,6 +79,22 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
toast.error("Error updating the Compose config"); toast.error("Error updating the Compose config");
}); });
}; };
// Add keyboard shortcut for Ctrl+S/Cmd+S
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [form, onSubmit, isLoading]);
return ( return (
<> <>
<div className="w-full flex flex-col gap-4 "> <div className="w-full flex flex-col gap-4 ">

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -58,6 +59,7 @@ const BitbucketProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>; type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@@ -84,6 +86,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
bitbucketId: "", bitbucketId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(BitbucketProviderSchema), resolver: zodResolver(BitbucketProviderSchema),
}); });
@@ -130,6 +133,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath, composePath: data.composePath,
bitbucketId: data.bitbucketId || "", bitbucketId: data.bitbucketId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -145,6 +149,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
sourceType: "bitbucket", sourceType: "bitbucket",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -469,6 +474,21 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -19,6 +19,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -43,6 +44,7 @@ const GitProviderSchema = z.object({
branch: z.string().min(1, "Branch required"), branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(), sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitProvider = z.infer<typeof GitProviderSchema>; type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -65,6 +67,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composePath: "./docker-compose.yml", composePath: "./docker-compose.yml",
sshKey: undefined, sshKey: undefined,
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GitProviderSchema), resolver: zodResolver(GitProviderSchema),
}); });
@@ -77,6 +80,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
repositoryURL: data.customGitUrl || "", repositoryURL: data.customGitUrl || "",
composePath: data.composePath, composePath: data.composePath,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -91,6 +95,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composePath: values.composePath, composePath: values.composePath,
composeStatus: "idle", composeStatus: "idle",
watchPaths: values.watchPaths || [], watchPaths: values.watchPaths || [],
enableSubmodules: values.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Git Provider Saved"); toast.success("Git Provider Saved");
@@ -295,6 +300,21 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -59,6 +60,7 @@ const GiteaProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GiteaProvider = z.infer<typeof GiteaProviderSchema>; type GiteaProvider = z.infer<typeof GiteaProviderSchema>;
@@ -83,6 +85,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
giteaId: "", giteaId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GiteaProviderSchema), resolver: zodResolver(GiteaProviderSchema),
}); });
@@ -136,6 +139,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath || "./docker-compose.yml", composePath: data.composePath || "./docker-compose.yml",
giteaId: data.giteaId || "", giteaId: data.giteaId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -151,6 +155,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
sourceType: "gitea", sourceType: "gitea",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
} as any) } as any)
.then(async () => { .then(async () => {
toast.success("Service Provider Saved"); toast.success("Service Provider Saved");
@@ -469,6 +474,21 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">

View File

@@ -30,6 +30,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -57,6 +58,7 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GithubProvider = z.infer<typeof GithubProviderSchema>; type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -82,6 +84,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
githubId: "", githubId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GithubProviderSchema), resolver: zodResolver(GithubProviderSchema),
}); });
@@ -125,6 +128,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath, composePath: data.composePath,
githubId: data.githubId || "", githubId: data.githubId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -140,6 +144,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
sourceType: "github", sourceType: "github",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -460,6 +465,21 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -31,6 +31,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -60,6 +61,7 @@ const GitlabProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
}); });
type GitlabProvider = z.infer<typeof GitlabProviderSchema>; type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
@@ -87,6 +89,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
gitlabId: "", gitlabId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
enableSubmodules: false,
}, },
resolver: zodResolver(GitlabProviderSchema), resolver: zodResolver(GitlabProviderSchema),
}); });
@@ -136,6 +139,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath, composePath: data.composePath,
gitlabId: data.gitlabId || "", gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data, form]); }, [form.reset, data, form]);
@@ -153,6 +157,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
sourceType: "gitlab", sourceType: "gitlab",
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -485,6 +490,21 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
</FormItem>
)}
/>
</div> </div>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button <Button

View File

@@ -77,6 +77,14 @@ const RestoreBackupSchema = z.object({
type RestoreBackup = z.infer<typeof RestoreBackupSchema>; type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
const formatBytes = (bytes: number): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
};
export const RestoreBackup = ({ export const RestoreBackup = ({
databaseId, databaseId,
databaseType, databaseType,
@@ -101,7 +109,7 @@ export const RestoreBackup = ({
const debouncedSetSearch = debounce((value: string) => { const debouncedSetSearch = debounce((value: string) => {
setDebouncedSearchTerm(value); setDebouncedSearchTerm(value);
}, 150); }, 350);
const handleSearchChange = (value: string) => { const handleSearchChange = (value: string) => {
setSearch(value); setSearch(value);
@@ -271,7 +279,7 @@ export const RestoreBackup = ({
</Badge> </Badge>
)} )}
</FormLabel> </FormLabel>
<Popover> <Popover modal>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<FormControl> <FormControl>
<Button <Button
@@ -308,28 +316,51 @@ export const RestoreBackup = ({
</div> </div>
) : ( ) : (
<ScrollArea className="h-64"> <ScrollArea className="h-64">
<CommandGroup> <CommandGroup className="w-96">
{files.map((file) => ( {files?.map((file) => (
<CommandItem <CommandItem
value={file} value={file.Path}
key={file} key={file.Path}
onSelect={() => { onSelect={() => {
form.setValue("backupFile", file); form.setValue("backupFile", file.Path);
setSearch(file); if (file.IsDir) {
setDebouncedSearchTerm(file); setSearch(`${file.Path}/`);
setDebouncedSearchTerm(`${file.Path}/`);
} else {
setSearch(file.Path);
setDebouncedSearchTerm(file.Path);
}
}} }}
> >
<div className="flex w-full justify-between"> <div className="flex w-full flex-col gap-1">
<span>{file}</span> <div className="flex w-full justify-between">
<span className="font-medium">
{file.Path}
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file.Path === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</div>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>
Size: {formatBytes(file.Size)}
</span>
{file.IsDir && (
<span className="text-blue-500">
Directory
</span>
)}
{file.Hashes?.MD5 && (
<span>MD5: {file.Hashes.MD5}</span>
)}
</div>
</div> </div>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
file === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@@ -55,7 +55,7 @@ export const AiForm = () => {
key={config.aiId} key={config.aiId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div> <div>
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{config.name} {config.name}

View File

@@ -70,7 +70,7 @@ export const ShowCertificates = () => {
key={certificate.certificateId} key={certificate.certificateId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2 flex-col"> <div className="flex gap-2 flex-col">
<span className="text-sm font-medium"> <span className="text-sm font-medium">

View File

@@ -13,53 +13,65 @@ export const extractExpirationDate = (certData: string): Date | null => {
bytes[i] = binaryStr.charCodeAt(i); bytes[i] = binaryStr.charCodeAt(i);
} }
let dateFound = 0; // ASN.1 tag for UTCTime is 0x17, GeneralizedTime is 0x18
// We need to find the second occurrence of either tag as it's the "not after" (expiration) date
let dateFound = false;
for (let i = 0; i < bytes.length - 2; i++) { for (let i = 0; i < bytes.length - 2; i++) {
if (bytes[i] === 0x17 || bytes[i] === 0x18) { // Look for sequence containing validity period (0x30)
const dateType = bytes[i]; if (bytes[i] === 0x30) {
const dateLength = bytes[i + 1]; // Check next bytes for UTCTime or GeneralizedTime
if (typeof dateLength === "undefined") continue; let j = i + 1;
while (j < bytes.length - 2) {
if (bytes[j] === 0x17 || bytes[j] === 0x18) {
const dateType = bytes[j];
const dateLength = bytes[j + 1];
if (typeof dateLength === "undefined") break;
if (dateFound === 0) { if (!dateFound) {
dateFound++; // Skip "not before" date
i += dateLength + 1; dateFound = true;
continue; j += dateLength + 2;
continue;
}
// Found "not after" date
let dateStr = "";
for (let k = 0; k < dateLength; k++) {
const charCode = bytes[j + 2 + k];
if (typeof charCode === "undefined") continue;
dateStr += String.fromCharCode(charCode);
}
if (dateType === 0x17) {
// UTCTime (YYMMDDhhmmssZ)
const year = Number.parseInt(dateStr.slice(0, 2));
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
return new Date(
Date.UTC(
fullYear,
Number.parseInt(dateStr.slice(2, 4)) - 1,
Number.parseInt(dateStr.slice(4, 6)),
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
),
);
}
// GeneralizedTime (YYYYMMDDhhmmssZ)
return new Date(
Date.UTC(
Number.parseInt(dateStr.slice(0, 4)),
Number.parseInt(dateStr.slice(4, 6)) - 1,
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
Number.parseInt(dateStr.slice(12, 14)),
),
);
}
j++;
} }
let dateStr = "";
for (let j = 0; j < dateLength; j++) {
const charCode = bytes[i + 2 + j];
if (typeof charCode === "undefined") continue;
dateStr += String.fromCharCode(charCode);
}
if (dateType === 0x17) {
// UTCTime (YYMMDDhhmmssZ)
const year = Number.parseInt(dateStr.slice(0, 2));
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
return new Date(
Date.UTC(
fullYear,
Number.parseInt(dateStr.slice(2, 4)) - 1,
Number.parseInt(dateStr.slice(4, 6)),
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
),
);
}
// GeneralizedTime (YYYYMMDDhhmmssZ)
return new Date(
Date.UTC(
Number.parseInt(dateStr.slice(0, 4)),
Number.parseInt(dateStr.slice(4, 6)) - 1,
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
Number.parseInt(dateStr.slice(12, 14)),
),
);
} }
} }
return null; return null;

View File

@@ -54,7 +54,7 @@ export const ShowRegistry = () => {
key={registry.registryId} key={registry.registryId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2 flex-col"> <div className="flex gap-2 flex-col">
<span className="text-sm font-medium"> <span className="text-sm font-medium">

View File

@@ -55,7 +55,7 @@ export const ShowDestinations = () => {
key={destination.destinationId} key={destination.destinationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="text-sm"> <span className="text-sm">
{index + 1}. {destination.name} {index + 1}. {destination.name}

View File

@@ -61,7 +61,7 @@ export const ShowNotifications = () => {
key={notification.notificationId} key={notification.notificationId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<span className="text-sm flex flex-row items-center gap-4"> <span className="text-sm flex flex-row items-center gap-4">
{notification.notificationType === "slack" && ( {notification.notificationType === "slack" && (
<div className="flex items-center justify-center rounded-lg"> <div className="flex items-center justify-center rounded-lg">

View File

@@ -22,6 +22,9 @@ export const ShowDokployActions = () => {
const { mutateAsync: reloadServer, isLoading } = const { mutateAsync: reloadServer, isLoading } =
api.settings.reloadServer.useMutation(); api.settings.reloadServer.useMutation();
const { mutateAsync: cleanRedis } = api.settings.cleanRedis.useMutation();
const { mutateAsync: reloadRedis } = api.settings.reloadRedis.useMutation();
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild disabled={isLoading}> <DropdownMenuTrigger asChild disabled={isLoading}>
@@ -69,6 +72,36 @@ export const ShowDokployActions = () => {
{t("settings.server.webServer.updateServerIp")} {t("settings.server.webServer.updateServerIp")}
</DropdownMenuItem> </DropdownMenuItem>
</UpdateServerIp> </UpdateServerIp>
<DropdownMenuItem
className="cursor-pointer"
onClick={async () => {
await cleanRedis()
.then(async () => {
toast.success("Redis cleaned");
})
.catch(() => {
toast.error("Error cleaning Redis");
});
}}
>
Clean Redis
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={async () => {
await reloadRedis()
.then(async () => {
toast.success("Redis reloaded");
})
.catch(() => {
toast.error("Error reloading Redis");
});
}}
>
Reload Redis
</DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@@ -56,7 +56,7 @@ export const ShowDestinations = () => {
key={sshKey.sshKeyId} key={sshKey.sshKeyId}
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
> >
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full"> <div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium"> <span className="text-sm font-medium">

View File

@@ -133,17 +133,6 @@ export const UserNav = () => {
Servers Servers
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{data?.role === "owner" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings");
}}
>
Settings
</DropdownMenuItem>
)}
</> </>
)} )}
</DropdownMenuGroup> </DropdownMenuGroup>

View File

@@ -26,15 +26,20 @@ const dockerComposeServices = [
{ label: "secrets", type: "keyword", info: "Define secrets" }, { label: "secrets", type: "keyword", info: "Define secrets" },
].map((opt) => ({ ].map((opt) => ({
...opt, ...opt,
apply: (view: EditorView, completion: Completion) => { apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
) => {
const insert = `${completion.label}:`; const insert = `${completion.label}:`;
view.dispatch({ view.dispatch({
changes: { changes: {
from: view.state.selection.main.from, from,
to: view.state.selection.main.to, to,
insert, insert,
}, },
selection: { anchor: view.state.selection.main.from + insert.length }, selection: { anchor: from + insert.length },
}); });
}, },
})); }));
@@ -74,15 +79,20 @@ const dockerComposeServiceOptions = [
{ label: "networks", type: "keyword", info: "Networks to join" }, { label: "networks", type: "keyword", info: "Networks to join" },
].map((opt) => ({ ].map((opt) => ({
...opt, ...opt,
apply: (view: EditorView, completion: Completion) => { apply: (
view: EditorView,
completion: Completion,
from: number,
to: number,
) => {
const insert = `${completion.label}: `; const insert = `${completion.label}: `;
view.dispatch({ view.dispatch({
changes: { changes: {
from: view.state.selection.main.from, from,
to: view.state.selection.main.to, to,
insert, insert,
}, },
selection: { anchor: view.state.selection.main.from + insert.length }, selection: { anchor: from + insert.length },
}); });
}, },
})); }));
@@ -99,6 +109,7 @@ function dockerComposeComplete(
const line = context.state.doc.lineAt(context.pos); const line = context.state.doc.lineAt(context.pos);
const indentation = /^\s*/.exec(line.text)?.[0].length || 0; const indentation = /^\s*/.exec(line.text)?.[0].length || 0;
// If we're at the root level
if (indentation === 0) { if (indentation === 0) {
return { return {
from: word.from, from: word.from,

View File

@@ -0,0 +1,2 @@
ALTER TABLE "application" ADD COLUMN "enableSubmodules" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "enableSubmodules" boolean DEFAULT false;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "application" ALTER COLUMN "enableSubmodules" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "compose" ALTER COLUMN "enableSubmodules" SET NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -596,6 +596,20 @@
"when": 1743923992280, "when": 1743923992280,
"tag": "0084_thin_iron_lad", "tag": "0084_thin_iron_lad",
"breakpoints": true "breakpoints": true
},
{
"idx": 85,
"version": "7",
"when": 1745705609181,
"tag": "0085_equal_captain_stacy",
"breakpoints": true
},
{
"idx": 86,
"version": "7",
"when": 1745706676004,
"tag": "0086_rainy_gertrude_yorkes",
"breakpoints": true
} }
] ]
} }

View File

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

View File

@@ -215,7 +215,7 @@ const Service = (
router.push(newPath); router.push(newPath);
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden", "flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden",

View File

@@ -212,15 +212,15 @@ const Service = (
router.push(newPath); router.push(newPath);
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId isCloud && data?.serverId
? "md:grid-cols-7" ? "lg:grid-cols-7"
: data?.serverId : data?.serverId
? "md:grid-cols-6" ? "lg:grid-cols-6"
: "md:grid-cols-7", : "lg:grid-cols-7",
)} )}
> >
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>

View File

@@ -182,7 +182,7 @@ const Mariadb = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -183,7 +183,7 @@ const Mongo = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -183,7 +183,7 @@ const MySql = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start ", "md:grid md:w-fit max-md:overflow-y-scroll justify-start ",

View File

@@ -182,7 +182,7 @@ const Postgresql = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -182,7 +182,7 @@ const Redis = (
router.push(newPath, undefined, { shallow: true }); router.push(newPath, undefined, { shallow: true });
}} }}
> >
<div className="flex flex-row items-center justify-between w-full gap-4"> <div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList <TabsList
className={cn( className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start", "md:grid md:w-fit max-md:overflow-y-scroll justify-start",

View File

@@ -355,6 +355,7 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle", applicationStatus: "idle",
githubId: input.githubId, githubId: input.githubId,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@@ -382,6 +383,7 @@ export const applicationRouter = createTRPCRouter({
gitlabProjectId: input.gitlabProjectId, gitlabProjectId: input.gitlabProjectId,
gitlabPathNamespace: input.gitlabPathNamespace, gitlabPathNamespace: input.gitlabPathNamespace,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@@ -407,6 +409,7 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle", applicationStatus: "idle",
bitbucketId: input.bitbucketId, bitbucketId: input.bitbucketId,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@@ -432,6 +435,7 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle", applicationStatus: "idle",
giteaId: input.giteaId, giteaId: input.giteaId,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;
@@ -479,6 +483,7 @@ export const applicationRouter = createTRPCRouter({
sourceType: "git", sourceType: "git",
applicationStatus: "idle", applicationStatus: "idle",
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
enableSubmodules: input.enableSubmodules,
}); });
return true; return true;

View File

@@ -50,6 +50,18 @@ import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod";
interface RcloneFile {
Path: string;
Name: string;
Size: number;
IsDir: boolean;
Tier?: string;
Hashes?: {
MD5?: string;
SHA1?: string;
};
}
export const backupRouter = createTRPCRouter({ export const backupRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
.input(apiCreateBackup) .input(apiCreateBackup)
@@ -268,7 +280,7 @@ export const backupRouter = createTRPCRouter({
: input.search; : input.search;
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath; const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`; const listCommand = `rclone lsjson ${rcloneFlags.join(" ")} "${searchPath}" --no-mimetype --no-modtime 2>/dev/null`;
let stdout = ""; let stdout = "";
@@ -280,20 +292,35 @@ export const backupRouter = createTRPCRouter({
stdout = result.stdout; stdout = result.stdout;
} }
const files = stdout.split("\n").filter(Boolean); let files: RcloneFile[] = [];
try {
files = JSON.parse(stdout) as RcloneFile[];
} catch (error) {
console.error("Error parsing JSON response:", error);
console.error("Raw stdout:", stdout);
throw new Error("Failed to parse backup files list");
}
// Limit to first 100 files
const results = baseDir const results = baseDir
? files.map((file) => `${baseDir}${file}`) ? files.map((file) => ({
...file,
Path: `${baseDir}${file.Path}`,
}))
: files; : files;
if (searchTerm) { if (searchTerm) {
return results.filter((file) => return results
file.toLowerCase().includes(searchTerm.toLowerCase()), .filter((file) =>
); file.Path.toLowerCase().includes(searchTerm.toLowerCase()),
)
.slice(0, 100);
} }
return results; return results.slice(0, 100);
} catch (error) { } catch (error) {
console.error("Error in listBackupFiles:", error);
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: message:

View File

@@ -31,7 +31,6 @@ export const mountRouter = createTRPCRouter({
update: protectedProcedure update: protectedProcedure
.input(apiUpdateMount) .input(apiUpdateMount)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await updateMount(input.mountId, input); return await updateMount(input.mountId, input);
return true;
}), }),
}); });

View File

@@ -79,6 +79,33 @@ export const settingsRouter = createTRPCRouter({
await execAsync(`docker service update --force ${stdout.trim()}`); await execAsync(`docker service update --force ${stdout.trim()}`);
return true; return true;
}), }),
cleanRedis: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
const { stdout: containerId } = await execAsync(
`docker ps --filter "name=dokploy-redis" --filter "status=running" -q | head -n 1`,
);
if (!containerId) {
throw new Error("Redis container not found");
}
const redisContainerId = containerId.trim();
await execAsync(`docker exec -i ${redisContainerId} redis-cli flushall`);
return true;
}),
reloadRedis: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
await execAsync("docker service scale dokploy-redis=0");
await execAsync("docker service scale dokploy-redis=1");
return true;
}),
reloadTraefik: adminProcedure reloadTraefik: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {

View File

@@ -21,6 +21,7 @@ import { setupTerminalWebSocketServer } from "./wss/terminal";
config({ path: ".env" }); config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10); const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const HOST = process.env.HOST || "0.0.0.0";
const dev = process.env.NODE_ENV !== "production"; const dev = process.env.NODE_ENV !== "production";
const app = next({ dev, turbopack: process.env.TURBOPACK === "1" }); const app = next({ dev, turbopack: process.env.TURBOPACK === "1" });
const handle = app.getRequestHandler(); const handle = app.getRequestHandler();
@@ -55,8 +56,8 @@ void app.prepare().then(async () => {
await migration(); await migration();
} }
server.listen(PORT); server.listen(PORT, HOST);
console.log("Server Started:", PORT); console.log(`Server Started on: http://${HOST}:${PORT}`);
if (!IS_CLOUD) { if (!IS_CLOUD) {
console.log("Starting Deployment Worker"); console.log("Starting Deployment Worker");
const { deploymentWorker } = await import("./queues/deployments-queue"); const { deploymentWorker } = await import("./queues/deployments-queue");

View File

@@ -182,6 +182,7 @@ export const applications = pgTable("application", {
onDelete: "set null", onDelete: "set null",
}, },
), ),
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
dockerfile: text("dockerfile"), dockerfile: text("dockerfile"),
dockerContextPath: text("dockerContextPath"), dockerContextPath: text("dockerContextPath"),
dockerBuildStage: text("dockerBuildStage"), dockerBuildStage: text("dockerBuildStage"),
@@ -470,6 +471,7 @@ export const apiSaveGithubProvider = createSchema
buildPath: true, buildPath: true,
githubId: true, githubId: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@@ -484,6 +486,7 @@ export const apiSaveGitlabProvider = createSchema
gitlabProjectId: true, gitlabProjectId: true,
gitlabPathNamespace: true, gitlabPathNamespace: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@@ -496,6 +499,7 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketId: true, bitbucketId: true,
applicationId: true, applicationId: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@@ -508,6 +512,7 @@ export const apiSaveGiteaProvider = createSchema
giteaRepository: true, giteaRepository: true,
giteaId: true, giteaId: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required(); .required();
@@ -528,6 +533,7 @@ export const apiSaveGitProvider = createSchema
customGitBuildPath: true, customGitBuildPath: true,
customGitUrl: true, customGitUrl: true,
watchPaths: true, watchPaths: true,
enableSubmodules: true,
}) })
.required() .required()
.merge( .merge(

View File

@@ -72,6 +72,7 @@ export const compose = pgTable("compose", {
), ),
command: text("command").notNull().default(""), command: text("command").notNull().default(""),
// //
enableSubmodules: boolean("enableSubmodules").notNull().default(false),
composePath: text("composePath").notNull().default("./docker-compose.yml"), composePath: text("composePath").notNull().default("./docker-compose.yml"),
suffix: text("suffix").notNull().default(""), suffix: text("suffix").notNull().default(""),
randomize: boolean("randomize").notNull().default(false), randomize: boolean("randomize").notNull().default(false),

View File

@@ -201,7 +201,7 @@ const { handler, api } = betterAuth({
const host = const host =
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? "http://localhost:3000" ? "http://localhost:3000"
: "https://dokploy.com"; : "https://app.dokploy.com";
const inviteLink = `${host}/invitation?token=${data.id}`; const inviteLink = `${host}/invitation?token=${data.id}`;
await sendEmail({ await sendEmail({

View File

@@ -356,6 +356,7 @@ export const deployRemoteCompose = async ({
deployment.logPath, deployment.logPath,
true, true,
); );
console.log(command);
} else if (compose.sourceType === "raw") { } else if (compose.sourceType === "raw") {
command += getCreateComposeFileCommand(compose, deployment.logPath); command += getCreateComposeFileCommand(compose, deployment.logPath);
} else if (compose.sourceType === "gitea") { } else if (compose.sourceType === "gitea") {

View File

@@ -144,7 +144,8 @@ export const updateMount = async (
await deleteFileMount(mountId); await deleteFileMount(mountId);
await createFileMount(mountId); await createFileMount(mountId);
} }
return mount;
return await findMountById(mountId);
}); });
}; };

View File

@@ -76,7 +76,7 @@ CURRENT_USER=$USER
echo "Installing requirements for: OS: $OS_TYPE" echo "Installing requirements for: OS: $OS_TYPE"
if [ $EUID != 0 ]; then if [ $EUID != 0 ]; then
echo "Please run this script as root or with sudo ❌" echo "Please run this script as root or with sudo ❌"
exit exit
fi fi
@@ -263,7 +263,7 @@ const setupMainDirectory = () => `
# Create the /etc/dokploy directory # Create the /etc/dokploy directory
mkdir -p /etc/dokploy mkdir -p /etc/dokploy
chmod 777 /etc/dokploy chmod 777 /etc/dokploy
echo "Directory /etc/dokploy created ✅" echo "Directory /etc/dokploy created ✅"
fi fi
`; `;
@@ -276,16 +276,16 @@ export const setupSwarm = () => `
# Get IP address # Get IP address
get_ip() { get_ip() {
local ip="" local ip=""
# Try IPv4 with multiple services # Try IPv4 with multiple services
# First attempt: ifconfig.io # First attempt: ifconfig.io
ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null) ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
# Second attempt: icanhazip.com # Second attempt: icanhazip.com
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null) ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
fi fi
# Third attempt: ipecho.net # Third attempt: ipecho.net
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
@@ -295,12 +295,12 @@ export const setupSwarm = () => `
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
# Try IPv6 with ifconfig.io # Try IPv6 with ifconfig.io
ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null) ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
# Try IPv6 with icanhazip.com # Try IPv6 with icanhazip.com
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null) ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
fi fi
# Try IPv6 with ipecho.net # Try IPv6 with ipecho.net
if [ -z "\$ip" ]; then if [ -z "\$ip" ]; then
ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
@@ -549,7 +549,7 @@ export const createTraefikInstance = () => {
sleep 8 sleep 8
echo "Traefik migrated to Standalone ✅" echo "Traefik migrated to Standalone ✅"
fi fi
if docker inspect dokploy-traefik > /dev/null 2>&1; then if docker inspect dokploy-traefik > /dev/null 2>&1; then
echo "Traefik already exists ✅" echo "Traefik already exists ✅"
else else
@@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then if command_exists nixpacks; then
echo "Nixpacks already installed ✅" echo "Nixpacks already installed ✅"
else else
export NIXPACKS_VERSION=1.29.1 export NIXPACKS_VERSION=1.35.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅" echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi fi

View File

@@ -1,4 +1,4 @@
import { randomBytes } from "node:crypto"; import { randomBytes, createHmac } from "node:crypto";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
@@ -24,6 +24,12 @@ export interface Template {
domains: DomainSchema[]; domains: DomainSchema[];
} }
export interface GenerateJWTOptions {
length?: number;
secret?: string;
payload?: Record<string, unknown> | undefined;
}
export const generateRandomDomain = ({ export const generateRandomDomain = ({
serverIp, serverIp,
projectName, projectName,
@@ -61,8 +67,48 @@ export function generateBase64(bytes = 32): string {
return randomBytes(bytes).toString("base64"); return randomBytes(bytes).toString("base64");
} }
export function generateJwt(length = 256): string { function safeBase64(str: string): string {
return randomBytes(length).toString("hex"); return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
function objToJWTBase64(obj: any): string {
return safeBase64(
Buffer.from(JSON.stringify(obj), "utf8").toString("base64"),
);
}
export function generateJwt(options: GenerateJWTOptions = {}): string {
let { length, secret, payload = {} } = options;
if (length) {
return randomBytes(length).toString("hex");
}
const encodedHeader = objToJWTBase64({
alg: "HS256",
typ: "JWT",
});
if (!payload.iss) {
payload.iss = "dokploy";
}
if (!payload.iat) {
payload.iat = Math.floor(Date.now() / 1000);
}
if (!payload.exp) {
payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000);
}
const encodedPayload = objToJWTBase64({
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000),
...payload,
});
if (!secret) {
secret = randomBytes(32).toString("hex");
}
const signature = safeBase64(
createHmac("SHA256", secret)
.update(`${encodedHeader}.${encodedPayload}`)
.digest("base64"),
);
return `${encodedHeader}.${encodedPayload}.${signature}`;
} }
/** /**

View File

@@ -65,7 +65,7 @@ export interface Template {
/** /**
* Process a string value and replace variables * Process a string value and replace variables
*/ */
function processValue( export function processValue(
value: string, value: string,
variables: Record<string, string>, variables: Record<string, string>,
schema: Schema, schema: Schema,
@@ -84,11 +84,11 @@ function processValue(
const length = Number.parseInt(varName.split(":")[1], 10) || 32; const length = Number.parseInt(varName.split(":")[1], 10) || 32;
return generateBase64(length); return generateBase64(length);
} }
if (varName.startsWith("password:")) { if (varName.startsWith("password:")) {
const length = Number.parseInt(varName.split(":")[1], 10) || 16; const length = Number.parseInt(varName.split(":")[1], 10) || 16;
return generatePassword(length); return generatePassword(length);
} }
if (varName === "password") { if (varName === "password") {
return generatePassword(16); return generatePassword(16);
} }
@@ -97,14 +97,31 @@ function processValue(
const length = Number.parseInt(varName.split(":")[1], 10) || 8; const length = Number.parseInt(varName.split(":")[1], 10) || 8;
return generateHash(length); return generateHash(length);
} }
if (varName === "hash") {
return generateHash();
}
if (varName === "uuid") { if (varName === "uuid") {
return crypto.randomUUID(); return crypto.randomUUID();
} }
if (varName === "timestamp") { if (varName === "timestamp" || varName === "timestampms") {
return Date.now().toString(); return Date.now().toString();
} }
if (varName === "timestamps") {
return Math.round(Date.now() / 1000).toString();
}
if (varName.startsWith("timestampms:")) {
return new Date(varName.slice(12)).getTime().toString();
}
if (varName.startsWith("timestamps:")) {
return Math.round(
new Date(varName.slice(11)).getTime() / 1000,
).toString();
}
if (varName === "randomPort") { if (varName === "randomPort") {
return Math.floor(Math.random() * 65535).toString(); return Math.floor(Math.random() * 65535).toString();
} }
@@ -114,8 +131,34 @@ function processValue(
} }
if (varName.startsWith("jwt:")) { if (varName.startsWith("jwt:")) {
const length = Number.parseInt(varName.split(":")[1], 10) || 256; const params: string[] = varName.split(":").slice(1);
return generateJwt(length); if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) {
return generateJwt({ length: Number.parseInt(params[0], 10) });
}
let [secret, payload] = params;
if (typeof payload === "string" && variables[payload]) {
payload = variables[payload];
}
if (
typeof payload === "string" &&
payload.startsWith("{") &&
payload.endsWith("}")
) {
try {
payload = JSON.parse(payload);
} catch (e) {
// If payload is not a valid JSON, invalid it
payload = undefined;
console.error("Invalid JWT payload", e);
}
}
if (typeof payload !== "object") {
payload = undefined;
}
return generateJwt({
secret: secret ? variables[secret] || secret : undefined,
payload: payload as any,
});
} }
if (varName === "username") { if (varName === "username") {
@@ -147,7 +190,7 @@ export function processVariables(
): Record<string, string> { ): Record<string, string> {
const variables: Record<string, string> = {}; const variables: Record<string, string> = {};
// First pass: Process variables that don't depend on other variables // First pass: Process some variables that don't depend on other variables
for (const [key, value] of Object.entries(template.variables)) { for (const [key, value] of Object.entries(template.variables)) {
if (typeof value !== "string") continue; if (typeof value !== "string") continue;
@@ -161,6 +204,8 @@ export function processVariables(
const match = value.match(/\${password:(\d+)}/); const match = value.match(/\${password:(\d+)}/);
const length = match?.[1] ? Number.parseInt(match[1], 10) : 16; const length = match?.[1] ? Number.parseInt(match[1], 10) : 16;
variables[key] = generatePassword(length); variables[key] = generatePassword(length);
} else if (value === "${hash}") {
variables[key] = generateHash();
} else if (value.startsWith("${hash:")) { } else if (value.startsWith("${hash:")) {
const match = value.match(/\${hash:(\d+)}/); const match = value.match(/\${hash:(\d+)}/);
const length = match?.[1] ? Number.parseInt(match[1], 10) : 8; const length = match?.[1] ? Number.parseInt(match[1], 10) : 8;

View File

@@ -40,68 +40,84 @@ export const sendDokployRestartNotifications = async () => {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim(); `${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, { try {
title: decorate(">", "`✅` Dokploy Server Restarted"), await sendDiscordNotification(discord, {
color: 0x57f287, title: decorate(">", "`✅` Dokploy Server Restarted"),
fields: [ color: 0x57f287,
{ fields: [
name: decorate("`📅`", "Date"), {
value: `<t:${unixDate}:D>`, name: decorate("`📅`", "Date"),
inline: true, value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
}, },
{ });
name: decorate("`⌚`", "Time"), } catch (error) {
value: `<t:${unixDate}:t>`, console.log(error);
inline: true, }
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Restart Notification",
},
});
} }
if (gotify) { if (gotify) {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`; `${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification( try {
gotify, await sendGotifyNotification(
decorate("✅", "Dokploy Server Restarted"), gotify,
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`, decorate("", "Dokploy Server Restarted"),
); `${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
} catch (error) {
console.log(error);
}
} }
if (telegram) { if (telegram) {
await sendTelegramNotification( try {
telegram, await sendTelegramNotification(
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`, telegram,
); `<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
} catch (error) {
console.log(error);
}
} }
if (slack) { if (slack) {
const { channel } = slack; const { channel } = slack;
await sendSlackNotification(slack, { try {
channel: channel, await sendSlackNotification(slack, {
attachments: [ channel: channel,
{ attachments: [
color: "#00FF00", {
pretext: ":white_check_mark: *Dokploy Server Restarted*", color: "#00FF00",
fields: [ pretext: ":white_check_mark: *Dokploy Server Restarted*",
{ fields: [
title: "Time", {
value: date.toLocaleString(), title: "Time",
short: true, value: date.toLocaleString(),
}, short: true,
], },
}, ],
], },
}); ],
});
} catch (error) {
console.log(error);
}
} }
} }
}; };

View File

@@ -37,6 +37,7 @@ export const cloneBitbucketRepository = async (
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
bitbucket, bitbucket,
enableSubmodules,
} = entity; } = entity;
if (!bitbucketId) { if (!bitbucketId) {
@@ -53,25 +54,23 @@ export const cloneBitbucketRepository = async (
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`; const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
try { try {
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync( const cloneArgs = [
"git", "clone",
[ "--branch",
"clone", bitbucketBranch!,
"--branch", "--depth",
bitbucketBranch!, "1",
"--depth", ...(enableSubmodules ? ["--recurse-submodules"] : []),
"1", cloneUrl,
"--recurse-submodules", outputPath,
cloneUrl, "--progress",
outputPath, ];
"--progress",
], await spawnAsync("git", cloneArgs, (data) => {
(data) => { if (writeStream.writable) {
if (writeStream.writable) { writeStream.write(data);
writeStream.write(data); }
} });
},
);
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
} catch (error) { } catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`); writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -89,6 +88,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
bitbucketOwner, bitbucketOwner,
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
enableSubmodules,
} = entity; } = entity;
if (!bitbucketId) { if (!bitbucketId) {
@@ -106,17 +106,19 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`; const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try { try {
await spawnAsync("git", [ const cloneArgs = [
"clone", "clone",
"--branch", "--branch",
bitbucketBranch!, bitbucketBranch!,
"--depth", "--depth",
"1", "1",
"--recurse-submodules", ...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
]); ];
await spawnAsync("git", cloneArgs);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -131,6 +133,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
serverId, serverId,
enableSubmodules,
} = compose; } = compose;
if (!serverId) { if (!serverId) {
@@ -153,11 +156,11 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`; const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
try { try {
const command = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, cloneCommand);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -176,6 +179,7 @@ export const getBitbucketCloneCommand = async (
bitbucketBranch, bitbucketBranch,
bitbucketId, bitbucketId,
serverId, serverId,
enableSubmodules,
} = entity; } = entity;
if (!serverId) { if (!serverId) {
@@ -207,7 +211,7 @@ export const getBitbucketCloneCommand = async (
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath}; echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1; exit 1;
fi fi

View File

@@ -17,12 +17,19 @@ export const cloneGitRepository = async (
customGitUrl?: string | null; customGitUrl?: string | null;
customGitBranch?: string | null; customGitBranch?: string | null;
customGitSSHKeyId?: string | null; customGitSSHKeyId?: string | null;
enableSubmodules?: boolean;
}, },
logPath: string, logPath: string,
isCompose = false, isCompose = false,
) => { ) => {
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(); const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity; const {
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
throw new TRPCError({ throw new TRPCError({
@@ -70,19 +77,21 @@ export const cloneGitRepository = async (
} }
const { port } = sanitizeRepoPathSSH(customGitUrl); const { port } = sanitizeRepoPathSSH(customGitUrl);
const cloneArgs = [
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
customGitUrl,
outputPath,
"--progress",
];
await spawnAsync( await spawnAsync(
"git", "git",
[ cloneArgs,
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
"--recurse-submodules",
customGitUrl,
outputPath,
"--progress",
],
(data) => { (data) => {
if (writeStream.writable) { if (writeStream.writable) {
writeStream.write(data); writeStream.write(data);
@@ -114,6 +123,7 @@ export const getCustomGitCloneCommand = async (
customGitBranch?: string | null; customGitBranch?: string | null;
customGitSSHKeyId?: string | null; customGitSSHKeyId?: string | null;
serverId: string | null; serverId: string | null;
enableSubmodules: boolean;
}, },
logPath: string, logPath: string,
isCompose = false, isCompose = false,
@@ -125,6 +135,7 @@ export const getCustomGitCloneCommand = async (
customGitBranch, customGitBranch,
customGitSSHKeyId, customGitSSHKeyId,
serverId, serverId,
enableSubmodules,
} = entity; } = entity;
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
@@ -181,7 +192,7 @@ export const getCustomGitCloneCommand = async (
} }
command.push( command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath}; echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
exit 1; exit 1;
fi fi
@@ -261,8 +272,15 @@ export const cloneGitRawRepository = async (entity: {
customGitUrl?: string | null; customGitUrl?: string | null;
customGitBranch?: string | null; customGitBranch?: string | null;
customGitSSHKeyId?: string | null; customGitSSHKeyId?: string | null;
enableSubmodules?: boolean;
}) => { }) => {
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity; const {
appName,
customGitUrl,
customGitBranch,
customGitSSHKeyId,
enableSubmodules,
} = entity;
if (!customGitUrl || !customGitBranch) { if (!customGitUrl || !customGitBranch) {
throw new TRPCError({ throw new TRPCError({
@@ -307,29 +325,26 @@ export const cloneGitRawRepository = async (entity: {
} }
const { port } = sanitizeRepoPathSSH(customGitUrl); const { port } = sanitizeRepoPathSSH(customGitUrl);
await spawnAsync( const cloneArgs = [
"git", "clone",
[ "--branch",
"clone", customGitBranch,
"--branch", "--depth",
customGitBranch, "1",
"--depth", ...(enableSubmodules ? ["--recurse-submodules"] : []),
"1", customGitUrl,
"--recurse-submodules", outputPath,
customGitUrl, "--progress",
outputPath, ];
"--progress",
], await spawnAsync("git", cloneArgs, (_data) => {}, {
(_data) => {}, env: {
{ ...process.env,
env: { ...(customGitSSHKeyId && {
...process.env, GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
...(customGitSSHKeyId && { }),
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
}, },
); });
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -342,6 +357,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
customGitUrl, customGitUrl,
customGitSSHKeyId, customGitSSHKeyId,
serverId, serverId,
enableSubmodules,
} = compose; } = compose;
if (!serverId) { if (!serverId) {
@@ -396,7 +412,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
} }
command.push( command.push(
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} ; then `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then
echo "[ERROR] Fail to clone the repository "; echo "[ERROR] Fail to clone the repository ";
exit 1; exit 1;
fi fi

View File

@@ -119,6 +119,7 @@ export const getGiteaCloneCommand = async (
giteaRepository, giteaRepository,
serverId, serverId,
gitea, gitea,
enableSubmodules,
} = entity; } = entity;
if (!serverId) { if (!serverId) {
@@ -155,7 +156,7 @@ export const getGiteaCloneCommand = async (
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${giteaBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath}; echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
exit 1; exit 1;
fi fi
@@ -174,7 +175,14 @@ export const cloneGiteaRepository = async (
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, giteaBranch, giteaId, giteaOwner, giteaRepository } = entity; const {
appName,
giteaBranch,
giteaId,
giteaOwner,
giteaRepository,
enableSubmodules,
} = entity;
if (!giteaId) { if (!giteaId) {
throw new TRPCError({ throw new TRPCError({
@@ -211,7 +219,7 @@ export const cloneGiteaRepository = async (
giteaBranch!, giteaBranch!,
"--depth", "--depth",
"1", "1",
"--recurse-submodules", ...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
@@ -232,7 +240,14 @@ export const cloneGiteaRepository = async (
}; };
export const cloneRawGiteaRepository = async (entity: Compose) => { export const cloneRawGiteaRepository = async (entity: Compose) => {
const { appName, giteaRepository, giteaOwner, giteaBranch, giteaId } = entity; const {
appName,
giteaRepository,
giteaOwner,
giteaBranch,
giteaId,
enableSubmodules,
} = entity;
const { COMPOSE_PATH } = paths(); const { COMPOSE_PATH } = paths();
if (!giteaId) { if (!giteaId) {
@@ -265,7 +280,7 @@ export const cloneRawGiteaRepository = async (entity: Compose) => {
giteaBranch!, giteaBranch!,
"--depth", "--depth",
"1", "1",
"--recurse-submodules", ...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
@@ -283,6 +298,7 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
giteaBranch, giteaBranch,
giteaId, giteaId,
serverId, serverId,
enableSubmodules,
} = compose; } = compose;
if (!serverId) { if (!serverId) {
@@ -307,7 +323,7 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${giteaBranch} --depth 1 ${cloneUrl} ${outputPath} git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {

View File

@@ -83,6 +83,7 @@ interface CloneGithubRepository {
repository: string | null; repository: string | null;
logPath: string; logPath: string;
type?: "application" | "compose"; type?: "application" | "compose";
enableSubmodules: boolean;
} }
export const cloneGithubRepository = async ({ export const cloneGithubRepository = async ({
logPath, logPath,
@@ -92,7 +93,8 @@ export const cloneGithubRepository = async ({
const isCompose = type === "compose"; const isCompose = type === "compose";
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, repository, owner, branch, githubId } = entity; const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) { if (!githubId) {
throw new TRPCError({ throw new TRPCError({
@@ -128,25 +130,23 @@ export const cloneGithubRepository = async ({
try { try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync( const cloneArgs = [
"git", "clone",
[ "--branch",
"clone", branch!,
"--branch", "--depth",
branch!, "1",
"--depth", ...(enableSubmodules ? ["--recurse-submodules"] : []),
"1", cloneUrl,
"--recurse-submodules", outputPath,
cloneUrl, "--progress",
outputPath, ];
"--progress",
], await spawnAsync("git", cloneArgs, (data) => {
(data) => { if (writeStream.writable) {
if (writeStream.writable) { writeStream.write(data);
writeStream.write(data); }
} });
},
);
writeStream.write(`\nCloned ${repoclone}: ✅\n`); writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) { } catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`); writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -161,7 +161,15 @@ export const getGithubCloneCommand = async ({
type = "application", type = "application",
...entity ...entity
}: CloneGithubRepository & { serverId: string }) => { }: CloneGithubRepository & { serverId: string }) => {
const { appName, repository, owner, branch, githubId, serverId } = entity; const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = entity;
const isCompose = type === "compose"; const isCompose = type === "compose";
if (!serverId) { if (!serverId) {
throw new TRPCError({ throw new TRPCError({
@@ -216,7 +224,7 @@ export const getGithubCloneCommand = async ({
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath}; echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
exit 1; exit 1;
fi fi
@@ -227,7 +235,8 @@ echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
}; };
export const cloneRawGithubRepository = async (entity: Compose) => { export const cloneRawGithubRepository = async (entity: Compose) => {
const { appName, repository, owner, branch, githubId } = entity; const { appName, repository, owner, branch, githubId, enableSubmodules } =
entity;
if (!githubId) { if (!githubId) {
throw new TRPCError({ throw new TRPCError({
@@ -245,24 +254,33 @@ export const cloneRawGithubRepository = async (entity: Compose) => {
await recreateDirectory(outputPath); await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`; const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try { try {
await spawnAsync("git", [ const cloneArgs = [
"clone", "clone",
"--branch", "--branch",
branch!, branch!,
"--depth", "--depth",
"1", "1",
"--recurse-submodules", ...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
]); ];
await spawnAsync("git", cloneArgs);
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => { export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
const { appName, repository, owner, branch, githubId, serverId } = compose; const {
appName,
repository,
owner,
branch,
githubId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) { if (!serverId) {
throw new TRPCError({ throw new TRPCError({
@@ -288,7 +306,7 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath} git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {

View File

@@ -90,8 +90,14 @@ export const cloneGitlabRepository = async (
isCompose = false, isCompose = false,
) => { ) => {
const writeStream = createWriteStream(logPath, { flags: "a" }); const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, gitlabBranch, gitlabId, gitlab, gitlabPathNamespace } = const {
entity; appName,
gitlabBranch,
gitlabId,
gitlab,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) { if (!gitlabId) {
throw new TRPCError({ throw new TRPCError({
@@ -127,25 +133,23 @@ export const cloneGitlabRepository = async (
try { try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`); writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync( const cloneArgs = [
"git", "clone",
[ "--branch",
"clone", gitlabBranch!,
"--branch", "--depth",
gitlabBranch!, "1",
"--depth", ...(enableSubmodules ? ["--recurse-submodules"] : []),
"1", cloneUrl,
"--recurse-submodules", outputPath,
cloneUrl, "--progress",
outputPath, ];
"--progress",
], await spawnAsync("git", cloneArgs, (data) => {
(data) => { if (writeStream.writable) {
if (writeStream.writable) { writeStream.write(data);
writeStream.write(data); }
} });
},
);
writeStream.write(`\nCloned ${repoclone}: ✅\n`); writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) { } catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`); writeStream.write(`ERROR Clonning: ${error}: ❌`);
@@ -167,6 +171,7 @@ export const getGitlabCloneCommand = async (
gitlabId, gitlabId,
serverId, serverId,
gitlab, gitlab,
enableSubmodules,
} = entity; } = entity;
if (!serverId) { if (!serverId) {
@@ -222,7 +227,7 @@ export const getGitlabCloneCommand = async (
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
mkdir -p ${outputPath}; mkdir -p ${outputPath};
if ! git clone --branch ${gitlabBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then if ! git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath}; echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1; exit 1;
fi fi
@@ -330,7 +335,13 @@ export const getGitlabBranches = async (input: {
}; };
export const cloneRawGitlabRepository = async (entity: Compose) => { export const cloneRawGitlabRepository = async (entity: Compose) => {
const { appName, gitlabBranch, gitlabId, gitlabPathNamespace } = entity; const {
appName,
gitlabBranch,
gitlabId,
gitlabPathNamespace,
enableSubmodules,
} = entity;
if (!gitlabId) { if (!gitlabId) {
throw new TRPCError({ throw new TRPCError({
@@ -351,24 +362,32 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`; const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
try { try {
await spawnAsync("git", [ const cloneArgs = [
"clone", "clone",
"--branch", "--branch",
gitlabBranch!, gitlabBranch!,
"--depth", "--depth",
"1", "1",
"--recurse-submodules", ...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl, cloneUrl,
outputPath, outputPath,
"--progress", "--progress",
]); ];
await spawnAsync("git", cloneArgs);
} catch (error) { } catch (error) {
throw error; throw error;
} }
}; };
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => { export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
const { appName, gitlabPathNamespace, branch, gitlabId, serverId } = compose; const {
appName,
gitlabPathNamespace,
branch,
gitlabId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) { if (!serverId) {
throw new TRPCError({ throw new TRPCError({
@@ -392,7 +411,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath} git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`; `;
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} catch (error) { } catch (error) {