@geekmidas/cli 0.39.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 +674 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +653 -101
- 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/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 +40 -11
- package/src/deploy/dokploy-api.ts +99 -0
- package/src/deploy/domain.ts +125 -0
- package/src/deploy/index.ts +366 -148
- package/src/deploy/secrets.ts +182 -0
- package/src/deploy/sniffer.ts +180 -0
- package/src/dev/index.ts +11 -0
- package/src/docker/index.ts +17 -2
- package/src/docker/templates.ts +171 -1
- 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,172 +817,343 @@ export async function workspaceDeployCommand(
|
|
|
778
817
|
);
|
|
779
818
|
}
|
|
780
819
|
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
production: true,
|
|
791
|
-
stage,
|
|
792
|
-
});
|
|
793
|
-
logger.log(' ✓ Workspace build complete');
|
|
794
|
-
} catch (error) {
|
|
795
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
796
|
-
logger.log(` ✗ Workspace build failed: ${message}`);
|
|
797
|
-
throw error;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
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
|
+
);
|
|
800
829
|
|
|
801
|
-
//
|
|
802
|
-
|
|
830
|
+
// Track deployed app public URLs for frontend builds
|
|
831
|
+
const publicUrls: Record<string, string> = {};
|
|
803
832
|
const results: AppDeployResult[] = [];
|
|
833
|
+
const dokployConfig = workspace.deploy.dokploy;
|
|
804
834
|
|
|
805
|
-
|
|
806
|
-
|
|
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...');
|
|
807
840
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
);
|
|
841
|
+
for (const appName of backendApps) {
|
|
842
|
+
const app = workspace.apps[appName]!;
|
|
811
843
|
|
|
812
|
-
|
|
813
|
-
// Find or create application in Dokploy
|
|
814
|
-
const dokployAppName = `${workspace.name}-${appName}`;
|
|
815
|
-
let application: DokployApplication | undefined;
|
|
844
|
+
logger.log(`\n ⚙️ Deploying ${appName}...`);
|
|
816
845
|
|
|
817
846
|
try {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
message
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
+
}
|
|
838
869
|
}
|
|
839
|
-
}
|
|
840
870
|
|
|
841
|
-
|
|
842
|
-
|
|
871
|
+
// Get encrypted secrets for this app
|
|
872
|
+
const appSecrets = encryptedSecrets.get(appName);
|
|
873
|
+
const buildArgs: string[] = [];
|
|
843
874
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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`);
|
|
881
|
+
}
|
|
849
882
|
|
|
850
|
-
|
|
883
|
+
// Build Docker image with encrypted secrets
|
|
884
|
+
const imageName = `${workspace.name}-${appName}`;
|
|
885
|
+
const imageRef = registry
|
|
886
|
+
? `${registry}/${imageName}:${imageTag}`
|
|
887
|
+
: `${imageName}:${imageTag}`;
|
|
851
888
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
889
|
+
logger.log(` Building Docker image: ${imageRef}`);
|
|
890
|
+
|
|
891
|
+
await deployDocker({
|
|
892
|
+
stage,
|
|
893
|
+
tag: imageTag,
|
|
894
|
+
skipPush: false,
|
|
895
|
+
config: {
|
|
896
|
+
registry,
|
|
897
|
+
imageName,
|
|
898
|
+
appName,
|
|
899
|
+
},
|
|
900
|
+
buildArgs,
|
|
901
|
+
});
|
|
862
902
|
|
|
863
|
-
|
|
864
|
-
|
|
903
|
+
// Prepare environment variables - ONLY inject GKM_MASTER_KEY
|
|
904
|
+
const envVars: string[] = [`NODE_ENV=production`, `PORT=${app.port}`];
|
|
865
905
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
if (depUrl) {
|
|
870
|
-
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}`);
|
|
871
909
|
}
|
|
872
|
-
}
|
|
873
910
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
`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'),
|
|
884
920
|
);
|
|
921
|
+
|
|
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
|
+
}
|
|
952
|
+
|
|
953
|
+
results.push({
|
|
954
|
+
appName,
|
|
955
|
+
type: app.type,
|
|
956
|
+
success: true,
|
|
957
|
+
applicationId: application.applicationId,
|
|
958
|
+
imageRef,
|
|
959
|
+
});
|
|
960
|
+
|
|
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
|
+
});
|
|
973
|
+
|
|
974
|
+
logger.log(` ✓ ${appName} image pushed (app already exists)`);
|
|
885
975
|
}
|
|
886
|
-
}
|
|
976
|
+
} catch (error) {
|
|
977
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
978
|
+
logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
|
|
887
979
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
980
|
+
results.push({
|
|
981
|
+
appName,
|
|
982
|
+
type: app.type,
|
|
983
|
+
success: false,
|
|
984
|
+
error: message,
|
|
893
985
|
});
|
|
894
986
|
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
envVars.join('\n'),
|
|
987
|
+
// Abort on backend failure to prevent incomplete deployment
|
|
988
|
+
throw new Error(
|
|
989
|
+
`Backend deployment failed for ${appName}. Aborting to prevent partial deployment.`,
|
|
899
990
|
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
900
994
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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...');
|
|
904
1000
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const appUrl = `http://${dokployAppName}:${app.port}`;
|
|
908
|
-
deployedAppUrls[appName] = appUrl;
|
|
1001
|
+
for (const appName of frontendApps) {
|
|
1002
|
+
const app = workspace.apps[appName]!;
|
|
909
1003
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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),
|
|
916
1057
|
});
|
|
917
1058
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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}`);
|
|
923
1142
|
|
|
924
1143
|
results.push({
|
|
925
1144
|
appName,
|
|
926
1145
|
type: app.type,
|
|
927
|
-
success:
|
|
928
|
-
|
|
1146
|
+
success: false,
|
|
1147
|
+
error: message,
|
|
929
1148
|
});
|
|
930
|
-
|
|
931
|
-
logger.log(` ✓ ${appName} image pushed (app already exists)`);
|
|
1149
|
+
// Don't abort on frontend failures - continue with other frontends
|
|
932
1150
|
}
|
|
933
|
-
} catch (error) {
|
|
934
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
935
|
-
logger.log(` ✗ Failed to deploy ${appName}: ${message}`);
|
|
936
|
-
|
|
937
|
-
results.push({
|
|
938
|
-
appName,
|
|
939
|
-
type: app.type,
|
|
940
|
-
success: false,
|
|
941
|
-
error: message,
|
|
942
|
-
});
|
|
943
1151
|
}
|
|
944
1152
|
}
|
|
945
1153
|
|
|
1154
|
+
// ==================================================================
|
|
946
1155
|
// Summary
|
|
1156
|
+
// ==================================================================
|
|
947
1157
|
const successCount = results.filter((r) => r.success).length;
|
|
948
1158
|
const failedCount = results.filter((r) => !r.success).length;
|
|
949
1159
|
|
|
@@ -955,6 +1165,14 @@ export async function workspaceDeployCommand(
|
|
|
955
1165
|
logger.log(` Failed: ${failedCount}`);
|
|
956
1166
|
}
|
|
957
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
|
+
|
|
958
1176
|
return {
|
|
959
1177
|
apps: results,
|
|
960
1178
|
projectId: project.projectId,
|