@ddt-tools/cli 0.2.0 → 0.2.4
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/advise-tests-YNMKVJCD.js +87 -0
- package/dist/advise-tests-YNMKVJCD.js.map +1 -0
- package/dist/ai-NTNPYEKZ.js +86 -0
- package/dist/ai-NTNPYEKZ.js.map +1 -0
- package/dist/anonymize-LERTWUQO.js +139 -0
- package/dist/anonymize-LERTWUQO.js.map +1 -0
- package/dist/approval-GGZGKIU4.js +73 -0
- package/dist/approval-GGZGKIU4.js.map +1 -0
- package/dist/approval-chain-GWJKZHVU.js +118 -0
- package/dist/approval-chain-GWJKZHVU.js.map +1 -0
- package/dist/audit-log-2PH55BU4.js +159 -0
- package/dist/audit-log-2PH55BU4.js.map +1 -0
- package/dist/backlog-QNXGOUF4.js +76 -0
- package/dist/backlog-QNXGOUF4.js.map +1 -0
- package/dist/bisect-W3XKKRWG.js +111 -0
- package/dist/bisect-W3XKKRWG.js.map +1 -0
- package/dist/bookmarks-XVOGXGMC.js +107 -0
- package/dist/bookmarks-XVOGXGMC.js.map +1 -0
- package/dist/branch-S3I2IJGQ.js +103 -0
- package/dist/branch-S3I2IJGQ.js.map +1 -0
- package/dist/build-MP3JQEFO.js +20 -0
- package/dist/build-MP3JQEFO.js.map +1 -0
- package/dist/catalog-3J3NFNXP.js +137 -0
- package/dist/catalog-3J3NFNXP.js.map +1 -0
- package/dist/changelog-ZQAH3ULB.js +216 -0
- package/dist/changelog-ZQAH3ULB.js.map +1 -0
- package/dist/chunk-2FT6HXKS.js +55 -0
- package/dist/chunk-2FT6HXKS.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-DL3V7UJ2.js +25 -0
- package/dist/chunk-DL3V7UJ2.js.map +1 -0
- package/dist/chunk-VM2H4LAO.js +15 -0
- package/dist/chunk-VM2H4LAO.js.map +1 -0
- package/dist/chunk-XFXG347C.js +40 -0
- package/dist/chunk-XFXG347C.js.map +1 -0
- package/dist/cli.js +499 -19402
- package/dist/cli.js.map +1 -1
- package/dist/compare-P7JOV76O.js +379 -0
- package/dist/compare-P7JOV76O.js.map +1 -0
- package/dist/compare-profiles-H33CXZPD.js +219 -0
- package/dist/compare-profiles-H33CXZPD.js.map +1 -0
- package/dist/completion-ZSNCQKJ2.js +89 -0
- package/dist/completion-ZSNCQKJ2.js.map +1 -0
- package/dist/connection-CDGVEFUC.js +148 -0
- package/dist/connection-CDGVEFUC.js.map +1 -0
- package/dist/cost-estimate-S2MKHT2H.js +321 -0
- package/dist/cost-estimate-S2MKHT2H.js.map +1 -0
- package/dist/data-compare-46ZI7KHL.js +128 -0
- package/dist/data-compare-46ZI7KHL.js.map +1 -0
- package/dist/data-fit-WGEPLD5S.js +127 -0
- package/dist/data-fit-WGEPLD5S.js.map +1 -0
- package/dist/deploy-status-4H5KJFRC.js +58 -0
- package/dist/deploy-status-4H5KJFRC.js.map +1 -0
- package/dist/design-ILX3ZSWW.js +135 -0
- package/dist/design-ILX3ZSWW.js.map +1 -0
- package/dist/diagnose-WPUL67E4.js +150 -0
- package/dist/diagnose-WPUL67E4.js.map +1 -0
- package/dist/discover-DEO2R5T6.js +78 -0
- package/dist/discover-DEO2R5T6.js.map +1 -0
- package/dist/docs-QNY3MUVO.js +183 -0
- package/dist/docs-QNY3MUVO.js.map +1 -0
- package/dist/drift-FDRNPWQA.js +233 -0
- package/dist/drift-FDRNPWQA.js.map +1 -0
- package/dist/drift-gate-6BWWWMHW.js +103 -0
- package/dist/drift-gate-6BWWWMHW.js.map +1 -0
- package/dist/error-lookup-4R3Y4RBC.js +56 -0
- package/dist/error-lookup-4R3Y4RBC.js.map +1 -0
- package/dist/errorReporting-3LPE2IJY.js +109 -0
- package/dist/errorReporting-3LPE2IJY.js.map +1 -0
- package/dist/exec-JOLH5LPT.js +122 -0
- package/dist/exec-JOLH5LPT.js.map +1 -0
- package/dist/explain-NS26WE2Y.js +189 -0
- package/dist/explain-NS26WE2Y.js.map +1 -0
- package/dist/explorer-GSYYYOAL.js +58 -0
- package/dist/explorer-GSYYYOAL.js.map +1 -0
- package/dist/extract-4LWEZG4O.js +152 -0
- package/dist/extract-4LWEZG4O.js.map +1 -0
- package/dist/features-KQV4OFIZ.js +54 -0
- package/dist/features-KQV4OFIZ.js.map +1 -0
- package/dist/feedback-CBLGXUEG.js +158 -0
- package/dist/feedback-CBLGXUEG.js.map +1 -0
- package/dist/find-SMXRCZ76.js +176 -0
- package/dist/find-SMXRCZ76.js.map +1 -0
- package/dist/format-HMGG6MY3.js +277 -0
- package/dist/format-HMGG6MY3.js.map +1 -0
- package/dist/generate-W7VLBDLI.js +160 -0
- package/dist/generate-W7VLBDLI.js.map +1 -0
- package/dist/graph-YYL5UYCJ.js +168 -0
- package/dist/graph-YYL5UYCJ.js.map +1 -0
- package/dist/history-GDRFP4PG.js +184 -0
- package/dist/history-GDRFP4PG.js.map +1 -0
- package/dist/hosts-DRFZTMIJ.js +45 -0
- package/dist/hosts-DRFZTMIJ.js.map +1 -0
- package/dist/impact-A4NU6CB2.js +63 -0
- package/dist/impact-A4NU6CB2.js.map +1 -0
- package/dist/import-2RNYDL4E.js +79 -0
- package/dist/import-2RNYDL4E.js.map +1 -0
- package/dist/index.cjs +11 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/init-EAOGNGXI.js +54 -0
- package/dist/init-EAOGNGXI.js.map +1 -0
- package/dist/install-hooks-G3Y5LVXK.js +109 -0
- package/dist/install-hooks-G3Y5LVXK.js.map +1 -0
- package/dist/license-Z5YSC7XQ.js +43 -0
- package/dist/license-Z5YSC7XQ.js.map +1 -0
- package/dist/lineage-C5CGVP36.js +555 -0
- package/dist/lineage-C5CGVP36.js.map +1 -0
- package/dist/lint-AQFPZ3WG.js +144 -0
- package/dist/lint-AQFPZ3WG.js.map +1 -0
- package/dist/mcp-F7FND5X7.js +343 -0
- package/dist/mcp-F7FND5X7.js.map +1 -0
- package/dist/migrate-from-dbt-K4ELOWUD.js +156 -0
- package/dist/migrate-from-dbt-K4ELOWUD.js.map +1 -0
- package/dist/migrate-platform-E7VZFPO5.js +91 -0
- package/dist/migrate-platform-E7VZFPO5.js.map +1 -0
- package/dist/optimize-WUJ5ZN5Y.js +109 -0
- package/dist/optimize-WUJ5ZN5Y.js.map +1 -0
- package/dist/perf-UULZSREY.js +200 -0
- package/dist/perf-UULZSREY.js.map +1 -0
- package/dist/pii-QHU32VML.js +146 -0
- package/dist/pii-QHU32VML.js.map +1 -0
- package/dist/pilot-BR6GVK32.js +29 -0
- package/dist/pilot-BR6GVK32.js.map +1 -0
- package/dist/pr-comment-2FOA3EXG.js +81 -0
- package/dist/pr-comment-2FOA3EXG.js.map +1 -0
- package/dist/preview-XNY422OU.js +46 -0
- package/dist/preview-XNY422OU.js.map +1 -0
- package/dist/profile-SQTBNKYS.js +98 -0
- package/dist/profile-SQTBNKYS.js.map +1 -0
- package/dist/promote-FSGUPIPD.js +417 -0
- package/dist/promote-FSGUPIPD.js.map +1 -0
- package/dist/publish-AYCRMCE2.js +739 -0
- package/dist/publish-AYCRMCE2.js.map +1 -0
- package/dist/purge-Y5IOTXKA.js +56 -0
- package/dist/purge-Y5IOTXKA.js.map +1 -0
- package/dist/query-log-SDDGMJLJ.js +112 -0
- package/dist/query-log-SDDGMJLJ.js.map +1 -0
- package/dist/refactor-TC7S43F2.js +5809 -0
- package/dist/refactor-TC7S43F2.js.map +1 -0
- package/dist/refresh-MDJYOYV5.js +39 -0
- package/dist/refresh-MDJYOYV5.js.map +1 -0
- package/dist/replay-E4664A5K.js +118 -0
- package/dist/replay-E4664A5K.js.map +1 -0
- package/dist/revert-QWQWCJJB.js +111 -0
- package/dist/revert-QWQWCJJB.js.map +1 -0
- package/dist/review-7CAVLD67.js +164 -0
- package/dist/review-7CAVLD67.js.map +1 -0
- package/dist/rollback-suggest-C6D5YFCA.js +79 -0
- package/dist/rollback-suggest-C6D5YFCA.js.map +1 -0
- package/dist/safer-alternative-QR4QEFUV.js +84 -0
- package/dist/safer-alternative-QR4QEFUV.js.map +1 -0
- package/dist/safety-OFWUFLK4.js +165 -0
- package/dist/safety-OFWUFLK4.js.map +1 -0
- package/dist/savings-MEBE4TXI.js +95 -0
- package/dist/savings-MEBE4TXI.js.map +1 -0
- package/dist/scan-secrets-XCUBMLHL.js +54 -0
- package/dist/scan-secrets-XCUBMLHL.js.map +1 -0
- package/dist/schema-7JZIG6QR.js +447 -0
- package/dist/schema-7JZIG6QR.js.map +1 -0
- package/dist/script-BMYVBHFR.js +167 -0
- package/dist/script-BMYVBHFR.js.map +1 -0
- package/dist/search-TA3C3AZT.js +151 -0
- package/dist/search-TA3C3AZT.js.map +1 -0
- package/dist/seed-W4Q3L2IU.js +101 -0
- package/dist/seed-W4Q3L2IU.js.map +1 -0
- package/dist/sketch-6B2V6FJV.js +83 -0
- package/dist/sketch-6B2V6FJV.js.map +1 -0
- package/dist/snapshot-YMVS322L.js +171 -0
- package/dist/snapshot-YMVS322L.js.map +1 -0
- package/dist/snippets-EVTN63OU.js +74 -0
- package/dist/snippets-EVTN63OU.js.map +1 -0
- package/dist/standards-FGJW3CQL.js +238 -0
- package/dist/standards-FGJW3CQL.js.map +1 -0
- package/dist/suggest-V3LVIFZ5.js +44 -0
- package/dist/suggest-V3LVIFZ5.js.map +1 -0
- package/dist/suggest-constraints-EX2FCWOQ.js +154 -0
- package/dist/suggest-constraints-EX2FCWOQ.js.map +1 -0
- package/dist/suite-YTQ3CNX5.js +85 -0
- package/dist/suite-YTQ3CNX5.js.map +1 -0
- package/dist/telemetry-KOIY3NEQ.js +90 -0
- package/dist/telemetry-KOIY3NEQ.js.map +1 -0
- package/dist/template-MUJ6X6LN.js +396 -0
- package/dist/template-MUJ6X6LN.js.map +1 -0
- package/dist/test-XFSQHR2S.js +169 -0
- package/dist/test-XFSQHR2S.js.map +1 -0
- package/dist/trial-GFTGYCR3.js +31 -0
- package/dist/trial-GFTGYCR3.js.map +1 -0
- package/dist/validate-LFDEZFFH.js +107 -0
- package/dist/validate-LFDEZFFH.js.map +1 -0
- package/dist/verify-KRDYOJCR.js +76 -0
- package/dist/verify-KRDYOJCR.js.map +1 -0
- package/dist/watch-FSG23RR3.js +80 -0
- package/dist/watch-FSG23RR3.js.map +1 -0
- package/dist/xcompare-U4TXTTIR.js +87 -0
- package/dist/xcompare-U4TXTTIR.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli.cjs +0 -19298
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.ts +0 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/profile.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { profile, createConnection, getProfile } from "@ddt-tools/core";
|
|
8
|
+
function profileCommand() {
|
|
9
|
+
const cmd = new Command("profile");
|
|
10
|
+
cmd.description(
|
|
11
|
+
"Compute per-column profile statistics (nulls, distinct, min/max, top values) for a table. Rows are read from a JSON file in v1 (live-warehouse executeRows wiring is a documented follow-up)."
|
|
12
|
+
).argument(
|
|
13
|
+
"<fqn>",
|
|
14
|
+
"Fully-qualified table name to embed in the profile output (CATALOG.SCHEMA.TABLE)."
|
|
15
|
+
).option(
|
|
16
|
+
"--rows <path>",
|
|
17
|
+
"JSON file containing the rows to profile (TableRow[]). Mutually exclusive with --live."
|
|
18
|
+
).option(
|
|
19
|
+
"--live <profile>",
|
|
20
|
+
"Connection profile name to fetch rows live from Databricks via SELECT * FROM <fqn>. Mutually exclusive with --rows."
|
|
21
|
+
).option(
|
|
22
|
+
"--row-limit <n>",
|
|
23
|
+
"Cap rows fetched in live mode. Default 50000. Use 0 for unbounded (DANGER on prod tables).",
|
|
24
|
+
"50000"
|
|
25
|
+
).option(
|
|
26
|
+
"--columns <list>",
|
|
27
|
+
"Comma-separated column list. Defaults to every key seen in the rows."
|
|
28
|
+
).option("--top-n <n>", "Cap the top-values list per column. Default 10.", "10").option(
|
|
29
|
+
"--sampled",
|
|
30
|
+
"Mark the row set as sampled (caller-supplied sample, not the full table).",
|
|
31
|
+
false
|
|
32
|
+
).option(
|
|
33
|
+
"--sample-strategy <label>",
|
|
34
|
+
'Free-form sampling label (e.g. "TABLESAMPLE(1 PERCENT)"). Default "FULL".'
|
|
35
|
+
).option("--population-rows <n>", "When --sampled, the full population row count for context.").option("--format <fmt>", "Output format: json | markdown. Default json.", "json").option("-o, --output <path>", "Write the profile to a file instead of stdout.").action(async (fqn, opts) => {
|
|
36
|
+
await runProfile(fqn, opts);
|
|
37
|
+
});
|
|
38
|
+
return cmd;
|
|
39
|
+
}
|
|
40
|
+
async function runProfile(fqn, opts) {
|
|
41
|
+
const rowsPath = opts.rows;
|
|
42
|
+
const liveProfile = opts.live;
|
|
43
|
+
if (Boolean(rowsPath) === Boolean(liveProfile)) {
|
|
44
|
+
throw new Error("Specify exactly one of --rows or --live.");
|
|
45
|
+
}
|
|
46
|
+
const rows = liveProfile ? await fetchLiveRows(liveProfile, fqn, Number(opts.rowLimit ?? "50000")) : await readRowsFile(String(rowsPath));
|
|
47
|
+
const columns = opts.columns ? String(opts.columns).split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
48
|
+
const topN = Number(opts.topN ?? "10") || 10;
|
|
49
|
+
const sampled = opts.sampled === true;
|
|
50
|
+
const populationRowCount = opts.populationRows ? Number(opts.populationRows) : void 0;
|
|
51
|
+
const sampleStrategy = opts.sampleStrategy ? String(opts.sampleStrategy) : void 0;
|
|
52
|
+
const tableProfile = profile.analyzeRows(fqn, rows, {
|
|
53
|
+
...columns ? { columns } : {},
|
|
54
|
+
topN,
|
|
55
|
+
sampled,
|
|
56
|
+
...populationRowCount !== void 0 ? { populationRowCount } : {},
|
|
57
|
+
...sampleStrategy ? { sampleStrategy } : {}
|
|
58
|
+
});
|
|
59
|
+
const format = String(opts.format ?? "json").toLowerCase();
|
|
60
|
+
const output = format === "markdown" ? profile.renderProfileMarkdown(tableProfile) : JSON.stringify(tableProfile, null, 2);
|
|
61
|
+
if (opts.output) {
|
|
62
|
+
const outPath = path.resolve(String(opts.output));
|
|
63
|
+
await fs.writeFile(outPath, output, "utf8");
|
|
64
|
+
console.error(`profile: wrote ${outPath} (columns=${tableProfile.columns.length})`);
|
|
65
|
+
} else {
|
|
66
|
+
process.stdout.write(output + "\n");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function readRowsFile(p) {
|
|
70
|
+
const rowsJson = await fs.readFile(path.resolve(p), "utf8");
|
|
71
|
+
const rows = JSON.parse(rowsJson);
|
|
72
|
+
if (!Array.isArray(rows)) {
|
|
73
|
+
throw new Error(`--rows file must contain a TableRow[] (JSON array). Got: ${typeof rows}.`);
|
|
74
|
+
}
|
|
75
|
+
return rows;
|
|
76
|
+
}
|
|
77
|
+
async function fetchLiveRows(profileName, fqn, rowLimit) {
|
|
78
|
+
const limit = rowLimit > 0 ? ` LIMIT ${Math.floor(rowLimit)}` : "";
|
|
79
|
+
const sql = `SELECT * FROM ${fqn}${limit}`;
|
|
80
|
+
const cp = await getProfile(profileName);
|
|
81
|
+
const conn = createConnection(cp);
|
|
82
|
+
console.error(`profile: connecting to ${cp.auth.host}\u2026`);
|
|
83
|
+
await conn.connect();
|
|
84
|
+
try {
|
|
85
|
+
const rows = await conn.executeRows(sql);
|
|
86
|
+
console.error(
|
|
87
|
+
`profile: fetched ${rows.length} row(s) from ${fqn}${rowLimit > 0 && rows.length === rowLimit ? " (LIMIT reached \u2014 increase --row-limit for full-table profile)" : ""}.`
|
|
88
|
+
);
|
|
89
|
+
return rows;
|
|
90
|
+
} finally {
|
|
91
|
+
await conn.disconnect().catch(() => void 0);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
profileCommand,
|
|
96
|
+
runProfile
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=profile-SQTBNKYS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/profile.ts"],"sourcesContent":["/**\n * `ddt profile <fqn>` — item 4 of the constraint-enforcement initiative.\n *\n * Reads rows from either:\n * - a JSON file (`--rows`), or\n * - a live warehouse (`--live <profile>`) via `DatabricksConnection.executeRows`.\n *\n * Live mode runs `SELECT * FROM <fqn> LIMIT --row-limit` via the SQL\n * Statement Execution API. Composes with `ddt suggest-constraints`.\n *\n * Mirrors `sdt profile`.\n */\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { Command } from 'commander';\nimport { profile, createConnection, getProfile } from '@ddt-tools/core';\n\nexport function profileCommand(): Command {\n const cmd = new Command('profile');\n cmd\n .description(\n 'Compute per-column profile statistics (nulls, distinct, min/max, top values) for a table. ' +\n 'Rows are read from a JSON file in v1 (live-warehouse executeRows wiring is a documented follow-up).',\n )\n .argument(\n '<fqn>',\n 'Fully-qualified table name to embed in the profile output (CATALOG.SCHEMA.TABLE).',\n )\n .option(\n '--rows <path>',\n 'JSON file containing the rows to profile (TableRow[]). Mutually exclusive with --live.',\n )\n .option(\n '--live <profile>',\n 'Connection profile name to fetch rows live from Databricks via SELECT * FROM <fqn>. Mutually exclusive with --rows.',\n )\n .option(\n '--row-limit <n>',\n 'Cap rows fetched in live mode. Default 50000. Use 0 for unbounded (DANGER on prod tables).',\n '50000',\n )\n .option(\n '--columns <list>',\n 'Comma-separated column list. Defaults to every key seen in the rows.',\n )\n .option('--top-n <n>', 'Cap the top-values list per column. Default 10.', '10')\n .option(\n '--sampled',\n 'Mark the row set as sampled (caller-supplied sample, not the full table).',\n false,\n )\n .option(\n '--sample-strategy <label>',\n 'Free-form sampling label (e.g. \"TABLESAMPLE(1 PERCENT)\"). Default \"FULL\".',\n )\n .option('--population-rows <n>', 'When --sampled, the full population row count for context.')\n .option('--format <fmt>', 'Output format: json | markdown. Default json.', 'json')\n .option('-o, --output <path>', 'Write the profile to a file instead of stdout.')\n .action(async (fqn: string, opts: Record<string, unknown>) => {\n await runProfile(fqn, opts);\n });\n return cmd;\n}\n\nexport async function runProfile(fqn: string, opts: Record<string, unknown>): Promise<void> {\n const rowsPath = opts.rows as string | undefined;\n const liveProfile = opts.live as string | undefined;\n if (Boolean(rowsPath) === Boolean(liveProfile)) {\n throw new Error('Specify exactly one of --rows or --live.');\n }\n const rows: profile.TableRow[] = liveProfile\n ? await fetchLiveRows(liveProfile, fqn, Number(opts.rowLimit ?? '50000'))\n : await readRowsFile(String(rowsPath));\n const columns = opts.columns\n ? String(opts.columns)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n : undefined;\n const topN = Number(opts.topN ?? '10') || 10;\n const sampled = opts.sampled === true;\n const populationRowCount = opts.populationRows ? Number(opts.populationRows) : undefined;\n const sampleStrategy = opts.sampleStrategy ? String(opts.sampleStrategy) : undefined;\n\n const tableProfile = profile.analyzeRows(fqn, rows, {\n ...(columns ? { columns } : {}),\n topN,\n sampled,\n ...(populationRowCount !== undefined ? { populationRowCount } : {}),\n ...(sampleStrategy ? { sampleStrategy } : {}),\n });\n\n const format = String(opts.format ?? 'json').toLowerCase();\n const output =\n format === 'markdown'\n ? profile.renderProfileMarkdown(tableProfile)\n : JSON.stringify(tableProfile, null, 2);\n\n if (opts.output) {\n const outPath = path.resolve(String(opts.output));\n await fs.writeFile(outPath, output, 'utf8');\n console.error(`profile: wrote ${outPath} (columns=${tableProfile.columns.length})`);\n } else {\n process.stdout.write(output + '\\n');\n }\n}\n\nasync function readRowsFile(p: string): Promise<profile.TableRow[]> {\n const rowsJson = await fs.readFile(path.resolve(p), 'utf8');\n const rows = JSON.parse(rowsJson) as profile.TableRow[];\n if (!Array.isArray(rows)) {\n throw new Error(`--rows file must contain a TableRow[] (JSON array). Got: ${typeof rows}.`);\n }\n return rows;\n}\n\nasync function fetchLiveRows(\n profileName: string,\n fqn: string,\n rowLimit: number,\n): Promise<profile.TableRow[]> {\n const limit = rowLimit > 0 ? ` LIMIT ${Math.floor(rowLimit)}` : '';\n const sql = `SELECT * FROM ${fqn}${limit}`;\n const cp = await getProfile(profileName);\n const conn = createConnection(cp);\n console.error(`profile: connecting to ${cp.auth.host}…`);\n await conn.connect();\n try {\n const rows = await conn.executeRows(sql);\n console.error(\n `profile: fetched ${rows.length} row(s) from ${fqn}${rowLimit > 0 && rows.length === rowLimit ? ' (LIMIT reached — increase --row-limit for full-table profile)' : ''}.`,\n );\n return rows as profile.TableRow[];\n } finally {\n await conn.disconnect().catch(() => undefined);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import "./chunk-DGUM43GV.js";
|
|
2
|
+
|
|
3
|
+
// src/commands/promote.ts
|
|
4
|
+
import { execFile } from "child_process";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import os from "os";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import {
|
|
11
|
+
CompareEngine,
|
|
12
|
+
ScriptGenerator,
|
|
13
|
+
InMemorySource,
|
|
14
|
+
AccountExtractor,
|
|
15
|
+
createConnection,
|
|
16
|
+
defaultExtractors,
|
|
17
|
+
getProfile,
|
|
18
|
+
safety,
|
|
19
|
+
diffPromotionEnvConfigs,
|
|
20
|
+
renderEnvConfigDiffMarkdown,
|
|
21
|
+
parsePromotionProfile,
|
|
22
|
+
filterApprovalsByProfile,
|
|
23
|
+
evaluatePromotionGate,
|
|
24
|
+
summarizePromotionPlan
|
|
25
|
+
} from "@ddt-tools/core";
|
|
26
|
+
var execFileP = promisify(execFile);
|
|
27
|
+
function promoteCommand() {
|
|
28
|
+
const cmd = new Command("promote");
|
|
29
|
+
cmd.description(
|
|
30
|
+
"Branch-per-env deploy: compare live source\u2192target, optionally open a PR with the deploy bundle."
|
|
31
|
+
).option(
|
|
32
|
+
"--from <profile>",
|
|
33
|
+
"Source environment connection profile (e.g. dev). Required for root action."
|
|
34
|
+
).option(
|
|
35
|
+
"--to <profile>",
|
|
36
|
+
"Target environment connection profile (e.g. stage). Required for root action."
|
|
37
|
+
).option("--catalog <catalog>", "Limit to a single catalog.").option("--schema <schema>", "Limit to a single schema (requires --catalog).").option(
|
|
38
|
+
"--from-profile <path>",
|
|
39
|
+
"Path to source promotion profile JSON (parsed via parsePromotionProfile). Defaults to `.ddt/profiles/<from>.json` when present. PROMOTE.1+.3 gate wireup."
|
|
40
|
+
).option(
|
|
41
|
+
"--to-profile <path>",
|
|
42
|
+
"Path to target promotion profile JSON (parsed via parsePromotionProfile). Defaults to `.ddt/profiles/<to>.json` when present. PROMOTE.1+.3 gate wireup."
|
|
43
|
+
).option(
|
|
44
|
+
"--approve <approver>",
|
|
45
|
+
"Record an approval from <approver> (repeatable). Counts toward target.requiredApprovals; filtered by approver whitelist if the target profile declares one.",
|
|
46
|
+
collectRepeatable,
|
|
47
|
+
[]
|
|
48
|
+
).option(
|
|
49
|
+
"--reject <approver>",
|
|
50
|
+
"Record an explicit rejection from <approver> (repeatable). Any single rejection blocks regardless of approvals count; filtered by approver whitelist.",
|
|
51
|
+
collectRepeatable,
|
|
52
|
+
[]
|
|
53
|
+
).option(
|
|
54
|
+
"--force",
|
|
55
|
+
"Bypass safety-classifier blockers on non-production targets. NEVER bypasses EXPLICIT_REJECTION; refused outright when target.isProduction is true.",
|
|
56
|
+
false
|
|
57
|
+
).option(
|
|
58
|
+
"--open-pr",
|
|
59
|
+
"Open a GitHub PR with the deploy bundle. Requires `gh` CLI + --repo.",
|
|
60
|
+
false
|
|
61
|
+
).option("--repo <slug>", "GitHub repo slug (owner/repo) \u2014 required with --open-pr.").option("--base <branch>", "PR base branch. Default: main.", "main").option("--branch <name>", "PR head branch name. Default: ddt-promote/<timestamp>.").option("-o, --output <path>", "Write the deploy bundle to <path> (markdown). Default: stdout.").action(async (opts) => {
|
|
62
|
+
if (!opts.from || !opts.to) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"required options '--from <profile>' and '--to <profile>' must be specified"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const fromProfile = await getProfile(String(opts.from));
|
|
68
|
+
const toProfile = await getProfile(String(opts.to));
|
|
69
|
+
const scope = {
|
|
70
|
+
...opts.catalog ? { catalog: String(opts.catalog) } : {},
|
|
71
|
+
...opts.schema ? { schema: String(opts.schema) } : {}
|
|
72
|
+
};
|
|
73
|
+
const extract = async (profile, label) => {
|
|
74
|
+
const conn = createConnection(profile);
|
|
75
|
+
try {
|
|
76
|
+
await conn.connect();
|
|
77
|
+
const account = new AccountExtractor(defaultExtractors());
|
|
78
|
+
const objs = await account.extract(conn, scope);
|
|
79
|
+
return new InMemorySource(objs, { kind: "live", label });
|
|
80
|
+
} finally {
|
|
81
|
+
await conn.disconnect();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const fromSource = await extract(fromProfile, `from:${fromProfile.auth.host}`);
|
|
85
|
+
const toSource = await extract(toProfile, `to:${toProfile.auth.host}`);
|
|
86
|
+
const engine = new CompareEngine();
|
|
87
|
+
const result = await engine.compare(fromSource, toSource);
|
|
88
|
+
const totalChanges = result.summary.added + result.summary.removed + result.summary.modified;
|
|
89
|
+
if (totalChanges === 0) {
|
|
90
|
+
console.log(
|
|
91
|
+
`Nothing to promote: ${String(opts.from)} \u2192 ${String(opts.to)} is already in sync.`
|
|
92
|
+
);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const generator = new ScriptGenerator();
|
|
96
|
+
const script = generator.generate(result, {});
|
|
97
|
+
const safetyAssessment = safety.assess(result);
|
|
98
|
+
const fromPromotionProfile = await loadPromotionProfileForCli(
|
|
99
|
+
opts.fromProfile ? String(opts.fromProfile) : void 0,
|
|
100
|
+
String(opts.from),
|
|
101
|
+
".ddt",
|
|
102
|
+
"--from-profile"
|
|
103
|
+
);
|
|
104
|
+
const toPromotionProfile = await loadPromotionProfileForCli(
|
|
105
|
+
opts.toProfile ? String(opts.toProfile) : void 0,
|
|
106
|
+
String(opts.to),
|
|
107
|
+
".ddt",
|
|
108
|
+
"--to-profile"
|
|
109
|
+
);
|
|
110
|
+
const gate = evaluatePromoteCliGate({
|
|
111
|
+
from: String(opts.from),
|
|
112
|
+
to: String(opts.to),
|
|
113
|
+
fromProfile: fromPromotionProfile,
|
|
114
|
+
toProfile: toPromotionProfile,
|
|
115
|
+
approvers: parseApproverList(opts.approve),
|
|
116
|
+
rejections: parseRejectionList(opts.reject),
|
|
117
|
+
force: opts.force === true,
|
|
118
|
+
safety: safetyAssessment
|
|
119
|
+
});
|
|
120
|
+
if (!gate.decision.proceed) {
|
|
121
|
+
process.stderr.write(`${gate.summary.headline}
|
|
122
|
+
`);
|
|
123
|
+
process.stderr.write(renderPromoteGateMarkdown(gate));
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (gate.decision.warnings.length > 0) {
|
|
128
|
+
process.stderr.write(`${gate.summary.headline}
|
|
129
|
+
`);
|
|
130
|
+
for (const w of gate.decision.warnings) {
|
|
131
|
+
process.stderr.write(` warning: ${w.code} \u2014 ${w.message}
|
|
132
|
+
`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const lines = [];
|
|
136
|
+
lines.push(`# Promote: ${String(opts.from)} \u2192 ${String(opts.to)}`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push(`Generated by \`ddt promote\` at ${(/* @__PURE__ */ new Date()).toISOString()}.`);
|
|
139
|
+
lines.push("");
|
|
140
|
+
lines.push(renderPromoteGateMarkdown(gate));
|
|
141
|
+
lines.push("## Diff summary");
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push(`- **Added**: ${result.summary.added}`);
|
|
144
|
+
lines.push(`- **Removed**: ${result.summary.removed}`);
|
|
145
|
+
lines.push(`- **Modified**: ${result.summary.modified}`);
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push("## Safety");
|
|
148
|
+
lines.push("");
|
|
149
|
+
const groups = safety.groupByReversibility(safetyAssessment);
|
|
150
|
+
lines.push(safety.formatReversibilityBuckets(groups));
|
|
151
|
+
lines.push("");
|
|
152
|
+
lines.push("## Migration script");
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push("```sql");
|
|
155
|
+
lines.push(script.sql);
|
|
156
|
+
lines.push("```");
|
|
157
|
+
const bundle = lines.join("\n");
|
|
158
|
+
if (opts.output) {
|
|
159
|
+
await fs.writeFile(String(opts.output), bundle, "utf8");
|
|
160
|
+
console.log(`Wrote deploy bundle \u2192 ${String(opts.output)}`);
|
|
161
|
+
} else if (!opts.openPr) {
|
|
162
|
+
console.log(bundle);
|
|
163
|
+
}
|
|
164
|
+
if (opts.openPr) {
|
|
165
|
+
if (!opts.repo) throw new Error("--repo <owner/repo> is required with --open-pr");
|
|
166
|
+
const branchName = String(
|
|
167
|
+
opts.branch ?? `ddt-promote/${String(opts.from)}-to-${String(opts.to)}-${Date.now()}`
|
|
168
|
+
);
|
|
169
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "ddt-promote-"));
|
|
170
|
+
const bodyPath = path.join(tmpDir, "pr-body.md");
|
|
171
|
+
await fs.writeFile(bodyPath, bundle, "utf8");
|
|
172
|
+
const totalChanges2 = result.summary.added + result.summary.removed + result.summary.modified;
|
|
173
|
+
const title = `Promote: ${String(opts.from)} \u2192 ${String(opts.to)} (${totalChanges2} changes)`;
|
|
174
|
+
const args = [
|
|
175
|
+
"pr",
|
|
176
|
+
"create",
|
|
177
|
+
"--repo",
|
|
178
|
+
String(opts.repo),
|
|
179
|
+
"--base",
|
|
180
|
+
String(opts.base),
|
|
181
|
+
"--head",
|
|
182
|
+
branchName,
|
|
183
|
+
"--title",
|
|
184
|
+
title,
|
|
185
|
+
"--body-file",
|
|
186
|
+
bodyPath
|
|
187
|
+
];
|
|
188
|
+
try {
|
|
189
|
+
const { stdout } = await execFileP("gh", args);
|
|
190
|
+
console.log(stdout.trim());
|
|
191
|
+
} catch (err) {
|
|
192
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
193
|
+
throw new Error(
|
|
194
|
+
`gh pr create failed: ${msg}
|
|
195
|
+
Manual workaround: the bundle is at ${bodyPath}; paste it into a PR by hand.`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
cmd.command("diff").description(
|
|
201
|
+
"Diff two promotion-environment profile JSON files (PROMOTE.2 / PROMOTE.3 wireup). Pure file I/O \u2014 never compares secret values, only secret keys."
|
|
202
|
+
).requiredOption(
|
|
203
|
+
"--source <path>",
|
|
204
|
+
"Path to the source .ddt/profiles/<env>.json (parsed via parsePromotionProfile)."
|
|
205
|
+
).requiredOption("--target <path>", "Path to the target profile JSON.").option("--format <fmt>", "Output format: markdown (default) | json", "markdown").action(async (opts) => {
|
|
206
|
+
const format = String(opts.format ?? "markdown").toLowerCase();
|
|
207
|
+
if (format !== "markdown" && format !== "json") {
|
|
208
|
+
process.stderr.write(
|
|
209
|
+
`Invalid --format ${JSON.stringify(opts.format)}; expected "markdown" or "json".
|
|
210
|
+
`
|
|
211
|
+
);
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const sourcePath = path.resolve(String(opts.source));
|
|
216
|
+
const targetPath = path.resolve(String(opts.target));
|
|
217
|
+
const sourceText = await fs.readFile(sourcePath, "utf8");
|
|
218
|
+
const targetText = await fs.readFile(targetPath, "utf8");
|
|
219
|
+
let sourceJson;
|
|
220
|
+
let targetJson;
|
|
221
|
+
try {
|
|
222
|
+
sourceJson = JSON.parse(sourceText);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
emitDiffError(
|
|
225
|
+
format,
|
|
226
|
+
"PROFILE_PARSE_ERROR",
|
|
227
|
+
`--source is not valid JSON: ${describeErr(err)}`
|
|
228
|
+
);
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
targetJson = JSON.parse(targetText);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
emitDiffError(
|
|
236
|
+
format,
|
|
237
|
+
"PROFILE_PARSE_ERROR",
|
|
238
|
+
`--target is not valid JSON: ${describeErr(err)}`
|
|
239
|
+
);
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const sourceParse = parsePromotionProfile(sourceJson);
|
|
244
|
+
if (!sourceParse.profile) {
|
|
245
|
+
emitDiffError(format, "PROFILE_INVALID", renderParseErrors("--source", sourceParse.errors));
|
|
246
|
+
process.exitCode = 2;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const targetParse = parsePromotionProfile(targetJson);
|
|
250
|
+
if (!targetParse.profile) {
|
|
251
|
+
emitDiffError(format, "PROFILE_INVALID", renderParseErrors("--target", targetParse.errors));
|
|
252
|
+
process.exitCode = 2;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const source = profileToEnvConfig(sourceParse.profile);
|
|
256
|
+
const target = profileToEnvConfig(targetParse.profile);
|
|
257
|
+
const diff = diffPromotionEnvConfigs(source, target);
|
|
258
|
+
if (format === "json") {
|
|
259
|
+
process.stdout.write(JSON.stringify(diff, null, 2) + "\n");
|
|
260
|
+
} else {
|
|
261
|
+
const md = renderEnvConfigDiffMarkdown(diff);
|
|
262
|
+
process.stdout.write(md);
|
|
263
|
+
if (!md.endsWith("\n")) process.stdout.write("\n");
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
return cmd;
|
|
267
|
+
}
|
|
268
|
+
function profileToEnvConfig(profile) {
|
|
269
|
+
return {
|
|
270
|
+
name: profile.name,
|
|
271
|
+
profileName: profile.profileName,
|
|
272
|
+
isProduction: profile.isProduction,
|
|
273
|
+
requiredApprovals: profile.requiredApprovals,
|
|
274
|
+
...profile.variables ? { variables: profile.variables } : {},
|
|
275
|
+
...profile.secretKeys ? { secretKeys: profile.secretKeys } : {},
|
|
276
|
+
...profile.profileFields ? { profileFields: profile.profileFields } : {}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function describeErr(err) {
|
|
280
|
+
return err instanceof Error ? err.message : String(err);
|
|
281
|
+
}
|
|
282
|
+
function renderParseErrors(label, errors) {
|
|
283
|
+
const lines = errors.map((e) => ` - ${e.path}: ${e.message}`);
|
|
284
|
+
return `${label} failed parsePromotionProfile:
|
|
285
|
+
${lines.join("\n")}`;
|
|
286
|
+
}
|
|
287
|
+
function emitDiffError(format, errorCode, message) {
|
|
288
|
+
if (format === "json") {
|
|
289
|
+
process.stdout.write(JSON.stringify({ ok: false, errorCode, message }) + "\n");
|
|
290
|
+
} else {
|
|
291
|
+
process.stderr.write(`promote diff refused: ${errorCode}
|
|
292
|
+
${message}
|
|
293
|
+
`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function collectRepeatable(value, prev) {
|
|
297
|
+
return [...prev, value];
|
|
298
|
+
}
|
|
299
|
+
function parseApproverList(raw) {
|
|
300
|
+
if (!Array.isArray(raw)) return [];
|
|
301
|
+
const out = [];
|
|
302
|
+
for (const v of raw) {
|
|
303
|
+
if (typeof v === "string" && v.length > 0) out.push(v);
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
function parseRejectionList(raw) {
|
|
308
|
+
if (!Array.isArray(raw)) return [];
|
|
309
|
+
const out = [];
|
|
310
|
+
for (const v of raw) {
|
|
311
|
+
if (typeof v !== "string" || v.length === 0) continue;
|
|
312
|
+
out.push({ approver: v });
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
function profileToEnvironment(name, profile) {
|
|
317
|
+
if (profile) {
|
|
318
|
+
return {
|
|
319
|
+
name: profile.name,
|
|
320
|
+
profileName: profile.profileName,
|
|
321
|
+
isProduction: profile.isProduction,
|
|
322
|
+
requiredApprovals: profile.requiredApprovals
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return { name, profileName: name, isProduction: false, requiredApprovals: 0 };
|
|
326
|
+
}
|
|
327
|
+
function evaluatePromoteCliGate(opts) {
|
|
328
|
+
const now = opts.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
329
|
+
const source = profileToEnvironment(opts.from, opts.fromProfile);
|
|
330
|
+
const target = profileToEnvironment(opts.to, opts.toProfile);
|
|
331
|
+
const approvals = opts.approvers.map((approver) => ({ approver, at: now }));
|
|
332
|
+
const rejections = opts.rejections.map((r) => ({
|
|
333
|
+
approver: r.approver,
|
|
334
|
+
at: now,
|
|
335
|
+
...r.reason !== void 0 ? { reason: r.reason } : {}
|
|
336
|
+
}));
|
|
337
|
+
let approvalState = { approvals, rejections };
|
|
338
|
+
if (opts.toProfile) {
|
|
339
|
+
approvalState = filterApprovalsByProfile(opts.toProfile, approvalState);
|
|
340
|
+
}
|
|
341
|
+
const inputs = {
|
|
342
|
+
source,
|
|
343
|
+
target,
|
|
344
|
+
safety: opts.safety,
|
|
345
|
+
approvalState,
|
|
346
|
+
force: opts.force
|
|
347
|
+
};
|
|
348
|
+
const decision = evaluatePromotionGate(inputs);
|
|
349
|
+
const summary = summarizePromotionPlan(inputs);
|
|
350
|
+
return { decision, summary, source, target };
|
|
351
|
+
}
|
|
352
|
+
function renderPromoteGateMarkdown(outcome) {
|
|
353
|
+
const lines = [];
|
|
354
|
+
lines.push("## Promotion gate");
|
|
355
|
+
lines.push("");
|
|
356
|
+
lines.push(`**${outcome.summary.headline}**`);
|
|
357
|
+
lines.push("");
|
|
358
|
+
lines.push(
|
|
359
|
+
`- **Source**: ${outcome.source.name} (profile \`${outcome.source.profileName}\`${outcome.source.isProduction ? ", production" : ""})`
|
|
360
|
+
);
|
|
361
|
+
lines.push(
|
|
362
|
+
`- **Target**: ${outcome.target.name} (profile \`${outcome.target.profileName}\`${outcome.target.isProduction ? ", production" : ""})`
|
|
363
|
+
);
|
|
364
|
+
lines.push(
|
|
365
|
+
`- **Approvals**: ${outcome.summary.approvalsRecorded} recorded / ${outcome.summary.approvalsRequired} required`
|
|
366
|
+
);
|
|
367
|
+
lines.push("");
|
|
368
|
+
if (outcome.decision.blockers.length > 0) {
|
|
369
|
+
lines.push(`### Blockers (${outcome.decision.blockers.length})`);
|
|
370
|
+
lines.push("");
|
|
371
|
+
for (const b of outcome.decision.blockers) {
|
|
372
|
+
lines.push(`- **${b.code}** \u2014 ${b.message}`);
|
|
373
|
+
}
|
|
374
|
+
lines.push("");
|
|
375
|
+
}
|
|
376
|
+
if (outcome.decision.warnings.length > 0) {
|
|
377
|
+
lines.push(`### Warnings (${outcome.decision.warnings.length})`);
|
|
378
|
+
lines.push("");
|
|
379
|
+
for (const w of outcome.decision.warnings) {
|
|
380
|
+
lines.push(`- **${w.code}** \u2014 ${w.message}`);
|
|
381
|
+
}
|
|
382
|
+
lines.push("");
|
|
383
|
+
}
|
|
384
|
+
return lines.join("\n");
|
|
385
|
+
}
|
|
386
|
+
async function loadPromotionProfileForCli(explicitPath, envName, configDir, flagName) {
|
|
387
|
+
const targetPath = explicitPath ? path.resolve(explicitPath) : path.resolve(path.join(configDir, "profiles", `${envName}.json`));
|
|
388
|
+
let text;
|
|
389
|
+
try {
|
|
390
|
+
text = await fs.readFile(targetPath, "utf8");
|
|
391
|
+
} catch (err) {
|
|
392
|
+
if (explicitPath !== void 0) {
|
|
393
|
+
throw new Error(`${flagName} ${targetPath} could not be read: ${describeErr(err)}`);
|
|
394
|
+
}
|
|
395
|
+
return void 0;
|
|
396
|
+
}
|
|
397
|
+
let json;
|
|
398
|
+
try {
|
|
399
|
+
json = JSON.parse(text);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
throw new Error(`${targetPath} is not valid JSON: ${describeErr(err)}`);
|
|
402
|
+
}
|
|
403
|
+
const parsed = parsePromotionProfile(json);
|
|
404
|
+
if (!parsed.profile) {
|
|
405
|
+
throw new Error(renderParseErrors(targetPath, parsed.errors));
|
|
406
|
+
}
|
|
407
|
+
return parsed.profile;
|
|
408
|
+
}
|
|
409
|
+
export {
|
|
410
|
+
evaluatePromoteCliGate,
|
|
411
|
+
loadPromotionProfileForCli,
|
|
412
|
+
parseApproverList,
|
|
413
|
+
parseRejectionList,
|
|
414
|
+
promoteCommand,
|
|
415
|
+
renderPromoteGateMarkdown
|
|
416
|
+
};
|
|
417
|
+
//# sourceMappingURL=promote-FSGUPIPD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/promote.ts"],"sourcesContent":["/**\n * `ddt promote --from <profile> --to <profile>` — branch-per-env deploy\n * automation.\n *\n * Compares two live workspaces (typically dev → stage or stage → prod),\n * generates the migration script + safety report, and either:\n * - prints the bundle to stdout (default), or\n * - opens a pull request with the diff + safety report attached\n * (`--open-pr` + `--repo`). Requires `gh` (GitHub CLI) on PATH.\n *\n * Stage promotion workflow: when a feature is verified in dev, run\n * `ddt promote --from dev --to stage --open-pr`; the bot generates a\n * \"promote ABC → stage\" PR with the SQL preview + safety findings + AI\n * narration so the reviewer can approve without re-running compare.\n */\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { promises as fs } from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport {\n CompareEngine,\n ScriptGenerator,\n InMemorySource,\n AccountExtractor,\n createConnection,\n defaultExtractors,\n getProfile,\n safety,\n diffPromotionEnvConfigs,\n renderEnvConfigDiffMarkdown,\n parsePromotionProfile,\n filterApprovalsByProfile,\n evaluatePromotionGate,\n summarizePromotionPlan,\n type EvaluatePromotionGateInputs,\n type PromotionApprovalState,\n type PromotionEnvironment,\n type PromotionEnvironmentConfig,\n type PromotionGateDecision,\n type PromotionPlanSummary,\n type PromotionProfile,\n} from '@ddt-tools/core';\ntype SafetyAssessment = safety.SafetyAssessment;\n\nconst execFileP = promisify(execFile);\n\nexport function promoteCommand(): Command {\n const cmd = new Command('promote');\n cmd\n .description(\n 'Branch-per-env deploy: compare live source→target, optionally open a PR with the deploy bundle.',\n )\n .option(\n '--from <profile>',\n 'Source environment connection profile (e.g. dev). Required for root action.',\n )\n .option(\n '--to <profile>',\n 'Target environment connection profile (e.g. stage). Required for root action.',\n )\n .option('--catalog <catalog>', 'Limit to a single catalog.')\n .option('--schema <schema>', 'Limit to a single schema (requires --catalog).')\n .option(\n '--from-profile <path>',\n 'Path to source promotion profile JSON (parsed via parsePromotionProfile). ' +\n 'Defaults to `.ddt/profiles/<from>.json` when present. PROMOTE.1+.3 gate wireup.',\n )\n .option(\n '--to-profile <path>',\n 'Path to target promotion profile JSON (parsed via parsePromotionProfile). ' +\n 'Defaults to `.ddt/profiles/<to>.json` when present. PROMOTE.1+.3 gate wireup.',\n )\n .option(\n '--approve <approver>',\n 'Record an approval from <approver> (repeatable). Counts toward target.requiredApprovals; ' +\n 'filtered by approver whitelist if the target profile declares one.',\n collectRepeatable,\n [] as string[],\n )\n .option(\n '--reject <approver>',\n 'Record an explicit rejection from <approver> (repeatable). Any single rejection blocks ' +\n 'regardless of approvals count; filtered by approver whitelist.',\n collectRepeatable,\n [] as string[],\n )\n .option(\n '--force',\n 'Bypass safety-classifier blockers on non-production targets. NEVER bypasses ' +\n 'EXPLICIT_REJECTION; refused outright when target.isProduction is true.',\n false,\n )\n .option(\n '--open-pr',\n 'Open a GitHub PR with the deploy bundle. Requires `gh` CLI + --repo.',\n false,\n )\n .option('--repo <slug>', 'GitHub repo slug (owner/repo) — required with --open-pr.')\n .option('--base <branch>', 'PR base branch. Default: main.', 'main')\n .option('--branch <name>', 'PR head branch name. Default: ddt-promote/<timestamp>.')\n .option('-o, --output <path>', 'Write the deploy bundle to <path> (markdown). Default: stdout.')\n .action(async (opts) => {\n if (!opts.from || !opts.to) {\n throw new Error(\n \"required options '--from <profile>' and '--to <profile>' must be specified\",\n );\n }\n const fromProfile = await getProfile(String(opts.from));\n const toProfile = await getProfile(String(opts.to));\n const scope = {\n ...(opts.catalog ? { catalog: String(opts.catalog) } : {}),\n ...(opts.schema ? { schema: String(opts.schema) } : {}),\n };\n\n // Extract both sides.\n const extract = async (profile: typeof fromProfile, label: string) => {\n const conn = createConnection(profile);\n try {\n await conn.connect();\n const account = new AccountExtractor(defaultExtractors());\n const objs = await account.extract(conn, scope);\n return new InMemorySource(objs, { kind: 'live', label });\n } finally {\n await conn.disconnect();\n }\n };\n const fromSource = await extract(fromProfile, `from:${fromProfile.auth.host}`);\n const toSource = await extract(toProfile, `to:${toProfile.auth.host}`);\n\n // Compare from → to (i.e., apply differences from `from` onto `to`).\n const engine = new CompareEngine();\n const result = await engine.compare(fromSource, toSource);\n\n const totalChanges = result.summary.added + result.summary.removed + result.summary.modified;\n if (totalChanges === 0) {\n console.log(\n `Nothing to promote: ${String(opts.from)} → ${String(opts.to)} is already in sync.`,\n );\n return;\n }\n\n // Generate migration script.\n const generator = new ScriptGenerator();\n const script = generator.generate(result, {});\n const safetyAssessment = safety.assess(result);\n\n const fromPromotionProfile = await loadPromotionProfileForCli(\n opts.fromProfile ? String(opts.fromProfile) : undefined,\n String(opts.from),\n '.ddt',\n '--from-profile',\n );\n const toPromotionProfile = await loadPromotionProfileForCli(\n opts.toProfile ? String(opts.toProfile) : undefined,\n String(opts.to),\n '.ddt',\n '--to-profile',\n );\n\n const gate = evaluatePromoteCliGate({\n from: String(opts.from),\n to: String(opts.to),\n fromProfile: fromPromotionProfile,\n toProfile: toPromotionProfile,\n approvers: parseApproverList(opts.approve),\n rejections: parseRejectionList(opts.reject),\n force: opts.force === true,\n safety: safetyAssessment,\n });\n\n if (!gate.decision.proceed) {\n process.stderr.write(`${gate.summary.headline}\\n`);\n process.stderr.write(renderPromoteGateMarkdown(gate));\n process.exitCode = 1;\n return;\n }\n if (gate.decision.warnings.length > 0) {\n process.stderr.write(`${gate.summary.headline}\\n`);\n for (const w of gate.decision.warnings) {\n process.stderr.write(` warning: ${w.code} — ${w.message}\\n`);\n }\n }\n\n // Build the deploy bundle (markdown).\n const lines: string[] = [];\n lines.push(`# Promote: ${String(opts.from)} → ${String(opts.to)}`);\n lines.push('');\n lines.push(`Generated by \\`ddt promote\\` at ${new Date().toISOString()}.`);\n lines.push('');\n lines.push(renderPromoteGateMarkdown(gate));\n lines.push('## Diff summary');\n lines.push('');\n lines.push(`- **Added**: ${result.summary.added}`);\n lines.push(`- **Removed**: ${result.summary.removed}`);\n lines.push(`- **Modified**: ${result.summary.modified}`);\n lines.push('');\n lines.push('## Safety');\n lines.push('');\n const groups = safety.groupByReversibility(safetyAssessment);\n lines.push(safety.formatReversibilityBuckets(groups));\n lines.push('');\n lines.push('## Migration script');\n lines.push('');\n lines.push('```sql');\n lines.push(script.sql);\n lines.push('```');\n const bundle = lines.join('\\n');\n\n // Emit bundle.\n if (opts.output) {\n await fs.writeFile(String(opts.output), bundle, 'utf8');\n console.log(`Wrote deploy bundle → ${String(opts.output)}`);\n } else if (!opts.openPr) {\n console.log(bundle);\n }\n\n if (opts.openPr) {\n if (!opts.repo) throw new Error('--repo <owner/repo> is required with --open-pr');\n const branchName = String(\n opts.branch ?? `ddt-promote/${String(opts.from)}-to-${String(opts.to)}-${Date.now()}`,\n );\n const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ddt-promote-'));\n const bodyPath = path.join(tmpDir, 'pr-body.md');\n await fs.writeFile(bodyPath, bundle, 'utf8');\n const totalChanges =\n result.summary.added + result.summary.removed + result.summary.modified;\n const title = `Promote: ${String(opts.from)} → ${String(opts.to)} (${totalChanges} changes)`;\n // Open PR via `gh` — leans on the user's auth + branch creation.\n const args = [\n 'pr',\n 'create',\n '--repo',\n String(opts.repo),\n '--base',\n String(opts.base),\n '--head',\n branchName,\n '--title',\n title,\n '--body-file',\n bodyPath,\n ];\n try {\n const { stdout } = await execFileP('gh', args);\n console.log(stdout.trim());\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(\n `gh pr create failed: ${msg}\\n` +\n `Manual workaround: the bundle is at ${bodyPath}; paste it into a PR by hand.`,\n );\n }\n }\n });\n\n cmd\n .command('diff')\n .description(\n 'Diff two promotion-environment profile JSON files (PROMOTE.2 / PROMOTE.3 wireup). ' +\n 'Pure file I/O — never compares secret values, only secret keys.',\n )\n .requiredOption(\n '--source <path>',\n 'Path to the source .ddt/profiles/<env>.json (parsed via parsePromotionProfile).',\n )\n .requiredOption('--target <path>', 'Path to the target profile JSON.')\n .option('--format <fmt>', 'Output format: markdown (default) | json', 'markdown')\n .action(async (opts: { source: string; target: string; format?: string }) => {\n const format = String(opts.format ?? 'markdown').toLowerCase();\n if (format !== 'markdown' && format !== 'json') {\n process.stderr.write(\n `Invalid --format ${JSON.stringify(opts.format)}; expected \"markdown\" or \"json\".\\n`,\n );\n process.exitCode = 1;\n return;\n }\n\n const sourcePath = path.resolve(String(opts.source));\n const targetPath = path.resolve(String(opts.target));\n const sourceText = await fs.readFile(sourcePath, 'utf8');\n const targetText = await fs.readFile(targetPath, 'utf8');\n\n let sourceJson: unknown;\n let targetJson: unknown;\n try {\n sourceJson = JSON.parse(sourceText);\n } catch (err) {\n emitDiffError(\n format,\n 'PROFILE_PARSE_ERROR',\n `--source is not valid JSON: ${describeErr(err)}`,\n );\n process.exitCode = 1;\n return;\n }\n try {\n targetJson = JSON.parse(targetText);\n } catch (err) {\n emitDiffError(\n format,\n 'PROFILE_PARSE_ERROR',\n `--target is not valid JSON: ${describeErr(err)}`,\n );\n process.exitCode = 1;\n return;\n }\n\n const sourceParse = parsePromotionProfile(sourceJson);\n if (!sourceParse.profile) {\n emitDiffError(format, 'PROFILE_INVALID', renderParseErrors('--source', sourceParse.errors));\n process.exitCode = 2;\n return;\n }\n const targetParse = parsePromotionProfile(targetJson);\n if (!targetParse.profile) {\n emitDiffError(format, 'PROFILE_INVALID', renderParseErrors('--target', targetParse.errors));\n process.exitCode = 2;\n return;\n }\n\n const source = profileToEnvConfig(sourceParse.profile);\n const target = profileToEnvConfig(targetParse.profile);\n const diff = diffPromotionEnvConfigs(source, target);\n\n if (format === 'json') {\n process.stdout.write(JSON.stringify(diff, null, 2) + '\\n');\n } else {\n const md = renderEnvConfigDiffMarkdown(diff);\n process.stdout.write(md);\n if (!md.endsWith('\\n')) process.stdout.write('\\n');\n }\n });\n\n return cmd;\n}\n\nfunction profileToEnvConfig(profile: PromotionProfile): PromotionEnvironmentConfig {\n return {\n name: profile.name,\n profileName: profile.profileName,\n isProduction: profile.isProduction,\n requiredApprovals: profile.requiredApprovals,\n ...(profile.variables ? { variables: profile.variables } : {}),\n ...(profile.secretKeys ? { secretKeys: profile.secretKeys } : {}),\n ...(profile.profileFields ? { profileFields: profile.profileFields } : {}),\n };\n}\n\nfunction describeErr(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nfunction renderParseErrors(\n label: string,\n errors: readonly { path: string; message: string }[],\n): string {\n const lines = errors.map((e) => ` - ${e.path}: ${e.message}`);\n return `${label} failed parsePromotionProfile:\\n${lines.join('\\n')}`;\n}\n\nfunction emitDiffError(format: string, errorCode: string, message: string): void {\n if (format === 'json') {\n process.stdout.write(JSON.stringify({ ok: false, errorCode, message }) + '\\n');\n } else {\n process.stderr.write(`promote diff refused: ${errorCode}\\n${message}\\n`);\n }\n}\n\nfunction collectRepeatable(value: string, prev: string[]): string[] {\n return [...prev, value];\n}\n\nexport function parseApproverList(raw: unknown): string[] {\n if (!Array.isArray(raw)) return [];\n const out: string[] = [];\n for (const v of raw) {\n if (typeof v === 'string' && v.length > 0) out.push(v);\n }\n return out;\n}\n\nexport interface PromoteCliRejection {\n readonly approver: string;\n readonly reason?: string;\n}\n\nexport function parseRejectionList(raw: unknown): PromoteCliRejection[] {\n if (!Array.isArray(raw)) return [];\n const out: PromoteCliRejection[] = [];\n for (const v of raw) {\n if (typeof v !== 'string' || v.length === 0) continue;\n out.push({ approver: v });\n }\n return out;\n}\n\n/**\n * PROMOTE.1 + PROMOTE.3 gate inputs for the CLI orchestrator. Pure data\n * — file I/O happens in the caller, this helper composes the substrate.\n */\nexport interface PromoteCliGateOptions {\n readonly from: string;\n readonly to: string;\n readonly fromProfile?: PromotionProfile;\n readonly toProfile?: PromotionProfile;\n readonly approvers: readonly string[];\n readonly rejections: readonly PromoteCliRejection[];\n readonly force: boolean;\n readonly safety: SafetyAssessment;\n readonly now?: string;\n}\n\nexport interface PromoteCliGateOutcome {\n readonly decision: PromotionGateDecision;\n readonly summary: PromotionPlanSummary;\n readonly source: PromotionEnvironment;\n readonly target: PromotionEnvironment;\n}\n\nfunction profileToEnvironment(\n name: string,\n profile: PromotionProfile | undefined,\n): PromotionEnvironment {\n if (profile) {\n return {\n name: profile.name,\n profileName: profile.profileName,\n isProduction: profile.isProduction,\n requiredApprovals: profile.requiredApprovals,\n };\n }\n return { name, profileName: name, isProduction: false, requiredApprovals: 0 };\n}\n\n/**\n * Pure composition of parsePromotionProfile + filterApprovalsByProfile +\n * evaluatePromotionGate + summarizePromotionPlan. Unit-testable without\n * a live warehouse connection — pin tests cover production --force\n * refusal, EXPLICIT_REJECTION via --reject, INSUFFICIENT_APPROVALS,\n * approver-whitelist filtering, and warnings-only success.\n */\nexport function evaluatePromoteCliGate(opts: PromoteCliGateOptions): PromoteCliGateOutcome {\n const now = opts.now ?? new Date().toISOString();\n const source = profileToEnvironment(opts.from, opts.fromProfile);\n const target = profileToEnvironment(opts.to, opts.toProfile);\n const approvals = opts.approvers.map((approver) => ({ approver, at: now }));\n const rejections = opts.rejections.map((r) => ({\n approver: r.approver,\n at: now,\n ...(r.reason !== undefined ? { reason: r.reason } : {}),\n }));\n let approvalState: PromotionApprovalState = { approvals, rejections };\n if (opts.toProfile) {\n approvalState = filterApprovalsByProfile(opts.toProfile, approvalState);\n }\n const inputs: EvaluatePromotionGateInputs = {\n source,\n target,\n safety: opts.safety,\n approvalState,\n force: opts.force,\n };\n const decision = evaluatePromotionGate(inputs);\n const summary = summarizePromotionPlan(inputs);\n return { decision, summary, source, target };\n}\n\n/** Markdown rendering of a promotion-gate outcome — embedded in the\n * deploy bundle (so the PR body carries the decision) and printed to\n * stderr on refusal. */\nexport function renderPromoteGateMarkdown(outcome: PromoteCliGateOutcome): string {\n const lines: string[] = [];\n lines.push('## Promotion gate');\n lines.push('');\n lines.push(`**${outcome.summary.headline}**`);\n lines.push('');\n lines.push(\n `- **Source**: ${outcome.source.name} (profile \\`${outcome.source.profileName}\\`${outcome.source.isProduction ? ', production' : ''})`,\n );\n lines.push(\n `- **Target**: ${outcome.target.name} (profile \\`${outcome.target.profileName}\\`${outcome.target.isProduction ? ', production' : ''})`,\n );\n lines.push(\n `- **Approvals**: ${outcome.summary.approvalsRecorded} recorded / ${outcome.summary.approvalsRequired} required`,\n );\n lines.push('');\n if (outcome.decision.blockers.length > 0) {\n lines.push(`### Blockers (${outcome.decision.blockers.length})`);\n lines.push('');\n for (const b of outcome.decision.blockers) {\n lines.push(`- **${b.code}** — ${b.message}`);\n }\n lines.push('');\n }\n if (outcome.decision.warnings.length > 0) {\n lines.push(`### Warnings (${outcome.decision.warnings.length})`);\n lines.push('');\n for (const w of outcome.decision.warnings) {\n lines.push(`- **${w.code}** — ${w.message}`);\n }\n lines.push('');\n }\n return lines.join('\\n');\n}\n\n/**\n * Load a promotion profile from disk for the CLI gate. Behavior:\n * - If `explicitPath` is set, the file MUST exist + parse cleanly.\n * Missing / malformed → throws with a precise error.\n * - If `explicitPath` is undefined, tries the default well-known path\n * `<configDir>/profiles/<envName>.json`. Missing file → returns\n * `undefined` silently (no profile mode, gate runs with defaults).\n * Malformed → throws (so a typo is not silently ignored).\n */\nexport async function loadPromotionProfileForCli(\n explicitPath: string | undefined,\n envName: string,\n configDir: string,\n flagName: string,\n): Promise<PromotionProfile | undefined> {\n const targetPath = explicitPath\n ? path.resolve(explicitPath)\n : path.resolve(path.join(configDir, 'profiles', `${envName}.json`));\n let text: string;\n try {\n text = await fs.readFile(targetPath, 'utf8');\n } catch (err) {\n if (explicitPath !== undefined) {\n throw new Error(`${flagName} ${targetPath} could not be read: ${describeErr(err)}`);\n }\n return undefined; // silent fallback for default well-known path\n }\n let json: unknown;\n try {\n json = JSON.parse(text);\n } catch (err) {\n throw new Error(`${targetPath} is not valid JSON: ${describeErr(err)}`);\n }\n const parsed = parsePromotionProfile(json);\n if (!parsed.profile) {\n throw new Error(renderParseErrors(targetPath, parsed.errors));\n }\n return parsed.profile;\n}\n"],"mappings":"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","names":["totalChanges"]}
|