@geekmidas/cli 1.0.1 → 1.1.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/CHANGELOG.md +12 -0
- package/dist/{SSMStateProvider-C4wp4AZe.mjs → SSMStateProvider-BjCi_58g.mjs} +16 -7
- package/dist/SSMStateProvider-BjCi_58g.mjs.map +1 -0
- package/dist/{SSMStateProvider-BxAPU99a.cjs → SSMStateProvider-D79o_JjM.cjs} +16 -7
- package/dist/SSMStateProvider-D79o_JjM.cjs.map +1 -0
- package/dist/{config-BGeJsW1r.cjs → config-CKfif10N.cjs} +2 -2
- package/dist/{config-BGeJsW1r.cjs.map → config-CKfif10N.cjs.map} +1 -1
- package/dist/{config-C6awcFBx.mjs → config-ClfjsfwH.mjs} +2 -2
- package/dist/{config-C6awcFBx.mjs.map → config-ClfjsfwH.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- package/dist/{index-KFEbMIRa.d.mts → index-CHQs8G3q.d.mts} +6 -1
- package/dist/index-CHQs8G3q.d.mts.map +1 -0
- package/dist/{index-B5rGIc4g.d.cts → index-afBljZKY.d.cts} +6 -1
- package/dist/index-afBljZKY.d.cts.map +1 -0
- package/dist/index.cjs +39 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +39 -26
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-BMFmLnX6.mjs → openapi-C6sa0L8b.mjs} +2 -2
- package/dist/{openapi-BMFmLnX6.mjs.map → openapi-C6sa0L8b.mjs.map} +1 -1
- package/dist/{openapi-D1KXv2Ml.cjs → openapi-D3p6s8UA.cjs} +2 -2
- package/dist/{openapi-D1KXv2Ml.cjs.map → openapi-D3p6s8UA.cjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.mjs +3 -3
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +1 -1
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-BFRUOOrh.cjs → workspace-CjT323qw.cjs} +3 -2
- package/dist/{workspace-BFRUOOrh.cjs.map → workspace-CjT323qw.cjs.map} +1 -1
- package/dist/{workspace-DAxG3_H2.mjs → workspace-CmITpum4.mjs} +3 -2
- package/dist/{workspace-DAxG3_H2.mjs.map → workspace-CmITpum4.mjs.map} +1 -1
- package/package.json +4 -4
- package/scripts/sync-versions.ts +25 -3
- package/src/deploy/SSMStateProvider.ts +20 -7
- package/src/deploy/StateProvider.ts +1 -1
- package/src/deploy/__tests__/SSMStateProvider.spec.ts +15 -8
- package/src/deploy/__tests__/state-e2e.spec.ts +385 -0
- package/src/init/__tests__/init.spec.ts +10 -1
- package/src/init/versions.ts +24 -2
- package/src/secrets/__tests__/storage.spec.ts +6 -2
- package/src/workspace/__tests__/index.spec.ts +61 -0
- package/src/workspace/index.ts +1 -0
- package/src/workspace/types.ts +7 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs.map +0 -1
- package/dist/SSMStateProvider-C4wp4AZe.mjs.map +0 -1
- package/dist/index-B5rGIc4g.d.cts.map +0 -1
- package/dist/index-KFEbMIRa.d.mts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"tsx": "~4.20.3",
|
|
56
56
|
"@geekmidas/constructs": "~1.0.0",
|
|
57
57
|
"@geekmidas/envkit": "~1.0.0",
|
|
58
|
-
"@geekmidas/
|
|
58
|
+
"@geekmidas/logger": "~1.0.0",
|
|
59
59
|
"@geekmidas/schema": "~1.0.0",
|
|
60
|
-
"@geekmidas/
|
|
60
|
+
"@geekmidas/errors": "~1.0.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/lodash.kebabcase": "^4.1.9",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"typescript": "^5.8.2",
|
|
68
68
|
"vitest": "^3.2.4",
|
|
69
69
|
"zod": "~4.1.13",
|
|
70
|
-
"@geekmidas/testkit": "1.0.
|
|
70
|
+
"@geekmidas/testkit": "1.0.1"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"@geekmidas/telescope": "~1.0.0"
|
package/scripts/sync-versions.ts
CHANGED
|
@@ -16,7 +16,6 @@ const PACKAGES = [
|
|
|
16
16
|
'audit',
|
|
17
17
|
'auth',
|
|
18
18
|
'cache',
|
|
19
|
-
'cli',
|
|
20
19
|
'client',
|
|
21
20
|
'cloud',
|
|
22
21
|
'constructs',
|
|
@@ -54,16 +53,39 @@ function generateVersionsFile(): string {
|
|
|
54
53
|
versions[`@geekmidas/${pkg}`] = `~${version}`;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
const content =
|
|
56
|
+
const content = `import { createRequire } from 'node:module';
|
|
57
|
+
|
|
58
|
+
const require = createRequire(import.meta.url);
|
|
59
|
+
|
|
60
|
+
// Load package.json - handles both bundled (flat dist/) and source (nested src/init/)
|
|
61
|
+
function loadPackageJson(): { version: string } {
|
|
62
|
+
try {
|
|
63
|
+
// Try flat dist path first (../package.json from dist/)
|
|
64
|
+
return require('../package.json');
|
|
65
|
+
} catch {
|
|
66
|
+
// Fall back to nested source path (../../package.json from src/init/)
|
|
67
|
+
return require('../../package.json');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pkg = loadPackageJson();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* CLI version resolved from package.json at runtime
|
|
75
|
+
*/
|
|
76
|
+
export const CLI_VERSION = \`~\${pkg.version}\`;
|
|
77
|
+
|
|
78
|
+
/**
|
|
58
79
|
* Package versions for @geekmidas packages
|
|
59
80
|
*
|
|
60
|
-
* AUTO-GENERATED - Do not edit manually
|
|
81
|
+
* AUTO-GENERATED (except CLI) - Do not edit manually
|
|
61
82
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
62
83
|
*/
|
|
63
84
|
export const GEEKMIDAS_VERSIONS = {
|
|
64
85
|
${Object.entries(versions)
|
|
65
86
|
.map(([pkg, version]) => `\t'${pkg}': '${version}',`)
|
|
66
87
|
.join('\n')}
|
|
88
|
+
'@geekmidas/cli': CLI_VERSION,
|
|
67
89
|
} as const;
|
|
68
90
|
|
|
69
91
|
export type GeekmidasPackage = keyof typeof GEEKMIDAS_VERSIONS;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ParameterNotFound,
|
|
13
13
|
PutParameterCommand,
|
|
14
14
|
SSMClient,
|
|
15
|
+
type SSMClientConfig,
|
|
15
16
|
} from '@aws-sdk/client-ssm';
|
|
16
17
|
import type { AwsRegion, StateProvider } from './StateProvider';
|
|
17
18
|
import type { DokployStageState } from './state';
|
|
@@ -19,8 +20,12 @@ import type { DokployStageState } from './state';
|
|
|
19
20
|
export interface SSMStateProviderOptions {
|
|
20
21
|
/** Workspace name (used in parameter path) */
|
|
21
22
|
workspaceName: string;
|
|
22
|
-
/** AWS region
|
|
23
|
-
region
|
|
23
|
+
/** AWS region */
|
|
24
|
+
region?: AwsRegion;
|
|
25
|
+
/** AWS credentials (optional - uses default credential chain if not provided) */
|
|
26
|
+
credentials?: SSMClientConfig['credentials'];
|
|
27
|
+
/** Custom endpoint (for LocalStack or other S3-compatible services) */
|
|
28
|
+
endpoint?: string;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
/**
|
|
@@ -30,14 +35,22 @@ export interface SSMStateProviderOptions {
|
|
|
30
35
|
* Parameter path: /gkm/{workspaceName}/{stage}/state
|
|
31
36
|
*/
|
|
32
37
|
export class SSMStateProvider implements StateProvider {
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
constructor(
|
|
39
|
+
readonly workspaceName: string,
|
|
40
|
+
private readonly client: SSMClient,
|
|
41
|
+
) {}
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Create an SSMStateProvider with a new SSMClient.
|
|
45
|
+
*/
|
|
46
|
+
static create(options: SSMStateProviderOptions): SSMStateProvider {
|
|
47
|
+
const client = new SSMClient({
|
|
39
48
|
region: options.region,
|
|
49
|
+
credentials: options.credentials,
|
|
50
|
+
endpoint: options.endpoint,
|
|
40
51
|
});
|
|
52
|
+
|
|
53
|
+
return new SSMStateProvider(options.workspaceName, client);
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
/**
|
|
@@ -158,7 +158,7 @@ export async function createStateProvider(
|
|
|
158
158
|
const { CachedStateProvider } = await import('./CachedStateProvider');
|
|
159
159
|
|
|
160
160
|
const local = new LocalStateProvider(workspaceRoot);
|
|
161
|
-
const ssm =
|
|
161
|
+
const ssm = SSMStateProvider.create({
|
|
162
162
|
workspaceName,
|
|
163
163
|
region: (config as SSMStateConfig).region,
|
|
164
164
|
});
|
|
@@ -44,14 +44,8 @@ describe('SSMStateProvider', () => {
|
|
|
44
44
|
},
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
provider
|
|
48
|
-
|
|
49
|
-
region: 'us-east-1',
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Override the client's endpoint for localstack
|
|
53
|
-
// @ts-expect-error - accessing private property for testing
|
|
54
|
-
provider.client = client;
|
|
47
|
+
// Create provider with injected client
|
|
48
|
+
provider = new SSMStateProvider(workspaceName, client);
|
|
55
49
|
});
|
|
56
50
|
|
|
57
51
|
afterEach(async () => {
|
|
@@ -70,6 +64,19 @@ describe('SSMStateProvider', () => {
|
|
|
70
64
|
client.destroy();
|
|
71
65
|
});
|
|
72
66
|
|
|
67
|
+
describe('static create', () => {
|
|
68
|
+
it('should create provider with options', () => {
|
|
69
|
+
const provider = SSMStateProvider.create({
|
|
70
|
+
workspaceName: 'my-workspace',
|
|
71
|
+
region: 'us-west-2',
|
|
72
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(provider).toBeInstanceOf(SSMStateProvider);
|
|
76
|
+
expect(provider.workspaceName).toBe('my-workspace');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
73
80
|
describe('read', () => {
|
|
74
81
|
it('should return null when parameter does not exist', async () => {
|
|
75
82
|
const state = await provider.read('nonexistent-stage');
|
|
@@ -0,0 +1,385 @@
|
|
|
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
|
+
environmentId: 'env_e2e_123',
|
|
46
|
+
applications: { api: 'app_e2e_123', web: 'app_e2e_456' },
|
|
47
|
+
services: { postgresId: 'pg_e2e_123', redisId: 'redis_e2e_123' },
|
|
48
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
49
|
+
...overrides,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('Local State Provider', () => {
|
|
53
|
+
let testDir: string;
|
|
54
|
+
|
|
55
|
+
beforeEach(async () => {
|
|
56
|
+
testDir = join(tmpdir(), `gkm-e2e-local-${Date.now()}`);
|
|
57
|
+
await mkdir(testDir, { recursive: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
await rm(testDir, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should write state to filesystem when config has state.provider = local', async () => {
|
|
65
|
+
// 1. Create workspace config with local state provider
|
|
66
|
+
const config: WorkspaceConfig = {
|
|
67
|
+
name: 'e2e-local-test',
|
|
68
|
+
apps: {
|
|
69
|
+
api: {
|
|
70
|
+
type: 'backend',
|
|
71
|
+
path: 'apps/api',
|
|
72
|
+
port: 3000,
|
|
73
|
+
routes: './src/**/*.ts',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
state: { provider: 'local' },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// 2. Normalize the workspace (simulates loadWorkspaceConfig)
|
|
80
|
+
const workspace = normalizeWorkspace(config, testDir);
|
|
81
|
+
|
|
82
|
+
// 3. Verify state config is passed through
|
|
83
|
+
expect(workspace.state).toEqual({ provider: 'local' });
|
|
84
|
+
|
|
85
|
+
// 4. Create state provider from workspace config
|
|
86
|
+
const provider = await createStateProvider({
|
|
87
|
+
config: workspace.state,
|
|
88
|
+
workspaceRoot: workspace.root,
|
|
89
|
+
workspaceName: workspace.name,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 5. Write state
|
|
93
|
+
const state = createTestState();
|
|
94
|
+
await provider.write(testStage, state);
|
|
95
|
+
|
|
96
|
+
// 6. Verify state was written to filesystem directly
|
|
97
|
+
const stateFilePath = join(testDir, '.gkm', `deploy-${testStage}.json`);
|
|
98
|
+
const fileContent = await readFile(stateFilePath, 'utf-8');
|
|
99
|
+
const storedState = JSON.parse(fileContent);
|
|
100
|
+
|
|
101
|
+
expect(storedState.provider).toBe('dokploy');
|
|
102
|
+
expect(storedState.stage).toBe(testStage);
|
|
103
|
+
expect(storedState.environmentId).toBe('env_e2e_123');
|
|
104
|
+
expect(storedState.applications).toEqual({
|
|
105
|
+
api: 'app_e2e_123',
|
|
106
|
+
web: 'app_e2e_456',
|
|
107
|
+
});
|
|
108
|
+
expect(storedState.services).toEqual({
|
|
109
|
+
postgresId: 'pg_e2e_123',
|
|
110
|
+
redisId: 'redis_e2e_123',
|
|
111
|
+
});
|
|
112
|
+
expect(storedState.lastDeployedAt).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should write state to filesystem when state config is undefined (default)', async () => {
|
|
116
|
+
// 1. Create workspace config WITHOUT state (should default to local)
|
|
117
|
+
const config: WorkspaceConfig = {
|
|
118
|
+
name: 'e2e-default-test',
|
|
119
|
+
apps: {
|
|
120
|
+
api: {
|
|
121
|
+
type: 'backend',
|
|
122
|
+
path: 'apps/api',
|
|
123
|
+
port: 3000,
|
|
124
|
+
routes: './src/**/*.ts',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
// No state config - should default to local
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// 2. Normalize the workspace
|
|
131
|
+
const workspace = normalizeWorkspace(config, testDir);
|
|
132
|
+
|
|
133
|
+
// 3. Verify state config is undefined
|
|
134
|
+
expect(workspace.state).toBeUndefined();
|
|
135
|
+
|
|
136
|
+
// 4. Create state provider (should create LocalStateProvider)
|
|
137
|
+
const provider = await createStateProvider({
|
|
138
|
+
config: workspace.state,
|
|
139
|
+
workspaceRoot: workspace.root,
|
|
140
|
+
workspaceName: workspace.name,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// 5. Write state
|
|
144
|
+
const state = createTestState();
|
|
145
|
+
await provider.write(testStage, state);
|
|
146
|
+
|
|
147
|
+
// 6. Verify state was written to filesystem
|
|
148
|
+
const stateFilePath = join(testDir, '.gkm', `deploy-${testStage}.json`);
|
|
149
|
+
const fileContent = await readFile(stateFilePath, 'utf-8');
|
|
150
|
+
const storedState = JSON.parse(fileContent);
|
|
151
|
+
|
|
152
|
+
expect(storedState.environmentId).toBe('env_e2e_123');
|
|
153
|
+
expect(storedState.applications.api).toBe('app_e2e_123');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should read state back correctly through provider', async () => {
|
|
157
|
+
const config: WorkspaceConfig = {
|
|
158
|
+
name: 'e2e-read-test',
|
|
159
|
+
apps: {
|
|
160
|
+
api: {
|
|
161
|
+
type: 'backend',
|
|
162
|
+
path: 'apps/api',
|
|
163
|
+
port: 3000,
|
|
164
|
+
routes: './src/**/*.ts',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
state: { provider: 'local' },
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const workspace = normalizeWorkspace(config, testDir);
|
|
171
|
+
const provider = await createStateProvider({
|
|
172
|
+
config: workspace.state,
|
|
173
|
+
workspaceRoot: workspace.root,
|
|
174
|
+
workspaceName: workspace.name,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Write state
|
|
178
|
+
const originalState = createTestState();
|
|
179
|
+
await provider.write(testStage, originalState);
|
|
180
|
+
|
|
181
|
+
// Read state back
|
|
182
|
+
const readState = await provider.read(testStage);
|
|
183
|
+
|
|
184
|
+
expect(readState).not.toBeNull();
|
|
185
|
+
expect(readState!.environmentId).toBe('env_e2e_123');
|
|
186
|
+
expect(readState!.applications).toEqual({
|
|
187
|
+
api: 'app_e2e_123',
|
|
188
|
+
web: 'app_e2e_456',
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('SSM State Provider', () => {
|
|
194
|
+
const LOCALSTACK_ENDPOINT = 'http://localhost:4566';
|
|
195
|
+
const workspaceName = 'e2e-ssm-test';
|
|
196
|
+
let ssmClient: SSMClient;
|
|
197
|
+
let testDir: string;
|
|
198
|
+
|
|
199
|
+
beforeAll(() => {
|
|
200
|
+
process.env.AWS_ACCESS_KEY_ID = 'test';
|
|
201
|
+
process.env.AWS_SECRET_ACCESS_KEY = 'test';
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
beforeEach(async () => {
|
|
205
|
+
testDir = join(tmpdir(), `gkm-e2e-ssm-${Date.now()}`);
|
|
206
|
+
await mkdir(testDir, { recursive: true });
|
|
207
|
+
|
|
208
|
+
ssmClient = new SSMClient({
|
|
209
|
+
region: 'us-east-1',
|
|
210
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
211
|
+
credentials: {
|
|
212
|
+
accessKeyId: 'test',
|
|
213
|
+
secretAccessKey: 'test',
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
afterEach(async () => {
|
|
219
|
+
// Clean up SSM parameter
|
|
220
|
+
try {
|
|
221
|
+
await ssmClient.send(
|
|
222
|
+
new DeleteParameterCommand({
|
|
223
|
+
Name: `/gkm/${workspaceName}/${testStage}/state`,
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
} catch {
|
|
227
|
+
// Ignore if parameter doesn't exist
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Clean up test directory
|
|
231
|
+
await rm(testDir, { recursive: true, force: true });
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
afterAll(() => {
|
|
235
|
+
ssmClient.destroy();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should write state to SSM when config has state.provider = ssm', async () => {
|
|
239
|
+
// 1. Create workspace config with SSM state provider
|
|
240
|
+
const config: WorkspaceConfig = {
|
|
241
|
+
name: workspaceName,
|
|
242
|
+
apps: {
|
|
243
|
+
api: {
|
|
244
|
+
type: 'backend',
|
|
245
|
+
path: 'apps/api',
|
|
246
|
+
port: 3000,
|
|
247
|
+
routes: './src/**/*.ts',
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
state: { provider: 'ssm', region: 'us-east-1' },
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// 2. Normalize the workspace
|
|
254
|
+
const workspace = normalizeWorkspace(config, testDir);
|
|
255
|
+
|
|
256
|
+
// 3. Verify state config is passed through
|
|
257
|
+
expect(workspace.state).toEqual({ provider: 'ssm', region: 'us-east-1' });
|
|
258
|
+
|
|
259
|
+
// 4. Create providers with injected SSM client (for LocalStack)
|
|
260
|
+
const local = new LocalStateProvider(workspace.root);
|
|
261
|
+
const ssm = new SSMStateProvider(workspace.name, ssmClient);
|
|
262
|
+
const provider = new CachedStateProvider(ssm, local);
|
|
263
|
+
|
|
264
|
+
// 5. Write state
|
|
265
|
+
const state = createTestState();
|
|
266
|
+
await provider.write(testStage, state);
|
|
267
|
+
|
|
268
|
+
// 6. Query SSM directly to verify state was written
|
|
269
|
+
const response = await ssmClient.send(
|
|
270
|
+
new GetParameterCommand({
|
|
271
|
+
Name: `/gkm/${workspaceName}/${testStage}/state`,
|
|
272
|
+
WithDecryption: true,
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(response.Parameter).toBeDefined();
|
|
277
|
+
expect(response.Parameter!.Value).toBeDefined();
|
|
278
|
+
|
|
279
|
+
const storedState = JSON.parse(response.Parameter!.Value!);
|
|
280
|
+
expect(storedState.provider).toBe('dokploy');
|
|
281
|
+
expect(storedState.stage).toBe(testStage);
|
|
282
|
+
expect(storedState.environmentId).toBe('env_e2e_123');
|
|
283
|
+
expect(storedState.applications).toEqual({
|
|
284
|
+
api: 'app_e2e_123',
|
|
285
|
+
web: 'app_e2e_456',
|
|
286
|
+
});
|
|
287
|
+
expect(storedState.services).toEqual({
|
|
288
|
+
postgresId: 'pg_e2e_123',
|
|
289
|
+
redisId: 'redis_e2e_123',
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should read state from SSM correctly', async () => {
|
|
294
|
+
// 1. Pre-populate SSM with state
|
|
295
|
+
const preExistingState: DokployStageState = {
|
|
296
|
+
provider: 'dokploy',
|
|
297
|
+
stage: testStage,
|
|
298
|
+
environmentId: 'env_pre_existing',
|
|
299
|
+
applications: { api: 'app_pre_123' },
|
|
300
|
+
services: { postgresId: 'pg_pre_123' },
|
|
301
|
+
lastDeployedAt: '2024-06-01T00:00:00.000Z',
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
await ssmClient.send(
|
|
305
|
+
new PutParameterCommand({
|
|
306
|
+
Name: `/gkm/${workspaceName}/${testStage}/state`,
|
|
307
|
+
Value: JSON.stringify(preExistingState),
|
|
308
|
+
Type: 'SecureString',
|
|
309
|
+
Overwrite: true,
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// 2. Create workspace config with SSM state provider
|
|
314
|
+
const config: WorkspaceConfig = {
|
|
315
|
+
name: workspaceName,
|
|
316
|
+
apps: {
|
|
317
|
+
api: {
|
|
318
|
+
type: 'backend',
|
|
319
|
+
path: 'apps/api',
|
|
320
|
+
port: 3000,
|
|
321
|
+
routes: './src/**/*.ts',
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
state: { provider: 'ssm', region: 'us-east-1' },
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const workspace = normalizeWorkspace(config, testDir);
|
|
328
|
+
|
|
329
|
+
// 3. Create providers with injected SSM client
|
|
330
|
+
const local = new LocalStateProvider(workspace.root);
|
|
331
|
+
const ssm = new SSMStateProvider(workspace.name, ssmClient);
|
|
332
|
+
const provider = new CachedStateProvider(ssm, local);
|
|
333
|
+
|
|
334
|
+
// 4. Read state through provider
|
|
335
|
+
const readState = await provider.read(testStage);
|
|
336
|
+
|
|
337
|
+
expect(readState).not.toBeNull();
|
|
338
|
+
expect(readState!.environmentId).toBe('env_pre_existing');
|
|
339
|
+
expect(readState!.applications.api).toBe('app_pre_123');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should use CachedStateProvider that syncs local and remote', async () => {
|
|
343
|
+
const config: WorkspaceConfig = {
|
|
344
|
+
name: workspaceName,
|
|
345
|
+
apps: {
|
|
346
|
+
api: {
|
|
347
|
+
type: 'backend',
|
|
348
|
+
path: 'apps/api',
|
|
349
|
+
port: 3000,
|
|
350
|
+
routes: './src/**/*.ts',
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
state: { provider: 'ssm', region: 'us-east-1' },
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const workspace = normalizeWorkspace(config, testDir);
|
|
357
|
+
|
|
358
|
+
// Create providers with injected SSM client
|
|
359
|
+
const local = new LocalStateProvider(workspace.root);
|
|
360
|
+
const ssm = new SSMStateProvider(workspace.name, ssmClient);
|
|
361
|
+
const provider = new CachedStateProvider(ssm, local);
|
|
362
|
+
|
|
363
|
+
// Write state
|
|
364
|
+
const state = createTestState();
|
|
365
|
+
await provider.write(testStage, state);
|
|
366
|
+
|
|
367
|
+
// Verify both local and SSM have the state
|
|
368
|
+
// Check local file
|
|
369
|
+
const localFilePath = join(testDir, '.gkm', `deploy-${testStage}.json`);
|
|
370
|
+
const localContent = await readFile(localFilePath, 'utf-8');
|
|
371
|
+
const localState = JSON.parse(localContent);
|
|
372
|
+
expect(localState.environmentId).toBe('env_e2e_123');
|
|
373
|
+
|
|
374
|
+
// Check SSM directly
|
|
375
|
+
const ssmResponse = await ssmClient.send(
|
|
376
|
+
new GetParameterCommand({
|
|
377
|
+
Name: `/gkm/${workspaceName}/${testStage}/state`,
|
|
378
|
+
WithDecryption: true,
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
const ssmState = JSON.parse(ssmResponse.Parameter!.Value!);
|
|
382
|
+
expect(ssmState.environmentId).toBe('env_e2e_123');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
6
|
import { initCommand } from '../index.js';
|
|
7
7
|
|
|
8
|
+
// Project names used in tests - keystore directories are created at ~/.gkm/{name}
|
|
9
|
+
const TEST_PROJECT_NAMES = ['my-api', 'my-monorepo', 'my-fullstack'];
|
|
10
|
+
|
|
8
11
|
describe('initCommand', () => {
|
|
9
12
|
let tempDir: string;
|
|
10
13
|
let originalCwd: string;
|
|
@@ -19,6 +22,12 @@ describe('initCommand', () => {
|
|
|
19
22
|
afterEach(async () => {
|
|
20
23
|
process.chdir(originalCwd);
|
|
21
24
|
await rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
|
|
26
|
+
// Clean up keystore directories created at ~/.gkm/{project-name}
|
|
27
|
+
const gkmDir = join(homedir(), '.gkm');
|
|
28
|
+
for (const name of TEST_PROJECT_NAMES) {
|
|
29
|
+
await rm(join(gkmDir, name), { recursive: true, force: true });
|
|
30
|
+
}
|
|
22
31
|
});
|
|
23
32
|
|
|
24
33
|
describe('non-monorepo', () => {
|
package/src/init/versions.ts
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// Load package.json - handles both bundled (flat dist/) and source (nested src/init/)
|
|
6
|
+
function loadPackageJson(): { version: string } {
|
|
7
|
+
try {
|
|
8
|
+
// Try flat dist path first (../package.json from dist/)
|
|
9
|
+
return require('../package.json');
|
|
10
|
+
} catch {
|
|
11
|
+
// Fall back to nested source path (../../package.json from src/init/)
|
|
12
|
+
return require('../../package.json');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const pkg = loadPackageJson();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* CLI version resolved from package.json at runtime
|
|
20
|
+
*/
|
|
21
|
+
export const CLI_VERSION = `~${pkg.version}`;
|
|
22
|
+
|
|
1
23
|
/**
|
|
2
24
|
* Package versions for @geekmidas packages
|
|
3
25
|
*
|
|
4
|
-
* AUTO-GENERATED - Do not edit manually
|
|
26
|
+
* AUTO-GENERATED (except CLI) - Do not edit manually
|
|
5
27
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
6
28
|
*/
|
|
7
29
|
export const GEEKMIDAS_VERSIONS = {
|
|
8
30
|
'@geekmidas/audit': '~1.0.0',
|
|
9
31
|
'@geekmidas/auth': '~1.0.0',
|
|
10
32
|
'@geekmidas/cache': '~1.0.0',
|
|
11
|
-
'@geekmidas/cli': '~1.0.0',
|
|
12
33
|
'@geekmidas/client': '~1.0.0',
|
|
13
34
|
'@geekmidas/cloud': '~1.0.0',
|
|
14
35
|
'@geekmidas/constructs': '~1.0.0',
|
|
@@ -25,6 +46,7 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
25
46
|
'@geekmidas/studio': '~1.0.0',
|
|
26
47
|
'@geekmidas/telescope': '~1.0.0',
|
|
27
48
|
'@geekmidas/testkit': '~1.0.0',
|
|
49
|
+
'@geekmidas/cli': CLI_VERSION,
|
|
28
50
|
} as const;
|
|
29
51
|
|
|
30
52
|
export type GeekmidasPackage = keyof typeof GEEKMIDAS_VERSIONS;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { mkdir, rm } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
6
|
import {
|
|
7
7
|
getSecretsDir,
|
|
@@ -56,6 +56,10 @@ describe('file operations', () => {
|
|
|
56
56
|
if (existsSync(tempDir)) {
|
|
57
57
|
await rm(tempDir, { recursive: true });
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
// Clean up keystore directory created at ~/.gkm/{tempDir-basename}
|
|
61
|
+
const keystoreDir = join(homedir(), '.gkm', basename(tempDir));
|
|
62
|
+
await rm(keystoreDir, { recursive: true, force: true });
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
describe('writeStageSecrets / readStageSecrets', () => {
|