diff --git a/apps/proxy/Caddyfile.aio.ce b/apps/proxy/Caddyfile.aio.ce
index 9cf6d8dd8e8..2041f0e1231 100644
--- a/apps/proxy/Caddyfile.aio.ce
+++ b/apps/proxy/Caddyfile.aio.ce
@@ -1,3 +1,14 @@
+{
+ {$CERT_EMAIL}
+ acme_ca {$CERT_ACME_CA:https://acme-v02.api.letsencrypt.org/directory}
+ {$CERT_ACME_DNS}
+ servers {
+ max_header_size 25MB
+ client_ip_headers X-Forwarded-For X-Real-IP
+ trusted_proxies static {$TRUSTED_PROXIES:0.0.0.0/0}
+ }
+}
+
(plane_proxy) {
request_body {
max_size {$FILE_SIZE_LIMIT}
@@ -30,17 +41,6 @@
}
}
-{
- {$CERT_EMAIL}
- acme_ca {$CERT_ACME_CA:https://acme-v02.api.letsencrypt.org/directory}
- {$CERT_ACME_DNS}
- servers {
- max_header_size 25MB
- client_ip_headers X-Forwarded-For X-Real-IP
- trusted_proxies static {$TRUSTED_PROXIES:0.0.0.0/0}
- }
-}
-
{$SITE_ADDRESS} {
import plane_proxy
}
\ No newline at end of file
diff --git a/apps/proxy/Caddyfile.ce b/apps/proxy/Caddyfile.ce
index 14559f27219..d9b54c99f15 100644
--- a/apps/proxy/Caddyfile.ce
+++ b/apps/proxy/Caddyfile.ce
@@ -1,39 +1,37 @@
-(plane_proxy) {
- request_body {
- max_size {$FILE_SIZE_LIMIT}
- }
-
- redir /spaces /spaces/ permanent
- reverse_proxy /spaces/* space:3000
-
- redir /god-mode /god-mode/ permanent
- reverse_proxy /god-mode/* admin:3000
-
- reverse_proxy /live/* live:3000
-
- reverse_proxy /api/* api:8000
-
- reverse_proxy /auth/* api:8000
-
- reverse_proxy /static/* api:8000
-
- reverse_proxy /{$BUCKET_NAME}/* plane-minio:9000
- reverse_proxy /{$BUCKET_NAME} plane-minio:9000
-
- reverse_proxy /* web:3000
+{
+ email {$CERT_EMAIL}
+ acme_ca {$CERT_ACME_CA:https://acme-v02.api.letsencrypt.org/directory}
+ # acme_dns {$CERT_ACME_DNS} # if Let's Encrypt
+
+ servers {
+ max_header_size 25MB
+ client_ip_headers X-Forwarded-For X-Real-IP
+ trusted_proxies static {$TRUSTED_PROXIES:0.0.0.0/0}
+ }
}
-{
- {$CERT_EMAIL}
- acme_ca {$CERT_ACME_CA:https://acme-v02.api.letsencrypt.org/directory}
- {$CERT_ACME_DNS}
- servers {
- max_header_size 25MB
- client_ip_headers X-Forwarded-For X-Real-IP
- trusted_proxies static {$TRUSTED_PROXIES:0.0.0.0/0}
- }
+(plane_proxy) {
+ request_body {
+ max_size {$FILE_SIZE_LIMIT}
+ }
+
+ redir /spaces /spaces/ permanent
+ reverse_proxy /spaces/* space:3000
+
+ redir /god-mode /god-mode/ permanent
+ reverse_proxy /god-mode/* admin:3000
+
+ reverse_proxy /live/* live:3000
+ reverse_proxy /api/* api:8000
+ reverse_proxy /auth/* api:8000
+ reverse_proxy /static/* api:8000
+
+ reverse_proxy /{$BUCKET_NAME}/* plane-minio:9000
+ reverse_proxy /{$BUCKET_NAME} plane-minio:9000
+
+ reverse_proxy /* web:3000
}
{$SITE_ADDRESS} {
- import plane_proxy
+ import plane_proxy
}
\ No newline at end of file
diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/header.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/header.tsx
index 82fe31cce46..a0281d09889 100644
--- a/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/header.tsx
+++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/active-cycles/header.tsx
@@ -11,8 +11,6 @@ import { CycleIcon } from "@plane/propel/icons";
import { Breadcrumbs, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
-// plane web components
-import { UpgradeBadge } from "@/plane-web/components/workspace/upgrade-badge";
export const WorkspaceActiveCycleHeader = observer(function WorkspaceActiveCycleHeader() {
const { t } = useTranslation();
@@ -29,7 +27,6 @@ export const WorkspaceActiveCycleHeader = observer(function WorkspaceActiveCycle
}
/>
-
);
diff --git a/apps/web/ce/components/active-cycles/root.tsx b/apps/web/ce/components/active-cycles/root.tsx
index 81805656b30..e73bb7935e7 100644
--- a/apps/web/ce/components/active-cycles/root.tsx
+++ b/apps/web/ce/components/active-cycles/root.tsx
@@ -4,9 +4,6 @@
* See the LICENSE file for details.
*/
-// local imports
-import { WorkspaceActiveCyclesUpgrade } from "./workspace-active-cycles-upgrade";
-
export function WorkspaceActiveCyclesRoot() {
- return ;
+ return null;
}
diff --git a/apps/web/ce/components/issues/bulk-operations/root.tsx b/apps/web/ce/components/issues/bulk-operations/root.tsx
index 567fd12d232..04dbf5a3549 100644
--- a/apps/web/ce/components/issues/bulk-operations/root.tsx
+++ b/apps/web/ce/components/issues/bulk-operations/root.tsx
@@ -5,8 +5,6 @@
*/
import { observer } from "mobx-react";
-// components
-import { BulkOperationsUpgradeBanner } from "@/components/issues/bulk-operations/upgrade-banner";
// hooks
import { useMultipleSelectStore } from "@/hooks/store/use-multiple-select-store";
import type { TSelectionHelper } from "@/hooks/use-multiple-select";
@@ -16,12 +14,6 @@ type Props = {
selectionHelpers: TSelectionHelper;
};
-export const IssueBulkOperationsRoot = observer(function IssueBulkOperationsRoot(props: Props) {
- const { className, selectionHelpers } = props;
- // store hooks
- const { isSelectionActive } = useMultipleSelectStore();
-
- if (!isSelectionActive || selectionHelpers.isSelectionDisabled) return null;
-
- return ;
+export const IssueBulkOperationsRoot = observer(function IssueBulkOperationsRoot(_props: Props) {
+ return null;
});
diff --git a/apps/web/ce/components/pages/editor/embed/issue-embed-upgrade-card.tsx b/apps/web/ce/components/pages/editor/embed/issue-embed-upgrade-card.tsx
index bcd5cebc409..faa64d76c6a 100644
--- a/apps/web/ce/components/pages/editor/embed/issue-embed-upgrade-card.tsx
+++ b/apps/web/ce/components/pages/editor/embed/issue-embed-upgrade-card.tsx
@@ -4,36 +4,6 @@
* See the LICENSE file for details.
*/
-// plane imports
-import { getButtonStyling } from "@plane/propel/button";
-import { cn } from "@plane/utils";
-// components
-import { ProIcon } from "@/components/common/pro-icon";
-
-export function IssueEmbedUpgradeCard(props: any) {
- return (
-
-
-
-
- Embed and access issues in pages seamlessly, upgrade to Plane Pro now.
-
-
-
- Upgrade
-
-
- );
+export function IssueEmbedUpgradeCard(_props: any) {
+ return null;
}
diff --git a/apps/web/ce/components/workspace/billing/root.tsx b/apps/web/ce/components/workspace/billing/root.tsx
index 33100a4372a..a55c0c586a5 100644
--- a/apps/web/ce/components/workspace/billing/root.tsx
+++ b/apps/web/ce/components/workspace/billing/root.tsx
@@ -4,45 +4,16 @@
* See the LICENSE file for details.
*/
-import { useState } from "react";
import { observer } from "mobx-react";
// plane imports
-import { DEFAULT_PRODUCT_BILLING_FREQUENCY, SUBSCRIPTION_WITH_BILLING_FREQUENCY } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
-import type { TBillingFrequency, TProductBillingFrequency } from "@plane/types";
-import { EProductSubscriptionEnum } from "@plane/types";
// components
import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item";
import { SettingsHeading } from "@/components/settings/heading";
-// local imports
-import { PlansComparison } from "./comparison/root";
export const BillingRoot = observer(function BillingRoot() {
- const [isCompareAllFeaturesSectionOpen, setIsCompareAllFeaturesSectionOpen] = useState(false);
- const [productBillingFrequency, setProductBillingFrequency] = useState(
- DEFAULT_PRODUCT_BILLING_FREQUENCY
- );
const { t } = useTranslation();
- /**
- * Retrieves the billing frequency for a given subscription type
- * @param {EProductSubscriptionEnum} subscriptionType - Type of subscription to get frequency for
- * @returns {TBillingFrequency | undefined} - Billing frequency if subscription supports it, undefined otherwise
- */
- const getBillingFrequency = (subscriptionType: EProductSubscriptionEnum): TBillingFrequency | undefined =>
- SUBSCRIPTION_WITH_BILLING_FREQUENCY.includes(subscriptionType)
- ? productBillingFrequency[subscriptionType]
- : undefined;
-
- /**
- * Updates the billing frequency for a specific subscription type
- * @param {EProductSubscriptionEnum} subscriptionType - Type of subscription to update
- * @param {TBillingFrequency} frequency - New billing frequency to set
- * @returns {void}
- */
- const setBillingFrequency = (subscriptionType: EProductSubscriptionEnum, frequency: TBillingFrequency): void =>
- setProductBillingFrequency({ ...productBillingFrequency, [subscriptionType]: frequency });
-
return (
@@ -57,15 +28,6 @@ export const BillingRoot = observer(function BillingRoot() {
/>
-
);
});
diff --git a/apps/web/ce/components/workspace/edition-badge.tsx b/apps/web/ce/components/workspace/edition-badge.tsx
index 34ef7049876..6cce9a964cf 100644
--- a/apps/web/ce/components/workspace/edition-badge.tsx
+++ b/apps/web/ce/components/workspace/edition-badge.tsx
@@ -4,7 +4,6 @@
* See the LICENSE file for details.
*/
-import { useState } from "react";
import { observer } from "mobx-react";
// ui
import { useTranslation } from "@plane/i18n";
@@ -12,35 +11,19 @@ import { Tooltip } from "@plane/propel/tooltip";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
import packageJson from "package.json";
-// local components
-import { PaidPlanUpgradeModal } from "../license";
import { Button } from "@plane/propel/button";
export const WorkspaceEditionBadge = observer(function WorkspaceEditionBadge() {
- // states
- const [isPaidPlanPurchaseModalOpen, setIsPaidPlanPurchaseModalOpen] = useState(false);
// translation
const { t } = useTranslation();
// platform
const { isMobile } = usePlatformOS();
return (
- <>
- setIsPaidPlanPurchaseModalOpen(false)}
- />
-
-
-
- >
+
+
+
);
});
diff --git a/apps/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx b/apps/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx
index 4014f8fb28c..d1a4b067719 100644
--- a/apps/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx
+++ b/apps/web/ce/components/workspace/sidebar/extended-sidebar-item.tsx
@@ -26,7 +26,6 @@ import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useUser, useUserPermissions } from "@/hooks/store/user";
import { useWorkspaceNavigationPreferences } from "@/hooks/use-navigation-preferences";
// local imports
-import { UpgradeBadge } from "../upgrade-badge";
import { getSidebarNavigationItemIcon } from "./helper";
type TExtendedSidebarItemProps = {
@@ -199,11 +198,6 @@ export const ExtendedSidebarItem = observer(function ExtendedSidebarItem(props:
- {item.key === "active_cycles" && (
-
-
-
- )}
{isPinned ? (
- {t("sidebar.pro")}
-
- );
+export function UpgradeBadge(_props: TUpgradeBadge) {
+ return null;
}
diff --git a/apps/web/core/components/estimates/create/stage-one.tsx b/apps/web/core/components/estimates/create/stage-one.tsx
index f6b4c999d3e..952a840b529 100644
--- a/apps/web/core/components/estimates/create/stage-one.tsx
+++ b/apps/web/core/components/estimates/create/stage-one.tsx
@@ -14,7 +14,6 @@ import type { TEstimateSystemKeys } from "@plane/types";
import { convertMinutesToHoursMinutesString } from "@plane/utils";
// plane web imports
import { isEstimateSystemEnabled } from "@/plane-web/components/estimates/helper";
-import { UpgradeBadge } from "@/plane-web/components/workspace/upgrade-badge";
import { RadioInput } from "../radio-select";
// local imports
@@ -53,7 +52,6 @@ export function EstimateCreateStageOne(props: TEstimateCreateStageOne) {
) : !isEnabled ? (
{t(ESTIMATE_SYSTEMS[currentSystem]?.i18n_name)}
-
) : (
{t(ESTIMATE_SYSTEMS[currentSystem]?.i18n_name)}
diff --git a/apps/web/core/components/project/settings/features-list.tsx b/apps/web/core/components/project/settings/features-list.tsx
index 8605bf50807..eed99729807 100644
--- a/apps/web/core/components/project/settings/features-list.tsx
+++ b/apps/web/core/components/project/settings/features-list.tsx
@@ -8,7 +8,6 @@ import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { setPromiseToast } from "@plane/propel/toast";
-import { Tooltip } from "@plane/propel/tooltip";
import type { IProject } from "@plane/types";
import { CycleIcon, IntakeIcon, ModuleIcon, PageIcon, ViewsIcon } from "@plane/propel/icons";
// components
@@ -16,8 +15,6 @@ import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-it
import { SettingsHeading } from "@/components/settings/heading";
// hooks
import { useProject } from "@/hooks/store/use-project";
-// plane web imports
-import { UpgradeBadge } from "@/plane-web/components/workspace/upgrade-badge";
// local imports
import { ProjectFeatureToggle } from "./helper";
@@ -34,7 +31,6 @@ const PROJECT_FEATURES_LIST = {
title: "Cycles",
description: "Timebox work as you see fit per project and change frequency from one period to the next.",
icon: ,
- isPro: false,
isEnabled: true,
},
modules: {
@@ -43,7 +39,6 @@ const PROJECT_FEATURES_LIST = {
title: "Modules",
description: "Group work into sub-project-like set-ups with their own leads and assignees.",
icon: ,
- isPro: false,
isEnabled: true,
},
views: {
@@ -52,7 +47,6 @@ const PROJECT_FEATURES_LIST = {
title: "Views",
description: "Save sorts, filters, and display options for later or share them.",
icon: ,
- isPro: false,
isEnabled: true,
},
pages: {
@@ -61,7 +55,6 @@ const PROJECT_FEATURES_LIST = {
title: "Pages",
description: "Write anything like you write anything.",
icon: ,
- isPro: false,
isEnabled: true,
},
inbox: {
@@ -70,7 +63,6 @@ const PROJECT_FEATURES_LIST = {
title: "Intake",
description: "Consider and discuss work items before you add them to your project.",
icon: ,
- isPro: false,
isEnabled: true,
},
};
@@ -119,11 +111,6 @@ export const ProjectFeaturesList = observer(function ProjectFeaturesList(props:
title={
{t(featureItem.key)}
- {featureItem.isPro && (
-
-
-
- )}
}
description={t(`${featureItem.key}_description`)}
@@ -138,9 +125,6 @@ export const ProjectFeaturesList = observer(function ProjectFeaturesList(props:
/>
}
/>
- {/* {currentProjectDetails?.[featureItem.property as keyof IProject] && (
- {featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)}
- )} */}
))}
diff --git a/apps/web/core/components/workspace/sidebar/workspace-menu-item.tsx b/apps/web/core/components/workspace/sidebar/workspace-menu-item.tsx
index 0199dc7091d..8b8bd4196ca 100644
--- a/apps/web/core/components/workspace/sidebar/workspace-menu-item.tsx
+++ b/apps/web/core/components/workspace/sidebar/workspace-menu-item.tsx
@@ -17,8 +17,6 @@ import { SidebarNavItem } from "@/components/sidebar/sidebar-navigation";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useUserPermissions } from "@/hooks/store/user";
-// plane web imports
-import { UpgradeBadge } from "@/plane-web/components/workspace/upgrade-badge";
export type SidebarWorkspaceMenuItemProps = {
item: {
@@ -56,20 +54,17 @@ export const SidebarWorkspaceMenuItem = observer(function SidebarWorkspaceMenuIt
const isActive = item.href === pathname;
return (
- handleLinkClick()}>
-
-
-
-
{t(item.labelTranslationKey)}
-
-
-
-
-
-
+ handleLinkClick()}>
+
+
+
+
{t(item.labelTranslationKey)}
+
+
+
);
});
diff --git a/plane-app.backup_20260330_144144/docker-compose.yaml b/plane-app.backup_20260330_144144/docker-compose.yaml
new file mode 100644
index 00000000000..a939b92d667
--- /dev/null
+++ b/plane-app.backup_20260330_144144/docker-compose.yaml
@@ -0,0 +1,255 @@
+x-db-env: &db-env
+ PGHOST: ${PGHOST:-plane-db}
+ PGDATABASE: ${PGDATABASE:-plane}
+ POSTGRES_USER: ${POSTGRES_USER:-plane}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
+ POSTGRES_DB: ${POSTGRES_DB:-plane}
+ POSTGRES_PORT: ${POSTGRES_PORT:-5432}
+ PGDATA: ${PGDATA:-/var/lib/postgresql/data}
+
+x-redis-env: &redis-env
+ REDIS_HOST: ${REDIS_HOST:-plane-redis}
+ REDIS_PORT: ${REDIS_PORT:-6379}
+ REDIS_URL: ${REDIS_URL:-redis://plane-redis:6379/}
+
+x-minio-env: &minio-env
+ MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-access-key}
+ MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-secret-key}
+
+x-aws-s3-env: &aws-s3-env
+ AWS_REGION: ${AWS_REGION:-}
+ AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-access-key}
+ AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-secret-key}
+ AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
+ AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
+
+x-proxy-env: &proxy-env
+ APP_DOMAIN: ${APP_DOMAIN:-localhost}
+ FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
+ CERT_EMAIL: ${CERT_EMAIL}
+ CERT_ACME_CA: ${CERT_ACME_CA}
+ CERT_ACME_DNS: ${CERT_ACME_DNS}
+ LISTEN_HTTP_PORT: ${LISTEN_HTTP_PORT:-80}
+ LISTEN_HTTPS_PORT: ${LISTEN_HTTPS_PORT:-443}
+ BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
+ SITE_ADDRESS: ${SITE_ADDRESS:-:80}
+
+x-mq-env: &mq-env # RabbitMQ Settings
+ RABBITMQ_HOST: ${RABBITMQ_HOST:-plane-mq}
+ RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
+ RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
+ RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane}
+ RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
+ RABBITMQ_VHOST: ${RABBITMQ_VHOST:-plane}
+
+x-live-env: &live-env
+ API_BASE_URL: ${API_BASE_URL:-http://api:8000}
+ LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-2FiJk1U2aiVPEQtzLehYGlTSnTnrs7LW}
+
+x-app-env: &app-env
+ WEB_URL: ${WEB_URL:-http://localhost}
+ DEBUG: ${DEBUG:-0}
+ CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
+ GUNICORN_WORKERS: 1
+ USE_MINIO: ${USE_MINIO:-1}
+ DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
+ SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
+ AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
+ API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
+ MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
+ LIVE_SERVER_SECRET_KEY: ${LIVE_SERVER_SECRET_KEY:-2FiJk1U2aiVPEQtzLehYGlTSnTnrs7LW}
+
+services:
+ web:
+ image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-v1.2.3}
+ deploy:
+ replicas: ${WEB_REPLICAS:-1}
+ restart_policy:
+ condition: any
+ depends_on:
+ - api
+ - worker
+
+ space:
+ image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-v1.2.3}
+ deploy:
+ replicas: ${SPACE_REPLICAS:-1}
+ restart_policy:
+ condition: any
+ depends_on:
+ - api
+ - worker
+ - web
+
+ admin:
+ image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-v1.2.3}
+ deploy:
+ replicas: ${ADMIN_REPLICAS:-1}
+ restart_policy:
+ condition: any
+ depends_on:
+ - api
+ - web
+
+ live:
+ image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-v1.2.3}
+ environment:
+ <<: [*live-env, *redis-env]
+ deploy:
+ replicas: ${LIVE_REPLICAS:-1}
+ restart_policy:
+ condition: any
+ depends_on:
+ - api
+ - web
+
+ api:
+ image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.2.3}
+ command: ./bin/docker-entrypoint-api.sh
+ deploy:
+ replicas: ${API_REPLICAS:-1}
+ restart_policy:
+ condition: any
+ volumes:
+ - logs_api:/code/plane/logs
+ environment:
+ <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
+ depends_on:
+ - plane-db
+ - plane-redis
+ - plane-mq
+
+ worker:
+ image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.2.3}
+ command: ./bin/docker-entrypoint-worker.sh
+ deploy:
+ replicas: ${WORKER_REPLICAS:-1}
+ restart_policy:
+ condition: any
+ volumes:
+ - logs_worker:/code/plane/logs
+ environment:
+ <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
+ depends_on:
+ - api
+ - plane-db
+ - plane-redis
+ - plane-mq
+
+ beat-worker:
+ image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.2.3}
+ command: ./bin/docker-entrypoint-beat.sh
+ deploy:
+ replicas: ${BEAT_WORKER_REPLICAS:-1}
+ restart_policy:
+ condition: any
+ volumes:
+ - logs_beat-worker:/code/plane/logs
+ environment:
+ <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
+ depends_on:
+ - api
+ - plane-db
+ - plane-redis
+ - plane-mq
+
+ migrator:
+ image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.2.3}
+ command: ./bin/docker-entrypoint-migrator.sh
+ deploy:
+ replicas: 1
+ restart_policy:
+ condition: on-failure
+ volumes:
+ - logs_migrator:/code/plane/logs
+ environment:
+ <<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *proxy-env]
+ depends_on:
+ - plane-db
+ - plane-redis
+
+ # Comment this if you already have a database running
+ plane-db:
+ image: postgres:15.7-alpine
+ command: postgres -c 'max_connections=1000'
+ deploy:
+ replicas: 1
+ restart_policy:
+ condition: any
+ environment:
+ <<: *db-env
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+
+ plane-redis:
+ image: valkey/valkey:7.2.11-alpine
+ deploy:
+ replicas: 1
+ restart_policy:
+ condition: any
+ volumes:
+ - redisdata:/data
+
+ plane-mq:
+ image: rabbitmq:3.13.6-management-alpine
+ deploy:
+ replicas: 1
+ restart_policy:
+ condition: any
+ environment:
+ <<: *mq-env
+ volumes:
+ - rabbitmq_data:/var/lib/rabbitmq
+
+ # Comment this if you using any external s3 compatible storage
+ plane-minio:
+ image: minio/minio:latest
+ command: server /export --console-address ":9090"
+ deploy:
+ replicas: 1
+ restart_policy:
+ condition: any
+ environment:
+ <<: *minio-env
+ volumes:
+ - uploads:/export
+
+ # Comment this if you already have a reverse proxy running
+ proxy:
+ image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-v1.2.3}
+ deploy:
+ replicas: 1
+ restart_policy:
+ condition: any
+ environment:
+ <<: *proxy-env
+ ports:
+ - target: 80
+ published: ${LISTEN_HTTP_PORT:-80}
+ protocol: tcp
+ mode: host
+ - target: 443
+ published: ${LISTEN_HTTPS_PORT:-443}
+ protocol: tcp
+ mode: host
+ volumes:
+ - proxy_config:/config
+ - proxy_data:/data
+ depends_on:
+ - web
+ - api
+ - space
+ - admin
+ - live
+
+volumes:
+ pgdata:
+ redisdata:
+ uploads:
+ logs_api:
+ logs_worker:
+ logs_beat-worker:
+ logs_migrator:
+ rabbitmq_data:
+ proxy_config:
+ proxy_data:
diff --git a/plane-app.backup_20260330_144144/plane.env b/plane-app.backup_20260330_144144/plane.env
new file mode 100644
index 00000000000..624c1b701a4
--- /dev/null
+++ b/plane-app.backup_20260330_144144/plane.env
@@ -0,0 +1,85 @@
+APP_DOMAIN=localhost
+APP_RELEASE=v1.2.3
+
+WEB_REPLICAS=1
+SPACE_REPLICAS=1
+ADMIN_REPLICAS=1
+API_REPLICAS=1
+WORKER_REPLICAS=1
+BEAT_WORKER_REPLICAS=1
+LIVE_REPLICAS=1
+
+LISTEN_HTTP_PORT=80
+LISTEN_HTTPS_PORT=443
+
+WEB_URL=http://${APP_DOMAIN}
+DEBUG=0
+CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
+API_BASE_URL=http://api:8000
+
+#DB SETTINGS
+PGHOST=plane-db
+PGDATABASE=plane
+POSTGRES_USER=plane
+POSTGRES_PASSWORD=plane
+POSTGRES_DB=plane
+POSTGRES_PORT=5432
+PGDATA=/var/lib/postgresql/data
+DATABASE_URL=
+
+# REDIS SETTINGS
+REDIS_HOST=plane-redis
+REDIS_PORT=6379
+REDIS_URL=
+
+# RabbitMQ Settings
+RABBITMQ_HOST=plane-mq
+RABBITMQ_PORT=5672
+RABBITMQ_USER=plane
+RABBITMQ_PASSWORD=plane
+RABBITMQ_VHOST=plane
+AMQP_URL=
+
+# If SSL Cert to be generated, set CERT_EMAIl="email "
+CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
+TRUSTED_PROXIES=0.0.0.0/0
+SITE_ADDRESS=:80
+CERT_EMAIL=
+
+
+
+# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL
+# CERT_ACME_DNS="acme_dns "
+CERT_ACME_DNS=
+
+
+# Secret Key
+SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
+
+# DATA STORE SETTINGS
+USE_MINIO=1
+AWS_REGION=
+AWS_ACCESS_KEY_ID=access-key
+AWS_SECRET_ACCESS_KEY=secret-key
+AWS_S3_ENDPOINT_URL=http://plane-minio:9000
+AWS_S3_BUCKET_NAME=uploads
+FILE_SIZE_LIMIT=5242880
+
+# Gunicorn Workers
+GUNICORN_WORKERS=1
+
+# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
+# DOCKER_PLATFORM=linux/amd64
+
+# Force HTTPS for handling SSL Termination
+MINIO_ENDPOINT_SSL=0
+
+# API key rate limit
+API_KEY_RATE_LIMIT=60/minute
+
+# Live server environment variables
+# WARNING: You must set a secure value for LIVE_SERVER_SECRET_KEY in production environments.
+LIVE_SERVER_SECRET_KEY=
+DOCKERHUB_USER=artifacts.plane.so/makeplane
+PULL_POLICY=if_not_present
+CUSTOM_BUILD=false
diff --git a/plane-app/docker-compose.yaml b/plane-app/docker-compose.yaml
new file mode 100644
index 00000000000..787ba640aa6
--- /dev/null
+++ b/plane-app/docker-compose.yaml
@@ -0,0 +1,177 @@
+services:
+ web:
+ container_name: web
+ build:
+ context: .
+ dockerfile: ./apps/web/Dockerfile.web
+ args:
+ DOCKER_BUILDKIT: 1
+ restart: always
+ depends_on:
+ - api
+
+ admin:
+ container_name: admin
+ build:
+ context: .
+ dockerfile: ./apps/admin/Dockerfile.admin
+ args:
+ DOCKER_BUILDKIT: 1
+ restart: always
+ depends_on:
+ - api
+ - web
+
+ space:
+ container_name: space
+ build:
+ context: .
+ dockerfile: ./apps/space/Dockerfile.space
+ args:
+ DOCKER_BUILDKIT: 1
+ restart: always
+ depends_on:
+ - api
+ - web
+
+ api:
+ container_name: api
+ build:
+ context: ./apps/api
+ dockerfile: Dockerfile.api
+ args:
+ DOCKER_BUILDKIT: 1
+ restart: always
+ command: ./bin/docker-entrypoint-api.sh
+ env_file:
+ - ./apps/api/.env
+ depends_on:
+ - plane-db
+ - plane-redis
+
+ worker:
+ container_name: bgworker
+ build:
+ context: ./apps/api
+ dockerfile: Dockerfile.api
+ args:
+ DOCKER_BUILDKIT: 1
+ restart: always
+ command: ./bin/docker-entrypoint-worker.sh
+ env_file:
+ - ./apps/api/.env
+ depends_on:
+ - api
+ - plane-db
+ - plane-redis
+
+ beat-worker:
+ container_name: beatworker
+ build:
+ context: ./apps/api
+ dockerfile: Dockerfile.api
+ args:
+ DOCKER_BUILDKIT: 1
+ restart: always
+ command: ./bin/docker-entrypoint-beat.sh
+ env_file:
+ - ./apps/api/.env
+ depends_on:
+ - api
+ - plane-db
+ - plane-redis
+
+ migrator:
+ container_name: plane-migrator
+ build:
+ context: ./apps/api
+ dockerfile: Dockerfile.api
+ args:
+ DOCKER_BUILDKIT: 1
+ restart: no
+ command: ./bin/docker-entrypoint-migrator.sh
+ env_file:
+ - ./apps/api/.env
+ depends_on:
+ - plane-db
+ - plane-redis
+
+ live:
+ container_name: plane-live
+ build:
+ context: .
+ dockerfile: ./apps/live/Dockerfile.live
+ args:
+ DOCKER_BUILDKIT: 1
+ restart: always
+
+ plane-db:
+ container_name: plane-db
+ image: postgres:15.7-alpine
+ restart: always
+ command: postgres -c 'max_connections=1000'
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ env_file:
+ - .env
+ environment:
+ POSTGRES_USER: ${POSTGRES_USER}
+ POSTGRES_DB: ${POSTGRES_DB}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+ PGDATA: /var/lib/postgresql/data
+
+ plane-redis:
+ container_name: plane-redis
+ image: valkey/valkey:7.2.11-alpine
+ restart: always
+ volumes:
+ - redisdata:/data
+
+ plane-mq:
+ container_name: plane-mq
+ image: rabbitmq:3.13.6-management-alpine
+ restart: always
+ env_file:
+ - .env
+ environment:
+ RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
+ RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
+ RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST}
+ volumes:
+ - rabbitmq_data:/var/lib/rabbitmq
+
+ plane-minio:
+ container_name: plane-minio
+ image: minio/minio
+ restart: always
+ command: server /export --console-address ":9090"
+ volumes:
+ - uploads:/export
+ environment:
+ MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
+ MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
+
+ # Comment this if you already have a reverse proxy running
+ proxy:
+ container_name: proxy
+ build:
+ context: ./apps/proxy
+ dockerfile: Dockerfile.ce
+ restart: always
+ ports:
+ - ${LISTEN_HTTP_PORT}:80
+ - ${LISTEN_HTTPS_PORT}:443
+ environment:
+ FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
+ BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
+ depends_on:
+ - web
+ - api
+ - space
+ - admin
+
+volumes:
+ pgdata:
+ redisdata:
+ uploads:
+ rabbitmq_data:
diff --git a/plane-app/ex.pln b/plane-app/ex.pln
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/plane-app/plane.env b/plane-app/plane.env
new file mode 100644
index 00000000000..c6a8ce36965
--- /dev/null
+++ b/plane-app/plane.env
@@ -0,0 +1,85 @@
+APP_DOMAIN=82.114.228.228
+APP_RELEASE=v1.2.3
+
+WEB_REPLICAS=1
+SPACE_REPLICAS=1
+ADMIN_REPLICAS=1
+API_REPLICAS=1
+WORKER_REPLICAS=1
+BEAT_WORKER_REPLICAS=1
+LIVE_REPLICAS=1
+
+LISTEN_HTTP_PORT=80
+LISTEN_HTTPS_PORT=443
+
+WEB_URL=http://${APP_DOMAIN}
+DEBUG=0
+CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
+API_BASE_URL=http://api:8000
+
+#DB SETTINGS
+PGHOST=plane-db
+PGDATABASE=plane
+POSTGRES_USER=plane
+POSTGRES_PASSWORD=plane
+POSTGRES_DB=plane
+POSTGRES_PORT=5432
+PGDATA=/var/lib/postgresql/data
+DATABASE_URL=
+
+# REDIS SETTINGS
+REDIS_HOST=plane-redis
+REDIS_PORT=6379
+REDIS_URL=
+
+# RabbitMQ Settings
+RABBITMQ_HOST=plane-mq
+RABBITMQ_PORT=5672
+RABBITMQ_USER=plane
+RABBITMQ_PASSWORD=plane
+RABBITMQ_VHOST=plane
+AMQP_URL=
+
+# If SSL Cert to be generated, set CERT_EMAIl="email "
+CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
+TRUSTED_PROXIES=0.0.0.0/0
+SITE_ADDRESS=:80
+CERT_EMAIL=
+
+
+
+# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL
+# CERT_ACME_DNS="acme_dns "
+CERT_ACME_DNS=
+
+
+# Secret Key
+SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
+
+# DATA STORE SETTINGS
+USE_MINIO=1
+AWS_REGION=
+AWS_ACCESS_KEY_ID=access-key
+AWS_SECRET_ACCESS_KEY=secret-key
+AWS_S3_ENDPOINT_URL=http://plane-minio:9000
+AWS_S3_BUCKET_NAME=uploads
+FILE_SIZE_LIMIT=5242880
+
+# Gunicorn Workers
+GUNICORN_WORKERS=1
+
+# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
+# DOCKER_PLATFORM=linux/amd64
+
+# Force HTTPS for handling SSL Termination
+MINIO_ENDPOINT_SSL=0
+
+# API key rate limit
+API_KEY_RATE_LIMIT=60/minute
+
+# Live server environment variables
+# WARNING: You must set a secure value for LIVE_SERVER_SECRET_KEY in production environments.
+LIVE_SERVER_SECRET_KEY=
+DOCKERHUB_USER=artifacts.plane.so/makeplane
+PULL_POLICY=if_not_present
+CUSTOM_BUILD=false
diff --git a/plane.env b/plane.env
new file mode 100644
index 00000000000..f4a8a6bd043
--- /dev/null
+++ b/plane.env
@@ -0,0 +1,60 @@
+# Database Settings
+POSTGRES_USER="plane"
+POSTGRES_PASSWORD="plane"
+POSTGRES_DB="plane"
+PGDATA="/var/lib/postgresql/data"
+
+# Redis Settings
+REDIS_HOST="plane-redis"
+REDIS_PORT="6379"
+
+# RabbitMQ Settings
+RABBITMQ_HOST="plane-mq"
+RABBITMQ_PORT="5672"
+RABBITMQ_USER="plane"
+RABBITMQ_PASSWORD="plane"
+RABBITMQ_VHOST="plane"
+
+LISTEN_HTTP_PORT=80
+LISTEN_HTTPS_PORT=443
+
+# AWS Settings
+AWS_REGION=""
+AWS_ACCESS_KEY_ID="access-key"
+AWS_SECRET_ACCESS_KEY="secret-key"
+AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
+# Changing this requires change in the proxy config for uploads if using minio setup
+AWS_S3_BUCKET_NAME="uploads"
+# Maximum file upload limit
+FILE_SIZE_LIMIT=5242880
+
+# GPT settings
+OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
+OPENAI_API_KEY="sk-" # deprecated
+GPT_ENGINE="gpt-3.5-turbo" # deprecated
+
+# Settings related to Docker
+DOCKERIZED=1 # deprecated
+
+# set to 1 If using the pre-configured minio setup
+USE_MINIO=1
+
+# If SSL Cert to be generated, set CERT_EMAIl="email "
+CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
+TRUSTED_PROXIES=0.0.0.0/0
+SITE_ADDRESS=:80
+CERT_EMAIL=
+
+# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL
+# CERT_ACME_DNS="acme_dns "
+CERT_ACME_DNS=
+
+# Force HTTPS for handling SSL Termination
+MINIO_ENDPOINT_SSL=0
+
+# API key rate limit
+API_KEY_RATE_LIMIT="60/minute"
+
+# Custom build settings
+APP_RELEASE=local
+CUSTOM_BUILD=true
diff --git a/setup.sh b/setup.sh
index 12fcceae41c..c26fba22b8a 100755
--- a/setup.sh
+++ b/setup.sh
@@ -1,97 +1,714 @@
#!/bin/bash
-# Plane Project Setup Script
-# This script prepares the local development environment by setting up all necessary .env files
-# https://github.com/makeplane/plane
-
-# Set colors for output messages
-GREEN='\033[0;32m'
-BLUE='\033[0;34m'
-YELLOW='\033[1;33m'
-RED='\033[0;31m'
-BOLD='\033[1m'
-NC='\033[0m' # No Color
-
-# Print header
-echo -e "${BOLD}${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
-echo -e "${BOLD}${BLUE} Plane - Project Management Tool ${NC}"
-echo -e "${BOLD}${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
-echo -e "${BOLD}Setting up your development environment...${NC}\n"
-
-# Function to handle file copying with error checking
-copy_env_file() {
- local source=$1
- local destination=$2
-
- if [ ! -f "$source" ]; then
- echo -e "${RED}Error: Source file $source does not exist.${NC}"
+BRANCH=${BRANCH:-master}
+SCRIPT_DIR=$PWD
+SERVICE_FOLDER=plane-app
+PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
+export APP_RELEASE=stable
+export DOCKERHUB_USER=artifacts.plane.so/makeplane
+export PULL_POLICY=${PULL_POLICY:-if_not_present}
+export GH_REPO=makeplane/plane
+export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
+export FALLBACK_DOWNLOAD_URL="https://raw.githubusercontent.com/$GH_REPO/$BRANCH/deployments/cli/community"
+
+CPU_ARCH=$(uname -m)
+OS_NAME=$(uname)
+UPPER_CPU_ARCH=$(tr '[:lower:]' '[:upper:]' <<< "$CPU_ARCH")
+
+mkdir -p $PLANE_INSTALL_DIR/archive
+DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
+DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
+
+function print_header() {
+clear
+
+cat <<"EOF"
+--------------------------------------------
+ ____ _ /////////
+| _ \| | __ _ _ __ ___ /////////
+| |_) | |/ _` | '_ \ / _ \ ///// /////
+| __/| | (_| | | | | __/ ///// /////
+|_| |_|\__,_|_| |_|\___| ////
+ ////
+--------------------------------------------
+Project management tool from the future
+--------------------------------------------
+EOF
+}
+
+function spinner() {
+ local pid=$1
+ local delay=.5
+ local spinstr='|/-\'
+
+ if ! ps -p "$pid" > /dev/null; then
+ echo "Invalid PID: $pid"
+ return 1
+ fi
+ while ps -p "$pid" > /dev/null; do
+ local temp=${spinstr#?}
+ printf " [%c] " "$spinstr" >&2
+ local spinstr=$temp${spinstr%"$temp"}
+ sleep $delay
+ printf "\b\b\b\b\b\b" >&2
+ done
+ printf " \b\b\b\b" >&2
+}
+
+function checkLatestRelease(){
+ echo "Checking for the latest release..." >&2
+ local latest_release=$(curl -sSL https://api.github.com/repos/$GH_REPO/releases/latest | grep -o '"tag_name": "[^"]*"' | sed 's/"tag_name": "//;s/"//g')
+ if [ -z "$latest_release" ]; then
+ echo "Failed to check for the latest release. Exiting..." >&2
+ exit 1
+ fi
+
+ echo $latest_release
+}
+
+function initialize(){
+ printf "Please wait while we check the availability of Docker images for the selected release ($APP_RELEASE) with ${UPPER_CPU_ARCH} support." >&2
+
+ if [ "$CUSTOM_BUILD" == "true" ]; then
+ echo "" >&2
+ echo "" >&2
+ echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2
+ echo "build"
return 1
fi
- cp "$source" "$destination"
+ local IMAGE_NAME=makeplane/plane-proxy
+ local IMAGE_TAG=${APP_RELEASE}
+ docker manifest inspect "${IMAGE_NAME}:${IMAGE_TAG}" | grep -q "\"architecture\": \"${CPU_ARCH}\"" &
+ local pid=$!
+ spinner "$pid"
+
+ echo "" >&2
+
+ wait "$pid"
if [ $? -eq 0 ]; then
- echo -e "${GREEN}✓${NC} Copied $destination"
+ echo "Plane supports ${CPU_ARCH}" >&2
+ echo "available"
+ return 0
else
- echo -e "${RED}✗${NC} Failed to copy $destination"
+ echo "" >&2
+ echo "" >&2
+ echo "${UPPER_CPU_ARCH} images are not available for selected release ($APP_RELEASE)." >&2
+ echo "" >&2
+ echo "build"
return 1
fi
}
+function getEnvValue() {
+ local key=$1
+ local file=$2
+
+ if [ -z "$key" ] || [ -z "$file" ]; then
+ echo "Invalid arguments supplied"
+ exit 1
+ fi
+
+ if [ -f "$file" ]; then
+ grep -q "^$key=" "$file"
+ if [ $? -eq 0 ]; then
+ local value
+ value=$(grep "^$key=" "$file" | cut -d'=' -f2)
+ echo "$value"
+ else
+ echo ""
+ fi
+ fi
+}
+function updateEnvFile() {
+ local key=$1
+ local value=$2
+ local file=$3
+
+ if [ -z "$key" ] || [ -z "$value" ] || [ -z "$file" ]; then
+ echo "Invalid arguments supplied"
+ exit 1
+ fi
+
+ if [ -f "$file" ]; then
+ # check if key exists in the file
+ grep -q "^$key=" "$file"
+ if [ $? -ne 0 ]; then
+ echo "$key=$value" >> "$file"
+ return
+ else
+ if [ "$OS_NAME" == "Darwin" ]; then
+ value=$(echo "$value" | sed 's/|/\\|/g')
+ sed -i '' "s|^$key=.*|$key=$value|g" "$file"
+ else
+ value=$(echo "$value" | sed 's/\//\\\//g')
+ sed -i "s/^$key=.*/$key=$value/g" "$file"
+ fi
+ fi
+ else
+ echo "File not found: $file"
+ exit 1
+ fi
+}
+
+function updateCustomVariables(){
+ echo "Updating custom variables..." >&2
+ updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
+ updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
+ updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
+ updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
+ echo "Custom variables updated successfully" >&2
+}
+
+function syncEnvFile(){
+ echo "Syncing environment variables..." >&2
+ if [ -f "$PLANE_INSTALL_DIR/plane.env.bak" ]; then
+ updateCustomVariables
+
+ # READ keys of plane.env and update the values from plane.env.bak
+ while IFS= read -r line
+ do
+ # ignore is the line is empty or starts with #
+ if [ -z "$line" ] || [[ $line == \#* ]]; then
+ continue
+ fi
+ key=$(echo "$line" | cut -d'=' -f1)
+ value=$(getEnvValue "$key" "$PLANE_INSTALL_DIR/plane.env.bak")
+ if [ -n "$value" ]; then
+ updateEnvFile "$key" "$value" "$DOCKER_ENV_PATH"
+ fi
+ done < "$DOCKER_ENV_PATH"
+ fi
+ echo "Environment variables synced successfully" >&2
+}
+
+function buildYourOwnImage(){
+ echo "Building images locally..."
+
+ export DOCKERHUB_USER="myplane"
+ export APP_RELEASE="local"
+ export PULL_POLICY="never"
+ CUSTOM_BUILD="true"
+
+ # checkout the code to ~/tmp/plane folder and build the images
+ local PLANE_TEMP_CODE_DIR=~/tmp/plane
+ rm -rf $PLANE_TEMP_CODE_DIR
+ mkdir -p $PLANE_TEMP_CODE_DIR
+ REPO=https://github.com/$GH_REPO.git
+ git clone "$REPO" "$PLANE_TEMP_CODE_DIR" --branch "$BRANCH" --single-branch --depth 1
+
+ cp "$PLANE_TEMP_CODE_DIR/deployments/cli/community/build.yml" "$PLANE_TEMP_CODE_DIR/build.yml"
+
+ cd "$PLANE_TEMP_CODE_DIR" || exit
+
+ /bin/bash -c "$COMPOSE_CMD -f build.yml build --no-cache" >&2
+ if [ $? -ne 0 ]; then
+ echo "Build failed. Exiting..."
+ exit 1
+ fi
+ echo "Build completed successfully"
+ echo ""
+ echo "You can now start the services by running the command: ./setup.sh start"
+ echo ""
+}
+
+function install() {
+ echo "Begin Installing Plane"
+ echo ""
+
+ if [ "$APP_RELEASE" == "stable" ]; then
+ export APP_RELEASE=$(checkLatestRelease)
+ fi
+
+ local build_image=$(initialize)
+
+ if [ "$build_image" == "build" ]; then
+ # ask for confirmation to continue building the images
+ echo "Do you want to continue with building the Docker images locally?"
+ read -p "Continue? [y/N]: " confirm
+ if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
+ echo "Exiting..."
+ exit 0
+ fi
+ fi
+
+ if [ "$build_image" == "build" ]; then
+ download "true"
+ else
+ download "false"
+ fi
+}
+
+function download() {
+ local LOCAL_BUILD=$1
+ cd $SCRIPT_DIR
+ TS=$(date +%s)
+ if [ -f "$PLANE_INSTALL_DIR/docker-compose.yaml" ]
+ then
+ mv $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml
+ fi
+
+ RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml?$(date +%s)")
+ BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
+ STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
+
+ if [ "$STATUS" -eq 200 ]; then
+ echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml
+ else
+ # Fallback to download from the raw github url
+ RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/docker-compose.yml?$(date +%s)")
+ BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
+ STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
+
+ if [ "$STATUS" -eq 200 ]; then
+ echo "$BODY" > $PLANE_INSTALL_DIR/docker-compose.yaml
+ else
+ echo "Failed to download docker-compose.yml. HTTP Status: $STATUS"
+ echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/docker-compose.yml"
+ mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml
+ exit 1
+ fi
+ fi
+
+ RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env?$(date +%s)")
+ BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
+ STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
+
+ if [ "$STATUS" -eq 200 ]; then
+ echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
+ else
+ # Fallback to download from the raw github url
+ RESPONSE=$(curl -sSL -H 'Cache-Control: no-cache, no-store' -w "HTTPSTATUS:%{http_code}" "$FALLBACK_DOWNLOAD_URL/variables.env?$(date +%s)")
+ BODY=$(echo "$RESPONSE" | sed -e 's/HTTPSTATUS\:.*//g')
+ STATUS=$(echo "$RESPONSE" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
+
+ if [ "$STATUS" -eq 200 ]; then
+ echo "$BODY" > $PLANE_INSTALL_DIR/variables-upgrade.env
+ else
+ echo "Failed to download variables.env. HTTP Status: $STATUS"
+ echo "URL: $RELEASE_DOWNLOAD_URL/$APP_RELEASE/variables.env"
+ mv $PLANE_INSTALL_DIR/archive/$TS.docker-compose.yaml $PLANE_INSTALL_DIR/docker-compose.yaml
+ exit 1
+ fi
+ fi
+
+ if [ -f "$DOCKER_ENV_PATH" ];
+ then
+ cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/archive/$TS.env"
+ cp "$DOCKER_ENV_PATH" "$PLANE_INSTALL_DIR/plane.env.bak"
+ fi
+
+ mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
+
+ syncEnvFile
+
+ if [ "$LOCAL_BUILD" == "true" ]; then
+ export DOCKERHUB_USER="myplane"
+ export APP_RELEASE="local"
+ export PULL_POLICY="never"
+ CUSTOM_BUILD="true"
+
+ buildYourOwnImage
+
+ if [ $? -ne 0 ]; then
+ echo ""
+ echo "Build failed. Exiting..."
+ exit 1
+ fi
+ updateCustomVariables
+ else
+ CUSTOM_BUILD="false"
+ updateCustomVariables
+ /bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH pull --policy always"
+
+ if [ $? -ne 0 ]; then
+ echo ""
+ echo "Failed to pull the images. Exiting..."
+ exit 1
+ fi
+ fi
+
+ echo ""
+ echo "Most recent version of Plane is now available for you to use"
+ echo ""
+ echo "In case of 'Upgrade', please check the 'plane.env 'file for any new variables and update them accordingly"
+ echo ""
+}
+function startServices() {
+ /bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --pull if_not_present --quiet-pull"
-# Export character encoding settings for macOS compatibility
-export LC_ALL=C
-export LC_CTYPE=C
-echo -e "${YELLOW}Setting up environment files...${NC}"
+ local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
+ if [ -n "$migrator_container_id" ]; then
+ local idx=0
+ while docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do
+ local message=">> Waiting for Data Migration to finish"
+ local dots=$(printf '%*s' $idx | tr ' ' '.')
+ echo -ne "\r$message$dots"
+ ((idx++))
+ sleep 1
+ done
+ fi
+ printf "\r\033[K"
+ echo ""
+ echo " Data Migration completed successfully ✅"
+
+ # if migrator exit status is not 0, show error message and exit
+ if [ -n "$migrator_container_id" ]; then
+ local migrator_exit_code=$(docker inspect --format='{{.State.ExitCode}}' $migrator_container_id)
+ if [ $migrator_exit_code -ne 0 ]; then
+ echo "Plane Server failed to start ❌"
+ # stopServices
+ echo
+ echo "Please check the logs for the 'migrator' service and resolve the issue(s)."
+ echo "Stop the services by running the command: ./setup.sh stop"
+ exit 1
+ fi
+ fi
+
+ local api_container_id=$(docker container ls -q -f "name=$SERVICE_FOLDER-api")
+
+ # Verify container exists
+ if [ -z "$api_container_id" ]; then
+ echo " Error: API container not found. Please check if services are running."
+ exit 1
+ fi
+
+ local idx2=0
+ local api_ready=true # assume success, flip on timeout
+ local max_wait_time=300 # 5 minutes timeout
+ local start_time=$(date +%s)
+
+ echo " Waiting for API Service to be ready..."
+ while ! docker exec "$api_container_id" python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/', timeout=3)" > /dev/null 2>&1; do
+ local current_time=$(date +%s)
+ local elapsed_time=$((current_time - start_time))
+
+ if [ $elapsed_time -gt $max_wait_time ]; then
+ echo ""
+ echo " API Service health check timed out after 5 minutes"
+ echo " Checking if API container is still running..."
+ if docker ps | grep -q "$SERVICE_FOLDER-api"; then
+ echo " API container is running but did not pass the health-check. Continuing without marking it ready."
+ api_ready=false
+ break
+ else
+ echo " API container is not running. Please check logs."
+ exit 1
+ fi
+ fi
+
+ local message=">> Waiting for API Service to Start (${elapsed_time}s)"
+ local dots=$(printf '%*s' $idx2 | tr ' ' '.')
+ echo -ne "\r$message$dots"
+ ((idx2++))
+ sleep 1
+ done
+ printf "\r\033[K"
+ if [ "$api_ready" = true ]; then
+ echo " API Service started successfully ✅"
+ else
+ echo " ⚠️ API Service did not respond to health-check – please verify manually."
+ fi
+ source "${DOCKER_ENV_PATH}"
+ echo " Plane Server started successfully ✅"
+ echo ""
+ echo " You can access the application at $WEB_URL"
+ echo ""
+
+}
+function stopServices() {
+ /bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH down"
+}
+function restartServices() {
+ stopServices
+ startServices
+}
+function upgrade() {
+ local latest_release=$(checkLatestRelease)
+
+ echo ""
+ echo "Current release: $APP_RELEASE"
+
+ if [ "$latest_release" == "$APP_RELEASE" ]; then
+ echo ""
+ echo "You are already using the latest release"
+ exit 0
+ fi
+
+ echo "Latest release: $latest_release"
+ echo ""
+
+ # Check for confirmation to upgrade
+ echo "Do you want to upgrade to the latest release ($latest_release)?"
+ read -p "Continue? [y/N]: " confirm
+
+ if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
+ echo "Exiting..."
+ exit 0
+ fi
+
+ export APP_RELEASE=$latest_release
+
+ echo "Upgrading Plane to the latest release..."
+ echo ""
+
+ echo "***** STOPPING SERVICES ****"
+ stopServices
+
+ echo
+ echo "***** DOWNLOADING STABLE VERSION ****"
+ install
-# Copy all environment example files
-services=("" "web" "api" "space" "admin" "live")
-success=true
+ echo "***** PLEASE VALIDATE AND START SERVICES ****"
+}
+function viewSpecificLogs(){
+ local SERVICE_NAME=$1
-for service in "${services[@]}"; do
- if [ "$service" == "" ]; then
- # Handle root .env file
- prefix="./"
+ if /bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH ps | grep -q '$SERVICE_NAME'"; then
+ echo "Service '$SERVICE_NAME' is running."
else
- # Handle service .env files in apps folder
- prefix="./apps/$service/"
+ echo "Service '$SERVICE_NAME' is not running."
+ fi
+
+ /bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH logs -f $SERVICE_NAME"
+}
+function viewLogs(){
+
+ ARG_SERVICE_NAME=$2
+
+ if [ -z "$ARG_SERVICE_NAME" ];
+ then
+ echo
+ echo "Select a Service you want to view the logs for:"
+ echo " 1) Web"
+ echo " 2) Space"
+ echo " 3) API"
+ echo " 4) Worker"
+ echo " 5) Beat-Worker"
+ echo " 6) Migrator"
+ echo " 7) Proxy"
+ echo " 8) Redis"
+ echo " 9) Postgres"
+ echo " 10) Minio"
+ echo " 11) RabbitMQ"
+ echo " 0) Back to Main Menu"
+ echo
+ read -p "Service: " DOCKER_SERVICE_NAME
+
+ until (( DOCKER_SERVICE_NAME >= 0 && DOCKER_SERVICE_NAME <= 11 )); do
+ echo "Invalid selection. Please enter a number between 0 and 11."
+ read -p "Service: " DOCKER_SERVICE_NAME
+ done
+
+ if [ -z "$DOCKER_SERVICE_NAME" ];
+ then
+ echo "INVALID SERVICE NAME SUPPLIED"
+ else
+ case $DOCKER_SERVICE_NAME in
+ 1) viewSpecificLogs "web";;
+ 2) viewSpecificLogs "space";;
+ 3) viewSpecificLogs "api";;
+ 4) viewSpecificLogs "worker";;
+ 5) viewSpecificLogs "beat-worker";;
+ 6) viewSpecificLogs "migrator";;
+ 7) viewSpecificLogs "proxy";;
+ 8) viewSpecificLogs "plane-redis";;
+ 9) viewSpecificLogs "plane-db";;
+ 10) viewSpecificLogs "plane-minio";;
+ 11) viewSpecificLogs "plane-mq";;
+ 0) askForAction;;
+ *) echo "INVALID SERVICE NAME SUPPLIED";;
+ esac
+ fi
+ elif [ -n "$ARG_SERVICE_NAME" ];
+ then
+ ARG_SERVICE_NAME=$(echo "$ARG_SERVICE_NAME" | tr '[:upper:]' '[:lower:]')
+ case $ARG_SERVICE_NAME in
+ web) viewSpecificLogs "web";;
+ space) viewSpecificLogs "space";;
+ api) viewSpecificLogs "api";;
+ worker) viewSpecificLogs "worker";;
+ beat-worker) viewSpecificLogs "beat-worker";;
+ migrator) viewSpecificLogs "migrator";;
+ proxy) viewSpecificLogs "proxy";;
+ redis) viewSpecificLogs "plane-redis";;
+ postgres) viewSpecificLogs "plane-db";;
+ minio) viewSpecificLogs "plane-minio";;
+ rabbitmq) viewSpecificLogs "plane-mq";;
+ *) echo "INVALID SERVICE NAME SUPPLIED";;
+ esac
+ else
+ echo "INVALID SERVICE NAME SUPPLIED"
+ fi
+}
+function backup_container_dir() {
+ local BACKUP_FOLDER=$1
+ local CONTAINER_NAME=$2
+ local CONTAINER_DATA_DIR=$3
+ local SERVICE_FOLDER=$4
+
+ echo "Backing up $CONTAINER_NAME data..."
+ local CONTAINER_ID=$(/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH ps -q $CONTAINER_NAME")
+ if [ -z "$CONTAINER_ID" ]; then
+ echo "Error: $CONTAINER_NAME container not found. Make sure the services are running."
+ return 1
+ fi
+
+ # Create a temporary directory for the backup
+ mkdir -p "$BACKUP_FOLDER/$SERVICE_FOLDER"
+
+ # Copy the data directory from the running container
+ echo "Copying $CONTAINER_NAME data directory..."
+ docker cp -q "$CONTAINER_ID:$CONTAINER_DATA_DIR/." "$BACKUP_FOLDER/$SERVICE_FOLDER/"
+ local cp_status=$?
+
+ if [ $cp_status -ne 0 ]; then
+ echo "Error: Failed to copy $SERVICE_FOLDER data"
+ rm -rf $BACKUP_FOLDER/$SERVICE_FOLDER
+ return 1
+ fi
+
+ # Create tar.gz of the data
+ cd "$BACKUP_FOLDER"
+ tar -czf "${SERVICE_FOLDER}.tar.gz" "$SERVICE_FOLDER/"
+ local tar_status=$?
+ if [ $tar_status -eq 0 ]; then
+ rm -rf "$SERVICE_FOLDER/"
+ fi
+ cd - > /dev/null
+
+ if [ $tar_status -ne 0 ]; then
+ echo "Error: Failed to create tar archive"
+ return 1
+ fi
+
+ echo "Successfully backed up $SERVICE_FOLDER data"
+}
+
+function backupData() {
+ local datetime=$(date +"%Y%m%d-%H%M")
+ local BACKUP_FOLDER=$PLANE_INSTALL_DIR/backup/$datetime
+ mkdir -p "$BACKUP_FOLDER"
+
+ # Check if docker-compose.yml exists
+ if [ ! -f "$DOCKER_FILE_PATH" ]; then
+ echo "Error: docker-compose.yml not found at $DOCKER_FILE_PATH"
+ exit 1
fi
- copy_env_file "${prefix}.env.example" "${prefix}.env" || success=false
-done
+ backup_container_dir "$BACKUP_FOLDER" "plane-db" "/var/lib/postgresql/data" "pgdata" || exit 1
+ backup_container_dir "$BACKUP_FOLDER" "plane-minio" "/export" "uploads" || exit 1
+ backup_container_dir "$BACKUP_FOLDER" "plane-mq" "/var/lib/rabbitmq" "rabbitmq_data" || exit 1
+ backup_container_dir "$BACKUP_FOLDER" "plane-redis" "/data" "redisdata" || exit 1
-# Generate SECRET_KEY for Django
-if [ -f "./apps/api/.env" ]; then
- echo -e "\n${YELLOW}Generating Django SECRET_KEY...${NC}"
- SECRET_KEY=$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)
+ echo ""
+ echo "Backup completed successfully. Backup files are stored in $BACKUP_FOLDER"
+ echo ""
+}
+function askForAction() {
+ local DEFAULT_ACTION=$1
+
+ if [ -z "$DEFAULT_ACTION" ];
+ then
+ echo
+ echo "Select a Action you want to perform:"
+ echo " 1) Install"
+ echo " 2) Start"
+ echo " 3) Stop"
+ echo " 4) Restart"
+ echo " 5) Upgrade"
+ echo " 6) View Logs"
+ echo " 7) Backup Data"
+ echo " 8) Exit"
+ echo
+ read -p "Action [2]: " ACTION
+ until [[ -z "$ACTION" || "$ACTION" =~ ^[1-8]$ ]]; do
+ echo "$ACTION: invalid selection."
+ read -p "Action [2]: " ACTION
+ done
- if [ -z "$SECRET_KEY" ]; then
- echo -e "${RED}Error: Failed to generate SECRET_KEY.${NC}"
- echo -e "${RED}Ensure 'tr' and 'head' commands are available on your system.${NC}"
- success=false
+ if [ -z "$ACTION" ];
+ then
+ ACTION=2
+ fi
+ echo
+ fi
+
+ if [ "$ACTION" == "1" ] || [ "$DEFAULT_ACTION" == "install" ];
+ then
+ install
+ # askForAction
+ elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ];
+ then
+ startServices
+ # askForAction
+ elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ];
+ then
+ stopServices
+ # askForAction
+ elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ];
+ then
+ restartServices
+ # askForAction
+ elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ];
+ then
+ upgrade
+ # askForAction
+ elif [ "$ACTION" == "6" ] || [ "$DEFAULT_ACTION" == "logs" ];
+ then
+ viewLogs "$@"
+ askForAction
+ elif [ "$ACTION" == "7" ] || [ "$DEFAULT_ACTION" == "backup" ];
+ then
+ backupData
+ elif [ "$ACTION" == "8" ]
+ then
+ exit 0
else
- echo -e "SECRET_KEY=\"$SECRET_KEY\"" >> ./apps/api/.env
- echo -e "${GREEN}✓${NC} Added SECRET_KEY to apps/api/.env"
+ echo "INVALID ACTION SUPPLIED"
fi
+}
+
+# if docker-compose is installed
+if command -v docker-compose &> /dev/null
+then
+ COMPOSE_CMD="docker-compose"
else
- echo -e "${RED}✗${NC} apps/api/.env not found. SECRET_KEY not added."
- success=false
+ COMPOSE_CMD="docker compose"
fi
-# Activate pnpm (version set in package.json)
-corepack enable pnpm || success=false
-# Install Node dependencies
-pnpm install || success=false
-
-# Summary
-echo -e "\n${YELLOW}Setup status:${NC}"
-if [ "$success" = true ]; then
- echo -e "${GREEN}✓${NC} Environment setup completed successfully!\n"
- echo -e "${BOLD}Next steps:${NC}"
- echo -e "1. Review the .env files in each folder if needed"
- echo -e "2. Start the services with: ${BOLD}docker compose -f docker-compose-local.yml up -d${NC}"
- echo -e "\n${GREEN}Happy coding! 🚀${NC}"
-else
- echo -e "${RED}✗${NC} Some issues occurred during setup. Please check the errors above.\n"
- echo -e "For help, visit: ${BLUE}https://github.com/makeplane/plane${NC}"
- exit 1
+if [ "$CPU_ARCH" == "x86_64" ] || [ "$CPU_ARCH" == "amd64" ]; then
+ CPU_ARCH="amd64"
+elif [ "$CPU_ARCH" == "aarch64" ] || [ "$CPU_ARCH" == "arm64" ]; then
+ CPU_ARCH="arm64"
+fi
+
+if [ -f "$DOCKER_ENV_PATH" ]; then
+ DOCKERHUB_USER=$(getEnvValue "DOCKERHUB_USER" "$DOCKER_ENV_PATH")
+ APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
+ PULL_POLICY=$(getEnvValue "PULL_POLICY" "$DOCKER_ENV_PATH")
+ CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
+
+ if [ -z "$DOCKERHUB_USER" ]; then
+ DOCKERHUB_USER=artifacts.plane.so/makeplane
+ updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
+ fi
+
+ if [ -z "$APP_RELEASE" ]; then
+ APP_RELEASE=stable
+ updateEnvFile "APP_RELEASE" "$APP_RELEASE" "$DOCKER_ENV_PATH"
+ fi
+
+ if [ -z "$PULL_POLICY" ]; then
+ PULL_POLICY=if_not_present
+ updateEnvFile "PULL_POLICY" "$PULL_POLICY" "$DOCKER_ENV_PATH"
+ fi
+
+ if [ -z "$CUSTOM_BUILD" ]; then
+ CUSTOM_BUILD=false
+ updateEnvFile "CUSTOM_BUILD" "$CUSTOM_BUILD" "$DOCKER_ENV_PATH"
+ fi
fi
+
+print_header
+askForAction "$@"
diff --git a/setup_lib.sh b/setup_lib.sh
new file mode 100644
index 00000000000..12fcceae41c
--- /dev/null
+++ b/setup_lib.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+# Plane Project Setup Script
+# This script prepares the local development environment by setting up all necessary .env files
+# https://github.com/makeplane/plane
+
+# Set colors for output messages
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BOLD='\033[1m'
+NC='\033[0m' # No Color
+
+# Print header
+echo -e "${BOLD}${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${BOLD}${BLUE} Plane - Project Management Tool ${NC}"
+echo -e "${BOLD}${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${BOLD}Setting up your development environment...${NC}\n"
+
+# Function to handle file copying with error checking
+copy_env_file() {
+ local source=$1
+ local destination=$2
+
+ if [ ! -f "$source" ]; then
+ echo -e "${RED}Error: Source file $source does not exist.${NC}"
+ return 1
+ fi
+
+ cp "$source" "$destination"
+
+ if [ $? -eq 0 ]; then
+ echo -e "${GREEN}✓${NC} Copied $destination"
+ else
+ echo -e "${RED}✗${NC} Failed to copy $destination"
+ return 1
+ fi
+}
+
+# Export character encoding settings for macOS compatibility
+export LC_ALL=C
+export LC_CTYPE=C
+echo -e "${YELLOW}Setting up environment files...${NC}"
+
+# Copy all environment example files
+services=("" "web" "api" "space" "admin" "live")
+success=true
+
+for service in "${services[@]}"; do
+ if [ "$service" == "" ]; then
+ # Handle root .env file
+ prefix="./"
+ else
+ # Handle service .env files in apps folder
+ prefix="./apps/$service/"
+ fi
+
+ copy_env_file "${prefix}.env.example" "${prefix}.env" || success=false
+done
+
+# Generate SECRET_KEY for Django
+if [ -f "./apps/api/.env" ]; then
+ echo -e "\n${YELLOW}Generating Django SECRET_KEY...${NC}"
+ SECRET_KEY=$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)
+
+ if [ -z "$SECRET_KEY" ]; then
+ echo -e "${RED}Error: Failed to generate SECRET_KEY.${NC}"
+ echo -e "${RED}Ensure 'tr' and 'head' commands are available on your system.${NC}"
+ success=false
+ else
+ echo -e "SECRET_KEY=\"$SECRET_KEY\"" >> ./apps/api/.env
+ echo -e "${GREEN}✓${NC} Added SECRET_KEY to apps/api/.env"
+ fi
+else
+ echo -e "${RED}✗${NC} apps/api/.env not found. SECRET_KEY not added."
+ success=false
+fi
+
+# Activate pnpm (version set in package.json)
+corepack enable pnpm || success=false
+# Install Node dependencies
+pnpm install || success=false
+
+# Summary
+echo -e "\n${YELLOW}Setup status:${NC}"
+if [ "$success" = true ]; then
+ echo -e "${GREEN}✓${NC} Environment setup completed successfully!\n"
+ echo -e "${BOLD}Next steps:${NC}"
+ echo -e "1. Review the .env files in each folder if needed"
+ echo -e "2. Start the services with: ${BOLD}docker compose -f docker-compose-local.yml up -d${NC}"
+ echo -e "\n${GREEN}Happy coding! 🚀${NC}"
+else
+ echo -e "${RED}✗${NC} Some issues occurred during setup. Please check the errors above.\n"
+ echo -e "For help, visit: ${BLUE}https://github.com/makeplane/plane${NC}"
+ exit 1
+fi