@metabase/cli 0.5.8-alpha.bryan-rci-28-lint-stage.198256c → 0.5.9
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/{ci-diZ50wlb.mjs → ci-jYpJfw-b.mjs} +1 -1
- package/dist/cli.mjs +4 -3
- package/dist/{create-BSIevt1L.mjs → create-W6plJIxv.mjs} +1 -1
- package/dist/{data-app-D8UNc8dc.mjs → data-app-DLKxuzzt.mjs} +4 -5
- package/dist/{data-app-project-CiBHndNR.mjs → data-app-project-tbEA14ZN.mjs} +1 -10
- package/dist/{run-DGG0ZqPA.mjs → run-Ch9KAm3W.mjs} +177 -393
- package/dist/{scaffold-UalCpCG0.mjs → scaffold-Pr1BC2uw.mjs} +1 -1
- package/dist/{typed-schema-MNW93duy.mjs → typed-schema-DTL_8ggU.mjs} +1 -1
- package/dist/{update-C3AnMkS7.mjs → update-D32XqpkV.mjs} +1 -1
- package/package.json +2 -3
- package/skill-data/data-app/SKILL.md +23 -17
- package/skill-data/data-app/template/README.md +4 -5
- package/skill-data/data-app/template/gitignore +0 -1
- package/skill-data/data-app/template/package.json +1 -2
- package/skill-data/data-app-ui/SKILL.md +1 -1
- package/skill-data/transform/SKILL.md +1 -1
- package/dist/set-bundle-CJed1vO8.mjs +0 -40
|
@@ -3,7 +3,7 @@ import { t as defineCommandGroup } from "./group-DxaqX3Gg.mjs";
|
|
|
3
3
|
var ci_default = defineCommandGroup({
|
|
4
4
|
name: "ci",
|
|
5
5
|
description: "Check a branch's transforms the way CI does",
|
|
6
|
-
subCommands: { run: () => import("./run-
|
|
6
|
+
subCommands: { run: () => import("./run-Ch9KAm3W.mjs").then((mod) => mod.default) }
|
|
7
7
|
});
|
|
8
8
|
//#endregion
|
|
9
9
|
export { ci_default as default };
|
package/dist/cli.mjs
CHANGED
|
@@ -527,7 +527,7 @@ function trustSystemCa() {
|
|
|
527
527
|
//#endregion
|
|
528
528
|
//#region package.json
|
|
529
529
|
var name = "@metabase/cli";
|
|
530
|
-
var version = "0.5.
|
|
530
|
+
var version = "0.5.9";
|
|
531
531
|
var description = "Metabase CLI";
|
|
532
532
|
//#endregion
|
|
533
533
|
//#region src/runtime/command-augment.ts
|
|
@@ -557,7 +557,7 @@ const main = defineCommand({
|
|
|
557
557
|
dashboard: () => import("./dashboard-CZTlBLWV.mjs").then((mod) => mod.default),
|
|
558
558
|
subscription: () => import("./subscription-DfvU-S6P.mjs").then((mod) => mod.default),
|
|
559
559
|
"data-action": () => import("./data-action-C4wpuM5b.mjs").then((mod) => mod.default),
|
|
560
|
-
"data-app": () => import("./data-app-
|
|
560
|
+
"data-app": () => import("./data-app-DLKxuzzt.mjs").then((mod) => mod.default),
|
|
561
561
|
"api-key": () => import("./api-key-fIfek7nB.mjs").then((mod) => mod.default),
|
|
562
562
|
alert: () => import("./alert-DzgnuHeD.mjs").then((mod) => mod.default),
|
|
563
563
|
collection: () => import("./collection-D_GyzwaY.mjs").then((mod) => mod.default),
|
|
@@ -568,7 +568,7 @@ const main = defineCommand({
|
|
|
568
568
|
"transform-job": () => import("./transform-job-BTM0t8YZ.mjs").then((mod) => mod.default),
|
|
569
569
|
"transform-tag": () => import("./transform-tag-DcJRURqM.mjs").then((mod) => mod.default),
|
|
570
570
|
"transform-test": () => import("./transform-test-CSRDJsx3.mjs").then((mod) => mod.default),
|
|
571
|
-
ci: () => import("./ci-
|
|
571
|
+
ci: () => import("./ci-jYpJfw-b.mjs").then((mod) => mod.default),
|
|
572
572
|
setting: () => import("./setting-h61KIVKH.mjs").then((mod) => mod.default),
|
|
573
573
|
search: () => import("./search-oxkqbIhJ.mjs").then((mod) => mod.default),
|
|
574
574
|
"git-sync": () => import("./git-sync-OiTQkETw.mjs").then((mod) => mod.default),
|
|
@@ -976,6 +976,7 @@ const FEATURE_RULES = {
|
|
|
976
976
|
},
|
|
977
977
|
transforms: { since: 59 },
|
|
978
978
|
transformJobActivation: { since: 61 },
|
|
979
|
+
builtInTransformJobsStartInactive: { since: 65 },
|
|
979
980
|
measures: { since: 59 },
|
|
980
981
|
collectionItemMeasures: { since: 61 },
|
|
981
982
|
explorations: { since: 65 },
|
|
@@ -9,7 +9,7 @@ var create_default = defineMetabaseCommand({
|
|
|
9
9
|
name: "create",
|
|
10
10
|
description: "Create a data app and its collection from JSON"
|
|
11
11
|
},
|
|
12
|
-
details: "The JSON body needs `name`, the slug. Optional: `display_name` (the slug by default), `description`, `allowed_hosts`, `enabled` (true by default), `bundle_path` and `version`. The app starts with an empty bundle;
|
|
12
|
+
details: "The JSON body needs `name`, the slug. Optional: `display_name` (the slug by default), `description`, `allowed_hosts`, `enabled` (true by default), `bundle_path` and `version`. The app starts with an empty bundle; a git-sync import replaces it with the `dist/index.js` committed in `data_apps/<slug>/`. The answer's `resource_collection_id` is the app's collection, where its questions and actions go. `mb data-app scaffold` copies the project template.",
|
|
13
13
|
skills: [{
|
|
14
14
|
skill: "data-app",
|
|
15
15
|
purpose: "how an app is created, built and published"
|
|
@@ -26,13 +26,12 @@ var data_app_default = defineCommandGroup({
|
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
28
|
subCommands: {
|
|
29
|
-
scaffold: () => import("./scaffold-
|
|
29
|
+
scaffold: () => import("./scaffold-Pr1BC2uw.mjs").then((mod) => mod.default),
|
|
30
30
|
list: () => import("./list-CSFgvwH_.mjs").then((mod) => mod.default),
|
|
31
31
|
get: () => import("./get-DDco3nzW.mjs").then((mod) => mod.default),
|
|
32
|
-
create: () => import("./create-
|
|
33
|
-
update: () => import("./update-
|
|
34
|
-
"typed-schema": () => import("./typed-schema-
|
|
35
|
-
"set-bundle": () => import("./set-bundle-CJed1vO8.mjs").then((mod) => mod.default),
|
|
32
|
+
create: () => import("./create-W6plJIxv.mjs").then((mod) => mod.default),
|
|
33
|
+
update: () => import("./update-D32XqpkV.mjs").then((mod) => mod.default),
|
|
34
|
+
"typed-schema": () => import("./typed-schema-DTL_8ggU.mjs").then((mod) => mod.default),
|
|
36
35
|
"set-enabled": () => import("./set-enabled-Q7t_Jjev.mjs").then((mod) => mod.default),
|
|
37
36
|
delete: () => import("./delete-z-gK8iJm.mjs").then((mod) => mod.default)
|
|
38
37
|
}
|
|
@@ -4,17 +4,8 @@ import { z } from "zod";
|
|
|
4
4
|
import { cp, mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
//#region src/runtime/data-app-project.ts
|
|
7
|
-
const DEFAULT_BUNDLE_PATH = "build/index.js";
|
|
8
7
|
const PackageManifest = z.object({}).loose();
|
|
9
8
|
const SHIPPED_GITIGNORE = "gitignore";
|
|
10
|
-
async function readBundle(path) {
|
|
11
|
-
try {
|
|
12
|
-
return await readFile(path, "utf8");
|
|
13
|
-
} catch (error) {
|
|
14
|
-
if (isFileNotFoundError(error)) throw new ConfigError(`bundle not found: ${path} — build the app first (npm run build)`);
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
9
|
async function writeProjectFile(path, text) {
|
|
19
10
|
await mkdir(dirname(path), { recursive: true });
|
|
20
11
|
await writeFile(path, text, "utf8");
|
|
@@ -55,4 +46,4 @@ async function scaffoldDataAppProject({ dir, templateDir }) {
|
|
|
55
46
|
await renamePackage(dir, packageName);
|
|
56
47
|
}
|
|
57
48
|
//#endregion
|
|
58
|
-
export { writeProjectFile as
|
|
49
|
+
export { writeProjectFile as n, scaffoldDataAppProject as t };
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { c as InternalError, f as ResponseShapeError,
|
|
1
|
+
import { c as InternalError, f as ResponseShapeError, h as ValidationError, i as AbortError, o as ConfigError, p as TimeoutError, r as isPlainObject, s as FailedResultError, u as NetworkError } from "./predicates-0C0TsWcJ.mjs";
|
|
2
2
|
import { R as CapabilityError, Y as HttpError, Z as isHttpNotFound, a as exitCodeFor, c as lineWriter, ct as connectionFlags, lt as formatFlags, m as serializeJson, mt as profileFlag, s as jsonLineWriter, t as interruptSignal, vt as parseInteger } from "./cli.mjs";
|
|
3
3
|
import { i as NotAuthenticatedError, t as defineMetabaseCommand } from "./runtime-fJ-A3m-R.mjs";
|
|
4
4
|
import { i as TransformTestRunResult, o as isTransformTestCode } from "./transform-test-D6QIyu8C.mjs";
|
|
5
5
|
import { a as runProcess, n as ProcessNotFoundError, o as runProcessBinary, r as ProcessTimeoutError, t as ProcessInterruptedError } from "./process-ByKCTa6A.mjs";
|
|
6
6
|
import { t as parseMetabaseYaml } from "./yaml-Ctv1l2-P.mjs";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import {
|
|
8
|
+
import { rename, rm, writeFile } from "node:fs/promises";
|
|
9
9
|
import { basename, dirname, join } from "node:path";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
10
|
import { randomUUID } from "node:crypto";
|
|
12
|
-
import { lint } from "@metabase/representations";
|
|
13
11
|
import { isDeepStrictEqual } from "node:util";
|
|
14
12
|
import { performance } from "node:perf_hooks";
|
|
15
13
|
//#region src/core/ci-coverage.ts
|
|
@@ -140,163 +138,6 @@ function coverageStage(assessment) {
|
|
|
140
138
|
}
|
|
141
139
|
};
|
|
142
140
|
}
|
|
143
|
-
//#endregion
|
|
144
|
-
//#region src/core/ci-lint.ts
|
|
145
|
-
const LINT_ERROR_RULES = [
|
|
146
|
-
"schema",
|
|
147
|
-
"duplicate-entity-id",
|
|
148
|
-
"dangling-transform-id",
|
|
149
|
-
"wrong-type-transform-id",
|
|
150
|
-
"transform-cycle",
|
|
151
|
-
"missing-input",
|
|
152
|
-
"unused-input",
|
|
153
|
-
"untestable-transform",
|
|
154
|
-
"duplicate-input",
|
|
155
|
-
"expectation-reads-other-table",
|
|
156
|
-
"row-columns-mismatch",
|
|
157
|
-
"duplicate-expectation-name",
|
|
158
|
-
"transform-collection",
|
|
159
|
-
"missing-incremental-table-tag",
|
|
160
|
-
"missing-target-schema"
|
|
161
|
-
];
|
|
162
|
-
const LINT_WARNING_RULES = [
|
|
163
|
-
"unknown-table",
|
|
164
|
-
"duplicate-target",
|
|
165
|
-
"reads-not-checked",
|
|
166
|
-
"no-expectations",
|
|
167
|
-
"unsupported-transform-engine",
|
|
168
|
-
"untestable-engine",
|
|
169
|
-
"upstream-column-mismatch",
|
|
170
|
-
"input-missing-read-column",
|
|
171
|
-
"table-qualified-column",
|
|
172
|
-
"live-sql-input"
|
|
173
|
-
];
|
|
174
|
-
const LintEntity = z.object({
|
|
175
|
-
model: z.string().nullable(),
|
|
176
|
-
entity_id: z.string().nullable(),
|
|
177
|
-
name: z.string().nullable()
|
|
178
|
-
});
|
|
179
|
-
const LintFindingFields = z.object({
|
|
180
|
-
file: z.string(),
|
|
181
|
-
entity: LintEntity,
|
|
182
|
-
message: z.string(),
|
|
183
|
-
related: z.array(z.string())
|
|
184
|
-
});
|
|
185
|
-
const LintError = LintFindingFields.extend({
|
|
186
|
-
rule: z.enum(LINT_ERROR_RULES),
|
|
187
|
-
severity: z.literal("error")
|
|
188
|
-
});
|
|
189
|
-
const LintWarning = LintFindingFields.extend({
|
|
190
|
-
rule: z.enum(LINT_WARNING_RULES),
|
|
191
|
-
severity: z.literal("warning")
|
|
192
|
-
});
|
|
193
|
-
const LintResult = z.object({
|
|
194
|
-
files: z.number().int().nonnegative(),
|
|
195
|
-
findings: z.array(z.discriminatedUnion("severity", [LintError, LintWarning]))
|
|
196
|
-
});
|
|
197
|
-
const ERROR_CHECK_KIND = {
|
|
198
|
-
schema: "yaml",
|
|
199
|
-
"transform-cycle": "cycles",
|
|
200
|
-
"duplicate-entity-id": "broken-refs",
|
|
201
|
-
"dangling-transform-id": "broken-refs",
|
|
202
|
-
"wrong-type-transform-id": "broken-refs",
|
|
203
|
-
"missing-input": "broken-refs",
|
|
204
|
-
"unused-input": "broken-refs",
|
|
205
|
-
"untestable-transform": "broken-refs",
|
|
206
|
-
"duplicate-input": "broken-refs",
|
|
207
|
-
"expectation-reads-other-table": "broken-refs",
|
|
208
|
-
"row-columns-mismatch": "broken-refs",
|
|
209
|
-
"duplicate-expectation-name": "broken-refs",
|
|
210
|
-
"transform-collection": "broken-refs",
|
|
211
|
-
"missing-incremental-table-tag": "broken-refs",
|
|
212
|
-
"missing-target-schema": "broken-refs"
|
|
213
|
-
};
|
|
214
|
-
const LINT_RUN_CHECK = {
|
|
215
|
-
kind: "lint",
|
|
216
|
-
id: "lint",
|
|
217
|
-
label: "Representation lint"
|
|
218
|
-
};
|
|
219
|
-
function failedLabel(finding) {
|
|
220
|
-
if (finding.entity.name === null) return `${finding.file}: ${finding.rule}`;
|
|
221
|
-
return `${finding.entity.name}: ${finding.rule}`;
|
|
222
|
-
}
|
|
223
|
-
function failedCheck(finding, occurrence) {
|
|
224
|
-
return {
|
|
225
|
-
check: {
|
|
226
|
-
kind: ERROR_CHECK_KIND[finding.rule],
|
|
227
|
-
id: `${finding.rule}:${finding.file}:${occurrence}`,
|
|
228
|
-
label: failedLabel(finding)
|
|
229
|
-
},
|
|
230
|
-
verdict: "failed",
|
|
231
|
-
warnings: [],
|
|
232
|
-
ms: 0,
|
|
233
|
-
detail: finding
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
function lintWarning(finding) {
|
|
237
|
-
return {
|
|
238
|
-
code: finding.rule,
|
|
239
|
-
message: `${finding.file}: ${finding.message}`
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
function warningsCheck(files, findings) {
|
|
243
|
-
return {
|
|
244
|
-
check: LINT_RUN_CHECK,
|
|
245
|
-
verdict: "passed",
|
|
246
|
-
warnings: findings.map(lintWarning),
|
|
247
|
-
ms: 0,
|
|
248
|
-
detail: {
|
|
249
|
-
files,
|
|
250
|
-
findings
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
function crashedCheck(message) {
|
|
255
|
-
return {
|
|
256
|
-
check: LINT_RUN_CHECK,
|
|
257
|
-
verdict: "error",
|
|
258
|
-
warnings: [],
|
|
259
|
-
ms: 0,
|
|
260
|
-
detail: { message }
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
function lintChecks(result) {
|
|
264
|
-
const failed = [];
|
|
265
|
-
const warnings = [];
|
|
266
|
-
const occurrences = /* @__PURE__ */ new Map();
|
|
267
|
-
for (const finding of result.findings) if (finding.severity === "error") {
|
|
268
|
-
const key = `${finding.rule}:${finding.file}`;
|
|
269
|
-
const previous = occurrences.get(key);
|
|
270
|
-
const occurrence = previous === void 0 ? 1 : previous + 1;
|
|
271
|
-
occurrences.set(key, occurrence);
|
|
272
|
-
failed.push(failedCheck(finding, occurrence));
|
|
273
|
-
} else warnings.push(finding);
|
|
274
|
-
return [...failed, warningsCheck(result.files, warnings)];
|
|
275
|
-
}
|
|
276
|
-
async function runLint(lintTree) {
|
|
277
|
-
let output;
|
|
278
|
-
try {
|
|
279
|
-
output = await lintTree();
|
|
280
|
-
} catch (error) {
|
|
281
|
-
if (error instanceof AbortError) throw error;
|
|
282
|
-
return [crashedCheck(`the linter threw: ${errorMessage(error)}`)];
|
|
283
|
-
}
|
|
284
|
-
const parsed = LintResult.safeParse(output);
|
|
285
|
-
if (!parsed.success) return [crashedCheck(`the linter answered an unreadable result: ${z.prettifyError(parsed.error)}`)];
|
|
286
|
-
return lintChecks(parsed.data);
|
|
287
|
-
}
|
|
288
|
-
function lintStage(coverage, lintTree) {
|
|
289
|
-
const coverageChecks = coverageStage(coverage);
|
|
290
|
-
return {
|
|
291
|
-
name: "lint",
|
|
292
|
-
run: async (report) => {
|
|
293
|
-
const stopped = await coverageChecks.run(report);
|
|
294
|
-
if (stopped !== null) return stopped;
|
|
295
|
-
for (const check of await runLint(lintTree)) report(check);
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
141
|
const StageName = z.enum(["lint", "unit"]);
|
|
301
142
|
const Verdict = z.enum([
|
|
302
143
|
"passed",
|
|
@@ -328,7 +169,6 @@ const Snapshot = z.object({
|
|
|
328
169
|
const Check = z.object({
|
|
329
170
|
kind: z.enum([
|
|
330
171
|
"coverage",
|
|
331
|
-
"lint",
|
|
332
172
|
"yaml",
|
|
333
173
|
"cycles",
|
|
334
174
|
"broken-refs",
|
|
@@ -456,34 +296,21 @@ const VERDICT_MARK = {
|
|
|
456
296
|
error: "!",
|
|
457
297
|
refused: "!"
|
|
458
298
|
};
|
|
459
|
-
const NO_FAILED_CHECKS =
|
|
460
|
-
const
|
|
461
|
-
"transform",
|
|
462
|
-
"
|
|
463
|
-
"
|
|
464
|
-
|
|
465
|
-
const FIX_TEXT = {
|
|
466
|
-
transform: "fix the transform",
|
|
467
|
-
tests: "add or restore transform tests",
|
|
468
|
-
lint: "fix what the linter flagged"
|
|
299
|
+
const NO_FAILED_CHECKS = "none";
|
|
300
|
+
const FAILED_TEXT = {
|
|
301
|
+
none: "FAILED fix the transform",
|
|
302
|
+
transform: "FAILED fix the transform",
|
|
303
|
+
tests: "FAILED add or restore transform tests",
|
|
304
|
+
both: "FAILED fix the transform and add the missing tests"
|
|
469
305
|
};
|
|
470
|
-
function fixFor(kind) {
|
|
471
|
-
switch (kind) {
|
|
472
|
-
case "coverage": return "tests";
|
|
473
|
-
case "transform-test": return "transform";
|
|
474
|
-
case "lint":
|
|
475
|
-
case "yaml":
|
|
476
|
-
case "cycles":
|
|
477
|
-
case "broken-refs": return "lint";
|
|
478
|
-
default: throw new InternalError(`check of no known kind: ${JSON.stringify(kind)}`);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
306
|
const ERRORED_TEXT = "ERRORED fix the test or the setup";
|
|
482
307
|
const CheckMessage = z.object({ message: z.string() });
|
|
483
308
|
const HttpFailureMessage = CheckMessage.extend({ status: z.number().int() });
|
|
484
309
|
function failedChecksAfter(failed, event) {
|
|
485
310
|
if (event.type !== "check.result" || event.verdict !== "failed") return failed;
|
|
486
|
-
|
|
311
|
+
const kind = event.check.kind === "coverage" ? "tests" : "transform";
|
|
312
|
+
if (failed === "none" || failed === kind) return kind;
|
|
313
|
+
return "both";
|
|
487
314
|
}
|
|
488
315
|
function ciEventLines(event, maxRows, failed) {
|
|
489
316
|
switch (event.type) {
|
|
@@ -508,9 +335,7 @@ function shortSha(sha) {
|
|
|
508
335
|
}
|
|
509
336
|
function jobEndText(state, failed) {
|
|
510
337
|
if (state === "passed") return "PASSED";
|
|
511
|
-
|
|
512
|
-
const fixes = FIX_ORDER.filter((fix) => failed.has(fix));
|
|
513
|
-
return `FAILED ${(fixes.length === 0 ? [FIX_TEXT.transform] : fixes.map((fix) => FIX_TEXT[fix])).join(" and ")}`;
|
|
338
|
+
return state === "failed" ? FAILED_TEXT[failed] : ERRORED_TEXT;
|
|
514
339
|
}
|
|
515
340
|
function jobExitCode(state) {
|
|
516
341
|
return state === "passed" ? PASSED_EXIT_CODE : exitCodeFor("failed");
|
|
@@ -594,208 +419,6 @@ function cellText(value) {
|
|
|
594
419
|
return value === null ? "null" : String(value);
|
|
595
420
|
}
|
|
596
421
|
//#endregion
|
|
597
|
-
//#region src/runtime/git.ts
|
|
598
|
-
const GIT = "git";
|
|
599
|
-
const TAR = "tar";
|
|
600
|
-
const NO_MATCH_EXIT_CODE = 1;
|
|
601
|
-
const NO_MERGE_BASE_EXIT_CODE = 1;
|
|
602
|
-
const UNKNOWN_REF_EXIT_CODE = 1;
|
|
603
|
-
const GIT_TIMEOUT_MS = 12e4;
|
|
604
|
-
async function withGit(repo, args, spawn) {
|
|
605
|
-
try {
|
|
606
|
-
return await spawn();
|
|
607
|
-
} catch (error) {
|
|
608
|
-
if (error instanceof ProcessNotFoundError) throw new ConfigError("git is not installed or not on PATH");
|
|
609
|
-
if (error instanceof ProcessInterruptedError) throw new AbortError(`interrupted while git ${args.join(" ")} ran in ${repo}`);
|
|
610
|
-
if (error instanceof ProcessTimeoutError) throw new ConfigError(`git ${args.join(" ")} did not finish within ${GIT_TIMEOUT_MS / 1e3} s in ${repo}`);
|
|
611
|
-
throw error;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
function runGit(repo, args, signal) {
|
|
615
|
-
return withGit(repo, args, () => runProcess(GIT, [
|
|
616
|
-
"-C",
|
|
617
|
-
repo,
|
|
618
|
-
...args
|
|
619
|
-
], {
|
|
620
|
-
signal,
|
|
621
|
-
timeoutMs: GIT_TIMEOUT_MS
|
|
622
|
-
}));
|
|
623
|
-
}
|
|
624
|
-
async function git(repo, args, signal) {
|
|
625
|
-
const result = await runGit(repo, args, signal);
|
|
626
|
-
if (result.exitCode !== 0) throw new ConfigError(`git ${args.join(" ")} failed in ${repo}: ${result.stderr.trim()}`);
|
|
627
|
-
return result.stdout;
|
|
628
|
-
}
|
|
629
|
-
async function repositoryRoot(repo, signal) {
|
|
630
|
-
const result = await runGit(repo, ["rev-parse", "--show-toplevel"], signal);
|
|
631
|
-
if (result.exitCode !== 0) throw new ConfigError(`--repo ${repo} is not inside a git work tree: ${result.stderr.trim()}`);
|
|
632
|
-
return result.stdout.trim();
|
|
633
|
-
}
|
|
634
|
-
async function resolveCommit(repo, ref, flag, signal) {
|
|
635
|
-
const result = await runGit(repo, [
|
|
636
|
-
"rev-parse",
|
|
637
|
-
"--verify",
|
|
638
|
-
"--quiet",
|
|
639
|
-
"--end-of-options",
|
|
640
|
-
`${ref}^{commit}`
|
|
641
|
-
], signal);
|
|
642
|
-
if (result.exitCode === UNKNOWN_REF_EXIT_CODE) throw new ConfigError(`${flag} ${ref} is not a commit in ${repo}`);
|
|
643
|
-
if (result.exitCode !== 0) throw new ConfigError(`git rev-parse failed in ${repo}: ${result.stderr.trim()}`);
|
|
644
|
-
return result.stdout.trim();
|
|
645
|
-
}
|
|
646
|
-
async function checkedOutCommit(repo, signal) {
|
|
647
|
-
const result = await runGit(repo, [
|
|
648
|
-
"rev-parse",
|
|
649
|
-
"--verify",
|
|
650
|
-
"--quiet",
|
|
651
|
-
"HEAD"
|
|
652
|
-
], signal);
|
|
653
|
-
if (result.exitCode === UNKNOWN_REF_EXIT_CODE) return null;
|
|
654
|
-
if (result.exitCode !== 0) throw new ConfigError(`git rev-parse HEAD failed in ${repo}: ${result.stderr.trim()}`);
|
|
655
|
-
return result.stdout.trim();
|
|
656
|
-
}
|
|
657
|
-
async function mergeBase(repo, base, head, signal) {
|
|
658
|
-
const result = await runGit(repo, [
|
|
659
|
-
"merge-base",
|
|
660
|
-
base,
|
|
661
|
-
head
|
|
662
|
-
], signal);
|
|
663
|
-
if (result.exitCode === NO_MERGE_BASE_EXIT_CODE) throw new ConfigError(`--base and --head share no history in ${repo}: the clone may be shallow (fetch the full history, e.g. actions/checkout with fetch-depth: 0), or the histories are unrelated`);
|
|
664
|
-
if (result.exitCode !== 0) throw new ConfigError(`git merge-base failed in ${repo}: ${result.stderr.trim()}`);
|
|
665
|
-
return result.stdout.trim();
|
|
666
|
-
}
|
|
667
|
-
function diffNameStatus(repo, from, to, signal) {
|
|
668
|
-
return git(repo, [
|
|
669
|
-
"diff",
|
|
670
|
-
"--name-status",
|
|
671
|
-
"-z",
|
|
672
|
-
"-M",
|
|
673
|
-
from,
|
|
674
|
-
to
|
|
675
|
-
], signal);
|
|
676
|
-
}
|
|
677
|
-
function statusPorcelain(repo, signal) {
|
|
678
|
-
return git(repo, [
|
|
679
|
-
"status",
|
|
680
|
-
"--porcelain",
|
|
681
|
-
"-z",
|
|
682
|
-
"--untracked-files=all"
|
|
683
|
-
], signal);
|
|
684
|
-
}
|
|
685
|
-
async function fileModesAt(repo, commit, signal) {
|
|
686
|
-
const output = await git(repo, [
|
|
687
|
-
"ls-tree",
|
|
688
|
-
"-r",
|
|
689
|
-
"-z",
|
|
690
|
-
"--full-tree",
|
|
691
|
-
commit
|
|
692
|
-
], signal);
|
|
693
|
-
const modes = /* @__PURE__ */ new Map();
|
|
694
|
-
for (const record of output.split("\0")) {
|
|
695
|
-
if (record === "") continue;
|
|
696
|
-
const space = record.indexOf(" ");
|
|
697
|
-
const tab = record.indexOf(" ");
|
|
698
|
-
if (space === -1 || tab < space) throw new InternalError(`git ls-tree -z: malformed entry "${record}"`);
|
|
699
|
-
modes.set(record.slice(tab + 1), record.slice(0, space));
|
|
700
|
-
}
|
|
701
|
-
return modes;
|
|
702
|
-
}
|
|
703
|
-
async function filesMatching(repo, commit, pattern, pathspecs, signal) {
|
|
704
|
-
const result = await runGit(repo, [
|
|
705
|
-
"grep",
|
|
706
|
-
"--no-color",
|
|
707
|
-
"-z",
|
|
708
|
-
"-l",
|
|
709
|
-
"-E",
|
|
710
|
-
"-e",
|
|
711
|
-
pattern,
|
|
712
|
-
commit,
|
|
713
|
-
"--",
|
|
714
|
-
...pathspecs
|
|
715
|
-
], signal);
|
|
716
|
-
if (result.exitCode === NO_MATCH_EXIT_CODE) return [];
|
|
717
|
-
if (result.exitCode !== 0) throw new ConfigError(`git grep failed in ${repo}: ${result.stderr.trim()}`);
|
|
718
|
-
const prefix = `${commit}:`;
|
|
719
|
-
return result.stdout.split("\0").filter((entry) => entry !== "").map((entry) => {
|
|
720
|
-
if (!entry.startsWith(prefix)) throw new InternalError(`git grep printed "${entry}", which does not start with ${prefix}`);
|
|
721
|
-
return entry.slice(prefix.length);
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
const NEWLINE = 10;
|
|
725
|
-
const utf8 = new TextDecoder("utf-8");
|
|
726
|
-
async function readFilesAt(repo, commit, paths, signal) {
|
|
727
|
-
const files = /* @__PURE__ */ new Map();
|
|
728
|
-
if (paths.length === 0) return files;
|
|
729
|
-
const multiline = paths.find((path) => path.includes("\n"));
|
|
730
|
-
if (multiline !== void 0) throw new ConfigError(`cannot read ${JSON.stringify(multiline)}: a path with a newline cannot be batched`);
|
|
731
|
-
const args = ["cat-file", "--batch"];
|
|
732
|
-
const result = await withGit(repo, args, () => runProcessBinary(GIT, [
|
|
733
|
-
"-C",
|
|
734
|
-
repo,
|
|
735
|
-
...args
|
|
736
|
-
], {
|
|
737
|
-
stdin: paths.map((path) => `${commit}:${path}\n`).join(""),
|
|
738
|
-
signal,
|
|
739
|
-
timeoutMs: GIT_TIMEOUT_MS
|
|
740
|
-
}));
|
|
741
|
-
if (result.exitCode !== 0) throw new ConfigError(`git cat-file --batch failed in ${repo}: ${result.stderr.trim()}`);
|
|
742
|
-
const output = result.stdout;
|
|
743
|
-
let offset = 0;
|
|
744
|
-
for (const path of paths) {
|
|
745
|
-
const headerEnd = output.indexOf(NEWLINE, offset);
|
|
746
|
-
if (headerEnd === -1) throw new InternalError(`git cat-file --batch stopped before ${path}`);
|
|
747
|
-
const header = utf8.decode(output.subarray(offset, headerEnd)).split(" ");
|
|
748
|
-
const size = Number(header[2]);
|
|
749
|
-
if (header[1] !== "blob" || !Number.isInteger(size)) throw new InternalError(`${path} is not a file at ${commit}, though git listed it there`);
|
|
750
|
-
const start = headerEnd + 1;
|
|
751
|
-
if (output[start + size] !== NEWLINE) throw new InternalError(`git cat-file --batch framed ${path} with ${size} bytes, but no newline follows them`);
|
|
752
|
-
files.set(path, utf8.decode(output.subarray(start, start + size)));
|
|
753
|
-
offset = start + size + 1;
|
|
754
|
-
}
|
|
755
|
-
if (offset !== output.length) throw new InternalError("git cat-file --batch printed more than the paths it was asked for");
|
|
756
|
-
return files;
|
|
757
|
-
}
|
|
758
|
-
async function withCommittedTree(repo, commit, signal, use) {
|
|
759
|
-
const root = await repositoryRoot(repo, signal);
|
|
760
|
-
const args = [
|
|
761
|
-
"archive",
|
|
762
|
-
"--format=tar",
|
|
763
|
-
commit
|
|
764
|
-
];
|
|
765
|
-
const archive = await withGit(root, args, () => runProcessBinary(GIT, [
|
|
766
|
-
"-C",
|
|
767
|
-
root,
|
|
768
|
-
...args
|
|
769
|
-
], {
|
|
770
|
-
signal,
|
|
771
|
-
timeoutMs: GIT_TIMEOUT_MS
|
|
772
|
-
}));
|
|
773
|
-
if (archive.exitCode !== 0) throw new ConfigError(`git archive ${commit} failed in ${root}: ${archive.stderr.trim()}`);
|
|
774
|
-
const folder = await mkdtemp(join(tmpdir(), "mb-ci-tree-"));
|
|
775
|
-
try {
|
|
776
|
-
const extract = await runProcess(TAR, [
|
|
777
|
-
"-x",
|
|
778
|
-
"-f",
|
|
779
|
-
"-",
|
|
780
|
-
"-C",
|
|
781
|
-
folder
|
|
782
|
-
], {
|
|
783
|
-
stdin: archive.stdout,
|
|
784
|
-
signal
|
|
785
|
-
}).catch((error) => {
|
|
786
|
-
if (error instanceof ProcessInterruptedError) throw new AbortError(`interrupted while tar extracted ${commit}`);
|
|
787
|
-
throw error;
|
|
788
|
-
});
|
|
789
|
-
if (extract.exitCode !== 0) throw new ConfigError(`tar could not extract ${commit}: ${extract.stderr.trim()}`);
|
|
790
|
-
return await use(folder);
|
|
791
|
-
} finally {
|
|
792
|
-
await rm(folder, {
|
|
793
|
-
recursive: true,
|
|
794
|
-
force: true
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
//#endregion
|
|
799
422
|
//#region src/core/ci-affected.ts
|
|
800
423
|
const SINGLE_PATH_STATUSES = /* @__PURE__ */ new Map([
|
|
801
424
|
["A", "added"],
|
|
@@ -1034,6 +657,167 @@ function unreadContentWarning(reasons) {
|
|
|
1034
657
|
};
|
|
1035
658
|
}
|
|
1036
659
|
//#endregion
|
|
660
|
+
//#region src/runtime/git.ts
|
|
661
|
+
const GIT = "git";
|
|
662
|
+
const NO_MATCH_EXIT_CODE = 1;
|
|
663
|
+
const NO_MERGE_BASE_EXIT_CODE = 1;
|
|
664
|
+
const UNKNOWN_REF_EXIT_CODE = 1;
|
|
665
|
+
const GIT_TIMEOUT_MS = 12e4;
|
|
666
|
+
async function withGit(repo, args, spawn) {
|
|
667
|
+
try {
|
|
668
|
+
return await spawn();
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (error instanceof ProcessNotFoundError) throw new ConfigError("git is not installed or not on PATH");
|
|
671
|
+
if (error instanceof ProcessInterruptedError) throw new AbortError(`interrupted while git ${args.join(" ")} ran in ${repo}`);
|
|
672
|
+
if (error instanceof ProcessTimeoutError) throw new ConfigError(`git ${args.join(" ")} did not finish within ${GIT_TIMEOUT_MS / 1e3} s in ${repo}`);
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function runGit(repo, args, signal) {
|
|
677
|
+
return withGit(repo, args, () => runProcess(GIT, [
|
|
678
|
+
"-C",
|
|
679
|
+
repo,
|
|
680
|
+
...args
|
|
681
|
+
], {
|
|
682
|
+
signal,
|
|
683
|
+
timeoutMs: GIT_TIMEOUT_MS
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
async function git(repo, args, signal) {
|
|
687
|
+
const result = await runGit(repo, args, signal);
|
|
688
|
+
if (result.exitCode !== 0) throw new ConfigError(`git ${args.join(" ")} failed in ${repo}: ${result.stderr.trim()}`);
|
|
689
|
+
return result.stdout;
|
|
690
|
+
}
|
|
691
|
+
async function repositoryRoot(repo, signal) {
|
|
692
|
+
const result = await runGit(repo, ["rev-parse", "--show-toplevel"], signal);
|
|
693
|
+
if (result.exitCode !== 0) throw new ConfigError(`--repo ${repo} is not inside a git work tree: ${result.stderr.trim()}`);
|
|
694
|
+
return result.stdout.trim();
|
|
695
|
+
}
|
|
696
|
+
async function resolveCommit(repo, ref, flag, signal) {
|
|
697
|
+
const result = await runGit(repo, [
|
|
698
|
+
"rev-parse",
|
|
699
|
+
"--verify",
|
|
700
|
+
"--quiet",
|
|
701
|
+
"--end-of-options",
|
|
702
|
+
`${ref}^{commit}`
|
|
703
|
+
], signal);
|
|
704
|
+
if (result.exitCode === UNKNOWN_REF_EXIT_CODE) throw new ConfigError(`${flag} ${ref} is not a commit in ${repo}`);
|
|
705
|
+
if (result.exitCode !== 0) throw new ConfigError(`git rev-parse failed in ${repo}: ${result.stderr.trim()}`);
|
|
706
|
+
return result.stdout.trim();
|
|
707
|
+
}
|
|
708
|
+
async function checkedOutCommit(repo, signal) {
|
|
709
|
+
const result = await runGit(repo, [
|
|
710
|
+
"rev-parse",
|
|
711
|
+
"--verify",
|
|
712
|
+
"--quiet",
|
|
713
|
+
"HEAD"
|
|
714
|
+
], signal);
|
|
715
|
+
if (result.exitCode === UNKNOWN_REF_EXIT_CODE) return null;
|
|
716
|
+
if (result.exitCode !== 0) throw new ConfigError(`git rev-parse HEAD failed in ${repo}: ${result.stderr.trim()}`);
|
|
717
|
+
return result.stdout.trim();
|
|
718
|
+
}
|
|
719
|
+
async function mergeBase(repo, base, head, signal) {
|
|
720
|
+
const result = await runGit(repo, [
|
|
721
|
+
"merge-base",
|
|
722
|
+
base,
|
|
723
|
+
head
|
|
724
|
+
], signal);
|
|
725
|
+
if (result.exitCode === NO_MERGE_BASE_EXIT_CODE) throw new ConfigError(`--base and --head share no history in ${repo}: the clone may be shallow (fetch the full history, e.g. actions/checkout with fetch-depth: 0), or the histories are unrelated`);
|
|
726
|
+
if (result.exitCode !== 0) throw new ConfigError(`git merge-base failed in ${repo}: ${result.stderr.trim()}`);
|
|
727
|
+
return result.stdout.trim();
|
|
728
|
+
}
|
|
729
|
+
function diffNameStatus(repo, from, to, signal) {
|
|
730
|
+
return git(repo, [
|
|
731
|
+
"diff",
|
|
732
|
+
"--name-status",
|
|
733
|
+
"-z",
|
|
734
|
+
"-M",
|
|
735
|
+
from,
|
|
736
|
+
to
|
|
737
|
+
], signal);
|
|
738
|
+
}
|
|
739
|
+
function statusPorcelain(repo, signal) {
|
|
740
|
+
return git(repo, [
|
|
741
|
+
"status",
|
|
742
|
+
"--porcelain",
|
|
743
|
+
"-z",
|
|
744
|
+
"--untracked-files=all"
|
|
745
|
+
], signal);
|
|
746
|
+
}
|
|
747
|
+
async function fileModesAt(repo, commit, signal) {
|
|
748
|
+
const output = await git(repo, [
|
|
749
|
+
"ls-tree",
|
|
750
|
+
"-r",
|
|
751
|
+
"-z",
|
|
752
|
+
"--full-tree",
|
|
753
|
+
commit
|
|
754
|
+
], signal);
|
|
755
|
+
const modes = /* @__PURE__ */ new Map();
|
|
756
|
+
for (const record of output.split("\0")) {
|
|
757
|
+
if (record === "") continue;
|
|
758
|
+
const space = record.indexOf(" ");
|
|
759
|
+
const tab = record.indexOf(" ");
|
|
760
|
+
if (space === -1 || tab < space) throw new InternalError(`git ls-tree -z: malformed entry "${record}"`);
|
|
761
|
+
modes.set(record.slice(tab + 1), record.slice(0, space));
|
|
762
|
+
}
|
|
763
|
+
return modes;
|
|
764
|
+
}
|
|
765
|
+
async function filesMatching(repo, commit, pattern, pathspecs, signal) {
|
|
766
|
+
const result = await runGit(repo, [
|
|
767
|
+
"grep",
|
|
768
|
+
"--no-color",
|
|
769
|
+
"-z",
|
|
770
|
+
"-l",
|
|
771
|
+
"-E",
|
|
772
|
+
"-e",
|
|
773
|
+
pattern,
|
|
774
|
+
commit,
|
|
775
|
+
"--",
|
|
776
|
+
...pathspecs
|
|
777
|
+
], signal);
|
|
778
|
+
if (result.exitCode === NO_MATCH_EXIT_CODE) return [];
|
|
779
|
+
if (result.exitCode !== 0) throw new ConfigError(`git grep failed in ${repo}: ${result.stderr.trim()}`);
|
|
780
|
+
const prefix = `${commit}:`;
|
|
781
|
+
return result.stdout.split("\0").filter((entry) => entry !== "").map((entry) => {
|
|
782
|
+
if (!entry.startsWith(prefix)) throw new InternalError(`git grep printed "${entry}", which does not start with ${prefix}`);
|
|
783
|
+
return entry.slice(prefix.length);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
const NEWLINE = 10;
|
|
787
|
+
const utf8 = new TextDecoder("utf-8");
|
|
788
|
+
async function readFilesAt(repo, commit, paths, signal) {
|
|
789
|
+
const files = /* @__PURE__ */ new Map();
|
|
790
|
+
if (paths.length === 0) return files;
|
|
791
|
+
const multiline = paths.find((path) => path.includes("\n"));
|
|
792
|
+
if (multiline !== void 0) throw new ConfigError(`cannot read ${JSON.stringify(multiline)}: a path with a newline cannot be batched`);
|
|
793
|
+
const args = ["cat-file", "--batch"];
|
|
794
|
+
const result = await withGit(repo, args, () => runProcessBinary(GIT, [
|
|
795
|
+
"-C",
|
|
796
|
+
repo,
|
|
797
|
+
...args
|
|
798
|
+
], {
|
|
799
|
+
stdin: paths.map((path) => `${commit}:${path}\n`).join(""),
|
|
800
|
+
signal,
|
|
801
|
+
timeoutMs: GIT_TIMEOUT_MS
|
|
802
|
+
}));
|
|
803
|
+
if (result.exitCode !== 0) throw new ConfigError(`git cat-file --batch failed in ${repo}: ${result.stderr.trim()}`);
|
|
804
|
+
const output = result.stdout;
|
|
805
|
+
let offset = 0;
|
|
806
|
+
for (const path of paths) {
|
|
807
|
+
const headerEnd = output.indexOf(NEWLINE, offset);
|
|
808
|
+
if (headerEnd === -1) throw new InternalError(`git cat-file --batch stopped before ${path}`);
|
|
809
|
+
const header = utf8.decode(output.subarray(offset, headerEnd)).split(" ");
|
|
810
|
+
const size = Number(header[2]);
|
|
811
|
+
if (header[1] !== "blob" || !Number.isInteger(size)) throw new InternalError(`${path} is not a file at ${commit}, though git listed it there`);
|
|
812
|
+
const start = headerEnd + 1;
|
|
813
|
+
if (output[start + size] !== NEWLINE) throw new InternalError(`git cat-file --batch framed ${path} with ${size} bytes, but no newline follows them`);
|
|
814
|
+
files.set(path, utf8.decode(output.subarray(start, start + size)));
|
|
815
|
+
offset = start + size + 1;
|
|
816
|
+
}
|
|
817
|
+
if (offset !== output.length) throw new InternalError("git cat-file --batch printed more than the paths it was asked for");
|
|
818
|
+
return files;
|
|
819
|
+
}
|
|
820
|
+
//#endregion
|
|
1037
821
|
//#region src/commands/ci/job-plan.ts
|
|
1038
822
|
const CONTENT_PATHSPECS = CONTENT_TOP_LEVEL_FOLDERS.map((folder) => `${folder}/*.yaml`);
|
|
1039
823
|
const FILE_MODES = /* @__PURE__ */ new Set(["100644", "100755"]);
|
|
@@ -1512,7 +1296,7 @@ var run_default = defineMetabaseCommand({
|
|
|
1512
1296
|
description: "Check the transforms a branch changed, reporting each check as it finishes"
|
|
1513
1297
|
},
|
|
1514
1298
|
details: [
|
|
1515
|
-
"Compares --head with its merge base with --base and finds the transforms whose files or tests changed. Two stages check them: `lint` fails each transform the branch adds or whose logic (source or target) it edits that has no transform test, fails each transform the branch leaves at head without the last of its transform tests, and warns about untested transforms whose logic the branch did not change.
|
|
1299
|
+
"Compares --head with its merge base with --base and finds the transforms whose files or tests changed. Two stages check them: `lint` fails each transform the branch adds or whose logic (source or target) it edits that has no transform test, fails each transform the branch leaves at head without the last of its transform tests, and warns about untested transforms whose logic the branch did not change. A transform whose source type is not `query`, such as a Python transform, cannot have transform tests, so `lint` does not fail it: an `untestable-transform` warning lists each such transform the branch adds, edits or strips of its tests. A change to the transform's query text, whitespace included, is an edit; a change to the YAML layout alone is not. A transform test is a TransformTest file at --head whose `transform_id` names the transform and whose `expectations` list is not empty; a test without expectations checks nothing, so it does not count. `unit` runs the instance's tests of the transforms in `job.start.checked`, on the Metabase instance that --url, MB_URL or --profile names, in that order, which must already hold the branch's content; it is absent when `checked` is empty, so a job with nothing to test never connects to Metabase, and it runs even when `lint` fails, so one job reports both stages' findings. A checked transform the branch tests but the instance lists no test for is `refused` with `detail.error_code` `ci.no-tests-on-instance`. Content files are the `.yaml` files Metabase's import reads: not dotfiles, under a top-level serialization folder such as `transforms/` or `collections/`. Only committed content is checked: when --head is the checked-out commit, a content file with uncommitted changes is listed in an `uncommitted-content` warning, and its committed version is checked.",
|
|
1516
1300
|
"With --format json, the default when stdout is not a terminal, writes one JSON event per line (`job.start`, then each stage and check, then `job.end`); `job.start.checked` lists the transforms the job covered, and a `no-content-changes` warning says when the change touches no content file, so no transform test ran; an `unread-content` warning lists the content files at --head that the change did not touch and this check cannot read, which coverage skips; each transform test's result is passed through as the check's `detail`, with every row the server sent. With --format text, the default on a terminal, writes the same events as lines for a person: each failed expectation with up to --max-rows lines of its missing and extra rows, keyed by the first column, then a count of the rest. The --results file is JSON whatever the format, and holds every row the server sent.",
|
|
1517
1301
|
"Exits 0 when the job passes. Exits 1 after `job.end` when it does not: `state` is `failed` when a check failed (fix the transform), and `errored` when no check failed but one could not judge the change (fix the test or the setup); a failure outranks errors, since it is a finding about the change. Exits 2 on a usage or config error: a bad flag, a --repo, --base or --head that git cannot resolve or relate, or, found after `job.start` when the unit stage connects, a configuration it cannot use, such as a browser-login profile bound to another URL; a missing credential is not one, since it ends the unit stage instead.",
|
|
1518
1302
|
"A test run that times out, loses its connection or gets an HTTP error is an `error` check whose `detail.error_code` starts with `ci.`, and the next test runs. A failure every later request would repeat ends the unit stage early, `errored` unless a check failed, with the reason in `stage.end.stopped`: no Metabase credential is configured (`ci.no-credential`, before any request), Metabase rejects the credential, the server has no transform testing, the entity-id lookup fails, or Metabase cannot be reached before a test runs. Exits 1 on a crash, such as a content file the change touched that does not parse at --head, and 130 on an interrupt: the stream stops before `job.end`, the error is on stderr, and no --results file is left, not even an earlier run's. When the reader closes stdout early, the job still runs to the end, writes --results and exits by its state."
|
|
@@ -1564,10 +1348,10 @@ var run_default = defineMetabaseCommand({
|
|
|
1564
1348
|
base: args.base,
|
|
1565
1349
|
head: args.head,
|
|
1566
1350
|
stages: async (plan) => {
|
|
1567
|
-
const lint
|
|
1568
|
-
if (plan.checked.length === 0) return [lint
|
|
1351
|
+
const lint = coverageStage(plan.coverage);
|
|
1352
|
+
if (plan.checked.length === 0) return [lint];
|
|
1569
1353
|
const connect = async () => clientPort(await getClient());
|
|
1570
|
-
return [lint
|
|
1354
|
+
return [lint, unitStage(plan.checked, untestedTransforms(plan.coverage), connect)];
|
|
1571
1355
|
},
|
|
1572
1356
|
job: randomUUID(),
|
|
1573
1357
|
now: () => /* @__PURE__ */ new Date(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { f as renderSummary, pt as outputFlags } from "./cli.mjs";
|
|
2
2
|
import { t as defineMetabaseCommand } from "./runtime-fJ-A3m-R.mjs";
|
|
3
3
|
import { n as findSkillByName, r as loadAllSkills } from "./skills-Dt95bt39.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { t as scaffoldDataAppProject } from "./data-app-project-tbEA14ZN.mjs";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
//#region src/commands/data-app/scaffold.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ct as connectionFlags, g as writeText, mt as profileFlag } from "./cli.mjs";
|
|
2
2
|
import { t as defineMetabaseCommand } from "./runtime-fJ-A3m-R.mjs";
|
|
3
3
|
import { n as slugArg, t as parseSlug } from "./slug-COdwq63O.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { n as writeProjectFile } from "./data-app-project-tbEA14ZN.mjs";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
//#region src/commands/data-app/typed-schema.ts
|
|
7
7
|
const TypedSchemaModule = z.string().describe("The generated TypeScript module of the app's queries and actions.");
|
|
@@ -10,7 +10,7 @@ var update_default = defineMetabaseCommand({
|
|
|
10
10
|
name: "update",
|
|
11
11
|
description: "Update a data app's fields, or enable or disable it"
|
|
12
12
|
},
|
|
13
|
-
details: "The JSON body carries only the fields to change: `display_name`, `description`, `allowed_hosts`, `enabled`, `bundle_path`, `version`. A disabled app opens only in Data Studio (Data apps → the app → Overview).
|
|
13
|
+
details: "The JSON body carries only the fields to change: `display_name`, `description`, `allowed_hosts`, `enabled`, `bundle_path`, `version`. A disabled app opens only in Data Studio (Data apps → the app → Overview).",
|
|
14
14
|
requires: ["dataApp.update"],
|
|
15
15
|
args: {
|
|
16
16
|
...outputFlags,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metabase/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "Metabase CLI",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"repository": {
|
|
@@ -25,11 +25,10 @@
|
|
|
25
25
|
"build": "tsdown",
|
|
26
26
|
"dev": "tsdown --watch",
|
|
27
27
|
"start": "node dist/cli.mjs",
|
|
28
|
-
"prepublishOnly": "tsdown && publint && ! grep -rE \"(from|import|require)[[:space:]]*\\(?['\\\"]@metabase
|
|
28
|
+
"prepublishOnly": "tsdown && publint && ! grep -rE \"(from|import|require)[[:space:]]*\\(?['\\\"]@metabase\" dist/"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^0.8.2",
|
|
32
|
-
"@metabase/representations": "1.6.1-alpha.br-galdre-reference-lint.sha-f1106aa",
|
|
33
32
|
"@napi-rs/keyring": "^1.3.0",
|
|
34
33
|
"ajv": "^8.17.1",
|
|
35
34
|
"ajv-formats": "^3.0.1",
|
|
@@ -12,17 +12,20 @@ A **data app** is a React app built into one JavaScript bundle, which Metabase r
|
|
|
12
12
|
The work, in order:
|
|
13
13
|
|
|
14
14
|
1. Create the app on the instance.
|
|
15
|
-
2. Scaffold the project and install the SDK.
|
|
15
|
+
2. Scaffold the project in the synced git repository and install the SDK.
|
|
16
16
|
3. Give it data: questions over the Library's published tables and metrics in the app's collection, then `src/metabase.ts` (`data-app-data`); writes are `data-app-actions`.
|
|
17
17
|
4. Start the dev preview (_Dev preview_), then write the UI under `src/` (`data-app-ui`; filter controls in `data-app-filters`).
|
|
18
|
-
5. Build and
|
|
18
|
+
5. Build, test, and push the source and the bundle together (_Build and publish_).
|
|
19
19
|
|
|
20
|
-
For every later change: first pass the checks in _An existing project_ (_Scaffold and install_), then regenerate `src/metabase.ts` if the app's collection changed, then build and
|
|
20
|
+
For every later change: first pass the checks in _An existing project_ (_Scaffold and install_), then regenerate `src/metabase.ts` if the app's collection changed, then build and publish (_Build and publish_). The bundle reaches Metabase only through git: a git-sync import loads the `dist/index.js` committed in the app's folder.
|
|
21
21
|
|
|
22
22
|
## Before you start
|
|
23
23
|
|
|
24
24
|
- Use an **admin** profile — only admins create apps and write the app's collection. `mb auth status --profile <name> --json | jq .user.isAdmin` prints `true` for one, and every command below takes that `--profile`; otherwise ask the user for an admin profile (`mb auth list`).
|
|
25
25
|
- Run `mb data-app list`. A failure means the instance has no data apps: stop and tell the user.
|
|
26
|
+
- The app ships through the instance's synced git repository. Check both ends first; if either fails, stop and tell the user which, changing nothing:
|
|
27
|
+
- `mb git-sync status --profile <name> --json` reports `remote_configured: true` and `sync_type: "read-write"`.
|
|
28
|
+
- The working folder is a clone of it: `git rev-parse --show-toplevel` succeeds, and `git remote -v` lists the repository `mb setting get remote-sync-url --profile <name>` names, in HTTPS or SSH form. That top level is `<repo>` below.
|
|
26
29
|
- Settle the **slug**, the `/apps/<slug>` URL: lowercase letters, digits and single dashes (`sales-overview`), at most 100 characters. If the app's purpose is unclear, ask "what's this app for?" and propose one.
|
|
27
30
|
|
|
28
31
|
## Create the app
|
|
@@ -39,14 +42,14 @@ mb data-app create --body '{"name":"sales-overview","display_name":"Sales overvi
|
|
|
39
42
|
mb data-app scaffold <repo>/data_apps/sales-overview && cd <repo>/data_apps/sales-overview
|
|
40
43
|
```
|
|
41
44
|
|
|
42
|
-
`scaffold [dir]` takes a **directory**, not the slug: it copies the Vite + React + TypeScript template into a new or empty directory (the current one when omitted) and needs no server.
|
|
45
|
+
`scaffold [dir]` takes a **directory**, not the slug: it copies the Vite + React + TypeScript template into a new or empty directory (the current one when omitted) and needs no server. The directory is `data_apps/<slug>/` in `<repo>`, where an export writes the app's `data_app.yaml` and bundle: an export leaves the source alone and an import ignores it, so the repository keeps the source with the app. `scaffold` needs an empty directory, so scaffold before the app's first export; for an app already in the repository, scaffold into a new directory and copy its files, dotfiles included (`cp -R <dir>/. data_apps/<slug>/`), in beside `data_app.yaml` and `dist/`; check `.gitignore` is there before `npm install`. Then install the SDK, then the rest:
|
|
43
46
|
|
|
44
47
|
```bash
|
|
45
48
|
npm install @metabase/embedding-sdk-react@<major>-beta # <major>: 66 for a version tag v1.66.x (`mb auth status --profile <name> --json | jq -r .version.tag`)
|
|
46
|
-
npm install && npm run typecheck
|
|
49
|
+
npm install && npm run typecheck
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
Skip the SDK line when `package.json` already points it at a local build (`file:` or `link:`), and never install `latest`. A tag without a version number, such as `vLOCAL_DEV`, is a development build: ask the user which SDK build to install. The starter renders "Hello, data app".
|
|
52
|
+
Skip the SDK line when `package.json` already points it at a local build (`file:` or `link:`), and never install `latest`. A tag without a version number, such as `vLOCAL_DEV`, is a development build: ask the user which SDK build to install. The starter renders "Hello, data app". Build only in _Build and publish_, after the export: both write `dist/index.js`.
|
|
50
53
|
|
|
51
54
|
The template is the bundle contract. Don't edit `package.json` by hand, and never rewrite `vite.config.ts`, `tsconfig.json` or `src/index.tsx`: `vite.config.ts` stays `export default dataAppConfig()` (its only option is `port`), and `src/index.tsx` default-exports the `DataAppFactory` — change it only to add a `providerProps` key. Metabase and the dev preview each wrap the app in a `<MetabaseProvider>`, so the app never renders one.
|
|
52
55
|
|
|
@@ -56,11 +59,11 @@ The template is the bundle contract. Don't edit `package.json` by hand, and neve
|
|
|
56
59
|
- `src/index.tsx` default-exports a `DataAppFactory` that takes no arguments and returns `{ component, providerProps? }`.
|
|
57
60
|
- `npm ls react react-dom` exits 0 and reports 19.x. A 19.x it marks `invalid` fails. This check applies whatever the instance or SDK version; to skip it, ask the user first.
|
|
58
61
|
|
|
59
|
-
Fix a failed React check yourself, before any other change: upgrade `react`, `react-dom`, `@types/react` and `@types/react-dom` to 19 until `npm run typecheck
|
|
62
|
+
Fix a failed React check yourself, before any other change: upgrade `react`, `react-dom`, `@types/react` and `@types/react-dom` to 19 until `npm run typecheck` passes and the diagnostics feed is clean (_Dev preview_); then publish (_Build and publish_). If either other check fails, stop: tell the user the project differs from the template, and ask whether to scaffold a fresh project and port the code over, or go on at their own risk.
|
|
60
63
|
|
|
61
64
|
## Dev preview
|
|
62
65
|
|
|
63
|
-
`npm run dev` serves the app at `http://localhost:5174` — or the next free port, in Vite's `Local:` line — through the same sandbox Metabase uses. An edit under `src/` rebuilds the bundle and remounts the app, resetting component state. The preview reads `DATA_APP_MB_URL` and an admin `DATA_APP_MB_API_KEY` from `.env.local` beside `package.json`. Write a new key straight into that file, never into the chat or the terminal:
|
|
66
|
+
`npm run dev` serves the app at `http://localhost:5174` — or the next free port, in Vite's `Local:` line — through the same sandbox Metabase uses. An edit under `src/` rebuilds the bundle and remounts the app, resetting component state. The preview reads `DATA_APP_MB_URL` and an admin `DATA_APP_MB_API_KEY` from `.env.local` beside `package.json`. Create that key only on the user's yes: ask with `AskUserQuestion` ("Create an admin API key `data-app <slug> dev` for the preview? It goes in `.env.local` and is deleted at handoff."). On a no, the user fills in `.env.local` from `.env.local.example`. Write a new key straight into that file, never into the chat or the terminal:
|
|
64
67
|
|
|
65
68
|
```bash
|
|
66
69
|
( umask 077
|
|
@@ -102,7 +105,7 @@ curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0"
|
|
|
102
105
|
- `clients: 0` means no tab is open, so empty `entries` prove nothing.
|
|
103
106
|
- Before an edit, note `nextEventId`; after it, read again with that value as `startEventId`.
|
|
104
107
|
- The feed answers for the running bundle: entries from replaced builds are counted in `staleEntries`, and anything still broken reports again under the new `buildId` (`?includeStale=true` shows the withheld ones). A failed build doesn't advance `buildId` and doesn't reach the feed — read the `npm run dev` terminal.
|
|
105
|
-
- Quote `summary` as it is when you report it, and follow `hint` when there is one — except that you never add the Metabase URL to `allowed_hosts`, and
|
|
108
|
+
- Quote `summary` as it is when you report it, and follow `hint` when there is one — except that you never add the Metabase URL to `allowed_hosts`, and you set `allowed_hosts` with `mb data-app update`, then export and pull (_Build and publish_, step 1), instead of editing `data_app.yaml`. A `frame-src` or `form-action` block can't be lifted in the preview (`data-app-ui`). The feed keeps the last 200 entries; `curl -X DELETE …/__data-app/diagnostics` clears it.
|
|
106
109
|
|
|
107
110
|
| `kind` | Fix |
|
|
108
111
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -113,12 +116,15 @@ curl -s "http://localhost:5174/__data-app/diagnostics?startEventId=0"
|
|
|
113
116
|
|
|
114
117
|
## Build and publish
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
mb
|
|
119
|
-
|
|
119
|
+
Build the bundle last, test it in the dev preview, then push it with the source in one commit:
|
|
120
|
+
|
|
121
|
+
1. Export the app's `data_app.yaml` and the questions and actions in its collection: `mb git-sync export` (`git-sync`), then in `<repo>` `git fetch`, `git switch <branch>`, `git pull --ff-only`.
|
|
122
|
+
2. `npm run typecheck && npm run build`, which overwrites the exported `dist/index.js`.
|
|
123
|
+
3. With `npm run dev` running and a preview tab open, read the diagnostics feed (_Dev preview_) with every page mounted; fix what it reports and build again.
|
|
124
|
+
4. Commit `data_apps/<slug>/` — the source and `dist/index.js` together — and push.
|
|
125
|
+
5. `mb git-sync import --profile <name>`, whatever a cached `has-remote-changes` says. The new bundle is live at once.
|
|
120
126
|
|
|
121
|
-
|
|
127
|
+
A new app is on from the start; `mb data-app set-enabled <slug> false` turns it off, which only the user decides. An app turned off opens only in Data Studio → Data apps → the app → Overview; `/apps/<slug>` says "not found", for admins too. Give the user `<metabase-url>/apps/<slug>`.
|
|
122
128
|
|
|
123
129
|
A viewer needs one of their groups assigned to the app (otherwise "You don't have access"), which only the user can do (Data Studio → Data apps → the app → Permissions). A group reaches the app's collection and nothing more, so a viewer also needs data access to the tables its questions read.
|
|
124
130
|
|
|
@@ -126,7 +132,7 @@ A viewer needs one of their groups assigned to the app (otherwise "You don't hav
|
|
|
126
132
|
|
|
127
133
|
| Symptom | Fix |
|
|
128
134
|
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
129
|
-
|
|
|
135
|
+
| The app still runs the old bundle | `dist/index.js` was built before the export's pull, or isn't in the pushed commit: build, commit, push, import. |
|
|
130
136
|
| Blank app, `Bundle did not assign a function to __dataAppFactory__` | `src/index.tsx` must `export default` the factory and nothing else; otherwise the diagnostics feed has the real error. |
|
|
131
137
|
| "Failed to fetch the user, the session might be invalid." | A bad key in `.env.local`, or CORS (_Dev preview_). |
|
|
132
138
|
| A bundle of several MB | `vite.config.ts` no longer uses `dataAppConfig()`, or the SDK isn't the `<major>-beta` tag. |
|
|
@@ -139,5 +145,5 @@ A viewer needs one of their groups assigned to the app (otherwise "You don't hav
|
|
|
139
145
|
|
|
140
146
|
- **Turn off:** `mb data-app set-enabled <slug> false`. **Rename:** `mb data-app update <slug> --body '{"display_name":"…"}'` (also `description`, `allowed_hosts`).
|
|
141
147
|
- **Outdated:** an app Data Studio lists as _Outdated_ targets an older data-app contract: it won't open for anyone, and only admins see it, until it's migrated. Tell the user; don't change its code ad hoc.
|
|
142
|
-
- **Move to another instance:** the app, its collection and what the collection holds are git-synced content. `mb git-sync export` (`git-sync`) writes `data_apps/<slug>/` with the bundle, and the collection under `collections/data_apps/`; importing the repository on the other instance recreates them with the same entity IDs, so the same `src/metabase.ts` works there. The first import there creates the app on, like `create`, open to admins until the user assigns groups; later imports keep its on/off state. The import doesn't check that the questions' tables are published on that instance, so the Library content they read goes in the same export: flag the Library root once (`mb git-sync add-collection <library-id>`, `git-sync`), or its metrics, measures and segments stay out of the repository; a data action needs its database's toggle on there too.
|
|
143
|
-
- **Delete:** confirm with the user, then `mb data-app delete <slug> --yes` — it removes the app, its bundle, its collection and the questions and actions in it. The project directory stays:
|
|
148
|
+
- **Move to another instance:** the app, its collection and what the collection holds are git-synced content. `mb git-sync export` (`git-sync`) writes `data_apps/<slug>/` with the bundle, and the collection under `collections/data_apps/`; importing the repository on the other instance recreates them with the same entity IDs, so the same `src/metabase.ts` works there. The first import there creates the app on, like `create`, open to admins until the user assigns groups; later imports keep its on/off state. The import doesn't check that the questions' tables are published on that instance, so the Library content they read goes in the same export: flag the Library root once (`mb git-sync add-collection <library-id>`, `git-sync`), or its metrics, measures and segments stay out of the repository; a data action needs its database's toggle on there too. Never edit `data_app.yaml`; `dist/index.js` changes only through `npm run build` (_Build and publish_). The template's `.gitignore` keeps `node_modules/` and `.env.local` out. On a read-only synced instance only `set-enabled` works: creating, updating and deleting an app answer 403, and so does editing its collection; change it where it was built, push, and import there.
|
|
149
|
+
- **Delete:** confirm with the user, then `mb data-app delete <slug> --yes` — it removes the app, its bundle, its collection and the questions and actions in it. The project directory stays: the next export removes the app's `data_app.yaml`, bundle and collection files; after that export, switch and pull the same way, delete the source in `data_apps/<slug>/` in a commit of your own, push, and import.
|
|
@@ -20,14 +20,13 @@ npm install
|
|
|
20
20
|
```bash
|
|
21
21
|
npm run dev # preview at http://localhost:5174, in the sandbox Metabase runs apps in
|
|
22
22
|
npm run typecheck # type-checks src/ and vite.config.ts
|
|
23
|
-
npm run build # builds
|
|
23
|
+
npm run build # builds dist/index.js, the app's bundle
|
|
24
24
|
mb data-app typed-schema <slug> --out src/metabase.ts # the app's queries and actions
|
|
25
|
-
mb data-app set-bundle <slug> # makes build/index.js the app's bundle
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
`
|
|
30
|
-
|
|
27
|
+
The bundle ships through git: commit `dist/index.js` (the file
|
|
28
|
+
`data_app.yaml` names) with the source and push; the next git-sync import
|
|
29
|
+
loads it.
|
|
31
30
|
|
|
32
31
|
`npm run dev` reads `DATA_APP_MB_URL` and `DATA_APP_MB_API_KEY` from
|
|
33
32
|
`.env.local`; `.env.local.example` lists them.
|
|
@@ -38,7 +38,7 @@ Everything here runs inside the sandbox, the same in the dev preview as in Metab
|
|
|
38
38
|
| `navigator.geolocation` and other device APIs | Nothing. |
|
|
39
39
|
| Key, input, composition and clipboard listeners (`keydown`, `paste`, `input`, …) on `window` or `document` | A React handler (`onKeyDown`, `onPaste`, …) on your own element. |
|
|
40
40
|
|
|
41
|
-
**External APIs.** A raw `fetch` reaches only the origins in the app's `allowed_hosts`, exact or with a `*.` subdomain wildcard: `mb data-app update <slug> --body '{"allowed_hosts":["https://api.example.com"]}'`. The preview reads the `allowed_hosts:` list in `data_app.yaml` beside `package.json
|
|
41
|
+
**External APIs.** A raw `fetch` reaches only the origins in the app's `allowed_hosts`, exact or with a `*.` subdomain wildcard: `mb data-app update <slug> --body '{"allowed_hosts":["https://api.example.com"]}'`. The preview reads the `allowed_hosts:` list in `data_app.yaml` beside `package.json`, the app's exported manifest: refresh it by exporting and pulling the export branch (`data-app`, _Build and publish_, step 1), never by writing it. Then restart `npm run dev`. Never list the Metabase URL. In Metabase the same list governs `<iframe src>` and native `<form action>` submits, and the target must allow framing; the preview blocks both. Use `<form onSubmit>` with `preventDefault`: a native submit navigates the app away.
|
|
42
42
|
|
|
43
43
|
## Layout
|
|
44
44
|
|
|
@@ -271,6 +271,6 @@ mb transform-job set-active false --profile <name> --json # disable every job a
|
|
|
271
271
|
|
|
272
272
|
<!-- /requires -->
|
|
273
273
|
|
|
274
|
-
Every job row carries `active`; it is `null` on a server that cannot switch jobs off, where every job runs on schedule.
|
|
274
|
+
Every job row carries `active`; it is `null` on a server that cannot switch jobs off, where every job runs on schedule. A job with `active: false` never runs on schedule, and a fresh install of a newer server starts the built-in jobs that no transform uses that way: before relying on one, check `active`, and turn it on only with the user's yes (`transform-job update <id> --body '{"active":true}'`, admin only).
|
|
275
275
|
|
|
276
276
|
`transform-job run` is fire-and-forget — it returns `{message, started, run_id}` immediately (both `null` when the server does not number job runs: the request was accepted and whether a run started is unsaid), with no per-job-run polling (no `--wait`). Most ad-hoc agent work is one-off `transform run`, not job authoring.
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { ct as connectionFlags, f as renderSummary, mt as profileFlag, pt as outputFlags } from "./cli.mjs";
|
|
2
|
-
import { t as defineMetabaseCommand } from "./runtime-fJ-A3m-R.mjs";
|
|
3
|
-
import { t as DataApp } from "./data-app-BUW-HlUo.mjs";
|
|
4
|
-
import { t as dataAppView } from "./data-app-Blz61mG5.mjs";
|
|
5
|
-
import { n as slugArg, t as parseSlug } from "./slug-COdwq63O.mjs";
|
|
6
|
-
import { n as readBundle, t as DEFAULT_BUNDLE_PATH } from "./data-app-project-CiBHndNR.mjs";
|
|
7
|
-
//#region src/commands/data-app/set-bundle.ts
|
|
8
|
-
var set_bundle_default = defineMetabaseCommand({
|
|
9
|
-
meta: {
|
|
10
|
-
name: "set-bundle",
|
|
11
|
-
description: "Replace a data app's bundle with a built one"
|
|
12
|
-
},
|
|
13
|
-
details: "Reads the built bundle (`npm run build` writes `build/index.js`) and makes it the bundle Metabase serves at /apps/<slug>, at once. A new app is enabled; viewers reach it once a group is assigned. A disabled app opens only in Data Studio (Data apps → the app → Overview).",
|
|
14
|
-
skills: [{
|
|
15
|
-
skill: "data-app",
|
|
16
|
-
purpose: "build and publish an app"
|
|
17
|
-
}],
|
|
18
|
-
requires: ["dataApp.replaceBundle"],
|
|
19
|
-
args: {
|
|
20
|
-
...outputFlags,
|
|
21
|
-
...profileFlag,
|
|
22
|
-
...connectionFlags,
|
|
23
|
-
...slugArg,
|
|
24
|
-
bundle: {
|
|
25
|
-
type: "string",
|
|
26
|
-
description: "Built bundle file",
|
|
27
|
-
default: DEFAULT_BUNDLE_PATH
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
outputSchema: DataApp,
|
|
31
|
-
examples: ["mb data-app set-bundle sales-overview", "mb data-app set-bundle sales-overview --bundle out/index.js"],
|
|
32
|
-
async run({ args, ctx, getClient }) {
|
|
33
|
-
const slug = parseSlug(args.slug);
|
|
34
|
-
const bundle = await readBundle(args.bundle);
|
|
35
|
-
const app = await (await getClient()).dataApp.replaceBundle(slug, bundle);
|
|
36
|
-
renderSummary(app, dataAppView, `Set the bundle of data app ${slug}; it is ${app.enabled ? `served at /apps/${slug}` : "disabled, so it opens only in Data Studio"}.`, ctx);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
//#endregion
|
|
40
|
-
export { set_bundle_default as default };
|