@geekmidas/cli 1.0.2 → 1.2.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{SSMStateProvider-C4wp4AZe.mjs → SSMStateProvider-BjCi_58g.mjs} +16 -7
  3. package/dist/SSMStateProvider-BjCi_58g.mjs.map +1 -0
  4. package/dist/{SSMStateProvider-BxAPU99a.cjs → SSMStateProvider-D79o_JjM.cjs} +16 -7
  5. package/dist/SSMStateProvider-D79o_JjM.cjs.map +1 -0
  6. package/dist/{config-C6awcFBx.mjs → config-BQ4a36Rq.mjs} +2 -2
  7. package/dist/{config-C6awcFBx.mjs.map → config-BQ4a36Rq.mjs.map} +1 -1
  8. package/dist/{config-BGeJsW1r.cjs → config-Bayob8pB.cjs} +2 -2
  9. package/dist/{config-BGeJsW1r.cjs.map → config-Bayob8pB.cjs.map} +1 -1
  10. package/dist/config.cjs +2 -2
  11. package/dist/config.d.cts +1 -1
  12. package/dist/config.d.mts +1 -1
  13. package/dist/config.mjs +2 -2
  14. package/dist/{index-KFEbMIRa.d.mts → index-Bi9vGQJy.d.mts} +61 -13
  15. package/dist/index-Bi9vGQJy.d.mts.map +1 -0
  16. package/dist/{index-B5rGIc4g.d.cts → index-CufAAnge.d.cts} +61 -13
  17. package/dist/index-CufAAnge.d.cts.map +1 -0
  18. package/dist/index.cjs +14 -9
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +14 -9
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{openapi-D1KXv2Ml.cjs → openapi-BZP8jkI4.cjs} +2 -2
  23. package/dist/{openapi-D1KXv2Ml.cjs.map → openapi-BZP8jkI4.cjs.map} +1 -1
  24. package/dist/{openapi-BMFmLnX6.mjs → openapi-DrbBWq0s.mjs} +2 -2
  25. package/dist/{openapi-BMFmLnX6.mjs.map → openapi-DrbBWq0s.mjs.map} +1 -1
  26. package/dist/openapi.cjs +3 -3
  27. package/dist/openapi.mjs +3 -3
  28. package/dist/workspace/index.cjs +2 -1
  29. package/dist/workspace/index.d.cts +2 -2
  30. package/dist/workspace/index.d.mts +2 -2
  31. package/dist/workspace/index.mjs +2 -2
  32. package/dist/{workspace-BFRUOOrh.cjs → workspace-BMJE18LV.cjs} +46 -6
  33. package/dist/workspace-BMJE18LV.cjs.map +1 -0
  34. package/dist/{workspace-DAxG3_H2.mjs → workspace-CASoZOjs.mjs} +41 -7
  35. package/dist/workspace-CASoZOjs.mjs.map +1 -0
  36. package/package.json +4 -4
  37. package/src/deploy/SSMStateProvider.ts +20 -7
  38. package/src/deploy/StateProvider.ts +1 -1
  39. package/src/deploy/__tests__/CachedStateProvider.spec.ts +7 -0
  40. package/src/deploy/__tests__/LocalStateProvider.spec.ts +4 -0
  41. package/src/deploy/__tests__/SSMStateProvider.spec.ts +20 -8
  42. package/src/deploy/__tests__/dns-verification.spec.ts +1 -1
  43. package/src/deploy/__tests__/env-resolver.spec.ts +9 -9
  44. package/src/deploy/__tests__/state-e2e.spec.ts +387 -0
  45. package/src/deploy/__tests__/state.spec.ts +53 -29
  46. package/src/deploy/index.ts +6 -1
  47. package/src/deploy/state.ts +4 -0
  48. package/src/init/__tests__/init.spec.ts +10 -1
  49. package/src/init/versions.ts +1 -1
  50. package/src/secrets/__tests__/storage.spec.ts +6 -2
  51. package/src/workspace/__tests__/index.spec.ts +129 -0
  52. package/src/workspace/index.ts +44 -0
  53. package/src/workspace/schema.ts +17 -6
  54. package/src/workspace/types.ts +26 -9
  55. package/dist/SSMStateProvider-BxAPU99a.cjs.map +0 -1
  56. package/dist/SSMStateProvider-C4wp4AZe.mjs.map +0 -1
  57. package/dist/index-B5rGIc4g.d.cts.map +0 -1
  58. package/dist/index-KFEbMIRa.d.mts.map +0 -1
  59. package/dist/workspace-BFRUOOrh.cjs.map +0 -1
  60. package/dist/workspace-DAxG3_H2.mjs.map +0 -1
@@ -0,0 +1,387 @@
1
+ /**
2
+ * End-to-End State Provider Tests
3
+ *
4
+ * Tests the full flow from workspace config to state storage.
5
+ * - Local: verifies state is written to .gkm/deploy-{stage}.json
6
+ * - SSM: verifies state is written to AWS SSM Parameter Store (via LocalStack)
7
+ *
8
+ * SSM tests require LocalStack: docker compose up -d localstack
9
+ */
10
+
11
+ import { mkdir, readFile, rm } from 'node:fs/promises';
12
+ import { tmpdir } from 'node:os';
13
+ import { join } from 'node:path';
14
+ import {
15
+ DeleteParameterCommand,
16
+ GetParameterCommand,
17
+ PutParameterCommand,
18
+ SSMClient,
19
+ } from '@aws-sdk/client-ssm';
20
+ import {
21
+ afterAll,
22
+ afterEach,
23
+ beforeAll,
24
+ beforeEach,
25
+ describe,
26
+ expect,
27
+ it,
28
+ } from 'vitest';
29
+ import { normalizeWorkspace } from '../../workspace/index';
30
+ import type { WorkspaceConfig } from '../../workspace/types';
31
+ import { CachedStateProvider } from '../CachedStateProvider';
32
+ import { LocalStateProvider } from '../LocalStateProvider';
33
+ import { SSMStateProvider } from '../SSMStateProvider';
34
+ import { createStateProvider } from '../StateProvider';
35
+ import type { DokployStageState } from '../state';
36
+
37
+ describe('State Provider E2E', () => {
38
+ const testStage = 'e2e-test';
39
+
40
+ const createTestState = (
41
+ overrides?: Partial<DokployStageState>,
42
+ ): DokployStageState => ({
43
+ provider: 'dokploy',
44
+ stage: testStage,
45
+ projectId: 'proj_e2e_123',
46
+ environmentId: 'env_e2e_123',
47
+ applications: { api: 'app_e2e_123', web: 'app_e2e_456' },
48
+ services: { postgresId: 'pg_e2e_123', redisId: 'redis_e2e_123' },
49
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
50
+ ...overrides,
51
+ });
52
+
53
+ describe('Local State Provider', () => {
54
+ let testDir: string;
55
+
56
+ beforeEach(async () => {
57
+ testDir = join(tmpdir(), `gkm-e2e-local-${Date.now()}`);
58
+ await mkdir(testDir, { recursive: true });
59
+ });
60
+
61
+ afterEach(async () => {
62
+ await rm(testDir, { recursive: true, force: true });
63
+ });
64
+
65
+ it('should write state to filesystem when config has state.provider = local', async () => {
66
+ // 1. Create workspace config with local state provider
67
+ const config: WorkspaceConfig = {
68
+ name: 'e2e-local-test',
69
+ apps: {
70
+ api: {
71
+ type: 'backend',
72
+ path: 'apps/api',
73
+ port: 3000,
74
+ routes: './src/**/*.ts',
75
+ },
76
+ },
77
+ state: { provider: 'local' },
78
+ };
79
+
80
+ // 2. Normalize the workspace (simulates loadWorkspaceConfig)
81
+ const workspace = normalizeWorkspace(config, testDir);
82
+
83
+ // 3. Verify state config is passed through
84
+ expect(workspace.state).toEqual({ provider: 'local' });
85
+
86
+ // 4. Create state provider from workspace config
87
+ const provider = await createStateProvider({
88
+ config: workspace.state,
89
+ workspaceRoot: workspace.root,
90
+ workspaceName: workspace.name,
91
+ });
92
+
93
+ // 5. Write state
94
+ const state = createTestState();
95
+ await provider.write(testStage, state);
96
+
97
+ // 6. Verify state was written to filesystem directly
98
+ const stateFilePath = join(testDir, '.gkm', `deploy-${testStage}.json`);
99
+ const fileContent = await readFile(stateFilePath, 'utf-8');
100
+ const storedState = JSON.parse(fileContent);
101
+
102
+ expect(storedState.provider).toBe('dokploy');
103
+ expect(storedState.stage).toBe(testStage);
104
+ expect(storedState.environmentId).toBe('env_e2e_123');
105
+ expect(storedState.applications).toEqual({
106
+ api: 'app_e2e_123',
107
+ web: 'app_e2e_456',
108
+ });
109
+ expect(storedState.services).toEqual({
110
+ postgresId: 'pg_e2e_123',
111
+ redisId: 'redis_e2e_123',
112
+ });
113
+ expect(storedState.lastDeployedAt).toBeDefined();
114
+ });
115
+
116
+ it('should write state to filesystem when state config is undefined (default)', async () => {
117
+ // 1. Create workspace config WITHOUT state (should default to local)
118
+ const config: WorkspaceConfig = {
119
+ name: 'e2e-default-test',
120
+ apps: {
121
+ api: {
122
+ type: 'backend',
123
+ path: 'apps/api',
124
+ port: 3000,
125
+ routes: './src/**/*.ts',
126
+ },
127
+ },
128
+ // No state config - should default to local
129
+ };
130
+
131
+ // 2. Normalize the workspace
132
+ const workspace = normalizeWorkspace(config, testDir);
133
+
134
+ // 3. Verify state config is undefined
135
+ expect(workspace.state).toBeUndefined();
136
+
137
+ // 4. Create state provider (should create LocalStateProvider)
138
+ const provider = await createStateProvider({
139
+ config: workspace.state,
140
+ workspaceRoot: workspace.root,
141
+ workspaceName: workspace.name,
142
+ });
143
+
144
+ // 5. Write state
145
+ const state = createTestState();
146
+ await provider.write(testStage, state);
147
+
148
+ // 6. Verify state was written to filesystem
149
+ const stateFilePath = join(testDir, '.gkm', `deploy-${testStage}.json`);
150
+ const fileContent = await readFile(stateFilePath, 'utf-8');
151
+ const storedState = JSON.parse(fileContent);
152
+
153
+ expect(storedState.environmentId).toBe('env_e2e_123');
154
+ expect(storedState.applications.api).toBe('app_e2e_123');
155
+ });
156
+
157
+ it('should read state back correctly through provider', async () => {
158
+ const config: WorkspaceConfig = {
159
+ name: 'e2e-read-test',
160
+ apps: {
161
+ api: {
162
+ type: 'backend',
163
+ path: 'apps/api',
164
+ port: 3000,
165
+ routes: './src/**/*.ts',
166
+ },
167
+ },
168
+ state: { provider: 'local' },
169
+ };
170
+
171
+ const workspace = normalizeWorkspace(config, testDir);
172
+ const provider = await createStateProvider({
173
+ config: workspace.state,
174
+ workspaceRoot: workspace.root,
175
+ workspaceName: workspace.name,
176
+ });
177
+
178
+ // Write state
179
+ const originalState = createTestState();
180
+ await provider.write(testStage, originalState);
181
+
182
+ // Read state back
183
+ const readState = await provider.read(testStage);
184
+
185
+ expect(readState).not.toBeNull();
186
+ expect(readState!.environmentId).toBe('env_e2e_123');
187
+ expect(readState!.applications).toEqual({
188
+ api: 'app_e2e_123',
189
+ web: 'app_e2e_456',
190
+ });
191
+ });
192
+ });
193
+
194
+ describe('SSM State Provider', () => {
195
+ const LOCALSTACK_ENDPOINT = 'http://localhost:4566';
196
+ const workspaceName = 'e2e-ssm-test';
197
+ let ssmClient: SSMClient;
198
+ let testDir: string;
199
+
200
+ beforeAll(() => {
201
+ process.env.AWS_ACCESS_KEY_ID = 'test';
202
+ process.env.AWS_SECRET_ACCESS_KEY = 'test';
203
+ });
204
+
205
+ beforeEach(async () => {
206
+ testDir = join(tmpdir(), `gkm-e2e-ssm-${Date.now()}`);
207
+ await mkdir(testDir, { recursive: true });
208
+
209
+ ssmClient = new SSMClient({
210
+ region: 'us-east-1',
211
+ endpoint: LOCALSTACK_ENDPOINT,
212
+ credentials: {
213
+ accessKeyId: 'test',
214
+ secretAccessKey: 'test',
215
+ },
216
+ });
217
+ });
218
+
219
+ afterEach(async () => {
220
+ // Clean up SSM parameter
221
+ try {
222
+ await ssmClient.send(
223
+ new DeleteParameterCommand({
224
+ Name: `/gkm/${workspaceName}/${testStage}/state`,
225
+ }),
226
+ );
227
+ } catch {
228
+ // Ignore if parameter doesn't exist
229
+ }
230
+
231
+ // Clean up test directory
232
+ await rm(testDir, { recursive: true, force: true });
233
+ });
234
+
235
+ afterAll(() => {
236
+ ssmClient.destroy();
237
+ });
238
+
239
+ it('should write state to SSM when config has state.provider = ssm', async () => {
240
+ // 1. Create workspace config with SSM state provider
241
+ const config: WorkspaceConfig = {
242
+ name: workspaceName,
243
+ apps: {
244
+ api: {
245
+ type: 'backend',
246
+ path: 'apps/api',
247
+ port: 3000,
248
+ routes: './src/**/*.ts',
249
+ },
250
+ },
251
+ state: { provider: 'ssm', region: 'us-east-1' },
252
+ };
253
+
254
+ // 2. Normalize the workspace
255
+ const workspace = normalizeWorkspace(config, testDir);
256
+
257
+ // 3. Verify state config is passed through
258
+ expect(workspace.state).toEqual({ provider: 'ssm', region: 'us-east-1' });
259
+
260
+ // 4. Create providers with injected SSM client (for LocalStack)
261
+ const local = new LocalStateProvider(workspace.root);
262
+ const ssm = new SSMStateProvider(workspace.name, ssmClient);
263
+ const provider = new CachedStateProvider(ssm, local);
264
+
265
+ // 5. Write state
266
+ const state = createTestState();
267
+ await provider.write(testStage, state);
268
+
269
+ // 6. Query SSM directly to verify state was written
270
+ const response = await ssmClient.send(
271
+ new GetParameterCommand({
272
+ Name: `/gkm/${workspaceName}/${testStage}/state`,
273
+ WithDecryption: true,
274
+ }),
275
+ );
276
+
277
+ expect(response.Parameter).toBeDefined();
278
+ expect(response.Parameter!.Value).toBeDefined();
279
+
280
+ const storedState = JSON.parse(response.Parameter!.Value!);
281
+ expect(storedState.provider).toBe('dokploy');
282
+ expect(storedState.stage).toBe(testStage);
283
+ expect(storedState.environmentId).toBe('env_e2e_123');
284
+ expect(storedState.applications).toEqual({
285
+ api: 'app_e2e_123',
286
+ web: 'app_e2e_456',
287
+ });
288
+ expect(storedState.services).toEqual({
289
+ postgresId: 'pg_e2e_123',
290
+ redisId: 'redis_e2e_123',
291
+ });
292
+ });
293
+
294
+ it('should read state from SSM correctly', async () => {
295
+ // 1. Pre-populate SSM with state
296
+ const preExistingState: DokployStageState = {
297
+ provider: 'dokploy',
298
+ stage: testStage,
299
+ projectId: 'proj_test',
300
+ environmentId: 'env_pre_existing',
301
+ applications: { api: 'app_pre_123' },
302
+ services: { postgresId: 'pg_pre_123' },
303
+ lastDeployedAt: '2024-06-01T00:00:00.000Z',
304
+ };
305
+
306
+ await ssmClient.send(
307
+ new PutParameterCommand({
308
+ Name: `/gkm/${workspaceName}/${testStage}/state`,
309
+ Value: JSON.stringify(preExistingState),
310
+ Type: 'SecureString',
311
+ Overwrite: true,
312
+ }),
313
+ );
314
+
315
+ // 2. Create workspace config with SSM state provider
316
+ const config: WorkspaceConfig = {
317
+ name: workspaceName,
318
+ apps: {
319
+ api: {
320
+ type: 'backend',
321
+ path: 'apps/api',
322
+ port: 3000,
323
+ routes: './src/**/*.ts',
324
+ },
325
+ },
326
+ state: { provider: 'ssm', region: 'us-east-1' },
327
+ };
328
+
329
+ const workspace = normalizeWorkspace(config, testDir);
330
+
331
+ // 3. Create providers with injected SSM client
332
+ const local = new LocalStateProvider(workspace.root);
333
+ const ssm = new SSMStateProvider(workspace.name, ssmClient);
334
+ const provider = new CachedStateProvider(ssm, local);
335
+
336
+ // 4. Read state through provider
337
+ const readState = await provider.read(testStage);
338
+
339
+ expect(readState).not.toBeNull();
340
+ expect(readState!.environmentId).toBe('env_pre_existing');
341
+ expect(readState!.applications.api).toBe('app_pre_123');
342
+ });
343
+
344
+ it('should use CachedStateProvider that syncs local and remote', async () => {
345
+ const config: WorkspaceConfig = {
346
+ name: workspaceName,
347
+ apps: {
348
+ api: {
349
+ type: 'backend',
350
+ path: 'apps/api',
351
+ port: 3000,
352
+ routes: './src/**/*.ts',
353
+ },
354
+ },
355
+ state: { provider: 'ssm', region: 'us-east-1' },
356
+ };
357
+
358
+ const workspace = normalizeWorkspace(config, testDir);
359
+
360
+ // Create providers with injected SSM client
361
+ const local = new LocalStateProvider(workspace.root);
362
+ const ssm = new SSMStateProvider(workspace.name, ssmClient);
363
+ const provider = new CachedStateProvider(ssm, local);
364
+
365
+ // Write state
366
+ const state = createTestState();
367
+ await provider.write(testStage, state);
368
+
369
+ // Verify both local and SSM have the state
370
+ // Check local file
371
+ const localFilePath = join(testDir, '.gkm', `deploy-${testStage}.json`);
372
+ const localContent = await readFile(localFilePath, 'utf-8');
373
+ const localState = JSON.parse(localContent);
374
+ expect(localState.environmentId).toBe('env_e2e_123');
375
+
376
+ // Check SSM directly
377
+ const ssmResponse = await ssmClient.send(
378
+ new GetParameterCommand({
379
+ Name: `/gkm/${workspaceName}/${testStage}/state`,
380
+ WithDecryption: true,
381
+ }),
382
+ );
383
+ const ssmState = JSON.parse(ssmResponse.Parameter!.Value!);
384
+ expect(ssmState.environmentId).toBe('env_e2e_123');
385
+ });
386
+ });
387
+ });