anton-bakker-deploy-engine 0.1.0 → 0.2.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.
@@ -0,0 +1,23 @@
1
+ import type { GateDefinition, PipelineReport, CheckExecutor, ExecutionContext } from './types.js';
2
+ export declare class PipelineEngine {
3
+ private gates;
4
+ private checks;
5
+ private state;
6
+ private stateDir;
7
+ private ctx;
8
+ constructor(opts: {
9
+ gates: GateDefinition[];
10
+ checks: Map<string, CheckExecutor>;
11
+ ctx: ExecutionContext;
12
+ stateDir?: string;
13
+ });
14
+ private statePath;
15
+ private saveState;
16
+ /** Run all gates in order. Returns the pipeline report. */
17
+ run(): Promise<PipelineReport>;
18
+ private executeGate;
19
+ private buildReport;
20
+ private saveReport;
21
+ /** Clear state for a fresh run */
22
+ static clearState(stateDir: string, environment: string): void;
23
+ }
package/dist/engine.js ADDED
@@ -0,0 +1,156 @@
1
+ /**
2
+ * src/engine.ts — Pipeline gate executor with state persistence.
3
+ */
4
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
5
+ import { join } from 'path';
6
+ export class PipelineEngine {
7
+ gates;
8
+ checks;
9
+ state;
10
+ stateDir;
11
+ ctx;
12
+ constructor(opts) {
13
+ this.gates = opts.gates;
14
+ this.checks = opts.checks;
15
+ this.ctx = opts.ctx;
16
+ this.stateDir = opts.stateDir || join(opts.ctx.projectRoot, 'orchestrator');
17
+ // Load or create state
18
+ const statePath = this.statePath();
19
+ if (!opts.ctx.dryRun && existsSync(statePath)) {
20
+ this.state = JSON.parse(readFileSync(statePath, 'utf-8'));
21
+ this.ctx.log(`Resuming run ${this.state.runId} (${this.state.gates.filter(g => g.status === 'passed').length} gates already passed)`);
22
+ }
23
+ else {
24
+ this.state = {
25
+ runId: `deploy-${new Date().toISOString().replace(/[:.]/g, '-')}`,
26
+ environment: opts.ctx.environment,
27
+ commit: opts.ctx.exec('git rev-parse --short HEAD')?.trim() || 'unknown',
28
+ startedAt: new Date().toISOString(),
29
+ result: 'running',
30
+ gates: [],
31
+ };
32
+ }
33
+ }
34
+ statePath() {
35
+ return join(this.stateDir, `deploy-state-${this.ctx.environment}.json`);
36
+ }
37
+ saveState() {
38
+ if (this.ctx.dryRun)
39
+ return;
40
+ mkdirSync(this.stateDir, { recursive: true });
41
+ writeFileSync(this.statePath(), JSON.stringify(this.state, null, 2));
42
+ }
43
+ /** Run all gates in order. Returns the pipeline report. */
44
+ async run() {
45
+ const start = Date.now();
46
+ for (const gate of this.gates) {
47
+ // Skip if condition not met
48
+ if (gate.condition && !gate.condition(this.ctx.envConfig)) {
49
+ const skipped = {
50
+ id: gate.id, name: gate.name, status: 'skipped', onFail: gate.onFail,
51
+ checks: [], startedAt: new Date().toISOString(), completedAt: new Date().toISOString(), duration: 0,
52
+ };
53
+ this.state.gates.push(skipped);
54
+ this.ctx.log(`⏭️ Gate ${gate.name}: skipped (condition not met)`);
55
+ continue;
56
+ }
57
+ // Skip if already passed (resume)
58
+ const existing = this.state.gates.find(g => g.id === gate.id);
59
+ if (existing?.status === 'passed') {
60
+ this.ctx.log(`✅ Gate ${gate.name}: already passed (resuming)`);
61
+ continue;
62
+ }
63
+ // Execute gate
64
+ const gateResult = await this.executeGate(gate);
65
+ // Replace or append
66
+ const idx = this.state.gates.findIndex(g => g.id === gate.id);
67
+ if (idx >= 0)
68
+ this.state.gates[idx] = gateResult;
69
+ else
70
+ this.state.gates.push(gateResult);
71
+ this.saveState();
72
+ if (gateResult.status === 'failed') {
73
+ this.ctx.log(`❌ Gate ${gate.name} FAILED → ${gate.onFail.toUpperCase()}`);
74
+ this.state.result = gate.onFail === 'rollback' ? 'rolled-back' : 'failed';
75
+ this.state.completedAt = new Date().toISOString();
76
+ this.saveState();
77
+ return this.buildReport(Date.now() - start, gate);
78
+ }
79
+ }
80
+ this.state.result = 'passed';
81
+ this.state.completedAt = new Date().toISOString();
82
+ this.saveState();
83
+ this.saveReport(Date.now() - start);
84
+ return this.buildReport(Date.now() - start);
85
+ }
86
+ async executeGate(gate) {
87
+ const gateStart = Date.now();
88
+ this.ctx.log(`\n🔒 Gate ${gate.name}`);
89
+ const results = [];
90
+ for (const checkName of gate.checks) {
91
+ const executor = this.checks.get(checkName);
92
+ if (!executor) {
93
+ results.push({ name: checkName, passed: false, error: `No executor registered for '${checkName}'` });
94
+ this.ctx.log(` ❌ ${checkName}: no executor registered`);
95
+ continue;
96
+ }
97
+ const checkStart = Date.now();
98
+ try {
99
+ const result = await executor(this.ctx);
100
+ result.duration = Date.now() - checkStart;
101
+ results.push(result);
102
+ this.ctx.log(` ${result.passed ? '✅' : '❌'} ${result.name}: ${result.actual ?? (result.passed ? 'OK' : result.error)}`);
103
+ }
104
+ catch (err) {
105
+ const result = {
106
+ name: checkName, passed: false, duration: Date.now() - checkStart,
107
+ error: err.message,
108
+ };
109
+ results.push(result);
110
+ this.ctx.log(` ❌ ${checkName}: ${result.error}`);
111
+ }
112
+ }
113
+ return {
114
+ id: gate.id, name: gate.name, onFail: gate.onFail,
115
+ status: results.every(r => r.passed) ? 'passed' : 'failed',
116
+ checks: results,
117
+ startedAt: new Date(gateStart).toISOString(),
118
+ completedAt: new Date().toISOString(),
119
+ duration: Date.now() - gateStart,
120
+ };
121
+ }
122
+ buildReport(duration, failedGate) {
123
+ return {
124
+ state: this.state,
125
+ summary: {
126
+ totalGates: this.state.gates.length,
127
+ passed: this.state.gates.filter(g => g.status === 'passed').length,
128
+ failed: this.state.gates.filter(g => g.status === 'failed').length,
129
+ skipped: this.state.gates.filter(g => g.status === 'skipped').length,
130
+ duration,
131
+ failedGate: failedGate?.id,
132
+ action: failedGate?.onFail,
133
+ },
134
+ };
135
+ }
136
+ saveReport(duration) {
137
+ if (this.ctx.dryRun)
138
+ return;
139
+ const reportDir = join(this.stateDir, 'deploy-reports');
140
+ mkdirSync(reportDir, { recursive: true });
141
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 16);
142
+ const report = this.buildReport(duration);
143
+ writeFileSync(join(reportDir, `${ts}-${this.ctx.envShort}.json`), JSON.stringify(report, null, 2));
144
+ }
145
+ /** Clear state for a fresh run */
146
+ static clearState(stateDir, environment) {
147
+ const path = join(stateDir, `deploy-state-${environment}.json`);
148
+ if (existsSync(path)) {
149
+ const archiveDir = join(stateDir, 'archive');
150
+ mkdirSync(archiveDir, { recursive: true });
151
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
152
+ const { renameSync } = require('fs');
153
+ renameSync(path, join(archiveDir, `state-${ts}.json`));
154
+ }
155
+ }
156
+ }
package/dist/index.d.ts CHANGED
@@ -4,4 +4,7 @@
4
4
  * Self-validating AWS deployment engine.
5
5
  * 4-layer verification: exists → configured → functional → project-specific.
6
6
  */
7
- export { verifyAmplifyOutputs, verifyBuildArtifact, parseTableCount, assertTableCount, parseEcsStatus, assertCognitoGroups, assertApiHealth, deriveExpectations, summarize, type CheckResult, type PostBuildResult, type PostDeployResult, type DeployExpectations, } from './verify.js';
7
+ export { PipelineEngine } from './engine.js';
8
+ export { buildGates, DEFAULT_ENVIRONMENTS } from './pipeline.js';
9
+ export { verifyAmplifyOutputs, verifyBuildArtifact, parseTableCount, assertTableCount, parseEcsStatus, assertCognitoGroups, assertApiHealth, deriveExpectations, summarize, } from './verify.js';
10
+ export type { CheckResult, GateDefinition, GateResult, EnvironmentConfig, PipelineState, PipelineReport, CheckExecutor, ExecutionContext, } from './types.js';
package/dist/index.js CHANGED
@@ -4,12 +4,9 @@
4
4
  * Self-validating AWS deployment engine.
5
5
  * 4-layer verification: exists → configured → functional → project-specific.
6
6
  */
7
- export {
8
- // Post-build verification
9
- verifyAmplifyOutputs, verifyBuildArtifact,
10
- // Post-deploy assertions
11
- parseTableCount, assertTableCount, parseEcsStatus, assertCognitoGroups, assertApiHealth,
12
- // Expectations derivation
13
- deriveExpectations,
14
- // Summary
15
- summarize, } from './verify.js';
7
+ // Core engine
8
+ export { PipelineEngine } from './engine.js';
9
+ // Pipeline builder
10
+ export { buildGates, DEFAULT_ENVIRONMENTS } from './pipeline.js';
11
+ // Verification functions (Layer 1-3 checks)
12
+ export { verifyAmplifyOutputs, verifyBuildArtifact, parseTableCount, assertTableCount, parseEcsStatus, assertCognitoGroups, assertApiHealth, deriveExpectations, summarize, } from './verify.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * src/pipeline.ts — Builds a pipeline definition from environment config.
3
+ */
4
+ import type { GateDefinition, EnvironmentConfig } from './types.js';
5
+ /**
6
+ * Build the gate sequence for a given environment.
7
+ * No hardcoded environment checks — all driven by config.
8
+ */
9
+ export declare function buildGates(env: EnvironmentConfig): GateDefinition[];
10
+ /** Default environment configs */
11
+ export declare const DEFAULT_ENVIRONMENTS: Record<string, EnvironmentConfig>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Build the gate sequence for a given environment.
3
+ * No hardcoded environment checks — all driven by config.
4
+ */
5
+ export function buildGates(env) {
6
+ return [
7
+ {
8
+ id: 'preflight',
9
+ name: 'Preflight',
10
+ checks: ['aws-creds', 'deploy-lock'],
11
+ onFail: 'abort',
12
+ },
13
+ {
14
+ id: 'code-quality',
15
+ name: 'Code Quality',
16
+ checks: env.preDeployChecks,
17
+ onFail: 'abort',
18
+ },
19
+ {
20
+ id: 'infra-validation',
21
+ name: 'Infrastructure Validation',
22
+ checks: ['cdk-synth', 'derive-expectations'],
23
+ onFail: 'abort',
24
+ },
25
+ {
26
+ id: 'pre-deploy-safety',
27
+ name: 'Pre-Deploy Safety',
28
+ checks: [
29
+ ...(env.backupBefore ? ['backup-tables'] : []),
30
+ ...(env.migrations ? ['prepare-migrations'] : []),
31
+ ],
32
+ onFail: 'abort',
33
+ condition: (e) => e.backupBefore || e.migrations,
34
+ },
35
+ {
36
+ id: 'build',
37
+ name: 'Build',
38
+ checks: ['docker-build', 'frontend-build', 'post-build-verify'],
39
+ onFail: 'abort',
40
+ },
41
+ {
42
+ id: 'deploy',
43
+ name: 'Deploy',
44
+ checks: ['cdk-deploy', 'ecs-update', 'frontend-deploy', ...(env.seed.enabled ? ['seed-data'] : [])],
45
+ onFail: 'rollback',
46
+ },
47
+ {
48
+ id: 'inventory',
49
+ name: 'Post-Deploy Inventory',
50
+ checks: ['table-count', 'ecs-status', 'cognito-groups'],
51
+ onFail: 'rollback',
52
+ },
53
+ {
54
+ id: 'configuration',
55
+ name: 'Post-Deploy Configuration',
56
+ checks: ['pitr-enabled', 'billing-mode', 'https-enforced', 's3-not-public'],
57
+ onFail: 'alert',
58
+ },
59
+ {
60
+ id: 'functional',
61
+ name: 'Post-Deploy Functional',
62
+ checks: env.postDeployProbes,
63
+ onFail: env.rollbackOnProbeFailure ? 'rollback' : 'alert',
64
+ },
65
+ {
66
+ id: 'project',
67
+ name: 'Project Assertions',
68
+ checks: ['manifest-assertions', ...(env.e2eTests ? ['e2e-tests'] : [])],
69
+ onFail: 'alert',
70
+ },
71
+ ];
72
+ }
73
+ /** Default environment configs */
74
+ export const DEFAULT_ENVIRONMENTS = {
75
+ development: {
76
+ approval: 'never',
77
+ preDeployChecks: ['typecheck', 'lint', 'server-tests', 'schema-parse', 'merge-markers'],
78
+ migrations: true,
79
+ seed: { enabled: true, skipCalendar: true },
80
+ e2eTests: false,
81
+ backupBefore: false,
82
+ cdkNagLevel: 'warning',
83
+ postDeployProbes: ['api-health', 'table-count', 'ecs-status'],
84
+ rollbackOnProbeFailure: false,
85
+ },
86
+ staging: {
87
+ approval: 'never',
88
+ preDeployChecks: ['typecheck', 'lint', 'server-tests', 'web-tests', 'schema-parse', 'merge-markers'],
89
+ migrations: true,
90
+ seed: { enabled: true, skipCalendar: false },
91
+ e2eTests: true,
92
+ e2eTestSuite: 'web/e2e/migration/',
93
+ backupBefore: true,
94
+ cdkNagLevel: 'error',
95
+ postDeployProbes: ['api-health', 'table-count', 'ecs-status', 'cognito-groups', 'crud-probe'],
96
+ rollbackOnProbeFailure: true,
97
+ },
98
+ production: {
99
+ approval: 'broadening',
100
+ preDeployChecks: ['typecheck', 'lint', 'server-tests', 'web-tests', 'schema-parse', 'merge-markers', 'schema-backward-compat'],
101
+ migrations: true,
102
+ seed: { enabled: false },
103
+ e2eTests: true,
104
+ e2eTestSuite: 'web/e2e/smoke/',
105
+ backupBefore: true,
106
+ cdkNagLevel: 'error',
107
+ postDeployProbes: ['api-health', 'table-count', 'ecs-status', 'cognito-groups', 'crud-probe', 'auth-probe', 's3-probe'],
108
+ rollbackOnProbeFailure: true,
109
+ },
110
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * src/types.ts — Core types for the deployment pipeline engine.
3
+ */
4
+ export interface CheckResult {
5
+ name: string;
6
+ passed: boolean;
7
+ expected?: string | number;
8
+ actual?: string | number;
9
+ duration?: number;
10
+ error?: string;
11
+ }
12
+ export interface GateDefinition {
13
+ id: string;
14
+ name: string;
15
+ checks: string[];
16
+ onFail: 'abort' | 'rollback' | 'alert';
17
+ condition?: (env: EnvironmentConfig) => boolean;
18
+ }
19
+ export interface EnvironmentConfig {
20
+ approval: 'never' | 'broadening' | 'always';
21
+ preDeployChecks: string[];
22
+ migrations: boolean;
23
+ seed: {
24
+ enabled: boolean;
25
+ skipCalendar?: boolean;
26
+ };
27
+ e2eTests: boolean;
28
+ e2eTestSuite?: string;
29
+ backupBefore: boolean;
30
+ cdkNagLevel: 'warning' | 'error';
31
+ postDeployProbes: string[];
32
+ rollbackOnProbeFailure: boolean;
33
+ }
34
+ export interface GateResult {
35
+ id: string;
36
+ name: string;
37
+ status: 'passed' | 'failed' | 'skipped';
38
+ onFail: 'abort' | 'rollback' | 'alert';
39
+ checks: CheckResult[];
40
+ startedAt: string;
41
+ completedAt: string;
42
+ duration: number;
43
+ }
44
+ export interface PipelineState {
45
+ runId: string;
46
+ environment: string;
47
+ commit: string;
48
+ startedAt: string;
49
+ completedAt?: string;
50
+ result: 'running' | 'passed' | 'failed' | 'rolled-back';
51
+ gates: GateResult[];
52
+ }
53
+ export interface PipelineReport {
54
+ state: PipelineState;
55
+ summary: {
56
+ totalGates: number;
57
+ passed: number;
58
+ failed: number;
59
+ skipped: number;
60
+ duration: number;
61
+ failedGate?: string;
62
+ action?: 'abort' | 'rollback' | 'alert';
63
+ };
64
+ }
65
+ /** A check executor — runs a named check and returns a result */
66
+ export type CheckExecutor = (ctx: ExecutionContext) => Promise<CheckResult>;
67
+ export interface ExecutionContext {
68
+ environment: string;
69
+ envShort: string;
70
+ prefix: string;
71
+ region: string;
72
+ account: string;
73
+ projectRoot: string;
74
+ dryRun: boolean;
75
+ envConfig: EnvironmentConfig;
76
+ expectations?: Record<string, unknown>;
77
+ /** Run a shell command, return stdout */
78
+ exec: (cmd: string) => string | null;
79
+ /** Log a message */
80
+ log: (msg: string) => void;
81
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * src/types.ts — Core types for the deployment pipeline engine.
3
+ */
4
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anton-bakker-deploy-engine",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Self-validating AWS deployment engine with 4-layer verification: exists, configured, functional, project-specific",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -33,4 +33,4 @@
33
33
  "vitest": "^4.1.2",
34
34
  "@types/node": "^20.0.0"
35
35
  }
36
- }
36
+ }