@geekmidas/cli 1.10.41 → 1.12.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 +149 -0
- package/README.md +14 -0
- package/dist/{CachedStateProvider-37pG1HBx.cjs → CachedStateProvider-BdX7klRc.cjs} +1 -1
- package/dist/{CachedStateProvider-C8A-cCm5.mjs → CachedStateProvider-CzyFct42.mjs} +1 -1
- package/dist/{CachedStateProvider-C8A-cCm5.mjs.map → CachedStateProvider-CzyFct42.mjs.map} +1 -1
- package/dist/CachedStateProvider-GcGziXh1.mjs +3 -0
- package/dist/{CachedStateProvider-CxsXOKkg.cjs → CachedStateProvider-klLWZXWF.cjs} +1 -1
- package/dist/{CachedStateProvider-CxsXOKkg.cjs.map → CachedStateProvider-klLWZXWF.cjs.map} +1 -1
- package/dist/{HostingerProvider-ANWchdiK.mjs → HostingerProvider-B2ZYz7Yc.mjs} +1 -1
- package/dist/{HostingerProvider-ANWchdiK.mjs.map → HostingerProvider-B2ZYz7Yc.mjs.map} +1 -1
- package/dist/{HostingerProvider-5KYmwoK2.cjs → HostingerProvider-CrHfsgz9.cjs} +1 -1
- package/dist/{HostingerProvider-5KYmwoK2.cjs.map → HostingerProvider-CrHfsgz9.cjs.map} +1 -1
- package/dist/{LocalStateProvider-Dp0KkRcw.mjs → LocalStateProvider-CP49rjzt.mjs} +1 -1
- package/dist/{LocalStateProvider-Dp0KkRcw.mjs.map → LocalStateProvider-CP49rjzt.mjs.map} +1 -1
- package/dist/{LocalStateProvider-CLifRC0Y.cjs → LocalStateProvider-CalzNfNA.cjs} +1 -1
- package/dist/{LocalStateProvider-CLifRC0Y.cjs.map → LocalStateProvider-CalzNfNA.cjs.map} +1 -1
- package/dist/{Route53Provider-owQQ4pn6.cjs → Route53Provider-DwgXiLxt.cjs} +1 -1
- package/dist/{Route53Provider-owQQ4pn6.cjs.map → Route53Provider-DwgXiLxt.cjs.map} +1 -1
- package/dist/{Route53Provider-QoPgcXxn.mjs → Route53Provider-tlgBzrRQ.mjs} +1 -1
- package/dist/{Route53Provider-QoPgcXxn.mjs.map → Route53Provider-tlgBzrRQ.mjs.map} +1 -1
- package/dist/{SSMStateProvider-CksOTB8M.mjs → SSMStateProvider-BlrTiV5m.mjs} +1 -1
- package/dist/{SSMStateProvider-CksOTB8M.mjs.map → SSMStateProvider-BlrTiV5m.mjs.map} +1 -1
- package/dist/{SSMStateProvider-CT8tjl9o.cjs → SSMStateProvider-Bt6r8CTL.cjs} +1 -1
- package/dist/{SSMStateProvider-CT8tjl9o.cjs.map → SSMStateProvider-Bt6r8CTL.cjs.map} +1 -1
- package/dist/{backup-provisioner-C4noe75O.cjs → backup-provisioner-B3HBN-2k.cjs} +1 -1
- package/dist/{backup-provisioner-C4noe75O.cjs.map → backup-provisioner-B3HBN-2k.cjs.map} +1 -1
- package/dist/{backup-provisioner-BEXoHTuC.mjs → backup-provisioner-Xunq-p_8.mjs} +1 -1
- package/dist/{backup-provisioner-BEXoHTuC.mjs.map → backup-provisioner-Xunq-p_8.mjs.map} +1 -1
- package/dist/{bundler-c6QvGUBM.mjs → bundler-DkA4qzUD.mjs} +2 -2
- package/dist/{bundler-c6QvGUBM.mjs.map → bundler-DkA4qzUD.mjs.map} +1 -1
- package/dist/{bundler-BG6P249p.cjs → bundler-Dufq3uvI.cjs} +2 -2
- package/dist/{bundler-BG6P249p.cjs.map → bundler-Dufq3uvI.cjs.map} +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/dokploy-api-BPYkG-ZZ.cjs +3 -0
- package/dist/dokploy-api-BiSzWM4v.mjs +3 -0
- package/dist/{dokploy-api-Dvyq-LsM.cjs → dokploy-api-CqOIKHme.cjs} +1 -1
- package/dist/{dokploy-api-Dvyq-LsM.cjs.map → dokploy-api-CqOIKHme.cjs.map} +1 -1
- package/dist/{dokploy-api-BIT0683o.mjs → dokploy-api-sfZFlWuE.mjs} +1 -1
- package/dist/{dokploy-api-BIT0683o.mjs.map → dokploy-api-sfZFlWuE.mjs.map} +1 -1
- package/dist/{encryption-CipsS0KW.cjs → encryption-BVS7Vmzs.cjs} +1 -1
- package/dist/encryption-Bd_m4q3g.mjs +3 -0
- package/dist/{encryption-DSkIlEZ6.mjs → encryption-CO5Q4Bhf.mjs} +1 -1
- package/dist/{encryption-DSkIlEZ6.mjs.map → encryption-CO5Q4Bhf.mjs.map} +1 -1
- package/dist/{encryption-DgKS-Dv9.cjs → encryption-MfXAiFTm.cjs} +1 -1
- package/dist/{encryption-DgKS-Dv9.cjs.map → encryption-MfXAiFTm.cjs.map} +1 -1
- package/dist/{index-BBvGMG_A.d.mts → index-Bw548Bta.d.mts} +2 -2
- package/dist/{index-Dz2a7xQU.d.cts.map → index-Bw548Bta.d.mts.map} +1 -1
- package/dist/{index-Dz2a7xQU.d.cts → index-DTpkeFb8.d.cts} +2 -2
- package/dist/{index-BBvGMG_A.d.mts.map → index-DTpkeFb8.d.cts.map} +1 -1
- package/dist/index.cjs +863 -1330
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +990 -1457
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-DHp49UJV.cjs → openapi-BzNOC7_i.cjs} +14 -2
- package/dist/openapi-BzNOC7_i.cjs.map +1 -0
- package/dist/{openapi-DF9GyhyZ.mjs → openapi-Cfwi0fqF.mjs} +14 -2
- package/dist/openapi-Cfwi0fqF.mjs.map +1 -0
- package/dist/{openapi-react-query-C4UdILaI.mjs → openapi-react-query-C4iIvpPf.mjs} +1 -1
- package/dist/{openapi-react-query-C4UdILaI.mjs.map → openapi-react-query-C4iIvpPf.mjs.map} +1 -1
- package/dist/{openapi-react-query-DYbBq-WJ.cjs → openapi-react-query-CYWik72n.cjs} +1 -1
- package/dist/{openapi-react-query-DYbBq-WJ.cjs.map → openapi-react-query-CYWik72n.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{reconcile-CYGmqXvq.mjs → reconcile-B2FoJE0a.mjs} +1 -1
- package/dist/{reconcile-CYGmqXvq.mjs.map → reconcile-B2FoJE0a.mjs.map} +1 -1
- package/dist/{reconcile-D93YG7G5.cjs → reconcile-BzqKJ2Rt.cjs} +1 -1
- package/dist/{reconcile-D93YG7G5.cjs.map → reconcile-BzqKJ2Rt.cjs.map} +1 -1
- package/dist/setup-DDXUXzzl.mjs +765 -0
- package/dist/setup-DDXUXzzl.mjs.map +1 -0
- package/dist/setup-DLwhfyt-.cjs +8 -0
- package/dist/setup-Dh-B7YTX.mjs +8 -0
- package/dist/setup-WZ-xqAQO.cjs +849 -0
- package/dist/setup-WZ-xqAQO.cjs.map +1 -0
- package/dist/{sync-COnAugP-.mjs → sync-B5UHXwRM.mjs} +1 -1
- package/dist/sync-BLA94rgC.cjs +4 -0
- package/dist/{sync-D_NowTkZ.mjs → sync-CSooqRQ8.mjs} +1 -1
- package/dist/{sync-D_NowTkZ.mjs.map → sync-CSooqRQ8.mjs.map} +1 -1
- package/dist/{sync-DGXXSk2v.cjs → sync-o_CtEbvR.cjs} +1 -1
- package/dist/{sync-DGXXSk2v.cjs.map → sync-o_CtEbvR.cjs.map} +1 -1
- package/dist/{types-DdHfUbxk.d.cts → types-B3GQUg3k.d.mts} +3 -1
- package/dist/{types-DdHfUbxk.d.cts.map → types-B3GQUg3k.d.mts.map} +1 -1
- package/dist/{types-OszPdw9m.d.mts → types-BWQDiooe.d.cts} +3 -1
- package/dist/{types-OszPdw9m.d.mts.map → types-BWQDiooe.d.cts.map} +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/package.json +6 -5
- package/src/build/__tests__/index-new.spec.ts +3 -1
- package/src/build/__tests__/manifests.spec.ts +2 -2
- package/src/build/index.ts +61 -21
- package/src/build/manifests.ts +62 -5
- package/src/dev/index.ts +25 -11
- package/src/generators/EndpointGenerator.ts +14 -2
- package/src/generators/QueueGenerator.ts +245 -0
- package/src/generators/SubscriberGenerator.ts +5 -0
- package/src/generators/TopicGenerator.ts +45 -0
- package/src/generators/__tests__/QueueGenerator.spec.ts +141 -0
- package/src/generators/index.ts +2 -0
- package/src/index.ts +4 -0
- package/src/init/versions.ts +6 -6
- package/src/setup/__tests__/auto-setup.spec.ts +147 -0
- package/src/setup/index.ts +46 -6
- package/src/test/index.ts +17 -0
- package/src/types.ts +14 -51
- package/dist/CachedStateProvider-CC0DyO09.mjs +0 -3
- package/dist/dokploy-api-BOx71j6F.cjs +0 -3
- package/dist/dokploy-api-BZGk-lkD.mjs +0 -3
- package/dist/encryption-Db2zvGqM.mjs +0 -3
- package/dist/openapi-DF9GyhyZ.mjs.map +0 -1
- package/dist/openapi-DHp49UJV.cjs.map +0 -1
- package/dist/sync-D1Pa30oV.cjs +0 -4
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { type Queue, QueueBuilder } from '@geekmidas/constructs/queue';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import {
|
|
7
|
+
cleanupDir,
|
|
8
|
+
createMockBuildContext,
|
|
9
|
+
createTempDir,
|
|
10
|
+
} from '../../__tests__/test-helpers';
|
|
11
|
+
import type { GeneratedConstruct } from '../Generator';
|
|
12
|
+
import { QueueGenerator } from '../QueueGenerator';
|
|
13
|
+
|
|
14
|
+
const schema = z.object({ orderId: z.string() });
|
|
15
|
+
|
|
16
|
+
describe('QueueGenerator', () => {
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
let outputDir: string;
|
|
19
|
+
let generator: QueueGenerator;
|
|
20
|
+
let context: ReturnType<typeof createMockBuildContext>;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
tempDir = await createTempDir();
|
|
24
|
+
outputDir = join(tempDir, 'output');
|
|
25
|
+
generator = new QueueGenerator();
|
|
26
|
+
context = createMockBuildContext();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await cleanupDir(tempDir);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const createQueueConstruct = (
|
|
34
|
+
key: string,
|
|
35
|
+
name: string,
|
|
36
|
+
): GeneratedConstruct<Queue<any, any, any, any>> => {
|
|
37
|
+
const queue = new QueueBuilder()
|
|
38
|
+
.queue(name)
|
|
39
|
+
.batchSize(5)
|
|
40
|
+
.message(schema)
|
|
41
|
+
.handle(async () => {});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
key,
|
|
45
|
+
name: key.toLowerCase(),
|
|
46
|
+
construct: queue,
|
|
47
|
+
path: {
|
|
48
|
+
absolute: join(tempDir, `${key}.ts`),
|
|
49
|
+
relative: `${key}.ts`,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe('isConstruct', () => {
|
|
55
|
+
it('identifies queues and rejects everything else', () => {
|
|
56
|
+
const queue = new QueueBuilder()
|
|
57
|
+
.queue('orders')
|
|
58
|
+
.message(schema)
|
|
59
|
+
.handle(async () => {});
|
|
60
|
+
|
|
61
|
+
expect(generator.isConstruct(queue)).toBe(true);
|
|
62
|
+
expect(generator.isConstruct({})).toBe(false);
|
|
63
|
+
expect(generator.isConstruct(null)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('aws-lambda provider', () => {
|
|
68
|
+
it('generates one AWSLambdaQueue handler per queue', async () => {
|
|
69
|
+
const constructs = [
|
|
70
|
+
createQueueConstruct('ordersQueue', 'orders'),
|
|
71
|
+
createQueueConstruct('emailsQueue', 'emails'),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const infos = await generator.build(context, constructs, outputDir, {
|
|
75
|
+
provider: 'aws-lambda',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(infos).toHaveLength(2);
|
|
79
|
+
expect(infos[0]).toMatchObject({
|
|
80
|
+
name: 'orders',
|
|
81
|
+
handler: expect.stringContaining('queues/ordersQueue.handler'),
|
|
82
|
+
batchSize: 5,
|
|
83
|
+
});
|
|
84
|
+
// Env requirement from the (sniffed) producer flows nowhere here, but the
|
|
85
|
+
// consumer queue's own services would; assert the field exists.
|
|
86
|
+
expect(infos[0].environment).toBeDefined();
|
|
87
|
+
|
|
88
|
+
const handler = await readFile(
|
|
89
|
+
join(outputDir, 'queues', 'ordersQueue.ts'),
|
|
90
|
+
'utf-8',
|
|
91
|
+
);
|
|
92
|
+
expect(handler).toContain(
|
|
93
|
+
"import { AWSLambdaQueue } from '@geekmidas/constructs/aws'",
|
|
94
|
+
);
|
|
95
|
+
expect(handler).toContain('import { ordersQueue }');
|
|
96
|
+
expect(handler).toContain('new AWSLambdaQueue(envParser, ordersQueue)');
|
|
97
|
+
expect(handler).toContain('export const handler = adapter.handler');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns an empty array for no queues', async () => {
|
|
101
|
+
const infos = await generator.build(context, [], outputDir, {
|
|
102
|
+
provider: 'aws-lambda',
|
|
103
|
+
});
|
|
104
|
+
expect(infos).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('server provider', () => {
|
|
109
|
+
it('generates queues.ts with the setupQueues poller even when empty', async () => {
|
|
110
|
+
const infos = await generator.build(context, [], outputDir, {
|
|
111
|
+
provider: 'server',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(infos).toEqual([]);
|
|
115
|
+
|
|
116
|
+
const content = await readFile(join(outputDir, 'queues.ts'), 'utf-8');
|
|
117
|
+
expect(content).toContain('export async function setupQueues');
|
|
118
|
+
expect(content).toContain('const queues = [');
|
|
119
|
+
expect(content).toContain('EVENT_SUBSCRIBER_CONNECTION_STRING');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('subscribes each queue by its name and validates the payload', async () => {
|
|
123
|
+
const constructs = [
|
|
124
|
+
createQueueConstruct('ordersQueue', 'orders'),
|
|
125
|
+
createQueueConstruct('emailsQueue', 'emails'),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
await generator.build(context, constructs, outputDir, {
|
|
129
|
+
provider: 'server',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const content = await readFile(join(outputDir, 'queues.ts'), 'utf-8');
|
|
133
|
+
expect(content).toContain('import { ordersQueue }');
|
|
134
|
+
expect(content).toContain('import { emailsQueue }');
|
|
135
|
+
// A queue subscribes to a single "type" — its own name.
|
|
136
|
+
expect(content).toContain('eventSubscriber.subscribe([queue.name]');
|
|
137
|
+
expect(content).toContain("queue.messageSchema['~standard'].validate");
|
|
138
|
+
expect(content).toContain('messages: [validation.value]');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
package/src/generators/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -236,6 +236,10 @@ program
|
|
|
236
236
|
.option('--watch', 'Enable watch mode')
|
|
237
237
|
.option('--coverage', 'Generate coverage report')
|
|
238
238
|
.option('--ui', 'Open Vitest UI')
|
|
239
|
+
.option(
|
|
240
|
+
'--auto-setup',
|
|
241
|
+
'Generate a fresh stage (secrets + key) from the workspace config when none exists (for CI; also via GKM_AUTO_SETUP)',
|
|
242
|
+
)
|
|
239
243
|
.argument('[pattern]', 'Pattern to filter tests')
|
|
240
244
|
.action(async (pattern: string | undefined, options: TestOptions) => {
|
|
241
245
|
try {
|
package/src/init/versions.ts
CHANGED
|
@@ -30,22 +30,22 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
30
30
|
'@geekmidas/audit': '~2.0.1',
|
|
31
31
|
'@geekmidas/auth': '~2.0.1',
|
|
32
32
|
'@geekmidas/cache': '~1.1.1',
|
|
33
|
-
'@geekmidas/client': '~
|
|
33
|
+
'@geekmidas/client': '~6.0.0',
|
|
34
34
|
'@geekmidas/cloud': '~1.0.1',
|
|
35
|
-
'@geekmidas/constructs': '~
|
|
35
|
+
'@geekmidas/constructs': '~4.0.1',
|
|
36
36
|
'@geekmidas/db': '~1.0.2',
|
|
37
37
|
'@geekmidas/emailkit': '~1.0.1',
|
|
38
38
|
'@geekmidas/envkit': '~1.0.7',
|
|
39
39
|
'@geekmidas/errors': '~1.0.1',
|
|
40
40
|
'@geekmidas/events': '~1.1.4',
|
|
41
41
|
'@geekmidas/logger': '~1.0.2',
|
|
42
|
-
'@geekmidas/rate-limit': '~
|
|
43
|
-
'@geekmidas/schema': '~1.0.
|
|
44
|
-
'@geekmidas/services': '~1.
|
|
42
|
+
'@geekmidas/rate-limit': '~3.0.0',
|
|
43
|
+
'@geekmidas/schema': '~1.0.3',
|
|
44
|
+
'@geekmidas/services': '~1.1.1',
|
|
45
45
|
'@geekmidas/storage': '~2.0.3',
|
|
46
46
|
'@geekmidas/studio': '~1.0.1',
|
|
47
47
|
'@geekmidas/telescope': '~1.0.1',
|
|
48
|
-
'@geekmidas/testkit': '~
|
|
48
|
+
'@geekmidas/testkit': '~2.0.0',
|
|
49
49
|
'@geekmidas/cli': CLI_VERSION,
|
|
50
50
|
} as const;
|
|
51
51
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { readStageSecrets, secretsExist } from '../../secrets/storage.js';
|
|
7
|
+
import type { NormalizedWorkspace } from '../../workspace/types.js';
|
|
8
|
+
import { createFreshWorkspaceSecrets, ensureStageSecrets } from '../index.js';
|
|
9
|
+
|
|
10
|
+
function createWorkspace(
|
|
11
|
+
overrides: Partial<NormalizedWorkspace> = {},
|
|
12
|
+
): NormalizedWorkspace {
|
|
13
|
+
return {
|
|
14
|
+
name: 'test-project',
|
|
15
|
+
root: '/tmp/test-project',
|
|
16
|
+
apps: {
|
|
17
|
+
api: {
|
|
18
|
+
type: 'backend',
|
|
19
|
+
port: 3000,
|
|
20
|
+
root: '/tmp/test-project/apps/api',
|
|
21
|
+
packageName: '@test/api',
|
|
22
|
+
routes: './src/endpoints/**/*.ts',
|
|
23
|
+
dependencies: [],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
services: { db: true },
|
|
27
|
+
deploy: {},
|
|
28
|
+
shared: {},
|
|
29
|
+
secrets: {},
|
|
30
|
+
...overrides,
|
|
31
|
+
} as NormalizedWorkspace;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('createFreshWorkspaceSecrets', () => {
|
|
35
|
+
it('generates postgres credentials when db service is enabled', () => {
|
|
36
|
+
const secrets = createFreshWorkspaceSecrets(
|
|
37
|
+
'development',
|
|
38
|
+
createWorkspace(),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(secrets.stage).toBe('development');
|
|
42
|
+
expect(secrets.services.postgres).toBeDefined();
|
|
43
|
+
expect(secrets.services.postgres?.password).toBeTruthy();
|
|
44
|
+
expect(secrets.urls.DATABASE_URL).toContain('postgresql://');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('generates a randomized password each call', () => {
|
|
48
|
+
const a = createFreshWorkspaceSecrets('development', createWorkspace());
|
|
49
|
+
const b = createFreshWorkspaceSecrets('development', createWorkspace());
|
|
50
|
+
|
|
51
|
+
expect(a.services.postgres?.password).not.toBe(
|
|
52
|
+
b.services.postgres?.password,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('adds single-app custom secrets', () => {
|
|
57
|
+
const secrets = createFreshWorkspaceSecrets(
|
|
58
|
+
'development',
|
|
59
|
+
createWorkspace(),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(secrets.custom.NODE_ENV).toBe('development');
|
|
63
|
+
expect(secrets.custom.JWT_SECRET).toBeTruthy();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('ensureStageSecrets', () => {
|
|
68
|
+
let testDir: string;
|
|
69
|
+
const originalGkmConfigPath = process.env.GKM_CONFIG_PATH;
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
testDir = join(
|
|
73
|
+
tmpdir(),
|
|
74
|
+
`gkm-auto-setup-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
75
|
+
);
|
|
76
|
+
mkdirSync(testDir, { recursive: true });
|
|
77
|
+
|
|
78
|
+
// Minimal single-app workspace config with a db service.
|
|
79
|
+
writeFileSync(
|
|
80
|
+
join(testDir, 'gkm.config.ts'),
|
|
81
|
+
`import { defineWorkspace } from '@geekmidas/cli/config';
|
|
82
|
+
|
|
83
|
+
export default defineWorkspace({
|
|
84
|
+
name: 'test-workspace',
|
|
85
|
+
apps: {
|
|
86
|
+
api: {
|
|
87
|
+
type: 'backend',
|
|
88
|
+
path: 'apps/api',
|
|
89
|
+
port: 3001,
|
|
90
|
+
routes: './src/endpoints/**/*.ts',
|
|
91
|
+
envParser: './src/config/env#envParser',
|
|
92
|
+
logger: './src/config/logger#logger',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
services: { db: true },
|
|
96
|
+
});
|
|
97
|
+
`,
|
|
98
|
+
);
|
|
99
|
+
const apiDir = join(testDir, 'apps', 'api');
|
|
100
|
+
mkdirSync(apiDir, { recursive: true });
|
|
101
|
+
writeFileSync(
|
|
102
|
+
join(apiDir, 'package.json'),
|
|
103
|
+
JSON.stringify({ name: '@test/api', version: '0.0.1' }),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
process.env.GKM_CONFIG_PATH = join(testDir, 'gkm.config.ts');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterEach(async () => {
|
|
110
|
+
if (originalGkmConfigPath === undefined) {
|
|
111
|
+
delete process.env.GKM_CONFIG_PATH;
|
|
112
|
+
} else {
|
|
113
|
+
process.env.GKM_CONFIG_PATH = originalGkmConfigPath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await rm(testDir, { recursive: true, force: true });
|
|
117
|
+
// writeStageSecrets mints a key at ~/.gkm/{basename(root)}
|
|
118
|
+
await rm(join(homedir(), '.gkm', basename(testDir)), {
|
|
119
|
+
recursive: true,
|
|
120
|
+
force: true,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('generates a decryptable stage when none exists', async () => {
|
|
125
|
+
expect(secretsExist('development', testDir)).toBe(false);
|
|
126
|
+
|
|
127
|
+
const generated = await ensureStageSecrets('development', testDir);
|
|
128
|
+
|
|
129
|
+
expect(generated).toBe(true);
|
|
130
|
+
expect(secretsExist('development', testDir)).toBe(true);
|
|
131
|
+
|
|
132
|
+
// Round-trips through the keystore the same way `gkm test` reads it.
|
|
133
|
+
const read = await readStageSecrets('development', testDir);
|
|
134
|
+
expect(read?.services.postgres).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('is a no-op when secrets already exist', async () => {
|
|
138
|
+
await ensureStageSecrets('development', testDir);
|
|
139
|
+
const before = await readStageSecrets('development', testDir);
|
|
140
|
+
|
|
141
|
+
const generated = await ensureStageSecrets('development', testDir);
|
|
142
|
+
|
|
143
|
+
expect(generated).toBe(false);
|
|
144
|
+
const after = await readStageSecrets('development', testDir);
|
|
145
|
+
expect(after).toEqual(before);
|
|
146
|
+
});
|
|
147
|
+
});
|
package/src/setup/index.ts
CHANGED
|
@@ -288,13 +288,16 @@ export function reconcileSecrets(
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
/**
|
|
291
|
-
*
|
|
291
|
+
* Build a fresh StageSecrets object for a workspace: service credentials,
|
|
292
|
+
* connection URLs, and custom secrets. Pure — does not write to disk or touch
|
|
293
|
+
* SSM. Service passwords are freshly randomized, so the result must be the same
|
|
294
|
+
* one used to start the matching Docker containers.
|
|
295
|
+
* @internal Exported for reuse by `gkm test` auto-setup.
|
|
292
296
|
*/
|
|
293
|
-
|
|
297
|
+
export function createFreshWorkspaceSecrets(
|
|
294
298
|
stage: string,
|
|
295
299
|
workspace: NormalizedWorkspace,
|
|
296
|
-
|
|
297
|
-
) {
|
|
300
|
+
): StageSecrets {
|
|
298
301
|
// Determine services from workspace config
|
|
299
302
|
const serviceNames: ComposeServiceName[] = [];
|
|
300
303
|
if (workspace.services.db) serviceNames.push('postgres');
|
|
@@ -313,8 +316,7 @@ async function generateFreshSecrets(
|
|
|
313
316
|
// Generate fullstack-aware custom secrets
|
|
314
317
|
const isMultiApp = Object.keys(workspace.apps).length > 1;
|
|
315
318
|
if (isMultiApp) {
|
|
316
|
-
|
|
317
|
-
secrets.custom = customSecrets;
|
|
319
|
+
secrets.custom = generateFullstackCustomSecrets(workspace);
|
|
318
320
|
} else {
|
|
319
321
|
secrets.custom = {
|
|
320
322
|
NODE_ENV: 'development',
|
|
@@ -324,6 +326,44 @@ async function generateFreshSecrets(
|
|
|
324
326
|
};
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
return secrets;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Ensure a usable secret set exists for a stage, generating a fresh one from the
|
|
334
|
+
* workspace config when none is present. No-op if secrets already exist.
|
|
335
|
+
*
|
|
336
|
+
* Powers `gkm test --auto-setup` / `GKM_AUTO_SETUP`: CI can run without a
|
|
337
|
+
* committed secrets file or a shared encryption key — the stage is regenerated
|
|
338
|
+
* from the committed `gkm.config.ts`, and `writeStageSecrets` mints a local key.
|
|
339
|
+
*
|
|
340
|
+
* @returns true if fresh secrets were generated, false if existing ones were kept
|
|
341
|
+
*/
|
|
342
|
+
export async function ensureStageSecrets(
|
|
343
|
+
stage: string,
|
|
344
|
+
cwd: string = process.cwd(),
|
|
345
|
+
): Promise<boolean> {
|
|
346
|
+
const { workspace } = await loadWorkspaceConfig(cwd);
|
|
347
|
+
|
|
348
|
+
if (secretsExist(stage, workspace.root)) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const secrets = createFreshWorkspaceSecrets(stage, workspace);
|
|
353
|
+
await writeStageSecrets(secrets, workspace.root);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Generate fresh secrets for the workspace.
|
|
359
|
+
*/
|
|
360
|
+
async function generateFreshSecrets(
|
|
361
|
+
stage: string,
|
|
362
|
+
workspace: NormalizedWorkspace,
|
|
363
|
+
options: SetupOptions,
|
|
364
|
+
) {
|
|
365
|
+
const secrets = createFreshWorkspaceSecrets(stage, workspace);
|
|
366
|
+
|
|
327
367
|
// Write secrets
|
|
328
368
|
await writeStageSecrets(secrets, workspace.root);
|
|
329
369
|
logger.log(` Secrets written to .gkm/secrets/${stage}.json`);
|
package/src/test/index.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface TestOptions {
|
|
|
21
21
|
ui?: boolean;
|
|
22
22
|
/** Pattern to filter tests */
|
|
23
23
|
pattern?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Generate a fresh stage (secrets + encryption key) from the workspace config
|
|
26
|
+
* when none exists, instead of requiring committed secrets / a shared key.
|
|
27
|
+
* Intended for CI. Also enabled by the GKM_AUTO_SETUP env var.
|
|
28
|
+
*/
|
|
29
|
+
autoSetup?: boolean;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/**
|
|
@@ -33,6 +39,17 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
|
|
|
33
39
|
|
|
34
40
|
console.log(`\n🧪 Running tests with ${stage} environment...\n`);
|
|
35
41
|
|
|
42
|
+
// 0. Auto-setup (CI): regenerate a fresh stage from the workspace config when
|
|
43
|
+
// none exists, so tests run without committed secrets or a shared key.
|
|
44
|
+
const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
|
|
45
|
+
if (autoSetup) {
|
|
46
|
+
const { ensureStageSecrets } = await import('../setup/index.js');
|
|
47
|
+
const generated = await ensureStageSecrets(stage, cwd);
|
|
48
|
+
if (generated) {
|
|
49
|
+
console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
// 1. Load .env files
|
|
37
54
|
const defaultEnv = loadEnvFiles('.env');
|
|
38
55
|
if (defaultEnv.loaded.length > 0) {
|
package/src/types.ts
CHANGED
|
@@ -239,6 +239,8 @@ export interface GkmConfig {
|
|
|
239
239
|
functions?: Routes;
|
|
240
240
|
crons?: Routes;
|
|
241
241
|
subscribers?: Routes;
|
|
242
|
+
queues?: Routes;
|
|
243
|
+
topics?: Routes;
|
|
242
244
|
envParser: string;
|
|
243
245
|
logger: string;
|
|
244
246
|
providers?: ProvidersConfig;
|
|
@@ -347,54 +349,15 @@ export interface BuildResult {
|
|
|
347
349
|
masterKey?: string;
|
|
348
350
|
}
|
|
349
351
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
handler: string;
|
|
363
|
-
timeout?: number;
|
|
364
|
-
memorySize?: number;
|
|
365
|
-
environment?: string[];
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export interface CronInfo {
|
|
369
|
-
name: string;
|
|
370
|
-
handler: string;
|
|
371
|
-
schedule: string;
|
|
372
|
-
timeout?: number;
|
|
373
|
-
memorySize?: number;
|
|
374
|
-
environment?: string[];
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
export interface SubscriberInfo {
|
|
378
|
-
name: string;
|
|
379
|
-
handler: string;
|
|
380
|
-
subscribedEvents: string[];
|
|
381
|
-
timeout?: number;
|
|
382
|
-
memorySize?: number;
|
|
383
|
-
environment?: string[];
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export interface RoutesManifest {
|
|
387
|
-
routes: RouteInfo[];
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export interface FunctionsManifest {
|
|
391
|
-
functions: FunctionInfo[];
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export interface CronsManifest {
|
|
395
|
-
crons: CronInfo[];
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export interface SubscribersManifest {
|
|
399
|
-
subscribers: SubscriberInfo[];
|
|
400
|
-
}
|
|
352
|
+
// The deployment manifest types are a shared, dependency-free data contract
|
|
353
|
+
// in `@geekmidas/manifest`, re-exported here for back-compat with existing
|
|
354
|
+
// `@geekmidas/cli` imports.
|
|
355
|
+
export type {
|
|
356
|
+
CronInfo,
|
|
357
|
+
FunctionInfo,
|
|
358
|
+
Manifest,
|
|
359
|
+
QueueInfo,
|
|
360
|
+
RouteInfo,
|
|
361
|
+
SubscriberInfo,
|
|
362
|
+
TopicInfo,
|
|
363
|
+
} from '@geekmidas/manifest';
|