@fjall/deploy-core 2.27.0 → 2.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.minified +1 -1
- package/dist/src/aws/organisations/backup.d.ts +7 -2
- package/dist/src/aws/organisations/backup.js +2 -2
- package/dist/src/aws/organisations/index.d.ts +1 -1
- package/dist/src/aws/organisations/index.js +1 -1
- package/dist/src/aws/organisations/oidcProvider.d.ts +37 -0
- package/dist/src/aws/organisations/oidcProvider.js +1 -1
- package/dist/src/aws/organisations/organisation.d.ts +21 -0
- package/dist/src/aws/organisations/organisation.js +1 -1
- package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +45 -0
- package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
- package/dist/src/aws/utils/cloudformationEventTypes.d.ts +1 -1
- package/dist/src/aws/utils/cloudformationEvents.d.ts +2 -1
- package/dist/src/aws/utils/cloudformationEvents.js +1 -1
- package/dist/src/aws/utils/stackStatus.js +1 -1
- package/dist/src/index.d.ts +12 -1
- package/dist/src/index.js +1 -1
- package/dist/src/orchestration/activeDeploymentGuard.d.ts +41 -0
- package/dist/src/orchestration/activeDeploymentGuard.js +8 -4
- package/dist/src/orchestration/application/applicationDeploy.d.ts +9 -0
- package/dist/src/orchestration/application/applicationDeploy.js +1 -1
- package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +20 -3
- package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
- package/dist/src/orchestration/application/applicationDestroy.js +1 -1
- package/dist/src/orchestration/application/approvalGate.d.ts +15 -1
- package/dist/src/orchestration/application/approvalGate.js +1 -1
- package/dist/src/orchestration/application/destructionGate.d.ts +164 -0
- package/dist/src/orchestration/application/destructionGate.js +5 -0
- package/dist/src/orchestration/application/leaseGate.d.ts +30 -0
- package/dist/src/orchestration/application/leaseGate.js +1 -0
- package/dist/src/orchestration/application/plan/approvalToken.d.ts +15 -0
- package/dist/src/orchestration/application/plan/approvalToken.js +1 -1
- package/dist/src/orchestration/application/plan/changeSetEscalation.d.ts +46 -0
- package/dist/src/orchestration/application/plan/changeSetEscalation.js +1 -0
- package/dist/src/orchestration/application/plan/computeDeployPlan.d.ts +13 -0
- package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
- package/dist/src/orchestration/application/plan/index.d.ts +4 -1
- package/dist/src/orchestration/application/plan/index.js +1 -1
- package/dist/src/orchestration/application/plan/loadDeployPlan.d.ts +32 -0
- package/dist/src/orchestration/application/plan/loadDeployPlan.js +1 -0
- package/dist/src/orchestration/application/plan/pinnedNames.d.ts +31 -0
- package/dist/src/orchestration/application/plan/pinnedNames.js +1 -0
- package/dist/src/orchestration/application/plan/registryOverlay.d.ts +83 -0
- package/dist/src/orchestration/application/plan/registryOverlay.js +1 -0
- package/dist/src/orchestration/application/plan/renderDeployPlan.d.ts +4 -1
- package/dist/src/orchestration/application/plan/renderDeployPlan.js +1 -1
- package/dist/src/orchestration/application/plan/types.d.ts +31 -0
- package/dist/src/orchestration/application/plan/types.js +1 -0
- package/dist/src/orchestration/contextHelpers.d.ts +37 -1
- package/dist/src/orchestration/contextHelpers.js +1 -1
- package/dist/src/orchestration/drift/classifyDriftFailure.d.ts +31 -0
- package/dist/src/orchestration/drift/classifyDriftFailure.js +1 -0
- package/dist/src/orchestration/drift/detectStackDrift.d.ts +37 -0
- package/dist/src/orchestration/drift/detectStackDrift.js +1 -0
- package/dist/src/orchestration/drift/driftPreFlight.d.ts +73 -0
- package/dist/src/orchestration/drift/driftPreFlight.js +7 -0
- package/dist/src/orchestration/drift/driftProbe.d.ts +113 -0
- package/dist/src/orchestration/drift/driftProbe.js +1 -0
- package/dist/src/orchestration/drift/driftSuspectJournal.d.ts +40 -0
- package/dist/src/orchestration/drift/driftSuspectJournal.js +1 -0
- package/dist/src/orchestration/drift/preFlightGate.d.ts +44 -0
- package/dist/src/orchestration/drift/preFlightGate.js +2 -0
- package/dist/src/orchestration/index.d.ts +19 -0
- package/dist/src/orchestration/index.js +1 -1
- package/dist/src/orchestration/organisation/cascadeDestroyHelpers.d.ts +7 -0
- package/dist/src/orchestration/organisation/cascadeDestroyHelpers.js +1 -1
- package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
- package/dist/src/orchestration/organisation/organisationDestroy.js +3 -3
- package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +8 -1
- package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
- package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +5 -5
- package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +21 -2
- package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
- package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
- package/dist/src/orchestration/remediation/driftRepairGate.d.ts +66 -0
- package/dist/src/orchestration/remediation/driftRepairGate.js +1 -0
- package/dist/src/orchestration/remediation/forensicsCapture.d.ts +49 -0
- package/dist/src/orchestration/remediation/forensicsCapture.js +1 -0
- package/dist/src/orchestration/remediation/forgetResource.d.ts +423 -0
- package/dist/src/orchestration/remediation/forgetResource.js +1 -0
- package/dist/src/orchestration/remediation/pinRemediation.d.ts +121 -0
- package/dist/src/orchestration/remediation/pinRemediation.js +1 -0
- package/dist/src/orchestration/remediation/postFailureRepairOffer.d.ts +77 -0
- package/dist/src/orchestration/remediation/postFailureRepairOffer.js +14 -0
- package/dist/src/orchestration/remediation/recreatePreFlight.d.ts +47 -0
- package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -0
- package/dist/src/orchestration/remediation/recreateResource.d.ts +110 -0
- package/dist/src/orchestration/remediation/recreateResource.js +1 -0
- package/dist/src/orchestration/remediation/remediationJournal.d.ts +88 -0
- package/dist/src/orchestration/remediation/remediationJournal.js +2 -0
- package/dist/src/orchestration/remediation/removalUpdate.d.ts +135 -0
- package/dist/src/orchestration/remediation/removalUpdate.js +1 -0
- package/dist/src/orchestration/remediation/retainFlip.d.ts +294 -0
- package/dist/src/orchestration/remediation/retainFlip.js +1 -0
- package/dist/src/services/application/ApplicationStackService.d.ts +5 -3
- package/dist/src/services/application/ApplicationStackService.js +1 -1
- package/dist/src/services/application/applicationStackHelpers.d.ts +8 -6
- package/dist/src/services/application/applicationStackHelpers.js +3 -3
- package/dist/src/services/index.d.ts +2 -2
- package/dist/src/services/index.js +1 -1
- package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +15 -0
- package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -1
- package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +8 -0
- package/dist/src/services/infrastructure/CdkOutputAnalyser.js +2 -1
- package/dist/src/services/infrastructure/CdkService.js +3 -3
- package/dist/src/services/infrastructure/CfnRegistryService.d.ts +68 -0
- package/dist/src/services/infrastructure/CfnRegistryService.js +1 -0
- package/dist/src/services/infrastructure/CloudFormationService.d.ts +5 -2
- package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
- package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +23 -3
- package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
- package/dist/src/services/infrastructure/cfnRegistrySeed.d.ts +10 -0
- package/dist/src/services/infrastructure/cfnRegistrySeed.js +1 -0
- package/dist/src/services/infrastructure/changeSetProbe.d.ts +88 -0
- package/dist/src/services/infrastructure/changeSetProbe.js +1 -0
- package/dist/src/services/infrastructure/index.d.ts +2 -0
- package/dist/src/services/infrastructure/index.js +1 -1
- package/dist/src/types/callbackKeys.d.ts +1 -1
- package/dist/src/types/callbackKeys.js +1 -1
- package/dist/src/types/callbacks.d.ts +22 -0
- package/dist/src/types/config/entitlements.d.ts +9 -0
- package/dist/src/types/config/orgConfig.d.ts +6 -0
- package/dist/src/types/deploymentEventSchema.d.ts +1 -0
- package/dist/src/types/deploymentEventSchema.js +1 -1
- package/dist/src/types/destruction.d.ts +212 -0
- package/dist/src/types/destruction.js +1 -0
- package/dist/src/types/drift.d.ts +72 -0
- package/dist/src/types/drift.js +1 -0
- package/dist/src/types/index.d.ts +8 -2
- package/dist/src/types/index.js +1 -1
- package/dist/src/types/params.d.ts +142 -0
- package/dist/src/types/remediation.d.ts +126 -0
- package/dist/src/types/remediation.js +1 -0
- package/package.json +5 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
import{join as Y}from"path";import{CloudFormationClient as ne}from"@aws-sdk/client-cloudformation";import{EC2Client as oe}from"@aws-sdk/client-ec2";import{success as v,failure as l}from"@fjall/generator";import{logger as N}from"@fjall/util/logger";import{ImageTagSchema as ie,imageTagParameterName as se,maskSensitiveOutput as k}from"@fjall/util";import{parseDockerServicesFromManifest as ce}from"@fjall/util/manifest";import{stubCallerIdentity as le}from"../../types/deployment/index.js";import{getApplicationDeployOrder as de,getApplicationStackName as J,getApplicationStepName as Q,getApplicationStepId as V,APPLICATION_STACKS as S}from"../../types/operations.js";import{CdkContextBuilder as pe}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as ue,bootstrapOrFail as ge}from"../contextHelpers.js";import{runDetectionPipeline as fe}from"./detectionPipeline.js";import{STEP_IDS as R,STEP_NAMES as me}from"../../types/stepDefinitions.js";import{StepRegistry as ke}from"../../steps/stepRegistry.js";import{checkTargetReadiness as ye}from"../../aws/targetReadiness.js";import{cascadeHomeRegion as he}from"../organisation/cascadeHelpers.js";import{withStepLifecycle as be}from"../stepLifecycle.js";import{DOCKER_BUILD_STEP_NAME as X,runDockerBuild as Se}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as we,deployParallelPhase as Te,deployStackSequential as De,runDockerPreCompute as ve,createBuildCallbacks as Ee}from"./applicationDeployHelpers.js";import{captureDatabaseConnectionOutputs as Ce,captureServiceTaskDefinitions as Oe,reconcileDatabaseEndpointChange as Ie}from"./databaseEndpointReconcile.js";import{deployCodeOnly as Ne}from"./codeOnlyDeploy.js";import{collectFullDeployArtefacts as Re}from"./artefactCollection.js";import{runApprovalGate as Pe}from"./approvalGate.js";const P="applicationDeploy";function Ae(e){const i=Object.entries(e);if(i.length===0)return;const a={};for(const[r,s]of i){const h=se(r);a[h]=s}return a}function Me(e){if(e?.mode!==void 0&&e.mode!=="rollback"){if(e.imageTag)return l(new Error(`--image-tag requires rollback mode (explicit mode '${e.mode}' was supplied alongside an image tag)`));if(e.serviceImageTags)return l(new Error(`serviceImageTags requires rollback mode (explicit mode '${e.mode}' was supplied alongside serviceImageTags)`))}if(e?.mode==="rollback"){if(e.imageTag&&e.serviceImageTags)return l(new Error("rollback mode accepts exactly one of --image-tag or serviceImageTags \u2014 they are mutually exclusive"));if(!e.imageTag&&!e.serviceImageTags)return l(new Error("rollback mode requires --image-tag <tag> or serviceImageTags naming the image(s) to roll to"))}if(e?.serviceImageTags!==void 0){if(e.serviceName)return l(new Error("serviceImageTags already names its services \u2014 --service cannot be combined with it"));const i=Object.entries(e.serviceImageTags);if(i.length===0)return l(new Error("serviceImageTags must name at least one service"));for(const[a,r]of i){const s=ie.safeParse(r);if(!s.success)return l(new Error(`serviceImageTags.${a} is not a valid image tag: ${s.error.message}`))}}return v(void 0)}async function nt(e,i,a){const{callbacks:r,options:s}=e,h=Date.now(),E=s?.mode??(s?.imageTag||s?.serviceImageTags?"rollback":s?.deployOnly?"code-only":"full");if(E==="restart"){const t=new Error('mode "restart" is not a deploy \u2014 call the restart() entry point (fjall rollout) instead');return r.onError?.(t),l(t)}const B=Me(s);if(!B.success){const t=k(B.error.message),n=new Error(t);return r.onError?.(n),l(n)}const C=pe.buildDeploymentContext({deployType:"application",target:a.appName,path:a.path,region:i.awsProvider.getRegion(),callerIdentity:le(i.awsProvider.getAccountId()),...ue({orgConfig:e.orgConfig,identity:e.identity,skipOidc:e.options?.skipOidc})},{verbose:s?.verbose,infraOnly:s?.infraOnly},e.orgConfig),u=i.frameworkRegistry.resolve({appPath:a.path});let O;const w=E==="rollback";if(u&&!w){O=u.builder.plan({appPath:a.path},u.detection);const t=Ee(r),n=await u.builder.build(a.path,O,t,{skipBuild:s?.skipBuild,infraOnly:s?.infraOnly});if(!n.success){const o=new Error(k(n.error.message));return r.onError?.(o),l(o)}}if(E==="code-only"||w){r.onLog?.(w?s?.serviceImageTags?`Rollback mode \u2014 rolling ${Object.keys(s.serviceImageTags).length} service(s) to their release-recorded image tags`:`Rollback mode \u2014 rolling to image tag ${s?.imageTag}`:"Deploy-only mode \u2014 skipping infrastructure pipeline","info");const t=Y(a.path,"cdk.out"),n=ce(t),o=n.length>0,p=u?.detection.hasDockerfile===!0,d=o||p;N.debug(P,"Deploy-only branch entered",{mode:E,imageTag:s?.imageTag,serviceImageTags:s?.serviceImageTags,appName:a.appName,appPath:a.path,cdkOutPath:t,dockerProviderAvailable:e.dockerProvider!==void 0,builderName:u?.builder.name,hasDockerfileFromManifest:o,hasDockerfileFromDisk:p,hasDockerfile:d,manifestDockerServiceCount:n.length,manifestDockerPaths:n.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 f={};if(!w&&e.dockerProvider!==void 0&&d){N.debug(P,"Running Docker build before code-only deploy",{source:o?"manifest":"disk"});const y=await Se(e,i,a,r);if(!y.success)return l(y.error);f=y.data.contentHashTagsByService}else N.debug(P,"Skipping Docker build",{reason:w?"rollback":e.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});return Ne(e,i,a,f)}r.onLog?.("Analysing infrastructure\u2026","info");const A=await fe(a,i,C,r);if(!A.success){const t=new Error(k(A.error.message));return r.onError?.(t),l(t)}const c=A.data;try{await r.onDetectionComplete?.({...c,builderName:u?.builder.name??"unknown"})}catch(t){const n=t instanceof Error?t.message:String(t),o=new Error(k(n));return r.onError?.(o),l(o)}const Z={deploymentType:"application",operation:"deploy",deployOnly:!1,infraOnly:s?.infraOnly??!1,hasDockerfile:c.hasDockerfile,pattern:c.pattern,resources:c.resources,...u&&{builderName:u.builder.name}},L=ke.getSteps(Z),_=L.findIndex(t=>t.id===R.TARGET_READINESS),j=await be(r,{stepId:R.TARGET_READINESS,stepName:me.TARGET_READINESS,..._>=0&&{stepIndex:_,totalSteps:L.length}},async()=>{if(s?.skipReadinessCheck)return r.onLog?.("Skipping target readiness check (--skip-readiness-check)","warn"),{kind:"skipped",data:void 0};const t=i.awsProvider.getAccountId(),n=e.orgConfig?.providerAccounts.find(f=>f.id===t)?.name,o=i.awsProvider.getCredentials(),p=he(e.orgConfig),d=await ye({cloudFormation:i.awsProvider.getClient(ne),ec2:new oe({region:p,...o!==void 0&&{credentials:o}})},{id:t,...n!==void 0&&{name:n}},i.awsProvider.getRegion(),e.orgConfig,e.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))}},e.abortSignal);if(!j.success)return j;const g=O?O.deployOrder:de({pattern:c.pattern,resources:c.resources}),b=g.length,H=c.hasDockerfile&&e.dockerProvider!==void 0&&g.includes(S.COMPUTE)&&s?.infraOnly!==!0;if(!c.hasDifferences&&!s?.force&&!H){r.onLog?.("No infrastructure changes detected","info"),r.onLog?.(s?.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 t=c.hasDockerfile&&e.dockerProvider!==void 0&&g.includes(S.COMPUTE);for(let o=0;o<g.length;o++){const p=g[o];t&&p===S.COMPUTE&&(r.onStepStart?.(R.DOCKER_OPERATIONS,X),r.onStepComplete?.(R.DOCKER_OPERATIONS,X,"skipped"));const d=V(p,"deploy"),f=Q(p,"deploy");r.onStepStart?.(d,f,o,b),r.onStepComplete?.(d,f,"skipped",o,b)}const n=await i.stackService.resolveWebsiteUrl(a.appName);return v({target:a.appName,deploymentType:"application",outputs:n?{websiteUrl:n}:void 0,noChanges:!0,artefacts:[],durationMs:Date.now()-h})}if(e.approvalGate!==void 0||s?.approvalToken!==void 0&&s.approvalToken!==""||s?.planOnly===!0){const t=Y(a.path,"cdk.out"),n=await Pe({approvalGate:e.approvalGate,options:s??{},target:a.appName,cdkOutPath:t,detection:{stackChanges:c.stackChanges,currentHashes:c.currentHashes},cfnService:i.cfnService,callbacks:r});if(!n.success)return n;const o=n.data;if(o.kind==="rejected")return r.onLog?.(`Deploy rejected: ${k(o.reason)}`,"warn"),v({target:a.appName,deploymentType:"application",rejected:!0,artefacts:[],...o.planDetail!==void 0?{planDetail:o.planDetail}:{},durationMs:Date.now()-h});if(o.kind==="awaiting")return v({target:a.appName,deploymentType:"application",awaitingApproval:!0,approvalToken:o.approvalToken,assemblyDigest:o.assemblyDigest,planDetail:o.planDetail,artefacts:[],durationMs:Date.now()-h})}const U=await ge(i,C,r);if(!U.success)return U;const G=J(a.appName,S.DATABASE),M=g.includes(S.DATABASE)&&((c.stackChanges.get(G)??!0)||s?.force===!0),ee=M?await Ce(i.cfnService,a.appName):void 0,te=M?await Oe(i.ecsResolver,i.ecsService,a.appName):void 0,T={},D=new Map;let $;const I=async()=>{!M||!D.has(G)||await Ie({appName:a.appName,connectionOutputsBefore:ee,serviceTaskDefinitionsBefore:te,services:{cfnService:i.cfnService,ecsService:i.ecsService,ecsResolver:i.ecsResolver},callbacks:r,...e.confirmDatabaseEndpointRestart!==void 0?{confirmRestart:e.confirmDatabaseEndpointRestart}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})};for(let t=0;t<g.length;t++){const n=g[t],o=J(a.appName,n),p=V(n,"deploy"),d=Q(n,"deploy"),f=c.stackChanges.get(o)??!0,y=n===S.COMPUTE&&H;if(!f&&!s?.force&&!y){r.onStepStart?.(p,d,t,b),r.onLog?.(`Skipping ${n} \u2014 no changes detected`,"info"),r.onStepComplete?.(p,d,"skipped",t,b);continue}const x=we(g,t,c.stackChanges,a.appName,s?.force);if(x.length>=2){const z=await Te(x,a,i,C,r,t,b,c,T,D);if(!z.success)return await I(),l(z.error);t+=x.length-1;continue}const m=await ve(n,e,i,a,r,c.hasDockerfile);if(m!==null&&!m.success)return await I(),l(m.error);m!==null&&m.success&&($=m.data);const ae=m!==null&&m.success?m.data.contentHashTagsByService:{},K=Ae(ae),W=await De(n,i,C,r,t,b,T,K!==void 0?{parameters:K}:void 0);if(!W.success)return await I(),l(W.error);const q=c.currentHashes.get(o);q&&D.set(o,q)}if(await I(),D.size>0){const t=await i.hashService.updateStateAfterDeploy(a.path,D);if(!t.success){const n=k(t.error.message);N.debug(P,"Failed to update state file",{error:n}),r.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy unchanged stacks: ${n}`,"warn")}}const F=await i.stackService.resolveWebsiteUrl(a.appName);F&&(T.websiteUrl=F);const re=await Re(i,a.appName,$,r);return v({target:a.appName,deploymentType:"application",outputs:Object.keys(T).length>0?T:void 0,artefacts:re,durationMs:Date.now()-h})}export{nt as deployApplication};
|
|
1
|
+
import{join as ie}from"path";import{CloudFormationClient as he}from"@aws-sdk/client-cloudformation";import{EC2Client as Se}from"@aws-sdk/client-ec2";import{success as E,failure as d}from"@fjall/generator";import{logger as M}from"@fjall/util/logger";import{ImageTagSchema as be,getErrorMessage as se,imageTagParameterName as we,maskSensitiveOutput as y}from"@fjall/util";import{parseDockerServicesFromManifest as Te}from"@fjall/util/manifest";import{stubCallerIdentity as Ne}from"../../types/deployment/index.js";import{getApplicationDeployOrder as Ee,getApplicationStackName as I,getApplicationStepName as ce,getApplicationStepId as le,APPLICATION_STACKS as P}from"../../types/operations.js";import{CdkContextBuilder as Pe}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as ve,bootstrapOrFail as De}from"../contextHelpers.js";import{runDetectionPipeline as Re}from"./detectionPipeline.js";import{STEP_IDS as G,STEP_NAMES as Ie}from"../../types/stepDefinitions.js";import{StepRegistry as Ce}from"../../steps/stepRegistry.js";import{checkTargetReadiness as Oe}from"../../aws/targetReadiness.js";import{cascadeHomeRegion as Ae}from"../organisation/cascadeHelpers.js";import{withStepLifecycle as Le}from"../stepLifecycle.js";import{DOCKER_BUILD_STEP_NAME as de,runDockerBuild as Me}from"./dockerBuildHelper.js";import{getParallelPhase2Stacks as Fe,getStackNamesToDeploy as $e,deployParallelPhase as xe,deployStackSequential as Be,runDockerPreCompute as je,createBuildCallbacks as Ge}from"./applicationDeployHelpers.js";import{captureDatabaseConnectionOutputs as _e,captureServiceTaskDefinitions as Ue,reconcileDatabaseEndpointChange as He}from"./databaseEndpointReconcile.js";import{deployCodeOnly as Ke}from"./codeOnlyDeploy.js";import{checkDeployPathStackAvailability as ue}from"../activeDeploymentGuard.js";import{assertLeaseBeforeStack as _}from"./leaseGate.js";import{collectFullDeployArtefacts as We}from"./artefactCollection.js";import{CfnRegistryService as qe}from"../../services/infrastructure/CfnRegistryService.js";import{ChangeSetProbe as ze}from"../../services/infrastructure/changeSetProbe.js";import{runApprovalGate as Je}from"./approvalGate.js";import{buildDestructionTicket as Ye,runDestructionGate as Qe}from"./destructionGate.js";import{DESTRUCTION_RESUME_TTL_MS as Ve,signApprovalToken as Xe}from"./plan/approvalToken.js";import{computeAssemblyDigest as Ze}from"./plan/assemblyDigest.js";import{createMemoisedPlanLoader as et}from"./plan/loadDeployPlan.js";import{runDriftPreFlightGate as tt}from"../drift/preFlightGate.js";import{completeForgetAfterDeploy as rt}from"../remediation/forgetResource.js";import{composePostFailureRepairOffer as pe}from"../remediation/postFailureRepairOffer.js";import{renderPinReportLines as at,runPinRemediation as nt}from"../remediation/pinRemediation.js";import{runRecreatePreFlight as ot}from"../remediation/recreatePreFlight.js";import{runRecreateSurgery as it}from"../remediation/recreateResource.js";const F="applicationDeploy";function st(e){const a=Object.entries(e);if(a.length===0)return;const n={};for(const[t,i]of a){const h=we(t);n[h]=i}return n}function ct(e){if(e?.mode!==void 0&&e.mode!=="rollback"){if(e.imageTag)return d(new Error(`--image-tag requires rollback mode (explicit mode '${e.mode}' was supplied alongside an image tag)`));if(e.serviceImageTags)return d(new Error(`serviceImageTags requires rollback mode (explicit mode '${e.mode}' was supplied alongside serviceImageTags)`))}if(e?.mode==="rollback"){if(e.imageTag&&e.serviceImageTags)return d(new Error("rollback mode accepts exactly one of --image-tag or serviceImageTags \u2014 they are mutually exclusive"));if(!e.imageTag&&!e.serviceImageTags)return d(new Error("rollback mode requires --image-tag <tag> or serviceImageTags naming the image(s) to roll to"))}if(e?.serviceImageTags!==void 0){if(e.serviceName)return d(new Error("serviceImageTags already names its services \u2014 --service cannot be combined with it"));const a=Object.entries(e.serviceImageTags);if(a.length===0)return d(new Error("serviceImageTags must name at least one service"));for(const[n,t]of a){const i=be.safeParse(t);if(!i.success)return d(new Error(`serviceImageTags.${n} is not a valid image tag: ${i.error.message}`))}}return E(void 0)}function ge(e,a,n){if(e.onStackTargets===void 0||n.length===0)return;const t=a.awsProvider.getAccountId(),i=a.awsProvider.getRegion();e.onStackTargets(n.map(h=>({stackName:h,...t!==""?{accountId:t}:{},...i!==""?{region:i}:{}})))}async function qt(e,a,n){const{callbacks:t,options:i}=e,h=Date.now(),$=i?.mode??(i?.imageTag||i?.serviceImageTags?"rollback":i?.deployOnly?"code-only":"full");if($==="restart"){const r=new Error('mode "restart" is not a deploy \u2014 call the restart() entry point (fjall rollout) instead');return t.onError?.(r),d(r)}const J=ct(i);if(!J.success){const r=y(J.error.message),o=new Error(r);return t.onError?.(o),d(o)}const x=Pe.buildDeploymentContext({deployType:"application",target:n.appName,path:n.path,region:a.awsProvider.getRegion(),callerIdentity:Ne(a.awsProvider.getAccountId()),...ve({orgConfig:e.orgConfig,identity:e.identity,skipOidc:e.options?.skipOidc})},{verbose:i?.verbose,infraOnly:i?.infraOnly},e.orgConfig),b=a.frameworkRegistry.resolve({appPath:n.path});let B;const C=$==="rollback";if(b&&!C){B=b.builder.plan({appPath:n.path},b.detection);const r=Ge(t),o=await b.builder.build(n.path,B,r,{skipBuild:i?.skipBuild,infraOnly:i?.infraOnly});if(!o.success){const s=new Error(y(o.error.message));return t.onError?.(s),d(s)}}if($==="code-only"||C){t.onLog?.(C?i?.serviceImageTags?`Rollback mode \u2014 rolling ${Object.keys(i.serviceImageTags).length} service(s) to their release-recorded image tags`:`Rollback mode \u2014 rolling to image tag ${i?.imageTag}`:"Deploy-only mode \u2014 skipping infrastructure pipeline","info");const r=ie(n.path,"cdk.out"),o=Te(r),s=o.length>0,p=b?.detection.hasDockerfile===!0,l=s||p;M.debug(F,"Deploy-only branch entered",{mode:$,imageTag:i?.imageTag,serviceImageTags:i?.serviceImageTags,appName:n.appName,appPath:n.path,cdkOutPath:r,dockerProviderAvailable:e.dockerProvider!==void 0,builderName:b?.builder.name,hasDockerfileFromManifest:s,hasDockerfileFromDisk:p,hasDockerfile:l,manifestDockerServiceCount:o.length,manifestDockerPaths:o.map(m=>m.docker.path)}),!s&&!p&&t.onLog?.("No Dockerfile detected via manifest or appPath \u2014 skipping Docker build. If this app uses a cross-repo Dockerfile, ensure a full deploy has run first to populate cdk.out/fjall-manifest.json.","warn");const u=await ue([I(n.appName,P.COMPUTE)],a.cfnService,e.abortSignal);if(!u.success)return t.onError?.(u.error),d(u.error);ge(t,a,[I(n.appName,P.COMPUTE)]);let k={};if(!C&&e.dockerProvider!==void 0&&l){M.debug(F,"Running Docker build before code-only deploy",{source:s?"manifest":"disk"});const m=await Me(e,a,n,t);if(!m.success)return d(m.error);k=m.data.contentHashTagsByService}else M.debug(F,"Skipping Docker build",{reason:C?"rollback":e.dockerProvider===void 0?"no dockerProvider":"no Dockerfile detected"});const c=await _(e.assertLeaseForStack,I(n.appName,P.COMPUTE),e.abortSignal);return c!==void 0?(t.onError?.(c),d(c)):Ke(e,a,n,k)}t.onLog?.("Analysing infrastructure\u2026","info");const U=await Re(n,a,x,t);if(!U.success){const r=new Error(y(U.error.message));return t.onError?.(r),d(r)}const f=U.data;try{await t.onDetectionComplete?.({...f,builderName:b?.builder.name??"unknown"})}catch(r){const o=new Error(y(se(r)));return t.onError?.(o),d(o)}const fe={deploymentType:"application",operation:"deploy",deployOnly:!1,infraOnly:i?.infraOnly??!1,hasDockerfile:f.hasDockerfile,pattern:f.pattern,resources:f.resources,...b&&{builderName:b.builder.name}},Y=Ce.getSteps(fe),Q=Y.findIndex(r=>r.id===G.TARGET_READINESS),V=await Le(t,{stepId:G.TARGET_READINESS,stepName:Ie.TARGET_READINESS,...Q>=0&&{stepIndex:Q,totalSteps:Y.length}},async()=>{if(i?.skipReadinessCheck)return t.onLog?.("Skipping target readiness check (--skip-readiness-check)","warn"),{kind:"skipped",data:void 0};const r=a.awsProvider.getAccountId(),o=e.orgConfig?.providerAccounts.find(u=>u.id===r)?.name,s=a.awsProvider.getCredentials(),p=Ae(e.orgConfig),l=await Oe({cloudFormation:a.awsProvider.getClient(he),ec2:new Se({region:p,...s!==void 0&&{credentials:s}})},{id:r,...o!==void 0&&{name:o}},a.awsProvider.getRegion(),e.orgConfig,e.abortSignal);return l.success?l.data.ready?{kind:"completed",data:void 0}:{kind:"error",error:new Error(y(l.data.advisory))}:{kind:"error",error:new Error(y(l.error.message))}},e.abortSignal);if(!V.success)return V;const w=B?B.deployOrder:Ee({pattern:f.pattern,resources:f.resources}),v=w.length,X=f.hasDockerfile&&e.dockerProvider!==void 0&&w.includes(P.COMPUTE)&&i?.infraOnly!==!0;if(!f.hasDifferences&&!i?.force&&!X){t.onLog?.("No infrastructure changes detected","info"),t.onLog?.(i?.infraOnly===!0?"Infrastructure-only mode \u2014 application code was not rebuilt. Run with --deploy-only to deploy code changes.":"Nothing to deploy. Run with --deploy-only to redeploy application code.","info");const r=f.hasDockerfile&&e.dockerProvider!==void 0&&w.includes(P.COMPUTE);for(let s=0;s<w.length;s++){const p=w[s];r&&p===P.COMPUTE&&(t.onStepStart?.(G.DOCKER_OPERATIONS,de),t.onStepComplete?.(G.DOCKER_OPERATIONS,de,"skipped"));const l=le(p,"deploy"),u=ce(p,"deploy");t.onStepStart?.(l,u,s,v),t.onStepComplete?.(l,u,"skipped",s,v)}const o=await a.stackService.resolveWebsiteUrl(n.appName);return E({target:n.appName,deploymentType:"application",outputs:o?{websiteUrl:o}:void 0,noChanges:!0,artefacts:[],durationMs:Date.now()-h})}const H=ie(n.path,"cdk.out"),N=$e({deployOrder:w,stackChanges:f.stackChanges,appName:n.appName,force:i?.force===!0,computeBuildPossible:X});ge(t,a,[...N]);const K=await ue([...N],a.cfnService,e.abortSignal);if(!K.success)return t.onError?.(K.error),d(K.error);const Z=Ze(f.currentHashes),W=et({cfnService:a.cfnService,cdkOutPath:H,changedStacks:[...N],assemblyDigest:Z,registry:new qe(a.awsProvider),changeSetProbe:new ze(a.awsProvider),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}),q=await tt(a.awsProvider,{stackNames:[...N],cdkOutPath:H,cfnService:a.cfnService,callbacks:t,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(q.kind==="blocked"){const r=new Error(q.message);return t.onError?.(r),d(r)}const ee=q.verifiedAbsentByNecessity??[],te=e.approvalGate!==void 0||i?.approvalToken!==void 0&&i.approvalToken!==""||i?.planOnly===!0;if(te){const r=await Je({approvalGate:e.approvalGate,options:i??{},target:n.appName,cdkOutPath:H,detection:{stackChanges:f.stackChanges,currentHashes:f.currentHashes},cfnService:a.cfnService,loadPlan:W,callbacks:t,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!r.success)return r;const o=r.data;if(o.kind==="rejected")return t.onLog?.(`Deploy rejected: ${y(o.reason)}`,"warn"),E({target:n.appName,deploymentType:"application",rejected:!0,artefacts:[],...o.planDetail!==void 0?{planDetail:o.planDetail}:{},durationMs:Date.now()-h});if(o.kind==="awaiting"){const s=await W(),p=s.success?Ye(s.data,ee):void 0;return E({target:n.appName,deploymentType:"application",awaitingApproval:!0,approvalToken:o.approvalToken,assemblyDigest:o.assemblyDigest,planDetail:o.planDetail,...p!==void 0?{destructionTicket:p}:{},artefacts:[],durationMs:Date.now()-h})}}const T=await Qe({loadPlan:W,options:i??{},target:n.appName,callbacks:t,verifiedAbsentByNecessity:ee,...e.destructionConsentGate!==void 0?{consentGate:e.destructionConsentGate}:{}});if(!T.success)return t.onError?.(T.error),d(T.error);if(T.data.kind==="withheld"){const r=te?Xe({assemblyDigest:Z,ttlMs:Ve}):void 0;return E({target:n.appName,deploymentType:"application",destructionPending:!0,destructionTicket:T.data.ticket,destructionOutcome:T.data.outcome,destructionHaltReasons:T.data.reasons,...r!==void 0?{destructionResumeToken:r.token,destructionResumeExpiresAt:r.expiresAt}:{},artefacts:[],durationMs:Date.now()-h})}if(T.data.kind==="proceed"){const{ticket:r,verbsByPhysicalName:o}=T.data,s=r.resources.filter(l=>o.get(l.physicalName)==="pin");if(s.length>0){t.onLog?.(`Pin remediation for ${n.appName}: probing live value(s) for ${s.length} consented pin(s) \u2014 deploy stopped for config write-back; nothing was applied`,"warn");const l=await nt(a.awsProvider,{targets:s.map(u=>({stack:u.stack,logicalId:u.logicalId,resourceType:u.resourceType,physicalName:u.physicalName,offendingProperties:u.offendingProperties})),...e.infrastructureSource!==void 0?{infrastructureContent:e.infrastructureSource.content,infrastructureFilePath:e.infrastructureSource.filePath}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});for(const u of at(l))t.onLog?.(u,"warn");return s.length<r.resources.length&&t.onLog?.("Remaining consented change(s) were not applied \u2014 the config write-back changes the plan, so they re-consent on the next deploy","warn"),E({target:n.appName,deploymentType:"application",pinPending:!0,pinReport:l,pinDeferredConsents:r.resources.length-s.length,destructionTicket:r,artefacts:[],durationMs:Date.now()-h})}const p=r.resources.filter(l=>l.finding==="pinned-name-replacement"&&o.get(l.physicalName)==="recreate");if(p.length>0){const l=new Map;for(const c of p){const m=l.get(c.stack)??[];m.push(c),l.set(c.stack,m)}const u=await ot(a.awsProvider,{stacks:[...l].map(([c,m])=>({stackName:c,targets:m.map(S=>({logicalId:S.logicalId,resourceType:S.resourceType,physicalId:S.physicalName}))})),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!u.success){const c=new Error(y(u.error.message));return t.onError?.(c),d(c)}const k=[];for(const[c,m]of l){const S=await _(e.assertLeaseForStack,c,e.abortSignal,"destroy");if(S!==void 0)return k.length>0&&t.onLog?.(`Surgery already completed on ${k.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`,"warn"),t.onError?.(S),d(S);const R=m.map(g=>g.physicalName);t.onLog?.(`Recreate remediation for ${n.appName}: orchestrating the pinned-name replacement of ${R.join(", ")} on ${c}`,"warn");const L=await it(a.awsProvider,{stackName:c,targets:m.map(g=>({logicalId:g.logicalId,resourceType:g.resourceType,physicalId:g.physicalName})),appName:n.appName,consentedPhysicalNames:R,doneJournalPolicy:"archive-and-restart",plannedReplacementLogicalIds:m.map(g=>g.logicalId),onProgress:g=>t.onLog?.(g,"info"),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(!L.success){const g=k.length>0?` Surgery already completed on ${k.join("; ")} \u2014 those resources were removed and re-running the deploy resumes their journals and re-creates them.`:"",j=new Error(y(`${L.error.message}${g}`));return t.onError?.(j),d(j)}k.push(`${c} (${R.join(", ")})`)}}}const re=await De(a,x,t);if(!re.success)return re;const ae=I(n.appName,P.DATABASE),z=N.has(ae),me=z?await _e(a.cfnService,n.appName):void 0,ke=z?await Ue(a.ecsResolver,a.ecsService,n.appName):void 0,O={},A=new Map;let ne;const D=async()=>{!z||!A.has(ae)||await He({appName:n.appName,connectionOutputsBefore:me,serviceTaskDefinitionsBefore:ke,services:{cfnService:a.cfnService,ecsService:a.ecsService,ecsResolver:a.ecsResolver},callbacks:t,...e.confirmDatabaseEndpointRestart!==void 0?{confirmRestart:e.confirmDatabaseEndpointRestart}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})};for(let r=0;r<w.length;r++){const o=w[r],s=I(n.appName,o),p=le(o,"deploy"),l=ce(o,"deploy");if(!N.has(s)){t.onStepStart?.(p,l,r,v),t.onLog?.(`Skipping ${o} \u2014 no changes detected`,"info"),t.onStepComplete?.(p,l,"skipped",r,v);continue}const u=await _(e.assertLeaseForStack,s,e.abortSignal);if(u!==void 0)return await D(),t.onError?.(u),d(u);const k=Fe(w,r,n.appName,N);if(k.length>=2){const g=await xe(k,n,a,x,t,r,v,f,O,A);if(!g.success)return await D(),d(await pe(a.awsProvider,{stackNames:k.map(j=>I(n.appName,j)),appName:n.appName,originalError:g.error,cfnService:a.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));r+=k.length-1;continue}const c=await je(o,e,a,n,t,f.hasDockerfile);if(c!==null&&!c.success)return await D(),d(c.error);c!==null&&c.success&&(ne=c.data);const m=c!==null&&c.success?c.data.contentHashTagsByService:{},S=st(m);if(c!==null){const g=await _(e.assertLeaseForStack,s,e.abortSignal);if(g!==void 0)return await D(),t.onError?.(g),d(g)}const R=await Be(o,a,x,t,r,v,O,S!==void 0?{parameters:S}:void 0);if(!R.success)return await D(),d(await pe(a.awsProvider,{stackNames:[s],appName:n.appName,originalError:R.error,cfnService:a.cfnService,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}));const L=f.currentHashes.get(s);L&&A.set(s,L)}if(await D(),A.size>0){const r=await a.hashService.updateStateAfterDeploy(n.path,A);if(!r.success){const o=y(r.error.message);M.debug(F,"Failed to update state file",{error:o}),t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy unchanged stacks: ${o}`,"warn")}}const oe=await a.stackService.resolveWebsiteUrl(n.appName);oe&&(O.websiteUrl=oe);const ye=await We(a,n.appName,ne,t);return await rt(a.awsProvider,{stackNames:[...N],appName:n.appName,onLog:(r,o)=>t.onLog?.(r,o),...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}}).catch(r=>{M.debug(F,"Forget completion sweep failed",{error:y(se(r))})}),E({target:n.appName,deploymentType:"application",outputs:Object.keys(O).length>0?O:void 0,artefacts:ye,durationMs:Date.now()-h})}export{qt as deployApplication,ge as emitStackTargets};
|
|
@@ -7,12 +7,29 @@ import type { DeployServices } from "../serviceFactory.js";
|
|
|
7
7
|
import type { DetectionResult } from "./detectionPipeline.js";
|
|
8
8
|
import { type DockerBuildProducts } from "./dockerBuildHelper.js";
|
|
9
9
|
import type { BuildCallbacks } from "../../types/frameworkBuilder.js";
|
|
10
|
+
/**
|
|
11
|
+
* Single source of the deploy loop's stack-inclusion predicate: a stack
|
|
12
|
+
* deploys when its template hash changed, when `--force` overrides the hash
|
|
13
|
+
* gate, or when Compute must run for an image build. The destruction gate
|
|
14
|
+
* MUST diff exactly this set — a second derivation lets a hash-unchanged
|
|
15
|
+
* stack deploy under `--force` without the gate ever seeing its live diff
|
|
16
|
+
* (local v1 templates vs a live stack moved to v2 out-of-band: hashes match,
|
|
17
|
+
* the stack escapes the plan, and the forced deploy applies a destructive
|
|
18
|
+
* v2→v1 revert with no ticket).
|
|
19
|
+
*/
|
|
20
|
+
export declare function getStackNamesToDeploy(params: {
|
|
21
|
+
deployOrder: readonly ApplicationStack[];
|
|
22
|
+
stackChanges: ReadonlyMap<string, boolean>;
|
|
23
|
+
appName: string;
|
|
24
|
+
force: boolean;
|
|
25
|
+
computeBuildPossible: boolean;
|
|
26
|
+
}): ReadonlySet<string>;
|
|
10
27
|
/**
|
|
11
28
|
* Identify Phase 2 stacks (Storage, Messaging, Database) that are consecutive
|
|
12
|
-
* in the deploy order and
|
|
13
|
-
* only if there are at least 2.
|
|
29
|
+
* in the deploy order and included in the to-deploy set. Returns them for
|
|
30
|
+
* parallel deployment only if there are at least 2.
|
|
14
31
|
*/
|
|
15
|
-
export declare function getParallelPhase2Stacks(deployOrder: readonly ApplicationStack[], currentIndex: number,
|
|
32
|
+
export declare function getParallelPhase2Stacks(deployOrder: readonly ApplicationStack[], currentIndex: number, appName: string, stackNamesToDeploy: ReadonlySet<string>): ApplicationStack[];
|
|
16
33
|
/**
|
|
17
34
|
* Deploy a group of stacks in parallel, reporting step events for each.
|
|
18
35
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{success as h,failure as
|
|
2
|
-
`);return
|
|
1
|
+
import{success as h,failure as m}from"@fjall/generator";import{maskSensitiveOutput as w}from"@fjall/util";import{isAborted as R}from"../../aws/organisations/types.js";import{APPLICATION_STACKS as B,getApplicationStackName as y,getApplicationStepName as N,getApplicationStepId as C,PARALLEL_DEPLOY_GROUPS as k}from"../../types/operations.js";import{emitProgress as L}from"../../services/supporting/helpers.js";import{forwardOutput as x,forwardResourceProgress as I}from"../contextHelpers.js";import{emptyBuildProducts as _,runDockerBuild as D}from"./dockerBuildHelper.js";import{runWelcomeImageSetup as $}from"./welcomeImageHelper.js";import{withStepLifecycle as j}from"../stepLifecycle.js";function W(e){const r=new Set;for(const n of e.deployOrder){const u=y(e.appName,n),o=e.stackChanges.get(u)??!0,a=n===B.COMPUTE&&e.computeBuildPossible;(o||e.force||a)&&r.add(u)}return r}function Y(e,r,n,u){const o=new Set(k.PHASE_2.stacks),a=[];for(let s=r;s<e.length&&o.has(e[s]);s++)a.push(e[s]);if(a.length<2)return[];const p=a.filter(s=>u.has(y(n,s)));return p.length>=2?p:[]}async function z(e,r,n,u,o,a,p,s,g,S){for(let t=0;t<e.length;t++){const i=e[t],c=C(i,"deploy"),d=N(i,"deploy");o.onStepStart?.(c,d,a+t,p)}L(o,"Deploying infrastructure in parallel\u2026"),o.onParallelPhaseStart?.(e,"Storage and database resources (parallel)");const l=await n.stackService.deployStacksInParallel(e,u,{onOutput:x(o),onResourceProgress:(t,i)=>{o.onResourceProgress?.(t),i&&o.onParallelStackResourceProgress?.(i,t)},onStackComplete:(t,i,c,d)=>{const P=C(t,"deploy"),A=N(t,"deploy"),E=a+e.indexOf(t),O=R(n.abortSignal);o.onStepComplete?.(P,A,i?"completed":O?"cancelled":"error",E,p),!i&&d&&!O&&o.onError?.(new Error(w(d.message)))}});if(!l.success)return o.onParallelPhaseComplete?.([]),m(l.error);const f=l.data.filter(t=>!t.success);if(o.onParallelPhaseComplete?.(l.data.map(t=>({stack:t.stack,success:t.success,error:t.error}))),f.length>0){const t=f.map(c=>c.stack).join(", "),i=f.map(c=>`${c.stack}: ${w(c.error?.message??"Unknown error")}`).join(`
|
|
2
|
+
`);return m(new Error(`Failed to deploy stacks: ${t}
|
|
3
3
|
|
|
4
|
-
${
|
|
4
|
+
${i}`))}for(const t of l.data){if(t.success&&t.outputs)for(const[d,P]of Object.entries(t.outputs))g[d]=String(P);const i=y(r.appName,t.stack),c=s.currentHashes.get(i);c&&S.set(i,c)}return h(void 0)}async function J(e,r,n,u,o,a,p,s){const g=C(e,"deploy"),S=N(e,"deploy");return j(u,{stepId:g,stepName:S,stepIndex:o,totalSteps:a},async()=>{const l=await r.stackService.deployStack(e,n,{onOutput:x(u),onResourceProgress:I(u)},s?.parameters!==void 0?{parameters:s.parameters}:void 0);if(!l.success)return{kind:"error",error:l.error};if(l.data.outputs)for(const[f,t]of Object.entries(l.data.outputs))p[f]=String(t);return{kind:"completed",data:void 0}},r.abortSignal)}async function Q(e,r,n,u,o,a){if(e!==B.COMPUTE||!r.dockerProvider)return null;if(a){const s=await D(r,n,u,o);return s.success?h(s.data):m(s.error)}const p=await $(r,n,u,o);return p.success?h(_()):m(p.error)}function V(e){return{onBuildStart:r=>{e.onOpenNextBuildStart?.(),e.onLog?.(`${r} build started`,"info")},onBuildProgress:(r,n)=>{e.onOpenNextProgress?.(n)},onBuildComplete:r=>{e.onOpenNextBuildComplete?.(),e.onLog?.(`${r} build complete`,"info")},onBuildError:(r,n)=>{e.onOpenNextBuildError?.(n)}}}export{V as createBuildCallbacks,z as deployParallelPhase,J as deployStackSequential,Y as getParallelPhase2Stacks,W as getStackNamesToDeploy,Q as runDockerPreCompute};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as w,failure as
|
|
1
|
+
import{success as w,failure as N}from"@fjall/generator";import{getErrorMessage as S}from"@fjall/util";import{logger as y}from"@fjall/util/logger";import{getApplicationDestroyOrder as b,getApplicationStackName as x,getApplicationStepName as h,getApplicationStepId as P}from"../../types/operations.js";import{emitStackTargets as D}from"./applicationDeploy.js";import{stubCallerIdentity as L}from"../../types/deployment/index.js";import{deriveResourcesFromManifestStacks as R}from"../../types/detection/patternDetection.js";import{CdkContextBuilder as A}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as F}from"../contextHelpers.js";import{deleteStateFile as I,readStateFile as O}from"../../types/config/FjallState.js";async function J(a,s,r){const{callbacks:t}=a,k=Date.now(),m=s.frameworkRegistry.resolve({appPath:r.path})?.detection.pattern??null;let d;try{const e=await O(r.path);if(e!==null){const o=Object.keys(e.templateHashes);o.length>0&&(d=R(o))}}catch(e){y.debug("applicationDestroy","Could not read state file for resource detection",{error:S(e)}),t.onLog?.("Could not read state file for resource detection \u2014 falling back to pattern detection","warn")}const C=A.buildDeploymentContext({deployType:"application",target:r.appName,path:r.path,region:s.awsProvider.getRegion(),callerIdentity:L(s.awsProvider.getAccountId()),...F({orgConfig:a.orgConfig,identity:a.identity})},{verbose:a.options?.verbose},a.orgConfig),p=b({pattern:m,resources:d}),l=p.length;t.onLog?.(`Destroying ${r.appName} (${l} stacks, ${m??"standard"} pattern)`,"info"),D(t,s,p.map(e=>x(r.appName,e)));const f=[],u=[],g=await s.stackService.destroyAllStacks(C,{onOutput:e=>{t.onOutput?.(e)},onLog:t.onLog,onStackCleanupProgress:t.onStackCleanupProgress,onResourceProgress:(e,o)=>{t.onResourceProgress?.(e),o&&t.onParallelStackResourceProgress?.(o,e)},onStackStart:(e,o)=>{const n=P(e,"destroy"),i=h(e,"destroy"),c=p.indexOf(e);t.onStepStart?.(n,i,c,l)},onStackComplete:async(e,o)=>{const n=P(e,"destroy"),i=h(e,"destroy"),c=p.indexOf(e);o.success?o.data?.skipped?(u.push(o.data.stackName||e),t.onStepComplete?.(n,i,"skipped",c,l)):(f.push(o.data?.stackName||e),t.onStepComplete?.(n,i,"completed",c,l)):(t.onStepComplete?.(n,i,"error",c,l),t.onError?.(o.error))},onParallelPhaseStart:(e,o)=>{t.onLog?.(`Parallel phase: ${o}`,"info"),t.onParallelPhaseStart?.(e,o)},onParallelPhaseComplete:e=>{const o=e.filter(n=>!n.success);o.length>0&&t.onLog?.(`Parallel phase completed with ${o.length} failure(s)`,"warn"),t.onParallelPhaseComplete?.(e)}},d,a.abortSignal,a.assertLeaseForStack);if(!g.success)return t.onError?.(g.error),N(g.error);try{await I(r.path)}catch(e){y.debug("applicationDestroy","Failed to delete state file (non-critical)",{error:S(e)}),t.onLog?.("Failed to delete state file (non-critical)","warn")}return w({target:r.appName,deploymentType:"application",stacksDestroyed:f,skippedStacks:u,durationMs:Date.now()-k})}export{J as destroyApplication};
|
|
@@ -16,7 +16,9 @@ import { type Result } from "@fjall/generator";
|
|
|
16
16
|
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
17
17
|
import type { DeployOptions } from "../../types/params.js";
|
|
18
18
|
import type { ApprovalGate, ApprovalGatePlanDetail } from "../../types/approval.js";
|
|
19
|
-
import {
|
|
19
|
+
import type { CfnRegistrySummaryReader } from "../../services/infrastructure/CfnRegistryService.js";
|
|
20
|
+
import type { CfnTemplateReader } from "./plan/computeDeployPlan.js";
|
|
21
|
+
import { type DeployPlanLoader } from "./plan/loadDeployPlan.js";
|
|
20
22
|
export interface RunApprovalGateParams {
|
|
21
23
|
/** The surface resolver. Optional — a token-resume or plan-only run needs none. */
|
|
22
24
|
approvalGate?: ApprovalGate;
|
|
@@ -28,7 +30,19 @@ export interface RunApprovalGateParams {
|
|
|
28
30
|
currentHashes: ReadonlyMap<string, string>;
|
|
29
31
|
};
|
|
30
32
|
cfnService: CfnTemplateReader;
|
|
33
|
+
/**
|
|
34
|
+
* Registry oracle for replacement-verdict recomputation. Optional — without
|
|
35
|
+
* it plan rows keep raw cloudformation-diff verdicts.
|
|
36
|
+
*/
|
|
37
|
+
registry?: CfnRegistrySummaryReader;
|
|
38
|
+
/**
|
|
39
|
+
* Injected (memoised) plan supplier — applicationDeploy shares one loader
|
|
40
|
+
* between this gate and the unconditional destruction gate so a plan-gated
|
|
41
|
+
* deploy never diffs twice. Omitted, the gate builds its own.
|
|
42
|
+
*/
|
|
43
|
+
loadPlan?: DeployPlanLoader;
|
|
31
44
|
callbacks: DeployCallbacks;
|
|
45
|
+
abortSignal?: AbortSignal;
|
|
32
46
|
}
|
|
33
47
|
export type ApprovalGateOutcome = {
|
|
34
48
|
kind: "proceed";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{success as o,failure as g}from"@fjall/generator";import{computeAssemblyDigest as k}from"./plan/assemblyDigest.js";import{loadDeployPlan as v}from"./plan/loadDeployPlan.js";import{renderPlanLines as h}from"./plan/renderDeployPlan.js";import{signApprovalToken as y,verifyApprovalToken as A}from"./plan/approvalToken.js";function w(e){switch(e){case"superseded":return"the plan changed since it was approved (re-plan and approve again)";case"expired":return"the approval window expired";case"tampered":return"the approval signature did not verify";case"malformed":return"the approval token was malformed"}}function m(e,n,r){const i=n.changeCount===0&&r.length>0?`no resource changes against the live stacks; template hashes differ from the last recorded deploy for ${r.length} stack(s) (${r.join(", ")}) \u2014 applying will redeploy them`:n.summary;e.onLog?.(`Deploy plan: ${i}`,"info");for(const d of h(n))e.onLog?.(d,"info")}async function D(e){const n=k(e.detection.currentHashes),r=e.options.approvalToken;if(r!==void 0&&r!==""){const t=A({token:r,assemblyDigest:n});if(t.ok)return e.callbacks.onLog?.("Approval token verified against the current plan \u2014 proceeding","info"),o({kind:"proceed"});const u=w(t.reason);return e.callbacks.onLog?.(`Approval refused: ${u}`,"warn"),o({kind:"rejected",reason:u})}const i=[...e.detection.stackChanges.entries()].filter(([,t])=>t).map(([t])=>t),p=await(e.loadPlan??(()=>v({cfnService:e.cfnService,cdkOutPath:e.cdkOutPath,changedStacks:i,assemblyDigest:n,...e.registry!==void 0?{registry:e.registry}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})))();if(!p.success)return g(p.error);const s=p.data;m(e.callbacks,s,i);const c={hashDriftStacks:i,resourceChangeCount:s.changeCount,resourceChangedStacks:[...new Set(s.changes.map(t=>t.stack))]},a=y({assemblyDigest:n});if(e.options.autoApprove===!0)return e.callbacks.onLog?.("Plan auto-approved (--auto-approve)","info"),o({kind:"proceed"});if(e.options.planOnly===!0)return o({kind:"awaiting",approvalToken:a.token,assemblyDigest:n,expiresAt:a.expiresAt,planDetail:c});if(e.approvalGate===void 0)return o({kind:"awaiting",approvalToken:a.token,assemblyDigest:n,expiresAt:a.expiresAt,planDetail:c});const f={target:e.target,plan:s,approvalToken:a.token,expiresAt:a.expiresAt,hasDestructiveChanges:s.hasDestructiveChanges},l=await e.approvalGate.resolve(f);switch(l.kind){case"approved":return o({kind:"proceed"});case"rejected":return o({kind:"rejected",reason:l.reason,planDetail:c});case"suspended":return o({kind:"awaiting",approvalToken:l.approvalToken,assemblyDigest:n,expiresAt:a.expiresAt,planDetail:c})}}export{D as runApprovalGate};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unconditional destruction gate — runs at the read→mutate boundary of
|
|
3
|
+
* every application deploy, independent of the approval gate. No flag can
|
|
4
|
+
* disengage it: `autoApprove`, `skipConfirmation`, `force`, and approval
|
|
5
|
+
* tokens never satisfy it (drift-remediation plan § 1.2 / § 4).
|
|
6
|
+
*
|
|
7
|
+
* Ticketing is stateful/pinned-only (the § 9 ceremony-fatigue defence):
|
|
8
|
+
* - a replacement that loses data → replacement-required
|
|
9
|
+
* - a replacement of a custom-named
|
|
10
|
+
* resource (CloudFormation will REFUSE) → pinned-name-replacement
|
|
11
|
+
* - a delete of a stateful resource → data-loss-delete
|
|
12
|
+
* Stateless unpinned replacements and stateless deletes never ticket — the
|
|
13
|
+
* plan renderer and approval gate already surface them.
|
|
14
|
+
*
|
|
15
|
+
* Consent = exact, case-sensitive `{physicalName, verb}` pairs covering EVERY
|
|
16
|
+
* ticket resource. Surplus, misspelt, or wrong-verb consents fail closed with
|
|
17
|
+
* a per-name verdict table [G6]. `recreate` on unpinned findings executes as
|
|
18
|
+
* CloudFormation's native replacement / the consented delete; `recreate` on a
|
|
19
|
+
* pinned-name row runs the orchestrated snapshot-flip surgery
|
|
20
|
+
* (`runRecreateSurgery` — the 3f pre-bootstrap block); `pin` proceeds into
|
|
21
|
+
* the pre-bootstrap pin arm (live-value probe + codemod offer — the deploy
|
|
22
|
+
* stops for config write-back, nothing is applied). Verbs without an
|
|
23
|
+
* execution arm (`forget` deploy-side, `repair`) withhold with a clear
|
|
24
|
+
* message rather than burning a doomed run.
|
|
25
|
+
*/
|
|
26
|
+
import { type Result } from "@fjall/generator";
|
|
27
|
+
import type { DeployCallbacks } from "../../types/callbacks.js";
|
|
28
|
+
import type { DeployOptions } from "../../types/params.js";
|
|
29
|
+
import type { DestructionConsent, DestructionConsentGate, DestructionConsentReport, DestructionFinding, DestructionOutcome, DestructionTicket, DestructionTicketResource, RemediationVerb } from "../../types/destruction.js";
|
|
30
|
+
import type { DeployPlanLoader } from "./plan/loadDeployPlan.js";
|
|
31
|
+
import type { DeployPlan } from "./plan/types.js";
|
|
32
|
+
export declare function legalRemediationVerbs(finding: DestructionFinding): readonly RemediationVerb[];
|
|
33
|
+
export declare function executableRemediationVerbs(finding: DestructionFinding): readonly RemediationVerb[];
|
|
34
|
+
/**
|
|
35
|
+
* [G11]/[H7] The verbs a surface may ADVERTISE for one ticket row — the
|
|
36
|
+
* row's legal vocabulary reduced to exactly what the gates would let
|
|
37
|
+
* execute. Advertising anything wider sells a consent the execution check
|
|
38
|
+
* (`collectExecutionHalts` here, the mirrored check in `runDriftRepairGate`)
|
|
39
|
+
* is guaranteed to halt; the operator types the ceremony, then the gate
|
|
40
|
+
* withholds — a dead-end loop the menu itself caused. The filter mirrors the
|
|
41
|
+
* halt quantifiers exactly:
|
|
42
|
+
* - `abort` always advertises — a matched abort is an operator decline,
|
|
43
|
+
* never an execution halt;
|
|
44
|
+
* - every other verb must be in `executableRemediationVerbs(finding)`;
|
|
45
|
+
* - `pin` additionally needs `pinProbeCoverage` over the offending
|
|
46
|
+
* properties (the v1 probe table).
|
|
47
|
+
* The gate halts stay as the backstop for hand-typed consents; the
|
|
48
|
+
* executable set and probe coverage live here (deploy-core) so no surface
|
|
49
|
+
* re-derives them. Consumers: the CLI's `--remediate` re-run hint, the
|
|
50
|
+
* interactive ceremony's verb menu, the agent ticket blocks and the shared
|
|
51
|
+
* ticket-row renderer below.
|
|
52
|
+
*/
|
|
53
|
+
export declare function advertisableRemediationVerbs(resource: Pick<DestructionTicketResource, "verbs" | "finding" | "resourceType" | "offendingProperties">): RemediationVerb[];
|
|
54
|
+
/**
|
|
55
|
+
* `hash(assemblyDigest + canonical resource set)` — the TOCTOU binding for
|
|
56
|
+
* consents minted before apply time. Rows are keyed and sorted so the digest
|
|
57
|
+
* is independent of plan ordering; verbs are deliberately excluded (a verb
|
|
58
|
+
* vocabulary change must not invalidate a consent to the same resource set).
|
|
59
|
+
*/
|
|
60
|
+
export declare function computeTicketDigest(assemblyDigest: string, resources: ReadonlyArray<Pick<DestructionTicketResource, "stack" | "logicalId" | "resourceType" | "physicalName" | "finding">>): string;
|
|
61
|
+
/**
|
|
62
|
+
* Build the destruction ticket from an overlay-corrected plan. Returns
|
|
63
|
+
* undefined when the plan contains no ticketable rows (the common case).
|
|
64
|
+
* `physicalName` is the template-pinned literal when one exists; otherwise
|
|
65
|
+
* the `remediationPlaceholderPhysicalId` address — still an exact,
|
|
66
|
+
* unambiguous string the operator can copy from the rendered ticket, and
|
|
67
|
+
* shared-source with the drift-repair ticket's fallback so the same resource
|
|
68
|
+
* reads identically on both tickets in the blocked-deploy → repair flow.
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildDestructionTicket(plan: DeployPlan, verifiedAbsentByNecessity?: ReadonlyArray<{
|
|
71
|
+
stackName: string;
|
|
72
|
+
logicalId: string;
|
|
73
|
+
}>): DestructionTicket | undefined;
|
|
74
|
+
export interface EvaluateDestructionConsentsParams {
|
|
75
|
+
ticket: DestructionTicket;
|
|
76
|
+
consents: readonly DestructionConsent[];
|
|
77
|
+
/**
|
|
78
|
+
* The ticket digest the consents were minted against (webapp pre-auth /
|
|
79
|
+
* resumed runs). A mismatch with the freshly recomputed digest fail-closes
|
|
80
|
+
* every consent — the plan changed between mint and apply.
|
|
81
|
+
*/
|
|
82
|
+
consentDigest?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Exact-match consent evaluation [G6]. Case-sensitive; never normalised.
|
|
86
|
+
* Every supplied consent gets a verdict row; every uncovered ticket resource
|
|
87
|
+
* fails the gate closed.
|
|
88
|
+
*/
|
|
89
|
+
export declare function evaluateDestructionConsents(params: EvaluateDestructionConsentsParams): DestructionConsentReport;
|
|
90
|
+
/**
|
|
91
|
+
* Renderer for ticket rows — every surface renders these same lines. [H6]
|
|
92
|
+
* The verbs line advertises through `advertisableRemediationVerbs` at render
|
|
93
|
+
* time (the ticket itself — and its digest — keeps the full legal
|
|
94
|
+
* vocabulary): a raw `resource.verbs` here would contradict the G11-filtered
|
|
95
|
+
* re-run hint on the same card.
|
|
96
|
+
*/
|
|
97
|
+
export declare function renderDestructionTicketLines(ticket: DestructionTicket): string[];
|
|
98
|
+
/** Renderer for the per-name verdict table [G6]. */
|
|
99
|
+
export declare function renderConsentVerdictLines(report: DestructionConsentReport): string[];
|
|
100
|
+
export interface RunDestructionGateParams {
|
|
101
|
+
/** The memoised plan supplier shared with the approval gate. */
|
|
102
|
+
loadPlan: DeployPlanLoader;
|
|
103
|
+
options: DeployOptions;
|
|
104
|
+
target: string;
|
|
105
|
+
callbacks: DeployCallbacks;
|
|
106
|
+
/**
|
|
107
|
+
* Interactive ceremony resolver (from `DeployParams.destructionConsentGate`).
|
|
108
|
+
* Consulted ONLY when the ticket is unsatisfied and NO flag consents were
|
|
109
|
+
* supplied — explicit `--remediate` consents that fail keep failing loudly
|
|
110
|
+
* with the verdict table [G6], never silently re-prompt. The resolution is
|
|
111
|
+
* re-evaluated through `evaluateDestructionConsents` + the digest TOCTOU
|
|
112
|
+
* check, so evaluation never moves surface-side.
|
|
113
|
+
*/
|
|
114
|
+
consentGate?: DestructionConsentGate;
|
|
115
|
+
/** Flipless-by-necessity targets the drift pre-flight gate re-proved
|
|
116
|
+
* absent THIS deploy — their delete rows are suppressed from the ticket
|
|
117
|
+
* (see `buildDestructionTicket`). */
|
|
118
|
+
verifiedAbsentByNecessity?: ReadonlyArray<{
|
|
119
|
+
stackName: string;
|
|
120
|
+
logicalId: string;
|
|
121
|
+
}>;
|
|
122
|
+
}
|
|
123
|
+
export type DestructionGateOutcome =
|
|
124
|
+
/** No ticketable rows — the deploy proceeds untouched. */
|
|
125
|
+
{
|
|
126
|
+
kind: "clear";
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Every resource consented with an executable verb — proceed. The deploy
|
|
130
|
+
* branches on `verbsByPhysicalName`: any `pin` row diverts into the
|
|
131
|
+
* pre-bootstrap pin arm instead of applying the destructive change.
|
|
132
|
+
*/
|
|
133
|
+
| {
|
|
134
|
+
kind: "proceed";
|
|
135
|
+
ticket: DestructionTicket;
|
|
136
|
+
/** The consented verb per exact physical name (matched rows only). */
|
|
137
|
+
verbsByPhysicalName: ReadonlyMap<string, RemediationVerb>;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Consent withheld / incomplete / stale — nothing may be applied.
|
|
141
|
+
* `outcome` says WHY (surfaces must render each variant truthfully — a
|
|
142
|
+
* consented-but-not-executable user is never told to consent again);
|
|
143
|
+
* `reasons` carries the halt/decline lines. Both are identifier-only
|
|
144
|
+
* (physical names, verbs) — pre-masked by construction.
|
|
145
|
+
*/
|
|
146
|
+
| {
|
|
147
|
+
kind: "withheld";
|
|
148
|
+
outcome: DestructionOutcome;
|
|
149
|
+
ticket: DestructionTicket;
|
|
150
|
+
report: DestructionConsentReport;
|
|
151
|
+
reasons: string[];
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Run the unconditional destruction gate. Loads the (memoised) plan, builds
|
|
155
|
+
* the ticket, evaluates consents, and either clears the boundary or withholds
|
|
156
|
+
* the deploy — emitting the ticket and verdict table over the callbacks so
|
|
157
|
+
* every surface renders the same lines. Deliberately carries NO ready-to-run
|
|
158
|
+
* command [G5]; the CLI's human path appends its own re-run hint.
|
|
159
|
+
*
|
|
160
|
+
* Failure messages are pre-masked at construction — the sole failure path
|
|
161
|
+
* routes the plan error through `maskSensitiveOutput`, so callers may surface
|
|
162
|
+
* `error` to callback/log sinks directly.
|
|
163
|
+
*/
|
|
164
|
+
export declare function runDestructionGate(params: RunDestructionGateParams): Promise<Result<DestructionGateOutcome, Error>>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{createHash as k}from"node:crypto";import{success as g,failure as N}from"@fjall/generator";import{maskSensitiveOutput as D}from"@fjall/util";import{remediationPlaceholderPhysicalId as x}from"../../types/remediation.js";import{pinProbeCoverage as m}from"../remediation/pinRemediation.js";const v={"replacement-required":["abort","pin","recreate"],"pinned-name-replacement":["abort","pin","recreate"],"data-loss-delete":["abort","forget","recreate"],"resource-deleted":["abort","repair","forget","recreate"],"stack-deleted":["abort","repair","recreate"]};function I(e){return v[e]}const f={"replacement-required":["pin","recreate"],"pinned-name-replacement":["pin","recreate"],"data-loss-delete":["recreate"],"resource-deleted":["forget"],"stack-deleted":[]};function C(e){return f[e]}function b(e){const i=C(e.finding);return e.verbs.filter(t=>t==="abort"||i.includes(t)&&(t!=="pin"||m(e.resourceType,e.offendingProperties).covered))}function L(e){if(e.action==="replace")return e.pinnedPhysicalName!==void 0?"pinned-name-replacement":e.dataLoss?"replacement-required":void 0;if(e.action==="delete"&&e.dataLoss&&!e.retained)return"data-loss-delete"}function P(e,i,t){if(i==="data-loss-delete")return"removed from the synthesised template \u2014 CloudFormation will delete the live resource and its data";const o=t.length>0?`${t.join(", ")} forces replacement`:"replacement required (no property-level detail available)",n=e.replacementMode==="may"||e.replacementMode==="conditional"?" (conditional verdict \u2014 worst-cased towards ceremony)":"";return i==="pinned-name-replacement"?`${o}${n}; CloudFormation refuses in-stack replacement of custom-named resources`:`${o}${n}`}function R(e,i){const t=k("sha256");t.update(e),t.update(`
|
|
2
|
+
`);const o=i.map(n=>`${n.stack}|${n.logicalId}|${n.resourceType}|${n.physicalName}|${n.finding}`).sort();for(const n of o)t.update(n),t.update(`
|
|
3
|
+
`);return t.digest("hex")}function T(e,i){const t=new Set((i??[]).map(n=>`${n.stackName}
|
|
4
|
+
${n.logicalId}`)),o=[];for(const n of e.changes){if(n.action==="delete"&&t.has(`${n.stack}
|
|
5
|
+
${n.address}`))continue;const s=L(n);if(s===void 0)continue;const r=n.propertyChanges.filter(d=>d.forcesReplacement).map(d=>d.path);o.push({stack:n.stack,logicalId:n.address,resourceType:n.resourceType,physicalName:n.pinnedPhysicalName??x(n.stack,n.address),finding:s,cause:P(n,s,r),offendingProperties:r,verdictSource:n.verdictSource??"cdk-spec (unverified)",dataLossRisk:n.dataLoss,verbs:[...v[s]]})}if(o.length!==0)return{ticketDigest:R(e.assemblyDigest,o),resources:o}}function y(e){const{ticket:i,consents:t,consentDigest:o}=e,n=o!==void 0&&o!==i.ticketDigest,s=new Map(i.resources.map(a=>[a.physicalName,a])),r=t.map(a=>{const u=s.get(a.physicalName);return u===void 0?{physicalName:a.physicalName,verb:a.verb,verdict:"unmatched",detail:"no pending destructive resource has this exact name"}:u.verbs.includes(a.verb)?{physicalName:a.physicalName,verb:a.verb,verdict:"matched"}:{physicalName:a.physicalName,verb:a.verb,verdict:"wrong-verb",detail:`legal verbs for ${u.finding}: ${b(u).join(", ")}`}}),d=new Set(r.filter(a=>a.verdict==="matched").map(a=>a.physicalName)),p=i.resources.map(a=>a.physicalName).filter(a=>!d.has(a)),h=!n&&p.length===0&&r.length>0&&r.every(a=>a.verdict==="matched");return{rows:r,uncovered:p,digestMismatch:n,satisfied:h,...n&&o!==void 0?{suppliedDigest:o,currentDigest:i.ticketDigest}:{}}}function E(e){const i=[];for(const t of e.resources){const o=t.dataLossRisk?" (data loss)":"";i.push(`! ${t.finding} ${t.resourceType} ${t.stack}/${t.logicalId}${o}`),i.push(` physical name: ${t.physicalName}`),i.push(` cause: ${t.cause}`),i.push(` legal verbs: ${b(t).join(" | ")}`)}return i}function M(e){const i=[];if(e.digestMismatch){const t=e.suppliedDigest!==void 0&&e.currentDigest!==void 0?` (consents were minted against '${e.suppliedDigest}'; the pending destructive changes now digest to '${e.currentDigest}')`:"";i.push(`consent digest mismatch: the plan changed since consent was given \u2014 re-review and consent again${t}`)}if(e.rows.length>0){i.push("consent verdicts:");for(const t of e.rows){const o=t.detail!==void 0?` \u2014 ${t.detail}`:"";i.push(` ${t.physicalName}=${t.verb} ${t.verdict}${o}`)}}if(e.uncovered.length>0){i.push("awaiting consent:");for(const t of e.uncovered)i.push(` ${t}`)}return i}function $(e,i){const t=new Map(e.resources.map(n=>[n.physicalName,n])),o=[];for(const n of i){if(n.verdict!=="matched")continue;const s=t.get(n.physicalName);if(s!==void 0){if(n.verb==="abort"){o.push(`${n.physicalName}: aborted by operator consent`);continue}if(!f[s.finding].includes(n.verb)){o.push(`${n.physicalName}: verb '${n.verb}' is accepted vocabulary but not yet executable in this release for ${s.finding} \u2014 executable today: ${f[s.finding].join(", ")||"none (abort only)"}`);continue}if(n.verb==="pin"){const r=m(s.resourceType,s.offendingProperties);r.covered||o.push(`${n.physicalName}: verb 'pin' has no live-value probe for ${r.unsupportedPairs.join(", ")} in this release \u2014 pin cannot capture the live value(s) into the config for this resource. Executable today for ${s.finding}: ${f[s.finding].filter(d=>d!=="pin").join(", ")||"none (abort only)"}`)}}}return o}async function A(e){const i=await e.loadPlan();if(!i.success)return N(new Error(D(`Destruction gate could not compute the deploy plan: ${i.error.message}`)));const t=T(i.data,e.verifiedAbsentByNecessity);if(t===void 0)return g({kind:"clear"});const o=c=>{e.callbacks.onLog?.(`Destruction gate: ${t.resources.length} destructive change(s) proceeding under named consent (${c.rows.map(l=>`${l.physicalName}=${l.verb}`).join(", ")})`,"warn");const w=new Map(c.rows.filter(l=>l.verdict==="matched").map(l=>[l.physicalName,l.verb]));return g({kind:"proceed",ticket:t,verbsByPhysicalName:w})},n=e.options.destructionConsents??[];let s=y({ticket:t,consents:n,...e.options.destructionConsentDigest!==void 0?{consentDigest:e.options.destructionConsentDigest}:{}}),r=s.satisfied?$(t,s.rows):[];if(s.satisfied&&r.length===0)return o(s);const d=[];if(e.consentGate!==void 0&&n.length===0&&e.options.destructionConsentDigest===void 0){const c=await e.consentGate.resolve({target:e.target,ticket:t});if(c.kind==="abort")d.push("declined at the interactive destruction ceremony \u2014 nothing was applied");else if(s=y({ticket:t,consents:c.consents,consentDigest:c.ticketDigest}),r=s.satisfied?$(t,s.rows):[],s.satisfied&&r.length===0)return o(s)}const p=[...d,...r],h=s.satisfied&&s.rows.some(c=>c.verdict==="matched"&&c.verb==="abort"),a=d.length>0||h?"declined":r.length>0?"consent-not-executable":s.rows.length>0||s.digestMismatch?"consents-invalid":"awaiting-consent",u={"awaiting-consent":`Destructive changes for ${e.target} require named consent \u2014 deploy withheld, nothing was applied`,declined:`Deployment of ${e.target} declined \u2014 the destructive changes were not consented; nothing was applied`,"consents-invalid":`Deployment of ${e.target} withheld \u2014 the supplied consents did not match the pending destructive changes; nothing was applied`,"consent-not-executable":`Deployment of ${e.target} withheld \u2014 consent was given, but the consented remediation is not yet executable in this release; nothing was applied`};e.callbacks.onLog?.(u[a],"warn");for(const c of E(t))e.callbacks.onLog?.(c,"warn");for(const c of M(s))e.callbacks.onLog?.(c,"warn");for(const c of p)e.callbacks.onLog?.(c,"warn");return(a==="awaiting-consent"||a==="consents-invalid")&&e.callbacks.onLog?.("Consent with --remediate <physicalName>=<verb> (repeatable; exact, case-sensitive) or --allow-replace <physicalName> for plain replacements. No blanket flag exists: --auto-approve, --skip-confirmation and approval tokens never satisfy this gate.","warn"),g({kind:"withheld",outcome:a,ticket:t,report:s,reasons:p})}export{b as advertisableRemediationVerbs,T as buildDestructionTicket,R as computeTicketDigest,y as evaluateDestructionConsents,C as executableRemediationVerbs,I as legalRemediationVerbs,M as renderConsentVerdictLines,E as renderDestructionTicketLines,A as runDestructionGate};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BaseServiceError } from "../../types/errors/ServiceError.js";
|
|
2
|
+
import type { DeployParams } from "../../types/params.js";
|
|
3
|
+
/**
|
|
4
|
+
* The deployment's lease was denied at a stack boundary (§5.3): the server has
|
|
5
|
+
* released or superseded this deployment (cancelled, swept, adopted by another
|
|
6
|
+
* runner, or force-released) while the engine was mid-flight. The engine aborts
|
|
7
|
+
* BEFORE starting the named stack's first mutating CloudFormation operation —
|
|
8
|
+
* already-completed stacks stand. The message is surface-neutral (no CLI flags
|
|
9
|
+
* or commands) and built from static literals + the stack name, so there is
|
|
10
|
+
* nothing to mask.
|
|
11
|
+
*/
|
|
12
|
+
export declare class LeaseDeniedError extends BaseServiceError {
|
|
13
|
+
readonly stackName: string;
|
|
14
|
+
constructor(message: string, stackName: string);
|
|
15
|
+
}
|
|
16
|
+
/** Which mutating operation the gate protects — drives the denial wording. */
|
|
17
|
+
export type LeaseGateOperation = "deploy" | "destroy";
|
|
18
|
+
/**
|
|
19
|
+
* Stack-boundary lease gate (§5.3). Called immediately before a stack's first
|
|
20
|
+
* mutating CloudFormation operation. Returns a typed {@link LeaseDeniedError}
|
|
21
|
+
* when the lease is denied (the caller must abort without starting the stack),
|
|
22
|
+
* or `undefined` when the deploy may proceed.
|
|
23
|
+
*
|
|
24
|
+
* Consumes the resolved gate resolver + shutdown signal rather than the whole
|
|
25
|
+
* `DeployParams`. Fails OPEN: a `"unknown"` verdict (the liveness probe could
|
|
26
|
+
* not reach the server) proceeds, because a lease is a liveness signal and
|
|
27
|
+
* CloudFormation's own ClientRequestToken is the fencing backstop. An absent
|
|
28
|
+
* resolver is a no-op gate.
|
|
29
|
+
*/
|
|
30
|
+
export declare function assertLeaseBeforeStack(assertLeaseForStack: DeployParams["assertLeaseForStack"], stackName: string, abortSignal?: AbortSignal, operation?: LeaseGateOperation): Promise<LeaseDeniedError | undefined>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{BaseServiceError as t}from"../../types/errors/ServiceError.js";class s extends t{stackName;constructor(e,n){super("LEASE_DENIED",e),this.stackName=n}}function o(r,e){return`Deployment lease denied ${e==="destroy"?`before destroying ${r}`:`before deploying ${r}`} \u2014 the server released or superseded this deployment (it may have been cancelled, adopted by another runner, or force-released). ${e==="destroy"?"Stacks already destroyed are gone; remaining stacks stand.":"Already-completed stacks stand."} Check the deployment in the Fjall dashboard.`}async function c(r,e,n,d="deploy"){if(r===void 0)return;if(await r(e,n)==="denied")return new s(o(e,d),e)}export{s as LeaseDeniedError,c as assertLeaseBeforeStack};
|
|
@@ -14,6 +14,21 @@
|
|
|
14
14
|
* verification falls back to the digest match alone.
|
|
15
15
|
*/
|
|
16
16
|
export declare const DEFAULT_APPROVAL_TTL_MS: number;
|
|
17
|
+
/**
|
|
18
|
+
* TTL for the token re-minted when the destruction gate withholds an
|
|
19
|
+
* approval-gated deploy. The consent ceremony can legitimately take far
|
|
20
|
+
* longer than {@link DEFAULT_APPROVAL_TTL_MS} — the webapp lets a
|
|
21
|
+
* `destructive_pending` row await a human for 24 hours
|
|
22
|
+
* (`sweepAbandonedDestructivePendingSystem`) before abandoning it, and the
|
|
23
|
+
* original 15-minute token would expire long before that, making the
|
|
24
|
+
* ceremony unreachable for approval-gated deploys. 24h + 1h margin covers a
|
|
25
|
+
* consent granted at the very end of the abandon window plus claim/queue
|
|
26
|
+
* latency. Digest binding is unchanged: the re-minted token still refuses as
|
|
27
|
+
* `superseded` the moment the plan moves, so only the TIME bound is relaxed —
|
|
28
|
+
* and the withheld run already verified the original approval against this
|
|
29
|
+
* same digest.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DESTRUCTION_RESUME_TTL_MS: number;
|
|
17
32
|
export declare const APPROVAL_TOKEN_PATTERN: RegExp;
|
|
18
33
|
export interface SignApprovalTokenOptions {
|
|
19
34
|
readonly assemblyDigest: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHmac as c,randomBytes as l,timingSafeEqual as m}from"node:crypto";import{digestHead as i}from"./assemblyDigest.js";const a=16,p=900*1e3,
|
|
1
|
+
import{createHmac as c,randomBytes as l,timingSafeEqual as m}from"node:crypto";import{digestHead as i}from"./assemblyDigest.js";const a=16,p=900*1e3,k=1500*60*1e3,T=/^([0-9a-f]{32})\.([0-9]{1,16})\.([0-9a-f]{32})$/;function E(){const e=process.env.FJALL_APPROVAL_TOKEN_SECRET;return e!==void 0&&e!==""?{secret:Buffer.from(e,"utf8"),stable:!0}:{secret:l(32),stable:!1}}const{secret:u,stable:S}=E();function g(e){const t=i(e.assemblyDigest),r=e.now??Date.now,o=e.ttlMs??p,n=r()+o,s=c("sha256",u).update(`${t}.${n}`).digest().subarray(0,a).toString("hex");return{token:`${t}.${n}.${s}`,expiresAt:new Date(n).toISOString()}}function L(e){const t=T.exec(e.token);if(t===null)return{ok:!1,reason:"malformed"};const r=t[1],o=t[2],n=t[3];if(r===void 0||o===void 0||n===void 0)return{ok:!1,reason:"malformed"};if(r!==i(e.assemblyDigest))return{ok:!1,reason:"superseded"};const s=parseInt(o,10);if(!Number.isFinite(s))return{ok:!1,reason:"malformed"};if((e.now??Date.now)()>s)return{ok:!1,reason:"expired"};if(S){const d=c("sha256",u).update(`${r}.${o}`).digest().subarray(0,a),f=Buffer.from(n,"hex");if(f.length!==a||!m(d,f))return{ok:!1,reason:"tampered"}}return{ok:!0}}export{T as APPROVAL_TOKEN_PATTERN,p as DEFAULT_APPROVAL_TTL_MS,k as DESTRUCTION_RESUME_TTL_MS,g as signApprovalToken,L as verifyApprovalToken};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change-set escalation — Layer 2 of the replacement oracle (drift-remediation
|
|
3
|
+
* plan § 2, [G4]). Runs the review-only change-set probe ONLY on rows whose
|
|
4
|
+
* Layer-1 verdict is `conditional`/`may` on a stateful or pinned-name
|
|
5
|
+
* resource: definite createOnly hits are already proven, and stateless
|
|
6
|
+
* unpinned conditionals never ticket, so probing either buys no proof. A
|
|
7
|
+
* non-engaged deploy issues zero probe calls.
|
|
8
|
+
*
|
|
9
|
+
* Verdict precedence: change-set > registry > cdk-spec.
|
|
10
|
+
* Replacement: False → the ceremony DOWNGRADES to a warning — the row
|
|
11
|
+
* becomes an in-place update (directive 2:
|
|
12
|
+
* never block on a false positive)
|
|
13
|
+
* Replacement: True → conditional upgrades to definite, verdictSource
|
|
14
|
+
* `"change-set"`
|
|
15
|
+
* Replacement: Conditional → stays worst-case (the registry verdict stands)
|
|
16
|
+
* probe failure → the registry verdict stands, annotated
|
|
17
|
+
* `"registry (change-set unverified)"` — degrade
|
|
18
|
+
* to the conservative verdict, never to silence
|
|
19
|
+
*
|
|
20
|
+
* A resource the change set omits entirely (including the benign
|
|
21
|
+
* `didn't contain changes` outcome) will not be touched by the update —
|
|
22
|
+
* treated as `False`.
|
|
23
|
+
*/
|
|
24
|
+
import type { ChangeSetProbeRunner } from "../../../services/infrastructure/changeSetProbe.js";
|
|
25
|
+
import type { StackTemplatePair } from "./buildDeployPlan.js";
|
|
26
|
+
import type { DeployPlan, DeployPlanResourceChange } from "./types.js";
|
|
27
|
+
/**
|
|
28
|
+
* The [G4] trigger predicate: a replacement row that is neither proven
|
|
29
|
+
* (`will`) nor irrelevant (`none`), on a resource that would actually ticket
|
|
30
|
+
* — stateful (data loss) or pinned-name (CloudFormation refusal).
|
|
31
|
+
*/
|
|
32
|
+
export declare function needsChangeSetEscalation(change: DeployPlanResourceChange): boolean;
|
|
33
|
+
export interface ApplyChangeSetEscalationParams {
|
|
34
|
+
plan: DeployPlan;
|
|
35
|
+
probe: ChangeSetProbeRunner;
|
|
36
|
+
/** The diffed template pairs — the probe submits `newTemplate`. */
|
|
37
|
+
stacks: readonly StackTemplatePair[];
|
|
38
|
+
abortSignal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Apply the change-set escalation to an overlay-corrected plan. Never throws
|
|
42
|
+
* and never fails the plan: a failed or aborted probe leaves the conservative
|
|
43
|
+
* registry verdict in place, annotated unverified. One probe per escalated
|
|
44
|
+
* stack covers every candidate row in it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyChangeSetEscalation(params: ApplyChangeSetEscalationParams): Promise<DeployPlan>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{maskSensitiveOutput as k}from"@fjall/util";import{logger as u}from"@fjall/util/logger";import{isAborted as S}from"../../../aws/organisations/types.js";import{isDataLoss as w,isStatefulResourceType as g}from"./classify.js";import{recountActions as T}from"./registryOverlay.js";import{renderPlanSummary as C}from"./renderDeployPlan.js";const p="ChangeSetEscalation";function M(t){return t.action!=="replace"||t.retained||t.replacementMode!=="conditional"&&t.replacementMode!=="may"?!1:g(t.resourceType)||t.pinnedPhysicalName!==void 0}function b(t){return`change-set preflight proved this update applies in place (${t.replacementMode} verdict downgraded)`}function N(t){return{...t,action:"update",replacementMode:"none",destructive:!1,dataLoss:!1,propertyChanges:t.propertyChanges.map(r=>({...r,forcesReplacement:!1})),verdictSource:"change-set",interruptionNote:b(t)}}function L(t){return{...t,action:"replace",replacementMode:"will",destructive:!0,dataLoss:w({action:"replace",retained:t.retained,resourceType:t.resourceType}),verdictSource:"change-set"}}function $(t){return t.verdictSource!=="registry"?t:{...t,verdictSource:"registry (change-set unverified)"}}async function A(t){const{plan:r,probe:f,stacks:l,abortSignal:i}=t,c=r.changes.filter(M);if(c.length===0)return r;const m=new Map(l.map(e=>[e.stackName,e])),y=[...new Set(c.map(e=>e.stack))],a=new Map;for(const e of y){if(S(i)){a.set(e,"unverified");continue}const n=m.get(e);if(n===void 0){u.warn(p,"No synthesised template for escalated stack",{stackName:e}),a.set(e,"unverified");continue}const s=await f.probeStackReplacements({stackName:e,template:n.newTemplate,...i!==void 0?{abortSignal:i}:{}});if(!s.success){u.warn(p,"Change-set probe degraded to registry verdict",{stackName:e,error:k(s.error.message)}),a.set(e,"unverified");continue}a.set(e,s.data.replacementByLogicalId)}const v=new Set(c.map(e=>`${e.stack}|${e.address}`)),o=r.changes.map(e=>{if(!v.has(`${e.stack}|${e.address}`))return e;const n=a.get(e.stack);if(n===void 0||n==="unverified")return $(e);const s=n.get(e.address)??"False";return s==="False"?N(e):s==="True"?L(e):(u.warn(p,"Change set could not decide \u2014 keeping worst case",{stack:e.stack,address:e.address,resourceType:e.resourceType}),e)}),d=T(o);return d["no-op"]=r.counts["no-op"],{...r,changes:o,counts:d,hasDestructiveChanges:o.some(e=>e.destructive),summary:C({counts:d,changes:o})}}export{A as applyChangeSetEscalation,M as needsChangeSetEscalation};
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type Result } from "@fjall/generator";
|
|
8
8
|
import type { CloudFormationError } from "../../../services/infrastructure/CloudFormationService.js";
|
|
9
|
+
import type { CfnRegistrySummaryReader } from "../../../services/infrastructure/CfnRegistryService.js";
|
|
10
|
+
import type { ChangeSetProbeRunner } from "../../../services/infrastructure/changeSetProbe.js";
|
|
9
11
|
import type { DeployPlan } from "./types.js";
|
|
10
12
|
/** The narrow CFN read surface the gate needs — CloudFormationService satisfies it. */
|
|
11
13
|
export interface CfnTemplateReader {
|
|
@@ -20,5 +22,16 @@ export interface ComputeDeployPlanParams {
|
|
|
20
22
|
cfnService: CfnTemplateReader;
|
|
21
23
|
stacks: ComputeDeployPlanStack[];
|
|
22
24
|
assemblyDigest: string;
|
|
25
|
+
/**
|
|
26
|
+
* Registry oracle for replacement-verdict recomputation. Optional — without
|
|
27
|
+
* it the plan carries raw cloudformation-diff verdicts (no `verdictSource`).
|
|
28
|
+
*/
|
|
29
|
+
registry?: CfnRegistrySummaryReader;
|
|
30
|
+
/**
|
|
31
|
+
* Change-set escalation probe [G4]. Optional — without it conditional
|
|
32
|
+
* verdicts stay worst-cased into the ceremony (the sanctioned D2 fallback).
|
|
33
|
+
*/
|
|
34
|
+
changeSetProbe?: ChangeSetProbeRunner;
|
|
35
|
+
abortSignal?: AbortSignal;
|
|
23
36
|
}
|
|
24
37
|
export declare function computeDeployPlan(params: ComputeDeployPlanParams): Promise<Result<DeployPlan, Error>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{success as
|
|
1
|
+
import{success as i,failure as s}from"@fjall/generator";import{maskSensitiveOutput as c}from"@fjall/util";import{buildDeployPlan as f}from"./buildDeployPlan.js";import{applyChangeSetEscalation as u}from"./changeSetEscalation.js";import{applyRegistryOverlay as p}from"./registryOverlay.js";function d(e){const r=e.trim();if(r==="")return i({});try{return i(JSON.parse(r))}catch(t){return s(new Error(`Failed to parse deployed template: ${c(t instanceof Error?t.message:String(t))}`))}}async function k(e){const r=[];for(const o of e.stacks){const n=await e.cfnService.getTemplate(o.stackName);let l={};if(n.success){const a=d(n.data);if(!a.success)return s(a.error);l=a.data}else if(n.error.errorType!=="stack_not_found")return s(n.error);r.push({stackName:o.stackName,oldTemplate:l,newTemplate:o.newTemplate})}let t=f(r,e.assemblyDigest);return e.registry!==void 0&&(t=await p({plan:t,registry:e.registry,stacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})),e.changeSetProbe!==void 0&&(t=await u({plan:t,probe:e.changeSetProbe,stacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})),i(t)}export{k as computeDeployPlan};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Deploy-plan computation + rendering — the approval gate's read-side core. */
|
|
2
|
-
export type
|
|
2
|
+
export { REPLACEMENT_VERDICT_SOURCES, type DeployPlan, type DeployPlanResourceChange, type ReplacementVerdictSource } from "./types.js";
|
|
3
3
|
export { buildDeployPlan, type StackTemplatePair } from "./buildDeployPlan.js";
|
|
4
|
+
export { applyRegistryOverlay, overlayResourceChange, registryVerdictForProperty, type ApplyRegistryOverlayParams, type OverlayResourceChangeResult, type RegistryVerdictDisagreement } from "./registryOverlay.js";
|
|
5
|
+
export { applyChangeSetEscalation, needsChangeSetEscalation, type ApplyChangeSetEscalationParams } from "./changeSetEscalation.js";
|
|
6
|
+
export { findPinnedPhysicalName, resourcePropertiesFromTemplate } from "./pinnedNames.js";
|
|
4
7
|
export { computeDeployPlan, type CfnTemplateReader, type ComputeDeployPlanParams, type ComputeDeployPlanStack } from "./computeDeployPlan.js";
|
|
5
8
|
export { computeAssemblyDigest, digestHead } from "./assemblyDigest.js";
|
|
6
9
|
export { classifyImpact, derivePropertyChanges, isDataLoss, isStatefulResourceType, type ImpactClassification } from "./classify.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{REPLACEMENT_VERDICT_SOURCES as o}from"./types.js";import{buildDeployPlan as t}from"./buildDeployPlan.js";import{applyRegistryOverlay as s,overlayResourceChange as n,registryVerdictForProperty as l}from"./registryOverlay.js";import{applyChangeSetEscalation as m,needsChangeSetEscalation as y}from"./changeSetEscalation.js";import{findPinnedPhysicalName as f,resourcePropertiesFromTemplate as T}from"./pinnedNames.js";import{computeDeployPlan as d}from"./computeDeployPlan.js";import{computeAssemblyDigest as x,digestHead as A}from"./assemblyDigest.js";import{classifyImpact as R,derivePropertyChanges as u,isDataLoss as C,isStatefulResourceType as L}from"./classify.js";import{renderPlanSummary as _,renderPlanLines as h,toWirePlanChanges as v}from"./renderDeployPlan.js";import{signApprovalToken as O,verifyApprovalToken as N,DEFAULT_APPROVAL_TTL_MS as V,APPROVAL_TOKEN_PATTERN as F}from"./approvalToken.js";export{F as APPROVAL_TOKEN_PATTERN,V as DEFAULT_APPROVAL_TTL_MS,o as REPLACEMENT_VERDICT_SOURCES,m as applyChangeSetEscalation,s as applyRegistryOverlay,t as buildDeployPlan,R as classifyImpact,x as computeAssemblyDigest,d as computeDeployPlan,u as derivePropertyChanges,A as digestHead,f as findPinnedPhysicalName,C as isDataLoss,L as isStatefulResourceType,y as needsChangeSetEscalation,n as overlayResourceChange,l as registryVerdictForProperty,h as renderPlanLines,_ as renderPlanSummary,T as resourcePropertiesFromTemplate,O as signApprovalToken,v as toWirePlanChanges,N as verifyApprovalToken};
|