mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-18 03:02:56 +00:00
Merge pull request #4978 from Dokploy/hotfix/v0.29.14
Hotfix v0.29.14: backport 20 bug fixes
This commit is contained in:
@@ -0,0 +1,104 @@
|
|||||||
|
import type { ApplicationNested } from "@dokploy/server/utils/builders";
|
||||||
|
import { getRailpackCommand } from "@dokploy/server/utils/builders/railpack";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const createApplication = (
|
||||||
|
overrides: Partial<ApplicationNested> = {},
|
||||||
|
): ApplicationNested =>
|
||||||
|
({
|
||||||
|
appName: "test-app",
|
||||||
|
buildType: "railpack",
|
||||||
|
sourceType: "git",
|
||||||
|
buildPath: "/",
|
||||||
|
railpackVersion: "0.15.4",
|
||||||
|
env: "TEST_VAR=one",
|
||||||
|
cleanCache: false,
|
||||||
|
environment: {
|
||||||
|
project: {
|
||||||
|
env: "",
|
||||||
|
},
|
||||||
|
env: "",
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
}) as unknown as ApplicationNested;
|
||||||
|
|
||||||
|
const getSecretsHash = (command: string) => {
|
||||||
|
const match = command.match(/secrets-hash=([a-f0-9]{64})/);
|
||||||
|
if (!match?.[1]) {
|
||||||
|
throw new Error("secrets-hash build arg was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("getRailpackCommand", () => {
|
||||||
|
it("includes secrets-hash without clean cache", () => {
|
||||||
|
const command = getRailpackCommand(createApplication());
|
||||||
|
|
||||||
|
expect(command).toContain("--build-arg secrets-hash=");
|
||||||
|
expect(command).not.toContain("cache-key=");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes cache-key only when clean cache is enabled", () => {
|
||||||
|
const command = getRailpackCommand(
|
||||||
|
createApplication({
|
||||||
|
cleanCache: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(command).toContain("--build-arg secrets-hash=");
|
||||||
|
expect(command).toContain("--build-arg cache-key=");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes secrets-hash when an environment value changes", () => {
|
||||||
|
const firstCommand = getRailpackCommand(
|
||||||
|
createApplication({
|
||||||
|
env: "TEST_VAR=one",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const secondCommand = getRailpackCommand(
|
||||||
|
createApplication({
|
||||||
|
env: "TEST_VAR=two",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getSecretsHash(firstCommand)).not.toEqual(
|
||||||
|
getSecretsHash(secondCommand),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes secrets-hash when referenced project or environment values change", () => {
|
||||||
|
const firstCommand = getRailpackCommand(
|
||||||
|
createApplication({
|
||||||
|
env: [
|
||||||
|
"PROJECT_VALUE=${{project.SHARED_VALUE}}",
|
||||||
|
"ENVIRONMENT_VALUE=${{environment.SHARED_VALUE}}",
|
||||||
|
].join("\n"),
|
||||||
|
environment: {
|
||||||
|
project: {
|
||||||
|
env: "SHARED_VALUE=one",
|
||||||
|
},
|
||||||
|
env: "SHARED_VALUE=alpha",
|
||||||
|
},
|
||||||
|
} as Partial<ApplicationNested>),
|
||||||
|
);
|
||||||
|
const secondCommand = getRailpackCommand(
|
||||||
|
createApplication({
|
||||||
|
env: [
|
||||||
|
"PROJECT_VALUE=${{project.SHARED_VALUE}}",
|
||||||
|
"ENVIRONMENT_VALUE=${{environment.SHARED_VALUE}}",
|
||||||
|
].join("\n"),
|
||||||
|
environment: {
|
||||||
|
project: {
|
||||||
|
env: "SHARED_VALUE=two",
|
||||||
|
},
|
||||||
|
env: "SHARED_VALUE=beta",
|
||||||
|
},
|
||||||
|
} as Partial<ApplicationNested>),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getSecretsHash(firstCommand)).not.toEqual(
|
||||||
|
getSecretsHash(secondCommand),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { prepareEnvironmentVariables } from "@dokploy/server/index";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const projectEnv = `
|
||||||
|
ENVIRONMENT=staging
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||||
|
`;
|
||||||
|
|
||||||
|
const environmentEnv = `
|
||||||
|
NODE_ENV=production
|
||||||
|
POSTGRES_HOST=postgres.internal
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
REDIS_URL=redis://redis.internal:6379
|
||||||
|
`;
|
||||||
|
|
||||||
|
const serviceEnv = `
|
||||||
|
NODE_ENV=\${{environment.NODE_ENV}}
|
||||||
|
REDIS_URL=\${{environment.REDIS_URL}}
|
||||||
|
PORT=3000
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A rollback replays the snapshot stored in `rollbacks.fullContext`, which keeps
|
||||||
|
* the service env, the environment env and the project env captured at deploy time.
|
||||||
|
*/
|
||||||
|
const fullContext = {
|
||||||
|
env: serviceEnv,
|
||||||
|
environment: {
|
||||||
|
env: environmentEnv,
|
||||||
|
project: {
|
||||||
|
env: projectEnv,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("prepareEnvironmentVariables for application rollback", () => {
|
||||||
|
it("resolves environment variables from the rollback snapshot", () => {
|
||||||
|
const result = prepareEnvironmentVariables(
|
||||||
|
fullContext.env,
|
||||||
|
fullContext.environment.project.env,
|
||||||
|
fullContext.environment.env,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
"NODE_ENV=production",
|
||||||
|
"REDIS_URL=redis://redis.internal:6379",
|
||||||
|
"PORT=3000",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves project and environment variables together on rollback", () => {
|
||||||
|
const rollbackEnv = `
|
||||||
|
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||||
|
POSTGRES_URL=postgres://\${{environment.POSTGRES_HOST}}:\${{environment.POSTGRES_PORT}}/app
|
||||||
|
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = prepareEnvironmentVariables(
|
||||||
|
rollbackEnv,
|
||||||
|
projectEnv,
|
||||||
|
environmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
|
"POSTGRES_URL=postgres://postgres.internal:5432/app",
|
||||||
|
"ENVIRONMENT=staging",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when the environment env of the snapshot is not passed", () => {
|
||||||
|
expect(() =>
|
||||||
|
prepareEnvironmentVariables(fullContext.env, projectEnv),
|
||||||
|
).toThrow("Invalid environment variable: environment.NODE_ENV");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maintains precedence: service > environment > project on rollback", () => {
|
||||||
|
const conflictingProjectEnv = `
|
||||||
|
NODE_ENV=project
|
||||||
|
API_URL=https://project.api.com
|
||||||
|
`;
|
||||||
|
|
||||||
|
const conflictingEnvironmentEnv = `
|
||||||
|
NODE_ENV=environment
|
||||||
|
API_URL=https://environment.api.com
|
||||||
|
`;
|
||||||
|
|
||||||
|
const rollbackEnv = `
|
||||||
|
NODE_ENV=service
|
||||||
|
PROJECT_API_URL=\${{project.API_URL}}
|
||||||
|
ENVIRONMENT_API_URL=\${{environment.API_URL}}
|
||||||
|
SELF_REFERENCE=\${{NODE_ENV}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = prepareEnvironmentVariables(
|
||||||
|
rollbackEnv,
|
||||||
|
conflictingProjectEnv,
|
||||||
|
conflictingEnvironmentEnv,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
"NODE_ENV=service",
|
||||||
|
"PROJECT_API_URL=https://project.api.com",
|
||||||
|
"ENVIRONMENT_API_URL=https://environment.api.com",
|
||||||
|
"SELF_REFERENCE=service",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -55,6 +55,28 @@ describe("processLogs", () => {
|
|||||||
expect(result.data).toHaveLength(2);
|
expect(result.data).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not throw when filtering by hostname and an entry has no RequestHost", () => {
|
||||||
|
const entryWithoutRequestHost = sampleLogEntry.replace(
|
||||||
|
/"RequestHost":"[^"]*",/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
const mixedEntries = `${sampleLogEntry}\n${entryWithoutRequestHost}`;
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
parseRawConfig(mixedEntries, undefined, undefined, "traefik.me"),
|
||||||
|
).not.toThrow();
|
||||||
|
|
||||||
|
const result = parseRawConfig(
|
||||||
|
mixedEntries,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"traefik.me",
|
||||||
|
);
|
||||||
|
expect(result.totalCount).toBe(1);
|
||||||
|
expect(result.data[0]?.RequestHost).toBe("s222-umami-c381af.traefik.me");
|
||||||
|
});
|
||||||
|
|
||||||
it("should filter out Dokploy dashboard requests", () => {
|
it("should filter out Dokploy dashboard requests", () => {
|
||||||
const dokployDashboardEntry = `{"ClientAddr":"172.71.187.131:9485","ClientHost":"172.71.187.131","ClientPort":"9485","ClientUsername":"-","DownstreamContentSize":14550,"DownstreamStatus":200,"Duration":57681682,"OriginContentSize":14550,"OriginDuration":57612242,"OriginStatus":200,"Overhead":69440,"RequestAddr":"hostinger.dokploy.com","RequestContentSize":0,"RequestCount":20142,"RequestHost":"hostinger.dokploy.com","RequestMethod":"GET","RequestPath":"/_next/data/cb_zzI4Rp9G7Q7djrFKh0/en/dashboard/traefik.json","RequestPort":"-","RequestProtocol":"HTTP/2.0","RequestScheme":"https","RetryAttempts":0,"RouterName":"dokploy-router-app-secure@file","ServiceAddr":"dokploy:3000","ServiceName":"dokploy-service-app@file","ServiceURL":"http://dokploy:3000","StartLocal":"2025-12-10T05:10:41.957755949Z","StartUTC":"2025-12-10T05:10:41.957755949Z","TLSCipher":"TLS_AES_128_GCM_SHA256","TLSVersion":"1.3","entryPointName":"websecure","level":"info","msg":"","time":"2025-12-10T05:10:42Z"}`;
|
const dokployDashboardEntry = `{"ClientAddr":"172.71.187.131:9485","ClientHost":"172.71.187.131","ClientPort":"9485","ClientUsername":"-","DownstreamContentSize":14550,"DownstreamStatus":200,"Duration":57681682,"OriginContentSize":14550,"OriginDuration":57612242,"OriginStatus":200,"Overhead":69440,"RequestAddr":"hostinger.dokploy.com","RequestContentSize":0,"RequestCount":20142,"RequestHost":"hostinger.dokploy.com","RequestMethod":"GET","RequestPath":"/_next/data/cb_zzI4Rp9G7Q7djrFKh0/en/dashboard/traefik.json","RequestPort":"-","RequestProtocol":"HTTP/2.0","RequestScheme":"https","RetryAttempts":0,"RouterName":"dokploy-router-app-secure@file","ServiceAddr":"dokploy:3000","ServiceName":"dokploy-service-app@file","ServiceURL":"http://dokploy:3000","StartLocal":"2025-12-10T05:10:41.957755949Z","StartUTC":"2025-12-10T05:10:41.957755949Z","TLSCipher":"TLS_AES_128_GCM_SHA256","TLSVersion":"1.3","entryPointName":"websecure","level":"info","msg":"","time":"2025-12-10T05:10:42Z"}`;
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ export const ShowDeployments = ({
|
|||||||
const [activeLog, setActiveLog] = useState<
|
const [activeLog, setActiveLog] = useState<
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
RouterOutputs["deployment"]["all"][number] | null
|
||||||
>(null);
|
>(null);
|
||||||
|
const [removingDeploymentIds, setRemovingDeploymentIds] = useState<
|
||||||
|
Set<string>
|
||||||
|
>(new Set());
|
||||||
const { data: deployments, isPending: isLoadingDeployments } =
|
const { data: deployments, isPending: isLoadingDeployments } =
|
||||||
api.deployment.allByType.useQuery(
|
api.deployment.allByType.useQuery(
|
||||||
{
|
{
|
||||||
@@ -81,7 +84,7 @@ export const ShowDeployments = ({
|
|||||||
api.rollback.rollback.useMutation();
|
api.rollback.rollback.useMutation();
|
||||||
const { mutateAsync: killProcess, isPending: isKillingProcess } =
|
const { mutateAsync: killProcess, isPending: isKillingProcess } =
|
||||||
api.deployment.killProcess.useMutation();
|
api.deployment.killProcess.useMutation();
|
||||||
const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } =
|
const { mutateAsync: removeDeployment } =
|
||||||
api.deployment.removeDeployment.useMutation();
|
api.deployment.removeDeployment.useMutation();
|
||||||
|
|
||||||
// Cancel deployment mutations
|
// Cancel deployment mutations
|
||||||
@@ -408,6 +411,11 @@ export const ShowDeployments = ({
|
|||||||
description="Are you sure you want to delete this deployment? This action cannot be undone."
|
description="Are you sure you want to delete this deployment? This action cannot be undone."
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
setRemovingDeploymentIds((deploymentIds) => {
|
||||||
|
const nextDeploymentIds = new Set(deploymentIds);
|
||||||
|
nextDeploymentIds.add(deployment.deploymentId);
|
||||||
|
return nextDeploymentIds;
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await removeDeployment({
|
await removeDeployment({
|
||||||
deploymentId: deployment.deploymentId,
|
deploymentId: deployment.deploymentId,
|
||||||
@@ -415,13 +423,25 @@ export const ShowDeployments = ({
|
|||||||
toast.success("Deployment deleted successfully");
|
toast.success("Deployment deleted successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Error deleting deployment");
|
toast.error("Error deleting deployment");
|
||||||
|
} finally {
|
||||||
|
setRemovingDeploymentIds((deploymentIds) => {
|
||||||
|
const nextDeploymentIds = new Set(
|
||||||
|
deploymentIds,
|
||||||
|
);
|
||||||
|
nextDeploymentIds.delete(
|
||||||
|
deployment.deploymentId,
|
||||||
|
);
|
||||||
|
return nextDeploymentIds;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
isLoading={isRemovingDeployment}
|
isLoading={removingDeploymentIds.has(
|
||||||
|
deployment.deploymentId,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
|
|||||||
@@ -209,7 +209,9 @@ export const createColumns = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
{validationState?.error ? (
|
{validationState?.isValid && validationState?.message ? (
|
||||||
|
<p>{validationState.message}</p>
|
||||||
|
) : validationState?.error ? (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="font-medium text-red-500">Error:</p>
|
<p className="font-medium text-red-500">Error:</p>
|
||||||
<p>{validationState.error}</p>
|
<p>{validationState.error}</p>
|
||||||
|
|||||||
@@ -626,7 +626,10 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
{validationState?.error ? (
|
{validationState?.isValid &&
|
||||||
|
validationState?.message ? (
|
||||||
|
<p>{validationState.message}</p>
|
||||||
|
) : validationState?.error ? (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="font-medium text-red-500">
|
<p className="font-medium text-red-500">
|
||||||
Error:
|
Error:
|
||||||
|
|||||||
@@ -60,14 +60,16 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
const currentBuildArgs = form.watch("buildArgs");
|
const currentBuildArgs = form.watch("buildArgs");
|
||||||
const currentBuildSecrets = form.watch("buildSecrets");
|
const currentBuildSecrets = form.watch("buildSecrets");
|
||||||
const currentCreateEnvFile = form.watch("createEnvFile");
|
const currentCreateEnvFile = form.watch("createEnvFile");
|
||||||
|
const { isDirty } = form.formState;
|
||||||
const hasChanges =
|
const hasChanges =
|
||||||
currentEnv !== (data?.env || "") ||
|
currentEnv !== (data?.env || "") ||
|
||||||
currentBuildArgs !== (data?.buildArgs || "") ||
|
currentBuildArgs !== (data?.buildArgs || "") ||
|
||||||
currentBuildSecrets !== (data?.buildSecrets || "") ||
|
currentBuildSecrets !== (data?.buildSecrets || "") ||
|
||||||
currentCreateEnvFile !== (data?.createEnvFile ?? true);
|
currentCreateEnvFile !== (data?.createEnvFile ?? true);
|
||||||
|
|
||||||
|
// Skip reset while editing so background refetches don't wipe edits
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data && !isDirty) {
|
||||||
form.reset({
|
form.reset({
|
||||||
env: data.env || "",
|
env: data.env || "",
|
||||||
buildArgs: data.buildArgs || "",
|
buildArgs: data.buildArgs || "",
|
||||||
@@ -75,7 +77,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
createEnvFile: data.createEnvFile ?? true,
|
createEnvFile: data.createEnvFile ?? true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, isDirty, form]);
|
||||||
|
|
||||||
const onSubmit = async (formData: EnvironmentSchema) => {
|
const onSubmit = async (formData: EnvironmentSchema) => {
|
||||||
mutateAsync({
|
mutateAsync({
|
||||||
@@ -87,6 +89,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Environments Added");
|
toast.success("Environments Added");
|
||||||
|
form.reset(formData);
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
+9
-5
@@ -447,14 +447,18 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -502,14 +506,14 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -242,14 +242,18 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -298,14 +302,14 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-5
@@ -477,14 +477,18 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="size-3 cursor-pointer hover:text-destructive"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
field.onChange(newPaths);
|
field.onChange(newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -531,14 +535,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+15
-7
@@ -438,8 +438,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={(value) => {
|
||||||
defaultValue={field.value}
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -487,14 +491,18 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="size-3 cursor-pointer hover:text-destructive"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
field.onChange(newPaths);
|
field.onChange(newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -543,14 +551,14 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-5
@@ -467,14 +467,18 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="size-3 cursor-pointer hover:text-destructive"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
field.onChange(newPaths);
|
field.onChange(newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -521,14 +525,14 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -58,8 +58,12 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
const handleRunManually = async (scheduleId: string) => {
|
const handleRunManually = async (scheduleId: string) => {
|
||||||
setRunningSchedules((prev) => new Set(prev).add(scheduleId));
|
setRunningSchedules((prev) => new Set(prev).add(scheduleId));
|
||||||
try {
|
try {
|
||||||
await runManually({ scheduleId });
|
const result = await runManually({ scheduleId });
|
||||||
toast.success("Schedule run successfully");
|
if (result.status === "error") {
|
||||||
|
toast.error("Schedule run failed, check the deployment logs");
|
||||||
|
} else {
|
||||||
|
toast.success("Schedule run successfully");
|
||||||
|
}
|
||||||
await refetchSchedules();
|
await refetchSchedules();
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Error running schedule");
|
toast.error("Error running schedule");
|
||||||
|
|||||||
+9
-5
@@ -451,14 +451,18 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -506,14 +510,14 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-5
@@ -251,14 +251,18 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -306,14 +310,14 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-5
@@ -442,14 +442,18 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -497,14 +501,14 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+15
-7
@@ -432,8 +432,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={(value) => {
|
||||||
defaultValue={field.value}
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -479,14 +483,18 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -538,14 +546,14 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+9
-5
@@ -468,14 +468,18 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
{field.value?.map((path, index) => (
|
{field.value?.map((path, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{path}
|
{path}
|
||||||
<X
|
<button
|
||||||
className="ml-1 size-3 cursor-pointer"
|
type="button"
|
||||||
|
aria-label="Remove watch path"
|
||||||
|
className="inline-flex items-center focus-visible:ring-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newPaths = [...(field.value || [])];
|
const newPaths = [...(field.value || [])];
|
||||||
newPaths.splice(index, 1);
|
newPaths.splice(index, 1);
|
||||||
form.setValue("watchPaths", newPaths);
|
form.setValue("watchPaths", newPaths);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<X className="ml-1 size-3 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -523,14 +527,14 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableSubmodules"
|
name="enableSubmodules"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
<FormLabel>Enable Submodules</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export const RestoreBackup = ({
|
|||||||
Restore Backup
|
Restore Backup
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center">
|
<DialogTitle className="flex items-center">
|
||||||
<RotateCcw className="mr-2 size-4" />
|
<RotateCcw className="mr-2 size-4" />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type * as React from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
"group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg:not(.cursor-pointer)]:pointer-events-none [&>svg]:size-3!",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
import { CheckIcon, ChevronRightIcon } from "lucide-react";
|
import { CheckIcon, ChevronRightIcon } from "lucide-react";
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||||
import type * as React from "react";
|
import * as React from "react";
|
||||||
import { markNestedPopupClosed } from "@/components/ui/nested-popup-context";
|
import { markNestedPopupClosed } from "@/components/ui/nested-popup-context";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function DropdownMenu({
|
function DropdownMenu({
|
||||||
|
open,
|
||||||
|
defaultOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
|
||||||
|
defaultOpen ?? false,
|
||||||
|
);
|
||||||
|
const isControlled = open !== undefined;
|
||||||
|
const isOpen = isControlled ? open : uncontrolledOpen;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Root
|
<DropdownMenuPrimitive.Root
|
||||||
data-slot="dropdown-menu"
|
data-slot="dropdown-menu"
|
||||||
onOpenChange={(open) => {
|
open={isOpen}
|
||||||
if (!open) {
|
onOpenChange={(nextOpen) => {
|
||||||
|
// Radix closes menus on window blur, unmounting any dialog rendered inside
|
||||||
|
if (!nextOpen && !document.hasFocus()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isControlled) {
|
||||||
|
setUncontrolledOpen(nextOpen);
|
||||||
|
}
|
||||||
|
if (!nextOpen) {
|
||||||
markNestedPopupClosed();
|
markNestedPopupClosed();
|
||||||
}
|
}
|
||||||
onOpenChange?.(open);
|
onOpenChange?.(nextOpen);
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
const sidebarMenuButtonVariants = cva(
|
||||||
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -473,8 +473,8 @@ const sidebarMenuButtonVariants = cva(
|
|||||||
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
|
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-8 text-sm",
|
default: "h-8 text-sm group-data-[collapsible=icon]:p-2!",
|
||||||
sm: "h-7 text-xs",
|
sm: "h-7 text-xs group-data-[collapsible=icon]:p-2!",
|
||||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.29.13",
|
"version": "v0.29.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import { buttonVariants } from "@/components/ui/button";
|
|||||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
statusCode: number;
|
statusCode?: number;
|
||||||
error?: Error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Custom404({ statusCode, error }: Props) {
|
export default function ErrorPage({ statusCode }: Props) {
|
||||||
const displayStatusCode = statusCode || 400;
|
const displayStatusCode = statusCode || 500;
|
||||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const appName = whitelabeling?.appName || "Dokploy";
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const logoUrl = whitelabeling?.logoUrl || undefined;
|
const logoUrl = whitelabeling?.logoUrl || undefined;
|
||||||
@@ -45,12 +44,6 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
{errorDescription}
|
{errorDescription}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{error && (
|
|
||||||
<div className="mt-3 text-red-500">
|
|
||||||
<p>{error.message}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/home"
|
href="/dashboard/home"
|
||||||
@@ -101,8 +94,7 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
ErrorPage.getInitialProps = ({ res, err }: NextPageContext) => {
|
||||||
Error.getInitialProps = ({ res, err }: NextPageContext) => {
|
const statusCode = res ? res.statusCode : err ? (err.statusCode ?? 500) : 404;
|
||||||
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
|
return { statusCode };
|
||||||
return { statusCode, error: err };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -119,9 +119,11 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
// If webhook doesn't provide image info, we'll use the configured image (old behavior)
|
// If webhook doesn't provide image info, we'll use the configured image (old behavior)
|
||||||
} else if (sourceType === "github") {
|
} else if (sourceType === "github") {
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
application.watchPaths,
|
application.watchPaths,
|
||||||
@@ -150,21 +152,29 @@ export default async function handler(
|
|||||||
let normalizedCommits: string[] = [];
|
let normalizedCommits: string[] = [];
|
||||||
|
|
||||||
if (provider === "github") {
|
if (provider === "github") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
} else if (provider === "gitlab") {
|
} else if (provider === "gitlab") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
} else if (provider === "gitea") {
|
} else if (provider === "gitea") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
} else if (provider === "soft-serve") {
|
} else if (provider === "soft-serve") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
@@ -179,9 +189,11 @@ export default async function handler(
|
|||||||
} else if (sourceType === "gitlab") {
|
} else if (sourceType === "gitlab") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
application.watchPaths,
|
application.watchPaths,
|
||||||
@@ -225,9 +237,11 @@ export default async function handler(
|
|||||||
} else if (sourceType === "gitea") {
|
} else if (sourceType === "gitea") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
application.watchPaths,
|
application.watchPaths,
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ export default async function handler(
|
|||||||
|
|
||||||
if (sourceType === "github") {
|
if (sourceType === "github") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
@@ -74,9 +76,11 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
} else if (sourceType === "gitlab") {
|
} else if (sourceType === "gitlab") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
@@ -125,17 +129,23 @@ export default async function handler(
|
|||||||
let normalizedCommits: string[] = [];
|
let normalizedCommits: string[] = [];
|
||||||
|
|
||||||
if (provider === "github") {
|
if (provider === "github") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
} else if (provider === "gitlab") {
|
} else if (provider === "gitlab") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
} else if (provider === "gitea") {
|
} else if (provider === "gitea") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
@@ -150,9 +160,11 @@ export default async function handler(
|
|||||||
} else if (sourceType === "gitea") {
|
} else if (sourceType === "gitea") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
|
|||||||
@@ -223,9 +223,11 @@ export default async function handler(
|
|||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
const deploymentHash = extractHash(req.headers, req.body);
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
const owner = getGithubRepositoryOwner(githubBody);
|
const owner = getGithubRepositoryOwner(githubBody);
|
||||||
const normalizedCommits = githubBody?.commits?.flatMap(
|
const normalizedCommits = githubBody?.commits?.flatMap((commit: any) => [
|
||||||
(commit: any) => commit.modified,
|
...(commit.added || []),
|
||||||
);
|
...(commit.modified || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
]);
|
||||||
|
|
||||||
const apps = await db.query.applications.findMany({
|
const apps = await db.query.applications.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
deleteAllMiddlewares,
|
deleteAllMiddlewares,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findEnvironmentById,
|
findEnvironmentById,
|
||||||
|
findPreviewDeploymentsByApplicationId,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
getAccessibleServerIds,
|
getAccessibleServerIds,
|
||||||
getApplicationStats,
|
getApplicationStats,
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
removeDeployments,
|
removeDeployments,
|
||||||
removeDirectoryCode,
|
removeDirectoryCode,
|
||||||
removeMonitoringDirectory,
|
removeMonitoringDirectory,
|
||||||
|
removePreviewDeployment,
|
||||||
removeService,
|
removeService,
|
||||||
removeTraefikConfig,
|
removeTraefikConfig,
|
||||||
startService,
|
startService,
|
||||||
@@ -234,6 +236,15 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previewDeploymentsList =
|
||||||
|
await findPreviewDeploymentsByApplicationId(input.applicationId);
|
||||||
|
|
||||||
|
for (const previewDeployment of previewDeploymentsList) {
|
||||||
|
try {
|
||||||
|
await removePreviewDeployment(previewDeployment.previewDeploymentId);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await db
|
const result = await db
|
||||||
.delete(applications)
|
.delete(applications)
|
||||||
.where(eq(applications.applicationId, input.applicationId))
|
.where(eq(applications.applicationId, input.applicationId))
|
||||||
|
|||||||
@@ -315,13 +315,17 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
await checkPermission(ctx, { schedule: ["create"] });
|
await checkPermission(ctx, { schedule: ["create"] });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await runCommand(input.scheduleId);
|
const deployment = await runCommand(input.scheduleId);
|
||||||
await audit(ctx, {
|
await audit(ctx, {
|
||||||
action: "run",
|
action: "run",
|
||||||
resourceType: "schedule",
|
resourceType: "schedule",
|
||||||
resourceId: input.scheduleId,
|
resourceId: input.scheduleId,
|
||||||
});
|
});
|
||||||
return true;
|
return {
|
||||||
|
status: deployment.status,
|
||||||
|
deploymentId: deployment.deploymentId,
|
||||||
|
logPath: deployment.logPath,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
|||||||
@@ -269,6 +269,12 @@
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cursor pointer on buttons */
|
||||||
|
button:not([disabled]),
|
||||||
|
[role="button"]:not([disabled]) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom scrollbar styling */
|
/* Custom scrollbar styling */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 0.3125rem;
|
width: 0.3125rem;
|
||||||
|
|||||||
@@ -180,6 +180,9 @@ export const initializeJobs = async () => {
|
|||||||
where: eq(schedules.enabled, true),
|
where: eq(schedules.enabled, true),
|
||||||
with: {
|
with: {
|
||||||
application: {
|
application: {
|
||||||
|
columns: {
|
||||||
|
applicationId: true,
|
||||||
|
},
|
||||||
with: {
|
with: {
|
||||||
server: true,
|
server: true,
|
||||||
},
|
},
|
||||||
@@ -227,6 +230,9 @@ export const initializeJobs = async () => {
|
|||||||
where: eq(volumeBackups.enabled, true),
|
where: eq(volumeBackups.enabled, true),
|
||||||
with: {
|
with: {
|
||||||
application: {
|
application: {
|
||||||
|
columns: {
|
||||||
|
applicationId: true,
|
||||||
|
},
|
||||||
with: {
|
with: {
|
||||||
server: true,
|
server: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Application } from "@dokploy/server/services/application";
|
import type { Application } from "@dokploy/server/services/application";
|
||||||
|
import type { Environment } from "@dokploy/server/services/environment";
|
||||||
import type { Mount } from "@dokploy/server/services/mount";
|
import type { Mount } from "@dokploy/server/services/mount";
|
||||||
import type { Port } from "@dokploy/server/services/port";
|
import type { Port } from "@dokploy/server/services/port";
|
||||||
import type { Project } from "@dokploy/server/services/project";
|
import type { Project } from "@dokploy/server/services/project";
|
||||||
@@ -27,7 +28,7 @@ export const rollbacks = pgTable("rollback", {
|
|||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
fullContext: jsonb("fullContext").$type<
|
fullContext: jsonb("fullContext").$type<
|
||||||
Application & {
|
Application & {
|
||||||
environment: {
|
environment: Environment & {
|
||||||
project: Project;
|
project: Project;
|
||||||
};
|
};
|
||||||
mounts: Mount[];
|
mounts: Mount[];
|
||||||
|
|||||||
@@ -30,12 +30,8 @@ export const finPortById = async (portId: string) => {
|
|||||||
where: eq(ports.portId, portId),
|
where: eq(ports.portId, portId),
|
||||||
with: {
|
with: {
|
||||||
application: {
|
application: {
|
||||||
with: {
|
columns: {
|
||||||
environment: {
|
applicationId: true,
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { manageDomain } from "../utils/traefik/domain";
|
|||||||
import { findApplicationById } from "./application";
|
import { findApplicationById } from "./application";
|
||||||
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
||||||
import { createDomain } from "./domain";
|
import { createDomain } from "./domain";
|
||||||
import { type Github, getIssueComment } from "./github";
|
import { findGithubById, getIssueComment } from "./github";
|
||||||
import { getWebServerSettings } from "./web-server-settings";
|
import { getWebServerSettings } from "./web-server-settings";
|
||||||
|
|
||||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||||
@@ -142,7 +142,17 @@ export const createPreviewDeployment = async (
|
|||||||
org?.ownerId || "",
|
org?.ownerId || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
const octokit = authGithub(application?.github as Github);
|
if (!application.githubId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Github Account not configured correctly",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// `findApplicationById` redacts `githubPrivateKey` from the `github`
|
||||||
|
// relation, so the provider must be refetched to authenticate.
|
||||||
|
const githubProvider = await findGithubById(application.githubId);
|
||||||
|
const octokit = authGithub(githubProvider);
|
||||||
|
|
||||||
const runningComment = getIssueComment(
|
const runningComment = getIssueComment(
|
||||||
application.name,
|
application.name,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
|||||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||||
import { type Application, findApplicationById } from "./application";
|
import { type Application, findApplicationById } from "./application";
|
||||||
import { findDeploymentById } from "./deployment";
|
import { findDeploymentById } from "./deployment";
|
||||||
|
import type { Environment } from "./environment";
|
||||||
import type { Mount } from "./mount";
|
import type { Mount } from "./mount";
|
||||||
import type { Port } from "./port";
|
import type { Port } from "./port";
|
||||||
import type { Project } from "./project";
|
import type { Project } from "./project";
|
||||||
@@ -105,19 +106,7 @@ export const findRollbackById = async (rollbackId: string) => {
|
|||||||
const result = await db.query.rollbacks.findFirst({
|
const result = await db.query.rollbacks.findFirst({
|
||||||
where: eq(rollbacks.rollbackId, rollbackId),
|
where: eq(rollbacks.rollbackId, rollbackId),
|
||||||
with: {
|
with: {
|
||||||
deployment: {
|
deployment: true,
|
||||||
with: {
|
|
||||||
application: {
|
|
||||||
with: {
|
|
||||||
environment: {
|
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,7 +202,7 @@ const rollbackApplication = async (
|
|||||||
image: string,
|
image: string,
|
||||||
serverId?: string | null,
|
serverId?: string | null,
|
||||||
fullContext?: Application & {
|
fullContext?: Application & {
|
||||||
environment: {
|
environment: Environment & {
|
||||||
project: Project;
|
project: Project;
|
||||||
};
|
};
|
||||||
mounts: Mount[];
|
mounts: Mount[];
|
||||||
@@ -275,6 +264,7 @@ const rollbackApplication = async (
|
|||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariables(
|
||||||
env,
|
env,
|
||||||
fullContext.environment.project.env,
|
fullContext.environment.project.env,
|
||||||
|
fullContext.environment.env,
|
||||||
);
|
);
|
||||||
|
|
||||||
let rollbackImage = image;
|
let rollbackImage = image;
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ export const findScheduleById = async (scheduleId: string) => {
|
|||||||
where: eq(schedules.scheduleId, scheduleId),
|
where: eq(schedules.scheduleId, scheduleId),
|
||||||
with: {
|
with: {
|
||||||
application: {
|
application: {
|
||||||
|
columns: {
|
||||||
|
applicationId: true,
|
||||||
|
appName: true,
|
||||||
|
serverId: true,
|
||||||
|
},
|
||||||
with: {
|
with: {
|
||||||
environment: {
|
environment: {
|
||||||
with: {
|
with: {
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
|
|||||||
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
||||||
with: {
|
with: {
|
||||||
application: {
|
application: {
|
||||||
|
columns: {
|
||||||
|
applicationId: true,
|
||||||
|
appName: true,
|
||||||
|
serverId: true,
|
||||||
|
},
|
||||||
with: {
|
with: {
|
||||||
environment: {
|
environment: {
|
||||||
with: {
|
with: {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export function parseRawConfig(
|
|||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
parsedLogs = parsedLogs.filter((log) =>
|
parsedLogs = parsedLogs.filter((log) =>
|
||||||
log.RequestHost.toLowerCase().includes(search.toLowerCase()),
|
(log.RequestHost ?? "").toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,14 +53,9 @@ export const getRailpackCommand = (application: ApplicationNested) => {
|
|||||||
"build",
|
"build",
|
||||||
"--builder",
|
"--builder",
|
||||||
builderName,
|
builderName,
|
||||||
...(cacheKey
|
"--build-arg",
|
||||||
? [
|
`secrets-hash=${secretsHash}`,
|
||||||
"--build-arg",
|
...(cacheKey ? ["--build-arg", `cache-key=${cacheKey}`] : []),
|
||||||
`secrets-hash=${secretsHash}`,
|
|
||||||
"--build-arg",
|
|
||||||
`cache-key=${cacheKey}`,
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
"--build-arg",
|
"--build-arg",
|
||||||
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
|
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
|
||||||
"-f",
|
"-f",
|
||||||
|
|||||||
@@ -55,25 +55,46 @@ export const runCommand = async (scheduleId: string) => {
|
|||||||
description: "Schedule",
|
description: "Schedule",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (scheduleType === "application" || scheduleType === "compose") {
|
try {
|
||||||
let containerId = "";
|
if (scheduleType === "application" || scheduleType === "compose") {
|
||||||
let serverId = "";
|
let containerId = "";
|
||||||
if (scheduleType === "application" && application) {
|
let serverId = "";
|
||||||
const container = await getServiceContainer(
|
if (scheduleType === "application" && application) {
|
||||||
application.appName,
|
const container = await getServiceContainer(
|
||||||
application.serverId,
|
application.appName,
|
||||||
);
|
application.serverId,
|
||||||
containerId = container?.Id || "";
|
);
|
||||||
serverId = application.serverId || "";
|
containerId = container?.Id || "";
|
||||||
}
|
serverId = application.serverId || "";
|
||||||
if (scheduleType === "compose" && compose) {
|
}
|
||||||
const container = await getComposeContainer(compose, serviceName || "");
|
if (scheduleType === "compose" && compose) {
|
||||||
containerId = container?.Id || "";
|
const container = await getComposeContainer(compose, serviceName || "");
|
||||||
serverId = compose.serverId || "";
|
containerId = container?.Id || "";
|
||||||
}
|
serverId = compose.serverId || "";
|
||||||
|
}
|
||||||
|
|
||||||
if (serverId) {
|
if (!containerId) {
|
||||||
try {
|
const target =
|
||||||
|
scheduleType === "compose"
|
||||||
|
? `service '${serviceName}' of compose '${compose?.name}'`
|
||||||
|
: `application '${application?.appName}'`;
|
||||||
|
const message = `Container not found for ${target}, make sure the service is running`;
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(
|
||||||
|
serverId,
|
||||||
|
`echo ${quote([`❌ ${message}`])} >> ${quote([deployment.logPath])}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const writeStream = createWriteStream(deployment.logPath, {
|
||||||
|
flags: "a",
|
||||||
|
});
|
||||||
|
writeStream.write(`❌ ${message}\n`);
|
||||||
|
writeStream.end();
|
||||||
|
}
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
await execAsyncRemote(
|
await execAsyncRemote(
|
||||||
serverId,
|
serverId,
|
||||||
`
|
`
|
||||||
@@ -86,47 +107,45 @@ export const runCommand = async (scheduleId: string) => {
|
|||||||
echo "✅ Command executed successfully" >> ${quote([deployment.logPath])};
|
echo "✅ Command executed successfully" >> ${quote([deployment.logPath])};
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} else {
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
const writeStream = createWriteStream(deployment.logPath, {
|
||||||
throw error;
|
flags: "a",
|
||||||
}
|
});
|
||||||
} else {
|
|
||||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
|
writeStream.write(
|
||||||
|
"This feature is not available in the cloud version.",
|
||||||
|
);
|
||||||
|
writeStream.end();
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
|
return { ...deployment, status: "error" as const };
|
||||||
|
}
|
||||||
writeStream.write(
|
writeStream.write(
|
||||||
"This feature is not available in the cloud version.",
|
`docker exec ${containerId} ${shellType} -c ${command}\n`,
|
||||||
|
);
|
||||||
|
await spawnAsync(
|
||||||
|
"docker",
|
||||||
|
["exec", containerId, shellType, "-c", command],
|
||||||
|
(data) => {
|
||||||
|
if (writeStream.writable) {
|
||||||
|
writeStream.write(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
writeStream.write("✅ Command executed successfully\n");
|
||||||
|
writeStream.end();
|
||||||
|
} catch (error) {
|
||||||
|
writeStream.write("❌ Command failed\n");
|
||||||
|
writeStream.write(
|
||||||
|
error instanceof Error ? error.message : "Unknown error",
|
||||||
);
|
);
|
||||||
writeStream.end();
|
writeStream.end();
|
||||||
return;
|
throw error;
|
||||||
}
|
}
|
||||||
writeStream.write(
|
|
||||||
`docker exec ${containerId} ${shellType} -c ${command}\n`,
|
|
||||||
);
|
|
||||||
await spawnAsync(
|
|
||||||
"docker",
|
|
||||||
["exec", containerId, shellType, "-c", command],
|
|
||||||
(data) => {
|
|
||||||
if (writeStream.writable) {
|
|
||||||
writeStream.write(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
writeStream.write("✅ Command executed successfully\n");
|
|
||||||
} catch (error) {
|
|
||||||
writeStream.write("❌ Command failed\n");
|
|
||||||
writeStream.write(
|
|
||||||
error instanceof Error ? error.message : "Unknown error",
|
|
||||||
);
|
|
||||||
writeStream.end();
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
} else if (scheduleType === "dokploy-server") {
|
||||||
} else if (scheduleType === "dokploy-server") {
|
|
||||||
try {
|
|
||||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||||
const { SCHEDULES_PATH } = paths();
|
const { SCHEDULES_PATH } = paths();
|
||||||
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
||||||
@@ -151,18 +170,13 @@ export const runCommand = async (scheduleId: string) => {
|
|||||||
cwd: fullPath,
|
cwd: fullPath,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} else if (scheduleType === "server") {
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else if (scheduleType === "server") {
|
|
||||||
try {
|
|
||||||
const { SCHEDULES_PATH } = paths(true);
|
const { SCHEDULES_PATH } = paths(true);
|
||||||
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
||||||
const command = `
|
const command = `
|
||||||
set -e
|
set -e
|
||||||
echo "Running script" >> ${deployment.logPath};
|
echo "Running script" >> ${deployment.logPath};
|
||||||
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
|
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
|
||||||
echo "❌ Command failed" >> ${deployment.logPath};
|
echo "❌ Command failed" >> ${deployment.logPath};
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
@@ -177,10 +191,11 @@ export const runCommand = async (scheduleId: string) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
|
return { ...deployment, status: "done" as const };
|
||||||
|
} catch {
|
||||||
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
|
return { ...deployment, status: "error" as const };
|
||||||
}
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user