@fjall/deploy-core 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.minified +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +1 -1
- package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -1
- package/dist/src/orchestration/cascadeHelpers.d.ts +29 -0
- package/dist/src/orchestration/cascadeHelpers.js +1 -1
- package/dist/src/orchestration/cascadeSummary.d.ts +87 -0
- package/dist/src/orchestration/cascadeSummary.js +1 -0
- package/dist/src/orchestration/contextHelpers.d.ts +2 -0
- package/dist/src/orchestration/contextHelpers.js +1 -1
- package/dist/src/orchestration/index.d.ts +4 -1
- package/dist/src/orchestration/index.js +1 -1
- package/dist/src/orchestration/organisationDeploy.js +5 -5
- package/dist/src/orchestration/organisationDestroy.js +3 -3
- package/dist/src/orchestration/stackCleanup.d.ts +2 -5
- package/dist/src/orchestration/stackCleanup.js +1 -1
- package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
- package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +1 -0
- package/dist/src/services/infrastructure/CloudFormationService.d.ts +7 -1
- package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
- package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
- package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -0
- package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
- package/dist/src/services/supporting/TemplateHashService.d.ts +21 -0
- package/dist/src/services/supporting/TemplateHashService.js +1 -1
- package/dist/src/types/FjallState.d.ts +21 -0
- package/dist/src/types/FjallState.js +1 -1
- package/dist/src/types/callbacks.d.ts +28 -3
- package/dist/src/types/constants.d.ts +10 -0
- package/dist/src/types/constants.js +1 -1
- package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
- package/dist/src/types/events.d.ts +3 -0
- package/package.json +4 -4
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
119 files minified at 2026-06-01T20:33:48.440Z
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
|
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)}
|
|
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};
|
|
@@ -22,6 +22,22 @@ export declare function partitionAccounts(providerAccounts: ProviderAccount[]):
|
|
|
22
22
|
platformAccount: ProviderAccount | undefined;
|
|
23
23
|
memberAccounts: ProviderAccount[];
|
|
24
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[];
|
|
25
41
|
export { buildCascadeRoleArn } from "./contextHelpers.js";
|
|
26
42
|
/**
|
|
27
43
|
* Deploy a single cascade account (platform or member).
|
|
@@ -29,6 +45,11 @@ export { buildCascadeRoleArn } from "./contextHelpers.js";
|
|
|
29
45
|
*/
|
|
30
46
|
export interface CascadeAccountResult {
|
|
31
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;
|
|
32
53
|
}
|
|
33
54
|
export interface DeployCascadeAccountOptions {
|
|
34
55
|
ipamPoolId?: string;
|
|
@@ -38,6 +59,14 @@ export interface DeployCascadeAccountOptions {
|
|
|
38
59
|
* synthesised app's `getConfig()` can resolve every account by name/id.
|
|
39
60
|
*/
|
|
40
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;
|
|
41
70
|
}
|
|
42
71
|
export declare function deployCascadeAccount(params: DeployParams, services: DeployServices, operation: OrganisationOperation, account: ProviderAccount, deployType: "platform" | "account", callbacks: DeployCallbacks, options?: DeployCascadeAccountOptions): Promise<Result<CascadeAccountResult>>;
|
|
43
72
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{join as
|
|
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 f,failure as u}from"@fjall/generator";import{getErrorMessage as y,maskSensitiveOutput as d,sleep as
|
|
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
|
|
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,5 +1,5 @@
|
|
|
1
|
-
import{success as
|
|
2
|
-
`);t.onLog?.(
|
|
3
|
-
${
|
|
4
|
-
`),
|
|
5
|
-
${s}`);return t.onError?.(
|
|
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
|
|
2
|
-
`),
|
|
3
|
-
${a}`);
|
|
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
|
-
|
|
16
|
-
export
|
|
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
|
|
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};
|
|
@@ -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
|
|
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{
|
|
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
|
|
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};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DEFAULT_REGION as r}from"@fjall/generator";class
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
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};
|
|
@@ -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.
|
|
3
|
+
"version": "2.7.1",
|
|
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.
|
|
77
|
-
"@fjall/util": "^2.
|
|
76
|
+
"@fjall/generator": "^2.7.1",
|
|
77
|
+
"@fjall/util": "^2.7.1",
|
|
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": "
|
|
86
|
+
"gitHead": "2b37679546b7695b1678148e0b8e1f349afac3d9"
|
|
87
87
|
}
|