@fjall/deploy-core 3.9.0 → 3.10.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.
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 233 files minified at 2026-07-20T22:26:35.124Z
1
+ 234 files minified at 2026-07-22T07:34:15.296Z
@@ -7,12 +7,25 @@ import type { DelegatedDomainDeployer } from "./domain/delegatedDomainDeploy.js"
7
7
  export interface DomainConfig {
8
8
  name: string;
9
9
  type: "apex" | "delegated";
10
+ /**
11
+ * Self-recorded home: the 12-digit AWS account id (and region) the last
12
+ * successful deploy of this domain ran in, from the provider's config
13
+ * entry. The cascade runs under ONE ambient identity, so `deployDomains`
14
+ * SKIPS any domain whose pin contradicts the executing identity rather
15
+ * than deploying it into the wrong account (the development.fjall.io
16
+ * incident). Absent on never-deployed / pre-3.9.0 entries: fail-open.
17
+ */
18
+ account?: string;
19
+ region?: string;
10
20
  }
11
21
  /**
12
22
  * Result of a domain deployment phase.
13
23
  */
14
24
  export interface DomainDeployResult {
15
25
  domainsDeployed: number;
26
+ /** Domains left untouched because their account/region pin names another
27
+ * home. A skip is expected topology, never an entry in `errors`. */
28
+ domainsSkipped: number;
16
29
  errors: string[];
17
30
  }
18
31
  /**
@@ -137,6 +137,16 @@ export interface DeployDomainsOptions {
137
137
  * `deployDelegatedDomain`, where the skip is loud, never silent.
138
138
  */
139
139
  skipCaaPreflight?: boolean;
140
+ /**
141
+ * The ambient identity this cascade executes under. When present, any
142
+ * domain whose self-recorded `account`/`region` pin (DomainConfig) names a
143
+ * different home is SKIPPED with a warning instead of deployed — the
144
+ * account-blind domains phase is how development.fjall.io was deployed
145
+ * into production. Absent (legacy callers): no partition, today's
146
+ * behaviour.
147
+ */
148
+ executingAccountId?: string;
149
+ executingRegion?: string;
140
150
  }
141
151
  /**
142
152
  * Deploy configured domains: apex domains sequentially, then delegated
@@ -144,5 +154,6 @@ export interface DeployDomainsOptions {
144
154
  */
145
155
  export declare function deployDomains(provider: DomainDeployProvider, callbacks: DeployCallbacks, options?: DeployDomainsOptions): Promise<{
146
156
  domainsDeployed: number;
157
+ domainsSkipped: number;
147
158
  errors: string[];
148
159
  }>;
@@ -1 +1 @@
1
- var z=Object.defineProperty;var g=(e,n)=>z(e,"name",{value:n,configurable:!0});import{join as T}from"path";import{success as j,failure as h}from"@fjall/generator";import{logger as R}from"@fjall/util/logger";import{maskSensitiveOutput as C,mapSettledWithConcurrency as Y}from"@fjall/util";import{ORGANISATION_TYPES as P,getOrganisationStackName as H}from"../../types/operations.js";import{CdkContextBuilder as W}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as X}from"../../types/deployment/index.js";import{CloudFormationService as M}from"../../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as q,regionSuffix as J}from"../../types/config/FjallState.js";import{BackupClient as Q}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as Z,describeBackupVaultExists as ee}from"../../aws/organisations/backup.js";import{buildParamsContext as te,collectStackOutputs as V,assumeCascadeRole as x,forwardOutput as E,reconcileOidcProviderIfCarried as G}from"../contextHelpers.js";import{assertLeaseBeforeStack as oe}from"../application/leaseGate.js";import{deployDelegatedDomain as re}from"../domain/delegatedDomainDeploy.js";import{runDriftPreFlightGate as ne}from"../drift/preFlightGate.js";import{accountTier as U}from"@fjall/util";import{buildBootstrapTagsIfResolved as ae}from"../bootstrapTags.js";import{hasOrganisationTierAccount as se}from"../../aws/organisations/accountGlobals.js";import{DEFAULT_REGION as _}from"../../aws/utils/regions.js";const ie=4;function xe(e){const n=e.find(t=>U(t)==="platform"),d=e.filter(t=>U(t)==="account");return{platformAccount:n,memberAccounts:d}}g(xe,"partitionAccounts");function ce(e){const n=e?.primaryRegion??_,d=e?.secondaryRegions??[],t=new Set([n,...d]),i=e?.disasterRecoveryRegion;return i&&t.add(i),[...t]}g(ce,"buildRegionList");function Ee(e){return ce(e)[0]??_}g(Ee,"cascadeHomeRegion");function de(e,n){return`${e} (${n})`}g(de,"cascadeOperationKey");function Fe(e,n){const d=[];for(const t of n)for(const i of e)d.push({account:i,region:t});return d}g(Fe,"buildAccountRegionPairs");import{buildCascadeRoleArn as He}from"../contextHelpers.js";async function Ne(e,n,d,t){d.onLog?.(`Verifying cascade role access for ${n.length} account(s)\u2026`,"info");const i=await Y(n,ie,async s=>x(e.awsProvider,s.id,s.region??e.awsProvider.getRegion(),`fjall-preflight-${s.name}`,t)),o=[];return i.forEach((s,a)=>{const r=n[a];if(r){if(s.status==="rejected"){o.push({accountId:r.id,accountName:r.name,error:s.reason instanceof Error?s.reason.message:String(s.reason)});return}s.value.success||o.push({accountId:r.id,accountName:r.name,error:s.value.error.message})}}),o.length===0&&d.onLog?.(`Cascade role access verified for ${n.length} account(s)`,"info"),o}g(Ne,"probeCascadeRoles");async function be(e,n,d,t,i,o,s){const a=s?.region??t.region??n.awsProvider.getRegion(),r=de(t.name,a),c=s?.orgConfig??e.orgConfig,u=s?.ipamPoolId;o.onCascadeAccountStart?.(r,t.id,a,i);const f=await x(n.awsProvider,t.id,a,`fjall-cascade-${t.name}`,e.abortSignal);if(!f.success)return o.onCascadeAccountComplete?.(r,!1,C(f.error.message),a),h(new Error(`Failed to assume role for ${t.name}: ${C(f.error.message)}`));const{provider:l,credentials:y}=f.data,A=T(e.workingDirectory,"fjall",i==="platform"?P.PLATFORM:P.ACCOUNT),$=T(A,`cdk.out.${t.id}.${J(a)}`),S=W.buildDeploymentContext({deployType:i,target:d.target,path:A,assemblyDir:$,environment:t.environment??void 0,region:a,accountName:t.name,callerIdentity:X(t.id),ipamPoolId:u,...te({orgConfig:c,identity:e.identity,skipOidc:e.options?.skipOidc,region:a,primaryRegion:s?.primaryRegion,trailLifecycle:t.trailLifecycle})},{verbose:e.options?.verbose},c);if(Z(t.environment,c?.disasterRecoveryRegion)){const p=await ee(l.getClient(Q),e.abortSignal);if(!p.success)return o.onCascadeAccountComplete?.(r,!1,C(p.error.message),a),h(new Error(`Backup vault probe failed for ${t.name}: ${C(p.error.message)}`));S.fjallAdoptBackupVault=p.data}o.onCascadeAccountPhaseChange?.(r,"synth",a);const O=await n.cdkService.runCdkSynth(S,E(o),y);if(!O.success)return o.onCascadeAccountComplete?.(r,!1,C(`Synth failed: ${O.error}`),a),h(new Error(`Synth failed for ${t.name}: ${C(O.error)}`));const m=H(i==="platform"?P.PLATFORM:P.ACCOUNT),F=q(A,t.id,a),w=new M(l),{changed:K,currentHash:N}=await n.hashService.compareCascadeStack($,m,F);if(!K&&e.options?.force!==!0&&await w.stackExists(m,void 0,e.abortSignal)){const p=await w.getStackOutputs(m,void 0,e.abortSignal);p.success||R.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:m,account:t.name});const B=V(p);return await G(S,l,o,e.abortSignal),o.onLog?.(`${t.name}: no infrastructure changes \u2014 skipping deploy`,"info"),o.onCascadeAccountComplete?.(r,!0,void 0,a,B,!0),j({outputs:B,skipped:!0})}const v=await ne(l,{stackNames:[m],cdkOutPath:$,cfnService:w,callbacks:o,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(v.kind==="blocked")return o.onCascadeAccountComplete?.(r,!1,v.message,a),h(new Error(v.message));const D=await oe(e.assertLeaseForStack,m,e.abortSignal);if(D!==void 0)return o.onCascadeAccountComplete?.(r,!1,D.message,a),h(D);o.onCascadeAccountPhaseChange?.(r,"bootstrap",a);const I=await n.cdkService.runCdkBootstrap(S,E(o),y,void 0,ae({account:t,estate:se(c?.providerAccounts)?"org":"solo",...s?.orgId!==void 0?{orgId:s.orgId}:{}}));if(!I.success)return o.onCascadeAccountComplete?.(r,!1,C(`Bootstrap failed: ${I.error}`),a),h(new Error(`Bootstrap failed for ${t.name}: ${C(I.error)}`));o.onCascadeAccountPhaseChange?.(r,"deploy",a);const k=await n.cdkService.runCdkDeploy(S,m,E(o),p=>o.onCascadeAccountResourceProgress?.(r,p,a),l,y);if(!k.success)return o.onCascadeAccountComplete?.(r,!1,C(k.error),a),h(new Error(C(k.error)));const b=await w.getStackOutputs(m,void 0,e.abortSignal);b.success||R.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:m,account:t.name});const L=V(b);return await G(S,l,o,e.abortSignal),N!==void 0&&((await n.hashService.persistCascadeStack(F,m,N)).success||R.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:m,account:t.name})),o.onCascadeAccountComplete?.(r,!0,void 0,a,L,!1),j({outputs:L,skipped:!1})}g(be,"deployCascadeAccount");async function Le(e,n,d,t){const i=new Map,o=e.awsProvider.getRegion(),s=await x(e.awsProvider,n.id,o,`fjall-ipam-read-${n.name}`,t);if(!s.success)return R.debug("organisationDeploy",`Cannot read Platform outputs: ${s.error.message}`),i;const a=new M(s.data.provider),r=H(P.PLATFORM),c=await a.getStackOutputs(r);if(!c.success)return R.debug("organisationDeploy",`Failed to read Platform stack outputs: ${c.error.message}`),i;const u=/^IpamPoolId(\d{12})(\w+)$/;for(const f of c.data){const l=f.OutputKey?.match(u);if(l&&f.OutputValue){const y=`${l[1]}-${l[2]}`;i.set(y,f.OutputValue)}}return i.size>0&&d.onLog?.(`Read ${i.size} IPAM pool ID(s) from Platform stack`,"info"),i}g(Le,"readPlatformIpamPoolIds");async function Be(e,n,d){const t=e.getDomains();if(t.length===0)return{domainsDeployed:0,errors:[]};n.onCascadePhaseStart?.("domains");const i=t.filter(r=>r.type==="apex"),o=t.filter(r=>r.type==="delegated");let s=0;const a=[];for(const r of i){const c=await e.deployDomain(r.name,n);c.success?s++:a.push(`${r.name}: ${c.error.message}`)}if(o.length>0){const r=await Promise.allSettled(o.map(c=>{const u=e.delegatedDeployer?.(c.name);return u!==void 0?re({deployer:u,callbacks:n,...d?.abortSignal!==void 0?{abortSignal:d.abortSignal}:{},...d?.skipCaaPreflight!==void 0?{skipCaaPreflight:d.skipCaaPreflight}:{}}):e.deployDomain(c.name,n)}));for(let c=0;c<r.length;c++){const u=r[c],f=o[c];if(!(!u||!f))if(u.status==="fulfilled")u.value.success?s++:a.push(`${f.name}: ${u.value.error.message}`);else{const l=u.reason instanceof Error?u.reason.message:String(u.reason);a.push(`${f.name}: ${l}`)}}}return n.onCascadePhaseComplete?.("domains"),{domainsDeployed:s,errors:a}}g(Be,"deployDomains");export{ie as CASCADE_MAX_CONCURRENCY,Fe as buildAccountRegionPairs,He as buildCascadeRoleArn,ce as buildRegionList,Ee as cascadeHomeRegion,de as cascadeOperationKey,be as deployCascadeAccount,Be as deployDomains,xe as partitionAccounts,Ne as probeCascadeRoles,Le as readPlatformIpamPoolIds};
1
+ var z=Object.defineProperty;var p=(e,t)=>z(e,"name",{value:t,configurable:!0});import{join as B}from"path";import{success as M,failure as y}from"@fjall/generator";import{logger as $}from"@fjall/util/logger";import{maskSensitiveOutput as C,mapSettledWithConcurrency as Y}from"@fjall/util";import{ORGANISATION_TYPES as w,getOrganisationStackName as T}from"../../types/operations.js";import{CdkContextBuilder as W}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as X}from"../../types/deployment/index.js";import{CloudFormationService as H}from"../../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as q,regionSuffix as J}from"../../types/config/FjallState.js";import{BackupClient as Q}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as Z,describeBackupVaultExists as ee}from"../../aws/organisations/backup.js";import{buildParamsContext as te,collectStackOutputs as V,assumeCascadeRole as I,forwardOutput as E,reconcileOidcProviderIfCarried as G}from"../contextHelpers.js";import{assertLeaseBeforeStack as oe}from"../application/leaseGate.js";import{deployDelegatedDomain as ne}from"../domain/delegatedDomainDeploy.js";import{runDriftPreFlightGate as re}from"../drift/preFlightGate.js";import{accountTier as U}from"@fjall/util";import{buildBootstrapTagsIfResolved as ae}from"../bootstrapTags.js";import{hasOrganisationTierAccount as ie}from"../../aws/organisations/accountGlobals.js";import{DEFAULT_REGION as _}from"../../aws/utils/regions.js";const se=4;function Ee(e){const t=e.find(o=>U(o)==="platform"),u=e.filter(o=>U(o)==="account");return{platformAccount:t,memberAccounts:u}}p(Ee,"partitionAccounts");function ce(e){const t=e?.primaryRegion??_,u=e?.secondaryRegions??[],o=new Set([t,...u]),s=e?.disasterRecoveryRegion;return s&&o.add(s),[...o]}p(ce,"buildRegionList");function be(e){return ce(e)[0]??_}p(be,"cascadeHomeRegion");function ue(e,t){return`${e} (${t})`}p(ue,"cascadeOperationKey");function Fe(e,t){const u=[];for(const o of t)for(const s of e)u.push({account:s,region:o});return u}p(Fe,"buildAccountRegionPairs");import{buildCascadeRoleArn as He}from"../contextHelpers.js";async function Ne(e,t,u,o){u.onLog?.(`Verifying cascade role access for ${t.length} account(s)\u2026`,"info");const s=await Y(t,se,async i=>I(e.awsProvider,i.id,i.region??e.awsProvider.getRegion(),`fjall-preflight-${i.name}`,o)),n=[];return s.forEach((i,r)=>{const a=t[r];if(a){if(i.status==="rejected"){n.push({accountId:a.id,accountName:a.name,error:i.reason instanceof Error?i.reason.message:String(i.reason)});return}i.value.success||n.push({accountId:a.id,accountName:a.name,error:i.value.error.message})}}),n.length===0&&u.onLog?.(`Cascade role access verified for ${t.length} account(s)`,"info"),n}p(Ne,"probeCascadeRoles");async function je(e,t,u,o,s,n,i){const r=i?.region??o.region??t.awsProvider.getRegion(),a=ue(o.name,r),l=i?.orgConfig??e.orgConfig,f=i?.ipamPoolId;n.onCascadeAccountStart?.(a,o.id,r,s);const c=await I(t.awsProvider,o.id,r,`fjall-cascade-${o.name}`,e.abortSignal);if(!c.success)return n.onCascadeAccountComplete?.(a,!1,C(c.error.message),r),y(new Error(`Failed to assume role for ${o.name}: ${C(c.error.message)}`));const{provider:d,credentials:h}=c.data,R=B(e.workingDirectory,"fjall",s==="platform"?w.PLATFORM:w.ACCOUNT),A=B(R,`cdk.out.${o.id}.${J(r)}`),S=W.buildDeploymentContext({deployType:s,target:u.target,path:R,assemblyDir:A,environment:o.environment??void 0,region:r,accountName:o.name,callerIdentity:X(o.id),ipamPoolId:f,...te({orgConfig:l,identity:e.identity,skipOidc:e.options?.skipOidc,region:r,primaryRegion:i?.primaryRegion,trailLifecycle:o.trailLifecycle})},{verbose:e.options?.verbose},l);if(Z(o.environment,l?.disasterRecoveryRegion)){const m=await ee(d.getClient(Q),e.abortSignal);if(!m.success)return n.onCascadeAccountComplete?.(a,!1,C(m.error.message),r),y(new Error(`Backup vault probe failed for ${o.name}: ${C(m.error.message)}`));S.fjallAdoptBackupVault=m.data}n.onCascadeAccountPhaseChange?.(a,"synth",r);const O=await t.cdkService.runCdkSynth(S,E(n),h);if(!O.success)return n.onCascadeAccountComplete?.(a,!1,C(`Synth failed: ${O.error}`),r),y(new Error(`Synth failed for ${o.name}: ${C(O.error)}`));const g=T(s==="platform"?w.PLATFORM:w.ACCOUNT),b=q(R,o.id,r),P=new H(d),{changed:K,currentHash:F}=await t.hashService.compareCascadeStack(A,g,b);if(!K&&e.options?.force!==!0&&await P.stackExists(g,void 0,e.abortSignal)){const m=await P.getStackOutputs(g,void 0,e.abortSignal);m.success||$.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:g,account:o.name});const L=V(m);return await G(S,d,n,e.abortSignal),n.onLog?.(`${o.name}: no infrastructure changes \u2014 skipping deploy`,"info"),n.onCascadeAccountComplete?.(a,!0,void 0,r,L,!0),M({outputs:L,skipped:!0})}const k=await re(d,{stackNames:[g],cdkOutPath:A,cfnService:P,callbacks:n,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(k.kind==="blocked")return n.onCascadeAccountComplete?.(a,!1,k.message,r),y(new Error(k.message));const x=await oe(e.assertLeaseForStack,g,e.abortSignal);if(x!==void 0)return n.onCascadeAccountComplete?.(a,!1,x.message,r),y(x);n.onCascadeAccountPhaseChange?.(a,"bootstrap",r);const v=await t.cdkService.runCdkBootstrap(S,E(n),h,void 0,ae({account:o,estate:ie(l?.providerAccounts)?"org":"solo",...i?.orgId!==void 0?{orgId:i.orgId}:{}}));if(!v.success)return n.onCascadeAccountComplete?.(a,!1,C(`Bootstrap failed: ${v.error}`),r),y(new Error(`Bootstrap failed for ${o.name}: ${C(v.error)}`));n.onCascadeAccountPhaseChange?.(a,"deploy",r);const D=await t.cdkService.runCdkDeploy(S,g,E(n),m=>n.onCascadeAccountResourceProgress?.(a,m,r),d,h);if(!D.success)return n.onCascadeAccountComplete?.(a,!1,C(D.error),r),y(new Error(C(D.error)));const N=await P.getStackOutputs(g,void 0,e.abortSignal);N.success||$.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:g,account:o.name});const j=V(N);return await G(S,d,n,e.abortSignal),F!==void 0&&((await t.hashService.persistCascadeStack(b,g,F)).success||$.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:g,account:o.name})),n.onCascadeAccountComplete?.(a,!0,void 0,r,j,!1),M({outputs:j,skipped:!1})}p(je,"deployCascadeAccount");async function Le(e,t,u,o){const s=new Map,n=e.awsProvider.getRegion(),i=await I(e.awsProvider,t.id,n,`fjall-ipam-read-${t.name}`,o);if(!i.success)return $.debug("organisationDeploy",`Cannot read Platform outputs: ${i.error.message}`),s;const r=new H(i.data.provider),a=T(w.PLATFORM),l=await r.getStackOutputs(a);if(!l.success)return $.debug("organisationDeploy",`Failed to read Platform stack outputs: ${l.error.message}`),s;const f=/^IpamPoolId(\d{12})(\w+)$/;for(const c of l.data){const d=c.OutputKey?.match(f);if(d&&c.OutputValue){const h=`${d[1]}-${d[2]}`;s.set(h,c.OutputValue)}}return s.size>0&&u.onLog?.(`Read ${s.size} IPAM pool ID(s) from Platform stack`,"info"),s}p(Le,"readPlatformIpamPoolIds");function de(e,t){const u=e.account!==void 0&&t?.executingAccountId!==void 0&&e.account!==t.executingAccountId,o=e.region!==void 0&&t?.executingRegion!==void 0&&e.region!==t.executingRegion;if(u)return`[${e.name}] skipped: its fjall-config.json entry records account ${e.account} as its home, but this cascade runs in account ${t?.executingAccountId} - deploy it separately with 'fjall domain deploy ${e.name} -t <target in account ${e.account}>' or run the cascade under that account.`;if(o)return`[${e.name}] skipped: its fjall-config.json entry records region ${e.region} as its home, but this cascade runs in region ${t?.executingRegion} - deploy it separately with 'fjall domain deploy ${e.name} --region ${e.region}', or update the entry's "region" key if this move is intentional.`}p(de,"foreignDomainSkipReason");async function Be(e,t,u){const o=e.getDomains();if(o.length===0)return{domainsDeployed:0,domainsSkipped:0,errors:[]};t.onCascadePhaseStart?.("domains");const s=[];let n=0;for(const f of o){const c=de(f,u);if(c!==void 0){n++,t.onProgress?.({type:"warning",message:c});continue}s.push(f)}const i=s.filter(f=>f.type==="apex"),r=s.filter(f=>f.type==="delegated");let a=0;const l=[];for(const f of i){const c=await e.deployDomain(f.name,t);c.success?a++:l.push(`${f.name}: ${c.error.message}`)}if(r.length>0){const f=await Promise.allSettled(r.map(c=>{const d=e.delegatedDeployer?.(c.name);return d!==void 0?ne({deployer:d,callbacks:t,...u?.abortSignal!==void 0?{abortSignal:u.abortSignal}:{},...u?.skipCaaPreflight!==void 0?{skipCaaPreflight:u.skipCaaPreflight}:{}}):e.deployDomain(c.name,t)}));for(let c=0;c<f.length;c++){const d=f[c],h=r[c];if(!(!d||!h))if(d.status==="fulfilled")d.value.success?a++:l.push(`${h.name}: ${d.value.error.message}`);else{const R=d.reason instanceof Error?d.reason.message:String(d.reason);l.push(`${h.name}: ${R}`)}}}return t.onCascadePhaseComplete?.("domains"),{domainsDeployed:a,domainsSkipped:n,errors:l}}p(Be,"deployDomains");export{se as CASCADE_MAX_CONCURRENCY,Fe as buildAccountRegionPairs,He as buildCascadeRoleArn,ce as buildRegionList,be as cascadeHomeRegion,ue as cascadeOperationKey,je as deployCascadeAccount,Be as deployDomains,Ee as partitionAccounts,Ne as probeCascadeRoles,Le as readPlatformIpamPoolIds};
@@ -1 +1 @@
1
- var W=Object.defineProperty;var b=(o,S)=>W(o,"name",{value:S,configurable:!0});import{failure as $}from"@fjall/generator";import{regionSuffix as X}from"../../types/config/FjallState.js";import{partitionAccounts as Y,deployCascadeAccount as T,readPlatformIpamPoolIds as z,deployDomains as B,buildRegionList as G,buildAccountRegionPairs as J,cascadeHomeRegion as K,CASCADE_MAX_CONCURRENCY as Q}from"../organisation/cascadeHelpers.js";import{projectScalarSummary as q}from"../organisation/cascadeSummary.js";import{getErrorMessage as V,maskSensitiveOutput as C,mapSettledWithConcurrency as Z}from"@fjall/util";import{INFRA_STEPS as E}from"./infraSteps.js";async function ie(o,S,N,H){const{callbacks:e}=o,{providerAccounts:U,effectiveOrgConfig:R,totalSteps:p,cascadeErrors:l,allCascadeOutputs:M,orgId:O}=H;let g=!1;e.onCascadeStart?.();const j=Date.now();let d=2,y=!1,h,D=!1;const I=[],_=b(t=>({members:I,...h!==void 0?{platform:h}:{},domainsDeployed:D,errors:l,totalDurationMs:t}),"buildLedger"),{platformAccount:r,memberAccounts:A}=Y(U),F=K(o.orgConfig);if(r){const{id:t,name:a}=E.CASCADE_PLATFORM;e.onStepStart?.(t,a,d,p),e.onCascadePhaseStart?.("platform");let c;const P=Date.now();try{c=await T(o,S,N,r,"platform",e,{orgConfig:R,primaryRegion:F,orgId:O})}catch(i){const n=C(V(i));l.push({accountId:r.id,error:n}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,a,"error",d,p),c=$(new Error(n))}const w=Date.now()-P;if(c.success){y=!0;const i=c.data.skipped===!0;i||(g=!0),c.data.outputs&&M.push({accountId:r.id,outputs:c.data.outputs}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,a,i?"skipped":"completed",d,p),h={accountId:r.id,result:i?"skipped":"succeeded",durationMs:w}}else l.some(i=>i.accountId===r.id)||(l.push({accountId:r.id,error:C(c.error.message)}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,a,"error",d,p)),h={accountId:r.id,result:"failed",durationMs:w,error:C(c.error.message)};d++}let L=new Map;if(y&&r&&(L=await z(S,r,e,o.abortSignal)),o.domainProvider){const t=await B(o.domainProvider,e,{...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{},...o.options?.skipCaaPreflight!==void 0?{skipCaaPreflight:o.options.skipCaaPreflight}:{}});D=t.domainsDeployed>0,D&&(g=!0);for(const a of t.errors)l.push({accountId:"domains",error:C(a)})}if(r!==void 0&&!y&&A.length>0){const{id:t,name:a}=E.CASCADE_ACCOUNTS;e.onStepStart?.(t,a,d,p),e.onStepComplete?.(t,a,"skipped",d,p),e.onLog?.("Skipping account cascade \u2014 platform deployment failed; platform is a prerequisite for member accounts.","warn")}else if(A.length>0){const{id:t,name:a}=E.CASCADE_ACCOUNTS;e.onStepStart?.(t,a,d,p),e.onCascadePhaseStart?.("accounts");const c=G(o.orgConfig),P=J(A,c);(await Z(P,Q,async({account:n,region:k})=>{const m=L.get(`${n.id}-${X(k)}`),s=Date.now();return{result:await T(o,S,N,n,"account",e,{ipamPoolId:m,orgConfig:R,region:k,primaryRegion:F,orgId:O}),durationMs:Date.now()-s}})).forEach((n,k)=>{const m=P[k];if(!m)return;const s=m.account;if(n.status==="rejected"){const u=C(n.reason instanceof Error?n.reason.message:String(n.reason));I.push({accountId:s.id,accountName:s.name,region:m.region,result:"failed",durationMs:0,error:u}),l.push({accountId:s.id,error:u});return}const{result:f,durationMs:x}=n.value;if(f.success){const u=f.data.skipped===!0;u||(g=!0),f.data.outputs&&M.push({accountId:s.id,outputs:f.data.outputs}),I.push({accountId:s.id,accountName:s.name,region:m.region,result:u?"skipped":"succeeded",durationMs:x})}else{const u=C(f.error.message);I.push({accountId:s.id,accountName:s.name,region:m.region,result:"failed",durationMs:x,error:u}),l.push({accountId:s.id,error:u})}});const i=q(_(0));e.onCascadePhaseComplete?.("accounts"),e.onStepComplete?.(t,a,i.accountsFailed>0?"error":i.accountsSkipped===A.length?"skipped":"completed",d,p)}const v=_(Date.now()-j);return e.onCascadeComplete?.(q(v)),e.onCascadeLedger?.(v),{anyCascadeDeployHappened:g}}b(ie,"executeCascade");export{ie as executeCascade};
1
+ var W=Object.defineProperty;var b=(o,l)=>W(o,"name",{value:l,configurable:!0});import{failure as $}from"@fjall/generator";import{regionSuffix as X}from"../../types/config/FjallState.js";import{partitionAccounts as Y,deployCascadeAccount as T,readPlatformIpamPoolIds as z,deployDomains as B,buildRegionList as G,buildAccountRegionPairs as J,cascadeHomeRegion as K,CASCADE_MAX_CONCURRENCY as Q}from"../organisation/cascadeHelpers.js";import{projectScalarSummary as q}from"../organisation/cascadeSummary.js";import{getErrorMessage as V,maskSensitiveOutput as C,mapSettledWithConcurrency as Z}from"@fjall/util";import{INFRA_STEPS as R}from"./infraSteps.js";async function ie(o,l,E,H){const{callbacks:e}=o,{providerAccounts:U,effectiveOrgConfig:N,totalSteps:u,cascadeErrors:m,allCascadeOutputs:M,orgId:O}=H;let S=!1;e.onCascadeStart?.();const j=Date.now();let d=2,w=!1,h,y=!1;const I=[],x=b(t=>({members:I,...h!==void 0?{platform:h}:{},domainsDeployed:y,errors:m,totalDurationMs:t}),"buildLedger"),{platformAccount:r,memberAccounts:A}=Y(U),_=K(o.orgConfig);if(r){const{id:t,name:a}=R.CASCADE_PLATFORM;e.onStepStart?.(t,a,d,u),e.onCascadePhaseStart?.("platform");let c;const P=Date.now();try{c=await T(o,l,E,r,"platform",e,{orgConfig:N,primaryRegion:_,orgId:O})}catch(i){const n=C(V(i));m.push({accountId:r.id,error:n}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,a,"error",d,u),c=$(new Error(n))}const D=Date.now()-P;if(c.success){w=!0;const i=c.data.skipped===!0;i||(S=!0),c.data.outputs&&M.push({accountId:r.id,outputs:c.data.outputs}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,a,i?"skipped":"completed",d,u),h={accountId:r.id,result:i?"skipped":"succeeded",durationMs:D}}else m.some(i=>i.accountId===r.id)||(m.push({accountId:r.id,error:C(c.error.message)}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(t,a,"error",d,u)),h={accountId:r.id,result:"failed",durationMs:D,error:C(c.error.message)};d++}let F=new Map;if(w&&r&&(F=await z(l,r,e,o.abortSignal)),o.domainProvider){const t=await B(o.domainProvider,e,{executingAccountId:l.awsProvider.getAccountId(),executingRegion:l.awsProvider.getRegion(),...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{},...o.options?.skipCaaPreflight!==void 0?{skipCaaPreflight:o.options.skipCaaPreflight}:{}});y=t.domainsDeployed>0,y&&(S=!0);for(const a of t.errors)m.push({accountId:"domains",error:C(a)})}if(r!==void 0&&!w&&A.length>0){const{id:t,name:a}=R.CASCADE_ACCOUNTS;e.onStepStart?.(t,a,d,u),e.onStepComplete?.(t,a,"skipped",d,u),e.onLog?.("Skipping account cascade \u2014 platform deployment failed; platform is a prerequisite for member accounts.","warn")}else if(A.length>0){const{id:t,name:a}=R.CASCADE_ACCOUNTS;e.onStepStart?.(t,a,d,u),e.onCascadePhaseStart?.("accounts");const c=G(o.orgConfig),P=J(A,c);(await Z(P,Q,async({account:n,region:k})=>{const f=F.get(`${n.id}-${X(k)}`),s=Date.now();return{result:await T(o,l,E,n,"account",e,{ipamPoolId:f,orgConfig:N,region:k,primaryRegion:_,orgId:O}),durationMs:Date.now()-s}})).forEach((n,k)=>{const f=P[k];if(!f)return;const s=f.account;if(n.status==="rejected"){const p=C(n.reason instanceof Error?n.reason.message:String(n.reason));I.push({accountId:s.id,accountName:s.name,region:f.region,result:"failed",durationMs:0,error:p}),m.push({accountId:s.id,error:p});return}const{result:g,durationMs:v}=n.value;if(g.success){const p=g.data.skipped===!0;p||(S=!0),g.data.outputs&&M.push({accountId:s.id,outputs:g.data.outputs}),I.push({accountId:s.id,accountName:s.name,region:f.region,result:p?"skipped":"succeeded",durationMs:v})}else{const p=C(g.error.message);I.push({accountId:s.id,accountName:s.name,region:f.region,result:"failed",durationMs:v,error:p}),m.push({accountId:s.id,error:p})}});const i=q(x(0));e.onCascadePhaseComplete?.("accounts"),e.onStepComplete?.(t,a,i.accountsFailed>0?"error":i.accountsSkipped===A.length?"skipped":"completed",d,u)}const L=x(Date.now()-j);return e.onCascadeComplete?.(q(L)),e.onCascadeLedger?.(L),{anyCascadeDeployHappened:S}}b(ie,"executeCascade");export{ie as executeCascade};
@@ -1,9 +1,11 @@
1
- var j=Object.defineProperty;var F=(N,e)=>j(N,"name",{value:e,configurable:!0});import{existsSync as U}from"fs";import{join as $}from"path";import{logger as O}from"@fjall/util/logger";import{success as v,failure as a}from"@fjall/generator";import{DEFAULT_REGION as S}from"../../aws/utils/regions.js";import{getErrorMessage as k,maskSensitiveOutput as h}from"@fjall/util";import{CdkEventMonitor as B,DEFAULT_DEPLOY_TIMEOUT_MS as G,settleMonitoringAnalysis as Y,startStackMonitoring as q}from"./CdkEventMonitoring.js";import{analyseDeployOutput as X,analyseDestroyResult as z,createEnhancedOutputCallback as H,formatAnalysedDeployFailure as _}from"./CdkOutputAnalyser.js";import{recordDriftSuspects as J}from"../../orchestration/drift/driftSuspectJournal.js";import{CdkCommandRunner as Q}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as V}from"./CdkArgumentBuilder.js";import{CdkProcessManager as Z}from"./CdkProcessManager.js";import{cancelInFlightStack as P}from"./cancelInFlightStack.js";import{isAborted as w}from"../../aws/organisations/types.js";import{wrapWithConstructMapEnrichment as x}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as ee,resolveStackName as K,getFallbackStackName as te,buildDeploymentCdkContext as A}from"./cdkServiceHelpers.js";import{ensureSynthDependencies as re}from"./synthDependencies.js";import{isTransientNetworkFailure as ne,isReattachableInProgressStatus as oe,MAX_TRANSIENT_NETWORK_RETRIES as T,TRANSIENT_RETRY_BACKOFF_MS as ae}from"./transientNetworkRecovery.js";import{sleepAbortable as ie}from"../../util/sleepAbortable.js";class Ie{static{F(this,"CdkService")}commandRunner;eventMonitor;abortSignal;constructor(e){const t=e?.processManager??new Z(new V,e?.abortSignal);this.commandRunner=new Q(t),this.eventMonitor=new B({eventLogWriterFactory:e?.eventLogWriterFactory,...e?.onFailureAnalysis!==void 0?{onFailureAnalysis:e.onFailureAnalysis}:{}}),this.abortSignal=e?.abortSignal}dispose(){this.commandRunner.dispose()}async checkDifferences(e,t,r){return this.commandRunner.checkDifferences(e,t,r)}async deploy(e,t,r){return this.commandRunner.deploy(e,t,r)}async destroy(e,t,r){return this.commandRunner.destroy(e,t,r)}async runImport(e,t,r){return this.commandRunner.runImport(e,t,r)}async synth(e,t){return this.commandRunner.synth(e,t)}async bootstrap(e,t,r){return this.commandRunner.bootstrap(e,t,r)}async runCdkSynth(e,t,r){const o=e.callerIdentity?.Account;try{const n=await re(e.path,{...t!==void 0?{onOutput:t}:{},...this.abortSignal!==void 0?{abortSignal:this.abortSignal}:{}});if(!n.success)return a(n.error);const i=await this.synth(e.path,{outputCallback:t,context:A(e,o,e.region||S),...e.assemblyDir!==void 0?{outputDir:e.assemblyDir}:{},...r!==void 0?{credentials:r}:{}});return i.success?v({message:"CloudFormation template synthesised",details:i.data.output?{synthesisTime:i.data.output}:void 0}):a(i.error||"Failed to synthesise CloudFormation template")}catch(n){return a(`CDK synth failed: ${h(k(n))}`)}}async runCdkBootstrap(e,t,r,o,n){const i=e.callerIdentity?.Account,u=e.region||S;try{if(!i)return a("No AWS account ID available");const l=$(e.path,"node_modules");if(!U(l))return a(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const d=await this.bootstrap(i,u,{outputCallback:t,credentials:r,...o!==void 0&&o!==""?{permissionsBoundary:o}:{},...n!==void 0&&Object.keys(n).length>0?{bootstrapTags:n}:{}});return d.success?v({message:"AWS environment bootstrapped"}):a(d.error||"Failed to bootstrap AWS environment")}catch(l){return a(`CDK bootstrap failed: ${h(k(l))}`)}}async runCdkDiff(e,t){const r=e.callerIdentity?.Account;try{const o=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:t,context:A(e,r,e.region||S)});return o.success?v({message:"Diff check complete",details:{hasDifferences:o.data.hasDifferences,details:o.data.details}}):a(`CDK diff failed: ${o.error.message}`)}catch(o){return a(`CDK diff failed: ${h(k(o))}`)}}async runCdkDeploy(e,t,r,o,n,i,u,l){const d=e.callerIdentity?.Account,s=e.region||S;if(!d)return a("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!n)return a("AwsProvider is required for deployment monitoring.");const g=e.assemblyDir??$(e.path,"cdk.out"),y=x(g,o);let p=null,b;try{const f=K(t,e)??te(e),m=await this.eventMonitor.createEventMonitor("deploy",f,s,e,n);p=m,r&&(r(h(`Starting CloudFormation deployment of ${f}...
1
+ var j=Object.defineProperty;var F=(E,e)=>j(E,"name",{value:e,configurable:!0});import{existsSync as U}from"fs";import{join as P}from"path";import{logger as T}from"@fjall/util/logger";import{success as v,failure as o}from"@fjall/generator";import{DEFAULT_REGION as S}from"../../aws/utils/regions.js";import{getErrorMessage as k,maskSensitiveOutput as g}from"@fjall/util";import{CdkEventMonitor as G,DEFAULT_DEPLOY_TIMEOUT_MS as B,settleMonitoringAnalysis as Y,startStackMonitoring as q}from"./CdkEventMonitoring.js";import{analyseDeployOutput as X,analyseDestroyResult as z,createEnhancedOutputCallback as H,formatAnalysedDeployFailure as _}from"./CdkOutputAnalyser.js";import{recordDriftSuspects as J}from"../../orchestration/drift/driftSuspectJournal.js";import{CdkCommandRunner as Q}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as V}from"./CdkArgumentBuilder.js";import{CdkProcessManager as Z}from"./CdkProcessManager.js";import{cancelInFlightStack as O}from"./cancelInFlightStack.js";import{isAborted as w}from"../../aws/organisations/types.js";import{wrapWithConstructMapEnrichment as x}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as ee,resolveStackName as K,getFallbackStackName as te,buildDeploymentCdkContext as I}from"./cdkServiceHelpers.js";import{ensureSynthDependencies as re}from"./synthDependencies.js";import{runInfraTypecheckPreflight as ne}from"./infraTypecheckPreflight.js";import{isTransientNetworkFailure as ae,isReattachableInProgressStatus as oe,MAX_TRANSIENT_NETWORK_RETRIES as N,TRANSIENT_RETRY_BACKOFF_MS as ie}from"./transientNetworkRecovery.js";import{sleepAbortable as se}from"../../util/sleepAbortable.js";class Fe{static{F(this,"CdkService")}commandRunner;eventMonitor;abortSignal;constructor(e){const t=e?.processManager??new Z(new V,e?.abortSignal);this.commandRunner=new Q(t),this.eventMonitor=new G({eventLogWriterFactory:e?.eventLogWriterFactory,...e?.onFailureAnalysis!==void 0?{onFailureAnalysis:e.onFailureAnalysis}:{}}),this.abortSignal=e?.abortSignal}dispose(){this.commandRunner.dispose()}async checkDifferences(e,t,r){return this.commandRunner.checkDifferences(e,t,r)}async deploy(e,t,r){return this.commandRunner.deploy(e,t,r)}async destroy(e,t,r){return this.commandRunner.destroy(e,t,r)}async runImport(e,t,r){return this.commandRunner.runImport(e,t,r)}async synth(e,t){return this.commandRunner.synth(e,t)}async bootstrap(e,t,r){return this.commandRunner.bootstrap(e,t,r)}async runCdkSynth(e,t,r){const a=e.callerIdentity?.Account;try{const n=await re(e.path,{...t!==void 0?{onOutput:t}:{},...this.abortSignal!==void 0?{abortSignal:this.abortSignal}:{}});if(!n.success)return o(n.error);const l=await ne(e.path,{...this.abortSignal!==void 0?{abortSignal:this.abortSignal}:{}});l.status==="failed"&&(t?.(`WARNING: TypeScript errors in ${e.path} \u2014 the synthesised template may be wrong. This check will block deploys in a future release.
2
+ ${l.details}
3
+ `),T.warn("CdkService","Infrastructure typecheck failed",{path:e.path}));const i=await this.synth(e.path,{outputCallback:t,context:I(e,a,e.region||S),...e.assemblyDir!==void 0?{outputDir:e.assemblyDir}:{},...r!==void 0?{credentials:r}:{}});return i.success?v({message:"CloudFormation template synthesised",details:i.data.output?{synthesisTime:i.data.output}:void 0}):o(i.error||"Failed to synthesise CloudFormation template")}catch(n){return o(`CDK synth failed: ${g(k(n))}`)}}async runCdkBootstrap(e,t,r,a,n){const l=e.callerIdentity?.Account,i=e.region||S;try{if(!l)return o("No AWS account ID available");const u=P(e.path,"node_modules");if(!U(u))return o(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const d=await this.bootstrap(l,i,{outputCallback:t,credentials:r,...a!==void 0&&a!==""?{permissionsBoundary:a}:{},...n!==void 0&&Object.keys(n).length>0?{bootstrapTags:n}:{}});return d.success?v({message:"AWS environment bootstrapped"}):o(d.error||"Failed to bootstrap AWS environment")}catch(u){return o(`CDK bootstrap failed: ${g(k(u))}`)}}async runCdkDiff(e,t){const r=e.callerIdentity?.Account;try{const a=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:t,context:I(e,r,e.region||S)});return a.success?v({message:"Diff check complete",details:{hasDifferences:a.data.hasDifferences,details:a.data.details}}):o(`CDK diff failed: ${a.error.message}`)}catch(a){return o(`CDK diff failed: ${g(k(a))}`)}}async runCdkDeploy(e,t,r,a,n,l,i,u){const d=e.callerIdentity?.Account,s=e.region||S;if(!d)return o("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!n)return o("AwsProvider is required for deployment monitoring.");const p=e.assemblyDir??P(e.path,"cdk.out"),y=x(p,a);let h=null,b;try{const f=K(t,e)??te(e),m=await this.eventMonitor.createEventMonitor("deploy",f,s,e,n);h=m,r&&(r(g(`Starting CloudFormation deployment of ${f}...
2
4
  `)),r(`Monitoring CloudFormation events (CDK process running in background)...
3
- `));const c={cdkOutput:"",actualStackName:f,stackDetected:!1,monitoringPromise:null},L=H(c,r,p,y);b=setTimeout(()=>{!c.stackDetected&&!c.monitoringPromise&&(O.debug("CdkService","Fallback monitoring STARTING",{targetStackName:f,stackDetected:c.stackDetected,hasOnResourceProgress:!!o}),c.monitoringPromise=q(m,f,y))},ee);for(let D=0;;D++){const C=await this.deploy(e.path,f,{verbose:e.options?.verbose,outputCallback:L,...e.assemblyDir!==void 0?{appDir:e.assemblyDir}:{useCdkOut:!0},cdkOutputLogger:m.getEventLogger()??void 0,context:A(e,d,s),credentials:i,...u!==void 0&&Object.keys(u).length>0&&{parameters:u},...l===!0&&{exclusively:!0}});if(w(this.abortSignal))return await P(f,s,i,r),a("Deployment cancelled");const R=X(c.cdkOutput,C,c.actualStackName);if(R.success)return R;const W=[c.cdkOutput,C.success?C.data.output??"":C.error,R.error].join(`
4
- `);if(ne(W)){r?.(`Transient network failure while running CDK for ${c.actualStackName} \u2014 probing CloudFormation for an in-flight operation...
5
- `);const M=await m.getStackStatus(c.actualStackName,this.abortSignal);if(M!==null&&oe(M.status))return clearTimeout(b),await this.reattachToInFlightOperation({stackName:c.actualStackName,currentStatus:M.status,region:s,accountId:d,monitor:m,state:c,credentials:i,onOutput:r,onResourceProgress:y});if(D<T){const E=ae[D]??15e3;if(r?.(`No CloudFormation operation in progress \u2014 nothing was mutated. Retrying the CDK deploy in ${E/1e3}s (retry ${D+1} of ${T})...
6
- `),await ie(E,this.abortSignal),w(this.abortSignal))return a("Deployment cancelled");c.cdkOutput="";continue}r?.(`No CloudFormation operation in progress and the retry budget (${T}) is exhausted \u2014 giving up.
7
- `)}const I=await Y(m,c.monitoringPromise);return I===null?R:(await this.journalDriftSuspects(d,s,c.actualStackName,I),a(_(I)))}}catch(f){const m=`CDK deploy failed: ${h(k(f))}`;return O.error("CdkService","CDK deployment exception",{error:m}),a(m)}finally{clearTimeout(b),p&&p.stopMonitoring()}}async reattachToInFlightOperation(e){const{stackName:t,monitor:r,state:o,onOutput:n}=e;n?.(`CloudFormation reports ${e.currentStatus} for ${t} \u2014 the operation survived the network failure. Re-attaching and monitoring it to completion...
8
- `),r.stopMonitoring(),o.monitoringPromise=null;const i=F(()=>{r.stopMonitoring()},"onAbort");this.abortSignal?.addEventListener("abort",i,{once:!0});const u=await r.waitForStackComplete(t,{timeout:G,...e.onResourceProgress!==void 0?{onResourceUpdate:e.onResourceProgress}:{}}).finally(()=>{this.abortSignal?.removeEventListener("abort",i)});if(w(this.abortSignal))return await P(t,e.region,e.credentials,n),a("Deployment cancelled");if(u.success){const s=u.status??"COMPLETE";return n?.(`${t} reached ${s} after re-attach.
9
- `),v({message:`Re-attached to the in-flight CloudFormation operation after a transient network failure; ${t} reached ${s}`,details:{reattached:!0,status:s}})}const l=r.getFailureAnalysis();if(l!==null)return await this.journalDriftSuspects(e.accountId,e.region,t,l),a(_(l));const d=u.failureReason!==void 0&&u.failureReason!==""?h(u.failureReason):"no failure reason reported";return a(`Re-attached to the in-flight CloudFormation operation after a transient network failure; ${t} reached ${u.status??"UNKNOWN"}: ${d}`)}async journalDriftSuspects(e,t,r,o){o.driftSuspects===void 0||o.driftSuspects.length===0||await J({accountId:e,region:t,stackName:r,recordedAt:new Date().toISOString(),suspects:o.driftSuspects.map(n=>({stackName:r,logicalId:n.logicalId,resourceType:n.resourceType,...n.physicalId!==void 0?{physicalId:n.physicalId}:{},failedStatus:n.failedStatus,statusReason:n.statusReason}))})}async runCdkDestroy(e,t,r,o,n,i,u){const l=e.callerIdentity?.Account,d=e.region||S;let s=null;try{const g=K(t,e);l&&g&&n&&(s=await this.eventMonitor.createEventMonitor("destroy",g,d,e,n),s.startMonitoring(g,p=>{o?.(p)},(p,b)=>{}));const y=await this.destroy(e.path,t,{verbose:e.options?.verbose,outputCallback:r,useCdkOut:i,cdkOutputLogger:s?.getEventLogger()??void 0,context:A(e,l,d),credentials:u});return w(this.abortSignal)?a("Destroy cancelled"):z(y)}catch(g){return a(`CDK destroy failed: ${h(k(g))}`)}finally{s&&s.stopMonitoring()}}}export{Ie as CdkService};
5
+ `));const c={cdkOutput:"",actualStackName:f,stackDetected:!1,monitoringPromise:null},L=H(c,r,h,y);b=setTimeout(()=>{!c.stackDetected&&!c.monitoringPromise&&(T.debug("CdkService","Fallback monitoring STARTING",{targetStackName:f,stackDetected:c.stackDetected,hasOnResourceProgress:!!a}),c.monitoringPromise=q(m,f,y))},ee);for(let D=0;;D++){const C=await this.deploy(e.path,f,{verbose:e.options?.verbose,outputCallback:L,...e.assemblyDir!==void 0?{appDir:e.assemblyDir}:{useCdkOut:!0},cdkOutputLogger:m.getEventLogger()??void 0,context:I(e,d,s),credentials:l,...i!==void 0&&Object.keys(i).length>0&&{parameters:i},...u===!0&&{exclusively:!0}});if(w(this.abortSignal))return await O(f,s,l,r),o("Deployment cancelled");const R=X(c.cdkOutput,C,c.actualStackName);if(R.success)return R;const W=[c.cdkOutput,C.success?C.data.output??"":C.error,R.error].join(`
6
+ `);if(ae(W)){r?.(`Transient network failure while running CDK for ${c.actualStackName} \u2014 probing CloudFormation for an in-flight operation...
7
+ `);const M=await m.getStackStatus(c.actualStackName,this.abortSignal);if(M!==null&&oe(M.status))return clearTimeout(b),await this.reattachToInFlightOperation({stackName:c.actualStackName,currentStatus:M.status,region:s,accountId:d,monitor:m,state:c,credentials:l,onOutput:r,onResourceProgress:y});if(D<N){const $=ie[D]??15e3;if(r?.(`No CloudFormation operation in progress \u2014 nothing was mutated. Retrying the CDK deploy in ${$/1e3}s (retry ${D+1} of ${N})...
8
+ `),await se($,this.abortSignal),w(this.abortSignal))return o("Deployment cancelled");c.cdkOutput="";continue}r?.(`No CloudFormation operation in progress and the retry budget (${N}) is exhausted \u2014 giving up.
9
+ `)}const A=await Y(m,c.monitoringPromise);return A===null?R:(await this.journalDriftSuspects(d,s,c.actualStackName,A),o(_(A)))}}catch(f){const m=`CDK deploy failed: ${g(k(f))}`;return T.error("CdkService","CDK deployment exception",{error:m}),o(m)}finally{clearTimeout(b),h&&h.stopMonitoring()}}async reattachToInFlightOperation(e){const{stackName:t,monitor:r,state:a,onOutput:n}=e;n?.(`CloudFormation reports ${e.currentStatus} for ${t} \u2014 the operation survived the network failure. Re-attaching and monitoring it to completion...
10
+ `),r.stopMonitoring(),a.monitoringPromise=null;const l=F(()=>{r.stopMonitoring()},"onAbort");this.abortSignal?.addEventListener("abort",l,{once:!0});const i=await r.waitForStackComplete(t,{timeout:B,...e.onResourceProgress!==void 0?{onResourceUpdate:e.onResourceProgress}:{}}).finally(()=>{this.abortSignal?.removeEventListener("abort",l)});if(w(this.abortSignal))return await O(t,e.region,e.credentials,n),o("Deployment cancelled");if(i.success){const s=i.status??"COMPLETE";return n?.(`${t} reached ${s} after re-attach.
11
+ `),v({message:`Re-attached to the in-flight CloudFormation operation after a transient network failure; ${t} reached ${s}`,details:{reattached:!0,status:s}})}const u=r.getFailureAnalysis();if(u!==null)return await this.journalDriftSuspects(e.accountId,e.region,t,u),o(_(u));const d=i.failureReason!==void 0&&i.failureReason!==""?g(i.failureReason):"no failure reason reported";return o(`Re-attached to the in-flight CloudFormation operation after a transient network failure; ${t} reached ${i.status??"UNKNOWN"}: ${d}`)}async journalDriftSuspects(e,t,r,a){a.driftSuspects===void 0||a.driftSuspects.length===0||await J({accountId:e,region:t,stackName:r,recordedAt:new Date().toISOString(),suspects:a.driftSuspects.map(n=>({stackName:r,logicalId:n.logicalId,resourceType:n.resourceType,...n.physicalId!==void 0?{physicalId:n.physicalId}:{},failedStatus:n.failedStatus,statusReason:n.statusReason}))})}async runCdkDestroy(e,t,r,a,n,l,i){const u=e.callerIdentity?.Account,d=e.region||S;let s=null;try{const p=K(t,e);u&&p&&n&&(s=await this.eventMonitor.createEventMonitor("destroy",p,d,e,n),s.startMonitoring(p,h=>{a?.(h)},(h,b)=>{}));const y=await this.destroy(e.path,t,{verbose:e.options?.verbose,outputCallback:r,useCdkOut:l,cdkOutputLogger:s?.getEventLogger()??void 0,context:I(e,u,d),credentials:i});return w(this.abortSignal)?o("Destroy cancelled"):z(y)}catch(p){return o(`CDK destroy failed: ${g(k(p))}`)}finally{s&&s.stopMonitoring()}}}export{Fe as CdkService};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Typechecks the customer's CDK app before synthesis.
3
+ *
4
+ * `cdk synth` executes infrastructure.ts via tsx, which strips types without
5
+ * checking them — a wrong construct prop synths silently wrong or dies at
6
+ * deploy time in CloudFormation language. Running the app dir's own tsc
7
+ * (`--noEmit -p <dir>`) surfaces the error as a readable compiler diagnostic
8
+ * before any AWS work starts.
9
+ *
10
+ * Warn-first rollout: a failed typecheck never blocks the deploy in this
11
+ * release — the caller reports it as a warning. Environmental problems
12
+ * (no tsconfig, no typescript install, tsc timeout) skip rather than fail,
13
+ * because the deploy has synthesised successfully without this gate for
14
+ * every release before it.
15
+ */
16
+ /** Timeout for the whole-project tsc run (2 minutes). */
17
+ export declare const INFRA_TYPECHECK_TIMEOUT_MS: number;
18
+ export type InfraTypecheckOutcome = {
19
+ status: "passed";
20
+ } | {
21
+ status: "skipped";
22
+ reason: string;
23
+ } | {
24
+ status: "failed";
25
+ details: string;
26
+ };
27
+ export interface InfraTypecheckOptions {
28
+ abortSignal?: AbortSignal;
29
+ }
30
+ export declare function clearInfraTypecheckCache(): void;
31
+ /**
32
+ * Typecheck the CDK app dir with its own tsc. Never throws; environmental
33
+ * failures return `skipped` so the deploy proceeds exactly as it did before
34
+ * this preflight existed.
35
+ *
36
+ * Messages are pre-masked at construction: `details` and every
37
+ * error-derived `reason` route through maskSensitiveOutput before return,
38
+ * so callers may surface them to callback/log sinks directly.
39
+ */
40
+ export declare function runInfraTypecheckPreflight(path: string, options?: InfraTypecheckOptions): Promise<InfraTypecheckOutcome>;
@@ -0,0 +1,2 @@
1
+ var b=Object.defineProperty;var s=(t,e)=>b(t,"name",{value:e,configurable:!0});import{existsSync as v}from"fs";import{createRequire as S}from"module";import{dirname as T,join as p,resolve as w}from"path";import{filterDangerousEnvVars as C,getErrorMessage as h,maskSensitiveOutput as f}from"@fjall/util";import{logger as m}from"@fjall/util/logger";import{spawnWithTimeout as I}from"../../orchestration/spawnHelpers.js";const k=120*1e3,y=1500,a=new Map,d=new Map;function O(){a.clear(),d.clear()}s(O,"clearInfraTypecheckCache");function A(t){const e=f(t.trim());return e.length>y?`\u2026${e.slice(-y)}`:e}s(A,"maskedTail");function o(t,e){return m.debug("CdkService","Infrastructure typecheck skipped",{path:t,reason:e}),{status:"skipped",reason:e}}s(o,"skipped");function E(t){try{const r=S(p(t,"package.json")).resolve("typescript/package.json");return p(T(r),"bin","tsc")}catch(e){m.debug("CdkService","Failed to resolve the app dir's typescript",{path:t,error:f(h(e))});return}}s(E,"resolveCustomerTsc");async function R(t,e={}){const r=w(t),c=a.get(r);if(c!==void 0)return c;const u=d.get(r);if(u!==void 0)return u;const n=_(r,t,e).finally(()=>{d.delete(r)});return d.set(r,n),n}s(R,"runInfraTypecheckPreflight");async function _(t,e,r){if(!v(p(e,"tsconfig.json")))return o(e,"no tsconfig.json in the app dir");const c=E(e);if(c===void 0)return o(e,"typescript is not installed in the app dir");const u=Date.now(),n=await I({command:process.execPath,args:[c,"--noEmit","-p",e],cwd:e,env:C(process.env),timeout:k,...r.abortSignal!==void 0?{abortSignal:r.abortSignal}:{}});switch(n.type){case"success":{if(n.code===0){m.debug("CdkService","Infrastructure typecheck passed",{path:e,durationMs:Date.now()-u});const g={status:"passed"};return a.set(t,g),g}const i=`${n.stdout}
2
+ ${n.stderr}`.trim();if(i==="")return o(e,`tsc exited with code ${n.code} and no diagnostics \u2014 possibly killed by the OS`);const l={status:"failed",details:A(i)};return a.set(t,l),l}case"timeout":{const i=o(e,`tsc timed out after ${k}ms`);return a.set(t,i),i}case"aborted":return o(e,"cancelled");case"enoent":case"spawn_error":return o(e,`tsc failed to start: ${f(n.error.message)}`)}}s(_,"typecheckUncached");export{k as INFRA_TYPECHECK_TIMEOUT_MS,O as clearInfraTypecheckCache,R as runInfraTypecheckPreflight};
@@ -106,8 +106,9 @@ export interface DeployParams {
106
106
  * as one `-c fjall:managedDomain:<zone>=<json>` context arg per entry
107
107
  * (CdkArgumentBuilder); constructs read it ahead of BYO zone props, with
108
108
  * explicit `managedDomain` props still winning. The CLI resolves these via
109
- * `DomainService.resolveDomainForApp`; surfaces without a `fjall/domains/`
110
- * checkout omit the field and constructs fall back to explicit props.
109
+ * `DomainService.resolveDomainForApp` (matching local `fjall/domains/`
110
+ * declarations plus fjall-config.json `domains` entries); surfaces with
111
+ * neither omit the field and constructs fall back to explicit props.
111
112
  */
112
113
  managedDomainBindings?: Record<string, string>;
113
114
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/deploy-core",
3
- "version": "3.9.0",
3
+ "version": "3.10.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",
@@ -88,8 +88,8 @@
88
88
  "@aws-sdk/client-ssm": "^3.1038.0",
89
89
  "@aws-sdk/client-sso-admin": "^3.1038.0",
90
90
  "@aws-sdk/client-sts": "^3.1038.0",
91
- "@fjall/generator": "^3.9.0",
92
- "@fjall/util": "^3.9.0",
91
+ "@fjall/generator": "^3.10.0",
92
+ "@fjall/util": "^3.10.0",
93
93
  "@smithy/node-http-handler": "^4.6.1",
94
94
  "aws-cdk": "^2.1128.1",
95
95
  "tsx": "^4.21.0",
@@ -100,5 +100,5 @@
100
100
  "typescript": "^6.0.3",
101
101
  "vitest": "^4.1.5"
102
102
  },
103
- "gitHead": "7cef1c5c3c09bb333888e65ede3d92b2d88c3c63"
103
+ "gitHead": "ce4e8471194f7e3e5e7df66844bf9387503983f6"
104
104
  }