@fjall/deploy-core 0.95.0 → 0.99.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.
Files changed (79) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/organisations/policies.d.ts +25 -3
  3. package/dist/src/aws/organisations/policies.js +1 -1
  4. package/dist/src/aws/organisations/serviceAccess.js +1 -1
  5. package/dist/src/aws/utils/arnParser.d.ts +24 -0
  6. package/dist/src/aws/utils/arnParser.js +1 -0
  7. package/dist/src/aws/utils/awsErrorHandler.d.ts +19 -0
  8. package/dist/src/aws/utils/awsErrorHandler.js +1 -0
  9. package/dist/src/aws/utils/cloudformationEvents.js +1 -1
  10. package/dist/src/aws/utils/index.d.ts +1 -0
  11. package/dist/src/aws/utils/index.js +1 -1
  12. package/dist/src/index.d.ts +4 -4
  13. package/dist/src/index.js +1 -1
  14. package/dist/src/orchestration/applicationDeploy.js +1 -1
  15. package/dist/src/orchestration/applicationDeployHelpers.d.ts +21 -12
  16. package/dist/src/orchestration/applicationDeployHelpers.js +3 -3
  17. package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -1
  18. package/dist/src/orchestration/codeOnlyDeploy.d.ts +19 -0
  19. package/dist/src/orchestration/codeOnlyDeploy.js +1 -0
  20. package/dist/src/orchestration/detectionPipeline.js +1 -1
  21. package/dist/src/orchestration/dockerBuildHelper.d.ts +19 -2
  22. package/dist/src/orchestration/dockerBuildHelper.js +1 -1
  23. package/dist/src/orchestration/dockerInterface.d.ts +63 -5
  24. package/dist/src/orchestration/index.d.ts +1 -1
  25. package/dist/src/orchestration/openNextBuild.js +3 -3
  26. package/dist/src/orchestration/serviceFactory.d.ts +6 -0
  27. package/dist/src/orchestration/serviceFactory.js +1 -1
  28. package/dist/src/orchestration/stackCleanup.js +1 -1
  29. package/dist/src/orchestration/stepLifecycle.d.ts +29 -0
  30. package/dist/src/orchestration/stepLifecycle.js +1 -0
  31. package/dist/src/services/application/ApplicationStackService.d.ts +9 -1
  32. package/dist/src/services/application/ApplicationStackService.js +1 -1
  33. package/dist/src/services/application/applicationStackHelpers.js +2 -2
  34. package/dist/src/services/index.d.ts +2 -2
  35. package/dist/src/services/index.js +1 -1
  36. package/dist/src/services/infrastructure/CdkArgumentBuilder.d.ts +10 -0
  37. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
  38. package/dist/src/services/infrastructure/CdkCommandRunner.d.ts +1 -0
  39. package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -2
  40. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -1
  41. package/dist/src/services/infrastructure/CdkProcessManager.js +1 -1
  42. package/dist/src/services/infrastructure/CdkService.d.ts +1 -1
  43. package/dist/src/services/infrastructure/CdkService.js +2 -2
  44. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +8 -1
  45. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  46. package/dist/src/services/infrastructure/EcrImageInspectorService.d.ts +32 -0
  47. package/dist/src/services/infrastructure/EcrImageInspectorService.js +1 -0
  48. package/dist/src/services/infrastructure/EcsService.d.ts +96 -0
  49. package/dist/src/services/infrastructure/EcsService.js +1 -0
  50. package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +58 -0
  51. package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -0
  52. package/dist/src/services/infrastructure/cdkServiceHelpers.d.ts +1 -3
  53. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
  54. package/dist/src/services/infrastructure/index.d.ts +3 -0
  55. package/dist/src/services/infrastructure/index.js +1 -1
  56. package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -1
  57. package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
  58. package/dist/src/steps/stepRegistry.js +1 -1
  59. package/dist/src/types/FjallState.js +1 -1
  60. package/dist/src/types/application/ApplicationServiceTypes.js +1 -1
  61. package/dist/src/types/callbacks.d.ts +43 -6
  62. package/dist/src/types/deployment/DeploymentTypes.d.ts +0 -1
  63. package/dist/src/types/errors/ServiceError.js +1 -1
  64. package/dist/src/types/events.d.ts +53 -0
  65. package/dist/src/types/index.d.ts +1 -1
  66. package/dist/src/types/params.d.ts +15 -0
  67. package/dist/src/types/stepDefinitions.d.ts +7 -4
  68. package/dist/src/types/stepDefinitions.js +1 -1
  69. package/package.json +22 -23
  70. package/dist/src/__test-utils__/awsMockHelpers.d.ts +0 -20
  71. package/dist/src/__test-utils__/awsMockHelpers.js +0 -1
  72. package/dist/src/__test-utils__/index.d.ts +0 -1
  73. package/dist/src/__test-utils__/index.js +0 -1
  74. package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.d.ts +0 -6
  75. package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.js +0 -1
  76. package/dist/src/orchestration/__tests__/cascadeTestHelpers.d.ts +0 -12
  77. package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +0 -1
  78. package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.d.ts +0 -9
  79. package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.js +0 -1
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 114 files minified at 2026-04-21T03:05:05.153Z
1
+ 116 files minified at 2026-05-22T01:26:30.966Z
@@ -1,7 +1,29 @@
1
1
  import { type OrganizationsClient } from "@aws-sdk/client-organizations";
2
2
  import { type Result } from "@fjall/generator";
3
+ export interface EnablePolicyTypesOptions {
4
+ /**
5
+ * Override the wait between ListRoots polls while the just-enabled policy
6
+ * types transition from PENDING_ENABLE to ENABLED. Tests pass 0 to skip the
7
+ * sleep; production callers rely on the default.
8
+ */
9
+ pollIntervalMs?: number;
10
+ /**
11
+ * Override the maximum time spent polling for ENABLED. Production callers
12
+ * rely on the default; tests pass a small value to keep the suite fast.
13
+ */
14
+ pollTimeoutMs?: number;
15
+ /**
16
+ * Optional shutdown signal — short-circuits the inter-poll sleep so SIGTERM
17
+ * during a long-running setup orchestration does not wait the full
18
+ * pollIntervalMs before exiting.
19
+ */
20
+ abortSignal?: AbortSignal;
21
+ }
3
22
  /**
4
- * Enable all required policy types on the organisation root.
5
- * Idempotent skips policy types that are already enabled.
23
+ * Enable all required policy types on the organisation root and wait until each
24
+ * propagates to ENABLED. EnablePolicyType can return synchronously while the
25
+ * type is still PENDING_ENABLE; callers that immediately deploy CfnPolicy
26
+ * resources of that type would otherwise race CFN's CreatePolicy and trip a
27
+ * PolicyTypeNotEnabledException. Idempotent — skips types already ENABLED.
6
28
  */
7
- export declare function enablePolicyTypes(client: OrganizationsClient, rootId: string): Promise<Result<void>>;
29
+ export declare function enablePolicyTypes(client: OrganizationsClient, rootId: string, options?: EnablePolicyTypesOptions): Promise<Result<void>>;
@@ -1 +1 @@
1
- import{EnablePolicyTypeCommand as i,PolicyType as r}from"@aws-sdk/client-organizations";import{success as a,failure as c}from"@fjall/generator";import{getErrorMessage as y}from"@fjall/util";import{extractErrorName as l,SDK_TIMEOUT_MS as p}from"./types.js";const m=[r.SERVICE_CONTROL_POLICY,r.TAG_POLICY,r.BACKUP_POLICY,r.AISERVICES_OPT_OUT_POLICY];async function C(t,n){try{for(const e of m)try{await t.send(new i({RootId:n,PolicyType:e}),{abortSignal:AbortSignal.timeout(p)})}catch(o){if(l(o)==="PolicyTypeAlreadyEnabledException")continue;throw o}return a(void 0)}catch(e){return c(new Error(`Failed to enable policy types: ${y(e)}`))}}export{C as enablePolicyTypes};
1
+ import{EnablePolicyTypeCommand as f,ListRootsCommand as u,PolicyType as a}from"@aws-sdk/client-organizations";import{success as d,failure as P}from"@fjall/generator";import{getErrorMessage as b}from"@fjall/util";import{extractErrorName as w,SDK_TIMEOUT_MS as y}from"./types.js";const E=[a.SERVICE_CONTROL_POLICY,a.TAG_POLICY,a.BACKUP_POLICY,a.AISERVICES_OPT_OUT_POLICY],_=2e3,L=6e4;async function g(i,t,e={}){try{for(const o of E)try{await i.send(new f({RootId:t,PolicyType:o}),{abortSignal:AbortSignal.timeout(y)})}catch(r){if(w(r)==="PolicyTypeAlreadyEnabledException")continue;throw r}return await I(i,t,e),d(void 0)}catch(o){return P(new Error(`Failed to enable policy types: ${b(o)}`))}}async function I(i,t,e){const o=e.pollIntervalMs??_,r=e.pollTimeoutMs??L,s=new Set(E),m=Date.now();for(;;){const T=(await i.send(new u({}),{abortSignal:AbortSignal.timeout(y)})).Roots?.find(n=>n.Id===t),l=new Set;for(const n of T?.PolicyTypes??[])n.Status==="ENABLED"&&n.Type&&l.add(n.Type);const c=[...s].filter(n=>!l.has(n));if(c.length===0)return;const p=Date.now()-m;if(p>=r)throw new Error(`Policy types still PENDING_ENABLE after ${p}ms: ${c.join(", ")}`);if(o>0&&(await S(o,e.abortSignal),e.abortSignal?.aborted===!0))throw new Error("Aborted while waiting for policy types to enable")}}function S(i,t){return t?.aborted===!0?Promise.resolve():new Promise(e=>{const o=setTimeout(()=>{t?.removeEventListener("abort",r),e()},i),r=()=>{clearTimeout(o),e()};t?.addEventListener("abort",r,{once:!0})})}export{g as enablePolicyTypes};
@@ -1 +1 @@
1
- import{EnableAWSServiceAccessCommand as n}from"@aws-sdk/client-organizations";import{success as s,failure as e}from"@fjall/generator";import{extractErrorName as m,SDK_TIMEOUT_MS as i,AWS_ERROR_NAMES as t}from"./types.js";import{getErrorMessage as o}from"@fjall/util";const w=["account.amazonaws.com","sso.amazonaws.com","ipam.amazonaws.com","ram.amazonaws.com","backup.amazonaws.com","member.org.stacksets.cloudformation.amazonaws.com","guardduty.amazonaws.com","securityhub.amazonaws.com","config.amazonaws.com","inspector2.amazonaws.com","access-analyzer.amazonaws.com"];async function f(c){try{for(const r of w)try{await c.send(new n({ServicePrincipal:r}),{abortSignal:AbortSignal.timeout(i)})}catch(a){if(m(a)===t.ACCESS_DENIED)return e(new Error(`Access denied when enabling service access for ${r}. Ensure your credentials have organizations:EnableAWSServiceAccess permission.`));throw new Error(`Service principal ${r}: ${o(a)}`)}return s(void 0)}catch(r){return e(new Error(`Failed to enable service access: ${o(r)}`))}}export{f as enableServiceAccess};
1
+ import{EnableAWSServiceAccessCommand as n}from"@aws-sdk/client-organizations";import{success as s,failure as e}from"@fjall/generator";import{extractErrorName as m,SDK_TIMEOUT_MS as i,AWS_ERROR_NAMES as t}from"./types.js";import{getErrorMessage as o}from"@fjall/util";const u=["account.amazonaws.com","sso.amazonaws.com","ipam.amazonaws.com","ram.amazonaws.com","backup.amazonaws.com","member.org.stacksets.cloudformation.amazonaws.com","guardduty.amazonaws.com","securityhub.amazonaws.com","config.amazonaws.com","inspector2.amazonaws.com","access-analyzer.amazonaws.com"];async function f(c){try{for(const a of u)try{await c.send(new n({ServicePrincipal:a}),{abortSignal:AbortSignal.timeout(i)})}catch(r){if(m(r)===t.ACCESS_DENIED)return e(new Error(`Access denied when enabling service access for ${a}. Ensure your credentials have organizations:EnableAWSServiceAccess permission.`));throw new Error(`Service principal ${a}: ${o(r)}`,{cause:r})}return s(void 0)}catch(a){return e(new Error(`Failed to enable service access: ${o(a)}`))}}export{f as enableServiceAccess};
@@ -0,0 +1,24 @@
1
+ /** Parsed representation of an AWS ARN. */
2
+ export interface ParsedArn {
3
+ readonly partition: string;
4
+ readonly service: string;
5
+ readonly region: string;
6
+ readonly accountId: string;
7
+ readonly resource: string;
8
+ }
9
+ /** Parse a raw ARN string into its parts. Returns undefined if not a valid ARN. */
10
+ export declare function parseArn(arn: string): ParsedArn | undefined;
11
+ /** Build a general ARN string. */
12
+ export declare function buildArn(service: string, region: string, accountId: string, resource: string, partition?: string): string;
13
+ /**
14
+ * Extract cluster + service + region + account from an ECS service ARN.
15
+ * Format: arn:aws:ecs:<region>:<account>:service/<cluster>/<service>
16
+ */
17
+ export declare function extractEcsServiceParts(arn: string): {
18
+ clusterName: string;
19
+ serviceName: string;
20
+ region: string;
21
+ accountId: string;
22
+ } | undefined;
23
+ /** Extract just the service name from an ECS service ARN (last path segment). */
24
+ export declare function extractEcsServiceName(arn: string): string | undefined;
@@ -0,0 +1 @@
1
+ function i(r){if(!r.startsWith("arn:"))return;const e=r.split(":");if(!(e.length<6))return{partition:e[1]??"",service:e[2]??"",region:e[3]??"",accountId:e[4]??"",resource:e.slice(5).join(":")}}function u(r,e,n,t,c="aws"){return`arn:${c}:${r}:${e}:${n}:${t}`}function s(r){const e=i(r);if(!e||e.service!=="ecs")return;const n=e.resource.match(/^service\/([^/]+)\/(.+)$/);if(n)return{clusterName:n[1]??"",serviceName:n[2]??"",region:e.region,accountId:e.accountId}}function o(r){return s(r)?.serviceName}export{u as buildArn,o as extractEcsServiceName,s as extractEcsServiceParts,i as parseArn};
@@ -0,0 +1,19 @@
1
+ /** AWS SDK v3 error shape with optional metadata. */
2
+ export interface AwsSdkError extends Error {
3
+ readonly $metadata?: {
4
+ httpStatusCode?: number;
5
+ };
6
+ readonly statusCode?: number;
7
+ }
8
+ export declare function isAwsSdkV3Error(error: unknown): error is Error & {
9
+ name: string;
10
+ };
11
+ export declare function hasAwsSdkMetadata(error: unknown): error is AwsSdkError;
12
+ export declare function getAwsErrorName(error: unknown): string | undefined;
13
+ export declare function getAwsErrorDetails(error: unknown): {
14
+ message: string;
15
+ name: string;
16
+ httpCode?: number;
17
+ };
18
+ export declare function isAccessDeniedError(error: unknown): boolean;
19
+ export declare function isImageAlreadyExistsError(error: unknown): boolean;
@@ -0,0 +1 @@
1
+ function n(t){return t instanceof Error&&typeof t.name=="string"}function a(t){return n(t)?"$metadata"in t||"statusCode"in t:!1}function u(t){if(!(typeof t!="object"||t===null)){if("name"in t&&typeof t.name=="string")return t.name;if("Code"in t&&typeof t.Code=="string")return t.Code}}function o(t){const s=u(t)??"UnknownError",i=t instanceof Error?t.message:String(t);let e;return a(t)&&(e=t.$metadata?.httpStatusCode??t.statusCode),{message:i,name:s,httpCode:e}}function d(t){return n(t)&&t.name==="AccessDeniedException"}function f(t){return n(t)&&t.name==="ImageAlreadyExistsException"}export{o as getAwsErrorDetails,u as getAwsErrorName,a as hasAwsSdkMetadata,d as isAccessDeniedError,n as isAwsSdkV3Error,f as isImageAlreadyExistsError};
@@ -1 +1 @@
1
- import{logger as u}from"@fjall/util/logger";import{getErrorMessage as p,sleep as d}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as y}from"@fjall/util/aws";import{isResourceEvent as x,STACK_NOT_FOUND_PATTERN as b,CDK_NO_STACKS_MATCH as K}from"@fjall/util/aws";import{CF_STACK_RESOURCE_TYPE as S}from"./cloudformationEventTypes.js";import{isTerminalState as v,isSuccessState as w,IpamConcurrencyTracker as C,pollStackEvents as E,handleStackCompletion as M,fetchStackStatus as T,fetchCurrentResources as L}from"./cloudformationEventHelpers.js";class D{aws;seenEventIds=new Set;isMonitoring=!1;pollInterval=null;activeNestedStacks=new Map;failureReasons=new Map;eventHistory=new Map;eventLogger=null;failureAnalyser;eventLogWriterFactory;lastAnalysis=null;deploymentStartTime=null;maxHistorySize=1e3;maxSeenEventIds=1e4;ipamTracker=new C;constructor(t,e){this.aws=t,this.failureAnalyser=e?.failureAnalyser??null,this.eventLogWriterFactory=e?.eventLogWriterFactory}enableLogging(t,e,o,s){this.eventLogWriterFactory&&(this.eventLogger=this.eventLogWriterFactory(t,e,o,s))}async startMonitoring(t,e,o){if(this.isMonitoring){u.debug("CloudFormation","startMonitoring SKIPPED - already monitoring",{stackName:t});return}u.debug("CloudFormation","startMonitoring STARTED",{stackName:t}),this.isMonitoring=!0,this.seenEventIds.clear(),this.eventHistory.clear(),this.deploymentStartTime=new Date;try{await this.pollEvents(t,()=>{})}catch(n){const c=p(n);c.includes(y)||u.debug("CloudFormation","Initial poll failed",{error:c})}let s=5e3;const a=1e4;let r=0,g=0;const l=async()=>{if(this.isMonitoring)try{const n=await this.pollEvents(t,e);r++,n==="throttled"?(g++,s=Math.min(3e4,5e3*Math.pow(2,g-1))):(g=0,r>20&&s<a?s=Math.min(a,s+1e3):r<=20&&(s=5e3)),n===!0?await this.handleStackComplete(t,o):this.pollInterval=setTimeout(l,s)}catch(n){u.debug("CloudFormation","Polling iteration error (continuing)",{error:p(n)}),this.pollInterval=setTimeout(l,s)}};this.pollInterval=setTimeout(l,s)}stopMonitoring(){u.debug("CloudFormation","stopMonitoring called",{wasMonitoring:this.isMonitoring,seenEventCount:this.seenEventIds.size}),this.isMonitoring=!1,this.pollInterval&&(clearTimeout(this.pollInterval),this.pollInterval=null),this.cleanup()}cleanup(){if(this.activeNestedStacks.clear(),this.failureReasons.clear(),this.eventHistory.clear(),this.ipamTracker.clear(),this.seenEventIds.size>this.maxSeenEventIds){const t=Array.from(this.seenEventIds),e=Math.floor(this.maxSeenEventIds/2);this.seenEventIds=new Set(t.slice(-e))}this.eventLogger&&(this.eventLogger=null)}async handleStackComplete(t,e){const o=new Map(this.failureReasons),s=new Map(this.eventHistory),a=this.eventLogger;this.stopMonitoring();const r=await M(this.aws,t,o,a,this.failureAnalyser,s);this.lastAnalysis=r.analysis,e&&e(r.success,r.failureMessage)}getResourceHistory(t){return this.eventHistory.get(t)||[]}getEventHistory(){return new Map(this.eventHistory)}getFailureAnalysis(){return this.lastAnalysis}getFirstFailureReason(){return this.failureReasons.size>0?Array.from(this.failureReasons.values())[0]??null:null}getEventLogger(){return this.eventLogger}getEventLogPath(){return this.eventLogger?.getLogPath()||null}getLogSummary(){return this.eventLogger?.getLogSummary()||null}async waitForStackComplete(t,e={}){const{timeout:o=30*60*1e3,pollInterval:s=2e3,onResourceUpdate:a,onStackComplete:r}=e,g=Date.now();let l,n=!1,c=!1,h;u.debug("CloudFormation","waitForStackComplete called",{stackName:t,timeout:o,pollInterval:s,hasOnResourceUpdate:!!a}),await this.startMonitoring(t,i=>{a&&a(i),i.resourceType===S&&i.logicalId===t&&(l=i.status,v(i.status)&&(n=!0,c=w(i.status),c||(h=this.getFirstFailureReason()||i.statusReason||"Stack operation failed")))},(i,f)=>{r&&r(i,f)});try{let i=!1,f=!1;for(;!n&&this.isMonitoring&&Date.now()-g<o;)await d(s),!i&&Date.now()-g>3e4&&!f&&(f=!0,await this.getStackStatus(t)||u.debug("CloudFormation","Stack not found after 30s, continuing to wait (CDK may be uploading assets)",{stackName:t})),l&&(i=!0);if(this.stopMonitoring(),!n){if(h)return{success:!1,status:"FAILED",failureReason:h,logPath:this.getLogSummary()||void 0};const m=await this.getStackStatus(t);return{success:!1,status:m?.status||"UNKNOWN",failureReason:`Deployment timed out after ${o/1e3} seconds. Stack status: ${m?.status||"UNKNOWN"}`,logPath:this.getLogSummary()||void 0}}return{success:c,status:l,failureReason:h,logPath:this.getLogSummary()||void 0}}catch(i){return this.stopMonitoring(),{success:!1,failureReason:`Monitoring error: ${p(i)}`,logPath:this.getLogSummary()||void 0}}}pollContext(){return{aws:this.aws,seenEventIds:this.seenEventIds,activeNestedStacks:this.activeNestedStacks,failureReasons:this.failureReasons,eventHistory:this.eventHistory,maxHistorySize:this.maxHistorySize,ipamTracker:this.ipamTracker,eventLogger:this.eventLogger}}async pollEvents(t,e){return E(this.pollContext(),t,e)}async getStackStatus(t){return T(this.aws,t)}async getCurrentResources(t){return L(this.aws,t)}}export{K as CDK_NO_STACKS_MATCH,D as CloudFormationEventMonitor,b as STACK_NOT_FOUND_PATTERN,x as isResourceEvent};
1
+ import{logger as u}from"@fjall/util/logger";import{getErrorMessage as p,sleep as d}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as y}from"@fjall/util/aws";import{isResourceEvent as x,STACK_NOT_FOUND_PATTERN as b,CDK_NO_STACKS_MATCH as K}from"@fjall/util/aws";import{CF_STACK_RESOURCE_TYPE as S}from"./cloudformationEventTypes.js";import{isTerminalState as v,isSuccessState as w,IpamConcurrencyTracker as C,pollStackEvents as E,handleStackCompletion as M,fetchStackStatus as T,fetchCurrentResources as L}from"./cloudformationEventHelpers.js";class D{aws;seenEventIds=new Set;isMonitoring=!1;pollInterval=null;activeNestedStacks=new Map;failureReasons=new Map;eventHistory=new Map;eventLogger=null;failureAnalyser;eventLogWriterFactory;lastAnalysis=null;deploymentStartTime=null;maxHistorySize=1e3;maxSeenEventIds=1e4;ipamTracker=new C;constructor(t,e){this.aws=t,this.failureAnalyser=e?.failureAnalyser??null,this.eventLogWriterFactory=e?.eventLogWriterFactory}enableLogging(t,e,o,s){this.eventLogWriterFactory&&(this.eventLogger=this.eventLogWriterFactory(t,e,o,s))}async startMonitoring(t,e,o){if(this.isMonitoring){u.debug("CloudFormation","startMonitoring SKIPPED - already monitoring",{stackName:t});return}u.debug("CloudFormation","startMonitoring STARTED",{stackName:t}),this.isMonitoring=!0,this.seenEventIds.clear(),this.eventHistory.clear(),this.deploymentStartTime=new Date;try{await this.pollEvents(t,()=>{})}catch(n){const c=p(n);c.includes(y)||u.debug("CloudFormation","Initial poll failed",{error:c})}let s=5e3;const a=1e4;let r=0,g=0;const l=async()=>{if(this.isMonitoring)try{const n=await this.pollEvents(t,e);r++,n==="throttled"?(g++,s=Math.min(3e4,5e3*Math.pow(2,g-1))):(g=0,r>20&&s<a?s=Math.min(a,s+1e3):r<=20&&(s=5e3)),n===!0?await this.handleStackComplete(t,o):this.pollInterval=setTimeout(l,s)}catch(n){u.debug("CloudFormation","Polling iteration error (continuing)",{error:p(n)}),this.pollInterval=setTimeout(l,s)}};this.pollInterval=setTimeout(l,s)}stopMonitoring(){u.debug("CloudFormation","stopMonitoring called",{wasMonitoring:this.isMonitoring,seenEventCount:this.seenEventIds.size}),this.isMonitoring=!1,this.pollInterval&&(clearTimeout(this.pollInterval),this.pollInterval=null),this.cleanup()}cleanup(){if(this.activeNestedStacks.clear(),this.failureReasons.clear(),this.eventHistory.clear(),this.ipamTracker.clear(),this.seenEventIds.size>this.maxSeenEventIds){const t=Array.from(this.seenEventIds),e=Math.floor(this.maxSeenEventIds/2);this.seenEventIds=new Set(t.slice(-e))}this.eventLogger&&(this.eventLogger=null)}async handleStackComplete(t,e){const o=new Map(this.failureReasons),s=new Map(this.eventHistory),a=this.eventLogger;this.stopMonitoring();const r=await M(this.aws,t,o,a,this.failureAnalyser,s);this.lastAnalysis=r.analysis,e&&e(r.success,r.failureMessage)}getResourceHistory(t){return this.eventHistory.get(t)||[]}getEventHistory(){return new Map(this.eventHistory)}getFailureAnalysis(){return this.lastAnalysis}getFirstFailureReason(){return this.failureReasons.size>0?Array.from(this.failureReasons.values())[0]??null:null}getEventLogger(){return this.eventLogger}getEventLogPath(){return this.eventLogger?.getLogPath()||null}getLogSummary(){return this.eventLogger?.getLogSummary()||null}async waitForStackComplete(t,e={}){const{timeout:o=1800*1e3,pollInterval:s=2e3,onResourceUpdate:a,onStackComplete:r}=e,g=Date.now();let l,n=!1,c=!1,h;u.debug("CloudFormation","waitForStackComplete called",{stackName:t,timeout:o,pollInterval:s,hasOnResourceUpdate:!!a}),await this.startMonitoring(t,i=>{a&&a(i),i.resourceType===S&&i.logicalId===t&&(l=i.status,v(i.status)&&(n=!0,c=w(i.status),c||(h=this.getFirstFailureReason()||i.statusReason||"Stack operation failed")))},(i,f)=>{r&&r(i,f)});try{let i=!1,f=!1;for(;!n&&this.isMonitoring&&Date.now()-g<o;)await d(s),!i&&Date.now()-g>3e4&&!f&&(f=!0,await this.getStackStatus(t)||u.debug("CloudFormation","Stack not found after 30s, continuing to wait (CDK may be uploading assets)",{stackName:t})),l&&(i=!0);if(this.stopMonitoring(),!n){if(h)return{success:!1,status:"FAILED",failureReason:h,logPath:this.getLogSummary()||void 0};const m=await this.getStackStatus(t);return{success:!1,status:m?.status||"UNKNOWN",failureReason:`Deployment timed out after ${o/1e3} seconds. Stack status: ${m?.status||"UNKNOWN"}`,logPath:this.getLogSummary()||void 0}}return{success:c,status:l,failureReason:h,logPath:this.getLogSummary()||void 0}}catch(i){return this.stopMonitoring(),{success:!1,failureReason:`Monitoring error: ${p(i)}`,logPath:this.getLogSummary()||void 0}}}pollContext(){return{aws:this.aws,seenEventIds:this.seenEventIds,activeNestedStacks:this.activeNestedStacks,failureReasons:this.failureReasons,eventHistory:this.eventHistory,maxHistorySize:this.maxHistorySize,ipamTracker:this.ipamTracker,eventLogger:this.eventLogger}}async pollEvents(t,e){return E(this.pollContext(),t,e)}async getStackStatus(t){return T(this.aws,t)}async getCurrentResources(t){return L(this.aws,t)}}export{K as CDK_NO_STACKS_MATCH,D as CloudFormationEventMonitor,b as STACK_NOT_FOUND_PATTERN,x as isResourceEvent};
@@ -3,3 +3,4 @@ export { type RegionInfo, DEFAULT_REGION, regions, AWS_REGIONS_METADATA, topRegi
3
3
  export { type AwsClientLike, type EventLogWriter, type EventFailureAnalyser, type EventLogWriterFactory, type EventMonitorDeps, CF_STACK_RESOURCE_TYPE } from "./cloudformationEventTypes.js";
4
4
  export { isTerminalState, isSuccessState, isIpamResource, IpamConcurrencyTracker, pollStackEvents, handleStackCompletion, fetchStackStatus, fetchCurrentResources, type PollContext, type StackCompletionResult } from "./cloudformationEventHelpers.js";
5
5
  export { type ResourceEvent, isResourceEvent, STACK_NOT_FOUND_PATTERN, CDK_NO_STACKS_MATCH, type FailureAnalysis, CloudFormationEventMonitor } from "./cloudformationEvents.js";
6
+ export { type AwsSdkError, isAwsSdkV3Error, hasAwsSdkMetadata, getAwsErrorName, getAwsErrorDetails, isAccessDeniedError, isImageAlreadyExistsError } from "./awsErrorHandler.js";
@@ -1 +1 @@
1
- import{stackStatusMap as o}from"./stackStatus.js";import{DEFAULT_REGION as n,regions as r,AWS_REGIONS_METADATA as a,topRegions as s,commonRegions as g,parseRegionList as R,isValidRegion as c,isValidRegionFormat as S,getSuggestions as m,validateRegion as p,validateRegionList as l,filterDuplicateRegions as T,getRegionOptions as u,getRegionOptionsExcluding as E,getRegionName as _,createRegionFormatter as C}from"./regions.js";import{CF_STACK_RESOURCE_TYPE as f}from"./cloudformationEventTypes.js";import{isTerminalState as d,isSuccessState as N,isIpamResource as x,IpamConcurrencyTracker as F,pollStackEvents as k,handleStackCompletion as v,fetchStackStatus as D,fetchCurrentResources as I}from"./cloudformationEventHelpers.js";import{isResourceEvent as M,STACK_NOT_FOUND_PATTERN as h,CDK_NO_STACKS_MATCH as L,CloudFormationEventMonitor as U}from"./cloudformationEvents.js";export{a as AWS_REGIONS_METADATA,L as CDK_NO_STACKS_MATCH,f as CF_STACK_RESOURCE_TYPE,U as CloudFormationEventMonitor,n as DEFAULT_REGION,F as IpamConcurrencyTracker,h as STACK_NOT_FOUND_PATTERN,g as commonRegions,C as createRegionFormatter,I as fetchCurrentResources,D as fetchStackStatus,T as filterDuplicateRegions,_ as getRegionName,u as getRegionOptions,E as getRegionOptionsExcluding,m as getSuggestions,v as handleStackCompletion,x as isIpamResource,M as isResourceEvent,N as isSuccessState,d as isTerminalState,c as isValidRegion,S as isValidRegionFormat,R as parseRegionList,k as pollStackEvents,r as regions,o as stackStatusMap,s as topRegions,p as validateRegion,l as validateRegionList};
1
+ import{stackStatusMap as o}from"./stackStatus.js";import{DEFAULT_REGION as i,regions as s,AWS_REGIONS_METADATA as a,topRegions as n,commonRegions as g,parseRegionList as c,isValidRegion as R,isValidRegionFormat as m,getSuggestions as E,validateRegion as S,validateRegionList as p,filterDuplicateRegions as A,getRegionOptions as l,getRegionOptionsExcluding as T,getRegionName as d,createRegionFormatter as u}from"./regions.js";import{CF_STACK_RESOURCE_TYPE as C}from"./cloudformationEventTypes.js";import{isTerminalState as x,isSuccessState as N,isIpamResource as O,IpamConcurrencyTracker as k,pollStackEvents as D,handleStackCompletion as F,fetchStackStatus as v,fetchCurrentResources as I}from"./cloudformationEventHelpers.js";import{isResourceEvent as h,STACK_NOT_FOUND_PATTERN as w,CDK_NO_STACKS_MATCH as K,CloudFormationEventMonitor as L}from"./cloudformationEvents.js";import{isAwsSdkV3Error as V,hasAwsSdkMetadata as y,getAwsErrorName as G,getAwsErrorDetails as P,isAccessDeniedError as H,isImageAlreadyExistsError as W}from"./awsErrorHandler.js";export{a as AWS_REGIONS_METADATA,K as CDK_NO_STACKS_MATCH,C as CF_STACK_RESOURCE_TYPE,L as CloudFormationEventMonitor,i as DEFAULT_REGION,k as IpamConcurrencyTracker,w as STACK_NOT_FOUND_PATTERN,g as commonRegions,u as createRegionFormatter,I as fetchCurrentResources,v as fetchStackStatus,A as filterDuplicateRegions,P as getAwsErrorDetails,G as getAwsErrorName,d as getRegionName,l as getRegionOptions,T as getRegionOptionsExcluding,E as getSuggestions,F as handleStackCompletion,y as hasAwsSdkMetadata,H as isAccessDeniedError,V as isAwsSdkV3Error,W as isImageAlreadyExistsError,O as isIpamResource,h as isResourceEvent,N as isSuccessState,x as isTerminalState,R as isValidRegion,m as isValidRegionFormat,c as parseRegionList,D as pollStackEvents,s as regions,o as stackStatusMap,n as topRegions,S as validateRegion,p as validateRegionList};
@@ -13,7 +13,7 @@
13
13
  */
14
14
  export { DeploymentEventSchema, DEPLOYMENT_EVENT_TYPES, DEPLOYMENT_EVENT_RESOURCE_CATEGORIES, CASCADE_PHASES, CASCADE_ACCOUNT_STATUSES } from "./types/index.js";
15
15
  export type { DeploymentEvent, DeploymentEventType, DeploymentEventResourceCategory, DeploymentEventCascadePhase, DeploymentEventCascadeAccountStatus } from "./types/index.js";
16
- export type { AwsCredentials, DeployIdentity, DeployCallbacks, StepCompleteStatus, ProgressEvent, ProgressEventType, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, ApiClientInterface, EntitlementsData, DeployParams, DeployOptions, DeploymentType, DeployResult, DestroyParams, DestroyOptions, DestroyResult, OrgConfig, ProviderAccount, SSOSession, Entitlements } from "./types/index.js";
16
+ export type { AwsCredentials, DeployIdentity, DeployCallbacks, StepCompleteStatus, ProgressEvent, ProgressEventType, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent, ApiClientInterface, EntitlementsData, DeployParams, DeployOptions, DeploymentType, DeployResult, DestroyParams, DestroyOptions, DestroyResult, OrgConfig, ProviderAccount, SSOSession, Entitlements } from "./types/index.js";
17
17
  export type { AwsProvider, AwsProviderCredentials, AwsSdkClientConstructor } from "./aws/index.js";
18
18
  export { SimpleAwsProvider } from "./aws/index.js";
19
19
  export { ensureOrganisationExists, describeOrganisation, enablePolicyTypes, enableServiceAccess, enableRamSharing, activateTrustedAccess, enableIpamDelegatedAdmin, updateBackupGlobalSettings, listAccounts, findAccount, createAccount, ensureOrganisationalUnitsExist, placeAccountsInOUs, buildAccountToOUMap, activateCostAllocationTags, checkIdentityCentreStatus, extractErrorName, isOULeaf, registerSecurityDelegates, SECURITY_SERVICE_PRINCIPALS } from "./aws/index.js";
@@ -30,8 +30,8 @@ export { detectPayloadPattern } from "./types/index.js";
30
30
  export { detectDatabase } from "./types/index.js";
31
31
  export { CloudFormationEventMonitor } from "./aws/index.js";
32
32
  export type { AwsClientLike, EventLogWriter, EventFailureAnalyser, EventLogWriterFactory, EventMonitorDeps } from "./aws/index.js";
33
- export { CdkService, CdkArgumentBuilder, CdkProcessManager, CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS, isCdkError, formatInfrastructureError, hasCdkDifferences, parseDiffOutput, CloudFormationService, CloudFormationError, TemplateHashService, TemplateHashError, type TemplateComparisonResult, CdkContextBuilder, emitProgress, PROGRESS_MESSAGES, parseBuildPhase, buildStepContextBuildConfig, convertCloudFormationOutputsToRecord, type CloudFormationOutput, ApplicationStackService } from "./services/index.js";
34
- export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions, ICdkProcessManager, DiffDetails, CloudFormationCallbacks } from "./services/index.js";
33
+ export { CdkService, CdkArgumentBuilder, CdkProcessManager, CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS, isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext, hasCdkDifferences, parseDiffOutput, CloudFormationService, CloudFormationError, EcsService, EcsError, EcsServiceResolver, type CfnExportsClient, TemplateHashService, TemplateHashError, type TemplateComparisonResult, CdkContextBuilder, emitProgress, PROGRESS_MESSAGES, parseBuildPhase, buildStepContextBuildConfig, convertCloudFormationOutputsToRecord, type CloudFormationOutput, ApplicationStackService } from "./services/index.js";
34
+ export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions, ICdkProcessManager, DiffDetails, CloudFormationCallbacks, ECSServiceInfo, ECSClusterInfo, DeploymentStatus, ECSDeploymentOptions } from "./services/index.js";
35
35
  export { CdkError } from "./types/errors/index.js";
36
36
  export { type ServiceErrorDetails, type ServiceError, BaseServiceError, ValidationError, AuthError, AwsError, DeploymentError, NetworkError, FileSystemError, ConfigError, toServiceError } from "./types/errors/index.js";
37
37
  export { filterDangerousEnvVars, maskSensitiveOutput, parseShellArgs, sleep } from "@fjall/util";
@@ -45,7 +45,7 @@ export { destroy } from "./orchestration/index.js";
45
45
  export { partitionAccounts } from "./orchestration/index.js";
46
46
  export { runOpenNextBuild } from "./orchestration/index.js";
47
47
  export { runOrganisationSetup } from "./orchestration/index.js";
48
- export type { OrgSetupPhase, OrgSetupCallbacks, OrgSetupConfig, OrgSetupResult, DockerProvider, DockerProgressCallback, DockerBuildParams, DockerBuildResult, ECRInitParams, ECRInitResult, TagImagesParams, TagImagesResult, DomainDeployProvider, DomainConfig, DomainDeployResult, DeployServices } from "./orchestration/index.js";
48
+ export type { OrgSetupPhase, OrgSetupCallbacks, OrgSetupConfig, OrgSetupResult, DockerProvider, DockerProgressCallback, DockerBuildParams, DockerBuildResult, ECRInitParams, ECRInitResult, TagImagesParams, TagImagesResult, TagByDigestParams, DomainDeployProvider, DomainConfig, DomainDeployResult, DeployServices } from "./orchestration/index.js";
49
49
  export type { FrameworkBuilder, FrameworkDetection, BuildPlan, BuildCommand, BuildCallbacks, DetectionContext, BuildOptions } from "./types/index.js";
50
50
  export { FrameworkRegistry, type ResolvedBuilder } from "./orchestration/index.js";
51
51
  export { openNextBuilder, dockerBuilder } 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 E}from"./types/index.js";import{SimpleAwsProvider as p}from"./aws/index.js";import{ensureOrganisationExists as S,describeOrganisation as l,enablePolicyTypes as c,enableServiceAccess as A,enableRamSharing as T,activateTrustedAccess as O,enableIpamDelegatedAdmin as m,updateBackupGlobalSettings as P,listAccounts as u,findAccount as _,createAccount as d,ensureOrganisationalUnitsExist as R,placeAccountsInOUs as C,buildAccountToOUMap as N,activateCostAllocationTags as f,checkIdentityCentreStatus as x,extractErrorName as D,isOULeaf as g,registerSecurityDelegates as I,SECURITY_SERVICE_PRINCIPALS as L}from"./aws/index.js";import{STEP_IDS as F,STEP_NAMES as y,INFRASTRUCTURE_STEP_NAMES as U,INFRA_STEP_NAME as v}from"./types/index.js";import{ProgressReporter as h,APPLICATION_STACKS as Y,ORGANISATION_TYPES as b,APPLICATION_DEPLOY_ORDER as G,APPLICATION_DESTROY_ORDER as B,OPENNEXT_DEPLOY_ORDER as w,OPENNEXT_DESTROY_ORDER as H,PARALLEL_DEPLOY_GROUPS as K,PARALLEL_DESTROY_GROUPS as V,OPENNEXT_PARALLEL_GROUPS as X,PARALLEL_OPERATION_TYPES as j,isApplicationOperation as q,isOrganisationOperation as z,getParallelDeployGroups as J,getParallelDestroyGroups as Q,getApplicationDeployOrder as W,getApplicationDestroyOrder as Z,getApplicationStackName as $,getOrganisationStackName as ee,isApplicationStack as re,getApplicationStepName as te,getApplicationStepId as oe,toPascalCase as ae,isOpenNextPattern as ie,OPENNEXT_PATTERNS as Ee,deriveResourcesFromManifestStacks as se,STACK_NOT_FOUND_PATTERN as pe,STACK_FAILED_STATE_PATTERN as ne,CDK_NO_STACKS_MATCH as Se,INFRASTRUCTURE_FILENAME as le,ApplicationError as ce,wrapApplicationError as Ae,FjallStateFileSchema as Te,readStateFile as Oe,writeStateFile as me,createEmptyState as Pe,deleteStateFile as ue,updateTemplateHash as _e,getStateFilePath as de,stubCallerIdentity as Re}from"./types/index.js";import{detectPattern as Ne}from"./types/index.js";import{detectPayloadPattern as xe}from"./types/index.js";import{detectDatabase as ge}from"./types/index.js";import{CloudFormationEventMonitor as Le}from"./aws/index.js";import{CdkService as Fe,CdkArgumentBuilder as ye,CdkProcessManager as Ue,CdkEventMonitor as ve,startStackMonitoring as Me,DEFAULT_DEPLOY_TIMEOUT_MS as he,isCdkError as Ye,formatInfrastructureError as be,hasCdkDifferences as Ge,parseDiffOutput as Be,CloudFormationService as we,CloudFormationError as He,TemplateHashService as Ke,TemplateHashError as Ve,CdkContextBuilder as Xe,emitProgress as je,PROGRESS_MESSAGES as qe,parseBuildPhase as ze,buildStepContextBuildConfig as Je,convertCloudFormationOutputsToRecord as Qe,ApplicationStackService as We}from"./services/index.js";import{CdkError as $e}from"./types/errors/index.js";import{BaseServiceError as rr,ValidationError as tr,AuthError as or,AwsError as ar,DeploymentError as ir,NetworkError as Er,FileSystemError as sr,ConfigError as pr,toServiceError as nr}from"./types/errors/index.js";import{filterDangerousEnvVars as lr,maskSensitiveOutput as cr,parseShellArgs as Ar,sleep as Tr}from"@fjall/util";import{hasDockerfile as mr}from"./util/dockerfileDetection.js";import{createSequencedCallbacks as ur}from"./util/sequencedCallbacks.js";import{fileExists as dr}from"@fjall/util/fsHelpers";import{success as Cr,failure as Nr,isSuccess as fr,isFailure as xr}from"@fjall/generator";import{deploy as gr}from"./orchestration/index.js";import{destroy as Lr}from"./orchestration/index.js";import{partitionAccounts as Fr}from"./orchestration/index.js";import{runOpenNextBuild as Ur}from"./orchestration/index.js";import{runOrganisationSetup as Mr}from"./orchestration/index.js";import{FrameworkRegistry as Yr}from"./orchestration/index.js";import{openNextBuilder as Gr,dockerBuilder as Br}from"./orchestration/index.js";import{StepRegistry as Hr,getDestroyStepId as Kr}from"./steps/index.js";export{G as APPLICATION_DEPLOY_ORDER,B as APPLICATION_DESTROY_ORDER,Y as APPLICATION_STACKS,ce as ApplicationError,We as ApplicationStackService,or as AuthError,ar as AwsError,rr as BaseServiceError,E as CASCADE_ACCOUNT_STATUSES,i as CASCADE_PHASES,Se as CDK_NO_STACKS_MATCH,ye as CdkArgumentBuilder,Xe as CdkContextBuilder,$e as CdkError,ve as CdkEventMonitor,Ue as CdkProcessManager,Fe as CdkService,He as CloudFormationError,Le as CloudFormationEventMonitor,we as CloudFormationService,pr as ConfigError,he as DEFAULT_DEPLOY_TIMEOUT_MS,a as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,o as DEPLOYMENT_EVENT_TYPES,ir as DeploymentError,t as DeploymentEventSchema,sr as FileSystemError,Te as FjallStateFileSchema,Yr as FrameworkRegistry,le as INFRASTRUCTURE_FILENAME,U as INFRASTRUCTURE_STEP_NAMES,v as INFRA_STEP_NAME,Er as NetworkError,w as OPENNEXT_DEPLOY_ORDER,H as OPENNEXT_DESTROY_ORDER,X as OPENNEXT_PARALLEL_GROUPS,Ee as OPENNEXT_PATTERNS,b as ORGANISATION_TYPES,K as PARALLEL_DEPLOY_GROUPS,V as PARALLEL_DESTROY_GROUPS,j as PARALLEL_OPERATION_TYPES,qe as PROGRESS_MESSAGES,h as ProgressReporter,L as SECURITY_SERVICE_PRINCIPALS,ne as STACK_FAILED_STATE_PATTERN,pe as STACK_NOT_FOUND_PATTERN,F as STEP_IDS,y as STEP_NAMES,p as SimpleAwsProvider,Hr as StepRegistry,Ve as TemplateHashError,Ke as TemplateHashService,tr as ValidationError,f as activateCostAllocationTags,O as activateTrustedAccess,N as buildAccountToOUMap,Je as buildStepContextBuildConfig,x as checkIdentityCentreStatus,Qe as convertCloudFormationOutputsToRecord,d as createAccount,Pe as createEmptyState,ur as createSequencedCallbacks,ue as deleteStateFile,gr as deploy,se as deriveResourcesFromManifestStacks,l as describeOrganisation,Lr as destroy,ge as detectDatabase,Ne as detectPattern,xe as detectPayloadPattern,Br as dockerBuilder,je as emitProgress,m as enableIpamDelegatedAdmin,c as enablePolicyTypes,T as enableRamSharing,A as enableServiceAccess,S as ensureOrganisationExists,R as ensureOrganisationalUnitsExist,D as extractErrorName,Nr as failure,dr as fileExists,lr as filterDangerousEnvVars,_ as findAccount,be as formatInfrastructureError,W as getApplicationDeployOrder,Z as getApplicationDestroyOrder,$ as getApplicationStackName,oe as getApplicationStepId,te as getApplicationStepName,Kr as getDestroyStepId,ee as getOrganisationStackName,J as getParallelDeployGroups,Q as getParallelDestroyGroups,de as getStateFilePath,Ge as hasCdkDifferences,mr as hasDockerfile,q as isApplicationOperation,re as isApplicationStack,Ye as isCdkError,xr as isFailure,g as isOULeaf,ie as isOpenNextPattern,z as isOrganisationOperation,fr as isSuccess,u as listAccounts,cr as maskSensitiveOutput,Gr as openNextBuilder,ze as parseBuildPhase,Be as parseDiffOutput,Ar as parseShellArgs,Fr as partitionAccounts,C as placeAccountsInOUs,Oe as readStateFile,I as registerSecurityDelegates,Ur as runOpenNextBuild,Mr as runOrganisationSetup,Tr as sleep,Me as startStackMonitoring,Re as stubCallerIdentity,Cr as success,ae as toPascalCase,nr as toServiceError,P as updateBackupGlobalSettings,_e as updateTemplateHash,Ae as wrapApplicationError,me as writeStateFile};
1
+ import{DeploymentEventSchema as t,DEPLOYMENT_EVENT_TYPES as o,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as a,CASCADE_PHASES as i,CASCADE_ACCOUNT_STATUSES as E}from"./types/index.js";import{SimpleAwsProvider as S}from"./aws/index.js";import{ensureOrganisationExists as p,describeOrganisation as c,enablePolicyTypes as l,enableServiceAccess as A,enableRamSharing as T,activateTrustedAccess as u,enableIpamDelegatedAdmin as O,updateBackupGlobalSettings as m,listAccounts as P,findAccount as _,createAccount as d,ensureOrganisationalUnitsExist as R,placeAccountsInOUs as C,buildAccountToOUMap as N,activateCostAllocationTags as f,checkIdentityCentreStatus as g,extractErrorName as x,isOULeaf as D,registerSecurityDelegates as I,SECURITY_SERVICE_PRINCIPALS as L}from"./aws/index.js";import{STEP_IDS as F,STEP_NAMES as v,INFRASTRUCTURE_STEP_NAMES as y,INFRA_STEP_NAME as U}from"./types/index.js";import{ProgressReporter as h,APPLICATION_STACKS as Y,ORGANISATION_TYPES as b,APPLICATION_DEPLOY_ORDER as G,APPLICATION_DESTROY_ORDER as B,OPENNEXT_DEPLOY_ORDER as w,OPENNEXT_DESTROY_ORDER as H,PARALLEL_DEPLOY_GROUPS as K,PARALLEL_DESTROY_GROUPS as V,OPENNEXT_PARALLEL_GROUPS as X,PARALLEL_OPERATION_TYPES as j,isApplicationOperation as q,isOrganisationOperation as z,getParallelDeployGroups as J,getParallelDestroyGroups as Q,getApplicationDeployOrder as W,getApplicationDestroyOrder as Z,getApplicationStackName as $,getOrganisationStackName as ee,isApplicationStack as re,getApplicationStepName as te,getApplicationStepId as oe,toPascalCase as ae,isOpenNextPattern as ie,OPENNEXT_PATTERNS as Ee,deriveResourcesFromManifestStacks as se,STACK_NOT_FOUND_PATTERN as Se,STACK_FAILED_STATE_PATTERN as ne,CDK_NO_STACKS_MATCH as pe,INFRASTRUCTURE_FILENAME as ce,ApplicationError as le,wrapApplicationError as Ae,FjallStateFileSchema as Te,readStateFile as ue,writeStateFile as Oe,createEmptyState as me,deleteStateFile as Pe,updateTemplateHash as _e,getStateFilePath as de,stubCallerIdentity as Re}from"./types/index.js";import{detectPattern as Ne}from"./types/index.js";import{detectPayloadPattern as ge}from"./types/index.js";import{detectDatabase as De}from"./types/index.js";import{CloudFormationEventMonitor as Le}from"./aws/index.js";import{CdkService as Fe,CdkArgumentBuilder as ve,CdkProcessManager as ye,CdkEventMonitor as Ue,startStackMonitoring as Me,DEFAULT_DEPLOY_TIMEOUT_MS as he,isCdkError as Ye,formatInfrastructureError as be,getStructuralHint as Ge,getSourceContext as Be,hasCdkDifferences as we,parseDiffOutput as He,CloudFormationService as Ke,CloudFormationError as Ve,EcsService as Xe,EcsError as je,EcsServiceResolver as qe,TemplateHashService as ze,TemplateHashError as Je,CdkContextBuilder as Qe,emitProgress as We,PROGRESS_MESSAGES as Ze,parseBuildPhase as $e,buildStepContextBuildConfig as er,convertCloudFormationOutputsToRecord as rr,ApplicationStackService as tr}from"./services/index.js";import{CdkError as ar}from"./types/errors/index.js";import{BaseServiceError as Er,ValidationError as sr,AuthError as Sr,AwsError as nr,DeploymentError as pr,NetworkError as cr,FileSystemError as lr,ConfigError as Ar,toServiceError as Tr}from"./types/errors/index.js";import{filterDangerousEnvVars as Or,maskSensitiveOutput as mr,parseShellArgs as Pr,sleep as _r}from"@fjall/util";import{hasDockerfile as Rr}from"./util/dockerfileDetection.js";import{createSequencedCallbacks as Nr}from"./util/sequencedCallbacks.js";import{fileExists as gr}from"@fjall/util/fsHelpers";import{success as Dr,failure as Ir,isSuccess as Lr,isFailure as kr}from"@fjall/generator";import{deploy as vr}from"./orchestration/index.js";import{destroy as Ur}from"./orchestration/index.js";import{partitionAccounts as hr}from"./orchestration/index.js";import{runOpenNextBuild as br}from"./orchestration/index.js";import{runOrganisationSetup as Br}from"./orchestration/index.js";import{FrameworkRegistry as Hr}from"./orchestration/index.js";import{openNextBuilder as Vr,dockerBuilder as Xr}from"./orchestration/index.js";import{StepRegistry as qr,getDestroyStepId as zr}from"./steps/index.js";export{G as APPLICATION_DEPLOY_ORDER,B as APPLICATION_DESTROY_ORDER,Y as APPLICATION_STACKS,le as ApplicationError,tr as ApplicationStackService,Sr as AuthError,nr as AwsError,Er as BaseServiceError,E as CASCADE_ACCOUNT_STATUSES,i as CASCADE_PHASES,pe as CDK_NO_STACKS_MATCH,ve as CdkArgumentBuilder,Qe as CdkContextBuilder,ar as CdkError,Ue as CdkEventMonitor,ye as CdkProcessManager,Fe as CdkService,Ve as CloudFormationError,Le as CloudFormationEventMonitor,Ke as CloudFormationService,Ar as ConfigError,he as DEFAULT_DEPLOY_TIMEOUT_MS,a as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,o as DEPLOYMENT_EVENT_TYPES,pr as DeploymentError,t as DeploymentEventSchema,je as EcsError,Xe as EcsService,qe as EcsServiceResolver,lr as FileSystemError,Te as FjallStateFileSchema,Hr as FrameworkRegistry,ce as INFRASTRUCTURE_FILENAME,y as INFRASTRUCTURE_STEP_NAMES,U as INFRA_STEP_NAME,cr as NetworkError,w as OPENNEXT_DEPLOY_ORDER,H as OPENNEXT_DESTROY_ORDER,X as OPENNEXT_PARALLEL_GROUPS,Ee as OPENNEXT_PATTERNS,b as ORGANISATION_TYPES,K as PARALLEL_DEPLOY_GROUPS,V as PARALLEL_DESTROY_GROUPS,j as PARALLEL_OPERATION_TYPES,Ze as PROGRESS_MESSAGES,h as ProgressReporter,L as SECURITY_SERVICE_PRINCIPALS,ne as STACK_FAILED_STATE_PATTERN,Se as STACK_NOT_FOUND_PATTERN,F as STEP_IDS,v as STEP_NAMES,S as SimpleAwsProvider,qr as StepRegistry,Je as TemplateHashError,ze as TemplateHashService,sr as ValidationError,f as activateCostAllocationTags,u as activateTrustedAccess,N as buildAccountToOUMap,er as buildStepContextBuildConfig,g as checkIdentityCentreStatus,rr as convertCloudFormationOutputsToRecord,d as createAccount,me as createEmptyState,Nr as createSequencedCallbacks,Pe as deleteStateFile,vr as deploy,se as deriveResourcesFromManifestStacks,c as describeOrganisation,Ur as destroy,De as detectDatabase,Ne as detectPattern,ge as detectPayloadPattern,Xr as dockerBuilder,We as emitProgress,O as enableIpamDelegatedAdmin,l as enablePolicyTypes,T as enableRamSharing,A as enableServiceAccess,p as ensureOrganisationExists,R as ensureOrganisationalUnitsExist,x as extractErrorName,Ir as failure,gr as fileExists,Or as filterDangerousEnvVars,_ as findAccount,be as formatInfrastructureError,W as getApplicationDeployOrder,Z as getApplicationDestroyOrder,$ as getApplicationStackName,oe as getApplicationStepId,te as getApplicationStepName,zr as getDestroyStepId,ee as getOrganisationStackName,J as getParallelDeployGroups,Q as getParallelDestroyGroups,Be as getSourceContext,de as getStateFilePath,Ge as getStructuralHint,we as hasCdkDifferences,Rr as hasDockerfile,q as isApplicationOperation,re as isApplicationStack,Ye as isCdkError,kr as isFailure,D as isOULeaf,ie as isOpenNextPattern,z as isOrganisationOperation,Lr as isSuccess,P as listAccounts,mr as maskSensitiveOutput,Vr as openNextBuilder,$e as parseBuildPhase,He as parseDiffOutput,Pr as parseShellArgs,hr as partitionAccounts,C as placeAccountsInOUs,ue as readStateFile,I as registerSecurityDelegates,br as runOpenNextBuild,Br as runOrganisationSetup,_r as sleep,Me as startStackMonitoring,Re as stubCallerIdentity,Dr as success,ae as toPascalCase,Tr as toServiceError,m as updateBackupGlobalSettings,_e as updateTemplateHash,Ae as wrapApplicationError,Oe as writeStateFile};
@@ -1 +1 @@
1
- import{success as R,failure as m}from"@fjall/generator";import{logger as x}from"@fjall/util/logger";import{stubCallerIdentity as I}from"../types/deployment/index.js";import{getApplicationDeployOrder as L,getApplicationStackName as B,getApplicationStepName as T,getApplicationStepId as F}from"../types/operations.js";import{CdkContextBuilder as M}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as U,bootstrapOrFail as E}from"./contextHelpers.js";import{runDetectionPipeline as H}from"./detectionPipeline.js";import{getParallelPhase2Stacks as W,deployParallelPhase as $,deployStackSequential as j,runDockerPreCompute as q,deployAllStacks as z,createBuildCallbacks as G}from"./applicationDeployHelpers.js";async function ee(l,n,o){const{callbacks:e,options:i}=l,h=Date.now(),s=M.buildDeploymentContext({deployType:"application",target:o.appName,path:o.path,region:n.awsProvider.getRegion(),callerIdentity:I(n.awsProvider.getAccountId()),...U({orgConfig:l.orgConfig,identity:l.identity,skipOidc:l.options?.skipOidc})},{verbose:i?.verbose,infraOnly:i?.infraOnly},l.orgConfig),c=n.frameworkRegistry.resolve({appPath:o.path});let p;if(c){p=c.builder.plan({appPath:o.path},c.detection);const t=G(e),a=await c.builder.build(o.path,p,t,{skipBuild:i?.skipBuild,infraOnly:i?.infraOnly});if(!a.success)return e.onError?.(a.error),m(a.error)}if(i?.deployOnly)return e.onLog?.("Deploy-only mode \u2014 skipping change detection","info"),z(l,n,o,s,h,p);e.onLog?.("Analysing infrastructure\u2026","info");const u=await H(o,n,s,e);if(!u.success)return e.onError?.(u.error),m(u.error);const r=u.data;if(await e.onDetectionComplete?.({...r,builderName:c?.builder.name??"unknown"}),!r.hasDifferences&&!i?.force)return e.onLog?.("No infrastructure changes detected","info"),R({target:o.appName,deploymentType:"application",durationMs:Date.now()-h});const w=await E(n,s,e);if(!w.success)return w;const f=p?p.deployOrder:L({pattern:r.pattern,resources:r.resources}),g=f.length,d={},y=new Map;for(let t=0;t<f.length;t++){const a=f[t],k=B(o.appName,a),S=F(a,"deploy"),D=T(a,"deploy");if(!(r.stackChanges.get(k)??!0)&&!i?.force){e.onStepStart?.(S,D,t,g),e.onLog?.(`Skipping ${a} \u2014 no changes detected`,"info"),e.onStepComplete?.(S,D,"skipped",t,g);continue}const b=W(f,t,r.stackChanges,i?.force);if(b.length>=2){const A=await $(b,o,n,s,e,t,g,r,d,y);if(!A.success)return m(A.error);t+=b.length-1;continue}const O=await q(a,l,n,o,e,r.hasDockerfile);if(O)return O;const N=await j(a,n,s,e,t,g,d);if(!N.success)return m(N.error);const P=r.currentHashes.get(k);P&&y.set(k,P)}if(y.size>0){const t=await n.hashService.updateStateAfterDeploy(o.path,y);t.success||(x.debug("applicationDeploy","Failed to update state file",{error:t.error.message}),e.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy unchanged stacks: ${t.error.message}`,"warn"))}const C=await n.stackService.resolveWebsiteUrl(o.appName);return C&&(d.websiteUrl=C),R({target:o.appName,deploymentType:"application",outputs:Object.keys(d).length>0?d:void 0,durationMs:Date.now()-h})}export{ee as deployApplication};
1
+ import{join as H}from"path";import{success as M,failure as g}from"@fjall/generator";import{logger as O}from"@fjall/util/logger";import{maskSensitiveOutput as w,toPascalCase as K}from"@fjall/util";import{parseDockerServicesFromManifest as W}from"@fjall/util/manifest";import{stubCallerIdentity as $}from"../types/deployment/index.js";import{getApplicationDeployOrder as G,getApplicationStackName as q,getApplicationStepName as B,getApplicationStepId as U,APPLICATION_STACKS as _}from"../types/operations.js";import{CdkContextBuilder as z}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as Y,bootstrapOrFail as J}from"./contextHelpers.js";import{runDetectionPipeline as Q}from"./detectionPipeline.js";import{STEP_IDS as x}from"../types/stepDefinitions.js";import{DOCKER_BUILD_STEP_NAME as j,runDockerBuild as V}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as X,deployParallelPhase as Z,deployStackSequential as ee,runDockerPreCompute as te,createBuildCallbacks as oe}from"./applicationDeployHelpers.js";import{deployCodeOnly as re}from"./codeOnlyDeploy.js";const N="applicationDeploy";function ne(s){const a=Object.entries(s);if(a.length===0)return;const o={};for(const[t,i]of a){const D=`${K(t)}ImageTag`;o[D]=i}return o}async function be(s,a,o){const{callbacks:t,options:i}=s,D=Date.now(),S=z.buildDeploymentContext({deployType:"application",target:o.appName,path:o.path,region:a.awsProvider.getRegion(),callerIdentity:$(a.awsProvider.getAccountId()),...Y({orgConfig:s.orgConfig,identity:s.identity,skipOidc:s.options?.skipOidc})},{verbose:i?.verbose,infraOnly:i?.infraOnly},s.orgConfig),u=a.frameworkRegistry.resolve({appPath:o.path});let C;const m=!!i?.imageTag;if(u&&!m){C=u.builder.plan({appPath:o.path},u.detection);const e=oe(t),r=await u.builder.build(o.path,C,e,{skipBuild:i?.skipBuild,infraOnly:i?.infraOnly});if(!r.success){const n=new Error(w(r.error.message));return t.onError?.(n),g(n)}}if(i?.deployOnly||m){t.onLog?.(m?`Rollback mode \u2014 rolling to image tag ${i?.imageTag}`:"Deploy-only mode \u2014 skipping infrastructure pipeline","info");const e=H(o.path,"cdk.out"),r=W(e),n=r.length>0,l=u?.detection.hasDockerfile===!0,p=n||l;O.debug(N,"Deploy-only branch entered",{isRollback:m,imageTag:i?.imageTag,appName:o.appName,appPath:o.path,cdkOutPath:e,dockerProviderAvailable:s.dockerProvider!==void 0,builderName:u?.builder.name,hasDockerfileFromManifest:n,hasDockerfileFromDisk:l,hasDockerfile:p,manifestDockerServiceCount:r.length,manifestDockerPaths:r.map(d=>d.docker.path)}),!n&&!l&&t.onLog?.("No Dockerfile detected via manifest or appPath \u2014 skipping Docker build. If this app uses a cross-repo Dockerfile, ensure a full deploy has run first to populate cdk.out/fjall-manifest.json.","warn");let h={};if(!m&&s.dockerProvider!==void 0&&p){O.debug(N,"Running Docker build before code-only deploy",{source:n?"manifest":"disk"});const d=await V(s,a,o,t);if(!d.success)return g(d.error);h=d.data}else O.debug(N,"Skipping Docker build",{reason:m?"rollback":s.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});return re(s,a,o,h)}t.onLog?.("Analysing infrastructure\u2026","info");const E=await Q(o,a,S,t);if(!E.success){const e=new Error(w(E.error.message));return t.onError?.(e),g(e)}const c=E.data;try{await t.onDetectionComplete?.({...c,builderName:u?.builder.name??"unknown"})}catch(e){const r=e instanceof Error?e.message:String(e),n=new Error(w(r));return t.onError?.(n),g(n)}const f=C?C.deployOrder:G({pattern:c.pattern,resources:c.resources}),k=f.length;if(!c.hasDifferences&&!i?.force){t.onLog?.("No infrastructure changes detected","info");const e=c.hasDockerfile&&s.dockerProvider!==void 0&&f.includes(_.COMPUTE);for(let n=0;n<f.length;n++){const l=f[n];e&&l===_.COMPUTE&&(t.onStepStart?.(x.DOCKER_OPERATIONS,j),t.onStepComplete?.(x.DOCKER_OPERATIONS,j,"skipped"));const p=U(l,"deploy"),h=B(l,"deploy");t.onStepStart?.(p,h,n,k),t.onStepComplete?.(p,h,"skipped",n,k)}const r=await a.stackService.resolveWebsiteUrl(o.appName);return M({target:o.appName,deploymentType:"application",outputs:r?{websiteUrl:r}:void 0,noChanges:!0,durationMs:Date.now()-D})}const T=await J(a,S,t);if(!T.success)return T;const b={},P=new Map;for(let e=0;e<f.length;e++){const r=f[e],n=q(o.appName,r),l=U(r,"deploy"),p=B(r,"deploy");if(!(c.stackChanges.get(n)??!0)&&!i?.force){t.onStepStart?.(l,p,e,k),t.onLog?.(`Skipping ${r} \u2014 no changes detected`,"info"),t.onStepComplete?.(l,p,"skipped",e,k);continue}const d=X(f,e,c.stackChanges,o.appName,i?.force);if(d.length>=2){const L=await Z(d,o,a,S,t,e,k,c,b,P);if(!L.success)return g(L.error);e+=d.length-1;continue}const y=await te(r,s,a,o,t,c.hasDockerfile);if(y!==null&&!y.success)return g(y.error);const F=y!==null&&y.success?y.data.contentHashTagsByService:{},A=ne(F),I=await ee(r,a,S,t,e,k,b,A!==void 0?{parameters:A}:void 0);if(!I.success)return g(I.error);const v=c.currentHashes.get(n);v&&P.set(n,v)}if(P.size>0){const e=await a.hashService.updateStateAfterDeploy(o.path,P);if(!e.success){const r=w(e.error.message);O.debug(N,"Failed to update state file",{error:r}),t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy unchanged stacks: ${r}`,"warn")}}const R=await a.stackService.resolveWebsiteUrl(o.appName);return R&&(b.websiteUrl=R),M({target:o.appName,deploymentType:"application",outputs:Object.keys(b).length>0?b:void 0,durationMs:Date.now()-D})}export{be as deployApplication};
@@ -1,39 +1,48 @@
1
1
  import { type Result } from "@fjall/generator";
2
- import type { DeployParams, DeployResult } from "../types/params.js";
2
+ import type { DeployParams } from "../types/params.js";
3
3
  import type { DeploymentContext } from "../types/deployment/DeploymentTypes.js";
4
4
  import type { DeployCallbacks } from "../types/callbacks.js";
5
5
  import type { ApplicationOperation, ApplicationStack } from "../types/operations.js";
6
6
  import type { DeployServices } from "./serviceFactory.js";
7
7
  import type { DetectionResult } from "./detectionPipeline.js";
8
- import type { BuildCallbacks, BuildPlan } from "../types/frameworkBuilder.js";
8
+ import type { BuildCallbacks } from "../types/frameworkBuilder.js";
9
9
  /**
10
10
  * Identify Phase 2 stacks (Storage, Messaging, Database) that are consecutive
11
11
  * in the deploy order and have changes. Returns them for parallel deployment
12
12
  * only if there are at least 2.
13
13
  */
14
- export declare function getParallelPhase2Stacks(deployOrder: readonly ApplicationStack[], currentIndex: number, stackChanges: Map<string, boolean>, force?: boolean): ApplicationStack[];
14
+ export declare function getParallelPhase2Stacks(deployOrder: readonly ApplicationStack[], currentIndex: number, stackChanges: Map<string, boolean>, appName: string, force?: boolean): ApplicationStack[];
15
15
  /**
16
16
  * Deploy a group of stacks in parallel, reporting step events for each.
17
17
  */
18
18
  export declare function deployParallelPhase(stacks: ApplicationStack[], operation: ApplicationOperation, services: DeployServices, context: DeploymentContext, callbacks: DeployCallbacks, startIndex: number, totalSteps: number, detection: DetectionResult, allOutputs: Record<string, string>, deployedHashes: Map<string, string>): Promise<Result<void>>;
19
+ interface DeployStackSequentialOptions {
20
+ parameters?: Record<string, string>;
21
+ }
19
22
  /**
20
23
  * Deploy a single stack with step lifecycle events and output collection.
21
24
  */
22
- export declare function deployStackSequential(stack: ApplicationStack, services: DeployServices, context: DeploymentContext, callbacks: DeployCallbacks, stepIndex: number, totalSteps: number, allOutputs: Record<string, string>): Promise<Result<void>>;
25
+ export declare function deployStackSequential(stack: ApplicationStack, services: DeployServices, context: DeploymentContext, callbacks: DeployCallbacks, stepIndex: number, totalSteps: number, allOutputs: Record<string, string>, deployOptions?: DeployStackSequentialOptions): Promise<Result<void>>;
26
+ export interface DockerPreComputeOutcome {
27
+ /**
28
+ * Map from service name (with optional target suffix) to the resolved
29
+ * content-hash tag pushed during this build phase. Empty for the legacy
30
+ * single-image fallback path and for welcome-image setup; the orchestrator
31
+ * skips `--parameters` plumbing in those cases.
32
+ */
33
+ contentHashTagsByService: Record<string, string>;
34
+ }
23
35
  /**
24
36
  * Run Docker build/push or welcome image setup before the Compute stack.
25
- * Returns a failure Result if Docker operations fail, or null if not applicable/successful.
37
+ * Returns a `Result<DockerPreComputeOutcome>` on success carrying the
38
+ * per-service content-hash tag map, a failure on docker error, or `null`
39
+ * when no docker work is required for this stack.
26
40
  */
27
- export declare function runDockerPreCompute(stack: ApplicationStack, params: DeployParams, services: DeployServices, operation: ApplicationOperation, callbacks: DeployCallbacks, hasDockerfileOnDisk: boolean): Promise<Result<DeployResult> | null>;
28
- /**
29
- * Deploy all stacks unconditionally (deployOnly mode).
30
- * Skips CDK synth/hash comparison — uses the framework registry to determine
31
- * deploy order, then deploys every stack sequentially.
32
- */
33
- export declare function deployAllStacks(params: DeployParams, services: DeployServices, operation: ApplicationOperation, context: DeploymentContext, startTime: number, plan?: BuildPlan): Promise<Result<DeployResult>>;
41
+ export declare function runDockerPreCompute(stack: ApplicationStack, params: DeployParams, services: DeployServices, operation: ApplicationOperation, callbacks: DeployCallbacks, hasDockerfileOnDisk: boolean): Promise<Result<DockerPreComputeOutcome> | null>;
34
42
  /**
35
43
  * Bridge DeployCallbacks to BuildCallbacks.
36
44
  * Fires both legacy OpenNext-specific callbacks and generic build callbacks
37
45
  * for backwards compatibility during the transition period.
38
46
  */
39
47
  export declare function createBuildCallbacks(callbacks: DeployCallbacks): BuildCallbacks;
48
+ export {};
@@ -1,4 +1,4 @@
1
- import{success as h,failure as P}from"@fjall/generator";import{APPLICATION_STACKS as R,getApplicationDeployOrder as A,getApplicationStackName as B,getApplicationStepName as y,getApplicationStepId as O,PARALLEL_DEPLOY_GROUPS as E}from"../types/operations.js";import{emitProgress as k}from"../services/supporting/helpers.js";import{bootstrapOrFail as x,forwardOutput as w,forwardResourceProgress as D}from"./contextHelpers.js";import{hasDockerfile as L}from"../util/dockerfileDetection.js";import{runDockerBuild as _}from"./dockerBuildHelper.js";import{runWelcomeImageSetup as j}from"./welcomeImageHelper.js";function q(t,o,s,i){const n=new Set(E.PHASE_2.stacks),a=[];for(let r=o;r<t.length&&n.has(t[r]);r++)a.push(t[r]);if(a.length<2)return[];if(!i){const r=a.filter(l=>{for(const[f,c]of s)if(f.endsWith(l)&&!c)return!1;return!0});return r.length>=2?r:[]}return a}async function G(t,o,s,i,n,a,r,l,f,c){for(let e=0;e<t.length;e++){const u=t[e],p=O(u,"deploy"),m=y(u,"deploy");n.onStepStart?.(p,m,a+e,r)}k(n,"Deploying infrastructure in parallel\u2026"),n.onParallelPhaseStart?.(t,"Storage and database resources (parallel)");const d=await s.stackService.deployStacksInParallel(t,i,{onOutput:w(n),onResourceProgress:(e,u)=>{n.onResourceProgress?.(e),u&&n.onParallelStackResourceProgress?.(u,e)},onStackComplete:(e,u,p,m)=>{const S=O(e,"deploy"),N=y(e,"deploy"),C=a+t.indexOf(e);n.onStepComplete?.(S,N,u?"completed":"error",C,r),!u&&m&&n.onError?.(m)}});if(!d.success)return n.onParallelPhaseComplete?.([]),P(d.error);const g=d.data.filter(e=>!e.success);if(n.onParallelPhaseComplete?.(d.data.map(e=>({stack:e.stack,success:e.success,error:e.error}))),g.length>0){const e=g.map(p=>p.stack).join(", "),u=g.map(p=>`${p.stack}: ${p.error?.message??"Unknown error"}`).join(`
2
- `);return P(new Error(`Failed to deploy stacks: ${e}
1
+ import{success as h,failure as S}from"@fjall/generator";import{maskSensitiveOutput as w}from"@fjall/util";import{APPLICATION_STACKS as R,getApplicationStackName as C,getApplicationStepName as y,getApplicationStepId as N,PARALLEL_DEPLOY_GROUPS as A}from"../types/operations.js";import{emitProgress as E}from"../services/supporting/helpers.js";import{forwardOutput as O,forwardResourceProgress as L}from"./contextHelpers.js";import{runDockerBuild as I}from"./dockerBuildHelper.js";import{runWelcomeImageSetup as _}from"./welcomeImageHelper.js";import{withStepLifecycle as k}from"./stepLifecycle.js";function F(r,n,i,p,t){const l=new Set(A.PHASE_2.stacks),u=[];for(let o=n;o<r.length&&l.has(r[o]);o++)u.push(r[o]);if(u.length<2)return[];if(!t){const o=u.filter(f=>{const m=C(p,f);return i.get(m)!==!1});return o.length>=2?o:[]}return u}async function G(r,n,i,p,t,l,u,o,f,m){for(let e=0;e<r.length;e++){const s=r[e],c=N(s,"deploy"),d=y(s,"deploy");t.onStepStart?.(c,d,l+e,u)}E(t,"Deploying infrastructure in parallel\u2026"),t.onParallelPhaseStart?.(r,"Storage and database resources (parallel)");const a=await i.stackService.deployStacksInParallel(r,p,{onOutput:O(t),onResourceProgress:(e,s)=>{t.onResourceProgress?.(e),s&&t.onParallelStackResourceProgress?.(s,e)},onStackComplete:(e,s,c,d)=>{const P=N(e,"deploy"),B=y(e,"deploy"),x=l+r.indexOf(e);t.onStepComplete?.(P,B,s?"completed":"error",x,u),!s&&d&&t.onError?.(new Error(w(d.message)))}});if(!a.success)return t.onParallelPhaseComplete?.([]),S(a.error);const g=a.data.filter(e=>!e.success);if(t.onParallelPhaseComplete?.(a.data.map(e=>({stack:e.stack,success:e.success,error:e.error}))),g.length>0){const e=g.map(c=>c.stack).join(", "),s=g.map(c=>`${c.stack}: ${w(c.error?.message??"Unknown error")}`).join(`
2
+ `);return S(new Error(`Failed to deploy stacks: ${e}
3
3
 
4
- ${u}`))}for(const e of d.data){if(e.success&&e.outputs)for(const[m,S]of Object.entries(e.outputs))f[m]=String(S);const u=B(o.appName,e.stack),p=l.currentHashes.get(u);p&&c.set(u,p)}return h(void 0)}async function U(t,o,s,i,n,a,r){const l=O(t,"deploy"),f=y(t,"deploy");i.onStepStart?.(l,f,n,a);const c=await o.stackService.deployStack(t,s,{onOutput:w(i),onResourceProgress:D(i)});if(!c.success)return i.onStepComplete?.(l,f,"error",n,a),i.onError?.(c.error),P(c.error);if(i.onStepComplete?.(l,f,"completed",n,a),c.data.outputs)for(const[d,g]of Object.entries(c.data.outputs))r[d]=String(g);return h(void 0)}async function $(t,o,s,i,n,a){if(t!==R.COMPUTE||!o.dockerProvider)return null;if(a){const r=await _(o,s,i,n);if(!r.success)return P(r.error)}else{const r=await j(o,s,i,n);if(!r.success)return P(r.error)}return null}async function K(t,o,s,i,n,a){const{callbacks:r}=t;let l;if(a)l=a.deployOrder;else{const e=o.frameworkRegistry.resolve({appPath:s.path});e?l=e.builder.plan({appPath:s.path},e.detection).deployOrder:l=A()}const f=l.length,c=await x(o,i,r);if(!c.success)return c;const d={};for(let e=0;e<l.length;e++){const u=l[e],p=await $(u,t,o,s,r,L(s.path));if(p)return p;const m=await U(u,o,i,r,e,f,d);if(!m.success)return P(m.error)}const g=await o.stackService.resolveWebsiteUrl(s.appName);return g&&(d.websiteUrl=g),h({target:s.appName,deploymentType:"application",outputs:Object.keys(d).length>0?d:void 0,durationMs:Date.now()-n})}function Y(t){return{onBuildStart:o=>{t.onOpenNextBuildStart?.(),t.onLog?.(`${o} build started`,"info")},onBuildProgress:(o,s)=>{t.onOpenNextProgress?.(s)},onBuildComplete:o=>{t.onOpenNextBuildComplete?.(),t.onLog?.(`${o} build complete`,"info")},onBuildError:(o,s)=>{t.onOpenNextBuildError?.(s)}}}export{Y as createBuildCallbacks,K as deployAllStacks,G as deployParallelPhase,U as deployStackSequential,q as getParallelPhase2Stacks,$ as runDockerPreCompute};
4
+ ${s}`))}for(const e of a.data){if(e.success&&e.outputs)for(const[d,P]of Object.entries(e.outputs))f[d]=String(P);const s=C(n.appName,e.stack),c=o.currentHashes.get(s);c&&m.set(s,c)}return h(void 0)}async function K(r,n,i,p,t,l,u,o){const f=N(r,"deploy"),m=y(r,"deploy");return k(p,{stepId:f,stepName:m,stepIndex:t,totalSteps:l},async()=>{const a=await n.stackService.deployStack(r,i,{onOutput:O(p),onResourceProgress:L(p)},o?.parameters!==void 0?{parameters:o.parameters}:void 0);if(!a.success)return{kind:"error",error:a.error};if(a.data.outputs)for(const[g,e]of Object.entries(a.data.outputs))u[g]=String(e);return{kind:"completed",data:void 0}})}async function M(r,n,i,p,t,l){if(r!==R.COMPUTE||!n.dockerProvider)return null;if(l){const o=await I(n,i,p,t);return o.success?h({contentHashTagsByService:o.data}):S(o.error)}const u=await _(n,i,p,t);return u.success?h({contentHashTagsByService:{}}):S(u.error)}function W(r){return{onBuildStart:n=>{r.onOpenNextBuildStart?.(),r.onLog?.(`${n} build started`,"info")},onBuildProgress:(n,i)=>{r.onOpenNextProgress?.(i)},onBuildComplete:n=>{r.onOpenNextBuildComplete?.(),r.onLog?.(`${n} build complete`,"info")},onBuildError:(n,i)=>{r.onOpenNextBuildError?.(i)}}}export{W as createBuildCallbacks,G as deployParallelPhase,K as deployStackSequential,F as getParallelPhase2Stacks,M as runDockerPreCompute};
@@ -1 +1 @@
1
- import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as c,getErrorMessage as A}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 K,STACK_FAILED_STATE_PATTERN as M}from"../types/constants.js";async function V(n,u,i,t,m,e,r){const o=Date.now(),a=`${t.name} (${e})`;r.onCascadeAccountStart?.(a,t.id,e,m);const f=await x(u.awsProvider,t.id,e,`fjall-cascade-destroy-${t.name}`);if(!f.success)return r.onCascadeAccountComplete?.(a,!1,c(f.error.message),e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-o,error:`AssumeRole failed: ${c(f.error.message)}`,skipped:!0};const{provider:v,credentials:l}=f.data,w=k.buildDeploymentContext({deployType:m,target:i.target,path:i.path,region:e,accountName:t.name,callerIdentity:P(t.id),...F({orgConfig:n.orgConfig,identity:n.identity})},{verbose:n.options?.verbose},n.orgConfig);r.onCascadeAccountPhaseChange?.(a,"synth",e);const y=await u.cdkService.runCdkSynth(w,I(r));if(!y.success){const s=c(`Synth failed: ${y.error}`);return r.onCascadeAccountComplete?.(a,!1,s,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-o,error:s}}r.onCascadeAccountPhaseChange?.(a,"destroy",e);const d=_(m==="platform"?h.PLATFORM:h.ACCOUNT),D=await u.cdkService.runCdkDestroy(w,d,I(r),s=>r.onCascadeAccountResourceProgress?.(a,s,e),v,!0,l);if(!D.success){const s=D.error;if(s.includes(M)){S.warn("cascadeDestroy",`CDK destroy failed on ${d} in failed state, retrying via CloudFormation API`,{region:e,account:t.name});try{await L(d,e,l,void 0,r)}catch(C){const E=`cleanupFailedStack threw for ${d}: ${A(C)}`;S.warn("cascadeDestroy",E),r.onLog?.(E,"warn")}const p=await H(d,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()-o};if(p.error){const C=c(p.error);return r.onCascadeAccountComplete?.(a,!1,C,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-o,error:C}}const T=c(`Stack ${d} cleanup attempted but stack still exists in ${e}`);return r.onCascadeAccountComplete?.(a,!1,T,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-o,error:T}}const N=c(s);return r.onCascadeAccountComplete?.(a,!1,N,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-o,error:N}}return r.onCascadeAccountComplete?.(a,!0,void 0,e),{accountName:t.name,accountId:t.id,region:e,success:!0,duration:Date.now()-o}}async function H(n,u,i){try{const e=(await new $({region:u,credentials:i,requestHandler:new O({requestTimeout:15e3})}).send(new R({StackName:n}))).Stacks?.[0]?.StackStatus;return!e||e==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${e} after cleanup attempt`}}catch(t){return t instanceof Error&&t.message?.includes(K)?{deleted:!0}:(S.debug("cascadeDestroy","Stack verification failed",{error:A(t)}),{deleted:!1,error:`Stack verification failed: ${A(t)}`})}}export{V as destroyCascadeAccount};
1
+ import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as s,getErrorMessage as E}from"@fjall/util";import{stubCallerIdentity as P}from"../types/deployment/index.js";import{CloudFormationClient as $,DescribeStacksCommand as R}from"@aws-sdk/client-cloudformation";import{NodeHttpHandler as O}from"@smithy/node-http-handler";import{ORGANISATION_TYPES as h,getOrganisationStackName as _}from"../types/operations.js";import{CdkContextBuilder as k}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as F,assumeCascadeRole as x,forwardOutput as I}from"./contextHelpers.js";import{cleanupFailedStack as L}from"./stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as M,STACK_FAILED_STATE_PATTERN as K}from"../types/constants.js";async function V(c,i,m,t,o,e,r){const n=Date.now(),a=`${t.name} (${e})`;r.onCascadeAccountStart?.(a,t.id,e,o);const f=await x(i.awsProvider,t.id,e,`fjall-cascade-destroy-${t.name}`);if(!f.success)return r.onCascadeAccountComplete?.(a,!1,s(f.error.message),e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:`AssumeRole failed: ${s(f.error.message)}`,skipped:!0};const{provider:v,credentials:l}=f.data,A=k.buildDeploymentContext({deployType:o,target:m.target,path:m.path,region:e,accountName:t.name,callerIdentity:P(t.id),...F({orgConfig:c.orgConfig,identity:c.identity})},{verbose:c.options?.verbose},c.orgConfig);r.onCascadeAccountPhaseChange?.(a,"synth",e);const w=await i.cdkService.runCdkSynth(A,I(r));if(!w.success){const d=s(`Synth failed: ${w.error}`);return r.onCascadeAccountComplete?.(a,!1,d,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:d}}r.onCascadeAccountPhaseChange?.(a,"destroy",e);const u=_(o==="platform"?h.PLATFORM:h.ACCOUNT),y=await i.cdkService.runCdkDestroy(A,u,I(r),d=>r.onCascadeAccountResourceProgress?.(a,d,e),v,!0,l);if(!y.success){const d=y.error;if(d.includes(K)){S.warn("cascadeDestroy",`CDK destroy failed on ${u} in failed state, retrying via CloudFormation API`,{region:e,account:t.name});try{await L(u,e,l,void 0,r)}catch(C){const T=`cleanupFailedStack threw for ${u}: ${s(E(C))}`;S.warn("cascadeDestroy",T),r.onLog?.(T,"warn")}const p=await H(u,e,l);if(p.deleted)return r.onCascadeAccountComplete?.(a,!0,void 0,e),{accountName:t.name,accountId:t.id,region:e,success:!0,duration:Date.now()-n};if(p.error){const C=s(p.error);return r.onCascadeAccountComplete?.(a,!1,C,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:C}}const N=s(`Stack ${u} cleanup attempted but stack still exists in ${e}`);return r.onCascadeAccountComplete?.(a,!1,N,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:N}}const D=s(d);return r.onCascadeAccountComplete?.(a,!1,D,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:D}}return r.onCascadeAccountComplete?.(a,!0,void 0,e),{accountName:t.name,accountId:t.id,region:e,success:!0,duration:Date.now()-n}}async function H(c,i,m){try{const e=(await new $({region:i,credentials:m,requestHandler:new O({requestTimeout:15e3})}).send(new R({StackName:c}))).Stacks?.[0]?.StackStatus;return!e||e==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${e} after cleanup attempt`}}catch(t){if(t instanceof Error&&t.message?.includes(M))return{deleted:!0};const o=s(E(t));return S.debug("cascadeDestroy","Stack verification failed",{error:o}),{deleted:!1,error:`Stack verification failed: ${o}`}}}export{V as destroyCascadeAccount};
@@ -0,0 +1,19 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import type { DeployParams, DeployResult } from "../types/params.js";
3
+ import type { ApplicationOperation } from "../types/operations.js";
4
+ import type { DeployServices } from "./serviceFactory.js";
5
+ /**
6
+ * Code-only deployment orchestrator.
7
+ *
8
+ * Replaces a buggy `forceNewDeployment: true` rollout (which left the task
9
+ * definition pinned at the previous image tag) with an explicit
10
+ * RegisterTaskDefinition + UpdateService(taskDefinition: newArn) sequence.
11
+ *
12
+ * `contentHashTagsByService` MUST come from the same build phase that just
13
+ * pushed the artifacts (`runDockerBuild` returns this map). Keys are
14
+ * `<service>` or `<service>-<target>`; values are the bare content-hash tag
15
+ * stem (`<service>(-<target>)?-sha-<12hex>`). `options.imageTag` (the
16
+ * explicit `--image-tag <tag>` rollback) bypasses this map and is used
17
+ * verbatim for every service.
18
+ */
19
+ export declare function deployCodeOnly(params: DeployParams, services: DeployServices, operation: ApplicationOperation, contentHashTagsByService: Record<string, string>): Promise<Result<DeployResult>>;
@@ -0,0 +1 @@
1
+ import{join as U}from"path";import{success as F,failure as $}from"@fjall/generator";import{logger as h}from"@fjall/util/logger";import{maskSensitiveOutput as S}from"@fjall/util";import{parseDockerServicesFromManifest as N}from"@fjall/util/manifest";import{APPLICATION_STACKS as O,getApplicationStepId as _,getApplicationStepName as j}from"../types/operations.js";import{extractEcsServiceName as L}from"../aws/utils/arnParser.js";import{stripTagOrDigest as P}from"../services/infrastructure/EcrImageInspectorService.js";import{withStepLifecycle as q}from"./stepLifecycle.js";const v="codeOnlyDeploy";function z(i,s){if(!i)return;const t=i.docker.target,n=t?`${i.name}-${t}`:i.name;return s[n]}function G(i,s){const t=s.toLowerCase();return i.find(n=>n.name.toLowerCase()===t)??i.find(n=>t.includes(n.name.toLowerCase()))}const H=0,K=1;async function re(i,s,t,n){const{callbacks:u,options:k}=i,I=Date.now(),A=_(O.COMPUTE,"deploy"),M=j(O.COMPUTE,"deploy");return q(u,{stepId:A,stepName:M,stepIndex:H,totalSteps:K},async()=>{const l=k?.imageTag;l&&u.onLog?.(`Rolling to supplied image tag ${l}`,"info");const D=N(U(t.path,"cdk.out"));h.debug(v,"Code-only rollout starting",{explicitRollbackTag:l,workingDirectory:i.workingDirectory,contentHashTagsByService:n,manifestServiceCount:D.length,manifestServices:D.map(r=>({name:r.name,target:r.docker.target}))}),u.onLog?.("Discovering ECS cluster and services\u2026","info");const o=await s.ecsResolver.getDeployableClusterAndServices(t.appName);if(!o.success)return h.debug(v,"ECS discovery failed",{appName:t.appName,error:S(o.error.message)}),{kind:"error",error:o.error};const{clusterArn:y,serviceArns:f}=o.data;if(h.debug(v,"ECS discovery complete",{appName:t.appName,clusterArn:y,serviceCount:f.length,serviceArns:f}),!y||f.length===0){const r=`No deployable ECS services found for ${t.appName}`;return u.onLog?.(r,"warn"),{kind:"skipped",data:{target:t.appName,deploymentType:"application",durationMs:Date.now()-I,noChanges:!0}}}const E=k?.serviceName?.toLowerCase(),d=E===""?void 0:E,m=d?f.filter(r=>{const c=(L(r)??"").toLowerCase();return c===d||c.endsWith(d)}):f;if(d&&m.length===0){const r=f.map(c=>L(c)??"unknown").join(", ");return{kind:"error",error:new Error(`Service '${d}' not found. Available: ${r||"none"}`)}}const g={},p=[];for(let r=0;r<m.length;r++){const c=m[r];if(!c)continue;const a=L(c)??`service-${r+1}`,T=`[${r+1}/${m.length}] ${a}`,C=G(D,a),b=z(C,n),w=l??b??`${a.toLowerCase()}-latest`;l===void 0&&b===void 0&&u.onLog?.(`No content-hash tag found for ${a}; falling back to mutable tag ${w}`,"warn"),h.debug(v,"Starting service rollout",{serviceName:a,serviceArn:c,imageTag:w,explicitRollbackTag:l,contentHashTag:b,manifestServiceMatched:C?.name,dockerTarget:C?.docker.target,index:r,total:m.length}),u.onECSUpdate?.(`${T}: rolling out image tag ${w}`,a);const e=await W(s,y,c,a,w,u);if(h.debug(v,"Service rollout completed",{serviceName:a,success:e.success,...e.success?{taskDefinitionArn:e.data.taskDefinitionArn,imageDigest:e.data.imageDigest,durationMs:e.data.durationMs}:{error:S(e.error.message)}}),!e.success){p.push(`${a}: ${S(e.error.message)}`);continue}g[`${a}TaskDefinition`]=e.data.taskDefinitionArn,g[`${a}PreviousTaskDefinition`]=e.data.previousTaskDefinitionArn,g[`${a}ImageTag`]=w,g[`${a}ImageUri`]=e.data.imageUri,g[`${a}EcrRepositoryArn`]=e.data.ecrRepositoryArn,e.data.imageDigest!==void 0&&(g[`${a}ImageDigest`]=e.data.imageDigest)}const R=Date.now()-I;if(p.length>0){const r=m.length-p.length,c=m.length,a=p.length===c?`All ${c} ECS service rollouts failed: ${p.join("; ")}`:`Partial rollout: ${r}/${c} services succeeded. Failures: ${p.join("; ")}`;return{kind:"error",error:new Error(a)}}return{kind:"completed",data:{target:t.appName,deploymentType:"application",outputs:Object.keys(g).length>0?g:void 0,durationMs:R}}})}async function W(i,s,t,n,u,k){const I=Date.now(),A=await i.ecsService.describeServices(s,[t]);if(!A.success)return $(new Error(`Failed to describe service ${n}: ${A.error.message}`));const M=A.data[0];if(!M?.taskDefinition)return $(new Error(`Service ${n} has no task definition`));const l=M.taskDefinition,D=await i.ecsService.getLatestTaskDefinition(l);if(!D.success)return $(new Error(`Failed to fetch task definition for ${n}: ${D.error.message}`));const o=D.data;if(!o||!o.containerDefinitions||o.containerDefinitions.length===0)return $(new Error(`Task definition for ${n} has no container definitions`));const y=o.containerDefinitions.find(e=>e.image)?.image;if(!y)return $(new Error(`Task definition for ${n} has no container image \u2014 cannot derive repository URI`));const f=P(y),E=await i.ecrImageInspector.getImageDigest(f,u);let d;E.success?d=E.data:k.onLog?.(`Could not resolve image digest for ${n} (${S(E.error.message)}); falling back to tag pinning`,"warn");const m=e=>d!==void 0?`${e}@${d}`:`${e}:${u}`,g=o.containerDefinitions.map(e=>e.image?{...e,image:m(P(e.image))}:e),p=o.family;if(!p)return $(new Error(`Task definition for ${n} has no family`));const R=await i.ecsService.registerTaskDefinition({family:p,taskRoleArn:o.taskRoleArn,executionRoleArn:o.executionRoleArn,networkMode:o.networkMode,containerDefinitions:g,volumes:o.volumes,placementConstraints:o.placementConstraints,requiresCompatibilities:o.requiresCompatibilities,cpu:o.cpu,memory:o.memory,runtimePlatform:o.runtimePlatform,proxyConfiguration:o.proxyConfiguration,inferenceAccelerators:o.inferenceAccelerators,pidMode:o.pidMode,ipcMode:o.ipcMode,ephemeralStorage:o.ephemeralStorage});if(!R.success)return $(new Error(`Failed to register task definition for ${n}: ${R.error.message}`));const r=R.data;k.onTaskDefRegistered?.({serviceName:n,taskDefinitionArn:r,previousTaskDefinitionArn:l,revision:Y(r),family:p,...d!==void 0?{imageDigest:d}:{}});const c=await i.ecsService.updateService(s,t,{taskDefinition:r,forceNewDeployment:!0});if(!c.success)return $(new Error(`Failed to update service ${n}: ${c.error.message}`));const a=await i.ecsService.pollDeployment({clusterArn:s,serviceArn:t,waitForCompletion:!0,progressCallback:e=>{const x=e.latestEvent?` \u2014 ${S(e.latestEvent)}`:"";k.onECSProgress?.(`${n}: ${S(e.message)}${x}`,e.percentComplete)}}),T=Date.now()-I;if(!a.success){const e=S(a.error.message);return k.onECSComplete?.({serviceName:n,serviceArn:t,success:!1,taskDefinitionArn:r,durationMs:T,reason:e}),$(new Error(`ECS rollout failed for ${n}: ${e}`))}const C=await i.ecsService.getDeploymentStatus(s,t);k.onECSComplete?.({serviceName:n,serviceArn:t,success:!0,taskDefinitionArn:r,durationMs:T,finalRunningCount:C.success?C.data.runningCount:void 0,finalDesiredCount:C.success?C.data.desiredCount:void 0}),h.debug(v,`Rolled out ${n} successfully`,{serviceArn:t,newTaskDefArn:r,previousTaskDefArn:l,durationMs:T});const b=`${f}:${u}`,w=X(f);return F({taskDefinitionArn:r,previousTaskDefinitionArn:l,imageUri:b,ecrRepositoryArn:w,imageDigest:d,durationMs:T})}function X(i){const s=i.match(/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/);if(!s)return"";const[,t,n,u]=s;return`arn:aws:ecr:${n}:${t}:repository/${u}`}function Y(i){const s=i.lastIndexOf(":");if(s<0)return 0;const t=i.slice(s+1);return/^\d+$/.test(t)?parseInt(t,10):0}export{re as deployCodeOnly};
@@ -1 +1 @@
1
- import{join as E}from"path";import{success as $,failure as m}from"@fjall/generator";import{logger as H}from"@fjall/util/logger";import{maskSensitiveOutput as d}from"@fjall/util";import{hasDockerfile as v}from"../util/dockerfileDetection.js";import{deriveResourcesFromManifestStacks as x}from"../types/patternDetection.js";import{emitProgress as C,PROGRESS_MESSAGES as D}from"../services/supporting/helpers.js";import{parseRequiredSecretsFromManifest as A}from"./manifestSecretParser.js";async function B(n,t,P,e){e.onDetectionPhaseChange?.("detect","started");const u=t.frameworkRegistry.resolve({appPath:n.path}),f=u?.detection.pattern??null,y=u?.detection.hasDatabase??!1;e.onLog?.(`Pattern detected: ${f??"standard"}`,"info"),e.onDetectionPhaseChange?.("detect","completed");const p=E(n.path,"cdk.out");e.onDetectionPhaseChange?.("synth","started"),C(e,D.SYNTH);const g=await t.cdkService.runCdkSynth(P,s=>e.onCdkOutput?.(s,"synth"));if(!g.success)return m(new Error(d(`CDK synthesis failed: ${g.error}`)));e.onDetectionPhaseChange?.("synth","completed");const r=A(p);r.length>0&&e.onLog?.(`Found ${r.length} required secret path(s) in manifest`,"info"),e.onDetectionPhaseChange?.("hash","started"),C(e,D.HASH);const a=await t.hashService.getTemplateHashes(p);if(!a.success)return m(new Error(d(`Template hash computation failed: ${a.error.message}`)));const i=a.data,h=await t.hashService.compareWithState(i,n.path);if(!h.success)return m(new Error(d(`Hash comparison failed: ${h.error.message}`)));const o=h.data;e.onDetectionPhaseChange?.("hash","completed");const c=new Map(o.stackChanges);for(const[s,l]of o.stackChanges)l||await t.cfnService.stackExists(s)||(H.debug("detectionPipeline","Stale hash detected \u2014 stack missing in CFN",{stackName:s}),c.set(s,!0));const w=Array.from(i.keys()),S=x(w),k=S.hasCompute&&v(n.path),R=Array.from(c.values()).some(Boolean);return e.onLog?.(`Detection complete: ${o.changedCount} changed, ${o.unchangedCount} unchanged`,"info"),$({pattern:f,hasDatabase:y,hasDifferences:R,stackChanges:c,currentHashes:i,resources:S,hasDockerfile:k,requiredSecrets:r})}export{B as runDetectionPipeline};
1
+ import{join as E}from"path";import{success as $,failure as d}from"@fjall/generator";import{logger as H}from"@fjall/util/logger";import{maskSensitiveOutput as u}from"@fjall/util";import{deriveResourcesFromManifestStacks as F}from"../types/patternDetection.js";import{emitProgress as C,PROGRESS_MESSAGES as D}from"../services/supporting/helpers.js";import{parseRequiredSecretsFromManifest as M}from"./manifestSecretParser.js";import{parseDockerServicesFromManifest as x}from"@fjall/util/manifest";async function K(o,t,P,e){e.onDetectionPhaseChange?.("detect","started");const f=t.frameworkRegistry.resolve({appPath:o.path}),p=f?.detection.pattern??null,y=f?.detection.hasDatabase??!1;e.onLog?.(`Pattern detected: ${p??"standard"}`,"info"),e.onDetectionPhaseChange?.("detect","completed");const r=E(o.path,"cdk.out");e.onDetectionPhaseChange?.("synth","started"),C(e,D.SYNTH);const g=await t.cdkService.runCdkSynth(P,s=>e.onCdkOutput?.(s,"synth"));if(!g.success)return d(new Error(u(`CDK synthesis failed: ${g.error}`)));e.onDetectionPhaseChange?.("synth","completed");const a=M(r);a.length>0&&e.onLog?.(`Found ${a.length} required secret path(s) in manifest`,"info"),e.onDetectionPhaseChange?.("hash","started"),C(e,D.HASH);const i=await t.hashService.getTemplateHashes(r);if(!i.success)return d(new Error(u(`Template hash computation failed: ${i.error.message}`)));const c=i.data,h=await t.hashService.compareWithState(c,o.path);if(!h.success)return d(new Error(u(`Hash comparison failed: ${h.error.message}`)));const n=h.data;e.onDetectionPhaseChange?.("hash","completed");const m=new Map(n.stackChanges);for(const[s,v]of n.stackChanges)v||await t.cfnService.stackExists(s)||(H.debug("detectionPipeline","Stale hash detected \u2014 stack missing in CFN",{stackName:s}),m.set(s,!0));const k=Array.from(c.keys()),S=F(k),w=x(r),R=S.hasCompute&&w.length>0,l=Array.from(m.values()).some(Boolean);return e.onLog?.(`Detection complete: ${n.changedCount} changed, ${n.unchangedCount} unchanged`,"info"),$({pattern:p,hasDatabase:y,hasDifferences:l,stackChanges:m,currentHashes:c,resources:S,hasDockerfile:R,requiredSecrets:a})}export{K as runDetectionPipeline};
@@ -3,8 +3,25 @@ import type { DeployParams } from "../types/params.js";
3
3
  import type { DeployCallbacks } from "../types/callbacks.js";
4
4
  import type { ApplicationOperation } from "../types/operations.js";
5
5
  import type { DeployServices } from "./serviceFactory.js";
6
+ export declare const DOCKER_BUILD_STEP_NAME = "Building and pushing Docker image";
6
7
  /**
7
8
  * Run Docker build and push before deploying the Compute stack.
8
- * Initialises ECR if needed, then builds and pushes the image.
9
+ *
10
+ * Reads the post-synth `cdk.out/fjall-manifest.json` to discover the set of
11
+ * services that need container images, resolves each service's
12
+ * `(buildContext, dockerfilePath, target)` tuple, groups identical builds,
13
+ * and pushes per-service `<service>(-<target>)?-latest` tags shaped to match
14
+ * the CDK-synthesised task definitions.
15
+ *
16
+ * After each group's push, the helper derives a content-hash tag from the
17
+ * digest (`deriveContentHashTag` at `@fjall/util`) and applies it via
18
+ * `tagByDigest` (manifest-list create, no layer re-upload). The resulting
19
+ * `<service>` → `<content-hash-tag>` map is returned so the orchestrator can
20
+ * plumb it into the CDK deploy via `--parameters <Service>ImageTag=<tag>`.
21
+ *
22
+ * Falls back to the legacy single-image path (`<appPath>/Dockerfile` →
23
+ * `<appName>:latest`) when the manifest is absent. The legacy path returns
24
+ * an empty content-hash tag map; the orchestrator skips parameter plumbing
25
+ * for that branch.
9
26
  */
10
- export declare function runDockerBuild(params: DeployParams, services: DeployServices, operation: ApplicationOperation, callbacks: DeployCallbacks): Promise<Result<void>>;
27
+ export declare function runDockerBuild(params: DeployParams, services: DeployServices, operation: ApplicationOperation, callbacks: DeployCallbacks): Promise<Result<Record<string, string>>>;
@@ -1 +1 @@
1
- import{success as u,failure as c}from"@fjall/generator";import{maskSensitiveOutput as m}from"@fjall/util";import{STEP_IDS as d}from"../types/stepDefinitions.js";const p="Building and pushing Docker image";async function C(f,s,r,e){const i=f.dockerProvider;if(!i)return u(void 0);const n=s.awsProvider.getAccountId();if(!n)return e.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),u(void 0);const a=s.awsProvider.getRegion();e.onStepStart?.(d.DOCKER_OPERATIONS,p),e.onLog?.("Initialising ECR repository\u2026","info");const g=await i.initialiseECR({appName:r.appName,region:a,accountId:n});g.success||e.onLog?.(`ECR initialisation failed: ${g.error.message}`,"warn"),e.onLog?.("Building and pushing Docker image\u2026","info");const t=await i.buildAndPush({appName:r.appName,appPath:r.path,region:a,accountId:n},(o,E,D,R)=>{e.onDockerProgress?.(m(o),E,D,R)});if(!t.success){e.onStepComplete?.(d.DOCKER_OPERATIONS,p,"error");const o=new Error(m(t.error.message));return e.onError?.(o),c(o)}return e.onStepComplete?.(d.DOCKER_OPERATIONS,p,"completed"),e.onLog?.(`Docker image pushed: ${t.data.imageUri}`,"info"),u(void 0)}export{C as runDockerBuild};
1
+ import{isAbsolute as T,join as v,dirname as k,resolve as y}from"node:path";import{success as D,failure as S}from"@fjall/generator";import{deriveContentHashTag as b,maskSensitiveOutput as C}from"@fjall/util";import{logger as E}from"@fjall/util/logger";import{parseDockerServicesFromManifest as w}from"@fjall/util/manifest";import{STEP_IDS as N}from"../types/stepDefinitions.js";const O="dockerBuildHelper",R="Building and pushing Docker image";function I(t,n){const r=t.docker.path,e=t.docker.context,s=T(r)?r:v(n,r);return{buildContext:e?T(e)?y(e):y(n,e):y(k(s)),dockerfilePath:s,target:t.docker.target}}function x(t){return`${t.buildContext}|${t.dockerfilePath}|${t.target??""}`}function A(t){return t.startsWith("cn-")?"amazonaws.com.cn":"amazonaws.com"}function L(t,n,r){return`${t}.dkr.ecr.${n}.${A(n)}/${r.toLowerCase()}`}function B(t,n){const r=n.name.toLowerCase(),e=n.docker.target,s=e?`-${e.toLowerCase()}`:"";return[`${t}:${r}${s}-latest`]}function _(t,n,r){const e=new Map;for(const s of t){const a=I(s,n),u=x(a),d=B(r,s),i=e.get(u);i?(i.members.push(s),i.latestTags.push(...d)):e.set(u,{identity:a,members:[s],latestTags:[...d]})}return Array.from(e.values())}async function K(t,n,r,e,s){const a=await t.buildAndPush({appName:n.appName,appPath:n.path,region:r,accountId:e},(u,d,i,p)=>{s.onDockerProgress?.(C(u),d,i,p)});return a.success?D({imageUri:a.data.imageUri,imageTag:a.data.imageTag}):S(a.error)}async function H(t,n,r,e,s,a,u){const d=t.members[0],i={appName:r.appName,appPath:t.identity.buildContext,region:e,accountId:s,buildContext:t.identity.buildContext,dockerfilePath:t.identity.dockerfilePath,imageTags:t.latestTags,serviceName:d.name,...t.identity.target!==void 0&&{target:t.identity.target}},p=await n.buildAndPush(i,(c,h,g,m)=>{u.onDockerProgress?.(C(c),h,g,m)});if(!p.success)return S(p.error);const f=p.data.imageDigest;if(typeof f!="string"||!f.startsWith("sha256:"))return S(new Error(`Buildx push succeeded but returned an invalid digest: ${C(String(f))}`));const l={},P=[];for(const c of t.members){const h=c.docker.target,g=b(f,c.name,h);if(!g.success)return S(g.error);const m=g.data,$=h?`${c.name}-${h}`:c.name;l[$]=m,P.push(`${a}:${m}`)}const o=await n.tagByDigest({sourceImage:a,digest:f,tags:P});return o.success?D({contentHashTagsByService:l}):S(o.error)}async function W(t,n,r,e){const s=t.dockerProvider;if(!s)return D({});const a=n.awsProvider.getAccountId();if(!a)return e.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),D({});const u=n.awsProvider.getRegion(),d=v(r.path,"cdk.out"),i=w(d),p=L(a,u,r.appName);E.debug(O,"runDockerBuild starting",{appName:r.appName,appPath:r.path,accountId:a,region:u,cdkOutPath:d,manifestServiceCount:i.length,manifestServices:i.map(o=>({name:o.name,path:o.docker.path,context:o.docker.context,target:o.docker.target})),stepId:N.DOCKER_OPERATIONS,stepName:R}),e.onStepStart?.(N.DOCKER_OPERATIONS,R),e.onLog?.("Initialising ECR repository\u2026","info");const f=await s.initialiseECR({appName:r.appName,region:u,accountId:a});if(f.success||e.onLog?.(`ECR initialisation failed: ${C(f.error.message)}`,"warn"),i.length===0){e.onLog?.("Building and pushing Docker image\u2026","info");const o=await K(s,r,u,a,e);if(!o.success){E.debug(O,"Docker buildAndPush (legacy) failed",{appName:r.appName,error:C(o.error.message)}),e.onStepComplete?.(N.DOCKER_OPERATIONS,R,"error");const c=new Error(C(o.error.message));return e.onError?.(c),S(c)}return e.onStepComplete?.(N.DOCKER_OPERATIONS,R,"completed"),e.onLog?.(`Docker image pushed: ${o.data.imageUri}`,"info"),D({})}const l=_(i,r.path,p);e.onLog?.(`Building and pushing ${i.length} service image(s) in ${l.length} group(s)\u2026`,"info");const P={};for(let o=0;o<l.length;o++){const c=l[o],h=c.members.map(m=>m.name).join(", ");e.onLog?.(`Building group ${o+1}/${l.length} (${h})\u2026`,"info");const g=await H(c,s,r,u,a,p,e);if(!g.success){E.debug(O,"Docker buildAndPush failed",{appName:r.appName,leader:c.members[0]?.name,members:h,error:C(g.error.message)}),e.onStepComplete?.(N.DOCKER_OPERATIONS,R,"error");const m=new Error(C(g.error.message));return e.onError?.(m),S(m)}for(const[m,$]of Object.entries(g.data.contentHashTagsByService))P[m]=$}return E.debug(O,"Docker buildAndPush succeeded",{appName:r.appName,services:i.map(o=>o.name),groupCount:l.length,contentHashTags:P}),e.onStepComplete?.(N.DOCKER_OPERATIONS,R,"completed"),e.onLog?.(`Docker images pushed for ${i.length} service(s)`,"info"),D(P)}export{R as DOCKER_BUILD_STEP_NAME,W as runDockerBuild};