@aws-blocks/core 0.1.7 → 0.1.10
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/db-naming.d.ts +17 -5
- package/dist/db-naming.d.ts.map +1 -1
- package/dist/db-naming.js +18 -6
- package/dist/db-naming.test.js +44 -3
- package/dist/hosting.d.ts +49 -0
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +47 -8
- package/dist/hosting.test.js +60 -0
- package/dist/scripts/deploy.d.ts.map +1 -1
- package/dist/scripts/deploy.js +4 -2
- package/dist/scripts/ensure-secrets.d.ts +5 -2
- package/dist/scripts/ensure-secrets.d.ts.map +1 -1
- package/dist/scripts/ensure-secrets.js +14 -6
- package/dist/scripts/external-migrations-step.d.ts.map +1 -1
- package/dist/scripts/external-migrations-step.js +5 -1
- package/dist/scripts/index.d.ts +1 -1
- package/dist/scripts/index.d.ts.map +1 -1
- package/dist/scripts/index.js +1 -1
- package/dist/scripts/sandbox.d.ts.map +1 -1
- package/dist/scripts/sandbox.js +10 -1
- package/dist/scripts/stack-id.d.ts +25 -0
- package/dist/scripts/stack-id.d.ts.map +1 -1
- package/dist/scripts/stack-id.js +25 -0
- package/dist/scripts/stack-id.test.js +51 -1
- package/dist/telemetry/client.d.ts +3 -1
- package/dist/telemetry/client.d.ts.map +1 -1
- package/dist/telemetry/client.js +20 -24
- package/dist/telemetry/telemetry-send-worker.d.ts +2 -0
- package/dist/telemetry/telemetry-send-worker.d.ts.map +1 -0
- package/dist/telemetry/telemetry-send-worker.js +58 -0
- package/dist/telemetry/telemetry.test.js +77 -1
- package/dist/telemetry/trackCommand.d.ts +1 -1
- package/dist/telemetry/trackCommand.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/db-naming.test.ts +50 -5
- package/src/db-naming.ts +18 -6
- package/src/hosting.test.ts +79 -0
- package/src/hosting.ts +105 -12
- package/src/scripts/deploy.ts +4 -2
- package/src/scripts/ensure-secrets.ts +17 -6
- package/src/scripts/external-migrations-step.ts +5 -1
- package/src/scripts/index.ts +1 -1
- package/src/scripts/sandbox.ts +10 -1
- package/src/scripts/stack-id.test.ts +61 -1
- package/src/scripts/stack-id.ts +26 -0
- package/src/telemetry/client.ts +22 -30
- package/src/telemetry/telemetry-send-worker.ts +60 -0
- package/src/telemetry/telemetry.test.ts +91 -1
- package/src/telemetry/trackCommand.ts +3 -3
- package/src/version.ts +1 -1
|
@@ -5,7 +5,7 @@ import assert from 'node:assert';
|
|
|
5
5
|
import { mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
|
-
import { getStackId, getSandboxId } from './stack-id.js';
|
|
8
|
+
import { getStackId, getSandboxId, getStackName } from './stack-id.js';
|
|
9
9
|
describe('getStackId', () => {
|
|
10
10
|
let tmpDir;
|
|
11
11
|
afterEach(() => {
|
|
@@ -52,3 +52,53 @@ describe('getSandboxId', () => {
|
|
|
52
52
|
assert.strictEqual(getSandboxId(tmpDir), 'alice-abc123');
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
|
+
describe('getStackName', () => {
|
|
56
|
+
let tmpDir;
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
if (tmpDir)
|
|
59
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
it('production is <stackId>-prod', () => {
|
|
62
|
+
tmpDir = join(tmpdir(), `stack-name-prod-${Date.now()}`);
|
|
63
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
64
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId: 'my-app-k7x2mf' }));
|
|
65
|
+
assert.strictEqual(getStackName({ sandbox: false, projectRoot: tmpDir }), 'my-app-k7x2mf-prod');
|
|
66
|
+
});
|
|
67
|
+
it('sandbox is <stackId>-<sandboxId>', () => {
|
|
68
|
+
tmpDir = join(tmpdir(), `stack-name-sbx-${Date.now()}`);
|
|
69
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
70
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId: 'my-app-k7x2mf' }));
|
|
71
|
+
mkdirSync(join(tmpDir, '.blocks-sandbox'), { recursive: true });
|
|
72
|
+
writeFileSync(join(tmpDir, '.blocks-sandbox', 'sandbox-id.txt'), 'alice-0d7e1c');
|
|
73
|
+
assert.strictEqual(getStackName({ sandbox: true, projectRoot: tmpDir }), 'my-app-k7x2mf-alice-0d7e1c');
|
|
74
|
+
});
|
|
75
|
+
it('throws actionable error when config is missing (fail fast, no silent fallback)', () => {
|
|
76
|
+
tmpDir = join(tmpdir(), `stack-name-missing-${Date.now()}`);
|
|
77
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
78
|
+
assert.throws(() => getStackName({ sandbox: false, projectRoot: tmpDir }), /\.blocks\/config\.json not found/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('getStackName sandbox id (get-or-create)', () => {
|
|
82
|
+
let tmpDir;
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
if (tmpDir)
|
|
85
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
it('creates and persists sandbox-id.txt on first call when missing', () => {
|
|
88
|
+
tmpDir = join(tmpdir(), `stack-name-getorcreate-${Date.now()}`);
|
|
89
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
90
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId: 'test-app' }));
|
|
91
|
+
// No .blocks-sandbox dir yet — getStackName creates the id rather than throwing.
|
|
92
|
+
const name = getStackName({ sandbox: true, projectRoot: tmpDir });
|
|
93
|
+
assert.match(name, /^test-app-[a-z0-9]+-[a-f0-9]{6}$/);
|
|
94
|
+
// Persisted so later callers/processes resolve the identical name.
|
|
95
|
+
const stored = readFileSync(join(tmpDir, '.blocks-sandbox', 'sandbox-id.txt'), 'utf-8').trim();
|
|
96
|
+
assert.strictEqual(name, `test-app-${stored}`);
|
|
97
|
+
});
|
|
98
|
+
it('reuses the same id across calls', () => {
|
|
99
|
+
tmpDir = join(tmpdir(), `stack-name-getorcreate-idem-${Date.now()}`);
|
|
100
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
101
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId: 'test-app' }));
|
|
102
|
+
assert.strictEqual(getStackName({ sandbox: true, projectRoot: tmpDir }), getStackName({ sandbox: true, projectRoot: tmpDir }));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -52,7 +52,9 @@ export declare function buildAndSendEvent(opts: BuildAndSendEventOptions): void;
|
|
|
52
52
|
/**
|
|
53
53
|
* Send a pre-built telemetry event to the collection endpoint.
|
|
54
54
|
*
|
|
55
|
-
*
|
|
55
|
+
* Spawns a detached subprocess that performs the HTTPS POST independently of
|
|
56
|
+
* the parent CLI process. This ensures the request completes even when the
|
|
57
|
+
* parent exits on failure paths before an in-process request would flush.
|
|
56
58
|
* Debug output available via `NODE_DEBUG=blocks-telemetry`.
|
|
57
59
|
*/
|
|
58
60
|
export declare function sendEvent(event: BlocksTelemetryEvent): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/telemetry/client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/telemetry/client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAYjF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAWzD;AAwBD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,wBAAwB,GAAG,oBAAoB,CAwD/E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,IAAI,CAatE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAgC3D"}
|
package/dist/telemetry/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { request as httpsRequest } from 'node:https';
|
|
2
|
-
import { request as httpRequest } from 'node:http';
|
|
3
1
|
import { existsSync, readFileSync, mkdirSync, openSync, writeSync, closeSync, writeFileSync, constants } from 'node:fs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
4
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { debuglog } from 'node:util';
|
|
6
6
|
import { CORE_VERSION } from '../version.js';
|
|
7
7
|
import { Scope } from '../common/index.js';
|
|
@@ -11,7 +11,6 @@ import { getInstallationId, getProjectId, generateEventId } from './identifiers.
|
|
|
11
11
|
// Debug log: writes to stderr via NODE_DEBUG=blocks-telemetry (developer-facing, for troubleshooting)
|
|
12
12
|
const debug = debuglog('blocks-telemetry');
|
|
13
13
|
const DEFAULT_ENDPOINT = 'https://blocks-telemetry.us-east-1.api.aws/metrics';
|
|
14
|
-
const TIMEOUT_MS = 500;
|
|
15
14
|
const TELEMETRY_VERSION = '1.0.0';
|
|
16
15
|
function getEndpoint() {
|
|
17
16
|
return process.env.BLOCKS_TELEMETRY_ENDPOINT || DEFAULT_ENDPOINT;
|
|
@@ -172,7 +171,9 @@ export function buildAndSendEvent(opts) {
|
|
|
172
171
|
/**
|
|
173
172
|
* Send a pre-built telemetry event to the collection endpoint.
|
|
174
173
|
*
|
|
175
|
-
*
|
|
174
|
+
* Spawns a detached subprocess that performs the HTTPS POST independently of
|
|
175
|
+
* the parent CLI process. This ensures the request completes even when the
|
|
176
|
+
* parent exits on failure paths before an in-process request would flush.
|
|
176
177
|
* Debug output available via `NODE_DEBUG=blocks-telemetry`.
|
|
177
178
|
*/
|
|
178
179
|
export function sendEvent(event) {
|
|
@@ -187,27 +188,22 @@ export function sendEvent(event) {
|
|
|
187
188
|
catch { }
|
|
188
189
|
}
|
|
189
190
|
debug('sending event to %s (%d bytes)', endpoint, Buffer.byteLength(payload));
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
method: 'POST',
|
|
198
|
-
headers: {
|
|
199
|
-
'Content-Type': 'application/json',
|
|
200
|
-
'Content-Length': Buffer.byteLength(payload),
|
|
201
|
-
},
|
|
202
|
-
timeout: TIMEOUT_MS,
|
|
203
|
-
}, (res) => {
|
|
204
|
-
debug('event sent (status=%d)', res.statusCode);
|
|
205
|
-
res.resume();
|
|
191
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
192
|
+
const workerPath = path.join(dir, 'telemetry-send-worker.js');
|
|
193
|
+
const child = spawn(process.execPath, [workerPath, endpoint], {
|
|
194
|
+
detached: true,
|
|
195
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
196
|
+
// Clear NODE_OPTIONS so inherited flags (e.g. --conditions=cdk) don't interfere
|
|
197
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
206
198
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
199
|
+
child.stdin.on('error', (err) => { debug('stdin write failed: %s', err.message); });
|
|
200
|
+
// Payload is small (<1KB JSON) so it fits in the kernel pipe buffer (~64KB)
|
|
201
|
+
// and survives the parent closing its fd on exit.
|
|
202
|
+
child.stdin.write(payload);
|
|
203
|
+
child.stdin.end();
|
|
204
|
+
child.on('error', (err) => { debug('spawn failed: %s', err.message); });
|
|
205
|
+
child.unref();
|
|
206
|
+
debug('spawned telemetry subprocess (pid=%d)', child.pid);
|
|
211
207
|
}
|
|
212
208
|
catch {
|
|
213
209
|
// Telemetry must never throw or affect the user's command
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-send-worker.d.ts","sourceRoot":"","sources":["../../src/telemetry/telemetry-send-worker.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Telemetry send worker — spawned as a detached subprocess.
|
|
5
|
+
* Reads JSON payload from stdin, POSTs it to the endpoint (argv[2]).
|
|
6
|
+
*
|
|
7
|
+
* Uses only Node built-ins — no project imports — so the compiled .js
|
|
8
|
+
* runs with bare `node` (no tsx needed).
|
|
9
|
+
*/
|
|
10
|
+
import { request as httpsRequest } from 'node:https';
|
|
11
|
+
import { request as httpRequest } from 'node:http';
|
|
12
|
+
const TIMEOUT_MS = 500;
|
|
13
|
+
const debug = (process.env.NODE_DEBUG || '').includes('blocks-telemetry');
|
|
14
|
+
const endpoint = process.argv[2];
|
|
15
|
+
if (!endpoint)
|
|
16
|
+
process.exit(1);
|
|
17
|
+
let payload = '';
|
|
18
|
+
process.stdin.setEncoding('utf-8');
|
|
19
|
+
process.stdin.on('data', (chunk) => { payload += chunk; });
|
|
20
|
+
process.stdin.on('end', () => {
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(endpoint);
|
|
23
|
+
const isHttps = url.protocol === 'https:';
|
|
24
|
+
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
25
|
+
const req = requestFn({
|
|
26
|
+
hostname: url.hostname,
|
|
27
|
+
port: url.port || (isHttps ? '443' : '80'),
|
|
28
|
+
path: url.pathname + url.search,
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
33
|
+
},
|
|
34
|
+
timeout: TIMEOUT_MS,
|
|
35
|
+
}, (res) => {
|
|
36
|
+
res.resume();
|
|
37
|
+
if (debug)
|
|
38
|
+
process.stderr.write(`BLOCKS-TELEMETRY: sent (status=${res.statusCode})\n`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
});
|
|
41
|
+
req.on('error', (e) => {
|
|
42
|
+
if (debug)
|
|
43
|
+
process.stderr.write(`BLOCKS-TELEMETRY: error: ${e.message}\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
46
|
+
req.on('timeout', () => {
|
|
47
|
+
if (debug)
|
|
48
|
+
process.stderr.write(`BLOCKS-TELEMETRY: timed out\n`);
|
|
49
|
+
req.destroy();
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
req.write(payload);
|
|
53
|
+
req.end();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
@@ -10,7 +10,7 @@ import { isCI, detectOS, detectNodeVersion, detectPackageManager, detectAgent, c
|
|
|
10
10
|
import { trackCommand, classifyError } from './trackCommand.js';
|
|
11
11
|
import { buildAndSendEvent, buildEvent, sendEvent, getTelemetryFilePath } from './client.js';
|
|
12
12
|
import { getInstallationId, getProjectId, generateEventId } from './identifiers.js';
|
|
13
|
-
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { spawnSync, spawn as spawnChild } from 'node:child_process';
|
|
14
14
|
import { Scope } from '../common/index.js';
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
describe('telemetry/consent', () => {
|
|
@@ -453,6 +453,82 @@ describe('telemetry/client', () => {
|
|
|
453
453
|
process.env = { ...originalEnv };
|
|
454
454
|
});
|
|
455
455
|
});
|
|
456
|
+
describe('telemetry/send-worker', () => {
|
|
457
|
+
it('worker POSTs payload from stdin to endpoint', async () => {
|
|
458
|
+
const received = [];
|
|
459
|
+
const server = await new Promise((resolve) => {
|
|
460
|
+
const s = createServer((req, res) => {
|
|
461
|
+
let body = '';
|
|
462
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
463
|
+
req.on('end', () => {
|
|
464
|
+
received.push(body);
|
|
465
|
+
res.writeHead(200);
|
|
466
|
+
res.end();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
s.listen(0, '127.0.0.1', () => resolve(s));
|
|
470
|
+
});
|
|
471
|
+
const addr = server.address();
|
|
472
|
+
const endpoint = `http://127.0.0.1:${addr.port}/collect`;
|
|
473
|
+
const payload = JSON.stringify({ test: true, command: 'dev' });
|
|
474
|
+
const workerPath = join(__dirname, 'telemetry-send-worker.js');
|
|
475
|
+
const exitCode = await new Promise((resolve) => {
|
|
476
|
+
const proc = spawnChild(process.execPath, [workerPath, endpoint], {
|
|
477
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
478
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
479
|
+
});
|
|
480
|
+
proc.stdin.write(payload);
|
|
481
|
+
proc.stdin.end();
|
|
482
|
+
proc.on('close', (code) => resolve(code));
|
|
483
|
+
});
|
|
484
|
+
assert.strictEqual(exitCode, 0);
|
|
485
|
+
assert.strictEqual(received.length, 1);
|
|
486
|
+
assert.deepStrictEqual(JSON.parse(received[0]), { test: true, command: 'dev' });
|
|
487
|
+
server.close();
|
|
488
|
+
});
|
|
489
|
+
it('worker exits with 1 on unreachable endpoint', async () => {
|
|
490
|
+
const payload = JSON.stringify({ test: true });
|
|
491
|
+
const workerPath = join(__dirname, 'telemetry-send-worker.js');
|
|
492
|
+
const exitCode = await new Promise((resolve) => {
|
|
493
|
+
const proc = spawnChild(process.execPath, [workerPath, 'http://127.0.0.1:1/unreachable'], {
|
|
494
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
495
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
496
|
+
});
|
|
497
|
+
proc.stdin.write(payload);
|
|
498
|
+
proc.stdin.end();
|
|
499
|
+
proc.on('close', (code) => resolve(code));
|
|
500
|
+
});
|
|
501
|
+
assert.strictEqual(exitCode, 1);
|
|
502
|
+
});
|
|
503
|
+
it('worker writes debug output to stderr when NODE_DEBUG is set', async () => {
|
|
504
|
+
const server = await new Promise((resolve) => {
|
|
505
|
+
const s = createServer((req, res) => {
|
|
506
|
+
let body = '';
|
|
507
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
508
|
+
req.on('end', () => { res.writeHead(200); res.end(); });
|
|
509
|
+
});
|
|
510
|
+
s.listen(0, '127.0.0.1', () => resolve(s));
|
|
511
|
+
});
|
|
512
|
+
const addr = server.address();
|
|
513
|
+
const endpoint = `http://127.0.0.1:${addr.port}/collect`;
|
|
514
|
+
const payload = JSON.stringify({ test: true });
|
|
515
|
+
const workerPath = join(__dirname, 'telemetry-send-worker.js');
|
|
516
|
+
const result = await new Promise((resolve) => {
|
|
517
|
+
const proc = spawnChild(process.execPath, [workerPath, endpoint], {
|
|
518
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
519
|
+
env: { ...process.env, NODE_OPTIONS: '', NODE_DEBUG: 'blocks-telemetry' },
|
|
520
|
+
});
|
|
521
|
+
let stderr = '';
|
|
522
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
523
|
+
proc.stdin.write(payload);
|
|
524
|
+
proc.stdin.end();
|
|
525
|
+
proc.on('close', (code) => resolve({ code, stderr }));
|
|
526
|
+
});
|
|
527
|
+
assert.strictEqual(result.code, 0);
|
|
528
|
+
assert.ok(result.stderr.includes('BLOCKS-TELEMETRY: sent (status=200)'), `Expected debug output, got: ${result.stderr}`);
|
|
529
|
+
server.close();
|
|
530
|
+
});
|
|
531
|
+
});
|
|
456
532
|
describe('telemetry/trackCommand integration', () => {
|
|
457
533
|
const originalEnv = { ...process.env };
|
|
458
534
|
afterEach(() => {
|
|
@@ -12,7 +12,7 @@ export declare function classifyError(error: unknown): {
|
|
|
12
12
|
*
|
|
13
13
|
* Measures wall-clock duration, classifies errors, and sends a single telemetry event
|
|
14
14
|
* after the command completes (success or failure). The telemetry send is fire-and-forget
|
|
15
|
-
* and bounded by a
|
|
15
|
+
* and bounded by a 500ms timeout — it will never delay the command or affect its exit code.
|
|
16
16
|
*
|
|
17
17
|
* If telemetry is disabled (via env var or config), the function is executed directly
|
|
18
18
|
* with zero overhead.
|
|
@@ -20,10 +20,10 @@ export function classifyError(error) {
|
|
|
20
20
|
if (msg.includes('cdk synth') || msg.includes('synthesis')) {
|
|
21
21
|
return { code: 'CDK_SYNTH_FAILED', phase: 'synth' };
|
|
22
22
|
}
|
|
23
|
-
if (msg.includes('cdk deploy') || msg.includes('deployment failed')) {
|
|
23
|
+
if (msg.includes('cdk deploy') || msg.includes('deployment failed') || (msg.includes('cdk') && msg.includes('deploy') && msg.includes('exited with code'))) {
|
|
24
24
|
return { code: 'CDK_DEPLOY_FAILED', phase: 'deploy' };
|
|
25
25
|
}
|
|
26
|
-
if (msg.includes('cdk destroy') || msg.includes('destroy failed')) {
|
|
26
|
+
if (msg.includes('cdk destroy') || msg.includes('destroy failed') || (msg.includes('cdk') && msg.includes('destroy') && msg.includes('exited with code'))) {
|
|
27
27
|
return { code: 'CDK_DESTROY_FAILED', phase: 'destroy' };
|
|
28
28
|
}
|
|
29
29
|
if (msg.includes('npm install') || msg.includes('npm err')) {
|
|
@@ -48,7 +48,7 @@ export function classifyError(error) {
|
|
|
48
48
|
*
|
|
49
49
|
* Measures wall-clock duration, classifies errors, and sends a single telemetry event
|
|
50
50
|
* after the command completes (success or failure). The telemetry send is fire-and-forget
|
|
51
|
-
* and bounded by a
|
|
51
|
+
* and bounded by a 500ms timeout — it will never delay the command or affect its exit code.
|
|
52
52
|
*
|
|
53
53
|
* If telemetry is disabled (via env var or config), the function is executed directly
|
|
54
54
|
* with zero overhead.
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CORE_VERSION = "0.1.
|
|
1
|
+
export declare const CORE_VERSION = "0.1.10";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY,WAAW,CAAC"}
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-version.mjs — do not edit manually
|
|
2
|
-
export const CORE_VERSION = '0.1.
|
|
2
|
+
export const CORE_VERSION = '0.1.10';
|
package/package.json
CHANGED
package/src/db-naming.test.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { test, describe } from 'node:test';
|
|
4
|
+
import { test, describe, afterEach } from 'node:test';
|
|
5
5
|
import assert from 'node:assert';
|
|
6
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
6
9
|
import { extractDbRef, dbConnectionParameterName } from './db-naming.js';
|
|
10
|
+
import { getStackName } from './scripts/stack-id.js';
|
|
7
11
|
|
|
8
12
|
describe('extractDbRef', () => {
|
|
9
13
|
test('pooler form (postgres.{ref}@) yields ref', () => {
|
|
@@ -39,10 +43,51 @@ describe('extractDbRef', () => {
|
|
|
39
43
|
});
|
|
40
44
|
|
|
41
45
|
describe('dbConnectionParameterName', () => {
|
|
42
|
-
test('
|
|
43
|
-
assert.strictEqual(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
test('formats stack name into parameter path', () => {
|
|
47
|
+
assert.strictEqual(dbConnectionParameterName('my-app-k7x2mf-prod'), '/my-app-k7x2mf-prod-db-url');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('two distinct stack names produce distinct parameter names', () => {
|
|
51
|
+
assert.notStrictEqual(
|
|
52
|
+
dbConnectionParameterName('app-a-111111-prod'),
|
|
53
|
+
dbConnectionParameterName('app-b-222222-prod'),
|
|
46
54
|
);
|
|
47
55
|
});
|
|
48
56
|
});
|
|
57
|
+
|
|
58
|
+
describe('cross-site invariant: write name == read name', () => {
|
|
59
|
+
let tmpDir: string;
|
|
60
|
+
let originalCwd: string;
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
if (originalCwd) process.chdir(originalCwd);
|
|
64
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function setupProject(stackId: string, sandboxId: string): string {
|
|
68
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'cross-site-'));
|
|
69
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
70
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId }));
|
|
71
|
+
mkdirSync(join(tmpDir, '.blocks-sandbox'), { recursive: true });
|
|
72
|
+
writeFileSync(join(tmpDir, '.blocks-sandbox', 'sandbox-id.txt'), sandboxId);
|
|
73
|
+
return tmpDir;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test('write-side name == read-side name (sandbox)', () => {
|
|
77
|
+
const root = setupProject('my-app-k7x2mf', 'alice-0d7e1c');
|
|
78
|
+
const writeName = dbConnectionParameterName(getStackName({ sandbox: true, projectRoot: root }));
|
|
79
|
+
originalCwd = process.cwd();
|
|
80
|
+
process.chdir(root);
|
|
81
|
+
const readName = dbConnectionParameterName(getStackName({ sandbox: true }));
|
|
82
|
+
assert.strictEqual(writeName, readName);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('write-side name == read-side name (production)', () => {
|
|
86
|
+
const root = setupProject('my-app-k7x2mf', 'alice-0d7e1c');
|
|
87
|
+
const writeName = dbConnectionParameterName(getStackName({ sandbox: false, projectRoot: root }));
|
|
88
|
+
originalCwd = process.cwd();
|
|
89
|
+
process.chdir(root);
|
|
90
|
+
const readName = dbConnectionParameterName(getStackName({ sandbox: false }));
|
|
91
|
+
assert.strictEqual(writeName, readName);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/src/db-naming.ts
CHANGED
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
* database connection string, and for the project ref derived from a Postgres
|
|
7
7
|
* connection string.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The connection-string parameter name is **stack-scoped** (embeds the
|
|
10
|
+
* deployment's stack name), so two Blocks apps in the same account + region +
|
|
11
|
+
* stage get distinct names and cannot overwrite each other's credentials.
|
|
12
|
+
*
|
|
13
|
+
* Two call sites compute the name: the pre-deploy writer (`ensure-secrets`) and
|
|
14
|
+
* the `db pull` generated wiring at synth. Both pass the result of
|
|
15
|
+
* `getStackName({ sandbox, projectRoot })` into this function, so they produce
|
|
16
|
+
* the same name by construction. The runtime Lambda does not call this function;
|
|
17
|
+
* it reads the name recorded at synth.
|
|
12
18
|
*/
|
|
13
19
|
|
|
14
20
|
/**
|
|
@@ -35,7 +41,13 @@ export function extractDbRef(connectionString: string): string {
|
|
|
35
41
|
throw new Error('Cannot extract database identifier from connection string.');
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
/**
|
|
45
|
+
* SSM SecureString parameter name for a deployment's external database
|
|
46
|
+
* connection string.
|
|
47
|
+
*
|
|
48
|
+
* Pure string transform: `/<stackName>-db-url`. The caller is responsible for
|
|
49
|
+
* computing the stack name via `getStackName({ sandbox, projectRoot })`.
|
|
50
|
+
*/
|
|
51
|
+
export function dbConnectionParameterName(stackName: string): string {
|
|
52
|
+
return `/${stackName}-db-url`;
|
|
41
53
|
}
|
package/src/hosting.test.ts
CHANGED
|
@@ -1428,4 +1428,83 @@ describe('Hosting', () => {
|
|
|
1428
1428
|
template.resourceCountIs('AWS::CloudFront::Distribution', 1);
|
|
1429
1429
|
});
|
|
1430
1430
|
});
|
|
1431
|
+
|
|
1432
|
+
// ── basePath prop (caller-declared source of truth) ─────────
|
|
1433
|
+
// Under KVS edge routing, basePath is no longer expressed as a per-behavior
|
|
1434
|
+
// PathPattern prefix — it lives in the KVS route table's `meta.bp`, which the
|
|
1435
|
+
// edge router uses for the canonical 308 + static strip. So these tests read
|
|
1436
|
+
// the basePath out of the RouteStoreKeys custom resource's Entries.
|
|
1437
|
+
describe('basePath prop', () => {
|
|
1438
|
+
const metaBasePath = (root: string, basePath?: string): string => {
|
|
1439
|
+
const app = new App();
|
|
1440
|
+
const stack = new Stack(app, 'BasePathStack', {
|
|
1441
|
+
env: { account: '123456789012', region: 'us-east-1' },
|
|
1442
|
+
});
|
|
1443
|
+
new Hosting(stack, 'Web', {
|
|
1444
|
+
root,
|
|
1445
|
+
framework: 'spa',
|
|
1446
|
+
buildOutputDir: 'dist',
|
|
1447
|
+
...(basePath !== undefined ? { basePath } : {}),
|
|
1448
|
+
});
|
|
1449
|
+
const tpl = Template.fromStack(stack).toJSON() as {
|
|
1450
|
+
Resources: Record<string, { Type: string; Properties?: any }>;
|
|
1451
|
+
};
|
|
1452
|
+
const kvKeys = Object.entries(tpl.Resources).find(
|
|
1453
|
+
([id, r]) =>
|
|
1454
|
+
r.Type === 'AWS::CloudFormation::CustomResource' &&
|
|
1455
|
+
/RouteStoreKeys/.test(id),
|
|
1456
|
+
);
|
|
1457
|
+
assert.ok(kvKeys, 'expected a RouteStoreKeys custom resource');
|
|
1458
|
+
const entries = JSON.parse(kvKeys![1].Properties.Entries);
|
|
1459
|
+
const meta = JSON.parse(entries.meta);
|
|
1460
|
+
return meta.bp as string;
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
it('records basePath in the KVS route table when set (SPA, no framework base)', () => {
|
|
1464
|
+
createSpaBuildOutput(tmpDir);
|
|
1465
|
+
assert.strictEqual(metaBasePath(tmpDir, '/app'), '/app');
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
it('normalizes a trailing slash (/app/ → /app)', () => {
|
|
1469
|
+
createSpaBuildOutput(tmpDir);
|
|
1470
|
+
assert.strictEqual(metaBasePath(tmpDir, '/app/'), '/app');
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
it('treats "/" as no base path', () => {
|
|
1474
|
+
createSpaBuildOutput(tmpDir);
|
|
1475
|
+
assert.strictEqual(metaBasePath(tmpDir, '/'), '');
|
|
1476
|
+
});
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
// ── P0.4: config.json ordering dependency ────────────────────
|
|
1480
|
+
describe('config.json deploy ordering (P0.4)', () => {
|
|
1481
|
+
it('BlocksConfigDeployment depends on the asset deployments', () => {
|
|
1482
|
+
// The asset deployments upload the whole static dir — including the
|
|
1483
|
+
// placeholder `.blocks-sandbox/config.json` — to the same key the
|
|
1484
|
+
// resolved config writes to. Without an ordering dependency the
|
|
1485
|
+
// placeholder can clobber the real config. The previous
|
|
1486
|
+
// `tryFindChild('AssetDeployment')` never matched the real child ids
|
|
1487
|
+
// (AssetDeploymentImmutable/Html/Mutable), so the dep was never wired.
|
|
1488
|
+
createSpaBuildOutput(tmpDir);
|
|
1489
|
+
const app = new App();
|
|
1490
|
+
const stack = new Stack(app, 'ConfigOrderStack');
|
|
1491
|
+
new Hosting(stack, 'Hosting', { root: tmpDir, api: MOCK_API });
|
|
1492
|
+
|
|
1493
|
+
const tpl = Template.fromStack(stack).toJSON() as {
|
|
1494
|
+
Resources: Record<string, { Type: string; DependsOn?: string[] }>;
|
|
1495
|
+
};
|
|
1496
|
+
const configId = Object.keys(tpl.Resources).find(
|
|
1497
|
+
(id) => /BlocksConfigDeployment/.test(id) && /CustomResource/.test(id),
|
|
1498
|
+
);
|
|
1499
|
+
assert.ok(configId, 'expected a BlocksConfigDeployment custom resource');
|
|
1500
|
+
|
|
1501
|
+
const dependsOn = tpl.Resources[configId].DependsOn ?? [];
|
|
1502
|
+
const assetDeps = dependsOn.filter((d) => /AssetDeployment/.test(d));
|
|
1503
|
+
assert.ok(
|
|
1504
|
+
assetDeps.length >= 1,
|
|
1505
|
+
`BlocksConfigDeployment must DependsOn the asset deployment(s); ` +
|
|
1506
|
+
`found DependsOn=${JSON.stringify(dependsOn)}`,
|
|
1507
|
+
);
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1431
1510
|
});
|