@fjall/deploy-core 2.23.1 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.minified +1 -1
- package/dist/src/aws/index.d.ts +2 -2
- package/dist/src/aws/index.js +1 -1
- package/dist/src/aws/organisations/identityCentreSnapshot.d.ts +47 -0
- package/dist/src/aws/organisations/identityCentreSnapshot.js +1 -0
- package/dist/src/aws/organisations/index.d.ts +5 -1
- package/dist/src/aws/organisations/index.js +1 -1
- package/dist/src/aws/organisations/pagination.d.ts +28 -0
- package/dist/src/aws/organisations/pagination.js +1 -0
- package/dist/src/index.d.ts +9 -5
- package/dist/src/index.js +1 -1
- package/dist/src/orchestration/application/applicationDeploy.js +1 -1
- package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +11 -12
- package/dist/src/orchestration/application/applicationDeployHelpers.js +2 -2
- package/dist/src/orchestration/application/approvalGate.d.ts +3 -1
- package/dist/src/orchestration/application/approvalGate.js +1 -1
- package/dist/src/orchestration/application/artefactCollection.d.ts +18 -0
- package/dist/src/orchestration/application/artefactCollection.js +1 -0
- package/dist/src/orchestration/application/buildSecretSession.d.ts +8 -0
- package/dist/src/orchestration/application/buildSecretSession.js +1 -1
- package/dist/src/orchestration/application/codeOnlyDeploy.d.ts +13 -0
- package/dist/src/orchestration/application/codeOnlyDeploy.js +1 -1
- package/dist/src/orchestration/application/deploySessionPolicy.d.ts +46 -0
- package/dist/src/orchestration/application/deploySessionPolicy.js +1 -0
- package/dist/src/orchestration/application/dockerBuildHelper.d.ts +21 -5
- package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
- package/dist/src/orchestration/application/serviceImageTagsRollback.d.ts +56 -0
- package/dist/src/orchestration/application/serviceImageTagsRollback.js +1 -0
- package/dist/src/orchestration/application/taskDefinitionRoll.d.ts +33 -0
- package/dist/src/orchestration/application/taskDefinitionRoll.js +1 -0
- package/dist/src/orchestration/deploy.js +1 -1
- package/dist/src/orchestration/dockerInterface.d.ts +5 -2
- package/dist/src/orchestration/index.d.ts +5 -0
- package/dist/src/orchestration/index.js +1 -1
- package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +1 -1
- package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
- package/dist/src/orchestration/restart/restartApplication.d.ts +69 -0
- package/dist/src/orchestration/restart/restartApplication.js +1 -0
- package/dist/src/orchestration/restart/secretsDrift.d.ts +54 -0
- package/dist/src/orchestration/restart/secretsDrift.js +1 -0
- package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -2
- 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 +9 -0
- package/dist/src/services/infrastructure/CloudFormationService.d.ts +17 -0
- package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
- package/dist/src/services/infrastructure/EcrImageInspectorService.d.ts +11 -1
- package/dist/src/services/infrastructure/EcrImageInspectorService.js +1 -1
- package/dist/src/services/infrastructure/SsmParameterMetadataService.d.ts +23 -0
- package/dist/src/services/infrastructure/SsmParameterMetadataService.js +1 -0
- package/dist/src/types/approval.d.ts +15 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/types/params.d.ts +39 -1
- package/package.json +5 -4
|
@@ -41,6 +41,15 @@ export interface CdkOptions {
|
|
|
41
41
|
*/
|
|
42
42
|
parameters?: Record<string, string>;
|
|
43
43
|
timeout?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Name of a pre-existing customer-managed IAM policy passed to
|
|
46
|
+
* `cdk bootstrap --custom-permissions-boundary <name>` — bounds the CDK
|
|
47
|
+
* `cfn-exec-role` (the second admin surface). CDK LOOKS THIS UP BY NAME; the
|
|
48
|
+
* policy MUST already exist in the target account (created by the
|
|
49
|
+
* oidc-connector stack — Phase E2), so the caller only sets it once the
|
|
50
|
+
* boundary is known to exist. Absent/empty ⇒ today's behaviour (no boundary).
|
|
51
|
+
*/
|
|
52
|
+
permissionsBoundary?: string;
|
|
44
53
|
passThroughCDK?: boolean;
|
|
45
54
|
stacks?: string[];
|
|
46
55
|
noLookups?: boolean;
|
|
@@ -93,4 +93,21 @@ export declare class CloudFormationService {
|
|
|
93
93
|
pollIntervalMs?: number;
|
|
94
94
|
onProgress?: (message: string) => void;
|
|
95
95
|
}): Promise<Result<void, CloudFormationError>>;
|
|
96
|
+
/**
|
|
97
|
+
* Converge a subset of a stack's parameters to new values, preserving every
|
|
98
|
+
* other parameter via `UsePreviousValue` (F4b — post-rollback CFN ImageTag
|
|
99
|
+
* reconcile). Keys in `parameterOverrides` absent from the stack's current
|
|
100
|
+
* parameters are reported as `skippedKeys` rather than failing the call —
|
|
101
|
+
* a stale key (a service the stack no longer has) should not block
|
|
102
|
+
* reconciling the keys that do exist.
|
|
103
|
+
*/
|
|
104
|
+
updateStackParameters(stackName: string, parameterOverrides: Record<string, string>, abortSignal?: AbortSignal): Promise<Result<{
|
|
105
|
+
updated: boolean;
|
|
106
|
+
skippedKeys: string[];
|
|
107
|
+
}, CloudFormationError>>;
|
|
108
|
+
/**
|
|
109
|
+
* Poll stack status until the parameter-only update completes, fails, or
|
|
110
|
+
* times out.
|
|
111
|
+
*/
|
|
112
|
+
private waitForUpdateStackParametersComplete;
|
|
96
113
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CloudFormationClient as
|
|
1
|
+
import{CloudFormationClient as f,DeleteStackCommand as _,DescribeStacksCommand as m,GetTemplateCommand as A,ListExportsCommand as O,UpdateStackCommand as P}from"@aws-sdk/client-cloudformation";import{stackStatusMap as T}from"../../aws/utils/stackStatus.js";import{maskSensitiveOutput as w}from"@fjall/util";import{success as d,failure as c}from"@fjall/generator";import{BaseServiceError as $}from"../../types/errors/ServiceError.js";import{logger as M}from"@fjall/util/logger";import{getErrorMessage as S,sleep as E}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as g}from"@fjall/util/aws";import{isCleanableState as F}from"../../types/constants.js";import{composeSdkAbortSignal as C,extractErrorName as R,isAborted as D}from"../../aws/organisations/types.js";class l extends ${errorType;stackName;stackStatus;constructor(e,r,t,n,s,a=!1){super(`CFN_${r.toUpperCase()}`,e,s,a),this.errorType=r,this.stackName=t,this.stackStatus=n}}class Y{aws;constructor(e){this.aws=e}classifyAwsError(e,r,t){const n=R(e),s=w(S(e));return n==="CredentialsError"||n==="UnauthorizedError"?new l(`AWS credentials error: ${s}`,"auth_error",t,void 0,e,!1):n==="Throttling"||n==="TooManyRequestsException"?new l(`AWS rate limit exceeded: ${s}`,"throttled",t,void 0,e,!0):n==="NetworkingError"||n==="ENOTFOUND"?new l(`Network error: ${s}`,"network_error",t,void 0,e,!0):new l(`${r}: ${s}`,"unknown",t,void 0,e)}async getStackOutputs(e,r){r?.onStackCheck?.(e);const t=this.aws.getClient(f),n=new m({StackName:e});try{const a=(await t.send(n)).Stacks?.[0];if(!a?.Outputs)return d([]);const o=a.Outputs.map(u=>({OutputKey:u.OutputKey,OutputValue:u.OutputValue,ExportName:u.ExportName}));return r?.onOutputsRetrieved?.(e,o.length),d(o)}catch(s){if(s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(g))return r?.onStackNotFound?.(e),d([]);const a=w(S(s));return c(new l(`Failed to get outputs for stack ${e}: ${a}`,"unknown",e,void 0,s))}}async getStackStatus(e,r){r?.onStackCheck?.(e);const t=this.aws.getClient(f),n=new m({StackName:e});try{const a=(await t.send(n)).Stacks?.[0];if(!a)return d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"});const o=a.StackStatus||"UNKNOWN",u=T[o]||T.UNKNOWN;return r?.onStackFound?.(e,o),d({status:o,safeToRedeploy:u.safeToRedeploy,description:u.description,statusReason:a.StackStatusReason})}catch(s){return s instanceof Error&&s.name==="ValidationError"&&s.message?.includes(g)?d({status:"DOES_NOT_EXIST",safeToRedeploy:"Yes",description:"Stack does not exist yet"}):c(this.classifyAwsError(s,`Failed to get stack status for ${e}`,e))}}async listAllExports(e){const r=this.aws.getClient(f),t=[];try{let n;do{const s=new O({NextToken:n}),a=await r.send(s),o=a.Exports||[];for(const u of o)u.Name&&u.Value&&t.push({Name:u.Name,Value:u.Value});if(e?.(o))break;n=a.NextToken}while(n);return d(t)}catch(n){const s=w(S(n));return c(new l(`Failed to list exports: ${s}`,"unknown",void 0,void 0,n,!1))}}async getExportsByNames(e){if(e.length===0)return d(new Map);const r=new Set(e),t=new Map,n=await this.listAllExports(s=>{for(const a of s)a.Name&&r.has(a.Name)&&a.Value&&t.set(a.Name,a.Value);return t.size>=r.size});return n.success?d(t):c(n.error)}async listExports(e){const r=await this.listAllExports();return r.success&&e?.onExportsRetrieved?.(r.data.length),r}async deleteStack(e){const r=this.aws.getClient(f);try{return await r.send(new _({StackName:e})),d(void 0)}catch(t){return c(this.classifyAwsError(t,`Failed to delete stack ${e}`,e))}}async stackExists(e,r){const t=r??this.aws.getClient(f);try{const s=(await t.send(new m({StackName:e}))).Stacks?.[0]?.StackStatus;return!!s&&s!=="REVIEW_IN_PROGRESS"&&!F(s)}catch(n){return n instanceof Error&&n.message?.includes(g)?!1:(M.debug("CloudFormationService","Error checking stack existence, assuming exists",{stackName:e,error:S(n)}),!0)}}async getTemplate(e){const r=this.aws.getClient(f);try{const t=await r.send(new A({StackName:e,TemplateStage:"Original"}));return d(t.TemplateBody??"")}catch(t){return t instanceof Error&&t.message?.includes(g)?c(new l(`Stack ${e} not found`,"stack_not_found",e)):c(this.classifyAwsError(t,`Failed to get template for stack ${e}`,e))}}async waitForDeleteComplete(e,r){const t=r?.timeoutMs??6e5,n=r?.pollIntervalMs??5e3,s=Date.now();for(;Date.now()-s<t;){const a=await this.getStackStatus(e);if(!a.success){if(a.error.recoverable){r?.onProgress?.(`Transient error polling stack ${e}, retrying: ${w(a.error.message)}`),await E(n);continue}return c(a.error)}const o=a.data?.status;if(o==="DELETE_COMPLETE"||o==="DOES_NOT_EXIST")return d(void 0);if(o==="DELETE_FAILED")return c(new l(`Stack ${e} deletion failed: ${a.data?.statusReason||"unknown reason"}`,"stack_failed",e,o,void 0,!1));r?.onProgress?.(`Stack ${e} status: ${o??"unknown"}`),await E(n)}return c(new l(`Timed out waiting for stack ${e} deletion after ${Math.round(t/1e3)}s`,"timeout",e,void 0,void 0,!0))}async updateStackParameters(e,r,t){const n=this.aws.getClient(f);let s;try{s=await n.send(new m({StackName:e}),{abortSignal:C(t)})}catch(i){return c(this.classifyAwsError(i,`Failed to describe stack ${e} before parameter reconcile`,e))}const a=s.Stacks?.[0];if(!a)return c(new l(`Stack ${e} not found`,"stack_not_found",e));const o=a.Parameters??[],u=new Set(o.map(i=>i.ParameterKey).filter(i=>i!==void 0)),k=Object.entries(r),h=k.filter(([i])=>!u.has(i)).map(([i])=>i),y=new Map(k.filter(([i])=>u.has(i)));if(y.size===0)return d({updated:!1,skippedKeys:h});const x=o.filter(i=>i.ParameterKey!==void 0).map(i=>y.has(i.ParameterKey)?{ParameterKey:i.ParameterKey,ParameterValue:y.get(i.ParameterKey)}:{ParameterKey:i.ParameterKey,UsePreviousValue:!0});try{await n.send(new P({StackName:e,UsePreviousTemplate:!0,Parameters:x,Capabilities:["CAPABILITY_IAM","CAPABILITY_NAMED_IAM"]}),{abortSignal:C(t)})}catch(i){return i instanceof Error&&i.name==="ValidationError"&&i.message?.includes("No updates are to be performed")?d({updated:!1,skippedKeys:h}):c(this.classifyAwsError(i,`Failed to update parameters for stack ${e}`,e))}return this.waitForUpdateStackParametersComplete(e,h,t)}async waitForUpdateStackParametersComplete(e,r,t){const a=Date.now();for(;Date.now()-a<6e5;){if(D(t))return c(new l(`Aborted waiting for stack ${e} parameter update`,"timeout",e,void 0,void 0,!0));const o=await this.getStackStatus(e);if(!o.success){if(o.error.recoverable){await E(5e3);continue}return c(o.error)}const u=o.data?.status??"UNKNOWN";if(u==="UPDATE_COMPLETE")return d({updated:!0,skippedKeys:r});if(I(u))return c(new l(`Stack ${e} parameter update failed: ${o.data?.statusReason||"unknown reason"}`,"stack_failed",e,u,void 0,!1));await E(5e3)}return c(new l(`Timed out waiting for stack ${e} parameter update after ${Math.round(6e5/1e3)}s`,"timeout",e,void 0,void 0,!0))}}function I(p){return p.endsWith("_IN_PROGRESS")?!1:p.startsWith("UPDATE_ROLLBACK_")||p==="UPDATE_FAILED"}export{l as CloudFormationError,Y as CloudFormationService};
|
|
@@ -28,5 +28,15 @@ export declare class EcrImageInspectorService {
|
|
|
28
28
|
* third-party registries). Returns a failure only when the ECR call
|
|
29
29
|
* itself errors or the tag exists but ECR returned no digest.
|
|
30
30
|
*/
|
|
31
|
-
getImageDigest(imageUri: string, imageTag: string): Promise<Result<string | undefined, Error>>;
|
|
31
|
+
getImageDigest(imageUri: string, imageTag: string, abortSignal?: AbortSignal): Promise<Result<string | undefined, Error>>;
|
|
32
|
+
/**
|
|
33
|
+
* Reverse lookup: the tags attached to an image digest. Needed by the
|
|
34
|
+
* restart operation — after a digest-pinned rollback the live container
|
|
35
|
+
* image reference carries no tag, but the restart Release's artefacts must
|
|
36
|
+
* record one for later `serviceImageTags` rollbacks to consume.
|
|
37
|
+
*
|
|
38
|
+
* Returns `success(undefined)` for non-ECR URIs (caller degrades),
|
|
39
|
+
* `success([])` when the image exists but is untagged.
|
|
40
|
+
*/
|
|
41
|
+
getImageTagsByDigest(imageUri: string, imageDigest: string, abortSignal?: AbortSignal): Promise<Result<string[] | undefined, Error>>;
|
|
32
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ECRClient as m,BatchGetImageCommand as
|
|
1
|
+
import{ECRClient as m,BatchGetImageCommand as h,DescribeImagesCommand as k}from"@aws-sdk/client-ecr";import{success as o,failure as c}from"@fjall/generator";import{logger as f}from"@fjall/util/logger";import{getErrorMessage as p}from"@fjall/util";import{composeSdkAbortSignal as I}from"../../aws/organisations/types.js";const E="EcrImageInspector",C=/^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/;function $(n){const r=n.lastIndexOf("@");if(r>0)return n.slice(0,r);const e=n.lastIndexOf(":");return e>0?n.slice(0,e):n}class G{awsProvider;constructor(r){this.awsProvider=r}async getImageDigest(r,e,d){const s=$(r).match(C);if(!s)return f.debug(E,"Image URI is not ECR \u2014 skipping digest lookup",{imageUri:r}),o(void 0);const[,,,t]=s;if(!t)return o(void 0);try{const u=await this.awsProvider.getClient(m).send(new h({repositoryName:t,imageIds:[{imageTag:e}]}),{abortSignal:I(d)}),a=u.failures??[];if(a.length>0){const R=a.map(l=>`${l.failureCode??"unknown"}:${l.failureReason??""}`).join("; ");return c(new Error(`ECR BatchGetImage rejected ${t}:${e} \u2014 ${R}`))}const g=u.images?.[0]?.imageId?.imageDigest;return g?o(g):c(new Error(`ECR BatchGetImage returned no digest for ${t}:${e}`))}catch(i){return c(new Error(`ECR digest lookup failed for ${t}:${e} \u2014 ${p(i)}`))}}async getImageTagsByDigest(r,e,d){const s=$(r).match(C);if(!s)return f.debug(E,"Image URI is not ECR \u2014 skipping tag lookup by digest",{imageUri:r}),o(void 0);const[,,,t]=s;if(!t)return o(void 0);try{const a=(await this.awsProvider.getClient(m).send(new k({repositoryName:t,imageIds:[{imageDigest:e}]}),{abortSignal:I(d)})).imageDetails?.[0];return a?o(a.imageTags??[]):c(new Error(`ECR DescribeImages returned no image for ${t}@${e}`))}catch(i){return c(new Error(`ECR tag lookup failed for ${t}@${e} \u2014 ${p(i)}`))}}}export{G as EcrImageInspectorService,$ as stripTagOrDigest};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Result } from "@fjall/generator";
|
|
2
|
+
import type { AwsProvider } from "../../aws/AwsProvider.js";
|
|
3
|
+
export interface SsmParameterMetadata {
|
|
4
|
+
name: string;
|
|
5
|
+
version?: number;
|
|
6
|
+
lastModifiedDate?: Date;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Metadata-only SSM reads for the secrets-drift probe. Deliberately uses
|
|
10
|
+
* DescribeParameters (never GetParameter/GetParameters): drift needs only
|
|
11
|
+
* `LastModifiedDate` + `Version`, and a value-bearing call would put secret
|
|
12
|
+
* contents in memory for no reason.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SsmParameterMetadataService {
|
|
15
|
+
private readonly awsProvider;
|
|
16
|
+
constructor(awsProvider: AwsProvider);
|
|
17
|
+
/**
|
|
18
|
+
* Describe parameters by exact name. Names absent from the result simply
|
|
19
|
+
* do not exist (or are not visible) — callers treat them as unknown, not
|
|
20
|
+
* as an error.
|
|
21
|
+
*/
|
|
22
|
+
describeParametersByName(names: string[], abortSignal?: AbortSignal): Promise<Result<SsmParameterMetadata[], Error>>;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{SSMClient as u,DescribeParametersCommand as l}from"@aws-sdk/client-ssm";import{success as m,failure as p}from"@fjall/generator";import{getErrorMessage as S,maskSensitiveOutput as P}from"@fjall/util";import{composeSdkAbortSignal as b,isAborted as c}from"../../aws/organisations/types.js";const o=50;class v{awsProvider;constructor(r){this.awsProvider=r}async describeParametersByName(r,i){if(r.length===0)return m([]);try{const a=this.awsProvider.getClient(u),n=[];for(let t=0;t<r.length&&!c(i);t+=o){const f=r.slice(t,t+o);let s;do{if(c(i))break;const d=await a.send(new l({ParameterFilters:[{Key:"Name",Option:"Equals",Values:f}],MaxResults:o,...s!==void 0?{NextToken:s}:{}}),{abortSignal:b(i)});for(const e of d.Parameters??[])e.Name!==void 0&&n.push({name:e.Name,...e.Version!==void 0?{version:Number(e.Version)}:{},...e.LastModifiedDate!==void 0?{lastModifiedDate:e.LastModifiedDate}:{}});s=d.NextToken}while(s!==void 0)}return m(n)}catch(a){return p(new Error(`SSM DescribeParameters failed: ${P(S(a))}`))}}}export{v as SsmParameterMetadataService};
|
|
@@ -10,6 +10,21 @@
|
|
|
10
10
|
* backward-compatible: the engine deploys exactly as it does today.
|
|
11
11
|
*/
|
|
12
12
|
import type { DeployPlan } from "../orchestration/application/plan/types.js";
|
|
13
|
+
/**
|
|
14
|
+
* Both detection signals behind a computed plan, distinguished so a parser can
|
|
15
|
+
* see WHY the gate stopped: hash drift (templates vs the last recorded deploy)
|
|
16
|
+
* is what engages the exit-2 outcome even when the resource-level diff against
|
|
17
|
+
* the live stacks is empty. Neither signal drives the exit code differently —
|
|
18
|
+
* this is observability, not control flow.
|
|
19
|
+
*/
|
|
20
|
+
export interface ApprovalGatePlanDetail {
|
|
21
|
+
/** Stacks whose synthesised template hash differs from the recorded deploy state. */
|
|
22
|
+
hashDriftStacks: string[];
|
|
23
|
+
/** Count of resource-level changes diffed against the live stacks. */
|
|
24
|
+
resourceChangeCount: number;
|
|
25
|
+
/** Stacks with at least one resource-level change against the live stacks. */
|
|
26
|
+
resourceChangedStacks: string[];
|
|
27
|
+
}
|
|
13
28
|
/** What the gate hands a surface resolver so it can decide. */
|
|
14
29
|
export interface ApprovalRequest {
|
|
15
30
|
/** Target app / deployment name. */
|
|
@@ -11,7 +11,7 @@ export type { ProgressEvent, ProgressEventType, ResourceEvent, AwsAuthResult, Ca
|
|
|
11
11
|
export { TRAIL_MIGRATION_PHASES, TRAIL_MIGRATION_STATUSES } from "./events.js";
|
|
12
12
|
export type { ApiClientInterface, EntitlementsData } from "./apiClient.js";
|
|
13
13
|
export type { DeployParams, DeployOptions, DeploymentType, DeployResult, DestroyParams, DestroyOptions, DestroyResult } from "./params.js";
|
|
14
|
-
export type { ApprovalGate, ApprovalRequest, GateResolution } from "./approval.js";
|
|
14
|
+
export type { ApprovalGate, ApprovalGatePlanDetail, ApprovalRequest, GateResolution } from "./approval.js";
|
|
15
15
|
export type { OrgConfig, ProviderAccount, RootAccessManagementMode, SSOSession } from "./config/orgConfig.js";
|
|
16
16
|
export type { Entitlements } from "./config/entitlements.js";
|
|
17
17
|
export type { StackOutput, StackOutputsRecord, DeploymentContext, StepOutput, ApplicationDeploymentContext, CallerIdentity } from "./deployment/index.js";
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Result } from "@fjall/generator";
|
|
2
|
+
import type { DeployMode, ServiceArtefact } from "@fjall/util";
|
|
2
3
|
import type { AwsCredentials, DeployIdentity, ScopedBuildSecretSessionMinter } from "./credentials.js";
|
|
3
4
|
import type { DeployCallbacks } from "./callbacks.js";
|
|
4
5
|
import type { ApiClientInterface } from "./apiClient.js";
|
|
5
6
|
import type { OrgConfig } from "./config/orgConfig.js";
|
|
6
7
|
import type { DockerProvider } from "../orchestration/dockerInterface.js";
|
|
7
8
|
import type { DomainDeployProvider } from "../orchestration/domainInterface.js";
|
|
8
|
-
import type { ApprovalGate } from "./approval.js";
|
|
9
|
+
import type { ApprovalGate, ApprovalGatePlanDetail } from "./approval.js";
|
|
9
10
|
import type { FailureAnalysis } from "@fjall/util/aws";
|
|
10
11
|
import type { DockerBuildSecretRef } from "@fjall/util/manifest/schemas";
|
|
11
12
|
export interface DeployParams {
|
|
@@ -77,6 +78,16 @@ export interface DeployParams {
|
|
|
77
78
|
approvalGate?: ApprovalGate;
|
|
78
79
|
}
|
|
79
80
|
export interface DeployOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Explicit deploy mode. When supplied it is authoritative — the engine
|
|
83
|
+
* branches on it alone. When omitted the engine derives it for legacy
|
|
84
|
+
* callers: `imageTag` present ⇒ `"rollback"`, else `deployOnly` ⇒
|
|
85
|
+
* `"code-only"`, else `"full"`. An explicit `"full"`/`"code-only"`
|
|
86
|
+
* combined with `imageTag` is rejected up-front (--image-tag requires
|
|
87
|
+
* rollback mode), making the redrive mode/imageTag mismatch (F9)
|
|
88
|
+
* unrepresentable.
|
|
89
|
+
*/
|
|
90
|
+
mode?: DeployMode;
|
|
80
91
|
skipConfirmation?: boolean;
|
|
81
92
|
force?: boolean;
|
|
82
93
|
cascade?: boolean;
|
|
@@ -102,6 +113,14 @@ export interface DeployOptions {
|
|
|
102
113
|
* build + push and rolls ECS to `<repo>:<imageTag>`. Implies `deployOnly`.
|
|
103
114
|
*/
|
|
104
115
|
imageTag?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Exact per-service image tags for rollback mode (F12), keyed by the bare
|
|
118
|
+
* service name recorded in a Release's `ServiceArtefact[]`. Mutually
|
|
119
|
+
* exclusive with `imageTag` — each service rolls to its EXACT supplied tag,
|
|
120
|
+
* bypassing the `rebaseRollbackTagForService` prefix heuristic entirely.
|
|
121
|
+
* Implies `deployOnly`.
|
|
122
|
+
*/
|
|
123
|
+
serviceImageTags?: Record<string, string>;
|
|
105
124
|
/**
|
|
106
125
|
* Approval-gate: require an explicit approve decision before mutating. The
|
|
107
126
|
* engine only gates when a gate is engaged (resolver injected, `planOnly`, or
|
|
@@ -151,6 +170,17 @@ export interface DeployResult {
|
|
|
151
170
|
deploymentType: DeploymentType;
|
|
152
171
|
/** Stack outputs (website URL, OIDC role ARN, etc.) */
|
|
153
172
|
outputs?: Record<string, string>;
|
|
173
|
+
/**
|
|
174
|
+
* Typed per-service image identity for every service this deploy rolled or
|
|
175
|
+
* pushed (`ServiceArtefactSchema` at `@fjall/util`). Populated on ALL
|
|
176
|
+
* application deploy modes — code-only and rollback from the explicit
|
|
177
|
+
* RegisterTaskDefinition rollout, full deploys from the build step plus a
|
|
178
|
+
* best-effort post-deploy ECS resolution (a resolution failure degrades
|
|
179
|
+
* `taskDefinitionArn` to absent, never fails the deploy). Empty for
|
|
180
|
+
* organisation deploys, cancelled/rejected/awaiting outcomes, and deploys
|
|
181
|
+
* that pushed no images.
|
|
182
|
+
*/
|
|
183
|
+
artefacts: ServiceArtefact[];
|
|
154
184
|
/** Per-account outputs from cascade deploys (accountId → stack outputs). */
|
|
155
185
|
cascadeOutputs?: Array<{
|
|
156
186
|
accountId: string;
|
|
@@ -208,6 +238,14 @@ export interface DeployResult {
|
|
|
208
238
|
* plan.
|
|
209
239
|
*/
|
|
210
240
|
assemblyDigest?: string;
|
|
241
|
+
/**
|
|
242
|
+
* Approval-gate: both detection signals behind the gate outcome (template-hash
|
|
243
|
+
* drift vs the recorded deploy state; resource-level diff vs the live stacks).
|
|
244
|
+
* Present whenever the gate computed a plan — always with `awaitingApproval`,
|
|
245
|
+
* and with `rejected` only when a resolver declined a computed plan (a refused
|
|
246
|
+
* resume token never computed one).
|
|
247
|
+
*/
|
|
248
|
+
planDetail?: ApprovalGatePlanDetail;
|
|
211
249
|
}
|
|
212
250
|
/**
|
|
213
251
|
* Parameters for the destroy entry point.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/deploy-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.25.0",
|
|
4
4
|
"description": "Shared deployment engine for Fjall — used by CLI and webapp worker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"@aws-sdk/client-ec2": "^3.1038.0",
|
|
73
73
|
"@aws-sdk/client-ecr": "^3.1039.0",
|
|
74
74
|
"@aws-sdk/client-iam": "^3.1038.0",
|
|
75
|
+
"@aws-sdk/client-identitystore": "^3.1079.0",
|
|
75
76
|
"@aws-sdk/client-kms": "^3.1065.0",
|
|
76
77
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
77
78
|
"@aws-sdk/client-ram": "^3.1038.0",
|
|
@@ -81,8 +82,8 @@
|
|
|
81
82
|
"@aws-sdk/client-ssm": "^3.1038.0",
|
|
82
83
|
"@aws-sdk/client-sso-admin": "^3.1038.0",
|
|
83
84
|
"@aws-sdk/client-sts": "^3.1038.0",
|
|
84
|
-
"@fjall/generator": "^2.
|
|
85
|
-
"@fjall/util": "^2.
|
|
85
|
+
"@fjall/generator": "^2.25.0",
|
|
86
|
+
"@fjall/util": "^2.25.0",
|
|
86
87
|
"@smithy/node-http-handler": "^4.6.1",
|
|
87
88
|
"aws-cdk": "^2.1128.1",
|
|
88
89
|
"tsx": "^4.21.0",
|
|
@@ -93,5 +94,5 @@
|
|
|
93
94
|
"typescript": "^6.0.3",
|
|
94
95
|
"vitest": "^4.1.5"
|
|
95
96
|
},
|
|
96
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "7c1a329184064aefa557c2c09de0965c4f8cd4fb"
|
|
97
98
|
}
|