anton-bakker-deploy-engine 0.2.0 → 0.2.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/README.md ADDED
@@ -0,0 +1,818 @@
1
+ # anton-bakker-deploy-engine
2
+
3
+ A self-validating AWS deployment verification library with a 4-layer verification model and a config-driven pipeline engine.
4
+
5
+ Zero runtime dependencies. TypeScript. ESM. Node.js ≥ 20.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Overview](#overview)
10
+ - [Installation](#installation)
11
+ - [Architecture](#architecture)
12
+ - [Verification Functions](#verification-functions)
13
+ - [verifyAmplifyOutputs](#verifyamplifyoutputs)
14
+ - [verifyBuildArtifact](#verifybuildartifact)
15
+ - [parseTableCount](#parsetablecount)
16
+ - [assertTableCount](#asserttablecount)
17
+ - [parseEcsStatus](#parseecsstatus)
18
+ - [assertCognitoGroups](#assertcognitogroups)
19
+ - [assertApiHealth](#assertapihealth)
20
+ - [deriveExpectations](#deriveexpectations)
21
+ - [summarize](#summarize)
22
+ - [Pipeline Engine](#pipeline-engine)
23
+ - [Concepts](#concepts)
24
+ - [Building Gates](#building-gates)
25
+ - [Writing Check Executors](#writing-check-executors)
26
+ - [Creating the Execution Context](#creating-the-execution-context)
27
+ - [Running the Pipeline](#running-the-pipeline)
28
+ - [State Persistence and Resume](#state-persistence-and-resume)
29
+ - [Clearing State](#clearing-state)
30
+ - [Default Environments](#default-environments)
31
+ - [Types Reference](#types-reference)
32
+ - [Publishing](#publishing)
33
+ - [Development](#development)
34
+ - [Licence](#licence)
35
+
36
+ ## Overview
37
+
38
+ Every AWS deployment can fail silently — tables missing, ECS tasks crashing, environment variables leaking into production builds. This library provides composable verification functions and a pipeline engine that catches these problems automatically.
39
+
40
+ The 4-layer verification model:
41
+
42
+ | Layer | Question | Examples |
43
+ |-------|----------|---------|
44
+ | **Exists** | Is the resource present? | amplify_outputs.json readable, index JS bundle exists |
45
+ | **Configured** | Is it set up correctly? | Version is 1.4, data.url is HTTPS, no localhost leaks |
46
+ | **Functional** | Is it working at runtime? | ECS service healthy, API responds, DynamoDB tables present |
47
+ | **Project-specific** | Does it match our source of truth? | Table count matches schema, Cognito groups match auth stack |
48
+
49
+ You can use the verification functions standalone (for scripts, CI checks, post-deploy validation) or wire them into the pipeline engine for a full gated deployment workflow.
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ npm install anton-bakker-deploy-engine
55
+ ```
56
+
57
+ Requires Node.js ≥ 20. Zero runtime dependencies — only uses Node.js built-ins (`fs`, `path`).
58
+
59
+ ## Architecture
60
+
61
+ ```
62
+ src/
63
+ ├── types.ts # All type definitions (CheckResult, GateDefinition, etc.)
64
+ ├── verify.ts # Standalone verification functions (Layer 1–4 checks)
65
+ ├── pipeline.ts # Gate builder + default environment configs
66
+ ├── engine.ts # PipelineEngine class (gate executor, state, resume)
67
+ └── index.ts # Public API — all exports
68
+ ```
69
+
70
+ Everything is exported from the package root:
71
+
72
+ ```typescript
73
+ import {
74
+ // Verification functions
75
+ verifyAmplifyOutputs, verifyBuildArtifact, parseTableCount,
76
+ assertTableCount, parseEcsStatus, assertCognitoGroups,
77
+ assertApiHealth, deriveExpectations, summarize,
78
+ // Pipeline engine
79
+ PipelineEngine, buildGates, DEFAULT_ENVIRONMENTS,
80
+ // Types
81
+ type CheckResult, type GateDefinition, type GateResult,
82
+ type EnvironmentConfig, type PipelineState, type PipelineReport,
83
+ type CheckExecutor, type ExecutionContext,
84
+ } from 'anton-bakker-deploy-engine';
85
+ ```
86
+
87
+ ## Verification Functions
88
+
89
+ All verification functions return `CheckResult` or `CheckResult[]`. Every result has:
90
+
91
+ ```typescript
92
+ interface CheckResult {
93
+ name: string; // Human-readable check description
94
+ passed: boolean; // Did it pass?
95
+ expected?: string | number; // What was expected
96
+ actual?: string | number; // What was found
97
+ duration?: number; // Milliseconds (set by pipeline engine)
98
+ error?: string; // Error message when passed === false
99
+ }
100
+ ```
101
+
102
+ ### verifyAmplifyOutputs
103
+
104
+ Validates that `amplify_outputs.json` has all required fields for a working Amplify deployment.
105
+
106
+ ```typescript
107
+ function verifyAmplifyOutputs(outputsPath: string): CheckResult[]
108
+ ```
109
+
110
+ **Checks performed:**
111
+ - `version` field is present and equals `"1.4"`
112
+ - `data.url` is present and starts with `https://`
113
+ - `auth.user_pool_id` is present
114
+ - `auth.user_pool_client_id` is present
115
+
116
+ **Example:**
117
+
118
+ ```typescript
119
+ const checks = verifyAmplifyOutputs('./amplify_outputs.json');
120
+ // Returns 6 CheckResults
121
+
122
+ // All passed?
123
+ if (checks.every(c => c.passed)) {
124
+ console.log('amplify_outputs.json is valid');
125
+ }
126
+
127
+ // Find failures
128
+ const failures = checks.filter(c => !c.passed);
129
+ failures.forEach(f => console.error(`${f.name}: ${f.error}`));
130
+ ```
131
+
132
+ **Error handling:** If the file doesn't exist or contains invalid JSON, returns a single failed `CheckResult` with the error message.
133
+
134
+ ### verifyBuildArtifact
135
+
136
+ Scans the built JavaScript bundle for environment leaks that should never reach production.
137
+
138
+ ```typescript
139
+ function verifyBuildArtifact(distDir: string): CheckResult[]
140
+ ```
141
+
142
+ **Parameters:**
143
+ - `distDir` — path to the build output directory (expects `dist/assets/index-*.js`)
144
+
145
+ **Checks performed:**
146
+ - No `localhost:4000` in the bundle (GraphQL sandbox leak)
147
+ - No `localhost:5173` in the bundle (Vite dev server leak)
148
+ - No `http://...elb.amazonaws.com` URLs (insecure ALB URLs)
149
+
150
+ **Example:**
151
+
152
+ ```typescript
153
+ const checks = verifyBuildArtifact('./dist');
154
+ const leaks = checks.filter(c => !c.passed);
155
+ if (leaks.length > 0) {
156
+ console.error('Environment leaks detected in build:');
157
+ leaks.forEach(l => console.error(` ${l.name}: ${l.actual}`));
158
+ process.exit(1);
159
+ }
160
+ ```
161
+
162
+ **Error handling:** Returns a failed result if `dist/assets` doesn't exist or no `index-*.js` file is found.
163
+
164
+ ### parseTableCount
165
+
166
+ Parses DynamoDB table count from AWS CLI output, filtering by a table name prefix.
167
+
168
+ ```typescript
169
+ function parseTableCount(
170
+ awsOutput: string | null,
171
+ prefix: string
172
+ ): CheckResult & { count: number }
173
+ ```
174
+
175
+ **Parameters:**
176
+ - `awsOutput` — raw output from `aws dynamodb list-tables` (JSON array of table names) or a plain number string
177
+ - `prefix` — table name prefix to filter by (e.g. `"s20-dev"`)
178
+
179
+ **Example:**
180
+
181
+ ```typescript
182
+ import { execSync } from 'child_process';
183
+
184
+ const output = execSync('aws dynamodb list-tables --output json --query TableNames').toString();
185
+ const result = parseTableCount(output, 's20-dev');
186
+ console.log(`Found ${result.count} tables with prefix s20-dev`);
187
+ ```
188
+
189
+ **Accepts two input formats:**
190
+ 1. JSON array: `["s20-dev-Employee", "s20-dev-Division", "other-table"]` → filters by prefix, returns count
191
+ 2. Plain number: `"124\n"` → parses as integer
192
+
193
+ ### assertTableCount
194
+
195
+ Verifies the actual table count meets an expected minimum.
196
+
197
+ ```typescript
198
+ function assertTableCount(actual: number, expected: number): CheckResult
199
+ ```
200
+
201
+ **Example:**
202
+
203
+ ```typescript
204
+ const { count } = parseTableCount(awsOutput, 's20-dev');
205
+ const check = assertTableCount(count, 124);
206
+ if (!check.passed) {
207
+ console.error(check.error); // "4 tables missing"
208
+ }
209
+ ```
210
+
211
+ ### parseEcsStatus
212
+
213
+ Parses ECS service health from AWS CLI JSON output.
214
+
215
+ ```typescript
216
+ function parseEcsStatus(awsOutput: string | null): CheckResult
217
+ ```
218
+
219
+ **Parameters:**
220
+ - `awsOutput` — JSON string with `status`, `running`/`runningCount`, and `desired`/`desiredCount` fields
221
+
222
+ **Passes when:** `status === 'ACTIVE'` AND `running >= desired` AND `desired > 0`
223
+
224
+ **Example:**
225
+
226
+ ```typescript
227
+ const output = execSync(`aws ecs describe-services \
228
+ --cluster my-cluster --services my-service \
229
+ --query 'services[0].{status:status,running:runningCount,desired:desiredCount}'`
230
+ ).toString();
231
+
232
+ const check = parseEcsStatus(output);
233
+ if (!check.passed) {
234
+ console.error(check.error); // "2 tasks not running"
235
+ }
236
+ ```
237
+
238
+ **Accepts both field name styles:** `running`/`desired` and `runningCount`/`desiredCount`.
239
+
240
+ ### assertCognitoGroups
241
+
242
+ Verifies that all required Cognito user pool groups exist.
243
+
244
+ ```typescript
245
+ function assertCognitoGroups(
246
+ actualGroups: string[],
247
+ requiredGroups: string[]
248
+ ): CheckResult
249
+ ```
250
+
251
+ **Example:**
252
+
253
+ ```typescript
254
+ const output = execSync(`aws cognito-idp list-groups \
255
+ --user-pool-id eu-central-1_ABC \
256
+ --query 'Groups[].GroupName' --output json`
257
+ ).toString();
258
+ const actual = JSON.parse(output);
259
+
260
+ const check = assertCognitoGroups(actual, ['administrators', 'managers', 'root']);
261
+ if (!check.passed) {
262
+ console.error(check.error); // "Missing: managers, root"
263
+ }
264
+ ```
265
+
266
+ ### assertApiHealth
267
+
268
+ Verifies an API health check response contains `__typename` (GraphQL introspection indicator).
269
+
270
+ ```typescript
271
+ function assertApiHealth(response: string | null): CheckResult
272
+ ```
273
+
274
+ **Example:**
275
+
276
+ ```typescript
277
+ const response = execSync('curl -s https://api.example.com/graphql -d \'{"query":"{__typename}"}\'').toString();
278
+ const check = assertApiHealth(response);
279
+ ```
280
+
281
+ ### deriveExpectations
282
+
283
+ Derives expected deployment values from project source-of-truth files. Reads a GraphQL schema to count model types (which become DynamoDB tables), parses an auth stack for Cognito group definitions, and reads infrastructure config for the API domain.
284
+
285
+ ```typescript
286
+ function deriveExpectations(opts: {
287
+ schemaPath: string; // Path to GraphQL schema file
288
+ authStackPath?: string; // Path to CDK auth stack (optional)
289
+ configPath: string; // Path to infrastructure config JSON
290
+ }): DeployExpectations
291
+ ```
292
+
293
+ **Returns:**
294
+
295
+ ```typescript
296
+ interface DeployExpectations {
297
+ tableCount: number; // Number of model types (= expected DynamoDB tables)
298
+ tableNames: string[]; // Model type names
299
+ cognitoGroups: string[]; // Group names found in auth stack
300
+ apiDomain: string; // Constructed from config dns.apiSubdomain + dns.zoneName
301
+ }
302
+ ```
303
+
304
+ **Schema parsing rules:**
305
+ - Counts all `type X {` definitions in the GraphQL schema
306
+ - Excludes: `Query`, `Mutation`, `Subscription`, types ending in `Input`, types ending in `Connection`
307
+
308
+ **Auth stack parsing:** Finds Cognito group names via two patterns:
309
+ - Direct assignment: `groupName: 'administrators'`
310
+ - Loop pattern: `for (... of ['administrators', 'managers', 'root'])`
311
+
312
+ **Example:**
313
+
314
+ ```typescript
315
+ const expectations = deriveExpectations({
316
+ schemaPath: 'server/src/schema/schema.graphql',
317
+ authStackPath: 'cdk/lib/auth-stack.ts',
318
+ configPath: 'cdk/config/infrastructure.json',
319
+ });
320
+
321
+ console.log(`Expecting ${expectations.tableCount} DynamoDB tables`);
322
+ console.log(`Required Cognito groups: ${expectations.cognitoGroups.join(', ')}`);
323
+ console.log(`API domain: ${expectations.apiDomain}`);
324
+
325
+ // Use with assertTableCount
326
+ const tableCheck = assertTableCount(actualCount, expectations.tableCount);
327
+ ```
328
+
329
+ **Config file format expected:**
330
+
331
+ ```json
332
+ {
333
+ "dns": {
334
+ "apiSubdomain": "api",
335
+ "zoneName": "example.com"
336
+ }
337
+ }
338
+ ```
339
+
340
+ If `dns.apiSubdomain` or `dns.zoneName` are missing, defaults to `api.example.com`.
341
+
342
+ ### summarize
343
+
344
+ Aggregates an array of check results into a single pass/fail summary.
345
+
346
+ ```typescript
347
+ function summarize(checks: CheckResult[]): {
348
+ passed: boolean; // true if all checks passed
349
+ total: number; // total number of checks
350
+ failed: number; // number of failed checks
351
+ errors: string[]; // error messages from failed checks
352
+ }
353
+ ```
354
+
355
+ **Example:**
356
+
357
+ ```typescript
358
+ const allChecks = [
359
+ ...verifyAmplifyOutputs('./amplify_outputs.json'),
360
+ ...verifyBuildArtifact('./dist'),
361
+ assertTableCount(count, expected),
362
+ parseEcsStatus(ecsOutput),
363
+ ];
364
+
365
+ const result = summarize(allChecks);
366
+ if (result.passed) {
367
+ console.log(`✅ All ${result.total} checks passed`);
368
+ } else {
369
+ console.error(`❌ ${result.failed}/${result.total} checks failed:`);
370
+ result.errors.forEach(e => console.error(` - ${e}`));
371
+ process.exit(1);
372
+ }
373
+ ```
374
+
375
+ ## Pipeline Engine
376
+
377
+ The pipeline engine runs a sequence of gates, where each gate contains one or more checks. Gates execute in order. When a gate fails, the engine takes the configured action (abort, rollback, or alert) and stops.
378
+
379
+ ### Concepts
380
+
381
+ **Gate** — a named group of checks with a failure action. Example: "Post-Deploy Inventory" gate runs `table-count`, `ecs-status`, and `cognito-groups` checks. If any fail, the action is `rollback`.
382
+
383
+ **Check executor** — an async function that runs a single named check and returns a `CheckResult`. You register these with the engine.
384
+
385
+ **Execution context** — environment information passed to every check executor (environment name, region, project root, a shell exec helper, etc.).
386
+
387
+ **State persistence** — the engine saves its state to disk after each gate. If a run is interrupted, it resumes from the last passed gate.
388
+
389
+ ### Building Gates
390
+
391
+ Use `buildGates()` to generate a gate sequence from an `EnvironmentConfig`:
392
+
393
+ ```typescript
394
+ import { buildGates, DEFAULT_ENVIRONMENTS } from 'anton-bakker-deploy-engine';
395
+
396
+ const gates = buildGates(DEFAULT_ENVIRONMENTS.staging);
397
+ ```
398
+
399
+ This produces 10 gates in order:
400
+
401
+ | # | Gate | Checks | On Fail |
402
+ |---|------|--------|---------|
403
+ | 1 | Preflight | `aws-creds`, `deploy-lock` | abort |
404
+ | 2 | Code Quality | From `preDeployChecks` config | abort |
405
+ | 3 | Infrastructure Validation | `cdk-synth`, `derive-expectations` | abort |
406
+ | 4 | Pre-Deploy Safety | `backup-tables` (if enabled), `prepare-migrations` (if enabled) | abort |
407
+ | 5 | Build | `docker-build`, `frontend-build`, `post-build-verify` | abort |
408
+ | 6 | Deploy | `cdk-deploy`, `ecs-update`, `frontend-deploy`, `seed-data` (if enabled) | rollback |
409
+ | 7 | Post-Deploy Inventory | `table-count`, `ecs-status`, `cognito-groups` | rollback |
410
+ | 8 | Post-Deploy Configuration | `pitr-enabled`, `billing-mode`, `https-enforced`, `s3-not-public` | alert |
411
+ | 9 | Post-Deploy Functional | From `postDeployProbes` config | rollback or alert |
412
+ | 10 | Project Assertions | `manifest-assertions`, `e2e-tests` (if enabled) | alert |
413
+
414
+ Gates 4 and 10 are conditional — they're skipped if the relevant config flags are off.
415
+
416
+ You can also build gates manually:
417
+
418
+ ```typescript
419
+ import type { GateDefinition } from 'anton-bakker-deploy-engine';
420
+
421
+ const gates: GateDefinition[] = [
422
+ {
423
+ id: 'preflight',
424
+ name: 'Preflight',
425
+ checks: ['aws-creds'],
426
+ onFail: 'abort',
427
+ },
428
+ {
429
+ id: 'deploy',
430
+ name: 'Deploy',
431
+ checks: ['cdk-deploy'],
432
+ onFail: 'rollback',
433
+ },
434
+ {
435
+ id: 'verify',
436
+ name: 'Post-Deploy Verification',
437
+ checks: ['api-health', 'table-count'],
438
+ onFail: 'rollback',
439
+ },
440
+ ];
441
+ ```
442
+
443
+ **Failure actions:**
444
+ - `abort` — stop the pipeline, do nothing else
445
+ - `rollback` — stop the pipeline, signal that a rollback is needed (the engine sets `result: 'rolled-back'`)
446
+ - `alert` — stop the pipeline, signal that an alert should be sent
447
+
448
+ **Conditional gates:** Add a `condition` function that receives the `EnvironmentConfig`. The gate is skipped if it returns `false`:
449
+
450
+ ```typescript
451
+ {
452
+ id: 'backup',
453
+ name: 'Backup',
454
+ checks: ['backup-tables'],
455
+ onFail: 'abort',
456
+ condition: (env) => env.backupBefore, // Only run if backups are enabled
457
+ }
458
+ ```
459
+
460
+ ### Writing Check Executors
461
+
462
+ A check executor is an async function that receives the `ExecutionContext` and returns a `CheckResult`:
463
+
464
+ ```typescript
465
+ import type { CheckExecutor } from 'anton-bakker-deploy-engine';
466
+
467
+ const typecheckExecutor: CheckExecutor = async (ctx) => {
468
+ const result = ctx.exec('npx tsc --noEmit');
469
+ return {
470
+ name: 'TypeScript type check',
471
+ passed: result !== null,
472
+ error: result === null ? 'tsc --noEmit failed' : undefined,
473
+ };
474
+ };
475
+
476
+ const tableCountExecutor: CheckExecutor = async (ctx) => {
477
+ const output = ctx.exec(`aws dynamodb list-tables --query TableNames --output json`);
478
+ const { count } = parseTableCount(output, ctx.prefix);
479
+ const expected = ctx.expectations?.tableCount as number ?? 0;
480
+ return assertTableCount(count, expected);
481
+ };
482
+
483
+ const apiHealthExecutor: CheckExecutor = async (ctx) => {
484
+ const response = ctx.exec(`curl -sf https://${ctx.prefix}.example.com/health`);
485
+ return assertApiHealth(response);
486
+ };
487
+ ```
488
+
489
+ Register executors in a `Map<string, CheckExecutor>`:
490
+
491
+ ```typescript
492
+ const checks = new Map<string, CheckExecutor>();
493
+ checks.set('typecheck', typecheckExecutor);
494
+ checks.set('table-count', tableCountExecutor);
495
+ checks.set('api-health', apiHealthExecutor);
496
+ ```
497
+
498
+ The check name in the map must match the check name referenced in gate definitions.
499
+
500
+ If a check executor throws an exception, the engine catches it and records a failed `CheckResult` with the error message.
501
+
502
+ If a gate references a check name that has no registered executor, the engine records a failed result with `"No executor registered for 'name'"`.
503
+
504
+ ### Creating the Execution Context
505
+
506
+ The `ExecutionContext` provides environment information to all check executors:
507
+
508
+ ```typescript
509
+ import type { ExecutionContext } from 'anton-bakker-deploy-engine';
510
+ import { execSync } from 'child_process';
511
+
512
+ const ctx: ExecutionContext = {
513
+ environment: 'staging', // Full environment name
514
+ envShort: 'stg', // Short code for prefixes
515
+ prefix: 's20-stg', // Resource name prefix
516
+ region: 'eu-central-1', // AWS region
517
+ account: '123456789012', // AWS account ID
518
+ projectRoot: process.cwd(), // Project root directory
519
+ dryRun: false, // If true, no state is persisted
520
+ envConfig: DEFAULT_ENVIRONMENTS.staging,
521
+ expectations: { // Optional — from deriveExpectations()
522
+ tableCount: 124,
523
+ cognitoGroups: ['administrators', 'managers'],
524
+ },
525
+ exec: (cmd) => { // Shell command executor
526
+ try {
527
+ return execSync(cmd, { encoding: 'utf-8', timeout: 60_000 });
528
+ } catch {
529
+ return null;
530
+ }
531
+ },
532
+ log: (msg) => process.stdout.write(msg + '\n'),
533
+ };
534
+ ```
535
+
536
+ **Fields:**
537
+
538
+ | Field | Type | Description |
539
+ |-------|------|-------------|
540
+ | `environment` | `string` | Full environment name (`development`, `staging`, `production`) |
541
+ | `envShort` | `string` | Short code used in resource prefixes (`dev`, `stg`, `prod`) |
542
+ | `prefix` | `string` | Resource name prefix (e.g. `s20-dev`) |
543
+ | `region` | `string` | AWS region |
544
+ | `account` | `string` | AWS account ID |
545
+ | `projectRoot` | `string` | Absolute path to project root |
546
+ | `dryRun` | `boolean` | When `true`, no state files are written to disk |
547
+ | `envConfig` | `EnvironmentConfig` | The environment configuration driving this run |
548
+ | `expectations` | `Record<string, unknown>` | Optional key-value store for derived expectations |
549
+ | `exec` | `(cmd: string) => string \| null` | Runs a shell command, returns stdout or `null` on failure |
550
+ | `log` | `(msg: string) => void` | Logging function |
551
+
552
+ ### Running the Pipeline
553
+
554
+ ```typescript
555
+ import { PipelineEngine, buildGates, DEFAULT_ENVIRONMENTS } from 'anton-bakker-deploy-engine';
556
+
557
+ const envConfig = DEFAULT_ENVIRONMENTS.staging;
558
+ const gates = buildGates(envConfig);
559
+
560
+ const engine = new PipelineEngine({
561
+ gates,
562
+ checks, // Map<string, CheckExecutor>
563
+ ctx, // ExecutionContext
564
+ stateDir: './orchestrator', // Optional — defaults to <projectRoot>/orchestrator
565
+ });
566
+
567
+ const report = await engine.run();
568
+ ```
569
+
570
+ **Constructor options:**
571
+
572
+ | Option | Type | Required | Default | Description |
573
+ |--------|------|----------|---------|-------------|
574
+ | `gates` | `GateDefinition[]` | Yes | — | Ordered list of gates to execute |
575
+ | `checks` | `Map<string, CheckExecutor>` | Yes | — | Registered check executors |
576
+ | `ctx` | `ExecutionContext` | Yes | — | Execution context |
577
+ | `stateDir` | `string` | No | `<projectRoot>/orchestrator` | Directory for state and report files |
578
+
579
+ **The `run()` method returns a `PipelineReport`:**
580
+
581
+ ```typescript
582
+ interface PipelineReport {
583
+ state: PipelineState;
584
+ summary: {
585
+ totalGates: number;
586
+ passed: number;
587
+ failed: number;
588
+ skipped: number;
589
+ duration: number; // Total milliseconds
590
+ failedGate?: string; // ID of the gate that failed
591
+ action?: 'abort' | 'rollback' | 'alert'; // Failure action
592
+ };
593
+ }
594
+ ```
595
+
596
+ **Handling the result:**
597
+
598
+ ```typescript
599
+ const report = await engine.run();
600
+
601
+ switch (report.state.result) {
602
+ case 'passed':
603
+ console.log(`✅ All ${report.summary.totalGates} gates passed in ${report.summary.duration}ms`);
604
+ break;
605
+ case 'failed':
606
+ console.error(`❌ Failed at gate: ${report.summary.failedGate} → ${report.summary.action}`);
607
+ break;
608
+ case 'rolled-back':
609
+ console.error(`🔄 Rolled back at gate: ${report.summary.failedGate}`);
610
+ // Trigger actual rollback logic here
611
+ break;
612
+ }
613
+ ```
614
+
615
+ ### State Persistence and Resume
616
+
617
+ The engine saves its state to `<stateDir>/deploy-state-<environment>.json` after each gate completes. If a run is interrupted (process crash, network failure, timeout), the next run automatically resumes from the last passed gate.
618
+
619
+ **State file location:** `orchestrator/deploy-state-staging.json`
620
+
621
+ **Resume behaviour:**
622
+ - Gates that already passed are skipped with a log message
623
+ - The failed gate is re-executed
624
+ - New gates run normally
625
+
626
+ **Reports:** On successful completion, the engine also saves a report to `<stateDir>/deploy-reports/<timestamp>-<envShort>.json`.
627
+
628
+ **Dry run mode:** When `ctx.dryRun` is `true`, no state or report files are written. Useful for testing.
629
+
630
+ ### Clearing State
631
+
632
+ To force a fresh run (discard previous state), archive the state file:
633
+
634
+ ```typescript
635
+ PipelineEngine.clearState('./orchestrator', 'staging');
636
+ ```
637
+
638
+ This moves `deploy-state-staging.json` to `orchestrator/archive/state-<timestamp>.json`. The next `engine.run()` starts from gate 1.
639
+
640
+ ### Default Environments
641
+
642
+ `DEFAULT_ENVIRONMENTS` provides pre-built configurations for three environments:
643
+
644
+ **development:**
645
+
646
+ ```typescript
647
+ {
648
+ approval: 'never',
649
+ preDeployChecks: ['typecheck', 'lint', 'server-tests', 'schema-parse', 'merge-markers'],
650
+ migrations: true,
651
+ seed: { enabled: true, skipCalendar: true },
652
+ e2eTests: false,
653
+ backupBefore: false,
654
+ cdkNagLevel: 'warning',
655
+ postDeployProbes: ['api-health', 'table-count', 'ecs-status'],
656
+ rollbackOnProbeFailure: false,
657
+ }
658
+ ```
659
+
660
+ **staging:**
661
+
662
+ ```typescript
663
+ {
664
+ approval: 'never',
665
+ preDeployChecks: ['typecheck', 'lint', 'server-tests', 'web-tests', 'schema-parse', 'merge-markers'],
666
+ migrations: true,
667
+ seed: { enabled: true, skipCalendar: false },
668
+ e2eTests: true,
669
+ e2eTestSuite: 'web/e2e/migration/',
670
+ backupBefore: true,
671
+ cdkNagLevel: 'error',
672
+ postDeployProbes: ['api-health', 'table-count', 'ecs-status', 'cognito-groups', 'crud-probe'],
673
+ rollbackOnProbeFailure: true,
674
+ }
675
+ ```
676
+
677
+ **production:**
678
+
679
+ ```typescript
680
+ {
681
+ approval: 'broadening',
682
+ preDeployChecks: ['typecheck', 'lint', 'server-tests', 'web-tests', 'schema-parse',
683
+ 'merge-markers', 'schema-backward-compat'],
684
+ migrations: true,
685
+ seed: { enabled: false },
686
+ e2eTests: true,
687
+ e2eTestSuite: 'web/e2e/smoke/',
688
+ backupBefore: true,
689
+ cdkNagLevel: 'error',
690
+ postDeployProbes: ['api-health', 'table-count', 'ecs-status', 'cognito-groups',
691
+ 'crud-probe', 'auth-probe', 's3-probe'],
692
+ rollbackOnProbeFailure: true,
693
+ }
694
+ ```
695
+
696
+ **EnvironmentConfig fields:**
697
+
698
+ | Field | Type | Description |
699
+ |-------|------|-------------|
700
+ | `approval` | `'never' \| 'broadening' \| 'always'` | When to require human approval |
701
+ | `preDeployChecks` | `string[]` | Check names to run in the Code Quality gate |
702
+ | `migrations` | `boolean` | Whether to run database migrations |
703
+ | `seed` | `{ enabled, skipCalendar? }` | Whether to seed data after deploy |
704
+ | `e2eTests` | `boolean` | Whether to run E2E tests |
705
+ | `e2eTestSuite` | `string` | Path to E2E test suite |
706
+ | `backupBefore` | `boolean` | Whether to backup DynamoDB tables before deploy |
707
+ | `cdkNagLevel` | `'warning' \| 'error'` | CDK Nag severity level |
708
+ | `postDeployProbes` | `string[]` | Check names to run in the Functional gate |
709
+ | `rollbackOnProbeFailure` | `boolean` | Whether functional probe failures trigger rollback |
710
+
711
+ You can extend or override defaults:
712
+
713
+ ```typescript
714
+ const myConfig: EnvironmentConfig = {
715
+ ...DEFAULT_ENVIRONMENTS.staging,
716
+ backupBefore: false, // Skip backups for faster deploys
717
+ postDeployProbes: [...DEFAULT_ENVIRONMENTS.staging.postDeployProbes, 'custom-probe'],
718
+ };
719
+ ```
720
+
721
+ ## Types Reference
722
+
723
+ All types are exported from the package root.
724
+
725
+ | Type | Description |
726
+ |------|-------------|
727
+ | `CheckResult` | Single pass/fail check with name, expected, actual, error, duration |
728
+ | `GateDefinition` | Gate configuration: id, name, check names, failure action, optional condition |
729
+ | `GateResult` | Gate execution result: status, checks, timing |
730
+ | `EnvironmentConfig` | Environment-specific deployment configuration |
731
+ | `PipelineState` | Full pipeline run state: runId, environment, commit, gates, result |
732
+ | `PipelineReport` | Pipeline state + summary statistics |
733
+ | `CheckExecutor` | `(ctx: ExecutionContext) => Promise<CheckResult>` — async check function |
734
+ | `ExecutionContext` | Runtime context passed to every check executor |
735
+ | `DeployExpectations` | Output of `deriveExpectations()`: table count/names, Cognito groups, API domain |
736
+
737
+ ## Publishing
738
+
739
+ The package is published to npm via `scripts/publish.sh`.
740
+
741
+ ```bash
742
+ # Publish (auto-detects tag, auto-bumps patch if version exists)
743
+ npm run release:publish
744
+
745
+ # Publish explicitly to latest tag
746
+ npm run release:publish:latest
747
+
748
+ # Publish to beta tag
749
+ NPM_TAG=beta npm run release:publish
750
+ ```
751
+
752
+ **What the publish script does:**
753
+
754
+ 1. Commits and pushes any uncommitted changes
755
+ 2. Runs tests (`npm test`)
756
+ 3. Builds (`npm run build`)
757
+ 4. Fetches the npm automation token from AWS Secrets Manager (`npm/automation-token/anton-bakker-deploy-engine`)
758
+ 5. If the current version is already published, auto-bumps patch, commits, and pushes
759
+ 6. Publishes to npm with a temporary `.npmrc` (token never stored on disk)
760
+
761
+ **Environment variables:**
762
+
763
+ | Variable | Default | Description |
764
+ |----------|---------|-------------|
765
+ | `AWS_PROFILE` | `BeyondAmbition` | AWS profile for Secrets Manager access |
766
+ | `AWS_REGION` | `eu-west-1` | AWS region for Secrets Manager |
767
+ | `NPM_SECRET_NAME` | `npm/automation-token/anton-bakker-deploy-engine` | Secrets Manager secret ID |
768
+ | `NPM_TAG` | `latest` | npm publish tag |
769
+ | `REGISTRY` | `https://registry.npmjs.org/` | npm registry URL |
770
+
771
+ ## Development
772
+
773
+ ```bash
774
+ # Install dependencies
775
+ npm install
776
+
777
+ # Run tests
778
+ npm test
779
+
780
+ # Type check (no output)
781
+ npx tsc --noEmit
782
+
783
+ # Build (compiles to dist/)
784
+ npm run build
785
+
786
+ # Lint (zero warnings tolerance)
787
+ npm run lint
788
+ ```
789
+
790
+ **Project structure:**
791
+
792
+ ```
793
+ ├── src/
794
+ │ ├── types.ts # Type definitions
795
+ │ ├── verify.ts # Verification functions
796
+ │ ├── engine.ts # PipelineEngine class
797
+ │ ├── pipeline.ts # Gate builder + default configs
798
+ │ └── index.ts # Public exports
799
+ ├── __tests__/
800
+ │ ├── verify.test.ts # 48 tests for verification functions
801
+ │ ├── engine.test.ts # 13 tests for engine + pipeline
802
+ │ └── fixtures/ # Test fixtures (schema, config, auth stack)
803
+ ├── scripts/
804
+ │ └── publish.sh # npm publish script
805
+ ├── eslint.config.js # ESLint flat config
806
+ ├── tsconfig.json # TypeScript config (strict, ESM, NodeNext)
807
+ └── package.json
808
+ ```
809
+
810
+ **Test runner:** Vitest. All 61 tests run in ~200ms.
811
+
812
+ **TypeScript config:** Strict mode, ESM (`"type": "module"`), `NodeNext` module resolution, source maps and declaration maps enabled.
813
+
814
+ **ESLint rules:** `no-console: error`, `@typescript-eslint/no-unused-vars: error`.
815
+
816
+ ## Licence
817
+
818
+ MIT