@fjall/deploy-core 2.32.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-16T21:24:20.849Z
1
+ 218 files minified at 2026-07-17T22:47:38.950Z
@@ -1 +1 @@
1
- import{isAbsolute as H,join as K,dirname as z,resolve as _}from"node:path";import{existsSync as F}from"node:fs";import{success as N,failure as h}from"@fjall/generator";import{deriveContentHashTag as J,getErrorMessage as M,maskSensitiveOutput as b,toKebab as x}from"@fjall/util";import{evaluateBakeGuard as X,evaluateResolvedBuildArgValues as Y,acknowledgedBuildArgKeys as q,createBakeWarningDeduper as Q,buildCacheRepositoryName as U,BAKE_GUARD_EXPOSURE_CLAUSE as Z}from"@fjall/util/docker";import{runWithBoundedConcurrency as ee,resolveBuildConcurrency as re}from"@fjall/util/buildConcurrency";import{logger as T}from"@fjall/util/logger";import{parseDockerServicesFromManifest as te,parseLambdaDockerServicesFromManifest as ne}from"@fjall/util/manifest";import{STEP_IDS as A}from"../../types/stepDefinitions.js";import{isAborted as G}from"../../aws/organisations/types.js";import{resolveBuildSecrets as oe,resolveSecretRefValue as ie,resolveBuildSecretSessionProvider as se,sourcedRefsFromBuildArgs as ae}from"./buildSecretResolver.js";import{partitionForRegion as de}from"./buildSecretSession.js";import{resolveBuildArgs as ce}from"./buildArgResolver.js";import{validateBuildGroup as ue}from"./buildGroupValidator.js";import{ensureBuildCacheRepository as le}from"./ecrCacheRepository.js";const L="dockerBuildHelper",C="Building and pushing Docker image";function fe(e,n,t){const r=K(n,e);if(F(r))return{path:r,base:n};if(t!==""&&t!==n){const o=K(t,e);if(F(o))return{path:o,base:t}}return{path:r,base:n}}function ge(e,n,t){const r=e.docker.path,o=e.docker.context;let s,a;if(H(r))s=r,a=n;else{const m=fe(r,n,t);s=m.path,a=m.base}return{buildContext:o?H(o)?_(o):_(a,o):_(z(s)),dockerfilePath:s,target:e.docker.target,resolvedViaFallback:a!==n}}function me(e){return`${e.buildContext}|${e.dockerfilePath}|${e.target??""}`}function pe(e,n){return`Dockerfile not found for service '${e}' at ${n}. Checked the synth directory and the working directory. If your Dockerfile lives at the project root with the CDK app in a subdirectory, set an absolute docker.context (the build context must share the Dockerfile's directory).`}function he(e){return e.startsWith("cn-")?"amazonaws.com.cn":"amazonaws.com"}function V(e,n){return`${e}.dkr.ecr.${n}.${he(n)}`}function be(e,n,t){return`${V(e,n)}/${x(t)}`}function ye(e,n,t){return`${V(e,n)}/${U(x(t))}`}function ve(e,n,t){const r=new Map;for(const o of e){const s=ge(o,n,t),a=me(s),g=r.get(a);g?g.members.push(o):r.set(a,{identity:s,members:[o]})}return Array.from(r.values())}async function Se(e,n,t,r,o){const s=await e.buildAndPush({appName:n.appName,appPath:n.path,region:t,accountId:r},(a,g,m,S)=>{o.onDockerProgress?.(b(a),g,m,S)});return s.success?N({imageUri:s.data.imageUri,imageTag:s.data.imageTag}):h(s.error)}function j(){return{contentHashTagsByService:{},imageDigestsByService:{}}}async function we(e,n,t,r,o,s,a,g,m,S,E,f,k,w){const l=e.members[0];if(!F(e.identity.dockerfilePath))return h(new Error(pe(l.name,e.identity.dockerfilePath)));e.identity.resolvedViaFallback&&g.onLog?.(`Building service '${l.name}' from ${e.identity.dockerfilePath} (resolved at the project root).`,"info");const y=[...l.docker.buildSecrets??[],...k??[]],P=ue({serviceName:l.name,dockerfilePath:e.identity.dockerfilePath,buildContext:e.identity.buildContext,...e.identity.target!==void 0&&{target:e.identity.target},...l.docker.buildArgs!==void 0&&{buildArgs:l.docker.buildArgs},...y.length>0&&{buildSecrets:y},members:e.members.map(i=>({serviceName:i.name,...i.docker.buildArgs!==void 0&&{buildArgs:i.docker.buildArgs},...i.docker.buildSecrets!==void 0&&{buildSecrets:i.docker.buildSecrets}}))});if(!P.success)return h(P.error);const d=X(l.docker.buildArgs,x(t.appName));for(const i of d.warnings){const v=b(i.message);w(v)&&g.onLog?.(v,"warn")}if(d.findings.length>0)return h(new Error(b(d.findings.map(i=>i.message).join(" "))));const c=y,D=ae(l.docker.buildArgs),I={appName:t.appName,region:r,accountId:o,partition:de(r)},$=await se(m,[...c,...D],I);if(!$.success)return h($.error);const O=$.data,R=await ce({manifestBuildArgs:l.docker.buildArgs,serviceDir:e.identity.buildContext,appDir:t.path,...E!==void 0&&{stage:E},shellEnv:process.env,...f!==void 0&&{flagBuildArgs:f},resolveSourcedValue:(i,v)=>ie(O,{id:i,...v},S)});if(!R.success)return h(R.error);const B=R.data.buildArgs;for(const i of R.data.inferredDotenvKeys){const v=b(`buildArg "${i}" was auto-declared from a .env file and will be baked into the image \u2014 ${Z}.`);w(v)&&g.onLog?.(v,"warn")}const u=Y(B,x(t.appName),q(l.docker.buildArgs));if(u.length>0)return h(new Error(b(u.map(i=>i.message).join(" "))));let p;if(c.length>0){const i=await oe(O,c,S);if(!i.success)return h(i.error);p=i.data}try{if(G(S))return h(new Error(`Build of service '${l.name}' was aborted before the docker build started`));const i={appName:t.appName,appPath:e.identity.buildContext,region:r,accountId:o,buildContext:e.identity.buildContext,dockerfilePath:e.identity.dockerfilePath,pushByDigest:!0,serviceName:l.name,...a!==void 0&&{cacheRepoUri:a},...e.identity.target!==void 0&&{target:e.identity.target},...Object.keys(B).length>0&&{buildArgs:B},...p!==void 0&&{buildSecrets:p.resolved}};return await Ee(e,i,n,s,g)}finally{p!==void 0&&await p.cleanup().catch(()=>g.onLog?.("Build-secret cleanup failed.","warn"))}}async function Ee(e,n,t,r,o){const s=await t.buildAndPush(n,(f,k,w,l)=>{o.onDockerProgress?.(b(f),k,w,l)});if(!s.success)return h(s.error);const a=s.data.imageDigest;if(typeof a!="string"||!a.startsWith("sha256:"))return h(new Error(`Buildx push succeeded but returned an invalid digest: ${b(String(a))}`));const g={},m={},S=[];for(const f of e.members){const k=f.docker.target,w=J(a,f.name,k);if(!w.success)return h(w.error);const l=w.data,y=f.name;g[y]=l,m[y]=a,S.push(`${r}:${l}`)}const E=await t.tagByDigest({sourceImage:r,digest:a,tags:S});return E.success?N({contentHashTagsByService:g,imageDigestsByService:m}):h(E.error)}function ke(e,n){const t=new Map(e.map(r=>[r.name,r]));for(const r of n){const o=t.get(r.name);if(o!==void 0&&JSON.stringify(o.docker)!==JSON.stringify(r.docker))throw new Error(`ECS service "${o.name}" and Lambda image key "${r.name}" share one build key but declare different docker configs \u2014 the second build would silently overwrite the first and both would deploy the same image. Rename one of them, or make the docker configs identical to deliberately share the build.`)}}async function Fe(e,n,t,r){const o=e.dockerProvider;if(!o)return N(j());const s=n.awsProvider.getAccountId();if(!s)return r.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),N(j());const a=n.awsProvider.getRegion(),g=K(t.path,"cdk.out");let m;try{const d=te(g),c=ne(g);ke(d,c),m=[...d,...c]}catch(d){const c=new Error(b(M(d)));return r.onError?.(c),h(c)}const S=be(s,a,t.appName);if(T.debug(L,"runDockerBuild starting",{appName:t.appName,appPath:t.path,accountId:s,region:a,cdkOutPath:g,manifestServiceCount:m.length,manifestServices:m.map(d=>({name:d.name,path:d.docker.path,context:d.docker.context,target:d.docker.target})),stepId:A.DOCKER_OPERATIONS,stepName:C}),r.onStepStart?.(A.DOCKER_OPERATIONS,C),m.length===0){r.onLog?.("Initialising ECR repository\u2026","info");const d=await o.initialiseECR({appName:t.appName,region:a,accountId:s});d.success||r.onLog?.(`ECR initialisation failed: ${b(d.error.message)}`,"warn"),r.onLog?.("Building and pushing Docker image\u2026","info");const c=await Se(o,t,a,s,r);if(!c.success){T.debug(L,"Docker buildAndPush (legacy) failed",{appName:t.appName,error:b(c.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const D=new Error(b(c.error.message));return r.onError?.(D),h(D)}return r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker image pushed: ${c.data.imageUri}`,"info"),N(j())}const E=await o.preflight?.();if(E!==void 0&&!E.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const d=new Error(b(E.error.message));return r.onError?.(d),h(d)}const f=ve(m,t.path,e.workingDirectory);let k=ye(s,a,t.appName);const w=await le(n.awsProvider,U(x(t.appName)),e.abortSignal);w.success||(k=void 0,r.onLog?.(`Building without registry cache \u2014 ${w.error.message}`,"warn")),r.onLog?.(`Building and pushing ${m.length} service image(s) in ${f.length} group(s)\u2026`,"info");let l=!1;if(o.beginEcrSession!==void 0){const d=await o.beginEcrSession({accountId:s,region:a});if(!d.success){r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const c=new Error(b(d.error.message));return r.onError?.(c),h(c)}l=!0}const y=new AbortController,P=()=>y.abort();e.abortSignal!==void 0&&(e.abortSignal.aborted?y.abort():e.abortSignal.addEventListener("abort",P,{once:!0}));try{const d=()=>G(e.abortSignal)?"the deployment was cancelled":"a sibling group failed";let c;const D=re(f.length),I=Q(),$=f.map((u,p)=>async()=>{const i=u.members.map(W=>W.name).join(", ");if(y.signal.aborted)throw new Error(`Build of group ${p+1}/${f.length} (${i}) was not started \u2014 ${d()}`);r.onLog?.(`Building group ${p+1}/${f.length} (${i})\u2026`,"info");const v=await we(u,o,t,a,s,S,k,r,n.awsProvider,y.signal,e.options?.environment,e.options?.flagBuildArgs,e.options?.flagBuildSecrets,I);if(!v.success)throw c===void 0&&(c={error:v.error,leader:u.members[0]?.name,members:i}),y.abort(),v.error;return v.data}),O=await ee($,D,{abortOnFirstFailure:!0,abortSignal:y.signal});if(c===void 0)for(let u=0;u<f.length;u++){const p=O[u];if(!(p!==void 0&&p.success)){c={error:p!==void 0&&!p.success?p.error:new Error(`Build of group ${u+1}/${f.length} never ran \u2014 ${d()}`),leader:f[u].members[0]?.name,members:f[u].members.map(i=>i.name).join(", ")};break}}if(c!==void 0){T.debug(L,"Docker buildAndPush failed",{appName:t.appName,leader:c.leader,members:c.members,error:b(c.error.message)}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"error");const u=new Error(b(c.error.message));return r.onError?.(u),h(u)}const R={},B={};for(const u of O)if(!(u===void 0||!u.success)){for(const[p,i]of Object.entries(u.data.contentHashTagsByService))R[p]=i;for(const[p,i]of Object.entries(u.data.imageDigestsByService))B[p]=i}return T.debug(L,"Docker buildAndPush succeeded",{appName:t.appName,services:m.map(u=>u.name),groupCount:f.length,contentHashTags:R}),r.onStepComplete?.(A.DOCKER_OPERATIONS,C,"completed"),r.onLog?.(`Docker images pushed for ${m.length} service(s)`,"info"),N({contentHashTagsByService:R,imageDigestsByService:B,repoUri:S})}finally{e.abortSignal?.removeEventListener("abort",P),l&&o.endEcrSession!==void 0&&await o.endEcrSession().catch(d=>{T.debug(L,"endEcrSession cleanup failed",{error:b(M(d))})})}}export{C as DOCKER_BUILD_STEP_NAME,be as ecrRepoUri,j as emptyBuildProducts,Fe as runDockerBuild};
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},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};
@@ -33,8 +33,12 @@ export interface DockerBuildParams {
33
33
  /**
34
34
  * When provided, the provider constructs registry-mode buildx cache args
35
35
  * (`--cache-from type=registry,ref=<cacheRepoUri>:<service>` and
36
- * `--cache-to type=registry,ref=<cacheRepoUri>:<service>,mode=max`) so
36
+ * `--cache-to type=registry,ref=<cacheRepoUri>:<service>,mode=<mode>`) so
37
37
  * incremental builds reuse layers across CI runs and developer machines.
38
+ * The cache-to mode defaults to `max`; providers may resolve it per
39
+ * environment (the CLI provider honours `FJALL_BUILD_CACHE_MODE` via
40
+ * `resolveBuildCacheMode` at `@fjall/util`, omitting `--cache-to` entirely
41
+ * for `off` while keeping `--cache-from` — reading a cache is free).
38
42
  * The orchestrator is responsible for ensuring the `<app>-cache` repo exists
39
43
  * before invoking the provider; missing repos surface as a buildx push error.
40
44
  */
@@ -77,6 +81,26 @@ export interface DockerBuildParams {
77
81
  * provider projects these 1:1 onto `BuildxBuildArgs.secrets`.
78
82
  */
79
83
  buildSecrets?: readonly ResolvedBuildSecret[];
84
+ /**
85
+ * True when this build group contains at least one container-Lambda image.
86
+ * Lambda's `CreateFunction`/`UpdateFunctionCode` rejects an OCI image index
87
+ * with attestation manifests — the shape buildx produces by default when
88
+ * pushing (`--provenance=true`/`--sbom=true`) — so the provider must
89
+ * disable both for a Lambda-targeted build. ECS tolerates either shape, so
90
+ * a mixed ECS+Lambda group (sharing one Dockerfile/context/target) disables
91
+ * both for the whole group rather than trying to push two variants.
92
+ */
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;
80
104
  }
81
105
  export interface DockerBuildResult {
82
106
  imageUri: string;
@@ -164,6 +188,12 @@ export interface DockerProvider {
164
188
  beginEcrSession?(params: {
165
189
  accountId: string;
166
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;
167
197
  }): Promise<Result<void>>;
168
198
  endEcrSession?(): Promise<void>;
169
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.32.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.32.0",
90
- "@fjall/util": "^2.32.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": "876a79a9ad0031a5919019c86867c28c1ab9dc92"
101
+ "gitHead": "8c553fca8f9737d445061ea75cf6c6c7db0d24c9"
102
102
  }