@geekmidas/cli 1.4.0 → 1.5.1
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/CHANGELOG.md +12 -0
- package/dist/{HostingerProvider-B9N-TKbp.mjs → HostingerProvider-402UdK89.mjs} +34 -1
- package/dist/HostingerProvider-402UdK89.mjs.map +1 -0
- package/dist/{HostingerProvider-DUV9-Tzg.cjs → HostingerProvider-BiXdHjiq.cjs} +34 -1
- package/dist/HostingerProvider-BiXdHjiq.cjs.map +1 -0
- package/dist/{Route53Provider-DOWmFnwN.mjs → Route53Provider-DbBo7Uz5.mjs} +55 -2
- package/dist/Route53Provider-DbBo7Uz5.mjs.map +1 -0
- package/dist/{Route53Provider-xrWuBXih.cjs → Route53Provider-kfJ77LmL.cjs} +55 -2
- package/dist/Route53Provider-kfJ77LmL.cjs.map +1 -0
- package/dist/backup-provisioner-B5e-F6zX.cjs +164 -0
- package/dist/backup-provisioner-B5e-F6zX.cjs.map +1 -0
- package/dist/backup-provisioner-BIArpmTr.mjs +163 -0
- package/dist/backup-provisioner-BIArpmTr.mjs.map +1 -0
- package/dist/{config-C1dM7aZb.cjs → config-BYn5yUt5.cjs} +2 -2
- package/dist/{config-C1dM7aZb.cjs.map → config-BYn5yUt5.cjs.map} +1 -1
- package/dist/{config-C1bidhvG.mjs → config-dLNQIvDR.mjs} +2 -2
- package/dist/{config-C1bidhvG.mjs.map → config-dLNQIvDR.mjs.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-z0833e7r.mjs → dokploy-api-2ldYoN3i.mjs} +131 -1
- package/dist/dokploy-api-2ldYoN3i.mjs.map +1 -0
- package/dist/dokploy-api-C93pveuy.mjs +3 -0
- package/dist/dokploy-api-CbDh4o93.cjs +3 -0
- package/dist/{dokploy-api-CQvhV6Hd.cjs → dokploy-api-DLgvEQlr.cjs} +131 -1
- package/dist/dokploy-api-DLgvEQlr.cjs.map +1 -0
- package/dist/{index-DzmZ6SUW.d.cts → index-Ba21_lNt.d.cts} +157 -29
- package/dist/index-Ba21_lNt.d.cts.map +1 -0
- package/dist/{index-DvpWzLD7.d.mts → index-Bj5VNxEL.d.mts} +158 -30
- package/dist/index-Bj5VNxEL.d.mts.map +1 -0
- package/dist/index.cjs +219 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +219 -68
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-9k6a6VA4.mjs → openapi-CMTyaIJJ.mjs} +2 -2
- package/dist/{openapi-9k6a6VA4.mjs.map → openapi-CMTyaIJJ.mjs.map} +1 -1
- package/dist/{openapi-Dcja4e1C.cjs → openapi-CqblwJZ4.cjs} +2 -2
- package/dist/{openapi-Dcja4e1C.cjs.map → openapi-CqblwJZ4.cjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{types-B9UZ7fOG.d.mts → types-CZg5iUgD.d.mts} +1 -1
- package/dist/{types-B9UZ7fOG.d.mts.map → types-CZg5iUgD.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CeFgIDC-.cjs → workspace-DIMnYaYt.cjs} +20 -2
- package/dist/{workspace-CeFgIDC-.cjs.map → workspace-DIMnYaYt.cjs.map} +1 -1
- package/dist/{workspace-Cb_I7oCJ.mjs → workspace-Dy8k7Wru.mjs} +20 -2
- package/dist/{workspace-Cb_I7oCJ.mjs.map → workspace-Dy8k7Wru.mjs.map} +1 -1
- package/examples/cron-example.ts +6 -6
- package/examples/function-example.ts +1 -1
- package/package.json +7 -5
- package/src/deploy/__tests__/Route53Provider.spec.ts +23 -0
- package/src/deploy/__tests__/backup-provisioner.spec.ts +428 -0
- package/src/deploy/__tests__/createDnsProvider.spec.ts +23 -0
- package/src/deploy/__tests__/env-resolver.spec.ts +239 -0
- package/src/deploy/__tests__/sniffer.spec.ts +104 -93
- package/src/deploy/__tests__/undeploy.spec.ts +758 -0
- package/src/deploy/backup-provisioner.ts +316 -0
- package/src/deploy/dns/DnsProvider.ts +39 -1
- package/src/deploy/dns/HostingerProvider.ts +74 -0
- package/src/deploy/dns/Route53Provider.ts +85 -1
- package/src/deploy/dns/index.ts +25 -0
- package/src/deploy/dokploy-api.ts +237 -0
- package/src/deploy/env-resolver.ts +11 -1
- package/src/deploy/index.ts +143 -37
- package/src/deploy/sniffer.ts +39 -7
- package/src/deploy/state.ts +171 -0
- package/src/deploy/undeploy.ts +407 -0
- package/src/generators/FunctionGenerator.ts +1 -1
- package/src/init/generators/monorepo.ts +4 -0
- package/src/init/generators/web.ts +45 -2
- package/src/init/versions.ts +2 -2
- package/src/workspace/schema.ts +34 -0
- package/src/workspace/types.ts +37 -37
- package/dist/HostingerProvider-B9N-TKbp.mjs.map +0 -1
- package/dist/HostingerProvider-DUV9-Tzg.cjs.map +0 -1
- package/dist/Route53Provider-DOWmFnwN.mjs.map +0 -1
- package/dist/Route53Provider-xrWuBXih.cjs.map +0 -1
- package/dist/dokploy-api-CQvhV6Hd.cjs.map +0 -1
- package/dist/dokploy-api-CWc02yyg.cjs +0 -3
- package/dist/dokploy-api-DSJYNx88.mjs +0 -3
- package/dist/dokploy-api-z0833e7r.mjs.map +0 -1
- package/dist/index-DvpWzLD7.d.mts.map +0 -1
- package/dist/index-DzmZ6SUW.d.cts.map +0 -1
|
@@ -154,6 +154,13 @@ export class DokployApi {
|
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Delete a project and all its resources
|
|
159
|
+
*/
|
|
160
|
+
async deleteProject(projectId: string): Promise<void> {
|
|
161
|
+
await this.post('project.remove', { projectId });
|
|
162
|
+
}
|
|
163
|
+
|
|
157
164
|
// ============================================
|
|
158
165
|
// Environment endpoints
|
|
159
166
|
// ============================================
|
|
@@ -315,6 +322,13 @@ export class DokployApi {
|
|
|
315
322
|
await this.post('application.deploy', { applicationId });
|
|
316
323
|
}
|
|
317
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Delete an application
|
|
327
|
+
*/
|
|
328
|
+
async deleteApplication(applicationId: string): Promise<void> {
|
|
329
|
+
await this.post('application.remove', { applicationId });
|
|
330
|
+
}
|
|
331
|
+
|
|
318
332
|
// ============================================
|
|
319
333
|
// Registry endpoints
|
|
320
334
|
// ============================================
|
|
@@ -505,6 +519,13 @@ export class DokployApi {
|
|
|
505
519
|
await this.post('postgres.update', { postgresId, ...updates });
|
|
506
520
|
}
|
|
507
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Delete a Postgres database
|
|
524
|
+
*/
|
|
525
|
+
async deletePostgres(postgresId: string): Promise<void> {
|
|
526
|
+
await this.post('postgres.remove', { postgresId });
|
|
527
|
+
}
|
|
528
|
+
|
|
508
529
|
// ============================================
|
|
509
530
|
// Redis endpoints
|
|
510
531
|
// ============================================
|
|
@@ -626,6 +647,13 @@ export class DokployApi {
|
|
|
626
647
|
await this.post('redis.update', { redisId, ...updates });
|
|
627
648
|
}
|
|
628
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Delete a Redis instance
|
|
652
|
+
*/
|
|
653
|
+
async deleteRedis(redisId: string): Promise<void> {
|
|
654
|
+
await this.post('redis.remove', { redisId });
|
|
655
|
+
}
|
|
656
|
+
|
|
629
657
|
// ============================================
|
|
630
658
|
// Domain endpoints
|
|
631
659
|
// ============================================
|
|
@@ -704,6 +732,144 @@ export class DokployApi {
|
|
|
704
732
|
serverId,
|
|
705
733
|
});
|
|
706
734
|
}
|
|
735
|
+
|
|
736
|
+
// ============================================
|
|
737
|
+
// Destination endpoints (backup storage)
|
|
738
|
+
// ============================================
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* List all backup destinations
|
|
742
|
+
*/
|
|
743
|
+
async listDestinations(): Promise<DokployDestination[]> {
|
|
744
|
+
return this.get<DokployDestination[]>('destination.all');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Get a destination by ID
|
|
749
|
+
*/
|
|
750
|
+
async getDestination(destinationId: string): Promise<DokployDestination> {
|
|
751
|
+
return this.get<DokployDestination>(
|
|
752
|
+
`destination.one?destinationId=${destinationId}`,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Create a new S3 backup destination
|
|
758
|
+
*/
|
|
759
|
+
async createDestination(
|
|
760
|
+
options: DokployDestinationCreate,
|
|
761
|
+
): Promise<DokployDestination> {
|
|
762
|
+
return this.post<DokployDestination>('destination.create', { ...options });
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Find a destination by name
|
|
767
|
+
*/
|
|
768
|
+
async findDestinationByName(
|
|
769
|
+
name: string,
|
|
770
|
+
): Promise<DokployDestination | undefined> {
|
|
771
|
+
const destinations = await this.listDestinations();
|
|
772
|
+
return destinations.find((d) => d.name === name);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Find or create a destination by name
|
|
777
|
+
*/
|
|
778
|
+
async findOrCreateDestination(
|
|
779
|
+
name: string,
|
|
780
|
+
options: Omit<DokployDestinationCreate, 'name'>,
|
|
781
|
+
): Promise<{ destination: DokployDestination; created: boolean }> {
|
|
782
|
+
const existing = await this.findDestinationByName(name);
|
|
783
|
+
if (existing) {
|
|
784
|
+
return { destination: existing, created: false };
|
|
785
|
+
}
|
|
786
|
+
const destination = await this.createDestination({ name, ...options });
|
|
787
|
+
return { destination, created: true };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Update a destination
|
|
792
|
+
*/
|
|
793
|
+
async updateDestination(
|
|
794
|
+
destinationId: string,
|
|
795
|
+
updates: Partial<DokployDestinationCreate>,
|
|
796
|
+
): Promise<void> {
|
|
797
|
+
await this.post('destination.update', { destinationId, ...updates });
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Delete a destination
|
|
802
|
+
*/
|
|
803
|
+
async deleteDestination(destinationId: string): Promise<void> {
|
|
804
|
+
await this.post('destination.remove', { destinationId });
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Test connection to a destination
|
|
809
|
+
*/
|
|
810
|
+
async testDestinationConnection(
|
|
811
|
+
destinationId: string,
|
|
812
|
+
): Promise<{ success: boolean }> {
|
|
813
|
+
return this.post<{ success: boolean }>('destination.testConnection', {
|
|
814
|
+
destinationId,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ============================================
|
|
819
|
+
// Backup endpoints (scheduled backups)
|
|
820
|
+
// ============================================
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Create a backup schedule for postgres
|
|
824
|
+
*/
|
|
825
|
+
async createPostgresBackup(
|
|
826
|
+
options: DokployBackupCreate,
|
|
827
|
+
): Promise<DokployBackup> {
|
|
828
|
+
return this.post<DokployBackup>('backup.create', {
|
|
829
|
+
...options,
|
|
830
|
+
databaseType: 'postgres',
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* List backups for a postgres database
|
|
836
|
+
*/
|
|
837
|
+
async listPostgresBackups(postgresId: string): Promise<DokployBackup[]> {
|
|
838
|
+
return this.get<DokployBackup[]>(
|
|
839
|
+
`backup.all?postgresId=${postgresId}&databaseType=postgres`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Get a backup by ID
|
|
845
|
+
*/
|
|
846
|
+
async getBackup(backupId: string): Promise<DokployBackup> {
|
|
847
|
+
return this.get<DokployBackup>(`backup.one?backupId=${backupId}`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Update a backup schedule
|
|
852
|
+
*/
|
|
853
|
+
async updateBackup(
|
|
854
|
+
backupId: string,
|
|
855
|
+
updates: Partial<DokployBackupUpdate>,
|
|
856
|
+
): Promise<void> {
|
|
857
|
+
await this.post('backup.update', { backupId, ...updates });
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Delete a backup schedule
|
|
862
|
+
*/
|
|
863
|
+
async deleteBackup(backupId: string): Promise<void> {
|
|
864
|
+
await this.post('backup.remove', { backupId });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Trigger a manual backup run
|
|
869
|
+
*/
|
|
870
|
+
async runBackupManually(backupId: string): Promise<void> {
|
|
871
|
+
await this.post('backup.manualBackup', { backupId });
|
|
872
|
+
}
|
|
707
873
|
}
|
|
708
874
|
|
|
709
875
|
// ============================================
|
|
@@ -835,6 +1001,77 @@ export interface DokployDomain extends DokployDomainCreate {
|
|
|
835
1001
|
createdAt?: string;
|
|
836
1002
|
}
|
|
837
1003
|
|
|
1004
|
+
// ============================================
|
|
1005
|
+
// Destination types (backup storage)
|
|
1006
|
+
// ============================================
|
|
1007
|
+
|
|
1008
|
+
export interface DokployDestination {
|
|
1009
|
+
destinationId: string;
|
|
1010
|
+
name: string;
|
|
1011
|
+
accessKey: string;
|
|
1012
|
+
bucket: string;
|
|
1013
|
+
region: string;
|
|
1014
|
+
endpoint: string | null;
|
|
1015
|
+
createdAt?: string;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export interface DokployDestinationCreate {
|
|
1019
|
+
/** User-friendly name for the destination */
|
|
1020
|
+
name: string;
|
|
1021
|
+
/** S3 access key ID */
|
|
1022
|
+
accessKey: string;
|
|
1023
|
+
/** S3 secret access key */
|
|
1024
|
+
secretAccessKey: string;
|
|
1025
|
+
/** S3 bucket name */
|
|
1026
|
+
bucket: string;
|
|
1027
|
+
/** AWS region (e.g., 'us-east-1') */
|
|
1028
|
+
region: string;
|
|
1029
|
+
/** Optional endpoint for S3-compatible services */
|
|
1030
|
+
endpoint?: string;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ============================================
|
|
1034
|
+
// Backup types (scheduled backups)
|
|
1035
|
+
// ============================================
|
|
1036
|
+
|
|
1037
|
+
export type DokployDatabaseType = 'postgres' | 'mysql' | 'mariadb' | 'mongo';
|
|
1038
|
+
|
|
1039
|
+
export interface DokployBackup {
|
|
1040
|
+
backupId: string;
|
|
1041
|
+
schedule: string;
|
|
1042
|
+
prefix: string;
|
|
1043
|
+
enabled: boolean;
|
|
1044
|
+
destinationId: string;
|
|
1045
|
+
postgresId?: string;
|
|
1046
|
+
databaseType: DokployDatabaseType;
|
|
1047
|
+
keepLatestCount: number | null;
|
|
1048
|
+
createdAt?: string;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export interface DokployBackupCreate {
|
|
1052
|
+
/** Cron schedule (e.g., '0 2 * * *' for 2 AM daily) */
|
|
1053
|
+
schedule: string;
|
|
1054
|
+
/** Backup file prefix (e.g., 'production/postgres') */
|
|
1055
|
+
prefix: string;
|
|
1056
|
+
/** Destination ID for backup storage */
|
|
1057
|
+
destinationId: string;
|
|
1058
|
+
/** Database name to backup */
|
|
1059
|
+
database: string;
|
|
1060
|
+
/** Postgres instance ID */
|
|
1061
|
+
postgresId: string;
|
|
1062
|
+
/** Enable/disable backup (default: true) */
|
|
1063
|
+
enabled?: boolean;
|
|
1064
|
+
/** Number of backups to keep (default: keep all) */
|
|
1065
|
+
keepLatestCount?: number;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
export interface DokployBackupUpdate {
|
|
1069
|
+
schedule?: string;
|
|
1070
|
+
prefix?: string;
|
|
1071
|
+
enabled?: boolean;
|
|
1072
|
+
keepLatestCount?: number;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
838
1075
|
/**
|
|
839
1076
|
* Create a Dokploy API client from stored credentials or environment
|
|
840
1077
|
*/
|
|
@@ -208,8 +208,18 @@ export function resolveEnvVar(
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
// Check dependency URLs (e.g., AUTH_URL -> dependencyUrls.auth)
|
|
211
|
+
// Also supports NEXT_PUBLIC_ prefix for frontend apps (NEXT_PUBLIC_AUTH_URL -> dependencyUrls.auth)
|
|
211
212
|
if (context.dependencyUrls && varName.endsWith('_URL')) {
|
|
212
|
-
|
|
213
|
+
let depName: string;
|
|
214
|
+
|
|
215
|
+
if (varName.startsWith('NEXT_PUBLIC_')) {
|
|
216
|
+
// NEXT_PUBLIC_AUTH_URL -> auth
|
|
217
|
+
depName = varName.slice(12, -4).toLowerCase();
|
|
218
|
+
} else {
|
|
219
|
+
// AUTH_URL -> auth
|
|
220
|
+
depName = varName.slice(0, -4).toLowerCase();
|
|
221
|
+
}
|
|
222
|
+
|
|
213
223
|
if (context.dependencyUrls[depName]) {
|
|
214
224
|
return context.dependencyUrls[depName];
|
|
215
225
|
}
|
package/src/deploy/index.ts
CHANGED
|
@@ -73,12 +73,7 @@ import {
|
|
|
73
73
|
type DokployPostgres,
|
|
74
74
|
type DokployRedis,
|
|
75
75
|
} from './dokploy-api';
|
|
76
|
-
import {
|
|
77
|
-
generatePublicUrlBuildArgs,
|
|
78
|
-
getPublicUrlArgNames,
|
|
79
|
-
isMainFrontendApp,
|
|
80
|
-
resolveHost,
|
|
81
|
-
} from './domain.js';
|
|
76
|
+
import { isMainFrontendApp, resolveHost } from './domain.js';
|
|
82
77
|
import {
|
|
83
78
|
type EnvResolverContext,
|
|
84
79
|
formatMissingVarsError,
|
|
@@ -93,10 +88,13 @@ import {
|
|
|
93
88
|
createEmptyState,
|
|
94
89
|
getAllAppCredentials,
|
|
95
90
|
getApplicationId,
|
|
91
|
+
getBackupState,
|
|
96
92
|
getPostgresId,
|
|
97
93
|
getRedisId,
|
|
98
94
|
setAppCredentials,
|
|
99
95
|
setApplicationId,
|
|
96
|
+
setBackupState,
|
|
97
|
+
setPostgresBackupId,
|
|
100
98
|
setPostgresId,
|
|
101
99
|
setRedisId,
|
|
102
100
|
} from './state.js';
|
|
@@ -330,29 +328,39 @@ async function initializePostgresUsers(
|
|
|
330
328
|
` Creating user "${user.name}" with schema "${schemaName}"...`,
|
|
331
329
|
);
|
|
332
330
|
|
|
333
|
-
// Create or update user
|
|
334
|
-
|
|
335
|
-
DO $$ BEGIN
|
|
336
|
-
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
|
|
337
|
-
CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
338
|
-
ELSE
|
|
339
|
-
ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
340
|
-
END IF;
|
|
341
|
-
END $$;
|
|
342
|
-
`);
|
|
343
|
-
|
|
331
|
+
// Create or update user with all settings in one DO block
|
|
332
|
+
// This avoids "tuple already updated by self" errors from multiple ALTER USER calls
|
|
344
333
|
if (user.usePublicSchema) {
|
|
345
334
|
// API uses public schema
|
|
335
|
+
await client.query(`
|
|
336
|
+
DO $$ BEGIN
|
|
337
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
|
|
338
|
+
CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
339
|
+
ELSE
|
|
340
|
+
ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
341
|
+
END IF;
|
|
342
|
+
END $$;
|
|
343
|
+
`);
|
|
346
344
|
await client.query(`
|
|
347
345
|
GRANT ALL ON SCHEMA public TO "${user.name}";
|
|
348
346
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "${user.name}";
|
|
349
347
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "${user.name}";
|
|
350
348
|
`);
|
|
351
349
|
} else {
|
|
352
|
-
// Other apps get their own schema
|
|
350
|
+
// Other apps get their own schema - combine user creation and search_path in one block
|
|
351
|
+
await client.query(`
|
|
352
|
+
DO $$ BEGIN
|
|
353
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${user.name}') THEN
|
|
354
|
+
CREATE USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
355
|
+
ELSE
|
|
356
|
+
ALTER USER "${user.name}" WITH PASSWORD '${user.password}';
|
|
357
|
+
END IF;
|
|
358
|
+
-- Set search_path in same transaction to avoid tuple conflict
|
|
359
|
+
ALTER USER "${user.name}" SET search_path TO "${schemaName}";
|
|
360
|
+
END $$;
|
|
361
|
+
`);
|
|
353
362
|
await client.query(`
|
|
354
363
|
CREATE SCHEMA IF NOT EXISTS "${schemaName}" AUTHORIZATION "${user.name}";
|
|
355
|
-
ALTER USER "${user.name}" SET search_path TO "${schemaName}";
|
|
356
364
|
GRANT USAGE ON SCHEMA "${schemaName}" TO "${user.name}";
|
|
357
365
|
GRANT ALL ON ALL TABLES IN SCHEMA "${schemaName}" TO "${user.name}";
|
|
358
366
|
ALTER DEFAULT PRIVILEGES IN SCHEMA "${schemaName}" GRANT ALL ON TABLES TO "${user.name}";
|
|
@@ -1254,6 +1262,51 @@ export async function workspaceDeployCommand(
|
|
|
1254
1262
|
}
|
|
1255
1263
|
}
|
|
1256
1264
|
|
|
1265
|
+
// ==================================================================
|
|
1266
|
+
// Provision backup destination if configured
|
|
1267
|
+
// ==================================================================
|
|
1268
|
+
if (workspace.deploy?.backups && provisionedPostgres) {
|
|
1269
|
+
logger.log('\n💾 Provisioning backup destination...');
|
|
1270
|
+
|
|
1271
|
+
const { provisionBackupDestination } = await import(
|
|
1272
|
+
'./backup-provisioner.js'
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1275
|
+
const backupState = await provisionBackupDestination({
|
|
1276
|
+
api,
|
|
1277
|
+
projectId: project.projectId,
|
|
1278
|
+
projectName: workspace.name,
|
|
1279
|
+
stage,
|
|
1280
|
+
config: workspace.deploy.backups,
|
|
1281
|
+
existingState: getBackupState(state),
|
|
1282
|
+
logger,
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
// Save backup state
|
|
1286
|
+
setBackupState(state, backupState);
|
|
1287
|
+
|
|
1288
|
+
// Create backup schedule for postgres if not already configured
|
|
1289
|
+
if (!backupState.postgresBackupId) {
|
|
1290
|
+
const backupSchedule = workspace.deploy.backups.schedule ?? '0 2 * * *';
|
|
1291
|
+
const backupRetention = workspace.deploy.backups.retention ?? 30;
|
|
1292
|
+
|
|
1293
|
+
logger.log(' Creating postgres backup schedule...');
|
|
1294
|
+
const backup = await api.createPostgresBackup({
|
|
1295
|
+
schedule: backupSchedule,
|
|
1296
|
+
prefix: `${stage}/postgres`,
|
|
1297
|
+
destinationId: backupState.destinationId,
|
|
1298
|
+
database: provisionedPostgres.databaseName,
|
|
1299
|
+
postgresId: provisionedPostgres.postgresId,
|
|
1300
|
+
enabled: true,
|
|
1301
|
+
keepLatestCount: backupRetention,
|
|
1302
|
+
});
|
|
1303
|
+
setPostgresBackupId(state, backup.backupId);
|
|
1304
|
+
logger.log(` ✓ Postgres backup schedule created (${backupSchedule})`);
|
|
1305
|
+
} else {
|
|
1306
|
+
logger.log(' ✓ Using existing postgres backup schedule');
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1257
1310
|
// Track deployed app public URLs for frontend builds
|
|
1258
1311
|
const publicUrls: Record<string, string> = {};
|
|
1259
1312
|
const results: AppDeployResult[] = [];
|
|
@@ -1576,13 +1629,71 @@ export async function workspaceDeployCommand(
|
|
|
1576
1629
|
// Store application ID in state
|
|
1577
1630
|
setApplicationId(state, appName, application.applicationId);
|
|
1578
1631
|
|
|
1579
|
-
//
|
|
1580
|
-
const
|
|
1632
|
+
// Build dependency URLs for frontend (same pattern as backend)
|
|
1633
|
+
const dependencyUrls: Record<string, string> = {};
|
|
1634
|
+
if (app.dependencies) {
|
|
1635
|
+
for (const dep of app.dependencies) {
|
|
1636
|
+
if (publicUrls[dep]) {
|
|
1637
|
+
dependencyUrls[dep] = publicUrls[dep];
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// Compute hostname for this frontend app
|
|
1643
|
+
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1644
|
+
const frontendHost = resolveHost(
|
|
1645
|
+
appName,
|
|
1646
|
+
app,
|
|
1647
|
+
stage,
|
|
1648
|
+
dokployConfig,
|
|
1649
|
+
isMainFrontend,
|
|
1650
|
+
);
|
|
1651
|
+
|
|
1652
|
+
// Build env context for frontend
|
|
1653
|
+
const envContext: EnvResolverContext = {
|
|
1654
|
+
app,
|
|
1655
|
+
appName,
|
|
1656
|
+
stage,
|
|
1657
|
+
state,
|
|
1658
|
+
appHostname: frontendHost,
|
|
1659
|
+
frontendUrls: [],
|
|
1660
|
+
userSecrets: stageSecrets ?? undefined,
|
|
1661
|
+
dependencyUrls,
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
// Resolve all env vars BEFORE Docker build (NEXT_PUBLIC_* must be present at build time)
|
|
1665
|
+
const sniffedVars = sniffedApps.get(appName)?.requiredEnvVars ?? [];
|
|
1666
|
+
const { valid, missing, resolved } = validateEnvVars(
|
|
1667
|
+
sniffedVars,
|
|
1668
|
+
envContext,
|
|
1669
|
+
);
|
|
1670
|
+
|
|
1671
|
+
if (!valid) {
|
|
1672
|
+
throw new Error(formatMissingVarsError(appName, missing, stage));
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (Object.keys(resolved).length > 0) {
|
|
1676
|
+
logger.log(
|
|
1677
|
+
` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(', ')}`,
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// Build args: all NEXT_PUBLIC_* vars must be present at Next.js build time
|
|
1682
|
+
const buildArgs: string[] = [];
|
|
1683
|
+
const publicUrlArgNames: string[] = [];
|
|
1684
|
+
|
|
1685
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
1686
|
+
if (key.startsWith('NEXT_PUBLIC_')) {
|
|
1687
|
+
buildArgs.push(`${key}=${value}`);
|
|
1688
|
+
publicUrlArgNames.push(key);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1581
1692
|
if (buildArgs.length > 0) {
|
|
1582
|
-
logger.log(`
|
|
1693
|
+
logger.log(` Build args: ${publicUrlArgNames.join(', ')}`);
|
|
1583
1694
|
}
|
|
1584
1695
|
|
|
1585
|
-
// Build Docker image with
|
|
1696
|
+
// Build Docker image with NEXT_PUBLIC_* vars as build args
|
|
1586
1697
|
const imageName = `${workspace.name}-${appName}`;
|
|
1587
1698
|
const imageRef = registry
|
|
1588
1699
|
? `${registry}/${imageName}:${imageTag}`
|
|
@@ -1600,17 +1711,22 @@ export async function workspaceDeployCommand(
|
|
|
1600
1711
|
appName,
|
|
1601
1712
|
},
|
|
1602
1713
|
buildArgs,
|
|
1603
|
-
// Pass
|
|
1604
|
-
publicUrlArgs:
|
|
1714
|
+
// Pass arg names for Dockerfile ARG generation
|
|
1715
|
+
publicUrlArgs: publicUrlArgNames,
|
|
1605
1716
|
});
|
|
1606
1717
|
|
|
1607
|
-
// Prepare environment variables
|
|
1718
|
+
// Prepare runtime environment variables
|
|
1608
1719
|
const envVars: string[] = [
|
|
1609
1720
|
`NODE_ENV=production`,
|
|
1610
1721
|
`PORT=${app.port}`,
|
|
1611
1722
|
`STAGE=${stage}`,
|
|
1612
1723
|
];
|
|
1613
1724
|
|
|
1725
|
+
// Add all resolved vars as runtime env (for SSR and server components)
|
|
1726
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
1727
|
+
envVars.push(`${key}=${value}`);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1614
1730
|
// Configure and deploy application in Dokploy
|
|
1615
1731
|
await api.saveDockerProvider(application.applicationId, imageRef, {
|
|
1616
1732
|
registryId,
|
|
@@ -1624,17 +1740,7 @@ export async function workspaceDeployCommand(
|
|
|
1624
1740
|
logger.log(` Deploying to Dokploy...`);
|
|
1625
1741
|
await api.deployApplication(application.applicationId);
|
|
1626
1742
|
|
|
1627
|
-
//
|
|
1628
|
-
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1629
|
-
const frontendHost = resolveHost(
|
|
1630
|
-
appName,
|
|
1631
|
-
app,
|
|
1632
|
-
stage,
|
|
1633
|
-
dokployConfig,
|
|
1634
|
-
isMainFrontend,
|
|
1635
|
-
);
|
|
1636
|
-
|
|
1637
|
-
// Check if domain already exists
|
|
1743
|
+
// Check if domain already exists (frontendHost computed earlier for env context)
|
|
1638
1744
|
const existingFrontendDomains = await api.getDomainsByApplicationId(
|
|
1639
1745
|
application.applicationId,
|
|
1640
1746
|
);
|
package/src/deploy/sniffer.ts
CHANGED
|
@@ -98,17 +98,49 @@ export async function sniffAppEnvironment(
|
|
|
98
98
|
): Promise<SniffedEnvironment> {
|
|
99
99
|
const { logWarnings = true } = options;
|
|
100
100
|
|
|
101
|
-
// 1. Frontend apps
|
|
101
|
+
// 1. Frontend apps - handle dependencies and config sniffing
|
|
102
102
|
if (app.type === 'frontend') {
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Auto-generate NEXT_PUBLIC_{DEP}_URL from dependencies
|
|
104
|
+
const depVars = (app.dependencies ?? []).map(
|
|
105
|
+
(dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// If config specified, sniff by importing the file(s)
|
|
109
|
+
// The file calls .parse() at module load, which triggers sniffer to capture vars
|
|
110
|
+
if (app.config) {
|
|
111
|
+
const sniffedVars: string[] = [];
|
|
112
|
+
|
|
113
|
+
// Collect config paths to sniff
|
|
114
|
+
const configPaths: string[] = [];
|
|
115
|
+
if (app.config.client) configPaths.push(app.config.client);
|
|
116
|
+
if (app.config.server) configPaths.push(app.config.server);
|
|
117
|
+
|
|
118
|
+
// Sniff each config file
|
|
119
|
+
for (const configPath of configPaths) {
|
|
120
|
+
const result = await sniffEntryFile(
|
|
121
|
+
configPath,
|
|
122
|
+
app.path,
|
|
123
|
+
workspacePath,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (logWarnings && result.error) {
|
|
127
|
+
console.warn(
|
|
128
|
+
`[sniffer] ${appName}: Config file "${configPath}" threw error during sniffing (env vars still captured): ${result.error.message}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
sniffedVars.push(...result.envVars);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Combine: dependency vars + sniffed vars (deduplicated)
|
|
136
|
+
const allVars = [...new Set([...depVars, ...sniffedVars])];
|
|
137
|
+
return { appName, requiredEnvVars: allVars };
|
|
138
|
+
}
|
|
105
139
|
|
|
106
|
-
|
|
107
|
-
if (app.requiredEnv && app.requiredEnv.length > 0) {
|
|
108
|
-
return { appName, requiredEnvVars: [...app.requiredEnv] };
|
|
140
|
+
return { appName, requiredEnvVars: depVars };
|
|
109
141
|
}
|
|
110
142
|
|
|
111
|
-
//
|
|
143
|
+
// 2. Entry apps - import entry file in subprocess to trigger config.parse()
|
|
112
144
|
if (app.entry) {
|
|
113
145
|
const result = await sniffEntryFile(app.entry, app.path, workspacePath);
|
|
114
146
|
|