@fjall/deploy-core 0.96.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
@@ -1,8 +1,9 @@
1
1
  import type { Result } from "@fjall/generator";
2
+ import type { DockerBuild } from "@fjall/util/manifest/schemas";
3
+ import type { BuildxProgressEvent } from "@fjall/util/docker";
2
4
  export interface DockerServiceConfig {
3
5
  name: string;
4
- dockerfilePath: string;
5
- dockerTarget?: string;
6
+ docker: DockerBuild;
6
7
  }
7
8
  export interface DockerBuildParams {
8
9
  appName: string;
@@ -12,10 +13,48 @@ export interface DockerBuildParams {
12
13
  imageTag?: string;
13
14
  services?: DockerServiceConfig[];
14
15
  platform?: string;
16
+ /**
17
+ * When provided, the provider constructs registry-mode buildx cache args
18
+ * (`--cache-from type=registry,ref=<cacheRepoUri>:<service>` and
19
+ * `--cache-to type=registry,ref=<cacheRepoUri>:<service>,mode=max`) so
20
+ * incremental builds reuse layers across CI runs and developer machines.
21
+ * The orchestrator is responsible for ensuring the `<app>-cache` repo exists
22
+ * before invoking the provider; missing repos surface as a buildx push error.
23
+ */
24
+ cacheRepoUri?: string;
25
+ /**
26
+ * Per-service identifier used as the cache-tag suffix when `cacheRepoUri`
27
+ * is set. When omitted the cache tag falls back to `appName.toLowerCase()`,
28
+ * which keeps single-service builds working without orchestrator changes.
29
+ */
30
+ serviceName?: string;
31
+ /** Optional Dockerfile target stage (used as part of the build identity). */
32
+ target?: string;
33
+ /** Optional explicit Dockerfile path (defaults to `<appPath>/Dockerfile`). */
34
+ dockerfilePath?: string;
35
+ /** Optional explicit build context path (defaults to `appPath`). */
36
+ buildContext?: string;
37
+ /**
38
+ * Optional explicit ECR tag list. When provided, the provider pushes every
39
+ * tag in one `buildxBuild` invocation (avoiding rebuilds for retag-only
40
+ * cases). When omitted, the provider falls back to a single tag derived
41
+ * from `imageTag` / `appName`.
42
+ */
43
+ imageTags?: readonly string[];
44
+ /** Optional buildArgs forwarded to buildx. */
45
+ buildArgs?: Readonly<Record<string, string>>;
15
46
  }
16
47
  export interface DockerBuildResult {
17
48
  imageUri: string;
18
49
  imageTag: string;
50
+ /**
51
+ * Content-addressed digest of the pushed buildx artifact. Required: every
52
+ * consumer (CLI + worker `DockerProvider`s) MUST surface this so the
53
+ * orchestrator can derive content-hash tags via
54
+ * `deriveContentHashTag(digest, serviceName, target?)` without
55
+ * re-inspecting the registry.
56
+ */
57
+ imageDigest: string;
19
58
  services?: Array<{
20
59
  name: string;
21
60
  imageUri: string;
@@ -45,14 +84,33 @@ export interface TagImagesResult {
45
84
  }
46
85
  /** Callback for Docker build/push progress reporting. */
47
86
  export type DockerProgressCallback = (message: string, percentage?: number, layerId?: string, status?: string) => void;
87
+ /**
88
+ * Parameters for adding manifest-list tags to an already-pushed image
89
+ * digest without re-uploading layers. Used by the orchestrator to apply the
90
+ * content-hash tag derived from a buildx push.
91
+ */
92
+ export interface TagByDigestParams {
93
+ /** Fully-qualified ECR repository URI without a tag suffix. */
94
+ sourceImage: string;
95
+ /** sha256:... digest returned by `buildAndPush`. */
96
+ digest: string;
97
+ /** Fully-qualified tag references to alias onto the digest. */
98
+ tags: readonly string[];
99
+ }
48
100
  /**
49
101
  * Interface for Docker operations (build, push, ECR initialisation).
50
- * CLI provides CliDockerProvider (wraps dockerode-based services).
51
- * Worker passes undefined (Docker ops skipped until container deployments enabled).
102
+ * CLI provides CliDockerProvider, worker provides WorkerDockerProvider —
103
+ * both wrap @fjall/util DockerCli (buildx CLI subprocess).
52
104
  */
53
105
  export interface DockerProvider {
54
- buildAndPush(params: DockerBuildParams, onProgress?: DockerProgressCallback): Promise<Result<DockerBuildResult>>;
106
+ buildAndPush(params: DockerBuildParams, onProgress?: DockerProgressCallback, onBuildxEvent?: (event: BuildxProgressEvent) => void): Promise<Result<DockerBuildResult>>;
55
107
  initialiseECR(params: ECRInitParams): Promise<Result<ECRInitResult>>;
108
+ /**
109
+ * Apply additional manifest-list tags to a pushed digest. The
110
+ * orchestrator uses this to attach the content-hash tag immediately
111
+ * after `buildAndPush` returns, without re-uploading layers.
112
+ */
113
+ tagByDigest(params: TagByDigestParams): Promise<Result<void>>;
56
114
  /** Tag existing ECR images for ECS services (non-Dockerfile apps). Optional. */
57
115
  tagImages?(params: TagImagesParams, onProgress?: DockerProgressCallback): Promise<Result<TagImagesResult>>;
58
116
  }
@@ -5,7 +5,7 @@ export { destroyOrganisation } from "./organisationDestroy.js";
5
5
  export { cleanupFailedStack, isCleanableState, SAFE_CLEANUP_STATES } from "./stackCleanup.js";
6
6
  export type { CascadeDestroyAccountResult } from "./cascadeDestroyHelpers.js";
7
7
  export { partitionAccounts } from "./cascadeHelpers.js";
8
- export type { DockerProvider, DockerProgressCallback, DockerServiceConfig, DockerBuildParams, DockerBuildResult, ECRInitParams, ECRInitResult, TagImagesParams, TagImagesResult } from "./dockerInterface.js";
8
+ export type { DockerProvider, DockerProgressCallback, DockerServiceConfig, DockerBuildParams, DockerBuildResult, ECRInitParams, ECRInitResult, TagImagesParams, TagImagesResult, TagByDigestParams } from "./dockerInterface.js";
9
9
  export type { DomainDeployProvider, DomainConfig, DomainDeployResult } from "./domainInterface.js";
10
10
  export type { DeployServices } from "./serviceFactory.js";
11
11
  export type { DetectionResult } from "./detectionPipeline.js";
@@ -1,3 +1,3 @@
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
+ 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 d,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 p=l(r,".bin","open-next");if(!g(p)){c.debug("openNextBuild","Package found but binary missing",{nodeModulesPath:r,binPath:p});continue}return c.debug("openNextBuild","Resolved OpenNext binary",{binPath:p}),f(p)}}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?.(d("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?.(d(m.trim()))}}),p=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(p)return p;if(o.type!=="success")return i(new Error("Unexpected spawn result"));if(o.code!==0){const m=d(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?.(d("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?.(d(u.error.message)),O(t),i(u.error)}const o=T(t);if(!o.success)return e.onOpenNextBuildError?.(d(o.error.message)),i(o.error);const p=o.data;e.onOpenNextBuildStart?.(),e.onOpenNextProgress?.(d("Starting OpenNext build..."));const m=y(globalThis.process.env),M=l(t,"node_modules"),a=await E({command:p,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?.(d(u.trim()))},onStderrData:u=>{e.onOpenNextProgress?.(d(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=d(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?.(d("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,6 +1,9 @@
1
1
  import { SimpleAwsProvider } from "../aws/SimpleAwsProvider.js";
2
2
  import { CdkService } from "../services/infrastructure/CdkService.js";
3
3
  import { CloudFormationService } from "../services/infrastructure/CloudFormationService.js";
4
+ import { EcsService } from "../services/infrastructure/EcsService.js";
5
+ import { EcsServiceResolver } from "../services/infrastructure/EcsServiceResolver.js";
6
+ import { EcrImageInspectorService } from "../services/infrastructure/EcrImageInspectorService.js";
4
7
  import { ApplicationStackService } from "../services/application/ApplicationStackService.js";
5
8
  import { TemplateHashService } from "../services/supporting/TemplateHashService.js";
6
9
  import { FrameworkRegistry } from "./builders/frameworkRegistry.js";
@@ -9,6 +12,9 @@ export interface DeployServices {
9
12
  awsProvider: SimpleAwsProvider;
10
13
  cdkService: CdkService;
11
14
  cfnService: CloudFormationService;
15
+ ecsService: EcsService;
16
+ ecsResolver: EcsServiceResolver;
17
+ ecrImageInspector: EcrImageInspectorService;
12
18
  stackService: ApplicationStackService;
13
19
  hashService: TemplateHashService;
14
20
  frameworkRegistry: FrameworkRegistry;
@@ -1 +1 @@
1
- import{SimpleAwsProvider as a}from"../aws/SimpleAwsProvider.js";import{CdkService as p}from"../services/infrastructure/CdkService.js";import{CdkArgumentBuilder as d}from"../services/infrastructure/CdkArgumentBuilder.js";import{CdkProcessManager as f}from"../services/infrastructure/CdkProcessManager.js";import{CloudFormationService as v}from"../services/infrastructure/CloudFormationService.js";import{ApplicationStackService as w}from"../services/application/ApplicationStackService.js";import{TemplateHashService as S}from"../services/supporting/TemplateHashService.js";import{FrameworkRegistry as l}from"./builders/frameworkRegistry.js";function x(i){const e=new a(i.awsCredentials);e.exportToEnv();const c=new d,r=new f(c),o=new p({processManager:r}),t=new v(e),n=new w(o,t,e),s=new S,m=l.createDefault();return{awsProvider:e,cdkService:o,cfnService:t,stackService:n,hashService:s,frameworkRegistry:m,dispose(){r.dispose()}}}export{x as createDeployServices};
1
+ import{SimpleAwsProvider as f}from"../aws/SimpleAwsProvider.js";import{CdkService as w}from"../services/infrastructure/CdkService.js";import{CdkArgumentBuilder as S}from"../services/infrastructure/CdkArgumentBuilder.js";import{CdkProcessManager as d}from"../services/infrastructure/CdkProcessManager.js";import{CloudFormationService as l}from"../services/infrastructure/CloudFormationService.js";import{EcsService as g}from"../services/infrastructure/EcsService.js";import{EcsServiceResolver as k}from"../services/infrastructure/EcsServiceResolver.js";import{EcrImageInspectorService as u}from"../services/infrastructure/EcrImageInspectorService.js";import{ApplicationStackService as C}from"../services/application/ApplicationStackService.js";import{TemplateHashService as E}from"../services/supporting/TemplateHashService.js";import{FrameworkRegistry as I}from"./builders/frameworkRegistry.js";function H(t){const e=new f(t.awsCredentials);e.exportToEnv();const i=new S,o=new d(i),c=new w({processManager:o}),r=new l(e),s=new g(e),n=new k(r),m=new u(e),p=new C(c,r,e),a=new E,v=I.createDefault();return{awsProvider:e,cdkService:c,cfnService:r,ecsService:s,ecsResolver:n,ecrImageInspector:m,stackService:p,hashService:a,frameworkRegistry:v,dispose(){o.dispose()}}}export{H as createDeployServices};
@@ -1 +1 @@
1
- import{CloudFormationClient as y,DescribeStacksCommand as w,DeleteStackCommand as C,ListStackResourcesCommand as D}from"@aws-sdk/client-cloudformation";import{S3Client as _,ListObjectVersionsCommand as $,DeleteObjectsCommand as A}from"@aws-sdk/client-s3";import{NodeHttpHandler as f}from"@smithy/node-http-handler";import{logger as o}from"@fjall/util/logger";import{getErrorMessage as S,sleep as h}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as k}from"../types/constants.js";const L=1e3,m=new Set(["ROLLBACK_FAILED","ROLLBACK_COMPLETE","DELETE_FAILED"]);function M(e){return m.has(e)}async function P(e,t,n){let d,s;n?.onStackCleanupProgress?.(t,"emptying-bucket");const c=1e3;let l=0;for(;l++<c;){let r;try{r=await e.send(new $({Bucket:t,KeyMarker:d,VersionIdMarker:s}))}catch(u){if(u instanceof Error&&(u.name==="NoSuchBucket"||u.message?.includes("NoSuchBucket"))){o.debug("stackCleanup",`Bucket ${t} no longer exists, skipping`);return}const p=`Unexpected error emptying bucket ${t}: ${S(u)}`;o.warn("stackCleanup",p),n?.onLog?.(p,"warn");return}const i=[...r.Versions??[],...r.DeleteMarkers??[]];if(i.length===0)break;for(let u=0;u<i.length;u+=L){const p=i.slice(u,u+L);try{await e.send(new A({Bucket:t,Delete:{Objects:p.map(a=>({Key:a.Key,VersionId:a.VersionId})),Quiet:!0}}))}catch(a){const E=`Failed to delete batch from ${t}: ${S(a)}`;o.warn("stackCleanup",E),n?.onLog?.(E,"warn")}}if(!r.IsTruncated)break;d=r.NextKeyMarker,s=r.NextVersionIdMarker}if(l>c){const r=`Bucket ${t} reached ${c} page limit \u2014 some objects may remain`;o.warn("stackCleanup",r),n?.onLog?.(r,"warn")}o.debug("stackCleanup",`Emptied bucket ${t}`)}async function g(e,t,n,d){const s=[];let c,r=0;do{if(r++>=100){o.warn("stackCleanup","Reached 100 page limit listing stack resources",{stackName:t});break}const i=await e.send(new D({StackName:t,NextToken:c}));for(const u of i.StackResourceSummaries??[])if(n(u)){const p=d(u);p&&s.push(p)}c=i.NextToken}while(c);return s}async function R(e,t){return g(e,t,n=>n.ResourceType==="AWS::S3::Bucket"&&n.ResourceStatus==="DELETE_FAILED",n=>n.PhysicalResourceId)}async function V(e,t,n,d,s){const c=d?.timeoutMs??3e5,l=d?.pollMs??5e3;try{const r=new y({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});let i;try{i=(await r.send(new w({StackName:e}))).Stacks?.[0]?.StackStatus}catch(a){if(a instanceof Error&&a.message?.includes(k)){o.debug("stackCleanup",`Stack ${e} does not exist, no cleanup needed`);return}o.warn("stackCleanup",`Failed to check stack status: ${S(a)}`,{stackName:e,region:t});return}if(!i||!M(i)){o.debug("stackCleanup",`Stack ${e} status ${i??"unknown"} is not cleanable, skipping`);return}o.warn("stackCleanup",`Cleaning up ${e} stack in ${i} state`,{region:t}),s?.onStackCleanupProgress?.(e,"deleting-stack");const u=new _({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});try{const a=await R(r,e);for(const E of a)o.warn("stackCleanup",`Emptying bucket ${E}`,{region:t}),await P(u,E,s)}catch(a){const E=`Failed to empty S3 buckets: ${S(a)}`;o.warn("stackCleanup",E,{stackName:e,region:t}),s?.onLog?.(E,"warn")}await r.send(new C({StackName:e})),s?.onStackCleanupProgress?.(e,"waiting");const p=await T(r,e,c,l);if(p==="DELETE_COMPLETE"){o.warn("stackCleanup",`${e} stack deleted successfully`,{region:t}),s?.onStackCleanupProgress?.(e,"complete");return}if(p==="DELETE_FAILED"){o.warn("stackCleanup",`${e} still in DELETE_FAILED, retrying with RetainResources`,{region:t});const a=await F(r,e);if(a.length===0)o.warn("stackCleanup",`${e} in DELETE_FAILED but no non-bucket resources to retain \u2014 cannot retry`,{region:t}),s?.onStackCleanupProgress?.(e,"error");else{await r.send(new C({StackName:e,RetainResources:a}));const E=await T(r,e,c,l);E==="DELETE_COMPLETE"?(o.warn("stackCleanup",`${e} stack deleted on retry (retained: ${a.join(", ")})`,{region:t}),s?.onStackCleanupProgress?.(e,"complete")):(o.warn("stackCleanup",`${e} stack still not deleted after retry: ${E}`,{region:t}),s?.onStackCleanupProgress?.(e,"error"))}}}catch(r){o.warn("stackCleanup",`Stack cleanup failed: ${S(r)}`,{stackName:e,region:t}),s?.onStackCleanupProgress?.(e,"error")}}async function T(e,t,n,d){const s=Date.now();for(;Date.now()-s<n;){await h(d);try{const l=(await e.send(new w({StackName:t}))).Stacks?.[0]?.StackStatus;if(!l||l==="DELETE_COMPLETE")return"DELETE_COMPLETE";if(l==="DELETE_FAILED")return"DELETE_FAILED";o.debug("stackCleanup",`Waiting for ${t}: ${l}`)}catch(c){if(c instanceof Error&&c.message?.includes(k))return"DELETE_COMPLETE";throw o.debug("stackCleanup",`Unexpected error polling ${t}: ${S(c)}`),c}}return"TIMEOUT"}async function F(e,t){return g(e,t,n=>n.ResourceStatus==="DELETE_FAILED"&&n.ResourceType!=="AWS::S3::Bucket",n=>n.LogicalResourceId)}export{m as SAFE_CLEANUP_STATES,V as cleanupFailedStack,M as isCleanableState};
1
+ import{CloudFormationClient as D,DescribeStacksCommand as C,DeleteStackCommand as k,ListStackResourcesCommand as _}from"@aws-sdk/client-cloudformation";import{S3Client as $,ListObjectVersionsCommand as A,DeleteObjectsCommand as h}from"@aws-sdk/client-s3";import{NodeHttpHandler as f}from"@smithy/node-http-handler";import{logger as o}from"@fjall/util/logger";import{getErrorMessage as S,maskSensitiveOutput as w,sleep as m}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as L}from"../types/constants.js";const g=1e3,M=new Set(["ROLLBACK_FAILED","ROLLBACK_COMPLETE","DELETE_FAILED"]);function P(e){return M.has(e)}async function R(e,t,n){let d,s;n?.onStackCleanupProgress?.(t,"emptying-bucket");const u=1e3;let l=0;for(;l++<u;){let r;try{r=await e.send(new A({Bucket:t,KeyMarker:d,VersionIdMarker:s}))}catch(c){if(c instanceof Error&&(c.name==="NoSuchBucket"||c.message?.includes("NoSuchBucket"))){o.debug("stackCleanup",`Bucket ${t} no longer exists, skipping`);return}const p=`Unexpected error emptying bucket ${t}: ${w(S(c))}`;o.warn("stackCleanup",p),n?.onLog?.(p,"warn");return}const i=[...r.Versions??[],...r.DeleteMarkers??[]];if(i.length===0)break;for(let c=0;c<i.length;c+=g){const p=i.slice(c,c+g);try{await e.send(new h({Bucket:t,Delete:{Objects:p.map(a=>({Key:a.Key,VersionId:a.VersionId})),Quiet:!0}}))}catch(a){const E=`Failed to delete batch from ${t}: ${w(S(a))}`;o.warn("stackCleanup",E),n?.onLog?.(E,"warn")}}if(!r.IsTruncated)break;d=r.NextKeyMarker,s=r.NextVersionIdMarker}if(l>u){const r=`Bucket ${t} reached ${u} page limit \u2014 some objects may remain`;o.warn("stackCleanup",r),n?.onLog?.(r,"warn")}o.debug("stackCleanup",`Emptied bucket ${t}`)}async function T(e,t,n,d){const s=[];let u,r=0;do{if(r++>=100){o.warn("stackCleanup","Reached 100 page limit listing stack resources",{stackName:t});break}const i=await e.send(new _({StackName:t,NextToken:u}));for(const c of i.StackResourceSummaries??[])if(n(c)){const p=d(c);p&&s.push(p)}u=i.NextToken}while(u);return s}async function F(e,t){return T(e,t,n=>n.ResourceType==="AWS::S3::Bucket"&&n.ResourceStatus==="DELETE_FAILED",n=>n.PhysicalResourceId)}async function j(e,t,n,d,s){const u=d?.timeoutMs??3e5,l=d?.pollMs??5e3;try{const r=new D({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});let i;try{i=(await r.send(new C({StackName:e}))).Stacks?.[0]?.StackStatus}catch(a){if(a instanceof Error&&a.message?.includes(L)){o.debug("stackCleanup",`Stack ${e} does not exist, no cleanup needed`);return}o.warn("stackCleanup",`Failed to check stack status: ${w(S(a))}`,{stackName:e,region:t});return}if(!i||!P(i)){o.debug("stackCleanup",`Stack ${e} status ${i??"unknown"} is not cleanable, skipping`);return}o.warn("stackCleanup",`Cleaning up ${e} stack in ${i} state`,{region:t}),s?.onStackCleanupProgress?.(e,"deleting-stack");const c=new $({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});try{const a=await F(r,e);for(const E of a)o.warn("stackCleanup",`Emptying bucket ${E}`,{region:t}),await R(c,E,s)}catch(a){const E=`Failed to empty S3 buckets: ${w(S(a))}`;o.warn("stackCleanup",E,{stackName:e,region:t}),s?.onLog?.(E,"warn")}await r.send(new k({StackName:e})),s?.onStackCleanupProgress?.(e,"waiting");const p=await y(r,e,u,l);if(p==="DELETE_COMPLETE"){o.warn("stackCleanup",`${e} stack deleted successfully`,{region:t}),s?.onStackCleanupProgress?.(e,"complete");return}if(p==="DELETE_FAILED"){o.warn("stackCleanup",`${e} still in DELETE_FAILED, retrying with RetainResources`,{region:t});const a=await I(r,e);if(a.length===0)o.warn("stackCleanup",`${e} in DELETE_FAILED but no non-bucket resources to retain \u2014 cannot retry`,{region:t}),s?.onStackCleanupProgress?.(e,"error");else{await r.send(new k({StackName:e,RetainResources:a}));const E=await y(r,e,u,l);E==="DELETE_COMPLETE"?(o.warn("stackCleanup",`${e} stack deleted on retry (retained: ${a.join(", ")})`,{region:t}),s?.onStackCleanupProgress?.(e,"complete")):(o.warn("stackCleanup",`${e} stack still not deleted after retry: ${E}`,{region:t}),s?.onStackCleanupProgress?.(e,"error"))}}}catch(r){o.warn("stackCleanup",`Stack cleanup failed: ${w(S(r))}`,{stackName:e,region:t}),s?.onStackCleanupProgress?.(e,"error")}}async function y(e,t,n,d){const s=Date.now();for(;Date.now()-s<n;){await m(d);try{const l=(await e.send(new C({StackName:t}))).Stacks?.[0]?.StackStatus;if(!l||l==="DELETE_COMPLETE")return"DELETE_COMPLETE";if(l==="DELETE_FAILED")return"DELETE_FAILED";o.debug("stackCleanup",`Waiting for ${t}: ${l}`)}catch(u){if(u instanceof Error&&u.message?.includes(L))return"DELETE_COMPLETE";throw o.debug("stackCleanup",`Unexpected error polling ${t}: ${S(u)}`),u}}return"TIMEOUT"}async function I(e,t){return T(e,t,n=>n.ResourceStatus==="DELETE_FAILED"&&n.ResourceType!=="AWS::S3::Bucket",n=>n.LogicalResourceId)}export{M as SAFE_CLEANUP_STATES,j as cleanupFailedStack,P as isCleanableState};
@@ -0,0 +1,29 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import type { DeployCallbacks } from "../types/callbacks.js";
3
+ export interface StepDescriptor {
4
+ readonly stepId: string;
5
+ readonly stepName: string;
6
+ readonly stepIndex?: number;
7
+ readonly totalSteps?: number;
8
+ }
9
+ export type StepOutcome<T> = {
10
+ kind: "completed";
11
+ data: T;
12
+ } | {
13
+ kind: "skipped";
14
+ data: T;
15
+ } | {
16
+ kind: "error";
17
+ error: Error;
18
+ };
19
+ /**
20
+ * Wraps a single-step async operation in the onStepStart → onStepComplete
21
+ * lifecycle envelope. The wrapper is the only path that emits these events,
22
+ * which structurally prevents the recurrence class where a new orchestrator
23
+ * branch forgets the matching onStepComplete (B-1 in 2026-04-30 deployment-flow review).
24
+ *
25
+ * If `fn` throws, the throw is caught, an "error" complete event is emitted,
26
+ * onError is invoked, and a failure Result is returned — so an exception
27
+ * cannot leave the step rail visually frozen.
28
+ */
29
+ export declare function withStepLifecycle<T>(callbacks: DeployCallbacks, step: StepDescriptor, fn: () => Promise<StepOutcome<T>>): Promise<Result<T>>;
@@ -0,0 +1 @@
1
+ import{success as p,failure as i}from"@fjall/generator";async function m(t,r,d){t.onStepStart?.(r.stepId,r.stepName,r.stepIndex,r.totalSteps);let e;try{e=await d()}catch(o){const n=o instanceof Error?o:new Error(String(o));return t.onStepComplete?.(r.stepId,r.stepName,"error",r.stepIndex,r.totalSteps),t.onError?.(n),i(n)}return t.onStepComplete?.(r.stepId,r.stepName,e.kind,r.stepIndex,r.totalSteps),e.kind==="error"?(t.onError?.(e.error),i(e.error)):p(e.data)}export{m as withStepLifecycle};
@@ -12,6 +12,14 @@ interface StackCallbacks {
12
12
  onOutput?: (chunk: string) => void;
13
13
  onResourceProgress?: (event: ResourceEvent) => void;
14
14
  }
15
+ interface DeployStackOptions {
16
+ /**
17
+ * CloudFormation `--parameters key=value` overrides forwarded verbatim to
18
+ * `cdk deploy`. Used by the orchestrator to pin per-service ECS image
19
+ * tags to the content-hash tag pushed in the preceding Docker phase.
20
+ */
21
+ parameters?: Record<string, string>;
22
+ }
15
23
  interface ParallelStackCallbacks {
16
24
  onOutput?: (chunk: string, stackId: ApplicationStack) => void;
17
25
  onResourceProgress?: (event: ResourceEvent, stackId: ApplicationStack) => void;
@@ -38,7 +46,7 @@ export declare class ApplicationStackService {
38
46
  /**
39
47
  * Deploy a specific stack type for an application
40
48
  */
41
- deployStack(stackType: ApplicationStack, context: DeploymentContext, callbacks?: StackCallbacks): Promise<Result<StackDeploymentData, ApplicationError>>;
49
+ deployStack(stackType: ApplicationStack, context: DeploymentContext, callbacks?: StackCallbacks, options?: DeployStackOptions): Promise<Result<StackDeploymentData, ApplicationError>>;
42
50
  /**
43
51
  * Deploy multiple stacks in parallel within a deployment phase.
44
52
  *
@@ -1 +1 @@
1
- import{STACK_NOT_FOUND_PATTERN as v,CDK_NO_STACKS_MATCH as P}from"../../types/constants.js";import{getApplicationStackName as y}from"../../types/operations.js";import{success as S,failure as k}from"@fjall/generator";import{ApplicationError as h}from"../../types/application/ApplicationServiceTypes.js";import{logger as m}from"@fjall/util/logger";import{convertCloudFormationOutputsToRecord as w}from"../supporting/helpers.js";import{destroyAllStacks as R,mapSettledResults as O,resolveWebsiteUrl as A}from"./applicationStackHelpers.js";class M{cdkService;cloudFormationService;aws;constructor(e,r,t){this.cdkService=e,this.cloudFormationService=r,this.aws=t}async runCdkSynth(e){return this.cdkService.runCdkSynth(e)}async deployStack(e,r,t){const n=r.target,a=y(n,e),c=await this.cdkService.runCdkDeploy(r,a,t?.onOutput,t?.onResourceProgress,this.aws);if(c.success){const o=c.data;m.debug("ApplicationStackService","CDK deploy result",{stackName:a,stackType:e,success:!0,message:o.message,status:o.status,skipped:o.details?.skipped,hasOutput:!!o.details?.output});const s=await this.cloudFormationService.getStackOutputs(a);m.debug("ApplicationStackService","Stack outputs after deploy",{stackName:a,hasOutputs:s.success&&s.data.length>0,outputCount:s.success?s.data.length:0});const i=s.success&&s.data.length>0?w(s.data):void 0;return S({stackType:e,stackName:a,skipped:o.details?.skipped===!0,outputs:i})}else{const o=c.error||`Failed to deploy ${e} infrastructure`;return m.error("ApplicationStackService","Stack deployment failed",{stackType:e,target:r.target,error:o}),k(new h(o,{errorType:"deployment_failed",appName:r.target,operation:"deployStack",stackType:e}))}}async deployStacksInParallel(e,r,t){const n=t?.onOutput,a=t?.onResourceProgress,c=t?.onStackComplete,o=await Promise.allSettled(e.map(async s=>{const i=Date.now(),u=await this.deployStack(s,r,{onOutput:n?p=>n(p,s):void 0,onResourceProgress:a?p=>a(p,s):void 0}),d=Date.now()-i,g={stack:s,success:u.success,error:u.success?void 0:u.error,duration:d,...u.success&&u.data.outputs!==void 0?{outputs:u.data.outputs}:{}};return!u.success&&u.error&&m.debug("deployStacksInParallel",`Stack ${s} failed`,{error:u.error.message}),c?.(s,u.success,d,u.success?void 0:u.error),g}));return S(O(o,e))}async destroyStack(e,r,t,n){const a=r.target,c=y(a,e),o=await this.cdkService.runCdkDestroy(r,c,t?.onOutput,t?.onResourceProgress,this.aws,n);return o.success?S({stackType:e,stackName:c,skipped:!1,outputs:void 0}):k(new h(o.error||`Failed to destroy ${e} stack`,{errorType:"destroy_failed",appName:r.target,operation:"destroyStack",stackType:e}))}async destroyStacksInParallel(e,r,t,n){const a=t?.onOutput,c=t?.onResourceProgress,o=t?.onStackComplete,s=await Promise.allSettled(e.map(async i=>{const u=Date.now(),d=await this.destroyStack(i,r,{onOutput:a?f=>a(f,i):void 0,onResourceProgress:c?f=>c(f,i):void 0},n),g=Date.now()-u,p=d.success?"":d.error?.message??"",C=p.includes(v)||p.includes(P),l={stack:i,success:d.success||C,error:d.success||C?void 0:d.error,duration:g};return!l.success&&l.error&&m.debug("destroyStacksInParallel",`Stack ${i} failed`,{error:l.error.message}),o?.(i,l.success,g,l.success?void 0:l.error),l}));return S(O(s,e))}async getStackOutputs(e,r){const t=y(e,r),n=await this.cloudFormationService.getStackOutputs(t);return n.success?S(w(n.data)):k(new h(`Failed to get outputs for ${t}`,{errorType:"stack_error",appName:e,operation:"getStackOutputs",stackType:r,details:n.error}))}async resolveWebsiteUrl(e){return A(e,this.cloudFormationService)}async destroyAllStacks(e,r,t){return R(this,e,r,t)}}export{M as ApplicationStackService};
1
+ import{STACK_NOT_FOUND_PATTERN as P,CDK_NO_STACKS_MATCH as A}from"../../types/constants.js";import{getApplicationStackName as y}from"../../types/operations.js";import{success as m,failure as k}from"@fjall/generator";import{ApplicationError as h}from"../../types/application/ApplicationServiceTypes.js";import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as v}from"@fjall/util";import{convertCloudFormationOutputsToRecord as w}from"../supporting/helpers.js";import{destroyAllStacks as R,mapSettledResults as O,resolveWebsiteUrl as D}from"./applicationStackHelpers.js";class E{cdkService;cloudFormationService;aws;constructor(e,t,r){this.cdkService=e,this.cloudFormationService=t,this.aws=r}async runCdkSynth(e){return this.cdkService.runCdkSynth(e)}async deployStack(e,t,r,n){const i=t.target,u=y(i,e),c=await this.cdkService.runCdkDeploy(t,u,r?.onOutput,r?.onResourceProgress,this.aws,void 0,n?.parameters);if(c.success){const s=c.data;S.debug("ApplicationStackService","CDK deploy result",{stackName:u,stackType:e,success:!0,message:s.message,status:s.status,skipped:s.details?.skipped,hasOutput:!!s.details?.output});const o=await this.cloudFormationService.getStackOutputs(u);S.debug("ApplicationStackService","Stack outputs after deploy",{stackName:u,hasOutputs:o.success&&o.data.length>0,outputCount:o.success?o.data.length:0});const a=o.success&&o.data.length>0?w(o.data):void 0;return m({stackType:e,stackName:u,skipped:s.details?.skipped===!0,outputs:a})}else{const s=v(c.error||`Failed to deploy ${e} infrastructure`);return S.error("ApplicationStackService","Stack deployment failed",{stackType:e,target:t.target,error:s}),k(new h(s,{errorType:"deployment_failed",appName:t.target,operation:"deployStack",stackType:e}))}}async deployStacksInParallel(e,t,r){const n=r?.onOutput,i=r?.onResourceProgress,u=r?.onStackComplete,c=await Promise.allSettled(e.map(async s=>{const o=Date.now(),a=await this.deployStack(s,t,{onOutput:n?p=>n(p,s):void 0,onResourceProgress:i?p=>i(p,s):void 0}),d=Date.now()-o,g={stack:s,success:a.success,error:a.success?void 0:a.error,duration:d,...a.success&&a.data.outputs!==void 0?{outputs:a.data.outputs}:{}};return!a.success&&a.error&&S.debug("deployStacksInParallel",`Stack ${s} failed`,{error:a.error.message}),u?.(s,a.success,d,a.success?void 0:a.error),g}));return m(O(c,e))}async destroyStack(e,t,r,n){const i=t.target,u=y(i,e),c=await this.cdkService.runCdkDestroy(t,u,r?.onOutput,r?.onResourceProgress,this.aws,n);if(c.success)return m({stackType:e,stackName:u,skipped:!1,outputs:void 0});{const s=v(c.error||`Failed to destroy ${e} stack`);return S.error("ApplicationStackService","Stack destroy failed",{stackType:e,target:t.target,error:s}),k(new h(s,{errorType:"destroy_failed",appName:t.target,operation:"destroyStack",stackType:e}))}}async destroyStacksInParallel(e,t,r,n){const i=r?.onOutput,u=r?.onResourceProgress,c=r?.onStackComplete,s=await Promise.allSettled(e.map(async o=>{const a=Date.now(),d=await this.destroyStack(o,t,{onOutput:i?f=>i(f,o):void 0,onResourceProgress:u?f=>u(f,o):void 0},n),g=Date.now()-a,p=d.success?"":d.error?.message??"",C=p.includes(P)||p.includes(A),l={stack:o,success:d.success||C,error:d.success||C?void 0:d.error,duration:g};return!l.success&&l.error&&S.debug("destroyStacksInParallel",`Stack ${o} failed`,{error:l.error.message}),c?.(o,l.success,g,l.success?void 0:l.error),l}));return m(O(s,e))}async getStackOutputs(e,t){const r=y(e,t),n=await this.cloudFormationService.getStackOutputs(r);return n.success?m(w(n.data)):k(new h(`Failed to get outputs for ${r}`,{errorType:"stack_error",appName:e,operation:"getStackOutputs",stackType:t,details:n.error}))}async resolveWebsiteUrl(e){return D(e,this.cloudFormationService)}async destroyAllStacks(e,t,r){return R(this,e,t,r)}}export{E as ApplicationStackService};
@@ -1,4 +1,4 @@
1
- import{STACK_NOT_FOUND_PATTERN as T,CDK_NO_STACKS_MATCH as k,INFRASTRUCTURE_FILENAME as E}from"../../types/constants.js";import{APPLICATION_STACKS as D,getApplicationStackName as S,getApplicationDestroyOrder as _,getParallelDestroyGroups as $}from"../../types/operations.js";import{FrameworkRegistry as U}from"../../orchestration/builders/frameworkRegistry.js";import{success as P,failure as h,isFailure as N}from"@fjall/generator";import{ApplicationError as O}from"../../types/application/ApplicationServiceTypes.js";import{logger as K}from"@fjall/util/logger";function R(t,r,e){if(t.success)return t;const n=N(t)?t.error.message:"";return n.includes(T)||n.includes(k)?P({stackName:S(e,r),stackType:r,outputs:{},skipped:!0}):t}function A(t,r){if(t.success)return null;const e=N(t)?t.error.message:"";if(e.includes("TSError")||e.includes("Unable to compile TypeScript")||e.includes("Subprocess exited with error")){const n=e.match(/infrastructure\.ts\(\d+,\d+\): error TS\d+: .+/),s=n?n[0]:`Check ${E} for syntax errors`;return{continue:!1,result:h(new O(`CDK synthesis failed: ${s}`,{errorType:"synth_failed",operation:`destroy-${r}`}))}}return e.includes(T)||e.includes(k)?{continue:!0,result:P({message:"Stack already deleted"})}:{continue:!1,result:h(new O(`Failed to destroy ${r} stack: ${e}`,{errorType:"destroy_failed",operation:`destroy-${r}`}))}}async function v(t,r,e,n){const s=r.target,l=U.createDefault().resolve({appPath:r.path});let f,y;if(l){const u=l.builder.plan({appPath:r.path},l.detection);f=u.parallelDestroy,y=u.destroyOrder}else f=!1,y=_({pattern:null,resources:n});if(f){const u=$();for(const C of u){const m=C.stacks;if(m.length===1){const a=m[0],p=S(s,a);e?.onStackStart?.(a,p);const o=await t.destroyStack(a,r,{onOutput:e?.onOutput?d=>e.onOutput?.(d,a):void 0,onResourceProgress:e?.onResourceProgress?d=>e.onResourceProgress?.(d,a):void 0}),i=R(o,a,s);e?.onStackComplete&&await e.onStackComplete(a,i);const c=A(i,a);if(c){if(c.continue)continue;return c.result}}else{const a=await t.runCdkSynth(r);if(!a.success)return h(new O(a.error,{errorType:"synth_failed",appName:s,operation:"destroy-parallel-synth"}));e?.onParallelPhaseStart?.(m,C.description);for(const o of m)e?.onStackStart?.(o,S(s,o));const p=await t.destroyStacksInParallel(m,r,{onOutput:e?.onOutput,onResourceProgress:e?.onResourceProgress,onStackComplete:async(o,i,c,d)=>{const g=i?P({stackName:S(s,o),stackType:o,outputs:{}}):h(d instanceof O?d:new O(d?.message??"Stack destruction failed",{errorType:"destroy_failed",appName:s,operation:`destroy-${o}`}));await e?.onStackComplete?.(o,g)}},!0);if(p.success){e?.onParallelPhaseComplete?.(p.data);const o=p.data.filter(i=>!i.success);if(o.length>0){const i=o.filter(c=>c.error&&!c.error.message.includes(T)&&!c.error.message.includes(k));if(i.length>0){const c=i.map(g=>g.stack).join(", "),d=i.map(g=>`${g.stack}: ${g.error?.message||"Unknown error"}`).join(`
1
+ import{maskSensitiveOutput as P}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as T,CDK_NO_STACKS_MATCH as D,INFRASTRUCTURE_FILENAME as $}from"../../types/constants.js";import{APPLICATION_STACKS as N,getApplicationStackName as S,getApplicationDestroyOrder as U,getParallelDestroyGroups as K}from"../../types/operations.js";import{FrameworkRegistry as L}from"../../orchestration/builders/frameworkRegistry.js";import{success as k,failure as h,isFailure as R}from"@fjall/generator";import{ApplicationError as O}from"../../types/application/ApplicationServiceTypes.js";import{logger as F}from"@fjall/util/logger";function A(t,r,e){if(t.success)return t;const n=R(t)?t.error.message:"";return n.includes(T)||n.includes(D)?k({stackName:S(e,r),stackType:r,outputs:{},skipped:!0}):t}function E(t,r){if(t.success)return null;const e=R(t)?P(t.error.message):"";if(e.includes("TSError")||e.includes("Unable to compile TypeScript")||e.includes("Subprocess exited with error")){const n=e.match(/infrastructure\.ts\(\d+,\d+\): error TS\d+: .+/),s=n?n[0]:`Check ${$} for syntax errors`;return{continue:!1,result:h(new O(`CDK synthesis failed: ${s}`,{errorType:"synth_failed",operation:`destroy-${r}`}))}}return e.includes(T)||e.includes(D)?{continue:!0,result:k({message:"Stack already deleted"})}:{continue:!1,result:h(new O(`Failed to destroy ${r} stack: ${e}`,{errorType:"destroy_failed",operation:`destroy-${r}`}))}}async function G(t,r,e,n){const s=r.target,l=L.createDefault().resolve({appPath:r.path});let f,y;if(l){const u=l.builder.plan({appPath:r.path},l.detection);f=u.parallelDestroy,y=u.destroyOrder}else f=!1,y=U({pattern:null,resources:n});if(f){const u=K();for(const C of u){const m=C.stacks;if(m.length===1){const a=m[0],p=S(s,a);e?.onStackStart?.(a,p);const o=await t.destroyStack(a,r,{onOutput:e?.onOutput?d=>e.onOutput?.(d,a):void 0,onResourceProgress:e?.onResourceProgress?d=>e.onResourceProgress?.(d,a):void 0}),i=A(o,a,s);e?.onStackComplete&&await e.onStackComplete(a,i);const c=E(i,a);if(c){if(c.continue)continue;return c.result}}else{const a=await t.runCdkSynth(r);if(!a.success)return h(new O(a.error,{errorType:"synth_failed",appName:s,operation:"destroy-parallel-synth"}));e?.onParallelPhaseStart?.(m,C.description);for(const o of m)e?.onStackStart?.(o,S(s,o));const p=await t.destroyStacksInParallel(m,r,{onOutput:e?.onOutput,onResourceProgress:e?.onResourceProgress,onStackComplete:async(o,i,c,d)=>{const g=d?.message!==void 0?P(d.message):"Stack destruction failed",_=i?k({stackName:S(s,o),stackType:o,outputs:{}}):h(d instanceof O?d:new O(g,{errorType:"destroy_failed",appName:s,operation:`destroy-${o}`}));await e?.onStackComplete?.(o,_)}},!0);if(p.success){e?.onParallelPhaseComplete?.(p.data);const o=p.data.filter(i=>!i.success);if(o.length>0){const i=o.filter(c=>c.error&&!c.error.message.includes(T)&&!c.error.message.includes(D));if(i.length>0){const c=i.map(g=>g.stack).join(", "),d=i.map(g=>`${g.stack}: ${g.error?.message??"Unknown error"}`).join(`
2
2
  `);return h(new O(`Failed to destroy stacks: ${c}
3
3
 
4
- ${d}`,{errorType:"destroy_failed",appName:s,operation:"destroy-parallel"}))}}}}}}else for(const u of y){const C=S(s,u);e?.onStackStart?.(u,C);const m=await t.destroyStack(u,r,{onOutput:e?.onOutput?o=>e.onOutput?.(o,u):void 0,onResourceProgress:e?.onResourceProgress?o=>e.onResourceProgress?.(o,u):void 0}),a=R(m,u,s);e?.onStackComplete&&await e.onStackComplete(u,a);const p=A(a,u);if(p){if(p.continue)continue;return p.result}}return P({message:"All stacks destroyed"})}function x(t,r){return t.map((e,n)=>e.status==="fulfilled"?e.value:{stack:r[n]??D.NETWORK,success:!1,error:e.reason instanceof Error?e.reason:new Error(String(e.reason)),duration:0})}async function B(t,r){try{const e=S(t,D.COMPUTE),n=await r.getStackOutputs(e);if(n.success&&n.data.length>0){const l=n.data.find(y=>y.OutputKey?.includes("LoadBalancerUrl"));if(l?.OutputValue)return l.OutputValue.toLowerCase();const f=n.data.find(y=>y.OutputKey?.includes("LoadBalancerDnsName"));if(f?.OutputValue)return`http://${f.OutputValue.toLowerCase()}`}const s=S(t,D.CDN),w=await r.getStackOutputs(s);if(w.success&&w.data.length>0){const l=w.data.find(f=>f.OutputKey?.includes("DistributionDomainName"));if(l?.OutputValue)return`https://${l.OutputValue.toLowerCase()}`}return}catch(e){K.warn("ApplicationStackService","URL resolution failed",{error:e instanceof Error?e.message:String(e)});return}}export{R as convertDestroyResultIfNotExists,v as destroyAllStacks,A as handleDestroyError,x as mapSettledResults,B as resolveWebsiteUrl};
4
+ ${d}`,{errorType:"destroy_failed",appName:s,operation:"destroy-parallel"}))}}}}}}else for(const u of y){const C=S(s,u);e?.onStackStart?.(u,C);const m=await t.destroyStack(u,r,{onOutput:e?.onOutput?o=>e.onOutput?.(o,u):void 0,onResourceProgress:e?.onResourceProgress?o=>e.onResourceProgress?.(o,u):void 0}),a=A(m,u,s);e?.onStackComplete&&await e.onStackComplete(u,a);const p=E(a,u);if(p){if(p.continue)continue;return p.result}}return k({message:"All stacks destroyed"})}function W(t,r){return t.map((e,n)=>e.status==="fulfilled"?e.value:{stack:r[n]??N.NETWORK,success:!1,error:e.reason instanceof Error?e.reason:new Error(String(e.reason)),duration:0})}async function H(t,r){try{const e=S(t,N.COMPUTE),n=await r.getStackOutputs(e);if(n.success&&n.data.length>0){const l=n.data.find(y=>y.OutputKey?.includes("LoadBalancerUrl"));if(l?.OutputValue)return l.OutputValue.toLowerCase();const f=n.data.find(y=>y.OutputKey?.includes("LoadBalancerDnsName"));if(f?.OutputValue)return`http://${f.OutputValue.toLowerCase()}`}const s=S(t,N.CDN),w=await r.getStackOutputs(s);if(w.success&&w.data.length>0){const l=w.data.find(f=>f.OutputKey?.includes("DistributionDomainName"));if(l?.OutputValue)return`https://${l.OutputValue.toLowerCase()}`}return}catch(e){F.warn("ApplicationStackService","URL resolution failed",{error:P(e instanceof Error?e.message:String(e))});return}}export{A as convertDestroyResultIfNotExists,G as destroyAllStacks,E as handleDestroyError,W as mapSettledResults,H as resolveWebsiteUrl};
@@ -1,5 +1,5 @@
1
- export { CdkService, CdkArgumentBuilder, CdkProcessManager, CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS, isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext, hasCdkDifferences, parseDiffOutput, CloudFormationService, CloudFormationError } from "./infrastructure/index.js";
2
- export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions, ICdkProcessManager, DiffDetails, CloudFormationCallbacks } from "./infrastructure/index.js";
1
+ export { CdkService, CdkArgumentBuilder, CdkProcessManager, CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS, isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext, hasCdkDifferences, parseDiffOutput, CloudFormationService, CloudFormationError, EcsService, EcsError, EcsServiceResolver, type CfnExportsClient } from "./infrastructure/index.js";
2
+ export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions, ICdkProcessManager, DiffDetails, CloudFormationCallbacks, ECSServiceInfo, ECSClusterInfo, DeploymentStatus, ECSDeploymentOptions } from "./infrastructure/index.js";
3
3
  export { TemplateHashService, TemplateHashError, type TemplateComparisonResult } from "./supporting/index.js";
4
4
  export { CdkContextBuilder } from "./supporting/index.js";
5
5
  export { emitProgress, PROGRESS_MESSAGES, parseBuildPhase, buildStepContextBuildConfig, convertCloudFormationOutputsToRecord, type CloudFormationOutput } from "./supporting/index.js";
@@ -1 +1 @@
1
- import{CdkService as t,CdkArgumentBuilder as o,CdkProcessManager as i,CdkEventMonitor as a,startStackMonitoring as n,DEFAULT_DEPLOY_TIMEOUT_MS as u,isCdkError as d,formatInfrastructureError as s,getStructuralHint as S,getSourceContext as c,hasCdkDifferences as p,parseDiffOutput as C,CloudFormationService as m,CloudFormationError as f}from"./infrastructure/index.js";import{TemplateHashService as E,TemplateHashError as k}from"./supporting/index.js";import{CdkContextBuilder as g}from"./supporting/index.js";import{emitProgress as M,PROGRESS_MESSAGES as T,parseBuildPhase as O,buildStepContextBuildConfig as P,convertCloudFormationOutputsToRecord as h}from"./supporting/index.js";import{ApplicationStackService as B}from"./application/index.js";export{B as ApplicationStackService,o as CdkArgumentBuilder,g as CdkContextBuilder,a as CdkEventMonitor,i as CdkProcessManager,t as CdkService,f as CloudFormationError,m as CloudFormationService,u as DEFAULT_DEPLOY_TIMEOUT_MS,T as PROGRESS_MESSAGES,k as TemplateHashError,E as TemplateHashService,P as buildStepContextBuildConfig,h as convertCloudFormationOutputsToRecord,M as emitProgress,s as formatInfrastructureError,c as getSourceContext,S as getStructuralHint,p as hasCdkDifferences,d as isCdkError,O as parseBuildPhase,C as parseDiffOutput,n as startStackMonitoring};
1
+ import{CdkService as o,CdkArgumentBuilder as t,CdkProcessManager as i,CdkEventMonitor as a,startStackMonitoring as c,DEFAULT_DEPLOY_TIMEOUT_MS as s,isCdkError as n,formatInfrastructureError as u,getStructuralHint as S,getSourceContext as d,hasCdkDifferences as E,parseDiffOutput as p,CloudFormationService as C,CloudFormationError as l,EcsService as m,EcsError as f,EcsServiceResolver as k}from"./infrastructure/index.js";import{TemplateHashService as x,TemplateHashError as g}from"./supporting/index.js";import{CdkContextBuilder as T}from"./supporting/index.js";import{emitProgress as P,PROGRESS_MESSAGES as h,parseBuildPhase as A,buildStepContextBuildConfig as B,convertCloudFormationOutputsToRecord as D}from"./supporting/index.js";import{ApplicationStackService as R}from"./application/index.js";export{R as ApplicationStackService,t as CdkArgumentBuilder,T as CdkContextBuilder,a as CdkEventMonitor,i as CdkProcessManager,o as CdkService,l as CloudFormationError,C as CloudFormationService,s as DEFAULT_DEPLOY_TIMEOUT_MS,f as EcsError,m as EcsService,k as EcsServiceResolver,h as PROGRESS_MESSAGES,g as TemplateHashError,x as TemplateHashService,B as buildStepContextBuildConfig,D as convertCloudFormationOutputsToRecord,P as emitProgress,u as formatInfrastructureError,d as getSourceContext,S as getStructuralHint,E as hasCdkDifferences,n as isCdkError,A as parseBuildPhase,p as parseDiffOutput,c as startStackMonitoring};
@@ -1,6 +1,16 @@
1
1
  import type { CdkContext, CdkOptions } from "./CdkService.js";
2
2
  export declare class CdkArgumentBuilder {
3
3
  buildContextArgs(context?: CdkContext): string[];
4
+ /**
5
+ * Emit `--parameters key=value` pairs for every entry in the supplied
6
+ * record. Returns an empty array when the record is undefined or empty so
7
+ * the caller can unconditionally spread the result into argv.
8
+ *
9
+ * Keys must match CloudFormation's parameter-name shape
10
+ * (`^[A-Za-z][A-Za-z0-9]*$`). Values must be non-empty and free of `,`
11
+ * (cdk's parameter-list separator) and newlines (which would break argv).
12
+ */
13
+ buildParameterArgs(parameters?: Record<string, string>): string[];
4
14
  injectCascadeCredentials(env: NodeJS.ProcessEnv, credentials?: CdkOptions["credentials"]): void;
5
15
  buildCdkEnv(options?: {
6
16
  context?: {
@@ -1 +1 @@
1
- import{filterDangerousEnvVars as a}from"@fjall/util";class c{buildContextArgs(i){const r=[];return i?.accountId&&r.push("-c",`accountId=${i.accountId}`),i?.environment&&r.push("-c",`environment=${i.environment}`),i?.managedAccount&&r.push("-c","managedAccount=true"),i?.accountName&&r.push("-c",`accountName=${i.accountName}`),i?.imageVersion&&r.push("-c",`imageVersion=${i.imageVersion}`),i?.orgId&&r.push("-c",`orgId=${i.orgId}`),i?.rootId&&r.push("-c",`rootId=${i.rootId}`),i?.managementAccountId&&r.push("-c",`managementAccountId=${i.managementAccountId}`),i?.ipamPoolId&&r.push("-c",`ipamPoolId=${i.ipamPoolId}`),i?.fjallOrgId&&r.push("-c",`fjallOrgId=${i.fjallOrgId}`),i?.fjallOidcConfigured&&r.push("-c",`fjallOidcConfigured=${i.fjallOidcConfigured}`),i?.orgConfig&&r.push("-c",`orgConfig=${i.orgConfig}`),r}injectCascadeCredentials(i,r){r&&(i.AWS_ACCESS_KEY_ID=r.accessKeyId,i.AWS_SECRET_ACCESS_KEY=r.secretAccessKey,r.sessionToken&&(i.AWS_SESSION_TOKEN=r.sessionToken))}buildCdkEnv(i){const r={...a(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1"};return i?.context?.region&&(r.AWS_REGION=i.context.region,r.AWS_DEFAULT_REGION=i.context.region,r.CDK_DEFAULT_REGION=i.context.region),i?.context?.accountId&&(r.CDK_DEFAULT_ACCOUNT=i.context.accountId),this.injectCascadeCredentials(r,i?.credentials),r}}export{c as CdkArgumentBuilder};
1
+ import{filterDangerousEnvVars as i}from"@fjall/util";class d{buildContextArgs(r){const e=[];return r?.accountId&&e.push("-c",`accountId=${r.accountId}`),r?.environment&&e.push("-c",`environment=${r.environment}`),r?.managedAccount&&e.push("-c","managedAccount=true"),r?.accountName&&e.push("-c",`accountName=${r.accountName}`),r?.orgId&&e.push("-c",`orgId=${r.orgId}`),r?.rootId&&e.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&e.push("-c",`managementAccountId=${r.managementAccountId}`),r?.ipamPoolId&&e.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&e.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&e.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.orgConfig&&e.push("-c",`orgConfig=${r.orgConfig}`),e}buildParameterArgs(r){if(r===void 0)return[];const e=Object.entries(r);if(e.length===0)return[];const o=[];for(const[a,n]of e){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(a))throw new Error(`Invalid CloudFormation parameter name "${a}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(n==="")throw new Error(`CloudFormation parameter "${a}" has an empty value.`);if(/[,\n\r]/.test(n))throw new Error(`CloudFormation parameter "${a}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);o.push("--parameters",`${a}=${n}`)}return o}injectCascadeCredentials(r,e){e&&(r.AWS_ACCESS_KEY_ID=e.accessKeyId,r.AWS_SECRET_ACCESS_KEY=e.secretAccessKey,delete r.AWS_SESSION_TOKEN,e.sessionToken&&(r.AWS_SESSION_TOKEN=e.sessionToken))}buildCdkEnv(r){const e={...i(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1"};return r?.context?.region&&(e.AWS_REGION=r.context.region,e.AWS_DEFAULT_REGION=r.context.region,e.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(e.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(e,r?.credentials),e}}export{d as CdkArgumentBuilder};
@@ -20,6 +20,7 @@ export interface CheckDifferencesResult {
20
20
  */
21
21
  export declare class CdkCommandRunner {
22
22
  private processManager;
23
+ private readonly argumentBuilder;
23
24
  constructor(processManager: ICdkProcessManager);
24
25
  dispose(): void;
25
26
  checkDifferences(path: string, stackName?: string, options?: CdkOptions): Promise<Result<CheckDifferencesResult, CdkError>>;
@@ -1,2 +1,2 @@
1
- import{tmpdir as C}from"os";import{logger as d}from"@fjall/util/logger";import{success as p,failure as n}from"@fjall/generator";import{CdkError as m}from"../../types/errors/CdkError.js";import{DEFAULT_REGION as l}from"../../aws/utils/regions.js";import{isCdkError as E,formatInfrastructureError as c}from"./CdkErrorFormatter.js";import{hasCdkDifferences as O,parseDiffOutput as P}from"./CdkOutputParser.js";import{DEFAULT_DEPLOY_TIMEOUT_MS as f}from"./CdkEventMonitoring.js";import{analyseBootstrapError as A}from"./CdkOutputAnalyser.js";const _=3e5,R=18e4,g=12e5,u=Object.freeze({APP:"--app",CI:"--ci",REQUIRE_APPROVAL:"--require-approval",APPROVAL_NEVER:"never",VERBOSE:"--verbose",NO_LOOKUPS:"--no-lookups",ALL:"--all"});class V{processManager;constructor(o){this.processManager=o}dispose(){this.processManager.dispose()}async checkDifferences(o,s,r){const e=["diff"];s?e.push(s):e.push(u.ALL),e.push("--no-color"),r?.noLookups&&e.push(u.NO_LOOKUPS);const t=await this.processManager.runCdkCommand(o,e,{...r,ignoreExitCode:!0,combineOutput:!0});if(!t.success)return n(new m(c(t.error,o),"diff_failed",void 0,void 0,t.error,void 0,!1));const a=t.data.output||"";if(E(a))return n(new m(c(a,o),"diff_failed",void 0,void 0,a,void 0,!1));const i=O(a),h=P(a);return p({hasDifferences:i,output:a,details:h})}async deploy(o,s,r){const e=["deploy"];r?.appDir?e.push(u.APP,r.appDir):r?.useCdkOut&&e.push(u.APP,"cdk.out"),s?e.push(s):e.push(u.ALL),e.push(u.REQUIRE_APPROVAL,u.APPROVAL_NEVER),e.push(u.CI),e.push("--no-version-reporting"),e.push("--no-path-metadata"),e.push("--no-asset-metadata"),r?.verbose?e.push(u.VERBOSE):e.push("--progress","events");const t={...r,timeout:r?.timeout||f};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(o,e,t):this.processManager.runCdkCommand(o,e,t)}async destroy(o,s,r){const e=["destroy"];r?.appDir?e.push(u.APP,r.appDir):r?.useCdkOut&&e.push(u.APP,"cdk.out"),s?e.push(s):e.push(u.ALL),e.push("--force"),r?.verbose&&e.push(u.VERBOSE);const t={...r,timeout:r?.timeout||f};return this.processManager.runCdkCommand(o,e,t)}async runImport(o,s,r){const e=["import"];r?.stacks&&r.stacks.length>0&&e.push(...r.stacks),s&&e.push("--resource-mapping",s),e.push(u.REQUIRE_APPROVAL,u.APPROVAL_NEVER),e.push(u.CI),r?.noLookups&&e.push(u.NO_LOOKUPS),r?.verbose&&(e.push(u.VERBOSE),e.push("--trace"),e.push("--debug"));const t={...r,timeout:r?.timeout||g};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(o,e,t):this.processManager.runCdkCommand(o,e,t)}async synth(o,s){s?.outputCallback?.(`Synthesising CloudFormation template...
2
- `);const r=s?.noLookups?[u.NO_LOOKUPS]:[],e=s?.outputDir?["--output",s.outputDir]:[],t=s?.context?.region||l,a=await this.processManager.runCdkCommand(o,["synth",...r,...e,u.CI,"--quiet"],{...s,context:{...s?.context,region:t},timeout:s?.timeout||_});if(!a.success){const i=a.error?c(a.error,o):"Failed to synthesise CloudFormation template";return n(i)}return a}async bootstrap(o,s,r){const e=C();d.debug("CdkService","Starting CDK bootstrap",{accountId:o,region:s,bootstrapPath:e,target:`aws://${o}/${s}`});const t=await this.processManager.runCdkCommand(e,["bootstrap",`aws://${o}/${s}`,"--cloudformation-execution-policies","arn:aws:iam::aws:policy/AdministratorAccess","-c",`accountId=${o}`,u.REQUIRE_APPROVAL,u.APPROVAL_NEVER,u.CI,"--quiet","--force"],{...r,timeout:r?.timeout||R,context:{region:s,accountId:o},credentials:r?.credentials,skipProjectCheck:!0,extraEnv:{TERM:"dumb",CDK_DISABLE_NOTICES:"true",CDK_DISABLE_PROGRESS_BAR:"true"}});return d.debug("CdkService",t.success?"Bootstrap completed successfully":"Bootstrap exited with non-zero code",{accountId:o,region:s,exitCode:t.success?t.data.exitCode:void 0,output:t.success&&t.data.output?.trim()||"(empty)",error:t.success?"(empty)":t.error.trim()||"(empty)"}),t.success?p({output:"AWS environment bootstrapped",exitCode:0}):t.error.includes("already bootstrapped")?p({output:"Environment is already bootstrapped",exitCode:0}):n(A(t.error))}}export{R as BOOTSTRAP_TIMEOUT_MS,V as CdkCommandRunner,g as IMPORT_TIMEOUT_MS,_ as SYNTH_TIMEOUT_MS};
1
+ import{tmpdir as l}from"os";import{logger as d}from"@fjall/util/logger";import{success as p,failure as n}from"@fjall/generator";import{CdkError as m}from"../../types/errors/CdkError.js";import{DEFAULT_REGION as C}from"../../aws/utils/regions.js";import{CdkArgumentBuilder as E}from"./CdkArgumentBuilder.js";import{isCdkError as O,formatInfrastructureError as c}from"./CdkErrorFormatter.js";import{hasCdkDifferences as A,parseDiffOutput as P}from"./CdkOutputParser.js";import{DEFAULT_DEPLOY_TIMEOUT_MS as f}from"./CdkEventMonitoring.js";import{analyseBootstrapError as g}from"./CdkOutputAnalyser.js";const _=3e5,R=18e4,L=12e5,o=Object.freeze({APP:"--app",CI:"--ci",REQUIRE_APPROVAL:"--require-approval",APPROVAL_NEVER:"never",VERBOSE:"--verbose",NO_LOOKUPS:"--no-lookups",ALL:"--all"});class U{processManager;argumentBuilder=new E;constructor(u){this.processManager=u}dispose(){this.processManager.dispose()}async checkDifferences(u,t,r){const e=["diff"];t?e.push(t):e.push(o.ALL),e.push("--no-color"),r?.noLookups&&e.push(o.NO_LOOKUPS);const s=await this.processManager.runCdkCommand(u,e,{...r,ignoreExitCode:!0,combineOutput:!0});if(!s.success)return n(new m(c(s.error,u),"diff_failed",void 0,void 0,s.error,void 0,!1));const a=s.data.output||"";if(O(a))return n(new m(c(a,u),"diff_failed",void 0,void 0,a,void 0,!1));const i=A(a),h=P(a);return p({hasDifferences:i,output:a,details:h})}async deploy(u,t,r){const e=["deploy"];r?.appDir?e.push(o.APP,r.appDir):r?.useCdkOut&&e.push(o.APP,"cdk.out"),t?e.push(t):e.push(o.ALL),e.push(o.REQUIRE_APPROVAL,o.APPROVAL_NEVER),e.push(o.CI),e.push("--no-version-reporting"),e.push("--no-path-metadata"),e.push("--no-asset-metadata"),r?.verbose?e.push(o.VERBOSE):e.push("--progress","events"),e.push(...this.argumentBuilder.buildParameterArgs(r?.parameters));const s={...r,timeout:r?.timeout||f};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(u,e,s):this.processManager.runCdkCommand(u,e,s)}async destroy(u,t,r){const e=["destroy"];r?.appDir?e.push(o.APP,r.appDir):r?.useCdkOut&&e.push(o.APP,"cdk.out"),t?e.push(t):e.push(o.ALL),e.push("--force"),r?.verbose&&e.push(o.VERBOSE);const s={...r,timeout:r?.timeout||f};return this.processManager.runCdkCommand(u,e,s)}async runImport(u,t,r){const e=["import"];r?.stacks&&r.stacks.length>0&&e.push(...r.stacks),t&&e.push("--resource-mapping",t),e.push(o.REQUIRE_APPROVAL,o.APPROVAL_NEVER),e.push(o.CI),r?.noLookups&&e.push(o.NO_LOOKUPS),r?.verbose&&(e.push(o.VERBOSE),e.push("--trace"),e.push("--debug"));const s={...r,timeout:r?.timeout||L};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(u,e,s):this.processManager.runCdkCommand(u,e,s)}async synth(u,t){t?.outputCallback?.(`Synthesising CloudFormation template...
2
+ `);const r=t?.noLookups?[o.NO_LOOKUPS]:[],e=t?.outputDir?["--output",t.outputDir]:[],s=t?.context?.region||C,a=await this.processManager.runCdkCommand(u,["synth",...r,...e,o.CI,"--quiet"],{...t,context:{...t?.context,region:s},timeout:t?.timeout||_});if(!a.success){const i=a.error?c(a.error,u):"Failed to synthesise CloudFormation template";return n(i)}return a}async bootstrap(u,t,r){const e=l();d.debug("CdkService","Starting CDK bootstrap",{accountId:u,region:t,bootstrapPath:e,target:`aws://${u}/${t}`});const s=await this.processManager.runCdkCommand(e,["bootstrap",`aws://${u}/${t}`,"--cloudformation-execution-policies","arn:aws:iam::aws:policy/AdministratorAccess","-c",`accountId=${u}`,o.REQUIRE_APPROVAL,o.APPROVAL_NEVER,o.CI,"--quiet","--force"],{...r,timeout:r?.timeout||R,context:{region:t,accountId:u},credentials:r?.credentials,skipProjectCheck:!0,extraEnv:{TERM:"dumb",CDK_DISABLE_NOTICES:"true",CDK_DISABLE_PROGRESS_BAR:"true"}});return d.debug("CdkService",s.success?"Bootstrap completed successfully":"Bootstrap exited with non-zero code",{accountId:u,region:t,exitCode:s.success?s.data.exitCode:void 0,output:s.success&&s.data.output?.trim()||"(empty)",error:s.success?"(empty)":s.error.trim()||"(empty)"}),s.success?p({output:"AWS environment bootstrapped",exitCode:0}):s.error.includes("already bootstrapped")?p({output:"Environment is already bootstrapped",exitCode:0}):n(g(s.error))}}export{R as BOOTSTRAP_TIMEOUT_MS,U as CdkCommandRunner,L as IMPORT_TIMEOUT_MS,_ as SYNTH_TIMEOUT_MS};
@@ -1 +1 @@
1
- import{logger as m}from"@fjall/util/logger";import{STACK_NOT_FOUND_PATTERN as p,CDK_NO_STACKS_MATCH as i}from"../../aws/utils/cloudformationEvents.js";import{success as n,failure as u}from"@fjall/generator";import{formatInfrastructureError as f}from"./CdkErrorFormatter.js";import{startStackMonitoring as k}from"./CdkEventMonitoring.js";function E(e,t,s){const r=new RegExp(`${s}[:\\s|]*.*?(?:stack has no resources|skipping deployment)`,"i").test(e),c=e.includes("no changes")||e.includes("Stack is up to date")||e.includes("No changes to deploy"),l=e.includes(i)||!t.success&&t.error.includes(i);if(t.success)return n({message:r?"Stack has no resources, skipped":c?"Infrastructure already up to date":"Deployment successful",status:c?"NO_CHANGES":void 0,details:{output:e,skipped:r}});if(l)return n({message:"Stack not defined, skipped",status:"SKIPPED",details:{output:e,skipped:!0}});let o=t.error||"Deployment failed";if(e.includes("failed:")){const d=e.match(/failed: .*?: (.*?)(?:\n|$)/);d&&(o=d[1])}return m.error("CdkService","CDK deployment returned failure",{errorMessage:o}),u(o)}function C(e){return e.includes("Access Denied")||e.includes("AccessDenied")?"Access denied. Check your AWS credentials and permissions":e.includes("ExpiredToken")?"AWS credentials have expired. Please refresh your credentials":e.includes("ENOTFOUND")||e.includes("ECONNREFUSED")?"Network error. Please check your internet connection and try again":e.includes("timeout")?"Bootstrap operation timed out":f(e)}function x(e){return e.success?n({message:"Stack destroyed successfully"}):e.error.includes(p)||e.error.includes(i)?n({message:"Stack already deleted or does not exist",details:{skipped:!0}}):u(e.error||"Destroy failed")}function N(e){const t=e.match(/Deploying stack\s+([^\s]+)/);if(t)return{detected:!0,stackName:t[1]};const s=e.match(/^([^\s|]+)\s*\|\s*\d+\/\d+/m);if(s)return{detected:!0,stackName:s[1]};const a=e.match(/arn:aws:cloudformation:[^:]+:[^:]+:stack\/([^/]+)\//);return a?{detected:!0,stackName:a[1]}:{detected:!1}}function A(e,t,s,a){return r=>{if(e.cdkOutput+=r,t?.(r),!e.stackDetected){const c=N(r);c.detected&&c.stackName&&(e.actualStackName=c.stackName,e.stackDetected=!0),e.stackDetected&&s&&!e.monitoringPromise&&(e.monitoringPromise=k(s,e.actualStackName,a))}}}export{C as analyseBootstrapError,E as analyseDeployOutput,x as analyseDestroyResult,A as createEnhancedOutputCallback,N as detectStackNameFromChunk};
1
+ import{logger as p}from"@fjall/util/logger";import{maskSensitiveOutput as u}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as f,CDK_NO_STACKS_MATCH as o}from"../../aws/utils/cloudformationEvents.js";import{success as i,failure as l}from"@fjall/generator";import{formatInfrastructureError as k}from"./CdkErrorFormatter.js";import{startStackMonitoring as N}from"./CdkEventMonitoring.js";function x(e,t,s){const r=new RegExp(`${s}[:\\s|]*.*?(?:stack has no resources|skipping deployment)`,"i").test(e),c=e.includes("no changes")||e.includes("Stack is up to date")||e.includes("No changes to deploy"),m=e.includes(o)||!t.success&&t.error.includes(o);if(t.success)return i({message:r?"Stack has no resources, skipped":c?"Infrastructure already up to date":"Deployment successful",status:c?"NO_CHANGES":void 0,details:{output:e,skipped:r}});if(m)return i({message:"Stack not defined, skipped",status:"SKIPPED",details:{output:e,skipped:!0}});let n=u(t.error||"Deployment failed");if(e.includes("failed:")){const d=e.match(/failed: .*?: (.*?)(?:\n|$)/);d&&(n=u(d[1]))}return p.error("CdkService","CDK deployment returned failure",{errorMessage:n}),l(n)}function A(e){return e.includes("Access Denied")||e.includes("AccessDenied")?"Access denied. Check your AWS credentials and permissions":e.includes("ExpiredToken")?"AWS credentials have expired. Please refresh your credentials":e.includes("ENOTFOUND")||e.includes("ECONNREFUSED")?"Network error. Please check your internet connection and try again":e.includes("timeout")?"Bootstrap operation timed out":k(e)}function P(e){return e.success?i({message:"Stack destroyed successfully"}):e.error.includes(f)||e.error.includes(o)?i({message:"Stack already deleted or does not exist",details:{skipped:!0}}):l(e.error||"Destroy failed")}function S(e){const t=e.match(/Deploying stack\s+([^\s]+)/);if(t)return{detected:!0,stackName:t[1]};const s=e.match(/^([^\s|]+)\s*\|\s*\d+\/\d+/m);if(s)return{detected:!0,stackName:s[1]};const a=e.match(/arn:aws:cloudformation:[^:]+:[^:]+:stack\/([^/]+)\//);return a?{detected:!0,stackName:a[1]}:{detected:!1}}function T(e,t,s,a){return r=>{if(e.cdkOutput+=r,t?.(r),!e.stackDetected){const c=S(r);c.detected&&c.stackName&&(e.actualStackName=c.stackName,e.stackDetected=!0),e.stackDetected&&s&&!e.monitoringPromise&&(e.monitoringPromise=N(s,e.actualStackName,a))}}}export{A as analyseBootstrapError,x as analyseDeployOutput,P as analyseDestroyResult,T as createEnhancedOutputCallback,S as detectStackNameFromChunk};
@@ -1,3 +1,3 @@
1
- import{spawn as K}from"child_process";import{existsSync as E}from"fs";import{join as T}from"path";import{createRequire as A}from"module";import{logger as $}from"@fjall/util/logger";import{filterDangerousEnvVars as M,maskSensitiveOutput as g}from"@fjall/util";import{success as H,failure as i}from"@fjall/generator";import{getErrorMessage as w}from"@fjall/util";function L(){try{const e=A(import.meta.url).resolve("aws-cdk/bin/cdk");return{command:process.execPath,prefixArgs:[e]}}catch(h){return $.debug("CdkService","Failed to resolve aws-cdk binary, falling back to npx",{error:h instanceof Error?h.message:String(h)}),{command:"npx",prefixArgs:["cdk"]}}}const x=L(),j=5e3;class v{runningProcesses=new Map;processCounter=0;argBuilder;exitHandler;sigintHandler;sigtermHandler;constructor(e){this.argBuilder=e,this.exitHandler=()=>this.cleanup(),this.sigintHandler=()=>{this.cleanup(),process.exit(130)},this.sigtermHandler=()=>{this.cleanup(),process.exit(143)},process.on("exit",this.exitHandler),process.on("SIGINT",this.sigintHandler),process.on("SIGTERM",this.sigtermHandler)}forceKillProcess(e){e.stdout?.destroy(),e.stderr?.destroy(),e.kill("SIGTERM");const c=setTimeout(()=>{e.exitCode===null&&e.kill("SIGKILL")},j);c.unref(),e.once("exit",()=>clearTimeout(c))}cleanup(){for(const[e,c]of this.runningProcesses)c.killed||(c.stdout?.destroy(),c.stderr?.destroy(),c.kill("SIGTERM"));this.runningProcesses.clear()}dispose(){this.cleanup(),process.removeListener("exit",this.exitHandler),process.removeListener("SIGINT",this.sigintHandler),process.removeListener("SIGTERM",this.sigtermHandler)}async runCdkCommandPassthrough(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}const a=T(e,"cdk.json");if(!E(a)){t(i(`No CDK project found in ${e}`));return}const m=this.argBuilder.buildContextArgs(r?.context),f=[...x.prefixArgs,...c,...m],S=this.argBuilder.buildCdkEnv(r),d=K(x.command,f,{cwd:e,env:S,stdio:"inherit",shell:!1,detached:!1});if(!d.pid){d.on("error",u=>{$.debug("CdkProcess","Spawn error on failed child process",{error:u.message})}),t(i(`Failed to spawn CDK process - no PID. cwd=${e}, args=${f.join(" ")}`));return}const C=`cdk-passthrough-${++this.processCounter}`;this.runningProcesses.set(C,d);let o=!1,l=!1;const p=r?.timeout??30*60*1e3,k=setTimeout(()=>{d.killed||(o=!0,this.forceKillProcess(d))},p);d.on("close",u=>{if(clearTimeout(k),this.runningProcesses.delete(C),!l){if(l=!0,o){t(i("CDK command timed out"));return}u===0||r?.ignoreExitCode?t(H({exitCode:u||0})):t(i(`CDK command failed with exit code ${u}`))}}),d.on("error",u=>{clearTimeout(k),this.runningProcesses.delete(C),!(l||o)&&(l=!0,t(i(`Failed to run CDK command: ${u.message}`)))})})}async runCdkCommand(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}if(!r?.skipProjectCheck){const s=T(e,"cdk.json");if(!E(s)){t(i(`No CDK project found in ${e}`));return}}let a="",m="",f=!1;const S=this.argBuilder.buildContextArgs(r?.context),d=[...x.prefixArgs,...c,...S],C={...this.argBuilder.buildCdkEnv(r),NO_COLOR:"1",...r?.extraEnv?M(r.extraEnv):{}};$.debug("CdkService","Spawning CDK process",{command:`${x.command} ${d.join(" ")}`,workingDir:e});const o=K(x.command,d,{cwd:e,env:C,stdio:["ignore","pipe","pipe"],shell:!1,detached:!1});if(!o.pid){const s=`Failed to spawn CDK process - no PID. cwd=${e}, args=${d.join(" ")}`;o.on("error",n=>{$.debug("CdkProcess","Deferred spawn error on failed child process",{error:n.message})}),t(i(s));return}const l=`cdk-${++this.processCounter}`;this.runningProcesses.set(l,o);let p=!1;const k=r?.timeout??30*60*1e3,u=setTimeout(()=>{o.killed||(f=!0,this.forceKillProcess(o))},k);o.stdout?.on("data",s=>{const n=s.toString();a+=n,r?.outputCallback&&r.outputCallback(g(n)),r?.cdkOutputLogger?.writeCdkOutput("stdout",g(n))}),o.stderr?.on("data",s=>{const n=s.toString();r?.cdkOutputLogger?.writeCdkOutput("stderr",g(n)),!n.includes("deprecated")&&!n.includes("npm WARN")&&!n.includes("ENOENT")&&(m+=n),r?.errorCallback&&r.errorCallback(g(n))}),o.on("error",s=>{clearTimeout(u),this.runningProcesses.delete(l),!(p||f)&&(p=!0,t(i(w(s))))}),o.on("close",s=>{if(clearTimeout(u),this.runningProcesses.delete(l),p)return;if(p=!0,f){t(i("CDK command timed out"));return}const n=s===0||r?.ignoreExitCode===!0&&s===1,O=a+(m?`
1
+ import{spawn as K}from"child_process";import{existsSync as E}from"fs";import{join as T}from"path";import{createRequire as A}from"module";import{logger as $}from"@fjall/util/logger";import{filterDangerousEnvVars as M,maskSensitiveOutput as g}from"@fjall/util";import{success as H,failure as i}from"@fjall/generator";import{getErrorMessage as w}from"@fjall/util";function L(){try{const e=A(import.meta.url).resolve("aws-cdk/bin/cdk");return{command:process.execPath,prefixArgs:[e]}}catch(h){return $.debug("CdkService","Failed to resolve aws-cdk binary, falling back to npx",{error:h instanceof Error?h.message:String(h)}),{command:"npx",prefixArgs:["cdk"]}}}const x=L(),j=5e3;class v{runningProcesses=new Map;processCounter=0;argBuilder;exitHandler;sigintHandler;sigtermHandler;constructor(e){this.argBuilder=e,this.exitHandler=()=>this.cleanup(),this.sigintHandler=()=>{this.cleanup(),process.exit(130)},this.sigtermHandler=()=>{this.cleanup(),process.exit(143)},process.on("exit",this.exitHandler),process.on("SIGINT",this.sigintHandler),process.on("SIGTERM",this.sigtermHandler)}forceKillProcess(e){e.stdout?.destroy(),e.stderr?.destroy(),e.kill("SIGTERM");const c=setTimeout(()=>{e.exitCode===null&&e.kill("SIGKILL")},j);c.unref(),e.once("exit",()=>clearTimeout(c))}cleanup(){for(const[e,c]of this.runningProcesses)c.killed||(c.stdout?.destroy(),c.stderr?.destroy(),c.kill("SIGTERM"));this.runningProcesses.clear()}dispose(){this.cleanup(),process.removeListener("exit",this.exitHandler),process.removeListener("SIGINT",this.sigintHandler),process.removeListener("SIGTERM",this.sigtermHandler)}async runCdkCommandPassthrough(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}const a=T(e,"cdk.json");if(!E(a)){t(i(`No CDK project found in ${e}`));return}const m=this.argBuilder.buildContextArgs(r?.context),f=[...x.prefixArgs,...c,...m],S=this.argBuilder.buildCdkEnv(r),d=K(x.command,f,{cwd:e,env:S,stdio:"inherit",shell:!1,detached:!1});if(!d.pid){d.on("error",u=>{$.debug("CdkProcess","Spawn error on failed child process",{error:u.message})}),t(i(`Failed to spawn CDK process - no PID. cwd=${e}, args=${f.join(" ")}`));return}const C=`cdk-passthrough-${++this.processCounter}`;this.runningProcesses.set(C,d);let o=!1,l=!1;const p=r?.timeout??1800*1e3,k=setTimeout(()=>{d.killed||(o=!0,this.forceKillProcess(d))},p);d.on("close",u=>{if(clearTimeout(k),this.runningProcesses.delete(C),!l){if(l=!0,o){t(i("CDK command timed out"));return}u===0||r?.ignoreExitCode?t(H({exitCode:u||0})):t(i(`CDK command failed with exit code ${u}`))}}),d.on("error",u=>{clearTimeout(k),this.runningProcesses.delete(C),!(l||o)&&(l=!0,t(i(`Failed to run CDK command: ${u.message}`)))})})}async runCdkCommand(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}if(!r?.skipProjectCheck){const s=T(e,"cdk.json");if(!E(s)){t(i(`No CDK project found in ${e}`));return}}let a="",m="",f=!1;const S=this.argBuilder.buildContextArgs(r?.context),d=[...x.prefixArgs,...c,...S],C={...this.argBuilder.buildCdkEnv(r),NO_COLOR:"1",...r?.extraEnv?M(r.extraEnv):{}};$.debug("CdkService","Spawning CDK process",{command:`${x.command} ${d.join(" ")}`,workingDir:e});const o=K(x.command,d,{cwd:e,env:C,stdio:["ignore","pipe","pipe"],shell:!1,detached:!1});if(!o.pid){const s=`Failed to spawn CDK process - no PID. cwd=${e}, args=${d.join(" ")}`;o.on("error",n=>{$.debug("CdkProcess","Deferred spawn error on failed child process",{error:n.message})}),t(i(s));return}const l=`cdk-${++this.processCounter}`;this.runningProcesses.set(l,o);let p=!1;const k=r?.timeout??1800*1e3,u=setTimeout(()=>{o.killed||(f=!0,this.forceKillProcess(o))},k);o.stdout?.on("data",s=>{const n=s.toString();a+=n,r?.outputCallback&&r.outputCallback(g(n)),r?.cdkOutputLogger?.writeCdkOutput("stdout",g(n))}),o.stderr?.on("data",s=>{const n=s.toString();r?.cdkOutputLogger?.writeCdkOutput("stderr",g(n)),!n.includes("deprecated")&&!n.includes("npm WARN")&&!n.includes("ENOENT")&&(m+=n),r?.errorCallback&&r.errorCallback(g(n))}),o.on("error",s=>{clearTimeout(u),this.runningProcesses.delete(l),!(p||f)&&(p=!0,t(i(w(s))))}),o.on("close",s=>{if(clearTimeout(u),this.runningProcesses.delete(l),p)return;if(p=!0,f){t(i("CDK command timed out"));return}const n=s===0||r?.ignoreExitCode===!0&&s===1,O=a+(m?`
2
2
  ${m}`:"");if(n){const P=r?.combineOutput?O:a;t(H({output:g(P),exitCode:s||0}));return}let b=m;if(a){const P=a.match(/❌.*?Error:(.*)$/m);P&&(b=P[1]?.trim()??"")}const I=b||`CDK command failed with exit code ${s}`,y=a?`${I}
3
3
  ${a}`:I;t(i(g(y)))})})}}export{v as CdkProcessManager};
@@ -21,6 +21,6 @@ export declare class CdkService {
21
21
  runCdkSynth(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
22
22
  runCdkBootstrap(context: DeploymentContext, onOutput?: (chunk: string) => void, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
23
23
  runCdkDiff(context: DeploymentContext, onOutput?: (chunk: string) => void): Promise<Result<StepOutput, string>>;
24
- runCdkDeploy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
24
+ runCdkDeploy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider, credentials?: CdkOptions["credentials"], parameters?: Record<string, string>): Promise<Result<StepOutput, string>>;
25
25
  runCdkDestroy(context: DeploymentContext, stackPattern?: string, onOutput?: (chunk: string) => void, onResourceProgress?: (event: ResourceEvent) => void, aws?: AwsProvider, useCdkOut?: boolean, credentials?: CdkOptions["credentials"]): Promise<Result<StepOutput, string>>;
26
26
  }
@@ -1,3 +1,3 @@
1
- import{existsSync as v}from"fs";import{join as M}from"path";import{logger as C}from"@fjall/util/logger";import{success as h,failure as s}from"@fjall/generator";import{DEFAULT_REGION as f}from"../../aws/utils/regions.js";import{getErrorMessage as g,maskSensitiveOutput as S}from"@fjall/util";import{CdkEventMonitor as I,startStackMonitoring as A}from"./CdkEventMonitoring.js";import{analyseDeployOutput as R,analyseDestroyResult as E,createEnhancedOutputCallback as F}from"./CdkOutputAnalyser.js";import{CdkCommandRunner as w}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as K}from"./CdkArgumentBuilder.js";import{CdkProcessManager as N}from"./CdkProcessManager.js";import{wrapWithConstructMapEnrichment as O}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as T,resolveStackName as D,getFallbackStackName as L,buildDeploymentCdkContext as y}from"./cdkServiceHelpers.js";class X{commandRunner;eventMonitor;constructor(e){const t=e?.processManager??new N(new K);this.commandRunner=new w(t),this.eventMonitor=new I({eventLogWriterFactory:e?.eventLogWriterFactory})}dispose(){this.commandRunner.dispose()}async checkDifferences(e,t,r){return this.commandRunner.checkDifferences(e,t,r)}async deploy(e,t,r){return this.commandRunner.deploy(e,t,r)}async destroy(e,t,r){return this.commandRunner.destroy(e,t,r)}async runImport(e,t,r){return this.commandRunner.runImport(e,t,r)}async synth(e,t){return this.commandRunner.synth(e,t)}async bootstrap(e,t,r){return this.commandRunner.bootstrap(e,t,r)}async runCdkSynth(e,t){const r=e.callerIdentity?.Account;try{const n=await this.synth(e.path,{outputCallback:t,context:y(e,r,e.region||f)});return n.success?h({message:"CloudFormation template synthesised",details:n.data.output?{synthesisTime:n.data.output}:void 0}):s(n.error||"Failed to synthesise CloudFormation template")}catch(n){return s(`CDK synth failed: ${g(n)}`)}}async runCdkBootstrap(e,t,r){const n=e.callerIdentity?.Account,u=e.region||f;try{if(!n)return s("No AWS account ID available");const d=M(e.path,"node_modules");if(!v(d))return s(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const l=await this.bootstrap(n,u,{outputCallback:t,credentials:r});return l.success?h({message:"AWS environment bootstrapped"}):s(l.error||"Failed to bootstrap AWS environment")}catch(d){return s(`CDK bootstrap failed: ${g(d)}`)}}async runCdkDiff(e,t){const r=e.callerIdentity?.Account;try{const n=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:t,context:y(e,r,e.region||f)});return n.success?h({message:"Diff check complete",details:{hasDifferences:n.data.hasDifferences,details:n.data.details}}):s(`CDK diff failed: ${n.error.message}`)}catch(n){return s(`CDK diff failed: ${g(n)}`)}}async runCdkDeploy(e,t,r,n,u,d){const l=e.callerIdentity?.Account,m=e.region||f;if(!l)return s("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!u)return s("AwsProvider is required for deployment monitoring.");const p=O(e.path,n);let o=null,c;try{const i=D(t,e)??L(e);o=await this.eventMonitor.createEventMonitor("deploy",i,m,e,u),r&&(r(S(`Starting CloudFormation deployment of ${i}...
1
+ import{existsSync as S}from"fs";import{join as A}from"path";import{logger as b}from"@fjall/util/logger";import{success as C,failure as a}from"@fjall/generator";import{DEFAULT_REGION as y}from"../../aws/utils/regions.js";import{getErrorMessage as g,maskSensitiveOutput as m}from"@fjall/util";import{CdkEventMonitor as I,startStackMonitoring as R}from"./CdkEventMonitoring.js";import{analyseDeployOutput as E,analyseDestroyResult as F,createEnhancedOutputCallback as w}from"./CdkOutputAnalyser.js";import{CdkCommandRunner as O}from"./CdkCommandRunner.js";import{CdkArgumentBuilder as K}from"./CdkArgumentBuilder.js";import{CdkProcessManager as N}from"./CdkProcessManager.js";import{wrapWithConstructMapEnrichment as T}from"./constructMapEnrichment.js";import{STACK_DETECTION_FALLBACK_MS as L,resolveStackName as D,getFallbackStackName as P,buildDeploymentCdkContext as k}from"./cdkServiceHelpers.js";class Y{commandRunner;eventMonitor;constructor(e){const t=e?.processManager??new N(new K);this.commandRunner=new O(t),this.eventMonitor=new I({eventLogWriterFactory:e?.eventLogWriterFactory})}dispose(){this.commandRunner.dispose()}async checkDifferences(e,t,r){return this.commandRunner.checkDifferences(e,t,r)}async deploy(e,t,r){return this.commandRunner.deploy(e,t,r)}async destroy(e,t,r){return this.commandRunner.destroy(e,t,r)}async runImport(e,t,r){return this.commandRunner.runImport(e,t,r)}async synth(e,t){return this.commandRunner.synth(e,t)}async bootstrap(e,t,r){return this.commandRunner.bootstrap(e,t,r)}async runCdkSynth(e,t){const r=e.callerIdentity?.Account;try{const n=await this.synth(e.path,{outputCallback:t,context:k(e,r,e.region||y)});return n.success?C({message:"CloudFormation template synthesised",details:n.data.output?{synthesisTime:n.data.output}:void 0}):a(n.error||"Failed to synthesise CloudFormation template")}catch(n){return a(`CDK synth failed: ${m(g(n))}`)}}async runCdkBootstrap(e,t,r){const n=e.callerIdentity?.Account,d=e.region||y;try{if(!n)return a("No AWS account ID available");const l=A(e.path,"node_modules");if(!S(l))return a(`Dependencies not installed. Please run 'npm install' in ${e.path} before deploying.`);const c=await this.bootstrap(n,d,{outputCallback:t,credentials:r});return c.success?C({message:"AWS environment bootstrapped"}):a(c.error||"Failed to bootstrap AWS environment")}catch(l){return a(`CDK bootstrap failed: ${m(g(l))}`)}}async runCdkDiff(e,t){const r=e.callerIdentity?.Account;try{const n=await this.checkDifferences(e.path,void 0,{verbose:e.options?.verbose,outputCallback:t,context:k(e,r,e.region||y)});return n.success?C({message:"Diff check complete",details:{hasDifferences:n.data.hasDifferences,details:n.data.details}}):a(`CDK diff failed: ${n.error.message}`)}catch(n){return a(`CDK diff failed: ${m(g(n))}`)}}async runCdkDeploy(e,t,r,n,d,l,c){const p=e.callerIdentity?.Account,f=e.region||y;if(!p)return a("AWS account ID not available. Please ensure AWS credentials are properly configured.");if(!d)return a("AwsProvider is required for deployment monitoring.");const u=T(e.path,n);let o=null,h;try{const s=D(t,e)??P(e);o=await this.eventMonitor.createEventMonitor("deploy",s,f,e,d),r&&(r(m(`Starting CloudFormation deployment of ${s}...
2
2
  `)),r(`Monitoring CloudFormation events (CDK process running in background)...
3
- `));const a={cdkOutput:"",actualStackName:i,stackDetected:!1,monitoringPromise:null},k=F(a,r,o,p);c=setTimeout(()=>{!a.stackDetected&&o&&!a.monitoringPromise&&(C.debug("CdkService","Fallback monitoring STARTING",{targetStackName:i,stackDetected:a.stackDetected,hasOnResourceProgress:!!n}),a.monitoringPromise=A(o,i,p))},T);const b=await this.deploy(e.path,i,{verbose:e.options?.verbose,outputCallback:k,useCdkOut:!0,cdkOutputLogger:o?.getEventLogger()??void 0,context:y(e,l,m),credentials:d});return R(a.cdkOutput,b,a.actualStackName)}catch(i){const a=`CDK deploy failed: ${g(i)}`;return C.error("CdkService","CDK deployment exception",{error:a}),s(a)}finally{clearTimeout(c),o&&o.stopMonitoring()}}async runCdkDestroy(e,t,r,n,u,d,l){const m=e.callerIdentity?.Account,p=e.region||f;let o=null;try{const c=D(t,e);m&&c&&u&&(o=await this.eventMonitor.createEventMonitor("destroy",c,p,e,u),o.startMonitoring(c,a=>{n?.(a)},(a,k)=>{}));const i=await this.destroy(e.path,t,{verbose:e.options?.verbose,outputCallback:r,useCdkOut:d,cdkOutputLogger:o?.getEventLogger()??void 0,context:y(e,m,p,{includeImageVersion:!1}),credentials:l});return E(i)}catch(c){return s(`CDK destroy failed: ${g(c)}`)}finally{o&&o.stopMonitoring()}}}export{X as CdkService};
3
+ `));const i={cdkOutput:"",actualStackName:s,stackDetected:!1,monitoringPromise:null},v=w(i,r,o,u);h=setTimeout(()=>{!i.stackDetected&&o&&!i.monitoringPromise&&(b.debug("CdkService","Fallback monitoring STARTING",{targetStackName:s,stackDetected:i.stackDetected,hasOnResourceProgress:!!n}),i.monitoringPromise=R(o,s,u))},L);const M=await this.deploy(e.path,s,{verbose:e.options?.verbose,outputCallback:v,useCdkOut:!0,cdkOutputLogger:o?.getEventLogger()??void 0,context:k(e,p,f),credentials:l,...c!==void 0&&Object.keys(c).length>0&&{parameters:c}});return E(i.cdkOutput,M,i.actualStackName)}catch(s){const i=`CDK deploy failed: ${m(g(s))}`;return b.error("CdkService","CDK deployment exception",{error:i}),a(i)}finally{clearTimeout(h),o&&o.stopMonitoring()}}async runCdkDestroy(e,t,r,n,d,l,c){const p=e.callerIdentity?.Account,f=e.region||y;let u=null;try{const o=D(t,e);p&&o&&d&&(u=await this.eventMonitor.createEventMonitor("destroy",o,f,e,d),u.startMonitoring(o,s=>{n?.(s)},(s,i)=>{}));const h=await this.destroy(e.path,t,{verbose:e.options?.verbose,outputCallback:r,useCdkOut:l,cdkOutputLogger:u?.getEventLogger()??void 0,context:k(e,p,f),credentials:c});return F(h)}catch(o){return a(`CDK destroy failed: ${m(g(o))}`)}finally{u&&u.stopMonitoring()}}}export{Y as CdkService};
@@ -6,7 +6,6 @@ export interface CdkContext {
6
6
  environment?: string;
7
7
  managedAccount?: boolean;
8
8
  accountName?: string;
9
- imageVersion?: string;
10
9
  orgId?: string;
11
10
  rootId?: string;
12
11
  managementAccountId?: string;
@@ -21,6 +20,14 @@ export interface CdkOptions {
21
20
  outputCallback?: (output: string) => void;
22
21
  errorCallback?: (error: string) => void;
23
22
  context?: CdkContext;
23
+ /**
24
+ * CloudFormation parameter overrides passed as `--parameters key=value`
25
+ * argv flags to `cdk deploy`. The orchestrator populates this with the
26
+ * per-service content-hash image tags it has just pushed (key
27
+ * `<ServiceName>ImageTag`) so the synthesised CDK template's
28
+ * `CfnParameter`s resolve to the immutable digest tag, not `latest`.
29
+ */
30
+ parameters?: Record<string, string>;
24
31
  timeout?: number;
25
32
  passThroughCDK?: boolean;
26
33
  stacks?: string[];
@@ -1 +1 @@
1
- import{CloudFormationClient as d,DeleteStackCommand as g,DescribeStacksCommand as p,ListExportsCommand as h}from"@aws-sdk/client-cloudformation";import{stackStatusMap as E}from"../../aws/utils/stackStatus.js";import{success as u,failure as c}from"@fjall/generator";import{BaseServiceError as k}from"../../types/errors/ServiceError.js";import{logger as x}from"@fjall/util/logger";import{getErrorMessage as f,sleep as S}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as w}from"@fjall/util/aws";import{extractErrorName as y}from"../../aws/organisations/types.js";class l extends k{errorType;stackName;stackStatus;constructor(t,r,n,e,s,o=!1){super(`CFN_${r.toUpperCase()}`,t,s,o),this.errorType=r,this.stackName=n,this.stackStatus=e}}class D{aws;constructor(t){this.aws=t}classifyAwsError(t,r,n){const e=y(t),s=f(t);return e==="CredentialsError"||e==="UnauthorizedError"?new l(`AWS credentials error: ${s}`,"auth_error",n,void 0,t,!1):e==="Throttling"||e==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${s}`,"throttled",n,void 0,t,!0):e==="NetworkingError"||e==="ENOTFOUND"?new l(`Network error: ${s}`,"network_error",n,void 0,t,!0):new l(`${r}: ${s}`,"unknown",n,void 0,t)}async getStackOutputs(t,r){r?.onStackCheck?.(t);const n=this.aws.getClient(d),e=new p({StackName:t});try{const o=(await n.send(e)).Stacks?.[0];if(!o?.Outputs)return u([]);const a=o.Outputs.map(i=>({OutputKey:i.OutputKey,OutputValue:i.OutputValue,ExportName:i.ExportName}));return r?.onOutputsRetrieved?.(t,a.length),u(a)}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(w)?(r?.onStackNotFound?.(t),u([])):c(new l(`Failed to get outputs for stack ${t}: ${f(s)}`,"unknown",t,void 0,s))}}async getStackStatus(t,r){r?.onStackCheck?.(t);const n=this.aws.getClient(d),e=new p({StackName:t});try{const o=(await n.send(e)).Stacks?.[0];if(!o)return u({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const a=o.StackStatus||"UNKNOWN",i=E[a]||E.UNKNOWN;return r?.onStackFound?.(t,a),u({status:a,safeToRedeploy:i.safeToRedeploy,description:i.description,statusReason:o.StackStatusReason})}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(w)?u({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(s,`Failed to get stack status for ${t}`,t))}}async listAllExports(t){const r=this.aws.getClient(d),n=[];try{let e;do{const s=new h({NextToken:e}),o=await r.send(s),a=o.Exports||[];for(const i of a)i.Name&&i.Value&&n.push({Name:i.Name,Value:i.Value});if(t?.(a))break;e=o.NextToken}while(e);return u(n)}catch(e){return c(new l(`Failed to list exports: ${f(e)}`,"unknown",void 0,void 0,e,!1))}}async getExportsByNames(t){if(t.length===0)return u(new Map);const r=new Set(t),n=new Map,e=await this.listAllExports(s=>{for(const o of s)o.Name&&r.has(o.Name)&&o.Value&&n.set(o.Name,o.Value);return n.size>=r.size});return e.success?u(n):c(e.error)}async listExports(t){const r=await this.listAllExports();return r.success&&t?.onExportsRetrieved?.(r.data.length),r}async deleteStack(t){const r=this.aws.getClient(d);try{return await r.send(new g({StackName:t})),u(void 0)}catch(n){return c(this.classifyAwsError(n,`Failed to delete stack ${t}`,t))}}async stackExists(t,r){const n=r??this.aws.getClient(d);try{const s=(await n.send(new p({StackName:t}))).Stacks?.[0]?.StackStatus;return!!s&&s!=="REVIEW_IN_PROGRESS"}catch(e){return e instanceof Error&&e.message?.includes(w)?!1:(x.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:t,error:f(e)}),!0)}}async waitForDeleteComplete(t,r){const n=r?.timeoutMs??6e5,e=r?.pollIntervalMs??5e3,s=Date.now();for(;Date.now()-s<n;){const o=await this.getStackStatus(t);if(!o.success){if(o.error.recoverable){r?.onProgress?.(`Transient error polling stack ${t}, retrying: ${o.error.message}`),await S(e);continue}return c(o.error)}const a=o.data?.status;if(a==="DELETE_COMPLETE"||a==="DOES_NOT_EXIST")return u(void 0);if(a==="DELETE_FAILED")return c(new l(`Stack ${t} deletion failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",t,a,void 0,!1));r?.onProgress?.(`Stack ${t} status: ${a??"unknown"}`),await S(e)}return c(new l(`Timed out waiting for stack ${t} deletion after ${Math.round(n/1e3)}s`,"timeout",t,void 0,void 0,!0))}}export{l as CloudFormationError,D as CloudFormationService};
1
+ import{CloudFormationClient as d,DeleteStackCommand as h,DescribeStacksCommand as w,ListExportsCommand as k}from"@aws-sdk/client-cloudformation";import{stackStatusMap as S}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as f}from"@fjall/util";import{success as u,failure as c}from"@fjall/generator";import{BaseServiceError as x}from"../../types/errors/ServiceError.js";import{logger as y}from"@fjall/util/logger";import{getErrorMessage as p,sleep as m}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as E}from"@fjall/util/aws";import{extractErrorName as T}from"../../aws/organisations/types.js";class l extends x{errorType;stackName;stackStatus;constructor(t,s,o,e,r,n=!1){super(`CFN_${s.toUpperCase()}`,t,r,n),this.errorType=s,this.stackName=o,this.stackStatus=e}}class A{aws;constructor(t){this.aws=t}classifyAwsError(t,s,o){const e=T(t),r=f(p(t));return e==="CredentialsError"||e==="UnauthorizedError"?new l(`AWS credentials error: ${r}`,"auth_error",o,void 0,t,!1):e==="Throttling"||e==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${r}`,"throttled",o,void 0,t,!0):e==="NetworkingError"||e==="ENOTFOUND"?new l(`Network error: ${r}`,"network_error",o,void 0,t,!0):new l(`${s}: ${r}`,"unknown",o,void 0,t)}async getStackOutputs(t,s){s?.onStackCheck?.(t);const o=this.aws.getClient(d),e=new w({StackName:t});try{const n=(await o.send(e)).Stacks?.[0];if(!n?.Outputs)return u([]);const a=n.Outputs.map(i=>({OutputKey:i.OutputKey,OutputValue:i.OutputValue,ExportName:i.ExportName}));return s?.onOutputsRetrieved?.(t,a.length),u(a)}catch(r){if(r instanceof Error&&r.name==="ValidationError"&&r.message?.includes(E))return s?.onStackNotFound?.(t),u([]);const n=f(p(r));return c(new l(`Failed to get outputs for stack ${t}: ${n}`,"unknown",t,void 0,r))}}async getStackStatus(t,s){s?.onStackCheck?.(t);const o=this.aws.getClient(d),e=new w({StackName:t});try{const n=(await o.send(e)).Stacks?.[0];if(!n)return u({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const a=n.StackStatus||"UNKNOWN",i=S[a]||S.UNKNOWN;return s?.onStackFound?.(t,a),u({status:a,safeToRedeploy:i.safeToRedeploy,description:i.description,statusReason:n.StackStatusReason})}catch(r){return r instanceof Error&&r.name==="ValidationError"&&r.message?.includes(E)?u({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(r,`Failed to get stack status for ${t}`,t))}}async listAllExports(t){const s=this.aws.getClient(d),o=[];try{let e;do{const r=new k({NextToken:e}),n=await s.send(r),a=n.Exports||[];for(const i of a)i.Name&&i.Value&&o.push({Name:i.Name,Value:i.Value});if(t?.(a))break;e=n.NextToken}while(e);return u(o)}catch(e){const r=f(p(e));return c(new l(`Failed to list exports: ${r}`,"unknown",void 0,void 0,e,!1))}}async getExportsByNames(t){if(t.length===0)return u(new Map);const s=new Set(t),o=new Map,e=await this.listAllExports(r=>{for(const n of r)n.Name&&s.has(n.Name)&&n.Value&&o.set(n.Name,n.Value);return o.size>=s.size});return e.success?u(o):c(e.error)}async listExports(t){const s=await this.listAllExports();return s.success&&t?.onExportsRetrieved?.(s.data.length),s}async deleteStack(t){const s=this.aws.getClient(d);try{return await s.send(new h({StackName:t})),u(void 0)}catch(o){return c(this.classifyAwsError(o,`Failed to delete stack ${t}`,t))}}async stackExists(t,s){const o=s??this.aws.getClient(d);try{const r=(await o.send(new w({StackName:t}))).Stacks?.[0]?.StackStatus;return!!r&&r!=="REVIEW_IN_PROGRESS"}catch(e){return e instanceof Error&&e.message?.includes(E)?!1:(y.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:t,error:p(e)}),!0)}}async waitForDeleteComplete(t,s){const o=s?.timeoutMs??6e5,e=s?.pollIntervalMs??5e3,r=Date.now();for(;Date.now()-r<o;){const n=await this.getStackStatus(t);if(!n.success){if(n.error.recoverable){s?.onProgress?.(`Transient error polling stack ${t}, retrying: ${f(n.error.message)}`),await m(e);continue}return c(n.error)}const a=n.data?.status;if(a==="DELETE_COMPLETE"||a==="DOES_NOT_EXIST")return u(void 0);if(a==="DELETE_FAILED")return c(new l(`Stack ${t} deletion failed: ${n.data?.statusReason||"unknown reason"}`,"stack_failed",t,a,void 0,!1));s?.onProgress?.(`Stack ${t} status: ${a??"unknown"}`),await m(e)}return c(new l(`Timed out waiting for stack ${t} deletion after ${Math.round(o/1e3)}s`,"timeout",t,void 0,void 0,!0))}}export{l as CloudFormationError,A as CloudFormationService};
@@ -0,0 +1,32 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
3
+ /**
4
+ * Split an image URI of shape `<repo>:<tag>` or `<repo>@sha256:<digest>` from
5
+ * its tag/digest. Prefers `@` over `:` when both appear (digest pinning), so a
6
+ * second-pass parse of a previously digest-pinned image returns the bare repo
7
+ * URI rather than `<repo>@sha256` truncated by the colon in the digest hash.
8
+ */
9
+ export declare function stripTagOrDigest(imageUri: string): string;
10
+ /**
11
+ * Inspect ECR images by tag to obtain the immutable sha256 digest.
12
+ *
13
+ * Used by the code-only deploy orchestrator to register task definitions
14
+ * pinned to `<repo>@sha256:<digest>` rather than `<repo>:<tag>`. Digest
15
+ * pinning defeats tag mutation: a subsequent CDK redeploy on the same
16
+ * commit will not silently flip running services to an older image,
17
+ * because the registered task definition references the immutable digest
18
+ * captured at code-only deploy time. (Plan AC4.)
19
+ */
20
+ export declare class EcrImageInspectorService {
21
+ private readonly awsProvider;
22
+ constructor(awsProvider: AwsProvider);
23
+ /**
24
+ * Look up the sha256 digest for an `(imageUri, imageTag)` pair.
25
+ *
26
+ * Returns `success(undefined)` when the repository URI is not an ECR
27
+ * URI — the caller should fall back to tag pinning in that case (e.g.
28
+ * third-party registries). Returns a failure only when the ECR call
29
+ * itself errors or the tag exists but ECR returned no digest.
30
+ */
31
+ getImageDigest(imageUri: string, imageTag: string): Promise<Result<string | undefined, Error>>;
32
+ }