@fjall/deploy-core 0.95.0 → 0.99.1
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/policies.d.ts +25 -3
- package/dist/src/aws/organisations/policies.js +1 -1
- package/dist/src/aws/organisations/serviceAccess.js +1 -1
- package/dist/src/aws/utils/arnParser.d.ts +24 -0
- package/dist/src/aws/utils/arnParser.js +1 -0
- package/dist/src/aws/utils/awsErrorHandler.d.ts +19 -0
- package/dist/src/aws/utils/awsErrorHandler.js +1 -0
- package/dist/src/aws/utils/cloudformationEvents.js +1 -1
- package/dist/src/aws/utils/index.d.ts +1 -0
- package/dist/src/aws/utils/index.js +1 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +1 -1
- package/dist/src/orchestration/applicationDeploy.js +1 -1
- package/dist/src/orchestration/applicationDeployHelpers.d.ts +21 -12
- package/dist/src/orchestration/applicationDeployHelpers.js +3 -3
- package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -1
- package/dist/src/orchestration/codeOnlyDeploy.d.ts +19 -0
- package/dist/src/orchestration/codeOnlyDeploy.js +1 -0
- package/dist/src/orchestration/detectionPipeline.js +1 -1
- package/dist/src/orchestration/dockerBuildHelper.d.ts +19 -2
- package/dist/src/orchestration/dockerBuildHelper.js +1 -1
- package/dist/src/orchestration/dockerInterface.d.ts +63 -5
- package/dist/src/orchestration/index.d.ts +1 -1
- package/dist/src/orchestration/openNextBuild.js +3 -3
- package/dist/src/orchestration/serviceFactory.d.ts +6 -0
- package/dist/src/orchestration/serviceFactory.js +1 -1
- package/dist/src/orchestration/stackCleanup.js +1 -1
- package/dist/src/orchestration/stepLifecycle.d.ts +29 -0
- package/dist/src/orchestration/stepLifecycle.js +1 -0
- package/dist/src/services/application/ApplicationStackService.d.ts +9 -1
- package/dist/src/services/application/ApplicationStackService.js +1 -1
- package/dist/src/services/application/applicationStackHelpers.js +2 -2
- package/dist/src/services/index.d.ts +2 -2
- package/dist/src/services/index.js +1 -1
- package/dist/src/services/infrastructure/CdkArgumentBuilder.d.ts +10 -0
- package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
- package/dist/src/services/infrastructure/CdkCommandRunner.d.ts +1 -0
- package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -2
- package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -1
- package/dist/src/services/infrastructure/CdkProcessManager.js +1 -1
- package/dist/src/services/infrastructure/CdkService.d.ts +1 -1
- package/dist/src/services/infrastructure/CdkService.js +2 -2
- package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +8 -1
- package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
- package/dist/src/services/infrastructure/EcrImageInspectorService.d.ts +32 -0
- package/dist/src/services/infrastructure/EcrImageInspectorService.js +1 -0
- package/dist/src/services/infrastructure/EcsService.d.ts +96 -0
- package/dist/src/services/infrastructure/EcsService.js +1 -0
- package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +58 -0
- package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -0
- package/dist/src/services/infrastructure/cdkServiceHelpers.d.ts +1 -3
- package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
- package/dist/src/services/infrastructure/index.d.ts +3 -0
- package/dist/src/services/infrastructure/index.js +1 -1
- package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -1
- package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
- package/dist/src/steps/stepRegistry.js +1 -1
- package/dist/src/types/FjallState.js +1 -1
- package/dist/src/types/application/ApplicationServiceTypes.js +1 -1
- package/dist/src/types/callbacks.d.ts +43 -6
- package/dist/src/types/deployment/DeploymentTypes.d.ts +0 -1
- package/dist/src/types/errors/ServiceError.js +1 -1
- package/dist/src/types/events.d.ts +53 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/types/params.d.ts +15 -0
- package/dist/src/types/stepDefinitions.d.ts +7 -4
- package/dist/src/types/stepDefinitions.js +1 -1
- package/package.json +22 -23
- package/dist/src/__test-utils__/awsMockHelpers.d.ts +0 -20
- package/dist/src/__test-utils__/awsMockHelpers.js +0 -1
- package/dist/src/__test-utils__/index.d.ts +0 -1
- package/dist/src/__test-utils__/index.js +0 -1
- package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.d.ts +0 -6
- package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.js +0 -1
- package/dist/src/orchestration/__tests__/cascadeTestHelpers.d.ts +0 -12
- package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +0 -1
- package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.d.ts +0 -9
- package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ECRClient as m,BatchGetImageCommand as p}from"@aws-sdk/client-ecr";import{success as n,failure as s}from"@fjall/generator";import{logger as g}from"@fjall/util/logger";import{getErrorMessage as I}from"@fjall/util";const E="EcrImageInspector",C=/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/;function R(t){const e=t.lastIndexOf("@");if(e>0)return t.slice(0,e);const r=t.lastIndexOf(":");return r>0?t.slice(0,r):t}class G{awsProvider;constructor(e){this.awsProvider=e}async getImageDigest(e,r){const c=R(e).match(C);if(!c)return g.debug(E,"Image URI is not ECR \u2014 skipping digest lookup",{imageUri:e}),n(void 0);const[,,,o]=c;if(!o)return n(void 0);try{const a=await this.awsProvider.getClient(m).send(new p({repositoryName:o,imageIds:[{imageTag:r}]})),d=a.failures??[];if(d.length>0){const l=d.map(f=>`${f.failureCode??"unknown"}:${f.failureReason??""}`).join("; ");return s(new Error(`ECR BatchGetImage rejected ${o}:${r} \u2014 ${l}`))}const u=a.images?.[0]?.imageId?.imageDigest;return u?n(u):s(new Error(`ECR BatchGetImage returned no digest for ${o}:${r}`))}catch(i){return s(new Error(`ECR digest lookup failed for ${o}:${r} \u2014 ${I(i)}`))}}}export{G as EcrImageInspectorService,R as stripTagOrDigest};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type RegisterTaskDefinitionCommandInput, type TaskDefinition } from "@aws-sdk/client-ecs";
|
|
2
|
+
import type { AwsProvider } from "../../aws/AwsProvider.js";
|
|
3
|
+
import { type Result } from "@fjall/generator";
|
|
4
|
+
import { BaseServiceError } from "../../types/errors/ServiceError.js";
|
|
5
|
+
export declare class EcsError extends BaseServiceError {
|
|
6
|
+
readonly errorType: "service_not_found" | "cluster_not_found" | "deployment_failed" | "deployment_timeout" | "task_definition_failed" | "permission_denied" | "network_error" | "unknown";
|
|
7
|
+
readonly resourceArn?: string | undefined;
|
|
8
|
+
constructor(message: string, errorType: "service_not_found" | "cluster_not_found" | "deployment_failed" | "deployment_timeout" | "task_definition_failed" | "permission_denied" | "network_error" | "unknown", resourceArn?: string | undefined, details?: unknown, recoverable?: boolean);
|
|
9
|
+
}
|
|
10
|
+
export interface ECSServiceInfo {
|
|
11
|
+
serviceArn?: string;
|
|
12
|
+
serviceName?: string;
|
|
13
|
+
clusterArn?: string;
|
|
14
|
+
status?: string;
|
|
15
|
+
desiredCount?: number;
|
|
16
|
+
runningCount?: number;
|
|
17
|
+
pendingCount?: number;
|
|
18
|
+
deployments?: Array<{
|
|
19
|
+
status?: string;
|
|
20
|
+
desiredCount?: number;
|
|
21
|
+
runningCount?: number;
|
|
22
|
+
pendingCount?: number;
|
|
23
|
+
failedTasks?: number;
|
|
24
|
+
rolloutState?: string;
|
|
25
|
+
rolloutStateReason?: string;
|
|
26
|
+
}>;
|
|
27
|
+
events?: Array<{
|
|
28
|
+
message?: string;
|
|
29
|
+
createdAt?: Date;
|
|
30
|
+
}>;
|
|
31
|
+
taskDefinition?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ECSClusterInfo {
|
|
34
|
+
clusterArn?: string;
|
|
35
|
+
clusterName?: string;
|
|
36
|
+
status?: string;
|
|
37
|
+
registeredContainerInstancesCount?: number;
|
|
38
|
+
runningTasksCount?: number;
|
|
39
|
+
pendingTasksCount?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface DeploymentStatus {
|
|
42
|
+
deploymentCompleted: boolean;
|
|
43
|
+
deploymentFailed: boolean;
|
|
44
|
+
runningCount: number;
|
|
45
|
+
desiredCount: number;
|
|
46
|
+
pendingCount: number;
|
|
47
|
+
deploymentCount: number;
|
|
48
|
+
message: string;
|
|
49
|
+
percentComplete?: number;
|
|
50
|
+
latestEvent?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ECSDeploymentOptions {
|
|
53
|
+
clusterArn: string;
|
|
54
|
+
serviceArn: string;
|
|
55
|
+
waitForCompletion?: boolean;
|
|
56
|
+
maxWaitTime?: number;
|
|
57
|
+
pollInterval?: number;
|
|
58
|
+
progressCallback?: (status: DeploymentStatus) => void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* ECS service using the AwsProvider interface.
|
|
62
|
+
*
|
|
63
|
+
* Constructor-injected provider matches the CloudFormationService pattern:
|
|
64
|
+
* deploy-core receives a pre-configured AwsProvider and never lazily resolves
|
|
65
|
+
* credentials. CLI passes its AwsContext (which implements AwsProvider);
|
|
66
|
+
* the worker passes SimpleAwsProvider.
|
|
67
|
+
*/
|
|
68
|
+
export declare class EcsService {
|
|
69
|
+
private aws;
|
|
70
|
+
constructor(aws: AwsProvider);
|
|
71
|
+
private getClient;
|
|
72
|
+
updateService(clusterArn: string, serviceArn: string, options?: {
|
|
73
|
+
desiredCount?: number;
|
|
74
|
+
taskDefinition?: string;
|
|
75
|
+
forceNewDeployment?: boolean;
|
|
76
|
+
}): Promise<Result<void, EcsError>>;
|
|
77
|
+
describeServices(clusterArn: string, serviceArns: string[]): Promise<Result<ECSServiceInfo[], EcsError>>;
|
|
78
|
+
getDeploymentStatus(clusterArn: string, serviceArn: string): Promise<Result<DeploymentStatus, EcsError>>;
|
|
79
|
+
isDeploying(clusterArn: string, serviceArn: string): Promise<Result<boolean, EcsError>>;
|
|
80
|
+
getServiceEvents(clusterArn: string, serviceArn: string): Promise<Result<Array<{
|
|
81
|
+
message: string;
|
|
82
|
+
timestamp: Date;
|
|
83
|
+
}>, EcsError>>;
|
|
84
|
+
pollDeployment(options: ECSDeploymentOptions): Promise<Result<void, EcsError>>;
|
|
85
|
+
private processDeploymentEvents;
|
|
86
|
+
listClusters(): Promise<Result<string[], EcsError>>;
|
|
87
|
+
describeClusters(clusterArns: string[]): Promise<Result<ECSClusterInfo[], EcsError>>;
|
|
88
|
+
listServices(clusterArn: string): Promise<Result<string[], EcsError>>;
|
|
89
|
+
registerTaskDefinition(taskDefinition: RegisterTaskDefinitionCommandInput): Promise<Result<string, EcsError>>;
|
|
90
|
+
getLatestTaskDefinition(family: string): Promise<Result<TaskDefinition | undefined, EcsError>>;
|
|
91
|
+
deployService(clusterArn: string, serviceArn: string, options?: {
|
|
92
|
+
taskDefinition?: string;
|
|
93
|
+
waitForCompletion?: boolean;
|
|
94
|
+
progressCallback?: (status: DeploymentStatus) => void;
|
|
95
|
+
}): Promise<Result<void, EcsError>>;
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ECSClient as E,UpdateServiceCommand as $,DescribeServicesCommand as D,DescribeClustersCommand as T,ListServicesCommand as F,ListClustersCommand as R,RegisterTaskDefinitionCommand as M,DescribeTaskDefinitionCommand as b}from"@aws-sdk/client-ecs";import{success as c,failure as l}from"@fjall/generator";import{BaseServiceError as N}from"../../types/errors/ServiceError.js";import{logger as h}from"@fjall/util/logger";import{getErrorMessage as g,maskSensitiveOutput as f,sleep as _}from"@fjall/util";import{getAwsErrorDetails as I}from"../../aws/utils/awsErrorHandler.js";class u extends N{errorType;resourceArn;constructor(n,t,e,s,i=!1){super(`ECS_${t.toUpperCase()}`,n,s,i),this.errorType=t,this.resourceArn=e}}class q{aws;constructor(n){this.aws=n}getClient(){return this.aws.getClient(E)}async updateService(n,t,e){try{const s=this.getClient(),i=new $({cluster:n,service:t,forceNewDeployment:e?.forceNewDeployment!==!1,desiredCount:e?.desiredCount,taskDefinition:e?.taskDefinition});return h.debug("EcsService",`UpdateService request: cluster=${n}, service=${t}, forceNewDeployment=${e?.forceNewDeployment!==!1}, taskDefinition=${e?.taskDefinition??"<unchanged>"}`),await s.send(i),c(void 0)}catch(s){const{message:i,name:o,httpCode:r}=I(s),a=f(i);h.error("EcsService",`UpdateService failed: ${o} - ${a}`,{clusterArn:n,serviceArn:t,httpCode:r,error:s});let m=`Failed to update ECS service: ${a}`;return r&&(m+=` (HTTP ${r})`),o!=="Error"&&o!=="Unknown"&&(m+=` [${o}]`),l(new u(m,"unknown",t,{clusterArn:n,error:s,httpCode:r,errorName:o},!1))}}async describeServices(n,t){try{const e=this.getClient(),s=new D({cluster:n,services:t,include:["TAGS"]}),i=await e.send(s);if(!i.services)return c([]);const o=i.services.map(r=>({serviceArn:r.serviceArn,serviceName:r.serviceName,clusterArn:r.clusterArn,status:r.status,desiredCount:r.desiredCount,runningCount:r.runningCount,pendingCount:r.pendingCount,deployments:r.deployments,events:r.events,taskDefinition:r.taskDefinition}));return c(o)}catch(e){const s=f(g(e));return l(new u(`Failed to describe services: ${s}`,"unknown",void 0,{clusterArn:n,serviceArns:t,error:e},!1))}}async getDeploymentStatus(n,t){const e=await this.describeServices(n,[t]);if(!e.success)return l(e.error);const s=e.data;if(s.length===0)return l(new u(`Service ${t} not found in cluster ${n}`,"service_not_found",t,{clusterArn:n}));const i=s[0],o=i.deployments||[],r=o.find(p=>p.status==="PRIMARY");if(!r)return c({deploymentCompleted:!1,deploymentFailed:!0,runningCount:0,desiredCount:0,pendingCount:0,deploymentCount:o.length,message:"No primary deployment found",percentComplete:0});const a=r.desiredCount||0,m=r.runningCount||0,C=r.pendingCount||0,y=a>0?Math.round(m/a*100):0,k=o.length===1&&m===a&&C===0,w=(r.failedTasks??0)>0||r.rolloutState==="FAILED";let d=`Running: ${m}/${a}`;if(d+=`, Pending: ${C}`,o.length>1&&(d+=` (${o.length} deployments active)`),w){const p=r.rolloutStateReason||(r.failedTasks?`${r.failedTasks} task(s) failed to start`:void 0);p&&(d+=` \u2014 ${p}`)}const v=i.events&&i.events.length>0?i.events[0]?.message:void 0;return c({deploymentCompleted:k,deploymentFailed:w,runningCount:m,desiredCount:a,pendingCount:C,deploymentCount:o.length,message:d,percentComplete:y,latestEvent:v})}async isDeploying(n,t){const e=await this.getDeploymentStatus(n,t);if(!e.success)return h.debug("EcsService","Could not determine deployment status, assuming not deploying",{error:e.error.message}),c(!1);const s=e.data;return c(!s.deploymentCompleted&&!s.deploymentFailed)}async getServiceEvents(n,t){try{const e=this.getClient(),s=new D({cluster:n,services:[t],include:["TAGS"]}),i=await e.send(s);if(!i.services||i.services.length===0)return c([]);const r=(i.services[0].events||[]).map(a=>({message:a.message||"",timestamp:a.createdAt||new Date}));return c(r)}catch(e){const s=f(g(e));return l(new u(`Failed to get service events: ${s}`,"unknown",t,{clusterArn:n,error:e}))}}async pollDeployment(n){const{clusterArn:t,serviceArn:e,waitForCompletion:s=!0,maxWaitTime:i=6e5,pollInterval:o=5e3,progressCallback:r}=n;if(!s)return c(void 0);const a=Date.now(),m=new Date(a-5e3);let C=null,y="";const k=new Set;for(;Date.now()-a<i;){const w=await this.getDeploymentStatus(t,e);if(!w.success)return l(w.error);const d=w.data,v=await this.getServiceEvents(t,e);if(v.success){const p=this.processDeploymentEvents(v.data,d,m,C,k,r);C=p.lastEventTimestamp,p.eventsReported?y=d.message:d.message!==y&&(r?.(d),y=d.message)}if(d.deploymentCompleted)return c(void 0);if(d.deploymentFailed)return l(new u("ECS deployment failed. Check the ECS console for detailed error information","deployment_failed",e,{clusterArn:t,status:d}));await _(o)}return l(new u(`ECS deployment timed out after ${i/1e3} seconds. The deployment is still in progress. Check the ECS console for status`,"deployment_timeout",e,{clusterArn:t,maxWaitTime:i}))}processDeploymentEvents(n,t,e,s,i,o){const r=n.filter(a=>!(a.timestamp<e||s&&a.timestamp<=s||i.has(a.message)));if(r.length===0)return{lastEventTimestamp:s,eventsReported:!1};for(const a of r)i.add(a.message),o?.({...t,latestEvent:a.message});return{lastEventTimestamp:r[r.length-1].timestamp,eventsReported:!0}}async listClusters(){try{const n=this.getClient(),t=new R({}),e=await n.send(t);return c(e.clusterArns||[])}catch(n){const t=f(g(n));return l(new u(`Failed to list clusters: ${t}`,"permission_denied",void 0,n))}}async describeClusters(n){try{const t=this.getClient(),e=new T({clusters:n}),s=await t.send(e);if(!s.clusters)return c([]);const i=s.clusters.map(o=>({clusterArn:o.clusterArn,clusterName:o.clusterName,status:o.status,registeredContainerInstancesCount:o.registeredContainerInstancesCount,runningTasksCount:o.runningTasksCount,pendingTasksCount:o.pendingTasksCount}));return c(i)}catch(t){const e=f(g(t));return l(new u(`Failed to describe clusters: ${e}`,"unknown",void 0,{clusterArns:n,error:t},!1))}}async listServices(n){try{const t=this.getClient(),e=new F({cluster:n}),s=await t.send(e);return c(s.serviceArns||[])}catch(t){const e=f(g(t));return l(new u(`Failed to list services: ${e}`,"unknown",n,{clusterArn:n,error:t},!1))}}async registerTaskDefinition(n){try{const t=this.getClient(),e=new M(n),s=await t.send(e);return s.taskDefinition?.taskDefinitionArn?c(s.taskDefinition.taskDefinitionArn):l(new u("Task definition registration failed - no ARN returned","task_definition_failed",void 0,n))}catch(t){const e=f(g(t));return l(new u(`Failed to register task definition: ${e}`,"task_definition_failed",void 0,{taskDefinition:n,error:t},!1))}}async getLatestTaskDefinition(n){try{const t=this.getClient(),e=new b({taskDefinition:n}),s=await t.send(e);return c(s.taskDefinition)}catch(t){const e=f(g(t));return l(new u(`Failed to get task definition: ${e}`,"unknown",void 0,{family:n,error:t},!1))}}async deployService(n,t,e){const s=await this.updateService(n,t,{forceNewDeployment:!0,taskDefinition:e?.taskDefinition});if(!s.success)return l(s.error);if(e?.waitForCompletion!==!1){const i=await this.pollDeployment({clusterArn:n,serviceArn:t,waitForCompletion:!0,progressCallback:e?.progressCallback});if(!i.success)return l(i.error)}return c(void 0)}}export{u as EcsError,q as EcsService};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import type { EcsError } from "./EcsService.js";
|
|
3
|
+
/**
|
|
4
|
+
* Structural shape the resolver consumes. Both deploy-core's
|
|
5
|
+
* `CloudFormationService` and the CLI's wrapper class implement this; typing
|
|
6
|
+
* against the interface lets the CLI pass its instance without an `as never`
|
|
7
|
+
* cast across the package boundary.
|
|
8
|
+
*/
|
|
9
|
+
export interface CfnExportsClient {
|
|
10
|
+
getExportsByNames(names: string[]): Promise<Result<Map<string, string>, {
|
|
11
|
+
message: string;
|
|
12
|
+
}>>;
|
|
13
|
+
listExports(): Promise<Result<Array<{
|
|
14
|
+
Name: string;
|
|
15
|
+
Value: string;
|
|
16
|
+
}>, {
|
|
17
|
+
message: string;
|
|
18
|
+
}>>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolves deployable ECS clusters and services from CloudFormation exports.
|
|
22
|
+
* Constructor-injected with a CFN exports client so the resolver does not
|
|
23
|
+
* pull in singletons from the CLI services barrel.
|
|
24
|
+
*/
|
|
25
|
+
export declare class EcsServiceResolver {
|
|
26
|
+
private cfService;
|
|
27
|
+
constructor(cfService: CfnExportsClient);
|
|
28
|
+
/**
|
|
29
|
+
* Resolve cluster + service ARNs by exact CloudFormation export names
|
|
30
|
+
* derived from manifest data. Avoids substring matching that can cause
|
|
31
|
+
* cross-app collisions when multiple apps share an account.
|
|
32
|
+
*/
|
|
33
|
+
getDeployableClusterAndServicesByManifest(manifestData: {
|
|
34
|
+
clusters: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
services: Array<{
|
|
37
|
+
name: string;
|
|
38
|
+
}>;
|
|
39
|
+
}>;
|
|
40
|
+
}): Promise<Result<{
|
|
41
|
+
clusterArn?: string;
|
|
42
|
+
serviceArns: string[];
|
|
43
|
+
}, EcsError>>;
|
|
44
|
+
/**
|
|
45
|
+
* Fallback resolution by app name using substring matching on
|
|
46
|
+
* CloudFormation exports. Prefer the manifest-based variant when manifest
|
|
47
|
+
* data is available.
|
|
48
|
+
*
|
|
49
|
+
* Export naming convention from infrastructure:
|
|
50
|
+
* Cluster: `${clusterName}DeployableCluster`
|
|
51
|
+
* Service: `${clusterName}${serviceName}DeployableService`
|
|
52
|
+
* where clusterName typically contains the app name (e.g. "StandardsapiCompute").
|
|
53
|
+
*/
|
|
54
|
+
getDeployableClusterAndServices(appName: string): Promise<Result<{
|
|
55
|
+
clusterArn?: string;
|
|
56
|
+
serviceArns: string[];
|
|
57
|
+
}, EcsError>>;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{success as n}from"@fjall/generator";import{logger as l}from"@fjall/util/logger";import{getErrorMessage as v,maskSensitiveOutput as d}from"@fjall/util";import{extractEcsServiceParts as f,buildArn as p}from"../../aws/utils/arnParser.js";class E{cfService;constructor(o){this.cfService=o}async getDeployableClusterAndServicesByManifest(o){try{const r=[];for(const e of o.clusters){r.push(`${e.name}DeployableCluster`);for(const s of e.services)r.push(`${e.name}${s.name}DeployableService`)}if(r.length===0)return n({serviceArns:[]});const i=await this.cfService.getExportsByNames(r);if(!i.success)return l.debug("EcsServiceResolver","Failed to get CloudFormation exports by name",{error:i.error.message}),n({serviceArns:[]});const u=i.data;let c;for(const e of o.clusters){const s=u.get(`${e.name}DeployableCluster`);if(s){c=s;break}}const t=[];for(const e of o.clusters)for(const s of e.services){const a=u.get(`${e.name}${s.name}DeployableService`);a&&t.push(a)}return l.debug("EcsServiceResolver",`Manifest-based discovery: found ${t.length} service(s), cluster=${c||"not found"}`),n({clusterArn:c,serviceArns:t})}catch(r){return l.warn("EcsServiceResolver","Manifest-based discovery failed",{error:d(v(r))}),n({serviceArns:[]})}}async getDeployableClusterAndServices(o){try{const r=await this.cfService.listExports();if(!r.success)return l.debug("EcsServiceResolver","Failed to list CloudFormation exports",{error:r.error.message}),n({serviceArns:[]});const i=o.toLowerCase(),c=r.data.filter(e=>{const s=e.Name?.toLowerCase()??"";return s.includes(i)&&s.endsWith("deployableservice")}).map(e=>e.Value).filter(e=>e!==void 0);let t;if(c.length>0){const e=f(c[0]);e&&(t=p("ecs",e.region,e.accountId,`cluster/${e.clusterName}`))}return t||(t=r.data.find(s=>{const a=s.Name?.toLowerCase()??"";return a.includes(i)&&a.endsWith("deployablecluster")})?.Value),l.debug("EcsServiceResolver",`Found ${c.length} deployable service(s) for app "${o}" in cluster ${t||"unknown"}`),n({clusterArn:t,serviceArns:c})}catch(r){return l.warn("EcsServiceResolver","Failed to get deployable services",{error:d(v(r))}),n({serviceArns:[]})}}}export{E as EcsServiceResolver};
|
|
@@ -4,6 +4,4 @@ import type { CdkContext } from "./CdkServiceTypes.js";
|
|
|
4
4
|
export declare const STACK_DETECTION_FALLBACK_MS = 5000;
|
|
5
5
|
export declare function resolveStackName(stackPattern: string | undefined, context: DeploymentContext): string | undefined;
|
|
6
6
|
export declare function getFallbackStackName(context: DeploymentContext): string;
|
|
7
|
-
export declare function buildDeploymentCdkContext(context: DeploymentContext, accountId: string | undefined, region: string
|
|
8
|
-
includeImageVersion?: boolean;
|
|
9
|
-
}): CdkContext;
|
|
7
|
+
export declare function buildDeploymentCdkContext(context: DeploymentContext, accountId: string | undefined, region: string): CdkContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getApplicationStackName as
|
|
1
|
+
import{getApplicationStackName as i,getOrganisationStackName as d,isApplicationStack as p}from"../../types/operations.js";const c=5e3;function u(o,a){if(o&&!o.includes("*"))return o;if(o){const e=o.match(/\*?(\w+)\*?/);if(e?.[1]){const n=e[1],r=a.target;return p(n)?i(r,n):`${r}${n}`}return o}}function g(o){const a=o.deployType;return a==="organisation"||a==="platform"||a==="account"?d(a):`${o.target}Network`}function l(o,a,e){return{accountId:a,region:e,managedAccount:o.isManagedAccount,accountName:o.accountName,orgId:o.orgId,rootId:o.rootId,managementAccountId:o.managementAccountId,ipamPoolId:o.ipamPoolId,fjallOrgId:o.fjallOrgId,fjallOidcConfigured:o.fjallOidcConfigured?"true":void 0,orgConfig:o.orgConfig}}export{c as STACK_DETECTION_FALLBACK_MS,l as buildDeploymentCdkContext,g as getFallbackStackName,u as resolveStackName};
|
|
@@ -8,3 +8,6 @@ export { CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS } from
|
|
|
8
8
|
export { isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext } from "./CdkErrorFormatter.js";
|
|
9
9
|
export { hasCdkDifferences, parseDiffOutput, type DiffDetails } from "./CdkOutputParser.js";
|
|
10
10
|
export { CloudFormationService, CloudFormationError, type CloudFormationCallbacks } from "./CloudFormationService.js";
|
|
11
|
+
export { EcsService, EcsError, type ECSServiceInfo, type ECSClusterInfo, type DeploymentStatus, type ECSDeploymentOptions } from "./EcsService.js";
|
|
12
|
+
export { EcsServiceResolver, type CfnExportsClient } from "./EcsServiceResolver.js";
|
|
13
|
+
export { EcrImageInspectorService, stripTagOrDigest } from "./EcrImageInspectorService.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CdkService as
|
|
1
|
+
import{CdkService as e}from"./CdkService.js";import{CdkArgumentBuilder as i}from"./CdkArgumentBuilder.js";import{CdkProcessManager as f}from"./CdkProcessManager.js";import{CdkEventMonitor as s,startStackMonitoring as p,DEFAULT_DEPLOY_TIMEOUT_MS as a}from"./CdkEventMonitoring.js";import{isCdkError as E,formatInfrastructureError as u,getStructuralHint as x,getSourceContext as d}from"./CdkErrorFormatter.js";import{hasCdkDifferences as S,parseDiffOutput as g}from"./CdkOutputParser.js";import{CloudFormationService as v,CloudFormationError as l}from"./CloudFormationService.js";import{EcsService as M,EcsError as I}from"./EcsService.js";import{EcsServiceResolver as T}from"./EcsServiceResolver.js";import{EcrImageInspectorService as _,stripTagOrDigest as A}from"./EcrImageInspectorService.js";export{i as CdkArgumentBuilder,s as CdkEventMonitor,f as CdkProcessManager,e as CdkService,l as CloudFormationError,v as CloudFormationService,a as DEFAULT_DEPLOY_TIMEOUT_MS,_ as EcrImageInspectorService,I as EcsError,M as EcsService,T as EcsServiceResolver,u as formatInfrastructureError,d as getSourceContext,x as getStructuralHint,S as hasCdkDifferences,E as isCdkError,g as parseDiffOutput,p as startStackMonitoring,A as stripTagOrDigest};
|
|
@@ -24,12 +24,12 @@ export declare class CdkContextBuilder {
|
|
|
24
24
|
isManagedAccount?: boolean;
|
|
25
25
|
accountName?: string;
|
|
26
26
|
logPath?: string;
|
|
27
|
-
imageVersion?: string;
|
|
28
27
|
orgId?: string;
|
|
29
28
|
rootId?: string;
|
|
30
29
|
managementAccountId?: string;
|
|
31
30
|
ipamPoolId?: string;
|
|
32
31
|
fjallOrgId?: string;
|
|
32
|
+
fjallOidcConfigured?: boolean;
|
|
33
33
|
orgConfig?: string;
|
|
34
34
|
}, options: {
|
|
35
35
|
verbose?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DEFAULT_REGION as t}from"@fjall/generator";class
|
|
1
|
+
import{DEFAULT_REGION as t}from"@fjall/generator";class l{static buildDeploymentContext(o,a,e){return{deployType:o.deployType,target:o.target,path:o.path,options:a,stackOutputs:o.stackOutputs||{},callerIdentity:o.callerIdentity,region:o.region||e?.primaryRegion||t,isManagedAccount:o.isManagedAccount,accountName:o.accountName,logPath:o.logPath,orgId:o.orgId,rootId:o.rootId,managementAccountId:o.managementAccountId,ipamPoolId:o.ipamPoolId,fjallOrgId:o.fjallOrgId,fjallOidcConfigured:o.fjallOidcConfigured,orgConfig:o.orgConfig}}static updateContext(o,a){return{...o,...a}}}export{l as CdkContextBuilder};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{APPLICATION_STACKS as n,APPLICATION_DEPLOY_ORDER as
|
|
1
|
+
import{APPLICATION_STACKS as n,APPLICATION_DEPLOY_ORDER as A,getApplicationStepName as a,getApplicationStepId as t}from"../types/index.js";import{STEP_IDS as e,STEP_NAMES as s,INFRA_STEP_NAME as c}from"../types/index.js";const O={Network:e.NETWORK_DESTROY,Storage:e.STORAGE_DESTROY,Messaging:e.MESSAGING_DESTROY,Database:e.DATABASE_DESTROY,Compute:e.COMPUTE_DESTROY,Cdn:e.CDN_DESTROY};function S(f){return O[f]??`${f.toLowerCase()}-destroy`}class g{static DEPLOYMENT_STEPS=new Map([["application-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.DETECT_CONFIG,name:s.PREPARE_DEPLOY},{id:e.BOOTSTRAP,name:s.BOOTSTRAP,conditions:{requiresInfra:!0}},{id:t(n.NETWORK,"deploy"),name:a(n.NETWORK,"deploy"),conditions:{requiresInfra:!0,requiresNetwork:!0}},{id:t(n.STORAGE,"deploy"),name:a(n.STORAGE,"deploy"),conditions:{requiresInfra:!0,requiresStorage:!0}},{id:t(n.MESSAGING,"deploy"),name:a(n.MESSAGING,"deploy"),conditions:{requiresInfra:!0,requiresMessaging:!0}},{id:t(n.DATABASE,"deploy"),name:a(n.DATABASE,"deploy"),conditions:{requiresInfra:!0,requiresDatabase:!0}},{id:e.DOCKER_OPERATIONS,name:"Docker operations",conditions:{requiresDocker:!0,requiresCompute:!0,skipForInfraOnly:!0}},{id:e.TAG_ECR_IMAGES,name:"Tagging container images",conditions:{requiresImageTagging:!0,requiresCompute:!0,skipForInfraOnly:!0}},{id:t(n.COMPUTE,"deploy"),name:a(n.COMPUTE,"deploy"),conditions:{requiresInfra:!0,requiresCompute:!0}},{id:t(n.CDN,"deploy"),name:a(n.CDN,"deploy"),conditions:{requiresInfra:!0,requiresCdn:!0}}]],["application-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.DETECT_CONFIG,name:s.CHECK_INFRA_STATE},{id:t(n.CDN,"destroy"),name:a(n.CDN,"destroy"),conditions:{requiresCdn:!0}},{id:t(n.COMPUTE,"destroy"),name:a(n.COMPUTE,"destroy")},{id:t(n.DATABASE,"destroy"),name:a(n.DATABASE,"destroy")},{id:t(n.MESSAGING,"destroy"),name:a(n.MESSAGING,"destroy"),conditions:{requiresMessaging:!0}},{id:t(n.STORAGE,"destroy"),name:a(n.STORAGE,"destroy"),conditions:{requiresStorage:!0}},{id:t(n.NETWORK,"destroy"),name:a(n.NETWORK,"destroy")}]],["organisation-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.ORG_SETUP,name:"Configuring organisation"},{id:e.PREPARE,name:s.PREPARE_DEPLOY},{id:e.BOOTSTRAP,name:s.BOOTSTRAP},{id:e.DEPLOY,name:"Deploying organisation",conditions:{requiresOrgChanges:!0}},{id:e.CASCADE_PLATFORM,name:"Deploying platform",conditions:{requiresPlatformAccount:!0,requiresPlatformChanges:!0}},{id:e.CASCADE_DOMAINS,name:"Deploying domains",conditions:{requiresDomainConfiguration:!0,requiresDomainChanges:!0}},{id:e.CASCADE_ACCOUNTS,name:"Deploying accounts",conditions:{requiresMemberAccounts:!0,requiresAccountChanges:!0}}]],["organisation-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:s.CHECK_INFRA_STATE},{id:e.CASCADE_ACCOUNTS,name:"Destroying account infrastructure",conditions:{requiresMemberAccounts:!0}},{id:e.CASCADE_PLATFORM,name:"Destroying platform infrastructure",conditions:{requiresPlatformAccount:!0}},{id:e.DESTROY,name:"Destroying organisation infrastructure"}]],["platform-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.CONNECT,name:c.CONNECT},{id:e.PREPARE_ENVIRONMENT,name:c.PREPARE},{id:e.DEPLOY,name:c.DEPLOY},{id:e.MONITORING,name:c.MONITORING}]],["platform-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:"Checking infrastructure state"},{id:e.ORG_DESTROY,name:"Destroying platform infrastructure"}]],["account-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.CONNECT,name:c.CONNECT},{id:e.PREPARE_ENVIRONMENT,name:c.PREPARE},{id:e.DEPLOY,name:c.DEPLOY},{id:e.MONITORING,name:c.MONITORING}]],["account-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:"Checking infrastructure state"},{id:e.ORG_DESTROY,name:"Destroying account infrastructure"}]]]);static getSteps(r){if(r.deploymentType==="application"&&r.operation==="deploy"&&r.deployOnly)return[{id:e.AUTH,name:s.AUTH},{id:e.DOCKER_OPERATIONS,name:"Build and push Docker container"},{id:t(n.COMPUTE,"deploy"),name:a(n.COMPUTE,"deploy")}];const o=`${r.deploymentType}-${r.operation}`;return(this.DEPLOYMENT_STEPS.get(o)??[]).filter(i=>{if(r.deploymentType==="application"){const E=r.builderName==="opennext";if(i.conditions?.requiresInfra&&r.deployOnly)return!1;if(r.resources){const d=r.resources;if(i.conditions?.requiresNetwork&&!d.hasNetwork||i.conditions?.requiresCompute&&!d.hasCompute||i.conditions?.requiresDatabase&&!d.hasDatabase||i.conditions?.requiresStorage&&!d.hasStorage||i.conditions?.requiresMessaging&&!d.hasMessaging||i.conditions?.requiresCdn&&!d.hasCdn)return!1}else if(r.operation==="deploy"&&!E&&(i.conditions?.requiresNetwork||i.conditions?.requiresCompute||i.conditions?.requiresDatabase||i.conditions?.requiresStorage||i.conditions?.requiresMessaging||i.conditions?.requiresCdn))return!1;if(i.conditions?.requiresDocker||i.id===e.DOCKER_OPERATIONS)return E||r.infraOnly||r.resources&&!r.resources.hasCompute?!1:r.deployOnly?r.hasDockerfile===!0:r.hasDockerfile===!0||r.hasDockerfile===!1&&!r.isManagedAccount;if(i.conditions?.requiresImageTagging)return r.infraOnly||E||r.resources&&!r.resources.hasCompute?!1:r.hasDockerfile===!1}return!(i.conditions?.requiresMemberAccounts&&!r.hasMemberAccounts||i.conditions?.requiresPlatformAccount&&!r.hasPlatformAccount||i.conditions?.requiresOrgChanges&&r.hasOrgChanges!==!0||i.conditions?.requiresPlatformChanges&&r.hasPlatformChanges!==!0||i.conditions?.requiresAccountChanges&&r.hasAccountChanges!==!0||i.conditions?.requiresDomainConfiguration&&!r.hasDomainConfiguration||i.conditions?.requiresDomainChanges&&r.hasDomainChanges!==!0)}).map(i=>i.id===e.DOCKER_OPERATIONS?{...i,name:r.hasDockerfile?"Building and pushing Docker image":"Initialising container repository"}:i)}static getStepNames(r){return this.getSteps(r).map(o=>o.name)}static getStepIndex(r,o){return this.getSteps(o).findIndex(l=>l.id===r)}static getStepById(r,o="application"){const u=`${o}-deploy`,i=(this.DEPLOYMENT_STEPS.get(u)||[]).find(T=>T.id===r);if(i)return i;const E=`${o}-destroy`;return(this.DEPLOYMENT_STEPS.get(E)||[]).find(T=>T.id===r)}static isStepIncluded(r,o){return this.getSteps(o).some(l=>l.id===r)}static createContext(r,o,u){return{deploymentType:r,operation:o,deployOnly:u?.deployOnly??!1,infraOnly:u?.infraOnly??!1,isManagedAccount:u?.isManagedAccount??!1,hasDockerfile:u?.hasDockerfile??!1,pattern:u?.pattern??null,builderName:u?.builderName,resources:u?.resources}}static getDeploymentTypes(){return["application","organisation","platform","account"]}static getStackNames(r){switch(r){case"application":return[...A];case"organisation":return["Organisation"];case"platform":return["Platform"];case"account":return["Account"];default:return[]}}static getInfraStepIds(){return[e.CFN_CHECK,e.BOOTSTRAP,e.DIFF,e.NETWORK,e.STORAGE,e.MESSAGING,e.DATABASE,e.COMPUTE,e.CDN]}static getDockerStepIds(){return[e.ECR_INIT,e.DOCKER_DEPLOY]}}export{g as StepRegistry,S as getDestroyStepId};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{z as e}from"zod";import{randomBytes as
|
|
1
|
+
import{z as e}from"zod";import{randomBytes as u}from"crypto";import{readFile as d,writeFile as m,unlink as c,rename as f,mkdir as h}from"fs/promises";import{dirname as S,join as g}from"path";import{fileExists as y}from"@fjall/util/fsHelpers";import{logger as i}from"@fjall/util/logger";import{getErrorMessage as s,maskSensitiveOutput as w}from"@fjall/util";const F=e.object({hash:e.string(),deployedAt:e.string(),stackStatus:e.string().optional()}).strict(),j=e.object({version:e.literal(1),lastDeployedAt:e.string().optional(),templateHashes:e.record(e.string(),F),metadata:e.record(e.string(),e.unknown()).optional()}).strict(),E=".fjall-state.json";function l(r){return g(r,E)}async function D(r){const a=l(r);if(!await y(a))return null;try{const t=await d(a,"utf-8"),n=JSON.parse(t),o=j.safeParse(n);return o.success?o.data:null}catch(t){return i.debug("FjallState","Failed to read state file",{path:a,error:s(t)}),null}}async function N(r,a){const t=l(r),n=`${t}.${Date.now()}-${u(4).toString("hex")}.tmp`;await h(S(t),{recursive:!0});try{await m(n,JSON.stringify(a,null,2),"utf-8"),await f(n,t)}catch(o){try{await c(n)}catch(p){i.debug("FjallState","Temp file cleanup failed (non-fatal)",{path:n,error:s(p)})}throw o}}function k(){return{version:1,templateHashes:{}}}async function v(r){const a=l(r);try{await c(a)}catch(t){(typeof t=="object"&&t!==null&&"code"in t?t.code:void 0)!=="ENOENT"&&i.warn("FjallState","Failed to delete state file",{path:a,error:w(s(t))})}}function I(r,a,t,n){return{...r,lastDeployedAt:new Date().toISOString(),templateHashes:{...r.templateHashes,[a]:{hash:t,deployedAt:new Date().toISOString(),...n!==void 0&&{stackStatus:n}}}}}export{j as FjallStateFileSchema,k as createEmptyState,v as deleteStateFile,l as getStateFilePath,D as readStateFile,I as updateTemplateHash,N as writeStateFile};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BaseServiceError as
|
|
1
|
+
import{BaseServiceError as o}from"../errors/ServiceError.js";import{getErrorMessage as i,maskSensitiveOutput as c}from"@fjall/util";function n(a,e,r,t,p){const s=c(i(p));return new m(`${a}: ${s}`,{errorType:e,appName:r,operation:t,details:p})}class m extends o{errorType;appName;operation;stackType;constructor(e,r){super(`APPLICATION_${r.errorType.toUpperCase()}`,e,r.details,r.recoverable??!1),this.errorType=r.errorType,this.appName=r.appName,this.operation=r.operation,this.stackType=r.stackType}}export{m as ApplicationError,n as wrapApplicationError};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProgressEvent, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase } from "./events.js";
|
|
1
|
+
import type { ProgressEvent, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent } from "./events.js";
|
|
2
2
|
import type { DetectionResult } from "./detection.js";
|
|
3
3
|
export type StepCompleteStatus = "completed" | "skipped" | "error";
|
|
4
4
|
/**
|
|
@@ -47,16 +47,53 @@ export interface DeployCallbacks {
|
|
|
47
47
|
*/
|
|
48
48
|
onDockerProgress?: (message: string, percentage?: number, layerId?: string, status?: string) => void;
|
|
49
49
|
/**
|
|
50
|
-
* @emittedBy
|
|
51
|
-
*
|
|
52
|
-
|
|
50
|
+
* @emittedBy engine — fired when the build+push pipeline begins for a service.
|
|
51
|
+
* Code-only deploys; full deploys still emit onDockerProgress only.
|
|
52
|
+
*/
|
|
53
|
+
onBuildPushStart?: (event: BuildPushStartEvent) => void;
|
|
54
|
+
/** @emittedBy engine — granular build+push progress for a service. */
|
|
55
|
+
onBuildPushProgress?: (event: BuildPushProgressEvent) => void;
|
|
56
|
+
/**
|
|
57
|
+
* @emittedBy engine — fired after a successful ECR push, with the digest of
|
|
58
|
+
* the just-pushed image. The digest is the immutable identity used to pin
|
|
59
|
+
* the task definition revision; consumers persist it to the version registry.
|
|
60
|
+
*/
|
|
61
|
+
onBuildPushComplete?: (event: BuildPushCompleteEvent) => void;
|
|
62
|
+
/**
|
|
63
|
+
* @emittedBy engine — fired after a new ECS task definition revision has
|
|
64
|
+
* been registered with the digest-pinned image. The new ARN supersedes the
|
|
65
|
+
* one captured by `previousTaskDefinitionArn` once UpdateService accepts it.
|
|
66
|
+
*/
|
|
67
|
+
onTaskDefRegistered?: (event: TaskDefRegisteredEvent) => void;
|
|
68
|
+
/**
|
|
69
|
+
* @emittedBy engine — ECS rolling update status. Re-annotated 2026-04-30
|
|
70
|
+
* (was caller-owned). The new code-only orchestrator and the future
|
|
71
|
+
* full-deploy ECS-tail step both emit from inside deploy-core; the CLI
|
|
72
|
+
* adapter and the webapp worker adapter consume identically.
|
|
53
73
|
*/
|
|
54
74
|
onECSUpdate?: (status: string, service?: string) => void;
|
|
55
75
|
/**
|
|
56
|
-
* @emittedBy
|
|
57
|
-
*
|
|
76
|
+
* @emittedBy engine — ECS rolling update progress with optional percentage.
|
|
77
|
+
* Re-annotated 2026-04-30 alongside `onECSUpdate`.
|
|
58
78
|
*/
|
|
59
79
|
onECSProgress?: (message: string, percentage?: number) => void;
|
|
80
|
+
/**
|
|
81
|
+
* @emittedBy engine — fired when the ECS service reaches a terminal state
|
|
82
|
+
* for a code-only deploy (stable or failed). `success` is true only when
|
|
83
|
+
* the rollout reached the desired count without rolloutState=FAILED.
|
|
84
|
+
*/
|
|
85
|
+
onECSComplete?: (event: ECSCompleteEvent) => void;
|
|
86
|
+
/**
|
|
87
|
+
* @emittedBy engine — fired when a migrations RunTask is dispatched.
|
|
88
|
+
* Code-only deploys with a `migrations` role in the manifest run migrations
|
|
89
|
+
* before any service rollout begins.
|
|
90
|
+
*/
|
|
91
|
+
onMigrationsStart?: (event: MigrationsStartEvent) => void;
|
|
92
|
+
/**
|
|
93
|
+
* @emittedBy engine — fired when the migrations task reaches STOPPED.
|
|
94
|
+
* `success` is true only when `lastStatus.exitCode === 0`.
|
|
95
|
+
*/
|
|
96
|
+
onMigrationsComplete?: (event: MigrationsCompleteEvent) => void;
|
|
60
97
|
/** @emittedBy engine */
|
|
61
98
|
onCDKBootstrap?: (status: string) => void;
|
|
62
99
|
/** @emittedBy engine */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
class a extends Error{code;details;recoverable;cause;constructor(e,t,r,o=!1,
|
|
1
|
+
import{maskSensitiveOutput as c}from"@fjall/util";class a extends Error{code;details;recoverable;cause;constructor(e,t,r,o=!1,n){super(t),this.code=e,this.details=r,this.recoverable=o,this.cause=n,this.name=this.constructor.name,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}}class l extends a{fields;rules;constructor(e,t,r,o){super("VALIDATION_ERROR",e,o,!1),this.fields=t,this.rules=r}}class p extends a{account;reason;constructor(e,t,r,o){super("AUTH_ERROR",e,o,!1),this.account=t,this.reason=r}}class h extends a{service;operation;awsErrorCode;constructor(e,t,r,o,n,i=!1){super("AWS_ERROR",e,n,i),this.service=t,this.operation=r,this.awsErrorCode=o}}class E extends a{stackName;deploymentPhase;constructor(e,t,r,o,n=!1){super("DEPLOYMENT_ERROR",e,o,n),this.stackName=t,this.deploymentPhase=r}}class R extends a{endpoint;statusCode;constructor(e,t,r,o){super("NETWORK_ERROR",e,void 0,!0,o),this.endpoint=t,this.statusCode=r}}class d extends a{path;operation;constructor(e,t,r,o){super("FILESYSTEM_ERROR",e,{path:t,operation:r},!1,o),this.path=t,this.operation=r}}class f extends a{configKey;configPath;constructor(e,t,r,o){super("CONFIG_ERROR",e,o,!1),this.configKey=t,this.configPath=r}}function x(s,e="UNKNOWN_ERROR"){return s instanceof a?s:s instanceof Error?new a(e,c(s.message),{name:s.name},!1,s):new a(e,c(String(s)),{rawError:s},!1)}export{p as AuthError,h as AwsError,a as BaseServiceError,f as ConfigError,E as DeploymentError,d as FileSystemError,R as NetworkError,l as ValidationError,x as toServiceError};
|
|
@@ -32,3 +32,56 @@ export interface CascadeDeploymentResult {
|
|
|
32
32
|
}>;
|
|
33
33
|
}
|
|
34
34
|
export type CascadePhase = DeploymentEventCascadePhase;
|
|
35
|
+
export interface BuildPushStartEvent {
|
|
36
|
+
serviceName: string;
|
|
37
|
+
imageTag: string;
|
|
38
|
+
ecrRepositoryUri: string;
|
|
39
|
+
}
|
|
40
|
+
export interface BuildPushProgressEvent {
|
|
41
|
+
serviceName: string;
|
|
42
|
+
message: string;
|
|
43
|
+
percentage?: number;
|
|
44
|
+
layerId?: string;
|
|
45
|
+
status?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface BuildPushCompleteEvent {
|
|
48
|
+
serviceName: string;
|
|
49
|
+
imageTag: string;
|
|
50
|
+
imageDigest: string;
|
|
51
|
+
imageUri: string;
|
|
52
|
+
ecrRepositoryArn: string;
|
|
53
|
+
durationMs: number;
|
|
54
|
+
}
|
|
55
|
+
export interface TaskDefRegisteredEvent {
|
|
56
|
+
serviceName: string;
|
|
57
|
+
taskDefinitionArn: string;
|
|
58
|
+
previousTaskDefinitionArn: string;
|
|
59
|
+
revision: number;
|
|
60
|
+
family: string;
|
|
61
|
+
imageDigest?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface ECSCompleteEvent {
|
|
64
|
+
serviceName: string;
|
|
65
|
+
serviceArn: string;
|
|
66
|
+
success: boolean;
|
|
67
|
+
taskDefinitionArn: string;
|
|
68
|
+
durationMs: number;
|
|
69
|
+
/** ECS rolloutStateReason or terminal error message when success=false. */
|
|
70
|
+
reason?: string;
|
|
71
|
+
finalRunningCount?: number;
|
|
72
|
+
finalDesiredCount?: number;
|
|
73
|
+
}
|
|
74
|
+
export interface MigrationsStartEvent {
|
|
75
|
+
family: string;
|
|
76
|
+
taskDefinitionArn: string;
|
|
77
|
+
imageDigest: string;
|
|
78
|
+
}
|
|
79
|
+
export interface MigrationsCompleteEvent {
|
|
80
|
+
family: string;
|
|
81
|
+
taskDefinitionArn: string;
|
|
82
|
+
success: boolean;
|
|
83
|
+
exitCode?: number;
|
|
84
|
+
durationMs: number;
|
|
85
|
+
/** ECS stoppedReason verbatim when success=false. */
|
|
86
|
+
reason?: string;
|
|
87
|
+
}
|
|
@@ -2,7 +2,7 @@ export { DeploymentEventSchema, DEPLOYMENT_EVENT_TYPES, DEPLOYMENT_EVENT_RESOURC
|
|
|
2
2
|
export type { DeploymentEvent, DeploymentEventType, DeploymentEventResourceCategory, DeploymentEventCascadePhase, DeploymentEventCascadeAccountStatus } from "./deploymentEventSchema.js";
|
|
3
3
|
export type { AwsCredentials, DeployIdentity } from "./credentials.js";
|
|
4
4
|
export type { DeployCallbacks, StepCompleteStatus } from "./callbacks.js";
|
|
5
|
-
export type { ProgressEvent, ProgressEventType, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase } from "./events.js";
|
|
5
|
+
export type { ProgressEvent, ProgressEventType, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent } from "./events.js";
|
|
6
6
|
export type { ApiClientInterface, EntitlementsData } from "./apiClient.js";
|
|
7
7
|
export type { DeployParams, DeployOptions, DeploymentType, DeployResult, DestroyParams, DestroyOptions, DestroyResult } from "./params.js";
|
|
8
8
|
export type { OrgConfig, ProviderAccount, SSOSession } from "./orgConfig.js";
|
|
@@ -62,6 +62,15 @@ export interface DeployOptions {
|
|
|
62
62
|
environment?: string;
|
|
63
63
|
/** Skip OIDC connector deployment in the Account stack — set when the OIDC connector was already provisioned separately (e.g. via the webapp CloudFormation quick-create flow). */
|
|
64
64
|
skipOidc?: boolean;
|
|
65
|
+
/** Restrict a code-only deploy to a single ECS service by name (case-insensitive). */
|
|
66
|
+
serviceName?: string;
|
|
67
|
+
/** Skip the pre-rollout migrations RunTask. Honoured by the code-only orchestrator. */
|
|
68
|
+
skipMigrations?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Roll forward/back to an existing image tag. When set, the orchestrator skips
|
|
71
|
+
* build + push and rolls ECS to `<repo>:<imageTag>`. Implies `deployOnly`.
|
|
72
|
+
*/
|
|
73
|
+
imageTag?: string;
|
|
65
74
|
}
|
|
66
75
|
export type DeploymentType = "application" | "organisation";
|
|
67
76
|
export interface DeployResult {
|
|
@@ -78,6 +87,12 @@ export interface DeployResult {
|
|
|
78
87
|
durationMs?: number;
|
|
79
88
|
/** Non-fatal warnings (e.g. cascade account failures) */
|
|
80
89
|
warnings?: string[];
|
|
90
|
+
/**
|
|
91
|
+
* Set when detection found no infrastructure changes and the engine
|
|
92
|
+
* short-circuited the deploy. Consumers use this to render the
|
|
93
|
+
* "already up to date" path instead of inferring it from output absence.
|
|
94
|
+
*/
|
|
95
|
+
noChanges?: boolean;
|
|
81
96
|
}
|
|
82
97
|
/**
|
|
83
98
|
* Parameters for the destroy entry point.
|
|
@@ -21,14 +21,11 @@ export declare const STEP_IDS: {
|
|
|
21
21
|
readonly DEPLOY: "deploy";
|
|
22
22
|
readonly DESTROY: "destroy";
|
|
23
23
|
readonly VERIFY: "verify";
|
|
24
|
-
readonly OPENNEXT_BUILD: "opennext-build";
|
|
25
24
|
readonly CDK_SYNTH: "cdk-synth";
|
|
26
25
|
readonly DOCKER_OPERATIONS: "docker-operations";
|
|
27
26
|
readonly ECR_INIT: "ecr-init";
|
|
28
27
|
readonly DOCKER_DEPLOY: "docker-deploy";
|
|
29
28
|
readonly TAG_ECR_IMAGES: "tag-ecr-images";
|
|
30
|
-
readonly ECS_UPDATE: "ecs-update";
|
|
31
|
-
readonly ECS_MONITOR: "ecs-monitor";
|
|
32
29
|
readonly ORG_SETUP: "org-setup";
|
|
33
30
|
readonly ORG_ENSURE: "org-ensure";
|
|
34
31
|
readonly ORG_POLICY_TYPES: "policy-types";
|
|
@@ -94,10 +91,16 @@ export type InfrastructureStepName = (typeof INFRASTRUCTURE_STEP_NAMES)[number];
|
|
|
94
91
|
export interface StepDefinition {
|
|
95
92
|
id: StepId;
|
|
96
93
|
name: string;
|
|
94
|
+
/**
|
|
95
|
+
* Heartbeat budget in milliseconds. When set, the UI marks the step as
|
|
96
|
+
* stalled if no progress event arrives within this window. The hook does
|
|
97
|
+
* not cancel the underlying operation; cancellation is handled by Ctrl-C
|
|
98
|
+
* elsewhere. Steps without a value skip the heartbeat timer.
|
|
99
|
+
*/
|
|
100
|
+
heartbeatMs?: number;
|
|
97
101
|
conditions?: {
|
|
98
102
|
requiresInfra?: boolean;
|
|
99
103
|
requiresDocker?: boolean;
|
|
100
|
-
requiresECS?: boolean;
|
|
101
104
|
requiresImageTagging?: boolean;
|
|
102
105
|
skipForInfraOnly?: boolean;
|
|
103
106
|
excludeForManagedAccount?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const E={AUTH:"auth",DETECT_CONFIG:"detect-config",BOOTSTRAP:"bootstrap",CFN_CHECK:"cfn-check",DIFF:"diff",DEPLOY:"deploy",DESTROY:"destroy",VERIFY:"verify",
|
|
1
|
+
const E={AUTH:"auth",DETECT_CONFIG:"detect-config",BOOTSTRAP:"bootstrap",CFN_CHECK:"cfn-check",DIFF:"diff",DEPLOY:"deploy",DESTROY:"destroy",VERIFY:"verify",CDK_SYNTH:"cdk-synth",DOCKER_OPERATIONS:"docker-operations",ECR_INIT:"ecr-init",DOCKER_DEPLOY:"docker-deploy",TAG_ECR_IMAGES:"tag-ecr-images",ORG_SETUP:"org-setup",ORG_ENSURE:"org-ensure",ORG_POLICY_TYPES:"policy-types",ORG_SERVICE_ACCESS:"service-access",ORG_CREATE_ACCOUNTS:"create-accounts",ORG_ENSURE_OUS:"ensure-ous",ORG_PLACE_ACCOUNTS:"place-accounts",IDENTITY_CENTRE:"identity-centre",ORG_ACCOUNTS:"accounts",ORG_COST_TAGS:"cost-tags",ORG_PROFILES:"profiles",PREPARE:"prepare",PREPARE_ENVIRONMENT:"prepare-environment",PLATFORM_ACCOUNT:"platform-account",ACCOUNT_CONTEXT:"account-context",CONNECT:"connect",MONITORING:"monitoring",ORG_DEPLOY:"organisation-deploy",ORG_DESTROY:"organisation-destroy",CASCADE_PLATFORM:"cascade-platform",CASCADE_DOMAINS:"cascade-domains",CASCADE_ACCOUNTS:"cascade-accounts",NETWORK:"network",STORAGE:"storage",MESSAGING:"messaging",DATABASE:"database",COMPUTE:"compute",CDN:"cdn",NETWORK_DESTROY:"network-destroy",STORAGE_DESTROY:"storage-destroy",MESSAGING_DESTROY:"messaging-destroy",DATABASE_DESTROY:"database-destroy",COMPUTE_DESTROY:"compute-destroy",CDN_DESTROY:"cdn-destroy"},t={AUTH:"Authenticating with AWS",PREPARE_DEPLOY:"Preparing deployment",PREPARE_DESTROY:"Preparing destruction",BOOTSTRAP:"Bootstrapping AWS environment",CHECK_INFRA_STATE:"Checking infrastructure state"},e=["Connect securely","Prepare environment","Deploy infrastructure","Enable monitoring"],o={CONNECT:e[0],PREPARE:e[1],DEPLOY:e[2],MONITORING:e[3]};export{e as INFRASTRUCTURE_STEP_NAMES,o as INFRA_STEP_NAME,E as STEP_IDS,t as STEP_NAMES};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/deploy-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.1",
|
|
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",
|
|
@@ -37,10 +37,6 @@
|
|
|
37
37
|
"./steps": {
|
|
38
38
|
"types": "./dist/src/steps/index.d.ts",
|
|
39
39
|
"default": "./dist/src/steps/index.js"
|
|
40
|
-
},
|
|
41
|
-
"./__test-utils__": {
|
|
42
|
-
"types": "./dist/src/__test-utils__/index.d.ts",
|
|
43
|
-
"default": "./dist/src/__test-utils__/index.js"
|
|
44
40
|
}
|
|
45
41
|
},
|
|
46
42
|
"files": [
|
|
@@ -54,31 +50,34 @@
|
|
|
54
50
|
"typecheck": "tsc --noEmit",
|
|
55
51
|
"test": "vitest run",
|
|
56
52
|
"test:watch": "vitest",
|
|
57
|
-
"format": "prettier --write \"src/**/*.ts\"",
|
|
58
|
-
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
59
|
-
"lint": "eslint src/"
|
|
53
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
54
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
55
|
+
"lint": "eslint src/",
|
|
56
|
+
"lint:fix": "eslint src/ --fix"
|
|
60
57
|
},
|
|
61
58
|
"engines": {
|
|
62
59
|
"node": ">=22.0.0"
|
|
63
60
|
},
|
|
64
61
|
"license": "SEE LICENSE IN LICENSE",
|
|
65
62
|
"dependencies": {
|
|
66
|
-
"@aws-sdk/client-backup": "^3.
|
|
67
|
-
"@aws-sdk/client-cloudformation": "^3.
|
|
68
|
-
"@aws-sdk/client-cost-explorer": "^3.
|
|
69
|
-
"@aws-sdk/client-ec2": "^3.
|
|
70
|
-
"@aws-sdk/client-
|
|
71
|
-
"@aws-sdk/client-
|
|
72
|
-
"@aws-sdk/client-
|
|
73
|
-
"@aws-sdk/client-
|
|
74
|
-
"@aws-sdk/client-
|
|
75
|
-
"@
|
|
76
|
-
"@fjall/
|
|
77
|
-
"@
|
|
78
|
-
"
|
|
63
|
+
"@aws-sdk/client-backup": "^3.1038.0",
|
|
64
|
+
"@aws-sdk/client-cloudformation": "^3.1038.0",
|
|
65
|
+
"@aws-sdk/client-cost-explorer": "^3.1038.0",
|
|
66
|
+
"@aws-sdk/client-ec2": "^3.1038.0",
|
|
67
|
+
"@aws-sdk/client-ecr": "^3.1039.0",
|
|
68
|
+
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
69
|
+
"@aws-sdk/client-ram": "^3.1038.0",
|
|
70
|
+
"@aws-sdk/client-s3": "^3.1038.0",
|
|
71
|
+
"@aws-sdk/client-sso-admin": "^3.1038.0",
|
|
72
|
+
"@aws-sdk/client-sts": "^3.1038.0",
|
|
73
|
+
"@fjall/generator": "^0.99.1",
|
|
74
|
+
"@fjall/util": "^0.99.1",
|
|
75
|
+
"@smithy/node-http-handler": "^4.6.1",
|
|
76
|
+
"zod": "^4.4.3"
|
|
79
77
|
},
|
|
80
78
|
"devDependencies": {
|
|
81
|
-
"
|
|
79
|
+
"@types/node": "^25.6.0",
|
|
80
|
+
"vitest": "^4.1.5"
|
|
82
81
|
},
|
|
83
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "0b8cc9b7c5017ca126c884da4cb29793dd26c96a"
|
|
84
83
|
}
|