@fjall/deploy-core 0.94.0 → 0.95.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 (54) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/organisations/accounts.js +1 -99
  3. package/dist/src/aws/organisations/backup.js +1 -30
  4. package/dist/src/aws/organisations/costAllocation.js +1 -28
  5. package/dist/src/aws/organisations/delegatedAdmin.js +3 -43
  6. package/dist/src/aws/organisations/identityCentre.js +1 -23
  7. package/dist/src/aws/organisations/ipam.js +1 -20
  8. package/dist/src/aws/organisations/organisation.js +1 -103
  9. package/dist/src/aws/organisations/organisationalUnits.js +1 -239
  10. package/dist/src/aws/organisations/policies.js +1 -37
  11. package/dist/src/aws/organisations/ram.js +1 -19
  12. package/dist/src/aws/organisations/serviceAccess.js +1 -44
  13. package/dist/src/aws/organisations/trustedAccess.js +1 -19
  14. package/dist/src/aws/utils/regions.js +1 -1
  15. package/dist/src/index.js +1 -65
  16. package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +1 -78
  17. package/dist/src/orchestration/activeDeploymentGuard.js +5 -39
  18. package/dist/src/orchestration/applicationDeploy.js +1 -149
  19. package/dist/src/orchestration/applicationDeployHelpers.js +4 -223
  20. package/dist/src/orchestration/applicationDestroy.js +1 -131
  21. package/dist/src/orchestration/builders/dockerBuilder.js +1 -98
  22. package/dist/src/orchestration/builders/openNextBuilder.js +1 -144
  23. package/dist/src/orchestration/cascadeHelpers.js +1 -160
  24. package/dist/src/orchestration/contextHelpers.js +1 -107
  25. package/dist/src/orchestration/deploy.js +1 -42
  26. package/dist/src/orchestration/destroy.js +1 -67
  27. package/dist/src/orchestration/detectionPipeline.js +1 -84
  28. package/dist/src/orchestration/dockerBuildHelper.js +1 -49
  29. package/dist/src/orchestration/dockerInterface.js +0 -1
  30. package/dist/src/orchestration/domainInterface.js +0 -1
  31. package/dist/src/orchestration/openNextBuild.js +3 -243
  32. package/dist/src/orchestration/organisationDeploy.js +3 -284
  33. package/dist/src/orchestration/organisationDestroy.js +3 -189
  34. package/dist/src/orchestration/organisationSetup.js +1 -247
  35. package/dist/src/orchestration/resolveOperation.js +1 -123
  36. package/dist/src/orchestration/welcomeImageHelper.js +1 -64
  37. package/dist/src/services/application/ApplicationStackService.js +1 -218
  38. package/dist/src/services/application/applicationStackHelpers.js +4 -248
  39. package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -244
  40. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -125
  41. package/dist/src/services/infrastructure/CdkProcessManager.js +3 -278
  42. package/dist/src/services/infrastructure/CdkService.js +3 -213
  43. package/dist/src/services/infrastructure/CloudFormationService.js +1 -248
  44. package/dist/src/services/infrastructure/ICdkProcessManager.js +0 -1
  45. package/dist/src/services/supporting/CdkContextBuilder.js +1 -44
  46. package/dist/src/services/supporting/TemplateHashService.js +1 -152
  47. package/dist/src/steps/stepRegistry.js +1 -505
  48. package/dist/src/types/apiClient.js +0 -1
  49. package/dist/src/types/detection.js +0 -1
  50. package/dist/src/types/frameworkBuilder.js +0 -8
  51. package/dist/src/types/params.js +0 -1
  52. package/dist/src/types/patternDetection.js +1 -88
  53. package/dist/src/types/stepDefinitions.js +1 -98
  54. package/package.json +4 -4
@@ -1,244 +1,2 @@
1
- import { tmpdir } from "os";
2
- import { logger } from "@fjall/util/logger";
3
- import { success, failure } from "@fjall/generator";
4
- import { CdkError } from "../../types/errors/CdkError.js";
5
- import { DEFAULT_REGION } from "../../aws/utils/regions.js";
6
- import { isCdkError, formatInfrastructureError } from "./CdkErrorFormatter.js";
7
- import { hasCdkDifferences, parseDiffOutput } from "./CdkOutputParser.js";
8
- import { DEFAULT_DEPLOY_TIMEOUT_MS } from "./CdkEventMonitoring.js";
9
- import { analyseBootstrapError } from "./CdkOutputAnalyser.js";
10
- /** Synth is CPU-bound template generation — 5 minutes is generous */
11
- export const SYNTH_TIMEOUT_MS = 300_000;
12
- /** Bootstrap creates a single stack with a handful of resources */
13
- export const BOOTSTRAP_TIMEOUT_MS = 180_000;
14
- /** Import maps existing resources into CDK state — slower than deploy due to resource lookup */
15
- export const IMPORT_TIMEOUT_MS = 1_200_000;
16
- /** CDK flags used across multiple commands */
17
- const CDK_FLAGS = Object.freeze({
18
- APP: "--app",
19
- CI: "--ci",
20
- REQUIRE_APPROVAL: "--require-approval",
21
- APPROVAL_NEVER: "never",
22
- VERBOSE: "--verbose",
23
- NO_LOOKUPS: "--no-lookups",
24
- ALL: "--all"
25
- });
26
- /**
27
- * Runs low-level CDK CLI commands (synth, deploy, destroy, diff, bootstrap, import).
28
- * Delegates process management to CdkProcessManager (or any ICdkProcessManager).
29
- */
30
- export class CdkCommandRunner {
31
- processManager;
32
- constructor(processManager) {
33
- this.processManager = processManager;
34
- }
35
- dispose() {
36
- this.processManager.dispose();
37
- }
38
- async checkDifferences(path, stackName, options) {
39
- const args = ["diff"];
40
- if (stackName) {
41
- args.push(stackName);
42
- }
43
- else {
44
- args.push(CDK_FLAGS.ALL);
45
- }
46
- // Add no-colour flag for cleaner parsing
47
- args.push("--no-color");
48
- if (options?.noLookups) {
49
- args.push(CDK_FLAGS.NO_LOOKUPS);
50
- }
51
- const result = await this.processManager.runCdkCommand(path, args, {
52
- ...options,
53
- // Diff command returns non-zero if differences exist
54
- ignoreExitCode: true,
55
- // Diff output includes stderr (CDK writes errors there)
56
- combineOutput: true
57
- });
58
- if (!result.success) {
59
- return failure(new CdkError(formatInfrastructureError(result.error, path), "diff_failed", undefined, undefined, result.error, undefined, false));
60
- }
61
- const output = result.data.output || "";
62
- // Check if the output indicates an error rather than a successful diff
63
- if (isCdkError(output)) {
64
- return failure(new CdkError(formatInfrastructureError(output, path), "diff_failed", undefined, undefined, output, undefined, false));
65
- }
66
- const hasDiffs = hasCdkDifferences(output);
67
- const details = parseDiffOutput(output);
68
- return success({
69
- hasDifferences: hasDiffs,
70
- output,
71
- details
72
- });
73
- }
74
- async deploy(path, stackName, options) {
75
- const args = ["deploy"];
76
- if (options?.appDir) {
77
- args.push(CDK_FLAGS.APP, options.appDir);
78
- }
79
- else if (options?.useCdkOut) {
80
- args.push(CDK_FLAGS.APP, "cdk.out");
81
- }
82
- if (stackName) {
83
- args.push(stackName);
84
- }
85
- else {
86
- args.push(CDK_FLAGS.ALL);
87
- }
88
- // Never require approval in programmatic mode
89
- args.push(CDK_FLAGS.REQUIRE_APPROVAL, CDK_FLAGS.APPROVAL_NEVER);
90
- args.push(CDK_FLAGS.CI);
91
- // Disable version reporting and metadata for cleaner output
92
- args.push("--no-version-reporting");
93
- args.push("--no-path-metadata");
94
- args.push("--no-asset-metadata");
95
- if (options?.verbose) {
96
- args.push(CDK_FLAGS.VERBOSE);
97
- }
98
- else {
99
- args.push("--progress", "events");
100
- }
101
- const deployOptions = {
102
- ...options,
103
- timeout: options?.timeout || DEFAULT_DEPLOY_TIMEOUT_MS
104
- };
105
- if (options?.passThroughCDK) {
106
- return this.processManager.runCdkCommandPassthrough(path, args, deployOptions);
107
- }
108
- return this.processManager.runCdkCommand(path, args, deployOptions);
109
- }
110
- async destroy(path, stackName, options) {
111
- const args = ["destroy"];
112
- if (options?.appDir) {
113
- args.push(CDK_FLAGS.APP, options.appDir);
114
- }
115
- else if (options?.useCdkOut) {
116
- args.push(CDK_FLAGS.APP, "cdk.out");
117
- }
118
- if (stackName) {
119
- args.push(stackName);
120
- }
121
- else {
122
- args.push(CDK_FLAGS.ALL);
123
- }
124
- args.push("--force");
125
- if (options?.verbose) {
126
- args.push(CDK_FLAGS.VERBOSE);
127
- }
128
- const destroyOptions = {
129
- ...options,
130
- timeout: options?.timeout || DEFAULT_DEPLOY_TIMEOUT_MS
131
- };
132
- return this.processManager.runCdkCommand(path, args, destroyOptions);
133
- }
134
- /**
135
- * Import existing AWS resources into CDK stack
136
- *
137
- * NOTE: This method is fully functional but currently unreachable as import
138
- * functionality is temporarily disabled at the AST layer (astCodeModification.ts).
139
- * When import is re-enabled, this method will work without changes.
140
- */
141
- async runImport(path, resourceMappingFile, options) {
142
- const args = ["import"];
143
- if (options?.stacks && options.stacks.length > 0) {
144
- args.push(...options.stacks);
145
- }
146
- if (resourceMappingFile) {
147
- args.push("--resource-mapping", resourceMappingFile);
148
- }
149
- args.push(CDK_FLAGS.REQUIRE_APPROVAL, CDK_FLAGS.APPROVAL_NEVER);
150
- args.push(CDK_FLAGS.CI);
151
- if (options?.noLookups) {
152
- args.push(CDK_FLAGS.NO_LOOKUPS);
153
- }
154
- if (options?.verbose) {
155
- args.push(CDK_FLAGS.VERBOSE);
156
- args.push("--trace");
157
- args.push("--debug");
158
- }
159
- const importOptions = {
160
- ...options,
161
- timeout: options?.timeout || IMPORT_TIMEOUT_MS
162
- };
163
- if (options?.passThroughCDK) {
164
- return this.processManager.runCdkCommandPassthrough(path, args, importOptions);
165
- }
166
- return this.processManager.runCdkCommand(path, args, importOptions);
167
- }
168
- async synth(path, options) {
169
- options?.outputCallback?.("Synthesising CloudFormation template...\n");
170
- const lookupArgs = options?.noLookups
171
- ? [CDK_FLAGS.NO_LOOKUPS]
172
- : [];
173
- const outputArgs = options?.outputDir
174
- ? ["--output", options.outputDir]
175
- : [];
176
- const region = options?.context?.region || DEFAULT_REGION;
177
- const result = await this.processManager.runCdkCommand(path, ["synth", ...lookupArgs, ...outputArgs, CDK_FLAGS.CI, "--quiet"], {
178
- ...options,
179
- context: { ...options?.context, region },
180
- timeout: options?.timeout || SYNTH_TIMEOUT_MS
181
- });
182
- if (!result.success) {
183
- const formatted = result.error
184
- ? formatInfrastructureError(result.error, path)
185
- : "Failed to synthesise CloudFormation template";
186
- return failure(formatted);
187
- }
188
- return result;
189
- }
190
- async bootstrap(accountId, region, options) {
191
- const bootstrapPath = tmpdir();
192
- logger.debug("CdkService", "Starting CDK bootstrap", {
193
- accountId,
194
- region,
195
- bootstrapPath,
196
- target: `aws://${accountId}/${region}`
197
- });
198
- const result = await this.processManager.runCdkCommand(bootstrapPath, [
199
- "bootstrap",
200
- `aws://${accountId}/${region}`,
201
- "--cloudformation-execution-policies",
202
- "arn:aws:iam::aws:policy/AdministratorAccess",
203
- "-c",
204
- `accountId=${accountId}`,
205
- CDK_FLAGS.REQUIRE_APPROVAL,
206
- CDK_FLAGS.APPROVAL_NEVER,
207
- CDK_FLAGS.CI,
208
- "--quiet",
209
- "--force"
210
- ], {
211
- ...options,
212
- timeout: options?.timeout || BOOTSTRAP_TIMEOUT_MS,
213
- context: { region, accountId },
214
- credentials: options?.credentials,
215
- skipProjectCheck: true,
216
- extraEnv: {
217
- TERM: "dumb",
218
- CDK_DISABLE_NOTICES: "true",
219
- CDK_DISABLE_PROGRESS_BAR: "true"
220
- }
221
- });
222
- logger.debug("CdkService", result.success
223
- ? "Bootstrap completed successfully"
224
- : "Bootstrap exited with non-zero code", {
225
- accountId,
226
- region,
227
- exitCode: result.success ? result.data.exitCode : undefined,
228
- output: result.success
229
- ? result.data.output?.trim() || "(empty)"
230
- : "(empty)",
231
- error: result.success ? "(empty)" : result.error.trim() || "(empty)"
232
- });
233
- if (!result.success) {
234
- if (result.error.includes("already bootstrapped")) {
235
- return success({
236
- output: "Environment is already bootstrapped",
237
- exitCode: 0
238
- });
239
- }
240
- return failure(analyseBootstrapError(result.error));
241
- }
242
- return success({ output: "AWS environment bootstrapped", exitCode: 0 });
243
- }
244
- }
1
+ import{tmpdir as C}from"os";import{logger as d}from"@fjall/util/logger";import{success as p,failure as n}from"@fjall/generator";import{CdkError as m}from"../../types/errors/CdkError.js";import{DEFAULT_REGION as l}from"../../aws/utils/regions.js";import{isCdkError as E,formatInfrastructureError as c}from"./CdkErrorFormatter.js";import{hasCdkDifferences as O,parseDiffOutput as P}from"./CdkOutputParser.js";import{DEFAULT_DEPLOY_TIMEOUT_MS as f}from"./CdkEventMonitoring.js";import{analyseBootstrapError as A}from"./CdkOutputAnalyser.js";const _=3e5,R=18e4,g=12e5,u=Object.freeze({APP:"--app",CI:"--ci",REQUIRE_APPROVAL:"--require-approval",APPROVAL_NEVER:"never",VERBOSE:"--verbose",NO_LOOKUPS:"--no-lookups",ALL:"--all"});class V{processManager;constructor(o){this.processManager=o}dispose(){this.processManager.dispose()}async checkDifferences(o,s,r){const e=["diff"];s?e.push(s):e.push(u.ALL),e.push("--no-color"),r?.noLookups&&e.push(u.NO_LOOKUPS);const t=await this.processManager.runCdkCommand(o,e,{...r,ignoreExitCode:!0,combineOutput:!0});if(!t.success)return n(new m(c(t.error,o),"diff_failed",void 0,void 0,t.error,void 0,!1));const a=t.data.output||"";if(E(a))return n(new m(c(a,o),"diff_failed",void 0,void 0,a,void 0,!1));const i=O(a),h=P(a);return p({hasDifferences:i,output:a,details:h})}async deploy(o,s,r){const e=["deploy"];r?.appDir?e.push(u.APP,r.appDir):r?.useCdkOut&&e.push(u.APP,"cdk.out"),s?e.push(s):e.push(u.ALL),e.push(u.REQUIRE_APPROVAL,u.APPROVAL_NEVER),e.push(u.CI),e.push("--no-version-reporting"),e.push("--no-path-metadata"),e.push("--no-asset-metadata"),r?.verbose?e.push(u.VERBOSE):e.push("--progress","events");const t={...r,timeout:r?.timeout||f};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(o,e,t):this.processManager.runCdkCommand(o,e,t)}async destroy(o,s,r){const e=["destroy"];r?.appDir?e.push(u.APP,r.appDir):r?.useCdkOut&&e.push(u.APP,"cdk.out"),s?e.push(s):e.push(u.ALL),e.push("--force"),r?.verbose&&e.push(u.VERBOSE);const t={...r,timeout:r?.timeout||f};return this.processManager.runCdkCommand(o,e,t)}async runImport(o,s,r){const e=["import"];r?.stacks&&r.stacks.length>0&&e.push(...r.stacks),s&&e.push("--resource-mapping",s),e.push(u.REQUIRE_APPROVAL,u.APPROVAL_NEVER),e.push(u.CI),r?.noLookups&&e.push(u.NO_LOOKUPS),r?.verbose&&(e.push(u.VERBOSE),e.push("--trace"),e.push("--debug"));const t={...r,timeout:r?.timeout||g};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(o,e,t):this.processManager.runCdkCommand(o,e,t)}async synth(o,s){s?.outputCallback?.(`Synthesising CloudFormation template...
2
+ `);const r=s?.noLookups?[u.NO_LOOKUPS]:[],e=s?.outputDir?["--output",s.outputDir]:[],t=s?.context?.region||l,a=await this.processManager.runCdkCommand(o,["synth",...r,...e,u.CI,"--quiet"],{...s,context:{...s?.context,region:t},timeout:s?.timeout||_});if(!a.success){const i=a.error?c(a.error,o):"Failed to synthesise CloudFormation template";return n(i)}return a}async bootstrap(o,s,r){const e=C();d.debug("CdkService","Starting CDK bootstrap",{accountId:o,region:s,bootstrapPath:e,target:`aws://${o}/${s}`});const t=await this.processManager.runCdkCommand(e,["bootstrap",`aws://${o}/${s}`,"--cloudformation-execution-policies","arn:aws:iam::aws:policy/AdministratorAccess","-c",`accountId=${o}`,u.REQUIRE_APPROVAL,u.APPROVAL_NEVER,u.CI,"--quiet","--force"],{...r,timeout:r?.timeout||R,context:{region:s,accountId:o},credentials:r?.credentials,skipProjectCheck:!0,extraEnv:{TERM:"dumb",CDK_DISABLE_NOTICES:"true",CDK_DISABLE_PROGRESS_BAR:"true"}});return d.debug("CdkService",t.success?"Bootstrap completed successfully":"Bootstrap exited with non-zero code",{accountId:o,region:s,exitCode:t.success?t.data.exitCode:void 0,output:t.success&&t.data.output?.trim()||"(empty)",error:t.success?"(empty)":t.error.trim()||"(empty)"}),t.success?p({output:"AWS environment bootstrapped",exitCode:0}):t.error.includes("already bootstrapped")?p({output:"Environment is already bootstrapped",exitCode:0}):n(A(t.error))}}export{R as BOOTSTRAP_TIMEOUT_MS,V as CdkCommandRunner,g as IMPORT_TIMEOUT_MS,_ as SYNTH_TIMEOUT_MS};
@@ -1,125 +1 @@
1
- import { logger } from "@fjall/util/logger";
2
- import { STACK_NOT_FOUND_PATTERN, CDK_NO_STACKS_MATCH } from "../../aws/utils/cloudformationEvents.js";
3
- import { success, failure } from "@fjall/generator";
4
- import { formatInfrastructureError } from "./CdkErrorFormatter.js";
5
- import { startStackMonitoring } from "./CdkEventMonitoring.js";
6
- /**
7
- * Analyse CDK deploy output and classify the result as success/skip/failure.
8
- */
9
- export function analyseDeployOutput(cdkOutput, result, stackName) {
10
- const stackSpecificNoResourcesPattern = new RegExp(`${stackName}[:\\s|]*.*?(?:stack has no resources|skipping deployment)`, "i");
11
- const hasEmptyStack = stackSpecificNoResourcesPattern.test(cdkOutput);
12
- const hasNoChanges = cdkOutput.includes("no changes") ||
13
- cdkOutput.includes("Stack is up to date") ||
14
- cdkOutput.includes("No changes to deploy");
15
- const noStacksMatch = cdkOutput.includes(CDK_NO_STACKS_MATCH) ||
16
- (!result.success && result.error.includes(CDK_NO_STACKS_MATCH));
17
- if (result.success) {
18
- return success({
19
- message: hasEmptyStack
20
- ? "Stack has no resources, skipped"
21
- : hasNoChanges
22
- ? "Infrastructure already up to date"
23
- : "Deployment successful",
24
- status: hasNoChanges ? "NO_CHANGES" : undefined,
25
- details: { output: cdkOutput, skipped: hasEmptyStack }
26
- });
27
- }
28
- if (noStacksMatch) {
29
- return success({
30
- message: "Stack not defined, skipped",
31
- status: "SKIPPED",
32
- details: { output: cdkOutput, skipped: true }
33
- });
34
- }
35
- let errorMessage = result.error || "Deployment failed";
36
- if (cdkOutput.includes("failed:")) {
37
- const errorMatch = cdkOutput.match(/failed: .*?: (.*?)(?:\n|$)/);
38
- if (errorMatch) {
39
- errorMessage = errorMatch[1];
40
- }
41
- }
42
- logger.error("CdkService", "CDK deployment returned failure", {
43
- errorMessage
44
- });
45
- return failure(errorMessage);
46
- }
47
- /**
48
- * Classify a bootstrap error into a user-friendly message.
49
- */
50
- export function analyseBootstrapError(errorInfo) {
51
- if (errorInfo.includes("Access Denied") ||
52
- errorInfo.includes("AccessDenied")) {
53
- return "Access denied. Check your AWS credentials and permissions";
54
- }
55
- if (errorInfo.includes("ExpiredToken")) {
56
- return "AWS credentials have expired. Please refresh your credentials";
57
- }
58
- if (errorInfo.includes("ENOTFOUND") || errorInfo.includes("ECONNREFUSED")) {
59
- return "Network error. Please check your internet connection and try again";
60
- }
61
- if (errorInfo.includes("timeout")) {
62
- return "Bootstrap operation timed out";
63
- }
64
- return formatInfrastructureError(errorInfo);
65
- }
66
- /**
67
- * Analyse CDK destroy result and classify it.
68
- */
69
- export function analyseDestroyResult(result) {
70
- if (!result.success) {
71
- const stackDoesNotExist = result.error.includes(STACK_NOT_FOUND_PATTERN) ||
72
- result.error.includes(CDK_NO_STACKS_MATCH);
73
- if (stackDoesNotExist) {
74
- return success({
75
- message: "Stack already deleted or does not exist",
76
- details: { skipped: true }
77
- });
78
- }
79
- return failure(result.error || "Destroy failed");
80
- }
81
- return success({ message: "Stack destroyed successfully" });
82
- }
83
- /**
84
- * Try to detect the actual CloudFormation stack name from a CDK output chunk.
85
- * CDK outputs the stack name in several formats during deployment.
86
- */
87
- export function detectStackNameFromChunk(chunk) {
88
- // Pattern 1: "Deploying stack <name>"
89
- const deployMatch = chunk.match(/Deploying stack\s+([^\s]+)/);
90
- if (deployMatch) {
91
- return { detected: true, stackName: deployMatch[1] };
92
- }
93
- // Pattern 2: "<stack-name> | <number>/<number> |"
94
- const progressMatch = chunk.match(/^([^\s|]+)\s*\|\s*\d+\/\d+/m);
95
- if (progressMatch) {
96
- return { detected: true, stackName: progressMatch[1] };
97
- }
98
- // Pattern 3: "Stack ARN:" line contains the stack name
99
- const arnMatch = chunk.match(/arn:aws:cloudformation:[^:]+:[^:]+:stack\/([^/]+)\//);
100
- if (arnMatch) {
101
- return { detected: true, stackName: arnMatch[1] };
102
- }
103
- return { detected: false };
104
- }
105
- /**
106
- * Create an enhanced output callback that detects the actual stack name from CDK output
107
- * and starts CloudFormation monitoring once detected.
108
- */
109
- export function createEnhancedOutputCallback(state, originalOutputCallback, cfnMonitor, onResourceProgress) {
110
- return (chunk) => {
111
- state.cdkOutput += chunk;
112
- originalOutputCallback?.(chunk);
113
- if (!state.stackDetected) {
114
- const detection = detectStackNameFromChunk(chunk);
115
- if (detection.detected && detection.stackName) {
116
- state.actualStackName = detection.stackName;
117
- state.stackDetected = true;
118
- }
119
- // Start monitoring once we detect the actual stack name
120
- if (state.stackDetected && cfnMonitor && !state.monitoringPromise) {
121
- state.monitoringPromise = startStackMonitoring(cfnMonitor, state.actualStackName, onResourceProgress);
122
- }
123
- }
124
- };
125
- }
1
+ import{logger as m}from"@fjall/util/logger";import{STACK_NOT_FOUND_PATTERN as p,CDK_NO_STACKS_MATCH as i}from"../../aws/utils/cloudformationEvents.js";import{success as n,failure as u}from"@fjall/generator";import{formatInfrastructureError as f}from"./CdkErrorFormatter.js";import{startStackMonitoring as k}from"./CdkEventMonitoring.js";function E(e,t,s){const r=new RegExp(`${s}[:\\s|]*.*?(?:stack has no resources|skipping deployment)`,"i").test(e),c=e.includes("no changes")||e.includes("Stack is up to date")||e.includes("No changes to deploy"),l=e.includes(i)||!t.success&&t.error.includes(i);if(t.success)return n({message:r?"Stack has no resources, skipped":c?"Infrastructure already up to date":"Deployment successful",status:c?"NO_CHANGES":void 0,details:{output:e,skipped:r}});if(l)return n({message:"Stack not defined, skipped",status:"SKIPPED",details:{output:e,skipped:!0}});let o=t.error||"Deployment failed";if(e.includes("failed:")){const d=e.match(/failed: .*?: (.*?)(?:\n|$)/);d&&(o=d[1])}return m.error("CdkService","CDK deployment returned failure",{errorMessage:o}),u(o)}function C(e){return e.includes("Access Denied")||e.includes("AccessDenied")?"Access denied. Check your AWS credentials and permissions":e.includes("ExpiredToken")?"AWS credentials have expired. Please refresh your credentials":e.includes("ENOTFOUND")||e.includes("ECONNREFUSED")?"Network error. Please check your internet connection and try again":e.includes("timeout")?"Bootstrap operation timed out":f(e)}function x(e){return e.success?n({message:"Stack destroyed successfully"}):e.error.includes(p)||e.error.includes(i)?n({message:"Stack already deleted or does not exist",details:{skipped:!0}}):u(e.error||"Destroy failed")}function N(e){const t=e.match(/Deploying stack\s+([^\s]+)/);if(t)return{detected:!0,stackName:t[1]};const s=e.match(/^([^\s|]+)\s*\|\s*\d+\/\d+/m);if(s)return{detected:!0,stackName:s[1]};const a=e.match(/arn:aws:cloudformation:[^:]+:[^:]+:stack\/([^/]+)\//);return a?{detected:!0,stackName:a[1]}:{detected:!1}}function A(e,t,s,a){return r=>{if(e.cdkOutput+=r,t?.(r),!e.stackDetected){const c=N(r);c.detected&&c.stackName&&(e.actualStackName=c.stackName,e.stackDetected=!0),e.stackDetected&&s&&!e.monitoringPromise&&(e.monitoringPromise=k(s,e.actualStackName,a))}}}export{C as analyseBootstrapError,E as analyseDeployOutput,x as analyseDestroyResult,A as createEnhancedOutputCallback,N as detectStackNameFromChunk};
@@ -1,278 +1,3 @@
1
- import { spawn } from "child_process";
2
- import { existsSync } from "fs";
3
- import { join } from "path";
4
- import { createRequire } from "module";
5
- import { logger } from "@fjall/util/logger";
6
- import { filterDangerousEnvVars, maskSensitiveOutput } from "@fjall/util";
7
- import { success, failure } from "@fjall/generator";
8
- import { getErrorMessage } from "@fjall/util";
9
- /**
10
- * Resolve CDK binary path from the caller's node_modules.
11
- * Falls back to "npx cdk" only if resolution fails (should never happen
12
- * since aws-cdk is a direct dependency of the calling package).
13
- */
14
- function getCdkBinaryPath() {
15
- try {
16
- const require = createRequire(import.meta.url);
17
- const cdkEntry = require.resolve("aws-cdk/bin/cdk");
18
- return { command: process.execPath, prefixArgs: [cdkEntry] };
19
- }
20
- catch (err) {
21
- logger.debug("CdkService", "Failed to resolve aws-cdk binary, falling back to npx", { error: err instanceof Error ? err.message : String(err) });
22
- return { command: "npx", prefixArgs: ["cdk"] };
23
- }
24
- }
25
- const cdkBin = getCdkBinaryPath();
26
- /** Grace period before escalating from SIGTERM to SIGKILL on a stuck CDK process */
27
- const SIGKILL_GRACE_PERIOD_MS = 5_000;
28
- export class CdkProcessManager {
29
- runningProcesses = new Map();
30
- processCounter = 0;
31
- argBuilder;
32
- exitHandler;
33
- sigintHandler;
34
- sigtermHandler;
35
- constructor(argBuilder) {
36
- this.argBuilder = argBuilder;
37
- // Register cleanup handlers (stored for deregistration)
38
- this.exitHandler = () => this.cleanup();
39
- this.sigintHandler = () => {
40
- this.cleanup();
41
- process.exit(130);
42
- };
43
- this.sigtermHandler = () => {
44
- this.cleanup();
45
- process.exit(143);
46
- };
47
- process.on("exit", this.exitHandler);
48
- process.on("SIGINT", this.sigintHandler);
49
- process.on("SIGTERM", this.sigtermHandler);
50
- }
51
- forceKillProcess(child) {
52
- child.stdout?.destroy();
53
- child.stderr?.destroy();
54
- child.kill("SIGTERM");
55
- const killTimer = setTimeout(() => {
56
- if (child.exitCode === null) {
57
- child.kill("SIGKILL");
58
- }
59
- }, SIGKILL_GRACE_PERIOD_MS);
60
- killTimer.unref();
61
- child.once("exit", () => clearTimeout(killTimer));
62
- }
63
- cleanup() {
64
- for (const [_name, child] of this.runningProcesses) {
65
- if (!child.killed) {
66
- child.stdout?.destroy();
67
- child.stderr?.destroy();
68
- child.kill("SIGTERM");
69
- }
70
- }
71
- this.runningProcesses.clear();
72
- }
73
- dispose() {
74
- this.cleanup();
75
- process.removeListener("exit", this.exitHandler);
76
- process.removeListener("SIGINT", this.sigintHandler);
77
- process.removeListener("SIGTERM", this.sigtermHandler);
78
- }
79
- async runCdkCommandPassthrough(workingDir, args, options) {
80
- return new Promise((resolve) => {
81
- if (!existsSync(workingDir)) {
82
- resolve(failure(`Directory not found: ${workingDir}`));
83
- return;
84
- }
85
- // Check for cdk.json in the working directory
86
- const cdkJsonPath = join(workingDir, "cdk.json");
87
- if (!existsSync(cdkJsonPath)) {
88
- resolve(failure(`No CDK project found in ${workingDir}`));
89
- return;
90
- }
91
- const contextArgs = this.argBuilder.buildContextArgs(options?.context);
92
- const fullArgs = [...cdkBin.prefixArgs, ...args, ...contextArgs];
93
- const env = this.argBuilder.buildCdkEnv(options);
94
- // Run CDK with stdio: 'inherit' for direct terminal output
95
- const child = spawn(cdkBin.command, fullArgs, {
96
- cwd: workingDir,
97
- env,
98
- stdio: "inherit", // Pass through to terminal directly
99
- shell: false,
100
- detached: false
101
- });
102
- if (!child.pid) {
103
- child.on("error", (err) => {
104
- logger.debug("CdkProcess", "Spawn error on failed child process", {
105
- error: err.message
106
- });
107
- });
108
- resolve(failure(`Failed to spawn CDK process - no PID. cwd=${workingDir}, args=${fullArgs.join(" ")}`));
109
- return;
110
- }
111
- // Track process for cleanup
112
- const processId = `cdk-passthrough-${++this.processCounter}`;
113
- this.runningProcesses.set(processId, child);
114
- let processKilled = false;
115
- let settled = false;
116
- // Timeout to prevent indefinite hangs (default 30 minutes, matching deploy)
117
- const timeoutMs = options?.timeout ?? 30 * 60 * 1000;
118
- const timeoutHandle = setTimeout(() => {
119
- if (!child.killed) {
120
- processKilled = true;
121
- this.forceKillProcess(child);
122
- }
123
- }, timeoutMs);
124
- child.on("close", (code) => {
125
- clearTimeout(timeoutHandle);
126
- this.runningProcesses.delete(processId);
127
- if (settled)
128
- return;
129
- settled = true;
130
- if (processKilled) {
131
- resolve(failure("CDK command timed out"));
132
- return;
133
- }
134
- if (code === 0 || options?.ignoreExitCode) {
135
- resolve(success({ exitCode: code || 0 }));
136
- }
137
- else {
138
- resolve(failure(`CDK command failed with exit code ${code}`));
139
- }
140
- });
141
- child.on("error", (err) => {
142
- clearTimeout(timeoutHandle);
143
- this.runningProcesses.delete(processId);
144
- if (settled || processKilled)
145
- return;
146
- settled = true;
147
- resolve(failure(`Failed to run CDK command: ${err.message}`));
148
- });
149
- });
150
- }
151
- async runCdkCommand(workingDir, args, options) {
152
- return new Promise((resolve) => {
153
- if (!existsSync(workingDir)) {
154
- resolve(failure(`Directory not found: ${workingDir}`));
155
- return;
156
- }
157
- // Check for cdk.json unless caller explicitly skips (e.g., bootstrap runs from tmpdir)
158
- if (!options?.skipProjectCheck) {
159
- const cdkJsonPath = join(workingDir, "cdk.json");
160
- if (!existsSync(cdkJsonPath)) {
161
- resolve(failure(`No CDK project found in ${workingDir}`));
162
- return;
163
- }
164
- }
165
- let output = "";
166
- let errorOutput = "";
167
- let processKilled = false;
168
- const contextArgs = this.argBuilder.buildContextArgs(options?.context);
169
- const fullArgs = [...cdkBin.prefixArgs, ...args, ...contextArgs];
170
- const env = {
171
- ...this.argBuilder.buildCdkEnv(options),
172
- NO_COLOR: "1",
173
- ...(options?.extraEnv ? filterDangerousEnvVars(options.extraEnv) : {})
174
- };
175
- // Try to spawn the CDK process
176
- logger.debug("CdkService", "Spawning CDK process", {
177
- command: `${cdkBin.command} ${fullArgs.join(" ")}`,
178
- workingDir
179
- });
180
- const child = spawn(cdkBin.command, fullArgs, {
181
- cwd: workingDir,
182
- env,
183
- stdio: ["ignore", "pipe", "pipe"],
184
- shell: false,
185
- detached: false
186
- });
187
- // Check if spawn was successful
188
- if (!child.pid) {
189
- const spawnError = `Failed to spawn CDK process - no PID. cwd=${workingDir}, args=${fullArgs.join(" ")}`;
190
- // CRITICAL: Attach error handler to prevent uncaught exception
191
- // Node.js emits an async 'error' event on the child process even after spawn fails
192
- // If we don't handle it, it becomes an uncaught exception and crashes the app
193
- child.on("error", (err) => {
194
- logger.debug("CdkProcess", "Deferred spawn error on failed child process", {
195
- error: err.message
196
- });
197
- });
198
- resolve(failure(spawnError));
199
- return;
200
- }
201
- // Track process for cleanup
202
- const processId = `cdk-${++this.processCounter}`;
203
- this.runningProcesses.set(processId, child);
204
- let settled = false;
205
- const timeoutMs = options?.timeout ?? 30 * 60 * 1000;
206
- const timeoutHandle = setTimeout(() => {
207
- if (!child.killed) {
208
- processKilled = true;
209
- this.forceKillProcess(child);
210
- }
211
- }, timeoutMs);
212
- child.stdout?.on("data", (data) => {
213
- const chunk = data.toString();
214
- output += chunk;
215
- if (options?.outputCallback) {
216
- options.outputCallback(maskSensitiveOutput(chunk));
217
- }
218
- options?.cdkOutputLogger?.writeCdkOutput("stdout", maskSensitiveOutput(chunk));
219
- });
220
- child.stderr?.on("data", (data) => {
221
- const chunk = data.toString();
222
- options?.cdkOutputLogger?.writeCdkOutput("stderr", maskSensitiveOutput(chunk));
223
- // Filter out non-critical warnings from the in-memory error buffer
224
- if (!chunk.includes("deprecated") &&
225
- !chunk.includes("npm WARN") &&
226
- !chunk.includes("ENOENT")) {
227
- errorOutput += chunk;
228
- }
229
- // Always send to callback for visibility
230
- if (options?.errorCallback) {
231
- options.errorCallback(maskSensitiveOutput(chunk));
232
- }
233
- });
234
- child.on("error", (error) => {
235
- clearTimeout(timeoutHandle);
236
- this.runningProcesses.delete(processId);
237
- if (settled || processKilled)
238
- return;
239
- settled = true;
240
- resolve(failure(getErrorMessage(error)));
241
- });
242
- child.on("close", (code) => {
243
- clearTimeout(timeoutHandle);
244
- this.runningProcesses.delete(processId);
245
- if (settled)
246
- return;
247
- settled = true;
248
- if (processKilled) {
249
- resolve(failure("CDK command timed out"));
250
- return;
251
- }
252
- const exitedClean = code === 0 || (options?.ignoreExitCode === true && code === 1);
253
- // For diff command, we need to include stderr in output since CDK outputs errors there
254
- const combinedOutput = output + (errorOutput ? `\n${errorOutput}` : "");
255
- if (exitedClean) {
256
- const rawOutput = options?.combineOutput ? combinedOutput : output;
257
- resolve(success({
258
- output: maskSensitiveOutput(rawOutput),
259
- exitCode: code || 0
260
- }));
261
- return;
262
- }
263
- // Parse error output for meaningful messages (match on raw before masking)
264
- let errorMessage = errorOutput;
265
- if (output) {
266
- const errorMatch = output.match(/❌.*?Error:(.*)$/m);
267
- if (errorMatch) {
268
- errorMessage = errorMatch[1]?.trim() ?? "";
269
- }
270
- }
271
- // Include stdout in the error string so callers can pattern-match on combined output
272
- const errorText = errorMessage || `CDK command failed with exit code ${code}`;
273
- const fullError = output ? `${errorText}\n${output}` : errorText;
274
- resolve(failure(maskSensitiveOutput(fullError)));
275
- });
276
- });
277
- }
278
- }
1
+ import{spawn as K}from"child_process";import{existsSync as E}from"fs";import{join as T}from"path";import{createRequire as A}from"module";import{logger as $}from"@fjall/util/logger";import{filterDangerousEnvVars as M,maskSensitiveOutput as g}from"@fjall/util";import{success as H,failure as i}from"@fjall/generator";import{getErrorMessage as w}from"@fjall/util";function L(){try{const e=A(import.meta.url).resolve("aws-cdk/bin/cdk");return{command:process.execPath,prefixArgs:[e]}}catch(h){return $.debug("CdkService","Failed to resolve aws-cdk binary, falling back to npx",{error:h instanceof Error?h.message:String(h)}),{command:"npx",prefixArgs:["cdk"]}}}const x=L(),j=5e3;class v{runningProcesses=new Map;processCounter=0;argBuilder;exitHandler;sigintHandler;sigtermHandler;constructor(e){this.argBuilder=e,this.exitHandler=()=>this.cleanup(),this.sigintHandler=()=>{this.cleanup(),process.exit(130)},this.sigtermHandler=()=>{this.cleanup(),process.exit(143)},process.on("exit",this.exitHandler),process.on("SIGINT",this.sigintHandler),process.on("SIGTERM",this.sigtermHandler)}forceKillProcess(e){e.stdout?.destroy(),e.stderr?.destroy(),e.kill("SIGTERM");const c=setTimeout(()=>{e.exitCode===null&&e.kill("SIGKILL")},j);c.unref(),e.once("exit",()=>clearTimeout(c))}cleanup(){for(const[e,c]of this.runningProcesses)c.killed||(c.stdout?.destroy(),c.stderr?.destroy(),c.kill("SIGTERM"));this.runningProcesses.clear()}dispose(){this.cleanup(),process.removeListener("exit",this.exitHandler),process.removeListener("SIGINT",this.sigintHandler),process.removeListener("SIGTERM",this.sigtermHandler)}async runCdkCommandPassthrough(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}const a=T(e,"cdk.json");if(!E(a)){t(i(`No CDK project found in ${e}`));return}const m=this.argBuilder.buildContextArgs(r?.context),f=[...x.prefixArgs,...c,...m],S=this.argBuilder.buildCdkEnv(r),d=K(x.command,f,{cwd:e,env:S,stdio:"inherit",shell:!1,detached:!1});if(!d.pid){d.on("error",u=>{$.debug("CdkProcess","Spawn error on failed child process",{error:u.message})}),t(i(`Failed to spawn CDK process - no PID. cwd=${e}, args=${f.join(" ")}`));return}const C=`cdk-passthrough-${++this.processCounter}`;this.runningProcesses.set(C,d);let o=!1,l=!1;const p=r?.timeout??30*60*1e3,k=setTimeout(()=>{d.killed||(o=!0,this.forceKillProcess(d))},p);d.on("close",u=>{if(clearTimeout(k),this.runningProcesses.delete(C),!l){if(l=!0,o){t(i("CDK command timed out"));return}u===0||r?.ignoreExitCode?t(H({exitCode:u||0})):t(i(`CDK command failed with exit code ${u}`))}}),d.on("error",u=>{clearTimeout(k),this.runningProcesses.delete(C),!(l||o)&&(l=!0,t(i(`Failed to run CDK command: ${u.message}`)))})})}async runCdkCommand(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}if(!r?.skipProjectCheck){const s=T(e,"cdk.json");if(!E(s)){t(i(`No CDK project found in ${e}`));return}}let a="",m="",f=!1;const S=this.argBuilder.buildContextArgs(r?.context),d=[...x.prefixArgs,...c,...S],C={...this.argBuilder.buildCdkEnv(r),NO_COLOR:"1",...r?.extraEnv?M(r.extraEnv):{}};$.debug("CdkService","Spawning CDK process",{command:`${x.command} ${d.join(" ")}`,workingDir:e});const o=K(x.command,d,{cwd:e,env:C,stdio:["ignore","pipe","pipe"],shell:!1,detached:!1});if(!o.pid){const s=`Failed to spawn CDK process - no PID. cwd=${e}, args=${d.join(" ")}`;o.on("error",n=>{$.debug("CdkProcess","Deferred spawn error on failed child process",{error:n.message})}),t(i(s));return}const l=`cdk-${++this.processCounter}`;this.runningProcesses.set(l,o);let p=!1;const k=r?.timeout??30*60*1e3,u=setTimeout(()=>{o.killed||(f=!0,this.forceKillProcess(o))},k);o.stdout?.on("data",s=>{const n=s.toString();a+=n,r?.outputCallback&&r.outputCallback(g(n)),r?.cdkOutputLogger?.writeCdkOutput("stdout",g(n))}),o.stderr?.on("data",s=>{const n=s.toString();r?.cdkOutputLogger?.writeCdkOutput("stderr",g(n)),!n.includes("deprecated")&&!n.includes("npm WARN")&&!n.includes("ENOENT")&&(m+=n),r?.errorCallback&&r.errorCallback(g(n))}),o.on("error",s=>{clearTimeout(u),this.runningProcesses.delete(l),!(p||f)&&(p=!0,t(i(w(s))))}),o.on("close",s=>{if(clearTimeout(u),this.runningProcesses.delete(l),p)return;if(p=!0,f){t(i("CDK command timed out"));return}const n=s===0||r?.ignoreExitCode===!0&&s===1,O=a+(m?`
2
+ ${m}`:"");if(n){const P=r?.combineOutput?O:a;t(H({output:g(P),exitCode:s||0}));return}let b=m;if(a){const P=a.match(/❌.*?Error:(.*)$/m);P&&(b=P[1]?.trim()??"")}const I=b||`CDK command failed with exit code ${s}`,y=a?`${I}
3
+ ${a}`:I;t(i(g(y)))})})}}export{v as CdkProcessManager};