@fjall/deploy-core 2.33.0 → 2.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 218 files minified at 2026-07-17T12:49:59.029Z
1
+ 218 files minified at 2026-07-17T22:47:38.950Z
@@ -1 +1 @@
1
- import{isAbsolute as M,join as _,dirname as z,resolve as F}from"node:path";import{existsSync as j}from"node:fs";import{success as N,failure as h}from"@fjall/generator";import{deriveContentHashTag as J,getErrorMessage as U,maskSensitiveOutput as b,toKebab as x}from"@fjall/util";import{evaluateBakeGuard as X,evaluateResolvedBuildArgValues as Y,acknowledgedBuildArgKeys as q,createBakeWarningDeduper as Q,buildCacheRepositoryName as G,BAKE_GUARD_EXPOSURE_CLAUSE as Z}from"@fjall/util/docker";import{runWithBoundedConcurrency as ee,resolveBuildConcurrency as re}from"@fjall/util/buildConcurrency";import{logger as L}from"@fjall/util/logger";import{parseDockerServicesFromManifest as te,parseLambdaDockerServicesFromManifest as ne}from"@fjall/util/manifest";import{STEP_IDS as A}from"../../types/stepDefinitions.js";import{isAborted as V}from"../../aws/organisations/types.js";import{resolveBuildSecrets as oe,resolveSecretRefValue as ie,resolveBuildSecretSessionProvider as se,sourcedRefsFromBuildArgs as ae}from"./buildSecretResolver.js";import{partitionForRegion as de}from"./buildSecretSession.js";import{resolveBuildArgs as ce}from"./buildArgResolver.js";import{validateBuildGroup as ue}from"./buildGroupValidator.js";import{ensureBuildCacheRepository as le}from"./ecrCacheRepository.js";const T="dockerBuildHelper",C="Building and pushing Docker image";function fe(e,n,t){const r=_(n,e);if(j(r))return{path:r,base:n};if(t!==""&&t!==n){const o=_(t,e);if(j(o))return{path:o,base:t}}return{path:r,base:n}}function me(e,n,t){const r=e.docker.path,o=e.docker.context;let s,a;if(M(r))s=r,a=n;else{const g=fe(r,n,t);s=g.path,a=g.base}return{buildContext:o?M(o)?F(o):F(a,o):F(z(s)),dockerfilePath:s,target:e.docker.target,resolvedViaFallback:a!==n}}function ge(e){return`${e.buildContext}|${e.dockerfilePath}|${e.target??""}`}function pe(e,n){return`Dockerfile not found for service '${e}' at ${n}. Checked the synth directory and the working directory. If your Dockerfile lives at the project root with the CDK app in a subdirectory, set an absolute docker.context (the build context must share the Dockerfile's directory).`}function he(e){return e.startsWith("cn-")?"amazonaws.com.cn":"amazonaws.com"}function W(e,n){return`${e}.dkr.ecr.${n}.${he(n)}`}function be(e,n,t){return`${W(e,n)}/${x(t)}`}function ye(e,n,t){return`${W(e,n)}/${G(x(t))}`}function ve(e,n,t){const r=new Map;for(const o of e){const s=me(o,n,t),a=ge(s),m=r.get(a);m?m.members.push(o):r.set(a,{identity:s,members:[o]})}return Array.from(r.values())}async function Se(e,n,t,r,o){const s=await e.buildAndPush({appName:n.appName,appPath:n.path,region:t,accountId:r},(a,m,g,S)=>{o.onDockerProgress?.(b(a),m,g,S)});return s.success?N({imageUri:s.data.imageUri,imageTag:s.data.imageTag}):h(s.error)}function H(){return{contentHashTagsByService:{},imageDigestsByService:{}}}async function we(e,n,t,r,o,s,a,m,g,S,E,f,k,w){const l=e.members[0];if(!j(e.identity.dockerfilePath))return h(new Error(pe(l.name,e.identity.dockerfilePath)));e.identity.resolvedViaFallback&&m.onLog?.(`Building service '${l.name}' from ${e.identity.dockerfilePath} (resolved at the project root).`,"info");const v=[...l.docker.buildSecrets??[],...k??[]],P=ue({serviceName:l.name,dockerfilePath:e.identity.dockerfilePath,buildContext:e.identity.buildContext,...e.identity.target!==void 0&&{target:e.identity.target},...l.docker.buildArgs!==void 0&&{buildArgs:l.docker.buildArgs},...v.length>0&&{buildSecrets:v},members:e.members.map(i=>({serviceName:i.name,...i.docker.buildArgs!==void 0&&{buildArgs:i.docker.buildArgs},...i.docker.buildSecrets!==void 0&&{buildSecrets:i.docker.buildSecrets}}))});if(!P.success)return h(P.error);const d=X(l.docker.buildArgs,x(t.appName));for(const i of d.warnings){const y=b(i.message);w(y)&&m.onLog?.(y,"warn")}if(d.findings.length>0)return h(new Error(b(d.findings.map(i=>i.message).join(" "))));const c=v,D=ae(l.docker.buildArgs),I={appName:t.appName,region:r,accountId:o,partition:de(r)},$=await se(g,[...c,...D],I);if(!$.success)return h($.error);const O=$.data,R=await ce({manifestBuildArgs:l.docker.buildArgs,serviceDir:e.identity.buildContext,appDir:t.path,...E!==void 0&&{stage:E},shellEnv:process.env,...f!==void 0&&{flagBuildArgs:f},resolveSourcedValue:(i,y)=>ie(O,{id:i,...y},S)});if(!R.success)return h(R.error);const B=R.data.buildArgs;for(const i of R.data.inferredDotenvKeys){const y=b(`buildArg "${i}" was auto-declared from a .env file and will be baked into the image \u2014 ${Z}.`);w(y)&&m.onLog?.(y,"warn")}const u=Y(B,x(t.appName),q(l.docker.buildArgs));if(u.length>0)return h(new Error(b(u.map(i=>i.message).join(" "))));let p;if(c.length>0){const i=await oe(O,c,S);if(!i.success)return h(i.error);p=i.data}try{if(V(S))return h(new Error(`Build of service '${l.name}' was aborted before the docker build started`));const i=e.members.some(K=>K.isLambda===!0),y={appName:t.appName,appPath:e.identity.buildContext,region:r,accountId:o,buildContext:e.identity.buildContext,dockerfilePath:e.identity.dockerfilePath,pushByDigest:!0,serviceName:l.name,targetsLambda:i,...a!==void 0&&{cacheRepoUri:a},...e.identity.target!==void 0&&{target:e.identity.target},...Object.keys(B).length>0&&{buildArgs:B},...p!==void 0&&{buildSecrets:p.resolved}};return await Ee(e,y,n,s,m)}finally{p!==void 0&&await p.cleanup().catch(()=>m.onLog?.("Build-secret cleanup failed.","warn"))}}async function Ee(e,n,t,r,o){const s=await t.buildAndPush(n,(f,k,w,l)=>{o.onDockerProgress?.(b(f),k,w,l)});if(!s.success)return h(s.error);const a=s.data.imageDigest;if(typeof a!="string"||!a.startsWith("sha256:"))return h(new Error(`Buildx push succeeded but returned an invalid digest: ${b(String(a))}`));const m={},g={},S=[];for(const f of e.members){const k=f.docker.target,w=J(a,f.name,k);if(!w.success)return h(w.error);const l=w.data,v=f.name;m[v]=l,g[v]=a,S.push(`${r}:${l}`)}const E=await t.tagByDigest({sourceImage:r,digest:a,tags:S});return E.success?N({contentHashTagsByService:m,imageDigestsByService:g}):h(E.error)}function ke(e,n){const t=new Map(e.map(r=>[r.name,r]));for(const r of n){const o=t.get(r.name);if(o!==void 0&&JSON.stringify(o.docker)!==JSON.stringify(r.docker))throw new Error(`ECS service "${o.name}" and Lambda image key "${r.name}" share one build key but declare different docker configs \u2014 the second build would silently overwrite the first and both would deploy the same image. Rename one of them, or make the docker configs identical to deliberately share the build.`)}}async function Fe(e,n,t,r){const o=e.dockerProvider;if(!o)return N(H());const s=n.awsProvider.getAccountId();if(!s)return r.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),N(H());const a=n.awsProvider.getRegion(),m=_(t.path,"cdk.out");let g;try{const d=te(m),c=ne(m);ke(d,c),g=[...d,...c]}catch(d){const c=new Error(b(U(d)));return r.onError?.(c),h(c)}const S=be(s,a,t.appName);if(L.debug(T,"runDockerBuild starting",{appName:t.appName,appPath:t.path,accountId:s,region:a,cdkOutPath:m,manifestServiceCount:g.length,manifestServices:g.map(d=>({name:d.name,path:d.docker.path,context:d.docker.context,target:d.docker.target})),stepId:A.DOCKER_OPERATIONS,stepName:C}),r.onStepStart?.(A.DOCKER_OPERATIONS,C),g.length===0){r.onLog?.("Initialising ECR repository\u2026","info");const d=await o.initialiseECR({appName:t.appName,region:a,accountId:s});d.success||r.onLog?.(`ECR initialisation failed: ${b(d.error.message)}`,"warn"),r.onLog?.("Building and pushing Docker image\u2026","info");const c=await Se(o,t,a,s,r);if(!c.success){L.debug(T,"Docker buildAndPush (legacy) failed",{appName:t.appName,error:b(c.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const D=new Error(b(c.error.message));return r.onError?.(D),h(D)}return r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker image pushed: ${c.data.imageUri}`,"info"),N(H())}const E=await o.preflight?.();if(E!==void 0&&!E.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const d=new Error(b(E.error.message));return r.onError?.(d),h(d)}const f=ve(g,t.path,e.workingDirectory);let k=ye(s,a,t.appName);const w=await le(n.awsProvider,G(x(t.appName)),e.abortSignal);w.success||(k=void 0,r.onLog?.(`Building without registry cache \u2014 ${w.error.message}`,"warn")),r.onLog?.(`Building and pushing ${g.length} service image(s) in ${f.length} group(s)\u2026`,"info");let l=!1;if(o.beginEcrSession!==void 0){const d=await o.beginEcrSession({accountId:s,region:a});if(!d.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const c=new Error(b(d.error.message));return r.onError?.(c),h(c)}l=!0}const v=new AbortController,P=()=>v.abort();e.abortSignal!==void 0&&(e.abortSignal.aborted?v.abort():e.abortSignal.addEventListener("abort",P,{once:!0}));try{const d=()=>V(e.abortSignal)?"the deployment was cancelled":"a sibling group failed";let c;const D=re(f.length),I=Q(),$=f.map((u,p)=>async()=>{const i=u.members.map(K=>K.name).join(", ");if(v.signal.aborted)throw new Error(`Build of group ${p+1}/${f.length} (${i}) was not started \u2014 ${d()}`);r.onLog?.(`Building group ${p+1}/${f.length} (${i})\u2026`,"info");const y=await we(u,o,t,a,s,S,k,r,n.awsProvider,v.signal,e.options?.environment,e.options?.flagBuildArgs,e.options?.flagBuildSecrets,I);if(!y.success)throw c===void 0&&(c={error:y.error,leader:u.members[0]?.name,members:i}),v.abort(),y.error;return y.data}),O=await ee($,D,{abortOnFirstFailure:!0,abortSignal:v.signal});if(c===void 0)for(let u=0;u<f.length;u++){const p=O[u];if(!(p!==void 0&&p.success)){c={error:p!==void 0&&!p.success?p.error:new Error(`Build of group ${u+1}/${f.length} never ran \u2014 ${d()}`),leader:f[u].members[0]?.name,members:f[u].members.map(i=>i.name).join(", ")};break}}if(c!==void 0){L.debug(T,"Docker buildAndPush failed",{appName:t.appName,leader:c.leader,members:c.members,error:b(c.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const u=new Error(b(c.error.message));return r.onError?.(u),h(u)}const R={},B={};for(const u of O)if(!(u===void 0||!u.success)){for(const[p,i]of Object.entries(u.data.contentHashTagsByService))R[p]=i;for(const[p,i]of Object.entries(u.data.imageDigestsByService))B[p]=i}return L.debug(T,"Docker buildAndPush succeeded",{appName:t.appName,services:g.map(u=>u.name),groupCount:f.length,contentHashTags:R}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker images pushed for ${g.length} service(s)`,"info"),N({contentHashTagsByService:R,imageDigestsByService:B,repoUri:S})}finally{e.abortSignal?.removeEventListener("abort",P),l&&o.endEcrSession!==void 0&&await o.endEcrSession().catch(d=>{L.debug(T,"endEcrSession cleanup failed",{error:b(U(d))})})}}export{C as DOCKER_BUILD_STEP_NAME,be as ecrRepoUri,H as emptyBuildProducts,Fe as runDockerBuild};
1
+ import{isAbsolute as M,join as _,dirname as z,resolve as F}from"node:path";import{existsSync as j}from"node:fs";import{success as N,failure as h}from"@fjall/generator";import{deriveContentHashTag as J,getErrorMessage as U,maskSensitiveOutput as b,toKebab as x}from"@fjall/util";import{evaluateBakeGuard as X,evaluateResolvedBuildArgValues as Y,acknowledgedBuildArgKeys as q,createBakeWarningDeduper as Q,buildCacheRepositoryName as G,BAKE_GUARD_EXPOSURE_CLAUSE as Z}from"@fjall/util/docker";import{runWithBoundedConcurrency as ee,resolveBuildConcurrency as re}from"@fjall/util/buildConcurrency";import{logger as L}from"@fjall/util/logger";import{parseDockerServicesFromManifest as te,parseLambdaDockerServicesFromManifest as ne}from"@fjall/util/manifest";import{STEP_IDS as A}from"../../types/stepDefinitions.js";import{isAborted as V}from"../../aws/organisations/types.js";import{resolveBuildSecrets as oe,resolveSecretRefValue as ie,resolveBuildSecretSessionProvider as se,sourcedRefsFromBuildArgs as ae}from"./buildSecretResolver.js";import{partitionForRegion as de}from"./buildSecretSession.js";import{resolveBuildArgs as ce}from"./buildArgResolver.js";import{validateBuildGroup as ue}from"./buildGroupValidator.js";import{ensureBuildCacheRepository as le}from"./ecrCacheRepository.js";const T="dockerBuildHelper",C="Building and pushing Docker image";function fe(e,n,t){const r=_(n,e);if(j(r))return{path:r,base:n};if(t!==""&&t!==n){const o=_(t,e);if(j(o))return{path:o,base:t}}return{path:r,base:n}}function ge(e,n,t){const r=e.docker.path,o=e.docker.context;let c,s;if(M(r))c=r,s=n;else{const m=fe(r,n,t);c=m.path,s=m.base}return{buildContext:o?M(o)?F(o):F(s,o):F(z(c)),dockerfilePath:c,target:e.docker.target,resolvedViaFallback:s!==n}}function me(e){return`${e.buildContext}|${e.dockerfilePath}|${e.target??""}`}function pe(e,n){return`Dockerfile not found for service '${e}' at ${n}. Checked the synth directory and the working directory. If your Dockerfile lives at the project root with the CDK app in a subdirectory, set an absolute docker.context (the build context must share the Dockerfile's directory).`}function he(e){return e.startsWith("cn-")?"amazonaws.com.cn":"amazonaws.com"}function W(e,n){return`${e}.dkr.ecr.${n}.${he(n)}`}function be(e,n,t){return`${W(e,n)}/${x(t)}`}function ye(e,n,t){return`${W(e,n)}/${G(x(t))}`}function ve(e,n,t){const r=new Map;for(const o of e){const c=ge(o,n,t),s=me(c),g=r.get(s);g?g.members.push(o):r.set(s,{identity:c,members:[o]})}return Array.from(r.values())}async function Se(e,n,t,r,o,c){const s=await e.buildAndPush({appName:n.appName,appPath:n.path,region:t,accountId:r,...o!==void 0&&{abortSignal:o}},(g,m,v,w)=>{c.onDockerProgress?.(b(g),m,v,w)});return s.success?N({imageUri:s.data.imageUri,imageTag:s.data.imageTag}):h(s.error)}function H(){return{contentHashTagsByService:{},imageDigestsByService:{}}}async function we(e,n,t,r,o,c,s,g,m,v,w,f,k,E){const l=e.members[0];if(!j(e.identity.dockerfilePath))return h(new Error(pe(l.name,e.identity.dockerfilePath)));e.identity.resolvedViaFallback&&g.onLog?.(`Building service '${l.name}' from ${e.identity.dockerfilePath} (resolved at the project root).`,"info");const S=[...l.docker.buildSecrets??[],...k??[]],P=ue({serviceName:l.name,dockerfilePath:e.identity.dockerfilePath,buildContext:e.identity.buildContext,...e.identity.target!==void 0&&{target:e.identity.target},...l.docker.buildArgs!==void 0&&{buildArgs:l.docker.buildArgs},...S.length>0&&{buildSecrets:S},members:e.members.map(i=>({serviceName:i.name,...i.docker.buildArgs!==void 0&&{buildArgs:i.docker.buildArgs},...i.docker.buildSecrets!==void 0&&{buildSecrets:i.docker.buildSecrets}}))});if(!P.success)return h(P.error);const a=X(l.docker.buildArgs,x(t.appName));for(const i of a.warnings){const y=b(i.message);E(y)&&g.onLog?.(y,"warn")}if(a.findings.length>0)return h(new Error(b(a.findings.map(i=>i.message).join(" "))));const d=S,D=ae(l.docker.buildArgs),I={appName:t.appName,region:r,accountId:o,partition:de(r)},$=await se(m,[...d,...D],I);if(!$.success)return h($.error);const O=$.data,R=await ce({manifestBuildArgs:l.docker.buildArgs,serviceDir:e.identity.buildContext,appDir:t.path,...w!==void 0&&{stage:w},shellEnv:process.env,...f!==void 0&&{flagBuildArgs:f},resolveSourcedValue:(i,y)=>ie(O,{id:i,...y},v)});if(!R.success)return h(R.error);const B=R.data.buildArgs;for(const i of R.data.inferredDotenvKeys){const y=b(`buildArg "${i}" was auto-declared from a .env file and will be baked into the image \u2014 ${Z}.`);E(y)&&g.onLog?.(y,"warn")}const u=Y(B,x(t.appName),q(l.docker.buildArgs));if(u.length>0)return h(new Error(b(u.map(i=>i.message).join(" "))));let p;if(d.length>0){const i=await oe(O,d,v);if(!i.success)return h(i.error);p=i.data}try{if(V(v))return h(new Error(`Build of service '${l.name}' was aborted before the docker build started`));const i=e.members.some(K=>K.isLambda===!0),y={appName:t.appName,appPath:e.identity.buildContext,region:r,accountId:o,buildContext:e.identity.buildContext,dockerfilePath:e.identity.dockerfilePath,pushByDigest:!0,serviceName:l.name,targetsLambda:i,...s!==void 0&&{cacheRepoUri:s},...e.identity.target!==void 0&&{target:e.identity.target},...Object.keys(B).length>0&&{buildArgs:B},...p!==void 0&&{buildSecrets:p.resolved},...v!==void 0&&{abortSignal:v}};return await Ee(e,y,n,c,g)}finally{p!==void 0&&await p.cleanup().catch(()=>g.onLog?.("Build-secret cleanup failed.","warn"))}}async function Ee(e,n,t,r,o){const c=await t.buildAndPush(n,(f,k,E,l)=>{o.onDockerProgress?.(b(f),k,E,l)});if(!c.success)return h(c.error);const s=c.data.imageDigest;if(typeof s!="string"||!s.startsWith("sha256:"))return h(new Error(`Buildx push succeeded but returned an invalid digest: ${b(String(s))}`));const g={},m={},v=[];for(const f of e.members){const k=f.docker.target,E=J(s,f.name,k);if(!E.success)return h(E.error);const l=E.data,S=f.name;g[S]=l,m[S]=s,v.push(`${r}:${l}`)}const w=await t.tagByDigest({sourceImage:r,digest:s,tags:v});return w.success?N({contentHashTagsByService:g,imageDigestsByService:m}):h(w.error)}function ke(e,n){const t=new Map(e.map(r=>[r.name,r]));for(const r of n){const o=t.get(r.name);if(o!==void 0&&JSON.stringify(o.docker)!==JSON.stringify(r.docker))throw new Error(`ECS service "${o.name}" and Lambda image key "${r.name}" share one build key but declare different docker configs \u2014 the second build would silently overwrite the first and both would deploy the same image. Rename one of them, or make the docker configs identical to deliberately share the build.`)}}async function Fe(e,n,t,r){const o=e.dockerProvider;if(!o)return N(H());const c=n.awsProvider.getAccountId();if(!c)return r.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),N(H());const s=n.awsProvider.getRegion(),g=_(t.path,"cdk.out");let m;try{const a=te(g),d=ne(g);ke(a,d),m=[...a,...d]}catch(a){const d=new Error(b(U(a)));return r.onError?.(d),h(d)}const v=be(c,s,t.appName);if(L.debug(T,"runDockerBuild starting",{appName:t.appName,appPath:t.path,accountId:c,region:s,cdkOutPath:g,manifestServiceCount:m.length,manifestServices:m.map(a=>({name:a.name,path:a.docker.path,context:a.docker.context,target:a.docker.target})),stepId:A.DOCKER_OPERATIONS,stepName:C}),r.onStepStart?.(A.DOCKER_OPERATIONS,C),m.length===0){r.onLog?.("Initialising ECR repository\u2026","info");const a=await o.initialiseECR({appName:t.appName,region:s,accountId:c});a.success||r.onLog?.(`ECR initialisation failed: ${b(a.error.message)}`,"warn"),r.onLog?.("Building and pushing Docker image\u2026","info");const d=await Se(o,t,s,c,e.abortSignal,r);if(!d.success){L.debug(T,"Docker buildAndPush (legacy) failed",{appName:t.appName,error:b(d.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const D=new Error(b(d.error.message));return r.onError?.(D),h(D)}return r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker image pushed: ${d.data.imageUri}`,"info"),N(H())}const w=await o.preflight?.();if(w!==void 0&&!w.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const a=new Error(b(w.error.message));return r.onError?.(a),h(a)}const f=ve(m,t.path,e.workingDirectory);let k=ye(c,s,t.appName);const E=await le(n.awsProvider,G(x(t.appName)),e.abortSignal);E.success||(k=void 0,r.onLog?.(`Building without registry cache \u2014 ${E.error.message}`,"warn")),r.onLog?.(`Building and pushing ${m.length} service image(s) in ${f.length} group(s)\u2026`,"info");let l=!1;if(o.beginEcrSession!==void 0){const a=await o.beginEcrSession({accountId:c,region:s,...e.abortSignal!==void 0&&{abortSignal:e.abortSignal}});if(!a.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const d=new Error(b(a.error.message));return r.onError?.(d),h(d)}l=!0}const S=new AbortController,P=()=>S.abort();e.abortSignal!==void 0&&(e.abortSignal.aborted?S.abort():e.abortSignal.addEventListener("abort",P,{once:!0}));try{const a=()=>V(e.abortSignal)?"the deployment was cancelled":"a sibling group failed";let d;const D=re(f.length),I=Q(),$=f.map((u,p)=>async()=>{const i=u.members.map(K=>K.name).join(", ");if(S.signal.aborted)throw new Error(`Build of group ${p+1}/${f.length} (${i}) was not started \u2014 ${a()}`);r.onLog?.(`Building group ${p+1}/${f.length} (${i})\u2026`,"info");const y=await we(u,o,t,s,c,v,k,r,n.awsProvider,S.signal,e.options?.environment,e.options?.flagBuildArgs,e.options?.flagBuildSecrets,I);if(!y.success)throw d===void 0&&(d={error:y.error,leader:u.members[0]?.name,members:i}),S.abort(),y.error;return y.data}),O=await ee($,D,{abortOnFirstFailure:!0,abortSignal:S.signal});if(d===void 0)for(let u=0;u<f.length;u++){const p=O[u];if(!(p!==void 0&&p.success)){d={error:p!==void 0&&!p.success?p.error:new Error(`Build of group ${u+1}/${f.length} never ran \u2014 ${a()}`),leader:f[u].members[0]?.name,members:f[u].members.map(i=>i.name).join(", ")};break}}if(d!==void 0){L.debug(T,"Docker buildAndPush failed",{appName:t.appName,leader:d.leader,members:d.members,error:b(d.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const u=new Error(b(d.error.message));return r.onError?.(u),h(u)}const R={},B={};for(const u of O)if(!(u===void 0||!u.success)){for(const[p,i]of Object.entries(u.data.contentHashTagsByService))R[p]=i;for(const[p,i]of Object.entries(u.data.imageDigestsByService))B[p]=i}return L.debug(T,"Docker buildAndPush succeeded",{appName:t.appName,services:m.map(u=>u.name),groupCount:f.length,contentHashTags:R}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker images pushed for ${m.length} service(s)`,"info"),N({contentHashTagsByService:R,imageDigestsByService:B,repoUri:v})}finally{e.abortSignal?.removeEventListener("abort",P),l&&o.endEcrSession!==void 0&&await o.endEcrSession().catch(a=>{L.debug(T,"endEcrSession cleanup failed",{error:b(U(a))})})}}export{C as DOCKER_BUILD_STEP_NAME,be as ecrRepoUri,H as emptyBuildProducts,Fe as runDockerBuild};
@@ -1 +1 @@
1
- const e={appName:!0,appPath:!0,region:!0,accountId:!0,imageTag:!0,services:!0,platform:!0,cacheRepoUri:!0,serviceName:!0,target:!0,dockerfilePath:!0,buildContext:!0,imageTags:!0,pushByDigest:!0,buildArgs:!0,buildSecrets:!0,targetsLambda:!0},t=Object.keys(e);export{t as ALL_DOCKER_BUILD_PARAM_KEYS};
1
+ const e={appName:!0,appPath:!0,region:!0,accountId:!0,imageTag:!0,services:!0,platform:!0,cacheRepoUri:!0,serviceName:!0,target:!0,dockerfilePath:!0,buildContext:!0,imageTags:!0,pushByDigest:!0,buildArgs:!0,buildSecrets:!0,targetsLambda:!0,abortSignal:!0},t=Object.keys(e);export{t as ALL_DOCKER_BUILD_PARAM_KEYS};
@@ -91,6 +91,16 @@ export interface DockerBuildParams {
91
91
  * both for the whole group rather than trying to push two variants.
92
92
  */
93
93
  targetsLambda?: boolean;
94
+ /**
95
+ * Caller abort signal (deploy cancel, worker SIGTERM, wave sibling-failure
96
+ * controller). CANONICAL RATIONALE — providers thread this to the underlying
97
+ * buildx subprocess so an in-flight build/push can be interrupted instead of
98
+ * running minutes past the cancellation. Killing buildx mid-push is safe:
99
+ * ECR deduplicates blobs so a retry re-uploads only what is missing, and the
100
+ * registry cache tag is mutable by design. Absent → the build runs to
101
+ * completion (pre-2.34 behaviour).
102
+ */
103
+ abortSignal?: AbortSignal;
94
104
  }
95
105
  export interface DockerBuildResult {
96
106
  imageUri: string;
@@ -178,6 +188,12 @@ export interface DockerProvider {
178
188
  beginEcrSession?(params: {
179
189
  accountId: string;
180
190
  region: string;
191
+ /**
192
+ * Declared from birth per robustness-standards § "Optional-capability seam
193
+ * methods carry `abortSignal?`" — implementers compose it with their SDK
194
+ * timeouts via `composeSdkAbortSignal`; ignoring it is permitted.
195
+ */
196
+ abortSignal?: AbortSignal;
181
197
  }): Promise<Result<void>>;
182
198
  endEcrSession?(): Promise<void>;
183
199
  buildAndPush(params: DockerBuildParams, onProgress?: DockerProgressCallback, onBuildxEvent?: (event: BuildxProgressEvent) => void): Promise<Result<DockerBuildResult>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/deploy-core",
3
- "version": "2.33.0",
3
+ "version": "2.34.0",
4
4
  "description": "Shared deployment engine for Fjall — used by CLI and webapp worker",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -86,8 +86,8 @@
86
86
  "@aws-sdk/client-ssm": "^3.1038.0",
87
87
  "@aws-sdk/client-sso-admin": "^3.1038.0",
88
88
  "@aws-sdk/client-sts": "^3.1038.0",
89
- "@fjall/generator": "^2.33.0",
90
- "@fjall/util": "^2.33.0",
89
+ "@fjall/generator": "^2.34.0",
90
+ "@fjall/util": "^2.34.0",
91
91
  "@smithy/node-http-handler": "^4.6.1",
92
92
  "aws-cdk": "^2.1128.1",
93
93
  "tsx": "^4.21.0",
@@ -98,5 +98,5 @@
98
98
  "typescript": "^6.0.3",
99
99
  "vitest": "^4.1.5"
100
100
  },
101
- "gitHead": "61c0d1917597c6d02e8e78b03420afdd194eaafc"
101
+ "gitHead": "8c553fca8f9737d445061ea75cf6c6c7db0d24c9"
102
102
  }