@friggframework/devtools 2.0.0-next.47 → 2.0.0-next.48
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/frigg-cli/README.md +1290 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +279 -0
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +548 -0
- package/frigg-cli/__tests__/unit/commands/deploy.test.js +320 -0
- package/frigg-cli/__tests__/unit/commands/doctor.test.js +309 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +400 -0
- package/frigg-cli/__tests__/unit/commands/ui.test.js +346 -0
- package/frigg-cli/__tests__/unit/dependencies.test.js +74 -0
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +366 -0
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +304 -0
- package/frigg-cli/__tests__/unit/version-detection.test.js +171 -0
- package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
- package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
- package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
- package/frigg-cli/__tests__/utils/test-setup.js +287 -0
- package/frigg-cli/build-command/index.js +66 -0
- package/frigg-cli/db-setup-command/index.js +193 -0
- package/frigg-cli/deploy-command/SPEC-DEPLOY-DRY-RUN.md +981 -0
- package/frigg-cli/deploy-command/index.js +302 -0
- package/frigg-cli/doctor-command/index.js +335 -0
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +301 -0
- package/frigg-cli/generate-command/azure-generator.js +43 -0
- package/frigg-cli/generate-command/gcp-generator.js +47 -0
- package/frigg-cli/generate-command/index.js +332 -0
- package/frigg-cli/generate-command/terraform-generator.js +555 -0
- package/frigg-cli/generate-iam-command.js +118 -0
- package/frigg-cli/index.js +173 -0
- package/frigg-cli/index.test.js +158 -0
- package/frigg-cli/init-command/backend-first-handler.js +756 -0
- package/frigg-cli/init-command/index.js +93 -0
- package/frigg-cli/init-command/template-handler.js +143 -0
- package/frigg-cli/install-command/backend-js.js +33 -0
- package/frigg-cli/install-command/commit-changes.js +16 -0
- package/frigg-cli/install-command/environment-variables.js +127 -0
- package/frigg-cli/install-command/environment-variables.test.js +136 -0
- package/frigg-cli/install-command/index.js +54 -0
- package/frigg-cli/install-command/install-package.js +13 -0
- package/frigg-cli/install-command/integration-file.js +30 -0
- package/frigg-cli/install-command/logger.js +12 -0
- package/frigg-cli/install-command/template.js +90 -0
- package/frigg-cli/install-command/validate-package.js +75 -0
- package/frigg-cli/jest.config.js +124 -0
- package/frigg-cli/package.json +63 -0
- package/frigg-cli/repair-command/index.js +564 -0
- package/frigg-cli/start-command/index.js +149 -0
- package/frigg-cli/start-command/start-command.test.js +297 -0
- package/frigg-cli/test/init-command.test.js +180 -0
- package/frigg-cli/test/npm-registry.test.js +319 -0
- package/frigg-cli/ui-command/index.js +154 -0
- package/frigg-cli/utils/app-resolver.js +319 -0
- package/frigg-cli/utils/backend-path.js +25 -0
- package/frigg-cli/utils/database-validator.js +154 -0
- package/frigg-cli/utils/error-messages.js +257 -0
- package/frigg-cli/utils/npm-registry.js +167 -0
- package/frigg-cli/utils/process-manager.js +199 -0
- package/frigg-cli/utils/repo-detection.js +405 -0
- package/infrastructure/create-frigg-infrastructure.js +125 -12
- package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
- package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
- package/infrastructure/domains/shared/resource-discovery.js +31 -2
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +1 -1
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +109 -5
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +310 -4
- package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
- package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
- package/infrastructure/infrastructure-composer.js +22 -0
- package/layers/prisma/.build-complete +3 -0
- package/package.json +18 -7
- package/management-ui/package-lock.json +0 -16517
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Discovery Configuration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for AppDefinition-level discovery control options
|
|
5
|
+
* addressing GitHub Issue #481 - Issue 5
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { shouldRunDiscovery, gatherDiscoveredResources } = require('./resource-discovery');
|
|
9
|
+
|
|
10
|
+
// Mock dependencies
|
|
11
|
+
jest.mock('./providers/provider-factory');
|
|
12
|
+
jest.mock('./cloudformation-discovery');
|
|
13
|
+
jest.mock('../networking/vpc-discovery');
|
|
14
|
+
jest.mock('../security/kms-discovery');
|
|
15
|
+
jest.mock('../database/aurora-discovery');
|
|
16
|
+
jest.mock('../parameters/ssm-discovery');
|
|
17
|
+
|
|
18
|
+
describe('AWS Discovery Configuration (Issue #481 - Issue 5)', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('shouldRunDiscovery - Priority Order', () => {
|
|
25
|
+
it('should use AppDefinition.aws.discovery.enabled when explicitly set to true', () => {
|
|
26
|
+
const appDefinition = {
|
|
27
|
+
aws: { discovery: { enabled: true } },
|
|
28
|
+
vpc: { enable: false }, // Would normally skip
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const result = shouldRunDiscovery(appDefinition);
|
|
32
|
+
|
|
33
|
+
expect(result).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should use AppDefinition.aws.discovery.enabled when explicitly set to false', () => {
|
|
37
|
+
const appDefinition = {
|
|
38
|
+
aws: { discovery: { enabled: false } },
|
|
39
|
+
vpc: { enable: true }, // Would normally run
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const result = shouldRunDiscovery(appDefinition);
|
|
43
|
+
|
|
44
|
+
expect(result).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should fall back to env var when AppDefinition not set', () => {
|
|
48
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
49
|
+
|
|
50
|
+
const appDefinition = {
|
|
51
|
+
vpc: { enable: true }, // Would normally run
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const result = shouldRunDiscovery(appDefinition);
|
|
55
|
+
|
|
56
|
+
expect(result).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should auto-detect when neither AppDefinition nor env var is set', () => {
|
|
60
|
+
const appDefinition = {
|
|
61
|
+
vpc: { enable: true },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = shouldRunDiscovery(appDefinition);
|
|
65
|
+
|
|
66
|
+
expect(result).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should prioritize AppDefinition over env var', () => {
|
|
70
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
71
|
+
|
|
72
|
+
const appDefinition = {
|
|
73
|
+
aws: { discovery: { enabled: true } }, // Explicit override
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const result = shouldRunDiscovery(appDefinition);
|
|
77
|
+
|
|
78
|
+
expect(result).toBe(true); // AppDefinition wins
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('AppDefinition.aws.discovery.enabled', () => {
|
|
83
|
+
it('should log when using AppDefinition configuration', () => {
|
|
84
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
85
|
+
|
|
86
|
+
const appDefinition = {
|
|
87
|
+
aws: { discovery: { enabled: true } },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
shouldRunDiscovery(appDefinition);
|
|
91
|
+
|
|
92
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
93
|
+
expect.stringContaining('AppDefinition.aws.discovery.enabled: true')
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
consoleSpy.mockRestore();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle explicit false value', () => {
|
|
100
|
+
const appDefinition = {
|
|
101
|
+
aws: { discovery: { enabled: false } },
|
|
102
|
+
vpc: { enable: true },
|
|
103
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const result = shouldRunDiscovery(appDefinition);
|
|
107
|
+
|
|
108
|
+
expect(result).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle undefined correctly (fall through to next priority)', () => {
|
|
112
|
+
const appDefinition = {
|
|
113
|
+
aws: { discovery: { enabled: undefined } },
|
|
114
|
+
vpc: { enable: true },
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const result = shouldRunDiscovery(appDefinition);
|
|
118
|
+
|
|
119
|
+
// Should skip when undefined (fall through to env var check)
|
|
120
|
+
expect(result).toBe(true); // Auto-detect kicks in
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('Auto-detection based on features', () => {
|
|
125
|
+
it('should run discovery when VPC is enabled', () => {
|
|
126
|
+
const appDefinition = {
|
|
127
|
+
vpc: { enable: true },
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
expect(shouldRunDiscovery(appDefinition)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should run discovery when KMS encryption is enabled', () => {
|
|
134
|
+
const appDefinition = {
|
|
135
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
expect(shouldRunDiscovery(appDefinition)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should run discovery when SSM is enabled', () => {
|
|
142
|
+
const appDefinition = {
|
|
143
|
+
ssm: { enable: true },
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
expect(shouldRunDiscovery(appDefinition)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should run discovery when PostgreSQL is enabled', () => {
|
|
150
|
+
const appDefinition = {
|
|
151
|
+
database: { postgres: { enable: true } },
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
expect(shouldRunDiscovery(appDefinition)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should not run discovery when no features are enabled', () => {
|
|
158
|
+
const appDefinition = {
|
|
159
|
+
vpc: { enable: false },
|
|
160
|
+
encryption: { fieldLevelEncryptionMethod: 'aes' },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
expect(shouldRunDiscovery(appDefinition)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('gatherDiscoveredResources - failOnError behavior', () => {
|
|
168
|
+
const { CloudProviderFactory } = require('./providers/provider-factory');
|
|
169
|
+
const { CloudFormationDiscovery } = require('./cloudformation-discovery');
|
|
170
|
+
|
|
171
|
+
beforeEach(() => {
|
|
172
|
+
const mockProvider = {
|
|
173
|
+
getVpcs: jest.fn(),
|
|
174
|
+
getKmsKeys: jest.fn(),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
CloudProviderFactory.create = jest.fn().mockReturnValue(mockProvider);
|
|
178
|
+
|
|
179
|
+
// Mock CloudFormation discovery to throw error
|
|
180
|
+
CloudFormationDiscovery.mockImplementation(() => ({
|
|
181
|
+
discoverFromStack: jest.fn().mockRejectedValue(
|
|
182
|
+
new Error('User is not authorized to perform: ec2:DescribeVpcs')
|
|
183
|
+
),
|
|
184
|
+
}));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should throw error when failOnError is true', async () => {
|
|
188
|
+
const appDefinition = {
|
|
189
|
+
name: 'test-app',
|
|
190
|
+
vpc: { enable: true },
|
|
191
|
+
aws: {
|
|
192
|
+
discovery: {
|
|
193
|
+
enabled: true,
|
|
194
|
+
failOnError: true,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await expect(gatherDiscoveredResources(appDefinition)).rejects.toThrow(
|
|
200
|
+
'User is not authorized'
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should return empty object when failOnError is false', async () => {
|
|
205
|
+
const appDefinition = {
|
|
206
|
+
name: 'test-app',
|
|
207
|
+
vpc: { enable: true },
|
|
208
|
+
aws: {
|
|
209
|
+
discovery: {
|
|
210
|
+
enabled: true,
|
|
211
|
+
failOnError: false,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const result = await gatherDiscoveredResources(appDefinition);
|
|
217
|
+
|
|
218
|
+
expect(result).toEqual({});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should default to false when failOnError is not set', async () => {
|
|
222
|
+
const appDefinition = {
|
|
223
|
+
name: 'test-app',
|
|
224
|
+
vpc: { enable: true },
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const result = await gatherDiscoveredResources(appDefinition);
|
|
228
|
+
|
|
229
|
+
expect(result).toEqual({});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should log helpful message when failing gracefully', async () => {
|
|
233
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
234
|
+
|
|
235
|
+
const appDefinition = {
|
|
236
|
+
name: 'test-app',
|
|
237
|
+
vpc: { enable: true },
|
|
238
|
+
aws: { discovery: { failOnError: false } },
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
await gatherDiscoveredResources(appDefinition);
|
|
242
|
+
|
|
243
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
244
|
+
expect.stringContaining('Set aws.discovery.failOnError = true')
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
consoleWarnSpy.mockRestore();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('Real-world scenarios', () => {
|
|
252
|
+
it('should handle restrictive IAM with explicit disable', () => {
|
|
253
|
+
const appDefinition = {
|
|
254
|
+
vpc: { enable: true },
|
|
255
|
+
aws: {
|
|
256
|
+
discovery: {
|
|
257
|
+
enabled: false, // Explicit disable for restrictive IAM
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const result = shouldRunDiscovery(appDefinition);
|
|
263
|
+
|
|
264
|
+
expect(result).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should allow strict mode for production deployments', async () => {
|
|
268
|
+
const { CloudProviderFactory } = require('./providers/provider-factory');
|
|
269
|
+
const { CloudFormationDiscovery } = require('./cloudformation-discovery');
|
|
270
|
+
|
|
271
|
+
CloudFormationDiscovery.mockImplementation(() => ({
|
|
272
|
+
discoverFromStack: jest.fn().mockRejectedValue(new Error('IAM error')),
|
|
273
|
+
}));
|
|
274
|
+
|
|
275
|
+
const appDefinition = {
|
|
276
|
+
name: 'prod-app',
|
|
277
|
+
vpc: { enable: true },
|
|
278
|
+
aws: {
|
|
279
|
+
discovery: {
|
|
280
|
+
enabled: true,
|
|
281
|
+
failOnError: true, // Strict mode for production
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
await expect(gatherDiscoveredResources(appDefinition)).rejects.toThrow();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should allow graceful degradation for dev environments', async () => {
|
|
290
|
+
const appDefinition = {
|
|
291
|
+
name: 'dev-app',
|
|
292
|
+
vpc: { enable: true },
|
|
293
|
+
aws: {
|
|
294
|
+
discovery: {
|
|
295
|
+
enabled: true,
|
|
296
|
+
failOnError: false, // Graceful for dev
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const result = await gatherDiscoveredResources(appDefinition);
|
|
302
|
+
|
|
303
|
+
expect(result).toEqual({});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -18,11 +18,26 @@ const { SsmDiscovery } = require('../parameters/ssm-discovery');
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Determine if AWS discovery should run
|
|
21
|
-
*
|
|
21
|
+
*
|
|
22
|
+
* Checks in priority order:
|
|
23
|
+
* 1. AppDefinition.aws.discovery.enabled (explicit opt-in/out)
|
|
24
|
+
* 2. FRIGG_SKIP_AWS_DISCOVERY environment variable
|
|
25
|
+
* 3. Auto-detection based on features enabled (VPC, KMS, SSM, PostgreSQL)
|
|
26
|
+
*
|
|
22
27
|
* @param {Object} appDefinition - Application definition
|
|
23
28
|
* @returns {boolean} True if discovery is needed
|
|
24
29
|
*/
|
|
25
30
|
function shouldRunDiscovery(appDefinition) {
|
|
31
|
+
// Priority 1: Check AppDefinition-level configuration
|
|
32
|
+
if (appDefinition.aws?.discovery?.enabled !== undefined) {
|
|
33
|
+
const enabled = appDefinition.aws.discovery.enabled;
|
|
34
|
+
console.log(
|
|
35
|
+
`⚙️ Using AppDefinition.aws.discovery.enabled: ${enabled}`
|
|
36
|
+
);
|
|
37
|
+
return enabled;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Priority 2: Check environment variable
|
|
26
41
|
console.log(
|
|
27
42
|
'⚙️ Checking FRIGG_SKIP_AWS_DISCOVERY:',
|
|
28
43
|
process.env.FRIGG_SKIP_AWS_DISCOVERY
|
|
@@ -35,6 +50,7 @@ function shouldRunDiscovery(appDefinition) {
|
|
|
35
50
|
return false;
|
|
36
51
|
}
|
|
37
52
|
|
|
53
|
+
// Priority 3: Auto-detect based on enabled features
|
|
38
54
|
return (
|
|
39
55
|
appDefinition.vpc?.enable === true ||
|
|
40
56
|
appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
|
|
@@ -177,10 +193,23 @@ async function gatherDiscoveredResources(appDefinition) {
|
|
|
177
193
|
console.error('❌ Cloud resource discovery failed:', error.message);
|
|
178
194
|
console.error('Stack:', error.stack);
|
|
179
195
|
|
|
180
|
-
//
|
|
196
|
+
// Check if discovery failures should fail the deployment
|
|
197
|
+
const failOnError = appDefinition.aws?.discovery?.failOnError ?? false;
|
|
198
|
+
|
|
199
|
+
if (failOnError) {
|
|
200
|
+
console.error(
|
|
201
|
+
'❌ Discovery failure blocking deployment (aws.discovery.failOnError = true)'
|
|
202
|
+
);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Graceful degradation - return empty resources and let validation handle it
|
|
181
207
|
console.warn(
|
|
182
208
|
'⚠️ Continuing with empty discovered resources. This may cause deployment issues if resources are required.'
|
|
183
209
|
);
|
|
210
|
+
console.warn(
|
|
211
|
+
'💡 Set aws.discovery.failOnError = true in AppDefinition to fail on discovery errors.'
|
|
212
|
+
);
|
|
184
213
|
return {};
|
|
185
214
|
}
|
|
186
215
|
}
|
|
@@ -224,7 +224,7 @@ function createBaseDefinition(
|
|
|
224
224
|
],
|
|
225
225
|
packager: 'npm',
|
|
226
226
|
keepNames: true,
|
|
227
|
-
keepOutputDirectory:
|
|
227
|
+
keepOutputDirectory: true, // Keep .esbuild directory to prevent ENOENT errors during packaging
|
|
228
228
|
exclude: [
|
|
229
229
|
'aws-sdk',
|
|
230
230
|
'@aws-sdk/*',
|
|
@@ -11,13 +11,34 @@ const path = require('path');
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Check if a process is still running
|
|
16
|
+
* @param {number} pid - Process ID to check
|
|
17
|
+
* @returns {boolean} True if process is running
|
|
18
|
+
*/
|
|
19
|
+
function isProcessRunning(pid) {
|
|
20
|
+
try {
|
|
21
|
+
// Signal 0 checks if process exists without killing it
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
/**
|
|
15
30
|
* Ensure Prisma Lambda Layer exists
|
|
16
|
-
*
|
|
31
|
+
*
|
|
17
32
|
* Automatically builds the layer if it doesn't exist.
|
|
18
33
|
* The layer contains ONLY the Prisma runtime client (minimal, ~10-15MB).
|
|
19
34
|
* Prisma CLI is bundled separately in the dbMigrate function.
|
|
20
|
-
*
|
|
35
|
+
*
|
|
36
|
+
* Domain Concept: Build Completion State & Process Locking
|
|
37
|
+
* - Uses .build-complete marker file to track successful builds
|
|
38
|
+
* - Uses .build-lock PID file to prevent concurrent builds
|
|
39
|
+
* - Waits for active builds to complete before starting new one
|
|
40
|
+
* - Cleans stale locks and incomplete builds before retry
|
|
41
|
+
*
|
|
21
42
|
* @param {Object} databaseConfig - Database configuration from app definition
|
|
22
43
|
* @returns {Promise<void>}
|
|
23
44
|
* @throws {Error} If layer build fails
|
|
@@ -25,27 +46,110 @@ const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
|
|
|
25
46
|
async function ensurePrismaLayerExists(databaseConfig = {}) {
|
|
26
47
|
const projectRoot = process.cwd();
|
|
27
48
|
const layerPath = path.join(projectRoot, 'layers/prisma');
|
|
49
|
+
const completionMarkerPath = path.join(layerPath, '.build-complete');
|
|
50
|
+
const lockFilePath = path.join(layerPath, '.build-lock');
|
|
28
51
|
|
|
29
|
-
// Check if
|
|
30
|
-
if (fs.existsSync(
|
|
52
|
+
// Check if build is complete (marker exists)
|
|
53
|
+
if (fs.existsSync(completionMarkerPath)) {
|
|
31
54
|
console.log('✓ Prisma Lambda Layer already exists at', layerPath);
|
|
32
55
|
return;
|
|
33
56
|
}
|
|
34
57
|
|
|
35
|
-
//
|
|
58
|
+
// Check for active build process
|
|
59
|
+
if (fs.existsSync(lockFilePath)) {
|
|
60
|
+
const lockPid = parseInt(fs.readFileSync(lockFilePath, 'utf-8').trim(), 10);
|
|
61
|
+
|
|
62
|
+
if (isProcessRunning(lockPid)) {
|
|
63
|
+
console.log(`⏳ Another build process (PID ${lockPid}) is active - waiting...`);
|
|
64
|
+
|
|
65
|
+
// Wait up to 60 seconds for the other process to complete
|
|
66
|
+
for (let i = 0; i < 60; i++) {
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
68
|
+
|
|
69
|
+
// Check if build completed
|
|
70
|
+
if (fs.existsSync(completionMarkerPath)) {
|
|
71
|
+
console.log('✓ Concurrent build completed successfully');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if process died
|
|
76
|
+
if (!isProcessRunning(lockPid)) {
|
|
77
|
+
console.log(`⚠ Build process ${lockPid} terminated - cleaning up stale lock`);
|
|
78
|
+
fs.rmSync(lockFilePath, { force: true });
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Timeout - check one final time
|
|
84
|
+
if (fs.existsSync(completionMarkerPath)) {
|
|
85
|
+
console.log('✓ Concurrent build completed');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Still locked after 60s - remove stale lock
|
|
90
|
+
console.log('⚠ Build timeout - removing stale lock and rebuilding');
|
|
91
|
+
fs.rmSync(lockFilePath, { force: true });
|
|
92
|
+
} else {
|
|
93
|
+
// Stale lock file (process not running)
|
|
94
|
+
console.log(`⚠ Stale lock file detected (PID ${lockPid} not running) - cleaning up`);
|
|
95
|
+
fs.rmSync(lockFilePath, { force: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check if incomplete build exists (directory without marker)
|
|
100
|
+
if (fs.existsSync(layerPath) && !fs.existsSync(completionMarkerPath)) {
|
|
101
|
+
console.log('⚠ Incomplete Prisma layer detected - will be cleaned by buildPrismaLayer()');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Build layer
|
|
36
105
|
console.log('📦 Prisma Lambda Layer not found - building automatically...');
|
|
37
106
|
console.log(' Building MINIMAL layer (runtime only, NO CLI)');
|
|
38
107
|
console.log(' CLI is packaged separately in dbMigrate function');
|
|
39
108
|
console.log(' This may take a minute on first deployment.\n');
|
|
40
109
|
|
|
110
|
+
// Create lock file with current process PID
|
|
111
|
+
try {
|
|
112
|
+
if (!fs.existsSync(layerPath)) {
|
|
113
|
+
fs.mkdirSync(layerPath, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
fs.writeFileSync(lockFilePath, process.pid.toString());
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.warn('⚠ Could not create lock file:', error.message);
|
|
118
|
+
}
|
|
119
|
+
|
|
41
120
|
try {
|
|
42
121
|
// Build layer WITHOUT CLI (runtime only for minimal size)
|
|
43
122
|
await buildPrismaLayer(databaseConfig);
|
|
123
|
+
|
|
124
|
+
// Create completion marker
|
|
125
|
+
fs.writeFileSync(
|
|
126
|
+
completionMarkerPath,
|
|
127
|
+
`Build completed: ${new Date().toISOString()}\nNode: ${process.version}\nPlatform: ${process.platform}\n`
|
|
128
|
+
);
|
|
129
|
+
|
|
44
130
|
console.log('✓ Prisma Lambda Layer built successfully (~10-15MB)\n');
|
|
45
131
|
} catch (error) {
|
|
132
|
+
// Clean up partial build on failure
|
|
133
|
+
if (fs.existsSync(layerPath)) {
|
|
134
|
+
try {
|
|
135
|
+
fs.rmSync(layerPath, { recursive: true, force: true });
|
|
136
|
+
} catch (cleanupError) {
|
|
137
|
+
console.warn('⚠ Could not clean failed build:', cleanupError.message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
46
141
|
console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
|
|
47
142
|
console.error(' You may need to run: npm install @friggframework/core\n');
|
|
48
143
|
throw error;
|
|
144
|
+
} finally {
|
|
145
|
+
// Always remove lock file when done (success or failure)
|
|
146
|
+
if (fs.existsSync(lockFilePath)) {
|
|
147
|
+
try {
|
|
148
|
+
fs.rmSync(lockFilePath, { force: true });
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.warn('⚠ Could not remove lock file:', error.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
49
153
|
}
|
|
50
154
|
}
|
|
51
155
|
|