@mondaydotcomorg/z2h-cli 0.29.4 → 0.30.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.
- package/dist/commands/deploy.js +19 -2
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +6 -0
- package/dist/esm/commands/deploy.mjs +20 -3
- package/dist/esm/commands/generate.d.ts.map +1 -1
- package/dist/esm/commands/generate.mjs +8 -2
- 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 +4 -4
- 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 +4 -4
- package/handler-api/backend-runner/channels/snowflake-channel/run-query.ts +11 -14
- package/handler-api/backend-runner/channels/with-timeout.ts +1 -1
- package/package.json +2 -2
- package/src/commands/__tests__/deploy.test.ts +28 -0
- package/src/commands/deploy.ts +26 -3
- package/src/commands/generate.ts +8 -1
- package/src/util/broker/app.ts +17 -4
package/dist/commands/deploy.js
CHANGED
|
@@ -90,7 +90,7 @@ async function buildAndStamp(opts) {
|
|
|
90
90
|
* register the release with the platform in one server-side transaction.
|
|
91
91
|
*/
|
|
92
92
|
async function uploadAndRegister(opts) {
|
|
93
|
-
const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description } = opts;
|
|
93
|
+
const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } = opts;
|
|
94
94
|
util_logger.info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
95
95
|
await util_s3_upload.uploadDir(paths.shadowBuild, keyPrefix);
|
|
96
96
|
// `integrations` is inferred from the backend sources in the working tree and
|
|
@@ -104,9 +104,25 @@ async function uploadAndRegister(opts) {
|
|
|
104
104
|
deployedBy: whoDeployed(),
|
|
105
105
|
isPreview,
|
|
106
106
|
});
|
|
107
|
-
await util_broker_app.registerOrUpdateApp({
|
|
107
|
+
await util_broker_app.registerOrUpdateApp({
|
|
108
|
+
appName,
|
|
109
|
+
token,
|
|
110
|
+
nextVersion,
|
|
111
|
+
entry,
|
|
112
|
+
gitRemote,
|
|
113
|
+
integrations,
|
|
114
|
+
description,
|
|
115
|
+
masterAppName,
|
|
116
|
+
});
|
|
108
117
|
}
|
|
109
118
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
119
|
+
// A preview inherits from the live app it previews, resolved by name server-side —
|
|
120
|
+
// fail fast with an actionable message if that app was never claimed/deployed (e.g. a
|
|
121
|
+
// local checkout scaffolded before `create` started claiming names), rather than
|
|
122
|
+
// letting a deep backend 404 surface as a generic "App registration failed".
|
|
123
|
+
if (!(await util_broker_app.fetchApp(appName))) {
|
|
124
|
+
throw new util_errors.DeployError('register_error', `Cannot preview "${appName}": it has never been created or deployed. Run 'z2h-cli create ${consumerName}' (or deploy it once) first.`);
|
|
125
|
+
}
|
|
110
126
|
await fs.ensureDir(paths.shadowDir);
|
|
111
127
|
const { previewId } = await shadow_prepare.resolvePortAndMetadata(paths, util_appName.cleanAppName(consumerName));
|
|
112
128
|
const previewSlug = util_appName.buildPreviewSlug(consumerName, previewId);
|
|
@@ -138,6 +154,7 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
|
138
154
|
entry,
|
|
139
155
|
gitRemote: '',
|
|
140
156
|
isPreview: true,
|
|
157
|
+
masterAppName: appName,
|
|
141
158
|
});
|
|
142
159
|
const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
|
|
143
160
|
util_logger.output(`[z2h-cli] preview ready ✅\n app: ${appName}\n previewAppName: ${previewAppName}\n open: ${previewUrl}`, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqF3F"}
|
|
@@ -141,6 +141,12 @@ async function generateCommand(appName, opts) {
|
|
|
141
141
|
if (existing?.deployed) {
|
|
142
142
|
throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
|
|
143
143
|
}
|
|
144
|
+
// generate skips `create` entirely, but the preview it deploys below still resolves this
|
|
145
|
+
// app's own row by name server-side — claim it now so that lookup finds something, same as
|
|
146
|
+
// `create` does for the normal flow.
|
|
147
|
+
if (!existing) {
|
|
148
|
+
await util_broker_app.claimApp(util_appName.mfAppName(appName), util_git_repo.defaultS3Remote(appName));
|
|
149
|
+
}
|
|
144
150
|
util_logger.info(`copying template "${template.id}" → ${appDir}`);
|
|
145
151
|
// Excludes node_modules (installed into templateDir itself by build-templates.mjs, never
|
|
146
152
|
// needed by the generated app), .build-shadow (a scratch dir that build-templates.mjs
|
|
@@ -14,7 +14,7 @@ import { resolveGitEnv } from '../util/git/git-env.mjs';
|
|
|
14
14
|
import { validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options.mjs';
|
|
15
15
|
import { setupS3Client } from '../util/s3/client.mjs';
|
|
16
16
|
import { getMachineAppName } from '../util/machine-identity.mjs';
|
|
17
|
-
import { registerOrUpdateApp } from '../util/broker/app.mjs';
|
|
17
|
+
import { fetchApp, registerOrUpdateApp } from '../util/broker/app.mjs';
|
|
18
18
|
import { openInBrowser } from '../util/open-browser.mjs';
|
|
19
19
|
import { resolvePortAndMetadata } from '../shadow/prepare.mjs';
|
|
20
20
|
import { tagDeployError, DeployError, grantReasonForStatus } from '../util/errors.mjs';
|
|
@@ -83,7 +83,7 @@ async function buildAndStamp(opts) {
|
|
|
83
83
|
* register the release with the platform in one server-side transaction.
|
|
84
84
|
*/
|
|
85
85
|
async function uploadAndRegister(opts) {
|
|
86
|
-
const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description } = opts;
|
|
86
|
+
const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } = opts;
|
|
87
87
|
info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
88
88
|
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
89
89
|
// `integrations` is inferred from the backend sources in the working tree and
|
|
@@ -97,9 +97,25 @@ async function uploadAndRegister(opts) {
|
|
|
97
97
|
deployedBy: whoDeployed(),
|
|
98
98
|
isPreview,
|
|
99
99
|
});
|
|
100
|
-
await registerOrUpdateApp({
|
|
100
|
+
await registerOrUpdateApp({
|
|
101
|
+
appName,
|
|
102
|
+
token,
|
|
103
|
+
nextVersion,
|
|
104
|
+
entry,
|
|
105
|
+
gitRemote,
|
|
106
|
+
integrations,
|
|
107
|
+
description,
|
|
108
|
+
masterAppName,
|
|
109
|
+
});
|
|
101
110
|
}
|
|
102
111
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
112
|
+
// A preview inherits from the live app it previews, resolved by name server-side —
|
|
113
|
+
// fail fast with an actionable message if that app was never claimed/deployed (e.g. a
|
|
114
|
+
// local checkout scaffolded before `create` started claiming names), rather than
|
|
115
|
+
// letting a deep backend 404 surface as a generic "App registration failed".
|
|
116
|
+
if (!(await fetchApp(appName))) {
|
|
117
|
+
throw new DeployError('register_error', `Cannot preview "${appName}": it has never been created or deployed. Run 'z2h-cli create ${consumerName}' (or deploy it once) first.`);
|
|
118
|
+
}
|
|
103
119
|
await ensureDir(paths.shadowDir);
|
|
104
120
|
const { previewId } = await resolvePortAndMetadata(paths, cleanAppName(consumerName));
|
|
105
121
|
const previewSlug = buildPreviewSlug(consumerName, previewId);
|
|
@@ -131,6 +147,7 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
|
131
147
|
entry,
|
|
132
148
|
gitRemote: '',
|
|
133
149
|
isPreview: true,
|
|
150
|
+
masterAppName: appName,
|
|
134
151
|
});
|
|
135
152
|
const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
|
|
136
153
|
output(`[z2h-cli] preview ready ✅\n app: ${appName}\n previewAppName: ${previewAppName}\n open: ${previewUrl}`, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/commands/generate.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAwHD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqF3F"}
|
|
@@ -5,10 +5,10 @@ import { execa } from 'execa';
|
|
|
5
5
|
import { pathExists, readdir } from 'fs-extra';
|
|
6
6
|
import { assertWorkspace } from '../util/workspace.mjs';
|
|
7
7
|
import { validateAppName, mfAppName, cleanAppName } from '../util/app-name.mjs';
|
|
8
|
-
import { fetchApp } from '../util/broker/app.mjs';
|
|
8
|
+
import { fetchApp, claimApp } from '../util/broker/app.mjs';
|
|
9
9
|
import { patchBuiltBundle, patchOneFile } from '../util/dashboard-templates/patch-bundle.mjs';
|
|
10
10
|
import { findTemplate } from '../util/dashboard-templates/load-catalog.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { defaultS3Remote, initRepo } from '../util/git/repo.mjs';
|
|
12
12
|
import { info, output } from '../util/logger.mjs';
|
|
13
13
|
|
|
14
14
|
// Must match zth-backend-runner's route pattern — fail at generate time, not at first invocation.
|
|
@@ -133,6 +133,12 @@ async function generateCommand(appName, opts) {
|
|
|
133
133
|
if (existing?.deployed) {
|
|
134
134
|
throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
|
|
135
135
|
}
|
|
136
|
+
// generate skips `create` entirely, but the preview it deploys below still resolves this
|
|
137
|
+
// app's own row by name server-side — claim it now so that lookup finds something, same as
|
|
138
|
+
// `create` does for the normal flow.
|
|
139
|
+
if (!existing) {
|
|
140
|
+
await claimApp(mfAppName(appName), defaultS3Remote(appName));
|
|
141
|
+
}
|
|
136
142
|
info(`copying template "${template.id}" → ${appDir}`);
|
|
137
143
|
// Excludes node_modules (installed into templateDir itself by build-templates.mjs, never
|
|
138
144
|
// needed by the generated app), .build-shadow (a scratch dir that build-templates.mjs
|
|
@@ -30,6 +30,8 @@ export interface RegisterOrUpdateOptions {
|
|
|
30
30
|
gitRemote: string;
|
|
31
31
|
integrations?: string[];
|
|
32
32
|
description?: string;
|
|
33
|
+
/** For a preview app's first registration, the real app it previews. Ignored past v1. */
|
|
34
|
+
masterAppName?: string;
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
37
|
* v1 creates the app row (+ owner grant, prompting for visibility); later
|
|
@@ -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;AA6CD,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,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BtF"}
|
|
@@ -47,11 +47,11 @@ 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(appName, token, version, entry, visibility, gitRemote, integrations, description) {
|
|
50
|
+
async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description, masterAppName) {
|
|
51
51
|
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
|
|
52
52
|
method: 'POST',
|
|
53
53
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
54
|
-
body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description }),
|
|
54
|
+
body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description, masterAppName }),
|
|
55
55
|
});
|
|
56
56
|
if (!res.ok) {
|
|
57
57
|
const text = await res.text();
|
|
@@ -77,12 +77,12 @@ async function updateApp(appName, token, version, entry, gitRemote, integrations
|
|
|
77
77
|
* deploy and preview so this branching logic lives in exactly one place.
|
|
78
78
|
*/
|
|
79
79
|
async function registerOrUpdateApp(opts) {
|
|
80
|
-
const { appName, token, nextVersion, entry, gitRemote, integrations, description } = opts;
|
|
80
|
+
const { appName, token, nextVersion, entry, gitRemote, integrations, description, masterAppName } = opts;
|
|
81
81
|
try {
|
|
82
82
|
if (nextVersion === 1) {
|
|
83
83
|
const visibility = await resolveAppVisibility(appName);
|
|
84
84
|
info(`registering "${appName}" → v${nextVersion} (${visibility})`);
|
|
85
|
-
await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description);
|
|
85
|
+
await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description, masterAppName);
|
|
86
86
|
}
|
|
87
87
|
else {
|
|
88
88
|
info(`updating "${appName}" → v${nextVersion}`);
|
|
@@ -30,6 +30,8 @@ export interface RegisterOrUpdateOptions {
|
|
|
30
30
|
gitRemote: string;
|
|
31
31
|
integrations?: string[];
|
|
32
32
|
description?: string;
|
|
33
|
+
/** For a preview app's first registration, the real app it previews. Ignored past v1. */
|
|
34
|
+
masterAppName?: string;
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
37
|
* v1 creates the app row (+ owner grant, prompting for visibility); later
|
|
@@ -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;AA6CD,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,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BtF"}
|
package/dist/util/broker/app.js
CHANGED
|
@@ -49,11 +49,11 @@ 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(appName, token, version, entry, visibility, gitRemote, integrations, description) {
|
|
52
|
+
async function registerApp(appName, token, version, entry, visibility, gitRemote, integrations, description, masterAppName) {
|
|
53
53
|
const res = await fetch(`${constants.brokerBaseUrl()}/z2h-cli/apps/register`, {
|
|
54
54
|
method: 'POST',
|
|
55
55
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...constants.routingHeaders() },
|
|
56
|
-
body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description }),
|
|
56
|
+
body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description, masterAppName }),
|
|
57
57
|
});
|
|
58
58
|
if (!res.ok) {
|
|
59
59
|
const text = await res.text();
|
|
@@ -79,12 +79,12 @@ async function updateApp(appName, token, version, entry, gitRemote, integrations
|
|
|
79
79
|
* deploy and preview so this branching logic lives in exactly one place.
|
|
80
80
|
*/
|
|
81
81
|
async function registerOrUpdateApp(opts) {
|
|
82
|
-
const { appName, token, nextVersion, entry, gitRemote, integrations, description } = opts;
|
|
82
|
+
const { appName, token, nextVersion, entry, gitRemote, integrations, description, masterAppName } = opts;
|
|
83
83
|
try {
|
|
84
84
|
if (nextVersion === 1) {
|
|
85
85
|
const visibility = await util_promptVisibility.resolveAppVisibility(appName);
|
|
86
86
|
util_logger.info(`registering "${appName}" → v${nextVersion} (${visibility})`);
|
|
87
|
-
await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description);
|
|
87
|
+
await registerApp(appName, token, nextVersion, entry, visibility, gitRemote, integrations, description, masterAppName);
|
|
88
88
|
}
|
|
89
89
|
else {
|
|
90
90
|
util_logger.info(`updating "${appName}" → v${nextVersion}`);
|
|
@@ -58,35 +58,32 @@ function hashQueryShape(rawSql: string): string {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* The permissions-aware pool returns different rows to different people
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
61
|
+
* The permissions-aware pool returns different rows to different people —
|
|
62
|
+
* including different rows to two people with the same roles/user_group, since
|
|
63
|
+
* Snowflake now enforces column/row-level grants per user. The cache key must
|
|
64
|
+
* therefore scope to the individual caller, exactly as bigbrain-zth keys it.
|
|
65
|
+
* The stateless pool reads as a fixed service account, so every caller shares
|
|
66
|
+
* one entry there.
|
|
66
67
|
*/
|
|
67
68
|
/**
|
|
68
69
|
* Returns the cache scope key for the current caller, or null when we must not
|
|
69
70
|
* cache at all.
|
|
70
71
|
*
|
|
71
|
-
* On the permissions-aware path we
|
|
72
|
+
* On the permissions-aware path we scope to the caller's user id, mirroring
|
|
72
73
|
* bigbrain-zth exactly. If there is no execution context (e.g. the call arrived
|
|
73
74
|
* outside a normal invoke flow) we fail **closed**: skipping both the cache read
|
|
74
75
|
* and the write prevents an unauthenticated scope-collapse where every caller
|
|
75
|
-
* would share one entry
|
|
76
|
+
* would share one entry.
|
|
76
77
|
*/
|
|
77
78
|
function cacheScope(useStateless: boolean): string | null {
|
|
78
79
|
if (useStateless) {
|
|
79
80
|
return 'stateless';
|
|
80
81
|
}
|
|
81
|
-
const user = context.execution.get()?.user as {
|
|
82
|
-
if (!user) {
|
|
82
|
+
const user = context.execution.get()?.user as { id?: number } | undefined;
|
|
83
|
+
if (!user?.id) {
|
|
83
84
|
return null;
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
-
return createHash('sha256')
|
|
87
|
-
.update(`${roles}|${user.user_group ?? ''}`)
|
|
88
|
-
.digest('hex')
|
|
89
|
-
.slice(0, 16);
|
|
86
|
+
return `user:${user.id}`;
|
|
90
87
|
}
|
|
91
88
|
|
|
92
89
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mondaydotcomorg/z2h-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"bin": "./bin/z2h-cli.js",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
],
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@aws-sdk/client-s3": "^3.706.0",
|
|
54
|
-
"@mondaydotcomorg/z2h-dashboard-templates": "^0.3.
|
|
54
|
+
"@mondaydotcomorg/z2h-dashboard-templates": "^0.3.6",
|
|
55
55
|
"@mondaydotcomorg/z2h-shared-utils": "^0.6.2",
|
|
56
56
|
"acorn": "^8.14.0",
|
|
57
57
|
"acorn-walk": "^8.3.4",
|
|
@@ -39,8 +39,21 @@ vi.mock('../../util/auth/broker-credential-provider', () => ({
|
|
|
39
39
|
fetchDeployVersion: vi.fn(),
|
|
40
40
|
}));
|
|
41
41
|
|
|
42
|
+
const { STUB_MASTER_APP } = vi.hoisted(() => ({
|
|
43
|
+
STUB_MASTER_APP: {
|
|
44
|
+
appName: 'mf-my-app',
|
|
45
|
+
version: 1,
|
|
46
|
+
entry: {},
|
|
47
|
+
deployed: true,
|
|
48
|
+
deployedBy: 'someone@x.com',
|
|
49
|
+
gitRemote: 's3://bucket/my-app/git',
|
|
50
|
+
createdBy: 'someone@x.com',
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
|
|
42
54
|
vi.mock('../../util/broker/app', () => ({
|
|
43
55
|
registerOrUpdateApp: vi.fn().mockResolvedValue(undefined),
|
|
56
|
+
fetchApp: vi.fn().mockResolvedValue(STUB_MASTER_APP),
|
|
44
57
|
}));
|
|
45
58
|
|
|
46
59
|
vi.mock('../../util/git/repo', () => ({
|
|
@@ -90,6 +103,7 @@ describe('deploy --preview', () => {
|
|
|
90
103
|
vi.mocked(brokerProvider.brokerCredentialProvider).mockClear();
|
|
91
104
|
vi.mocked(brokerProvider.fetchDeployVersion).mockReset().mockResolvedValue(0);
|
|
92
105
|
vi.mocked(brokerApp.registerOrUpdateApp).mockClear().mockResolvedValue(undefined);
|
|
106
|
+
vi.mocked(brokerApp.fetchApp).mockClear().mockResolvedValue(STUB_MASTER_APP);
|
|
93
107
|
vi.mocked(gitRepo.guardCleanTree).mockClear();
|
|
94
108
|
vi.mocked(gitRepo.pullAndResolve).mockClear();
|
|
95
109
|
vi.mocked(gitRepo.pushToRemote).mockClear();
|
|
@@ -148,6 +162,7 @@ describe('deploy --preview', () => {
|
|
|
148
162
|
entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 3, gitRemote: '' }),
|
|
149
163
|
gitRemote: '',
|
|
150
164
|
integrations: [],
|
|
165
|
+
masterAppName: 'mf-my-app',
|
|
151
166
|
});
|
|
152
167
|
});
|
|
153
168
|
|
|
@@ -165,6 +180,7 @@ describe('deploy --preview', () => {
|
|
|
165
180
|
entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 1, gitRemote: '' }),
|
|
166
181
|
gitRemote: '',
|
|
167
182
|
integrations: [],
|
|
183
|
+
masterAppName: 'mf-my-app',
|
|
168
184
|
});
|
|
169
185
|
});
|
|
170
186
|
|
|
@@ -220,6 +236,18 @@ describe('deploy --preview', () => {
|
|
|
220
236
|
expect((err as DeployError).reason).toBe('register_error');
|
|
221
237
|
});
|
|
222
238
|
|
|
239
|
+
it('fails fast with an actionable message when the live app was never created/deployed', async () => {
|
|
240
|
+
vi.mocked(brokerApp.fetchApp).mockResolvedValue(null);
|
|
241
|
+
|
|
242
|
+
const err = await deployCommand({ preview: true }).catch((e: unknown) => e);
|
|
243
|
+
|
|
244
|
+
expect(err).toBeInstanceOf(DeployError);
|
|
245
|
+
expect((err as DeployError).reason).toBe('register_error');
|
|
246
|
+
expect((err as Error).message).toMatch(/never been created or deployed/);
|
|
247
|
+
expect(buildCommand).not.toHaveBeenCalled();
|
|
248
|
+
expect(brokerApp.registerOrUpdateApp).not.toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
|
|
223
251
|
it('rejects deploying a preview for an app already named with the preview prefix', async () => {
|
|
224
252
|
await writeJson(path.join(tmp, 'package.json'), { name: 'zth-preview-abc123-my-app' });
|
|
225
253
|
await expect(deployCommand({ preview: true })).rejects.toThrow(/reserved for the ephemeral preview mechanism/);
|
package/src/commands/deploy.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { resolveGitEnv } from '../util/git/git-env';
|
|
|
20
20
|
import { type RemoteType, validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options';
|
|
21
21
|
import { setupS3Client } from '../util/s3/client';
|
|
22
22
|
import { getMachineAppName } from '../util/machine-identity';
|
|
23
|
-
import { registerOrUpdateApp } from '../util/broker/app';
|
|
23
|
+
import { fetchApp, registerOrUpdateApp } from '../util/broker/app';
|
|
24
24
|
import { openInBrowser } from '../util/open-browser';
|
|
25
25
|
import { resolvePortAndMetadata } from '../shadow/prepare';
|
|
26
26
|
import { DeployError, tagDeployError, grantReasonForStatus } from '../util/errors';
|
|
@@ -132,8 +132,10 @@ async function uploadAndRegister(opts: {
|
|
|
132
132
|
gitRemote: string;
|
|
133
133
|
isPreview: boolean;
|
|
134
134
|
description?: string;
|
|
135
|
+
masterAppName?: string;
|
|
135
136
|
}): Promise<void> {
|
|
136
|
-
const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description } =
|
|
137
|
+
const { paths, keyPrefix, appName, token, nextVersion, entry, gitRemote, isPreview, description, masterAppName } =
|
|
138
|
+
opts;
|
|
137
139
|
|
|
138
140
|
info(`uploading ${isPreview ? 'preview ' : ''}build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
139
141
|
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
@@ -150,10 +152,30 @@ async function uploadAndRegister(opts: {
|
|
|
150
152
|
isPreview,
|
|
151
153
|
});
|
|
152
154
|
|
|
153
|
-
await registerOrUpdateApp({
|
|
155
|
+
await registerOrUpdateApp({
|
|
156
|
+
appName,
|
|
157
|
+
token,
|
|
158
|
+
nextVersion,
|
|
159
|
+
entry,
|
|
160
|
+
gitRemote,
|
|
161
|
+
integrations,
|
|
162
|
+
description,
|
|
163
|
+
masterAppName,
|
|
164
|
+
});
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
async function deployPreview({ paths, consumerName, appName, token, opts }: DeployContext): Promise<void> {
|
|
168
|
+
// A preview inherits from the live app it previews, resolved by name server-side —
|
|
169
|
+
// fail fast with an actionable message if that app was never claimed/deployed (e.g. a
|
|
170
|
+
// local checkout scaffolded before `create` started claiming names), rather than
|
|
171
|
+
// letting a deep backend 404 surface as a generic "App registration failed".
|
|
172
|
+
if (!(await fetchApp(appName))) {
|
|
173
|
+
throw new DeployError(
|
|
174
|
+
'register_error',
|
|
175
|
+
`Cannot preview "${appName}": it has never been created or deployed. Run 'z2h-cli create ${consumerName}' (or deploy it once) first.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
157
179
|
await ensureDir(paths.shadowDir);
|
|
158
180
|
const { previewId } = await resolvePortAndMetadata(paths, cleanAppName(consumerName));
|
|
159
181
|
|
|
@@ -190,6 +212,7 @@ async function deployPreview({ paths, consumerName, appName, token, opts }: Depl
|
|
|
190
212
|
entry,
|
|
191
213
|
gitRemote: '',
|
|
192
214
|
isPreview: true,
|
|
215
|
+
masterAppName: appName,
|
|
193
216
|
});
|
|
194
217
|
|
|
195
218
|
const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
|
package/src/commands/generate.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { execa } from 'execa';
|
|
|
5
5
|
import { pathExists, readdir } from 'fs-extra';
|
|
6
6
|
import { assertWorkspace } from '../util/workspace';
|
|
7
7
|
import { validateAppName, cleanAppName, mfAppName } from '../util/app-name';
|
|
8
|
-
import { fetchApp } from '../util/broker/app';
|
|
8
|
+
import { claimApp, fetchApp } from '../util/broker/app';
|
|
9
9
|
import { patchBuiltBundle, patchOneFile, type PatchedConfig } from '../util/dashboard-templates/patch-bundle';
|
|
10
10
|
import { findTemplate, type DashboardTemplateEntry } from '../util/dashboard-templates/load-catalog';
|
|
11
11
|
import { initRepo, defaultS3Remote } from '../util/git/repo';
|
|
@@ -163,6 +163,13 @@ export async function generateCommand(appName: string, opts: GenerateOptions): P
|
|
|
163
163
|
throw new Error(`An app named "${appName}" already exists in the Z2H registry. Pick a different name.`);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// generate skips `create` entirely, but the preview it deploys below still resolves this
|
|
167
|
+
// app's own row by name server-side — claim it now so that lookup finds something, same as
|
|
168
|
+
// `create` does for the normal flow.
|
|
169
|
+
if (!existing) {
|
|
170
|
+
await claimApp(mfAppName(appName), defaultS3Remote(appName));
|
|
171
|
+
}
|
|
172
|
+
|
|
166
173
|
info(`copying template "${template.id}" → ${appDir}`);
|
|
167
174
|
// Excludes node_modules (installed into templateDir itself by build-templates.mjs, never
|
|
168
175
|
// needed by the generated app), .build-shadow (a scratch dir that build-templates.mjs
|
package/src/util/broker/app.ts
CHANGED
|
@@ -72,12 +72,13 @@ async function registerApp(
|
|
|
72
72
|
visibility: 'public' | 'private',
|
|
73
73
|
gitRemote: string,
|
|
74
74
|
integrations?: string[],
|
|
75
|
-
description?: string
|
|
75
|
+
description?: string,
|
|
76
|
+
masterAppName?: string
|
|
76
77
|
): Promise<void> {
|
|
77
78
|
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
|
|
78
79
|
method: 'POST',
|
|
79
80
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
80
|
-
body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description }),
|
|
81
|
+
body: JSON.stringify({ appName, version, entry, visibility, gitRemote, integrations, description, masterAppName }),
|
|
81
82
|
});
|
|
82
83
|
if (!res.ok) {
|
|
83
84
|
const text = await res.text();
|
|
@@ -114,6 +115,8 @@ export interface RegisterOrUpdateOptions {
|
|
|
114
115
|
gitRemote: string;
|
|
115
116
|
integrations?: string[];
|
|
116
117
|
description?: string;
|
|
118
|
+
/** For a preview app's first registration, the real app it previews. Ignored past v1. */
|
|
119
|
+
masterAppName?: string;
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
/**
|
|
@@ -123,13 +126,23 @@ export interface RegisterOrUpdateOptions {
|
|
|
123
126
|
* deploy and preview so this branching logic lives in exactly one place.
|
|
124
127
|
*/
|
|
125
128
|
export async function registerOrUpdateApp(opts: RegisterOrUpdateOptions): Promise<void> {
|
|
126
|
-
const { appName, token, nextVersion, entry, gitRemote, integrations, description } = opts;
|
|
129
|
+
const { appName, token, nextVersion, entry, gitRemote, integrations, description, masterAppName } = opts;
|
|
127
130
|
|
|
128
131
|
try {
|
|
129
132
|
if (nextVersion === 1) {
|
|
130
133
|
const visibility = await resolveAppVisibility(appName);
|
|
131
134
|
info(`registering "${appName}" → v${nextVersion} (${visibility})`);
|
|
132
|
-
await registerApp(
|
|
135
|
+
await registerApp(
|
|
136
|
+
appName,
|
|
137
|
+
token,
|
|
138
|
+
nextVersion,
|
|
139
|
+
entry,
|
|
140
|
+
visibility,
|
|
141
|
+
gitRemote,
|
|
142
|
+
integrations,
|
|
143
|
+
description,
|
|
144
|
+
masterAppName
|
|
145
|
+
);
|
|
133
146
|
} else {
|
|
134
147
|
info(`updating "${appName}" → v${nextVersion}`);
|
|
135
148
|
await updateApp(appName, token, nextVersion, entry, gitRemote, integrations, description);
|