@fjall/deploy-core 8.0.0 → 10.1.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 (34) hide show
  1. package/dist/.build-source-hash +31 -26
  2. package/dist/.minified +1 -1
  3. package/dist/src/index.d.ts +5 -2
  4. package/dist/src/index.js +1 -1
  5. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  6. package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
  7. package/dist/src/orchestration/application/deploySessionPolicy.js +1 -1
  8. package/dist/src/orchestration/application/ecsDeploymentTail.d.ts +48 -0
  9. package/dist/src/orchestration/application/ecsDeploymentTail.js +3 -0
  10. package/dist/src/orchestration/application/schemaGateImage.d.ts +32 -0
  11. package/dist/src/orchestration/application/schemaGateImage.js +1 -0
  12. package/dist/src/orchestration/deploy.js +1 -1
  13. package/dist/src/orchestration/failure/classifyFailure.d.ts +58 -0
  14. package/dist/src/orchestration/failure/classifyFailure.js +1 -0
  15. package/dist/src/orchestration/failure/deploymentFailureAttachment.d.ts +18 -0
  16. package/dist/src/orchestration/failure/deploymentFailureAttachment.js +1 -0
  17. package/dist/src/orchestration/index.d.ts +2 -2
  18. package/dist/src/orchestration/index.js +1 -1
  19. package/dist/src/orchestration/organisation/reconcileProviderAccounts.js +1 -1
  20. package/dist/src/orchestration/restart/restartApplication.d.ts +34 -0
  21. package/dist/src/orchestration/restart/restartApplication.js +1 -1
  22. package/dist/src/orchestration/stackCleanup/insightsLogGroups.d.ts +48 -0
  23. package/dist/src/orchestration/stackCleanup/insightsLogGroups.js +1 -0
  24. package/dist/src/orchestration/stackCleanup.d.ts +1 -0
  25. package/dist/src/orchestration/stackCleanup.js +1 -1
  26. package/dist/src/services/application/ApplicationStackService.js +1 -1
  27. package/dist/src/services/infrastructure/EcsService.d.ts +35 -0
  28. package/dist/src/services/infrastructure/EcsService.js +1 -1
  29. package/dist/src/types/callbackKeys.d.ts +1 -1
  30. package/dist/src/types/callbackKeys.js +1 -1
  31. package/dist/src/types/callbacks.d.ts +11 -0
  32. package/dist/src/types/config/FjallState.js +1 -1
  33. package/dist/src/types/events.d.ts +15 -0
  34. package/package.json +5 -4
@@ -0,0 +1 @@
1
+ var L=Object.defineProperty;var d=(e,t)=>L(e,"name",{value:t,configurable:!0});import{createHash as N}from"node:crypto";import{ECRClient as O,BatchCheckLayerAvailabilityCommand as G,CompleteLayerUploadCommand as D,CreateRepositoryCommand as H,DescribeImagesCommand as j,InitiateLayerUploadCommand as U,PutImageCommand as F,UploadLayerPartCommand as Y}from"@aws-sdk/client-ecr";import{success as S,failure as E}from"@fjall/generator";import{getErrorMessage as w,maskSensitiveOutput as p}from"@fjall/util";import{logger as _}from"@fjall/util/logger";import{parseSchemaGateImageTagsFromManifest as z}from"@fjall/util/manifest";import{SCHEMA_GATE_ECR_REPO_NAME as m,SCHEMA_GATE_PUBLIC_IMAGE_REPO as h}from"@fjall/util/migration";import{composeSdkAbortSignal as l,extractErrorName as $,isAborted as b}from"../../aws/organisations/types.js";const R="SchemaGateImage",k=h.indexOf("/"),g=h.slice(0,k),T=h.slice(k+1),s='The deploy was refused before any stack changed. Fix the cause and re-run, or set schemaGate: { image: "<uri>" } to bring your own gate image, or schemaGate: { materialise: false } to disable the gate container.',J=["application/vnd.oci.image.index.v1+json","application/vnd.oci.image.manifest.v1+json","application/vnd.docker.distribution.manifest.list.v2+json","application/vnd.docker.distribution.manifest.v2+json"].join(", "),V=new Set(["application/vnd.oci.image.index.v1+json","application/vnd.docker.distribution.manifest.list.v2+json"]),W=10*1024*1024,P=12e4;class r extends Error{static{d(this,"SchemaGateImageError")}}function y(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}d(y,"isRecord");async function le(e){const t=z(e.cdkOutPath);if(t.outcome==="manifest-unreadable")return E(new r(`Cannot determine the assembly's schema-gate image tags: ${p(t.reason)}. Re-run the build to regenerate the assembly, or remove the corrupt cdk.out and redeploy.`));const o=t.tags;if(o.length===0)return S(void 0);const a=e.awsProvider.getClient(O),n=e.fetchFn??fetch;try{for(const i of o){if(b(e.abortSignal))throw new r(`Aborted before ensuring schema-gate image '${m}:${i}'`);if(await X(a,i,e.abortSignal)){_.debug(R,"Schema-gate image already present",{tag:i});continue}e.onLog?.(`Schema-gate image ${m}:${i} is not in this account's ECR \u2014 mirroring it from ${h} (one-off per engine version)`,"info"),await Z(a,e.abortSignal),await q(a,n,i,e.abortSignal),e.onLog?.(`Mirrored schema-gate image ${m}:${i}`,"info")}return S(void 0)}catch(i){return i instanceof r?E(i):E(new Error(`Could not ensure the schema-gate image in this account's ECR: ${p(w(i))}. ${s}`))}}d(le,"ensureSchemaGateImage");async function X(e,t,o){try{return await e.send(new j({repositoryName:m,imageIds:[{imageTag:t}]}),{abortSignal:l(o)}),!0}catch(a){const n=$(a);if(n==="ImageNotFoundException"||n==="RepositoryNotFoundException")return!1;throw new r(`Could not check for schema-gate image '${m}:${t}': ${p(w(a))}. ${s}`)}}d(X,"isTagPresent");async function Z(e,t){try{await e.send(new H({repositoryName:m,imageScanningConfiguration:{scanOnPush:!0},imageTagMutability:"IMMUTABLE"}),{abortSignal:l(t)}),_.debug(R,"Created schema-gate repository",{repositoryName:m})}catch(o){if($(o)!=="RepositoryAlreadyExistsException")throw new r(`Could not create schema-gate repository '${m}': ${p(w(o))}. ${s}`)}}d(Z,"ensureGateRepository");async function q(e,t,o,a){const n=await K(t,a),i=await v(t,n,o,a);if(V.has(i.mediaType))for(const c of ee(i.parsed,o)){if(b(a))throw new r(`Aborted while mirroring schema-gate image '${m}:${o}'`);const f=await v(t,n,c,a);await x(e,t,n,f.parsed,o,a),await M(e,f.raw,f.mediaType,{imageDigest:c},a)}else await x(e,t,n,i.parsed,o,a);await M(e,i.raw,i.mediaType,{imageTag:o},a)}d(q,"mirrorTag");async function K(e,t){const o=`https://${g}/token/?service=${g}&scope=repository:${T}:pull`,a=await e(o,{signal:l(t)});if(!a.ok)throw new r(`Could not obtain an anonymous pull token from ${g}: HTTP ${a.status}. ${s}`);const n=await a.json();if(!y(n)||typeof n.token!="string"||n.token==="")throw new r(`The ${g} token endpoint returned no usable token. ${s}`);return n.token}d(K,"fetchPublicPullToken");async function v(e,t,o,a){const n=await e(`https://${g}/v2/${T}/manifests/${o}`,{headers:{Authorization:`Bearer ${t}`,Accept:J},signal:l(a)});if(!n.ok)throw new r(`Could not fetch manifest '${o}' from ${h}: HTTP ${n.status}. ${s}`);const i=await n.text();o.startsWith("sha256:")&&B(Buffer.from(i),o,`manifest ${o}`);let c;try{c=JSON.parse(i)}catch{throw new r(`Manifest '${o}' from ${h} is not valid JSON. ${s}`)}if(!y(c))throw new r(`Manifest '${o}' from ${h} has an unexpected shape. ${s}`);const f=n.headers.get("content-type"),A=f!==null&&f!==""?f:typeof c.mediaType=="string"?c.mediaType:"";return{raw:i,mediaType:A,parsed:c}}d(v,"fetchPublicManifest");const Q=/^sha256:[0-9a-f]{64}$/;function C(e,t){if(!Q.test(e))throw new r(`A descriptor for '${h}:${t}' carries a malformed digest. ${s}`);return e}d(C,"assertDigestShape");function ee(e,t){if(!Array.isArray(e.manifests))throw new r(`The index manifest for '${h}:${t}' carries no child manifests. ${s}`);const o=[];for(const a of e.manifests){if(!y(a)||typeof a.digest!="string")throw new r(`The index manifest for '${h}:${t}' has a malformed child descriptor. ${s}`);o.push(C(a.digest,t))}return o}d(ee,"collectIndexChildDigests");function te(e,t){const o=[],a=e.config;if(!y(a)||typeof a.digest!="string")throw new r(`An image manifest for '${h}:${t}' has no config digest. ${s}`);if(o.push(C(a.digest,t)),!Array.isArray(e.layers))throw new r(`An image manifest for '${h}:${t}' has no layer list. ${s}`);for(const n of e.layers){if(!y(n)||typeof n.digest!="string")throw new r(`An image manifest for '${h}:${t}' has a malformed layer descriptor. ${s}`);o.push(C(n.digest,t))}return o}d(te,"collectBlobDigests");async function x(e,t,o,a,n,i){const c=te(a,n),f=await e.send(new G({repositoryName:m,layerDigests:c}),{abortSignal:l(i)}),A=new Set((f.layers??[]).filter(u=>u.layerAvailability==="AVAILABLE").map(u=>u.layerDigest));for(const u of c){if(b(i))throw new r(`Aborted while mirroring schema-gate image '${m}:${n}'`);if(A.has(u))continue;const I=await oe(t,o,u,i);B(I,u,`blob ${u}`),await ae(e,u,I,i)}}d(x,"mirrorImageBlobs");async function oe(e,t,o,a){const n=await e(`https://${g}/v2/${T}/blobs/${o}`,{headers:{Authorization:`Bearer ${t}`},signal:l(a,P)});if(!n.ok)throw new r(`Could not download blob ${o} from ${h}: HTTP ${n.status}. ${s}`);return new Uint8Array(await n.arrayBuffer())}d(oe,"fetchPublicBlob");function B(e,t,o){const a=`sha256:${N("sha256").update(e).digest("hex")}`;if(a!==t)throw new r(`Integrity check failed for ${o} from ${h}: downloaded content hashes to ${a}. Refusing to push it. ${s}`)}d(B,"assertDigestMatches");async function ae(e,t,o,a){const i=(await e.send(new U({repositoryName:m}),{abortSignal:l(a)})).uploadId;if(i===void 0)throw new r(`ECR returned no upload id for blob ${t}. ${s}`);let c=0;for(;c<o.length;){if(b(a))throw new r(`Aborted while uploading blob ${t} to '${m}'`);const f=Math.min(c+W,o.length);await e.send(new Y({repositoryName:m,uploadId:i,partFirstByte:c,partLastByte:f-1,layerPartBlob:o.subarray(c,f)}),{abortSignal:l(a,P)}),c=f}try{await e.send(new D({repositoryName:m,uploadId:i,layerDigests:[t]}),{abortSignal:l(a)})}catch(f){if($(f)!=="LayerAlreadyExistsException")throw new r(`Could not complete the upload of blob ${t}: ${p(w(f))}. ${s}`)}}d(ae,"uploadBlob");async function M(e,t,o,a,n){try{await e.send(new F({repositoryName:m,imageManifest:t,...o!==""?{imageManifestMediaType:o}:{},...a}),{abortSignal:l(n)})}catch(i){if($(i)!=="ImageAlreadyExistsException")throw new r(`Could not push a schema-gate manifest to '${m}': ${p(w(i))}. ${s}`)}}d(M,"putImage");export{le as ensureSchemaGateImage};
@@ -1 +1 @@
1
- var w=Object.defineProperty;var l=(e,o)=>w(e,"name",{value:o,configurable:!0});import{success as f,failure as b}from"@fjall/generator";import{isApplicationOperation as g}from"../types/operations.js";import{isAborted as A}from"../aws/organisations/types.js";import{checkRegionEnabled as k,regionDisabledMessage as m}from"../aws/regions/regionOptStatus.js";import{createDeployServices as D}from"./serviceFactory.js";import{resolveOperation as v}from"./resolveOperation.js";import{deployApplication as C}from"./application/applicationDeploy.js";import{deployOrganisation as E}from"./organisation/organisationDeploy.js";async function B(e){const o=Date.now(),c=await k(e.awsCredentials.region,e.awsCredentials,e.abortSignal);if(c==="disabled"){const i=new Error(m(e.awsCredentials.region));return e.callbacks.onError?.(i),b(i)}c==="enabling"&&e.callbacks.onProgress?.({type:"warning",message:`Region ${e.awsCredentials.region} is still activating (opt-in status ENABLING); the deploy may fail until activation completes.`});let a;const p=e.callbacks.onFailureAnalysis,s={...e,callbacks:{...e.callbacks,onFailureAnalysis:l(i=>{a=i,p?.(i)},"onFailureAnalysis")}},d=D(s),n=await v(e.target,e.workingDirectory);if(!n.success)return e.callbacks.onError?.(n.error),n;const r=n.data,y=g(r)?"application":"organisation",t=g(r)?await C(s,d,r):await E(s,d,r);if(A(e.abortSignal))return f({target:e.target,deploymentType:y,cancelled:!0,artefacts:[],durationMs:Date.now()-o});if(!t.success)return t;const u=a!==void 0&&t.data.failureAnalysis===void 0?{...t.data,failureAnalysis:a}:t.data;return f({...u,...u.durationMs===void 0?{durationMs:Date.now()-o}:{}})}l(B,"deploy");export{B as deploy};
1
+ var C=Object.defineProperty;var l=(e,r)=>C(e,"name",{value:r,configurable:!0});import{success as p,failure as g}from"@fjall/generator";import{isApplicationOperation as y}from"../types/operations.js";import{isAborted as k}from"../aws/organisations/types.js";import{checkRegionEnabled as A,regionDisabledMessage as D}from"../aws/regions/regionOptStatus.js";import{createDeployServices as E}from"./serviceFactory.js";import{resolveOperation as F}from"./resolveOperation.js";import{classifyDeployFailure as S}from"./failure/classifyFailure.js";import{attachDeploymentFailure as O}from"./failure/deploymentFailureAttachment.js";import{deployApplication as h}from"./application/applicationDeploy.js";import{deployOrganisation as M}from"./organisation/organisationDeploy.js";async function $(e){const r=Date.now(),u=await A(e.awsCredentials.region,e.awsCredentials,e.abortSignal);if(u==="disabled"){const t=new Error(D(e.awsCredentials.region));return e.callbacks.onError?.(t),g(t)}u==="enabling"&&e.callbacks.onProgress?.({type:"warning",message:`Region ${e.awsCredentials.region} is still activating (opt-in status ENABLING); the deploy may fail until activation completes.`});let i,s;const b=e.callbacks.onFailureAnalysis,m=e.callbacks.onECSComplete,c={...e,callbacks:{...e.callbacks,onFailureAnalysis:l(t=>{i=t,b?.(t)},"onFailureAnalysis"),onECSComplete:l(t=>{t.success||(s=t),m?.(t)},"onECSComplete")}},d=E(c),n=await F(e.target,e.workingDirectory);if(!n.success)return e.callbacks.onError?.(n.error),n;const a=n.data,w=y(a)?"application":"organisation",o=y(a)?await h(c,d,a):await M(c,d,a);if(k(e.abortSignal))return p({target:e.target,deploymentType:w,cancelled:!0,artefacts:[],durationMs:Date.now()-r});if(!o.success){const t=S({error:o.error,...i!==void 0&&{analysis:i},...s!==void 0&&{ecsFailure:s}});return e.callbacks.onDeploymentFailure?.(t),g(O(o.error,t))}const f=i!==void 0&&o.data.failureAnalysis===void 0?{...o.data,failureAnalysis:i}:o.data;return p({...f,...f.durationMs===void 0?{durationMs:Date.now()-r}:{}})}l($,"deploy");export{$ as deploy};
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Deterministic terminal-failure classifier — maps the structured sources the
3
+ * engine already holds at the moment of failure (CFN failure analysis, ECS
4
+ * stopped-task forensics, the raw terminal error) onto the canonical
5
+ * `DeploymentFailure` record. Design:
6
+ * aiDocs/designs/2026-08-10-deployment-failure-taxonomy.md § 4.
7
+ *
8
+ * Classification happens HERE, where `category`/`stopCode`/`rolloutState`
9
+ * still exist as fields — never by regex over prose at render time (the
10
+ * design's explicitly rejected alternative). Everything dynamic is masked
11
+ * before it is bounded (mask-then-truncate).
12
+ */
13
+ import { type DeploymentFailure, type DeploymentFailurePhase } from "@fjall/util";
14
+ import { type FailureAnalysis } from "@fjall/util/aws";
15
+ import type { ECSCompleteEvent } from "../../types/events.js";
16
+ /**
17
+ * Bump when a mapping changes meaning — stored on every record so persisted
18
+ * rows can be re-classified offline.
19
+ */
20
+ export declare const DEPLOYMENT_FAILURE_CLASSIFIER_VERSION = "1";
21
+ /**
22
+ * Classify a failed CloudFormation stack operation from the analyser's
23
+ * structured output. The analysis nests verbatim (wire form) so no detail the
24
+ * monitor computed is lost.
25
+ */
26
+ export declare function classifyFromFailureAnalysis(analysis: FailureAnalysis, stackName?: string): DeploymentFailure;
27
+ /**
28
+ * Classify a failed ECS stabilisation from the tail's structured completion
29
+ * event. Returns undefined for a successful event.
30
+ */
31
+ export declare function classifyFromEcsCompletion(event: ECSCompleteEvent): DeploymentFailure | undefined;
32
+ export interface ClassifyUnclassifiedOptions {
33
+ /** The pipeline phase, when the caller knows where the failure occurred. */
34
+ phase?: Exclude<DeploymentFailurePhase, "unknown">;
35
+ /** Raw process/log output for excerpt extraction, when richer than the message. */
36
+ output?: string;
37
+ }
38
+ /**
39
+ * The taxonomy's first-class fallback: nothing structured survived, so the
40
+ * record is bounded and located rather than classified — bare `unclassified`
41
+ * (phase unknown) or `unclassified.<phase>`, always carrying a masked excerpt
42
+ * window when the source outgrows `detail`.
43
+ */
44
+ export declare function classifyUnclassified(message: string, options?: ClassifyUnclassifiedOptions): DeploymentFailure;
45
+ export interface ClassifyDeployFailureInput {
46
+ /** The terminal Result error. */
47
+ error: Error;
48
+ /** The last FailureAnalysis the CFN monitor emitted, when a stack failed. */
49
+ analysis?: FailureAnalysis;
50
+ /** The last failed ECS completion event, when the tail observed one. */
51
+ ecsFailure?: ECSCompleteEvent;
52
+ }
53
+ /**
54
+ * Classify a terminal deploy failure from the richest structured source
55
+ * available: CFN analysis first, then ECS forensics, then the unclassified
56
+ * fallback over the error message.
57
+ */
58
+ export declare function classifyDeployFailure(input: ClassifyDeployFailureInput): DeploymentFailure;
@@ -0,0 +1 @@
1
+ var h=Object.defineProperty;var n=(e,s)=>h(e,"name",{value:s,configurable:!0});import{DEPLOYMENT_FAILURE_LIMITS as i,DEPLOYMENT_FAILURE_SCHEMA_VERSION as u,extractErrorWindow as _,maskSensitiveOutput as k,toDeploymentFailureAnalysis as E}from"@fjall/util";const f="1",p="Deployment failed",C={"cfn.permissions":"AWS permissions blocked a resource change","cfn.validation":"AWS rejected a resource configuration","cfn.dependency":"A resource dependency was missing","cfn.limit":"An AWS service quota was exceeded","cfn.timeout":"A resource operation timed out","cfn.resource_create_failed":"A CloudFormation resource operation failed","ecs.circuit_breaker":"The new version failed to start and was rolled back","ecs.health_check_failed":"The new version failed its container health checks","ecs.task_start_failed":"Tasks from the new version kept stopping","ecs.stabilise_timeout":"The service did not stabilise in time"},T={permissions:"cfn.permissions",validation:"cfn.validation",dependency:"cfn.dependency",limit:"cfn.limit",network:"cfn.timeout",unknown:"cfn.resource_create_failed"},g={permissions:"customer_aws",dependency:"customer_aws",limit:"customer_aws",network:"customer_aws",validation:"indeterminate",unknown:"indeterminate"},F={"ecs.circuit_breaker":["Check the application logs for the stopped tasks \u2014 the new version could not start","The previously running version was left in place"],"ecs.health_check_failed":["Check the container health-check command and its start period","Confirm the application listens on its configured port"],"ecs.task_start_failed":["Check the application logs for the stopped tasks"],"ecs.stabilise_timeout":["Check the ECS service events for what kept the rollout from completing"]};function m(e){return C[e]??p}n(m,"failureTitle");function a(e,s,t){const c=k(e).trim(),o=c===""?t:c;return o.length>s?`${o.slice(0,s-1)}\u2026`:o}n(a,"boundText");function d(e,s){return e.length>s?e.slice(0,s):e}n(d,"boundIdentifier");function S(e,s){const t=e.rootCause.category,c=T[t],o=e.rootCause.resource,l={...o.logicalId!==""&&{logicalId:d(o.logicalId,i.resourceField)},...o.resourceType!==""&&{resourceType:d(o.resourceType,i.resourceField)},...s!==void 0&&s!==""&&{stackName:d(s,i.resourceField)}};return{schemaVersion:u,code:c,phase:"cloudformation",fault:g[t],title:m(c),detail:a(e.summary,i.detail,"A CloudFormation stack operation failed"),provider:{source:"cloudformation",message:a(e.rootCause.reason,i.providerMessage,"No status reason recorded")},...Object.keys(l).length>0&&{resource:l},remediation:e.remediation.filter(r=>r.trim()!=="").slice(0,i.remediationSteps).map(r=>r.length>i.remediationStep?`${r.slice(0,i.remediationStep-1)}\u2026`:r),analysis:E(e),classifierVersion:f}}n(S,"classifyFromFailureAnalysis");function y(e){if(e.success)return;let s,t;return e.stoppedReason!==void 0&&/health check/i.test(e.stoppedReason)?(s="ecs.health_check_failed",t="customer_code"):e.rolloutState==="FAILED"?(s="ecs.circuit_breaker",t="customer_code"):(e.stoppedTaskCount??0)>0?(s="ecs.task_start_failed",t="customer_code"):e.replaced===!0?(s="unclassified.ecs_stabilise",t="indeterminate"):(s="ecs.stabilise_timeout",t="indeterminate"),{schemaVersion:u,code:s,phase:"ecs_stabilise",fault:t,title:m(s),detail:a(e.reason??"",i.detail,`Service ${e.serviceName} did not reach a steady state`),provider:{source:"ecs",...e.stopCode!==void 0&&e.stopCode!==""&&{code:d(e.stopCode,i.providerCode)},message:a(e.stoppedReason??e.reason??"",i.providerMessage,"No stopped reason recorded")},remediation:[...F[s]??[]],classifierVersion:f}}n(y,"classifyFromEcsCompletion");function A(e,s={}){const t=s.phase??"unknown",c=s.phase===void 0?"unclassified":`unclassified.${s.phase}`,o=s.output??e,r=s.output!==void 0||o.length>i.detail?_(o):"";return{schemaVersion:u,code:c,phase:t,fault:"indeterminate",title:p,detail:a(e,i.detail,"The deployment failed"),...r!==""&&{excerpt:r},remediation:[],classifierVersion:f}}n(A,"classifyUnclassified");function L(e){if(e.analysis!==void 0)return S(e.analysis);if(e.ecsFailure!==void 0){const s=y(e.ecsFailure);if(s!==void 0)return s}return A(e.error.message)}n(L,"classifyDeployFailure");export{f as DEPLOYMENT_FAILURE_CLASSIFIER_VERSION,L as classifyDeployFailure,y as classifyFromEcsCompletion,S as classifyFromFailureAnalysis,A as classifyUnclassified};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Attach/read the classified `DeploymentFailure` on a terminal error WITHOUT
3
+ * wrapping it in a new class — consumers branch on error identity
4
+ * (`instanceof DriftRepairAvailableError` drives the CLI repair offer), so a
5
+ * wrapper would silently break them. This is the Result-side twin of the
6
+ * `onDeploymentFailure` callback.
7
+ */
8
+ import { type DeploymentFailure } from "@fjall/util";
9
+ /**
10
+ * Attach the record to the error, non-enumerably so serialisers and message
11
+ * formatters do not double-ship it. Returns the same error instance.
12
+ */
13
+ export declare function attachDeploymentFailure<E extends Error>(error: E, failure: DeploymentFailure): E;
14
+ /**
15
+ * Read a previously attached record; undefined when absent or malformed
16
+ * (validated via the schema, never cast).
17
+ */
18
+ export declare function getDeploymentFailure(error: unknown): DeploymentFailure | undefined;
@@ -0,0 +1 @@
1
+ var i=Object.defineProperty;var t=(e,n)=>i(e,"name",{value:n,configurable:!0});import{DeploymentFailureSchema as l}from"@fjall/util";const a="deploymentFailure";function d(e,n){return Object.defineProperty(e,a,{value:n,enumerable:!1,configurable:!0}),e}t(d,"attachDeploymentFailure");function f(e){if(typeof e!="object"||e===null||!(a in e))return;const n=e.deploymentFailure,u=l.safeParse(n);return u.success?u.data:void 0}t(f,"getDeploymentFailure");export{d as attachDeploymentFailure,f as getDeploymentFailure};
@@ -1,7 +1,7 @@
1
1
  export { deploy } from "./deploy.js";
2
2
  export { destroy } from "./destroy.js";
3
- export { restart, probeSecretsDrift } from "./restart/restartApplication.js";
4
- export type { RestartParams, RestartResult, SecretsDriftProbeParams } from "./restart/restartApplication.js";
3
+ export { restart, probeSecretsDrift, probeServiceDeploymentStatus } from "./restart/restartApplication.js";
4
+ export type { RestartParams, RestartResult, SecretsDriftProbeParams, ServiceDeploymentStatus, ApplicationServiceStatus } from "./restart/restartApplication.js";
5
5
  export { computeSecretsDrift, deriveStaleParameters, extractConsumedSsmParameters, ssmParameterNameFromValueFrom } from "./restart/secretsDrift.js";
6
6
  export type { ApplicationSecretsDrift, ServiceSecretsDrift, StaleParameter, SecretsDriftServices } from "./restart/secretsDrift.js";
7
7
  export { deployOrganisation } from "./organisation/organisationDeploy.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 l,deriveStaleParameters as p,extractConsumedSsmParameters as m,ssmParameterNameFromValueFrom as u}from"./restart/secretsDrift.js";import{deployOrganisation as d}from"./organisation/organisationDeploy.js";import{destroyOrganisation as S}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as P,emptyS3Bucket as T,preEmptyStackBuckets as A,formatQuarantineSuspectedMessage as y,formatRetainedBucketsMessage as E,isQuarantineDetail as D,isRetainedBucketsDetail as g,PRE_EMPTY_TAG_KEYS as _,isCleanableState as O,SAFE_CLEANUP_STATES as v}from"./stackCleanup.js";import{partitionAccounts as k,buildRegionList as B,buildAccountRegionPairs as L,cascadeHomeRegion as C,cascadeOperationKey as b}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as U,projectAccountRows as h}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as N,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 ce}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as pe,flattenAccountsToEnvironments as me,extractAllAccountNames as ue,accountsConfigToOUTree as fe,isStringArray as de,isAccountsConfig as Re,isOuOnlyAccountBucket as Se,OU_ONLY_ACCOUNT_BUCKETS as xe}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as Te,resolveSecretRefValue as Ae,resolveBuildSecretSessionProvider as ye,sourcedRefsFromBuildArgs as Ee}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as ge,partitionForRegion as _e}from"./application/buildSecretSession.js";import{buildDeploySessionPolicy as ve}from"./application/deploySessionPolicy.js";import{resolveBuildArgs as ke}from"./application/buildArgResolver.js";import{validateBuildGroup as Le}from"./application/buildGroupValidator.js";export*from"./domain/index.js";import{runOpenNextBuild as Ie}from"./application/openNextBuild.js";import{runOrganisationSetup as he,ORG_SETUP_PHASES as Ge}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as Me,computeDeployPlan as Ye,computeAssemblyDigest as Ve,digestHead as Je,classifyImpact as He,derivePropertyChanges as Qe,isDataLoss as we,isStatefulResourceType as je,renderPlanSummary as We,renderPlanLines as qe,toWirePlanChanges as ze,signApprovalToken as Xe,verifyApprovalToken as Ze,DEFAULT_APPROVAL_TTL_MS as $e,APPROVAL_TOKEN_PATTERN as er}from"./application/plan/index.js";import{runApprovalGate as tr,approvalRefusalReason as or}from"./application/approvalGate.js";import{runDestructionGate as ar,buildDestructionTicket as nr,evaluateDestructionConsents as sr,computeTicketDigest as cr,legalRemediationVerbs as lr,executableRemediationVerbs as pr,advertisableRemediationVerbs as mr,renderDestructionTicketLines as ur,renderConsentVerdictLines as fr}from"./application/destructionGate.js";import{loadDeployPlan as Rr,createMemoisedPlanLoader as Sr}from"./application/plan/loadDeployPlan.js";import{classifyDriftFailure as Pr,fetchLastOperationEvents as Tr}from"./drift/classifyDriftFailure.js";import{DriftProbe as yr}from"./drift/driftProbe.js";import{clearDriftSuspects as Dr,defaultDriftJournalDir as gr,readDriftSuspects as _r,recordDriftSuspects as Or}from"./drift/driftSuspectJournal.js";import{detectStackDrift as Fr}from"./drift/detectStackDrift.js";import{runDriftPreFlight as Br,formatDriftPreFlightBlock as Lr,DRIFT_PREFLIGHT_TRIGGER_STATUSES as Cr,DRIFT_PREFLIGHT_BUDGET_MS as br}from"./drift/driftPreFlight.js";import{runRoute53RecordPreflight as Ur}from"./drift/route53RecordPreflight.js";import{archiveCompletedRemediationJournal as Gr,computeRemediationOpId as Nr,defaultRemediationJournalDir as Kr,findRemediationJournalByOpId as Mr,listActiveRemediationOps as Yr,listActiveRemediationOpsForContext as Vr,readRemediationJournal as Jr,sweepExpiredRemediationJournals as Hr,writeRemediationJournal as Qr,REMEDIATION_JOURNAL_RETENTION_DAYS as wr}from"./remediation/remediationJournal.js";import{captureRemediationForensics as Wr,defaultRemediationForensicsDir as qr}from"./remediation/forensicsCapture.js";import{applyRetainFlip as Xr,composeFlipCapabilities as Zr,computeTemplateDigest as $r,deriveRetainFlipTemplate as et,findMarkedResourcesByOpId as rt,proveFlipMetadataOnly as tt,readRetainFlipState as ot,verifyRetainFlipDiff as it,FORGET_MARKER_KEY as at,FORGET_FLIP_SPEC as nt,RECREATE_MARKER_KEY as st}from"./remediation/retainFlip.js";import{assessForgetResumability as lt,completeForgetAfterConverge as pt,completeForgetAfterDeploy as mt,formatRecreateInFlightCures as ut,partitionPreFlightByRemediation as ft,runForgetSurgery as dt,synthTemplateDeclaresResource as Rt}from"./remediation/forgetResource.js";import{runRecreateSurgery as xt,snapshotPolicyForType as Pt,SNAPSHOT_CAPABLE_RESOURCE_TYPES as Tt}from"./remediation/recreateResource.js";import{runRecreatePreFlight as yt}from"./remediation/recreatePreFlight.js";import{deriveRemovalTemplate as Dt,findRemovalDeletionFailures as gt,findResourcesPresent as _t,proveRemovalTargetsOnly as Ot}from"./remediation/removalUpdate.js";import{advertisableDriftVerbs as Ft,buildDriftRepairTicket as kt,runDriftRepairGate as Bt}from"./remediation/driftRepairGate.js";import{formatPinOutcomeDetail as Ct,pinProbeCoverage as bt,renderPinReportLines as It,runPinRemediation as Ut}from"./remediation/pinRemediation.js";import{composePostFailureRepairOffer as Gt,formatDriftRepairOffer as Nt,DriftRepairAvailableError as Kt}from"./remediation/postFailureRepairOffer.js";import{captureLegacySurfaces as Yt,decodeLegacyAnchor as Vt,runIdentitySurfaceGate as Jt}from"./remediation/pinIdentity.js";import{collectIdentityPinCandidates as Qt,degradedIdentityPinOutcomes as wt,renderIdentityPinReportLines as jt,runIdentityPinMigration as Wt}from"./remediation/migrateIdentity.js";import{checkDeployPathStackAvailability as zt,isDeployPathBlockingStatus as Xt,StackUnavailableError as Zt}from"./activeDeploymentGuard.js";import{assertLeaseBeforeStack as eo,LeaseDeniedError as ro}from"./application/leaseGate.js";export{er as APPROVAL_TOKEN_PATTERN,$e as DEFAULT_APPROVAL_TTL_MS,br as DRIFT_PREFLIGHT_BUDGET_MS,Cr as DRIFT_PREFLIGHT_TRIGGER_STATUSES,yr as DriftProbe,Kt as DriftRepairAvailableError,nt as FORGET_FLIP_SPEC,at as FORGET_MARKER_KEY,ro as LeaseDeniedError,Ge as ORG_SETUP_PHASES,J as ORG_TRAIL_BUCKET_OUTPUT_KEY,xe as OU_ONLY_ACCOUNT_BUCKETS,_ as PRE_EMPTY_TAG_KEYS,st as RECREATE_MARKER_KEY,wr as REMEDIATION_JOURNAL_RETENTION_DAYS,v as SAFE_CLEANUP_STATES,Tt as SNAPSHOT_CAPABLE_RESOURCE_TYPES,Zt as StackUnavailableError,H as TRAIL_BUCKET_OUTPUT_KEY,Q as TRAIL_KEY_ARN_OUTPUT_KEY,fe as accountsConfigToOUTree,Ft as advertisableDriftVerbs,mr as advertisableRemediationVerbs,Xr as applyRetainFlip,or as approvalRefusalReason,Gr as archiveCompletedRemediationJournal,eo as assertLeaseBeforeStack,lt as assessForgetResumability,L as buildAccountRegionPairs,ge as buildBuildSecretSessionPolicy,Me as buildDeployPlan,ve as buildDeploySessionPolicy,nr as buildDestructionTicket,kt as buildDriftRepairTicket,B as buildRegionList,Yt as captureLegacySurfaces,Wr as captureRemediationForensics,C as cascadeHomeRegion,b as cascadeOperationKey,zt as checkDeployPathStackAvailability,Pr as classifyDriftFailure,He as classifyImpact,P as cleanupFailedStack,Dr as clearDriftSuspects,Qt as collectIdentityPinCandidates,pt as completeForgetAfterConverge,mt as completeForgetAfterDeploy,Zr as composeFlipCapabilities,Gt as composePostFailureRepairOffer,Ve as computeAssemblyDigest,Ye as computeDeployPlan,Nr as computeRemediationOpId,l as computeSecretsDrift,$r as computeTemplateDigest,cr as computeTicketDigest,Sr as createMemoisedPlanLoader,Y as decideNextTransition,Vt as decodeLegacyAnchor,j as decommissionMemberTrailStorage,gr as defaultDriftJournalDir,qr as defaultRemediationForensicsDir,Kr as defaultRemediationJournalDir,wt as degradedIdentityPinOutcomes,t as deploy,d as deployOrganisation,Qe as derivePropertyChanges,Dt as deriveRemovalTemplate,et as deriveRetainFlipTemplate,p as deriveStaleParameters,i as destroy,S as destroyOrganisation,Fr as detectStackDrift,Je as digestHead,T as emptyS3Bucket,ne as ensureEnforceSsl,sr as evaluateDestructionConsents,pr as executableRemediationVerbs,ue as extractAllAccountNames,m as extractConsumedSsmParameters,Tr as fetchLastOperationEvents,rt as findMarkedResourcesByOpId,Mr as findRemediationJournalByOpId,gt as findRemovalDeletionFailures,_t as findResourcesPresent,me as flattenAccountsToEnvironments,Lr as formatDriftPreFlightBlock,Nt as formatDriftRepairOffer,Ct as formatPinOutcomeDetail,y as formatQuarantineSuspectedMessage,ut as formatRecreateInFlightCures,E as formatRetainedBucketsMessage,Re as isAccountsConfig,O as isCleanableState,we as isDataLoss,Xt as isDeployPathBlockingStatus,ee as isEnforceSslStatement,Se as isOuOnlyAccountBucket,D as isQuarantineDetail,g as isRetainedBucketsDetail,je as isStatefulResourceType,de as isStringArray,lr as legalRemediationVerbs,Yr as listActiveRemediationOps,Vr as listActiveRemediationOpsForContext,Rr as loadDeployPlan,K as mergeReconciledProviderAccounts,pe as parseAccountsConfiguration,k as partitionAccounts,_e as partitionForRegion,ft as partitionPreFlightByRemediation,bt as pinProbeCoverage,A as preEmptyStackBuckets,s as probeSecretsDrift,h as projectAccountRows,U as projectScalarSummary,tt as proveFlipMetadataOnly,Ot as proveRemovalTargetsOnly,_r as readDriftSuspects,Jr as readRemediationJournal,ot as readRetainFlipState,N as reconcileProviderAccounts,V as reconcileTrailMigration,Or as recordDriftSuspects,fr as renderConsentVerdictLines,ur as renderDestructionTicketLines,jt as renderIdentityPinReportLines,It as renderPinReportLines,qe as renderPlanLines,We as renderPlanSummary,ke as resolveBuildArgs,ye as resolveBuildSecretSessionProvider,Te as resolveBuildSecrets,Ae as resolveSecretRefValue,n as restart,ce as restoreAndReconcileQuarantinedBucket,ie as restoreBucketPolicy,tr as runApprovalGate,ar as runDestructionGate,Br as runDriftPreFlight,Bt as runDriftRepairGate,dt as runForgetSurgery,Wt as runIdentityPinMigration,Jt as runIdentitySurfaceGate,Ie as runOpenNextBuild,he as runOrganisationSetup,Ut as runPinRemediation,yt as runRecreatePreFlight,xt as runRecreateSurgery,Ur as runRoute53RecordPreflight,Xe as signApprovalToken,Pt as snapshotPolicyForType,Ee as sourcedRefsFromBuildArgs,u as ssmParameterNameFromValueFrom,Hr as sweepExpiredRemediationJournals,Rt as synthTemplateDeclaresResource,ae as synthesiseEnforceSslDocument,te as toResourcePolicyStatements,ze as toWirePlanChanges,$ as triageBucketPolicy,q as unlockBucket,X as unlockQueue,Le as validateBuildGroup,Ze as verifyApprovalToken,it as verifyRetainFlipDiff,Qr as writeRemediationJournal};
1
+ import{deploy as t}from"./deploy.js";import{destroy as i}from"./destroy.js";import{restart as n,probeSecretsDrift as s,probeServiceDeploymentStatus as c}from"./restart/restartApplication.js";import{computeSecretsDrift as p,deriveStaleParameters as m,extractConsumedSsmParameters as u,ssmParameterNameFromValueFrom as f}from"./restart/secretsDrift.js";import{deployOrganisation as R}from"./organisation/organisationDeploy.js";import{destroyOrganisation as x}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as T,emptyS3Bucket as y,preEmptyStackBuckets as A,formatQuarantineSuspectedMessage as E,formatRetainedBucketsMessage as D,isQuarantineDetail as g,isRetainedBucketsDetail as _,PRE_EMPTY_TAG_KEYS as O,isCleanableState as v,SAFE_CLEANUP_STATES as F}from"./stackCleanup.js";import{partitionAccounts as B,buildRegionList as L,buildAccountRegionPairs as b,cascadeHomeRegion as C,cascadeOperationKey as I}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as h,projectAccountRows as G}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as K,mergeReconciledProviderAccounts as M}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as V,reconcileTrailMigration as J,ORG_TRAIL_BUCKET_OUTPUT_KEY as H,TRAIL_BUCKET_OUTPUT_KEY as Q,TRAIL_KEY_ARN_OUTPUT_KEY as w}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as W}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as z}from"./unlock/unlockBucket.js";import{unlockQueue as Z}from"./unlock/unlockQueue.js";import{triageBucketPolicy as ee,isEnforceSslStatement as re}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as oe}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as ae,synthesiseEnforceSslDocument as ne,ensureEnforceSsl as se}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as le}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as me,flattenAccountsToEnvironments as ue,extractAllAccountNames as fe,accountsConfigToOUTree as de,isStringArray as Re,isAccountsConfig as Se,isOuOnlyAccountBucket as xe,OU_ONLY_ACCOUNT_BUCKETS as Pe}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as ye,resolveSecretRefValue as Ae,resolveBuildSecretSessionProvider as Ee,sourcedRefsFromBuildArgs as De}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as _e,partitionForRegion as Oe}from"./application/buildSecretSession.js";import{buildDeploySessionPolicy as Fe}from"./application/deploySessionPolicy.js";import{resolveBuildArgs as Be}from"./application/buildArgResolver.js";import{validateBuildGroup as be}from"./application/buildGroupValidator.js";export*from"./domain/index.js";import{runOpenNextBuild as Ue}from"./application/openNextBuild.js";import{runOrganisationSetup as Ge,ORG_SETUP_PHASES as Ne}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as Ye,computeDeployPlan as Ve,computeAssemblyDigest as Je,digestHead as He,classifyImpact as Qe,derivePropertyChanges as we,isDataLoss as je,isStatefulResourceType as We,renderPlanSummary as qe,renderPlanLines as ze,toWirePlanChanges as Xe,signApprovalToken as Ze,verifyApprovalToken as $e,DEFAULT_APPROVAL_TTL_MS as er,APPROVAL_TOKEN_PATTERN as rr}from"./application/plan/index.js";import{runApprovalGate as or,approvalRefusalReason as ir}from"./application/approvalGate.js";import{runDestructionGate as nr,buildDestructionTicket as sr,evaluateDestructionConsents as cr,computeTicketDigest as lr,legalRemediationVerbs as pr,executableRemediationVerbs as mr,advertisableRemediationVerbs as ur,renderDestructionTicketLines as fr,renderConsentVerdictLines as dr}from"./application/destructionGate.js";import{loadDeployPlan as Sr,createMemoisedPlanLoader as xr}from"./application/plan/loadDeployPlan.js";import{classifyDriftFailure as Tr,fetchLastOperationEvents as yr}from"./drift/classifyDriftFailure.js";import{DriftProbe as Er}from"./drift/driftProbe.js";import{clearDriftSuspects as gr,defaultDriftJournalDir as _r,readDriftSuspects as Or,recordDriftSuspects as vr}from"./drift/driftSuspectJournal.js";import{detectStackDrift as kr}from"./drift/detectStackDrift.js";import{runDriftPreFlight as Lr,formatDriftPreFlightBlock as br,DRIFT_PREFLIGHT_TRIGGER_STATUSES as Cr,DRIFT_PREFLIGHT_BUDGET_MS as Ir}from"./drift/driftPreFlight.js";import{runRoute53RecordPreflight as hr}from"./drift/route53RecordPreflight.js";import{archiveCompletedRemediationJournal as Nr,computeRemediationOpId as Kr,defaultRemediationJournalDir as Mr,findRemediationJournalByOpId as Yr,listActiveRemediationOps as Vr,listActiveRemediationOpsForContext as Jr,readRemediationJournal as Hr,sweepExpiredRemediationJournals as Qr,writeRemediationJournal as wr,REMEDIATION_JOURNAL_RETENTION_DAYS as jr}from"./remediation/remediationJournal.js";import{captureRemediationForensics as qr,defaultRemediationForensicsDir as zr}from"./remediation/forensicsCapture.js";import{applyRetainFlip as Zr,composeFlipCapabilities as $r,computeTemplateDigest as et,deriveRetainFlipTemplate as rt,findMarkedResourcesByOpId as tt,proveFlipMetadataOnly as ot,readRetainFlipState as it,verifyRetainFlipDiff as at,FORGET_MARKER_KEY as nt,FORGET_FLIP_SPEC as st,RECREATE_MARKER_KEY as ct}from"./remediation/retainFlip.js";import{assessForgetResumability as pt,completeForgetAfterConverge as mt,completeForgetAfterDeploy as ut,formatRecreateInFlightCures as ft,partitionPreFlightByRemediation as dt,runForgetSurgery as Rt,synthTemplateDeclaresResource as St}from"./remediation/forgetResource.js";import{runRecreateSurgery as Pt,snapshotPolicyForType as Tt,SNAPSHOT_CAPABLE_RESOURCE_TYPES as yt}from"./remediation/recreateResource.js";import{runRecreatePreFlight as Et}from"./remediation/recreatePreFlight.js";import{deriveRemovalTemplate as gt,findRemovalDeletionFailures as _t,findResourcesPresent as Ot,proveRemovalTargetsOnly as vt}from"./remediation/removalUpdate.js";import{advertisableDriftVerbs as kt,buildDriftRepairTicket as Bt,runDriftRepairGate as Lt}from"./remediation/driftRepairGate.js";import{formatPinOutcomeDetail as Ct,pinProbeCoverage as It,renderPinReportLines as Ut,runPinRemediation as ht}from"./remediation/pinRemediation.js";import{composePostFailureRepairOffer as Nt,formatDriftRepairOffer as Kt,DriftRepairAvailableError as Mt}from"./remediation/postFailureRepairOffer.js";import{captureLegacySurfaces as Vt,decodeLegacyAnchor as Jt,runIdentitySurfaceGate as Ht}from"./remediation/pinIdentity.js";import{collectIdentityPinCandidates as wt,degradedIdentityPinOutcomes as jt,renderIdentityPinReportLines as Wt,runIdentityPinMigration as qt}from"./remediation/migrateIdentity.js";import{checkDeployPathStackAvailability as Xt,isDeployPathBlockingStatus as Zt,StackUnavailableError as $t}from"./activeDeploymentGuard.js";import{assertLeaseBeforeStack as ro,LeaseDeniedError as to}from"./application/leaseGate.js";export{rr as APPROVAL_TOKEN_PATTERN,er as DEFAULT_APPROVAL_TTL_MS,Ir as DRIFT_PREFLIGHT_BUDGET_MS,Cr as DRIFT_PREFLIGHT_TRIGGER_STATUSES,Er as DriftProbe,Mt as DriftRepairAvailableError,st as FORGET_FLIP_SPEC,nt as FORGET_MARKER_KEY,to as LeaseDeniedError,Ne as ORG_SETUP_PHASES,H as ORG_TRAIL_BUCKET_OUTPUT_KEY,Pe as OU_ONLY_ACCOUNT_BUCKETS,O as PRE_EMPTY_TAG_KEYS,ct as RECREATE_MARKER_KEY,jr as REMEDIATION_JOURNAL_RETENTION_DAYS,F as SAFE_CLEANUP_STATES,yt as SNAPSHOT_CAPABLE_RESOURCE_TYPES,$t as StackUnavailableError,Q as TRAIL_BUCKET_OUTPUT_KEY,w as TRAIL_KEY_ARN_OUTPUT_KEY,de as accountsConfigToOUTree,kt as advertisableDriftVerbs,ur as advertisableRemediationVerbs,Zr as applyRetainFlip,ir as approvalRefusalReason,Nr as archiveCompletedRemediationJournal,ro as assertLeaseBeforeStack,pt as assessForgetResumability,b as buildAccountRegionPairs,_e as buildBuildSecretSessionPolicy,Ye as buildDeployPlan,Fe as buildDeploySessionPolicy,sr as buildDestructionTicket,Bt as buildDriftRepairTicket,L as buildRegionList,Vt as captureLegacySurfaces,qr as captureRemediationForensics,C as cascadeHomeRegion,I as cascadeOperationKey,Xt as checkDeployPathStackAvailability,Tr as classifyDriftFailure,Qe as classifyImpact,T as cleanupFailedStack,gr as clearDriftSuspects,wt as collectIdentityPinCandidates,mt as completeForgetAfterConverge,ut as completeForgetAfterDeploy,$r as composeFlipCapabilities,Nt as composePostFailureRepairOffer,Je as computeAssemblyDigest,Ve as computeDeployPlan,Kr as computeRemediationOpId,p as computeSecretsDrift,et as computeTemplateDigest,lr as computeTicketDigest,xr as createMemoisedPlanLoader,V as decideNextTransition,Jt as decodeLegacyAnchor,W as decommissionMemberTrailStorage,_r as defaultDriftJournalDir,zr as defaultRemediationForensicsDir,Mr as defaultRemediationJournalDir,jt as degradedIdentityPinOutcomes,t as deploy,R as deployOrganisation,we as derivePropertyChanges,gt as deriveRemovalTemplate,rt as deriveRetainFlipTemplate,m as deriveStaleParameters,i as destroy,x as destroyOrganisation,kr as detectStackDrift,He as digestHead,y as emptyS3Bucket,se as ensureEnforceSsl,cr as evaluateDestructionConsents,mr as executableRemediationVerbs,fe as extractAllAccountNames,u as extractConsumedSsmParameters,yr as fetchLastOperationEvents,tt as findMarkedResourcesByOpId,Yr as findRemediationJournalByOpId,_t as findRemovalDeletionFailures,Ot as findResourcesPresent,ue as flattenAccountsToEnvironments,br as formatDriftPreFlightBlock,Kt as formatDriftRepairOffer,Ct as formatPinOutcomeDetail,E as formatQuarantineSuspectedMessage,ft as formatRecreateInFlightCures,D as formatRetainedBucketsMessage,Se as isAccountsConfig,v as isCleanableState,je as isDataLoss,Zt as isDeployPathBlockingStatus,re as isEnforceSslStatement,xe as isOuOnlyAccountBucket,g as isQuarantineDetail,_ as isRetainedBucketsDetail,We as isStatefulResourceType,Re as isStringArray,pr as legalRemediationVerbs,Vr as listActiveRemediationOps,Jr as listActiveRemediationOpsForContext,Sr as loadDeployPlan,M as mergeReconciledProviderAccounts,me as parseAccountsConfiguration,B as partitionAccounts,Oe as partitionForRegion,dt as partitionPreFlightByRemediation,It as pinProbeCoverage,A as preEmptyStackBuckets,s as probeSecretsDrift,c as probeServiceDeploymentStatus,G as projectAccountRows,h as projectScalarSummary,ot as proveFlipMetadataOnly,vt as proveRemovalTargetsOnly,Or as readDriftSuspects,Hr as readRemediationJournal,it as readRetainFlipState,K as reconcileProviderAccounts,J as reconcileTrailMigration,vr as recordDriftSuspects,dr as renderConsentVerdictLines,fr as renderDestructionTicketLines,Wt as renderIdentityPinReportLines,Ut as renderPinReportLines,ze as renderPlanLines,qe as renderPlanSummary,Be as resolveBuildArgs,Ee as resolveBuildSecretSessionProvider,ye as resolveBuildSecrets,Ae as resolveSecretRefValue,n as restart,le as restoreAndReconcileQuarantinedBucket,ae as restoreBucketPolicy,or as runApprovalGate,nr as runDestructionGate,Lr as runDriftPreFlight,Lt as runDriftRepairGate,Rt as runForgetSurgery,qt as runIdentityPinMigration,Ht as runIdentitySurfaceGate,Ue as runOpenNextBuild,Ge as runOrganisationSetup,ht as runPinRemediation,Et as runRecreatePreFlight,Pt as runRecreateSurgery,hr as runRoute53RecordPreflight,Ze as signApprovalToken,Tt as snapshotPolicyForType,De as sourcedRefsFromBuildArgs,f as ssmParameterNameFromValueFrom,Qr as sweepExpiredRemediationJournals,St as synthTemplateDeclaresResource,ne as synthesiseEnforceSslDocument,oe as toResourcePolicyStatements,Xe as toWirePlanChanges,ee as triageBucketPolicy,z as unlockBucket,Z as unlockQueue,be as validateBuildGroup,$e as verifyApprovalToken,at as verifyRetainFlipDiff,wr as writeRemediationJournal};
@@ -1 +1 @@
1
- var y=Object.defineProperty;var u=(t,i)=>y(t,"name",{value:i,configurable:!0});import{z as f}from"zod";import{success as E,failure as c}from"@fjall/generator";import{environmentToTier as g,stageFromWireEnvironment as P,normaliseError as w}from"@fjall/util";import{OrganizationsClient as S}from"@aws-sdk/client-organizations";import{listAccounts as h}from"../../aws/organisations/accounts.js";import{parseAccountsConfiguration as O,flattenAccountsToEnvironments as I}from"./accountsConfig.js";const C=6e4,M=f.object({Id:f.string().min(1),Name:f.string().min(1)});async function $(t,i){let n;const r=new Promise((o,e)=>{n=setTimeout(()=>e(new Error(`Timed out parsing ACCOUNTS configuration after ${C}ms`)),C)});let s;try{s=await Promise.race([O(i),r])}catch(o){return c(w(o))}finally{n!==void 0&&clearTimeout(n)}if(!s.success)return c(new Error(`Failed to parse ACCOUNTS configuration: ${s.error.message}`));if(s.data===null)return c(new Error("ACCOUNTS configuration file not found"));const N=I(s.data),d=new Map;for(const{accountName:o,environment:e}of N)d.set(o.toLowerCase(),{environment:e,displayName:o});let l;try{l=t.awsProvider.getClient(S)}catch(o){return c(w(o))}const a=await h(l);if(!a.success)return c(a.error);const p=new Map;for(const o of a.data){const e=M.safeParse(o);e.success&&p.set(e.data.Name.toLowerCase(),{id:e.data.Id,name:e.data.Name})}const A=[],v=[];for(const[o,{environment:e,displayName:T}]of d){const m=p.get(o);if(!m){v.push(T);continue}g(e)!=="organisation"&&A.push({id:m.id,name:m.name,tier:g(e),environment:P(e)})}return E({providerAccounts:A,missingAccountNames:v})}u($,"reconcileProviderAccounts");function j(t,i){const n=new Map;for(const r of t?.providerAccounts??[])n.set(r.id,r);for(const r of i)n.has(r.id)||n.set(r.id,r);return{...t??{},providerAccounts:Array.from(n.values())}}u(j,"mergeReconciledProviderAccounts");export{j as mergeReconciledProviderAccounts,$ as reconcileProviderAccounts};
1
+ var S=Object.defineProperty;var f=(t,i)=>S(t,"name",{value:i,configurable:!0});import{z as d}from"zod";import{success as h,failure as c}from"@fjall/generator";import{environmentToTier as w,stageFromWireEnvironment as y,normaliseError as N}from"@fjall/util";import{logger as E}from"@fjall/util/logger";import{OrganizationsClient as P}from"@aws-sdk/client-organizations";import{listAccounts as I}from"../../aws/organisations/accounts.js";import{parseAccountsConfiguration as O,flattenAccountsToEnvironments as M}from"./accountsConfig.js";const C=6e4,U=d.object({Id:d.string().min(1),Name:d.string().min(1)});async function W(t,i){let n;const r=new Promise((e,o)=>{n=setTimeout(()=>o(new Error(`Timed out parsing ACCOUNTS configuration after ${C}ms`)),C)});let a;try{a=await Promise.race([O(i),r])}catch(e){return c(N(e))}finally{n!==void 0&&clearTimeout(n)}if(!a.success)return c(new Error(`Failed to parse ACCOUNTS configuration: ${a.error.message}`));if(a.data===null)return c(new Error("ACCOUNTS configuration file not found"));const T=M(a.data),l=new Map;for(const{accountName:e,environment:o}of T)l.set(e.toLowerCase(),{environment:o,displayName:e});let p;try{p=t.awsProvider.getClient(P)}catch(e){return c(N(e))}const m=await I(p);if(!m.success)return c(m.error);const g=new Map;for(const e of m.data){const o=U.safeParse(e);if(!o.success){E.warn("reconcileProviderAccounts","Skipping AWS Organizations account that failed schema validation",{accountId:e.Id,accountName:e.Name,issues:o.error.issues.map(s=>`${s.path.map(String).join(".")}: ${s.message}`)});continue}g.set(o.data.Name.toLowerCase(),{id:o.data.Id,name:o.data.Name})}const A=[],v=[];for(const[e,{environment:o,displayName:s}]of l){const u=g.get(e);if(!u){v.push(s);continue}w(o)!=="organisation"&&A.push({id:u.id,name:u.name,tier:w(o),environment:y(o)})}return h({providerAccounts:A,missingAccountNames:v})}f(W,"reconcileProviderAccounts");function k(t,i){const n=new Map;for(const r of t?.providerAccounts??[])n.set(r.id,r);for(const r of i)n.has(r.id)||n.set(r.id,r);return{...t??{},providerAccounts:Array.from(n.values())}}f(k,"mergeReconciledProviderAccounts");export{k as mergeReconciledProviderAccounts,W as reconcileProviderAccounts};
@@ -57,6 +57,40 @@ export type SecretsDriftProbeParams = Omit<RestartParams, "callbacks">;
57
57
  * Used by the CLI's pre-confirm drift display and the webapp's drift pulse.
58
58
  */
59
59
  export declare function probeSecretsDrift(params: SecretsDriftProbeParams, injectedServices?: RestartServices): Promise<Result<ApplicationSecretsDrift, Error>>;
60
+ /** Per-service snapshot of the PRIMARY ECS deployment's rollout state. */
61
+ export interface ServiceDeploymentStatus {
62
+ serviceName: string;
63
+ serviceArn: string;
64
+ desiredCount: number;
65
+ runningCount: number;
66
+ pendingCount: number;
67
+ /** `COMPLETED` | `IN_PROGRESS` | `FAILED` — absent on services predating rollout-state reporting. */
68
+ rolloutState?: string;
69
+ rolloutStateReason?: string;
70
+ failedTasks?: number;
71
+ /** Task definition the PRIMARY deployment is rolling out. */
72
+ taskDefinition?: string;
73
+ /** Deployment count on the service — >1 means an old deployment is still draining. */
74
+ activeDeployments: number;
75
+ /** The smoke verdict for this service — see `isServiceDeploymentComplete`. */
76
+ completed: boolean;
77
+ }
78
+ export interface ApplicationServiceStatus {
79
+ target: string;
80
+ clusterArn: string;
81
+ services: ServiceDeploymentStatus[];
82
+ /** True only when EVERY discovered service's PRIMARY deployment is complete. */
83
+ allCompleted: boolean;
84
+ }
85
+ /**
86
+ * Read-only per-service deployment-status probe (`fjall services`) — the
87
+ * same resolution ladder as `restart()`/`probeSecretsDrift()`, reporting each
88
+ * service's PRIMARY deployment rollout state. This is the CD smoke surface
89
+ * from the worker-deploy-safety design (item 12): a worker that CloudFormation
90
+ * waved through but is now churning shows `IN_PROGRESS`/`FAILED` (or a
91
+ * running-count shortfall) here instead of being invisible to the gate.
92
+ */
93
+ export declare function probeServiceDeploymentStatus(params: SecretsDriftProbeParams, injectedServices?: RestartServices): Promise<Result<ApplicationServiceStatus, Error>>;
60
94
  /**
61
95
  * Restart an application's ECS services without building or deploying:
62
96
  * re-register each service's live task definition unchanged and roll the
@@ -1 +1 @@
1
- var x=Object.defineProperty;var p=(n,e)=>x(n,"name",{value:e,configurable:!0});import{success as k,failure as g}from"@fjall/generator";import{CONTENT_HASH_TAG_PATTERN as O,maskSensitiveOutput as E}from"@fjall/util";import{logger as L}from"@fjall/util/logger";import{SimpleAwsProvider as P}from"../../aws/SimpleAwsProvider.js";import{CloudFormationService as M}from"../../services/infrastructure/CloudFormationService.js";import{EcsService as _}from"../../services/infrastructure/EcsService.js";import{EcsServiceResolver as G}from"../../services/infrastructure/EcsServiceResolver.js";import{EcrImageInspectorService as F,stripTagOrDigest as H}from"../../services/infrastructure/EcrImageInspectorService.js";import{SsmParameterMetadataService as U}from"../../services/infrastructure/SsmParameterMetadataService.js";import{extractEcsServiceName as N}from"../../aws/utils/arnParser.js";import{isAborted as V}from"../../aws/organisations/types.js";import{matchesServiceName as j,resolveServiceTaskDefinition as B}from"../application/serviceImageTagsRollback.js";import{deriveEcrRepositoryArn as Y}from"../application/codeOnlyDeploy.js";import{registerAndRollTaskDefinition as q}from"../application/taskDefinitionRoll.js";import{computeSecretsDrift as h}from"./secretsDrift.js";const z="restartApplication";function b(n){const e=new P(n),i=new M(e);return{ecsService:new _(e),ecsResolver:new G(i),ecrImageInspector:new F(e),ssmService:new U(e)}}p(b,"createRestartServices");async function C(n,e){const{target:i}=e;let r,t;if(e.knownServices!==void 0)r=e.knownServices.clusterArn,t=e.knownServices.serviceArns;else{const s=await n.getDeployableClusterAndServices(i,e.abortSignal);if(!s.success)return g(new Error(`Failed to discover ECS services for ${i}: ${E(s.error.message)}`));r=s.data.clusterArn,t=s.data.serviceArns}if(r===void 0||t.length===0)return g(new Error(`No running ECS services found for ${i} \u2014 nothing to restart. Deploy the application first.`));if(e.serviceName!==void 0){const s=t.filter(o=>{const a=N(o);return a!==void 0&&e.serviceName!==void 0?j(a,e.serviceName):!1});if(s.length===0){const o=t.map(a=>N(a)).filter(a=>a!==void 0).join(", ");return g(new Error(`No ECS service matching "${e.serviceName}" found for ${i}. Live services: ${o||"none"}`))}t=s}return k({clusterArn:r,serviceArns:t})}p(C,"resolveRestartTargets");async function ue(n,e){const{ecsService:i,ecsResolver:r,ssmService:t}=e??b(n.awsCredentials),s=await C(r,n);return s.success?h({ecsService:i,ssmService:t},s.data,n.abortSignal):g(s.error)}p(ue,"probeSecretsDrift");async function le(n,e){const{target:i,callbacks:r,abortSignal:t}=n,s=Date.now(),{ecsService:o,ecsResolver:a,ecrImageInspector:A,ssmService:u}=e??b(n.awsCredentials),f=await C(a,n);if(!f.success)return r.onError?.(f.error),g(f.error);const{clusterArn:w,serviceArns:c}=f.data,d=await h({ecsService:o,ssmService:u},{clusterArn:w,serviceArns:c},t);let l,T={};d.success?(l=d.data,T=d.data.parameterVersions):r.onLog?.(`Could not compute secrets drift before restart: ${E(d.error.message)}. Restarting anyway.`,"warn");const D=[];for(const R of c){if(V(t))return k({target:i,artefacts:D,appliedParameterVersions:T,...l!==void 0?{preRestartDrift:l}:{},durationMs:Date.now()-s,cancelled:!0});const I=Date.now(),m=N(R)??R;r.onLog?.(`Restarting ${m}\u2026`,"info");const v=await B({ecsService:o},w,R,m,t);if(!v.success){const S=new Error(`Restart failed for ${m}: ${E(v.error.message)}`);return r.onError?.(S),g(S)}const $=await q({ecsService:o},{clusterArn:w,serviceArn:R,serviceName:m,taskDefinition:v.data.taskDefinition,containerDefinitions:v.data.containerDefinitions,previousTaskDefArn:v.data.previousTaskDefArn,startedAtMs:I},r,t);if(!$.success){const S=new Error(`Restart failed for ${m}: ${E($.error.message)}`);return r.onError?.(S),g(S)}const y=await J(A,m,v.data.containerDefinitions,$.data.newTaskDefArn,v.data.previousTaskDefArn,t);y!==void 0?D.push(y):r.onLog?.(`Restarted ${m}, but its image tag could not be recovered \u2014 the restart release will omit this service's artefact.`,"warn")}return L.debug(z,`Restarted ${i}`,{services:c.length,artefacts:D.length}),k({target:i,artefacts:D,appliedParameterVersions:T,...l!==void 0?{preRestartDrift:l}:{},durationMs:Date.now()-s})}p(le,"restart");async function J(n,e,i,r,t,s){const o=i.find(c=>c.image)?.image;if(o===void 0)return;const a=H(o),A=Y(a);let u,f;const w=o.lastIndexOf("@");if(w>0){f=o.slice(w+1);const c=await n.getImageTagsByDigest(a,f,s);if(c.success&&c.data!==void 0){const d=c.data;u=d.find(l=>O.test(l))??d[0]}}else{const c=o.lastIndexOf(":");if(c>0){u=o.slice(c+1);const d=await n.getImageDigest(a,u,s);d.success&&(f=d.data)}}if(!(u===void 0||u===""))return{serviceName:e,imageTag:u,imageUri:`${a}:${u}`,taskDefinitionArn:r,previousTaskDefinitionArn:t,...f!==void 0?{imageDigest:f}:{},...A!==""?{ecrRepositoryArn:A}:{}}}p(J,"deriveRestartArtefact");export{b as createRestartServices,ue as probeSecretsDrift,le as restart};
1
+ var x=Object.defineProperty;var S=(e,t)=>x(e,"name",{value:t,configurable:!0});import{success as E,failure as m}from"@fjall/generator";import{CONTENT_HASH_TAG_PATTERN as O,maskSensitiveOutput as R}from"@fjall/util";import{logger as M}from"@fjall/util/logger";import{SimpleAwsProvider as P}from"../../aws/SimpleAwsProvider.js";import{CloudFormationService as L}from"../../services/infrastructure/CloudFormationService.js";import{EcsService as _}from"../../services/infrastructure/EcsService.js";import{EcsServiceResolver as F}from"../../services/infrastructure/EcsServiceResolver.js";import{EcrImageInspectorService as G,stripTagOrDigest as H}from"../../services/infrastructure/EcrImageInspectorService.js";import{SsmParameterMetadataService as U}from"../../services/infrastructure/SsmParameterMetadataService.js";import{extractEcsServiceName as k}from"../../aws/utils/arnParser.js";import{isAborted as V}from"../../aws/organisations/types.js";import{matchesServiceName as Y,resolveServiceTaskDefinition as j}from"../application/serviceImageTagsRollback.js";import{deriveEcrRepositoryArn as B}from"../application/codeOnlyDeploy.js";import{registerAndRollTaskDefinition as q}from"../application/taskDefinitionRoll.js";import{computeSecretsDrift as N}from"./secretsDrift.js";const z="restartApplication";function h(e){const t=new P(e),d=new L(t);return{ecsService:new _(t),ecsResolver:new F(d),ecrImageInspector:new G(t),ssmService:new U(t)}}S(h,"createRestartServices");async function y(e,t,d){const{target:o}=t;let a,n;if(t.knownServices!==void 0)a=t.knownServices.clusterArn,n=t.knownServices.serviceArns;else{const r=await e.getDeployableClusterAndServices(o,t.abortSignal);if(!r.success)return m(new Error(`Failed to discover ECS services for ${o}: ${R(r.error.message)}`));a=r.data.clusterArn,n=r.data.serviceArns}if(a===void 0||n.length===0)return m(new Error(d??`No running ECS services found for ${o} \u2014 nothing to restart. Deploy the application first.`));if(t.serviceName!==void 0){const r=n.filter(f=>{const u=k(f);return u!==void 0&&t.serviceName!==void 0?Y(u,t.serviceName):!1});if(r.length===0){const f=n.map(u=>k(u)).filter(u=>u!==void 0).join(", ");return m(new Error(`No ECS service matching "${t.serviceName}" found for ${o}. Live services: ${f||"none"}`))}n=r}return E({clusterArn:a,serviceArns:n})}S(y,"resolveRestartTargets");async function le(e,t){const{ecsService:d,ecsResolver:o,ssmService:a}=t??h(e.awsCredentials),n=await y(o,e);return n.success?N({ecsService:d,ssmService:a},n.data,e.abortSignal):m(n.error)}S(le,"probeSecretsDrift");function J(e,t){return e===void 0?!1:e.rolloutState!==void 0?e.rolloutState==="COMPLETED":t===1&&(e.desiredCount??0)===(e.runningCount??0)&&(e.pendingCount??0)===0}S(J,"isServiceDeploymentComplete");async function ge(e,t){const{ecsService:d,ecsResolver:o}=t??h(e.awsCredentials),a=await y(o,e,`No ECS services found for ${e.target} \u2014 nothing to check. Deploy the application first.`);if(!a.success)return m(a.error);const{clusterArn:n,serviceArns:r}=a.data,f=await d.describeServices(n,r,e.abortSignal);if(!f.success)return m(new Error(`Failed to describe ECS services for ${e.target}: ${R(f.error.message)}`));const u=new Map(f.data.map(s=>[s.serviceArn,s])),l=r.map(s=>{const g=u.get(s),c=g?.deployments??[],i=c.find(v=>v.status==="PRIMARY");return{serviceName:g?.serviceName??k(s)??s,serviceArn:s,desiredCount:g?.desiredCount??0,runningCount:g?.runningCount??0,pendingCount:g?.pendingCount??0,...i?.rolloutState!==void 0?{rolloutState:i.rolloutState}:{},...i?.rolloutStateReason!==void 0?{rolloutStateReason:i.rolloutStateReason}:{},...i?.failedTasks!==void 0?{failedTasks:i.failedTasks}:{},...i?.taskDefinition!==void 0?{taskDefinition:i.taskDefinition}:{},activeDeployments:c.length,completed:J(i,c.length)}});return E({target:e.target,clusterArn:n,services:l,allCompleted:l.length>0&&l.every(s=>s.completed)})}S(ge,"probeServiceDeploymentStatus");async function ve(e,t){const{target:d,callbacks:o,abortSignal:a}=e,n=Date.now(),{ecsService:r,ecsResolver:f,ecrImageInspector:u,ssmService:l}=t??h(e.awsCredentials),s=await y(f,e);if(!s.success)return o.onError?.(s.error),m(s.error);const{clusterArn:g,serviceArns:c}=s.data,i=await N({ecsService:r,ssmService:l},{clusterArn:g,serviceArns:c},a);let v,T={};i.success?(v=i.data,T=i.data.parameterVersions):o.onLog?.(`Could not compute secrets drift before restart: ${R(i.error.message)}. Restarting anyway.`,"warn");const A=[];for(const C of c){if(V(a))return E({target:d,artefacts:A,appliedParameterVersions:T,...v!==void 0?{preRestartDrift:v}:{},durationMs:Date.now()-n,cancelled:!0});const I=Date.now(),p=k(C)??C;o.onLog?.(`Restarting ${p}\u2026`,"info");const w=await j({ecsService:r},g,C,p,a);if(!w.success){const D=new Error(`Restart failed for ${p}: ${R(w.error.message)}`);return o.onError?.(D),m(D)}const $=await q({ecsService:r},{clusterArn:g,serviceArn:C,serviceName:p,taskDefinition:w.data.taskDefinition,containerDefinitions:w.data.containerDefinitions,previousTaskDefArn:w.data.previousTaskDefArn,startedAtMs:I},o,a);if(!$.success){const D=new Error(`Restart failed for ${p}: ${R($.error.message)}`);return o.onError?.(D),m(D)}const b=await K(u,p,w.data.containerDefinitions,$.data.newTaskDefArn,w.data.previousTaskDefArn,a);b!==void 0?A.push(b):o.onLog?.(`Restarted ${p}, but its image tag could not be recovered \u2014 the restart release will omit this service's artefact.`,"warn")}return M.debug(z,`Restarted ${d}`,{services:c.length,artefacts:A.length}),E({target:d,artefacts:A,appliedParameterVersions:T,...v!==void 0?{preRestartDrift:v}:{},durationMs:Date.now()-n})}S(ve,"restart");async function K(e,t,d,o,a,n){const r=d.find(c=>c.image)?.image;if(r===void 0)return;const f=H(r),u=B(f);let l,s;const g=r.lastIndexOf("@");if(g>0){s=r.slice(g+1);const c=await e.getImageTagsByDigest(f,s,n);if(c.success&&c.data!==void 0){const i=c.data;l=i.find(v=>O.test(v))??i[0]}}else{const c=r.lastIndexOf(":");if(c>0){l=r.slice(c+1);const i=await e.getImageDigest(f,l,n);i.success&&(s=i.data)}}if(!(l===void 0||l===""))return{serviceName:t,imageTag:l,imageUri:`${f}:${l}`,taskDefinitionArn:o,previousTaskDefinitionArn:a,...s!==void 0?{imageDigest:s}:{},...u!==""?{ecrRepositoryArn:u}:{}}}S(K,"deriveRestartArtefact");export{h as createRestartServices,le as probeSecretsDrift,ge as probeServiceDeploymentStatus,ve as restart};
@@ -0,0 +1,48 @@
1
+ import type { CloudFormationClient } from "@aws-sdk/client-cloudformation";
2
+ import { type CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs";
3
+ import type { DeployCallbacks } from "../../types/callbacks.js";
4
+ /**
5
+ * Container Insights mints `/aws/ecs/containerinsights/<cluster>/performance`
6
+ * itself. No CDK or CloudFormation property owns it, so CFN cannot delete what
7
+ * it never created and stack deletion always strands it. Every Fjall ECS
8
+ * cluster enables Container Insights v2 unconditionally (`addCluster` in
9
+ * `resources/aws/compute/ecs.ts`), so every destroyed app leaks exactly one
10
+ * group per cluster.
11
+ *
12
+ * Shared with the deploy-test residue oracle, which matches the same shape
13
+ * (`deploy-tests/lib/residueOracle.mts`) — the two must agree or the pipeline
14
+ * flags residue this reap already removed.
15
+ */
16
+ export declare function insightsLogGroupName(clusterName: string): string;
17
+ /**
18
+ * Record the Container Insights log groups the stack's ECS clusters will
19
+ * strand, so {@link reapInsightsLogGroups} can delete them once the stack is
20
+ * gone.
21
+ *
22
+ * Call BEFORE the destroy — after CFN reports the stack deleted its resource
23
+ * list is unreadable, and this captured list is the only record of which
24
+ * groups the reap may touch. Deriving the names from clusters we are about to
25
+ * delete is what keeps the reap's deletion predicate closed: it can never name
26
+ * a group belonging to a live cluster, an unrelated app, or a shared
27
+ * AWS-managed group such as `RDSOSMetrics`.
28
+ *
29
+ * Returns [] on any read failure — no capture means no deletion, so a
30
+ * degraded read leaves today's leak rather than widening the blast radius.
31
+ */
32
+ export declare function captureInsightsLogGroups(cfnClient: CloudFormationClient, stackName: string, abortSignal?: AbortSignal): Promise<readonly string[]>;
33
+ export interface InsightsReapSummary {
34
+ /** Groups deleted by this pass. */
35
+ deleted: string[];
36
+ /** Groups a delete failed for; each warned via callbacks. */
37
+ failed: string[];
38
+ }
39
+ /**
40
+ * Delete the Container Insights log groups captured before the destroy.
41
+ *
42
+ * Run only after CFN reports the stack deleted. Per-group failures warn and
43
+ * continue — a stranded log group is a cost and tidiness problem, never a
44
+ * reason to fail a destroy that already succeeded. A group that does not exist
45
+ * is the ordinary case (a cluster that never ran a task never gets one) and is
46
+ * neither a deletion nor a failure.
47
+ */
48
+ export declare function reapInsightsLogGroups(logsClient: CloudWatchLogsClient, logGroupNames: readonly string[], callbacks?: DeployCallbacks, abortSignal?: AbortSignal): Promise<InsightsReapSummary>;
@@ -0,0 +1 @@
1
+ var l=Object.defineProperty;var i=(r,e)=>l(r,"name",{value:e,configurable:!0});import{DeleteLogGroupCommand as g}from"@aws-sdk/client-cloudwatch-logs";import{logger as c}from"@fjall/util/logger";import{getErrorMessage as d,maskSensitiveOutput as p}from"@fjall/util";import{composeSdkAbortSignal as f,extractErrorName as m,isAborted as h}from"../../aws/organisations/types.js";import{STACK_NOT_FOUND_PATTERN as C}from"../../types/constants.js";import{STACK_CLEANUP_LOG as a,warnViaCallbacks as R}from"./logging.js";import{collectStackResources as E}from"./stackResources.js";const N=["ResourceNotFoundException","ResourceNotFound"];function S(r){return`/aws/ecs/containerinsights/${r}/performance`}i(S,"insightsLogGroupName");function G(r){const e=r.startsWith("arn:")?r.split("/").pop():r;return e!==void 0&&e!==""?e:void 0}i(G,"clusterNameFromPhysicalId");async function T(r,e,u){try{const n=await E(r,e,o=>o.ResourceType==="AWS::ECS::Cluster",o=>o.PhysicalResourceId,u),t=[];for(const o of n){const s=G(o);s!==void 0&&t.push(S(s))}return t}catch(n){return n instanceof Error&&n.message?.includes(C)?(c.debug(a,`Stack ${e} does not exist, no Container Insights groups to capture`),[]):(c.debug(a,`Could not list ${e} resources for the Container Insights capture`,{stackName:e,error:p(d(n))}),[])}}i(T,"captureInsightsLogGroups");async function $(r,e,u,n){const t={deleted:[],failed:[]};for(const o of e){if(h(n))break;try{await r.send(new g({logGroupName:o}),{abortSignal:f(n)}),t.deleted.push(o)}catch(s){if(N.includes(m(s)))continue;t.failed.push(o),R(`Could not delete Container Insights log group ${o}: ${p(d(s))}`,u)}}return t.deleted.length>0&&c.debug(a,`Reaped ${t.deleted.length} Container Insights log group(s)`,{deleted:t.deleted}),t}i($,"reapInsightsLogGroups");export{T as captureInsightsLogGroups,S as insightsLogGroupName,$ as reapInsightsLogGroups};
@@ -13,4 +13,5 @@ export { BUCKET_GONE_ERROR_NAMES, PRE_EMPTY_TAG_KEYS, emptyS3Bucket, isBucketGon
13
13
  export { formatQuarantineSuspectedMessage, formatRetainedBucketsMessage } from "./stackCleanup/messages.js";
14
14
  export { capturedSweepBuckets, preEmptyStackBuckets, type PreEmptyBucketsSummary } from "./stackCleanup/preEmpty.js";
15
15
  export { cleanupFailedStack } from "./stackCleanup/failedStack.js";
16
+ export { captureInsightsLogGroups, insightsLogGroupName, reapInsightsLogGroups, type InsightsReapSummary } from "./stackCleanup/insightsLogGroups.js";
16
17
  export { sweepOrphanedDestroyBuckets, type OrphanSweepSummary } from "./stackCleanup/orphanSweep.js";
@@ -1 +1 @@
1
- import{SAFE_CLEANUP_STATES as r,isCleanableState as a}from"../types/constants.js";import{isQuarantineDetail as p,isRetainedBucketsDetail as s}from"../types/callbacks.js";import{BUCKET_GONE_ERROR_NAMES as u,PRE_EMPTY_TAG_KEYS as E,emptyS3Bucket as i,isBucketGoneError as m}from"./stackCleanup/bucketOps.js";import{formatQuarantineSuspectedMessage as n,formatRetainedBucketsMessage as f}from"./stackCleanup/messages.js";import{capturedSweepBuckets as B,preEmptyStackBuckets as _}from"./stackCleanup/preEmpty.js";import{cleanupFailedStack as d}from"./stackCleanup/failedStack.js";import{sweepOrphanedDestroyBuckets as R}from"./stackCleanup/orphanSweep.js";export{u as BUCKET_GONE_ERROR_NAMES,E as PRE_EMPTY_TAG_KEYS,r as SAFE_CLEANUP_STATES,B as capturedSweepBuckets,d as cleanupFailedStack,i as emptyS3Bucket,n as formatQuarantineSuspectedMessage,f as formatRetainedBucketsMessage,m as isBucketGoneError,a as isCleanableState,p as isQuarantineDetail,s as isRetainedBucketsDetail,_ as preEmptyStackBuckets,R as sweepOrphanedDestroyBuckets};
1
+ import{SAFE_CLEANUP_STATES as r,isCleanableState as o}from"../types/constants.js";import{isQuarantineDetail as s,isRetainedBucketsDetail as p}from"../types/callbacks.js";import{BUCKET_GONE_ERROR_NAMES as i,PRE_EMPTY_TAG_KEYS as c,emptyS3Bucket as m,isBucketGoneError as n}from"./stackCleanup/bucketOps.js";import{formatQuarantineSuspectedMessage as S,formatRetainedBucketsMessage as f}from"./stackCleanup/messages.js";import{capturedSweepBuckets as g,preEmptyStackBuckets as x}from"./stackCleanup/preEmpty.js";import{cleanupFailedStack as _}from"./stackCleanup/failedStack.js";import{captureInsightsLogGroups as l,insightsLogGroupName as G,reapInsightsLogGroups as R}from"./stackCleanup/insightsLogGroups.js";import{sweepOrphanedDestroyBuckets as T}from"./stackCleanup/orphanSweep.js";export{i as BUCKET_GONE_ERROR_NAMES,c as PRE_EMPTY_TAG_KEYS,r as SAFE_CLEANUP_STATES,l as captureInsightsLogGroups,g as capturedSweepBuckets,_ as cleanupFailedStack,m as emptyS3Bucket,S as formatQuarantineSuspectedMessage,f as formatRetainedBucketsMessage,G as insightsLogGroupName,n as isBucketGoneError,o as isCleanableState,s as isQuarantineDetail,p as isRetainedBucketsDetail,x as preEmptyStackBuckets,R as reapInsightsLogGroups,T as sweepOrphanedDestroyBuckets};
@@ -1 +1 @@
1
- var N=Object.defineProperty;var P=(C,e)=>N(C,"name",{value:e,configurable:!0});import{CloudFormationClient as F}from"@aws-sdk/client-cloudformation";import{S3Client as O}from"@aws-sdk/client-s3";import{capturedSweepBuckets as _,preEmptyStackBuckets as L,sweepOrphanedDestroyBuckets as B}from"../../orchestration/stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as E,CDK_NO_STACKS_MATCH as $}from"../../types/constants.js";import{assertLeaseBeforeStack as b}from"../../orchestration/application/leaseGate.js";import{getApplicationStackName as h}from"../../types/operations.js";import{success as f,failure as y}from"@fjall/generator";import{ApplicationError as w}from"../../types/application/ApplicationServiceTypes.js";import{logger as p}from"@fjall/util/logger";import{maskSensitiveOutput as A}from"@fjall/util";import{convertCloudFormationOutputsToRecord as R}from"../supporting/helpers.js";import{destroyAllStacks as I,mapSettledResults as D,resolveWebsiteUrl as K}from"./applicationStackHelpers.js";class Y{static{P(this,"ApplicationStackService")}cdkService;cloudFormationService;aws;constructor(e,o,r){this.cdkService=e,this.cloudFormationService=o,this.aws=r}async runCdkSynth(e){return this.cdkService.runCdkSynth(e)}async deployStack(e,o,r,n){const a=o.target,i=h(a,e),c=await this.cdkService.runCdkDeploy(o,i,r?.onOutput,r?.onResourceProgress,this.aws,void 0,n?.parameters,n?.exclusively);if(c.success){const s=c.data;p.debug("ApplicationStackService","CDK deploy result",{stackName:i,stackType:e,success:!0,message:s.message,status:s.status,skipped:s.details?.skipped,hasOutput:!!s.details?.output});const u=await this.cloudFormationService.getStackOutputs(i,void 0,n?.abortSignal);p.debug("ApplicationStackService","Stack outputs after deploy",{stackName:i,hasOutputs:u.success&&u.data.length>0,outputCount:u.success?u.data.length:0});const t=u.success&&u.data.length>0?R(u.data):void 0;return f({stackType:e,stackName:i,skipped:s.details?.skipped===!0,outputs:t})}else{const s=A(c.error||`Failed to deploy ${e} infrastructure`);return p.error("ApplicationStackService","Stack deployment failed",{stackType:e,target:o.target,error:s}),y(new w(s,{errorType:"deployment_failed",appName:o.target,operation:"deployStack",stackType:e}))}}async deployStacksInParallel(e,o,r){const n=r?.onOutput,a=r?.onResourceProgress,i=r?.onStackComplete,c=await Promise.allSettled(e.map(async s=>{const u=Date.now(),t=await this.deployStack(s,o,{onOutput:n?l=>n(l,s):void 0,onResourceProgress:a?l=>a(l,s):void 0},{exclusively:!0}),S=Date.now()-u,d={stack:s,success:t.success,error:t.success?void 0:t.error,duration:S,...t.success&&t.data.outputs!==void 0?{outputs:t.data.outputs}:{}};return!t.success&&t.error&&p.debug("deployStacksInParallel",`Stack ${s} failed`,{error:t.error.message}),i?.(s,t.success,S,t.success?void 0:t.error),d}));return f(D(c,e))}async destroyStack(e,o,r,n,a,i){const c=o.target,s=h(c,e);let u=[],t=n===!0;this.aws!==void 0&&!t&&(t=(await this.cdkService.runCdkSynth(o)).success);const S=await b(i,s,a,"destroy");if(S!==void 0)return y(S);if(this.aws!==void 0)if(t){const m=await L(this.aws.getClient(F),this.aws.getClient(O),s,{onLog:r?.onLog,onStackCleanupProgress:r?.onStackCleanupProgress},a);u=_(m)}else p.debug("ApplicationStackService","Skipping pre-empty pass after failed validation synth \u2014 the destroy will surface the error",{stackName:s});else p.debug("ApplicationStackService","No AWS provider available, skipping pre-empty pass",{stackName:s});const d=n===!0||t&&o.assemblyDir===void 0,l=await this.cdkService.runCdkDestroy(o,s,r?.onOutput,r?.onResourceProgress,this.aws,d);if(l.success)return this.aws!==void 0&&u.length>0&&await B(this.aws.getClient(O),u,{onLog:r?.onLog,onStackCleanupProgress:r?.onStackCleanupProgress},a),f({stackType:e,stackName:s,skipped:!1,outputs:void 0});{const m=A(l.error||`Failed to destroy ${e} stack`);return p.error("ApplicationStackService","Stack destroy failed",{stackType:e,target:o.target,error:m}),y(new w(m,{errorType:"destroy_failed",appName:o.target,operation:"destroyStack",stackType:e}))}}async destroyStacksInParallel(e,o,r,n,a){const i=r?.onOutput,c=r?.onResourceProgress,s=r?.onStackComplete,u=await Promise.allSettled(e.map(async t=>{const S=Date.now(),d=await this.destroyStack(t,o,{onOutput:i?k=>i(k,t):void 0,onResourceProgress:c?k=>c(k,t):void 0,onLog:r?.onLog,onStackCleanupProgress:r?.onStackCleanupProgress},n,a),l=Date.now()-S,m=d.success?"":d.error?.message??"",v=m.includes(E)||m.includes($),g={stack:t,success:d.success||v,error:d.success||v?void 0:d.error,duration:l};return!g.success&&g.error&&p.debug("destroyStacksInParallel",`Stack ${t} failed`,{error:g.error.message}),s?.(t,g.success,l,g.success?void 0:g.error),g}));return f(D(u,e))}async getStackOutputs(e,o,r){const n=h(e,o),a=await this.cloudFormationService.getStackOutputs(n,void 0,r);return a.success?f(R(a.data)):y(new w(`Failed to get outputs for ${n}`,{errorType:"stack_error",appName:e,operation:"getStackOutputs",stackType:o,details:a.error}))}async resolveWebsiteUrl(e){return K(e,this.cloudFormationService)}async destroyAllStacks(e,o,r,n,a){return I(this,e,o,r,n,a)}}export{Y as ApplicationStackService};
1
+ var N=Object.defineProperty;var P=(v,e)=>N(v,"name",{value:e,configurable:!0});import{CloudFormationClient as O}from"@aws-sdk/client-cloudformation";import{CloudWatchLogsClient as F}from"@aws-sdk/client-cloudwatch-logs";import{S3Client as A}from"@aws-sdk/client-s3";import{captureInsightsLogGroups as _,capturedSweepBuckets as I,preEmptyStackBuckets as B,reapInsightsLogGroups as E,sweepOrphanedDestroyBuckets as $}from"../../orchestration/stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as K,CDK_NO_STACKS_MATCH as M}from"../../types/constants.js";import{assertLeaseBeforeStack as W}from"../../orchestration/application/leaseGate.js";import{getApplicationStackName as w}from"../../types/operations.js";import{success as y,failure as h}from"@fjall/generator";import{ApplicationError as C}from"../../types/application/ApplicationServiceTypes.js";import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as R}from"@fjall/util";import{convertCloudFormationOutputsToRecord as D}from"../supporting/helpers.js";import{destroyAllStacks as G,mapSettledResults as L,resolveWebsiteUrl as U}from"./applicationStackHelpers.js";class te{static{P(this,"ApplicationStackService")}cdkService;cloudFormationService;aws;constructor(e,o,t){this.cdkService=e,this.cloudFormationService=o,this.aws=t}async runCdkSynth(e){return this.cdkService.runCdkSynth(e)}async deployStack(e,o,t,a){const n=o.target,u=w(n,e),c=await this.cdkService.runCdkDeploy(o,u,t?.onOutput,t?.onResourceProgress,this.aws,void 0,a?.parameters,a?.exclusively);if(c.success){const r=c.data;S.debug("ApplicationStackService","CDK deploy result",{stackName:u,stackType:e,success:!0,message:r.message,status:r.status,skipped:r.details?.skipped,hasOutput:!!r.details?.output});const i=await this.cloudFormationService.getStackOutputs(u,void 0,a?.abortSignal);S.debug("ApplicationStackService","Stack outputs after deploy",{stackName:u,hasOutputs:i.success&&i.data.length>0,outputCount:i.success?i.data.length:0});const s=i.success&&i.data.length>0?D(i.data):void 0;return y({stackType:e,stackName:u,skipped:r.details?.skipped===!0,outputs:s})}else{const r=R(c.error||`Failed to deploy ${e} infrastructure`);return S.error("ApplicationStackService","Stack deployment failed",{stackType:e,target:o.target,error:r}),h(new C(r,{errorType:"deployment_failed",appName:o.target,operation:"deployStack",stackType:e}))}}async deployStacksInParallel(e,o,t){const a=t?.onOutput,n=t?.onResourceProgress,u=t?.onStackComplete,c=await Promise.allSettled(e.map(async r=>{const i=Date.now(),s=await this.deployStack(r,o,{onOutput:a?l=>a(l,r):void 0,onResourceProgress:n?l=>n(l,r):void 0},{exclusively:!0}),p=Date.now()-i,d={stack:r,success:s.success,error:s.success?void 0:s.error,duration:p,...s.success&&s.data.outputs!==void 0?{outputs:s.data.outputs}:{}};return!s.success&&s.error&&S.debug("deployStacksInParallel",`Stack ${r} failed`,{error:s.error.message}),u?.(r,s.success,p,s.success?void 0:s.error),d}));return y(L(c,e))}async destroyStack(e,o,t,a,n,u){const c=o.target,r=w(c,e);let i=[],s=[],p=a===!0;this.aws!==void 0&&!p&&(p=(await this.cdkService.runCdkSynth(o)).success);const d=await W(u,r,n,"destroy");if(d!==void 0)return h(d);if(this.aws!==void 0)if(s=await _(this.aws.getClient(O),r,n),p){const g=await B(this.aws.getClient(O),this.aws.getClient(A),r,{onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},n);i=I(g)}else S.debug("ApplicationStackService","Skipping pre-empty pass after failed validation synth \u2014 the destroy will surface the error",{stackName:r});else S.debug("ApplicationStackService","No AWS provider available, skipping pre-empty pass",{stackName:r});const l=a===!0||p&&o.assemblyDir===void 0,f=await this.cdkService.runCdkDestroy(o,r,t?.onOutput,t?.onResourceProgress,this.aws,l);if(f.success)return this.aws!==void 0&&i.length>0&&await $(this.aws.getClient(A),i,{onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},n),this.aws!==void 0&&s.length>0&&await E(this.aws.getClient(F),s,{onLog:t?.onLog},n),y({stackType:e,stackName:r,skipped:!1,outputs:void 0});{const g=R(f.error||`Failed to destroy ${e} stack`);return S.error("ApplicationStackService","Stack destroy failed",{stackType:e,target:o.target,error:g}),h(new C(g,{errorType:"destroy_failed",appName:o.target,operation:"destroyStack",stackType:e}))}}async destroyStacksInParallel(e,o,t,a,n){const u=t?.onOutput,c=t?.onResourceProgress,r=t?.onStackComplete,i=await Promise.allSettled(e.map(async s=>{const p=Date.now(),d=await this.destroyStack(s,o,{onOutput:u?k=>u(k,s):void 0,onResourceProgress:c?k=>c(k,s):void 0,onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},a,n),l=Date.now()-p,f=d.success?"":d.error?.message??"",g=f.includes(K)||f.includes(M),m={stack:s,success:d.success||g,error:d.success||g?void 0:d.error,duration:l};return!m.success&&m.error&&S.debug("destroyStacksInParallel",`Stack ${s} failed`,{error:m.error.message}),r?.(s,m.success,l,m.success?void 0:m.error),m}));return y(L(i,e))}async getStackOutputs(e,o,t){const a=w(e,o),n=await this.cloudFormationService.getStackOutputs(a,void 0,t);return n.success?y(D(n.data)):h(new C(`Failed to get outputs for ${a}`,{errorType:"stack_error",appName:e,operation:"getStackOutputs",stackType:o,details:n.error}))}async resolveWebsiteUrl(e){return U(e,this.cloudFormationService)}async destroyAllStacks(e,o,t,a,n){return G(this,e,o,t,a,n)}}export{te as ApplicationStackService};
@@ -16,6 +16,7 @@ export interface ECSServiceInfo {
16
16
  runningCount?: number;
17
17
  pendingCount?: number;
18
18
  deployments?: Array<{
19
+ id?: string;
19
20
  status?: string;
20
21
  desiredCount?: number;
21
22
  runningCount?: number;
@@ -23,6 +24,8 @@ export interface ECSServiceInfo {
23
24
  failedTasks?: number;
24
25
  rolloutState?: string;
25
26
  rolloutStateReason?: string;
27
+ taskDefinition?: string;
28
+ createdAt?: Date;
26
29
  }>;
27
30
  events?: Array<{
28
31
  message?: string;
@@ -30,6 +33,26 @@ export interface ECSServiceInfo {
30
33
  }>;
31
34
  taskDefinition?: string;
32
35
  }
36
+ /**
37
+ * Forensic snapshot of a stopped ECS task — the fields the deployment tail
38
+ * narrates when a rolling update sheds tasks (`stoppedReason`, `stopCode`,
39
+ * per-container exit codes). All optional because ECS omits them for tasks
40
+ * stopped before placement.
41
+ */
42
+ export interface StoppedTaskInfo {
43
+ taskArn: string;
44
+ taskDefinitionArn?: string;
45
+ stoppedReason?: string;
46
+ stopCode?: string;
47
+ startedBy?: string;
48
+ createdAt?: Date;
49
+ stoppedAt?: Date;
50
+ containers: Array<{
51
+ name?: string;
52
+ exitCode?: number;
53
+ reason?: string;
54
+ }>;
55
+ }
33
56
  export interface ECSClusterInfo {
34
57
  clusterArn?: string;
35
58
  clusterName?: string;
@@ -95,4 +118,16 @@ export declare class EcsService {
95
118
  progressCallback?: (status: DeploymentStatus) => void;
96
119
  abortSignal?: AbortSignal;
97
120
  }): Promise<Result<void, EcsError>>;
121
+ /**
122
+ * List STOPPED task ARNs started by a specific originator (an ECS service
123
+ * deployment id, e.g. `ecs-svc/9223370…`). Paginated; an abort mid-way
124
+ * returns the pages gathered so far.
125
+ */
126
+ listStoppedTasks(clusterArn: string, startedBy: string, abortSignal?: AbortSignal): Promise<Result<string[], EcsError>>;
127
+ /**
128
+ * Describe stopped tasks and extract the forensic fields (`stoppedReason`,
129
+ * `stopCode`, container exit codes). Batched at the DescribeTasks limit of
130
+ * 100 ARNs per call; an abort mid-way returns the batches gathered so far.
131
+ */
132
+ describeTasks(clusterArn: string, taskArns: string[], abortSignal?: AbortSignal): Promise<Result<StoppedTaskInfo[], EcsError>>;
98
133
  }
@@ -1 +1 @@
1
- var T=Object.defineProperty;var b=(h,n)=>T(h,"name",{value:n,configurable:!0});import{ECSClient as F,UpdateServiceCommand as R,DescribeServicesCommand as $,DescribeClustersCommand as M,ListServicesCommand as N,ListClustersCommand as _,RegisterTaskDefinitionCommand as I,DescribeTaskDefinitionCommand as U}from"@aws-sdk/client-ecs";import{success as c,failure as l}from"@fjall/generator";import{BaseServiceError as x}from"../../types/errors/ServiceError.js";import{logger as E}from"@fjall/util/logger";import{getErrorMessage as w,maskSensitiveOutput as p,sleep as A}from"@fjall/util";import{getAwsErrorDetails as L}from"../../aws/utils/awsErrorHandler.js";import{composeSdkAbortSignal as g,isAborted as P}from"../../aws/organisations/types.js";class m extends x{static{b(this,"EcsError")}errorType;resourceArn;constructor(n,s,e,t,r=!1){super(`ECS_${s.toUpperCase()}`,n,t,r),this.errorType=s,this.resourceArn=e}}class z{static{b(this,"EcsService")}aws;constructor(n){this.aws=n}getClient(){return this.aws.getClient(F)}async updateService(n,s,e,t){try{const r=this.getClient(),a=new R({cluster:n,service:s,forceNewDeployment:e?.forceNewDeployment!==!1,desiredCount:e?.desiredCount,taskDefinition:e?.taskDefinition});return E.debug("EcsService",`UpdateService request: cluster=${n}, service=${s}, forceNewDeployment=${e?.forceNewDeployment!==!1}, taskDefinition=${e?.taskDefinition??"<unchanged>"}`),await r.send(a,{abortSignal:g(t)}),c(void 0)}catch(r){const{message:a,name:o,httpCode:i}=L(r),d=p(a);E.error("EcsService",`UpdateService failed: ${o} - ${d}`,{clusterArn:n,serviceArn:s,httpCode:i,error:r});let f=`Failed to update ECS service: ${d}`;return i&&(f+=` (HTTP ${i})`),o!=="Error"&&o!=="Unknown"&&(f+=` [${o}]`),l(new m(f,"unknown",s,{clusterArn:n,error:r,httpCode:i,errorName:o},!1))}}async describeServices(n,s,e){try{const t=this.getClient(),r=new $({cluster:n,services:s,include:["TAGS"]}),a=await t.send(r,{abortSignal:g(e)});if(!a.services)return c([]);const o=a.services.map(i=>({serviceArn:i.serviceArn,serviceName:i.serviceName,clusterArn:i.clusterArn,status:i.status,desiredCount:i.desiredCount,runningCount:i.runningCount,pendingCount:i.pendingCount,deployments:i.deployments,events:i.events,taskDefinition:i.taskDefinition}));return c(o)}catch(t){const r=p(w(t));return l(new m(`Failed to describe services: ${r}`,"unknown",void 0,{clusterArn:n,serviceArns:s,error:t},!1))}}async getDeploymentStatus(n,s,e){const t=await this.describeServices(n,[s],e);if(!t.success)return l(t.error);const r=t.data;if(r.length===0)return l(new m(`Service ${s} not found in cluster ${n}`,"service_not_found",s,{clusterArn:n}));const a=r[0],o=a.deployments||[],i=o.find(C=>C.status==="PRIMARY");if(!i)return c({deploymentCompleted:!1,deploymentFailed:!0,runningCount:0,desiredCount:0,pendingCount:0,deploymentCount:o.length,message:"No primary deployment found",percentComplete:0});const d=i.desiredCount||0,f=i.runningCount||0,y=i.pendingCount||0,v=d>0?Math.round(f/d*100):0,D=o.length===1&&f===d&&y===0,k=(i.failedTasks??0)>0||i.rolloutState==="FAILED";let u=`Running: ${f}/${d}`;if(u+=`, Pending: ${y}`,o.length>1&&(u+=` (${o.length} deployments active)`),k){const C=i.rolloutStateReason||(i.failedTasks?`${i.failedTasks} task(s) failed to start`:void 0);C&&(u+=` \u2014 ${C}`)}const S=a.events&&a.events.length>0?a.events[0]?.message:void 0;return c({deploymentCompleted:D,deploymentFailed:k,runningCount:f,desiredCount:d,pendingCount:y,deploymentCount:o.length,message:u,percentComplete:v,latestEvent:S})}async isDeploying(n,s){const e=await this.getDeploymentStatus(n,s);if(!e.success)return E.debug("EcsService","Could not determine deployment status, assuming not deploying",{error:e.error.message}),c(!1);const t=e.data;return c(!t.deploymentCompleted&&!t.deploymentFailed)}async getServiceEvents(n,s,e){try{const t=this.getClient(),r=new $({cluster:n,services:[s],include:["TAGS"]}),a=await t.send(r,{abortSignal:g(e)});if(!a.services||a.services.length===0)return c([]);const i=(a.services[0].events||[]).map(d=>({message:d.message||"",timestamp:d.createdAt||new Date}));return c(i)}catch(t){const r=p(w(t));return l(new m(`Failed to get service events: ${r}`,"unknown",s,{clusterArn:n,error:t}))}}async pollDeployment(n){const{clusterArn:s,serviceArn:e,waitForCompletion:t=!0,maxWaitTime:r=6e5,pollInterval:a=5e3,progressCallback:o,abortSignal:i}=n;if(!t)return c(void 0);const d=Date.now(),f=new Date(d-5e3);let y=null,v="";const D=new Set;for(;Date.now()-d<r;){if(P(i))return l(new m("Deployment polling cancelled by abort signal","unknown",e,{clusterArn:s}));const k=await this.getDeploymentStatus(s,e,i);if(!k.success)return l(k.error);const u=k.data,S=await this.getServiceEvents(s,e,i);if(S.success){const C=this.processDeploymentEvents(S.data,u,f,y,D,o);y=C.lastEventTimestamp,C.eventsReported?v=u.message:u.message!==v&&(o?.(u),v=u.message)}if(u.deploymentCompleted)return c(void 0);if(u.deploymentFailed)return l(new m("ECS deployment failed. Check the ECS console for detailed error information","deployment_failed",e,{clusterArn:s,status:u}));await A(a)}return l(new m(`ECS deployment timed out after ${r/1e3} seconds. The deployment is still in progress. Check the ECS console for status`,"deployment_timeout",e,{clusterArn:s,maxWaitTime:r}))}processDeploymentEvents(n,s,e,t,r,a){const o=n.filter(i=>!(i.timestamp<e||t&&i.timestamp<=t||r.has(i.message)));if(o.length===0)return{lastEventTimestamp:t,eventsReported:!1};for(const i of o)r.add(i.message),a?.({...s,latestEvent:i.message});return{lastEventTimestamp:o[o.length-1].timestamp,eventsReported:!0}}async listClusters(n){try{const s=this.getClient(),e=new _({}),t=await s.send(e,{abortSignal:g(n)});return c(t.clusterArns||[])}catch(s){const e=p(w(s));return l(new m(`Failed to list clusters: ${e}`,"permission_denied",void 0,s))}}async describeClusters(n,s){try{const e=this.getClient(),t=new M({clusters:n}),r=await e.send(t,{abortSignal:g(s)});if(!r.clusters)return c([]);const a=r.clusters.map(o=>({clusterArn:o.clusterArn,clusterName:o.clusterName,status:o.status,registeredContainerInstancesCount:o.registeredContainerInstancesCount,runningTasksCount:o.runningTasksCount,pendingTasksCount:o.pendingTasksCount}));return c(a)}catch(e){const t=p(w(e));return l(new m(`Failed to describe clusters: ${t}`,"unknown",void 0,{clusterArns:n,error:e},!1))}}async listServices(n,s){try{const e=this.getClient(),t=new N({cluster:n}),r=await e.send(t,{abortSignal:g(s)});return c(r.serviceArns||[])}catch(e){const t=p(w(e));return l(new m(`Failed to list services: ${t}`,"unknown",n,{clusterArn:n,error:e},!1))}}async registerTaskDefinition(n,s){try{const e=this.getClient(),t=new I(n),r=await e.send(t,{abortSignal:g(s)});return r.taskDefinition?.taskDefinitionArn?c(r.taskDefinition.taskDefinitionArn):l(new m("Task definition registration failed - no ARN returned","task_definition_failed",void 0,n))}catch(e){const t=p(w(e));return l(new m(`Failed to register task definition: ${t}`,"task_definition_failed",void 0,{taskDefinition:n,error:e},!1))}}async getLatestTaskDefinition(n,s){try{const e=this.getClient(),t=new U({taskDefinition:n}),r=await e.send(t,{abortSignal:g(s)});return c(r.taskDefinition)}catch(e){const t=p(w(e));return l(new m(`Failed to get task definition: ${t}`,"unknown",void 0,{family:n,error:e},!1))}}async deployService(n,s,e){const t=await this.updateService(n,s,{forceNewDeployment:!0,taskDefinition:e?.taskDefinition},e?.abortSignal);if(!t.success)return l(t.error);if(e?.waitForCompletion!==!1){const r=await this.pollDeployment({clusterArn:n,serviceArn:s,waitForCompletion:!0,progressCallback:e?.progressCallback,abortSignal:e?.abortSignal});if(!r.success)return l(r.error)}return c(void 0)}}export{m as EcsError,z as EcsService};
1
+ var F=Object.defineProperty;var b=(v,n)=>F(v,"name",{value:n,configurable:!0});import{ECSClient as M,UpdateServiceCommand as _,DescribeServicesCommand as $,DescribeClustersCommand as A,ListServicesCommand as N,ListClustersCommand as x,ListTasksCommand as I,DescribeTasksCommand as L,RegisterTaskDefinitionCommand as P,DescribeTaskDefinitionCommand as U}from"@aws-sdk/client-ecs";import{success as c,failure as d}from"@fjall/generator";import{BaseServiceError as B}from"../../types/errors/ServiceError.js";import{logger as E}from"@fjall/util/logger";import{getErrorMessage as C,maskSensitiveOutput as p,sleep as G}from"@fjall/util";import{getAwsErrorDetails as H}from"../../aws/utils/awsErrorHandler.js";import{composeSdkAbortSignal as g,isAborted as T}from"../../aws/organisations/types.js";class u extends B{static{b(this,"EcsError")}errorType;resourceArn;constructor(n,s,e,t,r=!1){super(`ECS_${s.toUpperCase()}`,n,t,r),this.errorType=s,this.resourceArn=e}}const R=100;class J{static{b(this,"EcsService")}aws;constructor(n){this.aws=n}getClient(){return this.aws.getClient(M)}async updateService(n,s,e,t){try{const r=this.getClient(),a=new _({cluster:n,service:s,forceNewDeployment:e?.forceNewDeployment!==!1,desiredCount:e?.desiredCount,taskDefinition:e?.taskDefinition});return E.debug("EcsService",`UpdateService request: cluster=${n}, service=${s}, forceNewDeployment=${e?.forceNewDeployment!==!1}, taskDefinition=${e?.taskDefinition??"<unchanged>"}`),await r.send(a,{abortSignal:g(t)}),c(void 0)}catch(r){const{message:a,name:i,httpCode:o}=H(r),l=p(a);E.error("EcsService",`UpdateService failed: ${i} - ${l}`,{clusterArn:n,serviceArn:s,httpCode:o,error:r});let f=`Failed to update ECS service: ${l}`;return o&&(f+=` (HTTP ${o})`),i!=="Error"&&i!=="Unknown"&&(f+=` [${i}]`),d(new u(f,"unknown",s,{clusterArn:n,error:r,httpCode:o,errorName:i},!1))}}async describeServices(n,s,e){try{const t=this.getClient(),r=new $({cluster:n,services:s,include:["TAGS"]}),a=await t.send(r,{abortSignal:g(e)});if(!a.services)return c([]);const i=a.services.map(o=>({serviceArn:o.serviceArn,serviceName:o.serviceName,clusterArn:o.clusterArn,status:o.status,desiredCount:o.desiredCount,runningCount:o.runningCount,pendingCount:o.pendingCount,deployments:o.deployments,events:o.events,taskDefinition:o.taskDefinition}));return c(i)}catch(t){const r=p(C(t));return d(new u(`Failed to describe services: ${r}`,"unknown",void 0,{clusterArn:n,serviceArns:s,error:t},!1))}}async getDeploymentStatus(n,s,e){const t=await this.describeServices(n,[s],e);if(!t.success)return d(t.error);const r=t.data;if(r.length===0)return d(new u(`Service ${s} not found in cluster ${n}`,"service_not_found",s,{clusterArn:n}));const a=r[0],i=a.deployments||[],o=i.find(k=>k.status==="PRIMARY");if(!o)return c({deploymentCompleted:!1,deploymentFailed:!0,runningCount:0,desiredCount:0,pendingCount:0,deploymentCount:i.length,message:"No primary deployment found",percentComplete:0});const l=o.desiredCount||0,f=o.runningCount||0,w=o.pendingCount||0,S=l>0?Math.round(f/l*100):0,D=i.length===1&&f===l&&w===0,y=(o.failedTasks??0)>0||o.rolloutState==="FAILED";let m=`Running: ${f}/${l}`;if(m+=`, Pending: ${w}`,i.length>1&&(m+=` (${i.length} deployments active)`),y){const k=o.rolloutStateReason||(o.failedTasks?`${o.failedTasks} task(s) failed to start`:void 0);k&&(m+=` \u2014 ${k}`)}const h=a.events&&a.events.length>0?a.events[0]?.message:void 0;return c({deploymentCompleted:D,deploymentFailed:y,runningCount:f,desiredCount:l,pendingCount:w,deploymentCount:i.length,message:m,percentComplete:S,latestEvent:h})}async isDeploying(n,s){const e=await this.getDeploymentStatus(n,s);if(!e.success)return E.debug("EcsService","Could not determine deployment status, assuming not deploying",{error:e.error.message}),c(!1);const t=e.data;return c(!t.deploymentCompleted&&!t.deploymentFailed)}async getServiceEvents(n,s,e){try{const t=this.getClient(),r=new $({cluster:n,services:[s],include:["TAGS"]}),a=await t.send(r,{abortSignal:g(e)});if(!a.services||a.services.length===0)return c([]);const o=(a.services[0].events||[]).map(l=>({message:l.message||"",timestamp:l.createdAt||new Date}));return c(o)}catch(t){const r=p(C(t));return d(new u(`Failed to get service events: ${r}`,"unknown",s,{clusterArn:n,error:t}))}}async pollDeployment(n){const{clusterArn:s,serviceArn:e,waitForCompletion:t=!0,maxWaitTime:r=6e5,pollInterval:a=5e3,progressCallback:i,abortSignal:o}=n;if(!t)return c(void 0);const l=Date.now(),f=new Date(l-5e3);let w=null,S="";const D=new Set;for(;Date.now()-l<r;){if(T(o))return d(new u("Deployment polling cancelled by abort signal","unknown",e,{clusterArn:s}));const y=await this.getDeploymentStatus(s,e,o);if(!y.success)return d(y.error);const m=y.data,h=await this.getServiceEvents(s,e,o);if(h.success){const k=this.processDeploymentEvents(h.data,m,f,w,D,i);w=k.lastEventTimestamp,k.eventsReported?S=m.message:m.message!==S&&(i?.(m),S=m.message)}if(m.deploymentCompleted)return c(void 0);if(m.deploymentFailed)return d(new u("ECS deployment failed. Check the ECS console for detailed error information","deployment_failed",e,{clusterArn:s,status:m}));await G(a)}return d(new u(`ECS deployment timed out after ${r/1e3} seconds. The deployment is still in progress. Check the ECS console for status`,"deployment_timeout",e,{clusterArn:s,maxWaitTime:r}))}processDeploymentEvents(n,s,e,t,r,a){const i=n.filter(o=>!(o.timestamp<e||t&&o.timestamp<=t||r.has(o.message)));if(i.length===0)return{lastEventTimestamp:t,eventsReported:!1};for(const o of i)r.add(o.message),a?.({...s,latestEvent:o.message});return{lastEventTimestamp:i[i.length-1].timestamp,eventsReported:!0}}async listClusters(n){try{const s=this.getClient(),e=new x({}),t=await s.send(e,{abortSignal:g(n)});return c(t.clusterArns||[])}catch(s){const e=p(C(s));return d(new u(`Failed to list clusters: ${e}`,"permission_denied",void 0,s))}}async describeClusters(n,s){try{const e=this.getClient(),t=new A({clusters:n}),r=await e.send(t,{abortSignal:g(s)});if(!r.clusters)return c([]);const a=r.clusters.map(i=>({clusterArn:i.clusterArn,clusterName:i.clusterName,status:i.status,registeredContainerInstancesCount:i.registeredContainerInstancesCount,runningTasksCount:i.runningTasksCount,pendingTasksCount:i.pendingTasksCount}));return c(a)}catch(e){const t=p(C(e));return d(new u(`Failed to describe clusters: ${t}`,"unknown",void 0,{clusterArns:n,error:e},!1))}}async listServices(n,s){try{const e=this.getClient(),t=new N({cluster:n}),r=await e.send(t,{abortSignal:g(s)});return c(r.serviceArns||[])}catch(e){const t=p(C(e));return d(new u(`Failed to list services: ${t}`,"unknown",n,{clusterArn:n,error:e},!1))}}async registerTaskDefinition(n,s){try{const e=this.getClient(),t=new P(n),r=await e.send(t,{abortSignal:g(s)});return r.taskDefinition?.taskDefinitionArn?c(r.taskDefinition.taskDefinitionArn):d(new u("Task definition registration failed - no ARN returned","task_definition_failed",void 0,n))}catch(e){const t=p(C(e));return d(new u(`Failed to register task definition: ${t}`,"task_definition_failed",void 0,{taskDefinition:n,error:e},!1))}}async getLatestTaskDefinition(n,s){try{const e=this.getClient(),t=new U({taskDefinition:n}),r=await e.send(t,{abortSignal:g(s)});return c(r.taskDefinition)}catch(e){const t=p(C(e));return d(new u(`Failed to get task definition: ${t}`,"unknown",void 0,{family:n,error:e},!1))}}async deployService(n,s,e){const t=await this.updateService(n,s,{forceNewDeployment:!0,taskDefinition:e?.taskDefinition},e?.abortSignal);if(!t.success)return d(t.error);if(e?.waitForCompletion!==!1){const r=await this.pollDeployment({clusterArn:n,serviceArn:s,waitForCompletion:!0,progressCallback:e?.progressCallback,abortSignal:e?.abortSignal});if(!r.success)return d(r.error)}return c(void 0)}async listStoppedTasks(n,s,e){try{const t=this.getClient(),r=[];let a;do{if(T(e))break;const i=await t.send(new I({cluster:n,startedBy:s,desiredStatus:"STOPPED",...a!==void 0&&{nextToken:a}}),{abortSignal:g(e)});r.push(...i.taskArns??[]),a=i.nextToken}while(a!==void 0);return c(r)}catch(t){const r=p(C(t));return d(new u(`Failed to list stopped tasks: ${r}`,"unknown",void 0,{clusterArn:n,startedBy:s,error:t},!1))}}async describeTasks(n,s,e){if(s.length===0)return c([]);try{const t=this.getClient(),r=[];for(let a=0;a<s.length&&!T(e);a+=R){const i=await t.send(new L({cluster:n,tasks:s.slice(a,a+R)}),{abortSignal:g(e)});for(const o of i.tasks??[])o.taskArn!==void 0&&r.push({taskArn:o.taskArn,taskDefinitionArn:o.taskDefinitionArn,stoppedReason:o.stoppedReason,stopCode:o.stopCode,startedBy:o.startedBy,createdAt:o.createdAt,stoppedAt:o.stoppedAt,containers:(o.containers??[]).map(l=>({name:l.name,exitCode:l.exitCode,reason:l.reason}))})}return c(r)}catch(t){const r=p(C(t));return d(new u(`Failed to describe tasks: ${r}`,"unknown",void 0,{clusterArn:n,taskCount:s.length,error:t},!1))}}}export{u as EcsError,J as EcsService};
@@ -1,6 +1,6 @@
1
1
  import type { DeployCallbacks } from "./callbacks.js";
2
2
  /**
3
- * Every {@link DeployCallbacks} key (47 callbacks + the `verbose` flag),
3
+ * Every {@link DeployCallbacks} key (48 callbacks + the `verbose` flag),
4
4
  * derived from the type-checked {@link CALLBACK_KEY_PRESENCE} map so the list
5
5
  * can never silently diverge from the interface.
6
6
  */
@@ -1 +1 @@
1
- const e={onStepStart:!0,onStepComplete:!0,onProgress:!0,onLog:!0,onOutput:!0,onResourceProgress:!0,onParallelPhaseStart:!0,onParallelPhaseComplete:!0,onParallelStackResourceProgress:!0,onDockerProgress:!0,onBuildPushStart:!0,onBuildPushProgress:!0,onBuildPushComplete:!0,onTaskDefRegistered:!0,onECSUpdate:!0,onECSProgress:!0,onECSComplete:!0,onMigrationsStart:!0,onMigrationsComplete:!0,onCDKBootstrap:!0,onCdkOutput:!0,onCascadeStart:!0,onCascadeAccountsReconciled:!0,onOrgChangesDetected:!0,onCascadeMissingAccounts:!0,onCascadePhaseStart:!0,onCascadePhaseComplete:!0,onCascadeAccountStart:!0,onCascadeAccountPhaseChange:!0,onCascadeAccountResourceProgress:!0,onCascadeAccountComplete:!0,onCascadeComplete:!0,onCascadeLedger:!0,onTrailMigrationPhase:!0,onTrailLifecycleChanged:!0,onStackCleanupProgress:!0,onDetectionComplete:!0,onDetectionPhaseChange:!0,onStackTargets:!0,onSSOUrl:!0,onAuthComplete:!0,onOpenNextBuildStart:!0,onOpenNextProgress:!0,onOpenNextBuildComplete:!0,onOpenNextBuildError:!0,onFailureAnalysis:!0,onError:!0,verbose:!0},t=Object.keys(e);export{t as ALL_CALLBACK_KEYS};
1
+ const e={onStepStart:!0,onStepComplete:!0,onProgress:!0,onLog:!0,onOutput:!0,onResourceProgress:!0,onParallelPhaseStart:!0,onParallelPhaseComplete:!0,onParallelStackResourceProgress:!0,onDockerProgress:!0,onBuildPushStart:!0,onBuildPushProgress:!0,onBuildPushComplete:!0,onTaskDefRegistered:!0,onECSUpdate:!0,onECSProgress:!0,onECSComplete:!0,onMigrationsStart:!0,onMigrationsComplete:!0,onCDKBootstrap:!0,onCdkOutput:!0,onCascadeStart:!0,onCascadeAccountsReconciled:!0,onOrgChangesDetected:!0,onCascadeMissingAccounts:!0,onCascadePhaseStart:!0,onCascadePhaseComplete:!0,onCascadeAccountStart:!0,onCascadeAccountPhaseChange:!0,onCascadeAccountResourceProgress:!0,onCascadeAccountComplete:!0,onCascadeComplete:!0,onCascadeLedger:!0,onTrailMigrationPhase:!0,onTrailLifecycleChanged:!0,onStackCleanupProgress:!0,onDetectionComplete:!0,onDetectionPhaseChange:!0,onStackTargets:!0,onSSOUrl:!0,onAuthComplete:!0,onOpenNextBuildStart:!0,onOpenNextProgress:!0,onOpenNextBuildComplete:!0,onOpenNextBuildError:!0,onFailureAnalysis:!0,onDeploymentFailure:!0,onError:!0,verbose:!0},t=Object.keys(e);export{t as ALL_CALLBACK_KEYS};
@@ -1,5 +1,6 @@
1
1
  import type { ProgressEvent, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent, TrailMigrationPhaseEvent } from "./events.js";
2
2
  import type { TrailLifecycleState } from "@fjall/util/config";
3
+ import type { DeploymentFailure } from "@fjall/util";
3
4
  import type { FailureAnalysis } from "@fjall/util/aws";
4
5
  import type { DetectionResult } from "./detection/detection.js";
5
6
  import type { CascadeLedger } from "../orchestration/organisation/cascadeSummary.js";
@@ -275,6 +276,16 @@ export interface DeployCallbacks {
275
276
  * webapp). Pairs with `onError`, which still carries the terminal Error.
276
277
  */
277
278
  onFailureAnalysis?: (analysis: FailureAnalysis) => void;
279
+ /**
280
+ * @emittedBy engine — fired once at terminal failure with the classified
281
+ * `DeploymentFailure` record (taxonomy code/phase/fault, provider layer,
282
+ * remediation, excerpt; aiDocs design 2026-08-10-deployment-failure-taxonomy
283
+ * § 3). Sibling to `onFailureAnalysis`, which it will eventually subsume —
284
+ * the CFN analysis nests inside the record's `analysis` field. The same
285
+ * record is attached to the terminal Result error and read back via
286
+ * `getDeploymentFailure`.
287
+ */
288
+ onDeploymentFailure?: (failure: DeploymentFailure) => void;
278
289
  /** @emittedBy engine */
279
290
  onError?: (error: Error) => void;
280
291
  verbose?: boolean;
@@ -1 +1 @@
1
- var d=Object.defineProperty;var n=(t,e)=>d(t,"name",{value:e,configurable:!0});import{z as a}from"zod";import{randomBytes as f}from"crypto";import{readFile as m,writeFile as S,unlink as c,rename as g,mkdir as h}from"fs/promises";import{dirname as y,join as p}from"path";import{fileExists as F}from"@fjall/util/fsHelpers";import{logger as i}from"@fjall/util/logger";import{getErrorMessage as l,maskSensitiveOutput as w}from"@fjall/util";const x=a.object({hash:a.string(),deployedAt:a.string(),stackStatus:a.string().optional()}).strict(),j=a.object({cloudformationStackName:a.string(),hostedZoneId:a.string().optional(),directoryHash:a.string().optional()}).strict(),E=a.object({version:a.literal(1),lastDeployedAt:a.string().optional(),templateHashes:a.record(a.string(),x),domain:j.optional(),metadata:a.record(a.string(),a.unknown()).optional()}).strict(),A=".fjall-state.json";function s(t){return p(t,A)}n(s,"getStateFilePath");function H(t){return t.replace(/-/g,"")}n(H,"regionSuffix");function J(t,e,r){return p(t,`.fjall-state.cascade-${e}-${H(r)}.json`)}n(J,"getCascadeStateFilePath");async function b(t){if(!await F(t))return null;try{const e=await m(t,"utf-8"),r=JSON.parse(e),o=E.safeParse(r);return o.success?o.data:null}catch(e){return i.debug("FjallState","Failed to read state file",{path:t,error:l(e)}),null}}n(b,"readStateFileAt");async function k(t,e){const r=`${t}.${Date.now()}-${f(4).toString("hex")}.tmp`;await h(y(t),{recursive:!0});try{await S(r,JSON.stringify(e,null,2),"utf-8"),await g(r,t)}catch(o){try{await c(r)}catch(u){i.debug("FjallState","Temp file cleanup failed (non-fatal)",{path:r,error:l(u)})}throw o}}n(k,"writeStateFileAt");async function M(t){return b(s(t))}n(M,"readStateFile");async function _(t,e){return k(s(t),e)}n(_,"writeStateFile");function z(){return{version:1,templateHashes:{}}}n(z,"createEmptyState");async function C(t){const e=s(t);try{await c(e)}catch(r){(typeof r=="object"&&r!==null&&"code"in r?r.code:void 0)!=="ENOENT"&&i.warn("FjallState","Failed to delete state file",{path:e,error:w(l(r))})}}n(C,"deleteStateFile");function L(t,e,r,o){return{...t,lastDeployedAt:new Date().toISOString(),templateHashes:{...t.templateHashes,[e]:{hash:r,deployedAt:new Date().toISOString(),...o!==void 0&&{stackStatus:o}}}}}n(L,"updateTemplateHash");export{E as FjallStateFileSchema,z as createEmptyState,C as deleteStateFile,J as getCascadeStateFilePath,s as getStateFilePath,M as readStateFile,b as readStateFileAt,H as regionSuffix,L as updateTemplateHash,_ as writeStateFile,k as writeStateFileAt};
1
+ var d=Object.defineProperty;var n=(t,e)=>d(t,"name",{value:e,configurable:!0});import{z as a}from"zod";import{randomBytes as f}from"crypto";import{readFile as m,writeFile as S,unlink as p,rename as g,mkdir as h}from"fs/promises";import{dirname as y,join as u}from"path";import{fileExists as F}from"@fjall/util/fsHelpers";import{logger as i}from"@fjall/util/logger";import{getErrorMessage as s,maskSensitiveOutput as w}from"@fjall/util";const j=a.object({hash:a.string(),deployedAt:a.string(),stackStatus:a.string().optional()}).strict(),x=a.object({cloudformationStackName:a.string(),hostedZoneId:a.string().optional(),directoryHash:a.string().optional()}).strict(),E=a.object({version:a.literal(1),lastDeployedAt:a.string().optional(),templateHashes:a.record(a.string(),j),domain:x.optional(),metadata:a.record(a.string(),a.unknown()).optional()}).strict(),A=".fjall-state.json";function c(t){return u(t,A)}n(c,"getStateFilePath");function b(t){return t.replace(/-/g,"")}n(b,"regionSuffix");function J(t,e,r){return u(t,`.fjall-state.cascade-${e}-${b(r)}.json`)}n(J,"getCascadeStateFilePath");async function H(t){if(!await F(t))return null;try{const e=await m(t,"utf-8"),r=JSON.parse(e),o=E.safeParse(r);return o.success?o.data:(i.debug("FjallState","State file failed schema validation",{path:t,issues:o.error.issues.map(l=>l.message)}),null)}catch(e){return i.debug("FjallState","Failed to read state file",{path:t,error:s(e)}),null}}n(H,"readStateFileAt");async function k(t,e){const r=`${t}.${Date.now()}-${f(4).toString("hex")}.tmp`;await h(y(t),{recursive:!0});try{await S(r,JSON.stringify(e,null,2),"utf-8"),await g(r,t)}catch(o){try{await p(r)}catch(l){i.debug("FjallState","Temp file cleanup failed (non-fatal)",{path:r,error:s(l)})}throw o}}n(k,"writeStateFileAt");async function M(t){return H(c(t))}n(M,"readStateFile");async function _(t,e){return k(c(t),e)}n(_,"writeStateFile");function z(){return{version:1,templateHashes:{}}}n(z,"createEmptyState");async function C(t){const e=c(t);try{await p(e)}catch(r){(typeof r=="object"&&r!==null&&"code"in r?r.code:void 0)!=="ENOENT"&&i.warn("FjallState","Failed to delete state file",{path:e,error:w(s(r))})}}n(C,"deleteStateFile");function L(t,e,r,o){return{...t,lastDeployedAt:new Date().toISOString(),templateHashes:{...t.templateHashes,[e]:{hash:r,deployedAt:new Date().toISOString(),...o!==void 0&&{stackStatus:o}}}}}n(L,"updateTemplateHash");export{E as FjallStateFileSchema,z as createEmptyState,C as deleteStateFile,J as getCascadeStateFilePath,c as getStateFilePath,M as readStateFile,H as readStateFileAt,b as regionSuffix,L as updateTemplateHash,_ as writeStateFile,k as writeStateFileAt};