@fjall/deploy-core 2.30.0 → 2.31.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 (67) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/organisations/oidcProvider.js +1 -1
  3. package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +2 -2
  4. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
  5. package/dist/src/index.d.ts +2 -2
  6. package/dist/src/index.js +1 -1
  7. package/dist/src/orchestration/activeDeploymentGuard.d.ts +1 -1
  8. package/dist/src/orchestration/activeDeploymentGuard.js +3 -3
  9. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  10. package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +19 -0
  11. package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
  12. package/dist/src/orchestration/application/applicationDestroy.js +1 -1
  13. package/dist/src/orchestration/application/artefactCollection.d.ts +9 -1
  14. package/dist/src/orchestration/application/artefactCollection.js +1 -1
  15. package/dist/src/orchestration/application/codeOnlyDeploy.d.ts +10 -1
  16. package/dist/src/orchestration/application/codeOnlyDeploy.js +1 -1
  17. package/dist/src/orchestration/application/databaseEndpointReconcile.d.ts +2 -2
  18. package/dist/src/orchestration/application/databaseEndpointReconcile.js +1 -1
  19. package/dist/src/orchestration/application/destroyPreFlight.d.ts +33 -0
  20. package/dist/src/orchestration/application/destroyPreFlight.js +1 -0
  21. package/dist/src/orchestration/application/detectionPipeline.d.ts +1 -1
  22. package/dist/src/orchestration/application/detectionPipeline.js +1 -1
  23. package/dist/src/orchestration/application/dockerBuildHelper.d.ts +1 -0
  24. package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
  25. package/dist/src/orchestration/application/lambdaCodeOnlyRollout.d.ts +90 -0
  26. package/dist/src/orchestration/application/lambdaCodeOnlyRollout.js +1 -0
  27. package/dist/src/orchestration/application/plan/computeDeployPlan.d.ts +1 -1
  28. package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
  29. package/dist/src/orchestration/application/serviceImageTagsRollback.d.ts +1 -1
  30. package/dist/src/orchestration/application/serviceImageTagsRollback.js +1 -1
  31. package/dist/src/orchestration/application/taskDefinitionRoll.d.ts +1 -1
  32. package/dist/src/orchestration/application/taskDefinitionRoll.js +1 -1
  33. package/dist/src/orchestration/destroy.js +1 -1
  34. package/dist/src/orchestration/drift/driftPreFlight.js +3 -3
  35. package/dist/src/orchestration/index.d.ts +1 -1
  36. package/dist/src/orchestration/index.js +1 -1
  37. package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
  38. package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +1 -1
  39. package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
  40. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +4 -4
  41. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
  42. package/dist/src/orchestration/probes/deleteBlockProbes.d.ts +38 -0
  43. package/dist/src/orchestration/probes/deleteBlockProbes.js +1 -0
  44. package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -1
  45. package/dist/src/orchestration/remediation/recreateResource.js +1 -1
  46. package/dist/src/orchestration/remediation/removalUpdate.d.ts +74 -13
  47. package/dist/src/orchestration/remediation/removalUpdate.js +1 -1
  48. package/dist/src/orchestration/restart/restartApplication.js +1 -1
  49. package/dist/src/orchestration/restart/secretsDrift.js +1 -1
  50. package/dist/src/orchestration/serviceFactory.d.ts +2 -0
  51. package/dist/src/orchestration/serviceFactory.js +1 -1
  52. package/dist/src/orchestration/stackCleanup/stackResources.d.ts +5 -3
  53. package/dist/src/services/application/ApplicationStackService.d.ts +2 -1
  54. package/dist/src/services/application/ApplicationStackService.js +1 -1
  55. package/dist/src/services/infrastructure/CloudFormationService.d.ts +11 -10
  56. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  57. package/dist/src/services/infrastructure/EcsService.d.ts +11 -9
  58. package/dist/src/services/infrastructure/EcsService.js +1 -1
  59. package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +4 -4
  60. package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
  61. package/dist/src/services/infrastructure/LambdaService.d.ts +62 -0
  62. package/dist/src/services/infrastructure/LambdaService.js +1 -0
  63. package/dist/src/types/application/ApplicationServiceTypes.d.ts +1 -0
  64. package/dist/src/types/application/ApplicationServiceTypes.js +1 -1
  65. package/dist/src/types/remediation.d.ts +1 -0
  66. package/dist/src/types/remediation.js +1 -1
  67. package/package.json +6 -4
@@ -10,7 +10,7 @@ import type { EcsServiceResolver } from "../../services/infrastructure/EcsServic
10
10
  * warn that endpoint stability could not be verified rather than silently
11
11
  * treating a failed read as "no endpoints".
12
12
  */
13
- export declare function captureDatabaseConnectionOutputs(cfnService: CloudFormationService, appName: string): Promise<Record<string, string> | undefined>;
13
+ export declare function captureDatabaseConnectionOutputs(cfnService: CloudFormationService, appName: string, abortSignal?: AbortSignal): Promise<Record<string, string> | undefined>;
14
14
  /**
15
15
  * Snapshot each live ECS service's current task-definition ARN before the
16
16
  * stack loop runs. The post-deploy reconcile compares these against the
@@ -26,7 +26,7 @@ export declare function captureDatabaseConnectionOutputs(cfnService: CloudFormat
26
26
  * both restart without the baseline — an extra roll is harmless, a missed one
27
27
  * is not.
28
28
  */
29
- export declare function captureServiceTaskDefinitions(ecsResolver: EcsServiceResolver, ecsService: EcsService, appName: string): Promise<Record<string, string> | undefined>;
29
+ export declare function captureServiceTaskDefinitions(ecsResolver: EcsServiceResolver, ecsService: EcsService, appName: string, abortSignal?: AbortSignal): Promise<Record<string, string> | undefined>;
30
30
  export interface DatabaseEndpointReconcileParams {
31
31
  appName: string;
32
32
  /** Pre-deploy connection-output capture; `undefined` when the read failed. */
@@ -1 +1 @@
1
- import{maskSensitiveOutput as g}from"@fjall/util";import{logger as b}from"@fjall/util/logger";import{getApplicationStackName as R,APPLICATION_STACKS as T}from"../../types/operations.js";import{extractEcsServiceName as N}from"../../aws/utils/arnParser.js";import{isAborted as C}from"../../aws/organisations/types.js";import{resolveServiceTaskDefinition as O}from"./serviceImageTagsRollback.js";import{registerAndRollTaskDefinition as E}from"./taskDefinitionRoll.js";const w="databaseEndpointReconcile",j=["Endpoint","Port"];async function I(a,t){const i=R(t,T.DATABASE),o=await a.getStackOutputs(i);if(!o.success){b.debug(w,"Could not read database stack outputs",{stackName:i,error:g(o.error.message)});return}const e={};for(const d of o.data){const r=d.OutputKey,l=d.OutputValue;r===void 0||l===void 0||j.some(u=>r.endsWith(u))&&(e[r]=l)}return e}async function W(a,t,i){const o=await a.getDeployableClusterAndServices(i);if(!o.success){b.debug(w,"Could not discover services pre-deploy",{appName:i,error:g(o.error.message)});return}const{clusterArn:e,serviceArns:d}=o.data;if(e===void 0||d.length===0)return{};const r=await t.describeServices(e,d);if(!r.success){b.debug(w,"Could not describe services pre-deploy",{appName:i,error:g(r.error.message)});return}const l={};for(const u of r.data)u.serviceArn!==void 0&&u.taskDefinition!==void 0&&(l[u.serviceArn]=u.taskDefinition);return l}function x(a,t){return Object.keys(a).filter(i=>t[i]!==void 0&&t[i]!==a[i]).sort()}async function q(a){const{appName:t,connectionOutputsBefore:i,services:o,callbacks:e,abortSignal:d}=a,r=`Run \`fjall rollout ${t}\` to restart them manually.`,l=`Could not verify the database endpoint was unchanged by this deploy. If the database was replaced (e.g. restored from a snapshot), run \`fjall rollout ${t}\` so running services reconnect to it.`;if(i===void 0)return e.onLog?.(l,"warn"),{kind:"unverified"};const u=await I(o.cfnService,t);if(u===void 0)return e.onLog?.(l,"warn"),{kind:"unverified"};const h=x(i,u);if(h.length===0)return{kind:"not-needed"};if(e.onLog?.(`Database connection outputs changed this deploy (${h.join(", ")}) \u2014 the database was replaced.`,"info"),C(d))return e.onLog?.(`Deploy cancelled before services were restarted onto the new database endpoint. ${r}`,"warn"),{kind:"cancelled"};const k=await o.ecsResolver.getDeployableClusterAndServices(t);if(!k.success)return e.onLog?.(`Could not discover this application's ECS services to restart them onto the new database endpoint: ${g(k.error.message)}. ${r}`,"warn"),{kind:"failed"};const{clusterArn:m,serviceArns:$}=k.data;if(m===void 0||$.length===0)return e.onLog?.(`Database connection outputs changed but no running ECS services were found to restart. If this application has running services, run \`fjall rollout ${t}\` so they reconnect to the new database endpoint.`,"warn"),{kind:"not-needed"};const A=a.serviceTaskDefinitionsBefore,L=A!==void 0&&Object.keys(A).length>0,v=[];let S=0,D=0;const y=(n,s)=>{e.onLog?.(`Could not restart ${n} onto the new database endpoint: ${g(s)}. ${r}`,"warn"),D+=1};for(const n of $){if(C(d))return e.onLog?.(`Deploy cancelled before services were restarted onto the new database endpoint. ${r}`,"warn"),{kind:"cancelled"};const s=N(n)??n,c=await O({ecsService:o.ecsService},m,n,s);if(!c.success){y(s,c.error.message);continue}const f=A?.[n];if(L&&f===void 0){S+=1;continue}if(f!==void 0&&f!==c.data.previousTaskDefArn){S+=1;continue}v.push({serviceArn:n,serviceName:s,resolved:c.data})}if(v.length===0)return D>0?{kind:"failed"}:(e.onLog?.(`Services were already relaunched during this deploy \u2014 no restart needed. If something other than this deploy relaunched them, run \`fjall rollout ${t}\` to be safe.`,"info"),{kind:"already-current"});if(a.confirmRestart!==void 0){const n={appName:t,changedOutputs:h,serviceCount:v.length};let s;try{s=await a.confirmRestart(n)}catch(c){return e.onLog?.(`Restart confirmation failed: ${g(c instanceof Error?c.message:String(c))}. ${r}`,"warn"),{kind:"failed"}}if(!s)return e.onLog?.(`Restart declined \u2014 running containers are still connected to the old database endpoint. ${r}`,"warn"),{kind:"declined"}}e.onLog?.(`Restarting ${v.length} service${v.length===1?"":"s"} so running containers reconnect to the new database\u2026`,"info");const p=[];for(const n of v){if(C(d))return e.onLog?.(`Deploy cancelled before all services were restarted onto the new database endpoint. ${r}`,"warn"),{kind:"cancelled"};let s=n.resolved;if(a.confirmRestart!==void 0){const f=await O({ecsService:o.ecsService},m,n.serviceArn,n.serviceName);if(!f.success){y(n.serviceName,f.error.message);continue}if(f.data.previousTaskDefArn!==n.resolved.previousTaskDefArn){e.onLog?.(`${n.serviceName} was relaunched while awaiting confirmation \u2014 skipping its restart.`,"info");continue}s=f.data}const c=await E({ecsService:o.ecsService},{clusterArn:m,serviceArn:n.serviceArn,serviceName:n.serviceName,taskDefinition:s.taskDefinition,containerDefinitions:s.containerDefinitions,previousTaskDefArn:s.previousTaskDefArn,startedAtMs:Date.now()},e);if(!c.success){y(n.serviceName,c.error.message);continue}p.push(n.serviceName)}return p.length>0&&(b.debug(w,"Restarted services after database replacement",{appName:t,changedOutputs:h,services:p,alreadyRolled:S}),e.onLog?.(`Restarted ${p.length} service${p.length===1?"":"s"} onto the new database endpoint.`,"info")),D>0?{kind:"failed"}:{kind:"reconciled",services:p}}export{I as captureDatabaseConnectionOutputs,W as captureServiceTaskDefinitions,q as reconcileDatabaseEndpointChange};
1
+ import{maskSensitiveOutput as h}from"@fjall/util";import{logger as w}from"@fjall/util/logger";import{getApplicationStackName as R,APPLICATION_STACKS as T}from"../../types/operations.js";import{extractEcsServiceName as N}from"../../aws/utils/arnParser.js";import{isAborted as C}from"../../aws/organisations/types.js";import{resolveServiceTaskDefinition as O}from"./serviceImageTagsRollback.js";import{registerAndRollTaskDefinition as E}from"./taskDefinitionRoll.js";const b="databaseEndpointReconcile",j=["Endpoint","Port"];async function I(s,t,a){const c=R(t,T.DATABASE),e=await s.getStackOutputs(c,void 0,a);if(!e.success){w.debug(b,"Could not read database stack outputs",{stackName:c,error:h(e.error.message)});return}const r={};for(const i of e.data){const l=i.OutputKey,f=i.OutputValue;l===void 0||f===void 0||j.some(d=>l.endsWith(d))&&(r[l]=f)}return r}async function W(s,t,a,c){const e=await s.getDeployableClusterAndServices(a,c);if(!e.success){w.debug(b,"Could not discover services pre-deploy",{appName:a,error:h(e.error.message)});return}const{clusterArn:r,serviceArns:i}=e.data;if(r===void 0||i.length===0)return{};const l=await t.describeServices(r,i,c);if(!l.success){w.debug(b,"Could not describe services pre-deploy",{appName:a,error:h(l.error.message)});return}const f={};for(const d of l.data)d.serviceArn!==void 0&&d.taskDefinition!==void 0&&(f[d.serviceArn]=d.taskDefinition);return f}function x(s,t){return Object.keys(s).filter(a=>t[a]!==void 0&&t[a]!==s[a]).sort()}async function q(s){const{appName:t,connectionOutputsBefore:a,services:c,callbacks:e,abortSignal:r}=s,i=`Run \`fjall rollout ${t}\` to restart them manually.`,l=`Could not verify the database endpoint was unchanged by this deploy. If the database was replaced (e.g. restored from a snapshot), run \`fjall rollout ${t}\` so running services reconnect to it.`;if(a===void 0)return e.onLog?.(l,"warn"),{kind:"unverified"};const f=await I(c.cfnService,t);if(f===void 0)return e.onLog?.(l,"warn"),{kind:"unverified"};const d=x(a,f);if(d.length===0)return{kind:"not-needed"};if(e.onLog?.(`Database connection outputs changed this deploy (${d.join(", ")}) \u2014 the database was replaced.`,"info"),C(r))return e.onLog?.(`Deploy cancelled before services were restarted onto the new database endpoint. ${i}`,"warn"),{kind:"cancelled"};const k=await c.ecsResolver.getDeployableClusterAndServices(t,r);if(!k.success)return e.onLog?.(`Could not discover this application's ECS services to restart them onto the new database endpoint: ${h(k.error.message)}. ${i}`,"warn"),{kind:"failed"};const{clusterArn:m,serviceArns:$}=k.data;if(m===void 0||$.length===0)return e.onLog?.(`Database connection outputs changed but no running ECS services were found to restart. If this application has running services, run \`fjall rollout ${t}\` so they reconnect to the new database endpoint.`,"warn"),{kind:"not-needed"};const A=s.serviceTaskDefinitionsBefore,L=A!==void 0&&Object.keys(A).length>0,p=[];let D=0,S=0;const y=(n,o)=>{e.onLog?.(`Could not restart ${n} onto the new database endpoint: ${h(o)}. ${i}`,"warn"),S+=1};for(const n of $){if(C(r))return e.onLog?.(`Deploy cancelled before services were restarted onto the new database endpoint. ${i}`,"warn"),{kind:"cancelled"};const o=N(n)??n,u=await O({ecsService:c.ecsService},m,n,o,r);if(!u.success){y(o,u.error.message);continue}const v=A?.[n];if(L&&v===void 0){D+=1;continue}if(v!==void 0&&v!==u.data.previousTaskDefArn){D+=1;continue}p.push({serviceArn:n,serviceName:o,resolved:u.data})}if(p.length===0)return S>0?{kind:"failed"}:(e.onLog?.(`Services were already relaunched during this deploy \u2014 no restart needed. If something other than this deploy relaunched them, run \`fjall rollout ${t}\` to be safe.`,"info"),{kind:"already-current"});if(s.confirmRestart!==void 0){const n={appName:t,changedOutputs:d,serviceCount:p.length};let o;try{o=await s.confirmRestart(n)}catch(u){return e.onLog?.(`Restart confirmation failed: ${h(u instanceof Error?u.message:String(u))}. ${i}`,"warn"),{kind:"failed"}}if(!o)return e.onLog?.(`Restart declined \u2014 running containers are still connected to the old database endpoint. ${i}`,"warn"),{kind:"declined"}}e.onLog?.(`Restarting ${p.length} service${p.length===1?"":"s"} so running containers reconnect to the new database\u2026`,"info");const g=[];for(const n of p){if(C(r))return e.onLog?.(`Deploy cancelled before all services were restarted onto the new database endpoint. ${i}`,"warn"),{kind:"cancelled"};let o=n.resolved;if(s.confirmRestart!==void 0){const v=await O({ecsService:c.ecsService},m,n.serviceArn,n.serviceName,r);if(!v.success){y(n.serviceName,v.error.message);continue}if(v.data.previousTaskDefArn!==n.resolved.previousTaskDefArn){e.onLog?.(`${n.serviceName} was relaunched while awaiting confirmation \u2014 skipping its restart.`,"info");continue}o=v.data}const u=await E({ecsService:c.ecsService},{clusterArn:m,serviceArn:n.serviceArn,serviceName:n.serviceName,taskDefinition:o.taskDefinition,containerDefinitions:o.containerDefinitions,previousTaskDefArn:o.previousTaskDefArn,startedAtMs:Date.now()},e,r);if(!u.success){y(n.serviceName,u.error.message);continue}g.push(n.serviceName)}return g.length>0&&(w.debug(b,"Restarted services after database replacement",{appName:t,changedOutputs:d,services:g,alreadyRolled:D}),e.onLog?.(`Restarted ${g.length} service${g.length===1?"":"s"} onto the new database endpoint.`,"info")),S>0?{kind:"failed"}:{kind:"reconciled",services:g}}export{I as captureDatabaseConnectionOutputs,W as captureServiceTaskDefinitions,q as reconcileDatabaseEndpointChange};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Destroy pre-flight — ONE pure-read pass over EVERY stack about to be
3
+ * destroyed, BEFORE the first mutation. The destroy engine empties
4
+ * pre-empty-tagged S3 buckets before each CDK delete, so a delete-refusing
5
+ * setting (RDS DeletionProtection — fjall's own database constructs default
6
+ * it ON — or DynamoDB DeletionProtectionEnabled) discovered mid-run would
7
+ * leave the application half-destroyed: buckets emptied, stack DELETE_FAILED,
8
+ * raw CFN error surfaced. Probing first fails closed with the exact cure
9
+ * command while nothing has been touched.
10
+ *
11
+ * Fail-closed polarity (matching the recreate pre-flight, opposite of the
12
+ * drift preFlightGate): an unverifiable resource blocks the destroy, because
13
+ * proceeding risks partial data loss rather than a merely stale answer.
14
+ * Already-deleted stacks are the sanctioned exception — the destroy engine
15
+ * converts them to skipped, so a missing stack has nothing to block.
16
+ *
17
+ * Failure messages are pre-masked at construction (identifier-only content
18
+ * plus masked SDK error text).
19
+ */
20
+ import { type Result } from "@fjall/generator";
21
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
22
+ export interface RunDestroyPreFlightParams {
23
+ /** EVERY stack the destroy will touch — the pass must clear all of them
24
+ * before the engine mutates any. */
25
+ stackNames: readonly string[];
26
+ abortSignal?: AbortSignal;
27
+ }
28
+ /**
29
+ * Run the delete-block probe over every live resource (in every stack about
30
+ * to be destroyed) whose type carries a registry entry. Pure reads only —
31
+ * the first failure aborts the whole destroy before anything is mutated.
32
+ */
33
+ export declare function runDestroyPreFlight(aws: AwsProvider, params: RunDestroyPreFlightParams): Promise<Result<void, Error>>;
@@ -0,0 +1 @@
1
+ import{CloudFormationClient as l}from"@aws-sdk/client-cloudformation";import{success as u,failure as n}from"@fjall/generator";import{getErrorMessage as f,maskSensitiveOutput as y}from"@fjall/util";import{logger as h}from"@fjall/util/logger";import{isAborted as g}from"../../aws/organisations/types.js";import{STACK_NOT_FOUND_PATTERN as p}from"../../types/constants.js";import{DELETE_BLOCK_PROBES as a}from"../probes/deleteBlockProbes.js";import{collectStackResources as m}from"../stackCleanup/stackResources.js";const b="DestroyPreFlight";async function S(s,r){const c=s.getClient(l);for(const t of r.stackNames){if(g(r.abortSignal))return n(new Error(`Destroy pre-flight aborted before ${t} was checked \u2014 nothing was destroyed in this run.`));let d;try{d=await m(c,t,e=>e.ResourceType!==void 0&&a[e.ResourceType]!==void 0&&e.ResourceStatus!=="DELETE_COMPLETE",e=>e.LogicalResourceId!==void 0&&e.PhysicalResourceId!==void 0&&e.PhysicalResourceId!==""&&e.ResourceType!==void 0?{logicalId:e.LogicalResourceId,physicalId:e.PhysicalResourceId,resourceType:e.ResourceType}:void 0,r.abortSignal)}catch(e){if(e instanceof Error&&e.message.includes(p)){h.debug(b,`Stack ${t} does not exist \u2014 nothing to probe`);continue}return n(new Error(`Destroy pre-flight for ${t}: could not list stack resources to verify deletability: ${y(f(e))}. Retry the destroy. Nothing was destroyed in this run.`))}for(const e of d){const o=a[e.resourceType];if(o===void 0)continue;const i=await o.probe(s,e.physicalId,r.abortSignal);if(!i.success)return n(new Error(`Destroy pre-flight for ${t}: could not verify that ${e.physicalId} (${e.logicalId}) is deletable \u2014 ${o.method} failed: ${i.error.message}. Retry the destroy. Nothing was destroyed in this run.`));if(i.data.blocked)return n(new Error(`Destroy pre-flight for ${t}: ${e.physicalId} (${e.logicalId}) has ${i.data.setting} enabled \u2014 CloudFormation cannot delete it, so the destroy would fail partway after other resources were already removed. Disable it on the live resource (e.g. '${o.disableCommand(e.physicalId)}'), then re-run the destroy. Nothing was destroyed in this run.`))}}return u(void 0)}export{S as runDestroyPreFlight};
@@ -11,4 +11,4 @@ export type { DetectionResult };
11
11
  * Detects the application pattern, synthesises infrastructure, computes
12
12
  * template hashes, and determines which stacks have changed.
13
13
  */
14
- export declare function runDetectionPipeline(operation: ApplicationOperation, services: DeployServices, context: DeploymentContext, callbacks: DeployCallbacks): Promise<Result<DetectionResult>>;
14
+ export declare function runDetectionPipeline(operation: ApplicationOperation, services: DeployServices, context: DeploymentContext, callbacks: DeployCallbacks, abortSignal?: AbortSignal): Promise<Result<DetectionResult>>;
@@ -1 +1 @@
1
- import{join as A}from"path";import{success as N,failure as n}from"@fjall/generator";import{logger as F}from"@fjall/util/logger";import{maskSensitiveOutput as o}from"@fjall/util";import{deriveResourcesFromManifestStacks as H}from"../../types/detection/patternDetection.js";import{emitProgress as P,PROGRESS_MESSAGES as w}from"../../services/supporting/helpers.js";import{parseRequiredSecretsFromManifest as M,parseImportedSecretNamesFromManifest as O}from"../manifestSecretParser.js";import{resolveSecretArns as L}from"../secretArnResolver.js";import{parseDockerServicesFromManifest as q}from"@fjall/util/manifest";async function Y(h,r,a,e){e.onDetectionPhaseChange?.("detect","started");const S=r.frameworkRegistry.resolve({appPath:h.path}),C=S?.detection.pattern??null,l=S?.detection.hasDatabase??!1;e.onLog?.(`Pattern detected: ${C??"standard"}`,"info"),e.onDetectionPhaseChange?.("detect","completed");const i=A(h.path,"cdk.out");e.onDetectionPhaseChange?.("synth","started"),P(e,w.SYNTH);const y=await r.cdkService.runCdkSynth(a,t=>e.onCdkOutput?.(t,"synth"));if(!y.success)return n(new Error(o(`CDK synthesis failed: ${y.error}`)));if(e.onDetectionPhaseChange?.("synth","completed"),a.resolvedSecretArns===void 0){const t=O(i);if(t.length>0){e.onLog?.(`Resolving ${t.length} imported secret ARN(s)\u2026`,"info");const s=await L(r.awsProvider,t);if(!s.success)return n(new Error(o(s.error.message)));a.resolvedSecretArns=JSON.stringify(s.data);const g=await r.cdkService.runCdkSynth(a,$=>e.onCdkOutput?.($,"synth"));if(!g.success)return n(new Error(o(`CDK synthesis failed: ${g.error}`)))}}const d=M(i);d.length>0&&e.onLog?.(`Found ${d.length} required secret path(s) in manifest`,"info"),e.onDetectionPhaseChange?.("hash","started"),P(e,w.HASH);const m=await r.hashService.getTemplateHashes(i);if(!m.success)return n(new Error(o(`Template hash computation failed: ${m.error.message}`)));const u=m.data,f=await r.hashService.compareWithState(u,h.path);if(!f.success)return n(new Error(o(`Hash comparison failed: ${f.error.message}`)));const c=f.data;e.onDetectionPhaseChange?.("hash","completed");const p=new Map(c.stackChanges);for(const[t,s]of c.stackChanges)s||await r.cfnService.stackExists(t)||(F.debug("detectionPipeline","Stale hash detected \u2014 stack missing in CFN",{stackName:t}),p.set(t,!0));const R=Array.from(u.keys()),D=H(R),v=q(i),k=D.hasCompute&&v.length>0,E=Array.from(p.values()).some(Boolean);return e.onLog?.(`Detection complete: ${c.changedCount} changed, ${c.unchangedCount} unchanged`,"info"),N({pattern:C,hasDatabase:l,hasDifferences:E,stackChanges:p,currentHashes:u,resources:D,hasDockerfile:k,requiredSecrets:d})}export{Y as runDetectionPipeline};
1
+ import{join as $}from"path";import{success as N,failure as n}from"@fjall/generator";import{logger as F}from"@fjall/util/logger";import{maskSensitiveOutput as o}from"@fjall/util";import{deriveResourcesFromManifestStacks as H}from"../../types/detection/patternDetection.js";import{emitProgress as w,PROGRESS_MESSAGES as l}from"../../services/supporting/helpers.js";import{parseRequiredSecretsFromManifest as M,parseImportedSecretNamesFromManifest as O}from"../manifestSecretParser.js";import{resolveSecretArns as L}from"../secretArnResolver.js";import{isAborted as q}from"../../aws/organisations/types.js";import{parseDockerDeclarationsFromManifest as T}from"@fjall/util/manifest";async function z(c,r,a,e,S){e.onDetectionPhaseChange?.("detect","started");const C=r.frameworkRegistry.resolve({appPath:c.path}),y=C?.detection.pattern??null,R=C?.detection.hasDatabase??!1;e.onLog?.(`Pattern detected: ${y??"standard"}`,"info"),e.onDetectionPhaseChange?.("detect","completed");const i=$(c.path,"cdk.out");e.onDetectionPhaseChange?.("synth","started"),w(e,l.SYNTH);const D=await r.cdkService.runCdkSynth(a,t=>e.onCdkOutput?.(t,"synth"));if(!D.success)return n(new Error(o(`CDK synthesis failed: ${D.error}`)));if(e.onDetectionPhaseChange?.("synth","completed"),a.resolvedSecretArns===void 0){const t=O(i);if(t.length>0){e.onLog?.(`Resolving ${t.length} imported secret ARN(s)\u2026`,"info");const s=await L(r.awsProvider,t);if(!s.success)return n(new Error(o(s.error.message)));a.resolvedSecretArns=JSON.stringify(s.data);const g=await r.cdkService.runCdkSynth(a,E=>e.onCdkOutput?.(E,"synth"));if(!g.success)return n(new Error(o(`CDK synthesis failed: ${g.error}`)))}}const d=M(i);d.length>0&&e.onLog?.(`Found ${d.length} required secret path(s) in manifest`,"info"),e.onDetectionPhaseChange?.("hash","started"),w(e,l.HASH);const m=await r.hashService.getTemplateHashes(i);if(!m.success)return n(new Error(o(`Template hash computation failed: ${m.error.message}`)));const f=m.data,u=await r.hashService.compareWithState(f,c.path);if(!u.success)return n(new Error(o(`Hash comparison failed: ${u.error.message}`)));const h=u.data;e.onDetectionPhaseChange?.("hash","completed");const p=new Map(h.stackChanges);for(const[t,s]of h.stackChanges){if(q(S))break;s||await r.cfnService.stackExists(t,void 0,S)||(F.debug("detectionPipeline","Stale hash detected \u2014 stack missing in CFN",{stackName:t}),p.set(t,!0))}const k=Array.from(f.keys()),P=H(k),v=P.hasCompute&&T(i).declaresBuild,A=Array.from(p.values()).some(Boolean);return e.onLog?.(`Detection complete: ${h.changedCount} changed, ${h.unchangedCount} unchanged`,"info"),N({pattern:y,hasDatabase:R,hasDifferences:A,stackChanges:p,currentHashes:f,resources:P,hasDockerfile:v,requiredSecrets:d})}export{z as runDetectionPipeline};
@@ -4,6 +4,7 @@ import type { DeployCallbacks } from "../../types/callbacks.js";
4
4
  import type { ApplicationOperation } from "../../types/operations.js";
5
5
  import type { DeployServices } from "../serviceFactory.js";
6
6
  export declare const DOCKER_BUILD_STEP_NAME = "Building and pushing Docker image";
7
+ export declare function ecrRepoUri(accountId: string, region: string, appName: string): string;
7
8
  /**
8
9
  * Everything the build phase knows about what it pushed. The tag map drives
9
10
  * the CDK `<Service>ImageTag` parameters; the digest map + repo URI feed the
@@ -1 +1 @@
1
- import{isAbsolute as H,join as _,dirname as M,resolve as K}from"node:path";import{existsSync as j}from"node:fs";import{success as P,failure as h}from"@fjall/generator";import{deriveContentHashTag as z,maskSensitiveOutput as b,toKebab as x}from"@fjall/util";import{evaluateBakeGuard as X,evaluateResolvedBuildArgValues as Y,acknowledgedBuildArgKeys as q,createBakeWarningDeduper as J,buildCacheRepositoryName as U,BAKE_GUARD_EXPOSURE_CLAUSE as Q}from"@fjall/util/docker";import{runWithBoundedConcurrency as Z,resolveBuildConcurrency as ee}from"@fjall/util/buildConcurrency";import{logger as T}from"@fjall/util/logger";import{parseDockerServicesFromManifest as re}from"@fjall/util/manifest";import{STEP_IDS as A}from"../../types/stepDefinitions.js";import{isAborted as G}from"../../aws/organisations/types.js";import{resolveBuildSecrets as te,resolveSecretRefValue as ne,resolveBuildSecretSessionProvider as oe,sourcedRefsFromBuildArgs as ie}from"./buildSecretResolver.js";import{partitionForRegion as se}from"./buildSecretSession.js";import{resolveBuildArgs as ae}from"./buildArgResolver.js";import{validateBuildGroup as de}from"./buildGroupValidator.js";import{ensureBuildCacheRepository as ce}from"./ecrCacheRepository.js";const I="dockerBuildHelper",C="Building and pushing Docker image";function ue(e,n,t){const r=_(n,e);if(j(r))return{path:r,base:n};if(t!==""&&t!==n){const i=_(t,e);if(j(i))return{path:i,base:t}}return{path:r,base:n}}function le(e,n,t){const r=e.docker.path,i=e.docker.context;let s,a;if(H(r))s=r,a=n;else{const p=ue(r,n,t);s=p.path,a=p.base}return{buildContext:i?H(i)?K(i):K(a,i):K(M(s)),dockerfilePath:s,target:e.docker.target,resolvedViaFallback:a!==n}}function fe(e){return`${e.buildContext}|${e.dockerfilePath}|${e.target??""}`}function ge(e,n){return`Dockerfile not found for service '${e}' at ${n}. Checked the synth directory and the working directory. If your Dockerfile lives at the project root with the CDK app in a subdirectory, set an absolute docker.context (the build context must share the Dockerfile's directory).`}function me(e){return e.startsWith("cn-")?"amazonaws.com.cn":"amazonaws.com"}function V(e,n){return`${e}.dkr.ecr.${n}.${me(n)}`}function pe(e,n,t){return`${V(e,n)}/${x(t)}`}function he(e,n,t){return`${V(e,n)}/${U(x(t))}`}function be(e,n,t){const r=new Map;for(const i of e){const s=le(i,n,t),a=fe(s),m=r.get(a);m?m.members.push(i):r.set(a,{identity:s,members:[i]})}return Array.from(r.values())}async function ve(e,n,t,r,i){const s=await e.buildAndPush({appName:n.appName,appPath:n.path,region:t,accountId:r},(a,m,p,y)=>{i.onDockerProgress?.(b(a),m,p,y)});return s.success?P({imageUri:s.data.imageUri,imageTag:s.data.imageTag}):h(s.error)}function F(){return{contentHashTagsByService:{},imageDigestsByService:{}}}async function Se(e,n,t,r,i,s,a,m,p,y,E,f,R,w){const l=e.members[0];if(!j(e.identity.dockerfilePath))return h(new Error(ge(l.name,e.identity.dockerfilePath)));e.identity.resolvedViaFallback&&m.onLog?.(`Building service '${l.name}' from ${e.identity.dockerfilePath} (resolved at the project root).`,"info");const v=[...l.docker.buildSecrets??[],...R??[]],N=de({serviceName:l.name,dockerfilePath:e.identity.dockerfilePath,buildContext:e.identity.buildContext,...e.identity.target!==void 0&&{target:e.identity.target},...l.docker.buildArgs!==void 0&&{buildArgs:l.docker.buildArgs},...v.length>0&&{buildSecrets:v},members:e.members.map(o=>({serviceName:o.name,...o.docker.buildArgs!==void 0&&{buildArgs:o.docker.buildArgs},...o.docker.buildSecrets!==void 0&&{buildSecrets:o.docker.buildSecrets}}))});if(!N.success)return h(N.error);const d=X(l.docker.buildArgs,x(t.appName));for(const o of d.warnings){const S=b(o.message);w(S)&&m.onLog?.(S,"warn")}if(d.findings.length>0)return h(new Error(b(d.findings.map(o=>o.message).join(" "))));const u=v,D=ie(l.docker.buildArgs),L={appName:t.appName,region:r,accountId:i,partition:se(r)},$=await oe(p,[...u,...D],L);if(!$.success)return h($.error);const O=$.data,k=await ae({manifestBuildArgs:l.docker.buildArgs,serviceDir:e.identity.buildContext,appDir:t.path,...E!==void 0&&{stage:E},shellEnv:process.env,...f!==void 0&&{flagBuildArgs:f},resolveSourcedValue:(o,S)=>ne(O,{id:o,...S},y)});if(!k.success)return h(k.error);const B=k.data.buildArgs;for(const o of k.data.inferredDotenvKeys){const S=b(`buildArg "${o}" was auto-declared from a .env file and will be baked into the image \u2014 ${Q}.`);w(S)&&m.onLog?.(S,"warn")}const c=Y(B,x(t.appName),q(l.docker.buildArgs));if(c.length>0)return h(new Error(b(c.map(o=>o.message).join(" "))));let g;if(u.length>0){const o=await te(O,u,y);if(!o.success)return h(o.error);g=o.data}try{if(G(y))return h(new Error(`Build of service '${l.name}' was aborted before the docker build started`));const o={appName:t.appName,appPath:e.identity.buildContext,region:r,accountId:i,buildContext:e.identity.buildContext,dockerfilePath:e.identity.dockerfilePath,pushByDigest:!0,serviceName:l.name,...a!==void 0&&{cacheRepoUri:a},...e.identity.target!==void 0&&{target:e.identity.target},...Object.keys(B).length>0&&{buildArgs:B},...g!==void 0&&{buildSecrets:g.resolved}};return await ye(e,o,n,s,m)}finally{g!==void 0&&await g.cleanup().catch(()=>m.onLog?.("Build-secret cleanup failed.","warn"))}}async function ye(e,n,t,r,i){const s=await t.buildAndPush(n,(f,R,w,l)=>{i.onDockerProgress?.(b(f),R,w,l)});if(!s.success)return h(s.error);const a=s.data.imageDigest;if(typeof a!="string"||!a.startsWith("sha256:"))return h(new Error(`Buildx push succeeded but returned an invalid digest: ${b(String(a))}`));const m={},p={},y=[];for(const f of e.members){const R=f.docker.target,w=z(a,f.name,R);if(!w.success)return h(w.error);const l=w.data,v=f.name;m[v]=l,p[v]=a,y.push(`${r}:${l}`)}const E=await t.tagByDigest({sourceImage:r,digest:a,tags:y});return E.success?P({contentHashTagsByService:m,imageDigestsByService:p}):h(E.error)}async function Le(e,n,t,r){const i=e.dockerProvider;if(!i)return P(F());const s=n.awsProvider.getAccountId();if(!s)return r.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),P(F());const a=n.awsProvider.getRegion(),m=_(t.path,"cdk.out"),p=re(m),y=pe(s,a,t.appName);if(T.debug(I,"runDockerBuild starting",{appName:t.appName,appPath:t.path,accountId:s,region:a,cdkOutPath:m,manifestServiceCount:p.length,manifestServices:p.map(d=>({name:d.name,path:d.docker.path,context:d.docker.context,target:d.docker.target})),stepId:A.DOCKER_OPERATIONS,stepName:C}),r.onStepStart?.(A.DOCKER_OPERATIONS,C),p.length===0){r.onLog?.("Initialising ECR repository\u2026","info");const d=await i.initialiseECR({appName:t.appName,region:a,accountId:s});d.success||r.onLog?.(`ECR initialisation failed: ${b(d.error.message)}`,"warn"),r.onLog?.("Building and pushing Docker image\u2026","info");const u=await ve(i,t,a,s,r);if(!u.success){T.debug(I,"Docker buildAndPush (legacy) failed",{appName:t.appName,error:b(u.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const D=new Error(b(u.error.message));return r.onError?.(D),h(D)}return r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker image pushed: ${u.data.imageUri}`,"info"),P(F())}const E=await i.preflight?.();if(E!==void 0&&!E.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const d=new Error(b(E.error.message));return r.onError?.(d),h(d)}const f=be(p,t.path,e.workingDirectory);let R=he(s,a,t.appName);const w=await ce(n.awsProvider,U(x(t.appName)),e.abortSignal);w.success||(R=void 0,r.onLog?.(`Building without registry cache \u2014 ${w.error.message}`,"warn")),r.onLog?.(`Building and pushing ${p.length} service image(s) in ${f.length} group(s)\u2026`,"info");let l=!1;if(i.beginEcrSession!==void 0){const d=await i.beginEcrSession({accountId:s,region:a});if(!d.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const u=new Error(b(d.error.message));return r.onError?.(u),h(u)}l=!0}const v=new AbortController,N=()=>v.abort();e.abortSignal!==void 0&&(e.abortSignal.aborted?v.abort():e.abortSignal.addEventListener("abort",N,{once:!0}));try{const d=()=>G(e.abortSignal)?"the deployment was cancelled":"a sibling group failed";let u;const D=ee(f.length),L=J(),$=f.map((c,g)=>async()=>{const o=c.members.map(W=>W.name).join(", ");if(v.signal.aborted)throw new Error(`Build of group ${g+1}/${f.length} (${o}) was not started \u2014 ${d()}`);r.onLog?.(`Building group ${g+1}/${f.length} (${o})\u2026`,"info");const S=await Se(c,i,t,a,s,y,R,r,n.awsProvider,v.signal,e.options?.environment,e.options?.flagBuildArgs,e.options?.flagBuildSecrets,L);if(!S.success)throw u===void 0&&(u={error:S.error,leader:c.members[0]?.name,members:o}),v.abort(),S.error;return S.data}),O=await Z($,D,{abortOnFirstFailure:!0,abortSignal:v.signal});if(u===void 0)for(let c=0;c<f.length;c++){const g=O[c];if(!(g!==void 0&&g.success)){u={error:g!==void 0&&!g.success?g.error:new Error(`Build of group ${c+1}/${f.length} never ran \u2014 ${d()}`),leader:f[c].members[0]?.name,members:f[c].members.map(o=>o.name).join(", ")};break}}if(u!==void 0){T.debug(I,"Docker buildAndPush failed",{appName:t.appName,leader:u.leader,members:u.members,error:b(u.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const c=new Error(b(u.error.message));return r.onError?.(c),h(c)}const k={},B={};for(const c of O)if(!(c===void 0||!c.success)){for(const[g,o]of Object.entries(c.data.contentHashTagsByService))k[g]=o;for(const[g,o]of Object.entries(c.data.imageDigestsByService))B[g]=o}return T.debug(I,"Docker buildAndPush succeeded",{appName:t.appName,services:p.map(c=>c.name),groupCount:f.length,contentHashTags:k}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker images pushed for ${p.length} service(s)`,"info"),P({contentHashTagsByService:k,imageDigestsByService:B,repoUri:y})}finally{e.abortSignal?.removeEventListener("abort",N),l&&i.endEcrSession!==void 0&&await i.endEcrSession().catch(d=>{T.debug(I,"endEcrSession cleanup failed",{error:b(d instanceof Error?d.message:String(d))})})}}export{C as DOCKER_BUILD_STEP_NAME,F as emptyBuildProducts,Le as runDockerBuild};
1
+ import{isAbsolute as H,join as K,dirname as z,resolve as _}from"node:path";import{existsSync as F}from"node:fs";import{success as N,failure as h}from"@fjall/generator";import{deriveContentHashTag as J,getErrorMessage as M,maskSensitiveOutput as b,toKebab as x}from"@fjall/util";import{evaluateBakeGuard as X,evaluateResolvedBuildArgValues as Y,acknowledgedBuildArgKeys as q,createBakeWarningDeduper as Q,buildCacheRepositoryName as U,BAKE_GUARD_EXPOSURE_CLAUSE as Z}from"@fjall/util/docker";import{runWithBoundedConcurrency as ee,resolveBuildConcurrency as re}from"@fjall/util/buildConcurrency";import{logger as T}from"@fjall/util/logger";import{parseDockerServicesFromManifest as te,parseLambdaDockerServicesFromManifest as ne}from"@fjall/util/manifest";import{STEP_IDS as A}from"../../types/stepDefinitions.js";import{isAborted as G}from"../../aws/organisations/types.js";import{resolveBuildSecrets as oe,resolveSecretRefValue as ie,resolveBuildSecretSessionProvider as se,sourcedRefsFromBuildArgs as ae}from"./buildSecretResolver.js";import{partitionForRegion as de}from"./buildSecretSession.js";import{resolveBuildArgs as ce}from"./buildArgResolver.js";import{validateBuildGroup as ue}from"./buildGroupValidator.js";import{ensureBuildCacheRepository as le}from"./ecrCacheRepository.js";const L="dockerBuildHelper",C="Building and pushing Docker image";function fe(e,n,t){const r=K(n,e);if(F(r))return{path:r,base:n};if(t!==""&&t!==n){const o=K(t,e);if(F(o))return{path:o,base:t}}return{path:r,base:n}}function ge(e,n,t){const r=e.docker.path,o=e.docker.context;let s,a;if(H(r))s=r,a=n;else{const m=fe(r,n,t);s=m.path,a=m.base}return{buildContext:o?H(o)?_(o):_(a,o):_(z(s)),dockerfilePath:s,target:e.docker.target,resolvedViaFallback:a!==n}}function me(e){return`${e.buildContext}|${e.dockerfilePath}|${e.target??""}`}function pe(e,n){return`Dockerfile not found for service '${e}' at ${n}. Checked the synth directory and the working directory. If your Dockerfile lives at the project root with the CDK app in a subdirectory, set an absolute docker.context (the build context must share the Dockerfile's directory).`}function he(e){return e.startsWith("cn-")?"amazonaws.com.cn":"amazonaws.com"}function V(e,n){return`${e}.dkr.ecr.${n}.${he(n)}`}function be(e,n,t){return`${V(e,n)}/${x(t)}`}function ye(e,n,t){return`${V(e,n)}/${U(x(t))}`}function ve(e,n,t){const r=new Map;for(const o of e){const s=ge(o,n,t),a=me(s),g=r.get(a);g?g.members.push(o):r.set(a,{identity:s,members:[o]})}return Array.from(r.values())}async function Se(e,n,t,r,o){const s=await e.buildAndPush({appName:n.appName,appPath:n.path,region:t,accountId:r},(a,g,m,S)=>{o.onDockerProgress?.(b(a),g,m,S)});return s.success?N({imageUri:s.data.imageUri,imageTag:s.data.imageTag}):h(s.error)}function j(){return{contentHashTagsByService:{},imageDigestsByService:{}}}async function we(e,n,t,r,o,s,a,g,m,S,E,f,k,w){const l=e.members[0];if(!F(e.identity.dockerfilePath))return h(new Error(pe(l.name,e.identity.dockerfilePath)));e.identity.resolvedViaFallback&&g.onLog?.(`Building service '${l.name}' from ${e.identity.dockerfilePath} (resolved at the project root).`,"info");const y=[...l.docker.buildSecrets??[],...k??[]],P=ue({serviceName:l.name,dockerfilePath:e.identity.dockerfilePath,buildContext:e.identity.buildContext,...e.identity.target!==void 0&&{target:e.identity.target},...l.docker.buildArgs!==void 0&&{buildArgs:l.docker.buildArgs},...y.length>0&&{buildSecrets:y},members:e.members.map(i=>({serviceName:i.name,...i.docker.buildArgs!==void 0&&{buildArgs:i.docker.buildArgs},...i.docker.buildSecrets!==void 0&&{buildSecrets:i.docker.buildSecrets}}))});if(!P.success)return h(P.error);const d=X(l.docker.buildArgs,x(t.appName));for(const i of d.warnings){const v=b(i.message);w(v)&&g.onLog?.(v,"warn")}if(d.findings.length>0)return h(new Error(b(d.findings.map(i=>i.message).join(" "))));const c=y,D=ae(l.docker.buildArgs),I={appName:t.appName,region:r,accountId:o,partition:de(r)},$=await se(m,[...c,...D],I);if(!$.success)return h($.error);const O=$.data,R=await ce({manifestBuildArgs:l.docker.buildArgs,serviceDir:e.identity.buildContext,appDir:t.path,...E!==void 0&&{stage:E},shellEnv:process.env,...f!==void 0&&{flagBuildArgs:f},resolveSourcedValue:(i,v)=>ie(O,{id:i,...v},S)});if(!R.success)return h(R.error);const B=R.data.buildArgs;for(const i of R.data.inferredDotenvKeys){const v=b(`buildArg "${i}" was auto-declared from a .env file and will be baked into the image \u2014 ${Z}.`);w(v)&&g.onLog?.(v,"warn")}const u=Y(B,x(t.appName),q(l.docker.buildArgs));if(u.length>0)return h(new Error(b(u.map(i=>i.message).join(" "))));let p;if(c.length>0){const i=await oe(O,c,S);if(!i.success)return h(i.error);p=i.data}try{if(G(S))return h(new Error(`Build of service '${l.name}' was aborted before the docker build started`));const i={appName:t.appName,appPath:e.identity.buildContext,region:r,accountId:o,buildContext:e.identity.buildContext,dockerfilePath:e.identity.dockerfilePath,pushByDigest:!0,serviceName:l.name,...a!==void 0&&{cacheRepoUri:a},...e.identity.target!==void 0&&{target:e.identity.target},...Object.keys(B).length>0&&{buildArgs:B},...p!==void 0&&{buildSecrets:p.resolved}};return await Ee(e,i,n,s,g)}finally{p!==void 0&&await p.cleanup().catch(()=>g.onLog?.("Build-secret cleanup failed.","warn"))}}async function Ee(e,n,t,r,o){const s=await t.buildAndPush(n,(f,k,w,l)=>{o.onDockerProgress?.(b(f),k,w,l)});if(!s.success)return h(s.error);const a=s.data.imageDigest;if(typeof a!="string"||!a.startsWith("sha256:"))return h(new Error(`Buildx push succeeded but returned an invalid digest: ${b(String(a))}`));const g={},m={},S=[];for(const f of e.members){const k=f.docker.target,w=J(a,f.name,k);if(!w.success)return h(w.error);const l=w.data,y=f.name;g[y]=l,m[y]=a,S.push(`${r}:${l}`)}const E=await t.tagByDigest({sourceImage:r,digest:a,tags:S});return E.success?N({contentHashTagsByService:g,imageDigestsByService:m}):h(E.error)}function ke(e,n){const t=new Map(e.map(r=>[r.name,r]));for(const r of n){const o=t.get(r.name);if(o!==void 0&&JSON.stringify(o.docker)!==JSON.stringify(r.docker))throw new Error(`ECS service "${o.name}" and Lambda image key "${r.name}" share one build key but declare different docker configs \u2014 the second build would silently overwrite the first and both would deploy the same image. Rename one of them, or make the docker configs identical to deliberately share the build.`)}}async function Fe(e,n,t,r){const o=e.dockerProvider;if(!o)return N(j());const s=n.awsProvider.getAccountId();if(!s)return r.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),N(j());const a=n.awsProvider.getRegion(),g=K(t.path,"cdk.out");let m;try{const d=te(g),c=ne(g);ke(d,c),m=[...d,...c]}catch(d){const c=new Error(b(M(d)));return r.onError?.(c),h(c)}const S=be(s,a,t.appName);if(T.debug(L,"runDockerBuild starting",{appName:t.appName,appPath:t.path,accountId:s,region:a,cdkOutPath:g,manifestServiceCount:m.length,manifestServices:m.map(d=>({name:d.name,path:d.docker.path,context:d.docker.context,target:d.docker.target})),stepId:A.DOCKER_OPERATIONS,stepName:C}),r.onStepStart?.(A.DOCKER_OPERATIONS,C),m.length===0){r.onLog?.("Initialising ECR repository\u2026","info");const d=await o.initialiseECR({appName:t.appName,region:a,accountId:s});d.success||r.onLog?.(`ECR initialisation failed: ${b(d.error.message)}`,"warn"),r.onLog?.("Building and pushing Docker image\u2026","info");const c=await Se(o,t,a,s,r);if(!c.success){T.debug(L,"Docker buildAndPush (legacy) failed",{appName:t.appName,error:b(c.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const D=new Error(b(c.error.message));return r.onError?.(D),h(D)}return r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker image pushed: ${c.data.imageUri}`,"info"),N(j())}const E=await o.preflight?.();if(E!==void 0&&!E.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const d=new Error(b(E.error.message));return r.onError?.(d),h(d)}const f=ve(m,t.path,e.workingDirectory);let k=ye(s,a,t.appName);const w=await le(n.awsProvider,U(x(t.appName)),e.abortSignal);w.success||(k=void 0,r.onLog?.(`Building without registry cache \u2014 ${w.error.message}`,"warn")),r.onLog?.(`Building and pushing ${m.length} service image(s) in ${f.length} group(s)\u2026`,"info");let l=!1;if(o.beginEcrSession!==void 0){const d=await o.beginEcrSession({accountId:s,region:a});if(!d.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const c=new Error(b(d.error.message));return r.onError?.(c),h(c)}l=!0}const y=new AbortController,P=()=>y.abort();e.abortSignal!==void 0&&(e.abortSignal.aborted?y.abort():e.abortSignal.addEventListener("abort",P,{once:!0}));try{const d=()=>G(e.abortSignal)?"the deployment was cancelled":"a sibling group failed";let c;const D=re(f.length),I=Q(),$=f.map((u,p)=>async()=>{const i=u.members.map(W=>W.name).join(", ");if(y.signal.aborted)throw new Error(`Build of group ${p+1}/${f.length} (${i}) was not started \u2014 ${d()}`);r.onLog?.(`Building group ${p+1}/${f.length} (${i})\u2026`,"info");const v=await we(u,o,t,a,s,S,k,r,n.awsProvider,y.signal,e.options?.environment,e.options?.flagBuildArgs,e.options?.flagBuildSecrets,I);if(!v.success)throw c===void 0&&(c={error:v.error,leader:u.members[0]?.name,members:i}),y.abort(),v.error;return v.data}),O=await ee($,D,{abortOnFirstFailure:!0,abortSignal:y.signal});if(c===void 0)for(let u=0;u<f.length;u++){const p=O[u];if(!(p!==void 0&&p.success)){c={error:p!==void 0&&!p.success?p.error:new Error(`Build of group ${u+1}/${f.length} never ran \u2014 ${d()}`),leader:f[u].members[0]?.name,members:f[u].members.map(i=>i.name).join(", ")};break}}if(c!==void 0){T.debug(L,"Docker buildAndPush failed",{appName:t.appName,leader:c.leader,members:c.members,error:b(c.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const u=new Error(b(c.error.message));return r.onError?.(u),h(u)}const R={},B={};for(const u of O)if(!(u===void 0||!u.success)){for(const[p,i]of Object.entries(u.data.contentHashTagsByService))R[p]=i;for(const[p,i]of Object.entries(u.data.imageDigestsByService))B[p]=i}return T.debug(L,"Docker buildAndPush succeeded",{appName:t.appName,services:m.map(u=>u.name),groupCount:f.length,contentHashTags:R}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker images pushed for ${m.length} service(s)`,"info"),N({contentHashTagsByService:R,imageDigestsByService:B,repoUri:S})}finally{e.abortSignal?.removeEventListener("abort",P),l&&o.endEcrSession!==void 0&&await o.endEcrSession().catch(d=>{T.debug(L,"endEcrSession cleanup failed",{error:b(M(d))})})}}export{C as DOCKER_BUILD_STEP_NAME,be as ecrRepoUri,j as emptyBuildProducts,Fe as runDockerBuild};
@@ -0,0 +1,90 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import { type ServiceArtefact } from "@fjall/util";
3
+ import { type ManifestLambdaDockerEntry } from "@fjall/util/manifest";
4
+ import type { DeployParams } from "../../types/params.js";
5
+ import type { ApplicationOperation } from "../../types/operations.js";
6
+ import type { DeployServices } from "../serviceFactory.js";
7
+ /**
8
+ * Lambda-parallel rollout logic for `deployCodeOnly`'s three modes, mirroring
9
+ * the ECS rollout helpers in `codeOnlyDeploy.ts`. Unlike ECS, Lambda needs no
10
+ * CloudFormation-export-based discovery step: `computeLambda.ts` sets
11
+ * `functionName: id` verbatim on the underlying `Function` construct, and
12
+ * `compute.ts`'s manifest branch sets `manifestLambda.name = id` — the SAME
13
+ * `id`. So the manifest's `ManifestLambdaDockerEntry.name` IS the live
14
+ * function's exact `FunctionName`; `parseLambdaDockerEntriesFromManifest` is
15
+ * a complete, sufficient "what to update, and by what name" source.
16
+ */
17
+ export interface LambdaRolloutOutcome {
18
+ outputs: Record<string, string>;
19
+ artefacts: ServiceArtefact[];
20
+ failures: string[];
21
+ /**
22
+ * Distinct `{imageKey, tag}` pairs actually rolled, deduped by imageKey —
23
+ * feed straight into `reconcileImageTagParameters` (rollback modes only;
24
+ * the primary path never reconciles, matching ECS's existing asymmetry).
25
+ */
26
+ rolledImageKeys: Array<{
27
+ serviceName: string;
28
+ imageTag: string;
29
+ }>;
30
+ }
31
+ /**
32
+ * Exact case-insensitive match on a manifest Lambda function name. Deliberately
33
+ * NOT `matchesServiceName` (serviceImageTagsRollback.ts): its `endsWith` suffix
34
+ * fallback exists for ECS live-ARN names that carry cluster prefixes, but a
35
+ * manifest Lambda `name` IS the exact `FunctionName` (see the module JSDoc
36
+ * above), so suffix matching would let `--service order` silently roll
37
+ * `processOrder`. Callers in `codeOnlyDeploy.ts` (the not-found gate) must use
38
+ * this same matcher — an exact filter behind a fuzzy gate turns a typo into a
39
+ * silent zero-artefact no-op.
40
+ */
41
+ export declare function matchesLambdaFunctionName(manifestName: string, candidate: string): boolean;
42
+ /**
43
+ * Primary path: roll every container Lambda whose `imageKey` was rebuilt
44
+ * this cycle. Builds a digest-pinned `ImageUri` directly from
45
+ * `imageDigestsByService` — no ECR `BatchGetImage` call needed, since
46
+ * `runDockerBuild` already resolved that digest for anything it just built.
47
+ */
48
+ export declare function rolloutLambdaPrimary(services: DeployServices, operation: ApplicationOperation, contentHashTagsByService: Record<string, string>, imageDigestsByService: Record<string, string>, repoUri: string, callbacks: DeployParams["callbacks"], targetService: string | undefined, abortSignal?: AbortSignal): Promise<LambdaRolloutOutcome>;
49
+ /**
50
+ * `--image-tag <tag>` rollback: re-base the supplied tag onto each Lambda's
51
+ * `imageKey` family via `rebaseRollbackTagForService` — reused UNCHANGED,
52
+ * it's pure string manipulation with no ECS-specific logic. `allFamilyNames`
53
+ * MUST be the combined ECS-service-name ∪ Lambda-imageKey set so a supplied
54
+ * tag re-bases correctly across both entity types in one rollback; passing
55
+ * only ECS names here would let an ECS-family tag silently misapply to an
56
+ * unrelated Lambda.
57
+ */
58
+ export declare function rolloutLambdaImageTag(services: DeployServices, operation: ApplicationOperation, explicitRollbackTag: string, allFamilyNames: readonly string[], callbacks: DeployParams["callbacks"], targetService: string | undefined, abortSignal?: AbortSignal): Promise<LambdaRolloutOutcome>;
59
+ /**
60
+ * Everything the `serviceImageTags` execute phase needs, resolved BEFORE any
61
+ * mutation on either entity type. `repoUri` is "" when nothing matched (no
62
+ * account/region lookup was needed).
63
+ */
64
+ export interface LambdaServiceImageTagsPreflight {
65
+ matched: Array<{
66
+ entry: ManifestLambdaDockerEntry;
67
+ tag: string;
68
+ }>;
69
+ digestsByFunctionName: Map<string, string>;
70
+ repoUri: string;
71
+ }
72
+ /**
73
+ * `serviceImageTags` map rollback, preflight half: exact-name match against
74
+ * manifest Lambda entries (simpler than ECS's ARN-suffix matching — Lambda
75
+ * function names are deterministic and exact, no live-ARN fuzziness to
76
+ * resolve), then resolve every matched entry's digest, mirroring
77
+ * `preflightResolveServiceImageTagDigests`'s atomic-fail-closed behaviour: a
78
+ * single bad tag fails the whole rollback. Split from the execute half so
79
+ * `deployCodeOnly` can run BOTH namespaces' preflights before mutating
80
+ * EITHER — a bad Lambda tag must abort while ECS is still untouched, and
81
+ * vice versa.
82
+ */
83
+ export declare function preflightLambdaServiceImageTags(services: DeployServices, operation: ApplicationOperation, serviceImageTags: Record<string, string>, abortSignal?: AbortSignal): Promise<Result<LambdaServiceImageTagsPreflight, Error>>;
84
+ /**
85
+ * `serviceImageTags` map rollback, execute half: roll every preflighted
86
+ * function to its digest-pinned image. Never fails fatally — per-function
87
+ * failures land in `outcome.failures`, matching the ECS roll loop's
88
+ * continue-on-failure semantics.
89
+ */
90
+ export declare function executeLambdaServiceImageTagsRollout(services: DeployServices, preflight: LambdaServiceImageTagsPreflight, callbacks: DeployParams["callbacks"], abortSignal?: AbortSignal): Promise<LambdaRolloutOutcome>;
@@ -0,0 +1 @@
1
+ import{join as h}from"path";import{success as R,failure as L}from"@fjall/generator";import{artefactOutputKey as y,maskSensitiveOutput as w}from"@fjall/util";import{parseLambdaDockerEntriesFromManifest as I}from"@fjall/util/manifest";import{isAborted as A}from"../../aws/organisations/types.js";import{ecrRepoUri as P}from"./dockerBuildHelper.js";import{rebaseRollbackTagForService as F,deriveEcrRepositoryArn as K}from"./codeOnlyDeploy.js";function $(){return{outputs:{},artefacts:[],failures:[],rolledImageKeys:[]}}function b(e,t,n){return A(n)?(e.failures.push(`${t}: rollout cancelled before this function was reached`),!0):!1}function N(e,t,n,a,i,c,r){const o=K(i),s=`${i}:${a}`;e.outputs[y(t,"ImageTag")]=a,e.outputs[y(t,"ImageUri")]=s,e.outputs[y(t,"ImageDigest")]=c,o!==""&&(e.outputs[y(t,"EcrRepositoryArn")]=o),e.artefacts.push({serviceName:t,imageTag:a,imageUri:s,imageDigest:c,...o!==""?{ecrRepositoryArn:o}:{},functionArn:r.functionArn,publishedVersion:r.version}),e.rolledImageKeys.some(f=>f.serviceName===n)||e.rolledImageKeys.push({serviceName:n,imageTag:a})}async function v(e,t,n,a,i,c,r,o){r.onLog?.(`Updating Lambda function ${n.name} to ${c}`,"info");const s=await e.lambdaService.updateFunctionCode(n.name,`${a}@${i}`,o);if(!s.success){t.failures.push(`${n.name}: ${w(s.error.message)}`);return}s.data.unverifiedReason!==void 0&&r.onLog?.(`Updated ${n.name}, but completion of the update could not be verified: ${s.data.unverifiedReason}`,"warn"),N(t,n.name,n.imageKey,c,a,i,s.data)}function O(e,t){return e.toLowerCase()===t.toLowerCase()}function k(e,t){return t?e.filter(n=>O(n.name,t)):e}async function j(e,t,n,a,i,c,r,o){const s=$(),f=h(t.path,"cdk.out"),p=k(I(f),r);for(const u of p){if(b(s,u.name,o))continue;const m=n[u.imageKey],d=a[u.imageKey];m===void 0||d===void 0||await v(e,s,u,i,d,m,c,o)}return s}async function V(e,t,n,a,i,c,r){const o=$(),s=h(t.path,"cdk.out"),f=k(I(s),c);if(f.length===0)return o;const p=e.awsProvider.getAccountId(),u=e.awsProvider.getRegion(),m=P(p,u,t.appName);for(const d of f){if(b(o,d.name,r))continue;const g=F(n,d.imageKey,a),l=await e.ecrImageInspector.getImageDigest(m,g,r);if(!l.success||l.data===void 0){const E=l.success?"not an ECR repository":w(l.error.message);o.failures.push(`${d.name}: image tag '${g}' (rebased from '${n}') could not be resolved to a digest: ${E}`);continue}await v(e,o,d,m,l.data,g,i,r)}return o}async function q(e,t,n,a){const i=h(t.path,"cdk.out"),r=I(i).map(u=>({entry:u,tag:n[u.name]})).filter(u=>u.tag!==void 0);if(r.length===0)return R({matched:r,digestsByFunctionName:new Map,repoUri:""});const o=e.awsProvider.getAccountId(),s=e.awsProvider.getRegion(),f=P(o,s,t.appName),p=new Map;for(const{entry:u,tag:m}of r){if(A(a))return L(new Error("Pre-flight cancelled. No function was rolled."));const d=await e.ecrImageInspector.getImageDigest(f,m,a);if(!d.success||d.data===void 0){const g=d.success?"not an ECR repository":w(d.error.message);return L(new Error(`Lambda function '${u.name}': image tag '${m}' could not be resolved to a digest: ${g}. No function was rolled.`))}p.set(u.name,d.data)}return R({matched:r,digestsByFunctionName:p,repoUri:f})}async function z(e,t,n,a){const i=$();for(const{entry:c,tag:r}of t.matched){if(b(i,c.name,a))continue;const o=t.digestsByFunctionName.get(c.name);o!==void 0&&await v(e,i,c,t.repoUri,o,r,n,a)}return i}export{z as executeLambdaServiceImageTagsRollout,O as matchesLambdaFunctionName,q as preflightLambdaServiceImageTags,V as rolloutLambdaImageTag,j as rolloutLambdaPrimary};
@@ -11,7 +11,7 @@ import type { ChangeSetProbeRunner } from "../../../services/infrastructure/chan
11
11
  import type { DeployPlan } from "./types.js";
12
12
  /** The narrow CFN read surface the gate needs — CloudFormationService satisfies it. */
13
13
  export interface CfnTemplateReader {
14
- getTemplate(stackName: string): Promise<Result<string, CloudFormationError>>;
14
+ getTemplate(stackName: string, abortSignal?: AbortSignal): Promise<Result<string, CloudFormationError>>;
15
15
  }
16
16
  export interface ComputeDeployPlanStack {
17
17
  stackName: string;
@@ -1 +1 @@
1
- import{success as i,failure as s}from"@fjall/generator";import{maskSensitiveOutput as c}from"@fjall/util";import{buildDeployPlan as f}from"./buildDeployPlan.js";import{applyChangeSetEscalation as u}from"./changeSetEscalation.js";import{applyRegistryOverlay as p}from"./registryOverlay.js";function d(e){const r=e.trim();if(r==="")return i({});try{return i(JSON.parse(r))}catch(t){return s(new Error(`Failed to parse deployed template: ${c(t instanceof Error?t.message:String(t))}`))}}async function k(e){const r=[];for(const o of e.stacks){const n=await e.cfnService.getTemplate(o.stackName);let l={};if(n.success){const a=d(n.data);if(!a.success)return s(a.error);l=a.data}else if(n.error.errorType!=="stack_not_found")return s(n.error);r.push({stackName:o.stackName,oldTemplate:l,newTemplate:o.newTemplate})}let t=f(r,e.assemblyDigest);return e.registry!==void 0&&(t=await p({plan:t,registry:e.registry,stacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})),e.changeSetProbe!==void 0&&(t=await u({plan:t,probe:e.changeSetProbe,stacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})),i(t)}export{k as computeDeployPlan};
1
+ import{success as l,failure as n}from"@fjall/generator";import{maskSensitiveOutput as c}from"@fjall/util";import{isAborted as f}from"../../../aws/organisations/types.js";import{buildDeployPlan as u}from"./buildDeployPlan.js";import{applyChangeSetEscalation as p}from"./changeSetEscalation.js";import{applyRegistryOverlay as d}from"./registryOverlay.js";function g(e){const r=e.trim();if(r==="")return l({});try{return l(JSON.parse(r))}catch(t){return n(new Error(`Failed to parse deployed template: ${c(t instanceof Error?t.message:String(t))}`))}}async function T(e){const r=[];for(const i of e.stacks){if(f(e.abortSignal))return n(new Error("Deploy-plan computation aborted"));const o=await e.cfnService.getTemplate(i.stackName,e.abortSignal);let s={};if(o.success){const a=g(o.data);if(!a.success)return n(a.error);s=a.data}else if(o.error.errorType!=="stack_not_found")return n(o.error);r.push({stackName:i.stackName,oldTemplate:s,newTemplate:i.newTemplate})}let t=u(r,e.assemblyDigest);return e.registry!==void 0&&(t=await d({plan:t,registry:e.registry,stacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})),e.changeSetProbe!==void 0&&(t=await p({plan:t,probe:e.changeSetProbe,stacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})),l(t)}export{T as computeDeployPlan};
@@ -41,7 +41,7 @@ export interface ResolvedServiceTaskDefinition {
41
41
  * resolution (map mode) and the rollout itself (legacy `--image-tag` mode) so
42
42
  * the describe → task-definition → repo-URI derivation lives in one place.
43
43
  */
44
- export declare function resolveServiceTaskDefinition(services: Pick<DeployServices, "ecsService">, clusterArn: string, serviceArn: string, serviceName: string): Promise<Result<ResolvedServiceTaskDefinition, Error>>;
44
+ export declare function resolveServiceTaskDefinition(services: Pick<DeployServices, "ecsService">, clusterArn: string, serviceArn: string, serviceName: string, abortSignal?: AbortSignal): Promise<Result<ResolvedServiceTaskDefinition, Error>>;
45
45
  export interface PreResolvedRollout {
46
46
  taskDefinition: ResolvedServiceTaskDefinition;
47
47
  imageDigest: string;
@@ -1 +1 @@
1
- import{success as h,failure as u}from"@fjall/generator";import{maskSensitiveOutput as p}from"@fjall/util";import{extractEcsServiceName as S}from"../../aws/utils/arnParser.js";import{isAborted as y}from"../../aws/organisations/types.js";import{stripTagOrDigest as E}from"../../services/infrastructure/EcrImageInspectorService.js";function $(c,g){const o=c.toLowerCase(),r=g.toLowerCase();return o===r||o.endsWith(r)}function L(c,g){const o=Object.entries(g),r=[],f=[],n=new Set,s=c.map(e=>{const t=S(e)??e;return{serviceArn:e,serviceName:t,lowerName:t.toLowerCase()}}),i=new Map,a=new Set;for(const e of s){const t=o.find(([d])=>d.toLowerCase()===e.lowerName);t!==void 0&&(i.set(e.serviceArn,t),a.add(t[0]))}const v=new Map,m=new Map;for(const e of s){if(i.has(e.serviceArn))continue;const t=o.filter(([d])=>!a.has(d)&&$(e.serviceName,d)).sort(([d],[l])=>l.length-d.length)[0];t!==void 0&&(v.set(e.serviceArn,t),m.set(t[0],(m.get(t[0])??0)+1))}for(const e of s){const t=v.get(e.serviceArn),d=i.get(e.serviceArn)??(t!==void 0&&m.get(t[0])===1?t:void 0);if(d===void 0){f.push(e.serviceName);continue}const[l,D]=d;n.add(l),r.push({serviceArn:e.serviceArn,serviceName:e.serviceName,tag:D,mapKey:l})}const w=o.map(([e])=>e).filter(e=>!n.has(e));return{matched:r,unmatchedLiveServiceNames:f,staleMapKeys:w}}async function k(c,g,o,r){const f=await c.ecsService.describeServices(g,[o]);if(!f.success)return u(new Error(`Failed to describe service ${r}: ${p(f.error.message)}`));const n=f.data[0];if(!n?.taskDefinition)return u(new Error(`Service ${r} has no task definition`));const s=n.taskDefinition,i=await c.ecsService.getLatestTaskDefinition(s);if(!i.success)return u(new Error(`Failed to fetch task definition for ${r}: ${p(i.error.message)}`));const a=i.data;if(!a||!a.containerDefinitions||a.containerDefinitions.length===0)return u(new Error(`Task definition for ${r} has no container definitions`));const v=a.containerDefinitions,m=v.find(e=>e.image)?.image;if(!m)return u(new Error(`Task definition for ${r} has no container image \u2014 cannot derive repository URI`));const w=E(m);return h({previousTaskDefArn:s,taskDefinition:a,containerDefinitions:v,repoUri:w})}async function I(c,g,o,r){const f=new Map;for(const n of o){if(y(r))return u(new Error("Pre-flight cancelled. No service was rolled."));const s=await k(c,g,n.serviceArn,n.serviceName);if(!s.success)return u(new Error(`Pre-flight failed resolving the current task definition for ${n.serviceName} (tag ${n.tag}): ${p(s.error.message)}. No service was rolled.`));const i=await c.ecrImageInspector.getImageDigest(s.data.repoUri,n.tag,r);if(!i.success||i.data===void 0){const a=i.success?"ECR returned no digest for this tag (non-ECR repository, or the image is missing)":p(i.error.message);return u(new Error(`Pre-flight digest resolution failed for ${n.serviceName} (tag ${n.tag}): ${a}. No service was rolled.`))}f.set(n.serviceArn,{taskDefinition:s.data,imageDigest:i.data})}return h(f)}export{L as matchServiceImageTagsToLiveServices,$ as matchesServiceName,I as preflightResolveServiceImageTagDigests,k as resolveServiceTaskDefinition};
1
+ import{success as h,failure as m}from"@fjall/generator";import{maskSensitiveOutput as w}from"@fjall/util";import{extractEcsServiceName as y}from"../../aws/utils/arnParser.js";import{isAborted as E}from"../../aws/organisations/types.js";import{stripTagOrDigest as $}from"../../services/infrastructure/EcrImageInspectorService.js";function k(a,g){const o=a.toLowerCase(),r=g.toLowerCase();return o===r||o.endsWith(r)}function L(a,g){const o=Object.entries(g),r=[],f=[],n=new Set,s=a.map(e=>{const t=y(e)??e;return{serviceArn:e,serviceName:t,lowerName:t.toLowerCase()}}),i=new Map,d=new Set;for(const e of s){const t=o.find(([c])=>c.toLowerCase()===e.lowerName);t!==void 0&&(i.set(e.serviceArn,t),d.add(t[0]))}const u=new Map,v=new Map;for(const e of s){if(i.has(e.serviceArn))continue;const t=o.filter(([c])=>!d.has(c)&&k(e.serviceName,c)).sort(([c],[p])=>p.length-c.length)[0];t!==void 0&&(u.set(e.serviceArn,t),v.set(t[0],(v.get(t[0])??0)+1))}for(const e of s){const t=u.get(e.serviceArn),c=i.get(e.serviceArn)??(t!==void 0&&v.get(t[0])===1?t:void 0);if(c===void 0){f.push(e.serviceName);continue}const[p,D]=c;n.add(p),r.push({serviceArn:e.serviceArn,serviceName:e.serviceName,tag:D,mapKey:p})}const l=o.map(([e])=>e).filter(e=>!n.has(e));return{matched:r,unmatchedLiveServiceNames:f,staleMapKeys:l}}async function N(a,g,o,r,f){const n=await a.ecsService.describeServices(g,[o],f);if(!n.success)return m(new Error(`Failed to describe service ${r}: ${w(n.error.message)}`));const s=n.data[0];if(!s?.taskDefinition)return m(new Error(`Service ${r} has no task definition`));const i=s.taskDefinition,d=await a.ecsService.getLatestTaskDefinition(i,f);if(!d.success)return m(new Error(`Failed to fetch task definition for ${r}: ${w(d.error.message)}`));const u=d.data;if(!u||!u.containerDefinitions||u.containerDefinitions.length===0)return m(new Error(`Task definition for ${r} has no container definitions`));const v=u.containerDefinitions,l=v.find(t=>t.image)?.image;if(!l)return m(new Error(`Task definition for ${r} has no container image \u2014 cannot derive repository URI`));const e=$(l);return h({previousTaskDefArn:i,taskDefinition:u,containerDefinitions:v,repoUri:e})}async function I(a,g,o,r){const f=new Map;for(const n of o){if(E(r))return m(new Error("Pre-flight cancelled. No service was rolled."));const s=await N(a,g,n.serviceArn,n.serviceName,r);if(!s.success)return m(new Error(`Pre-flight failed resolving the current task definition for ${n.serviceName} (tag ${n.tag}): ${w(s.error.message)}. No service was rolled.`));const i=await a.ecrImageInspector.getImageDigest(s.data.repoUri,n.tag,r);if(!i.success||i.data===void 0){const d=i.success?"ECR returned no digest for this tag (non-ECR repository, or the image is missing)":w(i.error.message);return m(new Error(`Pre-flight digest resolution failed for ${n.serviceName} (tag ${n.tag}): ${d}. No service was rolled.`))}f.set(n.serviceArn,{taskDefinition:s.data,imageDigest:i.data})}return h(f)}export{L as matchServiceImageTagsToLiveServices,k as matchesServiceName,I as preflightResolveServiceImageTagDigests,N as resolveServiceTaskDefinition};
@@ -29,5 +29,5 @@ export interface RollTaskDefinitionOk {
29
29
  * task-definition field omitted here is silently dropped from the new
30
30
  * revision on BOTH paths, so it must have exactly one home.
31
31
  */
32
- export declare function registerAndRollTaskDefinition(services: Pick<DeployServices, "ecsService">, input: RollTaskDefinitionInput, callbacks: DeployCallbacks): Promise<Result<RollTaskDefinitionOk, Error>>;
32
+ export declare function registerAndRollTaskDefinition(services: Pick<DeployServices, "ecsService">, input: RollTaskDefinitionInput, callbacks: DeployCallbacks, abortSignal?: AbortSignal): Promise<Result<RollTaskDefinitionOk, Error>>;
33
33
  export declare function extractRevision(taskDefArn: string): number;
@@ -1 +1 @@
1
- import{success as w,failure as l}from"@fjall/generator";import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as p}from"@fjall/util";const $="taskDefinitionRoll";async function T(n,a,r){const{clusterArn:f,serviceArn:s,serviceName:t,taskDefinition:e,containerDefinitions:A,previousTaskDefArn:g,imageDigest:C,startedAtMs:R}=a,m=e.family;if(!m)return l(new Error(`Task definition for ${t} has no family`));const d=await n.ecsService.registerTaskDefinition({family:m,taskRoleArn:e.taskRoleArn,executionRoleArn:e.executionRoleArn,networkMode:e.networkMode,containerDefinitions:A,volumes:e.volumes,placementConstraints:e.placementConstraints,requiresCompatibilities:e.requiresCompatibilities,cpu:e.cpu,memory:e.memory,runtimePlatform:e.runtimePlatform,proxyConfiguration:e.proxyConfiguration,inferenceAccelerators:e.inferenceAccelerators,pidMode:e.pidMode,ipcMode:e.ipcMode,ephemeralStorage:e.ephemeralStorage});if(!d.success)return l(new Error(`Failed to register task definition for ${t}: ${d.error.message}`));const i=d.data;r.onTaskDefRegistered?.({serviceName:t,taskDefinitionArn:i,previousTaskDefinitionArn:g,revision:y(i),family:m,...C!==void 0?{imageDigest:C}:{}});const D=await n.ecsService.updateService(f,s,{taskDefinition:i,forceNewDeployment:!0});if(!D.success)return l(new Error(`Failed to update service ${t}: ${D.error.message}`));const k=await n.ecsService.pollDeployment({clusterArn:f,serviceArn:s,waitForCompletion:!0,progressCallback:o=>{const v=o.latestEvent?` \u2014 ${p(o.latestEvent)}`:"";r.onECSProgress?.(`${t}: ${p(o.message)}${v}`,o.percentComplete)}}),c=Date.now()-R;if(!k.success){const o=p(k.error.message);return r.onECSComplete?.({serviceName:t,serviceArn:s,success:!1,taskDefinitionArn:i,durationMs:c,reason:o}),l(new Error(`ECS rollout failed for ${t}: ${o}`))}const u=await n.ecsService.getDeploymentStatus(f,s);return r.onECSComplete?.({serviceName:t,serviceArn:s,success:!0,taskDefinitionArn:i,durationMs:c,finalRunningCount:u.success?u.data.runningCount:void 0,finalDesiredCount:u.success?u.data.desiredCount:void 0}),S.debug($,`Rolled ${t} successfully`,{serviceArn:s,newTaskDefArn:i,previousTaskDefArn:g,durationMs:c}),w({newTaskDefArn:i,durationMs:c})}function y(n){const a=n.lastIndexOf(":");if(a<0)return 0;const r=n.slice(a+1);return/^\d+$/.test(r)?parseInt(r,10):0}export{y as extractRevision,T as registerAndRollTaskDefinition};
1
+ import{success as S,failure as l}from"@fjall/generator";import{logger as $}from"@fjall/util/logger";import{maskSensitiveOutput as p}from"@fjall/util";const y="taskDefinitionRoll";async function O(n,a,r,g){const{clusterArn:f,serviceArn:s,serviceName:t,taskDefinition:e,containerDefinitions:R,previousTaskDefArn:C,imageDigest:D,startedAtMs:v}=a,m=e.family;if(!m)return l(new Error(`Task definition for ${t} has no family`));const d=await n.ecsService.registerTaskDefinition({family:m,taskRoleArn:e.taskRoleArn,executionRoleArn:e.executionRoleArn,networkMode:e.networkMode,containerDefinitions:R,volumes:e.volumes,placementConstraints:e.placementConstraints,requiresCompatibilities:e.requiresCompatibilities,cpu:e.cpu,memory:e.memory,runtimePlatform:e.runtimePlatform,proxyConfiguration:e.proxyConfiguration,inferenceAccelerators:e.inferenceAccelerators,pidMode:e.pidMode,ipcMode:e.ipcMode,ephemeralStorage:e.ephemeralStorage},g);if(!d.success)return l(new Error(`Failed to register task definition for ${t}: ${d.error.message}`));const i=d.data;r.onTaskDefRegistered?.({serviceName:t,taskDefinitionArn:i,previousTaskDefinitionArn:C,revision:E(i),family:m,...D!==void 0?{imageDigest:D}:{}});const k=await n.ecsService.updateService(f,s,{taskDefinition:i,forceNewDeployment:!0},g);if(!k.success)return l(new Error(`Failed to update service ${t}: ${k.error.message}`));const A=await n.ecsService.pollDeployment({clusterArn:f,serviceArn:s,waitForCompletion:!0,progressCallback:o=>{const w=o.latestEvent?` \u2014 ${p(o.latestEvent)}`:"";r.onECSProgress?.(`${t}: ${p(o.message)}${w}`,o.percentComplete)}}),c=Date.now()-v;if(!A.success){const o=p(A.error.message);return r.onECSComplete?.({serviceName:t,serviceArn:s,success:!1,taskDefinitionArn:i,durationMs:c,reason:o}),l(new Error(`ECS rollout failed for ${t}: ${o}`))}const u=await n.ecsService.getDeploymentStatus(f,s);return r.onECSComplete?.({serviceName:t,serviceArn:s,success:!0,taskDefinitionArn:i,durationMs:c,finalRunningCount:u.success?u.data.runningCount:void 0,finalDesiredCount:u.success?u.data.desiredCount:void 0}),$.debug(y,`Rolled ${t} successfully`,{serviceArn:s,newTaskDefArn:i,previousTaskDefArn:C,durationMs:c}),S({newTaskDefArn:i,durationMs:c})}function E(n){const a=n.lastIndexOf(":");if(a<0)return 0;const r=n.slice(a+1);return/^\d+$/.test(r)?parseInt(r,10):0}export{E as extractRevision,O as registerAndRollTaskDefinition};
@@ -1 +1 @@
1
- import{success as s,failure as l}from"@fjall/generator";import{isApplicationOperation as u,isOrganisationOperation as g,getApplicationStackName as p,getOrganisationStackName as y,APPLICATION_STACKS as b}from"../types/operations.js";import{isAborted as d}from"../aws/organisations/types.js";import{createDeployServices as k}from"./serviceFactory.js";import{resolveOperation as w}from"./resolveOperation.js";import{destroyApplication as S}from"./application/applicationDestroy.js";import{destroyOrganisation as D}from"./organisation/organisationDestroy.js";import{checkActiveDeployments as v}from"./activeDeploymentGuard.js";async function I(t){const i=Date.now(),n=k({target:t.target,workingDirectory:t.workingDirectory,awsCredentials:t.awsCredentials,callbacks:t.callbacks,options:{verbose:t.options?.verbose,force:t.options?.force,cascade:t.options?.cascade,skipConfirmation:t.options?.skipConfirmation},orgConfig:t.orgConfig,identity:t.identity,...t.abortSignal!==void 0?{abortSignal:t.abortSignal}:{}}),r=await w(t.target,t.workingDirectory);if(!r.success)return t.callbacks.onError?.(r.error),r;const e=r.data;if(!t.options?.force){const o=u(e)?Object.values(b).map(f=>p(e.appName,f)):[y(e.type)],c=await v(o,n.cfnService);if(!c.success)return t.callbacks.onError?.(c.error),l(c.error)}if(u(e)){const o=await S(t,n,e);return d(t.abortSignal)?s({target:t.target,deploymentType:"application",cancelled:!0,durationMs:Date.now()-i}):o.success&&o.data.durationMs===void 0?s({...o.data,durationMs:Date.now()-i}):o}if(g(e)){const o=await D(t,n,e);return d(t.abortSignal)?s({target:t.target,deploymentType:"organisation",cancelled:!0,durationMs:Date.now()-i}):o}const a=new Error(`Unsupported destroy target: ${t.target}`);return t.callbacks.onError?.(a),l(a)}export{I as destroy};
1
+ import{success as a,failure as l}from"@fjall/generator";import{isApplicationOperation as g,isOrganisationOperation as f,getApplicationStackName as p,getOrganisationStackName as y,APPLICATION_STACKS as b}from"../types/operations.js";import{isAborted as u}from"../aws/organisations/types.js";import{createDeployServices as k}from"./serviceFactory.js";import{resolveOperation as w}from"./resolveOperation.js";import{destroyApplication as S}from"./application/applicationDestroy.js";import{destroyOrganisation as D}from"./organisation/organisationDestroy.js";import{checkActiveDeployments as v}from"./activeDeploymentGuard.js";async function I(t){const i=Date.now(),n=k({target:t.target,workingDirectory:t.workingDirectory,awsCredentials:t.awsCredentials,callbacks:t.callbacks,options:{verbose:t.options?.verbose,force:t.options?.force,cascade:t.options?.cascade,skipConfirmation:t.options?.skipConfirmation},orgConfig:t.orgConfig,identity:t.identity,...t.abortSignal!==void 0?{abortSignal:t.abortSignal}:{}}),r=await w(t.target,t.workingDirectory);if(!r.success)return t.callbacks.onError?.(r.error),r;const e=r.data;if(!t.options?.force){const o=g(e)?Object.values(b).map(d=>p(e.appName,d)):[y(e.type)],c=await v(o,n.cfnService,t.abortSignal);if(!c.success)return t.callbacks.onError?.(c.error),l(c.error)}if(g(e)){const o=await S(t,n,e);return u(t.abortSignal)?a({target:t.target,deploymentType:"application",cancelled:!0,durationMs:Date.now()-i}):o.success&&o.data.durationMs===void 0?a({...o.data,durationMs:Date.now()-i}):o}if(f(e)){const o=await D(t,n,e);return u(t.abortSignal)?a({target:t.target,deploymentType:"organisation",cancelled:!0,durationMs:Date.now()-i}):o}const s=new Error(`Unsupported destroy target: ${t.target}`);return t.callbacks.onError?.(s),l(s)}export{I as destroy};
@@ -1,6 +1,6 @@
1
- import{success as g}from"@fjall/generator";import{detectStackDrift as $}from"./detectStackDrift.js";import{readDriftSuspects as v}from"./driftSuspectJournal.js";const y=new Set(["UPDATE_ROLLBACK_COMPLETE","UPDATE_ROLLBACK_FAILED","UPDATE_FAILED"]),b=9e4;async function D(o,l){const n=l.budgetMs??b,d=l.abortSignal!==void 0?AbortSignal.any([l.abortSignal,AbortSignal.timeout(n)]):AbortSignal.any([AbortSignal.timeout(n)]),r={confirmed:[],unconfirmed:[],probedStacks:[]},u=await Promise.all(l.stackNames.map(async i=>{const a=await l.cfnService.getStackStatus(i);return{stackName:i,status:a.success?a.data?.status??null:null}}));for(const{stackName:i,status:a}of u){if(!(a!==null&&y.has(a))){const t=await v(o.aws.getAccountId(),o.aws.getRegion(),i,o.journal);if(t===null||t.suspects.length===0)continue}r.probedStacks.push(i);const c=await $(o,{stackName:i,abortSignal:d});if(c.success)for(const t of c.data.findings)t.verdict==="confirmed-deleted"?r.confirmed.push(t):(t.verdict==="unconfirmed"||t.verdict==="stack-missing")&&r.unconfirmed.push(t)}return g(r)}function I(o,l=[],n={}){const d=o.map(e=>` ${e.stackName} \u203A ${e.logicalId} (${e.resourceType})${e.physicalId!==void 0?` [${e.physicalId}]`:""} \u2014 ${e.verdictDetail}`),r=l.map(e=>`Remediation ${e} is in progress \u2014 run 'fjall drift resume ${e}' to continue it.`),u=(n.flipNotLive??[]).map(({finding:e,opId:s})=>`Remediation ${s} recorded its Retain flip on ${e.stackName} \u203A ${e.logicalId}, but the flip is no longer live on the stack \u2014 re-run 'fjall drift repair' to re-verify before deploying.`),i=(n.flipUnverified??[]).map(({finding:e,opId:s,message:h})=>`Remediation ${s} should complete with this deploy, but ${e.stackName} \u203A ${e.logicalId} could not be verified: the live template could not be read to confirm the Retain flip is still live \u2014 ${h}. Retry the deploy.`),a=(n.stillDeclared??[]).map(({finding:e,opId:s})=>`Remediation ${s} is ready to converge, but ${e.stackName} \u203A ${e.logicalId} is still declared in your fjall config \u2014 remove the resource from the config, then deploy to complete the forget.`),f=[...r,...u,...i,...a],c=new Set((n.flipUnverified??[]).map(({finding:e})=>`${e.stackName}/${e.logicalId}`)),t=o.length>0&&o.every(e=>c.has(`${e.stackName}/${e.logicalId}`)),m=t?`Deploy blocked: the Retain-flip state of ${o.length} mid-remediation resource(s) could not be verified (the live template could not be read):
2
- `:`Deploy blocked: CloudFormation still tracks ${o.length} resource(s) deleted outside CloudFormation \u2014 deploying would fail and re-wedge the stack:
3
- `,p=t?"Retry the deploy \u2014 verification is transient when the live template read fails; if it keeps failing, check CloudFormation access to the stack template.":"Run 'fjall drift detect' for the full report, then 'fjall drift repair --remediate <physicalName>=<verb>' to remediate confirmed deletions.";return m+`${d.join(`
1
+ import{success as g}from"@fjall/generator";import{detectStackDrift as $}from"./detectStackDrift.js";import{readDriftSuspects as v}from"./driftSuspectJournal.js";const y=new Set(["UPDATE_ROLLBACK_COMPLETE","UPDATE_ROLLBACK_FAILED","UPDATE_FAILED"]),b=9e4;async function D(i,l){const n=l.budgetMs??b,c=l.abortSignal!==void 0?AbortSignal.any([l.abortSignal,AbortSignal.timeout(n)]):AbortSignal.any([AbortSignal.timeout(n)]),r={confirmed:[],unconfirmed:[],probedStacks:[]},u=await Promise.all(l.stackNames.map(async o=>{const a=await l.cfnService.getStackStatus(o,void 0,c);return{stackName:o,status:a.success?a.data?.status??null:null}}));for(const{stackName:o,status:a}of u){if(!(a!==null&&y.has(a))){const t=await v(i.aws.getAccountId(),i.aws.getRegion(),o,i.journal);if(t===null||t.suspects.length===0)continue}r.probedStacks.push(o);const d=await $(i,{stackName:o,abortSignal:c});if(d.success)for(const t of d.data.findings)t.verdict==="confirmed-deleted"?r.confirmed.push(t):(t.verdict==="unconfirmed"||t.verdict==="stack-missing")&&r.unconfirmed.push(t)}return g(r)}function I(i,l=[],n={}){const c=i.map(e=>` ${e.stackName} \u203A ${e.logicalId} (${e.resourceType})${e.physicalId!==void 0?` [${e.physicalId}]`:""} \u2014 ${e.verdictDetail}`),r=l.map(e=>`Remediation ${e} is in progress \u2014 run 'fjall drift resume ${e}' to continue it.`),u=(n.flipNotLive??[]).map(({finding:e,opId:s})=>`Remediation ${s} recorded its Retain flip on ${e.stackName} \u203A ${e.logicalId}, but the flip is no longer live on the stack \u2014 re-run 'fjall drift repair' to re-verify before deploying.`),o=(n.flipUnverified??[]).map(({finding:e,opId:s,message:h})=>`Remediation ${s} should complete with this deploy, but ${e.stackName} \u203A ${e.logicalId} could not be verified: the live template could not be read to confirm the Retain flip is still live \u2014 ${h}. Retry the deploy.`),a=(n.stillDeclared??[]).map(({finding:e,opId:s})=>`Remediation ${s} is ready to converge, but ${e.stackName} \u203A ${e.logicalId} is still declared in your fjall config \u2014 remove the resource from the config, then deploy to complete the forget.`),f=[...r,...u,...o,...a],d=new Set((n.flipUnverified??[]).map(({finding:e})=>`${e.stackName}/${e.logicalId}`)),t=i.length>0&&i.every(e=>d.has(`${e.stackName}/${e.logicalId}`)),m=t?`Deploy blocked: the Retain-flip state of ${i.length} mid-remediation resource(s) could not be verified (the live template could not be read):
2
+ `:`Deploy blocked: CloudFormation still tracks ${i.length} resource(s) deleted outside CloudFormation \u2014 deploying would fail and re-wedge the stack:
3
+ `,p=t?"Retry the deploy \u2014 verification is transient when the live template read fails; if it keeps failing, check CloudFormation access to the stack template.":"Run 'fjall drift detect' for the full report, then 'fjall drift repair --remediate <physicalName>=<verb>' to remediate confirmed deletions.";return m+`${c.join(`
4
4
  `)}
5
5
  ${p}${f.length>0?`
6
6
  ${f.join(`
@@ -62,7 +62,7 @@ export { applyRetainFlip, composeFlipCapabilities, computeTemplateDigest, derive
62
62
  export { assessForgetResumability, completeForgetAfterConverge, completeForgetAfterDeploy, formatRecreateInFlightCures, partitionPreFlightByRemediation, runForgetSurgery, synthTemplateDeclaresResource, type CompleteForgetAfterDeployParams, type CompleteForgetOutcome, type CompleteForgetParams, type ForeignMarkerOperation, type ForgetOrchestrationOptions, type ForgetResumeAssessment, type ForgetResumeSource, type ForgetSurgeryParams, type ForgetSurgeryReport, type PreFlightRemediationDeps, type PreFlightRemediationPartition } from "./remediation/forgetResource.js";
63
63
  export { runRecreateSurgery, snapshotPolicyForType, SNAPSHOT_CAPABLE_RESOURCE_TYPES, type RecreateOrchestrationOptions, type RecreateSurgeryParams, type RecreateSurgeryReport } from "./remediation/recreateResource.js";
64
64
  export { runRecreatePreFlight, type RecreatePreFlightStack, type RunRecreatePreFlightParams } from "./remediation/recreatePreFlight.js";
65
- export { deriveRemovalTemplate, findRemovalDeletionFailures, proveRemovalTargetsOnly, type ProveRemovalTargetsOnlyParams, type RemovalDeletionFailure, type RemovalDerivation, type RemovalProofResult, type StrippedExport } from "./remediation/removalUpdate.js";
65
+ export { deriveRemovalTemplate, findRemovalDeletionFailures, findResourcesPresent, proveRemovalTargetsOnly, type CoRemovedResource, type ProveRemovalTargetsOnlyParams, type RemovalDeletionFailure, type RemovalDerivation, type RemovalProofResult, type StrippedExport } from "./remediation/removalUpdate.js";
66
66
  export { advertisableDriftVerbs, buildDriftRepairTicket, runDriftRepairGate, type DriftRepairGateOutcome, type RunDriftRepairGateParams } from "./remediation/driftRepairGate.js";
67
67
  export { formatPinOutcomeDetail, pinProbeCoverage, renderPinReportLines, runPinRemediation, type PinnedProperty, type PinProbeCoverage, type PinRemediationReport, type PinReportOnlyReason, type PinResourceOutcome, type PinTarget, type RunPinRemediationParams } from "./remediation/pinRemediation.js";
68
68
  export { composePostFailureRepairOffer, formatDriftRepairOffer, DriftRepairAvailableError, type ComposePostFailureRepairOfferParams } from "./remediation/postFailureRepairOffer.js";
@@ -1 +1 @@
1
- import{deploy as t}from"./deploy.js";import{destroy as i}from"./destroy.js";import{restart as n,probeSecretsDrift as s}from"./restart/restartApplication.js";import{computeSecretsDrift as p,deriveStaleParameters as c,extractConsumedSsmParameters as m,ssmParameterNameFromValueFrom as u}from"./restart/secretsDrift.js";import{deployOrganisation as d}from"./organisation/organisationDeploy.js";import{destroyOrganisation as T}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as x,emptyS3Bucket as P,preEmptyStackBuckets as A,formatQuarantineSuspectedMessage as E,formatRetainedBucketsMessage as D,isQuarantineDetail as y,isRetainedBucketsDetail as _,PRE_EMPTY_TAG_KEYS as g,isCleanableState as O,SAFE_CLEANUP_STATES as F}from"./stackCleanup.js";import{partitionAccounts as k,buildRegionList as B,buildAccountRegionPairs as b,cascadeHomeRegion as C,cascadeOperationKey as L}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as I,projectAccountRows as h}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as G,mergeReconciledProviderAccounts as K}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as Y,reconcileTrailMigration as V,ORG_TRAIL_BUCKET_OUTPUT_KEY as J,TRAIL_BUCKET_OUTPUT_KEY as H,TRAIL_KEY_ARN_OUTPUT_KEY as Q}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as j}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as q}from"./unlock/unlockBucket.js";import{unlockQueue as X}from"./unlock/unlockQueue.js";import{triageBucketPolicy as $,isEnforceSslStatement as ee}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as te}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as ie,synthesiseEnforceSslDocument as ae,ensureEnforceSsl as ne}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as le}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as ce,flattenAccountsToEnvironments as me,extractAllAccountNames as ue,accountsConfigToOUTree as fe,isStringArray as de,isAccountsConfig as Re,isOuOnlyAccountBucket as Te,OU_ONLY_ACCOUNT_BUCKETS as Se}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as Pe,resolveSecretRefValue as Ae,resolveBuildSecretSessionProvider as Ee,sourcedRefsFromBuildArgs as De}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as _e,partitionForRegion as ge}from"./application/buildSecretSession.js";import{buildDeploySessionPolicy as Fe}from"./application/deploySessionPolicy.js";import{resolveBuildArgs as ke}from"./application/buildArgResolver.js";import{validateBuildGroup as be}from"./application/buildGroupValidator.js";import{runOpenNextBuild as Le}from"./application/openNextBuild.js";import{runOrganisationSetup as Ie,ORG_SETUP_PHASES as he}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as Ke,computeDeployPlan as Me,computeAssemblyDigest as Ye,digestHead as Ve,classifyImpact as Je,derivePropertyChanges as He,isDataLoss as Qe,isStatefulResourceType as we,renderPlanSummary as je,renderPlanLines as We,toWirePlanChanges as qe,signApprovalToken as ze,verifyApprovalToken as Xe,DEFAULT_APPROVAL_TTL_MS as Ze,APPROVAL_TOKEN_PATTERN as $e}from"./application/plan/index.js";import{runApprovalGate as rr}from"./application/approvalGate.js";import{runDestructionGate as or,buildDestructionTicket as ir,evaluateDestructionConsents as ar,computeTicketDigest as nr,legalRemediationVerbs as sr,executableRemediationVerbs as lr,advertisableRemediationVerbs as pr,renderDestructionTicketLines as cr,renderConsentVerdictLines as mr}from"./application/destructionGate.js";import{loadDeployPlan as fr,createMemoisedPlanLoader as dr}from"./application/plan/loadDeployPlan.js";import{classifyDriftFailure as Tr,fetchLastOperationEvents as Sr}from"./drift/classifyDriftFailure.js";import{DriftProbe as Pr}from"./drift/driftProbe.js";import{clearDriftSuspects as Er,defaultDriftJournalDir as Dr,readDriftSuspects as yr,recordDriftSuspects as _r}from"./drift/driftSuspectJournal.js";import{detectStackDrift as Or}from"./drift/detectStackDrift.js";import{runDriftPreFlight as vr,formatDriftPreFlightBlock as kr,DRIFT_PREFLIGHT_TRIGGER_STATUSES as Br,DRIFT_PREFLIGHT_BUDGET_MS as br}from"./drift/driftPreFlight.js";import{archiveCompletedRemediationJournal as Lr,computeRemediationOpId as Ur,defaultRemediationJournalDir as Ir,findRemediationJournalByOpId as hr,listActiveRemediationOps as Nr,listActiveRemediationOpsForContext as Gr,readRemediationJournal as Kr,sweepExpiredRemediationJournals as Mr,writeRemediationJournal as Yr,REMEDIATION_JOURNAL_RETENTION_DAYS as Vr}from"./remediation/remediationJournal.js";import{captureRemediationForensics as Hr,defaultRemediationForensicsDir as Qr}from"./remediation/forensicsCapture.js";import{applyRetainFlip as jr,composeFlipCapabilities as Wr,computeTemplateDigest as qr,deriveRetainFlipTemplate as zr,findMarkedResourcesByOpId as Xr,proveFlipMetadataOnly as Zr,readRetainFlipState as $r,verifyRetainFlipDiff as et,FORGET_MARKER_KEY as rt,FORGET_FLIP_SPEC as tt,RECREATE_MARKER_KEY as ot}from"./remediation/retainFlip.js";import{assessForgetResumability as at,completeForgetAfterConverge as nt,completeForgetAfterDeploy as st,formatRecreateInFlightCures as lt,partitionPreFlightByRemediation as pt,runForgetSurgery as ct,synthTemplateDeclaresResource as mt}from"./remediation/forgetResource.js";import{runRecreateSurgery as ft,snapshotPolicyForType as dt,SNAPSHOT_CAPABLE_RESOURCE_TYPES as Rt}from"./remediation/recreateResource.js";import{runRecreatePreFlight as St}from"./remediation/recreatePreFlight.js";import{deriveRemovalTemplate as Pt,findRemovalDeletionFailures as At,proveRemovalTargetsOnly as Et}from"./remediation/removalUpdate.js";import{advertisableDriftVerbs as yt,buildDriftRepairTicket as _t,runDriftRepairGate as gt}from"./remediation/driftRepairGate.js";import{formatPinOutcomeDetail as Ft,pinProbeCoverage as vt,renderPinReportLines as kt,runPinRemediation as Bt}from"./remediation/pinRemediation.js";import{composePostFailureRepairOffer as Ct,formatDriftRepairOffer as Lt,DriftRepairAvailableError as Ut}from"./remediation/postFailureRepairOffer.js";import{checkDeployPathStackAvailability as ht,isDeployPathBlockingStatus as Nt,StackUnavailableError as Gt}from"./activeDeploymentGuard.js";import{assertLeaseBeforeStack as Mt,LeaseDeniedError as Yt}from"./application/leaseGate.js";export{$e as APPROVAL_TOKEN_PATTERN,Ze as DEFAULT_APPROVAL_TTL_MS,br as DRIFT_PREFLIGHT_BUDGET_MS,Br as DRIFT_PREFLIGHT_TRIGGER_STATUSES,Pr as DriftProbe,Ut as DriftRepairAvailableError,tt as FORGET_FLIP_SPEC,rt as FORGET_MARKER_KEY,Yt as LeaseDeniedError,he as ORG_SETUP_PHASES,J as ORG_TRAIL_BUCKET_OUTPUT_KEY,Se as OU_ONLY_ACCOUNT_BUCKETS,g as PRE_EMPTY_TAG_KEYS,ot as RECREATE_MARKER_KEY,Vr as REMEDIATION_JOURNAL_RETENTION_DAYS,F as SAFE_CLEANUP_STATES,Rt as SNAPSHOT_CAPABLE_RESOURCE_TYPES,Gt as StackUnavailableError,H as TRAIL_BUCKET_OUTPUT_KEY,Q as TRAIL_KEY_ARN_OUTPUT_KEY,fe as accountsConfigToOUTree,yt as advertisableDriftVerbs,pr as advertisableRemediationVerbs,jr as applyRetainFlip,Lr as archiveCompletedRemediationJournal,Mt as assertLeaseBeforeStack,at as assessForgetResumability,b as buildAccountRegionPairs,_e as buildBuildSecretSessionPolicy,Ke as buildDeployPlan,Fe as buildDeploySessionPolicy,ir as buildDestructionTicket,_t as buildDriftRepairTicket,B as buildRegionList,Hr as captureRemediationForensics,C as cascadeHomeRegion,L as cascadeOperationKey,ht as checkDeployPathStackAvailability,Tr as classifyDriftFailure,Je as classifyImpact,x as cleanupFailedStack,Er as clearDriftSuspects,nt as completeForgetAfterConverge,st as completeForgetAfterDeploy,Wr as composeFlipCapabilities,Ct as composePostFailureRepairOffer,Ye as computeAssemblyDigest,Me as computeDeployPlan,Ur as computeRemediationOpId,p as computeSecretsDrift,qr as computeTemplateDigest,nr as computeTicketDigest,dr as createMemoisedPlanLoader,Y as decideNextTransition,j as decommissionMemberTrailStorage,Dr as defaultDriftJournalDir,Qr as defaultRemediationForensicsDir,Ir as defaultRemediationJournalDir,t as deploy,d as deployOrganisation,He as derivePropertyChanges,Pt as deriveRemovalTemplate,zr as deriveRetainFlipTemplate,c as deriveStaleParameters,i as destroy,T as destroyOrganisation,Or as detectStackDrift,Ve as digestHead,P as emptyS3Bucket,ne as ensureEnforceSsl,ar as evaluateDestructionConsents,lr as executableRemediationVerbs,ue as extractAllAccountNames,m as extractConsumedSsmParameters,Sr as fetchLastOperationEvents,Xr as findMarkedResourcesByOpId,hr as findRemediationJournalByOpId,At as findRemovalDeletionFailures,me as flattenAccountsToEnvironments,kr as formatDriftPreFlightBlock,Lt as formatDriftRepairOffer,Ft as formatPinOutcomeDetail,E as formatQuarantineSuspectedMessage,lt as formatRecreateInFlightCures,D as formatRetainedBucketsMessage,Re as isAccountsConfig,O as isCleanableState,Qe as isDataLoss,Nt as isDeployPathBlockingStatus,ee as isEnforceSslStatement,Te as isOuOnlyAccountBucket,y as isQuarantineDetail,_ as isRetainedBucketsDetail,we as isStatefulResourceType,de as isStringArray,sr as legalRemediationVerbs,Nr as listActiveRemediationOps,Gr as listActiveRemediationOpsForContext,fr as loadDeployPlan,K as mergeReconciledProviderAccounts,ce as parseAccountsConfiguration,k as partitionAccounts,ge as partitionForRegion,pt as partitionPreFlightByRemediation,vt as pinProbeCoverage,A as preEmptyStackBuckets,s as probeSecretsDrift,h as projectAccountRows,I as projectScalarSummary,Zr as proveFlipMetadataOnly,Et as proveRemovalTargetsOnly,yr as readDriftSuspects,Kr as readRemediationJournal,$r as readRetainFlipState,G as reconcileProviderAccounts,V as reconcileTrailMigration,_r as recordDriftSuspects,mr as renderConsentVerdictLines,cr as renderDestructionTicketLines,kt as renderPinReportLines,We as renderPlanLines,je as renderPlanSummary,ke as resolveBuildArgs,Ee as resolveBuildSecretSessionProvider,Pe as resolveBuildSecrets,Ae as resolveSecretRefValue,n as restart,le as restoreAndReconcileQuarantinedBucket,ie as restoreBucketPolicy,rr as runApprovalGate,or as runDestructionGate,vr as runDriftPreFlight,gt as runDriftRepairGate,ct as runForgetSurgery,Le as runOpenNextBuild,Ie as runOrganisationSetup,Bt as runPinRemediation,St as runRecreatePreFlight,ft as runRecreateSurgery,ze as signApprovalToken,dt as snapshotPolicyForType,De as sourcedRefsFromBuildArgs,u as ssmParameterNameFromValueFrom,Mr as sweepExpiredRemediationJournals,mt as synthTemplateDeclaresResource,ae as synthesiseEnforceSslDocument,te as toResourcePolicyStatements,qe as toWirePlanChanges,$ as triageBucketPolicy,q as unlockBucket,X as unlockQueue,be as validateBuildGroup,Xe as verifyApprovalToken,et as verifyRetainFlipDiff,Yr as writeRemediationJournal};
1
+ import{deploy as t}from"./deploy.js";import{destroy as i}from"./destroy.js";import{restart as n,probeSecretsDrift as s}from"./restart/restartApplication.js";import{computeSecretsDrift as p,deriveStaleParameters as c,extractConsumedSsmParameters as m,ssmParameterNameFromValueFrom as u}from"./restart/secretsDrift.js";import{deployOrganisation as d}from"./organisation/organisationDeploy.js";import{destroyOrganisation as T}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as x,emptyS3Bucket as P,preEmptyStackBuckets as A,formatQuarantineSuspectedMessage as E,formatRetainedBucketsMessage as D,isQuarantineDetail as y,isRetainedBucketsDetail as _,PRE_EMPTY_TAG_KEYS as g,isCleanableState as O,SAFE_CLEANUP_STATES as F}from"./stackCleanup.js";import{partitionAccounts as k,buildRegionList as B,buildAccountRegionPairs as b,cascadeHomeRegion as C,cascadeOperationKey as L}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as I,projectAccountRows as h}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as G,mergeReconciledProviderAccounts as K}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as Y,reconcileTrailMigration as V,ORG_TRAIL_BUCKET_OUTPUT_KEY as J,TRAIL_BUCKET_OUTPUT_KEY as H,TRAIL_KEY_ARN_OUTPUT_KEY as Q}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as j}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as q}from"./unlock/unlockBucket.js";import{unlockQueue as X}from"./unlock/unlockQueue.js";import{triageBucketPolicy as $,isEnforceSslStatement as ee}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as te}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as ie,synthesiseEnforceSslDocument as ae,ensureEnforceSsl as ne}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as le}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as ce,flattenAccountsToEnvironments as me,extractAllAccountNames as ue,accountsConfigToOUTree as fe,isStringArray as de,isAccountsConfig as Re,isOuOnlyAccountBucket as Te,OU_ONLY_ACCOUNT_BUCKETS as Se}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as Pe,resolveSecretRefValue as Ae,resolveBuildSecretSessionProvider as Ee,sourcedRefsFromBuildArgs as De}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as _e,partitionForRegion as ge}from"./application/buildSecretSession.js";import{buildDeploySessionPolicy as Fe}from"./application/deploySessionPolicy.js";import{resolveBuildArgs as ke}from"./application/buildArgResolver.js";import{validateBuildGroup as be}from"./application/buildGroupValidator.js";import{runOpenNextBuild as Le}from"./application/openNextBuild.js";import{runOrganisationSetup as Ie,ORG_SETUP_PHASES as he}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as Ke,computeDeployPlan as Me,computeAssemblyDigest as Ye,digestHead as Ve,classifyImpact as Je,derivePropertyChanges as He,isDataLoss as Qe,isStatefulResourceType as we,renderPlanSummary as je,renderPlanLines as We,toWirePlanChanges as qe,signApprovalToken as ze,verifyApprovalToken as Xe,DEFAULT_APPROVAL_TTL_MS as Ze,APPROVAL_TOKEN_PATTERN as $e}from"./application/plan/index.js";import{runApprovalGate as rr}from"./application/approvalGate.js";import{runDestructionGate as or,buildDestructionTicket as ir,evaluateDestructionConsents as ar,computeTicketDigest as nr,legalRemediationVerbs as sr,executableRemediationVerbs as lr,advertisableRemediationVerbs as pr,renderDestructionTicketLines as cr,renderConsentVerdictLines as mr}from"./application/destructionGate.js";import{loadDeployPlan as fr,createMemoisedPlanLoader as dr}from"./application/plan/loadDeployPlan.js";import{classifyDriftFailure as Tr,fetchLastOperationEvents as Sr}from"./drift/classifyDriftFailure.js";import{DriftProbe as Pr}from"./drift/driftProbe.js";import{clearDriftSuspects as Er,defaultDriftJournalDir as Dr,readDriftSuspects as yr,recordDriftSuspects as _r}from"./drift/driftSuspectJournal.js";import{detectStackDrift as Or}from"./drift/detectStackDrift.js";import{runDriftPreFlight as vr,formatDriftPreFlightBlock as kr,DRIFT_PREFLIGHT_TRIGGER_STATUSES as Br,DRIFT_PREFLIGHT_BUDGET_MS as br}from"./drift/driftPreFlight.js";import{archiveCompletedRemediationJournal as Lr,computeRemediationOpId as Ur,defaultRemediationJournalDir as Ir,findRemediationJournalByOpId as hr,listActiveRemediationOps as Nr,listActiveRemediationOpsForContext as Gr,readRemediationJournal as Kr,sweepExpiredRemediationJournals as Mr,writeRemediationJournal as Yr,REMEDIATION_JOURNAL_RETENTION_DAYS as Vr}from"./remediation/remediationJournal.js";import{captureRemediationForensics as Hr,defaultRemediationForensicsDir as Qr}from"./remediation/forensicsCapture.js";import{applyRetainFlip as jr,composeFlipCapabilities as Wr,computeTemplateDigest as qr,deriveRetainFlipTemplate as zr,findMarkedResourcesByOpId as Xr,proveFlipMetadataOnly as Zr,readRetainFlipState as $r,verifyRetainFlipDiff as et,FORGET_MARKER_KEY as rt,FORGET_FLIP_SPEC as tt,RECREATE_MARKER_KEY as ot}from"./remediation/retainFlip.js";import{assessForgetResumability as at,completeForgetAfterConverge as nt,completeForgetAfterDeploy as st,formatRecreateInFlightCures as lt,partitionPreFlightByRemediation as pt,runForgetSurgery as ct,synthTemplateDeclaresResource as mt}from"./remediation/forgetResource.js";import{runRecreateSurgery as ft,snapshotPolicyForType as dt,SNAPSHOT_CAPABLE_RESOURCE_TYPES as Rt}from"./remediation/recreateResource.js";import{runRecreatePreFlight as St}from"./remediation/recreatePreFlight.js";import{deriveRemovalTemplate as Pt,findRemovalDeletionFailures as At,findResourcesPresent as Et,proveRemovalTargetsOnly as Dt}from"./remediation/removalUpdate.js";import{advertisableDriftVerbs as _t,buildDriftRepairTicket as gt,runDriftRepairGate as Ot}from"./remediation/driftRepairGate.js";import{formatPinOutcomeDetail as vt,pinProbeCoverage as kt,renderPinReportLines as Bt,runPinRemediation as bt}from"./remediation/pinRemediation.js";import{composePostFailureRepairOffer as Lt,formatDriftRepairOffer as Ut,DriftRepairAvailableError as It}from"./remediation/postFailureRepairOffer.js";import{checkDeployPathStackAvailability as Nt,isDeployPathBlockingStatus as Gt,StackUnavailableError as Kt}from"./activeDeploymentGuard.js";import{assertLeaseBeforeStack as Yt,LeaseDeniedError as Vt}from"./application/leaseGate.js";export{$e as APPROVAL_TOKEN_PATTERN,Ze as DEFAULT_APPROVAL_TTL_MS,br as DRIFT_PREFLIGHT_BUDGET_MS,Br as DRIFT_PREFLIGHT_TRIGGER_STATUSES,Pr as DriftProbe,It as DriftRepairAvailableError,tt as FORGET_FLIP_SPEC,rt as FORGET_MARKER_KEY,Vt as LeaseDeniedError,he as ORG_SETUP_PHASES,J as ORG_TRAIL_BUCKET_OUTPUT_KEY,Se as OU_ONLY_ACCOUNT_BUCKETS,g as PRE_EMPTY_TAG_KEYS,ot as RECREATE_MARKER_KEY,Vr as REMEDIATION_JOURNAL_RETENTION_DAYS,F as SAFE_CLEANUP_STATES,Rt as SNAPSHOT_CAPABLE_RESOURCE_TYPES,Kt as StackUnavailableError,H as TRAIL_BUCKET_OUTPUT_KEY,Q as TRAIL_KEY_ARN_OUTPUT_KEY,fe as accountsConfigToOUTree,_t as advertisableDriftVerbs,pr as advertisableRemediationVerbs,jr as applyRetainFlip,Lr as archiveCompletedRemediationJournal,Yt as assertLeaseBeforeStack,at as assessForgetResumability,b as buildAccountRegionPairs,_e as buildBuildSecretSessionPolicy,Ke as buildDeployPlan,Fe as buildDeploySessionPolicy,ir as buildDestructionTicket,gt as buildDriftRepairTicket,B as buildRegionList,Hr as captureRemediationForensics,C as cascadeHomeRegion,L as cascadeOperationKey,Nt as checkDeployPathStackAvailability,Tr as classifyDriftFailure,Je as classifyImpact,x as cleanupFailedStack,Er as clearDriftSuspects,nt as completeForgetAfterConverge,st as completeForgetAfterDeploy,Wr as composeFlipCapabilities,Lt as composePostFailureRepairOffer,Ye as computeAssemblyDigest,Me as computeDeployPlan,Ur as computeRemediationOpId,p as computeSecretsDrift,qr as computeTemplateDigest,nr as computeTicketDigest,dr as createMemoisedPlanLoader,Y as decideNextTransition,j as decommissionMemberTrailStorage,Dr as defaultDriftJournalDir,Qr as defaultRemediationForensicsDir,Ir as defaultRemediationJournalDir,t as deploy,d as deployOrganisation,He as derivePropertyChanges,Pt as deriveRemovalTemplate,zr as deriveRetainFlipTemplate,c as deriveStaleParameters,i as destroy,T as destroyOrganisation,Or as detectStackDrift,Ve as digestHead,P as emptyS3Bucket,ne as ensureEnforceSsl,ar as evaluateDestructionConsents,lr as executableRemediationVerbs,ue as extractAllAccountNames,m as extractConsumedSsmParameters,Sr as fetchLastOperationEvents,Xr as findMarkedResourcesByOpId,hr as findRemediationJournalByOpId,At as findRemovalDeletionFailures,Et as findResourcesPresent,me as flattenAccountsToEnvironments,kr as formatDriftPreFlightBlock,Ut as formatDriftRepairOffer,vt as formatPinOutcomeDetail,E as formatQuarantineSuspectedMessage,lt as formatRecreateInFlightCures,D as formatRetainedBucketsMessage,Re as isAccountsConfig,O as isCleanableState,Qe as isDataLoss,Gt as isDeployPathBlockingStatus,ee as isEnforceSslStatement,Te as isOuOnlyAccountBucket,y as isQuarantineDetail,_ as isRetainedBucketsDetail,we as isStatefulResourceType,de as isStringArray,sr as legalRemediationVerbs,Nr as listActiveRemediationOps,Gr as listActiveRemediationOpsForContext,fr as loadDeployPlan,K as mergeReconciledProviderAccounts,ce as parseAccountsConfiguration,k as partitionAccounts,ge as partitionForRegion,pt as partitionPreFlightByRemediation,kt as pinProbeCoverage,A as preEmptyStackBuckets,s as probeSecretsDrift,h as projectAccountRows,I as projectScalarSummary,Zr as proveFlipMetadataOnly,Dt as proveRemovalTargetsOnly,yr as readDriftSuspects,Kr as readRemediationJournal,$r as readRetainFlipState,G as reconcileProviderAccounts,V as reconcileTrailMigration,_r as recordDriftSuspects,mr as renderConsentVerdictLines,cr as renderDestructionTicketLines,Bt as renderPinReportLines,We as renderPlanLines,je as renderPlanSummary,ke as resolveBuildArgs,Ee as resolveBuildSecretSessionProvider,Pe as resolveBuildSecrets,Ae as resolveSecretRefValue,n as restart,le as restoreAndReconcileQuarantinedBucket,ie as restoreBucketPolicy,rr as runApprovalGate,or as runDestructionGate,vr as runDriftPreFlight,Ot as runDriftRepairGate,ct as runForgetSurgery,Le as runOpenNextBuild,Ie as runOrganisationSetup,bt as runPinRemediation,St as runRecreatePreFlight,ft as runRecreateSurgery,ze as signApprovalToken,dt as snapshotPolicyForType,De as sourcedRefsFromBuildArgs,u as ssmParameterNameFromValueFrom,Mr as sweepExpiredRemediationJournals,mt as synthTemplateDeclaresResource,ae as synthesiseEnforceSslDocument,te as toResourcePolicyStatements,qe as toWirePlanChanges,$ as triageBucketPolicy,q as unlockBucket,X as unlockQueue,be as validateBuildGroup,Xe as verifyApprovalToken,et as verifyRetainFlipDiff,Yr as writeRemediationJournal};
@@ -1 +1 @@
1
- import{join as b}from"path";import{success as j,failure as C}from"@fjall/generator";import{logger as R}from"@fjall/util/logger";import{maskSensitiveOutput as l,mapSettledWithConcurrency as K}from"@fjall/util";import{ORGANISATION_TYPES as S,getOrganisationStackName as H}from"../../types/operations.js";import{CdkContextBuilder as z}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as Y}from"../../types/deployment/index.js";import{CloudFormationService as M}from"../../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as W,regionSuffix as X}from"../../types/config/FjallState.js";import{BackupClient as q}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as J,describeBackupVaultExists as Q}from"../../aws/organisations/backup.js";import{buildParamsContext as Z,collectStackOutputs as T,assumeCascadeRole as k,forwardOutput as E,reconcileOidcProviderIfCarried as V}from"../contextHelpers.js";import{assertLeaseBeforeStack as ee}from"../application/leaseGate.js";import{runDriftPreFlightGate as te}from"../drift/preFlightGate.js";import{accountTier as G}from"@fjall/util";import{DEFAULT_REGION as U}from"../../aws/utils/regions.js";const oe=4;function we(e){const a=e.find(t=>G(t)==="platform"),c=e.filter(t=>G(t)==="account");return{platformAccount:a,memberAccounts:c}}function re(e){const a=e?.primaryRegion??U,c=e?.secondaryRegions??[],t=new Set([a,...c]),i=e?.disasterRecoveryRegion;return i&&t.add(i),[...t]}function Pe(e){return re(e)[0]??U}function ne(e,a){return`${e} (${a})`}function $e(e,a){const c=[];for(const t of a)for(const i of e)c.push({account:i,region:t});return c}import{buildCascadeRoleArn as Ee}from"../contextHelpers.js";async function Ae(e,a,c,t){c.onLog?.(`Verifying cascade role access for ${a.length} account(s)\u2026`,"info");const i=await K(a,oe,async s=>k(e.awsProvider,s.id,s.region??e.awsProvider.getRegion(),`fjall-preflight-${s.name}`,t)),r=[];return i.forEach((s,o)=>{const n=a[o];if(n){if(s.status==="rejected"){r.push({accountId:n.id,accountName:n.name,error:s.reason instanceof Error?s.reason.message:String(s.reason)});return}s.value.success||r.push({accountId:n.id,accountName:n.name,error:s.value.error.message})}}),r.length===0&&c.onLog?.(`Cascade role access verified for ${a.length} account(s)`,"info"),r}async function Oe(e,a,c,t,i,r,s){const o=s?.region??t.region??a.awsProvider.getRegion(),n=ne(t.name,o),u=s?.orgConfig??e.orgConfig,g=s?.ipamPoolId;r.onCascadeAccountStart?.(n,t.id,o,i);const f=await k(a.awsProvider,t.id,o,`fjall-cascade-${t.name}`,e.abortSignal);if(!f.success)return r.onCascadeAccountComplete?.(n,!1,l(f.error.message),o),C(new Error(`Failed to assume role for ${t.name}: ${l(f.error.message)}`));const{provider:m,credentials:y}=f.data,P=b(e.workingDirectory,"fjall",i==="platform"?S.PLATFORM:S.ACCOUNT),$=b(P,`cdk.out.${t.id}.${X(o)}`),h=z.buildDeploymentContext({deployType:i,target:c.target,path:P,assemblyDir:$,environment:t.environment??void 0,region:o,accountName:t.name,callerIdentity:Y(t.id),ipamPoolId:g,...Z({orgConfig:u,identity:e.identity,skipOidc:e.options?.skipOidc,region:o,primaryRegion:s?.primaryRegion,trailLifecycle:t.trailLifecycle})},{verbose:e.options?.verbose},u);if(J(t.environment,u?.disasterRecoveryRegion)){const p=await Q(m.getClient(q),e.abortSignal);if(!p.success)return r.onCascadeAccountComplete?.(n,!1,l(p.error.message),o),C(new Error(`Backup vault probe failed for ${t.name}: ${l(p.error.message)}`));h.fjallAdoptBackupVault=p.data}r.onCascadeAccountPhaseChange?.(n,"synth",o);const A=await a.cdkService.runCdkSynth(h,E(r),y);if(!A.success)return r.onCascadeAccountComplete?.(n,!1,l(`Synth failed: ${A.error}`),o),C(new Error(`Synth failed for ${t.name}: ${l(A.error)}`));const d=H(i==="platform"?S.PLATFORM:S.ACCOUNT),I=W(P,t.id,o),w=new M(m),{changed:_,currentHash:F}=await a.hashService.compareCascadeStack($,d,I);if(!_&&e.options?.force!==!0&&await w.stackExists(d)){const p=await w.getStackOutputs(d);p.success||R.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:d,account:t.name});const B=T(p);return await V(h,m,r,e.abortSignal),r.onLog?.(`${t.name}: no infrastructure changes \u2014 skipping deploy`,"info"),r.onCascadeAccountComplete?.(n,!0,void 0,o,B,!0),j({outputs:B,skipped:!0})}const O=await te(m,{stackNames:[d],cdkOutPath:$,cfnService:w,callbacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(O.kind==="blocked")return r.onCascadeAccountComplete?.(n,!1,O.message,o),C(new Error(O.message));const v=await ee(e.assertLeaseForStack,d,e.abortSignal);if(v!==void 0)return r.onCascadeAccountComplete?.(n,!1,v.message,o),C(v);r.onCascadeAccountPhaseChange?.(n,"bootstrap",o);const x=await a.cdkService.runCdkBootstrap(h,E(r),y);if(!x.success)return r.onCascadeAccountComplete?.(n,!1,l(`Bootstrap failed: ${x.error}`),o),C(new Error(`Bootstrap failed for ${t.name}: ${l(x.error)}`));r.onCascadeAccountPhaseChange?.(n,"deploy",o);const D=await a.cdkService.runCdkDeploy(h,d,E(r),p=>r.onCascadeAccountResourceProgress?.(n,p,o),m,y);if(!D.success)return r.onCascadeAccountComplete?.(n,!1,l(D.error),o),C(new Error(l(D.error)));const N=await w.getStackOutputs(d);N.success||R.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:d,account:t.name});const L=T(N);return await V(h,m,r,e.abortSignal),F!==void 0&&((await a.hashService.persistCascadeStack(I,d,F)).success||R.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:d,account:t.name})),r.onCascadeAccountComplete?.(n,!0,void 0,o,L,!1),j({outputs:L,skipped:!1})}async function ve(e,a,c,t){const i=new Map,r=e.awsProvider.getRegion(),s=await k(e.awsProvider,a.id,r,`fjall-ipam-read-${a.name}`,t);if(!s.success)return R.debug("organisationDeploy",`Cannot read Platform outputs: ${s.error.message}`),i;const o=new M(s.data.provider),n=H(S.PLATFORM),u=await o.getStackOutputs(n);if(!u.success)return R.debug("organisationDeploy",`Failed to read Platform stack outputs: ${u.error.message}`),i;const g=/^IpamPoolId(\d{12})(\w+)$/;for(const f of u.data){const m=f.OutputKey?.match(g);if(m&&f.OutputValue){const y=`${m[1]}-${m[2]}`;i.set(y,f.OutputValue)}}return i.size>0&&c.onLog?.(`Read ${i.size} IPAM pool ID(s) from Platform stack`,"info"),i}async function xe(e,a){const c=e.getDomains();if(c.length===0)return{domainsDeployed:0,errors:[]};a.onCascadePhaseStart?.("domains");const t=c.filter(o=>o.type==="apex"),i=c.filter(o=>o.type==="delegated");let r=0;const s=[];for(const o of t){const n=await e.deployDomain(o.name,a);n.success?r++:s.push(`${o.name}: ${n.error.message}`)}if(i.length>0){const o=await Promise.allSettled(i.map(n=>e.deployDomain(n.name,a)));for(let n=0;n<o.length;n++){const u=o[n],g=i[n];if(!(!u||!g))if(u.status==="fulfilled")u.value.success?r++:s.push(`${g.name}: ${u.value.error.message}`);else{const f=u.reason instanceof Error?u.reason.message:String(u.reason);s.push(`${g.name}: ${f}`)}}}return a.onCascadePhaseComplete?.("domains"),{domainsDeployed:r,errors:s}}export{oe as CASCADE_MAX_CONCURRENCY,$e as buildAccountRegionPairs,Ee as buildCascadeRoleArn,re as buildRegionList,Pe as cascadeHomeRegion,ne as cascadeOperationKey,Oe as deployCascadeAccount,xe as deployDomains,we as partitionAccounts,Ae as probeCascadeRoles,ve as readPlatformIpamPoolIds};
1
+ import{join as B}from"path";import{success as j,failure as C}from"@fjall/generator";import{logger as y}from"@fjall/util/logger";import{maskSensitiveOutput as p,mapSettledWithConcurrency as K}from"@fjall/util";import{ORGANISATION_TYPES as R,getOrganisationStackName as H}from"../../types/operations.js";import{CdkContextBuilder as z}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as Y}from"../../types/deployment/index.js";import{CloudFormationService as M}from"../../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as W,regionSuffix as X}from"../../types/config/FjallState.js";import{BackupClient as q}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as J,describeBackupVaultExists as Q}from"../../aws/organisations/backup.js";import{buildParamsContext as Z,collectStackOutputs as T,assumeCascadeRole as k,forwardOutput as E,reconcileOidcProviderIfCarried as V}from"../contextHelpers.js";import{assertLeaseBeforeStack as ee}from"../application/leaseGate.js";import{runDriftPreFlightGate as te}from"../drift/preFlightGate.js";import{accountTier as G}from"@fjall/util";import{DEFAULT_REGION as U}from"../../aws/utils/regions.js";const oe=4;function we(e){const a=e.find(t=>G(t)==="platform"),c=e.filter(t=>G(t)==="account");return{platformAccount:a,memberAccounts:c}}function re(e){const a=e?.primaryRegion??U,c=e?.secondaryRegions??[],t=new Set([a,...c]),i=e?.disasterRecoveryRegion;return i&&t.add(i),[...t]}function Pe(e){return re(e)[0]??U}function ne(e,a){return`${e} (${a})`}function $e(e,a){const c=[];for(const t of a)for(const i of e)c.push({account:i,region:t});return c}import{buildCascadeRoleArn as Ee}from"../contextHelpers.js";async function Ae(e,a,c,t){c.onLog?.(`Verifying cascade role access for ${a.length} account(s)\u2026`,"info");const i=await K(a,oe,async s=>k(e.awsProvider,s.id,s.region??e.awsProvider.getRegion(),`fjall-preflight-${s.name}`,t)),r=[];return i.forEach((s,o)=>{const n=a[o];if(n){if(s.status==="rejected"){r.push({accountId:n.id,accountName:n.name,error:s.reason instanceof Error?s.reason.message:String(s.reason)});return}s.value.success||r.push({accountId:n.id,accountName:n.name,error:s.value.error.message})}}),r.length===0&&c.onLog?.(`Cascade role access verified for ${a.length} account(s)`,"info"),r}async function Oe(e,a,c,t,i,r,s){const o=s?.region??t.region??a.awsProvider.getRegion(),n=ne(t.name,o),u=s?.orgConfig??e.orgConfig,g=s?.ipamPoolId;r.onCascadeAccountStart?.(n,t.id,o,i);const f=await k(a.awsProvider,t.id,o,`fjall-cascade-${t.name}`,e.abortSignal);if(!f.success)return r.onCascadeAccountComplete?.(n,!1,p(f.error.message),o),C(new Error(`Failed to assume role for ${t.name}: ${p(f.error.message)}`));const{provider:l,credentials:S}=f.data,P=B(e.workingDirectory,"fjall",i==="platform"?R.PLATFORM:R.ACCOUNT),$=B(P,`cdk.out.${t.id}.${X(o)}`),h=z.buildDeploymentContext({deployType:i,target:c.target,path:P,assemblyDir:$,environment:t.environment??void 0,region:o,accountName:t.name,callerIdentity:Y(t.id),ipamPoolId:g,...Z({orgConfig:u,identity:e.identity,skipOidc:e.options?.skipOidc,region:o,primaryRegion:s?.primaryRegion,trailLifecycle:t.trailLifecycle})},{verbose:e.options?.verbose},u);if(J(t.environment,u?.disasterRecoveryRegion)){const m=await Q(l.getClient(q),e.abortSignal);if(!m.success)return r.onCascadeAccountComplete?.(n,!1,p(m.error.message),o),C(new Error(`Backup vault probe failed for ${t.name}: ${p(m.error.message)}`));h.fjallAdoptBackupVault=m.data}r.onCascadeAccountPhaseChange?.(n,"synth",o);const A=await a.cdkService.runCdkSynth(h,E(r),S);if(!A.success)return r.onCascadeAccountComplete?.(n,!1,p(`Synth failed: ${A.error}`),o),C(new Error(`Synth failed for ${t.name}: ${p(A.error)}`));const d=H(i==="platform"?R.PLATFORM:R.ACCOUNT),I=W(P,t.id,o),w=new M(l),{changed:_,currentHash:F}=await a.hashService.compareCascadeStack($,d,I);if(!_&&e.options?.force!==!0&&await w.stackExists(d,void 0,e.abortSignal)){const m=await w.getStackOutputs(d,void 0,e.abortSignal);m.success||y.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:d,account:t.name});const b=T(m);return await V(h,l,r,e.abortSignal),r.onLog?.(`${t.name}: no infrastructure changes \u2014 skipping deploy`,"info"),r.onCascadeAccountComplete?.(n,!0,void 0,o,b,!0),j({outputs:b,skipped:!0})}const O=await te(l,{stackNames:[d],cdkOutPath:$,cfnService:w,callbacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(O.kind==="blocked")return r.onCascadeAccountComplete?.(n,!1,O.message,o),C(new Error(O.message));const v=await ee(e.assertLeaseForStack,d,e.abortSignal);if(v!==void 0)return r.onCascadeAccountComplete?.(n,!1,v.message,o),C(v);r.onCascadeAccountPhaseChange?.(n,"bootstrap",o);const x=await a.cdkService.runCdkBootstrap(h,E(r),S);if(!x.success)return r.onCascadeAccountComplete?.(n,!1,p(`Bootstrap failed: ${x.error}`),o),C(new Error(`Bootstrap failed for ${t.name}: ${p(x.error)}`));r.onCascadeAccountPhaseChange?.(n,"deploy",o);const D=await a.cdkService.runCdkDeploy(h,d,E(r),m=>r.onCascadeAccountResourceProgress?.(n,m,o),l,S);if(!D.success)return r.onCascadeAccountComplete?.(n,!1,p(D.error),o),C(new Error(p(D.error)));const N=await w.getStackOutputs(d,void 0,e.abortSignal);N.success||y.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:d,account:t.name});const L=T(N);return await V(h,l,r,e.abortSignal),F!==void 0&&((await a.hashService.persistCascadeStack(I,d,F)).success||y.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:d,account:t.name})),r.onCascadeAccountComplete?.(n,!0,void 0,o,L,!1),j({outputs:L,skipped:!1})}async function ve(e,a,c,t){const i=new Map,r=e.awsProvider.getRegion(),s=await k(e.awsProvider,a.id,r,`fjall-ipam-read-${a.name}`,t);if(!s.success)return y.debug("organisationDeploy",`Cannot read Platform outputs: ${s.error.message}`),i;const o=new M(s.data.provider),n=H(R.PLATFORM),u=await o.getStackOutputs(n);if(!u.success)return y.debug("organisationDeploy",`Failed to read Platform stack outputs: ${u.error.message}`),i;const g=/^IpamPoolId(\d{12})(\w+)$/;for(const f of u.data){const l=f.OutputKey?.match(g);if(l&&f.OutputValue){const S=`${l[1]}-${l[2]}`;i.set(S,f.OutputValue)}}return i.size>0&&c.onLog?.(`Read ${i.size} IPAM pool ID(s) from Platform stack`,"info"),i}async function xe(e,a){const c=e.getDomains();if(c.length===0)return{domainsDeployed:0,errors:[]};a.onCascadePhaseStart?.("domains");const t=c.filter(o=>o.type==="apex"),i=c.filter(o=>o.type==="delegated");let r=0;const s=[];for(const o of t){const n=await e.deployDomain(o.name,a);n.success?r++:s.push(`${o.name}: ${n.error.message}`)}if(i.length>0){const o=await Promise.allSettled(i.map(n=>e.deployDomain(n.name,a)));for(let n=0;n<o.length;n++){const u=o[n],g=i[n];if(!(!u||!g))if(u.status==="fulfilled")u.value.success?r++:s.push(`${g.name}: ${u.value.error.message}`);else{const f=u.reason instanceof Error?u.reason.message:String(u.reason);s.push(`${g.name}: ${f}`)}}}return a.onCascadePhaseComplete?.("domains"),{domainsDeployed:r,errors:s}}export{oe as CASCADE_MAX_CONCURRENCY,$e as buildAccountRegionPairs,Ee as buildCascadeRoleArn,re as buildRegionList,Pe as cascadeHomeRegion,ne as cascadeOperationKey,Oe as deployCascadeAccount,xe as deployDomains,we as partitionAccounts,Ae as probeCascadeRoles,ve as readPlatformIpamPoolIds};
@@ -44,4 +44,4 @@ export declare function failPrepareStep(callbacks: DeployCallbacks): void;
44
44
  /** Mask a failure message, surface it via onError, and return the failure. */
45
45
  export declare function maskAndFail(callbacks: DeployCallbacks, message: string): Result<never>;
46
46
  /** Best-effort stack-output read: log at debug on failure, never fail. */
47
- export declare function readStackOutputsBestEffort(services: DeployServices, callbacks: DeployCallbacks, stackName: string, failureLogMessage: string): Promise<Record<string, string> | undefined>;
47
+ export declare function readStackOutputsBestEffort(services: DeployServices, callbacks: DeployCallbacks, stackName: string, failureLogMessage: string, abortSignal?: AbortSignal): Promise<Record<string, string> | undefined>;
@@ -1 +1 @@
1
- import{failure as S}from"@fjall/generator";import{collectStackOutputs as C}from"../contextHelpers.js";import{maskSensitiveOutput as P}from"@fjall/util";import{INFRA_STEP_NAME as n,STEP_IDS as e,STEP_NAMES as o}from"../../types/stepDefinitions.js";const A={CONNECT:{id:e.CONNECT,name:n.CONNECT},PREPARE:{id:e.PREPARE_ENVIRONMENT,name:n.PREPARE},DEPLOY:{id:e.DEPLOY,name:n.DEPLOY},MONITORING:{id:e.MONITORING,name:n.MONITORING},ORG_DEPLOY:{id:e.ORG_DEPLOY,name:o.ORG_DEPLOY},CASCADE_PLATFORM:{id:e.CASCADE_PLATFORM,name:o.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:e.CASCADE_ACCOUNTS,name:o.CASCADE_ACCOUNTS}},a=4;function m(t,r){t.onStackTargets===void 0||r.length===0||t.onStackTargets(r)}function c(t){t.onStepComplete?.(A.PREPARE.id,A.PREPARE.name,"error",1,a)}function p(t,r){const E=new Error(P(r));return t.onError?.(E),S(E)}async function s(t,r,E,i){const O=await t.cfnService.getStackOutputs(E);return O.success||r.onLog?.(i,"debug"),C(O)}export{A as INFRA_STEPS,a as INFRA_STEP_TOTAL,m as emitOrgStackTargets,c as failPrepareStep,p as maskAndFail,s as readStackOutputsBestEffort};
1
+ import{failure as C}from"@fjall/generator";import{collectStackOutputs as P}from"../contextHelpers.js";import{maskSensitiveOutput as a}from"@fjall/util";import{INFRA_STEP_NAME as E,STEP_IDS as e,STEP_NAMES as o}from"../../types/stepDefinitions.js";const A={CONNECT:{id:e.CONNECT,name:E.CONNECT},PREPARE:{id:e.PREPARE_ENVIRONMENT,name:E.PREPARE},DEPLOY:{id:e.DEPLOY,name:E.DEPLOY},MONITORING:{id:e.MONITORING,name:E.MONITORING},ORG_DEPLOY:{id:e.ORG_DEPLOY,name:o.ORG_DEPLOY},CASCADE_PLATFORM:{id:e.CASCADE_PLATFORM,name:o.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:e.CASCADE_ACCOUNTS,name:o.CASCADE_ACCOUNTS}},R=4;function c(t,r){t.onStackTargets===void 0||r.length===0||t.onStackTargets(r)}function p(t){t.onStepComplete?.(A.PREPARE.id,A.PREPARE.name,"error",1,R)}function s(t,r){const n=new Error(a(r));return t.onError?.(n),C(n)}async function f(t,r,n,i,S){const O=await t.cfnService.getStackOutputs(n,void 0,S);return O.success||r.onLog?.(i,"debug"),P(O)}export{A as INFRA_STEPS,R as INFRA_STEP_TOTAL,c as emitOrgStackTargets,p as failPrepareStep,s as maskAndFail,f as readStackOutputsBestEffort};
@@ -1,5 +1,5 @@
1
- import{join as _}from"node:path";import{OrganizationsClient as Q}from"@aws-sdk/client-organizations";import{success as V,failure as v}from"@fjall/generator";import{ORGANISATION_TYPES as A,getOrganisationStackName as R}from"../../types/operations.js";import{synthOrFail as X,bootstrapOrFail as Z,forwardOutput as tt,forwardResourceProgress as et}from"../contextHelpers.js";import{partitionAccounts as ot,probeCascadeRoles as at,buildRegionList as rt,buildAccountRegionPairs as nt}from"../organisation/cascadeHelpers.js";import{assertLeaseBeforeStack as ct}from"../application/leaseGate.js";import{accountTier as it,maskSensitiveOutput as g}from"@fjall/util";import{findDevelopmentOuId as st}from"../../aws/organisations/organisationalUnits.js";import{buildOrgContext as dt,resolveOrgDetails as ut}from"./orgContext.js";import{INFRA_STEPS as B,emitOrgStackTargets as lt,maskAndFail as O,readStackOutputsBestEffort as Y}from"./infraSteps.js";import{resolveCascadeAccounts as gt}from"./resolveCascadeAccounts.js";import{executeCascade as ft}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as pt}from"./trailReconciliation.js";import{runDriftPreFlightGate as mt}from"../drift/preFlightGate.js";async function Et(n,e,I,z){const{callbacks:t,options:D}=n,{providerAccounts:f,effectiveOrgConfig:K,defaultRegion:U}=await gt(n,e),p=await ut(e,n.abortSignal);if(!p.success)return O(t,p.error.message);const w=await st(e.awsProvider.getClient(Q),p.data.rootId,n.abortSignal);if(!w.success)return O(t,w.error.message);const q={...p.data,...w.data!==void 0?{devOuId:w.data}:{}},J=f.find(o=>it(o)==="organisation"),d=dt(n,e,I,"organisation",q,void 0,J?.trailLifecycle),l=D?.cascade!==!1,{platformAccount:s,memberAccounts:P}=ot(f),E=l&&s!==void 0?1:0,L=l&&P.length>0?1:0,c=2+E+L,T=l?[...s!==void 0?[s]:[],...P]:[];if(T.length>0){const o=await at(e,T,t,n.abortSignal),a=s!==void 0?o.find(r=>r.accountId===s.id):void 0;if(a!==void 0)return O(t,`Pre-flight cascade role check failed for the platform account ${a.accountName} (${a.accountId}): ${a.error}`);for(const r of o)t.onProgress?.({type:"warning",message:g(`Pre-flight cascade role check failed for ${r.accountName} (${r.accountId}) \u2014 its cascade deploy is expected to fail: ${r.error}`)})}t.onCascadeAccountsReconciled?.({hasPlatformAccount:E>0,hasMemberAccounts:L>0});const{id:C,name:y}=B.PREPARE;t.onStepStart?.(C,y,0,c),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const F=await X(e,d,t,"CDK synthesis failed");if(!F.success)return t.onStepComplete?.(C,y,"error",0,c),F;const x=e.awsProvider.getAccountId(),N=e.awsProvider.getRegion();lt(t,[{stackName:R(A.ORGANISATION),...x!==""?{accountId:x}:{},...N!==""?{region:N}:{}},...l&&s!==void 0?[{stackName:R(A.PLATFORM),accountId:s.id,region:s.region??N}]:[],...l?nt(P,rt(n.orgConfig)).map(({account:o,region:a})=>({stackName:R(A.ACCOUNT),accountId:o.id,region:a})):[]]);const G=await Z(e,d,t);if(!G.success)return t.onStepComplete?.(C,y,"error",0,c),G;t.onStepComplete?.(C,y,"completed",0,c);const{id:m,name:h}=B.ORG_DEPLOY,i=R(A.ORGANISATION);let H=!0;const S=await e.hashService.getTemplateHashes(_(d.path,"cdk.out"));if(S.success){const o=await e.hashService.compareWithState(S.data,d.path);o.success?H=o.data.stackChanges.get(i)??!0:t.onLog?.(g(`Org root change detection failed \u2014 deploying to be safe: ${o.error.message}`),"warn")}else t.onLog?.(g(`Org root template hashing failed \u2014 deploying to be safe: ${S.error.message}`),"warn");const $=H||D?.force===!0||!await e.cfnService.stackExists(i);t.onOrgChangesDetected?.({hasOrgChanges:$});let k;if($){t.onStepStart?.(m,h,1,c);const o=await mt(e.awsProvider,{stackNames:[i],cdkOutPath:_(d.path,"cdk.out"),cfnService:e.cfnService,callbacks:t,...n.abortSignal!==void 0?{abortSignal:n.abortSignal}:{}});if(o.kind==="blocked")return t.onStepComplete?.(m,h,"error",1,c),O(t,o.message);const a=await ct(n.assertLeaseForStack,i,n.abortSignal);if(a!==void 0)return t.onStepComplete?.(m,h,"error",1,c),t.onError?.(a),v(a);const r=await e.cdkService.runCdkDeploy(d,i,tt(t),et(t),e.awsProvider);if(!r.success)return t.onStepComplete?.(m,h,"error",1,c),O(t,r.error);k=await Y(e,t,i,"Failed to read org stack outputs (non-critical)");const j=S.success?S.data.get(i):void 0;if(j!==void 0){const W=await e.hashService.updateStateAfterDeploy(d.path,new Map([[i,j]]));W.success||t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${g(W.error.message)}`,"warn")}t.onStepComplete?.(m,h,"completed",1,c)}else t.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),k=await Y(e,t,i,"Failed to read org stack outputs (non-critical)");const u=[],b=[];let M=$;if(l&&f.length>0){const{anyCascadeDeployHappened:o}=await ft(n,e,I,{providerAccounts:f,effectiveOrgConfig:K,totalSteps:c,cascadeErrors:u,allCascadeOutputs:b});if(o&&(M=!0),await pt(n,e,{orgOutputs:k,allCascadeOutputs:b,orgDetails:p.data,providerAccounts:f,defaultRegion:U}),u.length>0){const a=u.map(r=>` ${r.accountId}: ${r.error}`).join(`
1
+ import{join as _}from"node:path";import{OrganizationsClient as Q}from"@aws-sdk/client-organizations";import{success as V,failure as v}from"@fjall/generator";import{ORGANISATION_TYPES as A,getOrganisationStackName as R}from"../../types/operations.js";import{synthOrFail as X,bootstrapOrFail as Z,forwardOutput as tt,forwardResourceProgress as et}from"../contextHelpers.js";import{partitionAccounts as ot,probeCascadeRoles as at,buildRegionList as rt,buildAccountRegionPairs as nt}from"../organisation/cascadeHelpers.js";import{assertLeaseBeforeStack as it}from"../application/leaseGate.js";import{accountTier as ct,maskSensitiveOutput as g}from"@fjall/util";import{findDevelopmentOuId as st}from"../../aws/organisations/organisationalUnits.js";import{buildOrgContext as dt,resolveOrgDetails as ut}from"./orgContext.js";import{INFRA_STEPS as B,emitOrgStackTargets as lt,maskAndFail as O,readStackOutputsBestEffort as Y}from"./infraSteps.js";import{resolveCascadeAccounts as gt}from"./resolveCascadeAccounts.js";import{executeCascade as ft}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as pt}from"./trailReconciliation.js";import{runDriftPreFlightGate as mt}from"../drift/preFlightGate.js";async function Et(a,e,I,z){const{callbacks:t,options:D}=a,{providerAccounts:f,effectiveOrgConfig:K,defaultRegion:U}=await gt(a,e),p=await ut(e,a.abortSignal);if(!p.success)return O(t,p.error.message);const w=await st(e.awsProvider.getClient(Q),p.data.rootId,a.abortSignal);if(!w.success)return O(t,w.error.message);const q={...p.data,...w.data!==void 0?{devOuId:w.data}:{}},J=f.find(o=>ct(o)==="organisation"),d=dt(a,e,I,"organisation",q,void 0,J?.trailLifecycle),l=D?.cascade!==!1,{platformAccount:s,memberAccounts:P}=ot(f),E=l&&s!==void 0?1:0,L=l&&P.length>0?1:0,i=2+E+L,T=l?[...s!==void 0?[s]:[],...P]:[];if(T.length>0){const o=await at(e,T,t,a.abortSignal),r=s!==void 0?o.find(n=>n.accountId===s.id):void 0;if(r!==void 0)return O(t,`Pre-flight cascade role check failed for the platform account ${r.accountName} (${r.accountId}): ${r.error}`);for(const n of o)t.onProgress?.({type:"warning",message:g(`Pre-flight cascade role check failed for ${n.accountName} (${n.accountId}) \u2014 its cascade deploy is expected to fail: ${n.error}`)})}t.onCascadeAccountsReconciled?.({hasPlatformAccount:E>0,hasMemberAccounts:L>0});const{id:C,name:b}=B.PREPARE;t.onStepStart?.(C,b,0,i),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const F=await X(e,d,t,"CDK synthesis failed");if(!F.success)return t.onStepComplete?.(C,b,"error",0,i),F;const x=e.awsProvider.getAccountId(),N=e.awsProvider.getRegion();lt(t,[{stackName:R(A.ORGANISATION),...x!==""?{accountId:x}:{},...N!==""?{region:N}:{}},...l&&s!==void 0?[{stackName:R(A.PLATFORM),accountId:s.id,region:s.region??N}]:[],...l?nt(P,rt(a.orgConfig)).map(({account:o,region:r})=>({stackName:R(A.ACCOUNT),accountId:o.id,region:r})):[]]);const G=await Z(e,d,t);if(!G.success)return t.onStepComplete?.(C,b,"error",0,i),G;t.onStepComplete?.(C,b,"completed",0,i);const{id:m,name:S}=B.ORG_DEPLOY,c=R(A.ORGANISATION);let H=!0;const h=await e.hashService.getTemplateHashes(_(d.path,"cdk.out"));if(h.success){const o=await e.hashService.compareWithState(h.data,d.path);o.success?H=o.data.stackChanges.get(c)??!0:t.onLog?.(g(`Org root change detection failed \u2014 deploying to be safe: ${o.error.message}`),"warn")}else t.onLog?.(g(`Org root template hashing failed \u2014 deploying to be safe: ${h.error.message}`),"warn");const $=H||D?.force===!0||!await e.cfnService.stackExists(c,void 0,a.abortSignal);t.onOrgChangesDetected?.({hasOrgChanges:$});let y;if($){t.onStepStart?.(m,S,1,i);const o=await mt(e.awsProvider,{stackNames:[c],cdkOutPath:_(d.path,"cdk.out"),cfnService:e.cfnService,callbacks:t,...a.abortSignal!==void 0?{abortSignal:a.abortSignal}:{}});if(o.kind==="blocked")return t.onStepComplete?.(m,S,"error",1,i),O(t,o.message);const r=await it(a.assertLeaseForStack,c,a.abortSignal);if(r!==void 0)return t.onStepComplete?.(m,S,"error",1,i),t.onError?.(r),v(r);const n=await e.cdkService.runCdkDeploy(d,c,tt(t),et(t),e.awsProvider);if(!n.success)return t.onStepComplete?.(m,S,"error",1,i),O(t,n.error);y=await Y(e,t,c,"Failed to read org stack outputs (non-critical)",a.abortSignal);const j=h.success?h.data.get(c):void 0;if(j!==void 0){const W=await e.hashService.updateStateAfterDeploy(d.path,new Map([[c,j]]));W.success||t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${g(W.error.message)}`,"warn")}t.onStepComplete?.(m,S,"completed",1,i)}else t.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),y=await Y(e,t,c,"Failed to read org stack outputs (non-critical)",a.abortSignal);const u=[],k=[];let M=$;if(l&&f.length>0){const{anyCascadeDeployHappened:o}=await ft(a,e,I,{providerAccounts:f,effectiveOrgConfig:K,totalSteps:i,cascadeErrors:u,allCascadeOutputs:k});if(o&&(M=!0),await pt(a,e,{orgOutputs:y,allCascadeOutputs:k,orgDetails:p.data,providerAccounts:f,defaultRegion:U}),u.length>0){const r=u.map(n=>` ${n.accountId}: ${n.error}`).join(`
2
2
  `);t.onLog?.(g(`Cascade failed for ${u.length} target(s):
3
- ${a}`),"warn")}}if(u.length>0){const o=u.map(r=>g(`${r.accountId}: ${r.error}`)).join(`
4
- `),a=new Error(`Organisation root deployed, but the cascade failed for ${u.length} target(s):
5
- ${o}`);return t.onError?.(a),v(a)}return V({target:I.target,deploymentType:"organisation",outputs:k,artefacts:[],...b.length>0?{cascadeOutputs:b}:{},...M?{}:{noChanges:!0},durationMs:Date.now()-z})}export{Et as deployOrgWithCascade};
3
+ ${r}`),"warn")}}if(u.length>0){const o=u.map(n=>g(`${n.accountId}: ${n.error}`)).join(`
4
+ `),r=new Error(`Organisation root deployed, but the cascade failed for ${u.length} target(s):
5
+ ${o}`);return t.onError?.(r),v(r)}return V({target:I.target,deploymentType:"organisation",outputs:y,artefacts:[],...k.length>0?{cascadeOutputs:k}:{},...M?{}:{noChanges:!0},durationMs:Date.now()-z})}export{Et as deployOrgWithCascade};