anton-bakker-deploy-engine 0.1.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,7 @@
1
+ /**
2
+ * anton-bakker-deploy-engine
3
+ *
4
+ * Self-validating AWS deployment engine.
5
+ * 4-layer verification: exists → configured → functional → project-specific.
6
+ */
7
+ export { verifyAmplifyOutputs, verifyBuildArtifact, parseTableCount, assertTableCount, parseEcsStatus, assertCognitoGroups, assertApiHealth, deriveExpectations, summarize, type CheckResult, type PostBuildResult, type PostDeployResult, type DeployExpectations, } from './verify.js';
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * anton-bakker-deploy-engine
3
+ *
4
+ * Self-validating AWS deployment engine.
5
+ * 4-layer verification: exists → configured → functional → project-specific.
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';
@@ -0,0 +1,79 @@
1
+ /**
2
+ * verify.ts
3
+ * Location: anton-bakker-deploy-engine/src/verify.ts
4
+ *
5
+ * Generic AWS deployment verification functions.
6
+ * Validates amplify_outputs.json, build artifacts, DynamoDB, ECS, Cognito, and API health.
7
+ * Project-agnostic — works with any AWS CDK project.
8
+ */
9
+ export interface PostBuildResult {
10
+ passed: boolean;
11
+ checks: CheckResult[];
12
+ }
13
+ export interface CheckResult {
14
+ name: string;
15
+ passed: boolean;
16
+ expected?: string;
17
+ actual?: string;
18
+ error?: string;
19
+ }
20
+ /**
21
+ * Verify amplify_outputs.json has all required fields.
22
+ */
23
+ export declare function verifyAmplifyOutputs(outputsPath: string): CheckResult[];
24
+ /**
25
+ * Scan built JS for environment leaks.
26
+ */
27
+ export declare function verifyBuildArtifact(distDir: string): CheckResult[];
28
+ export interface PostDeployResult {
29
+ passed: boolean;
30
+ checks: CheckResult[];
31
+ tableCount: number;
32
+ }
33
+ /**
34
+ * Parse table count from AWS CLI JSON output.
35
+ */
36
+ export declare function parseTableCount(awsOutput: string | null, prefix: string): CheckResult & {
37
+ count: number;
38
+ };
39
+ /**
40
+ * Verify table count meets expected minimum.
41
+ */
42
+ export declare function assertTableCount(actual: number, expected: number): CheckResult;
43
+ /**
44
+ * Parse ECS service status from AWS CLI JSON output.
45
+ */
46
+ export declare function parseEcsStatus(awsOutput: string | null): CheckResult;
47
+ /**
48
+ * Verify Cognito groups contain all required groups.
49
+ */
50
+ export declare function assertCognitoGroups(actualGroups: string[], requiredGroups: string[]): CheckResult;
51
+ /**
52
+ * Verify API health check response.
53
+ */
54
+ export declare function assertApiHealth(response: string | null): CheckResult;
55
+ export interface DeployExpectations {
56
+ tableCount: number;
57
+ tableNames: string[];
58
+ cognitoGroups: string[];
59
+ apiDomain: string;
60
+ }
61
+ /**
62
+ * Derive expected values from source of truth files.
63
+ * Pass the paths to your schema, auth stack, and config — no assumptions about project layout.
64
+ */
65
+ export declare function deriveExpectations(opts: {
66
+ schemaPath: string;
67
+ authStackPath?: string;
68
+ configPath: string;
69
+ envShort: string;
70
+ }): DeployExpectations;
71
+ /**
72
+ * Aggregate check results into a pass/fail summary.
73
+ */
74
+ export declare function summarize(checks: CheckResult[]): {
75
+ passed: boolean;
76
+ total: number;
77
+ failed: number;
78
+ errors: string[];
79
+ };
package/dist/verify.js ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * verify.ts
3
+ * Location: anton-bakker-deploy-engine/src/verify.ts
4
+ *
5
+ * Generic AWS deployment verification functions.
6
+ * Validates amplify_outputs.json, build artifacts, DynamoDB, ECS, Cognito, and API health.
7
+ * Project-agnostic — works with any AWS CDK project.
8
+ */
9
+ import { readFileSync, readdirSync } from 'fs';
10
+ import { join } from 'path';
11
+ /**
12
+ * Verify amplify_outputs.json has all required fields.
13
+ */
14
+ export function verifyAmplifyOutputs(outputsPath) {
15
+ const results = [];
16
+ let outputs;
17
+ try {
18
+ outputs = JSON.parse(readFileSync(outputsPath, 'utf-8'));
19
+ }
20
+ catch (err) {
21
+ return [{ name: 'amplify_outputs.json readable', passed: false, error: `Cannot read: ${err.message}` }];
22
+ }
23
+ results.push({
24
+ name: 'version field present',
25
+ passed: !!outputs.version,
26
+ expected: '"1.4"',
27
+ actual: outputs.version ? String(outputs.version) : 'missing',
28
+ });
29
+ results.push({
30
+ name: 'version is 1.4',
31
+ passed: outputs.version === '1.4',
32
+ expected: '1.4',
33
+ actual: String(outputs.version ?? 'missing'),
34
+ });
35
+ const dataUrl = outputs.data?.url;
36
+ results.push({
37
+ name: 'data.url present',
38
+ passed: !!dataUrl,
39
+ expected: 'https://...',
40
+ actual: dataUrl || 'missing',
41
+ });
42
+ results.push({
43
+ name: 'data.url is HTTPS',
44
+ passed: !!dataUrl?.startsWith('https://'),
45
+ expected: 'starts with https://',
46
+ actual: dataUrl ? dataUrl.substring(0, 30) : 'missing',
47
+ });
48
+ const authPoolId = outputs.auth?.user_pool_id;
49
+ results.push({
50
+ name: 'auth.user_pool_id present',
51
+ passed: !!authPoolId,
52
+ expected: 'eu-central-1_...',
53
+ actual: authPoolId || 'missing',
54
+ });
55
+ const authClientId = outputs.auth?.user_pool_client_id;
56
+ results.push({
57
+ name: 'auth.user_pool_client_id present',
58
+ passed: !!authClientId,
59
+ expected: 'non-empty string',
60
+ actual: authClientId ? `${authClientId.substring(0, 8)}...` : 'missing',
61
+ });
62
+ return results;
63
+ }
64
+ /**
65
+ * Scan built JS for environment leaks.
66
+ */
67
+ export function verifyBuildArtifact(distDir) {
68
+ const results = [];
69
+ const assetsDir = join(distDir, 'assets');
70
+ let files;
71
+ try {
72
+ files = readdirSync(assetsDir);
73
+ }
74
+ catch {
75
+ return [{ name: 'dist/assets readable', passed: false, error: `Cannot read ${assetsDir}` }];
76
+ }
77
+ const indexJs = files.find(f => f.startsWith('index-') && f.endsWith('.js'));
78
+ if (!indexJs) {
79
+ return [{ name: 'index JS bundle exists', passed: false, error: 'No index-*.js found in dist/assets' }];
80
+ }
81
+ const content = readFileSync(join(assetsDir, indexJs), 'utf-8');
82
+ results.push({
83
+ name: 'no localhost:4000 in build',
84
+ passed: !content.includes('localhost:4000'),
85
+ expected: 'absent',
86
+ actual: content.includes('localhost:4000') ? 'FOUND — .env.local leaked' : 'absent',
87
+ });
88
+ results.push({
89
+ name: 'no localhost:5173 in build',
90
+ passed: !content.includes('localhost:5173'),
91
+ expected: 'absent',
92
+ actual: content.includes('localhost:5173') ? 'FOUND' : 'absent',
93
+ });
94
+ const httpAlbMatch = content.match(/http:\/\/[^"]*\.elb\.amazonaws\.com/);
95
+ results.push({
96
+ name: 'no HTTP ALB URLs in build',
97
+ passed: !httpAlbMatch,
98
+ expected: 'absent',
99
+ actual: httpAlbMatch ? `FOUND: ${httpAlbMatch[0]}` : 'absent',
100
+ });
101
+ return results;
102
+ }
103
+ /**
104
+ * Parse table count from AWS CLI JSON output.
105
+ */
106
+ export function parseTableCount(awsOutput, prefix) {
107
+ if (!awsOutput) {
108
+ return { name: 'DynamoDB table count', passed: false, count: 0, error: 'No output from AWS CLI' };
109
+ }
110
+ try {
111
+ const tables = JSON.parse(awsOutput);
112
+ const prefixed = tables.filter(t => t.startsWith(`${prefix}-`));
113
+ return {
114
+ name: 'DynamoDB table count',
115
+ passed: true,
116
+ count: prefixed.length,
117
+ actual: String(prefixed.length),
118
+ };
119
+ }
120
+ catch {
121
+ const count = parseInt(awsOutput.trim(), 10);
122
+ return {
123
+ name: 'DynamoDB table count',
124
+ passed: !isNaN(count),
125
+ count: isNaN(count) ? 0 : count,
126
+ actual: awsOutput.trim(),
127
+ };
128
+ }
129
+ }
130
+ /**
131
+ * Verify table count meets expected minimum.
132
+ */
133
+ export function assertTableCount(actual, expected) {
134
+ return {
135
+ name: `DynamoDB tables ≥ ${expected}`,
136
+ passed: actual >= expected,
137
+ expected: String(expected),
138
+ actual: String(actual),
139
+ error: actual < expected ? `${expected - actual} tables missing` : undefined,
140
+ };
141
+ }
142
+ /**
143
+ * Parse ECS service status from AWS CLI JSON output.
144
+ */
145
+ export function parseEcsStatus(awsOutput) {
146
+ if (!awsOutput) {
147
+ return { name: 'ECS service status', passed: false, error: 'No output from AWS CLI' };
148
+ }
149
+ try {
150
+ const ecs = JSON.parse(awsOutput);
151
+ const running = ecs.running ?? ecs.runningCount ?? 0;
152
+ const desired = ecs.desired ?? ecs.desiredCount ?? 0;
153
+ const status = ecs.status ?? 'UNKNOWN';
154
+ return {
155
+ name: 'ECS service healthy',
156
+ passed: status === 'ACTIVE' && running >= desired && desired > 0,
157
+ expected: `ACTIVE, running ≥ ${desired}`,
158
+ actual: `${status}, running=${running}, desired=${desired}`,
159
+ error: running < desired ? `${desired - running} tasks not running` : undefined,
160
+ };
161
+ }
162
+ catch {
163
+ return { name: 'ECS service status', passed: false, error: `Invalid JSON: ${awsOutput.substring(0, 100)}` };
164
+ }
165
+ }
166
+ /**
167
+ * Verify Cognito groups contain all required groups.
168
+ */
169
+ export function assertCognitoGroups(actualGroups, requiredGroups) {
170
+ const missing = requiredGroups.filter(g => !actualGroups.includes(g));
171
+ return {
172
+ name: 'Cognito required groups',
173
+ passed: missing.length === 0,
174
+ expected: requiredGroups.join(', '),
175
+ actual: actualGroups.join(', '),
176
+ error: missing.length > 0 ? `Missing: ${missing.join(', ')}` : undefined,
177
+ };
178
+ }
179
+ /**
180
+ * Verify API health check response.
181
+ */
182
+ export function assertApiHealth(response) {
183
+ return {
184
+ name: 'API health (__typename)',
185
+ passed: !!response?.includes('__typename'),
186
+ expected: 'contains __typename',
187
+ actual: response ? response.substring(0, 100) : 'no response',
188
+ };
189
+ }
190
+ /**
191
+ * Derive expected values from source of truth files.
192
+ * Pass the paths to your schema, auth stack, and config — no assumptions about project layout.
193
+ */
194
+ export function deriveExpectations(opts) {
195
+ const config = JSON.parse(readFileSync(opts.configPath, 'utf-8'));
196
+ // Count models from GraphQL schema — these become DynamoDB tables
197
+ const schema = readFileSync(opts.schemaPath, 'utf-8');
198
+ const allTypes = (schema.match(/^type (\w+) \{/gm) || []).map(m => m.match(/^type (\w+)/)[1]);
199
+ const tableNames = allTypes.filter(n => !['Query', 'Mutation', 'Subscription'].includes(n) &&
200
+ !n.endsWith('Input') && !n.endsWith('Connection'));
201
+ // Cognito groups from auth stack (optional)
202
+ let cognitoGroups = [];
203
+ if (opts.authStackPath) {
204
+ try {
205
+ const authStack = readFileSync(opts.authStackPath, 'utf-8');
206
+ // Match: groupName: 'X' (direct assignment)
207
+ const direct = [...authStack.matchAll(/groupName:\s*'(\w+)'/g)].map(m => m[1]);
208
+ // Match: for (... of ['X', 'Y', 'Z']) pattern used for group creation
209
+ const loopMatch = authStack.match(/\[([^\]]*)\].*?(?:groupName|Group)/s)
210
+ || authStack.match(/(?:groupName|Group).*?\[([^\]]*)\]/s)
211
+ || authStack.match(/for\s*\([^)]*of\s*\[([^\]]*)\]/);
212
+ const fromLoop = loopMatch
213
+ ? [...loopMatch[1].matchAll(/'(\w+)'/g)].map(m => m[1])
214
+ : [];
215
+ cognitoGroups = [...new Set([...direct, ...fromLoop])];
216
+ }
217
+ catch { /* auth stack may not exist yet */ }
218
+ }
219
+ const apiDomain = `${config.dns?.apiSubdomain || 'api'}.${config.dns?.zoneName || 'example.com'}`;
220
+ return {
221
+ tableCount: tableNames.length,
222
+ tableNames,
223
+ cognitoGroups,
224
+ apiDomain,
225
+ };
226
+ }
227
+ // ── Summary ─────────────────────────────────────────────────────────────
228
+ /**
229
+ * Aggregate check results into a pass/fail summary.
230
+ */
231
+ export function summarize(checks) {
232
+ const failed = checks.filter(c => !c.passed);
233
+ return {
234
+ passed: failed.length === 0,
235
+ total: checks.length,
236
+ failed: failed.length,
237
+ errors: failed.map(c => c.error || `${c.name}: expected ${c.expected}, got ${c.actual}`),
238
+ };
239
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "anton-bakker-deploy-engine",
3
+ "version": "0.1.0",
4
+ "description": "Self-validating AWS deployment engine with 4-layer verification: exists, configured, functional, project-specific",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "lint": "eslint src/ --max-warnings 0",
12
+ "prepublishOnly": "npm run build",
13
+ "release:publish": "bash scripts/publish.sh",
14
+ "release:publish:latest": "NPM_TAG=latest bash scripts/publish.sh"
15
+ },
16
+ "keywords": [
17
+ "aws",
18
+ "deployment",
19
+ "verification",
20
+ "cdk",
21
+ "devops"
22
+ ],
23
+ "author": "Anton Bakker",
24
+ "license": "MIT",
25
+ "files": [
26
+ "dist/"
27
+ ],
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "^5.8.3",
33
+ "vitest": "^4.1.2",
34
+ "@types/node": "^20.0.0"
35
+ }
36
+ }