@mondaydotcomorg/z2h-cli 0.34.0 → 0.34.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/CHANGELOG.md +2969 -0
- package/dist/backend/__tests__/ast.test.d.ts +2 -0
- package/dist/backend/__tests__/ast.test.d.ts.map +1 -0
- package/dist/backend/__tests__/parse.test.d.ts +2 -0
- package/dist/backend/__tests__/parse.test.d.ts.map +1 -0
- package/dist/backend/ast.d.ts +41 -0
- package/dist/backend/ast.d.ts.map +1 -0
- package/dist/backend/ast.js +152 -0
- package/dist/backend/handler-declaration.d.ts +17 -0
- package/dist/backend/handler-declaration.d.ts.map +1 -0
- package/dist/backend/handler-declaration.js +83 -0
- package/dist/backend/messages.d.ts +10 -0
- package/dist/backend/messages.d.ts.map +1 -0
- package/dist/backend/messages.js +14 -0
- package/dist/backend/parse.d.ts +11 -0
- package/dist/backend/parse.d.ts.map +1 -0
- package/dist/backend/parse.js +66 -0
- package/dist/backend/rules/__tests__/snowflake-static-sql.test.d.ts +2 -0
- package/dist/backend/rules/__tests__/snowflake-static-sql.test.d.ts.map +1 -0
- package/dist/backend/rules/index.d.ts +5 -0
- package/dist/backend/rules/index.d.ts.map +1 -0
- package/dist/backend/rules/index.js +8 -0
- package/dist/backend/rules/snowflake-static-sql.d.ts +15 -0
- package/dist/backend/rules/snowflake-static-sql.d.ts.map +1 -0
- package/dist/backend/rules/snowflake-static-sql.js +75 -0
- package/dist/backend/rules/types.d.ts +29 -0
- package/dist/backend/rules/types.d.ts.map +1 -0
- package/dist/backend/rules/types.js +1 -0
- package/dist/commands/__tests__/delete.test.d.ts +2 -0
- package/dist/commands/__tests__/delete.test.d.ts.map +1 -0
- package/dist/commands/backend.d.ts +0 -1
- package/dist/commands/backend.d.ts.map +1 -1
- package/dist/commands/backend.js +6 -6
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +16 -13
- package/dist/constants.d.ts +2 -13
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +60 -45
- package/dist/esm/backend/__tests__/ast.test.d.ts +2 -0
- package/dist/esm/backend/__tests__/ast.test.d.ts.map +1 -0
- package/dist/esm/backend/__tests__/parse.test.d.ts +2 -0
- package/dist/esm/backend/__tests__/parse.test.d.ts.map +1 -0
- package/dist/esm/backend/ast.d.ts +41 -0
- package/dist/esm/backend/ast.d.ts.map +1 -0
- package/dist/esm/backend/ast.mjs +146 -0
- package/dist/esm/backend/handler-declaration.d.ts +17 -0
- package/dist/esm/backend/handler-declaration.d.ts.map +1 -0
- package/dist/esm/backend/handler-declaration.mjs +78 -0
- package/dist/esm/backend/messages.d.ts +10 -0
- package/dist/esm/backend/messages.d.ts.map +1 -0
- package/dist/esm/backend/messages.mjs +12 -0
- package/dist/esm/backend/parse.d.ts +11 -0
- package/dist/esm/backend/parse.d.ts.map +1 -0
- package/dist/esm/backend/parse.mjs +64 -0
- package/dist/esm/backend/rules/__tests__/snowflake-static-sql.test.d.ts +2 -0
- package/dist/esm/backend/rules/__tests__/snowflake-static-sql.test.d.ts.map +1 -0
- package/dist/esm/backend/rules/index.d.ts +5 -0
- package/dist/esm/backend/rules/index.d.ts.map +1 -0
- package/dist/esm/backend/rules/index.mjs +6 -0
- package/dist/esm/backend/rules/snowflake-static-sql.d.ts +15 -0
- package/dist/esm/backend/rules/snowflake-static-sql.d.ts.map +1 -0
- package/dist/esm/backend/rules/snowflake-static-sql.mjs +73 -0
- package/dist/esm/backend/rules/types.d.ts +29 -0
- package/dist/esm/backend/rules/types.d.ts.map +1 -0
- package/dist/esm/backend/rules/types.mjs +1 -0
- package/dist/esm/commands/__tests__/delete.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/delete.test.d.ts.map +1 -0
- package/dist/esm/commands/backend.d.ts +0 -1
- package/dist/esm/commands/backend.d.ts.map +1 -1
- package/dist/esm/commands/backend.mjs +5 -5
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +9 -6
- package/dist/esm/constants.d.ts +2 -13
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.mjs +5 -35
- package/dist/esm/util/__tests__/usage-upload.test.d.ts +2 -0
- package/dist/esm/util/__tests__/usage-upload.test.d.ts.map +1 -0
- package/dist/esm/util/app-name.mjs +1 -1
- package/dist/esm/util/auth/__tests__/index.test.d.ts +2 -0
- package/dist/esm/util/auth/__tests__/index.test.d.ts.map +1 -0
- package/dist/esm/util/auth/broker-credential-provider.d.ts +6 -1
- package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -1
- package/dist/esm/util/auth/broker-credential-provider.mjs +2 -2
- package/dist/esm/util/auth/index.d.ts +3 -0
- package/dist/esm/util/auth/index.d.ts.map +1 -0
- package/dist/esm/util/auth/index.mjs +2 -0
- package/dist/esm/util/broker/app.d.ts +0 -1
- package/dist/esm/util/broker/app.d.ts.map +1 -1
- package/dist/esm/util/broker/app.mjs +3 -4
- package/dist/esm/util/git/repo.mjs +1 -1
- package/dist/esm/util/s3/client.mjs +1 -1
- package/dist/esm/util/s3/upload.mjs +1 -1
- package/dist/esm/util/usage-json.d.ts +2 -2
- package/dist/esm/util/usage-json.d.ts.map +1 -1
- package/dist/esm/util/usage-json.mjs +5 -2
- package/dist/esm/util/usage-upload.d.ts +3 -0
- package/dist/esm/util/usage-upload.d.ts.map +1 -0
- package/dist/esm/util/usage-upload.mjs +29 -0
- package/dist/util/__tests__/usage-upload.test.d.ts +2 -0
- package/dist/util/__tests__/usage-upload.test.d.ts.map +1 -0
- package/dist/util/app-name.js +6 -6
- package/dist/util/auth/__tests__/index.test.d.ts +2 -0
- package/dist/util/auth/__tests__/index.test.d.ts.map +1 -0
- package/dist/util/auth/broker-credential-provider.d.ts +6 -1
- package/dist/util/auth/broker-credential-provider.d.ts.map +1 -1
- package/dist/util/auth/broker-credential-provider.js +2 -2
- package/dist/util/auth/index.d.ts +3 -0
- package/dist/util/auth/index.d.ts.map +1 -0
- package/dist/util/auth/index.js +11 -0
- package/dist/util/broker/app.d.ts +0 -1
- package/dist/util/broker/app.d.ts.map +1 -1
- package/dist/util/broker/app.js +3 -4
- package/dist/util/git/repo.js +2 -2
- package/dist/util/s3/client.js +2 -2
- package/dist/util/s3/upload.js +2 -2
- package/dist/util/usage-json.d.ts +2 -2
- package/dist/util/usage-json.d.ts.map +1 -1
- package/dist/util/usage-json.js +5 -2
- package/dist/util/usage-upload.d.ts +3 -0
- package/dist/util/usage-upload.d.ts.map +1 -0
- package/dist/util/usage-upload.js +31 -0
- package/package.json +3 -3
- package/src/commands/__tests__/backend-deploy.test.ts +2 -4
- package/src/commands/__tests__/deploy.test.ts +43 -11
- package/src/commands/backend.ts +4 -5
- package/src/commands/deploy.ts +9 -5
- package/src/constants.ts +25 -47
- package/src/util/__tests__/usage-upload.test.ts +58 -0
- package/src/util/auth/__tests__/broker-credential-provider.test.ts +4 -4
- package/src/util/auth/broker-credential-provider.ts +4 -4
- package/src/util/broker/app.ts +2 -15
- package/src/util/usage-json.ts +11 -4
- package/src/util/usage-upload.ts +38 -0
|
@@ -18,6 +18,9 @@ import * as brokerApp from '../../util/broker/app';
|
|
|
18
18
|
import * as authStore from '../../util/auth/auth-store';
|
|
19
19
|
import * as gitRepo from '../../util/git/repo';
|
|
20
20
|
import { updateOrCreateUsageJson } from '../../util/usage-json';
|
|
21
|
+
import { putAppUsage } from '../../util/usage-upload';
|
|
22
|
+
|
|
23
|
+
vi.mock('../../util/usage-upload', () => ({ putAppUsage: vi.fn() }));
|
|
21
24
|
|
|
22
25
|
vi.mock('../build', () => ({
|
|
23
26
|
buildCommand: vi.fn(),
|
|
@@ -26,7 +29,7 @@ vi.mock('../build', () => ({
|
|
|
26
29
|
// Wraps the real `deployBackend` so most tests exercise it unchanged (no
|
|
27
30
|
// backend/handlers dir in the tmp fixture, so it resolves trivially), while
|
|
28
31
|
// letting individual tests override it to prove the upload/register ordering
|
|
29
|
-
// and passthrough of real handlerNames
|
|
32
|
+
// and passthrough of real handlerNames.
|
|
30
33
|
vi.mock('../backend', async importOriginal => {
|
|
31
34
|
const actual = await importOriginal<typeof import('../backend')>();
|
|
32
35
|
return { ...actual, deployBackend: vi.fn(actual.deployBackend) };
|
|
@@ -119,7 +122,7 @@ describe('deploy --preview', () => {
|
|
|
119
122
|
vi.mocked(setupS3Client).mockClear();
|
|
120
123
|
vi.mocked(openInBrowser).mockClear();
|
|
121
124
|
vi.mocked(brokerProvider.brokerCredentialProvider).mockClear();
|
|
122
|
-
vi.mocked(brokerProvider.fetchDeployVersion).mockReset().mockResolvedValue(0);
|
|
125
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockReset().mockResolvedValue({ version: 0, id: 7 });
|
|
123
126
|
vi.mocked(brokerApp.registerOrUpdateApp).mockClear().mockResolvedValue(undefined);
|
|
124
127
|
vi.mocked(brokerApp.fetchApp).mockClear().mockResolvedValue(STUB_MASTER_APP);
|
|
125
128
|
vi.mocked(gitRepo.guardCleanTree).mockClear();
|
|
@@ -163,7 +166,7 @@ describe('deploy --preview', () => {
|
|
|
163
166
|
|
|
164
167
|
it('reuses the sticky previewId from metadata', async () => {
|
|
165
168
|
await writeMetadata(STUB_PREVIEW_ID);
|
|
166
|
-
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue(2);
|
|
169
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue({ version: 2, id: 7 });
|
|
167
170
|
|
|
168
171
|
await deployCommand({ preview: true });
|
|
169
172
|
|
|
@@ -181,14 +184,13 @@ describe('deploy --preview', () => {
|
|
|
181
184
|
gitRemote: '',
|
|
182
185
|
integrations: [],
|
|
183
186
|
handlerNames: [],
|
|
184
|
-
apiUsages: {},
|
|
185
187
|
masterAppName: 'mf-my-app',
|
|
186
188
|
});
|
|
187
189
|
});
|
|
188
190
|
|
|
189
191
|
it('calls registerOrUpdateApp for a brand-new preview at version 1 with an empty gitRemote', async () => {
|
|
190
192
|
await writeMetadata(STUB_PREVIEW_ID);
|
|
191
|
-
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue(0);
|
|
193
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue({ version: 0, id: 7 });
|
|
192
194
|
|
|
193
195
|
await deployCommand({ preview: true });
|
|
194
196
|
|
|
@@ -201,7 +203,6 @@ describe('deploy --preview', () => {
|
|
|
201
203
|
gitRemote: '',
|
|
202
204
|
integrations: [],
|
|
203
205
|
handlerNames: [],
|
|
204
|
-
apiUsages: {},
|
|
205
206
|
masterAppName: 'mf-my-app',
|
|
206
207
|
});
|
|
207
208
|
});
|
|
@@ -225,6 +226,7 @@ describe('deploy --preview', () => {
|
|
|
225
226
|
expect(gitRepo.pullAndResolve).not.toHaveBeenCalled();
|
|
226
227
|
expect(gitRepo.pushToRemote).not.toHaveBeenCalled();
|
|
227
228
|
expect(updateOrCreateUsageJson).not.toHaveBeenCalled();
|
|
229
|
+
expect(putAppUsage).not.toHaveBeenCalled();
|
|
228
230
|
expect(gitRepo.initRepo).not.toHaveBeenCalled();
|
|
229
231
|
});
|
|
230
232
|
|
|
@@ -317,11 +319,10 @@ describe('deploy --preview', () => {
|
|
|
317
319
|
warnSpy.mockRestore();
|
|
318
320
|
});
|
|
319
321
|
|
|
320
|
-
it('forwards the real handlerNames
|
|
322
|
+
it('forwards the real handlerNames deployBackend reports to registerOrUpdateApp', async () => {
|
|
321
323
|
vi.mocked(backendCmd.deployBackend).mockResolvedValueOnce({
|
|
322
324
|
integrations: ['snowflake'],
|
|
323
325
|
handlerNames: ['call_snowflake'],
|
|
324
|
-
apiUsages: { call_snowflake: { snowflake: { query: [{ sql: 'x' }] } } },
|
|
325
326
|
});
|
|
326
327
|
|
|
327
328
|
await deployCommand({ preview: true });
|
|
@@ -330,7 +331,6 @@ describe('deploy --preview', () => {
|
|
|
330
331
|
expect.objectContaining({
|
|
331
332
|
integrations: ['snowflake'],
|
|
332
333
|
handlerNames: ['call_snowflake'],
|
|
333
|
-
apiUsages: { call_snowflake: { snowflake: { query: [{ sql: 'x' }] } } },
|
|
334
334
|
})
|
|
335
335
|
);
|
|
336
336
|
});
|
|
@@ -349,7 +349,7 @@ describe('deploy failure reasons', () => {
|
|
|
349
349
|
await ensureDir(path.join(tmp, '.zth', 'build'));
|
|
350
350
|
|
|
351
351
|
vi.spyOn(authStore, 'readZthAuth').mockResolvedValue({ token: 'jwt-stub' });
|
|
352
|
-
vi.mocked(brokerProvider.fetchDeployVersion).mockReset().mockResolvedValue(0);
|
|
352
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockReset().mockResolvedValue({ version: 0, id: 7 });
|
|
353
353
|
vi.mocked(gitRepo.getRemoteUrl).mockReset().mockResolvedValue('https://github.com/org/repo.git');
|
|
354
354
|
vi.mocked(gitRepo.guardCleanTree).mockReset().mockResolvedValue(undefined);
|
|
355
355
|
vi.mocked(gitRepo.pullAndResolve).mockReset().mockResolvedValue(undefined);
|
|
@@ -414,7 +414,7 @@ describe('deploy failure reasons', () => {
|
|
|
414
414
|
|
|
415
415
|
it('updates usage.json before pushing on a live deploy', async () => {
|
|
416
416
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, status: 200, text: async () => '' }));
|
|
417
|
-
vi.mocked(updateOrCreateUsageJson).mockReset().mockResolvedValue(
|
|
417
|
+
vi.mocked(updateOrCreateUsageJson).mockReset().mockResolvedValue({});
|
|
418
418
|
vi.mocked(gitRepo.pushToRemote).mockReset().mockRejectedValue(new Error('stop after push'));
|
|
419
419
|
await deployCommand().catch(() => {});
|
|
420
420
|
const [usageOrder] = vi.mocked(updateOrCreateUsageJson).mock.invocationCallOrder;
|
|
@@ -422,6 +422,38 @@ describe('deploy failure reasons', () => {
|
|
|
422
422
|
expect(usageOrder).toBeLessThan(pushOrder);
|
|
423
423
|
});
|
|
424
424
|
|
|
425
|
+
describe('usage upload', () => {
|
|
426
|
+
beforeEach(() => {
|
|
427
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, status: 200, text: async () => '' }));
|
|
428
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue({ version: 4, id: 7 });
|
|
429
|
+
vi.mocked(updateOrCreateUsageJson).mockReset().mockResolvedValue({ deployedBy: 'me' });
|
|
430
|
+
vi.mocked(gitRepo.pushToRemote).mockReset().mockResolvedValue(undefined);
|
|
431
|
+
vi.mocked(putAppUsage).mockClear();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('uploads under the new deploy version after the release is registered', async () => {
|
|
435
|
+
vi.mocked(brokerApp.registerOrUpdateApp).mockClear();
|
|
436
|
+
await deployCommand();
|
|
437
|
+
expect(putAppUsage).toHaveBeenCalledWith('mf-my-app', 7, { deployedBy: 'me' }, 5);
|
|
438
|
+
expect(vi.mocked(brokerApp.registerOrUpdateApp).mock.invocationCallOrder[0]).toBeLessThan(
|
|
439
|
+
vi.mocked(putAppUsage).mock.invocationCallOrder[0]
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('uploads nothing when registration fails', async () => {
|
|
444
|
+
vi.mocked(brokerApp.registerOrUpdateApp).mockRejectedValueOnce(new Error('broker down'));
|
|
445
|
+
await deployCommand().catch(() => {});
|
|
446
|
+
expect(putAppUsage).not.toHaveBeenCalled();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('uploads nothing on a dry run or when the build fails', async () => {
|
|
450
|
+
await deployCommand({ dryRun: true });
|
|
451
|
+
vi.mocked(buildCommand).mockRejectedValueOnce(new Error('tsc exploded'));
|
|
452
|
+
await deployCommand().catch(() => {});
|
|
453
|
+
expect(putAppUsage).not.toHaveBeenCalled();
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
425
457
|
// buildCommand throws plain errors (see commands/__tests__/build.test.ts), so a build
|
|
426
458
|
// failure surfaces as untagged here — see the propagation test below. The STS 401/403 →
|
|
427
459
|
// auth tagging lives in broker-credential-provider.ts — see its own test file.
|
package/src/commands/backend.ts
CHANGED
|
@@ -26,7 +26,6 @@ function currentAppName(): string {
|
|
|
26
26
|
export interface ReadDetectAndUploadResult {
|
|
27
27
|
integrations: string[];
|
|
28
28
|
handlerNames: string[];
|
|
29
|
-
apiUsages: Record<string, unknown>;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
async function readDetectAndUpload(
|
|
@@ -38,14 +37,14 @@ async function readDetectAndUpload(
|
|
|
38
37
|
logLabel: string
|
|
39
38
|
): Promise<ReadDetectAndUploadResult> {
|
|
40
39
|
if (!hasHandlers(appDir)) {
|
|
41
|
-
return { integrations: [], handlerNames: []
|
|
40
|
+
return { integrations: [], handlerNames: [] };
|
|
42
41
|
}
|
|
43
42
|
info(`reading backend handlers ${logLabel}`);
|
|
44
|
-
const { handlers, apiDomains
|
|
43
|
+
const { handlers, apiDomains } = await readHandlers(appDir);
|
|
45
44
|
const integrations = detectIntegrations(apiDomains);
|
|
46
45
|
const handlerNames = Object.keys(handlers);
|
|
47
46
|
if (Object.keys(handlers).length === 0) {
|
|
48
|
-
return { integrations, handlerNames
|
|
47
|
+
return { integrations, handlerNames };
|
|
49
48
|
}
|
|
50
49
|
const s3 = getS3Client();
|
|
51
50
|
const meta = JSON.stringify({ version, deployedBy, handlers: Object.keys(handlers) });
|
|
@@ -55,7 +54,7 @@ async function readDetectAndUpload(
|
|
|
55
54
|
...Object.entries(handlers).map(([name, code]) => put(`${prefix}/${name}.js`, code, 'application/javascript')),
|
|
56
55
|
put(`${prefix}/meta.json`, meta, 'application/json'),
|
|
57
56
|
]);
|
|
58
|
-
return { integrations, handlerNames
|
|
57
|
+
return { integrations, handlerNames };
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
/**
|
package/src/commands/deploy.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { readZthAuth } from '../util/auth/auth-store';
|
|
|
16
16
|
import { cleanAppName, mfAppName, validateAppName, buildPreviewSlug } from '../util/app-name';
|
|
17
17
|
import { getRemoteUrl, guardCleanTree, pullAndResolve, pushToRemote, initRepo } from '../util/git/repo';
|
|
18
18
|
import { updateOrCreateUsageJson } from '../util/usage-json';
|
|
19
|
+
import { putAppUsage } from '../util/usage-upload';
|
|
19
20
|
import { resolveGitEnv } from '../util/git/git-env';
|
|
20
21
|
import { type RemoteType, validateRemoteOptions, resolveRemoteUrl } from '../util/git/remote-options';
|
|
21
22
|
import { setupS3Client } from '../util/s3/client';
|
|
@@ -134,7 +135,7 @@ async function uploadAndRegister(opts: {
|
|
|
134
135
|
|
|
135
136
|
// Runs before the frontend upload so a `deployBackend` failure can't leave a
|
|
136
137
|
// preview's unversioned `MF` dir overwritten with no rollback.
|
|
137
|
-
const { integrations, handlerNames
|
|
138
|
+
const { integrations, handlerNames } = await deployBackend({
|
|
138
139
|
appDir: process.cwd(),
|
|
139
140
|
appName,
|
|
140
141
|
mfKeyPrefix: keyPrefix,
|
|
@@ -155,7 +156,6 @@ async function uploadAndRegister(opts: {
|
|
|
155
156
|
gitRemote,
|
|
156
157
|
integrations,
|
|
157
158
|
handlerNames,
|
|
158
|
-
apiUsages,
|
|
159
159
|
description,
|
|
160
160
|
masterAppName,
|
|
161
161
|
});
|
|
@@ -197,7 +197,7 @@ async function deployPreview({ paths, consumerName, appName, token, opts }: Depl
|
|
|
197
197
|
|
|
198
198
|
// No catch-all: every stage below tags its own DeployError reason at the throw site,
|
|
199
199
|
// and anything unclassified reports as `unknown` via reasonOf() in runCommand.
|
|
200
|
-
const nextVersion = (await fetchDeployVersion(previewAppName)) + 1;
|
|
200
|
+
const nextVersion = (await fetchDeployVersion(previewAppName)).version + 1;
|
|
201
201
|
|
|
202
202
|
info('building');
|
|
203
203
|
const entry = await buildAndStamp({
|
|
@@ -276,11 +276,13 @@ async function deployLive({ paths, consumerName, appName, token, opts }: DeployC
|
|
|
276
276
|
gitRemote = remote;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
const version = await fetchDeployVersion(appName);
|
|
279
|
+
const { version, id: appId } = await fetchDeployVersion(appName);
|
|
280
280
|
const nextVersion = version + 1;
|
|
281
281
|
|
|
282
282
|
const gitEnv = await resolveGitEnv(appName, gitRemote);
|
|
283
283
|
|
|
284
|
+
let usage: Record<string, unknown> = {};
|
|
285
|
+
|
|
284
286
|
// Sync git first so a divergence (GitConflictError) stops us.
|
|
285
287
|
// The skill commits the working tree (with an LLM-authored message) before
|
|
286
288
|
// invoking deploy, so the tree should be clean here.
|
|
@@ -289,7 +291,7 @@ async function deployLive({ paths, consumerName, appName, token, opts }: DeployC
|
|
|
289
291
|
await guardCleanTree(process.cwd());
|
|
290
292
|
info('syncing with git remote');
|
|
291
293
|
await pullAndResolve(process.cwd(), gitEnv);
|
|
292
|
-
await updateOrCreateUsageJson(process.cwd(), appName, whoAmI());
|
|
294
|
+
usage = await updateOrCreateUsageJson(process.cwd(), appName, whoAmI());
|
|
293
295
|
} catch (err) {
|
|
294
296
|
throw tagDeployError('git_error', err);
|
|
295
297
|
}
|
|
@@ -351,6 +353,8 @@ async function deployLive({ paths, consumerName, appName, token, opts }: DeployC
|
|
|
351
353
|
await releaseLockBestEffort(appName, token);
|
|
352
354
|
}
|
|
353
355
|
|
|
356
|
+
await putAppUsage(appName, appId, usage, nextVersion);
|
|
357
|
+
|
|
354
358
|
// Tags are a post-deploy concern: the S3 upload has already succeeded, so a
|
|
355
359
|
// tag failure never fails the deploy (handled inside).
|
|
356
360
|
await handleDeployTags(appName);
|
package/src/constants.ts
CHANGED
|
@@ -28,53 +28,27 @@ export const MAX_PORT = 19999;
|
|
|
28
28
|
// ─────────── Gitignore entries auto-appended to the consumer's file ────
|
|
29
29
|
export const GITIGNORE_ENTRIES = ['.zth/'];
|
|
30
30
|
|
|
31
|
-
// ───────────
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
export const MANIFEST_KEY = 'mf-assets';
|
|
53
|
-
export const MANIFEST_READ_URL = `${BUCKET_READ_DNS_URL}/${MANIFEST_KEY}`;
|
|
54
|
-
|
|
55
|
-
// ─────────── Skills bucket (read-only) ─────────
|
|
56
|
-
// bigbrain-zth mints read-only STS creds scoped to this bucket (mintDatavizSkillsCredentials);
|
|
57
|
-
// bigbrain-zth-agent syncs from it into its vendor/ at pod startup (syncDatavizVendor).
|
|
58
|
-
// Both import from here so the two sides can't drift.
|
|
59
|
-
export const S3_SKILLS_BUCKET_NAME = 'prod-use1-bigbrain-zth-skills';
|
|
60
|
-
|
|
61
|
-
// Prefix the dataviz design-system skills are published under
|
|
62
|
-
// (by data-cookbook's own CI on merge to master).
|
|
63
|
-
export const DATAVIZ_SKILLS_PREFIX = 'dataviz/';
|
|
64
|
-
|
|
65
|
-
// ─────────── App-name slug — backend & CLI both honor this ─────
|
|
66
|
-
// Lowercase kebab-case, must start with a letter, 2-50 chars total. The
|
|
67
|
-
// backend STS broker rejects anything else with 400; the CLI can validate
|
|
68
|
-
// upfront for friendlier errors.
|
|
69
|
-
export const APP_NAME_PATTERN = /^[a-z][a-z0-9-]{1,49}$/;
|
|
70
|
-
|
|
71
|
-
export const PREVIEW_APP_NAME_PREFIX = 'zth-preview-';
|
|
72
|
-
|
|
73
|
-
// ─────────── Backend channels — re-exported from shared-utils ─────
|
|
74
|
-
// Single source of truth lives in `@mondaydotcomorg/z2h-shared-utils` so the
|
|
75
|
-
// CLI detector and broker stay in lockstep. Kept here for existing
|
|
76
|
-
// `@mondaydotcomorg/z2h-cli/constants` consumers.
|
|
77
|
-
export { ALWAYS_ON_CHANNELS, DECLARED_INTEGRATIONS, type DeclaredIntegration } from '@mondaydotcomorg/z2h-shared-utils';
|
|
31
|
+
// ─────────── Re-exported from shared-utils (source of truth) ─────
|
|
32
|
+
// Kept here so existing CLI imports of ./constants keep working.
|
|
33
|
+
export {
|
|
34
|
+
ALWAYS_ON_CHANNELS,
|
|
35
|
+
APP_USAGE_BUCKET_NAME,
|
|
36
|
+
appUsageFolder,
|
|
37
|
+
APP_NAME_PATTERN,
|
|
38
|
+
BUCKET_READ_DNS_NAME,
|
|
39
|
+
BUCKET_READ_DNS_URL,
|
|
40
|
+
BUCKET_WRITE_NAME,
|
|
41
|
+
BUCKET_WRITE_URL,
|
|
42
|
+
DATAVIZ_SKILLS_PREFIX,
|
|
43
|
+
DECLARED_INTEGRATIONS,
|
|
44
|
+
MANIFEST_KEY,
|
|
45
|
+
MANIFEST_READ_URL,
|
|
46
|
+
PREVIEW_APP_NAME_PREFIX,
|
|
47
|
+
REGION,
|
|
48
|
+
S3_PREVIEWS_FOLDER,
|
|
49
|
+
S3_SKILLS_BUCKET_NAME,
|
|
50
|
+
type DeclaredIntegration,
|
|
51
|
+
} from '@mondaydotcomorg/z2h-shared-utils';
|
|
78
52
|
|
|
79
53
|
// ─────────── Broker origin + npm registry (shared §3 contract) ─────
|
|
80
54
|
// The bigbrain-zth broker fronts both the STS credential route and the npm
|
|
@@ -90,6 +64,10 @@ export function brokerBaseUrl(): string {
|
|
|
90
64
|
return process.env.Z2H_BROKER_BASE_URL || DEFAULT_BROKER_BASE_URL;
|
|
91
65
|
}
|
|
92
66
|
|
|
67
|
+
export function isProdBroker(): boolean {
|
|
68
|
+
return brokerBaseUrl() === DEFAULT_BROKER_BASE_URL;
|
|
69
|
+
}
|
|
70
|
+
|
|
93
71
|
// Returns Baggage header for monday-mirror routing when Z2H_ROUTING_KEY is set.
|
|
94
72
|
// No-op in production (returns {}) — only activates when the env var is set.
|
|
95
73
|
export function routingHeaders(): Record<string, string> {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { warn } from '../logger';
|
|
4
|
+
import { putAppUsage } from '../usage-upload';
|
|
5
|
+
|
|
6
|
+
const { send } = vi.hoisted(() => ({ send: vi.fn() }));
|
|
7
|
+
|
|
8
|
+
vi.mock('../logger', () => ({ warn: vi.fn() }));
|
|
9
|
+
vi.mock('../s3/client', () => ({ getS3Client: () => ({ send }) }));
|
|
10
|
+
|
|
11
|
+
describe('putAppUsage', () => {
|
|
12
|
+
const USAGE = { appName: 'stale', deployedBy: 'me', apiUsages: { a: 1 }, uploadVersion: 1 };
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
send.mockReset().mockResolvedValue({});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.unstubAllEnvs();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('uploads the usage under the given version', async () => {
|
|
23
|
+
await putAppUsage('mf-my-app', 7, USAGE, 4);
|
|
24
|
+
|
|
25
|
+
const { Bucket, Key, Body } = send.mock.calls[0][0].input;
|
|
26
|
+
expect(Bucket).toBe('de-integration-bigbrain');
|
|
27
|
+
expect(Key).toBe('prod/studio/entity=app_usage/mf-my-app_7/4/usage.json');
|
|
28
|
+
expect(JSON.parse(Body)).toEqual({
|
|
29
|
+
appName: 'mf-my-app',
|
|
30
|
+
appId: 7,
|
|
31
|
+
deployedBy: 'me',
|
|
32
|
+
apiUsages: { a: 1 },
|
|
33
|
+
uploadVersion: 4,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('skips with a warning when targeting a non-prod broker', async () => {
|
|
38
|
+
vi.stubEnv('Z2H_BROKER_BASE_URL', 'https://staging.example.com');
|
|
39
|
+
|
|
40
|
+
await putAppUsage('mf-my-app', 7, USAGE, 4);
|
|
41
|
+
|
|
42
|
+
expect(send).not.toHaveBeenCalled();
|
|
43
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('https://staging.example.com'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('skips when the app row id is unknown', async () => {
|
|
47
|
+
await putAppUsage('mf-my-app', null, USAGE, 4);
|
|
48
|
+
|
|
49
|
+
expect(send).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('warns with the redeploy fix instead of throwing when the upload fails', async () => {
|
|
53
|
+
send.mockRejectedValue(new Error('denied'));
|
|
54
|
+
|
|
55
|
+
await expect(putAppUsage('mf-my-app', 7, USAGE, 4)).resolves.toBeUndefined();
|
|
56
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('z2h-cli deploy'));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -40,11 +40,11 @@ function mockFetchOnce(body: unknown, init: { status?: number; text?: string } =
|
|
|
40
40
|
|
|
41
41
|
describe('fetchDeployVersion', () => {
|
|
42
42
|
it('POSTs the appName to the version endpoint and returns the version', async () => {
|
|
43
|
-
const fetchMock = mockFetchOnce({ version: 4 });
|
|
43
|
+
const fetchMock = mockFetchOnce({ version: 4, id: 9 });
|
|
44
44
|
|
|
45
|
-
const
|
|
45
|
+
const result = await fetchDeployVersion('my-app');
|
|
46
46
|
|
|
47
|
-
expect(
|
|
47
|
+
expect(result).toEqual({ version: 4, id: 9 });
|
|
48
48
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
49
49
|
`${DEFAULT_BROKER_BASE_URL}${DEPLOY_VERSION_PATH}`,
|
|
50
50
|
expect.objectContaining({
|
|
@@ -56,7 +56,7 @@ describe('fetchDeployVersion', () => {
|
|
|
56
56
|
|
|
57
57
|
it('returns 0 when the broker returns no version field (new app)', async () => {
|
|
58
58
|
mockFetchOnce({});
|
|
59
|
-
await expect(fetchDeployVersion('my-app')).resolves.
|
|
59
|
+
await expect(fetchDeployVersion('my-app')).resolves.toEqual({ version: 0, id: null });
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it('surfaces a remediation hint on 401, tagged broker_auth', async () => {
|
|
@@ -19,6 +19,7 @@ interface CredentialsResponse {
|
|
|
19
19
|
|
|
20
20
|
interface VersionResponse {
|
|
21
21
|
version?: number;
|
|
22
|
+
id?: number | null;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async function postToBroker(path: string, body: unknown): Promise<Response> {
|
|
@@ -67,12 +68,11 @@ async function assertBrokerOk(res: Response, errorLabel: string): Promise<void>
|
|
|
67
68
|
* credentials call so version derivation lives on its own endpoint — the CLI
|
|
68
69
|
* asks for the version first, then requests credentials scoped to it.
|
|
69
70
|
*/
|
|
70
|
-
export async function fetchDeployVersion(appName: string): Promise<
|
|
71
|
+
export async function fetchDeployVersion(appName: string): Promise<Required<VersionResponse>> {
|
|
71
72
|
const res = await postToBroker(DEPLOY_VERSION_PATH, { appName });
|
|
72
73
|
await assertBrokerOk(res, 'deploy');
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
return body.version ?? 0;
|
|
74
|
+
const { version = 0, id = null } = (await res.json()) as VersionResponse;
|
|
75
|
+
return { version, id };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
async function requestSts(path: string, body: unknown, errorLabel: string): Promise<AwsCredentialIdentity> {
|
package/src/util/broker/app.ts
CHANGED
|
@@ -72,7 +72,6 @@ export interface RegisterOrUpdateOptions {
|
|
|
72
72
|
gitRemote: string;
|
|
73
73
|
integrations?: string[];
|
|
74
74
|
handlerNames?: string[];
|
|
75
|
-
apiUsages?: Record<string, unknown>;
|
|
76
75
|
description?: string;
|
|
77
76
|
/**
|
|
78
77
|
* The base app a preview is for, sent only on first registration — the link
|
|
@@ -91,7 +90,6 @@ async function registerApp(params: RegisterOrUpdateOptions & { visibility: 'publ
|
|
|
91
90
|
gitRemote,
|
|
92
91
|
integrations,
|
|
93
92
|
handlerNames,
|
|
94
|
-
apiUsages,
|
|
95
93
|
description,
|
|
96
94
|
masterAppName,
|
|
97
95
|
} = params;
|
|
@@ -106,7 +104,6 @@ async function registerApp(params: RegisterOrUpdateOptions & { visibility: 'publ
|
|
|
106
104
|
gitRemote,
|
|
107
105
|
integrations,
|
|
108
106
|
handlerNames,
|
|
109
|
-
apiUsages,
|
|
110
107
|
description,
|
|
111
108
|
masterAppName,
|
|
112
109
|
}),
|
|
@@ -119,21 +116,11 @@ async function registerApp(params: RegisterOrUpdateOptions & { visibility: 'publ
|
|
|
119
116
|
|
|
120
117
|
/** Releases a new version onto an app that already exists. */
|
|
121
118
|
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;
|
|
119
|
+
const { appName, token, nextVersion: version, entry, gitRemote, integrations, handlerNames, description } = params;
|
|
133
120
|
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/update`, {
|
|
134
121
|
method: 'POST',
|
|
135
122
|
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...routingHeaders() },
|
|
136
|
-
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, handlerNames,
|
|
123
|
+
body: JSON.stringify({ appName, version, entry, gitRemote, integrations, handlerNames, description }),
|
|
137
124
|
});
|
|
138
125
|
if (!res.ok) {
|
|
139
126
|
const text = await res.text();
|
package/src/util/usage-json.ts
CHANGED
|
@@ -10,7 +10,7 @@ export function hasHandlers(appDir: string): boolean {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/** Other features may add top-level fields, so writes merge into the existing file instead of replacing it. */
|
|
13
|
-
export async function writeUsageJson(appDir: string, patch: Record<string, unknown>): Promise<
|
|
13
|
+
export async function writeUsageJson(appDir: string, patch: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
14
14
|
const file = path.join(appDir, 'usage.json');
|
|
15
15
|
let existing: Record<string, unknown> = {};
|
|
16
16
|
try {
|
|
@@ -20,16 +20,23 @@ export async function writeUsageJson(appDir: string, patch: Record<string, unkno
|
|
|
20
20
|
throw err;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
const merged = { ...existing, ...patch };
|
|
24
|
+
await writeJson(file, merged, { spaces: 2 });
|
|
25
|
+
return merged;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/** Refreshes the git-tracked `usage.json` and commits it if it changed, so a deploy's push carries it. */
|
|
27
|
-
export async function updateOrCreateUsageJson(
|
|
29
|
+
export async function updateOrCreateUsageJson(
|
|
30
|
+
appDir: string,
|
|
31
|
+
appName: string,
|
|
32
|
+
deployedBy: string
|
|
33
|
+
): Promise<Record<string, unknown>> {
|
|
28
34
|
const apiUsages = hasHandlers(appDir) ? (await readHandlers(appDir)).apiUsages : {};
|
|
29
|
-
await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
35
|
+
const usage = await writeUsageJson(appDir, { appName, deployedBy, apiUsages });
|
|
30
36
|
await execGit(['add', 'usage.json'], appDir);
|
|
31
37
|
const { exitCode } = await execa('git', ['diff', '--cached', '--quiet'], { cwd: appDir, reject: false });
|
|
32
38
|
if (exitCode !== 0) {
|
|
33
39
|
await execGit(['commit', '-m', 'Update usage.json'], appDir);
|
|
34
40
|
}
|
|
41
|
+
return usage;
|
|
35
42
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PutObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { APP_USAGE_BUCKET_NAME, appUsageFolder, brokerBaseUrl, isProdBroker } from '../constants';
|
|
3
|
+
import { warn } from './logger';
|
|
4
|
+
import { getS3Client } from './s3/client';
|
|
5
|
+
|
|
6
|
+
/** Snowpipe ignores re-uploads of an existing key, so the key is unique per app row id and deploy version. */
|
|
7
|
+
export async function putAppUsage(
|
|
8
|
+
appName: string,
|
|
9
|
+
appId: number | null,
|
|
10
|
+
usage: Record<string, unknown>,
|
|
11
|
+
uploadVersion: number
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
if (!isProdBroker()) {
|
|
14
|
+
warn(
|
|
15
|
+
`skipping app usage upload to Snowflake: ${brokerBaseUrl()} is not the production broker, and only production deploys are recorded.`
|
|
16
|
+
);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (appId === null) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await getS3Client().send(
|
|
24
|
+
new PutObjectCommand({
|
|
25
|
+
Bucket: APP_USAGE_BUCKET_NAME,
|
|
26
|
+
Key: `${appUsageFolder(appName, appId)}/${uploadVersion}/usage.json`,
|
|
27
|
+
Body: JSON.stringify({ ...usage, appName, appId, uploadVersion }),
|
|
28
|
+
ContentType: 'application/json',
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
warn(
|
|
33
|
+
`could not record app usage for Snowflake: ${
|
|
34
|
+
(err as Error).message
|
|
35
|
+
}. Your app is live. To record it, run \`z2h-cli deploy\` again (no changes or commit needed).`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|