@fjall/deploy-core 0.94.1 → 0.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/organisations/accounts.js +1 -99
  3. package/dist/src/aws/organisations/backup.js +1 -30
  4. package/dist/src/aws/organisations/costAllocation.js +1 -28
  5. package/dist/src/aws/organisations/delegatedAdmin.js +3 -43
  6. package/dist/src/aws/organisations/identityCentre.js +1 -23
  7. package/dist/src/aws/organisations/ipam.js +1 -20
  8. package/dist/src/aws/organisations/organisation.js +1 -103
  9. package/dist/src/aws/organisations/organisationalUnits.js +1 -239
  10. package/dist/src/aws/organisations/policies.js +1 -37
  11. package/dist/src/aws/organisations/ram.js +1 -19
  12. package/dist/src/aws/organisations/serviceAccess.js +1 -44
  13. package/dist/src/aws/organisations/trustedAccess.js +1 -19
  14. package/dist/src/aws/utils/regions.js +1 -1
  15. package/dist/src/index.js +1 -65
  16. package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +1 -78
  17. package/dist/src/orchestration/activeDeploymentGuard.js +5 -39
  18. package/dist/src/orchestration/applicationDeploy.js +1 -149
  19. package/dist/src/orchestration/applicationDeployHelpers.js +4 -223
  20. package/dist/src/orchestration/applicationDestroy.js +1 -131
  21. package/dist/src/orchestration/builders/dockerBuilder.js +1 -98
  22. package/dist/src/orchestration/builders/openNextBuilder.js +1 -144
  23. package/dist/src/orchestration/cascadeHelpers.js +1 -160
  24. package/dist/src/orchestration/contextHelpers.js +1 -107
  25. package/dist/src/orchestration/deploy.js +1 -42
  26. package/dist/src/orchestration/destroy.js +1 -67
  27. package/dist/src/orchestration/detectionPipeline.js +1 -84
  28. package/dist/src/orchestration/dockerBuildHelper.js +1 -49
  29. package/dist/src/orchestration/dockerInterface.js +0 -1
  30. package/dist/src/orchestration/domainInterface.js +0 -1
  31. package/dist/src/orchestration/openNextBuild.js +3 -243
  32. package/dist/src/orchestration/organisationDeploy.js +3 -284
  33. package/dist/src/orchestration/organisationDestroy.js +3 -189
  34. package/dist/src/orchestration/organisationSetup.js +1 -247
  35. package/dist/src/orchestration/resolveOperation.js +1 -123
  36. package/dist/src/orchestration/welcomeImageHelper.js +1 -64
  37. package/dist/src/services/application/ApplicationStackService.js +1 -218
  38. package/dist/src/services/application/applicationStackHelpers.js +4 -248
  39. package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -244
  40. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -125
  41. package/dist/src/services/infrastructure/CdkProcessManager.js +3 -278
  42. package/dist/src/services/infrastructure/CdkService.js +3 -213
  43. package/dist/src/services/infrastructure/CloudFormationService.js +1 -248
  44. package/dist/src/services/infrastructure/ICdkProcessManager.js +0 -1
  45. package/dist/src/services/supporting/CdkContextBuilder.js +1 -44
  46. package/dist/src/services/supporting/TemplateHashService.js +1 -152
  47. package/dist/src/steps/stepRegistry.js +1 -505
  48. package/dist/src/types/apiClient.js +0 -1
  49. package/dist/src/types/detection.js +0 -1
  50. package/dist/src/types/frameworkBuilder.js +0 -8
  51. package/dist/src/types/params.js +0 -1
  52. package/dist/src/types/patternDetection.js +1 -88
  53. package/dist/src/types/stepDefinitions.js +1 -98
  54. package/package.json +4 -4
@@ -1,223 +1,4 @@
1
- import { success, failure } from "@fjall/generator";
2
- import { APPLICATION_STACKS, getApplicationDeployOrder, getApplicationStackName, getApplicationStepName, getApplicationStepId, PARALLEL_DEPLOY_GROUPS } from "../types/operations.js";
3
- import { emitProgress } from "../services/supporting/helpers.js";
4
- import { bootstrapOrFail, forwardOutput, forwardResourceProgress } from "./contextHelpers.js";
5
- import { hasDockerfile } from "../util/dockerfileDetection.js";
6
- import { runDockerBuild } from "./dockerBuildHelper.js";
7
- import { runWelcomeImageSetup } from "./welcomeImageHelper.js";
8
- /**
9
- * Identify Phase 2 stacks (Storage, Messaging, Database) that are consecutive
10
- * in the deploy order and have changes. Returns them for parallel deployment
11
- * only if there are at least 2.
12
- */
13
- export function getParallelPhase2Stacks(deployOrder, currentIndex, stackChanges, force) {
14
- const phase2Set = new Set(PARALLEL_DEPLOY_GROUPS.PHASE_2.stacks);
15
- const candidates = [];
16
- for (let j = currentIndex; j < deployOrder.length; j++) {
17
- if (phase2Set.has(deployOrder[j])) {
18
- candidates.push(deployOrder[j]);
19
- }
20
- else {
21
- break;
22
- }
23
- }
24
- if (candidates.length < 2)
25
- return [];
26
- // Only include stacks that actually have changes (unless forced)
27
- if (!force) {
28
- const withChanges = candidates.filter((stack) => {
29
- // stackChanges is keyed by full stack name but we only have the type here;
30
- // since we don't know the app name, check all entries ending with the stack type
31
- for (const [key, changed] of stackChanges) {
32
- if (key.endsWith(stack) && !changed)
33
- return false;
34
- }
35
- return true;
36
- });
37
- return withChanges.length >= 2 ? withChanges : [];
38
- }
39
- return candidates;
40
- }
41
- /**
42
- * Deploy a group of stacks in parallel, reporting step events for each.
43
- */
44
- export async function deployParallelPhase(stacks, operation, services, context, callbacks, startIndex, totalSteps, detection, allOutputs, deployedHashes) {
45
- // Emit step start for each parallel stack
46
- for (let k = 0; k < stacks.length; k++) {
47
- const stack = stacks[k];
48
- const stepId = getApplicationStepId(stack, "deploy");
49
- const stepName = getApplicationStepName(stack, "deploy");
50
- callbacks.onStepStart?.(stepId, stepName, startIndex + k, totalSteps);
51
- }
52
- emitProgress(callbacks, "Deploying infrastructure in parallel…");
53
- callbacks.onParallelPhaseStart?.(stacks, "Storage and database resources (parallel)");
54
- const parallelResult = await services.stackService.deployStacksInParallel(stacks, context, {
55
- onOutput: forwardOutput(callbacks),
56
- onResourceProgress: (event, stackId) => {
57
- callbacks.onResourceProgress?.(event);
58
- if (stackId) {
59
- callbacks.onParallelStackResourceProgress?.(stackId, event);
60
- }
61
- },
62
- onStackComplete: (stack, stackSuccess, _duration, error) => {
63
- const stepId = getApplicationStepId(stack, "deploy");
64
- const stepName = getApplicationStepName(stack, "deploy");
65
- const idx = startIndex + stacks.indexOf(stack);
66
- callbacks.onStepComplete?.(stepId, stepName, stackSuccess ? "completed" : "error", idx, totalSteps);
67
- if (!stackSuccess && error) {
68
- callbacks.onError?.(error);
69
- }
70
- }
71
- });
72
- if (!parallelResult.success) {
73
- callbacks.onParallelPhaseComplete?.([]);
74
- return failure(parallelResult.error);
75
- }
76
- // Check for any failures
77
- const failures = parallelResult.data.filter((r) => !r.success);
78
- callbacks.onParallelPhaseComplete?.(parallelResult.data.map((r) => ({
79
- stack: r.stack,
80
- success: r.success,
81
- error: r.error
82
- })));
83
- if (failures.length > 0) {
84
- const failedStacks = failures.map((f) => f.stack).join(", ");
85
- const errorDetails = failures
86
- .map((f) => `${f.stack}: ${f.error?.message ?? "Unknown error"}`)
87
- .join("\n");
88
- return failure(new Error(`Failed to deploy stacks: ${failedStacks}\n\n${errorDetails}`));
89
- }
90
- // Collect outputs and track hashes for successful parallel stacks
91
- for (const result of parallelResult.data) {
92
- if (result.success && result.outputs) {
93
- for (const [key, value] of Object.entries(result.outputs)) {
94
- allOutputs[key] = String(value);
95
- }
96
- }
97
- const stackName = getApplicationStackName(operation.appName, result.stack);
98
- const hash = detection.currentHashes.get(stackName);
99
- if (hash) {
100
- deployedHashes.set(stackName, hash);
101
- }
102
- }
103
- return success(undefined);
104
- }
105
- /**
106
- * Deploy a single stack with step lifecycle events and output collection.
107
- */
108
- export async function deployStackSequential(stack, services, context, callbacks, stepIndex, totalSteps, allOutputs) {
109
- const stepId = getApplicationStepId(stack, "deploy");
110
- const stepName = getApplicationStepName(stack, "deploy");
111
- callbacks.onStepStart?.(stepId, stepName, stepIndex, totalSteps);
112
- const result = await services.stackService.deployStack(stack, context, {
113
- onOutput: forwardOutput(callbacks),
114
- onResourceProgress: forwardResourceProgress(callbacks)
115
- });
116
- if (!result.success) {
117
- callbacks.onStepComplete?.(stepId, stepName, "error", stepIndex, totalSteps);
118
- callbacks.onError?.(result.error);
119
- return failure(result.error);
120
- }
121
- callbacks.onStepComplete?.(stepId, stepName, "completed", stepIndex, totalSteps);
122
- if (result.data.outputs) {
123
- for (const [key, value] of Object.entries(result.data.outputs)) {
124
- allOutputs[key] = String(value);
125
- }
126
- }
127
- return success(undefined);
128
- }
129
- /**
130
- * Run Docker build/push or welcome image setup before the Compute stack.
131
- * Returns a failure Result if Docker operations fail, or null if not applicable/successful.
132
- */
133
- export async function runDockerPreCompute(stack, params, services, operation, callbacks, hasDockerfileOnDisk) {
134
- if (stack !== APPLICATION_STACKS.COMPUTE || !params.dockerProvider)
135
- return null;
136
- if (hasDockerfileOnDisk) {
137
- const dockerResult = await runDockerBuild(params, services, operation, callbacks);
138
- if (!dockerResult.success)
139
- return failure(dockerResult.error);
140
- }
141
- else {
142
- const ecrInitResult = await runWelcomeImageSetup(params, services, operation, callbacks);
143
- if (!ecrInitResult.success)
144
- return failure(ecrInitResult.error);
145
- }
146
- return null;
147
- }
148
- /**
149
- * Deploy all stacks unconditionally (deployOnly mode).
150
- * Skips CDK synth/hash comparison — uses the framework registry to determine
151
- * deploy order, then deploys every stack sequentially.
152
- */
153
- export async function deployAllStacks(params, services, operation, context, startTime, plan) {
154
- const { callbacks } = params;
155
- // Use plan from registry if available, otherwise resolve now
156
- let deployOrder;
157
- if (plan) {
158
- deployOrder = plan.deployOrder;
159
- }
160
- else {
161
- const resolved = services.frameworkRegistry.resolve({
162
- appPath: operation.path
163
- });
164
- if (resolved) {
165
- const freshPlan = resolved.builder.plan({ appPath: operation.path }, resolved.detection);
166
- deployOrder = freshPlan.deployOrder;
167
- }
168
- else {
169
- deployOrder = getApplicationDeployOrder();
170
- }
171
- }
172
- const totalSteps = deployOrder.length;
173
- // Bootstrap
174
- const bsResult = await bootstrapOrFail(services, context, callbacks);
175
- if (!bsResult.success)
176
- return bsResult;
177
- // Deploy all stacks sequentially
178
- const allOutputs = {};
179
- for (let i = 0; i < deployOrder.length; i++) {
180
- const stack = deployOrder[i];
181
- // Docker operations before Compute stack
182
- const dockerFailed = await runDockerPreCompute(stack, params, services, operation, callbacks, hasDockerfile(operation.path));
183
- if (dockerFailed)
184
- return dockerFailed;
185
- const deployResult = await deployStackSequential(stack, services, context, callbacks, i, totalSteps, allOutputs);
186
- if (!deployResult.success)
187
- return failure(deployResult.error);
188
- }
189
- // Resolve website URL
190
- const websiteUrl = await services.stackService.resolveWebsiteUrl(operation.appName);
191
- if (websiteUrl) {
192
- allOutputs.websiteUrl = websiteUrl;
193
- }
194
- return success({
195
- target: operation.appName,
196
- deploymentType: "application",
197
- outputs: Object.keys(allOutputs).length > 0 ? allOutputs : undefined,
198
- durationMs: Date.now() - startTime
199
- });
200
- }
201
- /**
202
- * Bridge DeployCallbacks to BuildCallbacks.
203
- * Fires both legacy OpenNext-specific callbacks and generic build callbacks
204
- * for backwards compatibility during the transition period.
205
- */
206
- export function createBuildCallbacks(callbacks) {
207
- return {
208
- onBuildStart: (builderName) => {
209
- callbacks.onOpenNextBuildStart?.();
210
- callbacks.onLog?.(`${builderName} build started`, "info");
211
- },
212
- onBuildProgress: (_builderName, message) => {
213
- callbacks.onOpenNextProgress?.(message);
214
- },
215
- onBuildComplete: (builderName) => {
216
- callbacks.onOpenNextBuildComplete?.();
217
- callbacks.onLog?.(`${builderName} build complete`, "info");
218
- },
219
- onBuildError: (_builderName, error) => {
220
- callbacks.onOpenNextBuildError?.(error);
221
- }
222
- };
223
- }
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}
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};
@@ -1,131 +1 @@
1
- import { success, failure } from "@fjall/generator";
2
- import { logger } from "@fjall/util/logger";
3
- import { getApplicationDestroyOrder, getApplicationStepName, getApplicationStepId } from "../types/operations.js";
4
- import { stubCallerIdentity } from "../types/deployment/index.js";
5
- import { deriveResourcesFromManifestStacks } from "../types/patternDetection.js";
6
- import { CdkContextBuilder } from "../services/supporting/CdkContextBuilder.js";
7
- import { buildParamsContext } from "./contextHelpers.js";
8
- import { deleteStateFile, readStateFile } from "../types/FjallState.js";
9
- /**
10
- * Core application destruction orchestration.
11
- *
12
- * Detects application pattern, determines destroy order (reverse of deploy),
13
- * then destroys stacks sequentially or in parallel groups. Delegates all
14
- * CDK operations to the ApplicationStackService.destroyAllStacks() method,
15
- * which handles "stack doesn't exist" → success conversion and parallel
16
- * phase orchestration for OpenNext patterns.
17
- */
18
- export async function destroyApplication(params, services, operation) {
19
- const { callbacks } = params;
20
- const startTime = Date.now();
21
- // 1. Detect pattern for correct destroy order
22
- const resolved = services.frameworkRegistry.resolve({
23
- appPath: operation.path
24
- });
25
- const pattern = resolved?.detection.pattern ?? null;
26
- // Derive resources from state file (if available from prior deploy)
27
- let destroyResources;
28
- try {
29
- const stateFile = await readStateFile(operation.path);
30
- if (stateFile !== null) {
31
- const stackNames = Object.keys(stateFile.templateHashes);
32
- if (stackNames.length > 0) {
33
- destroyResources = deriveResourcesFromManifestStacks(stackNames);
34
- }
35
- }
36
- }
37
- catch (err) {
38
- logger.debug("applicationDestroy", "Could not read state file for resource detection", { error: err instanceof Error ? err.message : String(err) });
39
- callbacks.onLog?.("Could not read state file for resource detection — falling back to pattern detection", "warn");
40
- }
41
- // 2. Build deployment context
42
- const context = CdkContextBuilder.buildDeploymentContext({
43
- deployType: "application",
44
- target: operation.appName,
45
- path: operation.path,
46
- region: services.awsProvider.getRegion(),
47
- callerIdentity: stubCallerIdentity(services.awsProvider.getAccountId()),
48
- ...buildParamsContext({
49
- orgConfig: params.orgConfig,
50
- identity: params.identity
51
- })
52
- }, {
53
- verbose: params.options?.verbose
54
- }, params.orgConfig);
55
- // 3. Determine destroy order
56
- const destroyOrder = getApplicationDestroyOrder({
57
- pattern,
58
- resources: destroyResources
59
- });
60
- const totalSteps = destroyOrder.length;
61
- callbacks.onLog?.(`Destroying ${operation.appName} (${totalSteps} stacks, ${pattern ?? "standard"} pattern)`, "info");
62
- // 4. Destroy stacks via ApplicationStackService
63
- const stacksDestroyed = [];
64
- const skippedStacks = [];
65
- const destroyResult = await services.stackService.destroyAllStacks(context, {
66
- onOutput: (chunk) => {
67
- callbacks.onOutput?.(chunk);
68
- },
69
- onResourceProgress: (event, stackId) => {
70
- callbacks.onResourceProgress?.(event);
71
- if (stackId) {
72
- callbacks.onParallelStackResourceProgress?.(stackId, event);
73
- }
74
- },
75
- onStackStart: (stackType, _stackName) => {
76
- const stepId = getApplicationStepId(stackType, "destroy");
77
- const stepName = getApplicationStepName(stackType, "destroy");
78
- const stepIndex = destroyOrder.indexOf(stackType);
79
- callbacks.onStepStart?.(stepId, stepName, stepIndex, totalSteps);
80
- },
81
- onStackComplete: async (stackType, result) => {
82
- const stepId = getApplicationStepId(stackType, "destroy");
83
- const stepName = getApplicationStepName(stackType, "destroy");
84
- const stepIndex = destroyOrder.indexOf(stackType);
85
- if (result.success) {
86
- if (result.data?.skipped) {
87
- skippedStacks.push(result.data.stackName || stackType);
88
- callbacks.onStepComplete?.(stepId, stepName, "skipped", stepIndex, totalSteps);
89
- }
90
- else {
91
- stacksDestroyed.push(result.data?.stackName || stackType);
92
- callbacks.onStepComplete?.(stepId, stepName, "completed", stepIndex, totalSteps);
93
- }
94
- }
95
- else {
96
- callbacks.onStepComplete?.(stepId, stepName, "error", stepIndex, totalSteps);
97
- callbacks.onError?.(result.error);
98
- }
99
- },
100
- onParallelPhaseStart: (stacks, description) => {
101
- callbacks.onLog?.(`Parallel phase: ${description}`, "info");
102
- callbacks.onParallelPhaseStart?.(stacks, description);
103
- },
104
- onParallelPhaseComplete: (results) => {
105
- const failed = results.filter((r) => !r.success);
106
- if (failed.length > 0) {
107
- callbacks.onLog?.(`Parallel phase completed with ${failed.length} failure(s)`, "warn");
108
- }
109
- callbacks.onParallelPhaseComplete?.(results);
110
- }
111
- }, destroyResources);
112
- if (!destroyResult.success) {
113
- callbacks.onError?.(destroyResult.error);
114
- return failure(destroyResult.error);
115
- }
116
- // 5. Clean up state file after successful destroy
117
- try {
118
- await deleteStateFile(operation.path);
119
- }
120
- catch (err) {
121
- logger.debug("applicationDestroy", "Failed to delete state file (non-critical)", { error: err instanceof Error ? err.message : String(err) });
122
- callbacks.onLog?.("Failed to delete state file (non-critical)", "warn");
123
- }
124
- return success({
125
- target: operation.appName,
126
- deploymentType: "application",
127
- stacksDestroyed,
128
- skippedStacks,
129
- durationMs: Date.now() - startTime
130
- });
131
- }
1
+ import{success as k,failure as w}from"@fjall/generator";import{logger as y}from"@fjall/util/logger";import{getApplicationDestroyOrder as b,getApplicationStepName as h,getApplicationStepId as S}from"../types/operations.js";import{stubCallerIdentity as N}from"../types/deployment/index.js";import{deriveResourcesFromManifestStacks as x}from"../types/patternDetection.js";import{CdkContextBuilder as D}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as R}from"./contextHelpers.js";import{deleteStateFile as I,readStateFile as O}from"../types/FjallState.js";async function H(n,c,r){const{callbacks:o}=n,P=Date.now(),f=c.frameworkRegistry.resolve({appPath:r.path})?.detection.pattern??null;let p;try{const e=await O(r.path);if(e!==null){const t=Object.keys(e.templateHashes);t.length>0&&(p=x(t))}}catch(e){y.debug("applicationDestroy","Could not read state file for resource detection",{error:e instanceof Error?e.message:String(e)}),o.onLog?.("Could not read state file for resource detection \u2014 falling back to pattern detection","warn")}const C=D.buildDeploymentContext({deployType:"application",target:r.appName,path:r.path,region:c.awsProvider.getRegion(),callerIdentity:N(c.awsProvider.getAccountId()),...R({orgConfig:n.orgConfig,identity:n.identity})},{verbose:n.options?.verbose},n.orgConfig),d=b({pattern:f,resources:p}),s=d.length;o.onLog?.(`Destroying ${r.appName} (${s} stacks, ${f??"standard"} pattern)`,"info");const g=[],u=[],m=await c.stackService.destroyAllStacks(C,{onOutput:e=>{o.onOutput?.(e)},onResourceProgress:(e,t)=>{o.onResourceProgress?.(e),t&&o.onParallelStackResourceProgress?.(t,e)},onStackStart:(e,t)=>{const a=S(e,"destroy"),l=h(e,"destroy"),i=d.indexOf(e);o.onStepStart?.(a,l,i,s)},onStackComplete:async(e,t)=>{const a=S(e,"destroy"),l=h(e,"destroy"),i=d.indexOf(e);t.success?t.data?.skipped?(u.push(t.data.stackName||e),o.onStepComplete?.(a,l,"skipped",i,s)):(g.push(t.data?.stackName||e),o.onStepComplete?.(a,l,"completed",i,s)):(o.onStepComplete?.(a,l,"error",i,s),o.onError?.(t.error))},onParallelPhaseStart:(e,t)=>{o.onLog?.(`Parallel phase: ${t}`,"info"),o.onParallelPhaseStart?.(e,t)},onParallelPhaseComplete:e=>{const t=e.filter(a=>!a.success);t.length>0&&o.onLog?.(`Parallel phase completed with ${t.length} failure(s)`,"warn"),o.onParallelPhaseComplete?.(e)}},p);if(!m.success)return o.onError?.(m.error),w(m.error);try{await I(r.path)}catch(e){y.debug("applicationDestroy","Failed to delete state file (non-critical)",{error:e instanceof Error?e.message:String(e)}),o.onLog?.("Failed to delete state file (non-critical)","warn")}return k({target:r.appName,deploymentType:"application",stacksDestroyed:g,skippedStacks:u,durationMs:Date.now()-P})}export{H as destroyApplication};
@@ -1,98 +1 @@
1
- /**
2
- * DockerBuilder — FrameworkBuilder implementation for standard Docker apps.
3
- *
4
- * Fallback builder (priority 100) that detects any Fjall app with an
5
- * infrastructure.ts file. Docker builds are NOT handled here — they are
6
- * handled by DockerProvider in the orchestration layer. This builder only
7
- * produces the deployment plan and resource detection.
8
- */
9
- import { existsSync, readFileSync } from "fs";
10
- import { join } from "path";
11
- import { success } from "@fjall/generator";
12
- import { logger } from "@fjall/util/logger";
13
- import { getErrorMessage } from "@fjall/util";
14
- import { hasDockerfile } from "../../util/dockerfileDetection.js";
15
- import { INFRASTRUCTURE_FILENAME } from "../../types/constants.js";
16
- import { getApplicationDeployOrder, getApplicationDestroyOrder } from "../../types/operations.js";
17
- /** Default resources when infrastructure.ts cannot be parsed */
18
- const DEFAULT_RESOURCES = {
19
- hasNetwork: true,
20
- hasCompute: true,
21
- hasDatabase: false,
22
- hasStorage: false,
23
- hasMessaging: false,
24
- hasCdn: false
25
- };
26
- /**
27
- * Read infrastructure.ts and derive resource flags from its content.
28
- */
29
- function detectResources(appPath) {
30
- try {
31
- const filePath = join(appPath, INFRASTRUCTURE_FILENAME);
32
- if (!existsSync(filePath))
33
- return { exists: false, hasDatabase: false };
34
- const content = readFileSync(filePath, "utf-8");
35
- const hasDatabase = content.includes("DatabaseFactory.build(");
36
- return { exists: true, hasDatabase };
37
- }
38
- catch (error) {
39
- logger.debug("dockerBuilder", "Failed to read infrastructure file", {
40
- appPath,
41
- error: getErrorMessage(error)
42
- });
43
- return { exists: false, hasDatabase: false };
44
- }
45
- }
46
- /**
47
- * Docker FrameworkBuilder for standard containerised applications.
48
- *
49
- * Priority 100 — checked last as the fallback builder. Any Fjall app with
50
- * an infrastructure.ts file is a valid Docker app unless a higher-priority
51
- * builder (e.g. OpenNext) claims it first.
52
- */
53
- export const dockerBuilder = {
54
- name: "docker",
55
- priority: 100,
56
- detect(context) {
57
- const { exists, hasDatabase } = detectResources(context.appPath);
58
- if (!exists) {
59
- return {
60
- detected: false,
61
- pattern: null,
62
- reason: "No infrastructure.ts found",
63
- resources: DEFAULT_RESOURCES,
64
- hasDatabase: false,
65
- hasDockerfile: false
66
- };
67
- }
68
- const dockerfile = hasDockerfile(context.appPath);
69
- return {
70
- detected: true,
71
- pattern: null,
72
- reason: dockerfile
73
- ? "infrastructure.ts found with Dockerfile"
74
- : "infrastructure.ts found (no Dockerfile)",
75
- resources: { ...DEFAULT_RESOURCES, hasDatabase },
76
- hasDatabase,
77
- hasDockerfile: dockerfile
78
- };
79
- },
80
- plan(_context, detection) {
81
- const resources = detection.resources;
82
- return {
83
- builderName: "docker",
84
- deployOrder: getApplicationDeployOrder({ resources }),
85
- destroyOrder: getApplicationDestroyOrder({ resources }),
86
- parallelDestroy: false,
87
- preBuildCommands: [],
88
- buildCommand: null,
89
- requiresDockerBuild: detection.hasDockerfile,
90
- resources
91
- };
92
- },
93
- async build(_appPath, _plan, _callbacks, _options) {
94
- // Docker builds are handled by DockerProvider in the orchestration layer,
95
- // not by the builder. This is the correct separation of concerns.
96
- return success(undefined);
97
- }
98
- };
1
+ import{existsSync as o,readFileSync as n}from"fs";import{join as i}from"path";import{success as l}from"@fjall/generator";import{logger as c}from"@fjall/util/logger";import{getErrorMessage as u}from"@fjall/util";import{hasDockerfile as f}from"../../util/dockerfileDetection.js";import{INFRASTRUCTURE_FILENAME as d}from"../../types/constants.js";import{getApplicationDeployOrder as p,getApplicationDestroyOrder as m}from"../../types/operations.js";const a={hasNetwork:!0,hasCompute:!0,hasDatabase:!1,hasStorage:!1,hasMessaging:!1,hasCdn:!1};function h(s){try{const e=i(s,d);return o(e)?{exists:!0,hasDatabase:n(e,"utf-8").includes("DatabaseFactory.build(")}:{exists:!1,hasDatabase:!1}}catch(e){return c.debug("dockerBuilder","Failed to read infrastructure file",{appPath:s,error:u(e)}),{exists:!1,hasDatabase:!1}}}const C={name:"docker",priority:100,detect(s){const{exists:e,hasDatabase:r}=h(s.appPath);if(!e)return{detected:!1,pattern:null,reason:"No infrastructure.ts found",resources:a,hasDatabase:!1,hasDockerfile:!1};const t=f(s.appPath);return{detected:!0,pattern:null,reason:t?"infrastructure.ts found with Dockerfile":"infrastructure.ts found (no Dockerfile)",resources:{...a,hasDatabase:r},hasDatabase:r,hasDockerfile:t}},plan(s,e){const r=e.resources;return{builderName:"docker",deployOrder:p({resources:r}),destroyOrder:m({resources:r}),parallelDestroy:!1,preBuildCommands:[],buildCommand:null,requiresDockerBuild:e.hasDockerfile,resources:r}},async build(s,e,r,t){return l(void 0)}};export{C as dockerBuilder};
@@ -1,144 +1 @@
1
- /**
2
- * OpenNextBuilder — FrameworkBuilder implementation for Next.js and Payload apps.
3
- *
4
- * Detects OpenNext patterns from infrastructure.ts, produces a build plan with
5
- * the correct stack ordering, and delegates build execution to `runOpenNextBuild()`.
6
- *
7
- * BUILD ORDERING NOTE: The OpenNext build runs BEFORE the detection pipeline
8
- * because CDK synth needs the `.open-next/` directory to exist. This builder's
9
- * `build()` method is called early in the deployment flow, not after detection.
10
- * The plan's `buildCommand` is declarative — actual execution goes through
11
- * `runOpenNextBuild()` which handles binary resolution, timeouts, and cleanup.
12
- */
13
- import { basename } from "path";
14
- import { success } from "@fjall/generator";
15
- import { logger } from "@fjall/util/logger";
16
- import { OPENNEXT_DEPLOY_ORDER, OPENNEXT_DESTROY_ORDER } from "../../types/operations.js";
17
- import { readInfrastructureContent } from "../../types/patternDetection.js";
18
- import { runOpenNextBuild, BUILD_TIMEOUT_MS, IMPORT_MAP_TIMEOUT_MS } from "../openNextBuild.js";
19
- /** Resource flags for all OpenNext apps (hasDatabase is dynamic) */
20
- function openNextResources(hasDatabase) {
21
- return {
22
- hasNetwork: true,
23
- hasCompute: true,
24
- hasDatabase,
25
- hasStorage: true,
26
- hasMessaging: true,
27
- hasCdn: true
28
- };
29
- }
30
- /**
31
- * OpenNext FrameworkBuilder for Next.js and Payload applications.
32
- *
33
- * Priority 10 — checked before Docker (priority 50) since OpenNext is more
34
- * specific. Apps matching an OpenNext pattern use Lambda-based deployment
35
- * with S3, DynamoDB, SQS, and CloudFront stacks.
36
- */
37
- export const openNextBuilder = {
38
- name: "opennext",
39
- priority: 10,
40
- detect(context) {
41
- const notDetected = {
42
- detected: false,
43
- pattern: null,
44
- reason: "No OpenNext pattern found",
45
- resources: openNextResources(false),
46
- hasDatabase: false,
47
- hasDockerfile: false
48
- };
49
- const content = readInfrastructureContent(context.appPath);
50
- if (!content) {
51
- return {
52
- ...notDetected,
53
- reason: "No infrastructure.ts found"
54
- };
55
- }
56
- if (!content.includes("PatternFactory")) {
57
- return {
58
- ...notDetected,
59
- reason: "No PatternFactory reference in infrastructure.ts"
60
- };
61
- }
62
- if (content.includes('type: "payload"')) {
63
- return {
64
- detected: true,
65
- pattern: "payload",
66
- reason: 'PatternFactory with type: "payload" detected',
67
- resources: openNextResources(true),
68
- hasDatabase: true,
69
- hasDockerfile: false
70
- };
71
- }
72
- if (content.includes('type: "nextjs"')) {
73
- // Next.js database detection is dynamic — check for explicit database usage
74
- const hasDatabase = content.includes("DatabaseFactory.build(") ||
75
- content.includes("database:");
76
- return {
77
- detected: true,
78
- pattern: "nextjs",
79
- reason: 'PatternFactory with type: "nextjs" detected',
80
- resources: openNextResources(hasDatabase),
81
- hasDatabase,
82
- hasDockerfile: false
83
- };
84
- }
85
- return {
86
- ...notDetected,
87
- reason: "PatternFactory found but no recognised pattern type"
88
- };
89
- },
90
- plan(_context, detection) {
91
- const preBuildCommands = detection.pattern === "payload"
92
- ? [
93
- {
94
- name: "payload-import-map",
95
- binary: "npx",
96
- args: ["payload", "generate:importmap"],
97
- timeoutMs: IMPORT_MAP_TIMEOUT_MS
98
- }
99
- ]
100
- : [];
101
- return {
102
- builderName: "opennext",
103
- deployOrder: OPENNEXT_DEPLOY_ORDER,
104
- destroyOrder: OPENNEXT_DESTROY_ORDER,
105
- parallelDestroy: true,
106
- preBuildCommands,
107
- buildCommand: {
108
- name: "open-next-build",
109
- binary: "open-next",
110
- args: ["build"],
111
- timeoutMs: BUILD_TIMEOUT_MS
112
- },
113
- requiresDockerBuild: false,
114
- resources: detection.resources
115
- };
116
- },
117
- async build(appPath, plan, callbacks, options) {
118
- if (options?.skipBuild || options?.infraOnly) {
119
- logger.debug("openNextBuilder", "Build skipped", {
120
- skipBuild: options.skipBuild,
121
- infraOnly: options.infraOnly
122
- });
123
- return success(undefined);
124
- }
125
- // Delegate to existing runOpenNextBuild which handles binary resolution,
126
- // import map generation, timeouts, stream cleanup, and artefact validation.
127
- // We build an ApplicationOperation and map BuildCallbacks to DeployCallbacks.
128
- const operation = {
129
- kind: "application",
130
- appName: basename(appPath) || "app",
131
- path: appPath
132
- };
133
- // Determine pattern from the plan's pre-build commands
134
- const isPayload = plan.preBuildCommands.some((cmd) => cmd.name === "payload-import-map");
135
- const pattern = isPayload ? "payload" : "nextjs";
136
- const result = await runOpenNextBuild(operation, pattern, {
137
- onOpenNextBuildStart: () => callbacks.onBuildStart?.("opennext"),
138
- onOpenNextProgress: (message) => callbacks.onBuildProgress?.("opennext", message),
139
- onOpenNextBuildComplete: () => callbacks.onBuildComplete?.("opennext"),
140
- onOpenNextBuildError: (error) => callbacks.onBuildError?.("opennext", error)
141
- });
142
- return result;
143
- }
144
- };
1
+ import{basename as i}from"path";import{success as p}from"@fjall/generator";import{logger as d}from"@fjall/util/logger";import{OPENNEXT_DEPLOY_ORDER as l,OPENNEXT_DESTROY_ORDER as c}from"../../types/operations.js";import{readInfrastructureContent as m}from"../../types/patternDetection.js";import{runOpenNextBuild as f,BUILD_TIMEOUT_MS as y,IMPORT_MAP_TIMEOUT_MS as x}from"../openNextBuild.js";function o(r){return{hasNetwork:!0,hasCompute:!0,hasDatabase:r,hasStorage:!0,hasMessaging:!0,hasCdn:!0}}const E={name:"opennext",priority:10,detect(r){const t={detected:!1,pattern:null,reason:"No OpenNext pattern found",resources:o(!1),hasDatabase:!1,hasDockerfile:!1},e=m(r.appPath);if(!e)return{...t,reason:"No infrastructure.ts found"};if(!e.includes("PatternFactory"))return{...t,reason:"No PatternFactory reference in infrastructure.ts"};if(e.includes('type: "payload"'))return{detected:!0,pattern:"payload",reason:'PatternFactory with type: "payload" detected',resources:o(!0),hasDatabase:!0,hasDockerfile:!1};if(e.includes('type: "nextjs"')){const n=e.includes("DatabaseFactory.build(")||e.includes("database:");return{detected:!0,pattern:"nextjs",reason:'PatternFactory with type: "nextjs" detected',resources:o(n),hasDatabase:n,hasDockerfile:!1}}return{...t,reason:"PatternFactory found but no recognised pattern type"}},plan(r,t){const e=t.pattern==="payload"?[{name:"payload-import-map",binary:"npx",args:["payload","generate:importmap"],timeoutMs:x}]:[];return{builderName:"opennext",deployOrder:l,destroyOrder:c,parallelDestroy:!0,preBuildCommands:e,buildCommand:{name:"open-next-build",binary:"open-next",args:["build"],timeoutMs:y},requiresDockerBuild:!1,resources:t.resources}},async build(r,t,e,n){if(n?.skipBuild||n?.infraOnly)return d.debug("openNextBuilder","Build skipped",{skipBuild:n.skipBuild,infraOnly:n.infraOnly}),p(void 0);const s={kind:"application",appName:i(r)||"app",path:r},u=t.preBuildCommands.some(a=>a.name==="payload-import-map")?"payload":"nextjs";return await f(s,u,{onOpenNextBuildStart:()=>e.onBuildStart?.("opennext"),onOpenNextProgress:a=>e.onBuildProgress?.("opennext",a),onOpenNextBuildComplete:()=>e.onBuildComplete?.("opennext"),onOpenNextBuildError:a=>e.onBuildError?.("opennext",a)})}};export{E as openNextBuilder};