@mondaydotcomorg/z2h-cli 0.33.1 → 0.33.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/dist/commands/backend.d.ts +6 -5
- package/dist/commands/backend.d.ts.map +1 -1
- package/dist/commands/backend.js +6 -12
- package/dist/commands/deploy.js +29 -16
- package/dist/esm/commands/backend.d.ts +6 -5
- package/dist/esm/commands/backend.d.ts.map +1 -1
- package/dist/esm/commands/backend.mjs +6 -12
- package/dist/esm/commands/deploy.mjs +29 -16
- package/dist/esm/util/broker/app.d.ts +2 -0
- package/dist/esm/util/broker/app.d.ts.map +1 -1
- package/dist/esm/util/broker/app.mjs +20 -7
- package/dist/util/broker/app.d.ts +2 -0
- package/dist/util/broker/app.d.ts.map +1 -1
- package/dist/util/broker/app.js +20 -7
- package/package.json +1 -1
- package/src/commands/__tests__/backend-deploy.test.ts +42 -21
- package/src/commands/__tests__/deploy.test.ts +53 -0
- package/src/commands/backend.ts +14 -14
- package/src/commands/deploy.ts +31 -17
- package/src/util/broker/app.ts +59 -50
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export declare function hasHandlers(appDir: string): boolean;
|
|
2
|
+
export interface ReadDetectAndUploadResult {
|
|
3
|
+
integrations: string[];
|
|
4
|
+
handlerNames: string[];
|
|
5
|
+
apiUsages: Record<string, unknown>;
|
|
6
|
+
}
|
|
2
7
|
/**
|
|
3
|
-
* Deploy the app's backend handlers to S3 and return the integrations detected
|
|
4
|
-
* from their sources. Each `.js` handler is uploaded as its own flat file — no
|
|
5
|
-
* compilation, no bundling, no npm deps.
|
|
6
|
-
*
|
|
7
8
|
* Integration sync note: the returned set is re-derived from the CURRENT backend
|
|
8
9
|
* sources on every deploy, and the platform set is reconciled to exactly match
|
|
9
10
|
* it (there is no stored declaration to fall back on). A deploy therefore always
|
|
@@ -17,7 +18,7 @@ export declare function deployBackend(opts: {
|
|
|
17
18
|
version: number;
|
|
18
19
|
deployedBy: string;
|
|
19
20
|
isPreview: boolean;
|
|
20
|
-
}): Promise<
|
|
21
|
+
}): Promise<ReadDetectAndUploadResult>;
|
|
21
22
|
/**
|
|
22
23
|
* Runs the same checks deploy does against the working tree, refreshing the
|
|
23
24
|
* local `usage.json` — no upload to S3. Throws on the first invalid handler.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/commands/backend.ts"],"names":[],"mappings":"AAyBA,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/commands/backend.ts"],"names":[],"mappings":"AAyBA,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAoBD,MAAM,WAAW,yBAAyB;IACxC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAiCD;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CASrC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe1F;AAsBD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,EAC5C,MAAM,GAAE,MAAsB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
package/dist/commands/backend.js
CHANGED
|
@@ -48,21 +48,19 @@ async function writeUsageJson(appDir, patch) {
|
|
|
48
48
|
const existing = await readExistingUsage(appDir);
|
|
49
49
|
await fs$1.writeJson(path__default.default.join(appDir, 'usage.json'), { ...existing, ...patch }, { spaces: 2 });
|
|
50
50
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Returns `[]` when there is no `backend/handlers/` dir (or it is empty of sources)
|
|
53
|
-
* so callers can always pass the result to `registerOrUpdateApp` without a separate gate.
|
|
54
|
-
*/
|
|
55
51
|
async function readDetectAndUpload(appDir, prefix, appName, version, deployedBy, logLabel) {
|
|
56
52
|
if (!hasHandlers(appDir)) {
|
|
57
|
-
|
|
53
|
+
await writeUsageJson(appDir, { appName, deployedBy, apiUsages: {} });
|
|
54
|
+
return { integrations: [], handlerNames: [], apiUsages: {} };
|
|
58
55
|
}
|
|
59
56
|
util_logger.info(`reading backend handlers ${logLabel}`);
|
|
60
57
|
const { handlers, apiDomains, apiUsages } = await backend_handlers.readHandlers(appDir);
|
|
61
58
|
const integrations = backend_detectIntegrations.detectIntegrations(apiDomains);
|
|
59
|
+
const handlerNames = Object.keys(handlers);
|
|
60
|
+
await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
62
61
|
if (Object.keys(handlers).length === 0) {
|
|
63
|
-
return integrations;
|
|
62
|
+
return { integrations, handlerNames, apiUsages };
|
|
64
63
|
}
|
|
65
|
-
await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
66
64
|
const s3 = util_s3_client.getS3Client();
|
|
67
65
|
const meta = JSON.stringify({ version, deployedBy, handlers: Object.keys(handlers) });
|
|
68
66
|
const put = (key, body, contentType) => s3.send(new clientS3.PutObjectCommand({ Bucket: constants.BUCKET_WRITE_NAME, Key: key, Body: body, ContentType: contentType }));
|
|
@@ -70,13 +68,9 @@ async function readDetectAndUpload(appDir, prefix, appName, version, deployedBy,
|
|
|
70
68
|
...Object.entries(handlers).map(([name, code]) => put(`${prefix}/${name}.js`, code, 'application/javascript')),
|
|
71
69
|
put(`${prefix}/meta.json`, meta, 'application/json'),
|
|
72
70
|
]);
|
|
73
|
-
return integrations;
|
|
71
|
+
return { integrations, handlerNames, apiUsages };
|
|
74
72
|
}
|
|
75
73
|
/**
|
|
76
|
-
* Deploy the app's backend handlers to S3 and return the integrations detected
|
|
77
|
-
* from their sources. Each `.js` handler is uploaded as its own flat file — no
|
|
78
|
-
* compilation, no bundling, no npm deps.
|
|
79
|
-
*
|
|
80
74
|
* Integration sync note: the returned set is re-derived from the CURRENT backend
|
|
81
75
|
* sources on every deploy, and the platform set is reconciled to exactly match
|
|
82
76
|
* it (there is no stored declaration to fall back on). A deploy therefore always
|
package/dist/commands/deploy.js
CHANGED
|
@@ -85,12 +85,9 @@ async function buildAndStamp(opts) {
|
|
|
85
85
|
*/
|
|
86
86
|
async function uploadAndRegister(opts) {
|
|
87
87
|
const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } = opts;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// reconciled to exactly match on register — see `deployBackend` for why this
|
|
92
|
-
// depends on deploying from the app's latest merged state.
|
|
93
|
-
const integrations = await commands_backend.deployBackend({
|
|
88
|
+
// Runs before the frontend upload so a `deployBackend` failure can't leave a
|
|
89
|
+
// preview's unversioned `MF` dir overwritten with no rollback.
|
|
90
|
+
const { integrations, handlerNames, apiUsages } = await commands_backend.deployBackend({
|
|
94
91
|
appDir: process.cwd(),
|
|
95
92
|
appName,
|
|
96
93
|
mfKeyPrefix: keyPrefix,
|
|
@@ -98,16 +95,32 @@ async function uploadAndRegister(opts) {
|
|
|
98
95
|
deployedBy: util_machineIdentity.whoAmI(),
|
|
99
96
|
isPreview,
|
|
100
97
|
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
98
|
+
try {
|
|
99
|
+
util_logger.info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
100
|
+
await util_s3_upload.uploadDir(paths.shadowBuild, keyPrefix);
|
|
101
|
+
await util_broker_app.registerOrUpdateApp({
|
|
102
|
+
appName,
|
|
103
|
+
token,
|
|
104
|
+
nextVersion,
|
|
105
|
+
entry,
|
|
106
|
+
gitRemote,
|
|
107
|
+
integrations,
|
|
108
|
+
handlerNames,
|
|
109
|
+
apiUsages,
|
|
110
|
+
description,
|
|
111
|
+
masterAppName,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
// A live deploy is safe either way — RDS's version gates what's actually served, so
|
|
116
|
+
// an orphaned S3 object is harmless — but a preview is served straight from its
|
|
117
|
+
// unversioned prefix with no such gate, so only it needs the warning below.
|
|
118
|
+
if (isPreview) {
|
|
119
|
+
util_logger.warn(`preview backend was already deployed before this failed — re-run "z2h-cli deploy --preview" to retry ` +
|
|
120
|
+
`(every step overwrites, so a retry fully repairs it)`);
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
111
124
|
}
|
|
112
125
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
113
126
|
// A preview inherits from the live app it previews, resolved by name server-side —
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export declare function hasHandlers(appDir: string): boolean;
|
|
2
|
+
export interface ReadDetectAndUploadResult {
|
|
3
|
+
integrations: string[];
|
|
4
|
+
handlerNames: string[];
|
|
5
|
+
apiUsages: Record<string, unknown>;
|
|
6
|
+
}
|
|
2
7
|
/**
|
|
3
|
-
* Deploy the app's backend handlers to S3 and return the integrations detected
|
|
4
|
-
* from their sources. Each `.js` handler is uploaded as its own flat file — no
|
|
5
|
-
* compilation, no bundling, no npm deps.
|
|
6
|
-
*
|
|
7
8
|
* Integration sync note: the returned set is re-derived from the CURRENT backend
|
|
8
9
|
* sources on every deploy, and the platform set is reconciled to exactly match
|
|
9
10
|
* it (there is no stored declaration to fall back on). A deploy therefore always
|
|
@@ -17,7 +18,7 @@ export declare function deployBackend(opts: {
|
|
|
17
18
|
version: number;
|
|
18
19
|
deployedBy: string;
|
|
19
20
|
isPreview: boolean;
|
|
20
|
-
}): Promise<
|
|
21
|
+
}): Promise<ReadDetectAndUploadResult>;
|
|
21
22
|
/**
|
|
22
23
|
* Runs the same checks deploy does against the working tree, refreshing the
|
|
23
24
|
* local `usage.json` — no upload to S3. Throws on the first invalid handler.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../../src/commands/backend.ts"],"names":[],"mappings":"AAyBA,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../../src/commands/backend.ts"],"names":[],"mappings":"AAyBA,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAoBD,MAAM,WAAW,yBAAyB;IACxC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAiCD;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CASrC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAe1F;AAsBD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,EAC5C,MAAM,GAAE,MAAsB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
|
@@ -42,21 +42,19 @@ async function writeUsageJson(appDir, patch) {
|
|
|
42
42
|
const existing = await readExistingUsage(appDir);
|
|
43
43
|
await writeJson(path.join(appDir, 'usage.json'), { ...existing, ...patch }, { spaces: 2 });
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Returns `[]` when there is no `backend/handlers/` dir (or it is empty of sources)
|
|
47
|
-
* so callers can always pass the result to `registerOrUpdateApp` without a separate gate.
|
|
48
|
-
*/
|
|
49
45
|
async function readDetectAndUpload(appDir, prefix, appName, version, deployedBy, logLabel) {
|
|
50
46
|
if (!hasHandlers(appDir)) {
|
|
51
|
-
|
|
47
|
+
await writeUsageJson(appDir, { appName, deployedBy, apiUsages: {} });
|
|
48
|
+
return { integrations: [], handlerNames: [], apiUsages: {} };
|
|
52
49
|
}
|
|
53
50
|
info(`reading backend handlers ${logLabel}`);
|
|
54
51
|
const { handlers, apiDomains, apiUsages } = await readHandlers(appDir);
|
|
55
52
|
const integrations = detectIntegrations(apiDomains);
|
|
53
|
+
const handlerNames = Object.keys(handlers);
|
|
54
|
+
await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
56
55
|
if (Object.keys(handlers).length === 0) {
|
|
57
|
-
return integrations;
|
|
56
|
+
return { integrations, handlerNames, apiUsages };
|
|
58
57
|
}
|
|
59
|
-
await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
60
58
|
const s3 = getS3Client();
|
|
61
59
|
const meta = JSON.stringify({ version, deployedBy, handlers: Object.keys(handlers) });
|
|
62
60
|
const put = (key, body, contentType) => s3.send(new PutObjectCommand({ Bucket: BUCKET_WRITE_NAME, Key: key, Body: body, ContentType: contentType }));
|
|
@@ -64,13 +62,9 @@ async function readDetectAndUpload(appDir, prefix, appName, version, deployedBy,
|
|
|
64
62
|
...Object.entries(handlers).map(([name, code]) => put(`${prefix}/${name}.js`, code, 'application/javascript')),
|
|
65
63
|
put(`${prefix}/meta.json`, meta, 'application/json'),
|
|
66
64
|
]);
|
|
67
|
-
return integrations;
|
|
65
|
+
return { integrations, handlerNames, apiUsages };
|
|
68
66
|
}
|
|
69
67
|
/**
|
|
70
|
-
* Deploy the app's backend handlers to S3 and return the integrations detected
|
|
71
|
-
* from their sources. Each `.js` handler is uploaded as its own flat file — no
|
|
72
|
-
* compilation, no bundling, no npm deps.
|
|
73
|
-
*
|
|
74
68
|
* Integration sync note: the returned set is re-derived from the CURRENT backend
|
|
75
69
|
* sources on every deploy, and the platform set is reconciled to exactly match
|
|
76
70
|
* it (there is no stored declaration to fall back on). A deploy therefore always
|
|
@@ -79,12 +79,9 @@ async function buildAndStamp(opts) {
|
|
|
79
79
|
*/
|
|
80
80
|
async function uploadAndRegister(opts) {
|
|
81
81
|
const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } = opts;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// reconciled to exactly match on register — see `deployBackend` for why this
|
|
86
|
-
// depends on deploying from the app's latest merged state.
|
|
87
|
-
const integrations = await deployBackend({
|
|
82
|
+
// Runs before the frontend upload so a `deployBackend` failure can't leave a
|
|
83
|
+
// preview's unversioned `MF` dir overwritten with no rollback.
|
|
84
|
+
const { integrations, handlerNames, apiUsages } = await deployBackend({
|
|
88
85
|
appDir: process.cwd(),
|
|
89
86
|
appName,
|
|
90
87
|
mfKeyPrefix: keyPrefix,
|
|
@@ -92,16 +89,32 @@ async function uploadAndRegister(opts) {
|
|
|
92
89
|
deployedBy: whoAmI(),
|
|
93
90
|
isPreview,
|
|
94
91
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
92
|
+
try {
|
|
93
|
+
info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
94
|
+
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
95
|
+
await registerOrUpdateApp({
|
|
96
|
+
appName,
|
|
97
|
+
token,
|
|
98
|
+
nextVersion,
|
|
99
|
+
entry,
|
|
100
|
+
gitRemote,
|
|
101
|
+
integrations,
|
|
102
|
+
handlerNames,
|
|
103
|
+
apiUsages,
|
|
104
|
+
description,
|
|
105
|
+
masterAppName,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
// A live deploy is safe either way — RDS's version gates what's actually served, so
|
|
110
|
+
// an orphaned S3 object is harmless — but a preview is served straight from its
|
|
111
|
+
// unversioned prefix with no such gate, so only it needs the warning below.
|
|
112
|
+
if (isPreview) {
|
|
113
|
+
warn(`preview backend was already deployed before this failed — re-run "z2h-cli deploy --preview" to retry ` +
|
|
114
|
+
`(every step overwrites, so a retry fully repairs it)`);
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
105
118
|
}
|
|
106
119
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
107
120
|
// A preview inherits from the live app it previews, resolved by name server-side —
|
|
@@ -29,6 +29,8 @@ export interface RegisterOrUpdateOptions {
|
|
|
29
29
|
entry: ManifestEntry;
|
|
30
30
|
gitRemote: string;
|
|
31
31
|
integrations?: string[];
|
|
32
|
+
handlerNames?: string[];
|
|
33
|
+
apiUsages?: Record<string, unknown>;
|
|
32
34
|
description?: string;
|
|
33
35
|
/**
|
|
34
36
|
* The base app a preview is for, sent only on first registration — the link
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA8DD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
|
|
@@ -47,11 +47,23 @@ async function claimApp(appName, gitRemote) {
|
|
|
47
47
|
throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
async function registerApp(
|
|
50
|
+
async function registerApp(params) {
|
|
51
|
+
const { appName, token, nextVersion: version, entry, visibility, gitRemote, integrations, handlerNames, apiUsages, description, masterAppName, } = params;
|
|
51
52
|
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
|
|
52
53
|
method: 'POST',
|
|
53
54
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
54
|
-
body: JSON.stringify({
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
appName,
|
|
57
|
+
version,
|
|
58
|
+
entry,
|
|
59
|
+
visibility,
|
|
60
|
+
gitRemote,
|
|
61
|
+
integrations,
|
|
62
|
+
handlerNames,
|
|
63
|
+
apiUsages,
|
|
64
|
+
description,
|
|
65
|
+
masterAppName,
|
|
66
|
+
}),
|
|
55
67
|
});
|
|
56
68
|
if (!res.ok) {
|
|
57
69
|
const text = await res.text();
|
|
@@ -59,11 +71,12 @@ async function registerApp(appName, token, version, entry, visibility, gitRemote
|
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
/** Releases a new version onto an app that already exists. */
|
|
62
|
-
async function updateApp(
|
|
74
|
+
async function updateApp(params) {
|
|
75
|
+
const { appName, token, nextVersion: version, entry, gitRemote, integrations, handlerNames, apiUsages, description, } = params;
|
|
63
76
|
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/update`, {
|
|
64
77
|
method: 'POST',
|
|
65
78
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
66
|
-
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, description }),
|
|
79
|
+
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, handlerNames, apiUsages, description }),
|
|
67
80
|
});
|
|
68
81
|
if (!res.ok) {
|
|
69
82
|
const text = await res.text();
|
|
@@ -77,16 +90,16 @@ async function updateApp(appName, token, version, entry, gitRemote, integrations
|
|
|
77
90
|
* deploy and preview so this branching logic lives in exactly one place.
|
|
78
91
|
*/
|
|
79
92
|
async function registerOrUpdateApp(opts) {
|
|
80
|
-
const { appName,
|
|
93
|
+
const { appName, nextVersion } = opts;
|
|
81
94
|
try {
|
|
82
95
|
if (nextVersion === 1) {
|
|
83
96
|
const visibility = await resolveAppVisibility(appName);
|
|
84
97
|
info(`registering "${appName}" → v${nextVersion} (${visibility})`);
|
|
85
|
-
await registerApp(
|
|
98
|
+
await registerApp({ ...opts, visibility });
|
|
86
99
|
}
|
|
87
100
|
else {
|
|
88
101
|
info(`updating "${appName}" → v${nextVersion}`);
|
|
89
|
-
await updateApp(
|
|
102
|
+
await updateApp(opts);
|
|
90
103
|
}
|
|
91
104
|
}
|
|
92
105
|
catch (err) {
|
|
@@ -29,6 +29,8 @@ export interface RegisterOrUpdateOptions {
|
|
|
29
29
|
entry: ManifestEntry;
|
|
30
30
|
gitRemote: string;
|
|
31
31
|
integrations?: string[];
|
|
32
|
+
handlerNames?: string[];
|
|
33
|
+
apiUsages?: Record<string, unknown>;
|
|
32
34
|
description?: string;
|
|
33
35
|
/**
|
|
34
36
|
* The base app a preview is for, sent only on first registration — the link
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/util/broker/app.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBvE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA8DD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtF"}
|
package/dist/util/broker/app.js
CHANGED
|
@@ -49,11 +49,23 @@ async function claimApp(appName, gitRemote) {
|
|
|
49
49
|
throw new Error(`Claiming app name failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
async function registerApp(
|
|
52
|
+
async function registerApp(params) {
|
|
53
|
+
const { appName, token, nextVersion: version, entry, visibility, gitRemote, integrations, handlerNames, apiUsages, description, masterAppName, } = params;
|
|
53
54
|
const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/register`, {
|
|
54
55
|
method: 'POST',
|
|
55
56
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
|
|
56
|
-
body: JSON.stringify({
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
appName,
|
|
59
|
+
version,
|
|
60
|
+
entry,
|
|
61
|
+
visibility,
|
|
62
|
+
gitRemote,
|
|
63
|
+
integrations,
|
|
64
|
+
handlerNames,
|
|
65
|
+
apiUsages,
|
|
66
|
+
description,
|
|
67
|
+
masterAppName,
|
|
68
|
+
}),
|
|
57
69
|
});
|
|
58
70
|
if (!res.ok) {
|
|
59
71
|
const text = await res.text();
|
|
@@ -61,11 +73,12 @@ async function registerApp(appName, token, version, entry, visibility, gitRemote
|
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
/** Releases a new version onto an app that already exists. */
|
|
64
|
-
async function updateApp(
|
|
76
|
+
async function updateApp(params) {
|
|
77
|
+
const { appName, token, nextVersion: version, entry, gitRemote, integrations, handlerNames, apiUsages, description, } = params;
|
|
65
78
|
const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/update`, {
|
|
66
79
|
method: 'POST',
|
|
67
80
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
|
|
68
|
-
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, description }),
|
|
81
|
+
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, handlerNames, apiUsages, description }),
|
|
69
82
|
});
|
|
70
83
|
if (!res.ok) {
|
|
71
84
|
const text = await res.text();
|
|
@@ -79,16 +92,16 @@ async function updateApp(appName, token, version, entry, gitRemote, integrations
|
|
|
79
92
|
* deploy and preview so this branching logic lives in exactly one place.
|
|
80
93
|
*/
|
|
81
94
|
async function registerOrUpdateApp(opts) {
|
|
82
|
-
const { appName,
|
|
95
|
+
const { appName, nextVersion } = opts;
|
|
83
96
|
try {
|
|
84
97
|
if (nextVersion === 1) {
|
|
85
98
|
const visibility = await util_promptVisibility.resolveAppVisibility(appName);
|
|
86
99
|
util_logger.info(`registering "${appName}" → v${nextVersion} (${visibility})`);
|
|
87
|
-
await registerApp(
|
|
100
|
+
await registerApp({ ...opts, visibility });
|
|
88
101
|
}
|
|
89
102
|
else {
|
|
90
103
|
util_logger.info(`updating "${appName}" → v${nextVersion}`);
|
|
91
|
-
await updateApp(
|
|
104
|
+
await updateApp(opts);
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
107
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -63,7 +63,29 @@ describe('deployBackend (live)', () => {
|
|
|
63
63
|
await rm(tmp, { recursive: true, force: true });
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
it('skips upload when
|
|
66
|
+
it('skips upload and resets usage.json when there is no handlers directory', async () => {
|
|
67
|
+
// hasHandlers is false here, so readHandlers is never reached — no mock needed.
|
|
68
|
+
await expect(
|
|
69
|
+
deployBackend({
|
|
70
|
+
appDir: tmp,
|
|
71
|
+
appName: 'mf-my-app',
|
|
72
|
+
mfKeyPrefix: 'mf-my-app/1',
|
|
73
|
+
version: 1,
|
|
74
|
+
deployedBy: 'test-user',
|
|
75
|
+
isPreview: false,
|
|
76
|
+
})
|
|
77
|
+
).resolves.toEqual({ integrations: [], handlerNames: [], apiUsages: {} });
|
|
78
|
+
expect(mockSend).not.toHaveBeenCalled();
|
|
79
|
+
await expect(readJson(path.join(tmp, 'usage.json'))).resolves.toEqual({
|
|
80
|
+
appName: 'mf-my-app',
|
|
81
|
+
deployedBy: 'test-user',
|
|
82
|
+
apiUsages: {},
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('skips upload and resets usage.json when backend/handlers exists but has no handler sources', async () => {
|
|
87
|
+
await ensureDir(path.join(tmp, 'backend', 'handlers'));
|
|
88
|
+
await writeJson(path.join(tmp, 'usage.json'), { apiUsages: { deleted_handler: {} }, keepMe: 1 });
|
|
67
89
|
readHandlersMock.mockResolvedValue({ handlers: {}, apiDomains: [], apiUsages: {} });
|
|
68
90
|
|
|
69
91
|
await expect(
|
|
@@ -75,8 +97,14 @@ describe('deployBackend (live)', () => {
|
|
|
75
97
|
deployedBy: 'test-user',
|
|
76
98
|
isPreview: false,
|
|
77
99
|
})
|
|
78
|
-
).resolves.toEqual([]);
|
|
100
|
+
).resolves.toEqual({ integrations: [], handlerNames: [], apiUsages: {} });
|
|
79
101
|
expect(mockSend).not.toHaveBeenCalled();
|
|
102
|
+
await expect(readJson(path.join(tmp, 'usage.json'))).resolves.toEqual({
|
|
103
|
+
keepMe: 1,
|
|
104
|
+
appName: 'mf-my-app',
|
|
105
|
+
deployedBy: 'test-user',
|
|
106
|
+
apiUsages: {},
|
|
107
|
+
});
|
|
80
108
|
});
|
|
81
109
|
|
|
82
110
|
it('transpiles and uploads handlers when backend/handlers exists', async () => {
|
|
@@ -98,11 +126,16 @@ describe('deployBackend (live)', () => {
|
|
|
98
126
|
deployedBy: 'test-user',
|
|
99
127
|
isPreview: false,
|
|
100
128
|
})
|
|
101
|
-
).resolves.toEqual(
|
|
129
|
+
).resolves.toEqual({
|
|
130
|
+
integrations: [],
|
|
131
|
+
handlerNames: ['call_snowflake'],
|
|
132
|
+
apiUsages: { call_snowflake: { snowflake: { query: [{ sql: 'x' }] } } },
|
|
133
|
+
});
|
|
102
134
|
expect(mockSend).toHaveBeenCalled();
|
|
103
135
|
const keys: string[] = mockSend.mock.calls.map((c: unknown[]) => (c[0] as { input: { Key: string } }).input.Key);
|
|
104
136
|
expect(keys.some(k => k.includes('/backend-1/'))).toBe(true);
|
|
105
|
-
// usage.json stays local-only
|
|
137
|
+
// usage.json stays local-only. meta.json is still uploaded as the S3 fallback,
|
|
138
|
+
// used only for a row whose `apps.handler_names` column is still null.
|
|
106
139
|
expect(keys.some(k => k.endsWith('/usage.json'))).toBe(false);
|
|
107
140
|
expect(keys.some(k => k.endsWith('/meta.json'))).toBe(true);
|
|
108
141
|
|
|
@@ -191,22 +224,6 @@ describe('deployBackend (preview)', () => {
|
|
|
191
224
|
await rm(tmp, { recursive: true, force: true });
|
|
192
225
|
});
|
|
193
226
|
|
|
194
|
-
it('skips upload when the handlers directory holds nothing', async () => {
|
|
195
|
-
readHandlersMock.mockResolvedValue({ handlers: {}, apiDomains: [], apiUsages: {} });
|
|
196
|
-
|
|
197
|
-
await expect(
|
|
198
|
-
deployBackend({
|
|
199
|
-
appDir: tmp,
|
|
200
|
-
appName: 'mf-zth-preview-abc12345-my-app',
|
|
201
|
-
mfKeyPrefix: 'previews/mf-zth-preview-abc12345-my-app/MF',
|
|
202
|
-
version: 1,
|
|
203
|
-
deployedBy: 'test-user',
|
|
204
|
-
isPreview: true,
|
|
205
|
-
})
|
|
206
|
-
).resolves.toEqual([]);
|
|
207
|
-
expect(mockSend).not.toHaveBeenCalled();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
227
|
it('uploads handlers under the MS prefix for preview', async () => {
|
|
211
228
|
await ensureDir(path.join(tmp, 'backend', 'handlers'));
|
|
212
229
|
await writeFile(path.join(tmp, 'backend', 'handlers', 'call_snowflake.ts'), '// placeholder');
|
|
@@ -225,7 +242,11 @@ describe('deployBackend (preview)', () => {
|
|
|
225
242
|
deployedBy: 'test-user',
|
|
226
243
|
isPreview: true,
|
|
227
244
|
})
|
|
228
|
-
).resolves.toEqual(
|
|
245
|
+
).resolves.toEqual({
|
|
246
|
+
integrations: ['monday'],
|
|
247
|
+
handlerNames: ['call_snowflake'],
|
|
248
|
+
apiUsages: { call_snowflake: { monday: { query: [{ query: 'x' }] } } },
|
|
249
|
+
});
|
|
229
250
|
expect(mockSend).toHaveBeenCalled();
|
|
230
251
|
const keys: string[] = mockSend.mock.calls.map((c: unknown[]) => (c[0] as { input: { Key: string } }).input.Key);
|
|
231
252
|
expect(keys.some(k => k.includes('/MS/'))).toBe(true);
|
|
@@ -7,7 +7,9 @@ import { deployCommand } from '../deploy';
|
|
|
7
7
|
import { DeployError, reasonOf } from '../../util/errors';
|
|
8
8
|
import { buildPreviewSlug } from '../../util/app-name';
|
|
9
9
|
import { setOutputMode } from '../../util/logger';
|
|
10
|
+
import * as loggerModule from '../../util/logger';
|
|
10
11
|
import { buildCommand } from '../build';
|
|
12
|
+
import * as backendCmd from '../backend';
|
|
11
13
|
import { uploadDir } from '../../util/s3/upload';
|
|
12
14
|
import { setupS3Client } from '../../util/s3/client';
|
|
13
15
|
import { openInBrowser } from '../../util/open-browser';
|
|
@@ -20,6 +22,15 @@ vi.mock('../build', () => ({
|
|
|
20
22
|
buildCommand: vi.fn(),
|
|
21
23
|
}));
|
|
22
24
|
|
|
25
|
+
// Wraps the real `deployBackend` so most tests exercise it unchanged (no
|
|
26
|
+
// backend/handlers dir in the tmp fixture, so it resolves trivially), while
|
|
27
|
+
// letting individual tests override it to prove the upload/register ordering
|
|
28
|
+
// and passthrough of real handlerNames/apiUsages.
|
|
29
|
+
vi.mock('../backend', async importOriginal => {
|
|
30
|
+
const actual = await importOriginal<typeof import('../backend')>();
|
|
31
|
+
return { ...actual, deployBackend: vi.fn(actual.deployBackend) };
|
|
32
|
+
});
|
|
33
|
+
|
|
23
34
|
vi.mock('../../util/s3/upload', () => ({
|
|
24
35
|
uploadDir: vi.fn().mockResolvedValue([]),
|
|
25
36
|
}));
|
|
@@ -97,6 +108,7 @@ describe('deploy --preview', () => {
|
|
|
97
108
|
buildDir: path.join(tmp, '.zth', 'build'),
|
|
98
109
|
assetManifest: ASSET_MANIFEST,
|
|
99
110
|
});
|
|
111
|
+
vi.mocked(backendCmd.deployBackend).mockClear();
|
|
100
112
|
vi.mocked(uploadDir).mockClear();
|
|
101
113
|
vi.mocked(setupS3Client).mockClear();
|
|
102
114
|
vi.mocked(openInBrowser).mockClear();
|
|
@@ -162,6 +174,8 @@ describe('deploy --preview', () => {
|
|
|
162
174
|
entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 3, gitRemote: '' }),
|
|
163
175
|
gitRemote: '',
|
|
164
176
|
integrations: [],
|
|
177
|
+
handlerNames: [],
|
|
178
|
+
apiUsages: {},
|
|
165
179
|
masterAppName: 'mf-my-app',
|
|
166
180
|
});
|
|
167
181
|
});
|
|
@@ -180,6 +194,8 @@ describe('deploy --preview', () => {
|
|
|
180
194
|
entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 1, gitRemote: '' }),
|
|
181
195
|
gitRemote: '',
|
|
182
196
|
integrations: [],
|
|
197
|
+
handlerNames: [],
|
|
198
|
+
apiUsages: {},
|
|
183
199
|
masterAppName: 'mf-my-app',
|
|
184
200
|
});
|
|
185
201
|
});
|
|
@@ -274,6 +290,43 @@ describe('deploy --preview', () => {
|
|
|
274
290
|
|
|
275
291
|
writeSpy.mockRestore();
|
|
276
292
|
});
|
|
293
|
+
|
|
294
|
+
it('never uploads the frontend or registers when deployBackend fails', async () => {
|
|
295
|
+
vi.mocked(backendCmd.deployBackend).mockRejectedValueOnce(new Error('backend upload exploded'));
|
|
296
|
+
|
|
297
|
+
await expect(deployCommand({ preview: true })).rejects.toThrow('backend upload exploded');
|
|
298
|
+
|
|
299
|
+
expect(uploadDir).not.toHaveBeenCalled();
|
|
300
|
+
expect(brokerApp.registerOrUpdateApp).not.toHaveBeenCalled();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('warns that a retry will repair it when the frontend upload fails after the backend already deployed', async () => {
|
|
304
|
+
vi.mocked(uploadDir).mockRejectedValueOnce(new Error('S3 exploded'));
|
|
305
|
+
const warnSpy = vi.spyOn(loggerModule, 'warn').mockImplementation(() => {});
|
|
306
|
+
|
|
307
|
+
await expect(deployCommand({ preview: true })).rejects.toThrow('S3 exploded');
|
|
308
|
+
|
|
309
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('re-run "z2h-cli deploy --preview"'));
|
|
310
|
+
warnSpy.mockRestore();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('forwards the real handlerNames and apiUsages deployBackend reports to registerOrUpdateApp', async () => {
|
|
314
|
+
vi.mocked(backendCmd.deployBackend).mockResolvedValueOnce({
|
|
315
|
+
integrations: ['snowflake'],
|
|
316
|
+
handlerNames: ['call_snowflake'],
|
|
317
|
+
apiUsages: { call_snowflake: { snowflake: { query: [{ sql: 'x' }] } } },
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await deployCommand({ preview: true });
|
|
321
|
+
|
|
322
|
+
expect(brokerApp.registerOrUpdateApp).toHaveBeenCalledWith(
|
|
323
|
+
expect.objectContaining({
|
|
324
|
+
integrations: ['snowflake'],
|
|
325
|
+
handlerNames: ['call_snowflake'],
|
|
326
|
+
apiUsages: { call_snowflake: { snowflake: { query: [{ sql: 'x' }] } } },
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
});
|
|
277
330
|
});
|
|
278
331
|
|
|
279
332
|
describe('deploy failure reasons', () => {
|
package/src/commands/backend.ts
CHANGED
|
@@ -45,10 +45,12 @@ async function writeUsageJson(appDir: string, patch: Record<string, unknown>): P
|
|
|
45
45
|
await writeJson(path.join(appDir, 'usage.json'), { ...existing, ...patch }, { spaces: 2 });
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
export interface ReadDetectAndUploadResult {
|
|
49
|
+
integrations: string[];
|
|
50
|
+
handlerNames: string[];
|
|
51
|
+
apiUsages: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
52
54
|
async function readDetectAndUpload(
|
|
53
55
|
appDir: string,
|
|
54
56
|
prefix: string,
|
|
@@ -56,17 +58,19 @@ async function readDetectAndUpload(
|
|
|
56
58
|
version: number,
|
|
57
59
|
deployedBy: string,
|
|
58
60
|
logLabel: string
|
|
59
|
-
): Promise<
|
|
61
|
+
): Promise<ReadDetectAndUploadResult> {
|
|
60
62
|
if (!hasHandlers(appDir)) {
|
|
61
|
-
|
|
63
|
+
await writeUsageJson(appDir, { appName, deployedBy, apiUsages: {} });
|
|
64
|
+
return { integrations: [], handlerNames: [], apiUsages: {} };
|
|
62
65
|
}
|
|
63
66
|
info(`reading backend handlers ${logLabel}`);
|
|
64
67
|
const { handlers, apiDomains, apiUsages } = await readHandlers(appDir);
|
|
65
68
|
const integrations = detectIntegrations(apiDomains);
|
|
69
|
+
const handlerNames = Object.keys(handlers);
|
|
70
|
+
await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
66
71
|
if (Object.keys(handlers).length === 0) {
|
|
67
|
-
return integrations;
|
|
72
|
+
return { integrations, handlerNames, apiUsages };
|
|
68
73
|
}
|
|
69
|
-
await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
70
74
|
const s3 = getS3Client();
|
|
71
75
|
const meta = JSON.stringify({ version, deployedBy, handlers: Object.keys(handlers) });
|
|
72
76
|
const put = (key: string, body: string, contentType: string) =>
|
|
@@ -75,14 +79,10 @@ async function readDetectAndUpload(
|
|
|
75
79
|
...Object.entries(handlers).map(([name, code]) => put(`${prefix}/${name}.js`, code, 'application/javascript')),
|
|
76
80
|
put(`${prefix}/meta.json`, meta, 'application/json'),
|
|
77
81
|
]);
|
|
78
|
-
return integrations;
|
|
82
|
+
return { integrations, handlerNames, apiUsages };
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/**
|
|
82
|
-
* Deploy the app's backend handlers to S3 and return the integrations detected
|
|
83
|
-
* from their sources. Each `.js` handler is uploaded as its own flat file — no
|
|
84
|
-
* compilation, no bundling, no npm deps.
|
|
85
|
-
*
|
|
86
86
|
* Integration sync note: the returned set is re-derived from the CURRENT backend
|
|
87
87
|
* sources on every deploy, and the platform set is reconciled to exactly match
|
|
88
88
|
* it (there is no stored declaration to fall back on). A deploy therefore always
|
|
@@ -96,7 +96,7 @@ export async function deployBackend(opts: {
|
|
|
96
96
|
version: number;
|
|
97
97
|
deployedBy: string;
|
|
98
98
|
isPreview: boolean;
|
|
99
|
-
}): Promise<
|
|
99
|
+
}): Promise<ReadDetectAndUploadResult> {
|
|
100
100
|
const { appDir, appName, mfKeyPrefix, version, deployedBy, isPreview } = opts;
|
|
101
101
|
const prefix = isPreview ? `${mfKeyPrefix.replace(/\/MF$/, '')}/MS` : `${appName}/backend-${version}`;
|
|
102
102
|
const logLabel = isPreview ? 'for preview' : `for "${appName}"`;
|
package/src/commands/deploy.ts
CHANGED
|
@@ -131,13 +131,9 @@ async function uploadAndRegister(opts: {
|
|
|
131
131
|
const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } =
|
|
132
132
|
opts;
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// `integrations` is inferred from the backend sources in the working tree and
|
|
138
|
-
// reconciled to exactly match on register — see `deployBackend` for why this
|
|
139
|
-
// depends on deploying from the app's latest merged state.
|
|
140
|
-
const integrations = await deployBackend({
|
|
134
|
+
// Runs before the frontend upload so a `deployBackend` failure can't leave a
|
|
135
|
+
// preview's unversioned `MF` dir overwritten with no rollback.
|
|
136
|
+
const { integrations, handlerNames, apiUsages } = await deployBackend({
|
|
141
137
|
appDir: process.cwd(),
|
|
142
138
|
appName,
|
|
143
139
|
mfKeyPrefix: keyPrefix,
|
|
@@ -146,16 +142,34 @@ async function uploadAndRegister(opts: {
|
|
|
146
142
|
isPreview,
|
|
147
143
|
});
|
|
148
144
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
145
|
+
try {
|
|
146
|
+
info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
147
|
+
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
148
|
+
|
|
149
|
+
await registerOrUpdateApp({
|
|
150
|
+
appName,
|
|
151
|
+
token,
|
|
152
|
+
nextVersion,
|
|
153
|
+
entry,
|
|
154
|
+
gitRemote,
|
|
155
|
+
integrations,
|
|
156
|
+
handlerNames,
|
|
157
|
+
apiUsages,
|
|
158
|
+
description,
|
|
159
|
+
masterAppName,
|
|
160
|
+
});
|
|
161
|
+
} catch (err) {
|
|
162
|
+
// A live deploy is safe either way — RDS's version gates what's actually served, so
|
|
163
|
+
// an orphaned S3 object is harmless — but a preview is served straight from its
|
|
164
|
+
// unversioned prefix with no such gate, so only it needs the warning below.
|
|
165
|
+
if (isPreview) {
|
|
166
|
+
warn(
|
|
167
|
+
`preview backend was already deployed before this failed — re-run "z2h-cli deploy --preview" to retry ` +
|
|
168
|
+
`(every step overwrites, so a retry fully repairs it)`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
async function deployPreview({ paths, consumerName, appName, token, opts }: DeployContext): Promise<void> {
|
package/src/util/broker/app.ts
CHANGED
|
@@ -64,21 +64,52 @@ export async function claimApp(appName: string, gitRemote: string): Promise<void
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
appName: string
|
|
69
|
-
token: string
|
|
70
|
-
|
|
71
|
-
entry: ManifestEntry
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
export interface RegisterOrUpdateOptions {
|
|
68
|
+
appName: string;
|
|
69
|
+
token: string;
|
|
70
|
+
nextVersion: number;
|
|
71
|
+
entry: ManifestEntry;
|
|
72
|
+
gitRemote: string;
|
|
73
|
+
integrations?: string[];
|
|
74
|
+
handlerNames?: string[];
|
|
75
|
+
apiUsages?: Record<string, unknown>;
|
|
76
|
+
description?: string;
|
|
77
|
+
/**
|
|
78
|
+
* The base app a preview is for, sent only on first registration — the link
|
|
79
|
+
* is set once and is immutable thereafter, so a later update doesn't need it.
|
|
80
|
+
*/
|
|
81
|
+
masterAppName?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function registerApp(params: RegisterOrUpdateOptions & { visibility: 'public' | 'private' }): Promise<void> {
|
|
85
|
+
const {
|
|
86
|
+
appName,
|
|
87
|
+
token,
|
|
88
|
+
nextVersion: version,
|
|
89
|
+
entry,
|
|
90
|
+
visibility,
|
|
91
|
+
gitRemote,
|
|
92
|
+
integrations,
|
|
93
|
+
handlerNames,
|
|
94
|
+
apiUsages,
|
|
95
|
+
description,
|
|
96
|
+
masterAppName,
|
|
97
|
+
} = params;
|
|
78
98
|
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
|
|
79
99
|
method: 'POST',
|
|
80
100
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
81
|
-
body: JSON.stringify({
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
appName,
|
|
103
|
+
version,
|
|
104
|
+
entry,
|
|
105
|
+
visibility,
|
|
106
|
+
gitRemote,
|
|
107
|
+
integrations,
|
|
108
|
+
handlerNames,
|
|
109
|
+
apiUsages,
|
|
110
|
+
description,
|
|
111
|
+
masterAppName,
|
|
112
|
+
}),
|
|
82
113
|
});
|
|
83
114
|
if (!res.ok) {
|
|
84
115
|
const text = await res.text();
|
|
@@ -87,19 +118,22 @@ async function registerApp(
|
|
|
87
118
|
}
|
|
88
119
|
|
|
89
120
|
/** Releases a new version onto an app that already exists. */
|
|
90
|
-
async function updateApp(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
async function updateApp(params: RegisterOrUpdateOptions): Promise<void> {
|
|
122
|
+
const {
|
|
123
|
+
appName,
|
|
124
|
+
token,
|
|
125
|
+
nextVersion: version,
|
|
126
|
+
entry,
|
|
127
|
+
gitRemote,
|
|
128
|
+
integrations,
|
|
129
|
+
handlerNames,
|
|
130
|
+
apiUsages,
|
|
131
|
+
description,
|
|
132
|
+
} = params;
|
|
99
133
|
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/update`, {
|
|
100
134
|
method: 'POST',
|
|
101
135
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
102
|
-
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, description }),
|
|
136
|
+
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, handlerNames, apiUsages, description }),
|
|
103
137
|
});
|
|
104
138
|
if (!res.ok) {
|
|
105
139
|
const text = await res.text();
|
|
@@ -107,21 +141,6 @@ async function updateApp(
|
|
|
107
141
|
}
|
|
108
142
|
}
|
|
109
143
|
|
|
110
|
-
export interface RegisterOrUpdateOptions {
|
|
111
|
-
appName: string;
|
|
112
|
-
token: string;
|
|
113
|
-
nextVersion: number;
|
|
114
|
-
entry: ManifestEntry;
|
|
115
|
-
gitRemote: string;
|
|
116
|
-
integrations?: string[];
|
|
117
|
-
description?: string;
|
|
118
|
-
/**
|
|
119
|
-
* The base app a preview is for, sent only on first registration — the link
|
|
120
|
-
* is set once and is immutable thereafter, so a later update doesn't need it.
|
|
121
|
-
*/
|
|
122
|
-
masterAppName?: string;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
144
|
/**
|
|
126
145
|
* v1 creates the app row (+ owner grant, prompting for visibility); later
|
|
127
146
|
* versions release onto it via update instead — the two are separate
|
|
@@ -129,26 +148,16 @@ export interface RegisterOrUpdateOptions {
|
|
|
129
148
|
* deploy and preview so this branching logic lives in exactly one place.
|
|
130
149
|
*/
|
|
131
150
|
export async function registerOrUpdateApp(opts: RegisterOrUpdateOptions): Promise<void> {
|
|
132
|
-
const { appName,
|
|
151
|
+
const { appName, nextVersion } = opts;
|
|
133
152
|
|
|
134
153
|
try {
|
|
135
154
|
if (nextVersion === 1) {
|
|
136
155
|
const visibility = await resolveAppVisibility(appName);
|
|
137
156
|
info(`registering "${appName}" → v${nextVersion} (${visibility})`);
|
|
138
|
-
await registerApp(
|
|
139
|
-
appName,
|
|
140
|
-
token,
|
|
141
|
-
nextVersion,
|
|
142
|
-
entry,
|
|
143
|
-
visibility,
|
|
144
|
-
gitRemote,
|
|
145
|
-
integrations,
|
|
146
|
-
description,
|
|
147
|
-
masterAppName
|
|
148
|
-
);
|
|
157
|
+
await registerApp({ ...opts, visibility });
|
|
149
158
|
} else {
|
|
150
159
|
info(`updating "${appName}" → v${nextVersion}`);
|
|
151
|
-
await updateApp(
|
|
160
|
+
await updateApp(opts);
|
|
152
161
|
}
|
|
153
162
|
} catch (err) {
|
|
154
163
|
// registerApp/updateApp already tag the non-ok-response case; this catches anything
|