@geekmidas/cli 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{bundler-DQIuE3Kn.mjs → bundler-Db83tLti.mjs} +2 -2
- package/dist/{bundler-DQIuE3Kn.mjs.map → bundler-Db83tLti.mjs.map} +1 -1
- package/dist/{bundler-CyHg1v_T.cjs → bundler-DsXfFSCU.cjs} +2 -2
- package/dist/{bundler-CyHg1v_T.cjs.map → bundler-DsXfFSCU.cjs.map} +1 -1
- package/dist/{config-BC5n1a2D.mjs → config-C0b0jdmU.mjs} +2 -2
- package/dist/{config-BC5n1a2D.mjs.map → config-C0b0jdmU.mjs.map} +1 -1
- package/dist/{config-BAE9LFC1.cjs → config-xVZsRjN7.cjs} +2 -2
- package/dist/{config-BAE9LFC1.cjs.map → config-xVZsRjN7.cjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/dokploy-api-Bdmk5ImW.cjs +3 -0
- package/dist/{dokploy-api-C5czOZoc.cjs → dokploy-api-BdxOMH_V.cjs} +43 -1
- package/dist/{dokploy-api-C5czOZoc.cjs.map → dokploy-api-BdxOMH_V.cjs.map} +1 -1
- package/dist/{dokploy-api-B9qR2Yn1.mjs → dokploy-api-DWsqNjwP.mjs} +43 -1
- package/dist/{dokploy-api-B9qR2Yn1.mjs.map → dokploy-api-DWsqNjwP.mjs.map} +1 -1
- package/dist/dokploy-api-tZSZaHd9.mjs +3 -0
- package/dist/{encryption-JtMsiGNp.mjs → encryption-BC4MAODn.mjs} +1 -1
- package/dist/{encryption-JtMsiGNp.mjs.map → encryption-BC4MAODn.mjs.map} +1 -1
- package/dist/encryption-Biq0EZ4m.cjs +4 -0
- package/dist/encryption-CQXBZGkt.mjs +3 -0
- package/dist/{encryption-BAz0xQ1Q.cjs → encryption-DaCB_NmS.cjs} +13 -3
- package/dist/{encryption-BAz0xQ1Q.cjs.map → encryption-DaCB_NmS.cjs.map} +1 -1
- package/dist/{index-C7TkoYmt.d.mts → index-CXa3odEw.d.mts} +68 -7
- package/dist/index-CXa3odEw.d.mts.map +1 -0
- package/dist/{index-CpchsC9w.d.cts → index-E8Nu2Rxl.d.cts} +67 -6
- package/dist/index-E8Nu2Rxl.d.cts.map +1 -0
- package/dist/index.cjs +787 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +767 -125
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CjYeF-Tg.mjs → openapi-D3pA6FfZ.mjs} +2 -2
- package/dist/{openapi-CjYeF-Tg.mjs.map → openapi-D3pA6FfZ.mjs.map} +1 -1
- package/dist/{openapi-a-e3Y8WA.cjs → openapi-DhcCtKzM.cjs} +2 -2
- package/dist/{openapi-a-e3Y8WA.cjs.map → openapi-DhcCtKzM.cjs.map} +1 -1
- package/dist/{openapi-react-query-DvNpdDpM.cjs → openapi-react-query-C_MxpBgF.cjs} +1 -1
- package/dist/{openapi-react-query-DvNpdDpM.cjs.map → openapi-react-query-C_MxpBgF.cjs.map} +1 -1
- package/dist/{openapi-react-query-5rSortLH.mjs → openapi-react-query-ZoP9DPbY.mjs} +1 -1
- package/dist/{openapi-react-query-5rSortLH.mjs.map → openapi-react-query-ZoP9DPbY.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{types-K2uQJ-FO.d.mts → types-BtGL-8QS.d.mts} +1 -1
- package/dist/{types-K2uQJ-FO.d.mts.map → types-BtGL-8QS.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-My0A4IRO.cjs → workspace-BDAhr6Kb.cjs} +33 -4
- package/dist/{workspace-My0A4IRO.cjs.map → workspace-BDAhr6Kb.cjs.map} +1 -1
- package/dist/{workspace-DFJ3sWfY.mjs → workspace-D_6ZCaR_.mjs} +33 -4
- package/dist/{workspace-DFJ3sWfY.mjs.map → workspace-D_6ZCaR_.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/build/index.ts +23 -6
- package/src/deploy/__tests__/domain.spec.ts +231 -0
- package/src/deploy/__tests__/secrets.spec.ts +300 -0
- package/src/deploy/__tests__/sniffer.spec.ts +221 -0
- package/src/deploy/docker.ts +58 -29
- package/src/deploy/dokploy-api.ts +99 -0
- package/src/deploy/domain.ts +125 -0
- package/src/deploy/index.ts +364 -145
- package/src/deploy/secrets.ts +182 -0
- package/src/deploy/sniffer.ts +180 -0
- package/src/dev/index.ts +155 -9
- package/src/docker/index.ts +17 -2
- package/src/docker/templates.ts +171 -1
- package/src/index.ts +18 -1
- package/src/init/generators/auth.ts +2 -0
- package/src/init/versions.ts +2 -2
- package/src/workspace/index.ts +2 -0
- package/src/workspace/schema.ts +32 -6
- package/src/workspace/types.ts +64 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/dokploy-api-B0w17y4_.mjs +0 -3
- package/dist/dokploy-api-BnGeUqN4.cjs +0 -3
- package/dist/index-C7TkoYmt.d.mts.map +0 -1
- package/dist/index-CpchsC9w.d.cts.map +0 -1
package/src/deploy/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { storeDokployRegistryId } from '../auth/credentials';
|
|
10
10
|
import { buildCommand } from '../build/index';
|
|
11
11
|
import { type GkmConfig, loadConfig, loadWorkspaceConfig } from '../config';
|
|
12
|
+
import { readStageSecrets } from '../secrets/storage.js';
|
|
12
13
|
import {
|
|
13
14
|
getAppBuildOrder,
|
|
14
15
|
getDeployTargetError,
|
|
@@ -18,7 +19,18 @@ import type { NormalizedWorkspace } from '../workspace/types.js';
|
|
|
18
19
|
import { deployDocker, resolveDockerConfig } from './docker';
|
|
19
20
|
import { deployDokploy } from './dokploy';
|
|
20
21
|
import { DokployApi, type DokployApplication } from './dokploy-api';
|
|
22
|
+
import {
|
|
23
|
+
generatePublicUrlBuildArgs,
|
|
24
|
+
getPublicUrlArgNames,
|
|
25
|
+
isMainFrontendApp,
|
|
26
|
+
resolveHost,
|
|
27
|
+
} from './domain.js';
|
|
21
28
|
import { updateConfig } from './init';
|
|
29
|
+
import {
|
|
30
|
+
generateSecretsReport,
|
|
31
|
+
prepareSecretsForAllApps,
|
|
32
|
+
} from './secrets.js';
|
|
33
|
+
import { sniffAllApps } from './sniffer.js';
|
|
22
34
|
import type {
|
|
23
35
|
AppDeployResult,
|
|
24
36
|
DeployOptions,
|
|
@@ -559,17 +571,23 @@ export function generateTag(stage: string): string {
|
|
|
559
571
|
|
|
560
572
|
/**
|
|
561
573
|
* Deploy all apps in a workspace to Dokploy.
|
|
562
|
-
*
|
|
563
|
-
* -
|
|
564
|
-
* -
|
|
565
|
-
* -
|
|
574
|
+
*
|
|
575
|
+
* Two-phase orchestration:
|
|
576
|
+
* - PHASE 1: Deploy backend apps (with encrypted secrets)
|
|
577
|
+
* - PHASE 2: Deploy frontend apps (with public URLs from backends)
|
|
578
|
+
*
|
|
579
|
+
* Security model:
|
|
580
|
+
* - Backend apps get encrypted secrets embedded at build time
|
|
581
|
+
* - Only GKM_MASTER_KEY is injected as Dokploy env var
|
|
582
|
+
* - Frontend apps get public URLs baked in at build time (no secrets)
|
|
583
|
+
*
|
|
566
584
|
* @internal Exported for testing
|
|
567
585
|
*/
|
|
568
586
|
export async function workspaceDeployCommand(
|
|
569
587
|
workspace: NormalizedWorkspace,
|
|
570
588
|
options: DeployOptions,
|
|
571
589
|
): Promise<WorkspaceDeployResult> {
|
|
572
|
-
const { provider, stage, tag,
|
|
590
|
+
const { provider, stage, tag, apps: selectedApps } = options;
|
|
573
591
|
|
|
574
592
|
if (provider !== 'dokploy') {
|
|
575
593
|
throw new Error(
|
|
@@ -608,8 +626,6 @@ export async function workspaceDeployCommand(
|
|
|
608
626
|
}
|
|
609
627
|
|
|
610
628
|
// Filter apps by deploy target
|
|
611
|
-
// In Phase 1, only 'dokploy' is supported. Other targets should have been
|
|
612
|
-
// caught at config validation, but we handle it gracefully here too.
|
|
613
629
|
const dokployApps = appsToDeployNames.filter((name) => {
|
|
614
630
|
const app = workspace.apps[name]!;
|
|
615
631
|
const target = app.resolvedDeployTarget;
|
|
@@ -628,18 +644,44 @@ export async function workspaceDeployCommand(
|
|
|
628
644
|
);
|
|
629
645
|
}
|
|
630
646
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
647
|
+
appsToDeployNames = dokployApps;
|
|
648
|
+
|
|
649
|
+
// ==================================================================
|
|
650
|
+
// PREFLIGHT: Load secrets and sniff environment requirements
|
|
651
|
+
// ==================================================================
|
|
652
|
+
logger.log('\n🔐 Loading secrets and analyzing environment requirements...');
|
|
653
|
+
|
|
654
|
+
// Load secrets for this stage
|
|
655
|
+
const stageSecrets = await readStageSecrets(stage, workspace.root);
|
|
656
|
+
if (!stageSecrets) {
|
|
657
|
+
logger.log(` ⚠️ No secrets found for stage "${stage}"`);
|
|
658
|
+
logger.log(` Run "gkm secrets:init --stage ${stage}" to create secrets`);
|
|
638
659
|
}
|
|
639
660
|
|
|
640
|
-
|
|
661
|
+
// Sniff environment variables for all apps
|
|
662
|
+
const sniffedApps = await sniffAllApps(workspace.apps, workspace.root);
|
|
663
|
+
|
|
664
|
+
// Prepare encrypted secrets for backend apps
|
|
665
|
+
const encryptedSecrets = stageSecrets
|
|
666
|
+
? prepareSecretsForAllApps(stageSecrets, sniffedApps)
|
|
667
|
+
: new Map();
|
|
668
|
+
|
|
669
|
+
// Report on secrets preparation
|
|
670
|
+
if (stageSecrets) {
|
|
671
|
+
const report = generateSecretsReport(encryptedSecrets, sniffedApps);
|
|
672
|
+
if (report.appsWithSecrets.length > 0) {
|
|
673
|
+
logger.log(` ✓ Encrypted secrets for: ${report.appsWithSecrets.join(', ')}`);
|
|
674
|
+
}
|
|
675
|
+
if (report.appsWithMissingSecrets.length > 0) {
|
|
676
|
+
for (const { appName, missing } of report.appsWithMissingSecrets) {
|
|
677
|
+
logger.log(` ⚠️ ${appName}: Missing secrets: ${missing.join(', ')}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
641
681
|
|
|
642
|
-
//
|
|
682
|
+
// ==================================================================
|
|
683
|
+
// SETUP: Credentials, Project, Registry
|
|
684
|
+
// ==================================================================
|
|
643
685
|
let creds = await getDokployCredentials();
|
|
644
686
|
if (!creds) {
|
|
645
687
|
logger.log("\n📋 Dokploy credentials not found. Let's set them up.");
|
|
@@ -684,7 +726,6 @@ export async function workspaceDeployCommand(
|
|
|
684
726
|
|
|
685
727
|
if (project) {
|
|
686
728
|
logger.log(` Found existing project: ${project.name}`);
|
|
687
|
-
// Get or create environment for stage
|
|
688
729
|
const projectDetails = await api.getProject(project.projectId);
|
|
689
730
|
const environments = projectDetails.environments ?? [];
|
|
690
731
|
const matchingEnv = environments.find(
|
|
@@ -703,7 +744,6 @@ export async function workspaceDeployCommand(
|
|
|
703
744
|
logger.log(` Creating project: ${projectName}`);
|
|
704
745
|
const result = await api.createProject(projectName);
|
|
705
746
|
project = result.project;
|
|
706
|
-
// Create environment for stage if different from default
|
|
707
747
|
if (result.environment.name.toLowerCase() !== stage.toLowerCase()) {
|
|
708
748
|
logger.log(` Creating "${stage}" environment...`);
|
|
709
749
|
const env = await api.createEnvironment(project.projectId, stage);
|
|
@@ -733,7 +773,6 @@ export async function workspaceDeployCommand(
|
|
|
733
773
|
if (!registryId) {
|
|
734
774
|
const registries = await api.listRegistries();
|
|
735
775
|
if (registries.length > 0) {
|
|
736
|
-
// Use first available registry
|
|
737
776
|
registryId = registries[0]!.registryId;
|
|
738
777
|
await storeDokployRegistryId(registryId);
|
|
739
778
|
logger.log(` Using registry: ${registries[0]!.registryName}`);
|
|
@@ -778,171 +817,343 @@ export async function workspaceDeployCommand(
|
|
|
778
817
|
);
|
|
779
818
|
}
|
|
780
819
|
|
|
781
|
-
//
|
|
782
|
-
|
|
820
|
+
// ==================================================================
|
|
821
|
+
// Separate apps by type for two-phase deployment
|
|
822
|
+
// ==================================================================
|
|
823
|
+
const backendApps = appsToDeployNames.filter(
|
|
824
|
+
(name) => workspace.apps[name]!.type === 'backend',
|
|
825
|
+
);
|
|
826
|
+
const frontendApps = appsToDeployNames.filter(
|
|
827
|
+
(name) => workspace.apps[name]!.type === 'frontend',
|
|
828
|
+
);
|
|
783
829
|
|
|
784
|
-
//
|
|
785
|
-
|
|
830
|
+
// Track deployed app public URLs for frontend builds
|
|
831
|
+
const publicUrls: Record<string, string> = {};
|
|
786
832
|
const results: AppDeployResult[] = [];
|
|
833
|
+
const dokployConfig = workspace.deploy.dokploy;
|
|
787
834
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
835
|
+
// ==================================================================
|
|
836
|
+
// PHASE 1: Deploy backend apps (with encrypted secrets)
|
|
837
|
+
// ==================================================================
|
|
838
|
+
if (backendApps.length > 0) {
|
|
839
|
+
logger.log('\n📦 PHASE 1: Deploying backend applications...');
|
|
791
840
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
);
|
|
841
|
+
for (const appName of backendApps) {
|
|
842
|
+
const app = workspace.apps[appName]!;
|
|
795
843
|
|
|
796
|
-
|
|
797
|
-
// Find or create application in Dokploy
|
|
798
|
-
const dokployAppName = `${workspace.name}-${appName}`;
|
|
799
|
-
let application: DokployApplication | undefined;
|
|
844
|
+
logger.log(`\n ⚙️ Deploying ${appName}...`);
|
|
800
845
|
|
|
801
846
|
try {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
message
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
847
|
+
const dokployAppName = `${workspace.name}-${appName}`;
|
|
848
|
+
let application: DokployApplication | undefined;
|
|
849
|
+
|
|
850
|
+
// Create or find application
|
|
851
|
+
try {
|
|
852
|
+
application = await api.createApplication(
|
|
853
|
+
dokployAppName,
|
|
854
|
+
project.projectId,
|
|
855
|
+
environmentId,
|
|
856
|
+
);
|
|
857
|
+
logger.log(` Created application: ${application.applicationId}`);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
const message =
|
|
860
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
861
|
+
if (
|
|
862
|
+
message.includes('already exists') ||
|
|
863
|
+
message.includes('duplicate')
|
|
864
|
+
) {
|
|
865
|
+
logger.log(` Application already exists`);
|
|
866
|
+
} else {
|
|
867
|
+
throw error;
|
|
868
|
+
}
|
|
822
869
|
}
|
|
823
|
-
}
|
|
824
870
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
// For workspace, we need to build from the app directory
|
|
829
|
-
const originalCwd = process.cwd();
|
|
830
|
-
const fullAppPath = `${workspace.root}/${appPath}`;
|
|
871
|
+
// Get encrypted secrets for this app
|
|
872
|
+
const appSecrets = encryptedSecrets.get(appName);
|
|
873
|
+
const buildArgs: string[] = [];
|
|
831
874
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
});
|
|
839
|
-
} finally {
|
|
840
|
-
process.chdir(originalCwd);
|
|
875
|
+
if (appSecrets && appSecrets.secretCount > 0) {
|
|
876
|
+
buildArgs.push(
|
|
877
|
+
`GKM_ENCRYPTED_CREDENTIALS=${appSecrets.payload.encrypted}`,
|
|
878
|
+
);
|
|
879
|
+
buildArgs.push(`GKM_CREDENTIALS_IV=${appSecrets.payload.iv}`);
|
|
880
|
+
logger.log(` Encrypted ${appSecrets.secretCount} secrets`);
|
|
841
881
|
}
|
|
842
|
-
}
|
|
843
882
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
883
|
+
// Build Docker image with encrypted secrets
|
|
884
|
+
const imageName = `${workspace.name}-${appName}`;
|
|
885
|
+
const imageRef = registry
|
|
886
|
+
? `${registry}/${imageName}:${imageTag}`
|
|
887
|
+
: `${imageName}:${imageTag}`;
|
|
849
888
|
|
|
850
|
-
|
|
889
|
+
logger.log(` Building Docker image: ${imageRef}`);
|
|
851
890
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
891
|
+
await deployDocker({
|
|
892
|
+
stage,
|
|
893
|
+
tag: imageTag,
|
|
894
|
+
skipPush: false,
|
|
895
|
+
config: {
|
|
896
|
+
registry,
|
|
897
|
+
imageName,
|
|
898
|
+
appName,
|
|
899
|
+
},
|
|
900
|
+
buildArgs,
|
|
901
|
+
});
|
|
861
902
|
|
|
862
|
-
|
|
863
|
-
|
|
903
|
+
// Prepare environment variables - ONLY inject GKM_MASTER_KEY
|
|
904
|
+
const envVars: string[] = [`NODE_ENV=production`, `PORT=${app.port}`];
|
|
864
905
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if (depUrl) {
|
|
869
|
-
envVars.push(`${dep.toUpperCase()}_URL=${depUrl}`);
|
|
906
|
+
// Add master key for runtime decryption (NOT plain secrets)
|
|
907
|
+
if (appSecrets && appSecrets.masterKey) {
|
|
908
|
+
envVars.push(`GKM_MASTER_KEY=${appSecrets.masterKey}`);
|
|
870
909
|
}
|
|
871
|
-
}
|
|
872
910
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
`REDIS_URL=\${REDIS_URL:-redis://${workspace.name}-cache:6379}`,
|
|
911
|
+
// Configure and deploy application in Dokploy
|
|
912
|
+
if (application) {
|
|
913
|
+
await api.saveDockerProvider(application.applicationId, imageRef, {
|
|
914
|
+
registryId,
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
await api.saveApplicationEnv(
|
|
918
|
+
application.applicationId,
|
|
919
|
+
envVars.join('\n'),
|
|
883
920
|
);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
921
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
922
|
+
logger.log(` Deploying to Dokploy...`);
|
|
923
|
+
await api.deployApplication(application.applicationId);
|
|
924
|
+
|
|
925
|
+
// Create domain for this app
|
|
926
|
+
try {
|
|
927
|
+
const host = resolveHost(
|
|
928
|
+
appName,
|
|
929
|
+
app,
|
|
930
|
+
stage,
|
|
931
|
+
dokployConfig,
|
|
932
|
+
false, // Backend apps are not main frontend
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
await api.createDomain({
|
|
936
|
+
host,
|
|
937
|
+
port: app.port,
|
|
938
|
+
https: true,
|
|
939
|
+
certificateType: 'letsencrypt',
|
|
940
|
+
applicationId: application.applicationId,
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
const publicUrl = `https://${host}`;
|
|
944
|
+
publicUrls[appName] = publicUrl;
|
|
945
|
+
logger.log(` ✓ Domain: ${publicUrl}`);
|
|
946
|
+
} catch (domainError) {
|
|
947
|
+
// Domain might already exist, try to get public URL anyway
|
|
948
|
+
const host = resolveHost(appName, app, stage, dokployConfig, false);
|
|
949
|
+
publicUrls[appName] = `https://${host}`;
|
|
950
|
+
logger.log(` ℹ Domain already configured: https://${host}`);
|
|
951
|
+
}
|
|
893
952
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
953
|
+
results.push({
|
|
954
|
+
appName,
|
|
955
|
+
type: app.type,
|
|
956
|
+
success: true,
|
|
957
|
+
applicationId: application.applicationId,
|
|
958
|
+
imageRef,
|
|
959
|
+
});
|
|
899
960
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
961
|
+
logger.log(` ✓ ${appName} deployed successfully`);
|
|
962
|
+
} else {
|
|
963
|
+
// Application already exists
|
|
964
|
+
const host = resolveHost(appName, app, stage, dokployConfig, false);
|
|
965
|
+
publicUrls[appName] = `https://${host}`;
|
|
966
|
+
|
|
967
|
+
results.push({
|
|
968
|
+
appName,
|
|
969
|
+
type: app.type,
|
|
970
|
+
success: true,
|
|
971
|
+
imageRef,
|
|
972
|
+
});
|
|
903
973
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
974
|
+
logger.log(` ✓ ${appName} image pushed (app already exists)`);
|
|
975
|
+
}
|
|
976
|
+
} catch (error) {
|
|
977
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
978
|
+
logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
|
|
908
979
|
|
|
909
980
|
results.push({
|
|
910
981
|
appName,
|
|
911
982
|
type: app.type,
|
|
912
|
-
success:
|
|
913
|
-
|
|
914
|
-
imageRef,
|
|
983
|
+
success: false,
|
|
984
|
+
error: message,
|
|
915
985
|
});
|
|
916
986
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
987
|
+
// Abort on backend failure to prevent incomplete deployment
|
|
988
|
+
throw new Error(
|
|
989
|
+
`Backend deployment failed for ${appName}. Aborting to prevent partial deployment.`,
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// ==================================================================
|
|
996
|
+
// PHASE 2: Deploy frontend apps (with public URLs from backends)
|
|
997
|
+
// ==================================================================
|
|
998
|
+
if (frontendApps.length > 0) {
|
|
999
|
+
logger.log('\n🌐 PHASE 2: Deploying frontend applications...');
|
|
1000
|
+
|
|
1001
|
+
for (const appName of frontendApps) {
|
|
1002
|
+
const app = workspace.apps[appName]!;
|
|
1003
|
+
|
|
1004
|
+
logger.log(`\n 🌐 Deploying ${appName}...`);
|
|
1005
|
+
|
|
1006
|
+
try {
|
|
1007
|
+
const dokployAppName = `${workspace.name}-${appName}`;
|
|
1008
|
+
let application: DokployApplication | undefined;
|
|
1009
|
+
|
|
1010
|
+
// Create or find application
|
|
1011
|
+
try {
|
|
1012
|
+
application = await api.createApplication(
|
|
1013
|
+
dokployAppName,
|
|
1014
|
+
project.projectId,
|
|
1015
|
+
environmentId,
|
|
1016
|
+
);
|
|
1017
|
+
logger.log(` Created application: ${application.applicationId}`);
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
const message =
|
|
1020
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
1021
|
+
if (
|
|
1022
|
+
message.includes('already exists') ||
|
|
1023
|
+
message.includes('duplicate')
|
|
1024
|
+
) {
|
|
1025
|
+
logger.log(` Application already exists`);
|
|
1026
|
+
} else {
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Generate public URL build args from dependencies
|
|
1032
|
+
const buildArgs = generatePublicUrlBuildArgs(app, publicUrls);
|
|
1033
|
+
if (buildArgs.length > 0) {
|
|
1034
|
+
logger.log(` Public URLs: ${buildArgs.join(', ')}`);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Build Docker image with public URLs
|
|
1038
|
+
const imageName = `${workspace.name}-${appName}`;
|
|
1039
|
+
const imageRef = registry
|
|
1040
|
+
? `${registry}/${imageName}:${imageTag}`
|
|
1041
|
+
: `${imageName}:${imageTag}`;
|
|
1042
|
+
|
|
1043
|
+
logger.log(` Building Docker image: ${imageRef}`);
|
|
1044
|
+
|
|
1045
|
+
await deployDocker({
|
|
1046
|
+
stage,
|
|
1047
|
+
tag: imageTag,
|
|
1048
|
+
skipPush: false,
|
|
1049
|
+
config: {
|
|
1050
|
+
registry,
|
|
1051
|
+
imageName,
|
|
1052
|
+
appName,
|
|
1053
|
+
},
|
|
1054
|
+
buildArgs,
|
|
1055
|
+
// Pass public URL arg names for Dockerfile generation
|
|
1056
|
+
publicUrlArgs: getPublicUrlArgNames(app),
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// Prepare environment variables - no secrets needed
|
|
1060
|
+
const envVars: string[] = [`NODE_ENV=production`, `PORT=${app.port}`];
|
|
1061
|
+
|
|
1062
|
+
// Configure and deploy application in Dokploy
|
|
1063
|
+
if (application) {
|
|
1064
|
+
await api.saveDockerProvider(application.applicationId, imageRef, {
|
|
1065
|
+
registryId,
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
await api.saveApplicationEnv(
|
|
1069
|
+
application.applicationId,
|
|
1070
|
+
envVars.join('\n'),
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
logger.log(` Deploying to Dokploy...`);
|
|
1074
|
+
await api.deployApplication(application.applicationId);
|
|
1075
|
+
|
|
1076
|
+
// Create domain for this app
|
|
1077
|
+
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1078
|
+
try {
|
|
1079
|
+
const host = resolveHost(
|
|
1080
|
+
appName,
|
|
1081
|
+
app,
|
|
1082
|
+
stage,
|
|
1083
|
+
dokployConfig,
|
|
1084
|
+
isMainFrontend,
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
await api.createDomain({
|
|
1088
|
+
host,
|
|
1089
|
+
port: app.port,
|
|
1090
|
+
https: true,
|
|
1091
|
+
certificateType: 'letsencrypt',
|
|
1092
|
+
applicationId: application.applicationId,
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
const publicUrl = `https://${host}`;
|
|
1096
|
+
publicUrls[appName] = publicUrl;
|
|
1097
|
+
logger.log(` ✓ Domain: ${publicUrl}`);
|
|
1098
|
+
} catch (domainError) {
|
|
1099
|
+
const host = resolveHost(
|
|
1100
|
+
appName,
|
|
1101
|
+
app,
|
|
1102
|
+
stage,
|
|
1103
|
+
dokployConfig,
|
|
1104
|
+
isMainFrontend,
|
|
1105
|
+
);
|
|
1106
|
+
publicUrls[appName] = `https://${host}`;
|
|
1107
|
+
logger.log(` ℹ Domain already configured: https://${host}`);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
results.push({
|
|
1111
|
+
appName,
|
|
1112
|
+
type: app.type,
|
|
1113
|
+
success: true,
|
|
1114
|
+
applicationId: application.applicationId,
|
|
1115
|
+
imageRef,
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
logger.log(` ✓ ${appName} deployed successfully`);
|
|
1119
|
+
} else {
|
|
1120
|
+
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1121
|
+
const host = resolveHost(
|
|
1122
|
+
appName,
|
|
1123
|
+
app,
|
|
1124
|
+
stage,
|
|
1125
|
+
dokployConfig,
|
|
1126
|
+
isMainFrontend,
|
|
1127
|
+
);
|
|
1128
|
+
publicUrls[appName] = `https://${host}`;
|
|
1129
|
+
|
|
1130
|
+
results.push({
|
|
1131
|
+
appName,
|
|
1132
|
+
type: app.type,
|
|
1133
|
+
success: true,
|
|
1134
|
+
imageRef,
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
logger.log(` ✓ ${appName} image pushed (app already exists)`);
|
|
1138
|
+
}
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
1141
|
+
logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
|
|
922
1142
|
|
|
923
1143
|
results.push({
|
|
924
1144
|
appName,
|
|
925
1145
|
type: app.type,
|
|
926
|
-
success:
|
|
927
|
-
|
|
1146
|
+
success: false,
|
|
1147
|
+
error: message,
|
|
928
1148
|
});
|
|
929
|
-
|
|
930
|
-
logger.log(` ✓ ${appName} image pushed (app already exists)`);
|
|
1149
|
+
// Don't abort on frontend failures - continue with other frontends
|
|
931
1150
|
}
|
|
932
|
-
} catch (error) {
|
|
933
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
934
|
-
logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
|
|
935
|
-
|
|
936
|
-
results.push({
|
|
937
|
-
appName,
|
|
938
|
-
type: app.type,
|
|
939
|
-
success: false,
|
|
940
|
-
error: message,
|
|
941
|
-
});
|
|
942
1151
|
}
|
|
943
1152
|
}
|
|
944
1153
|
|
|
1154
|
+
// ==================================================================
|
|
945
1155
|
// Summary
|
|
1156
|
+
// ==================================================================
|
|
946
1157
|
const successCount = results.filter((r) => r.success).length;
|
|
947
1158
|
const failedCount = results.filter((r) => !r.success).length;
|
|
948
1159
|
|
|
@@ -954,6 +1165,14 @@ export async function workspaceDeployCommand(
|
|
|
954
1165
|
logger.log(` Failed: ${failedCount}`);
|
|
955
1166
|
}
|
|
956
1167
|
|
|
1168
|
+
// Print deployed URLs
|
|
1169
|
+
if (Object.keys(publicUrls).length > 0) {
|
|
1170
|
+
logger.log('\n 📡 Deployed URLs:');
|
|
1171
|
+
for (const [name, url] of Object.entries(publicUrls)) {
|
|
1172
|
+
logger.log(` ${name}: ${url}`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
957
1176
|
return {
|
|
958
1177
|
apps: results,
|
|
959
1178
|
projectId: project.projectId,
|