@fjall/deploy-core 2.24.0 → 2.25.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.
Files changed (24) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/index.d.ts +2 -0
  3. package/dist/src/index.js +1 -1
  4. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  5. package/dist/src/orchestration/application/codeOnlyDeploy.d.ts +6 -0
  6. package/dist/src/orchestration/application/codeOnlyDeploy.js +1 -1
  7. package/dist/src/orchestration/application/serviceImageTagsRollback.d.ts +56 -0
  8. package/dist/src/orchestration/application/serviceImageTagsRollback.js +1 -0
  9. package/dist/src/orchestration/application/taskDefinitionRoll.d.ts +33 -0
  10. package/dist/src/orchestration/application/taskDefinitionRoll.js +1 -0
  11. package/dist/src/orchestration/index.d.ts +4 -0
  12. package/dist/src/orchestration/index.js +1 -1
  13. package/dist/src/orchestration/restart/restartApplication.d.ts +69 -0
  14. package/dist/src/orchestration/restart/restartApplication.js +1 -0
  15. package/dist/src/orchestration/restart/secretsDrift.d.ts +54 -0
  16. package/dist/src/orchestration/restart/secretsDrift.js +1 -0
  17. package/dist/src/services/infrastructure/CloudFormationService.d.ts +17 -0
  18. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  19. package/dist/src/services/infrastructure/EcrImageInspectorService.d.ts +11 -1
  20. package/dist/src/services/infrastructure/EcrImageInspectorService.js +1 -1
  21. package/dist/src/services/infrastructure/SsmParameterMetadataService.d.ts +23 -0
  22. package/dist/src/services/infrastructure/SsmParameterMetadataService.js +1 -0
  23. package/dist/src/types/params.d.ts +8 -0
  24. package/package.json +4 -4
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 178 files minified at 2026-07-05T07:50:53.066Z
1
+ 183 files minified at 2026-07-06T11:56:38.163Z
@@ -44,6 +44,8 @@ export { fileExists } from "@fjall/util/fsHelpers";
44
44
  export { type Result, success, failure, isSuccess, isFailure } from "@fjall/generator";
45
45
  export { deploy } from "./orchestration/index.js";
46
46
  export { destroy } from "./orchestration/index.js";
47
+ export { restart, probeSecretsDrift, computeSecretsDrift, deriveStaleParameters, extractConsumedSsmParameters } from "./orchestration/index.js";
48
+ export type { RestartParams, RestartResult, SecretsDriftProbeParams, ApplicationSecretsDrift, ServiceSecretsDrift, StaleParameter } from "./orchestration/index.js";
47
49
  export { partitionAccounts } from "./orchestration/index.js";
48
50
  export { buildRegionList, buildAccountRegionPairs, cascadeHomeRegion, cascadeOperationKey } from "./orchestration/index.js";
49
51
  export type { AccountRegionPair } from "./orchestration/index.js";
package/dist/src/index.js CHANGED
@@ -1 +1 @@
1
- import{DeploymentEventSchema as t,DEPLOYMENT_EVENT_TYPES as o,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as E,DEPLOYMENT_EVENT_STATUS_REASON_MAX as a,DEPLOYMENT_EVENT_ERROR_MESSAGE_MAX as i,DEPLOYMENT_EVENT_TRAIL_DETAIL_MAX as s,CASCADE_PHASES as S,CASCADE_ACCOUNT_STATUSES as n}from"./types/index.js";import{DeployEventSchema as A,StepProgressSchema as T,DeployResourceSchema as _,PlanChangeSchema as l,PlanPropertyChangeSchema as p,ChangeCountsSchema as O,DEPLOY_EVENT_CONTRACT as P,DEPLOY_EVENT_CONTRACT_MAJOR as R,DEPLOY_EVENT_CONTRACT_MINOR as u,SUPPORTED_CONTRACT_MARKERS as m,DEPLOY_EVENT_ID_MAX as d,DEPLOY_EVENT_NAME_MAX as C,DEPLOY_EVENT_MESSAGE_MAX as N,DEPLOY_EVENT_STATUS_REASON_MAX as D,DEPLOY_EVENT_ERROR_MESSAGE_MAX as L,DEPLOY_EVENT_TRAIL_DETAIL_MAX as f,DEPLOY_EVENT_OUTPUT_MAX as g,DEPLOY_EVENT_ARN_MAX as I,DEPLOY_EVENT_URL_MAX as y,STEP_COMPLETE_STATUSES as x,LOG_LEVELS as U,STACK_CLEANUP_PHASES as M,CASCADE_ACCOUNT_PHASES as v,DEPLOY_RESULTS as k,DETECTION_PHASES as Y,PHASE_STATUSES as h,BUILDERS as b,CASCADE_OUTCOME_RESULTS as V,PLAN_ACTIONS as F,REPLACEMENT_MODES as B,APPROVAL_KINDS as G,APPROVAL_DECISIONS as K,ALL_PROGRESS_KINDS as X,createDeployEmitter as H,parseDeployEvent as w,parseDeployContractVersion as j}from"./types/index.js";import{SimpleAwsProvider as q}from"./aws/index.js";import{checkTargetReadiness as W,describeTargetReadinessReason as Z,buildTargetUnreadyAdvisory as z,buildTargetSetUnreadyAdvisory as $,probeAccountStackExists as ee}from"./aws/index.js";import{checkRegionEnabled as te,regionDisabledMessage as oe}from"./aws/index.js";import{ensureOrganisationExists as ae,describeOrganisation as ie,enablePolicyTypes as se,enableServiceAccess as Se,SERVICE_PRINCIPALS as ne,enableRamSharing as ce,activateTrustedAccess as Ae,enableIpamDelegatedAdmin as Te,updateBackupGlobalSettings as _e,listAccounts as le,findAccount as pe,createAccount as Oe,ensureOrganisationalUnitsExist as Pe,placeAccountsInOUs as Re,buildAccountToOUMap as ue,activateCostAllocationTags as me,checkIdentityCentreStatus as de,snapshotIdentityCentre as Ce,composeSdkAbortSignal as Ne,extractErrorName as De,isAborted as Le,DEFAULT_MAX_PAGES as fe,IDENTITY_STORE_PAGE_SIZE as ge,drainPages as Ie,isOULeaf as ye,registerSecurityDelegates as xe,SECURITY_SERVICE_PRINCIPALS as Ue,enableCentralisedRootAccess as Me,ROOT_ACCESS_FEATURES as ve,RootAccessEnablementError as ke}from"./aws/index.js";import{STEP_IDS as he,STEP_NAMES as be,INFRASTRUCTURE_STEP_NAMES as Ve,INFRA_STEP_NAME as Fe}from"./types/index.js";import{ProgressReporter as Ge,APPLICATION_STACKS as Ke,ORGANISATION_TYPES as Xe,APPLICATION_DEPLOY_ORDER as He,APPLICATION_DESTROY_ORDER as we,OPENNEXT_DEPLOY_ORDER as je,OPENNEXT_DESTROY_ORDER as Qe,PARALLEL_DEPLOY_GROUPS as qe,PARALLEL_DESTROY_GROUPS as Je,OPENNEXT_PARALLEL_GROUPS as We,PARALLEL_OPERATION_TYPES as Ze,isApplicationOperation as ze,isOrganisationOperation as $e,getParallelDeployGroups as er,getParallelDestroyGroups as rr,getApplicationDeployOrder as tr,getApplicationDestroyOrder as or,getApplicationStackName as Er,getOrganisationStackName as ar,isApplicationStack as ir,isQuarantineDetail as sr,isRetainedBucketsDetail as Sr,getApplicationStepName as nr,getApplicationStepId as cr,toPascalCase as Ar,isOpenNextPattern as Tr,OPENNEXT_PATTERNS as _r,deriveResourcesFromManifestStacks as lr,STACK_NOT_FOUND_PATTERN as pr,STACK_FAILED_STATE_PATTERN as Or,CDK_NO_STACKS_MATCH as Pr,INFRASTRUCTURE_FILENAME as Rr,ApplicationError as ur,wrapApplicationError as mr,FjallStateFileSchema as dr,readStateFile as Cr,writeStateFile as Nr,createEmptyState as Dr,deleteStateFile as Lr,updateTemplateHash as fr,getStateFilePath as gr,stubCallerIdentity as Ir}from"./types/index.js";import{CloudFormationEventMonitor as xr}from"./aws/index.js";import{CdkService as Mr,CdkArgumentBuilder as vr,CdkProcessManager as kr,CdkEventMonitor as Yr,startStackMonitoring as hr,DEFAULT_DEPLOY_TIMEOUT_MS as br,isCdkError as Vr,formatInfrastructureError as Fr,getStructuralHint as Br,getSourceContext as Gr,hasCdkDifferences as Kr,parseDiffOutput as Xr,CloudFormationService as Hr,CloudFormationError as wr,EcsService as jr,EcsError as Qr,EcsServiceResolver as qr,TemplateHashService as Jr,TemplateHashError as Wr,CdkContextBuilder as Zr,emitProgress as zr,PROGRESS_MESSAGES as $r,parseBuildPhase as et,buildStepContextBuildConfig as rt,convertCloudFormationOutputsToRecord as tt,ApplicationStackService as ot}from"./services/index.js";import{CdkError as at}from"./types/errors/index.js";import{BaseServiceError as st,ValidationError as St,AuthError as nt,AwsError as ct,DeploymentError as At,NetworkError as Tt,FileSystemError as _t,ConfigError as lt,toServiceError as pt}from"./types/errors/index.js";import{filterDangerousEnvVars as Pt,maskSensitiveOutput as Rt,parseShellArgs as ut,sleep as mt}from"@fjall/util";import{artefactOutputKey as Ct,DeployModeSchema as Nt,ServiceArtefactSchema as Dt,ServiceArtefactsSchema as Lt}from"@fjall/util";import{hasDockerfile as gt}from"./util/dockerfileDetection.js";import{sleepAbortable as yt}from"./util/sleepAbortable.js";import{createSequencedCallbacks as Ut}from"./util/sequencedCallbacks.js";import{fileExists as vt}from"@fjall/util/fsHelpers";import{success as Yt,failure as ht,isSuccess as bt,isFailure as Vt}from"@fjall/generator";import{deploy as Bt}from"./orchestration/index.js";import{destroy as Kt}from"./orchestration/index.js";import{partitionAccounts as Ht}from"./orchestration/index.js";import{buildRegionList as jt,buildAccountRegionPairs as Qt,cascadeHomeRegion as qt,cascadeOperationKey as Jt}from"./orchestration/index.js";import{projectScalarSummary as Zt,projectAccountRows as zt}from"./orchestration/index.js";import{reconcileProviderAccounts as eo,mergeReconciledProviderAccounts as ro}from"./orchestration/index.js";import{decideNextTransition as oo,reconcileTrailMigration as Eo,decommissionMemberTrailStorage as ao,ORG_TRAIL_BUCKET_OUTPUT_KEY as io,TRAIL_BUCKET_OUTPUT_KEY as so,TRAIL_KEY_ARN_OUTPUT_KEY as So}from"./orchestration/index.js";import{unlockBucket as co,unlockQueue as Ao}from"./orchestration/index.js";import{triageBucketPolicy as _o,isEnforceSslStatement as lo,toResourcePolicyStatements as po,restoreBucketPolicy as Oo,synthesiseEnforceSslDocument as Po,ensureEnforceSsl as Ro,restoreAndReconcileQuarantinedBucket as uo}from"./orchestration/index.js";import{verifyOrgTrailDelivery as Co}from"./aws/cloudtrail/orgTrailDelivery.js";import{assumeRootForTask as Do,isRootTaskPolicyArn as Lo,ROOT_TASK_POLICY_ARNS as fo,MAX_ROOT_SESSION_SECONDS as go}from"./aws/sts/assumeRoot.js";import{parseAccountsConfiguration as yo,flattenAccountsToEnvironments as xo,extractAllAccountNames as Uo,accountsConfigToOUTree as Mo,isStringArray as vo,isAccountsConfig as ko,isOuOnlyAccountBucket as Yo,OU_ONLY_ACCOUNT_BUCKETS as ho}from"./orchestration/index.js";import{runOpenNextBuild as Vo}from"./orchestration/index.js";import{runOrganisationSetup as Bo,ORG_SETUP_PHASES as Go}from"./orchestration/index.js";import{resolveBuildSecrets as Xo,resolveSecretRefValue as Ho,resolveBuildSecretSessionProvider as wo,sourcedRefsFromBuildArgs as jo,resolveBuildArgs as Qo,buildBuildSecretSessionPolicy as qo,buildDeploySessionPolicy as Jo,partitionForRegion as Wo,validateBuildGroup as Zo}from"./orchestration/index.js";import{FrameworkRegistry as $o}from"./orchestration/index.js";import{openNextBuilder as rE,dockerBuilder as tE}from"./orchestration/index.js";import{StepRegistry as EE,getDestroyStepId as aE}from"./steps/index.js";import{buildDeployPlan as sE,computeDeployPlan as SE,computeAssemblyDigest as nE,digestHead as cE,classifyImpact as AE,derivePropertyChanges as TE,isDataLoss as _E,isStatefulResourceType as lE,renderPlanSummary as pE,renderPlanLines as OE,toWirePlanChanges as PE,signApprovalToken as RE,verifyApprovalToken as uE,DEFAULT_APPROVAL_TTL_MS as mE,APPROVAL_TOKEN_PATTERN as dE,runApprovalGate as CE}from"./orchestration/index.js";export{X as ALL_PROGRESS_KINDS,He as APPLICATION_DEPLOY_ORDER,we as APPLICATION_DESTROY_ORDER,Ke as APPLICATION_STACKS,K as APPROVAL_DECISIONS,G as APPROVAL_KINDS,dE as APPROVAL_TOKEN_PATTERN,ur as ApplicationError,ot as ApplicationStackService,nt as AuthError,ct as AwsError,b as BUILDERS,st as BaseServiceError,v as CASCADE_ACCOUNT_PHASES,n as CASCADE_ACCOUNT_STATUSES,V as CASCADE_OUTCOME_RESULTS,S as CASCADE_PHASES,Pr as CDK_NO_STACKS_MATCH,vr as CdkArgumentBuilder,Zr as CdkContextBuilder,at as CdkError,Yr as CdkEventMonitor,kr as CdkProcessManager,Mr as CdkService,O as ChangeCountsSchema,wr as CloudFormationError,xr as CloudFormationEventMonitor,Hr as CloudFormationService,lt as ConfigError,mE as DEFAULT_APPROVAL_TTL_MS,br as DEFAULT_DEPLOY_TIMEOUT_MS,fe as DEFAULT_MAX_PAGES,i as DEPLOYMENT_EVENT_ERROR_MESSAGE_MAX,E as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,a as DEPLOYMENT_EVENT_STATUS_REASON_MAX,s as DEPLOYMENT_EVENT_TRAIL_DETAIL_MAX,o as DEPLOYMENT_EVENT_TYPES,I as DEPLOY_EVENT_ARN_MAX,P as DEPLOY_EVENT_CONTRACT,R as DEPLOY_EVENT_CONTRACT_MAJOR,u as DEPLOY_EVENT_CONTRACT_MINOR,L as DEPLOY_EVENT_ERROR_MESSAGE_MAX,d as DEPLOY_EVENT_ID_MAX,N as DEPLOY_EVENT_MESSAGE_MAX,C as DEPLOY_EVENT_NAME_MAX,g as DEPLOY_EVENT_OUTPUT_MAX,D as DEPLOY_EVENT_STATUS_REASON_MAX,f as DEPLOY_EVENT_TRAIL_DETAIL_MAX,y as DEPLOY_EVENT_URL_MAX,k as DEPLOY_RESULTS,Y as DETECTION_PHASES,A as DeployEventSchema,Nt as DeployModeSchema,_ as DeployResourceSchema,At as DeploymentError,t as DeploymentEventSchema,Qr as EcsError,jr as EcsService,qr as EcsServiceResolver,_t as FileSystemError,dr as FjallStateFileSchema,$o as FrameworkRegistry,ge as IDENTITY_STORE_PAGE_SIZE,Rr as INFRASTRUCTURE_FILENAME,Ve as INFRASTRUCTURE_STEP_NAMES,Fe as INFRA_STEP_NAME,U as LOG_LEVELS,go as MAX_ROOT_SESSION_SECONDS,Tt as NetworkError,je as OPENNEXT_DEPLOY_ORDER,Qe as OPENNEXT_DESTROY_ORDER,We as OPENNEXT_PARALLEL_GROUPS,_r as OPENNEXT_PATTERNS,Xe as ORGANISATION_TYPES,Go as ORG_SETUP_PHASES,io as ORG_TRAIL_BUCKET_OUTPUT_KEY,ho as OU_ONLY_ACCOUNT_BUCKETS,qe as PARALLEL_DEPLOY_GROUPS,Je as PARALLEL_DESTROY_GROUPS,Ze as PARALLEL_OPERATION_TYPES,h as PHASE_STATUSES,F as PLAN_ACTIONS,$r as PROGRESS_MESSAGES,l as PlanChangeSchema,p as PlanPropertyChangeSchema,Ge as ProgressReporter,B as REPLACEMENT_MODES,ve as ROOT_ACCESS_FEATURES,fo as ROOT_TASK_POLICY_ARNS,ke as RootAccessEnablementError,Ue as SECURITY_SERVICE_PRINCIPALS,ne as SERVICE_PRINCIPALS,M as STACK_CLEANUP_PHASES,Or as STACK_FAILED_STATE_PATTERN,pr as STACK_NOT_FOUND_PATTERN,x as STEP_COMPLETE_STATUSES,he as STEP_IDS,be as STEP_NAMES,m as SUPPORTED_CONTRACT_MARKERS,Dt as ServiceArtefactSchema,Lt as ServiceArtefactsSchema,q as SimpleAwsProvider,T as StepProgressSchema,EE as StepRegistry,so as TRAIL_BUCKET_OUTPUT_KEY,So as TRAIL_KEY_ARN_OUTPUT_KEY,Wr as TemplateHashError,Jr as TemplateHashService,St as ValidationError,Mo as accountsConfigToOUTree,me as activateCostAllocationTags,Ae as activateTrustedAccess,Ct as artefactOutputKey,Do as assumeRootForTask,Qt as buildAccountRegionPairs,ue as buildAccountToOUMap,qo as buildBuildSecretSessionPolicy,sE as buildDeployPlan,Jo as buildDeploySessionPolicy,jt as buildRegionList,rt as buildStepContextBuildConfig,$ as buildTargetSetUnreadyAdvisory,z as buildTargetUnreadyAdvisory,qt as cascadeHomeRegion,Jt as cascadeOperationKey,de as checkIdentityCentreStatus,te as checkRegionEnabled,W as checkTargetReadiness,AE as classifyImpact,Ne as composeSdkAbortSignal,nE as computeAssemblyDigest,SE as computeDeployPlan,tt as convertCloudFormationOutputsToRecord,Oe as createAccount,H as createDeployEmitter,Dr as createEmptyState,Ut as createSequencedCallbacks,oo as decideNextTransition,ao as decommissionMemberTrailStorage,Lr as deleteStateFile,Bt as deploy,TE as derivePropertyChanges,lr as deriveResourcesFromManifestStacks,ie as describeOrganisation,Z as describeTargetReadinessReason,Kt as destroy,cE as digestHead,tE as dockerBuilder,Ie as drainPages,zr as emitProgress,Me as enableCentralisedRootAccess,Te as enableIpamDelegatedAdmin,se as enablePolicyTypes,ce as enableRamSharing,Se as enableServiceAccess,Ro as ensureEnforceSsl,ae as ensureOrganisationExists,Pe as ensureOrganisationalUnitsExist,Uo as extractAllAccountNames,De as extractErrorName,ht as failure,vt as fileExists,Pt as filterDangerousEnvVars,pe as findAccount,xo as flattenAccountsToEnvironments,Fr as formatInfrastructureError,tr as getApplicationDeployOrder,or as getApplicationDestroyOrder,Er as getApplicationStackName,cr as getApplicationStepId,nr as getApplicationStepName,aE as getDestroyStepId,ar as getOrganisationStackName,er as getParallelDeployGroups,rr as getParallelDestroyGroups,Gr as getSourceContext,gr as getStateFilePath,Br as getStructuralHint,Kr as hasCdkDifferences,gt as hasDockerfile,Le as isAborted,ko as isAccountsConfig,ze as isApplicationOperation,ir as isApplicationStack,Vr as isCdkError,_E as isDataLoss,lo as isEnforceSslStatement,Vt as isFailure,ye as isOULeaf,Tr as isOpenNextPattern,$e as isOrganisationOperation,Yo as isOuOnlyAccountBucket,sr as isQuarantineDetail,Sr as isRetainedBucketsDetail,Lo as isRootTaskPolicyArn,lE as isStatefulResourceType,vo as isStringArray,bt as isSuccess,le as listAccounts,Rt as maskSensitiveOutput,ro as mergeReconciledProviderAccounts,rE as openNextBuilder,yo as parseAccountsConfiguration,et as parseBuildPhase,j as parseDeployContractVersion,w as parseDeployEvent,Xr as parseDiffOutput,ut as parseShellArgs,Ht as partitionAccounts,Wo as partitionForRegion,Re as placeAccountsInOUs,ee as probeAccountStackExists,zt as projectAccountRows,Zt as projectScalarSummary,Cr as readStateFile,eo as reconcileProviderAccounts,Eo as reconcileTrailMigration,oe as regionDisabledMessage,xe as registerSecurityDelegates,OE as renderPlanLines,pE as renderPlanSummary,Qo as resolveBuildArgs,wo as resolveBuildSecretSessionProvider,Xo as resolveBuildSecrets,Ho as resolveSecretRefValue,uo as restoreAndReconcileQuarantinedBucket,Oo as restoreBucketPolicy,CE as runApprovalGate,Vo as runOpenNextBuild,Bo as runOrganisationSetup,RE as signApprovalToken,mt as sleep,yt as sleepAbortable,Ce as snapshotIdentityCentre,jo as sourcedRefsFromBuildArgs,hr as startStackMonitoring,Ir as stubCallerIdentity,Yt as success,Po as synthesiseEnforceSslDocument,Ar as toPascalCase,po as toResourcePolicyStatements,pt as toServiceError,PE as toWirePlanChanges,_o as triageBucketPolicy,co as unlockBucket,Ao as unlockQueue,_e as updateBackupGlobalSettings,fr as updateTemplateHash,Zo as validateBuildGroup,uE as verifyApprovalToken,Co as verifyOrgTrailDelivery,mr as wrapApplicationError,Nr as writeStateFile};
1
+ import{DeploymentEventSchema as t,DEPLOYMENT_EVENT_TYPES as o,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as a,DEPLOYMENT_EVENT_STATUS_REASON_MAX as E,DEPLOYMENT_EVENT_ERROR_MESSAGE_MAX as i,DEPLOYMENT_EVENT_TRAIL_DETAIL_MAX as s,CASCADE_PHASES as S,CASCADE_ACCOUNT_STATUSES as n}from"./types/index.js";import{DeployEventSchema as A,StepProgressSchema as T,DeployResourceSchema as _,PlanChangeSchema as l,PlanPropertyChangeSchema as p,ChangeCountsSchema as O,DEPLOY_EVENT_CONTRACT as P,DEPLOY_EVENT_CONTRACT_MAJOR as R,DEPLOY_EVENT_CONTRACT_MINOR as u,SUPPORTED_CONTRACT_MARKERS as m,DEPLOY_EVENT_ID_MAX as d,DEPLOY_EVENT_NAME_MAX as C,DEPLOY_EVENT_MESSAGE_MAX as N,DEPLOY_EVENT_STATUS_REASON_MAX as D,DEPLOY_EVENT_ERROR_MESSAGE_MAX as L,DEPLOY_EVENT_TRAIL_DETAIL_MAX as f,DEPLOY_EVENT_OUTPUT_MAX as g,DEPLOY_EVENT_ARN_MAX as I,DEPLOY_EVENT_URL_MAX as x,STEP_COMPLETE_STATUSES as y,LOG_LEVELS as U,STACK_CLEANUP_PHASES as M,CASCADE_ACCOUNT_PHASES as v,DEPLOY_RESULTS as k,DETECTION_PHASES as Y,PHASE_STATUSES as h,BUILDERS as b,CASCADE_OUTCOME_RESULTS as V,PLAN_ACTIONS as F,REPLACEMENT_MODES as B,APPROVAL_KINDS as G,APPROVAL_DECISIONS as K,ALL_PROGRESS_KINDS as X,createDeployEmitter as H,parseDeployEvent as w,parseDeployContractVersion as j}from"./types/index.js";import{SimpleAwsProvider as q}from"./aws/index.js";import{checkTargetReadiness as W,describeTargetReadinessReason as Z,buildTargetUnreadyAdvisory as z,buildTargetSetUnreadyAdvisory as $,probeAccountStackExists as ee}from"./aws/index.js";import{checkRegionEnabled as te,regionDisabledMessage as oe}from"./aws/index.js";import{ensureOrganisationExists as Ee,describeOrganisation as ie,enablePolicyTypes as se,enableServiceAccess as Se,SERVICE_PRINCIPALS as ne,enableRamSharing as ce,activateTrustedAccess as Ae,enableIpamDelegatedAdmin as Te,updateBackupGlobalSettings as _e,listAccounts as le,findAccount as pe,createAccount as Oe,ensureOrganisationalUnitsExist as Pe,placeAccountsInOUs as Re,buildAccountToOUMap as ue,activateCostAllocationTags as me,checkIdentityCentreStatus as de,snapshotIdentityCentre as Ce,composeSdkAbortSignal as Ne,extractErrorName as De,isAborted as Le,DEFAULT_MAX_PAGES as fe,IDENTITY_STORE_PAGE_SIZE as ge,drainPages as Ie,isOULeaf as xe,registerSecurityDelegates as ye,SECURITY_SERVICE_PRINCIPALS as Ue,enableCentralisedRootAccess as Me,ROOT_ACCESS_FEATURES as ve,RootAccessEnablementError as ke}from"./aws/index.js";import{STEP_IDS as he,STEP_NAMES as be,INFRASTRUCTURE_STEP_NAMES as Ve,INFRA_STEP_NAME as Fe}from"./types/index.js";import{ProgressReporter as Ge,APPLICATION_STACKS as Ke,ORGANISATION_TYPES as Xe,APPLICATION_DEPLOY_ORDER as He,APPLICATION_DESTROY_ORDER as we,OPENNEXT_DEPLOY_ORDER as je,OPENNEXT_DESTROY_ORDER as Qe,PARALLEL_DEPLOY_GROUPS as qe,PARALLEL_DESTROY_GROUPS as Je,OPENNEXT_PARALLEL_GROUPS as We,PARALLEL_OPERATION_TYPES as Ze,isApplicationOperation as ze,isOrganisationOperation as $e,getParallelDeployGroups as er,getParallelDestroyGroups as rr,getApplicationDeployOrder as tr,getApplicationDestroyOrder as or,getApplicationStackName as ar,getOrganisationStackName as Er,isApplicationStack as ir,isQuarantineDetail as sr,isRetainedBucketsDetail as Sr,getApplicationStepName as nr,getApplicationStepId as cr,toPascalCase as Ar,isOpenNextPattern as Tr,OPENNEXT_PATTERNS as _r,deriveResourcesFromManifestStacks as lr,STACK_NOT_FOUND_PATTERN as pr,STACK_FAILED_STATE_PATTERN as Or,CDK_NO_STACKS_MATCH as Pr,INFRASTRUCTURE_FILENAME as Rr,ApplicationError as ur,wrapApplicationError as mr,FjallStateFileSchema as dr,readStateFile as Cr,writeStateFile as Nr,createEmptyState as Dr,deleteStateFile as Lr,updateTemplateHash as fr,getStateFilePath as gr,stubCallerIdentity as Ir}from"./types/index.js";import{CloudFormationEventMonitor as yr}from"./aws/index.js";import{CdkService as Mr,CdkArgumentBuilder as vr,CdkProcessManager as kr,CdkEventMonitor as Yr,startStackMonitoring as hr,DEFAULT_DEPLOY_TIMEOUT_MS as br,isCdkError as Vr,formatInfrastructureError as Fr,getStructuralHint as Br,getSourceContext as Gr,hasCdkDifferences as Kr,parseDiffOutput as Xr,CloudFormationService as Hr,CloudFormationError as wr,EcsService as jr,EcsError as Qr,EcsServiceResolver as qr,TemplateHashService as Jr,TemplateHashError as Wr,CdkContextBuilder as Zr,emitProgress as zr,PROGRESS_MESSAGES as $r,parseBuildPhase as et,buildStepContextBuildConfig as rt,convertCloudFormationOutputsToRecord as tt,ApplicationStackService as ot}from"./services/index.js";import{CdkError as Et}from"./types/errors/index.js";import{BaseServiceError as st,ValidationError as St,AuthError as nt,AwsError as ct,DeploymentError as At,NetworkError as Tt,FileSystemError as _t,ConfigError as lt,toServiceError as pt}from"./types/errors/index.js";import{filterDangerousEnvVars as Pt,maskSensitiveOutput as Rt,parseShellArgs as ut,sleep as mt}from"@fjall/util";import{artefactOutputKey as Ct,DeployModeSchema as Nt,ServiceArtefactSchema as Dt,ServiceArtefactsSchema as Lt}from"@fjall/util";import{hasDockerfile as gt}from"./util/dockerfileDetection.js";import{sleepAbortable as xt}from"./util/sleepAbortable.js";import{createSequencedCallbacks as Ut}from"./util/sequencedCallbacks.js";import{fileExists as vt}from"@fjall/util/fsHelpers";import{success as Yt,failure as ht,isSuccess as bt,isFailure as Vt}from"@fjall/generator";import{deploy as Bt}from"./orchestration/index.js";import{destroy as Kt}from"./orchestration/index.js";import{restart as Ht,probeSecretsDrift as wt,computeSecretsDrift as jt,deriveStaleParameters as Qt,extractConsumedSsmParameters as qt}from"./orchestration/index.js";import{partitionAccounts as Wt}from"./orchestration/index.js";import{buildRegionList as zt,buildAccountRegionPairs as $t,cascadeHomeRegion as eo,cascadeOperationKey as ro}from"./orchestration/index.js";import{projectScalarSummary as oo,projectAccountRows as ao}from"./orchestration/index.js";import{reconcileProviderAccounts as io,mergeReconciledProviderAccounts as so}from"./orchestration/index.js";import{decideNextTransition as no,reconcileTrailMigration as co,decommissionMemberTrailStorage as Ao,ORG_TRAIL_BUCKET_OUTPUT_KEY as To,TRAIL_BUCKET_OUTPUT_KEY as _o,TRAIL_KEY_ARN_OUTPUT_KEY as lo}from"./orchestration/index.js";import{unlockBucket as Oo,unlockQueue as Po}from"./orchestration/index.js";import{triageBucketPolicy as uo,isEnforceSslStatement as mo,toResourcePolicyStatements as Co,restoreBucketPolicy as No,synthesiseEnforceSslDocument as Do,ensureEnforceSsl as Lo,restoreAndReconcileQuarantinedBucket as fo}from"./orchestration/index.js";import{verifyOrgTrailDelivery as Io}from"./aws/cloudtrail/orgTrailDelivery.js";import{assumeRootForTask as yo,isRootTaskPolicyArn as Uo,ROOT_TASK_POLICY_ARNS as Mo,MAX_ROOT_SESSION_SECONDS as vo}from"./aws/sts/assumeRoot.js";import{parseAccountsConfiguration as Yo,flattenAccountsToEnvironments as ho,extractAllAccountNames as bo,accountsConfigToOUTree as Vo,isStringArray as Fo,isAccountsConfig as Bo,isOuOnlyAccountBucket as Go,OU_ONLY_ACCOUNT_BUCKETS as Ko}from"./orchestration/index.js";import{runOpenNextBuild as Ho}from"./orchestration/index.js";import{runOrganisationSetup as jo,ORG_SETUP_PHASES as Qo}from"./orchestration/index.js";import{resolveBuildSecrets as Jo,resolveSecretRefValue as Wo,resolveBuildSecretSessionProvider as Zo,sourcedRefsFromBuildArgs as zo,resolveBuildArgs as $o,buildBuildSecretSessionPolicy as ea,buildDeploySessionPolicy as ra,partitionForRegion as ta,validateBuildGroup as oa}from"./orchestration/index.js";import{FrameworkRegistry as Ea}from"./orchestration/index.js";import{openNextBuilder as sa,dockerBuilder as Sa}from"./orchestration/index.js";import{StepRegistry as ca,getDestroyStepId as Aa}from"./steps/index.js";import{buildDeployPlan as _a,computeDeployPlan as la,computeAssemblyDigest as pa,digestHead as Oa,classifyImpact as Pa,derivePropertyChanges as Ra,isDataLoss as ua,isStatefulResourceType as ma,renderPlanSummary as da,renderPlanLines as Ca,toWirePlanChanges as Na,signApprovalToken as Da,verifyApprovalToken as La,DEFAULT_APPROVAL_TTL_MS as fa,APPROVAL_TOKEN_PATTERN as ga,runApprovalGate as Ia}from"./orchestration/index.js";export{X as ALL_PROGRESS_KINDS,He as APPLICATION_DEPLOY_ORDER,we as APPLICATION_DESTROY_ORDER,Ke as APPLICATION_STACKS,K as APPROVAL_DECISIONS,G as APPROVAL_KINDS,ga as APPROVAL_TOKEN_PATTERN,ur as ApplicationError,ot as ApplicationStackService,nt as AuthError,ct as AwsError,b as BUILDERS,st as BaseServiceError,v as CASCADE_ACCOUNT_PHASES,n as CASCADE_ACCOUNT_STATUSES,V as CASCADE_OUTCOME_RESULTS,S as CASCADE_PHASES,Pr as CDK_NO_STACKS_MATCH,vr as CdkArgumentBuilder,Zr as CdkContextBuilder,Et as CdkError,Yr as CdkEventMonitor,kr as CdkProcessManager,Mr as CdkService,O as ChangeCountsSchema,wr as CloudFormationError,yr as CloudFormationEventMonitor,Hr as CloudFormationService,lt as ConfigError,fa as DEFAULT_APPROVAL_TTL_MS,br as DEFAULT_DEPLOY_TIMEOUT_MS,fe as DEFAULT_MAX_PAGES,i as DEPLOYMENT_EVENT_ERROR_MESSAGE_MAX,a as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,E as DEPLOYMENT_EVENT_STATUS_REASON_MAX,s as DEPLOYMENT_EVENT_TRAIL_DETAIL_MAX,o as DEPLOYMENT_EVENT_TYPES,I as DEPLOY_EVENT_ARN_MAX,P as DEPLOY_EVENT_CONTRACT,R as DEPLOY_EVENT_CONTRACT_MAJOR,u as DEPLOY_EVENT_CONTRACT_MINOR,L as DEPLOY_EVENT_ERROR_MESSAGE_MAX,d as DEPLOY_EVENT_ID_MAX,N as DEPLOY_EVENT_MESSAGE_MAX,C as DEPLOY_EVENT_NAME_MAX,g as DEPLOY_EVENT_OUTPUT_MAX,D as DEPLOY_EVENT_STATUS_REASON_MAX,f as DEPLOY_EVENT_TRAIL_DETAIL_MAX,x as DEPLOY_EVENT_URL_MAX,k as DEPLOY_RESULTS,Y as DETECTION_PHASES,A as DeployEventSchema,Nt as DeployModeSchema,_ as DeployResourceSchema,At as DeploymentError,t as DeploymentEventSchema,Qr as EcsError,jr as EcsService,qr as EcsServiceResolver,_t as FileSystemError,dr as FjallStateFileSchema,Ea as FrameworkRegistry,ge as IDENTITY_STORE_PAGE_SIZE,Rr as INFRASTRUCTURE_FILENAME,Ve as INFRASTRUCTURE_STEP_NAMES,Fe as INFRA_STEP_NAME,U as LOG_LEVELS,vo as MAX_ROOT_SESSION_SECONDS,Tt as NetworkError,je as OPENNEXT_DEPLOY_ORDER,Qe as OPENNEXT_DESTROY_ORDER,We as OPENNEXT_PARALLEL_GROUPS,_r as OPENNEXT_PATTERNS,Xe as ORGANISATION_TYPES,Qo as ORG_SETUP_PHASES,To as ORG_TRAIL_BUCKET_OUTPUT_KEY,Ko as OU_ONLY_ACCOUNT_BUCKETS,qe as PARALLEL_DEPLOY_GROUPS,Je as PARALLEL_DESTROY_GROUPS,Ze as PARALLEL_OPERATION_TYPES,h as PHASE_STATUSES,F as PLAN_ACTIONS,$r as PROGRESS_MESSAGES,l as PlanChangeSchema,p as PlanPropertyChangeSchema,Ge as ProgressReporter,B as REPLACEMENT_MODES,ve as ROOT_ACCESS_FEATURES,Mo as ROOT_TASK_POLICY_ARNS,ke as RootAccessEnablementError,Ue as SECURITY_SERVICE_PRINCIPALS,ne as SERVICE_PRINCIPALS,M as STACK_CLEANUP_PHASES,Or as STACK_FAILED_STATE_PATTERN,pr as STACK_NOT_FOUND_PATTERN,y as STEP_COMPLETE_STATUSES,he as STEP_IDS,be as STEP_NAMES,m as SUPPORTED_CONTRACT_MARKERS,Dt as ServiceArtefactSchema,Lt as ServiceArtefactsSchema,q as SimpleAwsProvider,T as StepProgressSchema,ca as StepRegistry,_o as TRAIL_BUCKET_OUTPUT_KEY,lo as TRAIL_KEY_ARN_OUTPUT_KEY,Wr as TemplateHashError,Jr as TemplateHashService,St as ValidationError,Vo as accountsConfigToOUTree,me as activateCostAllocationTags,Ae as activateTrustedAccess,Ct as artefactOutputKey,yo as assumeRootForTask,$t as buildAccountRegionPairs,ue as buildAccountToOUMap,ea as buildBuildSecretSessionPolicy,_a as buildDeployPlan,ra as buildDeploySessionPolicy,zt as buildRegionList,rt as buildStepContextBuildConfig,$ as buildTargetSetUnreadyAdvisory,z as buildTargetUnreadyAdvisory,eo as cascadeHomeRegion,ro as cascadeOperationKey,de as checkIdentityCentreStatus,te as checkRegionEnabled,W as checkTargetReadiness,Pa as classifyImpact,Ne as composeSdkAbortSignal,pa as computeAssemblyDigest,la as computeDeployPlan,jt as computeSecretsDrift,tt as convertCloudFormationOutputsToRecord,Oe as createAccount,H as createDeployEmitter,Dr as createEmptyState,Ut as createSequencedCallbacks,no as decideNextTransition,Ao as decommissionMemberTrailStorage,Lr as deleteStateFile,Bt as deploy,Ra as derivePropertyChanges,lr as deriveResourcesFromManifestStacks,Qt as deriveStaleParameters,ie as describeOrganisation,Z as describeTargetReadinessReason,Kt as destroy,Oa as digestHead,Sa as dockerBuilder,Ie as drainPages,zr as emitProgress,Me as enableCentralisedRootAccess,Te as enableIpamDelegatedAdmin,se as enablePolicyTypes,ce as enableRamSharing,Se as enableServiceAccess,Lo as ensureEnforceSsl,Ee as ensureOrganisationExists,Pe as ensureOrganisationalUnitsExist,bo as extractAllAccountNames,qt as extractConsumedSsmParameters,De as extractErrorName,ht as failure,vt as fileExists,Pt as filterDangerousEnvVars,pe as findAccount,ho as flattenAccountsToEnvironments,Fr as formatInfrastructureError,tr as getApplicationDeployOrder,or as getApplicationDestroyOrder,ar as getApplicationStackName,cr as getApplicationStepId,nr as getApplicationStepName,Aa as getDestroyStepId,Er as getOrganisationStackName,er as getParallelDeployGroups,rr as getParallelDestroyGroups,Gr as getSourceContext,gr as getStateFilePath,Br as getStructuralHint,Kr as hasCdkDifferences,gt as hasDockerfile,Le as isAborted,Bo as isAccountsConfig,ze as isApplicationOperation,ir as isApplicationStack,Vr as isCdkError,ua as isDataLoss,mo as isEnforceSslStatement,Vt as isFailure,xe as isOULeaf,Tr as isOpenNextPattern,$e as isOrganisationOperation,Go as isOuOnlyAccountBucket,sr as isQuarantineDetail,Sr as isRetainedBucketsDetail,Uo as isRootTaskPolicyArn,ma as isStatefulResourceType,Fo as isStringArray,bt as isSuccess,le as listAccounts,Rt as maskSensitiveOutput,so as mergeReconciledProviderAccounts,sa as openNextBuilder,Yo as parseAccountsConfiguration,et as parseBuildPhase,j as parseDeployContractVersion,w as parseDeployEvent,Xr as parseDiffOutput,ut as parseShellArgs,Wt as partitionAccounts,ta as partitionForRegion,Re as placeAccountsInOUs,ee as probeAccountStackExists,wt as probeSecretsDrift,ao as projectAccountRows,oo as projectScalarSummary,Cr as readStateFile,io as reconcileProviderAccounts,co as reconcileTrailMigration,oe as regionDisabledMessage,ye as registerSecurityDelegates,Ca as renderPlanLines,da as renderPlanSummary,$o as resolveBuildArgs,Zo as resolveBuildSecretSessionProvider,Jo as resolveBuildSecrets,Wo as resolveSecretRefValue,Ht as restart,fo as restoreAndReconcileQuarantinedBucket,No as restoreBucketPolicy,Ia as runApprovalGate,Ho as runOpenNextBuild,jo as runOrganisationSetup,Da as signApprovalToken,mt as sleep,xt as sleepAbortable,Ce as snapshotIdentityCentre,zo as sourcedRefsFromBuildArgs,hr as startStackMonitoring,Ir as stubCallerIdentity,Yt as success,Do as synthesiseEnforceSslDocument,Ar as toPascalCase,Co as toResourcePolicyStatements,pt as toServiceError,Na as toWirePlanChanges,uo as triageBucketPolicy,Oo as unlockBucket,Po as unlockQueue,_e as updateBackupGlobalSettings,fr as updateTemplateHash,oa as validateBuildGroup,La as verifyApprovalToken,Io as verifyOrgTrailDelivery,mr as wrapApplicationError,Nr as writeStateFile};
@@ -1 +1 @@
1
- import{join as $}from"path";import{CloudFormationClient as V}from"@aws-sdk/client-cloudformation";import{EC2Client as X}from"@aws-sdk/client-ec2";import{success as P,failure as g}from"@fjall/generator";import{logger as T}from"@fjall/util/logger";import{imageTagParameterName as Z,maskSensitiveOutput as y}from"@fjall/util";import{parseDockerServicesFromManifest as ee}from"@fjall/util/manifest";import{stubCallerIdentity as te}from"../../types/deployment/index.js";import{getApplicationDeployOrder as oe,getApplicationStackName as re,getApplicationStepName as W,getApplicationStepId as q,APPLICATION_STACKS as O}from"../../types/operations.js";import{CdkContextBuilder as ne}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as ae,bootstrapOrFail as ie}from"../contextHelpers.js";import{runDetectionPipeline as se}from"./detectionPipeline.js";import{STEP_IDS as N,STEP_NAMES as ce}from"../../types/stepDefinitions.js";import{StepRegistry as le}from"../../steps/stepRegistry.js";import{checkTargetReadiness as de}from"../../aws/targetReadiness.js";import{cascadeHomeRegion as pe}from"../organisation/cascadeHelpers.js";import{withStepLifecycle as ue}from"../stepLifecycle.js";import{DOCKER_BUILD_STEP_NAME as z,runDockerBuild as fe}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as ge,deployParallelPhase as me,deployStackSequential as ke,runDockerPreCompute as ye,createBuildCallbacks as he}from"./applicationDeployHelpers.js";import{deployCodeOnly as Se}from"./codeOnlyDeploy.js";import{collectFullDeployArtefacts as be}from"./artefactCollection.js";import{runApprovalGate as De}from"./approvalGate.js";const R="applicationDeploy";function we(i){const s=Object.entries(i);if(s.length===0)return;const n={};for(const[t,a]of s){const h=Z(t);n[h]=a}return n}async function qe(i,s,n){const{callbacks:t,options:a}=i,h=Date.now(),A=a?.mode??(a?.imageTag?"rollback":a?.deployOnly?"code-only":"full");if(a?.mode!==void 0&&a.mode!=="rollback"&&a.imageTag){const e=new Error(`--image-tag requires rollback mode (explicit mode '${a.mode}' was supplied alongside an image tag)`);return t.onError?.(e),g(e)}if(a?.mode==="rollback"&&!a.imageTag){const e=new Error("rollback mode requires --image-tag <tag> naming the image to roll to");return t.onError?.(e),g(e)}const w=ne.buildDeploymentContext({deployType:"application",target:n.appName,path:n.path,region:s.awsProvider.getRegion(),callerIdentity:te(s.awsProvider.getAccountId()),...ae({orgConfig:i.orgConfig,identity:i.identity,skipOidc:i.options?.skipOidc})},{verbose:a?.verbose,infraOnly:a?.infraOnly},i.orgConfig),p=s.frameworkRegistry.resolve({appPath:n.path});let C;const b=A==="rollback";if(p&&!b){C=p.builder.plan({appPath:n.path},p.detection);const e=he(t),r=await p.builder.build(n.path,C,e,{skipBuild:a?.skipBuild,infraOnly:a?.infraOnly});if(!r.success){const o=new Error(y(r.error.message));return t.onError?.(o),g(o)}}if(A==="code-only"||b){t.onLog?.(b?`Rollback mode \u2014 rolling to image tag ${a?.imageTag}`:"Deploy-only mode \u2014 skipping infrastructure pipeline","info");const e=$(n.path,"cdk.out"),r=ee(e),o=r.length>0,d=p?.detection.hasDockerfile===!0,l=o||d;T.debug(R,"Deploy-only branch entered",{mode:A,imageTag:a?.imageTag,appName:n.appName,appPath:n.path,cdkOutPath:e,dockerProviderAvailable:i.dockerProvider!==void 0,builderName:p?.builder.name,hasDockerfileFromManifest:o,hasDockerfileFromDisk:d,hasDockerfile:l,manifestDockerServiceCount:r.length,manifestDockerPaths:r.map(k=>k.docker.path)}),!o&&!d&&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");let u={};if(!b&&i.dockerProvider!==void 0&&l){T.debug(R,"Running Docker build before code-only deploy",{source:o?"manifest":"disk"});const k=await fe(i,s,n,t);if(!k.success)return g(k.error);u=k.data.contentHashTagsByService}else T.debug(R,"Skipping Docker build",{reason:b?"rollback":i.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});return Se(i,s,n,u)}t.onLog?.("Analysing infrastructure\u2026","info");const v=await se(n,s,w,t);if(!v.success){const e=new Error(y(v.error.message));return t.onError?.(e),g(e)}const c=v.data;try{await t.onDetectionComplete?.({...c,builderName:p?.builder.name??"unknown"})}catch(e){const r=e instanceof Error?e.message:String(e),o=new Error(y(r));return t.onError?.(o),g(o)}const Y={deploymentType:"application",operation:"deploy",deployOnly:!1,infraOnly:a?.infraOnly??!1,hasDockerfile:c.hasDockerfile,pattern:c.pattern,resources:c.resources,...p&&{builderName:p.builder.name}},M=le.getSteps(Y),L=M.findIndex(e=>e.id===N.TARGET_READINESS),_=await ue(t,{stepId:N.TARGET_READINESS,stepName:ce.TARGET_READINESS,...L>=0&&{stepIndex:L,totalSteps:M.length}},async()=>{if(a?.skipReadinessCheck)return t.onLog?.("Skipping target readiness check (--skip-readiness-check)","warn"),{kind:"skipped",data:void 0};const e=s.awsProvider.getAccountId(),r=i.orgConfig?.providerAccounts.find(u=>u.id===e)?.name,o=s.awsProvider.getCredentials(),d=pe(i.orgConfig),l=await de({cloudFormation:s.awsProvider.getClient(V),ec2:new X({region:d,...o!==void 0&&{credentials:o}})},{id:e,...r!==void 0&&{name:r}},s.awsProvider.getRegion(),i.orgConfig,i.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))}},i.abortSignal);if(!_.success)return _;const m=C?C.deployOrder:oe({pattern:c.pattern,resources:c.resources}),S=m.length,x=c.hasDockerfile&&i.dockerProvider!==void 0&&m.includes(O.COMPUTE)&&a?.infraOnly!==!0;if(!c.hasDifferences&&!a?.force&&!x){t.onLog?.("No infrastructure changes detected","info"),t.onLog?.(a?.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 e=c.hasDockerfile&&i.dockerProvider!==void 0&&m.includes(O.COMPUTE);for(let o=0;o<m.length;o++){const d=m[o];e&&d===O.COMPUTE&&(t.onStepStart?.(N.DOCKER_OPERATIONS,z),t.onStepComplete?.(N.DOCKER_OPERATIONS,z,"skipped"));const l=q(d,"deploy"),u=W(d,"deploy");t.onStepStart?.(l,u,o,S),t.onStepComplete?.(l,u,"skipped",o,S)}const r=await s.stackService.resolveWebsiteUrl(n.appName);return P({target:n.appName,deploymentType:"application",outputs:r?{websiteUrl:r}:void 0,noChanges:!0,artefacts:[],durationMs:Date.now()-h})}if(i.approvalGate!==void 0||a?.approvalToken!==void 0&&a.approvalToken!==""||a?.planOnly===!0){const e=$(n.path,"cdk.out"),r=await De({approvalGate:i.approvalGate,options:a??{},target:n.appName,cdkOutPath:e,detection:{stackChanges:c.stackChanges,currentHashes:c.currentHashes},cfnService:s.cfnService,callbacks:t});if(!r.success)return r;const o=r.data;if(o.kind==="rejected")return t.onLog?.(`Deploy rejected: ${y(o.reason)}`,"warn"),P({target:n.appName,deploymentType:"application",rejected:!0,artefacts:[],...o.planDetail!==void 0?{planDetail:o.planDetail}:{},durationMs:Date.now()-h});if(o.kind==="awaiting")return P({target:n.appName,deploymentType:"application",awaitingApproval:!0,approvalToken:o.approvalToken,assemblyDigest:o.assemblyDigest,planDetail:o.planDetail,artefacts:[],durationMs:Date.now()-h})}const B=await ie(s,w,t);if(!B.success)return B;const D={},E=new Map;let H;for(let e=0;e<m.length;e++){const r=m[e],o=re(n.appName,r),d=q(r,"deploy"),l=W(r,"deploy"),u=c.stackChanges.get(o)??!0,k=r===O.COMPUTE&&x;if(!u&&!a?.force&&!k){t.onStepStart?.(d,l,e,S),t.onLog?.(`Skipping ${r} \u2014 no changes detected`,"info"),t.onStepComplete?.(d,l,"skipped",e,S);continue}const I=ge(m,e,c.stackChanges,n.appName,a?.force);if(I.length>=2){const K=await me(I,n,s,w,t,e,S,c,D,E);if(!K.success)return g(K.error);e+=I.length-1;continue}const f=await ye(r,i,s,n,t,c.hasDockerfile);if(f!==null&&!f.success)return g(f.error);f!==null&&f.success&&(H=f.data);const Q=f!==null&&f.success?f.data.contentHashTagsByService:{},G=we(Q),j=await ke(r,s,w,t,e,S,D,G!==void 0?{parameters:G}:void 0);if(!j.success)return g(j.error);const F=c.currentHashes.get(o);F&&E.set(o,F)}if(E.size>0){const e=await s.hashService.updateStateAfterDeploy(n.path,E);if(!e.success){const r=y(e.error.message);T.debug(R,"Failed to update state file",{error:r}),t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy unchanged stacks: ${r}`,"warn")}}const U=await s.stackService.resolveWebsiteUrl(n.appName);U&&(D.websiteUrl=U);const J=await be(s,n.appName,H,t);return P({target:n.appName,deploymentType:"application",outputs:Object.keys(D).length>0?D:void 0,artefacts:J,durationMs:Date.now()-h})}export{qe as deployApplication};
1
+ import{join as q}from"path";import{CloudFormationClient as X}from"@aws-sdk/client-cloudformation";import{EC2Client as Z}from"@aws-sdk/client-ec2";import{success as S,failure as l}from"@fjall/generator";import{logger as I}from"@fjall/util/logger";import{ImageTagSchema as ee,imageTagParameterName as te,maskSensitiveOutput as k}from"@fjall/util";import{parseDockerServicesFromManifest as re}from"@fjall/util/manifest";import{stubCallerIdentity as ae}from"../../types/deployment/index.js";import{getApplicationDeployOrder as oe,getApplicationStackName as ne,getApplicationStepName as W,getApplicationStepId as z,APPLICATION_STACKS as O}from"../../types/operations.js";import{CdkContextBuilder as ie}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as se,bootstrapOrFail as ce}from"../contextHelpers.js";import{runDetectionPipeline as le}from"./detectionPipeline.js";import{STEP_IDS as P,STEP_NAMES as de}from"../../types/stepDefinitions.js";import{StepRegistry as pe}from"../../steps/stepRegistry.js";import{checkTargetReadiness as ue}from"../../aws/targetReadiness.js";import{cascadeHomeRegion as ge}from"../organisation/cascadeHelpers.js";import{withStepLifecycle as me}from"../stepLifecycle.js";import{DOCKER_BUILD_STEP_NAME as Y,runDockerBuild as fe}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as ke,deployParallelPhase as ye,deployStackSequential as he,runDockerPreCompute as be,createBuildCallbacks as Te}from"./applicationDeployHelpers.js";import{deployCodeOnly as we}from"./codeOnlyDeploy.js";import{collectFullDeployArtefacts as Se}from"./artefactCollection.js";import{runApprovalGate as ve}from"./approvalGate.js";const N="applicationDeploy";function De(t){const s=Object.entries(t);if(s.length===0)return;const n={};for(const[r,i]of s){const h=te(r);n[h]=i}return n}function Ee(t){if(t?.mode!==void 0&&t.mode!=="rollback"){if(t.imageTag)return l(new Error(`--image-tag requires rollback mode (explicit mode '${t.mode}' was supplied alongside an image tag)`));if(t.serviceImageTags)return l(new Error(`serviceImageTags requires rollback mode (explicit mode '${t.mode}' was supplied alongside serviceImageTags)`))}if(t?.mode==="rollback"){if(t.imageTag&&t.serviceImageTags)return l(new Error("rollback mode accepts exactly one of --image-tag or serviceImageTags \u2014 they are mutually exclusive"));if(!t.imageTag&&!t.serviceImageTags)return l(new Error("rollback mode requires --image-tag <tag> or serviceImageTags naming the image(s) to roll to"))}if(t?.serviceImageTags!==void 0){if(t.serviceName)return l(new Error("serviceImageTags already names its services \u2014 --service cannot be combined with it"));const s=Object.entries(t.serviceImageTags);if(s.length===0)return l(new Error("serviceImageTags must name at least one service"));for(const[n,r]of s){const i=ee.safeParse(r);if(!i.success)return l(new Error(`serviceImageTags.${n} is not a valid image tag: ${i.error.message}`))}}return S(void 0)}async function Je(t,s,n){const{callbacks:r,options:i}=t,h=Date.now(),v=i?.mode??(i?.imageTag||i?.serviceImageTags?"rollback":i?.deployOnly?"code-only":"full");if(v==="restart"){const e=new Error('mode "restart" is not a deploy \u2014 call the restart() entry point (fjall rollout) instead');return r.onError?.(e),l(e)}const M=Ee(i);if(!M.success){const e=k(M.error.message),a=new Error(e);return r.onError?.(a),l(a)}const D=ie.buildDeploymentContext({deployType:"application",target:n.appName,path:n.path,region:s.awsProvider.getRegion(),callerIdentity:ae(s.awsProvider.getAccountId()),...se({orgConfig:t.orgConfig,identity:t.identity,skipOidc:t.options?.skipOidc})},{verbose:i?.verbose,infraOnly:i?.infraOnly},t.orgConfig),u=s.frameworkRegistry.resolve({appPath:n.path});let E;const T=v==="rollback";if(u&&!T){E=u.builder.plan({appPath:n.path},u.detection);const e=Te(r),a=await u.builder.build(n.path,E,e,{skipBuild:i?.skipBuild,infraOnly:i?.infraOnly});if(!a.success){const o=new Error(k(a.error.message));return r.onError?.(o),l(o)}}if(v==="code-only"||T){r.onLog?.(T?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 e=q(n.path,"cdk.out"),a=re(e),o=a.length>0,p=u?.detection.hasDockerfile===!0,d=o||p;I.debug(N,"Deploy-only branch entered",{mode:v,imageTag:i?.imageTag,serviceImageTags:i?.serviceImageTags,appName:n.appName,appPath:n.path,cdkOutPath:e,dockerProviderAvailable:t.dockerProvider!==void 0,builderName:u?.builder.name,hasDockerfileFromManifest:o,hasDockerfileFromDisk:p,hasDockerfile:d,manifestDockerServiceCount:a.length,manifestDockerPaths:a.map(y=>y.docker.path)}),!o&&!p&&r.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");let g={};if(!T&&t.dockerProvider!==void 0&&d){I.debug(N,"Running Docker build before code-only deploy",{source:o?"manifest":"disk"});const y=await fe(t,s,n,r);if(!y.success)return l(y.error);g=y.data.contentHashTagsByService}else I.debug(N,"Skipping Docker build",{reason:T?"rollback":t.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});return we(t,s,n,g)}r.onLog?.("Analysing infrastructure\u2026","info");const R=await le(n,s,D,r);if(!R.success){const e=new Error(k(R.error.message));return r.onError?.(e),l(e)}const c=R.data;try{await r.onDetectionComplete?.({...c,builderName:u?.builder.name??"unknown"})}catch(e){const a=e instanceof Error?e.message:String(e),o=new Error(k(a));return r.onError?.(o),l(o)}const J={deploymentType:"application",operation:"deploy",deployOnly:!1,infraOnly:i?.infraOnly??!1,hasDockerfile:c.hasDockerfile,pattern:c.pattern,resources:c.resources,...u&&{builderName:u.builder.name}},x=pe.getSteps(J),L=x.findIndex(e=>e.id===P.TARGET_READINESS),_=await me(r,{stepId:P.TARGET_READINESS,stepName:de.TARGET_READINESS,...L>=0&&{stepIndex:L,totalSteps:x.length}},async()=>{if(i?.skipReadinessCheck)return r.onLog?.("Skipping target readiness check (--skip-readiness-check)","warn"),{kind:"skipped",data:void 0};const e=s.awsProvider.getAccountId(),a=t.orgConfig?.providerAccounts.find(g=>g.id===e)?.name,o=s.awsProvider.getCredentials(),p=ge(t.orgConfig),d=await ue({cloudFormation:s.awsProvider.getClient(X),ec2:new Z({region:p,...o!==void 0&&{credentials:o}})},{id:e,...a!==void 0&&{name:a}},s.awsProvider.getRegion(),t.orgConfig,t.abortSignal);return d.success?d.data.ready?{kind:"completed",data:void 0}:{kind:"error",error:new Error(k(d.data.advisory))}:{kind:"error",error:new Error(k(d.error.message))}},t.abortSignal);if(!_.success)return _;const f=E?E.deployOrder:oe({pattern:c.pattern,resources:c.resources}),b=f.length,j=c.hasDockerfile&&t.dockerProvider!==void 0&&f.includes(O.COMPUTE)&&i?.infraOnly!==!0;if(!c.hasDifferences&&!i?.force&&!j){r.onLog?.("No infrastructure changes detected","info"),r.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 e=c.hasDockerfile&&t.dockerProvider!==void 0&&f.includes(O.COMPUTE);for(let o=0;o<f.length;o++){const p=f[o];e&&p===O.COMPUTE&&(r.onStepStart?.(P.DOCKER_OPERATIONS,Y),r.onStepComplete?.(P.DOCKER_OPERATIONS,Y,"skipped"));const d=z(p,"deploy"),g=W(p,"deploy");r.onStepStart?.(d,g,o,b),r.onStepComplete?.(d,g,"skipped",o,b)}const a=await s.stackService.resolveWebsiteUrl(n.appName);return S({target:n.appName,deploymentType:"application",outputs:a?{websiteUrl:a}:void 0,noChanges:!0,artefacts:[],durationMs:Date.now()-h})}if(t.approvalGate!==void 0||i?.approvalToken!==void 0&&i.approvalToken!==""||i?.planOnly===!0){const e=q(n.path,"cdk.out"),a=await ve({approvalGate:t.approvalGate,options:i??{},target:n.appName,cdkOutPath:e,detection:{stackChanges:c.stackChanges,currentHashes:c.currentHashes},cfnService:s.cfnService,callbacks:r});if(!a.success)return a;const o=a.data;if(o.kind==="rejected")return r.onLog?.(`Deploy rejected: ${k(o.reason)}`,"warn"),S({target:n.appName,deploymentType:"application",rejected:!0,artefacts:[],...o.planDetail!==void 0?{planDetail:o.planDetail}:{},durationMs:Date.now()-h});if(o.kind==="awaiting")return S({target:n.appName,deploymentType:"application",awaitingApproval:!0,approvalToken:o.approvalToken,assemblyDigest:o.assemblyDigest,planDetail:o.planDetail,artefacts:[],durationMs:Date.now()-h})}const B=await ce(s,D,r);if(!B.success)return B;const w={},C=new Map;let H;for(let e=0;e<f.length;e++){const a=f[e],o=ne(n.appName,a),p=z(a,"deploy"),d=W(a,"deploy"),g=c.stackChanges.get(o)??!0,y=a===O.COMPUTE&&j;if(!g&&!i?.force&&!y){r.onStepStart?.(p,d,e,b),r.onLog?.(`Skipping ${a} \u2014 no changes detected`,"info"),r.onStepComplete?.(p,d,"skipped",e,b);continue}const A=ke(f,e,c.stackChanges,n.appName,i?.force);if(A.length>=2){const K=await ye(A,n,s,D,r,e,b,c,w,C);if(!K.success)return l(K.error);e+=A.length-1;continue}const m=await be(a,t,s,n,r,c.hasDockerfile);if(m!==null&&!m.success)return l(m.error);m!==null&&m.success&&(H=m.data);const V=m!==null&&m.success?m.data.contentHashTagsByService:{},G=De(V),$=await he(a,s,D,r,e,b,w,G!==void 0?{parameters:G}:void 0);if(!$.success)return l($.error);const F=c.currentHashes.get(o);F&&C.set(o,F)}if(C.size>0){const e=await s.hashService.updateStateAfterDeploy(n.path,C);if(!e.success){const a=k(e.error.message);I.debug(N,"Failed to update state file",{error:a}),r.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy unchanged stacks: ${a}`,"warn")}}const U=await s.stackService.resolveWebsiteUrl(n.appName);U&&(w.websiteUrl=U);const Q=await Se(s,n.appName,H,r);return S({target:n.appName,deploymentType:"application",outputs:Object.keys(w).length>0?w:void 0,artefacts:Q,durationMs:Date.now()-h})}export{Je as deployApplication};
@@ -54,6 +54,12 @@ export declare function rebaseRollbackTagForService(suppliedTag: string, service
54
54
  * per service via `rebaseRollbackTagForService` so each service rolls its OWN
55
55
  * `<service>-…` image family (a multi-image stack shares one ECR repo, so an
56
56
  * un-rebased tag would silently roll the wrong service's image).
57
+ *
58
+ * `options.serviceImageTags` (F12) is a THIRD, mutually-exclusive tag source:
59
+ * an exact per-service map (bare service name → tag) recorded by a prior
60
+ * Release. Unlike `imageTag`, map-supplied tags are never re-based — they
61
+ * already name the exact service — and never fall back to tag pinning; see
62
+ * `serviceImageTagsRollback.ts`.
57
63
  */
58
64
  export declare function deployCodeOnly(params: DeployParams, services: DeployServices, operation: ApplicationOperation, contentHashTagsByService: Record<string, string>): Promise<Result<DeployResult>>;
59
65
  /**
@@ -1 +1 @@
1
- import{join as N}from"path";import{success as W,failure as C}from"@fjall/generator";import{logger as b}from"@fjall/util/logger";import{artefactOutputKey as L,maskSensitiveOutput as y}from"@fjall/util";import{parseDockerServicesFromManifest as q}from"@fjall/util/manifest";import{APPLICATION_STACKS as _,getApplicationStepId as z,getApplicationStepName as G}from"../../types/operations.js";import{extractEcsServiceName as U}from"../../aws/utils/arnParser.js";import{stripTagOrDigest as j}from"../../services/infrastructure/EcrImageInspectorService.js";import{withStepLifecycle as K}from"../stepLifecycle.js";const I="codeOnlyDeploy";function H(t,c){if(t)return c[t.name]}function X(t,c){const r=c.toLowerCase();return t.find(e=>e.name.toLowerCase()===r)??t.find(e=>r.includes(e.name.toLowerCase()))}function Y(t,c,r){const e=c.toLowerCase(),l=t.toLowerCase(),g=r.map(m=>m.toLowerCase()).filter(m=>m!==""&&l.startsWith(`${m}-`)).sort((m,E)=>E.length-m.length)[0];if(g===void 0||g===e)return t;const v=t.slice(g.length);return`${e}${v}`}const J=0,Q=1;async function le(t,c,r,e){const{callbacks:l,options:g}=t,v=Date.now(),m=z(_.COMPUTE,"deploy"),E=G(_.COMPUTE,"deploy");return K(l,{stepId:m,stepName:E,stepIndex:J,totalSteps:Q},async()=>{const d=g?.imageTag;d&&l.onLog?.(`Rolling to supplied image tag ${d}`,"info");const S=q(N(r.path,"cdk.out"));b.debug(I,"Code-only rollout starting",{explicitRollbackTag:d,workingDirectory:t.workingDirectory,contentHashTagsByService:e,manifestServiceCount:S.length,manifestServices:S.map(i=>({name:i.name,target:i.docker.target}))}),l.onLog?.("Discovering ECS cluster and services\u2026","info");const h=await c.ecsResolver.getDeployableClusterAndServices(r.appName);if(!h.success)return b.debug(I,"ECS discovery failed",{appName:r.appName,error:y(h.error.message)}),{kind:"error",error:h.error};const{clusterArn:a,serviceArns:$}=h.data;if(b.debug(I,"ECS discovery complete",{appName:r.appName,clusterArn:a,serviceCount:$.length,serviceArns:$}),!a||$.length===0){const i=`No deployable ECS services found for ${r.appName}`;return l.onLog?.(i,"warn"),{kind:"skipped",data:{target:r.appName,deploymentType:"application",durationMs:Date.now()-v,noChanges:!0,artefacts:[]}}}const T=g?.serviceName?.toLowerCase(),w=T===""?void 0:T,f=w?$.filter(i=>{const u=(U(i)??"").toLowerCase();return u===w||u.endsWith(w)}):$;if(w&&f.length===0){const i=$.map(u=>U(u)??"unknown").join(", ");return{kind:"error",error:new Error(`Service '${w}' not found. Available: ${i||"none"}`)}}const D={},O=[],p=[],A=$.map(i=>U(i)).filter(i=>i!==void 0&&i!=="");for(let i=0;i<f.length;i++){const u=f[i];if(!u)continue;const n=U(u)??`service-${i+1}`,R=`[${i+1}/${f.length}] ${n}`,M=X(S,n),x=H(M,e);let o,P;if(d!==void 0)o=Y(d,n,A),o!==d?(P=d,l.onLog?.(`Re-based image tag ${d} \u2192 ${o} for ${n}`,"info")):A.length>1&&!d.toLowerCase().startsWith(`${n.toLowerCase()}-`)&&l.onLog?.(`Image tag ${d} has no recognised service prefix; applying verbatim to ${n} \u2014 confirm it points at this service's image`,"warn");else if(x!==void 0)o=x;else{const F=`No image available to deploy for ${n}: 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.`;l.onLog?.(F,"warn"),p.push(`${n}: ${F}`);continue}b.debug(I,"Starting service rollout",{serviceName:n,serviceArn:u,imageTag:o,explicitRollbackTag:d,contentHashTag:x,manifestServiceMatched:M?.name,dockerTarget:M?.docker.target,index:i,total:f.length}),l.onECSUpdate?.(`${R}: rolling out image tag ${o}`,n);const s=await V(c,a,u,n,o,l,P);if(b.debug(I,"Service rollout completed",{serviceName:n,success:s.success,...s.success?{taskDefinitionArn:s.data.taskDefinitionArn,imageDigest:s.data.imageDigest,durationMs:s.data.durationMs}:{error:y(s.error.message)}}),!s.success){p.push(`${n}: ${y(s.error.message)}`);continue}D[L(n,"TaskDefinition")]=s.data.taskDefinitionArn,D[L(n,"PreviousTaskDefinition")]=s.data.previousTaskDefinitionArn,D[L(n,"ImageTag")]=o,D[L(n,"ImageUri")]=s.data.imageUri,D[L(n,"EcrRepositoryArn")]=s.data.ecrRepositoryArn,s.data.imageDigest!==void 0&&(D[L(n,"ImageDigest")]=s.data.imageDigest),O.push({serviceName:n,imageTag:o,imageUri:s.data.imageUri,taskDefinitionArn:s.data.taskDefinitionArn,previousTaskDefinitionArn:s.data.previousTaskDefinitionArn,...s.data.imageDigest!==void 0?{imageDigest:s.data.imageDigest}:{},...s.data.ecrRepositoryArn!==""?{ecrRepositoryArn:s.data.ecrRepositoryArn}:{}})}const k=Date.now()-v;if(p.length>0){const i=f.length-p.length,u=f.length,n=p.length===u?`All ${u} ECS service rollouts failed: ${p.join("; ")}`:`Partial rollout: ${i}/${u} services succeeded. Failures: ${p.join("; ")}`;return{kind:"error",error:new Error(n)}}return{kind:"completed",data:{target:r.appName,deploymentType:"application",outputs:Object.keys(D).length>0?D:void 0,artefacts:O,durationMs:k}}},t.abortSignal)}async function V(t,c,r,e,l,g,v){const m=Date.now(),E=await t.ecsService.describeServices(c,[r]);if(!E.success)return C(new Error(`Failed to describe service ${e}: ${E.error.message}`));const d=E.data[0];if(!d?.taskDefinition)return C(new Error(`Service ${e} has no task definition`));const S=d.taskDefinition,h=await t.ecsService.getLatestTaskDefinition(S);if(!h.success)return C(new Error(`Failed to fetch task definition for ${e}: ${h.error.message}`));const a=h.data;if(!a||!a.containerDefinitions||a.containerDefinitions.length===0)return C(new Error(`Task definition for ${e} has no container definitions`));const $=a.containerDefinitions.find(o=>o.image)?.image;if(!$)return C(new Error(`Task definition for ${e} has no container image \u2014 cannot derive repository URI`));const T=j($),w=await t.ecrImageInspector.getImageDigest(T,l);let f;if(w.success)f=w.data;else{if(v!==void 0)return C(new Error(`Re-based image tag '${l}' (from supplied '${v}') was not found in ECR for ${e}: ${y(w.error.message)}. The supplied tag's build has no image for ${e} \u2014 supply that service's own tag, or pass --service to roll a single service.`));g.onLog?.(`Could not resolve image digest for ${e} (${y(w.error.message)}); falling back to tag pinning`,"warn")}const D=o=>f!==void 0?`${o}@${f}`:`${o}:${l}`,O=a.containerDefinitions.map(o=>o.image?{...o,image:D(j(o.image))}:o),p=a.family;if(!p)return C(new Error(`Task definition for ${e} has no family`));const A=await t.ecsService.registerTaskDefinition({family:p,taskRoleArn:a.taskRoleArn,executionRoleArn:a.executionRoleArn,networkMode:a.networkMode,containerDefinitions:O,volumes:a.volumes,placementConstraints:a.placementConstraints,requiresCompatibilities:a.requiresCompatibilities,cpu:a.cpu,memory:a.memory,runtimePlatform:a.runtimePlatform,proxyConfiguration:a.proxyConfiguration,inferenceAccelerators:a.inferenceAccelerators,pidMode:a.pidMode,ipcMode:a.ipcMode,ephemeralStorage:a.ephemeralStorage});if(!A.success)return C(new Error(`Failed to register task definition for ${e}: ${A.error.message}`));const k=A.data;g.onTaskDefRegistered?.({serviceName:e,taskDefinitionArn:k,previousTaskDefinitionArn:S,revision:B(k),family:p,...f!==void 0?{imageDigest:f}:{}});const i=await t.ecsService.updateService(c,r,{taskDefinition:k,forceNewDeployment:!0});if(!i.success)return C(new Error(`Failed to update service ${e}: ${i.error.message}`));const u=await t.ecsService.pollDeployment({clusterArn:c,serviceArn:r,waitForCompletion:!0,progressCallback:o=>{const P=o.latestEvent?` \u2014 ${y(o.latestEvent)}`:"";g.onECSProgress?.(`${e}: ${y(o.message)}${P}`,o.percentComplete)}}),n=Date.now()-m;if(!u.success){const o=y(u.error.message);return g.onECSComplete?.({serviceName:e,serviceArn:r,success:!1,taskDefinitionArn:k,durationMs:n,reason:o}),C(new Error(`ECS rollout failed for ${e}: ${o}`))}const R=await t.ecsService.getDeploymentStatus(c,r);g.onECSComplete?.({serviceName:e,serviceArn:r,success:!0,taskDefinitionArn:k,durationMs:n,finalRunningCount:R.success?R.data.runningCount:void 0,finalDesiredCount:R.success?R.data.desiredCount:void 0}),b.debug(I,`Rolled out ${e} successfully`,{serviceArn:r,newTaskDefArn:k,previousTaskDefArn:S,durationMs:n});const M=`${T}:${l}`,x=Z(T);return W({taskDefinitionArn:k,previousTaskDefinitionArn:S,imageUri:M,ecrRepositoryArn:x,imageDigest:f,durationMs:n})}function Z(t){const c=t.match(/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/);if(!c)return"";const[,r,e,l]=c;return`arn:aws:ecr:${e}:${r}:repository/${l}`}function B(t){const c=t.lastIndexOf(":");if(c<0)return 0;const r=t.slice(c+1);return/^\d+$/.test(r)?parseInt(r,10):0}export{le as deployCodeOnly,Z as deriveEcrRepositoryArn,H as lookupContentHashTag,Y as rebaseRollbackTagForService};
1
+ import{join as W}from"path";import{success as H,failure as O}from"@fjall/generator";import{logger as A}from"@fjall/util/logger";import{artefactOutputKey as I,imageTagParameterName as X,maskSensitiveOutput as C}from"@fjall/util";import{parseDockerServicesFromManifest as Y}from"@fjall/util/manifest";import{APPLICATION_STACKS as x,getApplicationStackName as q,getApplicationStepId as J,getApplicationStepName as Q}from"../../types/operations.js";import{extractEcsServiceName as L}from"../../aws/utils/arnParser.js";import{isAborted as K}from"../../aws/organisations/types.js";import{stripTagOrDigest as V}from"../../services/infrastructure/EcrImageInspectorService.js";import{withStepLifecycle as Z}from"../stepLifecycle.js";import{registerAndRollTaskDefinition as B}from"./taskDefinitionRoll.js";import{matchesServiceName as ee,matchServiceImageTagsToLiveServices as te,preflightResolveServiceImageTagDigests as re,resolveServiceTaskDefinition as ie}from"./serviceImageTagsRollback.js";const N="codeOnlyDeploy";function ne(t,s){if(t)return s[t.name]}function ae(t,s){const e=s.toLowerCase();return t.find(i=>i.name.toLowerCase()===e)??t.find(i=>e.includes(i.name.toLowerCase()))}function oe(t,s,e){const i=s.toLowerCase(),n=t.toLowerCase(),d=e.map(c=>c.toLowerCase()).filter(c=>c!==""&&n.startsWith(`${c}-`)).sort((c,m)=>m.length-c.length)[0];if(d===void 0||d===i)return t;const f=t.slice(d.length);return`${i}${f}`}const se=0,ce=1;function j(t,s,e,i,n){i[I(t,"TaskDefinition")]=e.taskDefinitionArn,i[I(t,"PreviousTaskDefinition")]=e.previousTaskDefinitionArn,i[I(t,"ImageTag")]=s,i[I(t,"ImageUri")]=e.imageUri,i[I(t,"EcrRepositoryArn")]=e.ecrRepositoryArn,e.imageDigest!==void 0&&(i[I(t,"ImageDigest")]=e.imageDigest),n.push({serviceName:t,imageTag:s,imageUri:e.imageUri,taskDefinitionArn:e.taskDefinitionArn,previousTaskDefinitionArn:e.previousTaskDefinitionArn,...e.imageDigest!==void 0?{imageDigest:e.imageDigest}:{},...e.ecrRepositoryArn!==""?{ecrRepositoryArn:e.ecrRepositoryArn}:{}})}async function F(t,s,e,i,n){if(e.length===0)return;const d=q(s,x.COMPUTE),f={};for(const{serviceName:m,imageTag:l}of e)f[X(m)]=l;const c=await t.cfnService.updateStackParameters(d,f,n);if(!c.success){i.onLog?.(`Rollback succeeded but the CloudFormation ImageTag parameter reconcile failed \u2014 the next full deploy may revert this rollback. Reason: ${C(c.error.message)}`,"warn");return}c.data.skippedKeys.length>0&&(i.onLog?.(`Rollback succeeded but ${c.data.skippedKeys.length} ImageTag parameter(s) were not found on stack ${d} (${c.data.skippedKeys.join(", ")}) \u2014 the next full deploy may revert those services`,"warn"),A.debug(N,"ImageTag reconcile skipped stale parameters",{stackName:d,skippedKeys:c.data.skippedKeys}))}async function Se(t,s,e,i){const{callbacks:n,options:d}=t,f=Date.now(),c=J(x.COMPUTE,"deploy"),m=Q(x.COMPUTE,"deploy");return Z(n,{stepId:c,stepName:m,stepIndex:se,totalSteps:ce},async()=>{const l=d?.imageTag,$=d?.serviceImageTags;l?n.onLog?.(`Rolling to supplied image tag ${l}`,"info"):$&&n.onLog?.(`Rolling ${Object.keys($).length} service(s) to their release-recorded image tags`,"info");const k=Y(W(e.path,"cdk.out"));A.debug(N,"Code-only rollout starting",{explicitRollbackTag:l,serviceImageTags:$,workingDirectory:t.workingDirectory,contentHashTagsByService:i,manifestServiceCount:k.length,manifestServices:k.map(o=>({name:o.name,target:o.docker.target}))}),n.onLog?.("Discovering ECS cluster and services\u2026","info");const u=await s.ecsResolver.getDeployableClusterAndServices(e.appName);if(!u.success)return A.debug(N,"ECS discovery failed",{appName:e.appName,error:C(u.error.message)}),{kind:"error",error:u.error};const{clusterArn:D,serviceArns:r}=u.data;if(A.debug(N,"ECS discovery complete",{appName:e.appName,clusterArn:D,serviceCount:r.length,serviceArns:r}),!D||r.length===0){const o=`No deployable ECS services found for ${e.appName}`;return n.onLog?.(o,"warn"),{kind:"skipped",data:{target:e.appName,deploymentType:"application",durationMs:Date.now()-f,noChanges:!0,artefacts:[]}}}if($!==void 0)return ge(s,e,D,r,$,n,t.abortSignal,f);const a=d?.serviceName?.toLowerCase(),y=a===""?void 0:a,p=y?r.filter(o=>ee(L(o)??"",y)):r;if(y&&p.length===0){const o=r.map(w=>L(w)??"unknown").join(", ");return{kind:"error",error:new Error(`Service '${y}' not found. Available: ${o||"none"}`)}}const v={},E=[],g=[],b=r.map(o=>L(o)).filter(o=>o!==void 0&&o!=="");for(let o=0;o<p.length;o++){if(K(t.abortSignal))return{kind:"error",error:new Error(`Rollout cancelled after ${E.length}/${p.length} services`)};const w=p[o];if(!w)continue;const h=L(w)??`service-${o+1}`,G=`[${o+1}/${p.length}] ${h}`,R=ae(k,h),M=ne(R,i);let T,P;if(l!==void 0)T=oe(l,h,b),T!==l?(P=l,n.onLog?.(`Re-based image tag ${l} \u2192 ${T} for ${h}`,"info")):b.length>1&&!l.toLowerCase().startsWith(`${h.toLowerCase()}-`)&&n.onLog?.(`Image tag ${l} has no recognised service prefix; applying verbatim to ${h} \u2014 confirm it points at this service's image`,"warn");else if(M!==void 0)T=M;else{const U=`No image available to deploy for ${h}: 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?.(U,"warn"),g.push(`${h}: ${U}`);continue}A.debug(N,"Starting service rollout",{serviceName:h,serviceArn:w,imageTag:T,explicitRollbackTag:l,contentHashTag:M,manifestServiceMatched:R?.name,dockerTarget:R?.docker.target,index:o,total:p.length}),n.onECSUpdate?.(`${G}: rolling out image tag ${T}`,h);const S=await _(s,D,w,h,T,n,P,void 0,t.abortSignal);if(A.debug(N,"Service rollout completed",{serviceName:h,success:S.success,...S.success?{taskDefinitionArn:S.data.taskDefinitionArn,imageDigest:S.data.imageDigest,durationMs:S.data.durationMs}:{error:C(S.error.message)}}),!S.success){g.push(`${h}: ${C(S.error.message)}`);continue}j(h,T,S.data,v,E)}const z=Date.now()-f;if(g.length>0){const o=p.length-g.length,w=p.length,h=g.length===w?`All ${w} ECS service rollouts failed: ${g.join("; ")}`:`Partial rollout: ${o}/${w} services succeeded. Failures: ${g.join("; ")}`;return{kind:"error",error:new Error(h)}}return l!==void 0&&await F(s,e.appName,E.map(o=>({serviceName:o.serviceName,imageTag:o.imageTag})),n,t.abortSignal),{kind:"completed",data:{target:e.appName,deploymentType:"application",outputs:Object.keys(v).length>0?v:void 0,artefacts:E,durationMs:z}}},t.abortSignal)}async function ge(t,s,e,i,n,d,f,c){const m=te(i,n);for(const r of m.unmatchedLiveServiceNames)d.onLog?.(`Service ${r} is not part of this release \u2014 leaving it untouched`,"warn");for(const r of m.staleMapKeys)d.onLog?.(`Service ${r} from the release no longer exists in the stack`,"warn");if(m.matched.length===0)return{kind:"error",error:new Error("None of the services in serviceImageTags matched a live ECS service in this stack \u2014 nothing to roll")};const l=await re(t,e,m.matched,f);if(!l.success)return{kind:"error",error:l.error};const $={},k=[],u=[];for(let r=0;r<m.matched.length;r++){if(K(f))return{kind:"error",error:new Error(`Rollout cancelled after ${k.length}/${m.matched.length} services`)};const a=m.matched[r];if(!a)continue;const y=`[${r+1}/${m.matched.length}] ${a.serviceName}`,p=l.data.get(a.serviceArn);if(p===void 0){u.push(`${a.serviceName}: pre-flight digest missing after a successful pre-flight`);continue}A.debug(N,"Starting service rollout (serviceImageTags)",{serviceName:a.serviceName,serviceArn:a.serviceArn,imageTag:a.tag,mapKey:a.mapKey,index:r,total:m.matched.length}),d.onECSUpdate?.(`${y}: rolling out image tag ${a.tag}`,a.serviceName);const v=await _(t,e,a.serviceArn,a.serviceName,a.tag,d,void 0,p,f);if(A.debug(N,"Service rollout completed (serviceImageTags)",{serviceName:a.serviceName,success:v.success,...v.success?{taskDefinitionArn:v.data.taskDefinitionArn,imageDigest:v.data.imageDigest,durationMs:v.data.durationMs}:{error:C(v.error.message)}}),!v.success){u.push(`${a.serviceName}: ${C(v.error.message)}`);continue}j(a.serviceName,a.tag,v.data,$,k)}const D=Date.now()-c;if(u.length>0){const r=m.matched.length-u.length,a=m.matched.length,y=u.length===a?`All ${a} ECS service rollouts failed: ${u.join("; ")}`:`Partial rollout: ${r}/${a} services succeeded. Failures: ${u.join("; ")}`;return{kind:"error",error:new Error(y)}}return await F(t,s.appName,k.map(r=>({serviceName:r.serviceName,imageTag:r.imageTag})),d,f),{kind:"completed",data:{target:s.appName,deploymentType:"application",outputs:Object.keys($).length>0?$:void 0,artefacts:k,durationMs:D}}}async function _(t,s,e,i,n,d,f,c,m){const l=Date.now();let $,k,u,D,r;if(c!==void 0)$=c.taskDefinition.previousTaskDefArn,k=c.taskDefinition.taskDefinition,u=c.taskDefinition.containerDefinitions,D=c.taskDefinition.repoUri,r=c.imageDigest;else{const g=await ie(t,s,e,i);if(!g.success)return O(g.error);$=g.data.previousTaskDefArn,k=g.data.taskDefinition,u=g.data.containerDefinitions,D=g.data.repoUri;const b=await t.ecrImageInspector.getImageDigest(D,n,m);if(b.success)r=b.data;else{if(f!==void 0)return O(new Error(`Re-based image tag '${n}' (from supplied '${f}') was not found in ECR for ${i}: ${C(b.error.message)}. The supplied tag's build has no image for ${i} \u2014 supply that service's own tag, or pass --service to roll a single service.`));d.onLog?.(`Could not resolve image digest for ${i} (${C(b.error.message)}); falling back to tag pinning`,"warn")}}const a=g=>r!==void 0?`${g}@${r}`:`${g}:${n}`,y=u.map(g=>g.image?{...g,image:a(V(g.image))}:g),p=await B(t,{clusterArn:s,serviceArn:e,serviceName:i,taskDefinition:k,containerDefinitions:y,previousTaskDefArn:$,...r!==void 0?{imageDigest:r}:{},startedAtMs:l},d);if(!p.success)return O(p.error);const v=`${D}:${n}`,E=de(D);return H({taskDefinitionArn:p.data.newTaskDefArn,previousTaskDefinitionArn:$,imageUri:v,ecrRepositoryArn:E,imageDigest:r,durationMs:p.data.durationMs})}function de(t){const s=t.match(/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/);if(!s)return"";const[,e,i,n]=s;return`arn:aws:ecr:${i}:${e}:repository/${n}`}export{Se as deployCodeOnly,de as deriveEcrRepositoryArn,ne as lookupContentHashTag,oe as rebaseRollbackTagForService};
@@ -0,0 +1,56 @@
1
+ import type { TaskDefinition } from "@aws-sdk/client-ecs";
2
+ import { type Result } from "@fjall/generator";
3
+ import type { DeployServices } from "../serviceFactory.js";
4
+ /**
5
+ * Case-insensitive service-name match shared by the single `--service`
6
+ * filter and the `serviceImageTags` map matcher: a live ECS service name
7
+ * matches a candidate when it equals the candidate outright, or ends with it
8
+ * (the ECS service name can carry a cluster/app prefix the candidate omits).
9
+ */
10
+ export declare function matchesServiceName(liveServiceName: string, candidate: string): boolean;
11
+ export interface MatchedServiceImageTag {
12
+ serviceArn: string;
13
+ serviceName: string;
14
+ tag: string;
15
+ mapKey: string;
16
+ }
17
+ export interface ServiceImageTagsMatchResult {
18
+ matched: MatchedServiceImageTag[];
19
+ /** Live ECS services with no corresponding entry in the supplied map. */
20
+ unmatchedLiveServiceNames: string[];
21
+ /** Map entries that named no live ECS service in this stack. */
22
+ staleMapKeys: string[];
23
+ }
24
+ /**
25
+ * Match every live ECS service against the `serviceImageTags` map (F12).
26
+ * A live service without a map entry is left untouched (not rolled); a map
27
+ * entry naming no live service is stale (the Release recorded a service the
28
+ * stack no longer runs). Both are reported for the caller to warn on —
29
+ * neither is fatal individually; the caller fails only when NOTHING matched.
30
+ */
31
+ export declare function matchServiceImageTagsToLiveServices(serviceArns: readonly string[], serviceImageTags: Record<string, string>): ServiceImageTagsMatchResult;
32
+ export interface ResolvedServiceTaskDefinition {
33
+ previousTaskDefArn: string;
34
+ taskDefinition: TaskDefinition;
35
+ containerDefinitions: NonNullable<TaskDefinition["containerDefinitions"]>;
36
+ repoUri: string;
37
+ }
38
+ /**
39
+ * Fetch a service's current task definition and derive its ECR repository
40
+ * URI from the first container image. Shared by the pre-flight digest
41
+ * resolution (map mode) and the rollout itself (legacy `--image-tag` mode) so
42
+ * the describe → task-definition → repo-URI derivation lives in one place.
43
+ */
44
+ export declare function resolveServiceTaskDefinition(services: Pick<DeployServices, "ecsService">, clusterArn: string, serviceArn: string, serviceName: string): Promise<Result<ResolvedServiceTaskDefinition, Error>>;
45
+ export interface PreResolvedRollout {
46
+ taskDefinition: ResolvedServiceTaskDefinition;
47
+ imageDigest: string;
48
+ }
49
+ /**
50
+ * Resolve an ECR digest for every matched (service, tag) pair BEFORE any
51
+ * service is rolled (F12). A map-supplied tag is meant to be exact — unlike
52
+ * the legacy `--image-tag` rebase path, it must NEVER fall back to tag
53
+ * pinning, so a missing digest (an ECR error, or a non-ECR repository
54
+ * returning no digest) fails the whole pre-flight and rolls nothing.
55
+ */
56
+ export declare function preflightResolveServiceImageTagDigests(services: DeployServices, clusterArn: string, matched: readonly MatchedServiceImageTag[], abortSignal?: AbortSignal): Promise<Result<Map<string, PreResolvedRollout>, Error>>;
@@ -0,0 +1 @@
1
+ import{success as h,failure as u}from"@fjall/generator";import{maskSensitiveOutput as p}from"@fjall/util";import{extractEcsServiceName as S}from"../../aws/utils/arnParser.js";import{isAborted as y}from"../../aws/organisations/types.js";import{stripTagOrDigest as E}from"../../services/infrastructure/EcrImageInspectorService.js";function $(c,g){const o=c.toLowerCase(),r=g.toLowerCase();return o===r||o.endsWith(r)}function L(c,g){const o=Object.entries(g),r=[],f=[],n=new Set,s=c.map(e=>{const t=S(e)??e;return{serviceArn:e,serviceName:t,lowerName:t.toLowerCase()}}),i=new Map,a=new Set;for(const e of s){const t=o.find(([d])=>d.toLowerCase()===e.lowerName);t!==void 0&&(i.set(e.serviceArn,t),a.add(t[0]))}const v=new Map,m=new Map;for(const e of s){if(i.has(e.serviceArn))continue;const t=o.filter(([d])=>!a.has(d)&&$(e.serviceName,d)).sort(([d],[l])=>l.length-d.length)[0];t!==void 0&&(v.set(e.serviceArn,t),m.set(t[0],(m.get(t[0])??0)+1))}for(const e of s){const t=v.get(e.serviceArn),d=i.get(e.serviceArn)??(t!==void 0&&m.get(t[0])===1?t:void 0);if(d===void 0){f.push(e.serviceName);continue}const[l,D]=d;n.add(l),r.push({serviceArn:e.serviceArn,serviceName:e.serviceName,tag:D,mapKey:l})}const w=o.map(([e])=>e).filter(e=>!n.has(e));return{matched:r,unmatchedLiveServiceNames:f,staleMapKeys:w}}async function k(c,g,o,r){const f=await c.ecsService.describeServices(g,[o]);if(!f.success)return u(new Error(`Failed to describe service ${r}: ${p(f.error.message)}`));const n=f.data[0];if(!n?.taskDefinition)return u(new Error(`Service ${r} has no task definition`));const s=n.taskDefinition,i=await c.ecsService.getLatestTaskDefinition(s);if(!i.success)return u(new Error(`Failed to fetch task definition for ${r}: ${p(i.error.message)}`));const a=i.data;if(!a||!a.containerDefinitions||a.containerDefinitions.length===0)return u(new Error(`Task definition for ${r} has no container definitions`));const v=a.containerDefinitions,m=v.find(e=>e.image)?.image;if(!m)return u(new Error(`Task definition for ${r} has no container image \u2014 cannot derive repository URI`));const w=E(m);return h({previousTaskDefArn:s,taskDefinition:a,containerDefinitions:v,repoUri:w})}async function I(c,g,o,r){const f=new Map;for(const n of o){if(y(r))return u(new Error("Pre-flight cancelled. No service was rolled."));const s=await k(c,g,n.serviceArn,n.serviceName);if(!s.success)return u(new Error(`Pre-flight failed resolving the current task definition for ${n.serviceName} (tag ${n.tag}): ${p(s.error.message)}. No service was rolled.`));const i=await c.ecrImageInspector.getImageDigest(s.data.repoUri,n.tag,r);if(!i.success||i.data===void 0){const a=i.success?"ECR returned no digest for this tag (non-ECR repository, or the image is missing)":p(i.error.message);return u(new Error(`Pre-flight digest resolution failed for ${n.serviceName} (tag ${n.tag}): ${a}. No service was rolled.`))}f.set(n.serviceArn,{taskDefinition:s.data,imageDigest:i.data})}return h(f)}export{L as matchServiceImageTagsToLiveServices,$ as matchesServiceName,I as preflightResolveServiceImageTagDigests,k as resolveServiceTaskDefinition};
@@ -0,0 +1,33 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import type { ContainerDefinition, TaskDefinition } from "@aws-sdk/client-ecs";
3
+ import type { DeployCallbacks } from "../../types/callbacks.js";
4
+ import type { DeployServices } from "../serviceFactory.js";
5
+ export interface RollTaskDefinitionInput {
6
+ clusterArn: string;
7
+ serviceArn: string;
8
+ serviceName: string;
9
+ /** The resolved live task definition the new revision is copied from. */
10
+ taskDefinition: TaskDefinition;
11
+ /** Container definitions for the new revision (image-rewritten or unchanged). */
12
+ containerDefinitions: ContainerDefinition[];
13
+ previousTaskDefArn: string;
14
+ /** Forwarded to the onTaskDefRegistered event when the roll is digest-pinned. */
15
+ imageDigest?: string;
16
+ /** Epoch ms the per-service operation started — keeps event durations inclusive of resolution time. */
17
+ startedAtMs: number;
18
+ }
19
+ export interface RollTaskDefinitionOk {
20
+ newTaskDefArn: string;
21
+ durationMs: number;
22
+ }
23
+ /**
24
+ * Register a new task-definition revision copied from a resolved live one,
25
+ * point the service at it, and poll the ECS deployment to completion.
26
+ *
27
+ * Shared tail of the code-only/rollback rollout AND the restart operation —
28
+ * the RegisterTaskDefinition field-copy list below is a coupled value: a
29
+ * task-definition field omitted here is silently dropped from the new
30
+ * revision on BOTH paths, so it must have exactly one home.
31
+ */
32
+ export declare function registerAndRollTaskDefinition(services: Pick<DeployServices, "ecsService">, input: RollTaskDefinitionInput, callbacks: DeployCallbacks): Promise<Result<RollTaskDefinitionOk, Error>>;
33
+ export declare function extractRevision(taskDefArn: string): number;
@@ -0,0 +1 @@
1
+ import{success as w,failure as l}from"@fjall/generator";import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as p}from"@fjall/util";const $="taskDefinitionRoll";async function T(n,a,r){const{clusterArn:f,serviceArn:s,serviceName:t,taskDefinition:e,containerDefinitions:A,previousTaskDefArn:g,imageDigest:C,startedAtMs:R}=a,m=e.family;if(!m)return l(new Error(`Task definition for ${t} has no family`));const d=await n.ecsService.registerTaskDefinition({family:m,taskRoleArn:e.taskRoleArn,executionRoleArn:e.executionRoleArn,networkMode:e.networkMode,containerDefinitions:A,volumes:e.volumes,placementConstraints:e.placementConstraints,requiresCompatibilities:e.requiresCompatibilities,cpu:e.cpu,memory:e.memory,runtimePlatform:e.runtimePlatform,proxyConfiguration:e.proxyConfiguration,inferenceAccelerators:e.inferenceAccelerators,pidMode:e.pidMode,ipcMode:e.ipcMode,ephemeralStorage:e.ephemeralStorage});if(!d.success)return l(new Error(`Failed to register task definition for ${t}: ${d.error.message}`));const i=d.data;r.onTaskDefRegistered?.({serviceName:t,taskDefinitionArn:i,previousTaskDefinitionArn:g,revision:y(i),family:m,...C!==void 0?{imageDigest:C}:{}});const D=await n.ecsService.updateService(f,s,{taskDefinition:i,forceNewDeployment:!0});if(!D.success)return l(new Error(`Failed to update service ${t}: ${D.error.message}`));const k=await n.ecsService.pollDeployment({clusterArn:f,serviceArn:s,waitForCompletion:!0,progressCallback:o=>{const v=o.latestEvent?` \u2014 ${p(o.latestEvent)}`:"";r.onECSProgress?.(`${t}: ${p(o.message)}${v}`,o.percentComplete)}}),c=Date.now()-R;if(!k.success){const o=p(k.error.message);return r.onECSComplete?.({serviceName:t,serviceArn:s,success:!1,taskDefinitionArn:i,durationMs:c,reason:o}),l(new Error(`ECS rollout failed for ${t}: ${o}`))}const u=await n.ecsService.getDeploymentStatus(f,s);return r.onECSComplete?.({serviceName:t,serviceArn:s,success:!0,taskDefinitionArn:i,durationMs:c,finalRunningCount:u.success?u.data.runningCount:void 0,finalDesiredCount:u.success?u.data.desiredCount:void 0}),S.debug($,`Rolled ${t} successfully`,{serviceArn:s,newTaskDefArn:i,previousTaskDefArn:g,durationMs:c}),w({newTaskDefArn:i,durationMs:c})}function y(n){const a=n.lastIndexOf(":");if(a<0)return 0;const r=n.slice(a+1);return/^\d+$/.test(r)?parseInt(r,10):0}export{y as extractRevision,T as registerAndRollTaskDefinition};
@@ -1,5 +1,9 @@
1
1
  export { deploy } from "./deploy.js";
2
2
  export { destroy } from "./destroy.js";
3
+ export { restart, probeSecretsDrift } from "./restart/restartApplication.js";
4
+ export type { RestartParams, RestartResult, SecretsDriftProbeParams } from "./restart/restartApplication.js";
5
+ export { computeSecretsDrift, deriveStaleParameters, extractConsumedSsmParameters, ssmParameterNameFromValueFrom } from "./restart/secretsDrift.js";
6
+ export type { ApplicationSecretsDrift, ServiceSecretsDrift, StaleParameter, SecretsDriftServices } from "./restart/secretsDrift.js";
3
7
  export { deployOrganisation } from "./organisation/organisationDeploy.js";
4
8
  export { destroyOrganisation } from "./organisation/organisationDestroy.js";
5
9
  export { cleanupFailedStack, emptyS3Bucket, preEmptyStackBuckets, formatQuarantineSuspectedMessage, formatRetainedBucketsMessage, isQuarantineDetail, isRetainedBucketsDetail, PRE_EMPTY_TAG_KEYS, isCleanableState, SAFE_CLEANUP_STATES } from "./stackCleanup.js";
@@ -1 +1 @@
1
- import{deploy as r}from"./deploy.js";import{destroy as i}from"./destroy.js";import{deployOrganisation as s}from"./organisation/organisationDeploy.js";import{destroyOrganisation as a}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as u,emptyS3Bucket as p,preEmptyStackBuckets as m,formatQuarantineSuspectedMessage as f,formatRetainedBucketsMessage as d,isQuarantineDetail as A,isRetainedBucketsDetail as S,PRE_EMPTY_TAG_KEYS as T,isCleanableState as x,SAFE_CLEANUP_STATES as P}from"./stackCleanup.js";import{partitionAccounts as y,buildRegionList as g,buildAccountRegionPairs as E,cascadeHomeRegion as R,cascadeOperationKey as B}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as k,projectAccountRows as U}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as C,mergeReconciledProviderAccounts as L}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as D,reconcileTrailMigration as N,ORG_TRAIL_BUCKET_OUTPUT_KEY as b,TRAIL_BUCKET_OUTPUT_KEY as Y,TRAIL_KEY_ARN_OUTPUT_KEY as M}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as G}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as Q}from"./unlock/unlockBucket.js";import{unlockQueue as H}from"./unlock/unlockQueue.js";import{triageBucketPolicy as j,isEnforceSslStatement as w}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as q}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as J,synthesiseEnforceSslDocument as X,ensureEnforceSsl as Z}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as ee}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as re,flattenAccountsToEnvironments as te,extractAllAccountNames as ie,accountsConfigToOUTree as ne,isStringArray as se,isAccountsConfig as ce,isOuOnlyAccountBucket as ae,OU_ONLY_ACCOUNT_BUCKETS as le}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as pe,resolveSecretRefValue as me,resolveBuildSecretSessionProvider as fe,sourcedRefsFromBuildArgs as de}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as Se,partitionForRegion as Te}from"./application/buildSecretSession.js";import{buildDeploySessionPolicy as Pe}from"./application/deploySessionPolicy.js";import{resolveBuildArgs as ye}from"./application/buildArgResolver.js";import{validateBuildGroup as Ee}from"./application/buildGroupValidator.js";import{runOpenNextBuild as Be}from"./application/openNextBuild.js";import{runOrganisationSetup as ke,ORG_SETUP_PHASES as Ue}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as Le,computeDeployPlan as Ke,computeAssemblyDigest as De,digestHead as Ne,classifyImpact as be,derivePropertyChanges as Ye,isDataLoss as Me,isStatefulResourceType as Fe,renderPlanSummary as Ge,renderPlanLines as Ie,toWirePlanChanges as Qe,signApprovalToken as he,verifyApprovalToken as He,DEFAULT_APPROVAL_TTL_MS as Ve,APPROVAL_TOKEN_PATTERN as je}from"./application/plan/index.js";import{runApprovalGate as We}from"./application/approvalGate.js";export{je as APPROVAL_TOKEN_PATTERN,Ve as DEFAULT_APPROVAL_TTL_MS,Ue as ORG_SETUP_PHASES,b as ORG_TRAIL_BUCKET_OUTPUT_KEY,le as OU_ONLY_ACCOUNT_BUCKETS,T as PRE_EMPTY_TAG_KEYS,P as SAFE_CLEANUP_STATES,Y as TRAIL_BUCKET_OUTPUT_KEY,M as TRAIL_KEY_ARN_OUTPUT_KEY,ne as accountsConfigToOUTree,E as buildAccountRegionPairs,Se as buildBuildSecretSessionPolicy,Le as buildDeployPlan,Pe as buildDeploySessionPolicy,g as buildRegionList,R as cascadeHomeRegion,B as cascadeOperationKey,be as classifyImpact,u as cleanupFailedStack,De as computeAssemblyDigest,Ke as computeDeployPlan,D as decideNextTransition,G as decommissionMemberTrailStorage,r as deploy,s as deployOrganisation,Ye as derivePropertyChanges,i as destroy,a as destroyOrganisation,Ne as digestHead,p as emptyS3Bucket,Z as ensureEnforceSsl,ie as extractAllAccountNames,te as flattenAccountsToEnvironments,f as formatQuarantineSuspectedMessage,d as formatRetainedBucketsMessage,ce as isAccountsConfig,x as isCleanableState,Me as isDataLoss,w as isEnforceSslStatement,ae as isOuOnlyAccountBucket,A as isQuarantineDetail,S as isRetainedBucketsDetail,Fe as isStatefulResourceType,se as isStringArray,L as mergeReconciledProviderAccounts,re as parseAccountsConfiguration,y as partitionAccounts,Te as partitionForRegion,m as preEmptyStackBuckets,U as projectAccountRows,k as projectScalarSummary,C as reconcileProviderAccounts,N as reconcileTrailMigration,Ie as renderPlanLines,Ge as renderPlanSummary,ye as resolveBuildArgs,fe as resolveBuildSecretSessionProvider,pe as resolveBuildSecrets,me as resolveSecretRefValue,ee as restoreAndReconcileQuarantinedBucket,J as restoreBucketPolicy,We as runApprovalGate,Be as runOpenNextBuild,ke as runOrganisationSetup,he as signApprovalToken,de as sourcedRefsFromBuildArgs,X as synthesiseEnforceSslDocument,q as toResourcePolicyStatements,Qe as toWirePlanChanges,j as triageBucketPolicy,Q as unlockBucket,H as unlockQueue,Ee as validateBuildGroup,He as verifyApprovalToken};
1
+ import{deploy as o}from"./deploy.js";import{destroy as i}from"./destroy.js";import{restart as a,probeSecretsDrift as n}from"./restart/restartApplication.js";import{computeSecretsDrift as l,deriveStaleParameters as m,extractConsumedSsmParameters as u,ssmParameterNameFromValueFrom as p}from"./restart/secretsDrift.js";import{deployOrganisation as d}from"./organisation/organisationDeploy.js";import{destroyOrganisation as A}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as T,emptyS3Bucket as P,preEmptyStackBuckets as _,formatQuarantineSuspectedMessage as y,formatRetainedBucketsMessage as g,isQuarantineDetail as E,isRetainedBucketsDetail as R,PRE_EMPTY_TAG_KEYS as B,isCleanableState as O,SAFE_CLEANUP_STATES as k}from"./stackCleanup.js";import{partitionAccounts as U,buildRegionList as C,buildAccountRegionPairs as L,cascadeHomeRegion as D,cascadeOperationKey as K}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as b,projectAccountRows as F}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as M,mergeReconciledProviderAccounts as G}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as Q,reconcileTrailMigration as V,ORG_TRAIL_BUCKET_OUTPUT_KEY as h,TRAIL_BUCKET_OUTPUT_KEY as H,TRAIL_KEY_ARN_OUTPUT_KEY as j}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as W}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as z}from"./unlock/unlockBucket.js";import{unlockQueue as X}from"./unlock/unlockQueue.js";import{triageBucketPolicy as $,isEnforceSslStatement as ee}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as oe}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as ie,synthesiseEnforceSslDocument as se,ensureEnforceSsl as ae}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as ce}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as me,flattenAccountsToEnvironments as ue,extractAllAccountNames as pe,accountsConfigToOUTree as fe,isStringArray as de,isAccountsConfig as Se,isOuOnlyAccountBucket as Ae,OU_ONLY_ACCOUNT_BUCKETS as xe}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as Pe,resolveSecretRefValue as _e,resolveBuildSecretSessionProvider as ye,sourcedRefsFromBuildArgs as ge}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as Re,partitionForRegion as Be}from"./application/buildSecretSession.js";import{buildDeploySessionPolicy as ke}from"./application/deploySessionPolicy.js";import{resolveBuildArgs as Ue}from"./application/buildArgResolver.js";import{validateBuildGroup as Le}from"./application/buildGroupValidator.js";import{runOpenNextBuild as Ke}from"./application/openNextBuild.js";import{runOrganisationSetup as be,ORG_SETUP_PHASES as Fe}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as Ge,computeDeployPlan as Ie,computeAssemblyDigest as Qe,digestHead as Ve,classifyImpact as he,derivePropertyChanges as He,isDataLoss as je,isStatefulResourceType as we,renderPlanSummary as We,renderPlanLines as qe,toWirePlanChanges as ze,signApprovalToken as Je,verifyApprovalToken as Xe,DEFAULT_APPROVAL_TTL_MS as Ze,APPROVAL_TOKEN_PATTERN as $e}from"./application/plan/index.js";import{runApprovalGate as rr}from"./application/approvalGate.js";export{$e as APPROVAL_TOKEN_PATTERN,Ze as DEFAULT_APPROVAL_TTL_MS,Fe as ORG_SETUP_PHASES,h as ORG_TRAIL_BUCKET_OUTPUT_KEY,xe as OU_ONLY_ACCOUNT_BUCKETS,B as PRE_EMPTY_TAG_KEYS,k as SAFE_CLEANUP_STATES,H as TRAIL_BUCKET_OUTPUT_KEY,j as TRAIL_KEY_ARN_OUTPUT_KEY,fe as accountsConfigToOUTree,L as buildAccountRegionPairs,Re as buildBuildSecretSessionPolicy,Ge as buildDeployPlan,ke as buildDeploySessionPolicy,C as buildRegionList,D as cascadeHomeRegion,K as cascadeOperationKey,he as classifyImpact,T as cleanupFailedStack,Qe as computeAssemblyDigest,Ie as computeDeployPlan,l as computeSecretsDrift,Q as decideNextTransition,W as decommissionMemberTrailStorage,o as deploy,d as deployOrganisation,He as derivePropertyChanges,m as deriveStaleParameters,i as destroy,A as destroyOrganisation,Ve as digestHead,P as emptyS3Bucket,ae as ensureEnforceSsl,pe as extractAllAccountNames,u as extractConsumedSsmParameters,ue as flattenAccountsToEnvironments,y as formatQuarantineSuspectedMessage,g as formatRetainedBucketsMessage,Se as isAccountsConfig,O as isCleanableState,je as isDataLoss,ee as isEnforceSslStatement,Ae as isOuOnlyAccountBucket,E as isQuarantineDetail,R as isRetainedBucketsDetail,we as isStatefulResourceType,de as isStringArray,G as mergeReconciledProviderAccounts,me as parseAccountsConfiguration,U as partitionAccounts,Be as partitionForRegion,_ as preEmptyStackBuckets,n as probeSecretsDrift,F as projectAccountRows,b as projectScalarSummary,M as reconcileProviderAccounts,V as reconcileTrailMigration,qe as renderPlanLines,We as renderPlanSummary,Ue as resolveBuildArgs,ye as resolveBuildSecretSessionProvider,Pe as resolveBuildSecrets,_e as resolveSecretRefValue,a as restart,ce as restoreAndReconcileQuarantinedBucket,ie as restoreBucketPolicy,rr as runApprovalGate,Ke as runOpenNextBuild,be as runOrganisationSetup,Je as signApprovalToken,ge as sourcedRefsFromBuildArgs,p as ssmParameterNameFromValueFrom,se as synthesiseEnforceSslDocument,oe as toResourcePolicyStatements,ze as toWirePlanChanges,$ as triageBucketPolicy,z as unlockBucket,X as unlockQueue,Le as validateBuildGroup,Xe as verifyApprovalToken};
@@ -0,0 +1,69 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import { type ServiceArtefact } from "@fjall/util";
3
+ import type { AwsCredentials } from "../../types/credentials.js";
4
+ import type { DeployCallbacks } from "../../types/callbacks.js";
5
+ import { EcsService } from "../../services/infrastructure/EcsService.js";
6
+ import { EcsServiceResolver } from "../../services/infrastructure/EcsServiceResolver.js";
7
+ import { EcrImageInspectorService } from "../../services/infrastructure/EcrImageInspectorService.js";
8
+ import { SsmParameterMetadataService } from "../../services/infrastructure/SsmParameterMetadataService.js";
9
+ import { type ApplicationSecretsDrift } from "./secretsDrift.js";
10
+ export interface RestartParams {
11
+ /** Application name. */
12
+ target: string;
13
+ awsCredentials: AwsCredentials;
14
+ callbacks: DeployCallbacks;
15
+ /**
16
+ * Pre-resolved ECS targets (the webapp worker knows them from the DB).
17
+ * When omitted, services are discovered from CloudFormation exports.
18
+ */
19
+ knownServices?: {
20
+ clusterArn: string;
21
+ serviceArns: string[];
22
+ };
23
+ /** Restrict the restart to a single ECS service by name (case-insensitive). */
24
+ serviceName?: string;
25
+ abortSignal?: AbortSignal;
26
+ }
27
+ export interface RestartResult {
28
+ target: string;
29
+ /**
30
+ * Per-service image identity of what is now running — the same images as
31
+ * before the restart, re-registered under a fresh task-definition revision.
32
+ * A service whose image tag could not be recovered (untagged digest) is
33
+ * omitted, with a warning.
34
+ */
35
+ artefacts: ServiceArtefact[];
36
+ /**
37
+ * Version of each consumed SSM parameter at restart time — what the
38
+ * relaunched tasks read. Recorded onto the restart Release.
39
+ */
40
+ appliedParameterVersions: Record<string, number>;
41
+ /** Drift state observed BEFORE the restart (what this restart healed). */
42
+ preRestartDrift?: ApplicationSecretsDrift;
43
+ durationMs: number;
44
+ cancelled?: boolean;
45
+ }
46
+ export interface RestartServices {
47
+ ecsService: EcsService;
48
+ ecsResolver: EcsServiceResolver;
49
+ ecrImageInspector: EcrImageInspectorService;
50
+ ssmService: SsmParameterMetadataService;
51
+ }
52
+ export declare function createRestartServices(awsCredentials: AwsCredentials): RestartServices;
53
+ export type SecretsDriftProbeParams = Omit<RestartParams, "callbacks">;
54
+ /**
55
+ * Read-only secrets-drift probe over an application's live ECS services —
56
+ * the same resolution ladder as `restart()` without rolling anything.
57
+ * Used by the CLI's pre-confirm drift display and the webapp's drift pulse.
58
+ */
59
+ export declare function probeSecretsDrift(params: SecretsDriftProbeParams, injectedServices?: RestartServices): Promise<Result<ApplicationSecretsDrift, Error>>;
60
+ /**
61
+ * Restart an application's ECS services without building or deploying:
62
+ * re-register each service's live task definition unchanged and roll the
63
+ * service onto the new revision. Relaunched tasks re-resolve their SSM
64
+ * `valueFrom` secrets at start-up — this is the lightweight rollout primitive
65
+ * that makes a secret rotation take effect (R2). The revision bump is what
66
+ * lets the secrets-drift probe self-heal: drift compares parameter
67
+ * modification time against task-definition registration time.
68
+ */
69
+ export declare function restart(params: RestartParams, injectedServices?: RestartServices): Promise<Result<RestartResult>>;
@@ -0,0 +1 @@
1
+ import{success as $,failure as g}from"@fjall/generator";import{CONTENT_HASH_TAG_PATTERN as b,maskSensitiveOutput as R}from"@fjall/util";import{logger as x}from"@fjall/util/logger";import{SimpleAwsProvider as O}from"../../aws/SimpleAwsProvider.js";import{CloudFormationService as L}from"../../services/infrastructure/CloudFormationService.js";import{EcsService as P}from"../../services/infrastructure/EcsService.js";import{EcsServiceResolver as M}from"../../services/infrastructure/EcsServiceResolver.js";import{EcrImageInspectorService as _,stripTagOrDigest as G}from"../../services/infrastructure/EcrImageInspectorService.js";import{SsmParameterMetadataService as F}from"../../services/infrastructure/SsmParameterMetadataService.js";import{extractEcsServiceName as k}from"../../aws/utils/arnParser.js";import{isAborted as H}from"../../aws/organisations/types.js";import{matchesServiceName as U,resolveServiceTaskDefinition as V}from"../application/serviceImageTagsRollback.js";import{deriveEcrRepositoryArn as j}from"../application/codeOnlyDeploy.js";import{registerAndRollTaskDefinition as B}from"../application/taskDefinitionRoll.js";import{computeSecretsDrift as y}from"./secretsDrift.js";const Y="restartApplication";function h(n){const e=new O(n),i=new L(e);return{ecsService:new P(e),ecsResolver:new M(i),ecrImageInspector:new _(e),ssmService:new F(e)}}async function C(n,e){const{target:i}=e;let r,s;if(e.knownServices!==void 0)r=e.knownServices.clusterArn,s=e.knownServices.serviceArns;else{const t=await n.getDeployableClusterAndServices(i);if(!t.success)return g(new Error(`Failed to discover ECS services for ${i}: ${R(t.error.message)}`));r=t.data.clusterArn,s=t.data.serviceArns}if(r===void 0||s.length===0)return g(new Error(`No running ECS services found for ${i} \u2014 nothing to restart. Deploy the application first.`));if(e.serviceName!==void 0){const t=s.filter(o=>{const a=k(o);return a!==void 0&&e.serviceName!==void 0?U(a,e.serviceName):!1});if(t.length===0){const o=s.map(a=>k(a)).filter(a=>a!==void 0).join(", ");return g(new Error(`No ECS service matching "${e.serviceName}" found for ${i}. Live services: ${o||"none"}`))}s=t}return $({clusterArn:r,serviceArns:s})}async function ce(n,e){const{ecsService:i,ecsResolver:r,ssmService:s}=e??h(n.awsCredentials),t=await C(r,n);return t.success?y({ecsService:i,ssmService:s},t.data,n.abortSignal):g(t.error)}async function fe(n,e){const{target:i,callbacks:r,abortSignal:s}=n,t=Date.now(),{ecsService:o,ecsResolver:a,ecrImageInspector:S,ssmService:u}=e??h(n.awsCredentials),f=await C(a,n);if(!f.success)return r.onError?.(f.error),g(f.error);const{clusterArn:w,serviceArns:c}=f.data,d=await y({ecsService:o,ssmService:u},{clusterArn:w,serviceArns:c},s);let l,E={};d.success?(l=d.data,E=d.data.parameterVersions):r.onLog?.(`Could not compute secrets drift before restart: ${R(d.error.message)}. Restarting anyway.`,"warn");const A=[];for(const D of c){if(H(s))return $({target:i,artefacts:A,appliedParameterVersions:E,...l!==void 0?{preRestartDrift:l}:{},durationMs:Date.now()-t,cancelled:!0});const I=Date.now(),m=k(D)??D;r.onLog?.(`Restarting ${m}\u2026`,"info");const v=await V({ecsService:o},w,D,m);if(!v.success){const p=new Error(`Restart failed for ${m}: ${R(v.error.message)}`);return r.onError?.(p),g(p)}const T=await B({ecsService:o},{clusterArn:w,serviceArn:D,serviceName:m,taskDefinition:v.data.taskDefinition,containerDefinitions:v.data.containerDefinitions,previousTaskDefArn:v.data.previousTaskDefArn,startedAtMs:I},r);if(!T.success){const p=new Error(`Restart failed for ${m}: ${R(T.error.message)}`);return r.onError?.(p),g(p)}const N=await q(S,m,v.data.containerDefinitions,T.data.newTaskDefArn,v.data.previousTaskDefArn,s);N!==void 0?A.push(N):r.onLog?.(`Restarted ${m}, but its image tag could not be recovered \u2014 the restart release will omit this service's artefact.`,"warn")}return x.debug(Y,`Restarted ${i}`,{services:c.length,artefacts:A.length}),$({target:i,artefacts:A,appliedParameterVersions:E,...l!==void 0?{preRestartDrift:l}:{},durationMs:Date.now()-t})}async function q(n,e,i,r,s,t){const o=i.find(c=>c.image)?.image;if(o===void 0)return;const a=G(o),S=j(a);let u,f;const w=o.lastIndexOf("@");if(w>0){f=o.slice(w+1);const c=await n.getImageTagsByDigest(a,f,t);if(c.success&&c.data!==void 0){const d=c.data;u=d.find(l=>b.test(l))??d[0]}}else{const c=o.lastIndexOf(":");if(c>0){u=o.slice(c+1);const d=await n.getImageDigest(a,u,t);d.success&&(f=d.data)}}if(!(u===void 0||u===""))return{serviceName:e,imageTag:u,imageUri:`${a}:${u}`,taskDefinitionArn:r,previousTaskDefinitionArn:s,...f!==void 0?{imageDigest:f}:{},...S!==""?{ecrRepositoryArn:S}:{}}}export{h as createRestartServices,ce as probeSecretsDrift,fe as restart};
@@ -0,0 +1,54 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import type { TaskDefinition } from "@aws-sdk/client-ecs";
3
+ import type { EcsService } from "../../services/infrastructure/EcsService.js";
4
+ import type { SsmParameterMetadata, SsmParameterMetadataService } from "../../services/infrastructure/SsmParameterMetadataService.js";
5
+ export interface StaleParameter {
6
+ name: string;
7
+ lastModifiedDate: string;
8
+ version?: number;
9
+ }
10
+ export interface ServiceSecretsDrift {
11
+ serviceName: string;
12
+ serviceArn: string;
13
+ taskDefinitionArn?: string;
14
+ taskDefRegisteredAt?: string;
15
+ /** SSM parameter names the live task definition injects (`secrets[].valueFrom`). */
16
+ consumedParameters: string[];
17
+ stale: boolean;
18
+ staleParameters: StaleParameter[];
19
+ }
20
+ export interface ApplicationSecretsDrift {
21
+ services: ServiceSecretsDrift[];
22
+ anyStale: boolean;
23
+ /** Latest known Version per consumed parameter name, across all services. */
24
+ parameterVersions: Record<string, number>;
25
+ }
26
+ /**
27
+ * Normalise an ECS `secrets[].valueFrom` reference to an SSM parameter name.
28
+ * Returns undefined for non-SSM references (Secrets Manager ARNs) — those are
29
+ * resolved by ECS from a different store and are out of drift scope.
30
+ */
31
+ export declare function ssmParameterNameFromValueFrom(valueFrom: string): string | undefined;
32
+ /** Collect the unique SSM parameter names a task definition injects. */
33
+ export declare function extractConsumedSsmParameters(taskDefinition: TaskDefinition): string[];
34
+ /**
35
+ * Pure staleness comparator: a parameter is stale for a service when it was
36
+ * modified AFTER the service's live task definition was registered — running
37
+ * tasks resolved the old value at launch and nothing has rolled them since.
38
+ * Self-heals on any roll (fresh registeredAt) and catches out-of-band writes
39
+ * (webapp, console, another machine) because the inputs are AWS ground truth.
40
+ */
41
+ export declare function deriveStaleParameters(taskDefRegisteredAt: Date | undefined, consumedNames: string[], parameters: readonly SsmParameterMetadata[]): StaleParameter[];
42
+ export interface SecretsDriftServices {
43
+ ecsService: Pick<EcsService, "describeServices" | "getLatestTaskDefinition">;
44
+ ssmService: Pick<SsmParameterMetadataService, "describeParametersByName">;
45
+ }
46
+ /**
47
+ * Compute per-service secrets drift for an application's live ECS services.
48
+ * Fails only when live state cannot be read at all; a service with no
49
+ * consumed SSM parameters reports clean.
50
+ */
51
+ export declare function computeSecretsDrift(services: SecretsDriftServices, target: {
52
+ clusterArn: string;
53
+ serviceArns: string[];
54
+ }, abortSignal?: AbortSignal): Promise<Result<ApplicationSecretsDrift, Error>>;
@@ -0,0 +1 @@
1
+ import{success as g,failure as S}from"@fjall/generator";import{extractEcsServiceName as D}from"../../aws/utils/arnParser.js";import{isAborted as h}from"../../aws/organisations/types.js";const N=/^arn:aws[a-z-]*:ssm:[a-z0-9-]*:\d{12}:parameter/;function k(t){if(t.startsWith("arn:")){const n=t.match(N);if(!n)return;const r=t.slice(n[0].length);return r.startsWith("/")?r:`/${r}`}return t}function w(t){const n=new Set;for(const r of t.containerDefinitions??[])for(const a of r.secrets??[]){if(a.valueFrom===void 0)continue;const s=k(a.valueFrom);s!==void 0&&n.add(s)}return[...n].sort()}function M(t,n,r){if(t===void 0)return[];const a=new Map(r.map(o=>[o.name,o])),s=[];for(const o of n){const i=a.get(o);i?.lastModifiedDate!==void 0&&i.lastModifiedDate.getTime()>t.getTime()&&s.push({name:o,lastModifiedDate:i.lastModifiedDate.toISOString(),...i.version!==void 0?{version:i.version}:{}})}return s}async function E(t,n,r){const a=await t.ecsService.describeServices(n.clusterArn,n.serviceArns);if(!a.success)return S(new Error(`Failed to describe services: ${a.error.message}`));const s=[],o=new Set;for(const e of a.data){if(h(r))break;const c=e.serviceArn??"",d=e.serviceName??(c!==""?D(c):void 0);if(d===void 0||d==="")continue;const m=e.taskDefinition;if(m===void 0){s.push({serviceName:d,serviceArn:c,consumed:[]});continue}const f=await t.ecsService.getLatestTaskDefinition(m);if(!f.success||f.data===void 0){s.push({serviceName:d,serviceArn:c,taskDefinitionArn:m,consumed:[]});continue}const l=w(f.data);l.forEach(v=>o.add(v)),s.push({serviceName:d,serviceArn:c,taskDefinitionArn:m,...f.data.registeredAt!==void 0?{registeredAt:f.data.registeredAt}:{},consumed:l})}const i=await t.ssmService.describeParametersByName([...o].sort(),r);if(!i.success)return S(i.error);const u=i.data,p={};for(const e of u)e.version!==void 0&&(p[e.name]=e.version);const A=s.map(e=>{const c=M(e.registeredAt,e.consumed,u);return{serviceName:e.serviceName,serviceArn:e.serviceArn,...e.taskDefinitionArn!==void 0?{taskDefinitionArn:e.taskDefinitionArn}:{},...e.registeredAt!==void 0?{taskDefRegisteredAt:e.registeredAt.toISOString()}:{},consumedParameters:e.consumed,stale:c.length>0,staleParameters:c}});return g({services:A,anyStale:A.some(e=>e.stale),parameterVersions:p})}export{E as computeSecretsDrift,M as deriveStaleParameters,w as extractConsumedSsmParameters,k as ssmParameterNameFromValueFrom};
@@ -93,4 +93,21 @@ export declare class CloudFormationService {
93
93
  pollIntervalMs?: number;
94
94
  onProgress?: (message: string) => void;
95
95
  }): Promise<Result<void, CloudFormationError>>;
96
+ /**
97
+ * Converge a subset of a stack's parameters to new values, preserving every
98
+ * other parameter via `UsePreviousValue` (F4b — post-rollback CFN ImageTag
99
+ * reconcile). Keys in `parameterOverrides` absent from the stack's current
100
+ * parameters are reported as `skippedKeys` rather than failing the call —
101
+ * a stale key (a service the stack no longer has) should not block
102
+ * reconciling the keys that do exist.
103
+ */
104
+ updateStackParameters(stackName: string, parameterOverrides: Record<string, string>, abortSignal?: AbortSignal): Promise<Result<{
105
+ updated: boolean;
106
+ skippedKeys: string[];
107
+ }, CloudFormationError>>;
108
+ /**
109
+ * Poll stack status until the parameter-only update completes, fails, or
110
+ * times out.
111
+ */
112
+ private waitForUpdateStackParametersComplete;
96
113
  }
@@ -1 +1 @@
1
- import{CloudFormationClient as d,DeleteStackCommand as h,DescribeStacksCommand as m,GetTemplateCommand as k,ListExportsCommand as y}from"@aws-sdk/client-cloudformation";import{stackStatusMap as S}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as f}from"@fjall/util";import{success as i,failure as c}from"@fjall/generator";import{BaseServiceError as T}from"../../types/errors/ServiceError.js";import{logger as x}from"@fjall/util/logger";import{getErrorMessage as p,sleep as E}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as w}from"@fjall/util/aws";import{isCleanableState as C}from"../../types/constants.js";import{extractErrorName as O}from"../../aws/organisations/types.js";class l extends T{errorType;stackName;stackStatus;constructor(t,e,s,n,r,o=!1){super(`CFN_${e.toUpperCase()}`,t,r,o),this.errorType=e,this.stackName=s,this.stackStatus=n}}class I{aws;constructor(t){this.aws=t}classifyAwsError(t,e,s){const n=O(t),r=f(p(t));return n==="CredentialsError"||n==="UnauthorizedError"?new l(`AWS credentials error: ${r}`,"auth_error",s,void 0,t,!1):n==="Throttling"||n==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${r}`,"throttled",s,void 0,t,!0):n==="NetworkingError"||n==="ENOTFOUND"?new l(`Network error: ${r}`,"network_error",s,void 0,t,!0):new l(`${e}: ${r}`,"unknown",s,void 0,t)}async getStackOutputs(t,e){e?.onStackCheck?.(t);const s=this.aws.getClient(d),n=new m({StackName:t});try{const o=(await s.send(n)).Stacks?.[0];if(!o?.Outputs)return i([]);const a=o.Outputs.map(u=>({OutputKey:u.OutputKey,OutputValue:u.OutputValue,ExportName:u.ExportName}));return e?.onOutputsRetrieved?.(t,a.length),i(a)}catch(r){if(r instanceof Error&&r.name==="ValidationError"&&r.message?.includes(w))return e?.onStackNotFound?.(t),i([]);const o=f(p(r));return c(new l(`Failed to get outputs for stack ${t}: ${o}`,"unknown",t,void 0,r))}}async getStackStatus(t,e){e?.onStackCheck?.(t);const s=this.aws.getClient(d),n=new m({StackName:t});try{const o=(await s.send(n)).Stacks?.[0];if(!o)return i({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const a=o.StackStatus||"UNKNOWN",u=S[a]||S.UNKNOWN;return e?.onStackFound?.(t,a),i({status:a,safeToRedeploy:u.safeToRedeploy,description:u.description,statusReason:o.StackStatusReason})}catch(r){return r instanceof Error&&r.name==="ValidationError"&&r.message?.includes(w)?i({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(r,`Failed to get stack status for ${t}`,t))}}async listAllExports(t){const e=this.aws.getClient(d),s=[];try{let n;do{const r=new y({NextToken:n}),o=await e.send(r),a=o.Exports||[];for(const u of a)u.Name&&u.Value&&s.push({Name:u.Name,Value:u.Value});if(t?.(a))break;n=o.NextToken}while(n);return i(s)}catch(n){const r=f(p(n));return c(new l(`Failed to list exports: ${r}`,"unknown",void 0,void 0,n,!1))}}async getExportsByNames(t){if(t.length===0)return i(new Map);const e=new Set(t),s=new Map,n=await this.listAllExports(r=>{for(const o of r)o.Name&&e.has(o.Name)&&o.Value&&s.set(o.Name,o.Value);return s.size>=e.size});return n.success?i(s):c(n.error)}async listExports(t){const e=await this.listAllExports();return e.success&&t?.onExportsRetrieved?.(e.data.length),e}async deleteStack(t){const e=this.aws.getClient(d);try{return await e.send(new h({StackName:t})),i(void 0)}catch(s){return c(this.classifyAwsError(s,`Failed to delete stack ${t}`,t))}}async stackExists(t,e){const s=e??this.aws.getClient(d);try{const r=(await s.send(new m({StackName:t}))).Stacks?.[0]?.StackStatus;return!!r&&r!=="REVIEW_IN_PROGRESS"&&!C(r)}catch(n){return n instanceof Error&&n.message?.includes(w)?!1:(x.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:t,error:p(n)}),!0)}}async getTemplate(t){const e=this.aws.getClient(d);try{const s=await e.send(new k({StackName:t,TemplateStage:"Original"}));return i(s.TemplateBody??"")}catch(s){return s instanceof Error&&s.message?.includes(w)?c(new l(`Stack ${t} not found`,"stack_not_found",t)):c(this.classifyAwsError(s,`Failed to get template for stack ${t}`,t))}}async waitForDeleteComplete(t,e){const s=e?.timeoutMs??6e5,n=e?.pollIntervalMs??5e3,r=Date.now();for(;Date.now()-r<s;){const o=await this.getStackStatus(t);if(!o.success){if(o.error.recoverable){e?.onProgress?.(`Transient error polling stack ${t}, retrying: ${f(o.error.message)}`),await E(n);continue}return c(o.error)}const a=o.data?.status;if(a==="DELETE_COMPLETE"||a==="DOES_NOT_EXIST")return i(void 0);if(a==="DELETE_FAILED")return c(new l(`Stack ${t} deletion failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",t,a,void 0,!1));e?.onProgress?.(`Stack ${t} status: ${a??"unknown"}`),await E(n)}return c(new l(`Timed out waiting for stack ${t} deletion after ${Math.round(s/1e3)}s`,"timeout",t,void 0,void 0,!0))}}export{l as CloudFormationError,I as CloudFormationService};
1
+ import{CloudFormationClient as f,DeleteStackCommand as _,DescribeStacksCommand as m,GetTemplateCommand as A,ListExportsCommand as O,UpdateStackCommand as P}from"@aws-sdk/client-cloudformation";import{stackStatusMap as T}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as w}from"@fjall/util";import{success as d,failure as c}from"@fjall/generator";import{BaseServiceError as $}from"../../types/errors/ServiceError.js";import{logger as M}from"@fjall/util/logger";import{getErrorMessage as S,sleep as E}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as g}from"@fjall/util/aws";import{isCleanableState as F}from"../../types/constants.js";import{composeSdkAbortSignal as C,extractErrorName as R,isAborted as D}from"../../aws/organisations/types.js";class l extends ${errorType;stackName;stackStatus;constructor(e,r,t,n,s,a=!1){super(`CFN_${r.toUpperCase()}`,e,s,a),this.errorType=r,this.stackName=t,this.stackStatus=n}}class Y{aws;constructor(e){this.aws=e}classifyAwsError(e,r,t){const n=R(e),s=w(S(e));return n==="CredentialsError"||n==="UnauthorizedError"?new l(`AWS credentials error: ${s}`,"auth_error",t,void 0,e,!1):n==="Throttling"||n==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${s}`,"throttled",t,void 0,e,!0):n==="NetworkingError"||n==="ENOTFOUND"?new l(`Network error: ${s}`,"network_error",t,void 0,e,!0):new l(`${r}: ${s}`,"unknown",t,void 0,e)}async getStackOutputs(e,r){r?.onStackCheck?.(e);const t=this.aws.getClient(f),n=new m({StackName:e});try{const a=(await t.send(n)).Stacks?.[0];if(!a?.Outputs)return d([]);const o=a.Outputs.map(u=>({OutputKey:u.OutputKey,OutputValue:u.OutputValue,ExportName:u.ExportName}));return r?.onOutputsRetrieved?.(e,o.length),d(o)}catch(s){if(s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(g))return r?.onStackNotFound?.(e),d([]);const a=w(S(s));return c(new l(`Failed to get outputs for stack ${e}: ${a}`,"unknown",e,void 0,s))}}async getStackStatus(e,r){r?.onStackCheck?.(e);const t=this.aws.getClient(f),n=new m({StackName:e});try{const a=(await t.send(n)).Stacks?.[0];if(!a)return d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const o=a.StackStatus||"UNKNOWN",u=T[o]||T.UNKNOWN;return r?.onStackFound?.(e,o),d({status:o,safeToRedeploy:u.safeToRedeploy,description:u.description,statusReason:a.StackStatusReason})}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(g)?d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(s,`Failed to get stack status for ${e}`,e))}}async listAllExports(e){const r=this.aws.getClient(f),t=[];try{let n;do{const s=new O({NextToken:n}),a=await r.send(s),o=a.Exports||[];for(const u of o)u.Name&&u.Value&&t.push({Name:u.Name,Value:u.Value});if(e?.(o))break;n=a.NextToken}while(n);return d(t)}catch(n){const s=w(S(n));return c(new l(`Failed to list exports: ${s}`,"unknown",void 0,void 0,n,!1))}}async getExportsByNames(e){if(e.length===0)return d(new Map);const r=new Set(e),t=new Map,n=await this.listAllExports(s=>{for(const a of s)a.Name&&r.has(a.Name)&&a.Value&&t.set(a.Name,a.Value);return t.size>=r.size});return n.success?d(t):c(n.error)}async listExports(e){const r=await this.listAllExports();return r.success&&e?.onExportsRetrieved?.(r.data.length),r}async deleteStack(e){const r=this.aws.getClient(f);try{return await r.send(new _({StackName:e})),d(void 0)}catch(t){return c(this.classifyAwsError(t,`Failed to delete stack ${e}`,e))}}async stackExists(e,r){const t=r??this.aws.getClient(f);try{const s=(await t.send(new m({StackName:e}))).Stacks?.[0]?.StackStatus;return!!s&&s!=="REVIEW_IN_PROGRESS"&&!F(s)}catch(n){return n instanceof Error&&n.message?.includes(g)?!1:(M.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:e,error:S(n)}),!0)}}async getTemplate(e){const r=this.aws.getClient(f);try{const t=await r.send(new A({StackName:e,TemplateStage:"Original"}));return d(t.TemplateBody??"")}catch(t){return t instanceof Error&&t.message?.includes(g)?c(new l(`Stack ${e} not found`,"stack_not_found",e)):c(this.classifyAwsError(t,`Failed to get template for stack ${e}`,e))}}async waitForDeleteComplete(e,r){const t=r?.timeoutMs??6e5,n=r?.pollIntervalMs??5e3,s=Date.now();for(;Date.now()-s<t;){const a=await this.getStackStatus(e);if(!a.success){if(a.error.recoverable){r?.onProgress?.(`Transient error polling stack ${e}, retrying: ${w(a.error.message)}`),await E(n);continue}return c(a.error)}const o=a.data?.status;if(o==="DELETE_COMPLETE"||o==="DOES_NOT_EXIST")return d(void 0);if(o==="DELETE_FAILED")return c(new l(`Stack ${e} deletion failed: ${a.data?.statusReason||"unknown reason"}`,"stack_failed",e,o,void 0,!1));r?.onProgress?.(`Stack ${e} status: ${o??"unknown"}`),await E(n)}return c(new l(`Timed out waiting for stack ${e} deletion after ${Math.round(t/1e3)}s`,"timeout",e,void 0,void 0,!0))}async updateStackParameters(e,r,t){const n=this.aws.getClient(f);let s;try{s=await n.send(new m({StackName:e}),{abortSignal:C(t)})}catch(i){return c(this.classifyAwsError(i,`Failed to describe stack ${e} before parameter reconcile`,e))}const a=s.Stacks?.[0];if(!a)return c(new l(`Stack ${e} not found`,"stack_not_found",e));const o=a.Parameters??[],u=new Set(o.map(i=>i.ParameterKey).filter(i=>i!==void 0)),k=Object.entries(r),h=k.filter(([i])=>!u.has(i)).map(([i])=>i),y=new Map(k.filter(([i])=>u.has(i)));if(y.size===0)return d({updated:!1,skippedKeys:h});const x=o.filter(i=>i.ParameterKey!==void 0).map(i=>y.has(i.ParameterKey)?{ParameterKey:i.ParameterKey,ParameterValue:y.get(i.ParameterKey)}:{ParameterKey:i.ParameterKey,UsePreviousValue:!0});try{await n.send(new P({StackName:e,UsePreviousTemplate:!0,Parameters:x,Capabilities:["CAPABILITY_IAM","CAPABILITY_NAMED_IAM"]}),{abortSignal:C(t)})}catch(i){return i instanceof Error&&i.name==="ValidationError"&&i.message?.includes("No updates are to be performed")?d({updated:!1,skippedKeys:h}):c(this.classifyAwsError(i,`Failed to update parameters for stack ${e}`,e))}return this.waitForUpdateStackParametersComplete(e,h,t)}async waitForUpdateStackParametersComplete(e,r,t){const a=Date.now();for(;Date.now()-a<6e5;){if(D(t))return c(new l(`Aborted waiting for stack ${e} parameter update`,"timeout",e,void 0,void 0,!0));const o=await this.getStackStatus(e);if(!o.success){if(o.error.recoverable){await E(5e3);continue}return c(o.error)}const u=o.data?.status??"UNKNOWN";if(u==="UPDATE_COMPLETE")return d({updated:!0,skippedKeys:r});if(I(u))return c(new l(`Stack ${e} parameter update failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",e,u,void 0,!1));await E(5e3)}return c(new l(`Timed out waiting for stack ${e} parameter update after ${Math.round(6e5/1e3)}s`,"timeout",e,void 0,void 0,!0))}}function I(p){return p.endsWith("_IN_PROGRESS")?!1:p.startsWith("UPDATE_ROLLBACK_")||p==="UPDATE_FAILED"}export{l as CloudFormationError,Y as CloudFormationService};
@@ -28,5 +28,15 @@ export declare class EcrImageInspectorService {
28
28
  * third-party registries). Returns a failure only when the ECR call
29
29
  * itself errors or the tag exists but ECR returned no digest.
30
30
  */
31
- getImageDigest(imageUri: string, imageTag: string): Promise<Result<string | undefined, Error>>;
31
+ getImageDigest(imageUri: string, imageTag: string, abortSignal?: AbortSignal): Promise<Result<string | undefined, Error>>;
32
+ /**
33
+ * Reverse lookup: the tags attached to an image digest. Needed by the
34
+ * restart operation — after a digest-pinned rollback the live container
35
+ * image reference carries no tag, but the restart Release's artefacts must
36
+ * record one for later `serviceImageTags` rollbacks to consume.
37
+ *
38
+ * Returns `success(undefined)` for non-ECR URIs (caller degrades),
39
+ * `success([])` when the image exists but is untagged.
40
+ */
41
+ getImageTagsByDigest(imageUri: string, imageDigest: string, abortSignal?: AbortSignal): Promise<Result<string[] | undefined, Error>>;
32
42
  }
@@ -1 +1 @@
1
- import{ECRClient as m,BatchGetImageCommand as p}from"@aws-sdk/client-ecr";import{success as n,failure as s}from"@fjall/generator";import{logger as g}from"@fjall/util/logger";import{getErrorMessage as I}from"@fjall/util";const E="EcrImageInspector",C=/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/;function R(t){const e=t.lastIndexOf("@");if(e>0)return t.slice(0,e);const r=t.lastIndexOf(":");return r>0?t.slice(0,r):t}class G{awsProvider;constructor(e){this.awsProvider=e}async getImageDigest(e,r){const c=R(e).match(C);if(!c)return g.debug(E,"Image URI is not ECR \u2014 skipping digest lookup",{imageUri:e}),n(void 0);const[,,,o]=c;if(!o)return n(void 0);try{const a=await this.awsProvider.getClient(m).send(new p({repositoryName:o,imageIds:[{imageTag:r}]})),d=a.failures??[];if(d.length>0){const l=d.map(f=>`${f.failureCode??"unknown"}:${f.failureReason??""}`).join("; ");return s(new Error(`ECR BatchGetImage rejected ${o}:${r} \u2014 ${l}`))}const u=a.images?.[0]?.imageId?.imageDigest;return u?n(u):s(new Error(`ECR BatchGetImage returned no digest for ${o}:${r}`))}catch(i){return s(new Error(`ECR digest lookup failed for ${o}:${r} \u2014 ${I(i)}`))}}}export{G as EcrImageInspectorService,R as stripTagOrDigest};
1
+ import{ECRClient as m,BatchGetImageCommand as h,DescribeImagesCommand as k}from"@aws-sdk/client-ecr";import{success as o,failure as c}from"@fjall/generator";import{logger as f}from"@fjall/util/logger";import{getErrorMessage as p}from"@fjall/util";import{composeSdkAbortSignal as I}from"../../aws/organisations/types.js";const E="EcrImageInspector",C=/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/;function $(n){const r=n.lastIndexOf("@");if(r>0)return n.slice(0,r);const e=n.lastIndexOf(":");return e>0?n.slice(0,e):n}class G{awsProvider;constructor(r){this.awsProvider=r}async getImageDigest(r,e,d){const s=$(r).match(C);if(!s)return f.debug(E,"Image URI is not ECR \u2014 skipping digest lookup",{imageUri:r}),o(void 0);const[,,,t]=s;if(!t)return o(void 0);try{const u=await this.awsProvider.getClient(m).send(new h({repositoryName:t,imageIds:[{imageTag:e}]}),{abortSignal:I(d)}),a=u.failures??[];if(a.length>0){const R=a.map(l=>`${l.failureCode??"unknown"}:${l.failureReason??""}`).join("; ");return c(new Error(`ECR BatchGetImage rejected ${t}:${e} \u2014 ${R}`))}const g=u.images?.[0]?.imageId?.imageDigest;return g?o(g):c(new Error(`ECR BatchGetImage returned no digest for ${t}:${e}`))}catch(i){return c(new Error(`ECR digest lookup failed for ${t}:${e} \u2014 ${p(i)}`))}}async getImageTagsByDigest(r,e,d){const s=$(r).match(C);if(!s)return f.debug(E,"Image URI is not ECR \u2014 skipping tag lookup by digest",{imageUri:r}),o(void 0);const[,,,t]=s;if(!t)return o(void 0);try{const a=(await this.awsProvider.getClient(m).send(new k({repositoryName:t,imageIds:[{imageDigest:e}]}),{abortSignal:I(d)})).imageDetails?.[0];return a?o(a.imageTags??[]):c(new Error(`ECR DescribeImages returned no image for ${t}@${e}`))}catch(i){return c(new Error(`ECR tag lookup failed for ${t}@${e} \u2014 ${p(i)}`))}}}export{G as EcrImageInspectorService,$ as stripTagOrDigest};
@@ -0,0 +1,23 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
3
+ export interface SsmParameterMetadata {
4
+ name: string;
5
+ version?: number;
6
+ lastModifiedDate?: Date;
7
+ }
8
+ /**
9
+ * Metadata-only SSM reads for the secrets-drift probe. Deliberately uses
10
+ * DescribeParameters (never GetParameter/GetParameters): drift needs only
11
+ * `LastModifiedDate` + `Version`, and a value-bearing call would put secret
12
+ * contents in memory for no reason.
13
+ */
14
+ export declare class SsmParameterMetadataService {
15
+ private readonly awsProvider;
16
+ constructor(awsProvider: AwsProvider);
17
+ /**
18
+ * Describe parameters by exact name. Names absent from the result simply
19
+ * do not exist (or are not visible) — callers treat them as unknown, not
20
+ * as an error.
21
+ */
22
+ describeParametersByName(names: string[], abortSignal?: AbortSignal): Promise<Result<SsmParameterMetadata[], Error>>;
23
+ }
@@ -0,0 +1 @@
1
+ import{SSMClient as u,DescribeParametersCommand as l}from"@aws-sdk/client-ssm";import{success as m,failure as p}from"@fjall/generator";import{getErrorMessage as S,maskSensitiveOutput as P}from"@fjall/util";import{composeSdkAbortSignal as b,isAborted as c}from"../../aws/organisations/types.js";const o=50;class v{awsProvider;constructor(r){this.awsProvider=r}async describeParametersByName(r,i){if(r.length===0)return m([]);try{const a=this.awsProvider.getClient(u),n=[];for(let t=0;t<r.length&&!c(i);t+=o){const f=r.slice(t,t+o);let s;do{if(c(i))break;const d=await a.send(new l({ParameterFilters:[{Key:"Name",Option:"Equals",Values:f}],MaxResults:o,...s!==void 0?{NextToken:s}:{}}),{abortSignal:b(i)});for(const e of d.Parameters??[])e.Name!==void 0&&n.push({name:e.Name,...e.Version!==void 0?{version:Number(e.Version)}:{},...e.LastModifiedDate!==void 0?{lastModifiedDate:e.LastModifiedDate}:{}});s=d.NextToken}while(s!==void 0)}return m(n)}catch(a){return p(new Error(`SSM DescribeParameters failed: ${P(S(a))}`))}}}export{v as SsmParameterMetadataService};
@@ -113,6 +113,14 @@ export interface DeployOptions {
113
113
  * build + push and rolls ECS to `<repo>:<imageTag>`. Implies `deployOnly`.
114
114
  */
115
115
  imageTag?: string;
116
+ /**
117
+ * Exact per-service image tags for rollback mode (F12), keyed by the bare
118
+ * service name recorded in a Release's `ServiceArtefact[]`. Mutually
119
+ * exclusive with `imageTag` — each service rolls to its EXACT supplied tag,
120
+ * bypassing the `rebaseRollbackTagForService` prefix heuristic entirely.
121
+ * Implies `deployOnly`.
122
+ */
123
+ serviceImageTags?: Record<string, string>;
116
124
  /**
117
125
  * Approval-gate: require an explicit approve decision before mutating. The
118
126
  * engine only gates when a gate is engaged (resolver injected, `planOnly`, or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/deploy-core",
3
- "version": "2.24.0",
3
+ "version": "2.25.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",
@@ -82,8 +82,8 @@
82
82
  "@aws-sdk/client-ssm": "^3.1038.0",
83
83
  "@aws-sdk/client-sso-admin": "^3.1038.0",
84
84
  "@aws-sdk/client-sts": "^3.1038.0",
85
- "@fjall/generator": "^2.24.0",
86
- "@fjall/util": "^2.24.0",
85
+ "@fjall/generator": "^2.25.0",
86
+ "@fjall/util": "^2.25.0",
87
87
  "@smithy/node-http-handler": "^4.6.1",
88
88
  "aws-cdk": "^2.1128.1",
89
89
  "tsx": "^4.21.0",
@@ -94,5 +94,5 @@
94
94
  "typescript": "^6.0.3",
95
95
  "vitest": "^4.1.5"
96
96
  },
97
- "gitHead": "616d7af846f0eebfa3409f5f47c98fbee836bad9"
97
+ "gitHead": "7c1a329184064aefa557c2c09de0965c4f8cd4fb"
98
98
  }