@fjall/deploy-core 2.19.1 → 2.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
152 files minified at 2026-06-
|
|
1
|
+
152 files minified at 2026-06-20T09:23:13.824Z
|
|
@@ -2,6 +2,28 @@ import { type Result } from "@fjall/generator";
|
|
|
2
2
|
import type { DeployParams, DeployResult } from "../types/params.js";
|
|
3
3
|
import type { ApplicationOperation } from "../types/operations.js";
|
|
4
4
|
import type { DeployServices } from "./serviceFactory.js";
|
|
5
|
+
/**
|
|
6
|
+
* Re-base an operator-supplied `--image-tag` onto a specific ECS service.
|
|
7
|
+
*
|
|
8
|
+
* A multi-image stack pushes each service's image under its own
|
|
9
|
+
* `<service>-…` tag family into ONE shared ECR repository (the webapp:
|
|
10
|
+
* `app-sha-<id>`, `scan-worker-sha-<id>`, `deploy-worker-sha-<id>`). The
|
|
11
|
+
* supplied rollback tag names exactly one of those families, but the same
|
|
12
|
+
* build identity (`-sha-<id>` / `-latest` suffix) exists for every service.
|
|
13
|
+
* Applying the supplied tag VERBATIM to every service rolls the wrong image
|
|
14
|
+
* onto the others — e.g. the worker services boot the app image, run
|
|
15
|
+
* `react-router serve`, and crash on app-only secrets. Because all services
|
|
16
|
+
* share one repo, the wrong tag genuinely resolves to a real digest, so the
|
|
17
|
+
* downstream digest-verify cannot catch it; the prefix must be corrected here.
|
|
18
|
+
*
|
|
19
|
+
* This strips the matched `<service>-` prefix and re-prepends the current
|
|
20
|
+
* service's name, preserving the build-identity suffix. Longest-prefix match
|
|
21
|
+
* guards against substring service names (`worker` vs `scan-worker`). When the
|
|
22
|
+
* supplied tag carries no recognised service prefix (a bare custom tag like
|
|
23
|
+
* `latest` or `v1.2.3`), it cannot be re-based and is returned verbatim — the
|
|
24
|
+
* caller surfaces a warning on multi-service stacks.
|
|
25
|
+
*/
|
|
26
|
+
export declare function rebaseRollbackTagForService(suppliedTag: string, serviceName: string, allServiceNames: readonly string[]): string;
|
|
5
27
|
/**
|
|
6
28
|
* Code-only deployment orchestrator.
|
|
7
29
|
*
|
|
@@ -13,7 +35,9 @@ import type { DeployServices } from "./serviceFactory.js";
|
|
|
13
35
|
* pushed the artifacts (`runDockerBuild` returns this map). Keys are
|
|
14
36
|
* `<service>` or `<service>-<target>`; values are the bare content-hash tag
|
|
15
37
|
* stem (`<service>(-<target>)?-sha-<12hex>`). `options.imageTag` (the
|
|
16
|
-
* explicit `--image-tag <tag>` rollback) bypasses this map
|
|
17
|
-
*
|
|
38
|
+
* explicit `--image-tag <tag>` rollback) bypasses this map; it is re-based
|
|
39
|
+
* per service via `rebaseRollbackTagForService` so each service rolls its OWN
|
|
40
|
+
* `<service>-…` image family (a multi-image stack shares one ECR repo, so an
|
|
41
|
+
* un-rebased tag would silently roll the wrong service's image).
|
|
18
42
|
*/
|
|
19
43
|
export declare function deployCodeOnly(params: DeployParams, services: DeployServices, operation: ApplicationOperation, contentHashTagsByService: Record<string, string>): Promise<Result<DeployResult>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{join as U}from"path";import{success as
|
|
1
|
+
import{join as U}from"path";import{success as _,failure as k}from"@fjall/generator";import{logger as L}from"@fjall/util/logger";import{maskSensitiveOutput as E}from"@fjall/util";import{parseDockerServicesFromManifest as j}from"@fjall/util/manifest";import{APPLICATION_STACKS as P,getApplicationStepId as N,getApplicationStepName as W}from"../types/operations.js";import{extractEcsServiceName as O}from"../aws/utils/arnParser.js";import{stripTagOrDigest as F}from"../services/infrastructure/EcrImageInspectorService.js";import{withStepLifecycle as q}from"./stepLifecycle.js";const I="codeOnlyDeploy";function z(n,a){if(!n)return;const o=n.docker.target,e=o?`${n.name}-${o}`:n.name;return a[e]}function G(n,a){const o=a.toLowerCase();return n.find(e=>e.name.toLowerCase()===o)??n.find(e=>o.includes(e.name.toLowerCase()))}function H(n,a,o){const e=a.toLowerCase(),c=n.toLowerCase(),g=o.map(m=>m.toLowerCase()).filter(m=>m!==""&&c.startsWith(`${m}-`)).sort((m,T)=>T.length-m.length)[0];if(g===void 0||g===e)return n;const v=n.slice(g.length);return`${e}${v}`}const K=0,X=1;async function se(n,a,o,e){const{callbacks:c,options:g}=n,v=Date.now(),m=N(P.COMPUTE,"deploy"),T=W(P.COMPUTE,"deploy");return q(c,{stepId:m,stepName:T,stepIndex:K,totalSteps:X},async()=>{const d=g?.imageTag;d&&c.onLog?.(`Rolling to supplied image tag ${d}`,"info");const S=j(U(o.path,"cdk.out"));L.debug(I,"Code-only rollout starting",{explicitRollbackTag:d,workingDirectory:n.workingDirectory,contentHashTagsByService:e,manifestServiceCount:S.length,manifestServices:S.map(t=>({name:t.name,target:t.docker.target}))}),c.onLog?.("Discovering ECS cluster and services\u2026","info");const D=await a.ecsResolver.getDeployableClusterAndServices(o.appName);if(!D.success)return L.debug(I,"ECS discovery failed",{appName:o.appName,error:E(D.error.message)}),{kind:"error",error:D.error};const{clusterArn:r,serviceArns:$}=D.data;if(L.debug(I,"ECS discovery complete",{appName:o.appName,clusterArn:r,serviceCount:$.length,serviceArns:$}),!r||$.length===0){const t=`No deployable ECS services found for ${o.appName}`;return c.onLog?.(t,"warn"),{kind:"skipped",data:{target:o.appName,deploymentType:"application",durationMs:Date.now()-v,noChanges:!0}}}const b=g?.serviceName?.toLowerCase(),w=b===""?void 0:b,f=w?$.filter(t=>{const l=(O(t)??"").toLowerCase();return l===w||l.endsWith(w)}):$;if(w&&f.length===0){const t=$.map(l=>O(l)??"unknown").join(", ");return{kind:"error",error:new Error(`Service '${w}' not found. Available: ${t||"none"}`)}}const C={},h=[],R=$.map(t=>O(t)).filter(t=>t!==void 0&&t!=="");for(let t=0;t<f.length;t++){const l=f[t];if(!l)continue;const i=O(l)??`service-${t+1}`,A=`[${t+1}/${f.length}] ${i}`,y=G(S,i),x=z(y,e);let p,s;d!==void 0?(p=H(d,i,R),p!==d?(s=d,c.onLog?.(`Re-based image tag ${d} \u2192 ${p} for ${i}`,"info")):R.length>1&&!d.toLowerCase().startsWith(`${i.toLowerCase()}-`)&&c.onLog?.(`Image tag ${d} has no recognised service prefix; applying verbatim to ${i} \u2014 confirm it points at this service's image`,"warn")):x!==void 0?p=x:(p=`${i.toLowerCase()}-latest`,c.onLog?.(`No content-hash tag found for ${i}; falling back to mutable tag ${p}`,"warn")),L.debug(I,"Starting service rollout",{serviceName:i,serviceArn:l,imageTag:p,explicitRollbackTag:d,contentHashTag:x,manifestServiceMatched:y?.name,dockerTarget:y?.docker.target,index:t,total:f.length}),c.onECSUpdate?.(`${A}: rolling out image tag ${p}`,i);const u=await Y(a,r,l,i,p,c,s);if(L.debug(I,"Service rollout completed",{serviceName:i,success:u.success,...u.success?{taskDefinitionArn:u.data.taskDefinitionArn,imageDigest:u.data.imageDigest,durationMs:u.data.durationMs}:{error:E(u.error.message)}}),!u.success){h.push(`${i}: ${E(u.error.message)}`);continue}C[`${i}TaskDefinition`]=u.data.taskDefinitionArn,C[`${i}PreviousTaskDefinition`]=u.data.previousTaskDefinitionArn,C[`${i}ImageTag`]=p,C[`${i}ImageUri`]=u.data.imageUri,C[`${i}EcrRepositoryArn`]=u.data.ecrRepositoryArn,u.data.imageDigest!==void 0&&(C[`${i}ImageDigest`]=u.data.imageDigest)}const M=Date.now()-v;if(h.length>0){const t=f.length-h.length,l=f.length,i=h.length===l?`All ${l} ECS service rollouts failed: ${h.join("; ")}`:`Partial rollout: ${t}/${l} services succeeded. Failures: ${h.join("; ")}`;return{kind:"error",error:new Error(i)}}return{kind:"completed",data:{target:o.appName,deploymentType:"application",outputs:Object.keys(C).length>0?C:void 0,durationMs:M}}})}async function Y(n,a,o,e,c,g,v){const m=Date.now(),T=await n.ecsService.describeServices(a,[o]);if(!T.success)return k(new Error(`Failed to describe service ${e}: ${T.error.message}`));const d=T.data[0];if(!d?.taskDefinition)return k(new Error(`Service ${e} has no task definition`));const S=d.taskDefinition,D=await n.ecsService.getLatestTaskDefinition(S);if(!D.success)return k(new Error(`Failed to fetch task definition for ${e}: ${D.error.message}`));const r=D.data;if(!r||!r.containerDefinitions||r.containerDefinitions.length===0)return k(new Error(`Task definition for ${e} has no container definitions`));const $=r.containerDefinitions.find(s=>s.image)?.image;if(!$)return k(new Error(`Task definition for ${e} has no container image \u2014 cannot derive repository URI`));const b=F($),w=await n.ecrImageInspector.getImageDigest(b,c);let f;if(w.success)f=w.data;else{if(v!==void 0)return k(new Error(`Re-based image tag '${c}' (from supplied '${v}') was not found in ECR for ${e}: ${E(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} (${E(w.error.message)}); falling back to tag pinning`,"warn")}const C=s=>f!==void 0?`${s}@${f}`:`${s}:${c}`,h=r.containerDefinitions.map(s=>s.image?{...s,image:C(F(s.image))}:s),R=r.family;if(!R)return k(new Error(`Task definition for ${e} has no family`));const M=await n.ecsService.registerTaskDefinition({family:R,taskRoleArn:r.taskRoleArn,executionRoleArn:r.executionRoleArn,networkMode:r.networkMode,containerDefinitions:h,volumes:r.volumes,placementConstraints:r.placementConstraints,requiresCompatibilities:r.requiresCompatibilities,cpu:r.cpu,memory:r.memory,runtimePlatform:r.runtimePlatform,proxyConfiguration:r.proxyConfiguration,inferenceAccelerators:r.inferenceAccelerators,pidMode:r.pidMode,ipcMode:r.ipcMode,ephemeralStorage:r.ephemeralStorage});if(!M.success)return k(new Error(`Failed to register task definition for ${e}: ${M.error.message}`));const t=M.data;g.onTaskDefRegistered?.({serviceName:e,taskDefinitionArn:t,previousTaskDefinitionArn:S,revision:Q(t),family:R,...f!==void 0?{imageDigest:f}:{}});const l=await n.ecsService.updateService(a,o,{taskDefinition:t,forceNewDeployment:!0});if(!l.success)return k(new Error(`Failed to update service ${e}: ${l.error.message}`));const i=await n.ecsService.pollDeployment({clusterArn:a,serviceArn:o,waitForCompletion:!0,progressCallback:s=>{const u=s.latestEvent?` \u2014 ${E(s.latestEvent)}`:"";g.onECSProgress?.(`${e}: ${E(s.message)}${u}`,s.percentComplete)}}),A=Date.now()-m;if(!i.success){const s=E(i.error.message);return g.onECSComplete?.({serviceName:e,serviceArn:o,success:!1,taskDefinitionArn:t,durationMs:A,reason:s}),k(new Error(`ECS rollout failed for ${e}: ${s}`))}const y=await n.ecsService.getDeploymentStatus(a,o);g.onECSComplete?.({serviceName:e,serviceArn:o,success:!0,taskDefinitionArn:t,durationMs:A,finalRunningCount:y.success?y.data.runningCount:void 0,finalDesiredCount:y.success?y.data.desiredCount:void 0}),L.debug(I,`Rolled out ${e} successfully`,{serviceArn:o,newTaskDefArn:t,previousTaskDefArn:S,durationMs:A});const x=`${b}:${c}`,p=J(b);return _({taskDefinitionArn:t,previousTaskDefinitionArn:S,imageUri:x,ecrRepositoryArn:p,imageDigest:f,durationMs:A})}function J(n){const a=n.match(/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/);if(!a)return"";const[,o,e,c]=a;return`arn:aws:ecr:${e}:${o}:repository/${c}`}function Q(n){const a=n.lastIndexOf(":");if(a<0)return 0;const o=n.slice(a+1);return/^\d+$/.test(o)?parseInt(o,10):0}export{se as deployCodeOnly,H as rebaseRollbackTagForService};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isAbsolute as
|
|
1
|
+
import{isAbsolute as O,join as E,dirname as T,resolve as R}from"node:path";import{existsSync as $}from"node:fs";import{success as D,failure as h}from"@fjall/generator";import{deriveContentHashTag as w,maskSensitiveOutput as k}from"@fjall/util";import{logger as S}from"@fjall/util/logger";import{parseDockerServicesFromManifest as A}from"@fjall/util/manifest";import{STEP_IDS as v}from"../types/stepDefinitions.js";const b="dockerBuildHelper",P="Building and pushing Docker image";function x(e,n,r){const t=E(n,e);if($(t))return{path:t,base:n};if(r!==""&&r!==n){const i=E(r,e);if($(i))return{path:i,base:r}}return{path:t,base:n}}function I(e,n,r){const t=e.docker.path,i=e.docker.context;let o,a;if(O(t))o=t,a=n;else{const c=x(t,n,r);o=c.path,a=c.base}return{buildContext:i?O(i)?R(i):R(a,i):R(T(o)),dockerfilePath:o,target:e.docker.target,resolvedViaFallback:a!==n}}function L(e){return`${e.buildContext}|${e.dockerfilePath}|${e.target??""}`}function B(e,n){return`Dockerfile not found for service '${e}' at ${n}. Checked the synth directory and the working directory. If your Dockerfile lives at the project root with the CDK app in a subdirectory, set an absolute docker.context (the build context must share the Dockerfile's directory).`}function _(e){return e.startsWith("cn-")?"amazonaws.com.cn":"amazonaws.com"}function K(e,n,r){return`${e}.dkr.ecr.${n}.${_(n)}/${r.toLowerCase()}`}function H(e,n){const r=n.name.toLowerCase(),t=n.docker.target,i=t?`-${t.toLowerCase()}`:"";return[`${e}:${r}${i}-latest`]}function j(e,n,r,t){const i=new Map;for(const o of e){const a=I(o,n,t),u=L(a),c=H(r,o),d=i.get(u);d?(d.members.push(o),d.latestTags.push(...c)):i.set(u,{identity:a,members:[o],latestTags:[...c]})}return Array.from(i.values())}async function F(e,n,r,t,i){const o=await e.buildAndPush({appName:n.appName,appPath:n.path,region:r,accountId:t},(a,u,c,d)=>{i.onDockerProgress?.(k(a),u,c,d)});return o.success?D({imageUri:o.data.imageUri,imageTag:o.data.imageTag}):h(o.error)}async function U(e,n,r,t,i,o,a){const u=e.members[0];if(!$(e.identity.dockerfilePath))return h(new Error(B(u.name,e.identity.dockerfilePath)));e.identity.resolvedViaFallback&&a.onLog?.(`Building service '${u.name}' from ${e.identity.dockerfilePath} (resolved at the project root).`,"info");const c={appName:r.appName,appPath:e.identity.buildContext,region:t,accountId:i,buildContext:e.identity.buildContext,dockerfilePath:e.identity.dockerfilePath,imageTags:e.latestTags,serviceName:u.name,...e.identity.target!==void 0&&{target:e.identity.target},...u.docker.buildArgs!==void 0&&{buildArgs:u.docker.buildArgs}},d=await n.buildAndPush(c,(g,C,f,m)=>{a.onDockerProgress?.(k(g),C,f,m)});if(!d.success)return h(d.error);const p=d.data.imageDigest;if(typeof p!="string"||!p.startsWith("sha256:"))return h(new Error(`Buildx push succeeded but returned an invalid digest: ${k(String(p))}`));const l={},y=[];for(const g of e.members){const C=g.docker.target,f=w(p,g.name,C);if(!f.success)return h(f.error);const m=f.data,N=g.name;l[N]=m,y.push(`${o}:${m}`)}const s=await n.tagByDigest({sourceImage:o,digest:p,tags:y});return s.success?D({contentHashTagsByService:l}):h(s.error)}async function J(e,n,r,t){const i=e.dockerProvider;if(!i)return D({});const o=n.awsProvider.getAccountId();if(!o)return t.onLog?.("Skipping Docker build \u2014 account ID not available","warn"),D({});const a=n.awsProvider.getRegion(),u=E(r.path,"cdk.out"),c=A(u),d=K(o,a,r.appName);S.debug(b,"runDockerBuild starting",{appName:r.appName,appPath:r.path,accountId:o,region:a,cdkOutPath:u,manifestServiceCount:c.length,manifestServices:c.map(s=>({name:s.name,path:s.docker.path,context:s.docker.context,target:s.docker.target})),stepId:v.DOCKER_OPERATIONS,stepName:P}),t.onStepStart?.(v.DOCKER_OPERATIONS,P),t.onLog?.("Initialising ECR repository\u2026","info");const p=await i.initialiseECR({appName:r.appName,region:a,accountId:o});if(p.success||t.onLog?.(`ECR initialisation failed: ${k(p.error.message)}`,"warn"),c.length===0){t.onLog?.("Building and pushing Docker image\u2026","info");const s=await F(i,r,a,o,t);if(!s.success){S.debug(b,"Docker buildAndPush (legacy) failed",{appName:r.appName,error:k(s.error.message)}),t.onStepComplete?.(v.DOCKER_OPERATIONS,P,"error");const g=new Error(k(s.error.message));return t.onError?.(g),h(g)}return t.onStepComplete?.(v.DOCKER_OPERATIONS,P,"completed"),t.onLog?.(`Docker image pushed: ${s.data.imageUri}`,"info"),D({})}const l=j(c,r.path,d,e.workingDirectory);t.onLog?.(`Building and pushing ${c.length} service image(s) in ${l.length} group(s)\u2026`,"info");const y={};for(let s=0;s<l.length;s++){const g=l[s],C=g.members.map(m=>m.name).join(", ");t.onLog?.(`Building group ${s+1}/${l.length} (${C})\u2026`,"info");const f=await U(g,i,r,a,o,d,t);if(!f.success){S.debug(b,"Docker buildAndPush failed",{appName:r.appName,leader:g.members[0]?.name,members:C,error:k(f.error.message)}),t.onStepComplete?.(v.DOCKER_OPERATIONS,P,"error");const m=new Error(k(f.error.message));return t.onError?.(m),h(m)}for(const[m,N]of Object.entries(f.data.contentHashTagsByService))y[m]=N}return S.debug(b,"Docker buildAndPush succeeded",{appName:r.appName,services:c.map(s=>s.name),groupCount:l.length,contentHashTags:y}),t.onStepComplete?.(v.DOCKER_OPERATIONS,P,"completed"),t.onLog?.(`Docker images pushed for ${c.length} service(s)`,"info"),D(y)}export{P as DOCKER_BUILD_STEP_NAME,J as runDockerBuild};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/deploy-core",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.2",
|
|
4
4
|
"description": "Shared deployment engine for Fjall — used by CLI and webapp worker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
"@aws-sdk/client-sqs": "^3.1067.0",
|
|
79
79
|
"@aws-sdk/client-sso-admin": "^3.1038.0",
|
|
80
80
|
"@aws-sdk/client-sts": "^3.1038.0",
|
|
81
|
-
"@fjall/generator": "^2.19.
|
|
82
|
-
"@fjall/util": "^2.19.
|
|
81
|
+
"@fjall/generator": "^2.19.2",
|
|
82
|
+
"@fjall/util": "^2.19.2",
|
|
83
83
|
"@smithy/node-http-handler": "^4.6.1",
|
|
84
84
|
"tsx": "^4.21.0",
|
|
85
85
|
"zod": "^4.4.3"
|
|
@@ -88,5 +88,5 @@
|
|
|
88
88
|
"@types/node": "^25.6.0",
|
|
89
89
|
"vitest": "^4.1.5"
|
|
90
90
|
},
|
|
91
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "9c77ec058ff9a6bbd09d318747ff9cf3152ee58b"
|
|
92
92
|
}
|