@fjall/deploy-core 2.20.1 → 2.22.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 (84) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/AwsProvider.d.ts +9 -0
  3. package/dist/src/aws/index.d.ts +3 -1
  4. package/dist/src/aws/index.js +1 -1
  5. package/dist/src/aws/regions/regionOptStatus.d.ts +26 -0
  6. package/dist/src/aws/regions/regionOptStatus.js +1 -0
  7. package/dist/src/aws/targetReadiness.d.ts +9 -0
  8. package/dist/src/aws/targetReadiness.js +1 -1
  9. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
  10. package/dist/src/aws/utils/cloudformationEventTypes.d.ts +6 -0
  11. package/dist/src/aws/utils/cloudformationEvents.d.ts +1 -0
  12. package/dist/src/aws/utils/cloudformationEvents.js +1 -1
  13. package/dist/src/index.d.ts +11 -3
  14. package/dist/src/index.js +1 -1
  15. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  16. package/dist/src/orchestration/application/applicationDeployHelpers.js +2 -2
  17. package/dist/src/orchestration/application/approvalGate.d.ts +44 -0
  18. package/dist/src/orchestration/application/approvalGate.js +1 -0
  19. package/dist/src/orchestration/application/buildArgResolver.d.ts +72 -0
  20. package/dist/src/orchestration/application/buildArgResolver.js +1 -0
  21. package/dist/src/orchestration/application/buildGroupValidator.d.ts +64 -0
  22. package/dist/src/orchestration/application/buildGroupValidator.js +1 -0
  23. package/dist/src/orchestration/application/buildSecretResolver.d.ts +57 -0
  24. package/dist/src/orchestration/application/buildSecretResolver.js +1 -0
  25. package/dist/src/orchestration/application/buildSecretSession.d.ts +40 -0
  26. package/dist/src/orchestration/application/buildSecretSession.js +1 -0
  27. package/dist/src/orchestration/application/codeOnlyDeploy.js +1 -1
  28. package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
  29. package/dist/src/orchestration/application/plan/approvalToken.d.ts +40 -0
  30. package/dist/src/orchestration/application/plan/approvalToken.js +1 -0
  31. package/dist/src/orchestration/application/plan/assemblyDigest.d.ts +21 -0
  32. package/dist/src/orchestration/application/plan/assemblyDigest.js +2 -0
  33. package/dist/src/orchestration/application/plan/buildDeployPlan.d.ts +14 -0
  34. package/dist/src/orchestration/application/plan/buildDeployPlan.js +1 -0
  35. package/dist/src/orchestration/application/plan/classify.d.ts +37 -0
  36. package/dist/src/orchestration/application/plan/classify.js +1 -0
  37. package/dist/src/orchestration/application/plan/computeDeployPlan.d.ts +24 -0
  38. package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -0
  39. package/dist/src/orchestration/application/plan/index.d.ts +8 -0
  40. package/dist/src/orchestration/application/plan/index.js +1 -0
  41. package/dist/src/orchestration/application/plan/renderDeployPlan.d.ts +18 -0
  42. package/dist/src/orchestration/application/plan/renderDeployPlan.js +1 -0
  43. package/dist/src/orchestration/application/plan/types.d.ts +54 -0
  44. package/dist/src/orchestration/application/plan/types.js +0 -0
  45. package/dist/src/orchestration/deploy.js +1 -1
  46. package/dist/src/orchestration/destroy.js +1 -1
  47. package/dist/src/orchestration/dockerBuildParamsKeys.d.ts +2 -0
  48. package/dist/src/orchestration/dockerBuildParamsKeys.js +1 -0
  49. package/dist/src/orchestration/dockerInterface.d.ts +34 -0
  50. package/dist/src/orchestration/index.d.ts +8 -1
  51. package/dist/src/orchestration/index.js +1 -1
  52. package/dist/src/orchestration/serviceFactory.d.ts +7 -0
  53. package/dist/src/orchestration/serviceFactory.js +1 -1
  54. package/dist/src/orchestration/stepLifecycle.d.ts +7 -1
  55. package/dist/src/orchestration/stepLifecycle.js +1 -1
  56. package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +3 -0
  57. package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -1
  58. package/dist/src/services/infrastructure/CdkProcessManager.d.ts +10 -1
  59. package/dist/src/services/infrastructure/CdkProcessManager.js +3 -3
  60. package/dist/src/services/infrastructure/CdkService.d.ts +1 -0
  61. package/dist/src/services/infrastructure/CdkService.js +2 -2
  62. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +15 -0
  63. package/dist/src/services/infrastructure/CloudFormationService.d.ts +11 -0
  64. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  65. package/dist/src/services/infrastructure/cancelInFlightStack.d.ts +22 -0
  66. package/dist/src/services/infrastructure/cancelInFlightStack.js +3 -0
  67. package/dist/src/types/approval.d.ts +53 -0
  68. package/dist/src/types/approval.js +0 -0
  69. package/dist/src/types/callbackKeys.d.ts +1 -1
  70. package/dist/src/types/callbackKeys.js +1 -1
  71. package/dist/src/types/callbacks.d.ts +18 -4
  72. package/dist/src/types/config/entitlements.d.ts +7 -0
  73. package/dist/src/types/deployEmitter.d.ts +85 -0
  74. package/dist/src/types/deployEmitter.js +1 -0
  75. package/dist/src/types/deployEvent.d.ts +907 -0
  76. package/dist/src/types/deployEvent.js +1 -0
  77. package/dist/src/types/deployEventParse.d.ts +52 -0
  78. package/dist/src/types/deployEventParse.js +1 -0
  79. package/dist/src/types/deployEventTypeGuards.d.ts +10 -0
  80. package/dist/src/types/deployEventTypeGuards.js +1 -0
  81. package/dist/src/types/index.d.ts +6 -1
  82. package/dist/src/types/index.js +1 -1
  83. package/dist/src/types/params.d.ts +103 -0
  84. package/package.json +8 -4
@@ -1 +1 @@
1
- import{deploy as t}from"./deploy.js";import{destroy as n}from"./destroy.js";import{deployOrganisation as i}from"./organisation/organisationDeploy.js";import{destroyOrganisation as s}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as m,emptyS3Bucket as p,preEmptyStackBuckets as l,formatQuarantineSuspectedMessage as f,formatRetainedBucketsMessage as T,isQuarantineDetail as x,isRetainedBucketsDetail as A,PRE_EMPTY_TAG_KEYS as S,isCleanableState as d,SAFE_CLEANUP_STATES as _}from"./stackCleanup.js";import{partitionAccounts as g,buildRegionList as O,buildAccountRegionPairs as R,cascadeHomeRegion as y,cascadeOperationKey as P}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as k,projectAccountRows as B}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as K,mergeReconciledProviderAccounts as N}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as L,reconcileTrailMigration as M,ORG_TRAIL_BUCKET_OUTPUT_KEY as b,TRAIL_BUCKET_OUTPUT_KEY as Q,TRAIL_KEY_ARN_OUTPUT_KEY as v}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as G}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as j}from"./unlock/unlockBucket.js";import{unlockQueue as H}from"./unlock/unlockQueue.js";import{triageBucketPolicy as w,isEnforceSslStatement as q}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as J}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as W,synthesiseEnforceSslDocument as X,ensureEnforceSsl as Z}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as ee}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as te,flattenAccountsToEnvironments as re,extractAllAccountNames as ne,accountsConfigToOUTree as ce,isStringArray as ie,isAccountsConfig as ae,isOuOnlyAccountBucket as se,OU_ONLY_ACCOUNT_BUCKETS as ue}from"./organisation/accountsConfig.js";import{runOpenNextBuild as pe}from"./application/openNextBuild.js";import{runOrganisationSetup as fe,ORG_SETUP_PHASES as Te}from"./organisation/organisationSetup.js";export*from"./builders/index.js";export{Te as ORG_SETUP_PHASES,b as ORG_TRAIL_BUCKET_OUTPUT_KEY,ue as OU_ONLY_ACCOUNT_BUCKETS,S as PRE_EMPTY_TAG_KEYS,_ as SAFE_CLEANUP_STATES,Q as TRAIL_BUCKET_OUTPUT_KEY,v as TRAIL_KEY_ARN_OUTPUT_KEY,ce as accountsConfigToOUTree,R as buildAccountRegionPairs,O as buildRegionList,y as cascadeHomeRegion,P as cascadeOperationKey,m as cleanupFailedStack,L as decideNextTransition,G as decommissionMemberTrailStorage,t as deploy,i as deployOrganisation,n as destroy,s as destroyOrganisation,p as emptyS3Bucket,Z as ensureEnforceSsl,ne as extractAllAccountNames,re as flattenAccountsToEnvironments,f as formatQuarantineSuspectedMessage,T as formatRetainedBucketsMessage,ae as isAccountsConfig,d as isCleanableState,q as isEnforceSslStatement,se as isOuOnlyAccountBucket,x as isQuarantineDetail,A as isRetainedBucketsDetail,ie as isStringArray,N as mergeReconciledProviderAccounts,te as parseAccountsConfiguration,g as partitionAccounts,l as preEmptyStackBuckets,B as projectAccountRows,k as projectScalarSummary,K as reconcileProviderAccounts,M as reconcileTrailMigration,ee as restoreAndReconcileQuarantinedBucket,W as restoreBucketPolicy,pe as runOpenNextBuild,fe as runOrganisationSetup,X as synthesiseEnforceSslDocument,J as toResourcePolicyStatements,w as triageBucketPolicy,j as unlockBucket,H as unlockQueue};
1
+ import{deploy as r}from"./deploy.js";import{destroy as i}from"./destroy.js";import{deployOrganisation as s}from"./organisation/organisationDeploy.js";import{destroyOrganisation as c}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as u,emptyS3Bucket as p,preEmptyStackBuckets as m,formatQuarantineSuspectedMessage as f,formatRetainedBucketsMessage as d,isQuarantineDetail as A,isRetainedBucketsDetail as T,PRE_EMPTY_TAG_KEYS as S,isCleanableState as x,SAFE_CLEANUP_STATES as P}from"./stackCleanup.js";import{partitionAccounts as g,buildRegionList as y,buildAccountRegionPairs as E,cascadeHomeRegion as R,cascadeOperationKey as B}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as k,projectAccountRows as U}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as C,mergeReconciledProviderAccounts as L}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as N,reconcileTrailMigration as D,ORG_TRAIL_BUCKET_OUTPUT_KEY as b,TRAIL_BUCKET_OUTPUT_KEY as Y,TRAIL_KEY_ARN_OUTPUT_KEY as M}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as G}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as Q}from"./unlock/unlockBucket.js";import{unlockQueue as H}from"./unlock/unlockQueue.js";import{triageBucketPolicy as j,isEnforceSslStatement as w}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as q}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as J,synthesiseEnforceSslDocument as X,ensureEnforceSsl as Z}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as ee}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as re,flattenAccountsToEnvironments as te,extractAllAccountNames as ie,accountsConfigToOUTree as ne,isStringArray as se,isAccountsConfig as ae,isOuOnlyAccountBucket as ce,OU_ONLY_ACCOUNT_BUCKETS as le}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as pe,resolveSecretRefValue as me,resolveBuildSecretSessionProvider as fe,sourcedRefsFromBuildArgs as de}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as Te,partitionForRegion as Se}from"./application/buildSecretSession.js";import{resolveBuildArgs as Pe}from"./application/buildArgResolver.js";import{validateBuildGroup as ge}from"./application/buildGroupValidator.js";import{runOpenNextBuild as Ee}from"./application/openNextBuild.js";import{runOrganisationSetup as Be,ORG_SETUP_PHASES as Oe}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as ve,computeDeployPlan as Ce,computeAssemblyDigest as Le,digestHead as Ke,classifyImpact as Ne,derivePropertyChanges as De,isDataLoss as be,isStatefulResourceType as Ye,renderPlanSummary as Me,renderPlanLines as Fe,toWirePlanChanges as Ge,signApprovalToken as Ie,verifyApprovalToken as Qe,DEFAULT_APPROVAL_TTL_MS as he,APPROVAL_TOKEN_PATTERN as He}from"./application/plan/index.js";import{runApprovalGate as je}from"./application/approvalGate.js";export{He as APPROVAL_TOKEN_PATTERN,he as DEFAULT_APPROVAL_TTL_MS,Oe as ORG_SETUP_PHASES,b as ORG_TRAIL_BUCKET_OUTPUT_KEY,le as OU_ONLY_ACCOUNT_BUCKETS,S as PRE_EMPTY_TAG_KEYS,P as SAFE_CLEANUP_STATES,Y as TRAIL_BUCKET_OUTPUT_KEY,M as TRAIL_KEY_ARN_OUTPUT_KEY,ne as accountsConfigToOUTree,E as buildAccountRegionPairs,Te as buildBuildSecretSessionPolicy,ve as buildDeployPlan,y as buildRegionList,R as cascadeHomeRegion,B as cascadeOperationKey,Ne as classifyImpact,u as cleanupFailedStack,Le as computeAssemblyDigest,Ce as computeDeployPlan,N as decideNextTransition,G as decommissionMemberTrailStorage,r as deploy,s as deployOrganisation,De as derivePropertyChanges,i as destroy,c as destroyOrganisation,Ke as digestHead,p as emptyS3Bucket,Z as ensureEnforceSsl,ie as extractAllAccountNames,te as flattenAccountsToEnvironments,f as formatQuarantineSuspectedMessage,d as formatRetainedBucketsMessage,ae as isAccountsConfig,x as isCleanableState,be as isDataLoss,w as isEnforceSslStatement,ce as isOuOnlyAccountBucket,A as isQuarantineDetail,T as isRetainedBucketsDetail,Ye as isStatefulResourceType,se as isStringArray,L as mergeReconciledProviderAccounts,re as parseAccountsConfiguration,g as partitionAccounts,Se as partitionForRegion,m as preEmptyStackBuckets,U as projectAccountRows,k as projectScalarSummary,C as reconcileProviderAccounts,D as reconcileTrailMigration,Fe as renderPlanLines,Me as renderPlanSummary,Pe as resolveBuildArgs,fe as resolveBuildSecretSessionProvider,pe as resolveBuildSecrets,me as resolveSecretRefValue,ee as restoreAndReconcileQuarantinedBucket,J as restoreBucketPolicy,je as runApprovalGate,Ee as runOpenNextBuild,Be as runOrganisationSetup,Ie as signApprovalToken,de as sourcedRefsFromBuildArgs,X as synthesiseEnforceSslDocument,q as toResourcePolicyStatements,Ge as toWirePlanChanges,j as triageBucketPolicy,Q as unlockBucket,H as unlockQueue,ge as validateBuildGroup,Qe as verifyApprovalToken};
@@ -18,6 +18,13 @@ export interface DeployServices {
18
18
  stackService: ApplicationStackService;
19
19
  hashService: TemplateHashService;
20
20
  frameworkRegistry: FrameworkRegistry;
21
+ /**
22
+ * Caller cancellation signal (from `DeployParams.abortSignal`). Orchestration
23
+ * helpers read it to render an aborted step as "cancelled" rather than
24
+ * "error"; the CDK execution layer (process manager + CdkService) acts on it
25
+ * to kill the child and roll the stack back.
26
+ */
27
+ abortSignal?: AbortSignal;
21
28
  /** Deregister process signal handlers and clean up child processes. */
22
29
  dispose(): void;
23
30
  }
@@ -1 +1 @@
1
- import{SimpleAwsProvider as f}from"../aws/SimpleAwsProvider.js";import{CdkService as w}from"../services/infrastructure/CdkService.js";import{CdkArgumentBuilder as S}from"../services/infrastructure/CdkArgumentBuilder.js";import{CdkProcessManager as d}from"../services/infrastructure/CdkProcessManager.js";import{CloudFormationService as l}from"../services/infrastructure/CloudFormationService.js";import{EcsService as g}from"../services/infrastructure/EcsService.js";import{EcsServiceResolver as k}from"../services/infrastructure/EcsServiceResolver.js";import{EcrImageInspectorService as u}from"../services/infrastructure/EcrImageInspectorService.js";import{ApplicationStackService as C}from"../services/application/ApplicationStackService.js";import{TemplateHashService as E}from"../services/supporting/TemplateHashService.js";import{FrameworkRegistry as I}from"./builders/frameworkRegistry.js";function H(t){const e=new f(t.awsCredentials);e.exportToEnv();const i=new S,o=new d(i),c=new w({processManager:o}),r=new l(e),s=new g(e),n=new k(r),m=new u(e),p=new C(c,r,e),a=new E,v=I.createDefault();return{awsProvider:e,cdkService:c,cfnService:r,ecsService:s,ecsResolver:n,ecrImageInspector:m,stackService:p,hashService:a,frameworkRegistry:v,dispose(){o.dispose()}}}export{H as createDeployServices};
1
+ import{SimpleAwsProvider as d}from"../aws/SimpleAwsProvider.js";import{CdkService as f}from"../services/infrastructure/CdkService.js";import{CdkArgumentBuilder as v}from"../services/infrastructure/CdkArgumentBuilder.js";import{CdkProcessManager as p}from"../services/infrastructure/CdkProcessManager.js";import{CloudFormationService as g}from"../services/infrastructure/CloudFormationService.js";import{EcsService as w}from"../services/infrastructure/EcsService.js";import{EcsServiceResolver as u}from"../services/infrastructure/EcsServiceResolver.js";import{EcrImageInspectorService as k}from"../services/infrastructure/EcrImageInspectorService.js";import{ApplicationStackService as b}from"../services/application/ApplicationStackService.js";import{TemplateHashService as y}from"../services/supporting/TemplateHashService.js";import{FrameworkRegistry as A}from"./builders/frameworkRegistry.js";function T(e){const r=new d(e.awsCredentials);r.exportToEnv();const t=new v,i=new p(t,e.abortSignal),n=new f({processManager:i,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},...e.callbacks.onFailureAnalysis!==void 0?{onFailureAnalysis:e.callbacks.onFailureAnalysis}:{}}),o=new g(r),c=new w(r),s=new u(o),a=new k(r),l=new b(n,o,r),m=new y,S=A.createDefault();return{awsProvider:r,cdkService:n,cfnService:o,ecsService:c,ecsResolver:s,ecrImageInspector:a,stackService:l,hashService:m,frameworkRegistry:S,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},dispose(){i.dispose()}}}export{T as createDeployServices};
@@ -25,5 +25,11 @@ export type StepOutcome<T> = {
25
25
  * If `fn` throws, the throw is caught, an "error" complete event is emitted,
26
26
  * onError is invoked, and a failure Result is returned — so an exception
27
27
  * cannot leave the step rail visually frozen.
28
+ *
29
+ * When `abortSignal` has fired (caller Ctrl-C / worker SIGTERM), a failing step
30
+ * is reported as **"cancelled"** rather than "error", and onError is suppressed
31
+ * — an abort is a deliberate stop, not a deploy failure. The failure Result is
32
+ * still returned so the orchestrator short-circuits; the top-level engine
33
+ * surfaces the cancellation via `DeployResult.cancelled`.
28
34
  */
29
- export declare function withStepLifecycle<T>(callbacks: DeployCallbacks, step: StepDescriptor, fn: () => Promise<StepOutcome<T>>): Promise<Result<T>>;
35
+ export declare function withStepLifecycle<T>(callbacks: DeployCallbacks, step: StepDescriptor, fn: () => Promise<StepOutcome<T>>, abortSignal?: AbortSignal): Promise<Result<T>>;
@@ -1 +1 @@
1
- import{success as p,failure as i}from"@fjall/generator";async function m(t,r,d){t.onStepStart?.(r.stepId,r.stepName,r.stepIndex,r.totalSteps);let e;try{e=await d()}catch(o){const n=o instanceof Error?o:new Error(String(o));return t.onStepComplete?.(r.stepId,r.stepName,"error",r.stepIndex,r.totalSteps),t.onError?.(n),i(n)}return t.onStepComplete?.(r.stepId,r.stepName,e.kind,r.stepIndex,r.totalSteps),e.kind==="error"?(t.onError?.(e.error),i(e.error)):p(e.data)}export{m as withStepLifecycle};
1
+ import{success as u,failure as c}from"@fjall/generator";import{isAborted as f}from"../aws/organisations/types.js";async function S(t,e,o,n){t.onStepStart?.(e.stepId,e.stepName,e.stepIndex,e.totalSteps);let r;try{r=await o()}catch(i){const m=i instanceof Error?i:new Error(String(i));return d(t,e,m,n)}return r.kind==="error"?d(t,e,r.error,n):(t.onStepComplete?.(e.stepId,e.stepName,r.kind,e.stepIndex,e.totalSteps),u(r.data))}function d(t,e,o,n){const r=f(n);return t.onStepComplete?.(e.stepId,e.stepName,r?"cancelled":"error",e.stepIndex,e.totalSteps),r||t.onError?.(o),c(o)}export{S as withStepLifecycle};
@@ -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 { DeploymentContext } from "../../types/deployment/DeploymentTypes.js";
3
4
  import type { AwsProvider } from "../../aws/AwsProvider.js";
@@ -12,8 +13,10 @@ export declare function startStackMonitoring(monitor: CloudFormationEventMonitor
12
13
  }>;
13
14
  export declare class CdkEventMonitor {
14
15
  private eventLogWriterFactory?;
16
+ private onFailureAnalysis?;
15
17
  constructor(options?: {
16
18
  eventLogWriterFactory?: EventLogWriterFactory;
19
+ onFailureAnalysis?: (analysis: FailureAnalysis) => void;
17
20
  });
18
21
  createEventMonitor(operation: "deploy" | "destroy", stackName: string, region: string, context: DeploymentContext, aws: AwsProvider): Promise<CloudFormationEventMonitor>;
19
22
  }
@@ -1 +1 @@
1
- import{logger as m}from"@fjall/util/logger";import{getErrorMessage as g}from"@fjall/util";import{CloudFormationFailureAnalyser as u}from"@fjall/util/aws";const p=18e5;function F(n,e,o){return n.waitForStackComplete(e,{timeout:p,pollInterval:2e3,onResourceUpdate:o,onStackComplete:(t,r)=>{}}).catch(t=>(m.debug("CdkService","Stack monitoring failed",{error:g(t)}),{success:!1,failureReason:"Monitoring failed"}))}class f{eventLogWriterFactory;constructor(e){this.eventLogWriterFactory=e?.eventLogWriterFactory}async createEventMonitor(e,o,t,r,i){const c=await import("../../aws/utils/cloudformationEvents.js"),a=new c.CloudFormationEventMonitor(i,{failureAnalyser:new u,eventLogWriterFactory:this.eventLogWriterFactory}),s=`${e}-${Date.now()}`,l=r.target||r.deployType||"deployment";return a.enableLogging(s,o,t,l),a}}export{f as CdkEventMonitor,p as DEFAULT_DEPLOY_TIMEOUT_MS,F as startStackMonitoring};
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};
@@ -6,10 +6,19 @@ export declare class CdkProcessManager {
6
6
  private runningProcesses;
7
7
  private processCounter;
8
8
  private argBuilder;
9
+ private readonly abortSignal?;
9
10
  private exitHandler;
10
11
  private sigintHandler;
11
12
  private sigtermHandler;
12
- constructor(argBuilder: CdkArgumentBuilder);
13
+ /**
14
+ * @param abortSignal When provided, the CALLER owns cancellation: spawned CDK
15
+ * children are wired to this signal (`{ signal }`), and the process-global
16
+ * SIGINT/SIGTERM handlers stand down (no `process.exit`) so the caller's
17
+ * abort flow can issue `CancelUpdateStack` and exit gracefully. Without it,
18
+ * the legacy fallback (clean up children, then `process.exit`) is preserved
19
+ * for callers that do not manage their own cancellation.
20
+ */
21
+ constructor(argBuilder: CdkArgumentBuilder, abortSignal?: AbortSignal);
13
22
  forceKillProcess(child: ChildProcess): void;
14
23
  cleanup(): void;
15
24
  dispose(): void;
@@ -1,3 +1,3 @@
1
- import{spawn as K}from"child_process";import{existsSync as E}from"fs";import{join as b}from"path";import{createRequire as A}from"module";import{logger as T}from"@fjall/util/logger";import{filterDangerousEnvVars as w,maskSensitiveOutput as g}from"@fjall/util";import{success as O,failure as i}from"@fjall/generator";import{getErrorMessage as L}from"@fjall/util";function j(){try{const e=A(import.meta.url).resolve("aws-cdk/bin/cdk");return{command:process.execPath,prefixArgs:[e]}}catch(h){return T.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=j(),G=5e3,H=1800*1e3;class J{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")},G);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(s=>{if(!E(e)){s(i(`Directory not found: ${e}`));return}const a=b(e,"cdk.json");if(!E(a)){s(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=>{T.debug("CdkProcess","Spawn error on failed child process",{error:u.message})}),s(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??H,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){s(i("CDK command timed out"));return}u===0||r?.ignoreExitCode?s(O({exitCode:u||0})):s(i(`CDK command failed with exit code ${u}`))}}),d.on("error",u=>{clearTimeout(k),this.runningProcesses.delete(C),!(l||o)&&(l=!0,s(i(`Failed to run CDK command: ${u.message}`)))})})}async runCdkCommand(e,c,r){return new Promise(s=>{if(!E(e)){s(i(`Directory not found: ${e}`));return}if(!r?.skipProjectCheck){const n=b(e,"cdk.json");if(!E(n)){s(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?w(r.extraEnv):{}};T.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 n=`Failed to spawn CDK process - no PID. cwd=${e}, args=${d.join(" ")}`;o.on("error",t=>{T.debug("CdkProcess","Deferred spawn error on failed child process",{error:t.message})}),s(i(n));return}const l=`cdk-${++this.processCounter}`;this.runningProcesses.set(l,o);let p=!1;const k=r?.timeout??H,u=setTimeout(()=>{o.killed||(f=!0,this.forceKillProcess(o))},k);o.stdout?.on("data",n=>{const t=n.toString();a+=t,r?.outputCallback&&r.outputCallback(g(t)),r?.cdkOutputLogger?.writeCdkOutput("stdout",g(t))}),o.stderr?.on("data",n=>{const t=n.toString();r?.cdkOutputLogger?.writeCdkOutput("stderr",g(t)),!t.includes("deprecated")&&!t.includes("npm WARN")&&!t.includes("ENOENT")&&!/\(node:\d+\)/.test(t)&&!t.includes("ExperimentalWarning")&&!t.includes("will no longer support")&&(m+=t),r?.errorCallback&&r.errorCallback(g(t))}),o.on("error",n=>{clearTimeout(u),this.runningProcesses.delete(l),!(p||f)&&(p=!0,s(i(L(n))))}),o.on("close",n=>{if(clearTimeout(u),this.runningProcesses.delete(l),p)return;if(p=!0,f){s(i("CDK command timed out"));return}const t=n===0||r?.ignoreExitCode===!0&&n===1,M=a+(m?`
2
- ${m}`:"");if(t){const P=r?.combineOutput?M:a;s(O({output:g(P),exitCode:n||0}));return}let $=m;if(a){const P=a.match(/❌.*?Error:(.*)$/m);P&&($=P[1]?.trim()??"")}const I=$||`CDK command failed with exit code ${n}`,y=a?`${I}
3
- ${a}`:I;s(i(g(y)))})})}}export{J as CdkProcessManager};
1
+ import{spawn as M}from"child_process";import{existsSync as b}from"fs";import{join as O}from"path";import{createRequire as A}from"module";import{logger as P}from"@fjall/util/logger";import{filterDangerousEnvVars as j,maskSensitiveOutput as f}from"@fjall/util";import{success as T,failure as n}from"@fjall/generator";import{getErrorMessage as y}from"@fjall/util";function H(){try{const e=A(import.meta.url).resolve("aws-cdk/bin/cdk");return T({command:process.execPath,prefixArgs:[e]})}catch(E){return P.error("CdkService","Failed to resolve the bundled aws-cdk CLI",{error:f(y(E))}),n("Fjall could not locate its bundled aws-cdk CLI. The deploy toolchain looks misconfigured \u2014 reinstall dependencies (npm install) or reinstall the Fjall CLI, then try again.")}}const G=5e3,L=1800*1e3;class W{runningProcesses=new Map;processCounter=0;argBuilder;abortSignal;exitHandler;sigintHandler;sigtermHandler;constructor(e,o){this.argBuilder=e,this.abortSignal=o,this.exitHandler=()=>this.cleanup(),this.sigintHandler=()=>{this.abortSignal===void 0&&(this.cleanup(),process.exit(130))},this.sigtermHandler=()=>{this.abortSignal===void 0&&(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 o=setTimeout(()=>{e.exitCode===null&&e.kill("SIGKILL")},G);o.unref(),e.once("exit",()=>clearTimeout(o))}cleanup(){for(const[e,o]of this.runningProcesses)o.killed||(o.stdout?.destroy(),o.stderr?.destroy(),o.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,o,t){return new Promise(r=>{if(!b(e)){r(n(`Directory not found: ${e}`));return}const l=O(e,"cdk.json");if(!b(l)){r(n(`No CDK project found in ${e}`));return}const u=H();if(!u.success){r(n(u.error));return}const g=u.data,C=this.argBuilder.buildContextArgs(t?.context),h=[...g.prefixArgs,...o,...C],I=this.argBuilder.buildCdkEnv(t),a=M(g.command,h,{cwd:e,env:I,stdio:"inherit",shell:!1,detached:!1,...this.abortSignal!==void 0?{signal:this.abortSignal,killSignal:"SIGTERM"}:{}});if(!a.pid){a.on("error",d=>{P.debug("CdkProcess","Spawn error on failed child process",{error:d.message})}),r(n(`Failed to spawn CDK process - no PID. cwd=${e}, args=${h.join(" ")}`));return}const k=`cdk-passthrough-${++this.processCounter}`;this.runningProcesses.set(k,a);let c=!1,m=!1;const p=t?.timeout??L,x=setTimeout(()=>{a.killed||(c=!0,this.forceKillProcess(a))},p);a.on("close",d=>{if(clearTimeout(x),this.runningProcesses.delete(k),!m){if(m=!0,c){r(n("CDK command timed out"));return}d===0||t?.ignoreExitCode?r(T({exitCode:d||0})):r(n(`CDK command failed with exit code ${d}`))}}),a.on("error",d=>{clearTimeout(x),this.runningProcesses.delete(k),!(m||c)&&(m=!0,r(n(`Failed to run CDK command: ${d.message}`)))})})}async runCdkCommand(e,o,t){return new Promise(r=>{if(!b(e)){r(n(`Directory not found: ${e}`));return}if(!t?.skipProjectCheck){const i=O(e,"cdk.json");if(!b(i)){r(n(`No CDK project found in ${e}`));return}}let l="",u="",g=!1;const C=H();if(!C.success){r(n(C.error));return}const h=C.data,I=this.argBuilder.buildContextArgs(t?.context),a=[...h.prefixArgs,...o,...I],k={...this.argBuilder.buildCdkEnv(t),NO_COLOR:"1",...t?.extraEnv?j(t.extraEnv):{}};P.debug("CdkService","Spawning CDK process",{command:`${h.command} ${a.join(" ")}`,workingDir:e});const c=M(h.command,a,{cwd:e,env:k,stdio:["ignore","pipe","pipe"],shell:!1,detached:!1,...this.abortSignal!==void 0?{signal:this.abortSignal,killSignal:"SIGTERM"}:{}});if(!c.pid){const i=`Failed to spawn CDK process - no PID. cwd=${e}, args=${a.join(" ")}`;c.on("error",s=>{P.debug("CdkProcess","Deferred spawn error on failed child process",{error:s.message})}),r(n(i));return}const m=`cdk-${++this.processCounter}`;this.runningProcesses.set(m,c);let p=!1;const x=t?.timeout??L,d=setTimeout(()=>{c.killed||(g=!0,this.forceKillProcess(c))},x);c.stdout?.on("data",i=>{const s=i.toString();l+=s,t?.outputCallback&&t.outputCallback(f(s)),t?.cdkOutputLogger?.writeCdkOutput("stdout",f(s))}),c.stderr?.on("data",i=>{const s=i.toString();t?.cdkOutputLogger?.writeCdkOutput("stderr",f(s)),!s.includes("deprecated")&&!s.includes("npm WARN")&&!s.includes("ENOENT")&&!/\(node:\d+\)/.test(s)&&!s.includes("ExperimentalWarning")&&!s.includes("will no longer support")&&(u+=s),t?.errorCallback&&t.errorCallback(f(s))}),c.on("error",i=>{clearTimeout(d),this.runningProcesses.delete(m),!(p||g)&&(p=!0,r(n(y(i))))}),c.on("close",i=>{if(clearTimeout(d),this.runningProcesses.delete(m),p)return;if(p=!0,g){r(n("CDK command timed out"));return}const s=i===0||t?.ignoreExitCode===!0&&i===1,R=l+(u?`
2
+ ${u}`:"");if(s){const S=t?.combineOutput?R:l;r(T({output:f(S),exitCode:i||0}));return}let $=u;if(l){const S=l.match(/❌.*?Error:(.*)$/m);S&&($=S[1]?.trim()??"")}const K=$||`CDK command failed with exit code ${i}`,w=l?`${K}
3
+ ${l}`:K;r(n(f(w)))})})}}export{W as CdkProcessManager};
@@ -10,6 +10,7 @@ export { type CheckDifferencesResult } from "./CdkCommandRunner.js";
10
10
  export declare class CdkService {
11
11
  private commandRunner;
12
12
  private eventMonitor;
13
+ private readonly abortSignal?;
13
14
  constructor(options?: CdkServiceOptions);
14
15
  dispose(): void;
15
16
  checkDifferences(path: string, stackName?: string, options?: CdkOptions): Promise<Result<CheckDifferencesResult, CdkError>>;
@@ -1,3 +1,3 @@
1
- import{existsSync as I}from"fs";import{join as b}from"path";import{logger as v}from"@fjall/util/logger";import{success as D,failure as s}from"@fjall/generator";import{DEFAULT_REGION as h}from"../../aws/utils/regions.js";import{getErrorMessage as k,maskSensitiveOutput as p}from"@fjall/util";import{CdkEventMonitor as R,startStackMonitoring as E}from"./CdkEventMonitoring.js";import{analyseDeployOutput as F,analyseDestroyResult as w,createEnhancedOutputCallback as O}from"./CdkOutputAnalyser.js";import{CdkCommandRunner as K}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as N}from"./CdkArgumentBuilder.js";import{CdkProcessManager as T}from"./CdkProcessManager.js";import{wrapWithConstructMapEnrichment as L}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as P,resolveStackName as M,getFallbackStackName as W,buildDeploymentCdkContext as C}from"./cdkServiceHelpers.js";class Z{commandRunner;eventMonitor;constructor(e){const r=e?.processManager??new T(new N);this.commandRunner=new K(r),this.eventMonitor=new R({eventLogWriterFactory:e?.eventLogWriterFactory})}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:C(e,n,e.region||h),...e.assemblyDir!==void 0?{outputDir:e.assemblyDir}:{},...t!==void 0?{credentials:t}:{}});return o.success?D({message:"CloudFormation template synthesised",details:o.data.output?{synthesisTime:o.data.output}:void 0}):s(o.error||"Failed to synthesise CloudFormation template")}catch(o){return s(`CDK synth failed: ${p(k(o))}`)}}async runCdkBootstrap(e,r,t){const n=e.callerIdentity?.Account,o=e.region||h;try{if(!n)return s("No AWS account ID available");const l=b(e.path,"node_modules");if(!I(l))return s(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const u=await this.bootstrap(n,o,{outputCallback:r,credentials:t});return u.success?D({message:"AWS environment bootstrapped"}):s(u.error||"Failed to bootstrap AWS environment")}catch(l){return s(`CDK bootstrap failed: ${p(k(l))}`)}}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:C(e,t,e.region||h)});return n.success?D({message:"Diff check complete",details:{hasDifferences:n.data.hasDifferences,details:n.data.details}}):s(`CDK diff failed: ${n.error.message}`)}catch(n){return s(`CDK diff failed: ${p(k(n))}`)}}async runCdkDeploy(e,r,t,n,o,l,u){const f=e.callerIdentity?.Account,y=e.region||h;if(!f)return s("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!o)return s("AwsProvider is required for deployment monitoring.");const m=e.assemblyDir??b(e.path,"cdk.out"),d=L(m,n);let i=null,g;try{const c=M(r,e)??W(e);i=await this.eventMonitor.createEventMonitor("deploy",c,y,e,o),t&&(t(p(`Starting CloudFormation deployment of ${c}...
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 p}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: ${p(k(o))}`)}}async runCdkBootstrap(e,r,t){const n=e.callerIdentity?.Account,o=e.region||h;try{if(!n)return a("No AWS account ID available");const u=D(e.path,"node_modules");if(!F(u))return a(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const l=await this.bootstrap(n,o,{outputCallback:r,credentials:t});return l.success?C({message:"AWS environment bootstrapped"}):a(l.error||"Failed to bootstrap AWS environment")}catch(u){return a(`CDK bootstrap failed: ${p(k(u))}`)}}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: ${p(k(n))}`)}}async runCdkDeploy(e,r,t,n,o,u,l){const y=e.callerIdentity?.Account,f=e.region||h;if(!y)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 c=null,g;try{const i=M(r,e)??_(e);c=await this.eventMonitor.createEventMonitor("deploy",i,f,e,o),t&&(t(p(`Starting CloudFormation deployment of ${i}...
2
2
  `)),t(`Monitoring CloudFormation events (CDK process running in background)...
3
- `));const a={cdkOutput:"",actualStackName:c,stackDetected:!1,monitoringPromise:null},S=O(a,t,i,d);g=setTimeout(()=>{!a.stackDetected&&i&&!a.monitoringPromise&&(v.debug("CdkService","Fallback monitoring STARTING",{targetStackName:c,stackDetected:a.stackDetected,hasOnResourceProgress:!!n}),a.monitoringPromise=E(i,c,d))},P);const A=await this.deploy(e.path,c,{verbose:e.options?.verbose,outputCallback:S,...e.assemblyDir!==void 0?{appDir:e.assemblyDir}:{useCdkOut:!0},cdkOutputLogger:i?.getEventLogger()??void 0,context:C(e,f,y),credentials:l,...u!==void 0&&Object.keys(u).length>0&&{parameters:u}});return F(a.cdkOutput,A,a.actualStackName)}catch(c){const a=`CDK deploy failed: ${p(k(c))}`;return v.error("CdkService","CDK deployment exception",{error:a}),s(a)}finally{clearTimeout(g),i&&i.stopMonitoring()}}async runCdkDestroy(e,r,t,n,o,l,u){const f=e.callerIdentity?.Account,y=e.region||h;let m=null;try{const d=M(r,e);f&&d&&o&&(m=await this.eventMonitor.createEventMonitor("destroy",d,y,e,o),m.startMonitoring(d,g=>{n?.(g)},(g,c)=>{}));const i=await this.destroy(e.path,r,{verbose:e.options?.verbose,outputCallback:t,useCdkOut:l,cdkOutputLogger:m?.getEventLogger()??void 0,context:C(e,f,y),credentials:u});return w(i)}catch(d){return s(`CDK destroy failed: ${p(k(d))}`)}finally{m&&m.stopMonitoring()}}}export{Z as CdkService};
3
+ `));const s={cdkOutput:"",actualStackName:i,stackDetected:!1,monitoringPromise:null},A=N(s,t,c,d);g=setTimeout(()=>{!s.stackDetected&&c&&!s.monitoringPromise&&(S.debug("CdkService","Fallback monitoring STARTING",{targetStackName:i,stackDetected:s.stackDetected,hasOnResourceProgress:!!n}),s.monitoringPromise=w(c,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:c?.getEventLogger()??void 0,context:b(e,y,f),credentials:u,...l!==void 0&&Object.keys(l).length>0&&{parameters:l}});return v(this.abortSignal)?(await P(i,f,u,t),a("Deployment cancelled")):E(s.cdkOutput,I,s.actualStackName)}catch(i){const s=`CDK deploy failed: ${p(k(i))}`;return S.error("CdkService","CDK deployment exception",{error:s}),a(s)}finally{clearTimeout(g),c&&c.stopMonitoring()}}async runCdkDestroy(e,r,t,n,o,u,l){const y=e.callerIdentity?.Account,f=e.region||h;let m=null;try{const d=M(r,e);y&&d&&o&&(m=await this.eventMonitor.createEventMonitor("destroy",d,f,e,o),m.startMonitoring(d,g=>{n?.(g)},(g,i)=>{}));const c=await this.destroy(e.path,r,{verbose:e.options?.verbose,outputCallback:t,useCdkOut:u,cdkOutputLogger:m?.getEventLogger()??void 0,context:b(e,y,f),credentials:l});return v(this.abortSignal)?a("Destroy cancelled"):K(c)}catch(d){return a(`CDK destroy failed: ${p(k(d))}`)}finally{m&&m.stopMonitoring()}}}export{te as CdkService};
@@ -1,5 +1,6 @@
1
1
  import type { EventLogWriterFactory } from "../../aws/utils/cloudformationEvents.js";
2
2
  import type { ICdkProcessManager } from "./ICdkProcessManager.js";
3
+ import type { FailureAnalysis } from "@fjall/util/aws";
3
4
  export interface CdkContext {
4
5
  accountId?: string;
5
6
  region?: string;
@@ -65,4 +66,18 @@ export interface CdkServiceOptions {
65
66
  * uses this instead of creating its own CdkProcessManager (which relies on
66
67
  * import.meta.url for binary resolution from deploy-core's node_modules). */
67
68
  processManager?: ICdkProcessManager;
69
+ /**
70
+ * Caller cancellation signal (CLI Ctrl-C / worker SIGTERM). When set,
71
+ * runCdkDeploy issues `CancelUpdateStack` on the in-flight stack after the
72
+ * aborted CDK child dies, so CloudFormation stops mutating the account.
73
+ * Sourced from `DeployParams.abortSignal`; the same signal is wired into the
74
+ * CDK child spawn via the process manager.
75
+ */
76
+ abortSignal?: AbortSignal;
77
+ /**
78
+ * Forwarded to every event monitor this service creates, so a stack failure
79
+ * emits its structured analysis over `DeployCallbacks.onFailureAnalysis`.
80
+ * Sourced from `DeployParams.callbacks.onFailureAnalysis` by the factory.
81
+ */
82
+ onFailureAnalysis?: (analysis: FailureAnalysis) => void;
68
83
  }
@@ -74,6 +74,17 @@ export declare class CloudFormationService {
74
74
  * to replace it.
75
75
  */
76
76
  stackExists(stackName: string, client?: CloudFormationClient): Promise<boolean>;
77
+ /**
78
+ * Fetch the deployed CloudFormation template for a stack, used by the deploy
79
+ * approval gate to diff the live template against the freshly synthesised one.
80
+ *
81
+ * Uses TemplateStage "Original" — the user-submitted template, which matches
82
+ * the raw cdk.out form (CDK resolves everything at synth, so Original and
83
+ * Processed coincide for CDK apps). A stack that has never deployed returns a
84
+ * stack_not_found failure; the gate treats that as an empty baseline (every
85
+ * resource is a creation).
86
+ */
87
+ getTemplate(stackName: string): Promise<Result<string, CloudFormationError>>;
77
88
  /**
78
89
  * Poll stack status until deletion completes, fails, or times out.
79
90
  */
@@ -1 +1 @@
1
- import{CloudFormationClient as d,DeleteStackCommand as h,DescribeStacksCommand as w,ListExportsCommand as k}from"@aws-sdk/client-cloudformation";import{stackStatusMap as S}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as f}from"@fjall/util";import{success as u,failure as c}from"@fjall/generator";import{BaseServiceError as x}from"../../types/errors/ServiceError.js";import{logger as y}from"@fjall/util/logger";import{getErrorMessage as p,sleep as m}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as E}from"@fjall/util/aws";import{isCleanableState as T}from"../../types/constants.js";import{extractErrorName as C}from"../../aws/organisations/types.js";class l extends x{errorType;stackName;stackStatus;constructor(t,s,o,r,e,n=!1){super(`CFN_${s.toUpperCase()}`,t,e,n),this.errorType=s,this.stackName=o,this.stackStatus=r}}class v{aws;constructor(t){this.aws=t}classifyAwsError(t,s,o){const r=C(t),e=f(p(t));return r==="CredentialsError"||r==="UnauthorizedError"?new l(`AWS credentials error: ${e}`,"auth_error",o,void 0,t,!1):r==="Throttling"||r==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${e}`,"throttled",o,void 0,t,!0):r==="NetworkingError"||r==="ENOTFOUND"?new l(`Network error: ${e}`,"network_error",o,void 0,t,!0):new l(`${s}: ${e}`,"unknown",o,void 0,t)}async getStackOutputs(t,s){s?.onStackCheck?.(t);const o=this.aws.getClient(d),r=new w({StackName:t});try{const n=(await o.send(r)).Stacks?.[0];if(!n?.Outputs)return u([]);const a=n.Outputs.map(i=>({OutputKey:i.OutputKey,OutputValue:i.OutputValue,ExportName:i.ExportName}));return s?.onOutputsRetrieved?.(t,a.length),u(a)}catch(e){if(e instanceof Error&&e.name==="ValidationError"&&e.message?.includes(E))return s?.onStackNotFound?.(t),u([]);const n=f(p(e));return c(new l(`Failed to get outputs for stack ${t}: ${n}`,"unknown",t,void 0,e))}}async getStackStatus(t,s){s?.onStackCheck?.(t);const o=this.aws.getClient(d),r=new w({StackName:t});try{const n=(await o.send(r)).Stacks?.[0];if(!n)return u({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const a=n.StackStatus||"UNKNOWN",i=S[a]||S.UNKNOWN;return s?.onStackFound?.(t,a),u({status:a,safeToRedeploy:i.safeToRedeploy,description:i.description,statusReason:n.StackStatusReason})}catch(e){return e instanceof Error&&e.name==="ValidationError"&&e.message?.includes(E)?u({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(e,`Failed to get stack status for ${t}`,t))}}async listAllExports(t){const s=this.aws.getClient(d),o=[];try{let r;do{const e=new k({NextToken:r}),n=await s.send(e),a=n.Exports||[];for(const i of a)i.Name&&i.Value&&o.push({Name:i.Name,Value:i.Value});if(t?.(a))break;r=n.NextToken}while(r);return u(o)}catch(r){const e=f(p(r));return c(new l(`Failed to list exports: ${e}`,"unknown",void 0,void 0,r,!1))}}async getExportsByNames(t){if(t.length===0)return u(new Map);const s=new Set(t),o=new Map,r=await this.listAllExports(e=>{for(const n of e)n.Name&&s.has(n.Name)&&n.Value&&o.set(n.Name,n.Value);return o.size>=s.size});return r.success?u(o):c(r.error)}async listExports(t){const s=await this.listAllExports();return s.success&&t?.onExportsRetrieved?.(s.data.length),s}async deleteStack(t){const s=this.aws.getClient(d);try{return await s.send(new h({StackName:t})),u(void 0)}catch(o){return c(this.classifyAwsError(o,`Failed to delete stack ${t}`,t))}}async stackExists(t,s){const o=s??this.aws.getClient(d);try{const e=(await o.send(new w({StackName:t}))).Stacks?.[0]?.StackStatus;return!!e&&e!=="REVIEW_IN_PROGRESS"&&!T(e)}catch(r){return r instanceof Error&&r.message?.includes(E)?!1:(y.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:t,error:p(r)}),!0)}}async waitForDeleteComplete(t,s){const o=s?.timeoutMs??6e5,r=s?.pollIntervalMs??5e3,e=Date.now();for(;Date.now()-e<o;){const n=await this.getStackStatus(t);if(!n.success){if(n.error.recoverable){s?.onProgress?.(`Transient error polling stack ${t}, retrying: ${f(n.error.message)}`),await m(r);continue}return c(n.error)}const a=n.data?.status;if(a==="DELETE_COMPLETE"||a==="DOES_NOT_EXIST")return u(void 0);if(a==="DELETE_FAILED")return c(new l(`Stack ${t} deletion failed: ${n.data?.statusReason||"unknown reason"}`,"stack_failed",t,a,void 0,!1));s?.onProgress?.(`Stack ${t} status: ${a??"unknown"}`),await m(r)}return c(new l(`Timed out waiting for stack ${t} deletion after ${Math.round(o/1e3)}s`,"timeout",t,void 0,void 0,!0))}}export{l as CloudFormationError,v as CloudFormationService};
1
+ import{CloudFormationClient as d,DeleteStackCommand as h,DescribeStacksCommand as m,GetTemplateCommand as k,ListExportsCommand as y}from"@aws-sdk/client-cloudformation";import{stackStatusMap as S}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as f}from"@fjall/util";import{success as i,failure as c}from"@fjall/generator";import{BaseServiceError as T}from"../../types/errors/ServiceError.js";import{logger as x}from"@fjall/util/logger";import{getErrorMessage as p,sleep as E}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as w}from"@fjall/util/aws";import{isCleanableState as C}from"../../types/constants.js";import{extractErrorName as O}from"../../aws/organisations/types.js";class l extends T{errorType;stackName;stackStatus;constructor(t,e,s,n,r,o=!1){super(`CFN_${e.toUpperCase()}`,t,r,o),this.errorType=e,this.stackName=s,this.stackStatus=n}}class I{aws;constructor(t){this.aws=t}classifyAwsError(t,e,s){const n=O(t),r=f(p(t));return n==="CredentialsError"||n==="UnauthorizedError"?new l(`AWS credentials error: ${r}`,"auth_error",s,void 0,t,!1):n==="Throttling"||n==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${r}`,"throttled",s,void 0,t,!0):n==="NetworkingError"||n==="ENOTFOUND"?new l(`Network error: ${r}`,"network_error",s,void 0,t,!0):new l(`${e}: ${r}`,"unknown",s,void 0,t)}async getStackOutputs(t,e){e?.onStackCheck?.(t);const s=this.aws.getClient(d),n=new m({StackName:t});try{const o=(await s.send(n)).Stacks?.[0];if(!o?.Outputs)return i([]);const a=o.Outputs.map(u=>({OutputKey:u.OutputKey,OutputValue:u.OutputValue,ExportName:u.ExportName}));return e?.onOutputsRetrieved?.(t,a.length),i(a)}catch(r){if(r instanceof Error&&r.name==="ValidationError"&&r.message?.includes(w))return e?.onStackNotFound?.(t),i([]);const o=f(p(r));return c(new l(`Failed to get outputs for stack ${t}: ${o}`,"unknown",t,void 0,r))}}async getStackStatus(t,e){e?.onStackCheck?.(t);const s=this.aws.getClient(d),n=new m({StackName:t});try{const o=(await s.send(n)).Stacks?.[0];if(!o)return i({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const a=o.StackStatus||"UNKNOWN",u=S[a]||S.UNKNOWN;return e?.onStackFound?.(t,a),i({status:a,safeToRedeploy:u.safeToRedeploy,description:u.description,statusReason:o.StackStatusReason})}catch(r){return r instanceof Error&&r.name==="ValidationError"&&r.message?.includes(w)?i({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(r,`Failed to get stack status for ${t}`,t))}}async listAllExports(t){const e=this.aws.getClient(d),s=[];try{let n;do{const r=new y({NextToken:n}),o=await e.send(r),a=o.Exports||[];for(const u of a)u.Name&&u.Value&&s.push({Name:u.Name,Value:u.Value});if(t?.(a))break;n=o.NextToken}while(n);return i(s)}catch(n){const r=f(p(n));return c(new l(`Failed to list exports: ${r}`,"unknown",void 0,void 0,n,!1))}}async getExportsByNames(t){if(t.length===0)return i(new Map);const e=new Set(t),s=new Map,n=await this.listAllExports(r=>{for(const o of r)o.Name&&e.has(o.Name)&&o.Value&&s.set(o.Name,o.Value);return s.size>=e.size});return n.success?i(s):c(n.error)}async listExports(t){const e=await this.listAllExports();return e.success&&t?.onExportsRetrieved?.(e.data.length),e}async deleteStack(t){const e=this.aws.getClient(d);try{return await e.send(new h({StackName:t})),i(void 0)}catch(s){return c(this.classifyAwsError(s,`Failed to delete stack ${t}`,t))}}async stackExists(t,e){const s=e??this.aws.getClient(d);try{const r=(await s.send(new m({StackName:t}))).Stacks?.[0]?.StackStatus;return!!r&&r!=="REVIEW_IN_PROGRESS"&&!C(r)}catch(n){return n instanceof Error&&n.message?.includes(w)?!1:(x.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:t,error:p(n)}),!0)}}async getTemplate(t){const e=this.aws.getClient(d);try{const s=await e.send(new k({StackName:t,TemplateStage:"Original"}));return i(s.TemplateBody??"")}catch(s){return s instanceof Error&&s.message?.includes(w)?c(new l(`Stack ${t} not found`,"stack_not_found",t)):c(this.classifyAwsError(s,`Failed to get template for stack ${t}`,t))}}async waitForDeleteComplete(t,e){const s=e?.timeoutMs??6e5,n=e?.pollIntervalMs??5e3,r=Date.now();for(;Date.now()-r<s;){const o=await this.getStackStatus(t);if(!o.success){if(o.error.recoverable){e?.onProgress?.(`Transient error polling stack ${t}, retrying: ${f(o.error.message)}`),await E(n);continue}return c(o.error)}const a=o.data?.status;if(a==="DELETE_COMPLETE"||a==="DOES_NOT_EXIST")return i(void 0);if(a==="DELETE_FAILED")return c(new l(`Stack ${t} deletion failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",t,a,void 0,!1));e?.onProgress?.(`Stack ${t} status: ${a??"unknown"}`),await E(n)}return c(new l(`Timed out waiting for stack ${t} deletion after ${Math.round(s/1e3)}s`,"timeout",t,void 0,void 0,!0))}}export{l as CloudFormationError,I as CloudFormationService};
@@ -0,0 +1,22 @@
1
+ interface CancelStackCredentials {
2
+ accessKeyId: string;
3
+ secretAccessKey: string;
4
+ sessionToken?: string;
5
+ }
6
+ export type CancelInFlightOutcome = "cancelled-update" | "create-in-progress" | "not-cancellable" | "no-op";
7
+ /**
8
+ * Describe the stack and, if it is mid-update, issue CancelUpdateStack.
9
+ *
10
+ * - `UPDATE_IN_PROGRESS` → CancelUpdateStack (CFN rolls back to last good state).
11
+ * - `CREATE_IN_PROGRESS` → CancelUpdateStack is invalid for a first create; we
12
+ * leave it (CFN's own create-failure default is rollback, and the next deploy's
13
+ * stack-cleanup reconciles a ROLLBACK_* stack) and report so the operator can
14
+ * `fjall destroy <target>` if they want it gone now.
15
+ * - anything else / no stack → no-op (abort fired before any CFN op, or the op
16
+ * already reached a terminal state).
17
+ *
18
+ * @param onOutput optional sink for an operator-facing line (already inside the
19
+ * deploy output stream — caller masks at its own boundary if needed).
20
+ */
21
+ export declare function cancelInFlightStack(stackName: string, region: string, credentials?: CancelStackCredentials, onOutput?: (message: string) => void): Promise<CancelInFlightOutcome>;
22
+ export {};
@@ -0,0 +1,3 @@
1
+ import{CloudFormationClient as p,DescribeStacksCommand as f,CancelUpdateStackCommand as C}from"@aws-sdk/client-cloudformation";import{NodeHttpHandler as m}from"@smithy/node-http-handler";import{logger as e}from"@fjall/util/logger";import{getErrorMessage as d,maskSensitiveOutput as u}from"@fjall/util";import{composeSdkAbortSignal as S}from"../../aws/organisations/types.js";import{STACK_NOT_FOUND_PATTERN as g}from"../../types/constants.js";const t="CancelInFlightStack",i=15e3;async function R(n,o,c,s){try{const a=new p({region:o,...c!==void 0?{credentials:c}:{},requestHandler:new m({requestTimeout:i})});let r;try{r=(await a.send(new f({StackName:n}),{abortSignal:S(void 0,i)})).Stacks?.[0]?.StackStatus}catch(l){return l instanceof Error&&l.message?.includes(g)?(e.debug(t,`Stack ${n} does not exist \u2014 nothing to cancel`),"no-op"):(e.warn(t,`Could not read stack status to cancel: ${u(d(l))}`,{stackName:n,region:o}),"no-op")}return r==="UPDATE_IN_PROGRESS"?(s?.(`Cancelling deployment \u2014 CloudFormation is rolling back ${n}\u2026
2
+ `),await a.send(new C({StackName:n}),{abortSignal:S(void 0,i)}),e.warn(t,`Requested rollback of ${n}`,{region:o}),"cancelled-update"):r==="CREATE_IN_PROGRESS"?(s?.(`Deployment cancelled. The initial stack ${n} is still being created in AWS; it will roll back if it fails, or run \`fjall destroy\` to remove it.
3
+ `),e.warn(t,`${n} is CREATE_IN_PROGRESS \u2014 CancelUpdateStack not applicable; leaving for CFN rollback / stack-cleanup`,{region:o}),"create-in-progress"):(e.debug(t,`Stack ${n} status ${r??"unknown"} is not an in-flight update \u2014 no cancel issued`),r===void 0?"no-op":"not-cancellable")}catch(a){return e.warn(t,`CancelUpdateStack attempt failed: ${u(d(a))}`,{stackName:n,region:o}),"no-op"}}export{R as cancelInFlightStack};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Engine-level approval gate — the injected "here's exactly what will change,
3
+ * approve to proceed" decision point at the read→mutate boundary of a deploy.
4
+ *
5
+ * Each surface (CLI interactive Ink, CLI non-interactive, webapp, agent-MCP)
6
+ * provides a resolver; parity is by construction — they all receive the same
7
+ * {@link ApprovalRequest} and return the same {@link GateResolution} union.
8
+ *
9
+ * Omitting the gate (`DeployParams.approvalGate` undefined) is fully
10
+ * backward-compatible: the engine deploys exactly as it does today.
11
+ */
12
+ import type { DeployPlan } from "../orchestration/application/plan/types.js";
13
+ /** What the gate hands a surface resolver so it can decide. */
14
+ export interface ApprovalRequest {
15
+ /** Target app / deployment name. */
16
+ target: string;
17
+ /** The computed, resource-level plan. */
18
+ plan: DeployPlan;
19
+ /**
20
+ * A signed, digest-bound token. A surface that approves out-of-band (webapp,
21
+ * MCP) echoes this back on the resuming apply call; the engine re-checks its
22
+ * bound digest against a fresh synth before mutating (TOCTOU guard).
23
+ */
24
+ approvalToken: string;
25
+ /** ISO-8601 expiry of the token. */
26
+ expiresAt: string;
27
+ /** The plan has destructive changes (a replace or a destroy). */
28
+ hasDestructiveChanges: boolean;
29
+ }
30
+ /**
31
+ * A surface's decision on an approval request.
32
+ *
33
+ * - `approved` — proceed to mutate (the surface confirmed synchronously).
34
+ * - `rejected` — decline before any mutation; the run ends as `rejected`.
35
+ * - `suspended` — return control without a synchronous decision (webapp phase 1:
36
+ * persist the awaiting state and resume later via the apply call with the
37
+ * token). The engine ends the run as awaiting, having mutated nothing.
38
+ */
39
+ export type GateResolution = {
40
+ kind: "approved";
41
+ actor?: string;
42
+ } | {
43
+ kind: "rejected";
44
+ reason: string;
45
+ actor?: string;
46
+ } | {
47
+ kind: "suspended";
48
+ approvalToken: string;
49
+ };
50
+ /** The injected approval gate a surface supplies on {@link DeployParams}. */
51
+ export interface ApprovalGate {
52
+ resolve(request: ApprovalRequest): Promise<GateResolution>;
53
+ }
File without changes
@@ -1,6 +1,6 @@
1
1
  import type { DeployCallbacks } from "./callbacks.js";
2
2
  /**
3
- * Every {@link DeployCallbacks} key (45 callbacks + the `verbose` flag),
3
+ * Every {@link DeployCallbacks} key (46 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,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,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,8 +1,13 @@
1
1
  import type { ProgressEvent, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent, TrailMigrationPhaseEvent } from "./events.js";
2
2
  import type { TrailLifecycleState } from "@fjall/util/config";
3
+ import type { FailureAnalysis } from "@fjall/util/aws";
3
4
  import type { DetectionResult } from "./detection/detection.js";
4
5
  import type { CascadeLedger } from "../orchestration/organisation/cascadeSummary.js";
5
- export type StepCompleteStatus = "completed" | "skipped" | "error";
6
+ import type { STEP_COMPLETE_STATUSES, STACK_CLEANUP_PHASES, CASCADE_ACCOUNT_PHASES, LOG_LEVELS } from "./deployEvent.js";
7
+ export type StepCompleteStatus = (typeof STEP_COMPLETE_STATUSES)[number];
8
+ export type StackCleanupPhase = (typeof STACK_CLEANUP_PHASES)[number];
9
+ export type CascadeAccountPhase = (typeof CASCADE_ACCOUNT_PHASES)[number];
10
+ export type LogLevel = (typeof LOG_LEVELS)[number];
6
11
  /**
7
12
  * Structured payload for the "quarantine-suspected" stack-cleanup phase:
8
13
  * an AccessDenied empty pass on a DELETE_FAILED bucket, the earliest signal
@@ -58,7 +63,7 @@ export interface DeployCallbacks {
58
63
  /** @emittedBy engine */
59
64
  onProgress?: (event: ProgressEvent) => void;
60
65
  /** @emittedBy engine */
61
- onLog?: (message: string, level?: "info" | "debug" | "warn") => void;
66
+ onLog?: (message: string, level?: LogLevel) => void;
62
67
  /** @emittedBy engine */
63
68
  onOutput?: (chunk: string) => void;
64
69
  /** @emittedBy engine — per-resource CFN events during sequential stack deployment. */
@@ -176,7 +181,7 @@ export interface DeployCallbacks {
176
181
  /** @emittedBy engine */
177
182
  onCascadeAccountStart?: (operationKey: string, accountId: string, region: string, deployType?: string) => void;
178
183
  /** @emittedBy engine — per-account bootstrap/synth/deploy/destroy phase transitions. */
179
- onCascadeAccountPhaseChange?: (operationKey: string, phase: "bootstrap" | "synth" | "deploy" | "destroy", region?: string) => void;
184
+ onCascadeAccountPhaseChange?: (operationKey: string, phase: CascadeAccountPhase, region?: string) => void;
180
185
  /** @emittedBy engine — per-account CFN resource events during cascade. */
181
186
  onCascadeAccountResourceProgress?: (operationKey: string, event: ResourceEvent, region?: string) => void;
182
187
  /**
@@ -215,7 +220,7 @@ export interface DeployCallbacks {
215
220
  * be renamed) and "retained-buckets" (the production-retained set, named
216
221
  * so operators delete deliberately).
217
222
  */
218
- onStackCleanupProgress?: (stackName: string, phase: "emptying-bucket" | "deleting-stack" | "waiting" | "complete" | "error" | "quarantine-suspected" | "retained-buckets", detail?: StackCleanupQuarantineDetail | StackCleanupRetainedBucketsDetail) => void;
223
+ onStackCleanupProgress?: (stackName: string, phase: StackCleanupPhase, detail?: StackCleanupQuarantineDetail | StackCleanupRetainedBucketsDetail) => void;
219
224
  /**
220
225
  * @emittedBy engine — fired after the detection pipeline completes with full analysis results.
221
226
  * May be async — engine awaits the return value so callers can run post-detection
@@ -239,6 +244,15 @@ export interface DeployCallbacks {
239
244
  onOpenNextBuildComplete?: () => void;
240
245
  /** @emittedBy engine — fired when OpenNext build fails. */
241
246
  onOpenNextBuildError?: (error: string) => void;
247
+ /**
248
+ * @emittedBy engine — fired once when a CloudFormation stack reaches a failed
249
+ * terminal state, carrying the structured root-cause analysis (category,
250
+ * dependency chain, remediation) the engine already computes. Lets every
251
+ * consumer render the same actionable failure detail instead of re-deriving
252
+ * it (CLI non-interactive) or under-rendering a single raw string (CLI Ink,
253
+ * webapp). Pairs with `onError`, which still carries the terminal Error.
254
+ */
255
+ onFailureAnalysis?: (analysis: FailureAnalysis) => void;
242
256
  /** @emittedBy engine */
243
257
  onError?: (error: Error) => void;
244
258
  verbose?: boolean;
@@ -22,6 +22,13 @@ export interface Entitlements {
22
22
  allowedTiers: string[];
23
23
  assetManagement: boolean;
24
24
  complianceAudit: "none" | "basic" | "full";
25
+ /**
26
+ * Governance/security tiers this plan may deploy (the `--security`
27
+ * foundation/compliance/hardened bundle). Optional for forward-compat: an
28
+ * older webapp omits it, which the CLI treats as "none entitled"
29
+ * (fail-closed) when gating a requested governance tier.
30
+ */
31
+ governanceTiers?: string[];
25
32
  };
26
33
  metricsRetentionHours: number;
27
34
  /** "full" = all visible, "limited" = top 1 only, rest blurred */
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The single typed producer for {@link DeployEvent}s — the one place that stamps
3
+ * the envelope (contract marker + monotonic `seq` + timestamp) and masks
4
+ * credential-bearing fields before they reach the `onEvent` sink. Engine code
5
+ * calls one method per signal; the method projects to the matching union member.
6
+ *
7
+ * Masking boundary: every value-bearing free-text field (messages, errors,
8
+ * reasons, output chunks, stack-output values) is routed through
9
+ * {@link maskSensitiveOutput} here, so no consumer downstream re-masks. Identifier
10
+ * fields (ARNs, account ids, resource names, digests) pass through unmasked — they
11
+ * disclose nothing exploitable.
12
+ *
13
+ * Param types are DERIVED from the schema via {@link PayloadOf}, so adding a field
14
+ * to a variant in `deployEvent.ts` updates the emitter's input type with no edit
15
+ * here; only new masking is manual.
16
+ */
17
+ import { DEPLOY_EVENT_CONTRACT, type DeployEvent, type ProgressKind } from "./deployEvent.js";
18
+ /** The envelope fields stamped on every event (contract is the literal type — B1). */
19
+ interface DeployEnvelopeShape {
20
+ contract: typeof DEPLOY_EVENT_CONTRACT;
21
+ deploymentId: string;
22
+ seq: number;
23
+ time: string;
24
+ }
25
+ type EventOf<K extends ProgressKind> = Extract<DeployEvent, {
26
+ progressKind: K;
27
+ }>;
28
+ /** A variant's payload — the member minus the envelope and the discriminator. */
29
+ type PayloadOf<K extends ProgressKind> = Omit<EventOf<K>, keyof DeployEnvelopeShape | "progressKind">;
30
+ /**
31
+ * The typed emitter. One method per progressKind; each returns the constructed
32
+ * event (so a caller can inspect or forward it). Methods whose payload carries no
33
+ * free-text take the payload verbatim; methods with maskable fields mask them.
34
+ */
35
+ export interface DeployEmitter {
36
+ stepStarted(p: PayloadOf<"step.started">): DeployEvent;
37
+ stepCompleted(p: PayloadOf<"step.completed">): DeployEvent;
38
+ log(p: PayloadOf<"log">): DeployEvent;
39
+ output(p: PayloadOf<"output">): DeployEvent;
40
+ resource(p: PayloadOf<"resource">): DeployEvent;
41
+ parallelPhase(p: PayloadOf<"phase.parallel">): DeployEvent;
42
+ buildStarted(p: PayloadOf<"build.started">): DeployEvent;
43
+ buildProgress(p: PayloadOf<"build.progress">): DeployEvent;
44
+ buildCompleted(p: PayloadOf<"build.completed">): DeployEvent;
45
+ taskDefRegistered(p: PayloadOf<"taskdef.registered">): DeployEvent;
46
+ ecsUpdate(p: PayloadOf<"ecs.update">): DeployEvent;
47
+ ecsProgress(p: PayloadOf<"ecs.progress">): DeployEvent;
48
+ ecsCompleted(p: PayloadOf<"ecs.completed">): DeployEvent;
49
+ migrationsStarted(p: PayloadOf<"migrations.started">): DeployEvent;
50
+ migrationsCompleted(p: PayloadOf<"migrations.completed">): DeployEvent;
51
+ cdkBootstrap(p: PayloadOf<"cdk.bootstrap">): DeployEvent;
52
+ cdkOutput(p: PayloadOf<"cdk.output">): DeployEvent;
53
+ detectionCompleted(p: PayloadOf<"detection.completed">): DeployEvent;
54
+ detectionPhase(p: PayloadOf<"detection.phase">): DeployEvent;
55
+ cascadeStarted(): DeployEvent;
56
+ cascadeAccountsReconciled(p: PayloadOf<"cascade.accountsReconciled">): DeployEvent;
57
+ cascadeOrgChanges(p: PayloadOf<"cascade.orgChanges">): DeployEvent;
58
+ cascadeMissingAccounts(p: PayloadOf<"cascade.missingAccounts">): DeployEvent;
59
+ cascadePhase(p: PayloadOf<"cascade.phase">): DeployEvent;
60
+ cascadeAccountStarted(p: PayloadOf<"cascade.account.started">): DeployEvent;
61
+ cascadeAccountPhase(p: PayloadOf<"cascade.account.phase">): DeployEvent;
62
+ cascadeAccountCompleted(p: PayloadOf<"cascade.account.completed">): DeployEvent;
63
+ cascadeCompleted(p: PayloadOf<"cascade.completed">): DeployEvent;
64
+ cascadeLedger(p: PayloadOf<"cascade.ledger">): DeployEvent;
65
+ trailPhase(p: PayloadOf<"trail.phase">): DeployEvent;
66
+ trailLifecycle(p: PayloadOf<"trail.lifecycle">): DeployEvent;
67
+ cleanupProgress(p: PayloadOf<"cleanup.progress">): DeployEvent;
68
+ authSsoUrl(p: PayloadOf<"auth.ssoUrl">): DeployEvent;
69
+ authCompleted(p: PayloadOf<"auth.completed">): DeployEvent;
70
+ error(p: PayloadOf<"error">): DeployEvent;
71
+ deployCompleted(p: PayloadOf<"deploy.completed">): DeployEvent;
72
+ planAvailable(p: PayloadOf<"plan.available">): DeployEvent;
73
+ approvalAwaiting(p: PayloadOf<"approval.awaiting">): DeployEvent;
74
+ approvalResolved(p: PayloadOf<"approval.resolved">): DeployEvent;
75
+ }
76
+ export interface CreateDeployEmitterOptions {
77
+ deploymentId: string;
78
+ onEvent: (event: DeployEvent) => void;
79
+ /** Defaults to `() => new Date().toISOString()` — Z-form RFC3339; the schema rejects offset times. */
80
+ now?: () => string;
81
+ /** First `seq` value (defaults to 0). */
82
+ startSeq?: number;
83
+ }
84
+ export declare function createDeployEmitter(options: CreateDeployEmitterOptions): DeployEmitter;
85
+ export {};
@@ -0,0 +1 @@
1
+ import{maskSensitiveOutput as v}from"@fjall/util";import{DEPLOY_EVENT_CONTRACT as y,DEPLOY_EVENT_ERROR_MESSAGE_MAX as d,DEPLOY_EVENT_MESSAGE_MAX as l,DEPLOY_EVENT_OUTPUT_MAX as m,DEPLOY_EVENT_STATUS_REASON_MAX as g,DEPLOY_EVENT_TRAIL_DETAIL_MAX as K,DEPLOY_EVENT_URL_MAX as h}from"./deployEvent.js";function t(i,c){const u=v(i);return u.length>c?u.slice(0,c):u}function A(i){const c={};for(const[u,p]of Object.entries(i))c[u]=t(p,g);return c}function D(i){const{deploymentId:c,onEvent:u}=i,p=i.now??(()=>new Date().toISOString());let f=i.startSeq??0;function n(){return{contract:y,deploymentId:c,seq:f++,time:p()}}function s(e){return u(e),e}return{stepStarted(e){const r={...n(),progressKind:"step.started",...e};return s(r)},stepCompleted(e){const r={...n(),progressKind:"step.completed",step:e.step,status:e.status,...e.progress!==void 0&&{progress:e.progress},...e.errorMessage!==void 0&&{errorMessage:t(e.errorMessage,d)}};return s(r)},log(e){const r={...n(),progressKind:"log",level:e.level,message:t(e.message,l)};return s(r)},output(e){const r={...n(),progressKind:"output",chunk:t(e.chunk,m)};return s(r)},resource(e){const r={...n(),progressKind:"resource",resource:{...e.resource,...e.resource.statusReason!==void 0&&{statusReason:t(e.resource.statusReason,g)}},...e.scope!==void 0&&{scope:e.scope}};return s(r)},parallelPhase(e){const r={...n(),progressKind:"phase.parallel",status:e.status,stacks:e.stacks,...e.description!==void 0&&{description:e.description},...e.results!==void 0&&{results:e.results.map(a=>({stack:a.stack,success:a.success,...a.error!==void 0&&{error:t(a.error,d)}}))}};return s(r)},buildStarted(e){const r={...n(),progressKind:"build.started",...e};return s(r)},buildProgress(e){const r={...n(),progressKind:"build.progress",builder:e.builder,message:t(e.message,l),...e.serviceName!==void 0&&{serviceName:e.serviceName},...e.percentage!==void 0&&{percentage:e.percentage},...e.layerId!==void 0&&{layerId:e.layerId},...e.status!==void 0&&{status:e.status}};return s(r)},buildCompleted(e){const r={...n(),progressKind:"build.completed",builder:e.builder,success:e.success,...e.serviceName!==void 0&&{serviceName:e.serviceName},...e.imageTag!==void 0&&{imageTag:e.imageTag},...e.imageDigest!==void 0&&{imageDigest:e.imageDigest},...e.imageUri!==void 0&&{imageUri:e.imageUri},...e.ecrRepositoryArn!==void 0&&{ecrRepositoryArn:e.ecrRepositoryArn},...e.durationMs!==void 0&&{durationMs:e.durationMs},...e.error!==void 0&&{error:t(e.error,d)}};return s(r)},taskDefRegistered(e){const r={...n(),progressKind:"taskdef.registered",...e};return s(r)},ecsUpdate(e){const r={...n(),progressKind:"ecs.update",...e};return s(r)},ecsProgress(e){const r={...n(),progressKind:"ecs.progress",message:t(e.message,l),...e.percentage!==void 0&&{percentage:e.percentage}};return s(r)},ecsCompleted(e){const r={...n(),progressKind:"ecs.completed",serviceName:e.serviceName,serviceArn:e.serviceArn,success:e.success,taskDefinitionArn:e.taskDefinitionArn,durationMs:e.durationMs,...e.reason!==void 0&&{reason:t(e.reason,g)},...e.finalRunningCount!==void 0&&{finalRunningCount:e.finalRunningCount},...e.finalDesiredCount!==void 0&&{finalDesiredCount:e.finalDesiredCount}};return s(r)},migrationsStarted(e){const r={...n(),progressKind:"migrations.started",...e};return s(r)},migrationsCompleted(e){const r={...n(),progressKind:"migrations.completed",family:e.family,taskDefinitionArn:e.taskDefinitionArn,success:e.success,durationMs:e.durationMs,...e.exitCode!==void 0&&{exitCode:e.exitCode},...e.reason!==void 0&&{reason:t(e.reason,g)}};return s(r)},cdkBootstrap(e){const r={...n(),progressKind:"cdk.bootstrap",...e};return s(r)},cdkOutput(e){const r={...n(),progressKind:"cdk.output",outputType:e.outputType,output:t(e.output,m)};return s(r)},detectionCompleted(e){const r={...n(),progressKind:"detection.completed",...e};return s(r)},detectionPhase(e){const r={...n(),progressKind:"detection.phase",...e};return s(r)},cascadeStarted(){const e={...n(),progressKind:"cascade.started"};return s(e)},cascadeAccountsReconciled(e){const r={...n(),progressKind:"cascade.accountsReconciled",...e};return s(r)},cascadeOrgChanges(e){const r={...n(),progressKind:"cascade.orgChanges",...e};return s(r)},cascadeMissingAccounts(e){const r={...n(),progressKind:"cascade.missingAccounts",...e};return s(r)},cascadePhase(e){const r={...n(),progressKind:"cascade.phase",...e};return s(r)},cascadeAccountStarted(e){const r={...n(),progressKind:"cascade.account.started",...e};return s(r)},cascadeAccountPhase(e){const r={...n(),progressKind:"cascade.account.phase",...e};return s(r)},cascadeAccountCompleted(e){const r={...n(),progressKind:"cascade.account.completed",operationKey:e.operationKey,success:e.success,...e.error!==void 0&&{error:t(e.error,d)},...e.region!==void 0&&{region:e.region},...e.outputs!==void 0&&{outputs:A(e.outputs)},...e.skipped!==void 0&&{skipped:e.skipped}};return s(r)},cascadeCompleted(e){const r={...n(),progressKind:"cascade.completed",result:{platformDeployed:e.result.platformDeployed,platformSkipped:e.result.platformSkipped,platformFailed:e.result.platformFailed,domainsDeployed:e.result.domainsDeployed,accountsDeployed:e.result.accountsDeployed,accountsSkipped:e.result.accountsSkipped,accountsFailed:e.result.accountsFailed,errors:e.result.errors.map(a=>({accountId:a.accountId,error:t(a.error,d)}))}};return s(r)},cascadeLedger(e){const{ledger:r}=e,a={...n(),progressKind:"cascade.ledger",ledger:{members:r.members.map(o=>({accountId:o.accountId,accountName:o.accountName,region:o.region,result:o.result,durationMs:o.durationMs,...o.error!==void 0&&{error:t(o.error,d)}})),...r.platform!==void 0&&{platform:{accountId:r.platform.accountId,result:r.platform.result,durationMs:r.platform.durationMs,...r.platform.error!==void 0&&{error:t(r.platform.error,d)}}},domainsDeployed:r.domainsDeployed,errors:r.errors.map(o=>({accountId:o.accountId,error:t(o.error,d)})),totalDurationMs:r.totalDurationMs}};return s(a)},trailPhase(e){const r={...n(),progressKind:"trail.phase",accountId:e.accountId,accountName:e.accountName,phase:e.phase,status:e.status,...e.detail!==void 0&&{detail:t(e.detail,K)}};return s(r)},trailLifecycle(e){const r={...n(),progressKind:"trail.lifecycle",accountId:e.accountId,to:e.to,...e.from!==void 0&&{from:e.from}};return s(r)},cleanupProgress(e){const r={...n(),progressKind:"cleanup.progress",stackName:e.stackName,phase:e.phase,...e.detail!==void 0&&{detail:e.detail}};return s(r)},authSsoUrl(e){const r={...n(),progressKind:"auth.ssoUrl",url:e.url===null?null:t(e.url,h)};return s(r)},authCompleted(e){const r={...n(),progressKind:"auth.completed",...e};return s(r)},error(e){const r={...n(),progressKind:"error",message:t(e.message,d),...e.name!==void 0&&{name:e.name}};return s(r)},deployCompleted(e){const r={...n(),progressKind:"deploy.completed",result:e.result,ok:e.ok,...e.errorType!==void 0&&{errorType:e.errorType},message:t(e.message,l)};return s(r)},planAvailable(e){const r={...n(),progressKind:"plan.available",plan:{summary:t(e.plan.summary,l),changeCount:e.plan.changeCount,hasDestructiveChanges:e.plan.hasDestructiveChanges,...e.plan.changes!==void 0&&{changes:e.plan.changes},...e.plan.diffRef!==void 0&&{diffRef:e.plan.diffRef},...e.plan.iamBroadening!==void 0&&{iamBroadening:e.plan.iamBroadening},...e.plan.securityGroupBroadening!==void 0&&{securityGroupBroadening:e.plan.securityGroupBroadening},...e.plan.changeCountsByAction!==void 0&&{changeCountsByAction:e.plan.changeCountsByAction}}};return s(r)},approvalAwaiting(e){const r={...n(),progressKind:"approval.awaiting",approval:{token:e.approval.token,kind:e.approval.kind,reason:t(e.approval.reason,l),expiresAt:e.approval.expiresAt,...e.approval.assemblyDigest!==void 0&&{assemblyDigest:e.approval.assemblyDigest},...e.approval.changeCounts!==void 0&&{changeCounts:e.approval.changeCounts},...e.approval.approvalClass!==void 0&&{approvalClass:e.approval.approvalClass},...e.approval.requiredApprovers!==void 0&&{requiredApprovers:e.approval.requiredApprovers}}};return s(r)},approvalResolved(e){const r={...n(),progressKind:"approval.resolved",...e};return s(r)}}}export{D as createDeployEmitter};