@fjall/deploy-core 2.27.0 → 2.29.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 (134) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/organisations/backup.d.ts +7 -2
  3. package/dist/src/aws/organisations/backup.js +2 -2
  4. package/dist/src/aws/organisations/index.d.ts +1 -1
  5. package/dist/src/aws/organisations/index.js +1 -1
  6. package/dist/src/aws/organisations/oidcProvider.d.ts +37 -0
  7. package/dist/src/aws/organisations/oidcProvider.js +1 -1
  8. package/dist/src/aws/organisations/organisation.d.ts +21 -0
  9. package/dist/src/aws/organisations/organisation.js +1 -1
  10. package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +45 -0
  11. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
  12. package/dist/src/aws/utils/cloudformationEventTypes.d.ts +1 -1
  13. package/dist/src/aws/utils/cloudformationEvents.d.ts +2 -1
  14. package/dist/src/aws/utils/cloudformationEvents.js +1 -1
  15. package/dist/src/aws/utils/stackStatus.js +1 -1
  16. package/dist/src/index.d.ts +12 -1
  17. package/dist/src/index.js +1 -1
  18. package/dist/src/orchestration/activeDeploymentGuard.d.ts +41 -0
  19. package/dist/src/orchestration/activeDeploymentGuard.js +8 -4
  20. package/dist/src/orchestration/application/applicationDeploy.d.ts +9 -0
  21. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  22. package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +20 -3
  23. package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
  24. package/dist/src/orchestration/application/applicationDestroy.js +1 -1
  25. package/dist/src/orchestration/application/approvalGate.d.ts +15 -1
  26. package/dist/src/orchestration/application/approvalGate.js +1 -1
  27. package/dist/src/orchestration/application/destructionGate.d.ts +164 -0
  28. package/dist/src/orchestration/application/destructionGate.js +5 -0
  29. package/dist/src/orchestration/application/leaseGate.d.ts +30 -0
  30. package/dist/src/orchestration/application/leaseGate.js +1 -0
  31. package/dist/src/orchestration/application/plan/approvalToken.d.ts +15 -0
  32. package/dist/src/orchestration/application/plan/approvalToken.js +1 -1
  33. package/dist/src/orchestration/application/plan/changeSetEscalation.d.ts +46 -0
  34. package/dist/src/orchestration/application/plan/changeSetEscalation.js +1 -0
  35. package/dist/src/orchestration/application/plan/computeDeployPlan.d.ts +13 -0
  36. package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
  37. package/dist/src/orchestration/application/plan/index.d.ts +4 -1
  38. package/dist/src/orchestration/application/plan/index.js +1 -1
  39. package/dist/src/orchestration/application/plan/loadDeployPlan.d.ts +32 -0
  40. package/dist/src/orchestration/application/plan/loadDeployPlan.js +1 -0
  41. package/dist/src/orchestration/application/plan/pinnedNames.d.ts +31 -0
  42. package/dist/src/orchestration/application/plan/pinnedNames.js +1 -0
  43. package/dist/src/orchestration/application/plan/registryOverlay.d.ts +83 -0
  44. package/dist/src/orchestration/application/plan/registryOverlay.js +1 -0
  45. package/dist/src/orchestration/application/plan/renderDeployPlan.d.ts +4 -1
  46. package/dist/src/orchestration/application/plan/renderDeployPlan.js +1 -1
  47. package/dist/src/orchestration/application/plan/types.d.ts +31 -0
  48. package/dist/src/orchestration/application/plan/types.js +1 -0
  49. package/dist/src/orchestration/contextHelpers.d.ts +37 -1
  50. package/dist/src/orchestration/contextHelpers.js +1 -1
  51. package/dist/src/orchestration/drift/classifyDriftFailure.d.ts +31 -0
  52. package/dist/src/orchestration/drift/classifyDriftFailure.js +1 -0
  53. package/dist/src/orchestration/drift/detectStackDrift.d.ts +37 -0
  54. package/dist/src/orchestration/drift/detectStackDrift.js +1 -0
  55. package/dist/src/orchestration/drift/driftPreFlight.d.ts +73 -0
  56. package/dist/src/orchestration/drift/driftPreFlight.js +7 -0
  57. package/dist/src/orchestration/drift/driftProbe.d.ts +113 -0
  58. package/dist/src/orchestration/drift/driftProbe.js +1 -0
  59. package/dist/src/orchestration/drift/driftSuspectJournal.d.ts +40 -0
  60. package/dist/src/orchestration/drift/driftSuspectJournal.js +1 -0
  61. package/dist/src/orchestration/drift/preFlightGate.d.ts +44 -0
  62. package/dist/src/orchestration/drift/preFlightGate.js +2 -0
  63. package/dist/src/orchestration/index.d.ts +19 -0
  64. package/dist/src/orchestration/index.js +1 -1
  65. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.d.ts +7 -0
  66. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.js +1 -1
  67. package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
  68. package/dist/src/orchestration/organisation/organisationDestroy.js +3 -3
  69. package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +8 -1
  70. package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
  71. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +5 -5
  72. package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +21 -2
  73. package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
  74. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
  75. package/dist/src/orchestration/remediation/driftRepairGate.d.ts +66 -0
  76. package/dist/src/orchestration/remediation/driftRepairGate.js +1 -0
  77. package/dist/src/orchestration/remediation/forensicsCapture.d.ts +49 -0
  78. package/dist/src/orchestration/remediation/forensicsCapture.js +1 -0
  79. package/dist/src/orchestration/remediation/forgetResource.d.ts +423 -0
  80. package/dist/src/orchestration/remediation/forgetResource.js +1 -0
  81. package/dist/src/orchestration/remediation/pinRemediation.d.ts +121 -0
  82. package/dist/src/orchestration/remediation/pinRemediation.js +1 -0
  83. package/dist/src/orchestration/remediation/postFailureRepairOffer.d.ts +77 -0
  84. package/dist/src/orchestration/remediation/postFailureRepairOffer.js +14 -0
  85. package/dist/src/orchestration/remediation/recreatePreFlight.d.ts +47 -0
  86. package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -0
  87. package/dist/src/orchestration/remediation/recreateResource.d.ts +110 -0
  88. package/dist/src/orchestration/remediation/recreateResource.js +1 -0
  89. package/dist/src/orchestration/remediation/remediationJournal.d.ts +88 -0
  90. package/dist/src/orchestration/remediation/remediationJournal.js +2 -0
  91. package/dist/src/orchestration/remediation/removalUpdate.d.ts +135 -0
  92. package/dist/src/orchestration/remediation/removalUpdate.js +1 -0
  93. package/dist/src/orchestration/remediation/retainFlip.d.ts +294 -0
  94. package/dist/src/orchestration/remediation/retainFlip.js +1 -0
  95. package/dist/src/services/application/ApplicationStackService.d.ts +5 -3
  96. package/dist/src/services/application/ApplicationStackService.js +1 -1
  97. package/dist/src/services/application/applicationStackHelpers.d.ts +8 -6
  98. package/dist/src/services/application/applicationStackHelpers.js +3 -3
  99. package/dist/src/services/index.d.ts +2 -2
  100. package/dist/src/services/index.js +1 -1
  101. package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +15 -0
  102. package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -1
  103. package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +8 -0
  104. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +2 -1
  105. package/dist/src/services/infrastructure/CdkService.js +3 -3
  106. package/dist/src/services/infrastructure/CfnRegistryService.d.ts +68 -0
  107. package/dist/src/services/infrastructure/CfnRegistryService.js +1 -0
  108. package/dist/src/services/infrastructure/CloudFormationService.d.ts +5 -2
  109. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  110. package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +23 -3
  111. package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
  112. package/dist/src/services/infrastructure/cfnRegistrySeed.d.ts +10 -0
  113. package/dist/src/services/infrastructure/cfnRegistrySeed.js +1 -0
  114. package/dist/src/services/infrastructure/changeSetProbe.d.ts +88 -0
  115. package/dist/src/services/infrastructure/changeSetProbe.js +1 -0
  116. package/dist/src/services/infrastructure/index.d.ts +2 -0
  117. package/dist/src/services/infrastructure/index.js +1 -1
  118. package/dist/src/types/callbackKeys.d.ts +1 -1
  119. package/dist/src/types/callbackKeys.js +1 -1
  120. package/dist/src/types/callbacks.d.ts +22 -0
  121. package/dist/src/types/config/entitlements.d.ts +9 -0
  122. package/dist/src/types/config/orgConfig.d.ts +6 -0
  123. package/dist/src/types/deploymentEventSchema.d.ts +1 -0
  124. package/dist/src/types/deploymentEventSchema.js +1 -1
  125. package/dist/src/types/destruction.d.ts +212 -0
  126. package/dist/src/types/destruction.js +1 -0
  127. package/dist/src/types/drift.d.ts +72 -0
  128. package/dist/src/types/drift.js +1 -0
  129. package/dist/src/types/index.d.ts +8 -2
  130. package/dist/src/types/index.js +1 -1
  131. package/dist/src/types/params.d.ts +142 -0
  132. package/dist/src/types/remediation.d.ts +126 -0
  133. package/dist/src/types/remediation.js +1 -0
  134. package/package.json +5 -4
@@ -1,5 +1,5 @@
1
- export { CdkService, CdkArgumentBuilder, CdkProcessManager, CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS, isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext, hasCdkDifferences, parseDiffOutput, CloudFormationService, CloudFormationError, EcsService, EcsError, EcsServiceResolver, type CfnExportsClient } from "./infrastructure/index.js";
2
- export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions, ICdkProcessManager, DiffDetails, CloudFormationCallbacks, ECSServiceInfo, ECSClusterInfo, DeploymentStatus, ECSDeploymentOptions } from "./infrastructure/index.js";
1
+ export { CdkService, CdkArgumentBuilder, CdkProcessManager, CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS, isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext, hasCdkDifferences, parseDiffOutput, CloudFormationService, CloudFormationError, CfnRegistryService, CFN_REGISTRY_SEED, EcsService, EcsError, EcsServiceResolver, type CfnExportsClient } from "./infrastructure/index.js";
2
+ export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions, ICdkProcessManager, DiffDetails, CloudFormationCallbacks, CfnRegistrySchemaSummary, CfnRegistryServiceOptions, CfnRegistrySummaryReader, ECSServiceInfo, ECSClusterInfo, DeploymentStatus, ECSDeploymentOptions } from "./infrastructure/index.js";
3
3
  export { TemplateHashService, TemplateHashError, type TemplateComparisonResult } from "./supporting/index.js";
4
4
  export { CdkContextBuilder } from "./supporting/index.js";
5
5
  export { emitProgress, PROGRESS_MESSAGES, parseBuildPhase, buildStepContextBuildConfig, convertCloudFormationOutputsToRecord, type CloudFormationOutput } from "./supporting/index.js";
@@ -1 +1 @@
1
- import{CdkService as o,CdkArgumentBuilder as t,CdkProcessManager as i,CdkEventMonitor as a,startStackMonitoring as c,DEFAULT_DEPLOY_TIMEOUT_MS as s,isCdkError as n,formatInfrastructureError as u,getStructuralHint as S,getSourceContext as d,hasCdkDifferences as E,parseDiffOutput as p,CloudFormationService as C,CloudFormationError as l,EcsService as m,EcsError as f,EcsServiceResolver as k}from"./infrastructure/index.js";import{TemplateHashService as x,TemplateHashError as g}from"./supporting/index.js";import{CdkContextBuilder as T}from"./supporting/index.js";import{emitProgress as P,PROGRESS_MESSAGES as h,parseBuildPhase as A,buildStepContextBuildConfig as B,convertCloudFormationOutputsToRecord as D}from"./supporting/index.js";import{ApplicationStackService as R}from"./application/index.js";export{R as ApplicationStackService,t as CdkArgumentBuilder,T as CdkContextBuilder,a as CdkEventMonitor,i as CdkProcessManager,o as CdkService,l as CloudFormationError,C as CloudFormationService,s as DEFAULT_DEPLOY_TIMEOUT_MS,f as EcsError,m as EcsService,k as EcsServiceResolver,h as PROGRESS_MESSAGES,g as TemplateHashError,x as TemplateHashService,B as buildStepContextBuildConfig,D as convertCloudFormationOutputsToRecord,P as emitProgress,u as formatInfrastructureError,d as getSourceContext,S as getStructuralHint,E as hasCdkDifferences,n as isCdkError,A as parseBuildPhase,p as parseDiffOutput,c as startStackMonitoring};
1
+ import{CdkService as o,CdkArgumentBuilder as t,CdkProcessManager as i,CdkEventMonitor as a,startStackMonitoring as c,DEFAULT_DEPLOY_TIMEOUT_MS as s,isCdkError as S,formatInfrastructureError as n,getStructuralHint as u,getSourceContext as E,hasCdkDifferences as d,parseDiffOutput as C,CloudFormationService as p,CloudFormationError as f,CfnRegistryService as l,CFN_REGISTRY_SEED as m,EcsService as v,EcsError as k,EcsServiceResolver as g}from"./infrastructure/index.js";import{TemplateHashService as R,TemplateHashError as T}from"./supporting/index.js";import{CdkContextBuilder as _}from"./supporting/index.js";import{emitProgress as F,PROGRESS_MESSAGES as O,parseBuildPhase as P,buildStepContextBuildConfig as h,convertCloudFormationOutputsToRecord as A}from"./supporting/index.js";import{ApplicationStackService as G}from"./application/index.js";export{G as ApplicationStackService,m as CFN_REGISTRY_SEED,t as CdkArgumentBuilder,_ as CdkContextBuilder,a as CdkEventMonitor,i as CdkProcessManager,o as CdkService,l as CfnRegistryService,f as CloudFormationError,p as CloudFormationService,s as DEFAULT_DEPLOY_TIMEOUT_MS,k as EcsError,v as EcsService,g as EcsServiceResolver,O as PROGRESS_MESSAGES,T as TemplateHashError,R as TemplateHashService,h as buildStepContextBuildConfig,A as convertCloudFormationOutputsToRecord,F as emitProgress,n as formatInfrastructureError,E as getSourceContext,u as getStructuralHint,d as hasCdkDifferences,S as isCdkError,P as parseBuildPhase,C as parseDiffOutput,c as startStackMonitoring};
@@ -11,6 +11,21 @@ export declare function startStackMonitoring(monitor: CloudFormationEventMonitor
11
11
  failureReason?: string;
12
12
  logPath?: string;
13
13
  }>;
14
+ /** Grace budget for the monitor to finish analysing after a failed deploy. */
15
+ export declare const MONITORING_ANALYSIS_GRACE_MS = 10000;
16
+ /**
17
+ * Await the monitor's failure analysis after a failed CDK exit, bounded by a
18
+ * grace timeout so a hung monitor can never deadlock the deploy result (Q12).
19
+ *
20
+ * Two waits are required: `waitForStackComplete` resolves from its own poll
21
+ * loop the moment the stack reaches a terminal state, while the analysis is
22
+ * computed slightly LATER by the event poll loop (`handleStackCompletion`
23
+ * awaits a DescribeStacks before storing it) — so after the monitoring
24
+ * promise settles, the analysis is polled briefly within the same grace
25
+ * budget. A null `monitoringPromise` means monitoring never started (CDK
26
+ * failed pre-CloudFormation): return immediately, no analysis is coming.
27
+ */
28
+ export declare function settleMonitoringAnalysis(monitor: CloudFormationEventMonitor | null, monitoringPromise: Promise<unknown> | null, graceMs?: number): Promise<FailureAnalysis | null>;
14
29
  export declare class CdkEventMonitor {
15
30
  private eventLogWriterFactory?;
16
31
  private onFailureAnalysis?;
@@ -1 +1 @@
1
- import{logger as u}from"@fjall/util/logger";import{getErrorMessage as m}from"@fjall/util";import{CloudFormationFailureAnalyser as y}from"@fjall/util/aws";const g=18e5;function v(n,e,t){return n.waitForStackComplete(e,{timeout:g,pollInterval:2e3,onResourceUpdate:t,onStackComplete:(o,r)=>{}}).catch(o=>(u.debug("CdkService","Stack monitoring failed",{error:m(o)}),{success:!1,failureReason:"Monitoring failed"}))}class f{eventLogWriterFactory;onFailureAnalysis;constructor(e){this.eventLogWriterFactory=e?.eventLogWriterFactory,this.onFailureAnalysis=e?.onFailureAnalysis}async createEventMonitor(e,t,o,r,a){const s=await import("../../aws/utils/cloudformationEvents.js"),i=new s.CloudFormationEventMonitor(a,{failureAnalyser:new y,eventLogWriterFactory:this.eventLogWriterFactory,...this.onFailureAnalysis!==void 0?{onFailureAnalysis:this.onFailureAnalysis}:{}}),l=`${e}-${Date.now()}`,c=r.target||r.deployType||"deployment";return i.enableLogging(l,t,o,c),i}}export{f as CdkEventMonitor,g as DEFAULT_DEPLOY_TIMEOUT_MS,v as startStackMonitoring};
1
+ import{logger as u}from"@fjall/util/logger";import{getErrorMessage as y,sleep as d}from"@fjall/util";import{CloudFormationFailureAnalyser as g}from"@fjall/util/aws";const F=18e5;function L(t,e,n){return t.waitForStackComplete(e,{timeout:F,pollInterval:2e3,onResourceUpdate:n,onStackComplete:(o,r)=>{}}).catch(o=>(u.debug("CdkService","Stack monitoring failed",{error:y(o)}),{success:!1,failureReason:"Monitoring failed"}))}const m=1e4,p=200;async function S(t,e,n=m){if(t===null)return null;if(e===null)return t.getFailureAnalysis();const o=Date.now()+n;let r;const i=new Promise(a=>{r=setTimeout(()=>a("grace-elapsed"),n)});try{if(await Promise.race([e,i])==="grace-elapsed")return t.getFailureAnalysis()}finally{r!==void 0&&clearTimeout(r)}for(;t.getFailureAnalysis()===null&&Date.now()<o;)await d(p);return t.getFailureAnalysis()}class M{eventLogWriterFactory;onFailureAnalysis;constructor(e){this.eventLogWriterFactory=e?.eventLogWriterFactory,this.onFailureAnalysis=e?.onFailureAnalysis}async createEventMonitor(e,n,o,r,i){const a=await import("../../aws/utils/cloudformationEvents.js"),s=new a.CloudFormationEventMonitor(i,{failureAnalyser:new g,eventLogWriterFactory:this.eventLogWriterFactory,...this.onFailureAnalysis!==void 0?{onFailureAnalysis:this.onFailureAnalysis}:{}}),l=`${e}-${Date.now()}`,c=r.target||r.deployType||"deployment";return s.enableLogging(l,n,o,c),s}}export{M as CdkEventMonitor,F as DEFAULT_DEPLOY_TIMEOUT_MS,m as MONITORING_ANALYSIS_GRACE_MS,S as settleMonitoringAnalysis,L as startStackMonitoring};
@@ -1,3 +1,4 @@
1
+ import type { FailureAnalysis } from "@fjall/util/aws";
1
2
  import { type ResourceEvent, type CloudFormationEventMonitor } from "../../aws/utils/cloudformationEvents.js";
2
3
  import type { StepOutput } from "../../types/deployment/DeploymentTypes.js";
3
4
  import type { Result } from "@fjall/generator";
@@ -6,6 +7,13 @@ import type { CdkOutput } from "./CdkService.js";
6
7
  * Analyse CDK deploy output and classify the result as success/skip/failure.
7
8
  */
8
9
  export declare function analyseDeployOutput(cdkOutput: string, result: Result<CdkOutput, string>, stackName: string): Result<StepOutput, string>;
10
+ /**
11
+ * Compose the returned deploy error from the monitor's settled failure
12
+ * analysis (Q12: the analysed root cause reaches the RETURNED error, not just
13
+ * the `onFailureAnalysis` stream). The analysis is already masked at the
14
+ * monitor boundary (`maskFailureAnalysis`).
15
+ */
16
+ export declare function formatAnalysedDeployFailure(analysis: FailureAnalysis): string;
9
17
  /**
10
18
  * Classify a bootstrap error into a user-friendly message.
11
19
  */
@@ -1 +1,2 @@
1
- import{logger as p}from"@fjall/util/logger";import{maskSensitiveOutput as u}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as f,CDK_NO_STACKS_MATCH as o}from"../../aws/utils/cloudformationEvents.js";import{success as i,failure as l}from"@fjall/generator";import{formatInfrastructureError as k}from"./CdkErrorFormatter.js";import{startStackMonitoring as N}from"./CdkEventMonitoring.js";function x(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"),m=e.includes(o)||!t.success&&t.error.includes(o);if(t.success)return i({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(m)return i({message:"Stack not defined, skipped",status:"SKIPPED",details:{output:e,skipped:!0}});let n=u(t.error||"Deployment failed");if(e.includes("failed:")){const d=e.match(/failed: .*?: (.*?)(?:\n|$)/);d&&(n=u(d[1]))}return p.error("CdkService","CDK deployment returned failure",{errorMessage:n}),l(n)}function A(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":k(e)}function P(e){return e.success?i({message:"Stack destroyed successfully"}):e.error.includes(f)||e.error.includes(o)?i({message:"Stack already deleted or does not exist",details:{skipped:!0}}):l(e.error||"Destroy failed")}function S(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 T(e,t,s,a){return r=>{if(e.cdkOutput+=r,t?.(r),!e.stackDetected){const c=S(r);c.detected&&c.stackName&&(e.actualStackName=c.stackName,e.stackDetected=!0),e.stackDetected&&s&&!e.monitoringPromise&&(e.monitoringPromise=N(s,e.actualStackName,a))}}}export{A as analyseBootstrapError,x as analyseDeployOutput,P as analyseDestroyResult,T as createEnhancedOutputCallback,S as detectStackNameFromChunk};
1
+ import{logger as f}from"@fjall/util/logger";import{maskSensitiveOutput as u}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as m,CDK_NO_STACKS_MATCH as a}from"../../aws/utils/cloudformationEvents.js";import{success as i,failure as l}from"@fjall/generator";import{formatInfrastructureError as k}from"./CdkErrorFormatter.js";import{startStackMonitoring as S}from"./CdkEventMonitoring.js";function x(e,t,s){const c=new RegExp(`${s}[:\\s|]*.*?(?:stack has no resources|skipping deployment)`,"i").test(e),r=e.includes("no changes")||e.includes("Stack is up to date")||e.includes("No changes to deploy"),p=e.includes(a)||!t.success&&t.error.includes(a);if(t.success)return i({message:c?"Stack has no resources, skipped":r?"Infrastructure already up to date":"Deployment successful",status:r?"NO_CHANGES":void 0,details:{output:e,skipped:c}});if(p)return i({message:"Stack not defined, skipped",status:"SKIPPED",details:{output:e,skipped:!0}});let n=u(t.error||"Deployment failed");if(e.includes("failed:")){const d=e.match(/failed: .*?: (.*?)(?:\n|$)/);d&&(n=u(d[1]))}return f.error("CdkService","CDK deployment returned failure",{errorMessage:n}),l(n)}function A(e){const t=[e.summary],{resource:s,reason:o}=e.rootCause;if(t.push(`Root cause: ${s.logicalId} \u2014 ${o}`),e.driftSuspects!==void 0&&e.driftSuspects.length>0){const c=e.driftSuspects.map(r=>`${r.logicalId} (${r.resourceType})`).join(", ");t.push(`Out-of-band deletion suspected: ${c}. Run 'fjall drift detect' to confirm; confirmed deletions are remediated with 'fjall drift repair'.`)}return t.join(`
2
+ `)}function P(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":k(e)}function T(e){return e.success?i({message:"Stack destroyed successfully"}):e.error.includes(m)||e.error.includes(a)?i({message:"Stack already deleted or does not exist",details:{skipped:!0}}):l(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 o=e.match(/arn:aws:cloudformation:[^:]+:[^:]+:stack\/([^/]+)\//);return o?{detected:!0,stackName:o[1]}:{detected:!1}}function R(e,t,s,o){return c=>{if(e.cdkOutput+=c,t?.(c),!e.stackDetected){const r=N(c);r.detected&&r.stackName&&(e.actualStackName=r.stackName,e.stackDetected=!0),e.stackDetected&&s&&!e.monitoringPromise&&(e.monitoringPromise=S(s,e.actualStackName,o))}}}export{P as analyseBootstrapError,x as analyseDeployOutput,T as analyseDestroyResult,R as createEnhancedOutputCallback,N as detectStackNameFromChunk,A as formatAnalysedDeployFailure};
@@ -1,3 +1,3 @@
1
- import{existsSync as F}from"fs";import{join as D}from"path";import{logger as S}from"@fjall/util/logger";import{success as C,failure as a}from"@fjall/generator";import{DEFAULT_REGION as h}from"../../aws/utils/regions.js";import{getErrorMessage as k,maskSensitiveOutput as y}from"@fjall/util";import{CdkEventMonitor as R,startStackMonitoring as w}from"./CdkEventMonitoring.js";import{analyseDeployOutput as E,analyseDestroyResult as K,createEnhancedOutputCallback as N}from"./CdkOutputAnalyser.js";import{CdkCommandRunner as O}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as T}from"./CdkArgumentBuilder.js";import{CdkProcessManager as L}from"./CdkProcessManager.js";import{cancelInFlightStack as P}from"./cancelInFlightStack.js";import{isAborted as v}from"../../aws/organisations/types.js";import{wrapWithConstructMapEnrichment as W}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as $,resolveStackName as M,getFallbackStackName as _,buildDeploymentCdkContext as b}from"./cdkServiceHelpers.js";class te{commandRunner;eventMonitor;abortSignal;constructor(e){const r=e?.processManager??new L(new T,e?.abortSignal);this.commandRunner=new O(r),this.eventMonitor=new R({eventLogWriterFactory:e?.eventLogWriterFactory,...e?.onFailureAnalysis!==void 0?{onFailureAnalysis:e.onFailureAnalysis}:{}}),this.abortSignal=e?.abortSignal}dispose(){this.commandRunner.dispose()}async checkDifferences(e,r,t){return this.commandRunner.checkDifferences(e,r,t)}async deploy(e,r,t){return this.commandRunner.deploy(e,r,t)}async destroy(e,r,t){return this.commandRunner.destroy(e,r,t)}async runImport(e,r,t){return this.commandRunner.runImport(e,r,t)}async synth(e,r){return this.commandRunner.synth(e,r)}async bootstrap(e,r,t){return this.commandRunner.bootstrap(e,r,t)}async runCdkSynth(e,r,t){const n=e.callerIdentity?.Account;try{const o=await this.synth(e.path,{outputCallback:r,context:b(e,n,e.region||h),...e.assemblyDir!==void 0?{outputDir:e.assemblyDir}:{},...t!==void 0?{credentials:t}:{}});return o.success?C({message:"CloudFormation template synthesised",details:o.data.output?{synthesisTime:o.data.output}:void 0}):a(o.error||"Failed to synthesise CloudFormation template")}catch(o){return a(`CDK synth failed: ${y(k(o))}`)}}async runCdkBootstrap(e,r,t,n){const o=e.callerIdentity?.Account,f=e.region||h;try{if(!o)return a("No AWS account ID available");const c=D(e.path,"node_modules");if(!F(c))return a(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const l=await this.bootstrap(o,f,{outputCallback:r,credentials:t,...n!==void 0&&n!==""?{permissionsBoundary:n}:{}});return l.success?C({message:"AWS environment bootstrapped"}):a(l.error||"Failed to bootstrap AWS environment")}catch(c){return a(`CDK bootstrap failed: ${y(k(c))}`)}}async runCdkDiff(e,r){const t=e.callerIdentity?.Account;try{const n=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:r,context:b(e,t,e.region||h)});return n.success?C({message:"Diff check complete",details:{hasDifferences:n.data.hasDifferences,details:n.data.details}}):a(`CDK diff failed: ${n.error.message}`)}catch(n){return a(`CDK diff failed: ${y(k(n))}`)}}async runCdkDeploy(e,r,t,n,o,f,c){const l=e.callerIdentity?.Account,p=e.region||h;if(!l)return a("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!o)return a("AwsProvider is required for deployment monitoring.");const m=e.assemblyDir??D(e.path,"cdk.out"),d=W(m,n);let u=null,g;try{const i=M(r,e)??_(e);u=await this.eventMonitor.createEventMonitor("deploy",i,p,e,o),t&&(t(y(`Starting CloudFormation deployment of ${i}...
2
- `)),t(`Monitoring CloudFormation events (CDK process running in background)...
3
- `));const s={cdkOutput:"",actualStackName:i,stackDetected:!1,monitoringPromise:null},A=N(s,t,u,d);g=setTimeout(()=>{!s.stackDetected&&u&&!s.monitoringPromise&&(S.debug("CdkService","Fallback monitoring STARTING",{targetStackName:i,stackDetected:s.stackDetected,hasOnResourceProgress:!!n}),s.monitoringPromise=w(u,i,d))},$);const I=await this.deploy(e.path,i,{verbose:e.options?.verbose,outputCallback:A,...e.assemblyDir!==void 0?{appDir:e.assemblyDir}:{useCdkOut:!0},cdkOutputLogger:u?.getEventLogger()??void 0,context:b(e,l,p),credentials:f,...c!==void 0&&Object.keys(c).length>0&&{parameters:c}});return v(this.abortSignal)?(await P(i,p,f,t),a("Deployment cancelled")):E(s.cdkOutput,I,s.actualStackName)}catch(i){const s=`CDK deploy failed: ${y(k(i))}`;return S.error("CdkService","CDK deployment exception",{error:s}),a(s)}finally{clearTimeout(g),u&&u.stopMonitoring()}}async runCdkDestroy(e,r,t,n,o,f,c){const l=e.callerIdentity?.Account,p=e.region||h;let m=null;try{const d=M(r,e);l&&d&&o&&(m=await this.eventMonitor.createEventMonitor("destroy",d,p,e,o),m.startMonitoring(d,g=>{n?.(g)},(g,i)=>{}));const u=await this.destroy(e.path,r,{verbose:e.options?.verbose,outputCallback:t,useCdkOut:f,cdkOutputLogger:m?.getEventLogger()??void 0,context:b(e,l,p),credentials:c});return v(this.abortSignal)?a("Destroy cancelled"):K(u)}catch(d){return a(`CDK destroy failed: ${y(k(d))}`)}finally{m&&m.stopMonitoring()}}}export{te as CdkService};
1
+ import{existsSync as N}from"fs";import{join as A}from"path";import{logger as I}from"@fjall/util/logger";import{success as v,failure as o}from"@fjall/generator";import{DEFAULT_REGION as b}from"../../aws/utils/regions.js";import{getErrorMessage as D,maskSensitiveOutput as g}from"@fjall/util";import{CdkEventMonitor as E,settleMonitoringAnalysis as T,startStackMonitoring as O}from"./CdkEventMonitoring.js";import{analyseDeployOutput as K,analyseDestroyResult as L,createEnhancedOutputCallback as P,formatAnalysedDeployFailure as W}from"./CdkOutputAnalyser.js";import{recordDriftSuspects as $}from"../../orchestration/drift/driftSuspectJournal.js";import{CdkCommandRunner as _}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as j}from"./CdkArgumentBuilder.js";import{CdkProcessManager as G}from"./CdkProcessManager.js";import{cancelInFlightStack as q}from"./cancelInFlightStack.js";import{isAborted as M}from"../../aws/organisations/types.js";import{wrapWithConstructMapEnrichment as B}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as U,resolveStackName as F,getFallbackStackName as z,buildDeploymentCdkContext as C}from"./cdkServiceHelpers.js";class le{commandRunner;eventMonitor;abortSignal;constructor(e){const t=e?.processManager??new G(new j,e?.abortSignal);this.commandRunner=new _(t),this.eventMonitor=new E({eventLogWriterFactory:e?.eventLogWriterFactory,...e?.onFailureAnalysis!==void 0?{onFailureAnalysis:e.onFailureAnalysis}:{}}),this.abortSignal=e?.abortSignal}dispose(){this.commandRunner.dispose()}async checkDifferences(e,t,r){return this.commandRunner.checkDifferences(e,t,r)}async deploy(e,t,r){return this.commandRunner.deploy(e,t,r)}async destroy(e,t,r){return this.commandRunner.destroy(e,t,r)}async runImport(e,t,r){return this.commandRunner.runImport(e,t,r)}async synth(e,t){return this.commandRunner.synth(e,t)}async bootstrap(e,t,r){return this.commandRunner.bootstrap(e,t,r)}async runCdkSynth(e,t,r){const n=e.callerIdentity?.Account;try{const a=await this.synth(e.path,{outputCallback:t,context:C(e,n,e.region||b),...e.assemblyDir!==void 0?{outputDir:e.assemblyDir}:{},...r!==void 0?{credentials:r}:{}});return a.success?v({message:"CloudFormation template synthesised",details:a.data.output?{synthesisTime:a.data.output}:void 0}):o(a.error||"Failed to synthesise CloudFormation template")}catch(a){return o(`CDK synth failed: ${g(D(a))}`)}}async runCdkBootstrap(e,t,r,n){const a=e.callerIdentity?.Account,p=e.region||b;try{if(!a)return o("No AWS account ID available");const l=A(e.path,"node_modules");if(!N(l))return o(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const u=await this.bootstrap(a,p,{outputCallback:t,credentials:r,...n!==void 0&&n!==""?{permissionsBoundary:n}:{}});return u.success?v({message:"AWS environment bootstrapped"}):o(u.error||"Failed to bootstrap AWS environment")}catch(l){return o(`CDK bootstrap failed: ${g(D(l))}`)}}async runCdkDiff(e,t){const r=e.callerIdentity?.Account;try{const n=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:t,context:C(e,r,e.region||b)});return n.success?v({message:"Diff check complete",details:{hasDifferences:n.data.hasDifferences,details:n.data.details}}):o(`CDK diff failed: ${n.error.message}`)}catch(n){return o(`CDK diff failed: ${g(D(n))}`)}}async runCdkDeploy(e,t,r,n,a,p,l){const u=e.callerIdentity?.Account,m=e.region||b;if(!u)return o("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!a)return o("AwsProvider is required for deployment monitoring.");const f=e.assemblyDir??A(e.path,"cdk.out"),d=B(f,n);let s=null,h;try{const c=F(t,e)??z(e);s=await this.eventMonitor.createEventMonitor("deploy",c,m,e,a),r&&(r(g(`Starting CloudFormation deployment of ${c}...
2
+ `)),r(`Monitoring CloudFormation events (CDK process running in background)...
3
+ `));const i={cdkOutput:"",actualStackName:c,stackDetected:!1,monitoringPromise:null},R=P(i,r,s,d);h=setTimeout(()=>{!i.stackDetected&&s&&!i.monitoringPromise&&(I.debug("CdkService","Fallback monitoring STARTING",{targetStackName:c,stackDetected:i.stackDetected,hasOnResourceProgress:!!n}),i.monitoringPromise=O(s,c,d))},U);const w=await this.deploy(e.path,c,{verbose:e.options?.verbose,outputCallback:R,...e.assemblyDir!==void 0?{appDir:e.assemblyDir}:{useCdkOut:!0},cdkOutputLogger:s?.getEventLogger()??void 0,context:C(e,u,m),credentials:p,...l!==void 0&&Object.keys(l).length>0&&{parameters:l}});if(M(this.abortSignal))return await q(c,m,p,r),o("Deployment cancelled");const S=K(i.cdkOutput,w,i.actualStackName);if(S.success)return S;const k=await T(s,i.monitoringPromise);return k===null?S:(k.driftSuspects!==void 0&&k.driftSuspects.length>0&&await $({accountId:u,region:m,stackName:i.actualStackName,recordedAt:new Date().toISOString(),suspects:k.driftSuspects.map(y=>({stackName:i.actualStackName,logicalId:y.logicalId,resourceType:y.resourceType,...y.physicalId!==void 0?{physicalId:y.physicalId}:{},failedStatus:y.failedStatus,statusReason:y.statusReason}))}),o(W(k)))}catch(c){const i=`CDK deploy failed: ${g(D(c))}`;return I.error("CdkService","CDK deployment exception",{error:i}),o(i)}finally{clearTimeout(h),s&&s.stopMonitoring()}}async runCdkDestroy(e,t,r,n,a,p,l){const u=e.callerIdentity?.Account,m=e.region||b;let f=null;try{const d=F(t,e);u&&d&&a&&(f=await this.eventMonitor.createEventMonitor("destroy",d,m,e,a),f.startMonitoring(d,h=>{n?.(h)},(h,c)=>{}));const s=await this.destroy(e.path,t,{verbose:e.options?.verbose,outputCallback:r,useCdkOut:p,cdkOutputLogger:f?.getEventLogger()??void 0,context:C(e,u,m),credentials:l});return M(this.abortSignal)?o("Destroy cancelled"):L(s)}catch(d){return o(`CDK destroy failed: ${g(D(d))}`)}finally{f&&f.stopMonitoring()}}}export{le as CdkService};
@@ -0,0 +1,68 @@
1
+ /**
2
+ * CloudFormation registry oracle — resolves the authoritative
3
+ * `createOnlyProperties` / `conditionalCreateOnlyProperties` /
4
+ * `primaryIdentifier` catalogue for a resource type via
5
+ * `cloudformation:DescribeType`.
6
+ *
7
+ * The registry is the ground truth the deploy plan's replacement verdicts are
8
+ * recomputed against: the CDK resource spec bundled into
9
+ * `@aws-cdk/cloudformation-diff` is provably stale (e.g. a false WILL_REPLACE
10
+ * on `AWS::RDS::DBInstance.Port`). Resolution order:
11
+ *
12
+ * in-memory (per run) → disk cache (7-day TTL, atomic writes) → live
13
+ * DescribeType → stale disk cache → baked seed snapshot
14
+ *
15
+ * Failure at every tier degrades — the caller annotates the affected rows
16
+ * `verdictSource: "cdk-spec (unverified)"` and the deploy proceeds; the oracle
17
+ * never fails a deploy by being unavailable.
18
+ */
19
+ import { type Result } from "@fjall/generator";
20
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
21
+ /** Replacement-relevant slice of a registry resource schema. */
22
+ export interface CfnRegistrySchemaSummary {
23
+ typeName: string;
24
+ /** JSON pointers (`/properties/X`, possibly nested) that force replacement. */
25
+ createOnlyProperties: string[];
26
+ /** JSON pointers that MAY force replacement depending on the transition. */
27
+ conditionalCreateOnlyProperties: string[];
28
+ /** JSON pointers identifying the resource (not always the naming property). */
29
+ primaryIdentifier: string[];
30
+ }
31
+ /**
32
+ * The narrow read surface the plan overlay needs — CfnRegistryService
33
+ * satisfies it; tests substitute a stub.
34
+ */
35
+ export interface CfnRegistrySummaryReader {
36
+ getSchemaSummary(typeName: string, abortSignal?: AbortSignal): Promise<Result<CfnRegistrySchemaSummary, Error>>;
37
+ }
38
+ export interface CfnRegistryServiceOptions {
39
+ /** Cache root; defaults to `~/.fjall/cache/cfn-registry`. */
40
+ cacheDir?: string;
41
+ /** Disk-cache freshness window; defaults to 7 days. */
42
+ cacheTtlMs?: number;
43
+ /** Baked seed override (defaults to the generated snapshot). */
44
+ seed?: Readonly<Record<string, CfnRegistrySchemaSummary>>;
45
+ /** Injectable clock for TTL tests. */
46
+ now?: () => number;
47
+ }
48
+ export declare class CfnRegistryService implements CfnRegistrySummaryReader {
49
+ private readonly aws;
50
+ private readonly cacheDir;
51
+ private readonly cacheTtlMs;
52
+ private readonly seed;
53
+ private readonly now;
54
+ private readonly memory;
55
+ constructor(aws: AwsProvider, options?: CfnRegistryServiceOptions);
56
+ getSchemaSummary(typeName: string, abortSignal?: AbortSignal): Promise<Result<CfnRegistrySchemaSummary, Error>>;
57
+ private isFresh;
58
+ private cachePath;
59
+ private describeType;
60
+ /** A corrupt, missing, or unreadable cache file is a miss — never a crash. */
61
+ private readCache;
62
+ /**
63
+ * Atomic tmp + rename — the cache dir can be shared by concurrent deploys
64
+ * (CLI sessions, worker containers), so a reader must never see a partial
65
+ * write. Failures are best-effort: logged and discarded.
66
+ */
67
+ private writeCache;
68
+ }
@@ -0,0 +1 @@
1
+ import{mkdir as y,readFile as l,rename as g,writeFile as p}from"node:fs/promises";import{homedir as w}from"node:os";import{join as u}from"node:path";import{CloudFormationClient as S,DescribeTypeCommand as b}from"@aws-sdk/client-cloudformation";import{z as a}from"zod";import{success as c,failure as h}from"@fjall/generator";import{getErrorMessage as f,maskSensitiveOutput as n}from"@fjall/util";import{logger as o}from"@fjall/util/logger";import{composeSdkAbortSignal as C}from"../../aws/organisations/types.js";import{CFN_REGISTRY_SEED as D}from"./cfnRegistrySeed.js";const d="CfnRegistryService",O=a.object({createOnlyProperties:a.array(a.string()).optional(),conditionalCreateOnlyProperties:a.array(a.string()).optional(),primaryIdentifier:a.array(a.string()).optional()}),P=a.object({typeName:a.string(),createOnlyProperties:a.array(a.string()),conditionalCreateOnlyProperties:a.array(a.string()),primaryIdentifier:a.array(a.string())}).strict(),T=a.object({cachedAt:a.string(),registryLastUpdated:a.string().optional(),summary:P}).strict(),E=10080*60*1e3;class _{aws;cacheDir;cacheTtlMs;seed;now;memory=new Map;constructor(e,t){this.aws=e,this.cacheDir=t?.cacheDir??u(w(),".fjall","cache","cfn-registry"),this.cacheTtlMs=t?.cacheTtlMs??E,this.seed=t?.seed??D,this.now=t?.now??(()=>Date.now())}async getSchemaSummary(e,t){const s=this.memory.get(e);if(s!==void 0)return c(s);const r=await this.readCache(e);if(r!==void 0&&this.isFresh(r))return this.memory.set(e,r.summary),c(r.summary);const i=await this.describeType(e,t);if(i.success)return this.memory.set(e,i.data.summary),await this.writeCache(e,i.data),c(i.data.summary);if(r!==void 0)return o.warn(d,"Live DescribeType failed \u2014 serving stale cache",{typeName:e,error:n(i.error.message)}),this.memory.set(e,r.summary),c(r.summary);const m=this.seed[e];return m!==void 0?(o.warn(d,"Live DescribeType failed \u2014 serving baked seed",{typeName:e,error:n(i.error.message)}),this.memory.set(e,m),c(m)):h(i.error)}isFresh(e){const t=Date.parse(e.cachedAt);return Number.isNaN(t)?!1:this.now()-t<=this.cacheTtlMs}cachePath(e){const t=`${e.replaceAll("::","-")}.json`;return u(this.cacheDir,this.aws.getRegion(),t)}async describeType(e,t){try{const r=await this.aws.getClient(S).send(new b({Type:"RESOURCE",TypeName:e}),{abortSignal:C(t)});if(r.Schema===void 0||r.Schema==="")return h(new Error(`DescribeType returned no schema for ${e}`));const i=O.safeParse(JSON.parse(r.Schema));return i.success?c({cachedAt:new Date(this.now()).toISOString(),...r.LastUpdated!==void 0?{registryLastUpdated:r.LastUpdated.toISOString()}:{},summary:{typeName:e,createOnlyProperties:i.data.createOnlyProperties??[],conditionalCreateOnlyProperties:i.data.conditionalCreateOnlyProperties??[],primaryIdentifier:i.data.primaryIdentifier??[]}}):h(new Error(`Unparsable registry schema for ${e}: ${n(i.error.message)}`))}catch(s){return h(new Error(`DescribeType failed for ${e}: ${n(f(s))}`))}}async readCache(e){const t=this.cachePath(e);try{const s=await l(t,"utf8"),r=T.safeParse(JSON.parse(s));if(!r.success){o.debug(d,"Discarding unparsable registry cache entry",{path:t,error:n(r.error.message)});return}return r.data}catch(s){o.debug(d,"Registry cache read missed",{path:t,error:n(f(s))});return}}async writeCache(e,t){const s=this.cachePath(e);try{await y(u(this.cacheDir,this.aws.getRegion()),{recursive:!0});const r=`${s}.tmp-${process.pid}`;await p(r,JSON.stringify(t,null,2),"utf8"),await g(r,s)}catch(r){o.debug(d,"Registry cache write failed",{path:s,error:n(f(r))})}}}export{_ as CfnRegistryService};
@@ -41,9 +41,12 @@ export declare class CloudFormationService {
41
41
  */
42
42
  getStackOutputs(stackName: string, callbacks?: CloudFormationCallbacks): Promise<Result<CloudFormationOutput[], CloudFormationError>>;
43
43
  /**
44
- * Get stack status directly
44
+ * Get stack status directly. When the caller carries an abort signal
45
+ * (deploy-path pre-flight under a cancellable deploy), it is composed with
46
+ * the SDK timeout; without one the send stays bare so existing polling
47
+ * callers keep their own timeout semantics.
45
48
  */
46
- getStackStatus(stackName: string, callbacks?: CloudFormationCallbacks): Promise<Result<CloudFormationStackStatus | null, CloudFormationError>>;
49
+ getStackStatus(stackName: string, callbacks?: CloudFormationCallbacks, abortSignal?: AbortSignal): Promise<Result<CloudFormationStackStatus | null, CloudFormationError>>;
47
50
  /**
48
51
  * Internal helper to list all CloudFormation exports with pagination.
49
52
  */
@@ -1 +1 @@
1
- import{CloudFormationClient as f,DeleteStackCommand as _,DescribeStacksCommand as m,GetTemplateCommand as A,ListExportsCommand as O,UpdateStackCommand as P}from"@aws-sdk/client-cloudformation";import{stackStatusMap as T}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as w}from"@fjall/util";import{success as d,failure as c}from"@fjall/generator";import{BaseServiceError as $}from"../../types/errors/ServiceError.js";import{logger as M}from"@fjall/util/logger";import{getErrorMessage as S,sleep as E}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as g}from"@fjall/util/aws";import{isCleanableState as F}from"../../types/constants.js";import{composeSdkAbortSignal as C,extractErrorName as R,isAborted as D}from"../../aws/organisations/types.js";class l extends ${errorType;stackName;stackStatus;constructor(e,r,t,n,s,a=!1){super(`CFN_${r.toUpperCase()}`,e,s,a),this.errorType=r,this.stackName=t,this.stackStatus=n}}class Y{aws;constructor(e){this.aws=e}classifyAwsError(e,r,t){const n=R(e),s=w(S(e));return n==="CredentialsError"||n==="UnauthorizedError"?new l(`AWS credentials error: ${s}`,"auth_error",t,void 0,e,!1):n==="Throttling"||n==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${s}`,"throttled",t,void 0,e,!0):n==="NetworkingError"||n==="ENOTFOUND"?new l(`Network error: ${s}`,"network_error",t,void 0,e,!0):new l(`${r}: ${s}`,"unknown",t,void 0,e)}async getStackOutputs(e,r){r?.onStackCheck?.(e);const t=this.aws.getClient(f),n=new m({StackName:e});try{const a=(await t.send(n)).Stacks?.[0];if(!a?.Outputs)return d([]);const o=a.Outputs.map(u=>({OutputKey:u.OutputKey,OutputValue:u.OutputValue,ExportName:u.ExportName}));return r?.onOutputsRetrieved?.(e,o.length),d(o)}catch(s){if(s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(g))return r?.onStackNotFound?.(e),d([]);const a=w(S(s));return c(new l(`Failed to get outputs for stack ${e}: ${a}`,"unknown",e,void 0,s))}}async getStackStatus(e,r){r?.onStackCheck?.(e);const t=this.aws.getClient(f),n=new m({StackName:e});try{const a=(await t.send(n)).Stacks?.[0];if(!a)return d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const o=a.StackStatus||"UNKNOWN",u=T[o]||T.UNKNOWN;return r?.onStackFound?.(e,o),d({status:o,safeToRedeploy:u.safeToRedeploy,description:u.description,statusReason:a.StackStatusReason})}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(g)?d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(s,`Failed to get stack status for ${e}`,e))}}async listAllExports(e){const r=this.aws.getClient(f),t=[];try{let n;do{const s=new O({NextToken:n}),a=await r.send(s),o=a.Exports||[];for(const u of o)u.Name&&u.Value&&t.push({Name:u.Name,Value:u.Value});if(e?.(o))break;n=a.NextToken}while(n);return d(t)}catch(n){const s=w(S(n));return c(new l(`Failed to list exports: ${s}`,"unknown",void 0,void 0,n,!1))}}async getExportsByNames(e){if(e.length===0)return d(new Map);const r=new Set(e),t=new Map,n=await this.listAllExports(s=>{for(const a of s)a.Name&&r.has(a.Name)&&a.Value&&t.set(a.Name,a.Value);return t.size>=r.size});return n.success?d(t):c(n.error)}async listExports(e){const r=await this.listAllExports();return r.success&&e?.onExportsRetrieved?.(r.data.length),r}async deleteStack(e){const r=this.aws.getClient(f);try{return await r.send(new _({StackName:e})),d(void 0)}catch(t){return c(this.classifyAwsError(t,`Failed to delete stack ${e}`,e))}}async stackExists(e,r){const t=r??this.aws.getClient(f);try{const s=(await t.send(new m({StackName:e}))).Stacks?.[0]?.StackStatus;return!!s&&s!=="REVIEW_IN_PROGRESS"&&!F(s)}catch(n){return n instanceof Error&&n.message?.includes(g)?!1:(M.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:e,error:S(n)}),!0)}}async getTemplate(e){const r=this.aws.getClient(f);try{const t=await r.send(new A({StackName:e,TemplateStage:"Original"}));return d(t.TemplateBody??"")}catch(t){return t instanceof Error&&t.message?.includes(g)?c(new l(`Stack ${e} not found`,"stack_not_found",e)):c(this.classifyAwsError(t,`Failed to get template for stack ${e}`,e))}}async waitForDeleteComplete(e,r){const t=r?.timeoutMs??6e5,n=r?.pollIntervalMs??5e3,s=Date.now();for(;Date.now()-s<t;){const a=await this.getStackStatus(e);if(!a.success){if(a.error.recoverable){r?.onProgress?.(`Transient error polling stack ${e}, retrying: ${w(a.error.message)}`),await E(n);continue}return c(a.error)}const o=a.data?.status;if(o==="DELETE_COMPLETE"||o==="DOES_NOT_EXIST")return d(void 0);if(o==="DELETE_FAILED")return c(new l(`Stack ${e} deletion failed: ${a.data?.statusReason||"unknown reason"}`,"stack_failed",e,o,void 0,!1));r?.onProgress?.(`Stack ${e} status: ${o??"unknown"}`),await E(n)}return c(new l(`Timed out waiting for stack ${e} deletion after ${Math.round(t/1e3)}s`,"timeout",e,void 0,void 0,!0))}async updateStackParameters(e,r,t){const n=this.aws.getClient(f);let s;try{s=await n.send(new m({StackName:e}),{abortSignal:C(t)})}catch(i){return c(this.classifyAwsError(i,`Failed to describe stack ${e} before parameter reconcile`,e))}const a=s.Stacks?.[0];if(!a)return c(new l(`Stack ${e} not found`,"stack_not_found",e));const o=a.Parameters??[],u=new Set(o.map(i=>i.ParameterKey).filter(i=>i!==void 0)),k=Object.entries(r),h=k.filter(([i])=>!u.has(i)).map(([i])=>i),y=new Map(k.filter(([i])=>u.has(i)));if(y.size===0)return d({updated:!1,skippedKeys:h});const x=o.filter(i=>i.ParameterKey!==void 0).map(i=>y.has(i.ParameterKey)?{ParameterKey:i.ParameterKey,ParameterValue:y.get(i.ParameterKey)}:{ParameterKey:i.ParameterKey,UsePreviousValue:!0});try{await n.send(new P({StackName:e,UsePreviousTemplate:!0,Parameters:x,Capabilities:["CAPABILITY_IAM","CAPABILITY_NAMED_IAM"]}),{abortSignal:C(t)})}catch(i){return i instanceof Error&&i.name==="ValidationError"&&i.message?.includes("No updates are to be performed")?d({updated:!1,skippedKeys:h}):c(this.classifyAwsError(i,`Failed to update parameters for stack ${e}`,e))}return this.waitForUpdateStackParametersComplete(e,h,t)}async waitForUpdateStackParametersComplete(e,r,t){const a=Date.now();for(;Date.now()-a<6e5;){if(D(t))return c(new l(`Aborted waiting for stack ${e} parameter update`,"timeout",e,void 0,void 0,!0));const o=await this.getStackStatus(e);if(!o.success){if(o.error.recoverable){await E(5e3);continue}return c(o.error)}const u=o.data?.status??"UNKNOWN";if(u==="UPDATE_COMPLETE")return d({updated:!0,skippedKeys:r});if(I(u))return c(new l(`Stack ${e} parameter update failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",e,u,void 0,!1));await E(5e3)}return c(new l(`Timed out waiting for stack ${e} parameter update after ${Math.round(6e5/1e3)}s`,"timeout",e,void 0,void 0,!0))}}function I(p){return p.endsWith("_IN_PROGRESS")?!1:p.startsWith("UPDATE_ROLLBACK_")||p==="UPDATE_FAILED"}export{l as CloudFormationError,Y as CloudFormationService};
1
+ import{CloudFormationClient as f,DeleteStackCommand as _,DescribeStacksCommand as w,GetTemplateCommand as A,ListExportsCommand as O,UpdateStackCommand as P}from"@aws-sdk/client-cloudformation";import{stackStatusMap as C}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as S}from"@fjall/util";import{success as d,failure as c}from"@fjall/generator";import{BaseServiceError as $}from"../../types/errors/ServiceError.js";import{logger as M}from"@fjall/util/logger";import{getErrorMessage as E,sleep as g}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as h}from"@fjall/util/aws";import{isCleanableState as F}from"../../types/constants.js";import{composeSdkAbortSignal as T,extractErrorName as R,isAborted as D}from"../../aws/organisations/types.js";class l extends ${errorType;stackName;stackStatus;constructor(e,r,t,n,a,s=!1){super(`CFN_${r.toUpperCase()}`,e,a,s),this.errorType=r,this.stackName=t,this.stackStatus=n}}class Y{aws;constructor(e){this.aws=e}classifyAwsError(e,r,t){const n=R(e),a=S(E(e));return n==="CredentialsError"||n==="UnauthorizedError"?new l(`AWS credentials error: ${a}`,"auth_error",t,void 0,e,!1):n==="Throttling"||n==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${a}`,"throttled",t,void 0,e,!0):n==="NetworkingError"||n==="ENOTFOUND"?new l(`Network error: ${a}`,"network_error",t,void 0,e,!0):new l(`${r}: ${a}`,"unknown",t,void 0,e)}async getStackOutputs(e,r){r?.onStackCheck?.(e);const t=this.aws.getClient(f),n=new w({StackName:e});try{const s=(await t.send(n)).Stacks?.[0];if(!s?.Outputs)return d([]);const o=s.Outputs.map(u=>({OutputKey:u.OutputKey,OutputValue:u.OutputValue,ExportName:u.ExportName}));return r?.onOutputsRetrieved?.(e,o.length),d(o)}catch(a){if(a instanceof Error&&a.name==="ValidationError"&&a.message?.includes(h))return r?.onStackNotFound?.(e),d([]);const s=S(E(a));return c(new l(`Failed to get outputs for stack ${e}: ${s}`,"unknown",e,void 0,a))}}async getStackStatus(e,r,t){r?.onStackCheck?.(e);const n=this.aws.getClient(f),a=new w({StackName:e});try{const o=(t!==void 0?await n.send(a,{abortSignal:T(t)}):await n.send(a)).Stacks?.[0];if(!o)return d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const u=o.StackStatus||"UNKNOWN",m=C[u]||C.UNKNOWN;return r?.onStackFound?.(e,u),d({status:u,safeToRedeploy:m.safeToRedeploy,description:m.description,statusReason:o.StackStatusReason})}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(h)?d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(s,`Failed to get stack status for ${e}`,e))}}async listAllExports(e){const r=this.aws.getClient(f),t=[];try{let n;do{const a=new O({NextToken:n}),s=await r.send(a),o=s.Exports||[];for(const u of o)u.Name&&u.Value&&t.push({Name:u.Name,Value:u.Value});if(e?.(o))break;n=s.NextToken}while(n);return d(t)}catch(n){const a=S(E(n));return c(new l(`Failed to list exports: ${a}`,"unknown",void 0,void 0,n,!1))}}async getExportsByNames(e){if(e.length===0)return d(new Map);const r=new Set(e),t=new Map,n=await this.listAllExports(a=>{for(const s of a)s.Name&&r.has(s.Name)&&s.Value&&t.set(s.Name,s.Value);return t.size>=r.size});return n.success?d(t):c(n.error)}async listExports(e){const r=await this.listAllExports();return r.success&&e?.onExportsRetrieved?.(r.data.length),r}async deleteStack(e){const r=this.aws.getClient(f);try{return await r.send(new _({StackName:e})),d(void 0)}catch(t){return c(this.classifyAwsError(t,`Failed to delete stack ${e}`,e))}}async stackExists(e,r){const t=r??this.aws.getClient(f);try{const a=(await t.send(new w({StackName:e}))).Stacks?.[0]?.StackStatus;return!!a&&a!=="REVIEW_IN_PROGRESS"&&!F(a)}catch(n){return n instanceof Error&&n.message?.includes(h)?!1:(M.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:e,error:E(n)}),!0)}}async getTemplate(e){const r=this.aws.getClient(f);try{const t=await r.send(new A({StackName:e,TemplateStage:"Original"}));return d(t.TemplateBody??"")}catch(t){return t instanceof Error&&t.message?.includes(h)?c(new l(`Stack ${e} not found`,"stack_not_found",e)):c(this.classifyAwsError(t,`Failed to get template for stack ${e}`,e))}}async waitForDeleteComplete(e,r){const t=r?.timeoutMs??6e5,n=r?.pollIntervalMs??5e3,a=Date.now();for(;Date.now()-a<t;){const s=await this.getStackStatus(e);if(!s.success){if(s.error.recoverable){r?.onProgress?.(`Transient error polling stack ${e}, retrying: ${S(s.error.message)}`),await g(n);continue}return c(s.error)}const o=s.data?.status;if(o==="DELETE_COMPLETE"||o==="DOES_NOT_EXIST")return d(void 0);if(o==="DELETE_FAILED")return c(new l(`Stack ${e} deletion failed: ${s.data?.statusReason||"unknown reason"}`,"stack_failed",e,o,void 0,!1));r?.onProgress?.(`Stack ${e} status: ${o??"unknown"}`),await g(n)}return c(new l(`Timed out waiting for stack ${e} deletion after ${Math.round(t/1e3)}s`,"timeout",e,void 0,void 0,!0))}async updateStackParameters(e,r,t){const n=this.aws.getClient(f);let a;try{a=await n.send(new w({StackName:e}),{abortSignal:T(t)})}catch(i){return c(this.classifyAwsError(i,`Failed to describe stack ${e} before parameter reconcile`,e))}const s=a.Stacks?.[0];if(!s)return c(new l(`Stack ${e} not found`,"stack_not_found",e));const o=s.Parameters??[],u=new Set(o.map(i=>i.ParameterKey).filter(i=>i!==void 0)),m=Object.entries(r),y=m.filter(([i])=>!u.has(i)).map(([i])=>i),k=new Map(m.filter(([i])=>u.has(i)));if(k.size===0)return d({updated:!1,skippedKeys:y});const x=o.filter(i=>i.ParameterKey!==void 0).map(i=>k.has(i.ParameterKey)?{ParameterKey:i.ParameterKey,ParameterValue:k.get(i.ParameterKey)}:{ParameterKey:i.ParameterKey,UsePreviousValue:!0});try{await n.send(new P({StackName:e,UsePreviousTemplate:!0,Parameters:x,Capabilities:["CAPABILITY_IAM","CAPABILITY_NAMED_IAM"]}),{abortSignal:T(t)})}catch(i){return i instanceof Error&&i.name==="ValidationError"&&i.message?.includes("No updates are to be performed")?d({updated:!1,skippedKeys:y}):c(this.classifyAwsError(i,`Failed to update parameters for stack ${e}`,e))}return this.waitForUpdateStackParametersComplete(e,y,t)}async waitForUpdateStackParametersComplete(e,r,t){const s=Date.now();for(;Date.now()-s<6e5;){if(D(t))return c(new l(`Aborted waiting for stack ${e} parameter update`,"timeout",e,void 0,void 0,!0));const o=await this.getStackStatus(e);if(!o.success){if(o.error.recoverable){await g(5e3);continue}return c(o.error)}const u=o.data?.status??"UNKNOWN";if(u==="UPDATE_COMPLETE")return d({updated:!0,skippedKeys:r});if(I(u))return c(new l(`Stack ${e} parameter update failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",e,u,void 0,!1));await g(5e3)}return c(new l(`Timed out waiting for stack ${e} parameter update after ${Math.round(6e5/1e3)}s`,"timeout",e,void 0,void 0,!0))}}function I(p){return p.endsWith("_IN_PROGRESS")?!1:p.startsWith("UPDATE_ROLLBACK_")||p==="UPDATE_FAILED"}export{l as CloudFormationError,Y as CloudFormationService};
@@ -17,6 +17,26 @@ export interface CfnExportsClient {
17
17
  message: string;
18
18
  }>>;
19
19
  }
20
+ /**
21
+ * Case-insensitive match of `appName` against a CloudFormation export name,
22
+ * anchored to PascalCase token boundaries: the match must start at a token
23
+ * start (position 0, an uppercase letter, or after a non-alphanumeric
24
+ * separator) and end at a token end (end of string, before an uppercase
25
+ * letter, or before a non-alphanumeric separator). Digits continue a token,
26
+ * so app "app" does not match "App2Compute".
27
+ *
28
+ * This keeps interior token-runs matching (app "webapp" matches
29
+ * "FjallWebAppCompute…") while rejecting the cross-app collisions an
30
+ * unanchored substring produced: app "api" no longer matches
31
+ * "StandardsapiCompute…" (mid-token) or "ApiserverCompute…" (token
32
+ * continues). Known residual: an app whose name equals a sibling's full
33
+ * leading token-run still collides ("standards" vs "StandardsApiCompute…") —
34
+ * the manifest-based exact variant is the complete fix when manifest data
35
+ * exists.
36
+ *
37
+ * @internal exported for direct unit testing only
38
+ */
39
+ export declare function matchesAppNameToken(exportName: string, appName: string): boolean;
20
40
  /**
21
41
  * Resolves deployable ECS clusters and services from CloudFormation exports.
22
42
  * Constructor-injected with a CFN exports client so the resolver does not
@@ -42,9 +62,9 @@ export declare class EcsServiceResolver {
42
62
  serviceArns: string[];
43
63
  }, EcsError>>;
44
64
  /**
45
- * Fallback resolution by app name using substring matching on
46
- * CloudFormation exports. Prefer the manifest-based variant when manifest
47
- * data is available.
65
+ * Fallback resolution by app name using token-boundary matching on
66
+ * CloudFormation exports (see `matchesAppNameToken`). Prefer the
67
+ * manifest-based variant when manifest data is available.
48
68
  *
49
69
  * Export naming convention from infrastructure:
50
70
  * Cluster: `${clusterName}DeployableCluster`
@@ -1 +1 @@
1
- import{success as n}from"@fjall/generator";import{logger as l}from"@fjall/util/logger";import{getErrorMessage as v,maskSensitiveOutput as d}from"@fjall/util";import{extractEcsServiceParts as f,buildArn as p}from"../../aws/utils/arnParser.js";class E{cfService;constructor(o){this.cfService=o}async getDeployableClusterAndServicesByManifest(o){try{const r=[];for(const e of o.clusters){r.push(`${e.name}DeployableCluster`);for(const s of e.services)r.push(`${e.name}${s.name}DeployableService`)}if(r.length===0)return n({serviceArns:[]});const i=await this.cfService.getExportsByNames(r);if(!i.success)return l.debug("EcsServiceResolver","Failed to get CloudFormation exports by name",{error:i.error.message}),n({serviceArns:[]});const u=i.data;let c;for(const e of o.clusters){const s=u.get(`${e.name}DeployableCluster`);if(s){c=s;break}}const t=[];for(const e of o.clusters)for(const s of e.services){const a=u.get(`${e.name}${s.name}DeployableService`);a&&t.push(a)}return l.debug("EcsServiceResolver",`Manifest-based discovery: found ${t.length} service(s), cluster=${c||"not found"}`),n({clusterArn:c,serviceArns:t})}catch(r){return l.warn("EcsServiceResolver","Manifest-based discovery failed",{error:d(v(r))}),n({serviceArns:[]})}}async getDeployableClusterAndServices(o){try{const r=await this.cfService.listExports();if(!r.success)return l.debug("EcsServiceResolver","Failed to list CloudFormation exports",{error:r.error.message}),n({serviceArns:[]});const i=o.toLowerCase(),c=r.data.filter(e=>{const s=e.Name?.toLowerCase()??"";return s.includes(i)&&s.endsWith("deployableservice")}).map(e=>e.Value).filter(e=>e!==void 0);let t;if(c.length>0){const e=f(c[0]);e&&(t=p("ecs",e.region,e.accountId,`cluster/${e.clusterName}`))}return t||(t=r.data.find(s=>{const a=s.Name?.toLowerCase()??"";return a.includes(i)&&a.endsWith("deployablecluster")})?.Value),l.debug("EcsServiceResolver",`Found ${c.length} deployable service(s) for app "${o}" in cluster ${t||"unknown"}`),n({clusterArn:t,serviceArns:c})}catch(r){return l.warn("EcsServiceResolver","Failed to get deployable services",{error:d(v(r))}),n({serviceArns:[]})}}}export{E as EcsServiceResolver};
1
+ import{success as l}from"@fjall/generator";import{logger as a}from"@fjall/util/logger";import{getErrorMessage as v,maskSensitiveOutput as p}from"@fjall/util";import{extractEcsServiceParts as b,buildArn as y}from"../../aws/utils/arnParser.js";function g(u,o){const s=u.toLowerCase(),i=o.toLowerCase();if(i.length===0)return!1;const n=r=>!/[a-zA-Z0-9]/.test(r),t=r=>/[A-Z]/.test(r);let e=s.indexOf(i);for(;e!==-1;){const r=u[e]??"",c=e>0?u[e-1]??"":"",f=e===0||t(r)||n(c),m=e+i.length,d=u[m],h=d===void 0||t(d)||n(d);if(f&&h)return!0;e=s.indexOf(i,e+1)}return!1}class C{cfService;constructor(o){this.cfService=o}async getDeployableClusterAndServicesByManifest(o){try{const s=[];for(const r of o.clusters){s.push(`${r.name}DeployableCluster`);for(const c of r.services)s.push(`${r.name}${c.name}DeployableService`)}if(s.length===0)return l({serviceArns:[]});const i=await this.cfService.getExportsByNames(s);if(!i.success)return a.debug("EcsServiceResolver","Failed to get CloudFormation exports by name",{error:i.error.message}),l({serviceArns:[]});const n=i.data;let t;for(const r of o.clusters){const c=n.get(`${r.name}DeployableCluster`);if(c){t=c;break}}const e=[];for(const r of o.clusters)for(const c of r.services){const f=n.get(`${r.name}${c.name}DeployableService`);f&&e.push(f)}return a.debug("EcsServiceResolver",`Manifest-based discovery: found ${e.length} service(s), cluster=${t||"not found"}`),l({clusterArn:t,serviceArns:e})}catch(s){return a.warn("EcsServiceResolver","Manifest-based discovery failed",{error:p(v(s))}),l({serviceArns:[]})}}async getDeployableClusterAndServices(o){try{const s=await this.cfService.listExports();if(!s.success)return a.debug("EcsServiceResolver","Failed to list CloudFormation exports",{error:s.error.message}),l({serviceArns:[]});const n=s.data.filter(e=>{const r=e.Name??"";return g(r,o)&&r.toLowerCase().endsWith("deployableservice")}).map(e=>e.Value).filter(e=>e!==void 0);let t;if(n.length>0){const e=b(n[0]);e&&(t=y("ecs",e.region,e.accountId,`cluster/${e.clusterName}`))}return t||(t=s.data.find(r=>{const c=r.Name??"";return g(c,o)&&c.toLowerCase().endsWith("deployablecluster")})?.Value),a.debug("EcsServiceResolver",`Found ${n.length} deployable service(s) for app "${o}" in cluster ${t||"unknown"}`),l({clusterArn:t,serviceArns:n})}catch(s){return a.warn("EcsServiceResolver","Failed to get deployable services",{error:p(v(s))}),l({serviceArns:[]})}}}export{C as EcsServiceResolver,g as matchesAppNameToken};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Baked CFN registry seed: the last-resort fallback when the live DescribeType
5
+ * call fails and no disk cache exists. Derived from the committed DescribeType
6
+ * fixtures; regenerate with `node scripts/generateCfnRegistrySeed.mjs` after
7
+ * refreshing the fixtures.
8
+ */
9
+ import type { CfnRegistrySchemaSummary } from "./CfnRegistryService.js";
10
+ export declare const CFN_REGISTRY_SEED: Readonly<Record<string, CfnRegistrySchemaSummary>>;
@@ -0,0 +1 @@
1
+ const e={"AWS::Backup::BackupVault":{typeName:"AWS::Backup::BackupVault",createOnlyProperties:["/properties/BackupVaultName","/properties/EncryptionKeyArn"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/BackupVaultName"]},"AWS::CloudTrail::Trail":{typeName:"AWS::CloudTrail::Trail",createOnlyProperties:["/properties/TrailName"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/TrailName"]},"AWS::DynamoDB::Table":{typeName:"AWS::DynamoDB::Table",createOnlyProperties:["/properties/TableName","/properties/ImportSourceSpecification"],conditionalCreateOnlyProperties:["/properties/KeySchema"],primaryIdentifier:["/properties/TableName"]},"AWS::EC2::Volume":{typeName:"AWS::EC2::Volume",createOnlyProperties:[],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/VolumeId"]},"AWS::ECR::Repository":{typeName:"AWS::ECR::Repository",createOnlyProperties:["/properties/RepositoryName","/properties/EncryptionConfiguration","/properties/EncryptionConfiguration/EncryptionType","/properties/EncryptionConfiguration/KmsKey"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/RepositoryName"]},"AWS::IAM::Role":{typeName:"AWS::IAM::Role",createOnlyProperties:["/properties/Path","/properties/RoleName"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/RoleName"]},"AWS::KMS::Alias":{typeName:"AWS::KMS::Alias",createOnlyProperties:["/properties/AliasName"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/AliasName"]},"AWS::KMS::Key":{typeName:"AWS::KMS::Key",createOnlyProperties:[],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/KeyId"]},"AWS::Lambda::Function":{typeName:"AWS::Lambda::Function",createOnlyProperties:["/properties/FunctionName","/properties/PackageType","/properties/TenancyConfig"],conditionalCreateOnlyProperties:["/properties/DurableConfig"],primaryIdentifier:["/properties/FunctionName"]},"AWS::Logs::LogGroup":{typeName:"AWS::Logs::LogGroup",createOnlyProperties:["/properties/LogGroupName"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/LogGroupName"]},"AWS::RDS::DBCluster":{typeName:"AWS::RDS::DBCluster",createOnlyProperties:["/properties/AvailabilityZones","/properties/ClusterScalabilityType","/properties/DBClusterIdentifier","/properties/DBSubnetGroupName","/properties/DBSystemId","/properties/DatabaseName","/properties/EngineMode","/properties/KmsKeyId","/properties/PubliclyAccessible","/properties/RestoreToTime","/properties/RestoreType","/properties/SnapshotIdentifier","/properties/SourceDBClusterIdentifier","/properties/SourceDbClusterResourceId","/properties/SourceRegion","/properties/StorageEncrypted","/properties/UseLatestRestorableTime"],conditionalCreateOnlyProperties:["/properties/Engine","/properties/GlobalClusterIdentifier","/properties/MasterUsername"],primaryIdentifier:["/properties/DBClusterIdentifier"]},"AWS::RDS::DBInstance":{typeName:"AWS::RDS::DBInstance",createOnlyProperties:["/properties/BackupTarget","/properties/CharacterSetName","/properties/CustomIAMInstanceProfile","/properties/DBClusterIdentifier","/properties/DBInstanceIdentifier","/properties/DBName","/properties/DBSubnetGroupName","/properties/DBSystemId","/properties/KmsKeyId","/properties/MasterUsername","/properties/NcharCharacterSetName","/properties/SourceRegion","/properties/StorageEncrypted","/properties/Timezone"],conditionalCreateOnlyProperties:["/properties/AutoMinorVersionUpgrade","/properties/AvailabilityZone","/properties/BackupRetentionPeriod","/properties/DBClusterSnapshotIdentifier","/properties/DBParameterGroupName","/properties/DBSnapshotIdentifier","/properties/Engine","/properties/MultiAZ","/properties/PerformanceInsightsKMSKeyId","/properties/PreferredMaintenanceWindow","/properties/RestoreTime","/properties/SourceDBClusterIdentifier","/properties/SourceDBInstanceAutomatedBackupsArn","/properties/SourceDBInstanceIdentifier","/properties/SourceDbiResourceId","/properties/StorageType","/properties/UseLatestRestorableTime"],primaryIdentifier:["/properties/DBInstanceIdentifier"]},"AWS::RDS::DBProxy":{typeName:"AWS::RDS::DBProxy",createOnlyProperties:["/properties/DBProxyName","/properties/EngineFamily","/properties/EndpointNetworkType","/properties/TargetConnectionNetworkType","/properties/VpcSubnetIds"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/DBProxyName"]},"AWS::RDS::GlobalCluster":{typeName:"AWS::RDS::GlobalCluster",createOnlyProperties:["/properties/GlobalClusterIdentifier","/properties/SourceDBClusterIdentifier","/properties/StorageEncrypted","/properties/Engine"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/GlobalClusterIdentifier"]},"AWS::S3::Bucket":{typeName:"AWS::S3::Bucket",createOnlyProperties:["/properties/BucketName","/properties/BucketNamePrefix","/properties/BucketNamespace"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/BucketName"]},"AWS::SNS::Topic":{typeName:"AWS::SNS::Topic",createOnlyProperties:["/properties/TopicName","/properties/FifoTopic"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/TopicArn"]},"AWS::SQS::Queue":{typeName:"AWS::SQS::Queue",createOnlyProperties:["/properties/FifoQueue","/properties/QueueName"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/QueueUrl"]},"AWS::SecretsManager::Secret":{typeName:"AWS::SecretsManager::Secret",createOnlyProperties:["/properties/Name"],conditionalCreateOnlyProperties:[],primaryIdentifier:["/properties/Id"]}};export{e as CFN_REGISTRY_SEED};
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Change-set-without-execute probe — Layer 2 of the replacement oracle
3
+ * (drift-remediation plan § 2). Creates a review-only CloudFormation change
4
+ * set (`fjall-preflight-<uuid>`, prefix-fenced from the cdk child's own
5
+ * change sets), reads each resource's authoritative `Replacement` verdict,
6
+ * and deletes the change set — nothing is ever executed.
7
+ *
8
+ * Escalation-only [G4]: callers invoke this ONLY for stacks whose registry
9
+ * overlay verdicts `conditional`/`may` on a stateful or pinned-name resource
10
+ * (`applyChangeSetEscalation`). The probe never fails a deploy: every failure
11
+ * degrades to the caller's registry verdict, annotated unverified.
12
+ *
13
+ * Failure messages are pre-masked at construction — a change-set
14
+ * `StatusReason` can embed template fragments, so every interpolation routes
15
+ * through `maskSensitiveOutput`.
16
+ */
17
+ import { type Result } from "@fjall/generator";
18
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
19
+ /** Review-only change sets are fenced off the cdk child's by this prefix. */
20
+ export declare const PREFLIGHT_CHANGE_SET_PREFIX = "fjall-preflight-";
21
+ /**
22
+ * `CreateChangeSet` accepts inline bodies up to 51,200 bytes; larger
23
+ * templates upload to the CDK bootstrap assets bucket and pass `TemplateURL`.
24
+ */
25
+ export declare const MAX_INLINE_TEMPLATE_BYTES = 51200;
26
+ /**
27
+ * The CDK bootstrap staging bucket for the default `hnb659fds` qualifier —
28
+ * where `cdk deploy` itself stages oversized templates, so the deploy
29
+ * principal always has access. Fjall bootstraps with the default qualifier.
30
+ */
31
+ export declare function bootstrapAssetsBucketName(accountId: string, region: string): string;
32
+ /** CloudFormation's authoritative per-resource replacement verdict. */
33
+ export type ChangeSetReplacementVerdict = "True" | "False" | "Conditional";
34
+ export interface StackReplacementProbeResult {
35
+ /** Logical id → CFN's replacement verdict. A resource CFN's change set
36
+ * omits entirely will not be touched — callers treat absence as `False`. */
37
+ replacementByLogicalId: Map<string, ChangeSetReplacementVerdict>;
38
+ /** True when CFN reported the update contains no changes at all (the
39
+ * benign `FAILED — didn't contain changes` outcome). */
40
+ noChanges: boolean;
41
+ }
42
+ export interface ProbeStackReplacementsParams {
43
+ stackName: string;
44
+ /** The already-synthesised cdk.out template, parsed from JSON. */
45
+ template: unknown;
46
+ abortSignal?: AbortSignal;
47
+ }
48
+ /**
49
+ * The narrow surface the plan escalation needs — ChangeSetProbe satisfies
50
+ * it; tests substitute a stub.
51
+ */
52
+ export interface ChangeSetProbeRunner {
53
+ probeStackReplacements(params: ProbeStackReplacementsParams): Promise<Result<StackReplacementProbeResult, Error>>;
54
+ }
55
+ export interface ChangeSetProbeOptions {
56
+ /** Poll cadence for DescribeChangeSet; defaults to 2 s. */
57
+ pollIntervalMs?: number;
58
+ /** Overall poll budget before the probe gives up; defaults to 90 s. */
59
+ pollTimeoutMs?: number;
60
+ /** Injectable clock for leak-sweep and timeout tests. */
61
+ now?: () => number;
62
+ }
63
+ export declare class ChangeSetProbe implements ChangeSetProbeRunner {
64
+ private readonly aws;
65
+ private readonly pollIntervalMs;
66
+ private readonly pollTimeoutMs;
67
+ private readonly now;
68
+ constructor(aws: AwsProvider, options?: ChangeSetProbeOptions);
69
+ probeStackReplacements(params: ProbeStackReplacementsParams): Promise<Result<StackReplacementProbeResult, Error>>;
70
+ /**
71
+ * Poll the change set to a terminal status, then page through its resource
72
+ * changes. `FAILED — didn't contain changes` is benign (the update is a
73
+ * no-op, so nothing can be replaced); any other FAILED is a probe failure.
74
+ */
75
+ private collectVerdicts;
76
+ /**
77
+ * Best-effort startup sweep: delete leaked `fjall-preflight-*` change sets
78
+ * older than one hour (a prior run whose finally-delete never fired). Runs
79
+ * only when a probe engages, so non-escalated deploys pay nothing [G4] — a
80
+ * leak can only exist on a stack a previous run probed. Never fails the
81
+ * probe.
82
+ */
83
+ private sweepLeakedChangeSets;
84
+ /** Stage an oversized template body in the CDK bootstrap assets bucket. */
85
+ private uploadTemplate;
86
+ /** Best-effort cleanup of the staged template — logged, never thrown. */
87
+ private deleteUploadedTemplate;
88
+ }
@@ -0,0 +1 @@
1
+ import{randomUUID as k}from"node:crypto";import{CloudFormationClient as A,CreateChangeSetCommand as N,DeleteChangeSetCommand as I,DescribeChangeSetCommand as L,ListChangeSetsCommand as $}from"@aws-sdk/client-cloudformation";import{DeleteObjectCommand as R,PutObjectCommand as P,S3Client as _}from"@aws-sdk/client-s3";import{success as T,failure as c}from"@fjall/generator";import{getErrorMessage as u,maskSensitiveOutput as p}from"@fjall/util";import{logger as m}from"@fjall/util/logger";import{composeSdkAbortSignal as d,isAborted as h}from"../../aws/organisations/types.js";import{sleepAbortable as B}from"../../util/sleepAbortable.js";const f="ChangeSetProbe",y="fjall-preflight-",D=51200;function M(S,a){return`cdk-hnb659fds-assets-${S}-${a}`}const x=3600*1e3,F=2e3,O=9e4,U=/didn't contain changes|No updates are to be performed/i,j=new Set(["CREATE_COMPLETE","FAILED"]);class z{aws;pollIntervalMs;pollTimeoutMs;now;constructor(a,e){this.aws=a,this.pollIntervalMs=e?.pollIntervalMs??F,this.pollTimeoutMs=e?.pollTimeoutMs??O,this.now=e?.now??(()=>Date.now())}async probeStackReplacements(a){const{stackName:e,abortSignal:o}=a;if(h(o))return c(new Error(`Change-set probe aborted before starting for ${e}`));await this.sweepLeakedChangeSets(e,o);const t=`${y}${k()}`,s=this.aws.getClient(A);let r;try{const n=JSON.stringify(a.template);let l;if(Buffer.byteLength(n,"utf8")>D){const i=await this.uploadTemplate(t,n,o);if(!i.success)return i;r=i.data.key,l={TemplateURL:i.data.url}}else l={TemplateBody:n};return await s.send(new N({StackName:e,ChangeSetName:t,ChangeSetType:"UPDATE",IncludeNestedStacks:!0,Capabilities:["CAPABILITY_IAM","CAPABILITY_NAMED_IAM","CAPABILITY_AUTO_EXPAND"],Description:"fjall preflight replacement probe \u2014 review-only, never executed",...l}),{abortSignal:d(o)}),await this.collectVerdicts(s,e,t,o)}catch(n){return c(new Error(`Change-set probe failed for ${e}: ${p(u(n))}`))}finally{await s.send(new I({StackName:e,ChangeSetName:t}),{abortSignal:d()}).catch(n=>{m.warn(f,"Failed to delete preflight change set",{stackName:e,changeSetName:t,error:p(u(n))})}),r!==void 0&&await this.deleteUploadedTemplate(r)}}async collectVerdicts(a,e,o,t){const s=this.now();let r,n;for(;!h(t);){const g=await a.send(new L({StackName:e,ChangeSetName:o}),{abortSignal:d(t)});if(r=g.Status,n=g.StatusReason,r!==void 0&&j.has(r))break;if(this.now()-s>=this.pollTimeoutMs)return c(new Error(`Change-set probe timed out after ${this.pollTimeoutMs}ms for ${e} (last status: ${r??"unknown"})`));await B(this.pollIntervalMs,t)}if(h(t))return c(new Error(`Change-set probe aborted while polling ${e}`));if(r==="FAILED")return n!==void 0&&U.test(n)?T({replacementByLogicalId:new Map,noChanges:!0}):c(new Error(`Change-set probe failed for ${e}: ${p(n??"no status reason reported")}`));const l=new Map;let i;do{if(h(t))return c(new Error(`Change-set probe aborted while reading ${e}`));if(i!==void 0&&this.now()-s>=this.pollTimeoutMs)return c(new Error(`Change-set probe timed out after ${this.pollTimeoutMs}ms while reading ${e} resource changes`));const g=await a.send(new L({StackName:e,ChangeSetName:o,...i!==void 0?{NextToken:i}:{}}),{abortSignal:d(t)});for(const E of g.Changes??[]){if(E.Type!=="Resource")continue;const C=E.ResourceChange,b=C?.LogicalResourceId;if(C===void 0||b===void 0)continue;const w=C.Replacement;(w==="True"||w==="False"||w==="Conditional")&&l.set(b,w)}i=g.NextToken}while(i!==void 0);return T({replacementByLogicalId:l,noChanges:!1})}async sweepLeakedChangeSets(a,e){const o=this.aws.getClient(A);try{let t;do{if(h(e))break;const s=await o.send(new $({StackName:a,...t!==void 0?{NextToken:t}:{}}),{abortSignal:d(e)});for(const r of s.Summaries??[]){if(h(e))break;const n=r.ChangeSetName;if(n===void 0||!n.startsWith(y))continue;const l=r.CreationTime?.getTime();l===void 0||this.now()-l<x||(m.warn(f,"Sweeping leaked preflight change set",{stackName:a,changeSetName:n}),await o.send(new I({StackName:a,ChangeSetName:n}),{abortSignal:d(e)}).catch(i=>{m.warn(f,"Failed to sweep leaked change set",{stackName:a,changeSetName:n,error:p(u(i))})}))}t=s.NextToken}while(t!==void 0)}catch(t){m.debug(f,"Leak sweep failed \u2014 continuing to probe",{stackName:a,error:p(u(t))})}}async uploadTemplate(a,e,o){const t=this.aws.getRegion(),s=M(this.aws.getAccountId(),t),r=`fjall-preflight/${a}.template.json`;try{return await this.aws.getClient(_).send(new P({Bucket:s,Key:r,Body:e,ContentType:"application/json"}),{abortSignal:d(o)}),T({key:r,url:`https://${s}.s3.${t}.amazonaws.com/${r}`})}catch(n){return c(new Error(`Change-set probe could not stage the template in ${s}: ${p(u(n))}`))}}async deleteUploadedTemplate(a){const e=M(this.aws.getAccountId(),this.aws.getRegion());try{await this.aws.getClient(_).send(new R({Bucket:e,Key:a}),{abortSignal:d()})}catch(o){m.warn(f,"Failed to delete staged preflight template",{bucket:e,key:a,error:p(u(o))})}}}export{z as ChangeSetProbe,D as MAX_INLINE_TEMPLATE_BYTES,y as PREFLIGHT_CHANGE_SET_PREFIX,M as bootstrapAssetsBucketName};
@@ -8,6 +8,8 @@ export { CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS } from
8
8
  export { isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext } from "./CdkErrorFormatter.js";
9
9
  export { hasCdkDifferences, parseDiffOutput, type DiffDetails } from "./CdkOutputParser.js";
10
10
  export { CloudFormationService, CloudFormationError, type CloudFormationCallbacks } from "./CloudFormationService.js";
11
+ export { CfnRegistryService, type CfnRegistrySchemaSummary, type CfnRegistryServiceOptions, type CfnRegistrySummaryReader } from "./CfnRegistryService.js";
12
+ export { CFN_REGISTRY_SEED } from "./cfnRegistrySeed.js";
11
13
  export { EcsService, EcsError, type ECSServiceInfo, type ECSClusterInfo, type DeploymentStatus, type ECSDeploymentOptions } from "./EcsService.js";
12
14
  export { EcsServiceResolver, type CfnExportsClient } from "./EcsServiceResolver.js";
13
15
  export { EcrImageInspectorService, stripTagOrDigest } from "./EcrImageInspectorService.js";
@@ -1 +1 @@
1
- import{CdkService as e}from"./CdkService.js";import{CdkArgumentBuilder as i}from"./CdkArgumentBuilder.js";import{CdkProcessManager as f}from"./CdkProcessManager.js";import{CdkEventMonitor as s,startStackMonitoring as p,DEFAULT_DEPLOY_TIMEOUT_MS as a}from"./CdkEventMonitoring.js";import{isCdkError as E,formatInfrastructureError as u,getStructuralHint as x,getSourceContext as d}from"./CdkErrorFormatter.js";import{hasCdkDifferences as S,parseDiffOutput as g}from"./CdkOutputParser.js";import{CloudFormationService as v,CloudFormationError as l}from"./CloudFormationService.js";import{EcsService as M,EcsError as I}from"./EcsService.js";import{EcsServiceResolver as T}from"./EcsServiceResolver.js";import{EcrImageInspectorService as _,stripTagOrDigest as A}from"./EcrImageInspectorService.js";export{i as CdkArgumentBuilder,s as CdkEventMonitor,f as CdkProcessManager,e as CdkService,l as CloudFormationError,v as CloudFormationService,a as DEFAULT_DEPLOY_TIMEOUT_MS,_ as EcrImageInspectorService,I as EcsError,M as EcsService,T as EcsServiceResolver,u as formatInfrastructureError,d as getSourceContext,x as getStructuralHint,S as hasCdkDifferences,E as isCdkError,g as parseDiffOutput,p as startStackMonitoring,A as stripTagOrDigest};
1
+ import{CdkService as o}from"./CdkService.js";import{CdkArgumentBuilder as f}from"./CdkArgumentBuilder.js";import{CdkProcessManager as c}from"./CdkProcessManager.js";import{CdkEventMonitor as p,startStackMonitoring as s,DEFAULT_DEPLOY_TIMEOUT_MS as E}from"./CdkEventMonitoring.js";import{isCdkError as a,formatInfrastructureError as x,getStructuralHint as S,getSourceContext as u}from"./CdkErrorFormatter.js";import{hasCdkDifferences as d,parseDiffOutput as g}from"./CdkOutputParser.js";import{CloudFormationService as k,CloudFormationError as D}from"./CloudFormationService.js";import{CfnRegistryService as I}from"./CfnRegistryService.js";import{CFN_REGISTRY_SEED as T}from"./cfnRegistrySeed.js";import{EcsService as F,EcsError as O}from"./EcsService.js";import{EcsServiceResolver as A}from"./EcsServiceResolver.js";import{EcrImageInspectorService as P,stripTagOrDigest as U}from"./EcrImageInspectorService.js";export{T as CFN_REGISTRY_SEED,f as CdkArgumentBuilder,p as CdkEventMonitor,c as CdkProcessManager,o as CdkService,I as CfnRegistryService,D as CloudFormationError,k as CloudFormationService,E as DEFAULT_DEPLOY_TIMEOUT_MS,P as EcrImageInspectorService,O as EcsError,F as EcsService,A as EcsServiceResolver,x as formatInfrastructureError,u as getSourceContext,S as getStructuralHint,d as hasCdkDifferences,a as isCdkError,g as parseDiffOutput,s as startStackMonitoring,U as stripTagOrDigest};
@@ -1,6 +1,6 @@
1
1
  import type { DeployCallbacks } from "./callbacks.js";
2
2
  /**
3
- * Every {@link DeployCallbacks} key (46 callbacks + the `verbose` flag),
3
+ * Every {@link DeployCallbacks} key (47 callbacks + the `verbose` flag),
4
4
  * derived from the type-checked {@link CALLBACK_KEY_PRESENCE} map so the list
5
5
  * can never silently diverge from the interface.
6
6
  */
@@ -1 +1 @@
1
- const e={onStepStart:!0,onStepComplete:!0,onProgress:!0,onLog:!0,onOutput:!0,onResourceProgress:!0,onParallelPhaseStart:!0,onParallelPhaseComplete:!0,onParallelStackResourceProgress:!0,onDockerProgress:!0,onBuildPushStart:!0,onBuildPushProgress:!0,onBuildPushComplete:!0,onTaskDefRegistered:!0,onECSUpdate:!0,onECSProgress:!0,onECSComplete:!0,onMigrationsStart:!0,onMigrationsComplete:!0,onCDKBootstrap:!0,onCdkOutput:!0,onCascadeStart:!0,onCascadeAccountsReconciled:!0,onOrgChangesDetected:!0,onCascadeMissingAccounts:!0,onCascadePhaseStart:!0,onCascadePhaseComplete:!0,onCascadeAccountStart:!0,onCascadeAccountPhaseChange:!0,onCascadeAccountResourceProgress:!0,onCascadeAccountComplete:!0,onCascadeComplete:!0,onCascadeLedger:!0,onTrailMigrationPhase:!0,onTrailLifecycleChanged:!0,onStackCleanupProgress:!0,onDetectionComplete:!0,onDetectionPhaseChange:!0,onSSOUrl:!0,onAuthComplete:!0,onOpenNextBuildStart:!0,onOpenNextProgress:!0,onOpenNextBuildComplete:!0,onOpenNextBuildError:!0,onFailureAnalysis:!0,onError:!0,verbose:!0},t=Object.keys(e);export{t as ALL_CALLBACK_KEYS};
1
+ const e={onStepStart:!0,onStepComplete:!0,onProgress:!0,onLog:!0,onOutput:!0,onResourceProgress:!0,onParallelPhaseStart:!0,onParallelPhaseComplete:!0,onParallelStackResourceProgress:!0,onDockerProgress:!0,onBuildPushStart:!0,onBuildPushProgress:!0,onBuildPushComplete:!0,onTaskDefRegistered:!0,onECSUpdate:!0,onECSProgress:!0,onECSComplete:!0,onMigrationsStart:!0,onMigrationsComplete:!0,onCDKBootstrap:!0,onCdkOutput:!0,onCascadeStart:!0,onCascadeAccountsReconciled:!0,onOrgChangesDetected:!0,onCascadeMissingAccounts:!0,onCascadePhaseStart:!0,onCascadePhaseComplete:!0,onCascadeAccountStart:!0,onCascadeAccountPhaseChange:!0,onCascadeAccountResourceProgress:!0,onCascadeAccountComplete:!0,onCascadeComplete:!0,onCascadeLedger:!0,onTrailMigrationPhase:!0,onTrailLifecycleChanged:!0,onStackCleanupProgress:!0,onDetectionComplete:!0,onDetectionPhaseChange:!0,onStackTargets:!0,onSSOUrl:!0,onAuthComplete:!0,onOpenNextBuildStart:!0,onOpenNextProgress:!0,onOpenNextBuildComplete:!0,onOpenNextBuildError:!0,onFailureAnalysis:!0,onError:!0,verbose:!0},t=Object.keys(e);export{t as ALL_CALLBACK_KEYS};
@@ -42,6 +42,20 @@ export interface StackCleanupRetainedBucketsDetail {
42
42
  export declare function isQuarantineDetail(detail: StackCleanupQuarantineDetail | StackCleanupRetainedBucketsDetail | undefined): detail is StackCleanupQuarantineDetail;
43
43
  /** Narrow an onStackCleanupProgress detail to the retained-buckets payload. */
44
44
  export declare function isRetainedBucketsDetail(detail: StackCleanupQuarantineDetail | StackCleanupRetainedBucketsDetail | undefined): detail is StackCleanupRetainedBucketsDetail;
45
+ /**
46
+ * One CloudFormation stack the deploy will mutate, surfaced post-synth so a
47
+ * reconciler can locate the physical resources a crashed run may have left
48
+ * mid-flight (§5.9). The coordinates are known before the first mutating CFN
49
+ * op, so they are reported up front rather than derived from failure output.
50
+ */
51
+ export interface StackTarget {
52
+ /** Synthesised stack name (e.g. "WebApp-Compute"). */
53
+ stackName: string;
54
+ /** Account the stack deploys into, when the engine has resolved the caller identity. */
55
+ accountId?: string;
56
+ /** Region the stack deploys into. */
57
+ region?: string;
58
+ }
45
59
  /**
46
60
  * Unified callback interface for deployment progress.
47
61
  *
@@ -232,6 +246,14 @@ export interface DeployCallbacks {
232
246
  * Replaces fragile string matching on onLog messages for pre-deployment progress.
233
247
  */
234
248
  onDetectionPhaseChange?: (phase: "detect" | "synth" | "hash" | "validate", status: "started" | "completed") => void;
249
+ /**
250
+ * @emittedBy engine — fired once post-synth, before the first mutating CFN
251
+ * operation, listing every stack the deploy will touch with the account and
252
+ * region it lands in. A reconciler persists these so a crashed run's
253
+ * in-flight stacks can be located without waiting for failure output. Emitted
254
+ * on both the full and code-only deploy paths.
255
+ */
256
+ onStackTargets?: (targets: readonly StackTarget[]) => void;
235
257
  /** @emittedBy caller — CLI fires during SSO/OIDC auth flows. */
236
258
  onSSOUrl?: (url: string | null) => void;
237
259
  /** @emittedBy caller — CLI fires after credential resolution. */
@@ -35,4 +35,13 @@ export interface Entitlements {
35
35
  costRecommendations: "full" | "limited";
36
36
  /** "full" = all details visible, "limited" = top 1 detail only, rest title-only */
37
37
  warningDetail: "full" | "limited";
38
+ /**
39
+ * Deploy-orchestration fail-closed policy (design §5.5). When true, the CLI
40
+ * must NOT proceed with an untracked deploy if it cannot establish a tracking
41
+ * row — it aborts unless `--force-untracked` is passed. Absent/false preserves
42
+ * today's availability-first behaviour (tracking degrades, deploy proceeds).
43
+ * Primary transport: it rides the entitlements round-trip so a cold-cache CI
44
+ * run learns it on the same request that authenticates.
45
+ */
46
+ requireTrackedDeploys?: boolean;
38
47
  }
@@ -23,4 +23,10 @@ export interface OrgConfig {
23
23
  * boundary — the engine never sees this union.
24
24
  */
25
25
  rootAccessManagement?: RootAccessManagementMode;
26
+ /**
27
+ * Deploy-orchestration fail-closed policy (design §5.5), SECONDARY source for
28
+ * the CLI's tracked-deploy gate — consulted only when the entitlements fetch
29
+ * (the primary transport) fails. See `Entitlements.requireTrackedDeploys`.
30
+ */
31
+ requireTrackedDeploys?: boolean;
26
32
  }
@@ -78,6 +78,7 @@ export declare const DeploymentEventSchema: z.ZodObject<{
78
78
  physicalId: z.ZodOptional<z.ZodString>;
79
79
  expectedDurationSeconds: z.ZodOptional<z.ZodNumber>;
80
80
  stack: z.ZodOptional<z.ZodString>;
81
+ clientRequestToken: z.ZodOptional<z.ZodString>;
81
82
  }, z.core.$strict>>;
82
83
  docker: z.ZodOptional<z.ZodObject<{
83
84
  message: z.ZodString;