@fjall/deploy-core 0.94.1 → 0.96.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,243 +1,3 @@
1
- /**
2
- * OpenNext build orchestration for Next.js and Payload applications.
3
- *
4
- * Resolves `@opennextjs/aws` from the app's own node_modules (not from
5
- * deploy-core's dependencies) and runs the build with timeout handling,
6
- * stream cleanup, and credential masking.
7
- *
8
- * For Payload apps, generates the import map before building.
9
- */
10
- import { createRequire } from "module";
11
- import { existsSync, statSync, rmSync } from "fs";
12
- import { join } from "path";
13
- import { success, failure } from "@fjall/generator";
14
- import { filterDangerousEnvVars, maskSensitiveOutput, parseShellArgs } from "@fjall/util";
15
- import { logger } from "@fjall/util/logger";
16
- import { isOpenNextPattern } from "../types/patternDetection.js";
17
- import { spawnWithTimeout } from "./spawnHelpers.js";
18
- const OPENNEXT_PACKAGE = "@opennextjs/aws";
19
- /** Default build timeout: 10 minutes */
20
- export const BUILD_TIMEOUT_MS = 600_000;
21
- /** Timeout for Payload import map generation: 30 seconds */
22
- export const IMPORT_MAP_TIMEOUT_MS = 30_000;
23
- /**
24
- * Resolve the `open-next` binary from the app's own node_modules.
25
- *
26
- * Uses `createRequire` anchored to the app path so that the binary
27
- * comes from the project's dependencies, not deploy-core's.
28
- */
29
- function resolveOpenNextBinary(appPath) {
30
- const require = createRequire(join(appPath, "package.json"));
31
- let searchPaths;
32
- try {
33
- searchPaths = require.resolve.paths(OPENNEXT_PACKAGE);
34
- }
35
- catch (err) {
36
- return failure(new Error(`Cannot determine module search paths for ${OPENNEXT_PACKAGE}. ` +
37
- `Ensure it is installed in your project. (${err instanceof Error ? err.message : String(err)})`));
38
- }
39
- if (!searchPaths || searchPaths.length === 0) {
40
- return failure(new Error(`Cannot determine module search paths for ${OPENNEXT_PACKAGE}. ` +
41
- "Ensure it is installed in your project."));
42
- }
43
- for (const nodeModulesPath of searchPaths) {
44
- const packageDir = join(nodeModulesPath, "@opennextjs", "aws");
45
- try {
46
- const stat = statSync(packageDir);
47
- if (stat.isDirectory()) {
48
- const binPath = join(nodeModulesPath, ".bin", "open-next");
49
- if (!existsSync(binPath)) {
50
- logger.debug("openNextBuild", "Package found but binary missing", {
51
- nodeModulesPath,
52
- binPath
53
- });
54
- continue;
55
- }
56
- logger.debug("openNextBuild", "Resolved OpenNext binary", {
57
- binPath
58
- });
59
- return success(binPath);
60
- }
61
- }
62
- catch (err) {
63
- logger.debug("openNextBuild", "Package not at location", {
64
- nodeModulesPath,
65
- error: String(err)
66
- });
67
- }
68
- }
69
- return failure(new Error(`Cannot find ${OPENNEXT_PACKAGE} package. ` +
70
- `Ensure it is installed in your project: npm install ${OPENNEXT_PACKAGE}`));
71
- }
72
- /**
73
- * Clean up the `.open-next/` directory on build failure to prevent
74
- * stale artefacts from being used by a subsequent CDK synth.
75
- */
76
- function cleanupOpenNextOutput(appPath) {
77
- const openNextDir = join(appPath, ".open-next");
78
- if (existsSync(openNextDir)) {
79
- try {
80
- rmSync(openNextDir, { recursive: true, force: true });
81
- logger.debug("openNextBuild", "Cleaned up stale .open-next directory", {
82
- path: openNextDir
83
- });
84
- }
85
- catch (err) {
86
- logger.debug("openNextBuild", "Failed to clean up .open-next", {
87
- error: String(err)
88
- });
89
- }
90
- }
91
- }
92
- /**
93
- * Handle the common error cases (timeout, enoent, spawn_error) from spawnWithTimeout.
94
- * Returns a failure Result for error cases, or undefined if the result is "success".
95
- */
96
- function handleSpawnError(result, labels, onCleanup, onError) {
97
- switch (result.type) {
98
- case "timeout": {
99
- onCleanup?.();
100
- onError?.(labels.timeoutMsg);
101
- return failure(new Error(labels.timeoutMsg));
102
- }
103
- case "enoent": {
104
- onCleanup?.();
105
- onError?.(labels.enoentMsg);
106
- return failure(new Error(labels.enoentMsg));
107
- }
108
- case "spawn_error": {
109
- const msg = `${labels.spawnErrorPrefix}: ${result.error.message}`;
110
- onCleanup?.();
111
- onError?.(msg);
112
- return failure(new Error(msg));
113
- }
114
- default:
115
- return undefined;
116
- }
117
- }
118
- /**
119
- * Generate the Payload CMS import map before running the OpenNext build.
120
- */
121
- async function generatePayloadImportMap(appPath, callbacks) {
122
- callbacks.onOpenNextProgress?.(maskSensitiveOutput("Generating Payload import map..."));
123
- const [cmd, ...args] = parseShellArgs("npx payload generate:importmap");
124
- const safeEnv = filterDangerousEnvVars(globalThis.process.env);
125
- const result = await spawnWithTimeout({
126
- command: cmd,
127
- args,
128
- cwd: appPath,
129
- env: { ...safeEnv, FORCE_COLOR: "0" },
130
- timeout: IMPORT_MAP_TIMEOUT_MS,
131
- onStdoutData: (output) => {
132
- callbacks.onOpenNextProgress?.(maskSensitiveOutput(output.trim()));
133
- }
134
- });
135
- const spawnError = handleSpawnError(result, {
136
- timeoutMsg: `Payload import map generation timed out after ${IMPORT_MAP_TIMEOUT_MS / 1000} seconds`,
137
- enoentMsg: "Payload CLI not found. Ensure payload is installed: npm install payload",
138
- spawnErrorPrefix: "Failed to generate Payload import map"
139
- });
140
- if (spawnError)
141
- return spawnError;
142
- if (result.type !== "success")
143
- return failure(new Error("Unexpected spawn result"));
144
- if (result.code !== 0) {
145
- const maskedOutput = maskSensitiveOutput(result.stderr || result.stdout);
146
- return failure(new Error(`Payload import map generation failed (exit code ${result.code}):\n${maskedOutput}`));
147
- }
148
- logger.debug("openNextBuild", "Payload import map generated", { appPath });
149
- callbacks.onOpenNextProgress?.(maskSensitiveOutput("Payload import map generated"));
150
- return success(undefined);
151
- }
152
- /**
153
- * Run the OpenNext build for a Next.js or Payload application.
154
- *
155
- * Checks pattern and options internally — returns `success(undefined)`
156
- * when build is not needed (wrong pattern, skipBuild, infraOnly).
157
- *
158
- * Must be called BEFORE the detection pipeline and BEFORE the
159
- * `deployOnly` early-return check, so built artefacts are available
160
- * for CDK synth.
161
- */
162
- export async function runOpenNextBuild(operation, pattern, callbacks, options) {
163
- // Guard: skip if not an OpenNext pattern
164
- if (!isOpenNextPattern(pattern)) {
165
- return success(undefined);
166
- }
167
- // Guard: skip if build explicitly disabled
168
- if (options?.skipBuild || options?.infraOnly) {
169
- logger.debug("openNextBuild", "Build skipped", {
170
- skipBuild: options?.skipBuild,
171
- infraOnly: options?.infraOnly
172
- });
173
- callbacks.onLog?.(options?.infraOnly
174
- ? "Infrastructure-only mode — skipping OpenNext build"
175
- : "Build skipped (--skip-build)", "info");
176
- return success(undefined);
177
- }
178
- const appPath = operation.path;
179
- // For Payload apps, generate import map first
180
- if (pattern === "payload") {
181
- const importMapResult = await generatePayloadImportMap(appPath, callbacks);
182
- if (!importMapResult.success) {
183
- callbacks.onOpenNextBuildError?.(importMapResult.error.message);
184
- cleanupOpenNextOutput(appPath);
185
- return failure(importMapResult.error);
186
- }
187
- }
188
- // Resolve OpenNext binary from the app's node_modules
189
- const binaryResult = resolveOpenNextBinary(appPath);
190
- if (!binaryResult.success) {
191
- callbacks.onOpenNextBuildError?.(binaryResult.error.message);
192
- return failure(binaryResult.error);
193
- }
194
- const openNextBinary = binaryResult.data;
195
- // Start build
196
- callbacks.onOpenNextBuildStart?.();
197
- callbacks.onOpenNextProgress?.(maskSensitiveOutput("Starting OpenNext build..."));
198
- const safeEnv = filterDangerousEnvVars(globalThis.process.env);
199
- const projectNodeModules = join(appPath, "node_modules");
200
- const result = await spawnWithTimeout({
201
- command: openNextBinary,
202
- args: ["build"],
203
- cwd: appPath,
204
- env: {
205
- ...safeEnv,
206
- FORCE_COLOR: "0",
207
- NODE_PATH: projectNodeModules,
208
- npm_config_loglevel: "error",
209
- PNPM_HOME: safeEnv.PNPM_HOME || ""
210
- },
211
- timeout: BUILD_TIMEOUT_MS,
212
- onStdoutData: (output) => {
213
- callbacks.onOpenNextProgress?.(maskSensitiveOutput(output.trim()));
214
- },
215
- onStderrData: (output) => {
216
- callbacks.onOpenNextProgress?.(maskSensitiveOutput(output.trim()));
217
- }
218
- });
219
- const spawnError = handleSpawnError(result, {
220
- timeoutMsg: `OpenNext build timed out after ${BUILD_TIMEOUT_MS / 1000} seconds`,
221
- enoentMsg: "OpenNext binary not found. Ensure Node.js is installed and in PATH.",
222
- spawnErrorPrefix: "Failed to spawn OpenNext build"
223
- }, () => cleanupOpenNextOutput(appPath), (msg) => callbacks.onOpenNextBuildError?.(msg));
224
- if (spawnError)
225
- return spawnError;
226
- if (result.type !== "success")
227
- return failure(new Error("Unexpected spawn result"));
228
- if (result.code !== 0) {
229
- cleanupOpenNextOutput(appPath);
230
- const maskedOutput = maskSensitiveOutput(result.stderr || result.stdout);
231
- callbacks.onOpenNextBuildError?.(`OpenNext build failed (exit code ${result.code})`);
232
- return failure(new Error(`OpenNext build failed (exit code ${result.code}):\n${maskedOutput}`));
233
- }
234
- // Verify .open-next directory was created
235
- const openNextDir = join(appPath, ".open-next");
236
- if (!existsSync(openNextDir)) {
237
- callbacks.onOpenNextBuildError?.("OpenNext build completed but .open-next directory not found");
238
- return failure(new Error("OpenNext build completed but .open-next directory not found"));
239
- }
240
- callbacks.onOpenNextBuildComplete?.();
241
- callbacks.onOpenNextProgress?.(maskSensitiveOutput("OpenNext build completed successfully"));
242
- return success(undefined);
243
- }
1
+ import{createRequire as _}from"module";import{existsSync as g,statSync as S,rmSync as $}from"fs";import{join as l}from"path";import{success as f,failure as i}from"@fjall/generator";import{filterDangerousEnvVars as y,maskSensitiveOutput as p,parseShellArgs as v}from"@fjall/util";import{logger as c}from"@fjall/util/logger";import{isOpenNextPattern as b}from"../types/patternDetection.js";import{spawnWithTimeout as E}from"./spawnHelpers.js";const x="@opennextjs/aws",P=6e5,w=3e4;function T(s){const n=_(l(s,"package.json"));let e;try{e=n.resolve.paths(x)}catch(r){return i(new Error(`Cannot determine module search paths for ${x}. Ensure it is installed in your project. (${r instanceof Error?r.message:String(r)})`))}if(!e||e.length===0)return i(new Error(`Cannot determine module search paths for ${x}. Ensure it is installed in your project.`));for(const r of e){const t=l(r,"@opennextjs","aws");try{if(S(t).isDirectory()){const d=l(r,".bin","open-next");if(!g(d)){c.debug("openNextBuild","Package found but binary missing",{nodeModulesPath:r,binPath:d});continue}return c.debug("openNextBuild","Resolved OpenNext binary",{binPath:d}),f(d)}}catch(o){c.debug("openNextBuild","Package not at location",{nodeModulesPath:r,error:String(o)})}}return i(new Error(`Cannot find ${x} package. Ensure it is installed in your project: npm install ${x}`))}function O(s){const n=l(s,".open-next");if(g(n))try{$(n,{recursive:!0,force:!0}),c.debug("openNextBuild","Cleaned up stale .open-next directory",{path:n})}catch(e){c.debug("openNextBuild","Failed to clean up .open-next",{error:String(e)})}}function B(s,n,e,r){switch(s.type){case"timeout":return e?.(),r?.(n.timeoutMsg),i(new Error(n.timeoutMsg));case"enoent":return e?.(),r?.(n.enoentMsg),i(new Error(n.enoentMsg));case"spawn_error":{const t=`${n.spawnErrorPrefix}: ${s.error.message}`;return e?.(),r?.(t),i(new Error(t))}default:return}}async function D(s,n){n.onOpenNextProgress?.(p("Generating Payload import map..."));const[e,...r]=v("npx payload generate:importmap"),t=y(globalThis.process.env),o=await E({command:e,args:r,cwd:s,env:{...t,FORCE_COLOR:"0"},timeout:w,onStdoutData:m=>{n.onOpenNextProgress?.(p(m.trim()))}}),d=B(o,{timeoutMsg:`Payload import map generation timed out after ${w/1e3} seconds`,enoentMsg:"Payload CLI not found. Ensure payload is installed: npm install payload",spawnErrorPrefix:"Failed to generate Payload import map"});if(d)return d;if(o.type!=="success")return i(new Error("Unexpected spawn result"));if(o.code!==0){const m=p(o.stderr||o.stdout);return i(new Error(`Payload import map generation failed (exit code ${o.code}):
2
+ ${m}`))}return c.debug("openNextBuild","Payload import map generated",{appPath:s}),n.onOpenNextProgress?.(p("Payload import map generated")),f(void 0)}async function H(s,n,e,r){if(!b(n))return f(void 0);if(r?.skipBuild||r?.infraOnly)return c.debug("openNextBuild","Build skipped",{skipBuild:r?.skipBuild,infraOnly:r?.infraOnly}),e.onLog?.(r?.infraOnly?"Infrastructure-only mode \u2014 skipping OpenNext build":"Build skipped (--skip-build)","info"),f(void 0);const t=s.path;if(n==="payload"){const u=await D(t,e);if(!u.success)return e.onOpenNextBuildError?.(u.error.message),O(t),i(u.error)}const o=T(t);if(!o.success)return e.onOpenNextBuildError?.(o.error.message),i(o.error);const d=o.data;e.onOpenNextBuildStart?.(),e.onOpenNextProgress?.(p("Starting OpenNext build..."));const m=y(globalThis.process.env),M=l(t,"node_modules"),a=await E({command:d,args:["build"],cwd:t,env:{...m,FORCE_COLOR:"0",NODE_PATH:M,npm_config_loglevel:"error",PNPM_HOME:m.PNPM_HOME||""},timeout:P,onStdoutData:u=>{e.onOpenNextProgress?.(p(u.trim()))},onStderrData:u=>{e.onOpenNextProgress?.(p(u.trim()))}}),N=B(a,{timeoutMsg:`OpenNext build timed out after ${P/1e3} seconds`,enoentMsg:"OpenNext binary not found. Ensure Node.js is installed and in PATH.",spawnErrorPrefix:"Failed to spawn OpenNext build"},()=>O(t),u=>e.onOpenNextBuildError?.(u));if(N)return N;if(a.type!=="success")return i(new Error("Unexpected spawn result"));if(a.code!==0){O(t);const u=p(a.stderr||a.stdout);return e.onOpenNextBuildError?.(`OpenNext build failed (exit code ${a.code})`),i(new Error(`OpenNext build failed (exit code ${a.code}):
3
+ ${u}`))}const h=l(t,".open-next");return g(h)?(e.onOpenNextBuildComplete?.(),e.onOpenNextProgress?.(p("OpenNext build completed successfully")),f(void 0)):(e.onOpenNextBuildError?.("OpenNext build completed but .open-next directory not found"),i(new Error("OpenNext build completed but .open-next directory not found")))}export{P as BUILD_TIMEOUT_MS,w as IMPORT_MAP_TIMEOUT_MS,H as runOpenNextBuild};
@@ -1,284 +1,3 @@
1
- import { success, failure } from "@fjall/generator";
2
- import { ORGANISATION_TYPES, getOrganisationStackName } from "../types/operations.js";
3
- import { CdkContextBuilder } from "../services/supporting/CdkContextBuilder.js";
4
- import { stubCallerIdentity } from "../types/deployment/index.js";
5
- import { buildParamsContext, collectStackOutputs, synthOrFail, bootstrapOrFail, forwardOutput, forwardResourceProgress } from "./contextHelpers.js";
6
- import { partitionAccounts, deployCascadeAccount, readPlatformIpamPoolIds, deployDomains } from "./cascadeHelpers.js";
7
- import { maskSensitiveOutput } from "@fjall/util";
8
- import { INFRA_STEP_NAME, STEP_IDS } from "../types/stepDefinitions.js";
9
- /**
10
- * Organisation deployment orchestration.
11
- *
12
- * Handles three target types:
13
- * - organisation: deploy org infra + cascade to platform + all accounts
14
- * - platform: deploy platform stack only
15
- * - account: deploy single account stack
16
- *
17
- * Auth and org setup (creating AWS accounts/OUs) are the caller's
18
- * responsibility. deploy-core receives credentials and deploys.
19
- */
20
- export async function deployOrganisation(params, services, operation) {
21
- const startTime = Date.now();
22
- switch (operation.type) {
23
- case ORGANISATION_TYPES.ORGANISATION:
24
- return deployOrgWithCascade(params, services, operation, startTime);
25
- case ORGANISATION_TYPES.PLATFORM:
26
- return deploySingleComponent(params, services, operation, "platform", startTime);
27
- case ORGANISATION_TYPES.ACCOUNT:
28
- return deploySingleComponent(params, services, operation, "account", startTime);
29
- default: {
30
- const _exhaustive = operation.type;
31
- return failure(new Error(`Unsupported organisation type: ${String(_exhaustive)}`));
32
- }
33
- }
34
- }
35
- /**
36
- * Build a deployment context for an organisation component.
37
- */
38
- function buildOrgContext(params, services, operation, deployType, accountName) {
39
- return CdkContextBuilder.buildDeploymentContext({
40
- deployType,
41
- target: operation.target,
42
- path: operation.path,
43
- region: services.awsProvider.getRegion(),
44
- accountName,
45
- callerIdentity: stubCallerIdentity(services.awsProvider.getAccountId()),
46
- ...buildParamsContext({
47
- orgConfig: params.orgConfig,
48
- identity: params.identity,
49
- skipOidc: params.options?.skipOidc
50
- })
51
- }, {
52
- verbose: params.options?.verbose,
53
- infraOnly: params.options?.infraOnly
54
- }, params.orgConfig);
55
- }
56
- const INFRA_STEPS = {
57
- CONNECT: { id: STEP_IDS.CONNECT, name: INFRA_STEP_NAME.CONNECT },
58
- PREPARE: { id: STEP_IDS.PREPARE_ENVIRONMENT, name: INFRA_STEP_NAME.PREPARE },
59
- DEPLOY: { id: STEP_IDS.DEPLOY, name: INFRA_STEP_NAME.DEPLOY },
60
- MONITORING: { id: STEP_IDS.MONITORING, name: INFRA_STEP_NAME.MONITORING },
61
- ORG_DEPLOY: {
62
- id: STEP_IDS.ORG_DEPLOY,
63
- name: "Deploying organisation infrastructure"
64
- }
65
- };
66
- const INFRA_STEP_TOTAL = 4;
67
- /**
68
- * Deploy a single organisation component (platform or account).
69
- *
70
- * Emits 4 named steps from INFRASTRUCTURE_STEP_NAMES: Connect securely →
71
- * Prepare environment → Deploy infrastructure → Enable monitoring.
72
- */
73
- async function deploySingleComponent(params, services, operation, deployType, startTime) {
74
- const { callbacks } = params;
75
- // Step 1: Connect securely — already seeded by the webapp trigger.
76
- // Complete it now: credentials are available by the time deploy-core runs.
77
- callbacks.onStepComplete?.(INFRA_STEPS.CONNECT.id, INFRA_STEPS.CONNECT.name, "completed", 0, INFRA_STEP_TOTAL);
78
- // Step 2: Prepare environment (synth + bootstrap)
79
- callbacks.onStepStart?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, 1, INFRA_STEP_TOTAL);
80
- const context = buildOrgContext(params, services, operation, deployType, deployType === "account" ? operation.target : undefined);
81
- // Synth
82
- callbacks.onLog?.(`Synthesising ${deployType} infrastructure…`, "info");
83
- const synthResult = await synthOrFail(services, context, callbacks, "CDK synthesis failed");
84
- if (!synthResult.success) {
85
- callbacks.onStepComplete?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, "error", 1, INFRA_STEP_TOTAL);
86
- return synthResult;
87
- }
88
- // Bootstrap
89
- const bsResult = await bootstrapOrFail(services, context, callbacks);
90
- if (!bsResult.success) {
91
- callbacks.onStepComplete?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, "error", 1, INFRA_STEP_TOTAL);
92
- return bsResult;
93
- }
94
- callbacks.onStepComplete?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, "completed", 1, INFRA_STEP_TOTAL);
95
- // Step 3: Deploy infrastructure
96
- const stackName = getOrganisationStackName(operation.type);
97
- callbacks.onStepStart?.(INFRA_STEPS.DEPLOY.id, INFRA_STEPS.DEPLOY.name, 2, INFRA_STEP_TOTAL);
98
- const deployResult = await services.cdkService.runCdkDeploy(context, stackName, forwardOutput(callbacks), forwardResourceProgress(callbacks), services.awsProvider);
99
- if (!deployResult.success) {
100
- callbacks.onStepComplete?.(INFRA_STEPS.DEPLOY.id, INFRA_STEPS.DEPLOY.name, "error", 2, INFRA_STEP_TOTAL);
101
- const error = new Error(maskSensitiveOutput(deployResult.error));
102
- callbacks.onError?.(error);
103
- return failure(error);
104
- }
105
- callbacks.onStepComplete?.(INFRA_STEPS.DEPLOY.id, INFRA_STEPS.DEPLOY.name, "completed", 2, INFRA_STEP_TOTAL);
106
- // Capture CloudFormation outputs (OIDC role ARN, etc.)
107
- const outputsResult = await services.cfnService.getStackOutputs(stackName);
108
- if (!outputsResult.success) {
109
- callbacks.onLog?.("Failed to read stack outputs (non-critical)", "debug");
110
- }
111
- const outputs = collectStackOutputs(outputsResult);
112
- // Step 4: Enable monitoring — CloudTrail + alarms are part of the
113
- // Account stack. Signal post-deploy readiness.
114
- callbacks.onStepStart?.(INFRA_STEPS.MONITORING.id, INFRA_STEPS.MONITORING.name, 3, INFRA_STEP_TOTAL);
115
- callbacks.onStepComplete?.(INFRA_STEPS.MONITORING.id, INFRA_STEPS.MONITORING.name, "completed", 3, INFRA_STEP_TOTAL);
116
- return success({
117
- target: operation.target,
118
- deploymentType: "organisation",
119
- outputs,
120
- durationMs: Date.now() - startTime
121
- });
122
- }
123
- /**
124
- * Full organisation deployment with cascade to platform + member accounts.
125
- */
126
- async function deployOrgWithCascade(params, services, operation, startTime) {
127
- const { callbacks, options } = params;
128
- const providerAccounts = params.orgConfig?.providerAccounts ?? [];
129
- const context = buildOrgContext(params, services, operation, "organisation");
130
- // Compute step counts upfront so pre-deploy failures can reference them
131
- const cascadeEnabled = options?.cascade !== false;
132
- const cascadeAccountCount = cascadeEnabled ? providerAccounts.length : 0;
133
- // Steps: prepare (synth+bootstrap) + org-deploy + cascade accounts
134
- const totalSteps = 2 + cascadeAccountCount;
135
- const { id: prepareStepId, name: prepareStepName } = INFRA_STEPS.PREPARE;
136
- // Step 0: Prepare (synth + bootstrap)
137
- callbacks.onStepStart?.(prepareStepId, prepareStepName, 0, totalSteps);
138
- // Synth
139
- callbacks.onLog?.("Synthesising organisation infrastructure…", "info");
140
- const synthResult = await synthOrFail(services, context, callbacks, "CDK synthesis failed");
141
- if (!synthResult.success) {
142
- callbacks.onStepComplete?.(prepareStepId, prepareStepName, "error", 0, totalSteps);
143
- return synthResult;
144
- }
145
- // Bootstrap org account
146
- const bsResult = await bootstrapOrFail(services, context, callbacks);
147
- if (!bsResult.success) {
148
- callbacks.onStepComplete?.(prepareStepId, prepareStepName, "error", 0, totalSteps);
149
- return bsResult;
150
- }
151
- callbacks.onStepComplete?.(prepareStepId, prepareStepName, "completed", 0, totalSteps);
152
- // Step 1: Deploy org infrastructure
153
- const { id: orgStepId, name: orgStepName } = INFRA_STEPS.ORG_DEPLOY;
154
- callbacks.onStepStart?.(orgStepId, orgStepName, 1, totalSteps);
155
- const orgStackName = getOrganisationStackName(ORGANISATION_TYPES.ORGANISATION);
156
- const orgResult = await services.cdkService.runCdkDeploy(context, orgStackName, forwardOutput(callbacks), forwardResourceProgress(callbacks), services.awsProvider);
157
- if (!orgResult.success) {
158
- callbacks.onStepComplete?.(orgStepId, orgStepName, "error", 1, totalSteps);
159
- const error = new Error(maskSensitiveOutput(orgResult.error));
160
- callbacks.onError?.(error);
161
- return failure(error);
162
- }
163
- // Capture org root stack outputs (OIDC role ARN, etc.)
164
- const orgOutputsResult = await services.cfnService.getStackOutputs(orgStackName);
165
- if (!orgOutputsResult.success) {
166
- callbacks.onLog?.("Failed to read org stack outputs (non-critical)", "debug");
167
- }
168
- const orgOutputs = collectStackOutputs(orgOutputsResult);
169
- callbacks.onStepComplete?.(orgStepId, orgStepName, "completed", 1, totalSteps);
170
- // Cascade to platform + domains + member accounts
171
- const cascadeErrors = [];
172
- const allCascadeOutputs = [];
173
- if (cascadeEnabled && providerAccounts.length > 0) {
174
- callbacks.onCascadeStart?.();
175
- let accountsDeployed = 0;
176
- let platformDeployed = false;
177
- let domainsDeployed = false;
178
- // Phase 1: Deploy platform account
179
- const { platformAccount, memberAccounts } = partitionAccounts(providerAccounts);
180
- if (platformAccount) {
181
- callbacks.onCascadePhaseStart?.("platform");
182
- const platformResult = await deployCascadeAccount(params, services, operation, platformAccount, "platform", callbacks);
183
- if (platformResult.success) {
184
- platformDeployed = true;
185
- if (platformResult.data.outputs) {
186
- allCascadeOutputs.push({
187
- accountId: platformAccount.id,
188
- outputs: platformResult.data.outputs
189
- });
190
- }
191
- }
192
- else {
193
- cascadeErrors.push({
194
- accountId: platformAccount.id,
195
- error: platformResult.error.message
196
- });
197
- }
198
- callbacks.onCascadePhaseComplete?.("platform");
199
- }
200
- // Phase 1.5: Read Platform stack outputs for IPAM pool IDs
201
- let ipamPoolIds = new Map();
202
- if (platformDeployed && platformAccount) {
203
- ipamPoolIds = await readPlatformIpamPoolIds(services, platformAccount, callbacks);
204
- }
205
- // Phase 2: Deploy domains (apex sequential, delegated parallel)
206
- if (params.domainProvider) {
207
- const domainResult = await deployDomains(params.domainProvider, callbacks);
208
- domainsDeployed = domainResult.domainsDeployed > 0;
209
- for (const err of domainResult.errors) {
210
- cascadeErrors.push({
211
- accountId: "domains",
212
- error: maskSensitiveOutput(err)
213
- });
214
- }
215
- }
216
- // Phase 3: Deploy member accounts in parallel
217
- if (memberAccounts.length > 0) {
218
- callbacks.onCascadePhaseStart?.("accounts");
219
- const region = services.awsProvider.getRegion();
220
- const memberSettled = await Promise.allSettled(memberAccounts.map((account) => {
221
- const regionSuffix = region.replace(/-/g, "");
222
- const ipamPoolId = ipamPoolIds.get(`${account.id}-${regionSuffix}`);
223
- return deployCascadeAccount(params, services, operation, account, "account", callbacks, ipamPoolId);
224
- }));
225
- memberSettled.forEach((settled, j) => {
226
- const account = memberAccounts[j];
227
- if (!account)
228
- return;
229
- if (settled.status === "rejected") {
230
- cascadeErrors.push({
231
- accountId: account.id,
232
- error: maskSensitiveOutput(settled.reason instanceof Error
233
- ? settled.reason.message
234
- : String(settled.reason))
235
- });
236
- return;
237
- }
238
- const result = settled.value;
239
- if (result.success) {
240
- accountsDeployed++;
241
- if (result.data.outputs) {
242
- allCascadeOutputs.push({
243
- accountId: account.id,
244
- outputs: result.data.outputs
245
- });
246
- }
247
- }
248
- else {
249
- cascadeErrors.push({
250
- accountId: account.id,
251
- error: result.error.message
252
- });
253
- }
254
- });
255
- callbacks.onCascadePhaseComplete?.("accounts");
256
- }
257
- callbacks.onCascadeComplete?.({
258
- platformDeployed,
259
- domainsDeployed,
260
- accountsDeployed,
261
- accountsFailed: cascadeErrors.length,
262
- errors: cascadeErrors
263
- });
264
- if (cascadeErrors.length > 0) {
265
- const errorSummary = cascadeErrors
266
- .map((e) => ` ${e.accountId}: ${e.error}`)
267
- .join("\n");
268
- callbacks.onLog?.(maskSensitiveOutput(`Cascade completed with ${cascadeErrors.length} failure(s):\n${errorSummary}`), "warn");
269
- }
270
- }
271
- const warnings = cascadeErrors.length > 0
272
- ? cascadeErrors.map((e) => maskSensitiveOutput(`${e.accountId}: ${e.error}`))
273
- : undefined;
274
- return success({
275
- target: operation.target,
276
- deploymentType: "organisation",
277
- outputs: orgOutputs,
278
- ...(allCascadeOutputs.length > 0
279
- ? { cascadeOutputs: allCascadeOutputs }
280
- : {}),
281
- durationMs: Date.now() - startTime,
282
- warnings
283
- });
284
- }
1
+ import{success as K,failure as M}from"@fjall/generator";import{ORGANISATION_TYPES as h,getOrganisationStackName as U}from"../types/operations.js";import{CdkContextBuilder as Z}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as tt}from"../types/deployment/index.js";import{buildParamsContext as et,collectStackOutputs as v,synthOrFail as B,bootstrapOrFail as V,forwardOutput as W,forwardResourceProgress as q}from"./contextHelpers.js";import{partitionAccounts as ot,deployCascadeAccount as z,readPlatformIpamPoolIds as nt,deployDomains as rt}from"./cascadeHelpers.js";import{maskSensitiveOutput as E}from"@fjall/util";import{INFRA_STEP_NAME as w,STEP_IDS as I}from"../types/stepDefinitions.js";async function ft(n,e,a){const s=Date.now();switch(a.type){case h.ORGANISATION:return at(n,e,a,s);case h.PLATFORM:return J(n,e,a,"platform",s);case h.ACCOUNT:return J(n,e,a,"account",s);default:{const t=a.type;return M(new Error(`Unsupported organisation type: ${String(t)}`))}}}function H(n,e,a,s,t){return Z.buildDeploymentContext({deployType:s,target:a.target,path:a.path,region:e.awsProvider.getRegion(),accountName:t,callerIdentity:tt(e.awsProvider.getAccountId()),...et({orgConfig:n.orgConfig,identity:n.identity,skipOidc:n.options?.skipOidc})},{verbose:n.options?.verbose,infraOnly:n.options?.infraOnly},n.orgConfig)}const o={CONNECT:{id:I.CONNECT,name:w.CONNECT},PREPARE:{id:I.PREPARE_ENVIRONMENT,name:w.PREPARE},DEPLOY:{id:I.DEPLOY,name:w.DEPLOY},MONITORING:{id:I.MONITORING,name:w.MONITORING},ORG_DEPLOY:{id:I.ORG_DEPLOY,name:"Deploying organisation infrastructure"}},d=4;async function J(n,e,a,s,t){const{callbacks:r}=n;r.onStepComplete?.(o.CONNECT.id,o.CONNECT.name,"completed",0,d),r.onStepStart?.(o.PREPARE.id,o.PREPARE.name,1,d);const m=H(n,e,a,s,s==="account"?a.target:void 0);r.onLog?.(`Synthesising ${s} infrastructure\u2026`,"info");const g=await B(e,m,r,"CDK synthesis failed");if(!g.success)return r.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"error",1,d),g;const R=await V(e,m,r);if(!R.success)return r.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"error",1,d),R;r.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"completed",1,d);const D=U(a.type);r.onStepStart?.(o.DEPLOY.id,o.DEPLOY.name,2,d);const u=await e.cdkService.runCdkDeploy(m,D,W(r),q(r),e.awsProvider);if(!u.success){r.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"error",2,d);const C=new Error(E(u.error));return r.onError?.(C),M(C)}r.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"completed",2,d);const f=await e.cfnService.getStackOutputs(D);f.success||r.onLog?.("Failed to read stack outputs (non-critical)","debug");const O=v(f);return r.onStepStart?.(o.MONITORING.id,o.MONITORING.name,3,d),r.onStepComplete?.(o.MONITORING.id,o.MONITORING.name,"completed",3,d),K({target:a.target,deploymentType:"organisation",outputs:O,durationMs:Date.now()-t})}async function at(n,e,a,s){const{callbacks:t,options:r}=n,m=n.orgConfig?.providerAccounts??[],g=H(n,e,a,"organisation"),R=r?.cascade!==!1,u=2+(R?m.length:0),{id:f,name:O}=o.PREPARE;t.onStepStart?.(f,O,0,u),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const C=await B(e,g,t,"CDK synthesis failed");if(!C.success)return t.onStepComplete?.(f,O,"error",0,u),C;const Y=await V(e,g,t);if(!Y.success)return t.onStepComplete?.(f,O,"error",0,u),Y;t.onStepComplete?.(f,O,"completed",0,u);const{id:T,name:L}=o.ORG_DEPLOY;t.onStepStart?.(T,L,1,u);const _=U(h.ORGANISATION),$=await e.cdkService.runCdkDeploy(g,_,W(t),q(t),e.awsProvider);if(!$.success){t.onStepComplete?.(T,L,"error",1,u);const l=new Error(E($.error));return t.onError?.(l),M(l)}const x=await e.cfnService.getStackOutputs(_);x.success||t.onLog?.("Failed to read org stack outputs (non-critical)","debug");const Q=v(x);t.onStepComplete?.(T,L,"completed",1,u);const c=[],y=[];if(R&&m.length>0){t.onCascadeStart?.();let l=0,k=!1,F=!1;const{platformAccount:P,memberAccounts:b}=ot(m);if(P){t.onCascadePhaseStart?.("platform");const i=await z(n,e,a,P,"platform",t);i.success?(k=!0,i.data.outputs&&y.push({accountId:P.id,outputs:i.data.outputs})):c.push({accountId:P.id,error:i.error.message}),t.onCascadePhaseComplete?.("platform")}let j=new Map;if(k&&P&&(j=await nt(e,P,t)),n.domainProvider){const i=await rt(n.domainProvider,t);F=i.domainsDeployed>0;for(const N of i.errors)c.push({accountId:"domains",error:E(N)})}if(b.length>0){t.onCascadePhaseStart?.("accounts");const i=e.awsProvider.getRegion();(await Promise.allSettled(b.map(p=>{const G=i.replace(/-/g,""),S=j.get(`${p.id}-${G}`);return z(n,e,a,p,"account",t,S)}))).forEach((p,G)=>{const S=b[G];if(!S)return;if(p.status==="rejected"){c.push({accountId:S.id,error:E(p.reason instanceof Error?p.reason.message:String(p.reason))});return}const A=p.value;A.success?(l++,A.data.outputs&&y.push({accountId:S.id,outputs:A.data.outputs})):c.push({accountId:S.id,error:A.error.message})}),t.onCascadePhaseComplete?.("accounts")}if(t.onCascadeComplete?.({platformDeployed:k,domainsDeployed:F,accountsDeployed:l,accountsFailed:c.length,errors:c}),c.length>0){const i=c.map(N=>` ${N.accountId}: ${N.error}`).join(`
2
+ `);t.onLog?.(E(`Cascade completed with ${c.length} failure(s):
3
+ ${i}`),"warn")}}const X=c.length>0?c.map(l=>E(`${l.accountId}: ${l.error}`)):void 0;return K({target:a.target,deploymentType:"organisation",outputs:Q,...y.length>0?{cascadeOutputs:y}:{},durationMs:Date.now()-s,warnings:X})}export{ft as deployOrganisation};