@fjall/deploy-core 3.11.0 → 3.11.2

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
- 234 files minified at 2026-07-23T21:49:23.545Z
1
+ 234 files minified at 2026-08-08T02:42:52.365Z
@@ -1 +1 @@
1
- var ve=Object.defineProperty;var v=(e,a)=>ve(e,"name",{value:a,configurable:!0});import{join as ue}from"path";import{CloudFormationClient as Pe}from"@aws-sdk/client-cloudformation";import{EC2Client as Re}from"@aws-sdk/client-ec2";import{success as C,failure as c}from"@fjall/generator";import{logger as $}from"@fjall/util/logger";import{ImageTagSchema as Ce,getErrorMessage as ge,imageTagParameterName as Ie,maskSensitiveOutput as y}from"@fjall/util";import{parseDockerDeclarationsFromManifest as Oe}from"@fjall/util/manifest";import{stubCallerIdentity as Ae}from"../../types/deployment/index.js";import{getApplicationDeployOrder as Le,getApplicationStackName as I,getApplicationStepName as pe,getApplicationStepId as fe,APPLICATION_STACKS as P}from"../../types/operations.js";import{CdkContextBuilder as Be}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as Me,bootstrapOrFail as Fe}from"../contextHelpers.js";import{buildBootstrapTagsIfResolved as $e}from"../bootstrapTags.js";import{hasOrganisationTierAccount as xe}from"../../aws/organisations/accountGlobals.js";import{runDetectionPipeline as je}from"./detectionPipeline.js";import{STEP_IDS as _,STEP_NAMES as Ge}from"../../types/stepDefinitions.js";import{StepRegistry as He}from"../../steps/stepRegistry.js";import{checkTargetReadiness as Ue}from"../../aws/targetReadiness.js";import{cascadeHomeRegion as _e}from"../organisation/cascadeHelpers.js";import{withStepLifecycle as Ke}from"../stepLifecycle.js";import{DOCKER_BUILD_STEP_NAME as me,runDockerBuild as We}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as qe,getStackNamesToDeploy as Ve,deployParallelPhase as ze,deployStackSequential as Je,runDockerPreCompute as Ye,runLambdaVersionPublish as Qe,createBuildCallbacks as Xe,willRunDockerBuild as Ze}from"./applicationDeployHelpers.js";import{verifyAssemblyIntegrity as ke}from"./assemblyIntegrity.js";import{captureDatabaseConnectionOutputs as et,captureServiceTaskDefinitions as tt,reconcileDatabaseEndpointChange as rt}from"./databaseEndpointReconcile.js";import{deployCodeOnly as at}from"./codeOnlyDeploy.js";import{checkDeployPathStackAvailability as he}from"../activeDeploymentGuard.js";import{assertLeaseBeforeStack as K}from"./leaseGate.js";import{collectFullDeployArtefacts as nt}from"./artefactCollection.js";import{CfnRegistryService as ot}from"../../services/infrastructure/CfnRegistryService.js";import{ChangeSetProbe as it}from"../../services/infrastructure/changeSetProbe.js";import{runApprovalGate as st}from"./approvalGate.js";import{buildDestructionTicket as ct,runDestructionGate as lt}from"./destructionGate.js";import{DESTRUCTION_RESUME_TTL_MS as dt,signApprovalToken as ut}from"./plan/approvalToken.js";import{computeAssemblyDigest as gt}from"./plan/assemblyDigest.js";import{createMemoisedPlanLoader as pt}from"./plan/loadDeployPlan.js";import{runDriftPreFlightGate as ft}from"../drift/preFlightGate.js";import{completeForgetAfterDeploy as mt}from"../remediation/forgetResource.js";import{composePostFailureRepairOffer as ye}from"../remediation/postFailureRepairOffer.js";import{renderPinReportLines as kt,runPinRemediation as ht}from"../remediation/pinRemediation.js";import{runRecreatePreFlight as yt}from"../remediation/recreatePreFlight.js";import{runRecreateSurgery as St}from"../remediation/recreateResource.js";const x="applicationDeploy";function bt(e){const a=Object.entries(e);if(a.length===0)return;const n={};for(const[t,i]of a){const S=Ie(t);n[S]=i}return n}v(bt,"buildCdkImageTagParameters");function wt(e){if(e?.mode!==void 0&&e.mode!=="rollback"){if(e.imageTag)return c(new Error(`--image-tag requires rollback mode (explicit mode '${e.mode}' was supplied alongside an image tag)`));if(e.serviceImageTags)return c(new Error(`serviceImageTags requires rollback mode (explicit mode '${e.mode}' was supplied alongside serviceImageTags)`))}if(e?.mode==="rollback"){if(e.imageTag&&e.serviceImageTags)return c(new Error("rollback mode accepts exactly one of --image-tag or serviceImageTags \u2014 they are mutually exclusive"));if(!e.imageTag&&!e.serviceImageTags)return c(new Error("rollback mode requires --image-tag <tag> or serviceImageTags naming the image(s) to roll to"))}if(e?.serviceImageTags!==void 0){if(e.serviceName)return c(new Error("serviceImageTags already names its services \u2014 --service cannot be combined with it"));const a=Object.entries(e.serviceImageTags);if(a.length===0)return c(new Error("serviceImageTags must name at least one service"));for(const[n,t]of a){const i=Ce.safeParse(t);if(!i.success)return c(new Error(`serviceImageTags.${n} is not a valid image tag: ${i.error.message}`))}}return C(void 0)}v(wt,"validateRollbackOptions");function Se(e,a,n){if(e.onStackTargets===void 0||n.length===0)return;const t=a.awsProvider.getAccountId(),i=a.awsProvider.getRegion();e.onStackTargets(n.map(S=>({stackName:S,...t!==""?{accountId:t}:{},...i!==""?{region:i}:{}})))}v(Se,"emitStackTargets");async function lr(e,a,n){const{callbacks:t,options:i}=e,S=Date.now(),j=i?.mode??(i?.imageTag||i?.serviceImageTags?"rollback":i?.deployOnly?"code-only":"full");if(j==="restart"){const r=new Error('mode "restart" is not a deploy \u2014 call the restart() entry point (fjall rollout) instead');return t.onError?.(r),c(r)}const Y=wt(i);if(!Y.success){const r=y(Y.error.message),o=new Error(r);return t.onError?.(o),c(o)}const G=Be.buildDeploymentContext({deployType:"application",target:n.appName,path:n.path,region:a.awsProvider.getRegion(),callerIdentity:Ae(a.awsProvider.getAccountId()),...Me({orgConfig:e.orgConfig,identity:e.identity,skipOidc:e.options?.skipOidc}),...e.managedDomainBindings!==void 0?{managedDomainBindings:e.managedDomainBindings}:{}},{verbose:i?.verbose,infraOnly:i?.infraOnly},e.orgConfig),T=a.frameworkRegistry.resolve({appPath:n.path});let Q;const L=j==="rollback";if(T&&!L){Q=T.builder.plan({appPath:n.path},T.detection);const r=Xe(t),o=await T.builder.build(n.path,Q,r,{skipBuild:i?.skipBuild,infraOnly:i?.infraOnly,...e.abortSignal!==void 0&&{abortSignal:e.abortSignal}});if(!o.success){const s=new Error(y(o.error.message));return t.onError?.(s),c(s)}}if(j==="code-only"||L){t.onLog?.(L?i?.serviceImageTags?`Rollback mode \u2014 rolling ${Object.keys(i.serviceImageTags).length} service(s) to their release-recorded image tags`:`Rollback mode \u2014 rolling to image tag ${i?.imageTag}`:"Deploy-only mode \u2014 skipping infrastructure pipeline","info");const r=ue(n.path,"cdk.out"),o=Oe(r),s=o.declaresBuild,p=T?.detection.hasDockerfile===!0,l=s||p;$.debug(x,"Deploy-only branch entered",{mode:j,imageTag:i?.imageTag,serviceImageTags:i?.serviceImageTags,appName:n.appName,appPath:n.path,cdkOutPath:r,dockerProviderAvailable:e.dockerProvider!==void 0,builderName:T?.builder.name,hasDockerfileFromManifest:s,hasDockerfileFromDisk:p,hasDockerfile:l,manifestDockerServiceCount:o.ecsServices.length,manifestDockerPaths:o.ecsServices.map(f=>f.docker.path),manifestLambdaDockerEntryCount:o.lambdaEntries.length}),!s&&!p&&t.onLog?.("No Dockerfile detected via manifest or appPath \u2014 skipping Docker build. If this app uses a cross-repo Dockerfile, ensure a full deploy has run first to populate cdk.out/fjall-manifest.json.","warn");const u=await he([I(n.appName,P.COMPUTE)],a.cfnService,e.abortSignal);if(!u.success)return t.onError?.(u.error),c(u.error);Se(t,a,[I(n.appName,P.COMPUTE)]);let k={},d;if(!L&&e.dockerProvider!==void 0&&l){$.debug(x,"Running Docker build before code-only deploy",{source:s?"manifest":"disk"});const f=await We(e,a,n,t);if(!f.success)return c(f.error);k=f.data.contentHashTagsByService,d=f.data}else $.debug(x,"Skipping Docker build",{reason:L?"rollback":e.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});const m=await K(e.assertLeaseForStack,I(n.appName,P.COMPUTE),e.abortSignal);return m!==void 0?(t.onError?.(m),c(m)):at(e,a,n,k,d)}t.onLog?.("Analysing infrastructure\u2026","info");const H=await je(n,a,G,t,e.abortSignal);if(!H.success){const r=new Error(y(H.error.message),{cause:H.error});return t.onError?.(r),c(r)}const g=H.data;try{await t.onDetectionComplete?.({...g,builderName:T?.builder.name??"unknown"})}catch(r){const o=new Error(y(ge(r)),{cause:r});return t.onError?.(o),c(o)}const be={deploymentType:"application",operation:"deploy",deployOnly:!1,infraOnly:i?.infraOnly??!1,hasDockerfile:g.hasDockerfile,pattern:g.pattern,resources:g.resources,...T&&{builderName:T.builder.name}},X=He.getSteps(be),Z=X.findIndex(r=>r.id===_.TARGET_READINESS),ee=await Ke(t,{stepId:_.TARGET_READINESS,stepName:Ge.TARGET_READINESS,...Z>=0&&{stepIndex:Z,totalSteps:X.length}},async()=>{if(i?.skipReadinessCheck)return t.onLog?.("Skipping target readiness check (--skip-readiness-check)","warn"),{kind:"skipped",data:void 0};const r=a.awsProvider.getAccountId(),o=e.orgConfig?.providerAccounts.find(u=>u.id===r)?.name,s=a.awsProvider.getCredentials(),p=_e(e.orgConfig),l=await Ue({cloudFormation:a.awsProvider.getClient(Pe),ec2:new Re({region:p,...s!==void 0&&{credentials:s}})},{id:r,...o!==void 0&&{name:o}},a.awsProvider.getRegion(),e.orgConfig,e.abortSignal);return l.success?l.data.ready?{kind:"completed",data:void 0}:{kind:"error",error:new Error(y(l.data.advisory))}:{kind:"error",error:new Error(y(l.error.message),{cause:l.error})}},e.abortSignal);if(!ee.success)return ee;const N=Le({pattern:g.pattern,resources:g.resources}),O=N.length,te=g.hasDockerfile&&e.dockerProvider!==void 0&&N.includes(P.COMPUTE)&&i?.infraOnly!==!0;if(!g.hasDifferences&&!i?.force&&!te){t.onLog?.("No infrastructure changes detected","info"),t.onLog?.(i?.infraOnly===!0?"Infrastructure-only mode \u2014 application code was not rebuilt. Run with --deploy-only to deploy code changes.":"Nothing to deploy. Run with --deploy-only to redeploy application code.","info");const r=g.hasDockerfile&&e.dockerProvider!==void 0&&N.includes(P.COMPUTE);for(let s=0;s<N.length;s++){const p=N[s];r&&p===P.COMPUTE&&(t.onStepStart?.(_.DOCKER_OPERATIONS,me),t.onStepComplete?.(_.DOCKER_OPERATIONS,me,"skipped"));const l=fe(p,"deploy"),u=pe(p,"deploy");t.onStepStart?.(l,u,s,O),t.onStepComplete?.(l,u,"skipped",s,O)}const o=await a.stackService.resolveWebsiteUrl(n.appName);return C({target:n.appName,deploymentType:"application",outputs:o?{websiteUrl:o}:void 0,noChanges:!0,artefacts:[],durationMs:Date.now()-S})}const A=ue(n.path,"cdk.out"),b=Ve({deployOrder:N,stackChanges:g.stackChanges,appName:n.appName,force:i?.force===!0,computeBuildPossible:te});Se(t,a,[...b]);const W=await he([...b],a.cfnService,e.abortSignal);if(!W.success)return t.onError?.(W.error),c(W.error);const re={stacksToDeploy:[...b],imageBuild:Ze({stackNamesToDeploy:b,appName:n.appName,hasDockerProvider:e.dockerProvider!==void 0,hasDockerfile:g.hasDockerfile})},ae=gt(g.currentHashes,re),q=pt({cfnService:a.cfnService,cdkOutPath:A,changedStacks:[...b],assemblyDigest:ae,registry:new ot(a.awsProvider),changeSetProbe:new it(a.awsProvider),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}),V=await ft(a.awsProvider,{stackNames:[...b],cdkOutPath:A,cfnService:a.cfnService,callbacks:t,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(V.kind==="blocked"){const r=new Error(V.message);return t.onError?.(r),c(r)}const ne=V.verifiedAbsentByNecessity??[],U=e.approvalGate!==void 0||i?.approvalToken!==void 0&&i.approvalToken!==""||i?.planOnly===!0;if(U){const r=await st({approvalGate:e.approvalGate,options:i??{},target:n.appName,cdkOutPath:A,detection:{stackChanges:g.stackChanges,currentHashes:g.currentHashes},scope:re,cfnService:a.cfnService,loadPlan:q,callbacks:t,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!r.success)return r;const o=r.data;if(o.kind==="rejected")return t.onLog?.(`Deploy rejected: ${y(o.reason)}`,"warn"),C({target:n.appName,deploymentType:"application",rejected:!0,artefacts:[],...o.planDetail!==void 0?{planDetail:o.planDetail}:{},durationMs:Date.now()-S});if(o.kind==="awaiting"){const s=await q(),p=s.success?ct(s.data,ne):void 0;return C({target:n.appName,deploymentType:"application",awaitingApproval:!0,approvalToken:o.approvalToken,assemblyDigest:o.assemblyDigest,planDetail:o.planDetail,...p!==void 0?{destructionTicket:p}:{},artefacts:[],durationMs:Date.now()-S})}}const E=await lt({loadPlan:q,options:i??{},target:n.appName,callbacks:t,verifiedAbsentByNecessity:ne,...e.destructionConsentGate!==void 0?{consentGate:e.destructionConsentGate}:{}});if(!E.success)return t.onError?.(E.error),c(E.error);if(E.data.kind==="withheld"){const r=U?ut({assemblyDigest:ae,ttlMs:dt}):void 0;return C({target:n.appName,deploymentType:"application",destructionPending:!0,destructionTicket:E.data.ticket,destructionOutcome:E.data.outcome,destructionHaltReasons:E.data.reasons,...r!==void 0?{destructionResumeToken:r.token,destructionResumeExpiresAt:r.expiresAt}:{},artefacts:[],durationMs:Date.now()-S})}if(E.data.kind==="proceed"){const{ticket:r,verbsByPhysicalName:o}=E.data,s=r.resources.filter(l=>o.get(l.physicalName)==="pin");if(s.length>0){t.onLog?.(`Pin remediation for ${n.appName}: probing live value(s) for ${s.length} consented pin(s) \u2014 deploy stopped for config write-back; nothing was applied`,"warn");const l=await ht(a.awsProvider,{targets:s.map(u=>({stack:u.stack,logicalId:u.logicalId,resourceType:u.resourceType,physicalName:u.physicalName,offendingProperties:u.offendingProperties})),...e.infrastructureSource!==void 0?{infrastructureContent:e.infrastructureSource.content,infrastructureFilePath:e.infrastructureSource.filePath}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});for(const u of kt(l))t.onLog?.(u,"warn");return s.length<r.resources.length&&t.onLog?.("Remaining consented change(s) were not applied \u2014 the config write-back changes the plan, so they re-consent on the next deploy","warn"),C({target:n.appName,deploymentType:"application",pinPending:!0,pinReport:l,pinDeferredConsents:r.resources.length-s.length,destructionTicket:r,artefacts:[],durationMs:Date.now()-S})}const p=r.resources.filter(l=>l.finding==="pinned-name-replacement"&&o.get(l.physicalName)==="recreate");if(p.length>0){const l=new Map;for(const d of p){const m=l.get(d.stack)??[];m.push(d),l.set(d.stack,m)}const u=await yt(a.awsProvider,{stacks:[...l].map(([d,m])=>({stackName:d,targets:m.map(f=>({logicalId:f.logicalId,resourceType:f.resourceType,physicalId:f.physicalName}))})),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!u.success){const d=new Error(y(u.error.message),{cause:u.error});return t.onError?.(d),c(d)}const k=[];for(const[d,m]of l){const f=await K(e.assertLeaseForStack,d,e.abortSignal,"destroy");if(f!==void 0)return k.length>0&&t.onLog?.(`Surgery already completed on ${k.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`,"warn"),t.onError?.(f),c(f);const R=m.map(h=>h.physicalName);t.onLog?.(`Recreate remediation for ${n.appName}: orchestrating the pinned-name replacement of ${R.join(", ")} on ${d}`,"warn");const F=await St(a.awsProvider,{stackName:d,targets:m.map(h=>({logicalId:h.logicalId,resourceType:h.resourceType,physicalId:h.physicalName})),appName:n.appName,consentedPhysicalNames:R,doneJournalPolicy:"archive-and-restart",plannedReplacementLogicalIds:m.map(h=>h.logicalId),onProgress:v(h=>t.onLog?.(h,"info"),"onProgress"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!F.success){const h=k.length>0?` Surgery already completed on ${k.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`:"",w=new Error(y(`${F.error.message}${h}`));return t.onError?.(w),c(w)}k.push(`${d} (${R.join(", ")})`)}}}const we=e.orgConfig!==void 0&&!xe(e.orgConfig.providerAccounts),Te=$e({account:e.orgConfig?.providerAccounts.find(r=>r.id===a.awsProvider.getAccountId()),estate:we?"solo":"org"}),oe=await Fe(a,G,t,Te);if(!oe.success)return oe;const ie=I(n.appName,P.DATABASE),z=b.has(ie),Ne=z?await et(a.cfnService,n.appName,e.abortSignal):void 0,Ee=z?await tt(a.ecsResolver,a.ecsService,n.appName,e.abortSignal):void 0,B={},M=new Map;let se,ce={};const D=v(async()=>{!z||!M.has(ie)||await rt({appName:n.appName,connectionOutputsBefore:Ne,serviceTaskDefinitionsBefore:Ee,services:{cfnService:a.cfnService,ecsService:a.ecsService,ecsResolver:a.ecsResolver},callbacks:t,...e.confirmDatabaseEndpointRestart!==void 0?{confirmRestart:e.confirmDatabaseEndpointRestart}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})},"reconcileIfDatabaseReplaced");for(let r=0;r<N.length;r++){const o=N[r],s=I(n.appName,o),p=fe(o,"deploy"),l=pe(o,"deploy");if(!b.has(s)){t.onStepStart?.(p,l,r,O),t.onLog?.(`Skipping ${o} \u2014 no changes detected`,"info"),t.onStepComplete?.(p,l,"skipped",r,O);continue}const u=await K(e.assertLeaseForStack,s,e.abortSignal);if(u!==void 0)return await D(),t.onError?.(u),c(u);const k=qe(N,r,n.appName,b);if(k.length>=2){const w=await ke({hashService:a.hashService,cdkOutPath:A,stackNames:k.map(J=>I(n.appName,J)),detectionHashes:g.currentHashes,approvalGated:U});if(!w.success)return await D(),t.onError?.(w.error),c(w.error);const de=await ze(k,n,a,G,t,r,O,g,B,M);if(!de.success)return await D(),c(await ye(a.awsProvider,{stackNames:k.map(J=>I(n.appName,J)),appName:n.appName,originalError:de.error,cfnService:a.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));r+=k.length-1;continue}const d=await Ye(o,e,a,n,t,g.hasDockerfile);if(d!==null&&!d.success)return await D(),c(d.error);d!==null&&d.success&&(se=d.data);const m=d!==null&&d.success?d.data.contentHashTagsByService:{},f=bt(m);if(d!==null){const w=await K(e.assertLeaseForStack,s,e.abortSignal);if(w!==void 0)return await D(),t.onError?.(w),c(w)}const R=await ke({hashService:a.hashService,cdkOutPath:A,stackNames:[s],detectionHashes:g.currentHashes,approvalGated:U});if(!R.success)return await D(),t.onError?.(R.error),c(R.error);const F=await Je(o,a,G,t,r,O,B,f!==void 0||e.abortSignal!==void 0?{...f!==void 0?{parameters:f}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}:void 0);if(!F.success)return await D(),c(await ye(a.awsProvider,{stackNames:[s],appName:n.appName,originalError:F.error,cfnService:a.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));o===P.COMPUTE&&(ce=await Qe(n,a,t,m,e.abortSignal));const h=g.currentHashes.get(s);h&&M.set(s,h)}if(await D(),M.size>0){const r=await a.hashService.updateStateAfterDeploy(n.path,M);if(!r.success){const o=y(r.error.message);$.debug(x,"Failed to update state file",{error:o}),t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy unchanged stacks: ${o}`,"warn")}}const le=await a.stackService.resolveWebsiteUrl(n.appName);le&&(B.websiteUrl=le);const De=await nt(a,n.appName,se,t,A,ce,e.abortSignal);return await mt(a.awsProvider,{stackNames:[...b],appName:n.appName,onLog:v((r,o)=>t.onLog?.(r,o),"onLog"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}).catch(r=>{$.debug(x,"Forget completion sweep failed",{error:y(ge(r))})}),C({target:n.appName,deploymentType:"application",outputs:Object.keys(B).length>0?B:void 0,artefacts:De,durationMs:Date.now()-S})}v(lr,"deployApplication");export{lr as deployApplication,Se as emitStackTargets};
1
+ var De=Object.defineProperty;var D=(e,a)=>De(e,"name",{value:a,configurable:!0});import{join as ue}from"path";import{CloudFormationClient as Pe}from"@aws-sdk/client-cloudformation";import{EC2Client as Re}from"@aws-sdk/client-ec2";import{success as C,failure as c}from"@fjall/generator";import{logger as $}from"@fjall/util/logger";import{ImageTagSchema as Ce,getErrorMessage as ge,imageTagParameterName as Ie,maskSensitiveOutput as y}from"@fjall/util";import{parseDockerDeclarationsFromManifest as Oe}from"@fjall/util/manifest";import{stubCallerIdentity as Ae}from"../../types/deployment/index.js";import{getApplicationDeployOrder as Le,getApplicationStackName as I,getApplicationStepName as fe,getApplicationStepId as pe,APPLICATION_STACKS as P}from"../../types/operations.js";import{CdkContextBuilder as Be}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as Me,bootstrapOrFail as Fe}from"../contextHelpers.js";import{buildBootstrapTagsIfResolved as $e}from"../bootstrapTags.js";import{hasOrganisationTierAccount as xe}from"../../aws/organisations/accountGlobals.js";import{runDetectionPipeline as je}from"./detectionPipeline.js";import{STEP_IDS as _,STEP_NAMES as Ge}from"../../types/stepDefinitions.js";import{StepRegistry as He}from"../../steps/stepRegistry.js";import{checkTargetReadiness as Ue}from"../../aws/targetReadiness.js";import{cascadeHomeRegion as _e}from"../organisation/cascadeHelpers.js";import{withStepLifecycle as qe}from"../stepLifecycle.js";import{DOCKER_BUILD_STEP_NAME as me,runDockerBuild as Ke}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as We,getStackNamesToDeploy as Ve,deployParallelPhase as ze,deployStackSequential as Je,runDockerPreCompute as Ye,runLambdaVersionPublish as Qe,createBuildCallbacks as Xe,willRunDockerBuild as Ze}from"./applicationDeployHelpers.js";import{verifyAssemblyIntegrity as ke}from"./assemblyIntegrity.js";import{captureDatabaseConnectionOutputs as et,captureServiceTaskDefinitions as tt,reconcileDatabaseEndpointChange as rt}from"./databaseEndpointReconcile.js";import{deployCodeOnly as at}from"./codeOnlyDeploy.js";import{checkDeployPathStackAvailability as he}from"../activeDeploymentGuard.js";import{assertLeaseBeforeStack as q}from"./leaseGate.js";import{collectFullDeployArtefacts as nt}from"./artefactCollection.js";import{CfnRegistryService as ot}from"../../services/infrastructure/CfnRegistryService.js";import{ChangeSetProbe as it}from"../../services/infrastructure/changeSetProbe.js";import{runApprovalGate as st}from"./approvalGate.js";import{buildDestructionTicket as ct,runDestructionGate as lt}from"./destructionGate.js";import{DESTRUCTION_RESUME_TTL_MS as dt,signApprovalToken as ut}from"./plan/approvalToken.js";import{computeAssemblyDigest as gt}from"./plan/assemblyDigest.js";import{createMemoisedPlanLoader as ft}from"./plan/loadDeployPlan.js";import{runDriftPreFlightGate as pt}from"../drift/preFlightGate.js";import{completeForgetAfterDeploy as mt}from"../remediation/forgetResource.js";import{composePostFailureRepairOffer as ye}from"../remediation/postFailureRepairOffer.js";import{renderPinReportLines as kt,runPinRemediation as ht}from"../remediation/pinRemediation.js";import{runRecreatePreFlight as yt}from"../remediation/recreatePreFlight.js";import{runRecreateSurgery as St}from"../remediation/recreateResource.js";const x="applicationDeploy";function bt(e){const a=Object.entries(e);if(a.length===0)return;const n={};for(const[t,i]of a){const S=Ie(t);n[S]=i}return n}D(bt,"buildCdkImageTagParameters");function wt(e){if(e?.mode!==void 0&&e.mode!=="rollback"){if(e.imageTag)return c(new Error(`--image-tag requires rollback mode (explicit mode '${e.mode}' was supplied alongside an image tag)`));if(e.serviceImageTags)return c(new Error(`serviceImageTags requires rollback mode (explicit mode '${e.mode}' was supplied alongside serviceImageTags)`))}if(e?.mode==="rollback"){if(e.imageTag&&e.serviceImageTags)return c(new Error("rollback mode accepts exactly one of --image-tag or serviceImageTags \u2014 they are mutually exclusive"));if(!e.imageTag&&!e.serviceImageTags)return c(new Error("rollback mode requires --image-tag <tag> or serviceImageTags naming the image(s) to roll to"))}if(e?.containerName!==void 0&&!e.imageTag)return c(new Error("--container-name requires --image-tag \u2014 it targets which container within the service's task definition receives the rolled-back image"));if(e?.serviceImageTags!==void 0){if(e.serviceName)return c(new Error("serviceImageTags already names its services \u2014 --service cannot be combined with it"));const a=Object.entries(e.serviceImageTags);if(a.length===0)return c(new Error("serviceImageTags must name at least one service"));for(const[n,t]of a){const i=Ce.safeParse(t);if(!i.success)return c(new Error(`serviceImageTags.${n} is not a valid image tag: ${i.error.message}`))}}return C(void 0)}D(wt,"validateRollbackOptions");function Se(e,a,n){if(e.onStackTargets===void 0||n.length===0)return;const t=a.awsProvider.getAccountId(),i=a.awsProvider.getRegion();e.onStackTargets(n.map(S=>({stackName:S,...t!==""?{accountId:t}:{},...i!==""?{region:i}:{}})))}D(Se,"emitStackTargets");async function lr(e,a,n){const{callbacks:t,options:i}=e,S=Date.now(),j=i?.mode??(i?.imageTag||i?.serviceImageTags?"rollback":i?.deployOnly?"code-only":"full");if(j==="restart"){const r=new Error('mode "restart" is not a deploy \u2014 call the restart() entry point (fjall rollout) instead');return t.onError?.(r),c(r)}const Y=wt(i);if(!Y.success){const r=y(Y.error.message),o=new Error(r);return t.onError?.(o),c(o)}const G=Be.buildDeploymentContext({deployType:"application",target:n.appName,path:n.path,region:a.awsProvider.getRegion(),callerIdentity:Ae(a.awsProvider.getAccountId()),...Me({orgConfig:e.orgConfig,identity:e.identity,skipOidc:e.options?.skipOidc}),...e.managedDomainBindings!==void 0?{managedDomainBindings:e.managedDomainBindings}:{}},{verbose:i?.verbose,infraOnly:i?.infraOnly},e.orgConfig),T=a.frameworkRegistry.resolve({appPath:n.path});let Q;const L=j==="rollback";if(T&&!L){Q=T.builder.plan({appPath:n.path},T.detection);const r=Xe(t),o=await T.builder.build(n.path,Q,r,{skipBuild:i?.skipBuild,infraOnly:i?.infraOnly,...e.abortSignal!==void 0&&{abortSignal:e.abortSignal}});if(!o.success){const s=new Error(y(o.error.message));return t.onError?.(s),c(s)}}if(j==="code-only"||L){t.onLog?.(L?i?.serviceImageTags?`Rollback mode \u2014 rolling ${Object.keys(i.serviceImageTags).length} service(s) to their release-recorded image tags`:`Rollback mode \u2014 rolling to image tag ${i?.imageTag}`:"Deploy-only mode \u2014 skipping infrastructure pipeline","info");const r=ue(n.path,"cdk.out"),o=Oe(r),s=o.declaresBuild,f=T?.detection.hasDockerfile===!0,l=s||f;$.debug(x,"Deploy-only branch entered",{mode:j,imageTag:i?.imageTag,serviceImageTags:i?.serviceImageTags,appName:n.appName,appPath:n.path,cdkOutPath:r,dockerProviderAvailable:e.dockerProvider!==void 0,builderName:T?.builder.name,hasDockerfileFromManifest:s,hasDockerfileFromDisk:f,hasDockerfile:l,manifestDockerServiceCount:o.ecsServices.length,manifestDockerPaths:o.ecsServices.map(p=>p.docker.path),manifestLambdaDockerEntryCount:o.lambdaEntries.length}),!s&&!f&&t.onLog?.("No Dockerfile detected via manifest or appPath \u2014 skipping Docker build. If this app uses a cross-repo Dockerfile, ensure a full deploy has run first to populate cdk.out/fjall-manifest.json.","warn");const u=await he([I(n.appName,P.COMPUTE)],a.cfnService,e.abortSignal);if(!u.success)return t.onError?.(u.error),c(u.error);Se(t,a,[I(n.appName,P.COMPUTE)]);let k={},d;if(!L&&e.dockerProvider!==void 0&&l){$.debug(x,"Running Docker build before code-only deploy",{source:s?"manifest":"disk"});const p=await Ke(e,a,n,t);if(!p.success)return c(p.error);k=p.data.contentHashTagsByService,d=p.data}else $.debug(x,"Skipping Docker build",{reason:L?"rollback":e.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});const m=await q(e.assertLeaseForStack,I(n.appName,P.COMPUTE),e.abortSignal);return m!==void 0?(t.onError?.(m),c(m)):at(e,a,n,k,d)}t.onLog?.("Analysing infrastructure\u2026","info");const H=await je(n,a,G,t,e.abortSignal);if(!H.success){const r=new Error(y(H.error.message),{cause:H.error});return t.onError?.(r),c(r)}const g=H.data;try{await t.onDetectionComplete?.({...g,builderName:T?.builder.name??"unknown"})}catch(r){const o=new Error(y(ge(r)),{cause:r});return t.onError?.(o),c(o)}const be={deploymentType:"application",operation:"deploy",deployOnly:!1,infraOnly:i?.infraOnly??!1,hasDockerfile:g.hasDockerfile,pattern:g.pattern,resources:g.resources,...T&&{builderName:T.builder.name}},X=He.getSteps(be),Z=X.findIndex(r=>r.id===_.TARGET_READINESS),ee=await qe(t,{stepId:_.TARGET_READINESS,stepName:Ge.TARGET_READINESS,...Z>=0&&{stepIndex:Z,totalSteps:X.length}},async()=>{if(i?.skipReadinessCheck)return t.onLog?.("Skipping target readiness check (--skip-readiness-check)","warn"),{kind:"skipped",data:void 0};const r=a.awsProvider.getAccountId(),o=e.orgConfig?.providerAccounts.find(u=>u.id===r)?.name,s=a.awsProvider.getCredentials(),f=_e(e.orgConfig),l=await Ue({cloudFormation:a.awsProvider.getClient(Pe),ec2:new Re({region:f,...s!==void 0&&{credentials:s}})},{id:r,...o!==void 0&&{name:o}},a.awsProvider.getRegion(),e.orgConfig,e.abortSignal);return l.success?l.data.ready?{kind:"completed",data:void 0}:{kind:"error",error:new Error(y(l.data.advisory))}:{kind:"error",error:new Error(y(l.error.message),{cause:l.error})}},e.abortSignal);if(!ee.success)return ee;const N=Le({pattern:g.pattern,resources:g.resources}),O=N.length,te=g.hasDockerfile&&e.dockerProvider!==void 0&&N.includes(P.COMPUTE)&&i?.infraOnly!==!0;if(!g.hasDifferences&&!i?.force&&!te){t.onLog?.("No infrastructure changes detected","info"),t.onLog?.(i?.infraOnly===!0?"Infrastructure-only mode \u2014 application code was not rebuilt. Run with --deploy-only to deploy code changes.":"Nothing to deploy. Run with --deploy-only to redeploy application code.","info");const r=g.hasDockerfile&&e.dockerProvider!==void 0&&N.includes(P.COMPUTE);for(let s=0;s<N.length;s++){const f=N[s];r&&f===P.COMPUTE&&(t.onStepStart?.(_.DOCKER_OPERATIONS,me),t.onStepComplete?.(_.DOCKER_OPERATIONS,me,"skipped"));const l=pe(f,"deploy"),u=fe(f,"deploy");t.onStepStart?.(l,u,s,O),t.onStepComplete?.(l,u,"skipped",s,O)}const o=await a.stackService.resolveWebsiteUrl(n.appName);return C({target:n.appName,deploymentType:"application",outputs:o?{websiteUrl:o}:void 0,noChanges:!0,artefacts:[],durationMs:Date.now()-S})}const A=ue(n.path,"cdk.out"),b=Ve({deployOrder:N,stackChanges:g.stackChanges,appName:n.appName,force:i?.force===!0,computeBuildPossible:te});Se(t,a,[...b]);const K=await he([...b],a.cfnService,e.abortSignal);if(!K.success)return t.onError?.(K.error),c(K.error);const re={stacksToDeploy:[...b],imageBuild:Ze({stackNamesToDeploy:b,appName:n.appName,hasDockerProvider:e.dockerProvider!==void 0,hasDockerfile:g.hasDockerfile,infraOnly:i?.infraOnly===!0})},ae=gt(g.currentHashes,re),W=ft({cfnService:a.cfnService,cdkOutPath:A,changedStacks:[...b],assemblyDigest:ae,registry:new ot(a.awsProvider),changeSetProbe:new it(a.awsProvider),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}),V=await pt(a.awsProvider,{stackNames:[...b],cdkOutPath:A,cfnService:a.cfnService,callbacks:t,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(V.kind==="blocked"){const r=new Error(V.message);return t.onError?.(r),c(r)}const ne=V.verifiedAbsentByNecessity??[],U=e.approvalGate!==void 0||i?.approvalToken!==void 0&&i.approvalToken!==""||i?.planOnly===!0;if(U){const r=await st({approvalGate:e.approvalGate,options:i??{},target:n.appName,cdkOutPath:A,detection:{stackChanges:g.stackChanges,currentHashes:g.currentHashes},scope:re,cfnService:a.cfnService,loadPlan:W,callbacks:t,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!r.success)return r;const o=r.data;if(o.kind==="rejected")return t.onLog?.(`Deploy rejected: ${y(o.reason)}`,"warn"),C({target:n.appName,deploymentType:"application",rejected:!0,artefacts:[],...o.planDetail!==void 0?{planDetail:o.planDetail}:{},durationMs:Date.now()-S});if(o.kind==="awaiting"){const s=await W(),f=s.success?ct(s.data,ne):void 0;return C({target:n.appName,deploymentType:"application",awaitingApproval:!0,approvalToken:o.approvalToken,assemblyDigest:o.assemblyDigest,planDetail:o.planDetail,...f!==void 0?{destructionTicket:f}:{},artefacts:[],durationMs:Date.now()-S})}}const E=await lt({loadPlan:W,options:i??{},target:n.appName,callbacks:t,verifiedAbsentByNecessity:ne,...e.destructionConsentGate!==void 0?{consentGate:e.destructionConsentGate}:{}});if(!E.success)return t.onError?.(E.error),c(E.error);if(E.data.kind==="withheld"){const r=U?ut({assemblyDigest:ae,ttlMs:dt}):void 0;return C({target:n.appName,deploymentType:"application",destructionPending:!0,destructionTicket:E.data.ticket,destructionOutcome:E.data.outcome,destructionHaltReasons:E.data.reasons,...r!==void 0?{destructionResumeToken:r.token,destructionResumeExpiresAt:r.expiresAt}:{},artefacts:[],durationMs:Date.now()-S})}if(E.data.kind==="proceed"){const{ticket:r,verbsByPhysicalName:o}=E.data,s=r.resources.filter(l=>o.get(l.physicalName)==="pin");if(s.length>0){t.onLog?.(`Pin remediation for ${n.appName}: probing live value(s) for ${s.length} consented pin(s) \u2014 deploy stopped for config write-back; nothing was applied`,"warn");const l=await ht(a.awsProvider,{targets:s.map(u=>({stack:u.stack,logicalId:u.logicalId,resourceType:u.resourceType,physicalName:u.physicalName,offendingProperties:u.offendingProperties})),...e.infrastructureSource!==void 0?{infrastructureContent:e.infrastructureSource.content,infrastructureFilePath:e.infrastructureSource.filePath}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});for(const u of kt(l))t.onLog?.(u,"warn");return s.length<r.resources.length&&t.onLog?.("Remaining consented change(s) were not applied \u2014 the config write-back changes the plan, so they re-consent on the next deploy","warn"),C({target:n.appName,deploymentType:"application",pinPending:!0,pinReport:l,pinDeferredConsents:r.resources.length-s.length,destructionTicket:r,artefacts:[],durationMs:Date.now()-S})}const f=r.resources.filter(l=>l.finding==="pinned-name-replacement"&&o.get(l.physicalName)==="recreate");if(f.length>0){const l=new Map;for(const d of f){const m=l.get(d.stack)??[];m.push(d),l.set(d.stack,m)}const u=await yt(a.awsProvider,{stacks:[...l].map(([d,m])=>({stackName:d,targets:m.map(p=>({logicalId:p.logicalId,resourceType:p.resourceType,physicalId:p.physicalName}))})),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!u.success){const d=new Error(y(u.error.message),{cause:u.error});return t.onError?.(d),c(d)}const k=[];for(const[d,m]of l){const p=await q(e.assertLeaseForStack,d,e.abortSignal,"destroy");if(p!==void 0)return k.length>0&&t.onLog?.(`Surgery already completed on ${k.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`,"warn"),t.onError?.(p),c(p);const R=m.map(h=>h.physicalName);t.onLog?.(`Recreate remediation for ${n.appName}: orchestrating the pinned-name replacement of ${R.join(", ")} on ${d}`,"warn");const F=await St(a.awsProvider,{stackName:d,targets:m.map(h=>({logicalId:h.logicalId,resourceType:h.resourceType,physicalId:h.physicalName})),appName:n.appName,consentedPhysicalNames:R,doneJournalPolicy:"archive-and-restart",plannedReplacementLogicalIds:m.map(h=>h.logicalId),onProgress:D(h=>t.onLog?.(h,"info"),"onProgress"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!F.success){const h=k.length>0?` Surgery already completed on ${k.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`:"",w=new Error(y(`${F.error.message}${h}`));return t.onError?.(w),c(w)}k.push(`${d} (${R.join(", ")})`)}}}const we=e.orgConfig!==void 0&&!xe(e.orgConfig.providerAccounts),Te=$e({account:e.orgConfig?.providerAccounts.find(r=>r.id===a.awsProvider.getAccountId()),estate:we?"solo":"org"}),oe=await Fe(a,G,t,Te);if(!oe.success)return oe;const ie=I(n.appName,P.DATABASE),z=b.has(ie),Ne=z?await et(a.cfnService,n.appName,e.abortSignal):void 0,Ee=z?await tt(a.ecsResolver,a.ecsService,n.appName,e.abortSignal):void 0,B={},M=new Map;let se,ce={};const v=D(async()=>{!z||!M.has(ie)||await rt({appName:n.appName,connectionOutputsBefore:Ne,serviceTaskDefinitionsBefore:Ee,services:{cfnService:a.cfnService,ecsService:a.ecsService,ecsResolver:a.ecsResolver},callbacks:t,...e.confirmDatabaseEndpointRestart!==void 0?{confirmRestart:e.confirmDatabaseEndpointRestart}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})},"reconcileIfDatabaseReplaced");for(let r=0;r<N.length;r++){const o=N[r],s=I(n.appName,o),f=pe(o,"deploy"),l=fe(o,"deploy");if(!b.has(s)){t.onStepStart?.(f,l,r,O),t.onLog?.(`Skipping ${o} \u2014 no changes detected`,"info"),t.onStepComplete?.(f,l,"skipped",r,O);continue}const u=await q(e.assertLeaseForStack,s,e.abortSignal);if(u!==void 0)return await v(),t.onError?.(u),c(u);const k=We(N,r,n.appName,b);if(k.length>=2){const w=await ke({hashService:a.hashService,cdkOutPath:A,stackNames:k.map(J=>I(n.appName,J)),detectionHashes:g.currentHashes,approvalGated:U});if(!w.success)return await v(),t.onError?.(w.error),c(w.error);const de=await ze(k,n,a,G,t,r,O,g,B,M);if(!de.success)return await v(),c(await ye(a.awsProvider,{stackNames:k.map(J=>I(n.appName,J)),appName:n.appName,originalError:de.error,cfnService:a.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));r+=k.length-1;continue}const d=await Ye(o,e,a,n,t,g.hasDockerfile);if(d!==null&&!d.success)return await v(),c(d.error);d!==null&&d.success&&(se=d.data);const m=d!==null&&d.success?d.data.contentHashTagsByService:{},p=bt(m);if(d!==null){const w=await q(e.assertLeaseForStack,s,e.abortSignal);if(w!==void 0)return await v(),t.onError?.(w),c(w)}const R=await ke({hashService:a.hashService,cdkOutPath:A,stackNames:[s],detectionHashes:g.currentHashes,approvalGated:U});if(!R.success)return await v(),t.onError?.(R.error),c(R.error);const F=await Je(o,a,G,t,r,O,B,p!==void 0||e.abortSignal!==void 0?{...p!==void 0?{parameters:p}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}:void 0);if(!F.success)return await v(),c(await ye(a.awsProvider,{stackNames:[s],appName:n.appName,originalError:F.error,cfnService:a.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));o===P.COMPUTE&&(ce=await Qe(n,a,t,m,e.abortSignal));const h=g.currentHashes.get(s);h&&M.set(s,h)}if(await v(),M.size>0){const r=await a.hashService.updateStateAfterDeploy(n.path,M);if(!r.success){const o=y(r.error.message);$.debug(x,"Failed to update state file",{error:o}),t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy unchanged stacks: ${o}`,"warn")}}const le=await a.stackService.resolveWebsiteUrl(n.appName);le&&(B.websiteUrl=le);const ve=await nt(a,n.appName,se,t,A,ce,e.abortSignal);return await mt(a.awsProvider,{stackNames:[...b],appName:n.appName,onLog:D((r,o)=>t.onLog?.(r,o),"onLog"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}).catch(r=>{$.debug(x,"Forget completion sweep failed",{error:y(ge(r))})}),C({target:n.appName,deploymentType:"application",outputs:Object.keys(B).length>0?B:void 0,artefacts:ve,durationMs:Date.now()-S})}D(lr,"deployApplication");export{lr as deployApplication,Se as emitStackTargets};
@@ -28,20 +28,19 @@ export declare function getStackNamesToDeploy(params: {
28
28
  /**
29
29
  * Single source of the deploy loop's "will an image build/push run" outcome,
30
30
  * bound into the approval digest (DeployScope.imageBuild) so a token approves
31
- * the exact build behaviour, not the flag that requested it. Mirrors the
32
- * loop's ACTUAL gates in order: the skip predicate (Compute must be in the
33
- * to-deploy set), `runDockerPreCompute`'s provider guard, and its
34
- * hasDockerfile branch. Deliberately NOT `computeBuildPossible`: that flag
35
- * decides whether Compute must deploy FOR a build (false under
36
- * `--infra-only`), but a hash-changed Compute stack still builds regardless —
37
- * the loop consults only the three gates above, so `--infra-only` reaches
38
- * this predicate solely through the to-deploy set.
31
+ * the exact build behaviour, not the flag that requested it. Mirrors
32
+ * `runDockerPreCompute`'s ACTUAL gates: the skip predicate (Compute must be
33
+ * in the to-deploy set), its provider/hasDockerfile guard, and — because a
34
+ * hash-changed Compute stack can still land in the to-deploy set for a
35
+ * genuine infra reason an explicit `infraOnly` check, so `--infra-only`
36
+ * never authorises an image build no matter why Compute is in that set.
39
37
  */
40
38
  export declare function willRunDockerBuild(params: {
41
39
  stackNamesToDeploy: ReadonlySet<string>;
42
40
  appName: string;
43
41
  hasDockerProvider: boolean;
44
42
  hasDockerfile: boolean;
43
+ infraOnly: boolean;
45
44
  }): boolean;
46
45
  /**
47
46
  * Identify Phase 2 stacks (Storage, Messaging, Database) that are consecutive
@@ -1,4 +1,4 @@
1
- var B=Object.defineProperty;var a=(e,r)=>B(e,"name",{value:r,configurable:!0});import{join as T}from"node:path";import{success as N,failure as S}from"@fjall/generator";import{maskSensitiveOutput as w}from"@fjall/util";import{parseLambdaDockerEntriesFromManifest as D}from"@fjall/util/manifest";import{isAborted as L}from"../../aws/organisations/types.js";import{APPLICATION_STACKS as k,getApplicationStackName as P,getApplicationStepName as E,getApplicationStepId as b,PARALLEL_DEPLOY_GROUPS as R}from"../../types/operations.js";import{emitProgress as $}from"../../services/supporting/helpers.js";import{forwardOutput as x,forwardResourceProgress as j}from"../contextHelpers.js";import{emptyBuildProducts as v,runDockerBuild as I}from"./dockerBuildHelper.js";import{runWelcomeImageSetup as _}from"./welcomeImageHelper.js";import{withStepLifecycle as U}from"../stepLifecycle.js";function X(e){const r=new Set;for(const s of e.deployOrder){const i=P(e.appName,s),o=e.stackChanges.get(i)??!0,u=s===k.COMPUTE&&e.computeBuildPossible;(o||e.force||u)&&r.add(i)}return r}a(X,"getStackNamesToDeploy");function Z(e){return e.stackNamesToDeploy.has(P(e.appName,k.COMPUTE))&&e.hasDockerProvider&&e.hasDockerfile}a(Z,"willRunDockerBuild");function ee(e,r,s,i){const o=new Set(R.PHASE_2.stacks),u=[];for(let n=r;n<e.length&&o.has(e[n]);n++)u.push(e[n]);if(u.length<2)return[];const l=u.filter(n=>i.has(P(s,n)));return l.length>=2?l:[]}a(ee,"getParallelPhase2Stacks");async function te(e,r,s,i,o,u,l,n,m,p){for(let t=0;t<e.length;t++){const c=e[t],f=b(c,"deploy"),g=E(c,"deploy");o.onStepStart?.(f,g,u+t,l)}$(o,"Deploying infrastructure in parallel\u2026"),o.onParallelPhaseStart?.(e,"Storage and database resources (parallel)");const d=await s.stackService.deployStacksInParallel(e,i,{onOutput:x(o),onResourceProgress:a((t,c)=>{o.onResourceProgress?.(t),c&&o.onParallelStackResourceProgress?.(c,t)},"onResourceProgress"),onStackComplete:a((t,c,f,g)=>{const y=b(t,"deploy"),O=E(t,"deploy"),A=u+e.indexOf(t),C=L(s.abortSignal);o.onStepComplete?.(y,O,c?"completed":C?"cancelled":"error",A,l),!c&&g&&!C&&o.onError?.(new Error(w(g.message)))},"onStackComplete")});if(!d.success)return o.onParallelPhaseComplete?.([]),S(d.error);const h=d.data.filter(t=>!t.success);if(o.onParallelPhaseComplete?.(d.data.map(t=>({stack:t.stack,success:t.success,error:t.error}))),h.length>0){const t=h.map(f=>f.stack).join(", "),c=h.map(f=>`${f.stack}: ${w(f.error?.message??"Unknown error")}`).join(`
1
+ var B=Object.defineProperty;var a=(e,r)=>B(e,"name",{value:r,configurable:!0});import{join as T}from"node:path";import{success as N,failure as S}from"@fjall/generator";import{maskSensitiveOutput as w}from"@fjall/util";import{parseLambdaDockerEntriesFromManifest as R}from"@fjall/util/manifest";import{isAborted as k}from"../../aws/organisations/types.js";import{APPLICATION_STACKS as E,getApplicationStackName as P,getApplicationStepName as b,getApplicationStepId as C,PARALLEL_DEPLOY_GROUPS as $}from"../../types/operations.js";import{emitProgress as D}from"../../services/supporting/helpers.js";import{forwardOutput as O,forwardResourceProgress as j}from"../contextHelpers.js";import{emptyBuildProducts as v,runDockerBuild as I}from"./dockerBuildHelper.js";import{runWelcomeImageSetup as _}from"./welcomeImageHelper.js";import{withStepLifecycle as U}from"../stepLifecycle.js";function X(e){const r=new Set;for(const i of e.deployOrder){const s=P(e.appName,i),o=e.stackChanges.get(s)??!0,u=i===E.COMPUTE&&e.computeBuildPossible;(o||e.force||u)&&r.add(s)}return r}a(X,"getStackNamesToDeploy");function Z(e){return!e.infraOnly&&e.stackNamesToDeploy.has(P(e.appName,E.COMPUTE))&&e.hasDockerProvider&&e.hasDockerfile}a(Z,"willRunDockerBuild");function ee(e,r,i,s){const o=new Set($.PHASE_2.stacks),u=[];for(let n=r;n<e.length&&o.has(e[n]);n++)u.push(e[n]);if(u.length<2)return[];const c=u.filter(n=>s.has(P(i,n)));return c.length>=2?c:[]}a(ee,"getParallelPhase2Stacks");async function te(e,r,i,s,o,u,c,n,m,f){for(let t=0;t<e.length;t++){const l=e[t],p=C(l,"deploy"),g=b(l,"deploy");o.onStepStart?.(p,g,u+t,c)}D(o,"Deploying infrastructure in parallel\u2026"),o.onParallelPhaseStart?.(e,"Storage and database resources (parallel)");const d=await i.stackService.deployStacksInParallel(e,s,{onOutput:O(o),onResourceProgress:a((t,l)=>{o.onResourceProgress?.(t),l&&o.onParallelStackResourceProgress?.(l,t)},"onResourceProgress"),onStackComplete:a((t,l,p,g)=>{const y=C(t,"deploy"),x=b(t,"deploy"),A=u+e.indexOf(t),L=k(i.abortSignal);o.onStepComplete?.(y,x,l?"completed":L?"cancelled":"error",A,c),!l&&g&&!L&&o.onError?.(new Error(w(g.message)))},"onStackComplete")});if(!d.success)return o.onParallelPhaseComplete?.([]),S(d.error);const h=d.data.filter(t=>!t.success);if(o.onParallelPhaseComplete?.(d.data.map(t=>({stack:t.stack,success:t.success,error:t.error}))),h.length>0){const t=h.map(p=>p.stack).join(", "),l=h.map(p=>`${p.stack}: ${w(p.error?.message??"Unknown error")}`).join(`
2
2
  `);return S(new Error(`Failed to deploy stacks: ${t}
3
3
 
4
- ${c}`))}for(const t of d.data){if(t.success&&t.outputs)for(const[g,y]of Object.entries(t.outputs))m[g]=String(y);const c=P(r.appName,t.stack),f=n.currentHashes.get(c);f&&p.set(c,f)}return N(void 0)}a(te,"deployParallelPhase");async function re(e,r,s,i,o,u,l,n){const m=b(e,"deploy"),p=E(e,"deploy");return U(i,{stepId:m,stepName:p,stepIndex:o,totalSteps:u},async()=>{const d=await r.stackService.deployStack(e,s,{onOutput:x(i),onResourceProgress:j(i)},n?.parameters!==void 0||n?.abortSignal!==void 0?{...n?.parameters!==void 0?{parameters:n.parameters}:{},...n?.abortSignal!==void 0?{abortSignal:n.abortSignal}:{}}:void 0);if(!d.success)return{kind:"error",error:d.error};if(d.data.outputs)for(const[h,t]of Object.entries(d.data.outputs))l[h]=String(t);return{kind:"completed",data:void 0}},r.abortSignal)}a(re,"deployStackSequential");async function oe(e,r,s,i,o,u){if(e!==k.COMPUTE||!r.dockerProvider)return null;if(u){const n=await I(r,s,i,o);return n.success?N(n.data):S(n.error)}const l=await _(r,s,i,o);return l.success?N(v()):S(l.error)}a(oe,"runDockerPreCompute");async function ne(e,r,s,i,o){if(Object.keys(i).length===0)return{};const u=T(e.path,"cdk.out"),n=D(u).filter(p=>i[p.imageKey]!==void 0);if(n.length===0)return{};const m={};for(const p of n){if(L(o)){s.onLog?.(`Skipping the Lambda version publish for '${p.name}' \u2014 the deployment was cancelled. The function's $LATEST code is up to date; only the published-version record is affected.`,"warn");continue}const d=await r.lambdaService.publishVersion(p.name,o);if(!d.success){s.onLog?.(`Published image for Lambda '${p.name}', but publishing a new version failed: ${w(d.error.message)}. The function's $LATEST code is up to date; only the published-version record is affected.`,"warn");continue}m[p.name]=d.data}return m}a(ne,"runLambdaVersionPublish");function se(e){return{onBuildStart:a(r=>{e.onOpenNextBuildStart?.(),e.onLog?.(`${r} build started`,"info")},"onBuildStart"),onBuildProgress:a((r,s)=>{e.onOpenNextProgress?.(s)},"onBuildProgress"),onBuildComplete:a(r=>{e.onOpenNextBuildComplete?.(),e.onLog?.(`${r} build complete`,"info")},"onBuildComplete"),onBuildError:a((r,s)=>{e.onOpenNextBuildError?.(s)},"onBuildError")}}a(se,"createBuildCallbacks");export{se as createBuildCallbacks,te as deployParallelPhase,re as deployStackSequential,ee as getParallelPhase2Stacks,X as getStackNamesToDeploy,oe as runDockerPreCompute,ne as runLambdaVersionPublish,Z as willRunDockerBuild};
4
+ ${l}`))}for(const t of d.data){if(t.success&&t.outputs)for(const[g,y]of Object.entries(t.outputs))m[g]=String(y);const l=P(r.appName,t.stack),p=n.currentHashes.get(l);p&&f.set(l,p)}return N(void 0)}a(te,"deployParallelPhase");async function re(e,r,i,s,o,u,c,n){const m=C(e,"deploy"),f=b(e,"deploy");return U(s,{stepId:m,stepName:f,stepIndex:o,totalSteps:u},async()=>{const d=await r.stackService.deployStack(e,i,{onOutput:O(s),onResourceProgress:j(s)},n?.parameters!==void 0||n?.abortSignal!==void 0?{...n?.parameters!==void 0?{parameters:n.parameters}:{},...n?.abortSignal!==void 0?{abortSignal:n.abortSignal}:{}}:void 0);if(!d.success)return{kind:"error",error:d.error};if(d.data.outputs)for(const[h,t]of Object.entries(d.data.outputs))c[h]=String(t);return{kind:"completed",data:void 0}},r.abortSignal)}a(re,"deployStackSequential");async function oe(e,r,i,s,o,u){if(e!==E.COMPUTE||!r.dockerProvider)return null;if(u&&r.options?.infraOnly!==!0){const n=await I(r,i,s,o);return n.success?N(n.data):S(n.error)}if(u)return null;const c=await _(r,i,s,o);return c.success?N(v()):S(c.error)}a(oe,"runDockerPreCompute");async function ne(e,r,i,s,o){if(Object.keys(s).length===0)return{};const u=T(e.path,"cdk.out"),n=R(u).filter(f=>s[f.imageKey]!==void 0);if(n.length===0)return{};const m={};for(const f of n){if(k(o)){i.onLog?.(`Skipping the Lambda version publish for '${f.name}' \u2014 the deployment was cancelled. The function's $LATEST code is up to date; only the published-version record is affected.`,"warn");continue}const d=await r.lambdaService.publishVersion(f.name,o);if(!d.success){i.onLog?.(`Published image for Lambda '${f.name}', but publishing a new version failed: ${w(d.error.message)}. The function's $LATEST code is up to date; only the published-version record is affected.`,"warn");continue}m[f.name]=d.data}return m}a(ne,"runLambdaVersionPublish");function ie(e){return{onBuildStart:a(r=>{e.onOpenNextBuildStart?.(),e.onLog?.(`${r} build started`,"info")},"onBuildStart"),onBuildProgress:a((r,i)=>{e.onOpenNextProgress?.(i)},"onBuildProgress"),onBuildComplete:a(r=>{e.onOpenNextBuildComplete?.(),e.onLog?.(`${r} build complete`,"info")},"onBuildComplete"),onBuildError:a((r,i)=>{e.onOpenNextBuildError?.(i)},"onBuildError")}}a(ie,"createBuildCallbacks");export{ie as createBuildCallbacks,te as deployParallelPhase,re as deployStackSequential,ee as getParallelPhase2Stacks,X as getStackNamesToDeploy,oe as runDockerPreCompute,ne as runLambdaVersionPublish,Z as willRunDockerBuild};
@@ -4,6 +4,7 @@ import type { DeployParams, DeployResult } from "../../types/params.js";
4
4
  import type { ApplicationOperation } from "../../types/operations.js";
5
5
  import type { DeployServices } from "../serviceFactory.js";
6
6
  import { type DockerBuildProducts } from "./dockerBuildHelper.js";
7
+ import { type PreResolvedRollout } from "./serviceImageTagsRollback.js";
7
8
  /**
8
9
  * Resolve the ECR image tag the rollout should pin against for a given
9
10
  * manifest service. Keys the content-hash tag map by the bare `<service>`
@@ -71,6 +72,15 @@ export declare function rebaseRollbackTagForService(suppliedTag: string, service
71
72
  * enough for that.
72
73
  */
73
74
  export declare function deployCodeOnly(params: DeployParams, services: DeployServices, operation: ApplicationOperation, contentHashTagsByService: Record<string, string>, dockerBuildProducts?: DockerBuildProducts): Promise<Result<DeployResult>>;
75
+ interface ServiceRolloutOk {
76
+ taskDefinitionArn: string;
77
+ previousTaskDefinitionArn: string;
78
+ imageUri: string;
79
+ ecrRepositoryArn: string;
80
+ imageDigest: string | undefined;
81
+ durationMs: number;
82
+ }
83
+ export declare function rolloutService(services: DeployServices, clusterArn: string, serviceArn: string, serviceName: string, imageTag: string, callbacks: DeployParams["callbacks"], rebasedFromTag?: string, preResolved?: PreResolvedRollout, abortSignal?: AbortSignal, containerName?: string): Promise<Result<ServiceRolloutOk, Error>>;
74
84
  /**
75
85
  * Derive an ECR repository ARN from an image URI of shape
76
86
  * `<account>.dkr.ecr.<region>.amazonaws.com/<repo>`. Returns "" when the URI
@@ -78,3 +88,4 @@ export declare function deployCodeOnly(params: DeployParams, services: DeploySer
78
88
  * can persist the imageUri without a misleading ARN.
79
89
  */
80
90
  export declare function deriveEcrRepositoryArn(repoUri: string): string;
91
+ export {};
@@ -1 +1 @@
1
- var ae=Object.defineProperty;var y=(e,r)=>ae(e,"name",{value:r,configurable:!0});import{join as Q}from"path";import{success as V,failure as G}from"@fjall/generator";import{logger as A}from"@fjall/util/logger";import{artefactOutputKey as O,imageTagParameterName as ne,maskSensitiveOutput as N}from"@fjall/util";import{parseDockerServicesFromManifest as ie,parseLambdaDockerEntriesFromManifest as oe}from"@fjall/util/manifest";import{APPLICATION_STACKS as W,getApplicationStackName as se,getApplicationStepId as ce,getApplicationStepName as ge}from"../../types/operations.js";import{extractEcsServiceName as F}from"../../aws/utils/arnParser.js";import{isAborted as Z}from"../../aws/organisations/types.js";import{stripTagOrDigest as le}from"../../services/infrastructure/EcrImageInspectorService.js";import{withStepLifecycle as de}from"../stepLifecycle.js";import{registerAndRollTaskDefinition as me}from"./taskDefinitionRoll.js";import{ecrRepoUri as fe}from"./dockerBuildHelper.js";import{matchesServiceName as ue,matchServiceImageTagsToLiveServices as pe,preflightResolveServiceImageTagDigests as he,resolveServiceTaskDefinition as ve}from"./serviceImageTagsRollback.js";import{executeLambdaServiceImageTagsRollout as ke,matchesLambdaFunctionName as $e,preflightLambdaServiceImageTags as we,rolloutLambdaImageTag as ye,rolloutLambdaPrimary as Se}from"./lambdaCodeOnlyRollout.js";const C="codeOnlyDeploy";function De(e,r){if(e)return r[e.name]}y(De,"lookupContentHashTag");function Te(e,r){const t=r.toLowerCase();return e.find(a=>a.name.toLowerCase()===t)??e.find(a=>t.includes(a.name.toLowerCase()))}y(Te,"findManifestServiceForEcsService");function be(e,r,t){const a=r.toLowerCase(),l=e.toLowerCase(),n=t.map(s=>s.toLowerCase()).filter(s=>s!==""&&l.startsWith(`${s}-`)).sort((s,v)=>v.length-s.length)[0];if(n===void 0||n===a)return e;const h=e.slice(n.length);return`${a}${h}`}y(be,"rebaseRollbackTagForService");const Ae=0,Ne=1;function B(e,r,t,a,l){a[O(e,"TaskDefinition")]=t.taskDefinitionArn,a[O(e,"PreviousTaskDefinition")]=t.previousTaskDefinitionArn,a[O(e,"ImageTag")]=r,a[O(e,"ImageUri")]=t.imageUri,a[O(e,"EcrRepositoryArn")]=t.ecrRepositoryArn,t.imageDigest!==void 0&&(a[O(e,"ImageDigest")]=t.imageDigest),l.push({serviceName:e,imageTag:r,imageUri:t.imageUri,taskDefinitionArn:t.taskDefinitionArn,previousTaskDefinitionArn:t.previousTaskDefinitionArn,...t.imageDigest!==void 0?{imageDigest:t.imageDigest}:{},...t.ecrRepositoryArn!==""?{ecrRepositoryArn:t.ecrRepositoryArn}:{}})}y(B,"recordRolloutOutputs");async function X(e,r,t,a,l){if(t.length===0)return;const n=se(r,W.COMPUTE),h={};for(const{serviceName:v,imageTag:D}of t)h[ne(v)]=D;const s=await e.cfnService.updateStackParameters(n,h,l);if(!s.success){a.onLog?.(`Rollback succeeded but the CloudFormation ImageTag parameter reconcile failed \u2014 the next full deploy may revert this rollback. Reason: ${N(s.error.message)}`,"warn");return}s.data.skippedKeys.length>0&&(a.onLog?.(`Rollback succeeded but ${s.data.skippedKeys.length} ImageTag parameter(s) were not found on stack ${n} (${s.data.skippedKeys.join(", ")}) \u2014 the next full deploy may revert those services`,"warn"),A.debug(C,"ImageTag reconcile skipped stale parameters",{stackName:n,skippedKeys:s.data.skippedKeys}))}y(X,"reconcileImageTagParameters");function ee(e,r,t,a){if(e.kind==="error")return r.failures.length>0?{kind:"error",error:new Error(`${e.error.message}; Lambda rollout failures: ${r.failures.join("; ")}`)}:e;if(r.failures.length>0)return{kind:"error",error:new Error(`Lambda rollout failures: ${r.failures.join("; ")}`)};const l=e.data,n={...l.outputs??{},...r.outputs};return{kind:"completed",data:{target:t.appName,deploymentType:"application",outputs:Object.keys(n).length>0?n:void 0,artefacts:[...l.artefacts,...r.artefacts],durationMs:Date.now()-a}}}y(ee,"mergeEcsAndLambdaOutcomes");async function Xe(e,r,t,a,l){const{callbacks:n,options:h}=e,s=Date.now(),v=ce(W.COMPUTE,"deploy"),D=ge(W.COMPUTE,"deploy");return de(n,{stepId:v,stepName:D,stepIndex:Ae,totalSteps:Ne},async()=>{const c=h?.imageTag,$=h?.serviceImageTags;c?n.onLog?.(`Rolling to supplied image tag ${c}`,"info"):$&&n.onLog?.(`Rolling ${Object.keys($).length} service(s) to their release-recorded image tags`,"info");const u=ie(Q(t.path,"cdk.out"));A.debug(C,"Code-only rollout starting",{explicitRollbackTag:c,serviceImageTags:$,workingDirectory:e.workingDirectory,contentHashTagsByService:a,manifestServiceCount:u.length,manifestServices:u.map(i=>({name:i.name,target:i.docker.target}))}),n.onLog?.("Discovering ECS cluster and services\u2026","info");const o=await r.ecsResolver.getDeployableClusterAndServices(t.appName,e.abortSignal);if(!o.success)return A.debug(C,"ECS discovery failed",{appName:t.appName,error:N(o.error.message)}),{kind:"error",error:o.error};const{clusterArn:p,serviceArns:k}=o.data;A.debug(C,"ECS discovery complete",{appName:t.appName,clusterArn:p,serviceCount:k.length,serviceArns:k});const f=oe(Q(t.path,"cdk.out")),R=Array.from(new Set(f.map(i=>i.imageKey))),U=p!==void 0&&k.length>0,K=f.length>0;if(!U&&!K){const i=`No deployable ECS services or container-image Lambda functions found for ${t.appName}`;return n.onLog?.(i,"warn"),{kind:"skipped",data:{target:t.appName,deploymentType:"application",durationMs:Date.now()-s,noChanges:!0,artefacts:[]}}}const d=p??"";if($!==void 0){const i=pe(U?k:[],$),m=await we(r,t,$,e.abortSignal);if(!m.success)return{kind:"error",error:m.error};const g=new Set(m.data.matched.map(w=>w.entry.name));for(const w of i.unmatchedLiveServiceNames)n.onLog?.(`Service ${w} is not part of this release \u2014 leaving it untouched`,"warn");for(const w of i.staleMapKeys)g.has(w)||n.onLog?.(`Service ${w} from the release no longer exists in the stack`,"warn");if(i.matched.length===0&&g.size===0)return{kind:"error",error:new Error("None of the services in serviceImageTags matched a live ECS service or container-image Lambda function in this stack \u2014 nothing to roll")};const x=i.matched.length>0?await he(r,d,i.matched,e.abortSignal):V(new Map);if(!x.success)return{kind:"error",error:x.error};const M=i.matched.length>0?await Ce(r,t,d,i.matched,x.data,n,e.abortSignal,s):{kind:"completed",data:{target:t.appName,deploymentType:"application",artefacts:[],durationMs:Date.now()-s}},P=await ke(r,m.data,n,e.abortSignal),S=[...M.kind==="completed"?M.data.artefacts.map(w=>({serviceName:w.serviceName,imageTag:w.imageTag})):[],...P.rolledImageKeys];return await X(r,t.appName,S,n,e.abortSignal),ee(M,P,t,s)}const L=h?.serviceName?.toLowerCase(),E=L===""?void 0:L,b=E?k.filter(i=>ue(F(i)??"",E)):k;if(E){const i=f.some(m=>$e(m.name,E));if(b.length===0&&!i){const m=[...k.map(g=>F(g)??"unknown"),...f.map(g=>g.name)];return{kind:"error",error:new Error(`Service '${E}' not found. Available: ${m.join(", ")||"none"}`)}}}const _={},j=[],I=[],z=[...k.map(i=>F(i)).filter(i=>i!==void 0&&i!==""),...R];for(let i=0;i<b.length;i++){if(Z(e.abortSignal))return{kind:"error",error:new Error(`Rollout cancelled after ${j.length}/${b.length} services`)};const m=b[i];if(!m)continue;const g=F(m)??`service-${i+1}`,x=`[${i+1}/${b.length}] ${g}`,M=Te(u,g),P=De(M,a);let S,w;if(c!==void 0)S=be(c,g,z),S!==c?(w=c,n.onLog?.(`Re-based image tag ${c} \u2192 ${S} for ${g}`,"info")):z.length>1&&!c.toLowerCase().startsWith(`${g.toLowerCase()}-`)&&n.onLog?.(`Image tag ${c} has no recognised service prefix; applying verbatim to ${g} \u2014 confirm it points at this service's image`,"warn");else if(P!==void 0)S=P;else{const J=`No image available to deploy for ${g}: no content-hash tag was produced this run. Run a full deploy to build the image, or pass --image-tag <tag> to roll a specific existing image.`;n.onLog?.(J,"warn"),I.push(`${g}: ${J}`);continue}A.debug(C,"Starting service rollout",{serviceName:g,serviceArn:m,imageTag:S,explicitRollbackTag:c,contentHashTag:P,manifestServiceMatched:M?.name,dockerTarget:M?.docker.target,index:i,total:b.length}),n.onECSUpdate?.(`${x}: rolling out image tag ${S}`,g);const T=await te(r,d,m,g,S,n,w,void 0,e.abortSignal);if(A.debug(C,"Service rollout completed",{serviceName:g,success:T.success,...T.success?{taskDefinitionArn:T.data.taskDefinitionArn,imageDigest:T.data.imageDigest,durationMs:T.data.durationMs}:{error:N(T.error.message)}}),!T.success){I.push(`${g}: ${N(T.error.message)}`);continue}B(g,S,T.data,_,j)}const re=Date.now()-s;if(I.length>0){const i=b.length-I.length,m=b.length,g=I.length===m?`All ${m} ECS service rollouts failed: ${I.join("; ")}`:`Partial rollout: ${i}/${m} services succeeded. Failures: ${I.join("; ")}`;return{kind:"error",error:new Error(g)}}const Y=j.map(i=>({serviceName:i.serviceName,imageTag:i.imageTag})),q={kind:"completed",data:{target:t.appName,deploymentType:"application",outputs:Object.keys(_).length>0?_:void 0,artefacts:j,durationMs:re}};if(!K)return c!==void 0&&await X(r,t.appName,Y,n,e.abortSignal),q;const H=c!==void 0?await ye(r,t,c,z,n,E,e.abortSignal):await Se(r,t,a,l?.imageDigestsByService??{},l?.repoUri??fe(r.awsProvider.getAccountId(),r.awsProvider.getRegion(),t.appName),n,E,e.abortSignal);return c!==void 0&&await X(r,t.appName,[...Y,...H.rolledImageKeys],n,e.abortSignal),ee(q,H,t,s)},e.abortSignal)}y(Xe,"deployCodeOnly");async function Ce(e,r,t,a,l,n,h,s){const v={},D=[],c=[];for(let u=0;u<a.length;u++){if(Z(h))return{kind:"error",error:new Error(`Rollout cancelled after ${D.length}/${a.length} services`)};const o=a[u];if(!o)continue;const p=`[${u+1}/${a.length}] ${o.serviceName}`,k=l.get(o.serviceArn);if(k===void 0){c.push(`${o.serviceName}: pre-flight digest missing after a successful pre-flight`);continue}A.debug(C,"Starting service rollout (serviceImageTags)",{serviceName:o.serviceName,serviceArn:o.serviceArn,imageTag:o.tag,mapKey:o.mapKey,index:u,total:a.length}),n.onECSUpdate?.(`${p}: rolling out image tag ${o.tag}`,o.serviceName);const f=await te(e,t,o.serviceArn,o.serviceName,o.tag,n,void 0,k,h);if(A.debug(C,"Service rollout completed (serviceImageTags)",{serviceName:o.serviceName,success:f.success,...f.success?{taskDefinitionArn:f.data.taskDefinitionArn,imageDigest:f.data.imageDigest,durationMs:f.data.durationMs}:{error:N(f.error.message)}}),!f.success){c.push(`${o.serviceName}: ${N(f.error.message)}`);continue}B(o.serviceName,o.tag,f.data,v,D)}const $=Date.now()-s;if(c.length>0){const u=a.length-c.length,o=a.length,p=c.length===o?`All ${o} ECS service rollouts failed: ${c.join("; ")}`:`Partial rollout: ${u}/${o} services succeeded. Failures: ${c.join("; ")}`;return{kind:"error",error:new Error(p)}}return{kind:"completed",data:{target:r.appName,deploymentType:"application",outputs:Object.keys(v).length>0?v:void 0,artefacts:D,durationMs:$}}}y(Ce,"rollOutServiceImageTagsMap");async function te(e,r,t,a,l,n,h,s,v){const D=Date.now();let c,$,u,o,p;if(s!==void 0)c=s.taskDefinition.previousTaskDefArn,$=s.taskDefinition.taskDefinition,u=s.taskDefinition.containerDefinitions,o=s.taskDefinition.repoUri,p=s.imageDigest;else{const d=await ve(e,r,t,a,v);if(!d.success)return G(d.error);c=d.data.previousTaskDefArn,$=d.data.taskDefinition,u=d.data.containerDefinitions,o=d.data.repoUri;const L=await e.ecrImageInspector.getImageDigest(o,l,v);if(L.success)p=L.data;else{if(h!==void 0)return G(new Error(`Re-based image tag '${l}' (from supplied '${h}') was not found in ECR for ${a}: ${N(L.error.message)}. The supplied tag's build has no image for ${a} \u2014 supply that service's own tag, or pass --service to roll a single service.`));n.onLog?.(`Could not resolve image digest for ${a} (${N(L.error.message)}); falling back to tag pinning`,"warn")}}const k=y(d=>p!==void 0?`${d}@${p}`:`${d}:${l}`,"buildContainerImage"),f=u.map(d=>d.image?{...d,image:k(le(d.image))}:d),R=await me(e,{clusterArn:r,serviceArn:t,serviceName:a,taskDefinition:$,containerDefinitions:f,previousTaskDefArn:c,...p!==void 0?{imageDigest:p}:{},startedAtMs:D},n,v);if(!R.success)return G(R.error);const U=`${o}:${l}`,K=Le(o);return V({taskDefinitionArn:R.data.newTaskDefArn,previousTaskDefinitionArn:c,imageUri:U,ecrRepositoryArn:K,imageDigest:p,durationMs:R.data.durationMs})}y(te,"rolloutService");function Le(e){const r=e.match(/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/);if(!r)return"";const[,t,a,l]=r;return`arn:aws:ecr:${a}:${t}:repository/${l}`}y(Le,"deriveEcrRepositoryArn");export{Xe as deployCodeOnly,Le as deriveEcrRepositoryArn,De as lookupContentHashTag,be as rebaseRollbackTagForService};
1
+ var ne=Object.defineProperty;var S=(e,r)=>ne(e,"name",{value:r,configurable:!0});import{join as Q}from"path";import{success as V,failure as F}from"@fjall/generator";import{logger as N}from"@fjall/util/logger";import{artefactOutputKey as P,imageTagParameterName as ie,maskSensitiveOutput as C}from"@fjall/util";import{parseDockerServicesFromManifest as oe,parseLambdaDockerEntriesFromManifest as se}from"@fjall/util/manifest";import{APPLICATION_STACKS as W,getApplicationStackName as ce,getApplicationStepId as ge,getApplicationStepName as le}from"../../types/operations.js";import{extractEcsServiceName as _}from"../../aws/utils/arnParser.js";import{isAborted as Z}from"../../aws/organisations/types.js";import{stripTagOrDigest as B}from"../../services/infrastructure/EcrImageInspectorService.js";import{withStepLifecycle as de}from"../stepLifecycle.js";import{registerAndRollTaskDefinition as me}from"./taskDefinitionRoll.js";import{ecrRepoUri as ue}from"./dockerBuildHelper.js";import{matchesServiceName as fe,matchServiceImageTagsToLiveServices as pe,preflightResolveServiceImageTagDigests as he,resolveServiceTaskDefinition as ve,selectContainerToRetag as ke}from"./serviceImageTagsRollback.js";import{executeLambdaServiceImageTagsRollout as $e,matchesLambdaFunctionName as we,preflightLambdaServiceImageTags as ye,rolloutLambdaImageTag as Se,rolloutLambdaPrimary as De}from"./lambdaCodeOnlyRollout.js";const L="codeOnlyDeploy";function Te(e,r){if(e)return r[e.name]}S(Te,"lookupContentHashTag");function be(e,r){const t=r.toLowerCase();return e.find(a=>a.name.toLowerCase()===t)??e.find(a=>t.includes(a.name.toLowerCase()))}S(be,"findManifestServiceForEcsService");function Ae(e,r,t){const a=r.toLowerCase(),d=e.toLowerCase(),n=t.map(s=>s.toLowerCase()).filter(s=>s!==""&&d.startsWith(`${s}-`)).sort((s,v)=>v.length-s.length)[0];if(n===void 0||n===a)return e;const p=e.slice(n.length);return`${a}${p}`}S(Ae,"rebaseRollbackTagForService");const Ne=0,Ce=1;function ee(e,r,t,a,d){a[P(e,"TaskDefinition")]=t.taskDefinitionArn,a[P(e,"PreviousTaskDefinition")]=t.previousTaskDefinitionArn,a[P(e,"ImageTag")]=r,a[P(e,"ImageUri")]=t.imageUri,a[P(e,"EcrRepositoryArn")]=t.ecrRepositoryArn,t.imageDigest!==void 0&&(a[P(e,"ImageDigest")]=t.imageDigest),d.push({serviceName:e,imageTag:r,imageUri:t.imageUri,taskDefinitionArn:t.taskDefinitionArn,previousTaskDefinitionArn:t.previousTaskDefinitionArn,...t.imageDigest!==void 0?{imageDigest:t.imageDigest}:{},...t.ecrRepositoryArn!==""?{ecrRepositoryArn:t.ecrRepositoryArn}:{}})}S(ee,"recordRolloutOutputs");async function X(e,r,t,a,d){if(t.length===0)return;const n=ce(r,W.COMPUTE),p={};for(const{serviceName:v,imageTag:T}of t)p[ie(v)]=T;const s=await e.cfnService.updateStackParameters(n,p,d);if(!s.success){a.onLog?.(`Rollback succeeded but the CloudFormation ImageTag parameter reconcile failed \u2014 the next full deploy may revert this rollback. Reason: ${C(s.error.message)}`,"warn");return}s.data.skippedKeys.length>0&&(a.onLog?.(`Rollback succeeded but ${s.data.skippedKeys.length} ImageTag parameter(s) were not found on stack ${n} (${s.data.skippedKeys.join(", ")}) \u2014 the next full deploy may revert those services`,"warn"),N.debug(L,"ImageTag reconcile skipped stale parameters",{stackName:n,skippedKeys:s.data.skippedKeys}))}S(X,"reconcileImageTagParameters");function te(e,r,t,a){if(e.kind==="error")return r.failures.length>0?{kind:"error",error:new Error(`${e.error.message}; Lambda rollout failures: ${r.failures.join("; ")}`)}:e;if(r.failures.length>0)return{kind:"error",error:new Error(`Lambda rollout failures: ${r.failures.join("; ")}`)};const d=e.data,n={...d.outputs??{},...r.outputs};return{kind:"completed",data:{target:t.appName,deploymentType:"application",outputs:Object.keys(n).length>0?n:void 0,artefacts:[...d.artefacts,...r.artefacts],durationMs:Date.now()-a}}}S(te,"mergeEcsAndLambdaOutcomes");async function Ye(e,r,t,a,d){const{callbacks:n,options:p}=e,s=Date.now(),v=ge(W.COMPUTE,"deploy"),T=le(W.COMPUTE,"deploy");return de(n,{stepId:v,stepName:T,stepIndex:Ne,totalSteps:Ce},async()=>{const c=p?.imageTag,k=p?.serviceImageTags;c?n.onLog?.(`Rolling to supplied image tag ${c}`,"info"):k&&n.onLog?.(`Rolling ${Object.keys(k).length} service(s) to their release-recorded image tags`,"info");const h=oe(Q(t.path,"cdk.out"));N.debug(L,"Code-only rollout starting",{explicitRollbackTag:c,serviceImageTags:k,workingDirectory:e.workingDirectory,contentHashTagsByService:a,manifestServiceCount:h.length,manifestServices:h.map(i=>({name:i.name,target:i.docker.target}))}),n.onLog?.("Discovering ECS cluster and services\u2026","info");const o=await r.ecsResolver.getDeployableClusterAndServices(t.appName,e.abortSignal);if(!o.success)return N.debug(L,"ECS discovery failed",{appName:t.appName,error:C(o.error.message)}),{kind:"error",error:o.error};const{clusterArn:$,serviceArns:m}=o.data;N.debug(L,"ECS discovery complete",{appName:t.appName,clusterArn:$,serviceCount:m.length,serviceArns:m});const u=se(Q(t.path,"cdk.out")),z=Array.from(new Set(u.map(i=>i.imageKey))),K=$!==void 0&&m.length>0,E=u.length>0;if(!K&&!E){const i=`No deployable ECS services or container-image Lambda functions found for ${t.appName}`;return n.onLog?.(i,"warn"),{kind:"skipped",data:{target:t.appName,deploymentType:"application",durationMs:Date.now()-s,noChanges:!0,artefacts:[]}}}const O=$??"";if(k!==void 0){const i=pe(K?m:[],k),f=await ye(r,t,k,e.abortSignal);if(!f.success)return{kind:"error",error:f.error};const g=new Set(f.data.matched.map(y=>y.entry.name));for(const y of i.unmatchedLiveServiceNames)n.onLog?.(`Service ${y} is not part of this release \u2014 leaving it untouched`,"warn");for(const y of i.staleMapKeys)g.has(y)||n.onLog?.(`Service ${y} from the release no longer exists in the stack`,"warn");if(i.matched.length===0&&g.size===0)return{kind:"error",error:new Error("None of the services in serviceImageTags matched a live ECS service or container-image Lambda function in this stack \u2014 nothing to roll")};const x=i.matched.length>0?await he(r,O,i.matched,e.abortSignal):V(new Map);if(!x.success)return{kind:"error",error:x.error};const M=i.matched.length>0?await Le(r,t,O,i.matched,x.data,n,e.abortSignal,s):{kind:"completed",data:{target:t.appName,deploymentType:"application",artefacts:[],durationMs:Date.now()-s}},R=await $e(r,f.data,n,e.abortSignal),D=[...M.kind==="completed"?M.data.artefacts.map(y=>({serviceName:y.serviceName,imageTag:y.imageTag})):[],...R.rolledImageKeys];return await X(r,t.appName,D,n,e.abortSignal),te(M,R,t,s)}const U=p?.serviceName?.toLowerCase(),l=U===""?void 0:U,w=l?m.filter(i=>fe(_(i)??"",l)):m;if(l){const i=u.some(f=>we(f.name,l));if(w.length===0&&!i){const f=[...m.map(g=>_(g)??"unknown"),...u.map(g=>g.name)];return{kind:"error",error:new Error(`Service '${l}' not found. Available: ${f.join(", ")||"none"}`)}}}const A={},j=[],I=[],G=[...m.map(i=>_(i)).filter(i=>i!==void 0&&i!==""),...z];for(let i=0;i<w.length;i++){if(Z(e.abortSignal))return{kind:"error",error:new Error(`Rollout cancelled after ${j.length}/${w.length} services`)};const f=w[i];if(!f)continue;const g=_(f)??`service-${i+1}`,x=`[${i+1}/${w.length}] ${g}`,M=be(h,g),R=Te(M,a);let D,y;if(c!==void 0)D=Ae(c,g,G),D!==c?(y=c,n.onLog?.(`Re-based image tag ${c} \u2192 ${D} for ${g}`,"info")):G.length>1&&!c.toLowerCase().startsWith(`${g.toLowerCase()}-`)&&n.onLog?.(`Image tag ${c} has no recognised service prefix; applying verbatim to ${g} \u2014 confirm it points at this service's image`,"warn");else if(R!==void 0)D=R;else{const J=`No image available to deploy for ${g}: no content-hash tag was produced this run. Run a full deploy to build the image, or pass --image-tag <tag> to roll a specific existing image.`;n.onLog?.(J,"warn"),I.push(`${g}: ${J}`);continue}N.debug(L,"Starting service rollout",{serviceName:g,serviceArn:f,imageTag:D,explicitRollbackTag:c,contentHashTag:R,manifestServiceMatched:M?.name,dockerTarget:M?.docker.target,index:i,total:w.length}),n.onECSUpdate?.(`${x}: rolling out image tag ${D}`,g);const b=await re(r,O,f,g,D,n,y,void 0,e.abortSignal,p?.containerName);if(N.debug(L,"Service rollout completed",{serviceName:g,success:b.success,...b.success?{taskDefinitionArn:b.data.taskDefinitionArn,imageDigest:b.data.imageDigest,durationMs:b.data.durationMs}:{error:C(b.error.message)}}),!b.success){I.push(`${g}: ${C(b.error.message)}`);continue}ee(g,D,b.data,A,j)}const ae=Date.now()-s;if(I.length>0){const i=w.length-I.length,f=w.length,g=I.length===f?`All ${f} ECS service rollouts failed: ${I.join("; ")}`:`Partial rollout: ${i}/${f} services succeeded. Failures: ${I.join("; ")}`;return{kind:"error",error:new Error(g)}}const Y=j.map(i=>({serviceName:i.serviceName,imageTag:i.imageTag})),q={kind:"completed",data:{target:t.appName,deploymentType:"application",outputs:Object.keys(A).length>0?A:void 0,artefacts:j,durationMs:ae}};if(!E)return c!==void 0&&await X(r,t.appName,Y,n,e.abortSignal),q;const H=c!==void 0?await Se(r,t,c,G,n,l,e.abortSignal):await De(r,t,a,d?.imageDigestsByService??{},d?.repoUri??ue(r.awsProvider.getAccountId(),r.awsProvider.getRegion(),t.appName),n,l,e.abortSignal);return c!==void 0&&await X(r,t.appName,[...Y,...H.rolledImageKeys],n,e.abortSignal),te(q,H,t,s)},e.abortSignal)}S(Ye,"deployCodeOnly");async function Le(e,r,t,a,d,n,p,s){const v={},T=[],c=[];for(let h=0;h<a.length;h++){if(Z(p))return{kind:"error",error:new Error(`Rollout cancelled after ${T.length}/${a.length} services`)};const o=a[h];if(!o)continue;const $=`[${h+1}/${a.length}] ${o.serviceName}`,m=d.get(o.serviceArn);if(m===void 0){c.push(`${o.serviceName}: pre-flight digest missing after a successful pre-flight`);continue}N.debug(L,"Starting service rollout (serviceImageTags)",{serviceName:o.serviceName,serviceArn:o.serviceArn,imageTag:o.tag,mapKey:o.mapKey,index:h,total:a.length}),n.onECSUpdate?.(`${$}: rolling out image tag ${o.tag}`,o.serviceName);const u=await re(e,t,o.serviceArn,o.serviceName,o.tag,n,void 0,m,p);if(N.debug(L,"Service rollout completed (serviceImageTags)",{serviceName:o.serviceName,success:u.success,...u.success?{taskDefinitionArn:u.data.taskDefinitionArn,imageDigest:u.data.imageDigest,durationMs:u.data.durationMs}:{error:C(u.error.message)}}),!u.success){c.push(`${o.serviceName}: ${C(u.error.message)}`);continue}ee(o.serviceName,o.tag,u.data,v,T)}const k=Date.now()-s;if(c.length>0){const h=a.length-c.length,o=a.length,$=c.length===o?`All ${o} ECS service rollouts failed: ${c.join("; ")}`:`Partial rollout: ${h}/${o} services succeeded. Failures: ${c.join("; ")}`;return{kind:"error",error:new Error($)}}return{kind:"completed",data:{target:r.appName,deploymentType:"application",outputs:Object.keys(v).length>0?v:void 0,artefacts:T,durationMs:k}}}S(Le,"rollOutServiceImageTagsMap");async function re(e,r,t,a,d,n,p,s,v,T){const c=Date.now();let k,h,o,$,m,u;if(s!==void 0)k=s.taskDefinition.previousTaskDefArn,h=s.taskDefinition.taskDefinition,o=s.taskDefinition.containerDefinitions,$=s.repoUri,m=s.imageDigest,u=s.containerName;else{const l=await ve(e,r,t,a,v);if(!l.success)return F(l.error);k=l.data.previousTaskDefArn,h=l.data.taskDefinition,o=l.data.containerDefinitions;const w=ke(o,a,T);if(!w.success)return F(w.error);u=w.data.name,$=B(w.data.image);const A=await e.ecrImageInspector.getImageDigest($,d,v);if(A.success)m=A.data;else{if(p!==void 0)return F(new Error(`Re-based image tag '${d}' (from supplied '${p}') was not found in ECR for ${a}: ${C(A.error.message)}. The supplied tag's build has no image for ${a} \u2014 supply that service's own tag, or pass --service to roll a single service.`));n.onLog?.(`Could not resolve image digest for ${a} (${C(A.error.message)}); falling back to tag pinning`,"warn")}}const z=S(l=>m!==void 0?`${l}@${m}`:`${l}:${d}`,"buildContainerImage"),K=o.map(l=>!l.image||l.name!==u?l:{...l,image:z(B(l.image))}),E=await me(e,{clusterArn:r,serviceArn:t,serviceName:a,taskDefinition:h,containerDefinitions:K,previousTaskDefArn:k,...m!==void 0?{imageDigest:m}:{},startedAtMs:c},n,v);if(!E.success)return F(E.error);const O=`${$}:${d}`,U=Ee($);return V({taskDefinitionArn:E.data.newTaskDefArn,previousTaskDefinitionArn:k,imageUri:O,ecrRepositoryArn:U,imageDigest:m,durationMs:E.data.durationMs})}S(re,"rolloutService");function Ee(e){const r=e.match(/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/);if(!r)return"";const[,t,a,d]=r;return`arn:aws:ecr:${a}:${t}:repository/${d}`}S(Ee,"deriveEcrRepositoryArn");export{Ye as deployCodeOnly,Ee as deriveEcrRepositoryArn,Te as lookupContentHashTag,Ae as rebaseRollbackTagForService,re as rolloutService};
@@ -35,6 +35,29 @@ export interface ResolvedServiceTaskDefinition {
35
35
  containerDefinitions: NonNullable<TaskDefinition["containerDefinitions"]>;
36
36
  repoUri: string;
37
37
  }
38
+ type ContainerDefinitionEntry = NonNullable<TaskDefinition["containerDefinitions"]>[number];
39
+ export interface RetagTarget {
40
+ name: string;
41
+ image: string;
42
+ }
43
+ /**
44
+ * Select the single container a code-only rollout should retag. A task
45
+ * definition can hold containers fjall never built — a Datadog agent, an
46
+ * OTel collector — pulling a fixed image that must never be touched by
47
+ * `--image-tag` or a fresh content-hash tag. Blindly rewriting every
48
+ * container's image (the historical behaviour) corrupts those, and for a
49
+ * service with more than one fjall-built container it applies one tag to
50
+ * images that don't share a digest.
51
+ *
52
+ * An explicit `containerName` targets that container by name
53
+ * (case-insensitive), erroring if none matches. Without one: a single
54
+ * imaged container is unambiguous; with several, the ECS "primary
55
+ * container" convention (the first with a port mapping — the one that
56
+ * receives ALB traffic, per `EcsContainerConfig`'s documented semantics)
57
+ * resolves it. When neither disambiguates, the caller must pass
58
+ * `--container-name` rather than have every container silently retagged.
59
+ */
60
+ export declare function selectContainerToRetag(containerDefinitions: readonly ContainerDefinitionEntry[], serviceName: string, containerName?: string): Result<RetagTarget, Error>;
38
61
  /**
39
62
  * Fetch a service's current task definition and derive its ECR repository
40
63
  * URI from the first container image. Shared by the pre-flight digest
@@ -45,6 +68,20 @@ export declare function resolveServiceTaskDefinition(services: Pick<DeployServic
45
68
  export interface PreResolvedRollout {
46
69
  taskDefinition: ResolvedServiceTaskDefinition;
47
70
  imageDigest: string;
71
+ /**
72
+ * The single container this rollout retags — selected once during
73
+ * pre-flight (via `selectContainerToRetag`) so the rollout step commits to
74
+ * exactly the container the digest below was resolved against.
75
+ */
76
+ containerName: string;
77
+ /**
78
+ * Repo URI of `containerName`'s current image. Deliberately independent of
79
+ * `taskDefinition.repoUri` (a generic first-image guess) — this is the repo
80
+ * the digest was actually resolved against, which can differ when the
81
+ * selected container isn't first in the task definition (e.g. a sidecar
82
+ * ordered ahead of the primary container).
83
+ */
84
+ repoUri: string;
48
85
  }
49
86
  /**
50
87
  * Resolve an ECR digest for every matched (service, tag) pair BEFORE any
@@ -54,3 +91,4 @@ export interface PreResolvedRollout {
54
91
  * returning no digest) fails the whole pre-flight and rolls nothing.
55
92
  */
56
93
  export declare function preflightResolveServiceImageTagDigests(services: DeployServices, clusterArn: string, matched: readonly MatchedServiceImageTag[], abortSignal?: AbortSignal): Promise<Result<Map<string, PreResolvedRollout>, Error>>;
94
+ export {};
@@ -1 +1 @@
1
- var E=Object.defineProperty;var l=(s,c)=>E(s,"name",{value:c,configurable:!0});import{success as D,failure as g}from"@fjall/generator";import{maskSensitiveOutput as h}from"@fjall/util";import{extractEcsServiceName as $}from"../../aws/utils/arnParser.js";import{isAborted as k}from"../../aws/organisations/types.js";import{stripTagOrDigest as N}from"../../services/infrastructure/EcrImageInspectorService.js";function S(s,c){const a=s.toLowerCase(),r=c.toLowerCase();return a===r||a.endsWith(r)}l(S,"matchesServiceName");function M(s,c){const a=Object.entries(c),r=[],d=[],n=new Set,o=s.map(e=>{const t=$(e)??e;return{serviceArn:e,serviceName:t,lowerName:t.toLowerCase()}}),i=new Map,u=new Set;for(const e of o){const t=a.find(([f])=>f.toLowerCase()===e.lowerName);t!==void 0&&(i.set(e.serviceArn,t),u.add(t[0]))}const m=new Map,v=new Map;for(const e of o){if(i.has(e.serviceArn))continue;const t=a.filter(([f])=>!u.has(f)&&S(e.serviceName,f)).sort(([f],[w])=>w.length-f.length)[0];t!==void 0&&(m.set(e.serviceArn,t),v.set(t[0],(v.get(t[0])??0)+1))}for(const e of o){const t=m.get(e.serviceArn),f=i.get(e.serviceArn)??(t!==void 0&&v.get(t[0])===1?t:void 0);if(f===void 0){d.push(e.serviceName);continue}const[w,y]=f;n.add(w),r.push({serviceArn:e.serviceArn,serviceName:e.serviceName,tag:y,mapKey:w})}const p=a.map(([e])=>e).filter(e=>!n.has(e));return{matched:r,unmatchedLiveServiceNames:d,staleMapKeys:p}}l(M,"matchServiceImageTagsToLiveServices");async function x(s,c,a,r,d){const n=await s.ecsService.describeServices(c,[a],d);if(!n.success)return g(new Error(`Failed to describe service ${r}: ${h(n.error.message)}`));const o=n.data[0];if(!o?.taskDefinition)return g(new Error(`Service ${r} has no task definition`));const i=o.taskDefinition,u=await s.ecsService.getLatestTaskDefinition(i,d);if(!u.success)return g(new Error(`Failed to fetch task definition for ${r}: ${h(u.error.message)}`));const m=u.data;if(!m||!m.containerDefinitions||m.containerDefinitions.length===0)return g(new Error(`Task definition for ${r} has no container definitions`));const v=m.containerDefinitions,p=v.find(t=>t.image)?.image;if(!p)return g(new Error(`Task definition for ${r} has no container image \u2014 cannot derive repository URI`));const e=N(p);return D({previousTaskDefArn:i,taskDefinition:m,containerDefinitions:v,repoUri:e})}l(x,"resolveServiceTaskDefinition");async function K(s,c,a,r){const d=new Map;for(const n of a){if(k(r))return g(new Error("Pre-flight cancelled. No service was rolled."));const o=await x(s,c,n.serviceArn,n.serviceName,r);if(!o.success)return g(new Error(`Pre-flight failed resolving the current task definition for ${n.serviceName} (tag ${n.tag}): ${h(o.error.message)}. No service was rolled.`));const i=await s.ecrImageInspector.getImageDigest(o.data.repoUri,n.tag,r);if(!i.success||i.data===void 0){const u=i.success?"ECR returned no digest for this tag (non-ECR repository, or the image is missing)":h(i.error.message);return g(new Error(`Pre-flight digest resolution failed for ${n.serviceName} (tag ${n.tag}): ${u}. No service was rolled.`))}d.set(n.serviceArn,{taskDefinition:o.data,imageDigest:i.data})}return D(d)}l(K,"preflightResolveServiceImageTagDigests");export{M as matchServiceImageTagsToLiveServices,S as matchesServiceName,K as preflightResolveServiceImageTagDigests,x as resolveServiceTaskDefinition};
1
+ var D=Object.defineProperty;var v=(a,c)=>D(a,"name",{value:c,configurable:!0});import{success as p,failure as d}from"@fjall/generator";import{maskSensitiveOutput as w}from"@fjall/util";import{extractEcsServiceName as k}from"../../aws/utils/arnParser.js";import{isAborted as C}from"../../aws/organisations/types.js";import{stripTagOrDigest as y}from"../../services/infrastructure/EcrImageInspectorService.js";function x(a,c){const s=a.toLowerCase(),n=c.toLowerCase();return s===n||s.endsWith(n)}v(x,"matchesServiceName");function b(a,c){const s=Object.entries(c),n=[],m=[],r=new Set,e=a.map(t=>{const i=k(t)??t;return{serviceArn:t,serviceName:i,lowerName:i.toLowerCase()}}),f=new Map,g=new Set;for(const t of e){const i=s.find(([u])=>u.toLowerCase()===t.lowerName);i!==void 0&&(f.set(t.serviceArn,i),g.add(i[0]))}const o=new Map,l=new Map;for(const t of e){if(f.has(t.serviceArn))continue;const i=s.filter(([u])=>!g.has(u)&&x(t.serviceName,u)).sort(([u],[$])=>$.length-u.length)[0];i!==void 0&&(o.set(t.serviceArn,i),l.set(i[0],(l.get(i[0])??0)+1))}for(const t of e){const i=o.get(t.serviceArn),u=f.get(t.serviceArn)??(i!==void 0&&l.get(i[0])===1?i:void 0);if(u===void 0){m.push(t.serviceName);continue}const[$,E]=u;r.add($),n.push({serviceArn:t.serviceArn,serviceName:t.serviceName,tag:E,mapKey:$})}const h=s.map(([t])=>t).filter(t=>!r.has(t));return{matched:n,unmatchedLiveServiceNames:m,staleMapKeys:h}}v(b,"matchServiceImageTagsToLiveServices");function N(a,c,s){const n=a.filter(e=>e.image!==void 0),m=n.map(e=>e.name||"(unnamed)").join(", ")||"none";if(s!==void 0){const e=n.find(f=>(f.name??"").toLowerCase()===s.toLowerCase());return e===void 0?d(new Error(`Container "${s}" not found on ${c}'s task definition. Available: ${m}`)):p({name:e.name??"",image:e.image})}if(n.length===0)return d(new Error(`${c}'s task definition has no container image to retag`));if(n.length===1){const e=n[0];return p({name:e.name??"",image:e.image})}const r=n.filter(e=>(e.portMappings?.length??0)>0);if(r.length===1){const e=r[0];return p({name:e.name??"",image:e.image})}return d(new Error(`${c} has multiple containers (${m}) and no single primary container could be determined \u2014 pass --container-name <name> to target one explicitly.`))}v(N,"selectContainerToRetag");async function A(a,c,s,n,m){const r=await a.ecsService.describeServices(c,[s],m);if(!r.success)return d(new Error(`Failed to describe service ${n}: ${w(r.error.message)}`));const e=r.data[0];if(!e?.taskDefinition)return d(new Error(`Service ${n} has no task definition`));const f=e.taskDefinition,g=await a.ecsService.getLatestTaskDefinition(f,m);if(!g.success)return d(new Error(`Failed to fetch task definition for ${n}: ${w(g.error.message)}`));const o=g.data;if(!o||!o.containerDefinitions||o.containerDefinitions.length===0)return d(new Error(`Task definition for ${n} has no container definitions`));const l=o.containerDefinitions,h=l.find(i=>i.image)?.image;if(!h)return d(new Error(`Task definition for ${n} has no container image \u2014 cannot derive repository URI`));const t=y(h);return p({previousTaskDefArn:f,taskDefinition:o,containerDefinitions:l,repoUri:t})}v(A,"resolveServiceTaskDefinition");async function P(a,c,s,n){const m=new Map;for(const r of s){if(C(n))return d(new Error("Pre-flight cancelled. No service was rolled."));const e=await A(a,c,r.serviceArn,r.serviceName,n);if(!e.success)return d(new Error(`Pre-flight failed resolving the current task definition for ${r.serviceName} (tag ${r.tag}): ${w(e.error.message)}. No service was rolled.`));const f=N(e.data.containerDefinitions,r.serviceName);if(!f.success)return d(new Error(`Pre-flight failed selecting a container to retag for ${r.serviceName} (tag ${r.tag}): ${w(f.error.message)}. No service was rolled.`));const g=y(f.data.image),o=await a.ecrImageInspector.getImageDigest(g,r.tag,n);if(!o.success||o.data===void 0){const l=o.success?"ECR returned no digest for this tag (non-ECR repository, or the image is missing)":w(o.error.message);return d(new Error(`Pre-flight digest resolution failed for ${r.serviceName} (tag ${r.tag}): ${l}. No service was rolled.`))}m.set(r.serviceArn,{taskDefinition:e.data,imageDigest:o.data,containerName:f.data.name,repoUri:g})}return p(m)}v(P,"preflightResolveServiceImageTagDigests");export{b as matchServiceImageTagsToLiveServices,x as matchesServiceName,P as preflightResolveServiceImageTagDigests,A as resolveServiceTaskDefinition,N as selectContainerToRetag};
@@ -203,6 +203,16 @@ export interface DeployOptions {
203
203
  * build + push and rolls ECS to `<repo>:<imageTag>`. Implies `deployOnly`.
204
204
  */
205
205
  imageTag?: string;
206
+ /**
207
+ * Target a specific container within a service's task definition for the
208
+ * `imageTag` rollback (case-insensitive). Required only when the service
209
+ * has more than one imaged container and no single primary (the container
210
+ * with a port mapping) can be determined — e.g. two fjall-built containers
211
+ * in one service. Never required for a sidecar-only service (a Datadog
212
+ * agent alongside one app container): the sidecar is left untouched
213
+ * automatically. Requires `imageTag`.
214
+ */
215
+ containerName?: string;
206
216
  /**
207
217
  * Exact per-service image tags for rollback mode (F12), keyed by the bare
208
218
  * service name recorded in a Release's `ServiceArtefact[]`. Mutually
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/deploy-core",
3
- "version": "3.11.0",
3
+ "version": "3.11.2",
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",
@@ -99,6 +99,5 @@
99
99
  "@types/node": "^26.0.0",
100
100
  "typescript": "^6.0.3",
101
101
  "vitest": "^4.1.5"
102
- },
103
- "gitHead": "a15125ea026d52428ff5433d098699899695ca3f"
102
+ }
104
103
  }