@fjall/deploy-core 2.5.0 → 2.7.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 (37) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/index.d.ts +4 -0
  3. package/dist/src/index.js +1 -1
  4. package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -1
  5. package/dist/src/orchestration/cascadeHelpers.d.ts +48 -5
  6. package/dist/src/orchestration/cascadeHelpers.js +1 -1
  7. package/dist/src/orchestration/cascadeSummary.d.ts +87 -0
  8. package/dist/src/orchestration/cascadeSummary.js +1 -0
  9. package/dist/src/orchestration/contextHelpers.d.ts +2 -0
  10. package/dist/src/orchestration/contextHelpers.js +1 -1
  11. package/dist/src/orchestration/index.d.ts +4 -1
  12. package/dist/src/orchestration/index.js +1 -1
  13. package/dist/src/orchestration/organisationDeploy.js +5 -3
  14. package/dist/src/orchestration/organisationDestroy.js +3 -3
  15. package/dist/src/orchestration/stackCleanup.d.ts +2 -5
  16. package/dist/src/orchestration/stackCleanup.js +1 -1
  17. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
  18. package/dist/src/services/infrastructure/CdkService.d.ts +1 -1
  19. package/dist/src/services/infrastructure/CdkService.js +3 -3
  20. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +1 -0
  21. package/dist/src/services/infrastructure/CloudFormationService.d.ts +7 -1
  22. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  23. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
  24. package/dist/src/services/infrastructure/constructMapEnrichment.d.ts +1 -1
  25. package/dist/src/services/infrastructure/constructMapEnrichment.js +1 -1
  26. package/dist/src/services/supporting/CdkContextBuilder.d.ts +3 -0
  27. package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
  28. package/dist/src/services/supporting/TemplateHashService.d.ts +21 -0
  29. package/dist/src/services/supporting/TemplateHashService.js +1 -1
  30. package/dist/src/types/FjallState.d.ts +21 -0
  31. package/dist/src/types/FjallState.js +1 -1
  32. package/dist/src/types/callbacks.d.ts +28 -3
  33. package/dist/src/types/constants.d.ts +10 -0
  34. package/dist/src/types/constants.js +1 -1
  35. package/dist/src/types/deployment/DeploymentTypes.d.ts +17 -0
  36. package/dist/src/types/events.d.ts +3 -0
  37. package/package.json +4 -4
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 118 files minified at 2026-05-29T04:05:16.658Z
1
+ 119 files minified at 2026-06-01T11:35:13.709Z
@@ -43,6 +43,10 @@ export { type Result, success, failure, isSuccess, isFailure } from "@fjall/gene
43
43
  export { deploy } from "./orchestration/index.js";
44
44
  export { destroy } from "./orchestration/index.js";
45
45
  export { partitionAccounts } from "./orchestration/index.js";
46
+ export { buildRegionList, buildAccountRegionPairs } from "./orchestration/index.js";
47
+ export type { AccountRegionPair } from "./orchestration/index.js";
48
+ export { projectScalarSummary, projectAccountRows } from "./orchestration/index.js";
49
+ export type { CascadeOutcomeResult, CascadeMemberOutcome, CascadePlatformOutcome, CascadeLedger, CascadeAccountRow, CascadePlatformRow, CascadeAccountProjection } from "./orchestration/index.js";
46
50
  export { reconcileProviderAccounts, mergeReconciledProviderAccounts } from "./orchestration/index.js";
47
51
  export type { ReconcileResult } from "./orchestration/index.js";
48
52
  export { parseAccountsConfiguration, flattenAccountsToEnvironments, extractAllAccountNames, accountsConfigToOUTree, isStringArray, isAccountsConfig } from "./orchestration/index.js";
package/dist/src/index.js CHANGED
@@ -1 +1 @@
1
- import{DeploymentEventSchema as t,DEPLOYMENT_EVENT_TYPES as o,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as a,CASCADE_PHASES as i,CASCADE_ACCOUNT_STATUSES as s}from"./types/index.js";import{SimpleAwsProvider as c}from"./aws/index.js";import{ensureOrganisationExists as p,describeOrganisation as S,enablePolicyTypes as l,enableServiceAccess as A,enableRamSharing as u,activateTrustedAccess as T,enableIpamDelegatedAdmin as m,updateBackupGlobalSettings as O,listAccounts as P,findAccount as d,createAccount as _,ensureOrganisationalUnitsExist as C,placeAccountsInOUs as f,buildAccountToOUMap as R,activateCostAllocationTags as N,checkIdentityCentreStatus as g,extractErrorName as x,isOULeaf as D,registerSecurityDelegates as I,SECURITY_SERVICE_PRINCIPALS as L}from"./aws/index.js";import{STEP_IDS as k,STEP_NAMES as F,INFRASTRUCTURE_STEP_NAMES as y,INFRA_STEP_NAME as U}from"./types/index.js";import{ProgressReporter as h,APPLICATION_STACKS as Y,ORGANISATION_TYPES as b,APPLICATION_DEPLOY_ORDER as G,APPLICATION_DESTROY_ORDER as B,OPENNEXT_DEPLOY_ORDER as w,OPENNEXT_DESTROY_ORDER as H,PARALLEL_DEPLOY_GROUPS as K,PARALLEL_DESTROY_GROUPS as V,OPENNEXT_PARALLEL_GROUPS as X,PARALLEL_OPERATION_TYPES as j,isApplicationOperation as q,isOrganisationOperation as z,getParallelDeployGroups as J,getParallelDestroyGroups as Q,getApplicationDeployOrder as W,getApplicationDestroyOrder as Z,getApplicationStackName as $,getOrganisationStackName as ee,isApplicationStack as re,getApplicationStepName as te,getApplicationStepId as oe,toPascalCase as ae,isOpenNextPattern as ie,OPENNEXT_PATTERNS as se,deriveResourcesFromManifestStacks as ne,STACK_NOT_FOUND_PATTERN as ce,STACK_FAILED_STATE_PATTERN as Ee,CDK_NO_STACKS_MATCH as pe,INFRASTRUCTURE_FILENAME as Se,ApplicationError as le,wrapApplicationError as Ae,FjallStateFileSchema as ue,readStateFile as Te,writeStateFile as me,createEmptyState as Oe,deleteStateFile as Pe,updateTemplateHash as de,getStateFilePath as _e,stubCallerIdentity as Ce}from"./types/index.js";import{detectPattern as Re}from"./types/index.js";import{detectPayloadPattern as ge}from"./types/index.js";import{detectDatabase as De}from"./types/index.js";import{CloudFormationEventMonitor as Le}from"./aws/index.js";import{CdkService as ke,CdkArgumentBuilder as Fe,CdkProcessManager as ye,CdkEventMonitor as Ue,startStackMonitoring as Me,DEFAULT_DEPLOY_TIMEOUT_MS as he,isCdkError as Ye,formatInfrastructureError as be,getStructuralHint as Ge,getSourceContext as Be,hasCdkDifferences as we,parseDiffOutput as He,CloudFormationService as Ke,CloudFormationError as Ve,EcsService as Xe,EcsError as je,EcsServiceResolver as qe,TemplateHashService as ze,TemplateHashError as Je,CdkContextBuilder as Qe,emitProgress as We,PROGRESS_MESSAGES as Ze,parseBuildPhase as $e,buildStepContextBuildConfig as er,convertCloudFormationOutputsToRecord as rr,ApplicationStackService as tr}from"./services/index.js";import{CdkError as ar}from"./types/errors/index.js";import{BaseServiceError as sr,ValidationError as nr,AuthError as cr,AwsError as Er,DeploymentError as pr,NetworkError as Sr,FileSystemError as lr,ConfigError as Ar,toServiceError as ur}from"./types/errors/index.js";import{filterDangerousEnvVars as mr,maskSensitiveOutput as Or,parseShellArgs as Pr,sleep as dr}from"@fjall/util";import{hasDockerfile as Cr}from"./util/dockerfileDetection.js";import{createSequencedCallbacks as Rr}from"./util/sequencedCallbacks.js";import{fileExists as gr}from"@fjall/util/fsHelpers";import{success as Dr,failure as Ir,isSuccess as Lr,isFailure as vr}from"@fjall/generator";import{deploy as Fr}from"./orchestration/index.js";import{destroy as Ur}from"./orchestration/index.js";import{partitionAccounts as hr}from"./orchestration/index.js";import{reconcileProviderAccounts as br,mergeReconciledProviderAccounts as Gr}from"./orchestration/index.js";import{parseAccountsConfiguration as wr,flattenAccountsToEnvironments as Hr,extractAllAccountNames as Kr,accountsConfigToOUTree as Vr,isStringArray as Xr,isAccountsConfig as jr}from"./orchestration/index.js";import{runOpenNextBuild as zr}from"./orchestration/index.js";import{runOrganisationSetup as Qr}from"./orchestration/index.js";import{FrameworkRegistry as Zr}from"./orchestration/index.js";import{openNextBuilder as et,dockerBuilder as rt}from"./orchestration/index.js";import{StepRegistry as ot,getDestroyStepId as at}from"./steps/index.js";export{G as APPLICATION_DEPLOY_ORDER,B as APPLICATION_DESTROY_ORDER,Y as APPLICATION_STACKS,le as ApplicationError,tr as ApplicationStackService,cr as AuthError,Er as AwsError,sr as BaseServiceError,s as CASCADE_ACCOUNT_STATUSES,i as CASCADE_PHASES,pe as CDK_NO_STACKS_MATCH,Fe as CdkArgumentBuilder,Qe as CdkContextBuilder,ar as CdkError,Ue as CdkEventMonitor,ye as CdkProcessManager,ke as CdkService,Ve as CloudFormationError,Le as CloudFormationEventMonitor,Ke as CloudFormationService,Ar as ConfigError,he as DEFAULT_DEPLOY_TIMEOUT_MS,a as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,o as DEPLOYMENT_EVENT_TYPES,pr as DeploymentError,t as DeploymentEventSchema,je as EcsError,Xe as EcsService,qe as EcsServiceResolver,lr as FileSystemError,ue as FjallStateFileSchema,Zr as FrameworkRegistry,Se as INFRASTRUCTURE_FILENAME,y as INFRASTRUCTURE_STEP_NAMES,U as INFRA_STEP_NAME,Sr as NetworkError,w as OPENNEXT_DEPLOY_ORDER,H as OPENNEXT_DESTROY_ORDER,X as OPENNEXT_PARALLEL_GROUPS,se as OPENNEXT_PATTERNS,b as ORGANISATION_TYPES,K as PARALLEL_DEPLOY_GROUPS,V as PARALLEL_DESTROY_GROUPS,j as PARALLEL_OPERATION_TYPES,Ze as PROGRESS_MESSAGES,h as ProgressReporter,L as SECURITY_SERVICE_PRINCIPALS,Ee as STACK_FAILED_STATE_PATTERN,ce as STACK_NOT_FOUND_PATTERN,k as STEP_IDS,F as STEP_NAMES,c as SimpleAwsProvider,ot as StepRegistry,Je as TemplateHashError,ze as TemplateHashService,nr as ValidationError,Vr as accountsConfigToOUTree,N as activateCostAllocationTags,T as activateTrustedAccess,R as buildAccountToOUMap,er as buildStepContextBuildConfig,g as checkIdentityCentreStatus,rr as convertCloudFormationOutputsToRecord,_ as createAccount,Oe as createEmptyState,Rr as createSequencedCallbacks,Pe as deleteStateFile,Fr as deploy,ne as deriveResourcesFromManifestStacks,S as describeOrganisation,Ur as destroy,De as detectDatabase,Re as detectPattern,ge as detectPayloadPattern,rt as dockerBuilder,We as emitProgress,m as enableIpamDelegatedAdmin,l as enablePolicyTypes,u as enableRamSharing,A as enableServiceAccess,p as ensureOrganisationExists,C as ensureOrganisationalUnitsExist,Kr as extractAllAccountNames,x as extractErrorName,Ir as failure,gr as fileExists,mr as filterDangerousEnvVars,d as findAccount,Hr as flattenAccountsToEnvironments,be as formatInfrastructureError,W as getApplicationDeployOrder,Z as getApplicationDestroyOrder,$ as getApplicationStackName,oe as getApplicationStepId,te as getApplicationStepName,at as getDestroyStepId,ee as getOrganisationStackName,J as getParallelDeployGroups,Q as getParallelDestroyGroups,Be as getSourceContext,_e as getStateFilePath,Ge as getStructuralHint,we as hasCdkDifferences,Cr as hasDockerfile,jr as isAccountsConfig,q as isApplicationOperation,re as isApplicationStack,Ye as isCdkError,vr as isFailure,D as isOULeaf,ie as isOpenNextPattern,z as isOrganisationOperation,Xr as isStringArray,Lr as isSuccess,P as listAccounts,Or as maskSensitiveOutput,Gr as mergeReconciledProviderAccounts,et as openNextBuilder,wr as parseAccountsConfiguration,$e as parseBuildPhase,He as parseDiffOutput,Pr as parseShellArgs,hr as partitionAccounts,f as placeAccountsInOUs,Te as readStateFile,br as reconcileProviderAccounts,I as registerSecurityDelegates,zr as runOpenNextBuild,Qr as runOrganisationSetup,dr as sleep,Me as startStackMonitoring,Ce as stubCallerIdentity,Dr as success,ae as toPascalCase,ur as toServiceError,O as updateBackupGlobalSettings,de as updateTemplateHash,Ae as wrapApplicationError,me as writeStateFile};
1
+ import{DeploymentEventSchema as t,DEPLOYMENT_EVENT_TYPES as o,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as a,CASCADE_PHASES as i,CASCADE_ACCOUNT_STATUSES as s}from"./types/index.js";import{SimpleAwsProvider as c}from"./aws/index.js";import{ensureOrganisationExists as p,describeOrganisation as S,enablePolicyTypes as l,enableServiceAccess as A,enableRamSharing as u,activateTrustedAccess as m,enableIpamDelegatedAdmin as T,updateBackupGlobalSettings as P,listAccounts as O,findAccount as d,createAccount as R,ensureOrganisationalUnitsExist as _,placeAccountsInOUs as f,buildAccountToOUMap as C,activateCostAllocationTags as g,checkIdentityCentreStatus as N,extractErrorName as x,isOULeaf as D,registerSecurityDelegates as I,SECURITY_SERVICE_PRINCIPALS as L}from"./aws/index.js";import{STEP_IDS as k,STEP_NAMES as y,INFRASTRUCTURE_STEP_NAMES as F,INFRA_STEP_NAME as U}from"./types/index.js";import{ProgressReporter as b,APPLICATION_STACKS as h,ORGANISATION_TYPES as Y,APPLICATION_DEPLOY_ORDER as G,APPLICATION_DESTROY_ORDER as B,OPENNEXT_DEPLOY_ORDER as w,OPENNEXT_DESTROY_ORDER as H,PARALLEL_DEPLOY_GROUPS as K,PARALLEL_DESTROY_GROUPS as V,OPENNEXT_PARALLEL_GROUPS as X,PARALLEL_OPERATION_TYPES as j,isApplicationOperation as q,isOrganisationOperation as z,getParallelDeployGroups as J,getParallelDestroyGroups as Q,getApplicationDeployOrder as W,getApplicationDestroyOrder as Z,getApplicationStackName as $,getOrganisationStackName as ee,isApplicationStack as re,getApplicationStepName as te,getApplicationStepId as oe,toPascalCase as ae,isOpenNextPattern as ie,OPENNEXT_PATTERNS as se,deriveResourcesFromManifestStacks as ne,STACK_NOT_FOUND_PATTERN as ce,STACK_FAILED_STATE_PATTERN as Ee,CDK_NO_STACKS_MATCH as pe,INFRASTRUCTURE_FILENAME as Se,ApplicationError as le,wrapApplicationError as Ae,FjallStateFileSchema as ue,readStateFile as me,writeStateFile as Te,createEmptyState as Pe,deleteStateFile as Oe,updateTemplateHash as de,getStateFilePath as Re,stubCallerIdentity as _e}from"./types/index.js";import{detectPattern as Ce}from"./types/index.js";import{detectPayloadPattern as Ne}from"./types/index.js";import{detectDatabase as De}from"./types/index.js";import{CloudFormationEventMonitor as Le}from"./aws/index.js";import{CdkService as ke,CdkArgumentBuilder as ye,CdkProcessManager as Fe,CdkEventMonitor as Ue,startStackMonitoring as Me,DEFAULT_DEPLOY_TIMEOUT_MS as be,isCdkError as he,formatInfrastructureError as Ye,getStructuralHint as Ge,getSourceContext as Be,hasCdkDifferences as we,parseDiffOutput as He,CloudFormationService as Ke,CloudFormationError as Ve,EcsService as Xe,EcsError as je,EcsServiceResolver as qe,TemplateHashService as ze,TemplateHashError as Je,CdkContextBuilder as Qe,emitProgress as We,PROGRESS_MESSAGES as Ze,parseBuildPhase as $e,buildStepContextBuildConfig as er,convertCloudFormationOutputsToRecord as rr,ApplicationStackService as tr}from"./services/index.js";import{CdkError as ar}from"./types/errors/index.js";import{BaseServiceError as sr,ValidationError as nr,AuthError as cr,AwsError as Er,DeploymentError as pr,NetworkError as Sr,FileSystemError as lr,ConfigError as Ar,toServiceError as ur}from"./types/errors/index.js";import{filterDangerousEnvVars as Tr,maskSensitiveOutput as Pr,parseShellArgs as Or,sleep as dr}from"@fjall/util";import{hasDockerfile as _r}from"./util/dockerfileDetection.js";import{createSequencedCallbacks as Cr}from"./util/sequencedCallbacks.js";import{fileExists as Nr}from"@fjall/util/fsHelpers";import{success as Dr,failure as Ir,isSuccess as Lr,isFailure as vr}from"@fjall/generator";import{deploy as yr}from"./orchestration/index.js";import{destroy as Ur}from"./orchestration/index.js";import{partitionAccounts as br}from"./orchestration/index.js";import{buildRegionList as Yr,buildAccountRegionPairs as Gr}from"./orchestration/index.js";import{projectScalarSummary as wr,projectAccountRows as Hr}from"./orchestration/index.js";import{reconcileProviderAccounts as Vr,mergeReconciledProviderAccounts as Xr}from"./orchestration/index.js";import{parseAccountsConfiguration as qr,flattenAccountsToEnvironments as zr,extractAllAccountNames as Jr,accountsConfigToOUTree as Qr,isStringArray as Wr,isAccountsConfig as Zr}from"./orchestration/index.js";import{runOpenNextBuild as et}from"./orchestration/index.js";import{runOrganisationSetup as tt}from"./orchestration/index.js";import{FrameworkRegistry as at}from"./orchestration/index.js";import{openNextBuilder as st,dockerBuilder as nt}from"./orchestration/index.js";import{StepRegistry as Et,getDestroyStepId as pt}from"./steps/index.js";export{G as APPLICATION_DEPLOY_ORDER,B as APPLICATION_DESTROY_ORDER,h as APPLICATION_STACKS,le as ApplicationError,tr as ApplicationStackService,cr as AuthError,Er as AwsError,sr as BaseServiceError,s as CASCADE_ACCOUNT_STATUSES,i as CASCADE_PHASES,pe as CDK_NO_STACKS_MATCH,ye as CdkArgumentBuilder,Qe as CdkContextBuilder,ar as CdkError,Ue as CdkEventMonitor,Fe as CdkProcessManager,ke as CdkService,Ve as CloudFormationError,Le as CloudFormationEventMonitor,Ke as CloudFormationService,Ar as ConfigError,be as DEFAULT_DEPLOY_TIMEOUT_MS,a as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,o as DEPLOYMENT_EVENT_TYPES,pr as DeploymentError,t as DeploymentEventSchema,je as EcsError,Xe as EcsService,qe as EcsServiceResolver,lr as FileSystemError,ue as FjallStateFileSchema,at as FrameworkRegistry,Se as INFRASTRUCTURE_FILENAME,F as INFRASTRUCTURE_STEP_NAMES,U as INFRA_STEP_NAME,Sr as NetworkError,w as OPENNEXT_DEPLOY_ORDER,H as OPENNEXT_DESTROY_ORDER,X as OPENNEXT_PARALLEL_GROUPS,se as OPENNEXT_PATTERNS,Y as ORGANISATION_TYPES,K as PARALLEL_DEPLOY_GROUPS,V as PARALLEL_DESTROY_GROUPS,j as PARALLEL_OPERATION_TYPES,Ze as PROGRESS_MESSAGES,b as ProgressReporter,L as SECURITY_SERVICE_PRINCIPALS,Ee as STACK_FAILED_STATE_PATTERN,ce as STACK_NOT_FOUND_PATTERN,k as STEP_IDS,y as STEP_NAMES,c as SimpleAwsProvider,Et as StepRegistry,Je as TemplateHashError,ze as TemplateHashService,nr as ValidationError,Qr as accountsConfigToOUTree,g as activateCostAllocationTags,m as activateTrustedAccess,Gr as buildAccountRegionPairs,C as buildAccountToOUMap,Yr as buildRegionList,er as buildStepContextBuildConfig,N as checkIdentityCentreStatus,rr as convertCloudFormationOutputsToRecord,R as createAccount,Pe as createEmptyState,Cr as createSequencedCallbacks,Oe as deleteStateFile,yr as deploy,ne as deriveResourcesFromManifestStacks,S as describeOrganisation,Ur as destroy,De as detectDatabase,Ce as detectPattern,Ne as detectPayloadPattern,nt as dockerBuilder,We as emitProgress,T as enableIpamDelegatedAdmin,l as enablePolicyTypes,u as enableRamSharing,A as enableServiceAccess,p as ensureOrganisationExists,_ as ensureOrganisationalUnitsExist,Jr as extractAllAccountNames,x as extractErrorName,Ir as failure,Nr as fileExists,Tr as filterDangerousEnvVars,d as findAccount,zr as flattenAccountsToEnvironments,Ye as formatInfrastructureError,W as getApplicationDeployOrder,Z as getApplicationDestroyOrder,$ as getApplicationStackName,oe as getApplicationStepId,te as getApplicationStepName,pt as getDestroyStepId,ee as getOrganisationStackName,J as getParallelDeployGroups,Q as getParallelDestroyGroups,Be as getSourceContext,Re as getStateFilePath,Ge as getStructuralHint,we as hasCdkDifferences,_r as hasDockerfile,Zr as isAccountsConfig,q as isApplicationOperation,re as isApplicationStack,he as isCdkError,vr as isFailure,D as isOULeaf,ie as isOpenNextPattern,z as isOrganisationOperation,Wr as isStringArray,Lr as isSuccess,O as listAccounts,Pr as maskSensitiveOutput,Xr as mergeReconciledProviderAccounts,st as openNextBuilder,qr as parseAccountsConfiguration,$e as parseBuildPhase,He as parseDiffOutput,Or as parseShellArgs,br as partitionAccounts,f as placeAccountsInOUs,Hr as projectAccountRows,wr as projectScalarSummary,me as readStateFile,Vr as reconcileProviderAccounts,I as registerSecurityDelegates,et as runOpenNextBuild,tt as runOrganisationSetup,dr as sleep,Me as startStackMonitoring,_e as stubCallerIdentity,Dr as success,ae as toPascalCase,ur as toServiceError,P as updateBackupGlobalSettings,de as updateTemplateHash,Ae as wrapApplicationError,Te as writeStateFile};
@@ -1 +1 @@
1
- import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as s,getErrorMessage as E}from"@fjall/util";import{stubCallerIdentity as P}from"../types/deployment/index.js";import{CloudFormationClient as $,DescribeStacksCommand as R}from"@aws-sdk/client-cloudformation";import{NodeHttpHandler as O}from"@smithy/node-http-handler";import{ORGANISATION_TYPES as h,getOrganisationStackName as _}from"../types/operations.js";import{CdkContextBuilder as k}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as F,assumeCascadeRole as x,forwardOutput as I}from"./contextHelpers.js";import{cleanupFailedStack as L}from"./stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as M,STACK_FAILED_STATE_PATTERN as K}from"../types/constants.js";async function V(c,i,m,t,o,e,r){const n=Date.now(),a=`${t.name} (${e})`;r.onCascadeAccountStart?.(a,t.id,e,o);const f=await x(i.awsProvider,t.id,e,`fjall-cascade-destroy-${t.name}`);if(!f.success)return r.onCascadeAccountComplete?.(a,!1,s(f.error.message),e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:`AssumeRole failed: ${s(f.error.message)}`,skipped:!0};const{provider:v,credentials:l}=f.data,A=k.buildDeploymentContext({deployType:o,target:m.target,path:m.path,region:e,accountName:t.name,callerIdentity:P(t.id),...F({orgConfig:c.orgConfig,identity:c.identity})},{verbose:c.options?.verbose},c.orgConfig);r.onCascadeAccountPhaseChange?.(a,"synth",e);const w=await i.cdkService.runCdkSynth(A,I(r));if(!w.success){const d=s(`Synth failed: ${w.error}`);return r.onCascadeAccountComplete?.(a,!1,d,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:d}}r.onCascadeAccountPhaseChange?.(a,"destroy",e);const u=_(o==="platform"?h.PLATFORM:h.ACCOUNT),y=await i.cdkService.runCdkDestroy(A,u,I(r),d=>r.onCascadeAccountResourceProgress?.(a,d,e),v,!0,l);if(!y.success){const d=y.error;if(d.includes(K)){S.warn("cascadeDestroy",`CDK destroy failed on ${u} in failed state, retrying via CloudFormation API`,{region:e,account:t.name});try{await L(u,e,l,void 0,r)}catch(C){const T=`cleanupFailedStack threw for ${u}: ${s(E(C))}`;S.warn("cascadeDestroy",T),r.onLog?.(T,"warn")}const p=await H(u,e,l);if(p.deleted)return r.onCascadeAccountComplete?.(a,!0,void 0,e),{accountName:t.name,accountId:t.id,region:e,success:!0,duration:Date.now()-n};if(p.error){const C=s(p.error);return r.onCascadeAccountComplete?.(a,!1,C,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:C}}const N=s(`Stack ${u} cleanup attempted but stack still exists in ${e}`);return r.onCascadeAccountComplete?.(a,!1,N,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:N}}const D=s(d);return r.onCascadeAccountComplete?.(a,!1,D,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:D}}return r.onCascadeAccountComplete?.(a,!0,void 0,e),{accountName:t.name,accountId:t.id,region:e,success:!0,duration:Date.now()-n}}async function H(c,i,m){try{const e=(await new $({region:i,credentials:m,requestHandler:new O({requestTimeout:15e3})}).send(new R({StackName:c}))).Stacks?.[0]?.StackStatus;return!e||e==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${e} after cleanup attempt`}}catch(t){if(t instanceof Error&&t.message?.includes(M))return{deleted:!0};const o=s(E(t));return S.debug("cascadeDestroy","Stack verification failed",{error:o}),{deleted:!1,error:`Stack verification failed: ${o}`}}}export{V as destroyCascadeAccount};
1
+ import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as s,getErrorMessage as E}from"@fjall/util";import{stubCallerIdentity as P}from"../types/deployment/index.js";import{CloudFormationClient as $,DescribeStacksCommand as R}from"@aws-sdk/client-cloudformation";import{NodeHttpHandler as O}from"@smithy/node-http-handler";import{ORGANISATION_TYPES as h,getOrganisationStackName as _}from"../types/operations.js";import{CdkContextBuilder as k}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as F,assumeCascadeRole as x,forwardOutput as I}from"./contextHelpers.js";import{cleanupFailedStack as L}from"./stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as M,STACK_FAILED_STATE_PATTERN as K}from"../types/constants.js";async function V(c,i,m,t,o,e,r){const n=Date.now(),a=`${t.name} (${e})`;r.onCascadeAccountStart?.(a,t.id,e,o);const f=await x(i.awsProvider,t.id,e,`fjall-cascade-destroy-${t.name}`);if(!f.success)return r.onCascadeAccountComplete?.(a,!1,s(f.error.message),e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:`AssumeRole failed: ${s(f.error.message)}`};const{provider:v,credentials:l}=f.data,A=k.buildDeploymentContext({deployType:o,target:m.target,path:m.path,region:e,accountName:t.name,callerIdentity:P(t.id),...F({orgConfig:c.orgConfig,identity:c.identity})},{verbose:c.options?.verbose},c.orgConfig);r.onCascadeAccountPhaseChange?.(a,"synth",e);const w=await i.cdkService.runCdkSynth(A,I(r));if(!w.success){const d=s(`Synth failed: ${w.error}`);return r.onCascadeAccountComplete?.(a,!1,d,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:d}}r.onCascadeAccountPhaseChange?.(a,"destroy",e);const u=_(o==="platform"?h.PLATFORM:h.ACCOUNT),y=await i.cdkService.runCdkDestroy(A,u,I(r),d=>r.onCascadeAccountResourceProgress?.(a,d,e),v,!0,l);if(!y.success){const d=y.error;if(d.includes(K)){S.warn("cascadeDestroy",`CDK destroy failed on ${u} in failed state, retrying via CloudFormation API`,{region:e,account:t.name});try{await L(u,e,l,void 0,r)}catch(C){const T=`cleanupFailedStack threw for ${u}: ${s(E(C))}`;S.warn("cascadeDestroy",T),r.onLog?.(T,"warn")}const p=await H(u,e,l);if(p.deleted)return r.onCascadeAccountComplete?.(a,!0,void 0,e),{accountName:t.name,accountId:t.id,region:e,success:!0,duration:Date.now()-n};if(p.error){const C=s(p.error);return r.onCascadeAccountComplete?.(a,!1,C,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:C}}const N=s(`Stack ${u} cleanup attempted but stack still exists in ${e}`);return r.onCascadeAccountComplete?.(a,!1,N,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:N}}const D=s(d);return r.onCascadeAccountComplete?.(a,!1,D,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:D}}return r.onCascadeAccountComplete?.(a,!0,void 0,e),{accountName:t.name,accountId:t.id,region:e,success:!0,duration:Date.now()-n}}async function H(c,i,m){try{const e=(await new $({region:i,credentials:m,requestHandler:new O({requestTimeout:15e3})}).send(new R({StackName:c}))).Stacks?.[0]?.StackStatus;return!e||e==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${e} after cleanup attempt`}}catch(t){if(t instanceof Error&&t.message?.includes(M))return{deleted:!0};const o=s(E(t));return S.debug("cascadeDestroy","Stack verification failed",{error:o}),{deleted:!1,error:`Stack verification failed: ${o}`}}}export{V as destroyCascadeAccount};
@@ -1,10 +1,19 @@
1
1
  import { type Result } from "@fjall/generator";
2
2
  import type { DeployParams } from "../types/params.js";
3
+ import type { OrgConfig } from "../types/orgConfig.js";
3
4
  import type { DeployCallbacks } from "../types/callbacks.js";
4
5
  import type { OrganisationOperation } from "../types/operations.js";
5
6
  import type { DeployServices } from "./serviceFactory.js";
6
7
  import type { DomainDeployProvider } from "./domainInterface.js";
7
8
  import type { ProviderAccount } from "@fjall/util/config";
9
+ /**
10
+ * Max member accounts deployed/destroyed in parallel during a cascade. Each
11
+ * task spawns a `cdk` process, assumes a role, synthesises and deploys — so the
12
+ * ceiling is the worker host's process/memory budget, NOT an AWS API limit
13
+ * (the accounts are distinct, so CloudFormation/STS quotas don't contend).
14
+ * Shared by deploy and destroy so they fan out identically.
15
+ */
16
+ export declare const CASCADE_MAX_CONCURRENCY = 4;
8
17
  /**
9
18
  * Partition provider accounts into platform and member accounts.
10
19
  * Used by both deploy and destroy orchestration.
@@ -13,6 +22,22 @@ export declare function partitionAccounts(providerAccounts: ProviderAccount[]):
13
22
  platformAccount: ProviderAccount | undefined;
14
23
  memberAccounts: ProviderAccount[];
15
24
  };
25
+ /** A single account paired with the region it deploys/destroys in. */
26
+ export interface AccountRegionPair {
27
+ account: ProviderAccount;
28
+ region: string;
29
+ }
30
+ /**
31
+ * Build the full list of cascade regions (primary + secondary + DR, deduped).
32
+ *
33
+ * Single source of truth for "all org regions" — deploy and destroy MUST agree
34
+ * on this set, else destroy sweeps regions deploy never created (or vice versa).
35
+ */
36
+ export declare function buildRegionList(orgConfig: OrgConfig | undefined): string[];
37
+ /**
38
+ * Build all account x region pairs for parallel cascade fan-out.
39
+ */
40
+ export declare function buildAccountRegionPairs(accounts: ProviderAccount[], regions: string[]): AccountRegionPair[];
16
41
  export { buildCascadeRoleArn } from "./contextHelpers.js";
17
42
  /**
18
43
  * Deploy a single cascade account (platform or member).
@@ -20,12 +45,30 @@ export { buildCascadeRoleArn } from "./contextHelpers.js";
20
45
  */
21
46
  export interface CascadeAccountResult {
22
47
  outputs?: Record<string, string>;
48
+ /**
49
+ * True when the synthesised stack was byte-identical to the last successful
50
+ * deploy and the stack still exists, so bootstrap + deploy were skipped.
51
+ */
52
+ skipped?: boolean;
23
53
  }
24
- export declare function deployCascadeAccount(params: DeployParams, services: DeployServices, operation: OrganisationOperation, account: {
25
- id: string;
26
- name: string;
27
- environment: string;
28
- }, deployType: "platform" | "account", callbacks: DeployCallbacks, ipamPoolId?: string): Promise<Result<CascadeAccountResult>>;
54
+ export interface DeployCascadeAccountOptions {
55
+ ipamPoolId?: string;
56
+ /**
57
+ * Effective org config carrying the reconciled provider accounts. The cascade
58
+ * context must serialise THIS (not the possibly-stale `params.orgConfig`) so the
59
+ * synthesised app's `getConfig()` can resolve every account by name/id.
60
+ */
61
+ orgConfig?: OrgConfig;
62
+ /** Region to deploy in; falls back to account.region then the provider region. */
63
+ region?: string;
64
+ /**
65
+ * True for non-home (secondary/DR) cascade regions — the synthesised account
66
+ * stack then skips the fixed-name org-global IAM resources that would collide
67
+ * across regions. Unset/false for the home (primary) region.
68
+ */
69
+ skipAccountGlobals?: boolean;
70
+ }
71
+ export declare function deployCascadeAccount(params: DeployParams, services: DeployServices, operation: OrganisationOperation, account: ProviderAccount, deployType: "platform" | "account", callbacks: DeployCallbacks, options?: DeployCascadeAccountOptions): Promise<Result<CascadeAccountResult>>;
29
72
  /**
30
73
  * Read Platform stack outputs to extract IPAM pool IDs for member accounts.
31
74
  * Output keys follow the pattern `IpamPoolId{12-digit-accountId}{regionSuffix}`.
@@ -1 +1 @@
1
- import{success as v,failure as P}from"@fjall/generator";import{logger as R}from"@fjall/util/logger";import{maskSensitiveOutput as l}from"@fjall/util";import{ORGANISATION_TYPES as O,getOrganisationStackName as A}from"../types/operations.js";import{CdkContextBuilder as x}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as N}from"../types/deployment/index.js";import{CloudFormationService as S}from"../services/infrastructure/CloudFormationService.js";import{buildParamsContext as T,collectStackOutputs as F,assumeCascadeRole as h,forwardOutput as D}from"./contextHelpers.js";import{STRUCTURAL_ENVIRONMENTS as $}from"@fjall/util";function _(r){const s=r.find(e=>e.environment===$.PLATFORM),i=r.filter(e=>e.environment!==$.ROOT&&e.environment!==$.PLATFORM);return{platformAccount:s,memberAccounts:i}}import{buildCascadeRoleArn as J}from"./contextHelpers.js";async function b(r,s,i,e,c,n,d){const o=`${e.name}-${e.id}`,t=s.awsProvider.getRegion();n.onCascadeAccountStart?.(o,e.id,t,c);const a=await h(s.awsProvider,e.id,t,`fjall-cascade-${e.name}`);if(!a.success)return n.onCascadeAccountComplete?.(o,!1,l(a.error.message),t),P(new Error(`Failed to assume role for ${e.name}: ${l(a.error.message)}`));const{provider:u,credentials:m}=a.data,p=x.buildDeploymentContext({deployType:c,target:i.target,path:i.path,region:t,accountName:e.name,callerIdentity:N(e.id),ipamPoolId:d,...T({orgConfig:r.orgConfig,identity:r.identity,skipOidc:r.options?.skipOidc})},{verbose:r.options?.verbose},r.orgConfig);n.onCascadeAccountPhaseChange?.(o,"bootstrap",t);const f=await s.cdkService.runCdkBootstrap(p,D(n),m);if(!f.success)return n.onCascadeAccountComplete?.(o,!1,l(`Bootstrap failed: ${f.error}`),t),P(new Error(`Bootstrap failed for ${e.name}: ${l(f.error)}`));n.onCascadeAccountPhaseChange?.(o,"deploy",t);const g=A(c==="platform"?O.PLATFORM:O.ACCOUNT),C=await s.cdkService.runCdkDeploy(p,g,D(n),I=>n.onCascadeAccountResourceProgress?.(o,I,t),u,m);if(!C.success)return n.onCascadeAccountComplete?.(o,!1,l(C.error),t),P(new Error(l(C.error)));const w=await new S(u).getStackOutputs(g);w.success||R.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:g,account:e.name});const y=F(w);return n.onCascadeAccountComplete?.(o,!0,void 0,t,y),v({outputs:y})}async function G(r,s,i){const e=new Map,c=r.awsProvider.getRegion(),n=await h(r.awsProvider,s.id,c,`fjall-ipam-read-${s.name}`);if(!n.success)return R.debug("organisationDeploy",`Cannot read Platform outputs: ${n.error.message}`),e;const d=new S(n.data.provider),o=A(O.PLATFORM),t=await d.getStackOutputs(o);if(!t.success)return R.debug("organisationDeploy",`Failed to read Platform stack outputs: ${t.error.message}`),e;const a=/^IpamPoolId(\d{12})(\w+)$/;for(const u of t.data){const m=u.OutputKey?.match(a);if(m&&u.OutputValue){const p=`${m[1]}-${m[2]}`;e.set(p,u.OutputValue)}}return e.size>0&&i.onLog?.(`Read ${e.size} IPAM pool ID(s) from Platform stack`,"info"),e}async function H(r,s){const i=r.getDomains();if(i.length===0)return{domainsDeployed:0,errors:[]};s.onCascadePhaseStart?.("domains");const e=i.filter(o=>o.type==="apex"),c=i.filter(o=>o.type==="delegated");let n=0;const d=[];for(const o of e){const t=await r.deployDomain(o.name,s);t.success?n++:d.push(`${o.name}: ${t.error.message}`)}if(c.length>0){const o=await Promise.allSettled(c.map(t=>r.deployDomain(t.name,s)));for(let t=0;t<o.length;t++){const a=o[t],u=c[t];if(!(!a||!u))if(a.status==="fulfilled")a.value.success?n++:d.push(`${u.name}: ${a.value.error.message}`);else{const m=a.reason instanceof Error?a.reason.message:String(a.reason);d.push(`${u.name}: ${m}`)}}}return s.onCascadePhaseComplete?.("domains"),{domainsDeployed:n,errors:d}}export{J as buildCascadeRoleArn,b as deployCascadeAccount,H as deployDomains,_ as partitionAccounts,G as readPlatformIpamPoolIds};
1
+ import{join as T}from"path";import{success as L,failure as h}from"@fjall/generator";import{logger as g}from"@fjall/util/logger";import{maskSensitiveOutput as f}from"@fjall/util";import{ORGANISATION_TYPES as C,getOrganisationStackName as M}from"../types/operations.js";import{CdkContextBuilder as H}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as b}from"../types/deployment/index.js";import{CloudFormationService as U}from"../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as V}from"../types/FjallState.js";import{buildParamsContext as z,collectStackOutputs as _,assumeCascadeRole as j,forwardOutput as D}from"./contextHelpers.js";import{STRUCTURAL_ENVIRONMENTS as k}from"@fjall/util";import{DEFAULT_REGION as K}from"../aws/utils/regions.js";const re=4;function ae(s){const r=s.find(e=>e.environment===k.PLATFORM),i=s.filter(e=>e.environment!==k.ROOT&&e.environment!==k.PLATFORM);return{platformAccount:r,memberAccounts:i}}function ie(s){const r=s?.primaryRegion??K,i=s?.secondaryRegions??[],e=[r,...i],a=s?.disasterRecoveryRegion;return a&&!e.includes(a)&&e.push(a),e}function ce(s,r){const i=[];for(const e of r)for(const a of s)i.push({account:a,region:e});return i}import{buildCascadeRoleArn as le}from"./contextHelpers.js";async function ue(s,r,i,e,a,n,u){const t=u?.region??e.region??r.awsProvider.getRegion(),o=`${e.name} (${t})`,c=u?.orgConfig??s.orgConfig,p=u?.ipamPoolId;n.onCascadeAccountStart?.(o,e.id,t,a);const d=await j(r.awsProvider,e.id,t,`fjall-cascade-${e.name}`);if(!d.success)return n.onCascadeAccountComplete?.(o,!1,f(d.error.message),t),h(new Error(`Failed to assume role for ${e.name}: ${f(d.error.message)}`));const{provider:R,credentials:A}=d.data,P=T(s.workingDirectory,"fjall",a==="platform"?C.PLATFORM:C.ACCOUNT),B=t.replace(/-/g,""),x=T(P,`cdk.out.${e.id}.${B}`),y=H.buildDeploymentContext({deployType:a,target:i.target,path:P,assemblyDir:x,environment:e.environment,region:t,accountName:e.name,callerIdentity:b(e.id),ipamPoolId:p,...z({orgConfig:c,identity:s.identity,skipOidc:s.options?.skipOidc,skipAccountGlobals:u?.skipAccountGlobals})},{verbose:s.options?.verbose},c);n.onCascadeAccountPhaseChange?.(o,"synth",t);const O=await r.cdkService.runCdkSynth(y,D(n),A);if(!O.success)return n.onCascadeAccountComplete?.(o,!1,f(`Synth failed: ${O.error}`),t),h(new Error(`Synth failed for ${e.name}: ${f(O.error)}`));const m=M(a==="platform"?C.PLATFORM:C.ACCOUNT),v=V(P,e.id,t),S=new U(R),{changed:G,currentHash:I}=await r.hashService.compareCascadeStack(x,m,v);if(!G&&s.options?.force!==!0&&await S.stackExists(m)){const l=await S.getStackOutputs(m);l.success||g.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:m,account:e.name});const E=_(l);return n.onLog?.(`${e.name}: no infrastructure changes \u2014 skipping deploy`,"info"),n.onCascadeAccountComplete?.(o,!0,void 0,t,E,!0),L({outputs:E,skipped:!0})}n.onCascadeAccountPhaseChange?.(o,"bootstrap",t);const w=await r.cdkService.runCdkBootstrap(y,D(n),A);if(!w.success)return n.onCascadeAccountComplete?.(o,!1,f(`Bootstrap failed: ${w.error}`),t),h(new Error(`Bootstrap failed for ${e.name}: ${f(w.error)}`));n.onCascadeAccountPhaseChange?.(o,"deploy",t);const $=await r.cdkService.runCdkDeploy(y,m,D(n),l=>n.onCascadeAccountResourceProgress?.(o,l,t),R,A);if(!$.success)return n.onCascadeAccountComplete?.(o,!1,f($.error),t),h(new Error(f($.error)));const F=await S.getStackOutputs(m);F.success||g.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:m,account:e.name});const N=_(F);return I!==void 0&&((await r.hashService.persistCascadeStack(v,m,I)).success||g.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:m,account:e.name})),n.onCascadeAccountComplete?.(o,!0,void 0,t,N,!1),L({outputs:N,skipped:!1})}async function de(s,r,i){const e=new Map,a=s.awsProvider.getRegion(),n=await j(s.awsProvider,r.id,a,`fjall-ipam-read-${r.name}`);if(!n.success)return g.debug("organisationDeploy",`Cannot read Platform outputs: ${n.error.message}`),e;const u=new U(n.data.provider),t=M(C.PLATFORM),o=await u.getStackOutputs(t);if(!o.success)return g.debug("organisationDeploy",`Failed to read Platform stack outputs: ${o.error.message}`),e;const c=/^IpamPoolId(\d{12})(\w+)$/;for(const p of o.data){const d=p.OutputKey?.match(c);if(d&&p.OutputValue){const R=`${d[1]}-${d[2]}`;e.set(R,p.OutputValue)}}return e.size>0&&i.onLog?.(`Read ${e.size} IPAM pool ID(s) from Platform stack`,"info"),e}async function pe(s,r){const i=s.getDomains();if(i.length===0)return{domainsDeployed:0,errors:[]};r.onCascadePhaseStart?.("domains");const e=i.filter(t=>t.type==="apex"),a=i.filter(t=>t.type==="delegated");let n=0;const u=[];for(const t of e){const o=await s.deployDomain(t.name,r);o.success?n++:u.push(`${t.name}: ${o.error.message}`)}if(a.length>0){const t=await Promise.allSettled(a.map(o=>s.deployDomain(o.name,r)));for(let o=0;o<t.length;o++){const c=t[o],p=a[o];if(!(!c||!p))if(c.status==="fulfilled")c.value.success?n++:u.push(`${p.name}: ${c.value.error.message}`);else{const d=c.reason instanceof Error?c.reason.message:String(c.reason);u.push(`${p.name}: ${d}`)}}}return r.onCascadePhaseComplete?.("domains"),{domainsDeployed:n,errors:u}}export{re as CASCADE_MAX_CONCURRENCY,ce as buildAccountRegionPairs,le as buildCascadeRoleArn,ie as buildRegionList,ue as deployCascadeAccount,pe as deployDomains,ae as partitionAccounts,de as readPlatformIpamPoolIds};
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Cascade outcome ledger + pure projections.
3
+ *
4
+ * Single source of truth for cascade summary counting. The engine accumulates
5
+ * ONE `CascadeLedger` of atomic per-(account, region) outcomes during a cascade,
6
+ * then projects it two ways:
7
+ *
8
+ * - `projectScalarSummary` → the wire-level `CascadeDeploymentResult` the webapp
9
+ * reads (scalar counts + platform flags + errors), emitted via `onCascadeComplete`.
10
+ * - `projectAccountRows` → the per-account rows the CLI summary renders, carried
11
+ * via `onCascadeLedger`.
12
+ *
13
+ * Both deploy and destroy build the same ledger shape — destroy feeds `succeeded`
14
+ * for a destroyed stack, so the projector is verb-agnostic (the scalar field names
15
+ * stay `*Deployed`, carrying destroyed counts for a destroy). Adding a new summary
16
+ * field means extending the ledger + one projector; every consumer inherits it.
17
+ */
18
+ import type { CascadeDeploymentResult } from "../types/events.js";
19
+ export type CascadeOutcomeResult = "succeeded" | "skipped" | "failed";
20
+ /** One account×region pair's outcome. */
21
+ export interface CascadeMemberOutcome {
22
+ accountId: string;
23
+ accountName: string;
24
+ region: string;
25
+ result: CascadeOutcomeResult;
26
+ durationMs: number;
27
+ /** Already masked by the producer. */
28
+ error?: string;
29
+ }
30
+ /** The platform account's outcome (single region). `skipped` only ever occurs on deploy. */
31
+ export interface CascadePlatformOutcome {
32
+ accountId: string;
33
+ result: CascadeOutcomeResult;
34
+ durationMs: number;
35
+ /** Already masked by the producer. */
36
+ error?: string;
37
+ }
38
+ /**
39
+ * The single accumulation a cascade produces. `errors` is the FULL masked list
40
+ * (platform + domains + members) the producer also uses for its failure gate —
41
+ * it is echoed verbatim, never re-derived from `members`.
42
+ */
43
+ export interface CascadeLedger {
44
+ members: CascadeMemberOutcome[];
45
+ platform?: CascadePlatformOutcome;
46
+ domainsDeployed: boolean;
47
+ errors: Array<{
48
+ accountId: string;
49
+ error: string;
50
+ }>;
51
+ totalDurationMs: number;
52
+ }
53
+ /** A single per-account row for the CLI summary. */
54
+ export interface CascadeAccountRow {
55
+ accountName: string;
56
+ accountId: string;
57
+ region: string;
58
+ success: boolean;
59
+ duration: number;
60
+ error?: string;
61
+ skipped?: boolean;
62
+ }
63
+ /** The platform row for the CLI summary. */
64
+ export interface CascadePlatformRow {
65
+ success: boolean;
66
+ duration: number;
67
+ error?: string;
68
+ skipped?: boolean;
69
+ }
70
+ export interface CascadeAccountProjection {
71
+ accountResults: CascadeAccountRow[];
72
+ platformResult?: CascadePlatformRow;
73
+ }
74
+ /**
75
+ * Project the ledger to the scalar `CascadeDeploymentResult` (the webapp wire shape).
76
+ *
77
+ * Distinct-account counting: an account with ANY succeeded region counts as deployed;
78
+ * with ANY failed region counts as failed; a succeed-in-A/fail-in-B account lands in
79
+ * BOTH (preserving the engine's independent-Set behaviour). An account counts as
80
+ * skipped only when every one of its regions skipped (so it is in neither set).
81
+ */
82
+ export declare function projectScalarSummary(ledger: CascadeLedger): CascadeDeploymentResult;
83
+ /**
84
+ * Project the ledger to the per-account rows + platform row the CLI summary renders.
85
+ * Errors are already masked in the ledger, so they pass through unchanged.
86
+ */
87
+ export declare function projectAccountRows(ledger: CascadeLedger): CascadeAccountProjection;
@@ -0,0 +1 @@
1
+ function p(e){const r=new Set,t=new Set,o=new Set;for(const n of e.members)n.result==="succeeded"?r.add(n.accountId):n.result==="failed"?t.add(n.accountId):o.add(n.accountId);let d=0;for(const n of o)!r.has(n)&&!t.has(n)&&d++;const{platform:s}=e,u=s!==void 0&&(s.result==="succeeded"||s.result==="skipped"),a=s!==void 0&&s.result==="skipped",i=s!==void 0&&s.result==="failed";return{platformDeployed:u,platformSkipped:a,platformFailed:i,domainsDeployed:e.domainsDeployed,accountsDeployed:r.size,accountsSkipped:d,accountsFailed:t.size,errors:e.errors}}function c(e){return e==="failed"?{success:!1}:{success:!0,skipped:e==="skipped"}}function f(e){const r=e.members.map(o=>({accountName:o.accountName,accountId:o.accountId,region:o.region,duration:o.durationMs,...c(o.result),...o.error!==void 0?{error:o.error}:{}}));if(e.platform===void 0)return{accountResults:r};const{platform:t}=e;return{accountResults:r,platformResult:{duration:t.durationMs,...c(t.result),...t.error!==void 0?{error:t.error}:{}}}}export{f as projectAccountRows,p as projectScalarSummary};
@@ -15,10 +15,12 @@ export declare function buildParamsContext(params: {
15
15
  orgConfig?: OrgConfig;
16
16
  identity?: DeployIdentity;
17
17
  skipOidc?: boolean;
18
+ skipAccountGlobals?: boolean;
18
19
  }): {
19
20
  orgConfig?: string;
20
21
  fjallOrgId?: string;
21
22
  fjallOidcConfigured?: boolean;
23
+ fjallAccountGlobalsConfigured?: boolean;
22
24
  };
23
25
  /** Forward onOutput callback — reduces lambda repetition across orchestration files. */
24
26
  export declare function forwardOutput(callbacks: DeployCallbacks): (chunk: string) => void;
@@ -1 +1 @@
1
- import{success as i,failure as u}from"@fjall/generator";import{getErrorMessage as a,maskSensitiveOutput as c}from"@fjall/util";import{SimpleAwsProvider as p}from"../aws/SimpleAwsProvider.js";function K(e){return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{}}}function l(e){return t=>e.onOutput?.(t)}function w(e){return t=>e.onResourceProgress?.(t)}function E(e){if(!e.success||e.data.length===0)return;const t={};for(const r of e.data)r.OutputKey&&r.OutputValue!==void 0&&(t[r.OutputKey]=r.OutputValue);return Object.keys(t).length>0?t:void 0}const y="fjall-deployment-role";function O(e){return`arn:aws:iam::${e}:role/${y}`}async function A(e,t,r,s){if(!e.assumeRole)return u(new Error("AwsProvider does not support assumeRole"));const n=O(t);let o;try{o=await e.assumeRole(n,s)}catch(f){return u(new Error(a(f)))}const d=new p({accessKeyId:o.accessKeyId,secretAccessKey:o.secretAccessKey,sessionToken:o.sessionToken,region:r,accountId:t});return i({provider:d,credentials:{accessKeyId:o.accessKeyId,secretAccessKey:o.secretAccessKey,sessionToken:o.sessionToken}})}async function R(e,t,r,s){const n=await e.cdkService.runCdkSynth(t,o=>r.onCdkOutput?.(o,"synth"));if(!n.success){const o=new Error(c(`${s}: ${n.error}`));return r.onError?.(o),u(o)}return i(void 0)}async function x(e,t,r){r.onCDKBootstrap?.("bootstrapping");const s=await e.cdkService.runCdkBootstrap(t,l(r));if(!s.success){r.onCDKBootstrap?.("failed");const n=new Error(c(`Bootstrap failed: ${s.error}`));return r.onError?.(n),u(n)}return r.onCDKBootstrap?.("complete"),i(void 0)}export{A as assumeCascadeRole,x as bootstrapOrFail,O as buildCascadeRoleArn,K as buildParamsContext,E as collectStackOutputs,l as forwardOutput,w as forwardResourceProgress,R as synthOrFail};
1
+ import{success as f,failure as u}from"@fjall/generator";import{getErrorMessage as y,maskSensitiveOutput as d,sleep as E}from"@fjall/util";import{logger as O}from"@fjall/util/logger";import{SimpleAwsProvider as R}from"../aws/SimpleAwsProvider.js";function k(e){return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{},...e.skipAccountGlobals?{fjallAccountGlobalsConfigured:!0}:{}}}function C(e){return r=>e.onOutput?.(r)}function M(e){return r=>e.onResourceProgress?.(r)}function T(e){if(!e.success||e.data.length===0)return;const r={};for(const t of e.data)t.OutputKey&&t.OutputValue!==void 0&&(r[t.OutputKey]=t.OutputValue);return Object.keys(r).length>0?r:void 0}const g="OrganizationAccountAccessRole",l=5,S=5e3,w=3e4;function $(e){return`arn:aws:iam::${e}:role/${g}`}async function D(e,r,t,s){if(!e.assumeRole)return u(new Error("AwsProvider does not support assumeRole"));const n=$(r),i=e.assumeRole.bind(e);let o;for(let c=0;c<=l;c++)try{o=await i(n,s);break}catch(a){const p=a instanceof Error?a.name:void 0;if(p==="AccessDenied"||p==="AccessDeniedException")return u(new Error(`Access denied assuming ${g} in account ${r}. The role may not exist or may not trust the management account.`));if(c<l){const m=Math.min(S*2**c,w);O.debug("assumeCascadeRole",`Attempt ${c+1} failed for account ${r}, retrying in ${Math.round(m/1e3)}s`,{error:d(y(a))}),await E(m);continue}return u(new Error(`Failed to assume role in account ${r} after ${l+1} attempts: ${d(y(a))}`))}if(!o)return u(new Error(`Failed to assume role in account ${r}`));const A=new R({accessKeyId:o.accessKeyId,secretAccessKey:o.secretAccessKey,sessionToken:o.sessionToken,region:t,accountId:r});return f({provider:A,credentials:{accessKeyId:o.accessKeyId,secretAccessKey:o.secretAccessKey,sessionToken:o.sessionToken}})}async function b(e,r,t,s){const n=await e.cdkService.runCdkSynth(r,i=>t.onCdkOutput?.(i,"synth"));if(!n.success){const i=new Error(d(`${s}: ${n.error}`));return t.onError?.(i),u(i)}return f(void 0)}async function B(e,r,t){t.onCDKBootstrap?.("bootstrapping");const s=await e.cdkService.runCdkBootstrap(r,C(t));if(!s.success){t.onCDKBootstrap?.("failed");const n=new Error(d(`Bootstrap failed: ${s.error}`));return t.onError?.(n),u(n)}return t.onCDKBootstrap?.("complete"),f(void 0)}export{D as assumeCascadeRole,B as bootstrapOrFail,$ as buildCascadeRoleArn,k as buildParamsContext,T as collectStackOutputs,C as forwardOutput,M as forwardResourceProgress,b as synthOrFail};
@@ -4,7 +4,10 @@ export { deployOrganisation } from "./organisationDeploy.js";
4
4
  export { destroyOrganisation } from "./organisationDestroy.js";
5
5
  export { cleanupFailedStack, isCleanableState, SAFE_CLEANUP_STATES } from "./stackCleanup.js";
6
6
  export type { CascadeDestroyAccountResult } from "./cascadeDestroyHelpers.js";
7
- export { partitionAccounts } from "./cascadeHelpers.js";
7
+ export { partitionAccounts, buildRegionList, buildAccountRegionPairs } from "./cascadeHelpers.js";
8
+ export type { AccountRegionPair } from "./cascadeHelpers.js";
9
+ export { projectScalarSummary, projectAccountRows } from "./cascadeSummary.js";
10
+ export type { CascadeOutcomeResult, CascadeMemberOutcome, CascadePlatformOutcome, CascadeLedger, CascadeAccountRow, CascadePlatformRow, CascadeAccountProjection } from "./cascadeSummary.js";
8
11
  export { reconcileProviderAccounts, mergeReconciledProviderAccounts } from "./reconcileProviderAccounts.js";
9
12
  export type { ReconcileResult } from "./reconcileProviderAccounts.js";
10
13
  export { parseAccountsConfiguration, flattenAccountsToEnvironments, extractAllAccountNames, accountsConfigToOUTree, isStringArray, isAccountsConfig } from "./accountsConfig.js";
@@ -1 +1 @@
1
- import{deploy as e}from"./deploy.js";import{destroy as n}from"./destroy.js";import{deployOrganisation as i}from"./organisationDeploy.js";import{destroyOrganisation as p}from"./organisationDestroy.js";import{cleanupFailedStack as f,isCleanableState as m,SAFE_CLEANUP_STATES as u}from"./stackCleanup.js";import{partitionAccounts as l}from"./cascadeHelpers.js";import{reconcileProviderAccounts as d,mergeReconciledProviderAccounts as g}from"./reconcileProviderAccounts.js";import{parseAccountsConfiguration as y,flattenAccountsToEnvironments as C,extractAllAccountNames as O,accountsConfigToOUTree as T,isStringArray as E,isAccountsConfig as v}from"./accountsConfig.js";import{runOpenNextBuild as P}from"./openNextBuild.js";import{runOrganisationSetup as U}from"./organisationSetup.js";export*from"./builders/index.js";export{u as SAFE_CLEANUP_STATES,T as accountsConfigToOUTree,f as cleanupFailedStack,e as deploy,i as deployOrganisation,n as destroy,p as destroyOrganisation,O as extractAllAccountNames,C as flattenAccountsToEnvironments,v as isAccountsConfig,m as isCleanableState,E as isStringArray,g as mergeReconciledProviderAccounts,y as parseAccountsConfiguration,l as partitionAccounts,d as reconcileProviderAccounts,P as runOpenNextBuild,U as runOrganisationSetup};
1
+ import{deploy as t}from"./deploy.js";import{destroy as n}from"./destroy.js";import{deployOrganisation as i}from"./organisationDeploy.js";import{destroyOrganisation as p}from"./organisationDestroy.js";import{cleanupFailedStack as u,isCleanableState as m,SAFE_CLEANUP_STATES as f}from"./stackCleanup.js";import{partitionAccounts as x,buildRegionList as A,buildAccountRegionPairs as d}from"./cascadeHelpers.js";import{projectScalarSummary as S,projectAccountRows as y}from"./cascadeSummary.js";import{reconcileProviderAccounts as O,mergeReconciledProviderAccounts as T}from"./reconcileProviderAccounts.js";import{parseAccountsConfiguration as P,flattenAccountsToEnvironments as R,extractAllAccountNames as b,accountsConfigToOUTree as v,isStringArray as N,isAccountsConfig as j}from"./accountsConfig.js";import{runOpenNextBuild as L}from"./openNextBuild.js";import{runOrganisationSetup as _}from"./organisationSetup.js";export*from"./builders/index.js";export{f as SAFE_CLEANUP_STATES,v as accountsConfigToOUTree,d as buildAccountRegionPairs,A as buildRegionList,u as cleanupFailedStack,t as deploy,i as deployOrganisation,n as destroy,p as destroyOrganisation,b as extractAllAccountNames,R as flattenAccountsToEnvironments,j as isAccountsConfig,m as isCleanableState,N as isStringArray,T as mergeReconciledProviderAccounts,P as parseAccountsConfiguration,x as partitionAccounts,y as projectAccountRows,S as projectScalarSummary,O as reconcileProviderAccounts,L as runOpenNextBuild,_ as runOrganisationSetup};
@@ -1,3 +1,5 @@
1
- import{success as W,failure as R}from"@fjall/generator";import{OrganizationsClient as lt}from"@aws-sdk/client-organizations";import{ORGANISATION_TYPES as Y,getOrganisationStackName as X}from"../types/operations.js";import{CdkContextBuilder as pt}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as mt}from"../types/deployment/index.js";import{ensureOrganisationExists as gt}from"../aws/organisations/organisation.js";import{buildParamsContext as ft,collectStackOutputs as Z,synthOrFail as tt,bootstrapOrFail as et,forwardOutput as ot,forwardResourceProgress as nt}from"./contextHelpers.js";import{partitionAccounts as rt,deployCascadeAccount as at,readPlatformIpamPoolIds as Ct,deployDomains as St}from"./cascadeHelpers.js";import{reconcileProviderAccounts as Et,mergeReconciledProviderAccounts as Ot}from"./reconcileProviderAccounts.js";import{maskSensitiveOutput as i,STRUCTURAL_ENVIRONMENTS as At}from"@fjall/util";import{INFRA_STEP_NAME as F,STEP_IDS as I,STEP_NAMES as j}from"../types/stepDefinitions.js";async function bt(n,e,r){const p=Date.now();switch(r.type){case Y.ORGANISATION:return Pt(n,e,r,p);case Y.PLATFORM:return it(n,e,r,"platform",p);case Y.ACCOUNT:return it(n,e,r,"account",p);default:{const t=r.type;return R(new Error(`Unsupported organisation type: ${String(t)}`))}}}function st(n,e,r,p,t,a){return pt.buildDeploymentContext({deployType:p,target:r.target,path:r.path,region:e.awsProvider.getRegion(),accountName:a,callerIdentity:mt(e.awsProvider.getAccountId()),orgId:t.orgId,rootId:t.rootId,managementAccountId:t.managementAccountId,...ft({orgConfig:n.orgConfig,identity:n.identity,skipOidc:n.options?.skipOidc})},{verbose:n.options?.verbose,infraOnly:n.options?.infraOnly},n.orgConfig)}async function ct(n){const e=n.awsProvider.getClient(lt),r=await gt(e);return r.success?W({orgId:r.data.orgId,rootId:r.data.rootId,managementAccountId:r.data.managementAccountId}):R(r.error)}const o={CONNECT:{id:I.CONNECT,name:F.CONNECT},PREPARE:{id:I.PREPARE_ENVIRONMENT,name:F.PREPARE},DEPLOY:{id:I.DEPLOY,name:F.DEPLOY},MONITORING:{id:I.MONITORING,name:F.MONITORING},ORG_DEPLOY:{id:I.ORG_DEPLOY,name:j.ORG_DEPLOY},CASCADE_PLATFORM:{id:I.CASCADE_PLATFORM,name:j.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:I.CASCADE_ACCOUNTS,name:j.CASCADE_ACCOUNTS}},g=4;async function it(n,e,r,p,t){const{callbacks:a}=n;a.onStepComplete?.(o.CONNECT.id,o.CONNECT.name,"completed",0,g),a.onStepStart?.(o.PREPARE.id,o.PREPARE.name,1,g);const f=await ct(e);if(!f.success){a.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"error",1,g);const O=new Error(i(f.error.message));return a.onError?.(O),R(O)}const D=st(n,e,r,p,f.data,p==="account"?r.target:void 0);a.onLog?.(`Synthesising ${p} infrastructure\u2026`,"info");const N=await tt(e,D,a,"CDK synthesis failed");if(!N.success)return a.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"error",1,g),N;const w=await et(e,D,a);if(!w.success)return a.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"error",1,g),w;a.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"completed",1,g);const y=X(r.type);a.onStepStart?.(o.DEPLOY.id,o.DEPLOY.name,2,g);const L=await e.cdkService.runCdkDeploy(D,y,ot(a),nt(a),e.awsProvider);if(!L.success){a.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"error",2,g);const O=new Error(i(L.error));return a.onError?.(O),R(O)}a.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"completed",2,g);const k=await e.cfnService.getStackOutputs(y);k.success||a.onLog?.("Failed to read stack outputs (non-critical)","debug");const $=Z(k);return a.onStepStart?.(o.MONITORING.id,o.MONITORING.name,3,g),a.onStepComplete?.(o.MONITORING.id,o.MONITORING.name,"completed",3,g),W({target:r.target,deploymentType:"organisation",outputs:$,durationMs:Date.now()-t})}async function Pt(n,e,r,p){const{callbacks:t,options:a}=n;let f=n.orgConfig?.providerAccounts??[];if(f.length===0||f.every(s=>s.environment===At.ROOT)){const s=await Et(e,n.workingDirectory);if(s.success){const{providerAccounts:C,missingAccountNames:A}=s.data;C.length>0&&(f=Ot(n.orgConfig,C).providerAccounts,t.onLog?.(`Reconciled ${C.length} account(s) from AWS Organizations`,"info")),A.length>0&&(t.onCascadeMissingAccounts?.(A),t.onProgress?.({type:"warning",message:i(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${A.join(", ")}`)}))}else t.onProgress?.({type:"warning",message:i(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${s.error.message}`)})}const N=await ct(e);if(!N.success){const s=new Error(i(N.error.message));return t.onError?.(s),R(s)}const w=st(n,e,r,"organisation",N.data),y=a?.cascade!==!1,{platformAccount:L,memberAccounts:k}=rt(f),$=y&&L!==void 0?1:0,O=y&&k.length>0?1:0,c=2+$+O,{id:_,name:b}=o.PREPARE;t.onStepStart?.(_,b,0,c),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const K=await tt(e,w,t,"CDK synthesis failed");if(!K.success)return t.onStepComplete?.(_,b,"error",0,c),K;const V=await et(e,w,t);if(!V.success)return t.onStepComplete?.(_,b,"error",0,c),V;t.onStepComplete?.(_,b,"completed",0,c);const{id:x,name:U}=o.ORG_DEPLOY;t.onStepStart?.(x,U,1,c);const B=X(Y.ORGANISATION),q=await e.cdkService.runCdkDeploy(w,B,ot(t),nt(t),e.awsProvider);if(!q.success){t.onStepComplete?.(x,U,"error",1,c);const s=new Error(i(q.error));return t.onError?.(s),R(s)}const H=await e.cfnService.getStackOutputs(B);H.success||t.onLog?.("Failed to read org stack outputs (non-critical)","debug");const ut=Z(H);t.onStepComplete?.(x,U,"completed",1,c);const u=[],M=[];if(y&&f.length>0){t.onCascadeStart?.();let s=0,C=0,A=!1,J=!1,P=2;const{platformAccount:S,memberAccounts:v}=rt(f);if(S){const{id:d,name:l}=o.CASCADE_PLATFORM;t.onStepStart?.(d,l,P,c),t.onCascadePhaseStart?.("platform");let E;try{E=await at(n,e,r,S,"platform",t)}catch(T){const m=i(T instanceof Error?T.message:String(T));u.push({accountId:S.id,error:m}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(d,l,"error",P,c),E=R(new Error(m))}E.success?(A=!0,E.data.outputs&&M.push({accountId:S.id,outputs:E.data.outputs}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(d,l,"completed",P,c)):u.some(T=>T.accountId===S.id)||(u.push({accountId:S.id,error:i(E.error.message)}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(d,l,"error",P,c)),P++}let Q=new Map;if(A&&S&&(Q=await Ct(e,S,t)),n.domainProvider){const d=await St(n.domainProvider,t);J=d.domainsDeployed>0;for(const l of d.errors)u.push({accountId:"domains",error:i(l)})}if(v.length>0){const{id:d,name:l}=o.CASCADE_ACCOUNTS;t.onStepStart?.(d,l,P,c),t.onCascadePhaseStart?.("accounts");const E=e.awsProvider.getRegion();(await Promise.allSettled(v.map(m=>{const z=E.replace(/-/g,""),h=Q.get(`${m.id}-${z}`);return at(n,e,r,m,"account",t,h)}))).forEach((m,z)=>{const h=v[z];if(!h)return;if(m.status==="rejected"){C++,u.push({accountId:h.id,error:i(m.reason instanceof Error?m.reason.message:String(m.reason))});return}const G=m.value;G.success?(s++,G.data.outputs&&M.push({accountId:h.id,outputs:G.data.outputs})):(C++,u.push({accountId:h.id,error:i(G.error.message)}))}),t.onCascadePhaseComplete?.("accounts"),t.onStepComplete?.(d,l,C===0?"completed":"error",P,c)}if(t.onCascadeComplete?.({platformDeployed:A,domainsDeployed:J,accountsDeployed:s,accountsFailed:C,errors:u}),u.length>0){const d=u.map(l=>` ${l.accountId}: ${l.error}`).join(`
2
- `);t.onLog?.(i(`Cascade completed with ${u.length} failure(s):
3
- ${d}`),"warn")}}const dt=u.length>0?u.map(s=>i(`${s.accountId}: ${s.error}`)):void 0;return W({target:r.target,deploymentType:"organisation",outputs:ut,...M.length>0?{cascadeOutputs:M}:{},durationMs:Date.now()-p,warnings:dt})}export{bt as deployOrganisation};
1
+ import{success as H,failure as P}from"@fjall/generator";import{OrganizationsClient as Pt}from"@aws-sdk/client-organizations";import{ORGANISATION_TYPES as W,getOrganisationStackName as dt}from"../types/operations.js";import{CdkContextBuilder as It}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as Nt}from"../types/deployment/index.js";import{ensureOrganisationExists as wt}from"../aws/organisations/organisation.js";import{buildParamsContext as yt,collectStackOutputs as ut,synthOrFail as lt,bootstrapOrFail as pt,forwardOutput as mt,forwardResourceProgress as gt}from"./contextHelpers.js";import{partitionAccounts as ft,deployCascadeAccount as Ct,readPlatformIpamPoolIds as ht,deployDomains as Dt,buildRegionList as Tt,buildAccountRegionPairs as kt,CASCADE_MAX_CONCURRENCY as bt}from"./cascadeHelpers.js";import{projectScalarSummary as St}from"./cascadeSummary.js";import{reconcileProviderAccounts as Lt,mergeReconciledProviderAccounts as Mt}from"./reconcileProviderAccounts.js";import{maskSensitiveOutput as u,STRUCTURAL_ENVIRONMENTS as _t,mapSettledWithConcurrency as Gt}from"@fjall/util";import{INFRA_STEP_NAME as j,STEP_IDS as h,STEP_NAMES as J}from"../types/stepDefinitions.js";async function qt(e,o,r){const g=Date.now();switch(r.type){case W.ORGANISATION:return Yt(e,o,r,g);case W.PLATFORM:return Ot(e,o,r,"platform",g);case W.ACCOUNT:return Ot(e,o,r,"account",g);default:{const t=r.type;return P(new Error(`Unsupported organisation type: ${String(t)}`))}}}function At(e,o,r,g,t,a){return It.buildDeploymentContext({deployType:g,target:r.target,path:r.path,region:o.awsProvider.getRegion(),accountName:a,callerIdentity:Nt(o.awsProvider.getAccountId()),orgId:t.orgId,rootId:t.rootId,managementAccountId:t.managementAccountId,...yt({orgConfig:e.orgConfig,identity:e.identity,skipOidc:e.options?.skipOidc})},{verbose:e.options?.verbose,infraOnly:e.options?.infraOnly},e.orgConfig)}async function Et(e){const o=e.awsProvider.getClient(Pt),r=await wt(o);return r.success?H({orgId:r.data.orgId,rootId:r.data.rootId,managementAccountId:r.data.managementAccountId}):P(r.error)}const n={CONNECT:{id:h.CONNECT,name:j.CONNECT},PREPARE:{id:h.PREPARE_ENVIRONMENT,name:j.PREPARE},DEPLOY:{id:h.DEPLOY,name:j.DEPLOY},MONITORING:{id:h.MONITORING,name:j.MONITORING},ORG_DEPLOY:{id:h.ORG_DEPLOY,name:J.ORG_DEPLOY},CASCADE_PLATFORM:{id:h.CASCADE_PLATFORM,name:J.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:h.CASCADE_ACCOUNTS,name:J.CASCADE_ACCOUNTS}},S=4;async function Ot(e,o,r,g,t){const{callbacks:a}=e;a.onStepComplete?.(n.CONNECT.id,n.CONNECT.name,"completed",0,S),a.onStepStart?.(n.PREPARE.id,n.PREPARE.name,1,S);const c=await Et(o);if(!c.success){a.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S);const I=new Error(u(c.error.message));return a.onError?.(I),P(I)}const Y=At(e,o,r,g,c.data,g==="account"?r.target:void 0);a.onLog?.(`Synthesising ${g} infrastructure\u2026`,"info");const b=await lt(o,Y,a,"CDK synthesis failed");if(!b.success)return a.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S),b;const L=await pt(o,Y,a);if(!L.success)return a.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S),L;a.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"completed",1,S);const D=dt(r.type);a.onStepStart?.(n.DEPLOY.id,n.DEPLOY.name,2,S);const T=await o.cdkService.runCdkDeploy(Y,D,mt(a),gt(a),o.awsProvider);if(!T.success){a.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"error",2,S);const I=new Error(u(T.error));return a.onError?.(I),P(I)}a.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"completed",2,S);const k=await o.cfnService.getStackOutputs(D);k.success||a.onLog?.("Failed to read stack outputs (non-critical)","debug");const z=ut(k);return a.onStepStart?.(n.MONITORING.id,n.MONITORING.name,3,S),a.onStepComplete?.(n.MONITORING.id,n.MONITORING.name,"completed",3,S),H({target:r.target,deploymentType:"organisation",outputs:z,durationMs:Date.now()-t})}async function Yt(e,o,r,g){const{callbacks:t,options:a}=e;let c=e.orgConfig?.providerAccounts??[];if(c.length===0||c.every(s=>s.environment===_t.ROOT)){const s=await Lt(o,e.workingDirectory);if(s.success){const{providerAccounts:i,missingAccountNames:E}=s.data;i.length>0&&(c=Mt(e.orgConfig,i).providerAccounts,t.onLog?.(`Reconciled ${i.length} account(s) from AWS Organizations`,"info")),E.length>0&&(t.onCascadeMissingAccounts?.(E),t.onProgress?.({type:"warning",message:u(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${E.join(", ")}`)}))}else t.onProgress?.({type:"warning",message:u(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${s.error.message}`)})}const b=e.orgConfig?.primaryRegion??o.awsProvider.getRegion();c=c.map(s=>s.region!==void 0?s:{...s,region:b});const L=e.orgConfig!==void 0?{...e.orgConfig,providerAccounts:c}:c.length>0?{providerAccounts:c}:void 0,D=await Et(o);if(!D.success){const s=new Error(u(D.error.message));return t.onError?.(s),P(s)}const T=At(e,o,r,"organisation",D.data),k=a?.cascade!==!1,{platformAccount:z,memberAccounts:I}=ft(c),Q=k&&z!==void 0?1:0,Z=k&&I.length>0?1:0,l=2+Q+Z;t.onCascadeAccountsReconciled?.({hasPlatformAccount:Q>0,hasMemberAccounts:Z>0});const{id:$,name:F}=n.PREPARE;t.onStepStart?.($,F,0,l),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const tt=await lt(o,T,t,"CDK synthesis failed");if(!tt.success)return t.onStepComplete?.($,F,"error",0,l),tt;const et=await pt(o,T,t);if(!et.success)return t.onStepComplete?.($,F,"error",0,l),et;t.onStepComplete?.($,F,"completed",0,l);const{id:K,name:V}=n.ORG_DEPLOY;t.onStepStart?.(K,V,1,l);const ot=dt(W.ORGANISATION),nt=await o.cdkService.runCdkDeploy(T,ot,mt(t),gt(t),o.awsProvider);if(!nt.success){t.onStepComplete?.(K,V,"error",1,l);const s=new Error(u(nt.error));return t.onError?.(s),P(s)}const rt=await o.cfnService.getStackOutputs(ot);rt.success||t.onLog?.("Failed to read org stack outputs (non-critical)","debug");const Rt=ut(rt);t.onStepComplete?.(K,V,"completed",1,l);const p=[],x=[];if(k&&c.length>0){t.onCascadeStart?.();const s=Date.now();let i=2,E=!1,U,at=!1;const v=[],st=d=>({members:v,...U!==void 0?{platform:U}:{},domainsDeployed:at,errors:p,totalDurationMs:d}),{platformAccount:A,memberAccounts:B}=ft(c);if(A){const{id:d,name:m}=n.CASCADE_PLATFORM;t.onStepStart?.(d,m,i,l),t.onCascadePhaseStart?.("platform");let f;const X=Date.now();try{f=await Ct(e,o,r,A,"platform",t,{orgConfig:L})}catch(O){const _=u(O instanceof Error?O.message:String(O));p.push({accountId:A.id,error:_}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(d,m,"error",i,l),f=P(new Error(_))}const M=Date.now()-X;if(f.success){E=!0;const O=f.data.skipped===!0;f.data.outputs&&x.push({accountId:A.id,outputs:f.data.outputs}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(d,m,O?"skipped":"completed",i,l),U={accountId:A.id,result:O?"skipped":"succeeded",durationMs:M}}else p.some(O=>O.accountId===A.id)||(p.push({accountId:A.id,error:u(f.error.message)}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(d,m,"error",i,l)),U={accountId:A.id,result:"failed",durationMs:M,error:u(f.error.message)};i++}let ct=new Map;if(E&&A&&(ct=await ht(o,A,t)),e.domainProvider){const d=await Dt(e.domainProvider,t);at=d.domainsDeployed>0;for(const m of d.errors)p.push({accountId:"domains",error:u(m)})}if(B.length>0){const{id:d,name:m}=n.CASCADE_ACCOUNTS;t.onStepStart?.(d,m,i,l),t.onCascadePhaseStart?.("accounts");const f=Tt(e.orgConfig),X=f[0]??b,M=kt(B,f);(await Gt(M,bt,async({account:R,region:G})=>{const N=G.replace(/-/g,""),C=ct.get(`${R.id}-${N}`),w=Date.now();return{result:await Ct(e,o,r,R,"account",t,{ipamPoolId:C,orgConfig:L,region:G,skipAccountGlobals:G!==X}),durationMs:Date.now()-w}})).forEach((R,G)=>{const N=M[G];if(!N)return;const C=N.account;if(R.status==="rejected"){const y=u(R.reason instanceof Error?R.reason.message:String(R.reason));v.push({accountId:C.id,accountName:C.name,region:N.region,result:"failed",durationMs:0,error:y}),p.push({accountId:C.id,error:y});return}const{result:w,durationMs:q}=R.value;if(w.success){const y=w.data.skipped===!0;w.data.outputs&&x.push({accountId:C.id,outputs:w.data.outputs}),v.push({accountId:C.id,accountName:C.name,region:N.region,result:y?"skipped":"succeeded",durationMs:q})}else{const y=u(w.error.message);v.push({accountId:C.id,accountName:C.name,region:N.region,result:"failed",durationMs:q,error:y}),p.push({accountId:C.id,error:y})}});const _=St(st(0));t.onCascadePhaseComplete?.("accounts"),t.onStepComplete?.(d,m,_.accountsFailed>0?"error":_.accountsSkipped===B.length?"skipped":"completed",i,l)}const it=st(Date.now()-s);if(t.onCascadeComplete?.(St(it)),t.onCascadeLedger?.(it),p.length>0){const d=p.map(m=>` ${m.accountId}: ${m.error}`).join(`
2
+ `);t.onLog?.(u(`Cascade failed for ${p.length} target(s):
3
+ ${d}`),"warn")}}if(p.length>0){const s=p.map(E=>u(`${E.accountId}: ${E.error}`)).join(`
4
+ `),i=new Error(`Organisation root deployed, but the cascade failed for ${p.length} target(s):
5
+ ${s}`);return t.onError?.(i),P(i)}return H({target:r.target,deploymentType:"organisation",outputs:Rt,...x.length>0?{cascadeOutputs:x}:{},durationMs:Date.now()-g})}export{qt as deployOrganisation};
@@ -1,3 +1,3 @@
1
- import{success as R,failure as O}from"@fjall/generator";import{maskSensitiveOutput as h,getErrorMessage as A}from"@fjall/util";import{stubCallerIdentity as I}from"../types/deployment/index.js";import{ORGANISATION_TYPES as P,getOrganisationStackName as T}from"../types/operations.js";import{CdkContextBuilder as $}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as _,synthOrFail as k,forwardOutput as N,forwardResourceProgress as L}from"./contextHelpers.js";import{destroyCascadeAccount as D}from"./cascadeDestroyHelpers.js";import{partitionAccounts as b}from"./cascadeHelpers.js";import{DEFAULT_REGION as v}from"../aws/utils/regions.js";import{STEP_IDS as G}from"../types/stepDefinitions.js";const S=G.ORG_DESTROY,C="Destroying organisation infrastructure";async function V(t,r,n){const e=Date.now(),{callbacks:o}=t,d=t.orgConfig?.providerAccounts??[],f=t.orgConfig?.primaryRegion??r.awsProvider.getRegion(),l=M(t),{platformAccount:g,memberAccounts:m}=b(d),E=t.options?.cascade!==!1;o.onLog?.(`Destroying organisation infrastructure (${d.length} accounts, ${l.length} region(s))`,"info");const s=[],p=[];if(E){if(o.onCascadeStart?.(),m.length>0){o.onCascadePhaseStart?.("accounts");const a=Y(m,l),c=await Promise.allSettled(a.map(({account:i,region:u})=>D(t,r,n,i,"account",u,o)));for(let i=0;i<c.length;i++){const u=c[i],y=a[i];!u||!y||(u.status==="fulfilled"?u.value.success?p.push(`Account-${y.account.name}-${y.region}`):s.push({accountId:y.account.id,error:u.value.error??"Unknown error"}):s.push({accountId:y.account.id,error:A(u.reason)}))}}if(g){o.onCascadePhaseStart?.("platform");const a=await D(t,r,n,g,"platform",f,o);a.success?p.push("Platform"):s.push({accountId:g.id,error:a.error??"Platform destroy failed"})}if(o.onCascadeComplete?.({platformDeployed:!1,domainsDeployed:!1,accountsDeployed:0,accountsFailed:s.length,errors:s}),s.length>0){const a=s.map(i=>` ${i.accountId}: ${i.error}`).join(`
2
- `),c=new Error(`Cascade destroy completed with ${s.length} failure(s):
3
- ${a}`);o.onError?.(c),o.onLog?.(h(c.message),"warn")}}if(s.length>0)return o.onLog?.("Skipping organisation root stack destroy due to cascade failures","warn"),R({target:n.target,deploymentType:"organisation",stacksDestroyed:p,durationMs:Date.now()-e,warnings:s.map(c=>h(`${c.accountId}: ${c.error}`))});o.onStepStart?.(S,C,0,1);const w=await x(t,r,n,f,o);if(w.success)p.push("Organisation"),o.onStepComplete?.(S,C,"completed",0,1);else return o.onStepComplete?.(S,C,"error",0,1),O(w.error);return R({target:n.target,deploymentType:"organisation",stacksDestroyed:p,durationMs:Date.now()-e})}async function x(t,r,n,e,o){const d=$.buildDeploymentContext({deployType:"organisation",target:n.target,path:n.path,region:e,callerIdentity:I(r.awsProvider.getAccountId()),..._({orgConfig:t.orgConfig,identity:t.identity})},{verbose:t.options?.verbose},t.orgConfig),f=T(P.ORGANISATION);o.onLog?.("Synthesising organisation infrastructure\u2026","info");const l=await k(r,d,o,"Organisation synth failed");if(!l.success)return l;o.onLog?.(`Destroying ${f} stack\u2026`,"info");const g=await r.cdkService.runCdkDestroy(d,f,N(o),L(o),r.awsProvider,!0);if(!g.success){const m=new Error(h(`Organisation destroy failed: ${g.error}`));return o.onError?.(m),O(m)}return R(void 0)}function M(t){const r=t.orgConfig?.primaryRegion??v,n=t.orgConfig?.secondaryRegions??[],e=[r,...n],o=t.orgConfig?.disasterRecoveryRegion;return o&&!e.includes(o)&&e.push(o),e}function Y(t,r){const n=[];for(const e of r)for(const o of t)n.push({account:o,region:e});return n}export{V as destroyOrganisation};
1
+ import{success as O,failure as P}from"@fjall/generator";import{maskSensitiveOutput as g,getErrorMessage as $,mapSettledWithConcurrency as M}from"@fjall/util";import{stubCallerIdentity as _}from"../types/deployment/index.js";import{ORGANISATION_TYPES as k,getOrganisationStackName as b}from"../types/operations.js";import{CdkContextBuilder as v}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as L,synthOrFail as x,forwardOutput as G,forwardResourceProgress as Y}from"./contextHelpers.js";import{destroyCascadeAccount as T}from"./cascadeDestroyHelpers.js";import{partitionAccounts as j,buildRegionList as U,buildAccountRegionPairs as B,CASCADE_MAX_CONCURRENCY as F}from"./cascadeHelpers.js";import{projectScalarSummary as W}from"./cascadeSummary.js";import{STEP_IDS as X}from"../types/stepDefinitions.js";const D=X.ORG_DESTROY,I="Destroying organisation infrastructure";async function et(r,e,i){const h=Date.now(),{callbacks:t}=r,l=r.orgConfig?.providerAccounts??[],f=r.orgConfig?.primaryRegion??e.awsProvider.getRegion(),m=U(r.orgConfig),{platformAccount:u,memberAccounts:p}=j(l),N=r.options?.cascade!==!1;t.onLog?.(`Destroying organisation infrastructure (${l.length} accounts, ${m.length} region(s))`,"info");const n=[],y=[],R=[];let w;if(N){t.onCascadeStart?.();const E=Date.now();if(p.length>0){t.onCascadePhaseStart?.("accounts");const a=B(p,m),d=await M(a,F,({account:s,region:c})=>T(r,e,i,s,"account",c,t));for(let s=0;s<d.length;s++){const c=d[s],o=a[s];if(!(!c||!o))if(c.status==="fulfilled")if(c.value.success)y.push(`Account-${o.account.name}-${o.region}`),R.push({accountId:o.account.id,accountName:o.account.name,region:o.region,result:"succeeded",durationMs:c.value.duration});else{const C=g(c.value.error??"Unknown error");R.push({accountId:o.account.id,accountName:o.account.name,region:o.region,result:"failed",durationMs:c.value.duration,error:C}),n.push({accountId:o.account.id,error:C})}else{const C=g($(c.reason));R.push({accountId:o.account.id,accountName:o.account.name,region:o.region,result:"failed",durationMs:0,error:C}),n.push({accountId:o.account.id,error:C})}}}if(u){t.onCascadePhaseStart?.("platform");const a=await T(r,e,i,u,"platform",f,t);if(a.success)y.push("Platform"),w={accountId:u.id,result:"succeeded",durationMs:a.duration};else{const d=g(a.error??"Platform destroy failed");n.push({accountId:u.id,error:d}),w={accountId:u.id,result:"failed",durationMs:a.duration,error:d}}}const S={members:R,...w!==void 0?{platform:w}:{},domainsDeployed:!1,errors:n,totalDurationMs:Date.now()-E};if(t.onCascadeComplete?.(W(S)),t.onCascadeLedger?.(S),n.length>0){const a=n.map(s=>` ${s.accountId}: ${s.error}`).join(`
2
+ `),d=new Error(`Cascade destroy completed with ${n.length} failure(s):
3
+ ${a}`);t.onError?.(d),t.onLog?.(g(d.message),"warn")}}if(n.length>0)return t.onLog?.("Skipping organisation root stack destroy due to cascade failures","warn"),O({target:i.target,deploymentType:"organisation",stacksDestroyed:y,durationMs:Date.now()-h,warnings:n.map(S=>g(`${S.accountId}: ${S.error}`))});t.onStepStart?.(D,I,0,1);const A=await q(r,e,i,f,t);if(A.success)y.push("Organisation"),t.onStepComplete?.(D,I,"completed",0,1);else return t.onStepComplete?.(D,I,"error",0,1),P(A.error);return O({target:i.target,deploymentType:"organisation",stacksDestroyed:y,durationMs:Date.now()-h})}async function q(r,e,i,h,t){const l=v.buildDeploymentContext({deployType:"organisation",target:i.target,path:i.path,region:h,callerIdentity:_(e.awsProvider.getAccountId()),...L({orgConfig:r.orgConfig,identity:r.identity})},{verbose:r.options?.verbose},r.orgConfig),f=b(k.ORGANISATION);t.onLog?.("Synthesising organisation infrastructure\u2026","info");const m=await x(e,l,t,"Organisation synth failed");if(!m.success)return m;t.onLog?.(`Destroying ${f} stack\u2026`,"info");const u=await e.cdkService.runCdkDestroy(l,f,G(t),Y(t),e.awsProvider,!0);if(!u.success){const p=new Error(g(`Organisation destroy failed: ${u.error}`));return t.onError?.(p),P(p)}return O(void 0)}export{et as destroyOrganisation};
@@ -12,10 +12,8 @@
12
12
  * (webapp worker, CLI via deploy-core).
13
13
  */
14
14
  import type { DeployCallbacks } from "../types/callbacks.js";
15
- /** Stack states safe to auto-delete -- never had a successful deployment. */
16
- export declare const SAFE_CLEANUP_STATES: Set<string>;
17
- /** Check whether a stack status is safe for automatic cleanup. */
18
- export declare function isCleanableState(status: string): boolean;
15
+ import { SAFE_CLEANUP_STATES, isCleanableState } from "../types/constants.js";
16
+ export { SAFE_CLEANUP_STATES, isCleanableState };
19
17
  interface StackCleanupCredentials {
20
18
  accessKeyId: string;
21
19
  secretAccessKey: string;
@@ -36,4 +34,3 @@ export declare function cleanupFailedStack(stackName: string, region: string, cr
36
34
  timeoutMs?: number;
37
35
  pollMs?: number;
38
36
  }, callbacks?: DeployCallbacks): Promise<void>;
39
- export {};
@@ -1 +1 @@
1
- import{CloudFormationClient as D,DescribeStacksCommand as C,DeleteStackCommand as k,ListStackResourcesCommand as _}from"@aws-sdk/client-cloudformation";import{S3Client as $,ListObjectVersionsCommand as A,DeleteObjectsCommand as h}from"@aws-sdk/client-s3";import{NodeHttpHandler as f}from"@smithy/node-http-handler";import{logger as o}from"@fjall/util/logger";import{getErrorMessage as S,maskSensitiveOutput as w,sleep as m}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as L}from"../types/constants.js";const g=1e3,M=new Set(["ROLLBACK_FAILED","ROLLBACK_COMPLETE","DELETE_FAILED"]);function P(e){return M.has(e)}async function R(e,t,n){let d,s;n?.onStackCleanupProgress?.(t,"emptying-bucket");const u=1e3;let l=0;for(;l++<u;){let r;try{r=await e.send(new A({Bucket:t,KeyMarker:d,VersionIdMarker:s}))}catch(c){if(c instanceof Error&&(c.name==="NoSuchBucket"||c.message?.includes("NoSuchBucket"))){o.debug("stackCleanup",`Bucket ${t} no longer exists, skipping`);return}const p=`Unexpected error emptying bucket ${t}: ${w(S(c))}`;o.warn("stackCleanup",p),n?.onLog?.(p,"warn");return}const i=[...r.Versions??[],...r.DeleteMarkers??[]];if(i.length===0)break;for(let c=0;c<i.length;c+=g){const p=i.slice(c,c+g);try{await e.send(new h({Bucket:t,Delete:{Objects:p.map(a=>({Key:a.Key,VersionId:a.VersionId})),Quiet:!0}}))}catch(a){const E=`Failed to delete batch from ${t}: ${w(S(a))}`;o.warn("stackCleanup",E),n?.onLog?.(E,"warn")}}if(!r.IsTruncated)break;d=r.NextKeyMarker,s=r.NextVersionIdMarker}if(l>u){const r=`Bucket ${t} reached ${u} page limit \u2014 some objects may remain`;o.warn("stackCleanup",r),n?.onLog?.(r,"warn")}o.debug("stackCleanup",`Emptied bucket ${t}`)}async function T(e,t,n,d){const s=[];let u,r=0;do{if(r++>=100){o.warn("stackCleanup","Reached 100 page limit listing stack resources",{stackName:t});break}const i=await e.send(new _({StackName:t,NextToken:u}));for(const c of i.StackResourceSummaries??[])if(n(c)){const p=d(c);p&&s.push(p)}u=i.NextToken}while(u);return s}async function F(e,t){return T(e,t,n=>n.ResourceType==="AWS::S3::Bucket"&&n.ResourceStatus==="DELETE_FAILED",n=>n.PhysicalResourceId)}async function j(e,t,n,d,s){const u=d?.timeoutMs??3e5,l=d?.pollMs??5e3;try{const r=new D({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});let i;try{i=(await r.send(new C({StackName:e}))).Stacks?.[0]?.StackStatus}catch(a){if(a instanceof Error&&a.message?.includes(L)){o.debug("stackCleanup",`Stack ${e} does not exist, no cleanup needed`);return}o.warn("stackCleanup",`Failed to check stack status: ${w(S(a))}`,{stackName:e,region:t});return}if(!i||!P(i)){o.debug("stackCleanup",`Stack ${e} status ${i??"unknown"} is not cleanable, skipping`);return}o.warn("stackCleanup",`Cleaning up ${e} stack in ${i} state`,{region:t}),s?.onStackCleanupProgress?.(e,"deleting-stack");const c=new $({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});try{const a=await F(r,e);for(const E of a)o.warn("stackCleanup",`Emptying bucket ${E}`,{region:t}),await R(c,E,s)}catch(a){const E=`Failed to empty S3 buckets: ${w(S(a))}`;o.warn("stackCleanup",E,{stackName:e,region:t}),s?.onLog?.(E,"warn")}await r.send(new k({StackName:e})),s?.onStackCleanupProgress?.(e,"waiting");const p=await y(r,e,u,l);if(p==="DELETE_COMPLETE"){o.warn("stackCleanup",`${e} stack deleted successfully`,{region:t}),s?.onStackCleanupProgress?.(e,"complete");return}if(p==="DELETE_FAILED"){o.warn("stackCleanup",`${e} still in DELETE_FAILED, retrying with RetainResources`,{region:t});const a=await I(r,e);if(a.length===0)o.warn("stackCleanup",`${e} in DELETE_FAILED but no non-bucket resources to retain \u2014 cannot retry`,{region:t}),s?.onStackCleanupProgress?.(e,"error");else{await r.send(new k({StackName:e,RetainResources:a}));const E=await y(r,e,u,l);E==="DELETE_COMPLETE"?(o.warn("stackCleanup",`${e} stack deleted on retry (retained: ${a.join(", ")})`,{region:t}),s?.onStackCleanupProgress?.(e,"complete")):(o.warn("stackCleanup",`${e} stack still not deleted after retry: ${E}`,{region:t}),s?.onStackCleanupProgress?.(e,"error"))}}}catch(r){o.warn("stackCleanup",`Stack cleanup failed: ${w(S(r))}`,{stackName:e,region:t}),s?.onStackCleanupProgress?.(e,"error")}}async function y(e,t,n,d){const s=Date.now();for(;Date.now()-s<n;){await m(d);try{const l=(await e.send(new C({StackName:t}))).Stacks?.[0]?.StackStatus;if(!l||l==="DELETE_COMPLETE")return"DELETE_COMPLETE";if(l==="DELETE_FAILED")return"DELETE_FAILED";o.debug("stackCleanup",`Waiting for ${t}: ${l}`)}catch(u){if(u instanceof Error&&u.message?.includes(L))return"DELETE_COMPLETE";throw o.debug("stackCleanup",`Unexpected error polling ${t}: ${S(u)}`),u}}return"TIMEOUT"}async function I(e,t){return T(e,t,n=>n.ResourceStatus==="DELETE_FAILED"&&n.ResourceType!=="AWS::S3::Bucket",n=>n.LogicalResourceId)}export{M as SAFE_CLEANUP_STATES,j as cleanupFailedStack,P as isCleanableState};
1
+ import{CloudFormationClient as $,DescribeStacksCommand as k,DeleteStackCommand as C,ListStackResourcesCommand as _}from"@aws-sdk/client-cloudformation";import{S3Client as m,ListObjectVersionsCommand as h,DeleteObjectsCommand as A}from"@aws-sdk/client-s3";import{NodeHttpHandler as f}from"@smithy/node-http-handler";import{logger as s}from"@fjall/util/logger";import{getErrorMessage as S,maskSensitiveOutput as w,sleep as M}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as g,SAFE_CLEANUP_STATES as P,isCleanableState as T}from"../types/constants.js";const L=1e3;async function R(e,t,n){let d,o;n?.onStackCleanupProgress?.(t,"emptying-bucket");const u=1e3;let l=0;for(;l++<u;){let r;try{r=await e.send(new h({Bucket:t,KeyMarker:d,VersionIdMarker:o}))}catch(c){if(c instanceof Error&&(c.name==="NoSuchBucket"||c.message?.includes("NoSuchBucket"))){s.debug("stackCleanup",`Bucket ${t} no longer exists, skipping`);return}const E=`Unexpected error emptying bucket ${t}: ${w(S(c))}`;s.warn("stackCleanup",E),n?.onLog?.(E,"warn");return}const i=[...r.Versions??[],...r.DeleteMarkers??[]];if(i.length===0)break;for(let c=0;c<i.length;c+=L){const E=i.slice(c,c+L);try{await e.send(new A({Bucket:t,Delete:{Objects:E.map(a=>({Key:a.Key,VersionId:a.VersionId})),Quiet:!0}}))}catch(a){const p=`Failed to delete batch from ${t}: ${w(S(a))}`;s.warn("stackCleanup",p),n?.onLog?.(p,"warn")}}if(!r.IsTruncated)break;d=r.NextKeyMarker,o=r.NextVersionIdMarker}if(l>u){const r=`Bucket ${t} reached ${u} page limit \u2014 some objects may remain`;s.warn("stackCleanup",r),n?.onLog?.(r,"warn")}s.debug("stackCleanup",`Emptied bucket ${t}`)}async function y(e,t,n,d){const o=[];let u,r=0;do{if(r++>=100){s.warn("stackCleanup","Reached 100 page limit listing stack resources",{stackName:t});break}const i=await e.send(new _({StackName:t,NextToken:u}));for(const c of i.StackResourceSummaries??[])if(n(c)){const E=d(c);E&&o.push(E)}u=i.NextToken}while(u);return o}async function F(e,t){return y(e,t,n=>n.ResourceType==="AWS::S3::Bucket"&&n.ResourceStatus==="DELETE_FAILED",n=>n.PhysicalResourceId)}async function H(e,t,n,d,o){const u=d?.timeoutMs??3e5,l=d?.pollMs??5e3;try{const r=new $({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});let i;try{i=(await r.send(new k({StackName:e}))).Stacks?.[0]?.StackStatus}catch(a){if(a instanceof Error&&a.message?.includes(g)){s.debug("stackCleanup",`Stack ${e} does not exist, no cleanup needed`);return}s.warn("stackCleanup",`Failed to check stack status: ${w(S(a))}`,{stackName:e,region:t});return}if(!i||!T(i)){s.debug("stackCleanup",`Stack ${e} status ${i??"unknown"} is not cleanable, skipping`);return}s.warn("stackCleanup",`Cleaning up ${e} stack in ${i} state`,{region:t}),o?.onStackCleanupProgress?.(e,"deleting-stack");const c=new m({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});try{const a=await F(r,e);for(const p of a)s.warn("stackCleanup",`Emptying bucket ${p}`,{region:t}),await R(c,p,o)}catch(a){const p=`Failed to empty S3 buckets: ${w(S(a))}`;s.warn("stackCleanup",p,{stackName:e,region:t}),o?.onLog?.(p,"warn")}await r.send(new C({StackName:e})),o?.onStackCleanupProgress?.(e,"waiting");const E=await D(r,e,u,l);if(E==="DELETE_COMPLETE"){s.warn("stackCleanup",`${e} stack deleted successfully`,{region:t}),o?.onStackCleanupProgress?.(e,"complete");return}if(E==="DELETE_FAILED"){s.warn("stackCleanup",`${e} still in DELETE_FAILED, retrying with RetainResources`,{region:t});const a=await I(r,e);if(a.length===0)s.warn("stackCleanup",`${e} in DELETE_FAILED but no non-bucket resources to retain \u2014 cannot retry`,{region:t}),o?.onStackCleanupProgress?.(e,"error");else{await r.send(new C({StackName:e,RetainResources:a}));const p=await D(r,e,u,l);p==="DELETE_COMPLETE"?(s.warn("stackCleanup",`${e} stack deleted on retry (retained: ${a.join(", ")})`,{region:t}),o?.onStackCleanupProgress?.(e,"complete")):(s.warn("stackCleanup",`${e} stack still not deleted after retry: ${p}`,{region:t}),o?.onStackCleanupProgress?.(e,"error"))}}}catch(r){s.warn("stackCleanup",`Stack cleanup failed: ${w(S(r))}`,{stackName:e,region:t}),o?.onStackCleanupProgress?.(e,"error")}}async function D(e,t,n,d){const o=Date.now();for(;Date.now()-o<n;){await M(d);try{const l=(await e.send(new k({StackName:t}))).Stacks?.[0]?.StackStatus;if(!l||l==="DELETE_COMPLETE")return"DELETE_COMPLETE";if(l==="DELETE_FAILED")return"DELETE_FAILED";s.debug("stackCleanup",`Waiting for ${t}: ${l}`)}catch(u){if(u instanceof Error&&u.message?.includes(g))return"DELETE_COMPLETE";throw s.debug("stackCleanup",`Unexpected error polling ${t}: ${S(u)}`),u}}return"TIMEOUT"}async function I(e,t){return y(e,t,n=>n.ResourceStatus==="DELETE_FAILED"&&n.ResourceType!=="AWS::S3::Bucket",n=>n.LogicalResourceId)}export{P as SAFE_CLEANUP_STATES,H as cleanupFailedStack,T as isCleanableState};
@@ -1 +1 @@
1
- import{filterDangerousEnvVars as i}from"@fjall/util";class d{buildContextArgs(r){const e=[];return r?.accountId&&e.push("-c",`accountId=${r.accountId}`),r?.environment&&e.push("-c",`environment=${r.environment}`),r?.managedAccount&&e.push("-c","managedAccount=true"),r?.accountName&&e.push("-c",`accountName=${r.accountName}`),r?.orgId&&e.push("-c",`orgId=${r.orgId}`),r?.rootId&&e.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&e.push("-c",`managementAccountId=${r.managementAccountId}`),r?.ipamPoolId&&e.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&e.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&e.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.orgConfig&&e.push("-c",`orgConfig=${r.orgConfig}`),e}buildParameterArgs(r){if(r===void 0)return[];const e=Object.entries(r);if(e.length===0)return[];const o=[];for(const[a,n]of e){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(a))throw new Error(`Invalid CloudFormation parameter name "${a}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(n==="")throw new Error(`CloudFormation parameter "${a}" has an empty value.`);if(/[,\n\r]/.test(n))throw new Error(`CloudFormation parameter "${a}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);o.push("--parameters",`${a}=${n}`)}return o}injectCascadeCredentials(r,e){e&&(r.AWS_ACCESS_KEY_ID=e.accessKeyId,r.AWS_SECRET_ACCESS_KEY=e.secretAccessKey,delete r.AWS_SESSION_TOKEN,e.sessionToken&&(r.AWS_SESSION_TOKEN=e.sessionToken))}buildCdkEnv(r){const e={...i(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1"};return r?.context?.region&&(e.AWS_REGION=r.context.region,e.AWS_DEFAULT_REGION=r.context.region,e.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(e.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(e,r?.credentials),e}}export{d as CdkArgumentBuilder};
1
+ import{filterDangerousEnvVars as i}from"@fjall/util";class d{buildContextArgs(r){const e=[];return r?.accountId&&e.push("-c",`accountId=${r.accountId}`),r?.environment&&e.push("-c",`environment=${r.environment}`),r?.managedAccount&&e.push("-c","managedAccount=true"),r?.accountName&&e.push("-c",`accountName=${r.accountName}`),r?.orgId&&e.push("-c",`orgId=${r.orgId}`),r?.rootId&&e.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&e.push("-c",`managementAccountId=${r.managementAccountId}`),r?.ipamPoolId&&e.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&e.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&e.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.fjallAccountGlobalsConfigured&&e.push("-c",`fjallAccountGlobalsConfigured=${r.fjallAccountGlobalsConfigured}`),r?.orgConfig&&e.push("-c",`orgConfig=${r.orgConfig}`),e}buildParameterArgs(r){if(r===void 0)return[];const e=Object.entries(r);if(e.length===0)return[];const o=[];for(const[a,n]of e){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(a))throw new Error(`Invalid CloudFormation parameter name "${a}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(n==="")throw new Error(`CloudFormation parameter "${a}" has an empty value.`);if(/[,\n\r]/.test(n))throw new Error(`CloudFormation parameter "${a}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);o.push("--parameters",`${a}=${n}`)}return o}injectCascadeCredentials(r,e){e&&(r.AWS_ACCESS_KEY_ID=e.accessKeyId,r.AWS_SECRET_ACCESS_KEY=e.secretAccessKey,delete r.AWS_SESSION_TOKEN,e.sessionToken&&(r.AWS_SESSION_TOKEN=e.sessionToken))}buildCdkEnv(r){const e={...i(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1"};return r?.context?.region&&(e.AWS_REGION=r.context.region,e.AWS_DEFAULT_REGION=r.context.region,e.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(e.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(e,r?.credentials),e}}export{d as CdkArgumentBuilder};
@@ -18,7 +18,7 @@ export declare class CdkService {
18
18
  runImport(path: string, resourceMappingFile?: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
19
19
  synth(path: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
20
20
  bootstrap(accountId: string, region: string, options?: CdkOptions): Promise<Result<CdkOutput, string>>;
21
- runCdkSynth(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
21
+ runCdkSynth(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
22
22
  runCdkBootstrap(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
23
23
  runCdkDiff(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
24
24
  runCdkDeploy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider, credentials?: CdkOptions["credentials"], parameters?: Record<string, string>): Promise<Result<StepOutput, string>>;
@@ -1,3 +1,3 @@
1
- import{existsSync as S}from"fs";import{join as A}from"path";import{logger as b}from"@fjall/util/logger";import{success as C,failure as a}from"@fjall/generator";import{DEFAULT_REGION as y}from"../../aws/utils/regions.js";import{getErrorMessage as g,maskSensitiveOutput as m}from"@fjall/util";import{CdkEventMonitor as I,startStackMonitoring as R}from"./CdkEventMonitoring.js";import{analyseDeployOutput as E,analyseDestroyResult as F,createEnhancedOutputCallback as w}from"./CdkOutputAnalyser.js";import{CdkCommandRunner as O}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as K}from"./CdkArgumentBuilder.js";import{CdkProcessManager as N}from"./CdkProcessManager.js";import{wrapWithConstructMapEnrichment as T}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as L,resolveStackName as D,getFallbackStackName as P,buildDeploymentCdkContext as k}from"./cdkServiceHelpers.js";class Y{commandRunner;eventMonitor;constructor(e){const t=e?.processManager??new N(new K);this.commandRunner=new O(t),this.eventMonitor=new I({eventLogWriterFactory:e?.eventLogWriterFactory})}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){const r=e.callerIdentity?.Account;try{const n=await this.synth(e.path,{outputCallback:t,context:k(e,r,e.region||y)});return n.success?C({message:"CloudFormation template synthesised",details:n.data.output?{synthesisTime:n.data.output}:void 0}):a(n.error||"Failed to synthesise CloudFormation template")}catch(n){return a(`CDK synth failed: ${m(g(n))}`)}}async runCdkBootstrap(e,t,r){const n=e.callerIdentity?.Account,d=e.region||y;try{if(!n)return a("No AWS account ID available");const l=A(e.path,"node_modules");if(!S(l))return a(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const c=await this.bootstrap(n,d,{outputCallback:t,credentials:r});return c.success?C({message:"AWS environment bootstrapped"}):a(c.error||"Failed to bootstrap AWS environment")}catch(l){return a(`CDK bootstrap failed: ${m(g(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:k(e,r,e.region||y)});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: ${m(g(n))}`)}}async runCdkDeploy(e,t,r,n,d,l,c){const p=e.callerIdentity?.Account,f=e.region||y;if(!p)return a("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!d)return a("AwsProvider is required for deployment monitoring.");const u=T(e.path,n);let o=null,h;try{const s=D(t,e)??P(e);o=await this.eventMonitor.createEventMonitor("deploy",s,f,e,d),r&&(r(m(`Starting CloudFormation deployment of ${s}...
2
- `)),r(`Monitoring CloudFormation events (CDK process running in background)...
3
- `));const i={cdkOutput:"",actualStackName:s,stackDetected:!1,monitoringPromise:null},v=w(i,r,o,u);h=setTimeout(()=>{!i.stackDetected&&o&&!i.monitoringPromise&&(b.debug("CdkService","Fallback monitoring STARTING",{targetStackName:s,stackDetected:i.stackDetected,hasOnResourceProgress:!!n}),i.monitoringPromise=R(o,s,u))},L);const M=await this.deploy(e.path,s,{verbose:e.options?.verbose,outputCallback:v,useCdkOut:!0,cdkOutputLogger:o?.getEventLogger()??void 0,context:k(e,p,f),credentials:l,...c!==void 0&&Object.keys(c).length>0&&{parameters:c}});return E(i.cdkOutput,M,i.actualStackName)}catch(s){const i=`CDK deploy failed: ${m(g(s))}`;return b.error("CdkService","CDK deployment exception",{error:i}),a(i)}finally{clearTimeout(h),o&&o.stopMonitoring()}}async runCdkDestroy(e,t,r,n,d,l,c){const p=e.callerIdentity?.Account,f=e.region||y;let u=null;try{const o=D(t,e);p&&o&&d&&(u=await this.eventMonitor.createEventMonitor("destroy",o,f,e,d),u.startMonitoring(o,s=>{n?.(s)},(s,i)=>{}));const h=await this.destroy(e.path,t,{verbose:e.options?.verbose,outputCallback:r,useCdkOut:l,cdkOutputLogger:u?.getEventLogger()??void 0,context:k(e,p,f),credentials:c});return F(h)}catch(o){return a(`CDK destroy failed: ${m(g(o))}`)}finally{u&&u.stopMonitoring()}}}export{Y as CdkService};
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}...
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};
@@ -12,6 +12,7 @@ export interface CdkContext {
12
12
  ipamPoolId?: string;
13
13
  fjallOrgId?: string;
14
14
  fjallOidcConfigured?: string;
15
+ fjallAccountGlobalsConfigured?: string;
15
16
  orgConfig?: string;
16
17
  }
17
18
  export interface CdkOptions {
@@ -65,7 +65,13 @@ export declare class CloudFormationService {
65
65
  */
66
66
  deleteStack(stackName: string): Promise<Result<void, CloudFormationError>>;
67
67
  /**
68
- * Check whether a CloudFormation stack exists and is in a deployable state.
68
+ * Check whether a CloudFormation stack exists as a deployable, healthy stack.
69
+ *
70
+ * Returns false for stacks that never reached a successful deployment
71
+ * (REVIEW_IN_PROGRESS and the SAFE_CLEANUP_STATES: ROLLBACK_FAILED /
72
+ * ROLLBACK_COMPLETE / DELETE_FAILED). Both callers are deploy skip-gates --
73
+ * treating a broken stack as "exists" would skip the deploy that is needed
74
+ * to replace it.
69
75
  */
70
76
  stackExists(stackName: string, client?: CloudFormationClient): Promise<boolean>;
71
77
  /**
@@ -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{extractErrorName as T}from"../../aws/organisations/types.js";class l extends x{errorType;stackName;stackStatus;constructor(t,s,o,e,r,n=!1){super(`CFN_${s.toUpperCase()}`,t,r,n),this.errorType=s,this.stackName=o,this.stackStatus=e}}class A{aws;constructor(t){this.aws=t}classifyAwsError(t,s,o){const e=T(t),r=f(p(t));return e==="CredentialsError"||e==="UnauthorizedError"?new l(`AWS credentials error: ${r}`,"auth_error",o,void 0,t,!1):e==="Throttling"||e==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${r}`,"throttled",o,void 0,t,!0):e==="NetworkingError"||e==="ENOTFOUND"?new l(`Network error: ${r}`,"network_error",o,void 0,t,!0):new l(`${s}: ${r}`,"unknown",o,void 0,t)}async getStackOutputs(t,s){s?.onStackCheck?.(t);const o=this.aws.getClient(d),e=new w({StackName:t});try{const n=(await o.send(e)).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(r){if(r instanceof Error&&r.name==="ValidationError"&&r.message?.includes(E))return s?.onStackNotFound?.(t),u([]);const n=f(p(r));return c(new l(`Failed to get outputs for stack ${t}: ${n}`,"unknown",t,void 0,r))}}async getStackStatus(t,s){s?.onStackCheck?.(t);const o=this.aws.getClient(d),e=new w({StackName:t});try{const n=(await o.send(e)).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(r){return r instanceof Error&&r.name==="ValidationError"&&r.message?.includes(E)?u({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 s=this.aws.getClient(d),o=[];try{let e;do{const r=new k({NextToken:e}),n=await s.send(r),a=n.Exports||[];for(const i of a)i.Name&&i.Value&&o.push({Name:i.Name,Value:i.Value});if(t?.(a))break;e=n.NextToken}while(e);return u(o)}catch(e){const r=f(p(e));return c(new l(`Failed to list exports: ${r}`,"unknown",void 0,void 0,e,!1))}}async getExportsByNames(t){if(t.length===0)return u(new Map);const s=new Set(t),o=new Map,e=await this.listAllExports(r=>{for(const n of r)n.Name&&s.has(n.Name)&&n.Value&&o.set(n.Name,n.Value);return o.size>=s.size});return e.success?u(o):c(e.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 r=(await o.send(new w({StackName:t}))).Stacks?.[0]?.StackStatus;return!!r&&r!=="REVIEW_IN_PROGRESS"}catch(e){return e instanceof Error&&e.message?.includes(E)?!1:(y.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:t,error:p(e)}),!0)}}async waitForDeleteComplete(t,s){const o=s?.timeoutMs??6e5,e=s?.pollIntervalMs??5e3,r=Date.now();for(;Date.now()-r<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(e);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(e)}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,A as CloudFormationService};
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 +1 @@
1
- import{getApplicationStackName as i,getOrganisationStackName as d,isApplicationStack as p}from"../../types/operations.js";const c=5e3;function u(o,a){if(o&&!o.includes("*"))return o;if(o){const e=o.match(/\*?(\w+)\*?/);if(e?.[1]){const n=e[1],r=a.target;return p(n)?i(r,n):`${r}${n}`}return o}}function g(o){const a=o.deployType;return a==="organisation"||a==="platform"||a==="account"?d(a):`${o.target}Network`}function l(o,a,e){return{accountId:a,region:e,managedAccount:o.isManagedAccount,accountName:o.accountName,orgId:o.orgId,rootId:o.rootId,managementAccountId:o.managementAccountId,ipamPoolId:o.ipamPoolId,fjallOrgId:o.fjallOrgId,fjallOidcConfigured:o.fjallOidcConfigured?"true":void 0,orgConfig:o.orgConfig}}export{c as STACK_DETECTION_FALLBACK_MS,l as buildDeploymentCdkContext,g as getFallbackStackName,u as resolveStackName};
1
+ import{getApplicationStackName as i,getOrganisationStackName as u,isApplicationStack as d}from"../../types/operations.js";const c=5e3;function t(o,n){if(o&&!o.includes("*"))return o;if(o){const e=o.match(/\*?(\w+)\*?/);if(e?.[1]){const a=e[1],r=n.target;return d(a)?i(r,a):`${r}${a}`}return o}}function p(o){const n=o.deployType;return n==="organisation"||n==="platform"||n==="account"?u(n):`${o.target}Network`}function f(o,n,e){return{accountId:n,region:e,environment:o.environment,managedAccount:o.isManagedAccount,accountName:o.accountName,orgId:o.orgId,rootId:o.rootId,managementAccountId:o.managementAccountId,ipamPoolId:o.ipamPoolId,fjallOrgId:o.fjallOrgId,fjallOidcConfigured:o.fjallOidcConfigured?"true":void 0,fjallAccountGlobalsConfigured:o.fjallAccountGlobalsConfigured?"true":void 0,orgConfig:o.orgConfig}}export{c as STACK_DETECTION_FALLBACK_MS,f as buildDeploymentCdkContext,p as getFallbackStackName,t as resolveStackName};
@@ -4,4 +4,4 @@ import type { ResourceEvent } from "../../aws/utils/cloudformationEvents.js";
4
4
  * and return a wrapped callback that enriches resource events with
5
5
  * group and constructPath fields.
6
6
  */
7
- export declare function wrapWithConstructMapEnrichment(projectPath: string, onResourceProgress?: (event: ResourceEvent) => void): ((event: ResourceEvent) => void) | undefined;
7
+ export declare function wrapWithConstructMapEnrichment(assemblyDir: string, onResourceProgress?: (event: ResourceEvent) => void): ((event: ResourceEvent) => void) | undefined;
@@ -1 +1 @@
1
- import{existsSync as u,readFileSync as s}from"fs";import{join as p}from"path";import{logger as i}from"@fjall/util/logger";import{enrichFromConstructMap as a,recordToConstructMap as f,FJALL_MANIFEST_FILENAME as d}from"@fjall/util/constructMap";import{getErrorMessage as m}from"@fjall/util";function C(n,c){if(!c)return;const t=p(n,"cdk.out",d);let e;try{if(u(t)){const r=JSON.parse(s(t,"utf-8")),o=typeof r=="object"&&r!==null?r.resourceMap:void 0;h(o)&&(e=f(o)),e&&e.size>0&&i.debug("CdkService",`Loaded construct map with ${e.size} entries`)}}catch(r){i.debug("CdkService",`Could not read construct map: ${m(r)}`)}return!e||e.size===0?c:r=>{const o=a(r.logicalId,r.resourceType,e);c({...r,...o.group!==void 0?{group:o.group}:{},...o.constructPath!==void 0?{constructPath:o.constructPath}:{}})}}function h(n){if(typeof n!="object"||n===null||Array.isArray(n))return!1;const c=Object.values(n);if(c.length===0)return!0;const t=c[0];return typeof t=="object"&&t!==null&&"constructPath"in t&&"group"in t&&"resourceType"in t}export{C as wrapWithConstructMapEnrichment};
1
+ import{existsSync as u,readFileSync as s}from"fs";import{join as p}from"path";import{logger as i}from"@fjall/util/logger";import{enrichFromConstructMap as a,recordToConstructMap as f,FJALL_MANIFEST_FILENAME as d}from"@fjall/util/constructMap";import{getErrorMessage as m}from"@fjall/util";function C(o,c){if(!c)return;const t=p(o,d);let e;try{if(u(t)){const r=JSON.parse(s(t,"utf-8")),n=typeof r=="object"&&r!==null?r.resourceMap:void 0;h(n)&&(e=f(n)),e&&e.size>0&&i.debug("CdkService",`Loaded construct map with ${e.size} entries`)}}catch(r){i.debug("CdkService",`Could not read construct map: ${m(r)}`)}return!e||e.size===0?c:r=>{const n=a(r.logicalId,r.resourceType,e);c({...r,...n.group!==void 0?{group:n.group}:{},...n.constructPath!==void 0?{constructPath:n.constructPath}:{}})}}function h(o){if(typeof o!="object"||o===null||Array.isArray(o))return!1;const c=Object.values(o);if(c.length===0)return!0;const t=c[0];return typeof t=="object"&&t!==null&&"constructPath"in t&&"group"in t&&"resourceType"in t}export{C as wrapWithConstructMapEnrichment};
@@ -18,6 +18,8 @@ export declare class CdkContextBuilder {
18
18
  deployType: "application" | "organisation" | "platform" | "account";
19
19
  target: string;
20
20
  path: string;
21
+ assemblyDir?: string;
22
+ environment?: string;
21
23
  callerIdentity?: CallerIdentity;
22
24
  region?: string;
23
25
  stackOutputs?: StackOutputsRecord;
@@ -30,6 +32,7 @@ export declare class CdkContextBuilder {
30
32
  ipamPoolId?: string;
31
33
  fjallOrgId?: string;
32
34
  fjallOidcConfigured?: boolean;
35
+ fjallAccountGlobalsConfigured?: boolean;
33
36
  orgConfig?: string;
34
37
  }, options: {
35
38
  verbose?: boolean;
@@ -1 +1 @@
1
- import{DEFAULT_REGION as t}from"@fjall/generator";class l{static buildDeploymentContext(o,a,e){return{deployType:o.deployType,target:o.target,path:o.path,options:a,stackOutputs:o.stackOutputs||{},callerIdentity:o.callerIdentity,region:o.region||e?.primaryRegion||t,isManagedAccount:o.isManagedAccount,accountName:o.accountName,logPath:o.logPath,orgId:o.orgId,rootId:o.rootId,managementAccountId:o.managementAccountId,ipamPoolId:o.ipamPoolId,fjallOrgId:o.fjallOrgId,fjallOidcConfigured:o.fjallOidcConfigured,orgConfig:o.orgConfig}}static updateContext(o,a){return{...o,...a}}}export{l as CdkContextBuilder};
1
+ import{DEFAULT_REGION as r}from"@fjall/generator";class t{static buildDeploymentContext(e,o,a){return{deployType:e.deployType,target:e.target,path:e.path,...e.assemblyDir!==void 0?{assemblyDir:e.assemblyDir}:{},...e.environment!==void 0?{environment:e.environment}:{},options:o,stackOutputs:e.stackOutputs||{},callerIdentity:e.callerIdentity,region:e.region||a?.primaryRegion||r,isManagedAccount:e.isManagedAccount,accountName:e.accountName,logPath:e.logPath,orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,ipamPoolId:e.ipamPoolId,fjallOrgId:e.fjallOrgId,fjallOidcConfigured:e.fjallOidcConfigured,fjallAccountGlobalsConfigured:e.fjallAccountGlobalsConfigured,orgConfig:e.orgConfig}}static updateContext(e,o){return{...e,...o}}}export{t as CdkContextBuilder};
@@ -26,6 +26,16 @@ export interface TemplateComparisonResult {
26
26
  /** Number of stacks unchanged */
27
27
  unchangedCount: number;
28
28
  }
29
+ /**
30
+ * Result of comparing a single cascade account's synthesised stack against its
31
+ * per-account state file.
32
+ */
33
+ export interface CascadeStackComparison {
34
+ /** True when the stack is new, changed, or its hash could not be read. */
35
+ changed: boolean;
36
+ /** Hash of the current synthesised template; undefined if hashing failed. */
37
+ currentHash: string | undefined;
38
+ }
29
39
  /**
30
40
  * Template Hash Service - provides hash-based change detection
31
41
  */
@@ -48,6 +58,17 @@ export declare class TemplateHashService {
48
58
  * Update state file with new hashes after successful deployment
49
59
  */
50
60
  updateStateAfterDeploy(appPath: string, deployedStacks: Map<string, string>, stackStatuses?: Map<string, string>): Promise<Result<void, TemplateHashError>>;
61
+ /**
62
+ * Compare a single cascade account's synthesised stack against its
63
+ * per-account state file. Returns `changed: true` on ANY read/hash failure so
64
+ * an uncertain cascade always redeploys (never silently skips a needed deploy).
65
+ */
66
+ compareCascadeStack(assemblyDir: string, stackName: string, stateFilePath: string): Promise<CascadeStackComparison>;
67
+ /**
68
+ * Persist a single cascade account's stack hash after a successful deploy.
69
+ * Writes to the per-account state file so the next same-source deploy can skip.
70
+ */
71
+ persistCascadeStack(stateFilePath: string, stackName: string, hash: string, stackStatus?: string): Promise<Result<void, TemplateHashError>>;
51
72
  /**
52
73
  * Get state file path for an application (exposed for testing/logging)
53
74
  */
@@ -1 +1 @@
1
- import{createHash as y}from"crypto";import{readFile as w,readdir as S}from"fs/promises";import{join as C,basename as H}from"path";import{fileExists as T}from"@fjall/util/fsHelpers";import{success as l,failure as c}from"@fjall/generator";import{BaseServiceError as E}from"../../types/errors/ServiceError.js";import{readStateFile as p,writeStateFile as _,createEmptyState as F,updateTemplateHash as N,getStateFilePath as u}from"../../types/FjallState.js";class f extends E{errorType;constructor(e,t,r,a=!1){super(`TEMPLATE_HASH_${t.toUpperCase()}`,e,r,a),this.errorType=t}}const d=".template.json";class b{async computeTemplateHash(e){try{const t=await w(e,"utf-8"),r=JSON.stringify(JSON.parse(t)),a=y("sha256").update(r).digest("hex");return l(a)}catch(t){return c(new f(`Failed to hash template: ${e}`,"hash_failed",{path:e,error:t}))}}async getTemplateHashes(e){if(!await T(e))return c(new f(`CDK output directory not found: ${e}`,"cdk_out_not_found",{path:e}));try{const r=(await S(e)).filter(s=>s.endsWith(d)),a=new Map;for(const s of r){const n=H(s,d),o=C(e,s),i=await this.computeTemplateHash(o);if(!i.success)return c(i.error);a.set(n,i.data)}return l(a)}catch(t){return c(new f(`Failed to read CDK output directory: ${e}`,"read_failed",{path:e,error:t}))}}async compareWithState(e,t){const r=await p(t),a=new Map;let s=0,n=0;for(const[o,i]of e){const h=r?.templateHashes[o],m=!h||h.hash!==i;a.set(o,m),m?s++:n++}if(r?.templateHashes)for(const o of Object.keys(r.templateHashes))e.has(o)||(a.set(o,!0),s++);return l({stackChanges:a,currentHashes:e,changedCount:s,unchangedCount:n})}async updateStateAfterDeploy(e,t,r){try{let a=await p(e)??F();for(const[s,n]of t){const o=r?.get(s);a=N(a,s,n,o)}return await _(e,a),l(void 0)}catch(a){return c(new f(`Failed to write state file: ${u(e)}`,"state_write_failed",{appPath:e,error:a}))}}getStateFilePath(e){return u(e)}stackHasChanges(e,t){return e.stackChanges.get(t)??!0}getChangedStacks(e){return Array.from(e.stackChanges.entries()).filter(([,t])=>t).map(([t])=>t)}getUnchangedStacks(e){return Array.from(e.stackChanges.entries()).filter(([,t])=>!t).map(([t])=>t)}}export{f as TemplateHashError,b as TemplateHashService};
1
+ import{createHash as C}from"crypto";import{readFile as T,readdir as _}from"fs/promises";import{join as m,basename as E}from"path";import{fileExists as F}from"@fjall/util/fsHelpers";import{success as h,failure as i}from"@fjall/generator";import{BaseServiceError as k}from"../../types/errors/ServiceError.js";import{readStateFile as p,writeStateFile as A,readStateFileAt as w,writeStateFileAt as $,createEmptyState as g,updateTemplateHash as y,getStateFilePath as S}from"../../types/FjallState.js";class l extends k{errorType;constructor(e,t,r,a=!1){super(`TEMPLATE_HASH_${t.toUpperCase()}`,e,r,a),this.errorType=t}}const u=".template.json";class J{async computeTemplateHash(e){try{const t=await T(e,"utf-8"),r=JSON.stringify(JSON.parse(t)),a=C("sha256").update(r).digest("hex");return h(a)}catch(t){return i(new l(`Failed to hash template: ${e}`,"hash_failed",{path:e,error:t}))}}async getTemplateHashes(e){if(!await F(e))return i(new l(`CDK output directory not found: ${e}`,"cdk_out_not_found",{path:e}));try{const r=(await _(e)).filter(s=>s.endsWith(u)),a=new Map;for(const s of r){const n=E(s,u),o=m(e,s),c=await this.computeTemplateHash(o);if(!c.success)return i(c.error);a.set(n,c.data)}return h(a)}catch(t){return i(new l(`Failed to read CDK output directory: ${e}`,"read_failed",{path:e,error:t}))}}async compareWithState(e,t){const r=await p(t),a=new Map;let s=0,n=0;for(const[o,c]of e){const f=r?.templateHashes[o],d=!f||f.hash!==c;a.set(o,d),d?s++:n++}if(r?.templateHashes)for(const o of Object.keys(r.templateHashes))e.has(o)||(a.set(o,!0),s++);return h({stackChanges:a,currentHashes:e,changedCount:s,unchangedCount:n})}async updateStateAfterDeploy(e,t,r){try{let a=await p(e)??g();for(const[s,n]of t){const o=r?.get(s);a=y(a,s,n,o)}return await A(e,a),h(void 0)}catch(a){return i(new l(`Failed to write state file: ${S(e)}`,"state_write_failed",{appPath:e,error:a}))}}async compareCascadeStack(e,t,r){const a=m(e,`${t}${u}`),s=await this.computeTemplateHash(a);if(!s.success)return{changed:!0,currentHash:void 0};const n=s.data,c=(await w(r))?.templateHashes[t];return{changed:!c||c.hash!==n,currentHash:n}}async persistCascadeStack(e,t,r,a){try{const s=await w(e)??g(),n=y(s,t,r,a);return await $(e,n),h(void 0)}catch(s){return i(new l(`Failed to write cascade state file: ${e}`,"state_write_failed",{stateFilePath:e,error:s}))}}getStateFilePath(e){return S(e)}stackHasChanges(e,t){return e.stackChanges.get(t)??!0}getChangedStacks(e){return Array.from(e.stackChanges.entries()).filter(([,t])=>t).map(([t])=>t)}getUnchangedStacks(e){return Array.from(e.stackChanges.entries()).filter(([,t])=>!t).map(([t])=>t)}}export{l as TemplateHashError,J as TemplateHashService};
@@ -24,6 +24,27 @@ export type FjallStateFile = z.infer<typeof FjallStateFileSchema>;
24
24
  * Get the state file path for an application
25
25
  */
26
26
  export declare function getStateFilePath(appPath: string): string;
27
+ /**
28
+ * Get the per-account state file path for a cascade target.
29
+ *
30
+ * Cascade member/platform accounts all synthesise the SAME stack name into a
31
+ * shared app dir (fjall/account, fjall/platform). A single shared state file
32
+ * would (a) collide on that stack name across accounts and (b) lose updates
33
+ * under the parallel member deploys. Keying the file by accountId+region keeps
34
+ * each account's hashes isolated and race-free (each account writes its own
35
+ * file).
36
+ */
37
+ export declare function getCascadeStateFilePath(appPath: string, accountId: string, region: string): string;
38
+ /**
39
+ * Read state file from an explicit state-file path.
40
+ * Returns null if file doesn't exist or is corrupt (graceful degradation).
41
+ */
42
+ export declare function readStateFileAt(statePath: string): Promise<FjallStateFile | null>;
43
+ /**
44
+ * Write a state file atomically (temp file + rename) to an explicit path.
45
+ * Prevents partial writes from corrupting the state file.
46
+ */
47
+ export declare function writeStateFileAt(statePath: string, state: FjallStateFile): Promise<void>;
27
48
  /**
28
49
  * Read state file from application directory.
29
50
  * Returns null if file doesn't exist or is corrupt (graceful degradation).
@@ -1 +1 @@
1
- import{z as e}from"zod";import{randomBytes as u}from"crypto";import{readFile as d,writeFile as m,unlink as c,rename as f,mkdir as h}from"fs/promises";import{dirname as S,join as g}from"path";import{fileExists as y}from"@fjall/util/fsHelpers";import{logger as i}from"@fjall/util/logger";import{getErrorMessage as s,maskSensitiveOutput as w}from"@fjall/util";const F=e.object({hash:e.string(),deployedAt:e.string(),stackStatus:e.string().optional()}).strict(),j=e.object({version:e.literal(1),lastDeployedAt:e.string().optional(),templateHashes:e.record(e.string(),F),metadata:e.record(e.string(),e.unknown()).optional()}).strict(),E=".fjall-state.json";function l(r){return g(r,E)}async function D(r){const a=l(r);if(!await y(a))return null;try{const t=await d(a,"utf-8"),n=JSON.parse(t),o=j.safeParse(n);return o.success?o.data:null}catch(t){return i.debug("FjallState","Failed to read state file",{path:a,error:s(t)}),null}}async function N(r,a){const t=l(r),n=`${t}.${Date.now()}-${u(4).toString("hex")}.tmp`;await h(S(t),{recursive:!0});try{await m(n,JSON.stringify(a,null,2),"utf-8"),await f(n,t)}catch(o){try{await c(n)}catch(p){i.debug("FjallState","Temp file cleanup failed (non-fatal)",{path:n,error:s(p)})}throw o}}function k(){return{version:1,templateHashes:{}}}async function v(r){const a=l(r);try{await c(a)}catch(t){(typeof t=="object"&&t!==null&&"code"in t?t.code:void 0)!=="ENOENT"&&i.warn("FjallState","Failed to delete state file",{path:a,error:w(s(t))})}}function I(r,a,t,n){return{...r,lastDeployedAt:new Date().toISOString(),templateHashes:{...r.templateHashes,[a]:{hash:t,deployedAt:new Date().toISOString(),...n!==void 0&&{stackStatus:n}}}}}export{j as FjallStateFileSchema,k as createEmptyState,v as deleteStateFile,l as getStateFilePath,D as readStateFile,I as updateTemplateHash,N as writeStateFile};
1
+ import{z as a}from"zod";import{randomBytes as u}from"crypto";import{readFile as d,writeFile as f,unlink as s,rename as m,mkdir as S}from"fs/promises";import{dirname as g,join as c}from"path";import{fileExists as h}from"@fjall/util/fsHelpers";import{logger as o}from"@fjall/util/logger";import{getErrorMessage as i,maskSensitiveOutput as y}from"@fjall/util";const F=a.object({hash:a.string(),deployedAt:a.string(),stackStatus:a.string().optional()}).strict(),w=a.object({version:a.literal(1),lastDeployedAt:a.string().optional(),templateHashes:a.record(a.string(),F),metadata:a.record(a.string(),a.unknown()).optional()}).strict(),x=".fjall-state.json";function l(t){return c(t,x)}function $(t,r,e){const n=e.replace(/-/g,"");return c(t,`.fjall-state.cascade-${r}-${n}.json`)}async function j(t){if(!await h(t))return null;try{const r=await d(t,"utf-8"),e=JSON.parse(r),n=w.safeParse(e);return n.success?n.data:null}catch(r){return o.debug("FjallState","Failed to read state file",{path:t,error:i(r)}),null}}async function E(t,r){const e=`${t}.${Date.now()}-${u(4).toString("hex")}.tmp`;await S(g(t),{recursive:!0});try{await f(e,JSON.stringify(r,null,2),"utf-8"),await m(e,t)}catch(n){try{await s(e)}catch(p){o.debug("FjallState","Temp file cleanup failed (non-fatal)",{path:e,error:i(p)})}throw n}}async function k(t){return j(l(t))}async function v(t,r){return E(l(t),r)}function I(){return{version:1,templateHashes:{}}}async function J(t){const r=l(t);try{await s(r)}catch(e){(typeof e=="object"&&e!==null&&"code"in e?e.code:void 0)!=="ENOENT"&&o.warn("FjallState","Failed to delete state file",{path:r,error:y(i(e))})}}function M(t,r,e,n){return{...t,lastDeployedAt:new Date().toISOString(),templateHashes:{...t.templateHashes,[r]:{hash:e,deployedAt:new Date().toISOString(),...n!==void 0&&{stackStatus:n}}}}}export{w as FjallStateFileSchema,I as createEmptyState,J as deleteStateFile,$ as getCascadeStateFilePath,l as getStateFilePath,k as readStateFile,j as readStateFileAt,M as updateTemplateHash,v as writeStateFile,E as writeStateFileAt};
@@ -1,5 +1,6 @@
1
1
  import type { ProgressEvent, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent } from "./events.js";
2
2
  import type { DetectionResult } from "./detection.js";
3
+ import type { CascadeLedger } from "../orchestration/cascadeSummary.js";
3
4
  export type StepCompleteStatus = "completed" | "skipped" | "error";
4
5
  /**
5
6
  * Unified callback interface for deployment progress.
@@ -100,6 +101,19 @@ export interface DeployCallbacks {
100
101
  onCdkOutput?: (output: string, type: "synth" | "diff") => void;
101
102
  /** @emittedBy engine */
102
103
  onCascadeStart?: () => void;
104
+ /**
105
+ * @emittedBy engine — fired once after account reconciliation, before the
106
+ * cascade begins, carrying the authoritative presence of platform/member
107
+ * accounts. Consumers that pre-declare a fixed step list (the CLI Ink UI)
108
+ * use it to reveal the "Deploying platform" / "Deploying accounts" pills,
109
+ * which are hidden at mount until the engine has reconciled the deployable
110
+ * account set. Flags already account for `cascade: false` (both report
111
+ * false when the cascade is disabled).
112
+ */
113
+ onCascadeAccountsReconciled?: (info: {
114
+ hasPlatformAccount: boolean;
115
+ hasMemberAccounts: boolean;
116
+ }) => void;
103
117
  /**
104
118
  * @emittedBy engine — fired once after account reconciliation when accounts
105
119
  * are declared in ACCOUNTS but not yet present in AWS Organizations (the
@@ -118,10 +132,21 @@ export interface DeployCallbacks {
118
132
  onCascadeAccountPhaseChange?: (operationKey: string, phase: "bootstrap" | "synth" | "deploy" | "destroy", region?: string) => void;
119
133
  /** @emittedBy engine — per-account CFN resource events during cascade. */
120
134
  onCascadeAccountResourceProgress?: (operationKey: string, event: ResourceEvent, region?: string) => void;
121
- /** @emittedBy engine */
122
- onCascadeAccountComplete?: (operationKey: string, success: boolean, error?: string, region?: string, outputs?: Record<string, string>) => void;
123
- /** @emittedBy engine */
135
+ /**
136
+ * @emittedBy engine `skipped` is true when the account was reconciled to its
137
+ * desired state without a deploy (synth byte-identical, stack present). The
138
+ * cascade summary distinguishes skipped from freshly-deployed accounts, so a
139
+ * skipped account is still `success: true` but must not inflate the deployed count.
140
+ */
141
+ onCascadeAccountComplete?: (operationKey: string, success: boolean, error?: string, region?: string, outputs?: Record<string, string>, skipped?: boolean) => void;
142
+ /** @emittedBy engine — scalar cascade summary (the webapp/worker wire shape). */
124
143
  onCascadeComplete?: (result: CascadeDeploymentResult) => void;
144
+ /**
145
+ * @emittedBy engine — the full per-(account, region) outcome ledger, emitted
146
+ * alongside `onCascadeComplete`. The CLI projects both rich rows and the scalar
147
+ * summary from it; the webapp/worker ignores it and reads the scalar above.
148
+ */
149
+ onCascadeLedger?: (ledger: CascadeLedger) => void;
125
150
  /** @emittedBy engine — progress during failed-state stack cleanup (S3 emptying, deletion). */
126
151
  onStackCleanupProgress?: (stackName: string, phase: "emptying-bucket" | "deleting-stack" | "waiting" | "complete" | "error") => void;
127
152
  /**
@@ -6,3 +6,13 @@ export declare const STACK_FAILED_STATE_PATTERN = "is in a failed state";
6
6
  export declare const CDK_NO_STACKS_MATCH = "No stacks match the name(s)";
7
7
  /** The canonical name for generated infrastructure files */
8
8
  export declare const INFRASTRUCTURE_FILENAME = "infrastructure.ts";
9
+ /**
10
+ * CloudFormation stack states that never reached a successful deployment.
11
+ * Safe to auto-delete, and must never be treated as a healthy stack a deploy
12
+ * can skip (ROLLBACK_FAILED / ROLLBACK_COMPLETE = create failed; DELETE_FAILED
13
+ * = delete already started). Deliberately excludes UPDATE_ROLLBACK_* — those
14
+ * retain live resources from a prior successful deploy.
15
+ */
16
+ export declare const SAFE_CLEANUP_STATES: ReadonlySet<string>;
17
+ /** Check whether a stack status is a never-deployed state safe for automatic cleanup. */
18
+ export declare function isCleanableState(status: string): boolean;
@@ -1 +1 @@
1
- const t="does not exist",s="is in a failed state",T="No stacks match the name(s)",e="infrastructure.ts";export{T as CDK_NO_STACKS_MATCH,e as INFRASTRUCTURE_FILENAME,s as STACK_FAILED_STATE_PATTERN,t as STACK_NOT_FOUND_PATTERN};
1
+ const A="does not exist",E="is in a failed state",T="No stacks match the name(s)",s="infrastructure.ts",e=new Set(["ROLLBACK_FAILED","ROLLBACK_COMPLETE","DELETE_FAILED"]);function o(t){return e.has(t)}export{T as CDK_NO_STACKS_MATCH,s as INFRASTRUCTURE_FILENAME,e as SAFE_CLEANUP_STATES,E as STACK_FAILED_STATE_PATTERN,A as STACK_NOT_FOUND_PATTERN,o as isCleanableState};
@@ -4,6 +4,14 @@ export interface DeploymentContext {
4
4
  deployType: "application" | "organisation" | "platform" | "account";
5
5
  target: string;
6
6
  accountName?: string;
7
+ /**
8
+ * CDK app environment selector (`root` | `platform` | <member env>). Passed
9
+ * to the synthesised app as `--context environment=…`, the highest-priority
10
+ * source in `getConfig()`. Set per cascade account so each account synths
11
+ * the correct `infrastructure.ts` branch deterministically, without relying
12
+ * on accountName→providerAccount resolution.
13
+ */
14
+ environment?: string;
7
15
  region?: string;
8
16
  callerIdentity?: CallerIdentity;
9
17
  stackOutputs?: StackOutputsRecord;
@@ -14,12 +22,21 @@ export interface DeploymentContext {
14
22
  ipamPoolId?: string;
15
23
  fjallOrgId?: string;
16
24
  fjallOidcConfigured?: boolean;
25
+ fjallAccountGlobalsConfigured?: boolean;
17
26
  orgConfig?: string;
18
27
  options: {
19
28
  verbose?: boolean;
20
29
  infraOnly?: boolean;
21
30
  };
22
31
  path: string;
32
+ /**
33
+ * Isolated cloud-assembly directory for this deployment. When set, synth
34
+ * writes the assembly here (`--output`) and deploy reads it (`--app`),
35
+ * instead of the shared `<path>/cdk.out`. The cascade gives every account
36
+ * its own `cdk.out.<accountId>.<region>` so parallel member synth+deploy
37
+ * cannot clobber one another's templates.
38
+ */
39
+ assemblyDir?: string;
23
40
  logPath?: string;
24
41
  }
25
42
  export interface StepOutput {
@@ -23,8 +23,11 @@ export interface AwsAuthResult {
23
23
  }
24
24
  export interface CascadeDeploymentResult {
25
25
  platformDeployed: boolean;
26
+ platformSkipped: boolean;
27
+ platformFailed: boolean;
26
28
  domainsDeployed: boolean;
27
29
  accountsDeployed: number;
30
+ accountsSkipped: number;
28
31
  accountsFailed: number;
29
32
  errors: Array<{
30
33
  accountId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/deploy-core",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Shared deployment engine for Fjall — used by CLI and webapp worker",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -73,8 +73,8 @@
73
73
  "@aws-sdk/client-s3": "^3.1038.0",
74
74
  "@aws-sdk/client-sso-admin": "^3.1038.0",
75
75
  "@aws-sdk/client-sts": "^3.1038.0",
76
- "@fjall/generator": "^2.5.0",
77
- "@fjall/util": "^2.5.0",
76
+ "@fjall/generator": "^2.7.0",
77
+ "@fjall/util": "^2.7.0",
78
78
  "@smithy/node-http-handler": "^4.6.1",
79
79
  "tsx": "^4.21.0",
80
80
  "zod": "^4.4.3"
@@ -83,5 +83,5 @@
83
83
  "@types/node": "^25.6.0",
84
84
  "vitest": "^4.1.5"
85
85
  },
86
- "gitHead": "5c8f0e004f5520c692f2ee2063c3558c2451f2cf"
86
+ "gitHead": "cfcfbb9f546974d62756e257fce012f629db79ce"
87
87
  }