@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,739 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachRelatedOptions
|
|
3
|
+
} from "./chunk-DL3V7UJ2.js";
|
|
4
|
+
import {
|
|
5
|
+
addMappingFlags,
|
|
6
|
+
buildMappingFromOptions,
|
|
7
|
+
describeMapping
|
|
8
|
+
} from "./chunk-2FT6HXKS.js";
|
|
9
|
+
import "./chunk-DGUM43GV.js";
|
|
10
|
+
|
|
11
|
+
// src/commands/publish.ts
|
|
12
|
+
import { Command } from "commander";
|
|
13
|
+
import { promises as fs } from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import {
|
|
16
|
+
CompareEngine,
|
|
17
|
+
DatabricksExecutor,
|
|
18
|
+
PacSource,
|
|
19
|
+
ProjectSource,
|
|
20
|
+
ScriptGenerator,
|
|
21
|
+
ai,
|
|
22
|
+
aiPreflight,
|
|
23
|
+
approval,
|
|
24
|
+
buildDeployManifest,
|
|
25
|
+
buildExecutionPlan,
|
|
26
|
+
colorizeMigrationScript,
|
|
27
|
+
compileSlice,
|
|
28
|
+
createConnection,
|
|
29
|
+
defaultSafePlan,
|
|
30
|
+
executePlan,
|
|
31
|
+
getProfile,
|
|
32
|
+
lintSql,
|
|
33
|
+
loadProject,
|
|
34
|
+
mergeDeployOptions,
|
|
35
|
+
pac as pacNs,
|
|
36
|
+
pacFreshness,
|
|
37
|
+
protectedObjects,
|
|
38
|
+
queryExecution,
|
|
39
|
+
recordDeployChangelog,
|
|
40
|
+
renderHtmlReport,
|
|
41
|
+
resolveProfile,
|
|
42
|
+
safety,
|
|
43
|
+
testFramework,
|
|
44
|
+
validatePlan
|
|
45
|
+
} from "@ddt-tools/core";
|
|
46
|
+
function publishCommand() {
|
|
47
|
+
const cmd = new Command("publish");
|
|
48
|
+
cmd.description("Generate (and optionally apply) a migration script from source \u2192 target.").option(
|
|
49
|
+
"--source <path>",
|
|
50
|
+
"Source: .ddtproj or .ddtpac (the desired state). Required for a normal publish; omit only with --restore-from-snapshot."
|
|
51
|
+
).option(
|
|
52
|
+
"--target <path>",
|
|
53
|
+
"Target: .ddtproj or .ddtpac (the current state). Required for a normal publish; omit only with --restore-from-snapshot."
|
|
54
|
+
).option(
|
|
55
|
+
"--restore-from-snapshot <batchId>",
|
|
56
|
+
"Recovery mode: skip the compare step and emit DROP/CREATE \u2026 SHALLOW CLONE against the snapshot batch <batchId> from the registry. Dry-run by default; pass --apply --yes to execute. This is the command printed by TRUST.4's post-deploy-smoke + TRUST.8's restore-hint when a deploy fails \u2014 they tell the operator to run `ddt publish --restore-from-snapshot <id>`."
|
|
57
|
+
).option(
|
|
58
|
+
"--snapshot-dir <path>",
|
|
59
|
+
"Snapshot registry directory used with --restore-from-snapshot.",
|
|
60
|
+
".ddt/snapshots"
|
|
61
|
+
).option(
|
|
62
|
+
"--out <path>",
|
|
63
|
+
"Write the generated SQL to this path (currently honored on the --restore-from-snapshot recovery path)."
|
|
64
|
+
).option("--dry-run", "Print the migration script and safety assessment without applying.", true).option(
|
|
65
|
+
"--apply",
|
|
66
|
+
"Apply the migration against --connection. Requires --connection. Default false.",
|
|
67
|
+
false
|
|
68
|
+
).option("--connection <name>", "Connection profile to apply against. Required with --apply.").option(
|
|
69
|
+
"--yes",
|
|
70
|
+
"Skip interactive confirmation when applying. Required with --apply (no TTY today).",
|
|
71
|
+
false
|
|
72
|
+
).option("--variables <kv>", "Comma-separated KEY=VALUE pairs for $(VAR) substitution.").option("--ignore-case", "Compare object FQNs case-insensitively.", false).option(
|
|
73
|
+
"--no-rollback",
|
|
74
|
+
"Skip rollback on partial failure (leaves DIRTY state). Default rollback ON."
|
|
75
|
+
).option(
|
|
76
|
+
"--require-reversible",
|
|
77
|
+
"Refuse before any DDL runs if any step lacks reverse SQL.",
|
|
78
|
+
false
|
|
79
|
+
).option(
|
|
80
|
+
"--manifest <path>",
|
|
81
|
+
"Write a JSON deploy manifest (every step + status + duration + reverse SQL) to <path>. Audit-friendly."
|
|
82
|
+
).option("--report-html <path>", "Write a self-contained HTML compare-report to <path>.").option(
|
|
83
|
+
"--no-lint",
|
|
84
|
+
"Skip the lint gate. Default: lint the generated script and refuse --apply on ERROR-severity findings."
|
|
85
|
+
).option(
|
|
86
|
+
"--webhook <url>",
|
|
87
|
+
"POST a deploy-summary JSON to this URL after --apply completes. Or set $DDT_NOTIFY_WEBHOOK."
|
|
88
|
+
).option(
|
|
89
|
+
"--profile <name>",
|
|
90
|
+
"Use the deploymentProfiles[<name>] overlay from --source (.ddtproj). Pulls profile.connection, profile.variables, profile.deployOptions on top of project defaults."
|
|
91
|
+
).option(
|
|
92
|
+
"--changelog [catalog]",
|
|
93
|
+
"After --apply, append a row to <catalog>.__ddt.deploy_log on the workspace. Defaults catalog to the connection profile's default catalog. Liquibase-style audit table."
|
|
94
|
+
).option(
|
|
95
|
+
"--no-slice",
|
|
96
|
+
"Disable the source's Project Slice (if it has one). Default: a sliced source partitions the diff automatically and target objects outside the slice are left untouched."
|
|
97
|
+
).option(
|
|
98
|
+
"--query-tag <tag>",
|
|
99
|
+
"Tag attached to every executed statement so system.query.history can attribute the deploy. Prepended as `-- DDT-TAG: <tag>` comment. Defaults to 'ddt:<projectName>@<projectVersion>:<unix-ms>' when omitted."
|
|
100
|
+
).option(
|
|
101
|
+
"--color <mode>",
|
|
102
|
+
"Color stdout: auto | always | never. Honors NO_COLOR / DDT_NO_COLOR env vars in auto mode.",
|
|
103
|
+
"auto"
|
|
104
|
+
).option(
|
|
105
|
+
"--ai-preflight",
|
|
106
|
+
"Before --apply, ask AI to grade the operator's plain-language deploy description against the actual diff + safety findings. Blocks on 'mismatch' unless --confirm-after-preflight-mismatch is also passed.",
|
|
107
|
+
false
|
|
108
|
+
).option(
|
|
109
|
+
"--ai-preflight-text <narrative>",
|
|
110
|
+
"Non-interactive form of --ai-preflight. Supplies the narrative directly instead of reading from stdin."
|
|
111
|
+
).option(
|
|
112
|
+
"--strict-ai-preflight",
|
|
113
|
+
"Also block on 'partial' (not just 'mismatch'). Useful for production deploys.",
|
|
114
|
+
false
|
|
115
|
+
).option(
|
|
116
|
+
"--confirm-after-preflight-mismatch",
|
|
117
|
+
"Proceed even when --ai-preflight returns a mismatch verdict. Acknowledges that the AI grader flagged a discrepancy.",
|
|
118
|
+
false
|
|
119
|
+
).option(
|
|
120
|
+
"--require-approvals <n>",
|
|
121
|
+
"Multi-approver gate: refuse --apply until <n> distinct approvers have signed off via `ddt approval add <deploy-id> --as <user>`. Team-tier."
|
|
122
|
+
).option(
|
|
123
|
+
"--approver <ids>",
|
|
124
|
+
"Comma-separated allow-list of approver IDs honoured by --require-approvals. Empty = any approver counts."
|
|
125
|
+
).option(
|
|
126
|
+
"--deploy-id <id>",
|
|
127
|
+
"Stable deploy identifier paired with --require-approvals (default: <connection>:<catalog>.<schema>)."
|
|
128
|
+
).option(
|
|
129
|
+
"--approvals-root <path>",
|
|
130
|
+
"Approvals directory (default: .ddt/approvals).",
|
|
131
|
+
path.join(".ddt", "approvals")
|
|
132
|
+
).option(
|
|
133
|
+
"--allow-protected <fqns>",
|
|
134
|
+
"Comma-separated FQNs to exempt from the .ddt-protection.json gate. Repeatable.",
|
|
135
|
+
(val, prev) => [...prev, val],
|
|
136
|
+
[]
|
|
137
|
+
).option(
|
|
138
|
+
"--allow-all-protected",
|
|
139
|
+
"Bypass all protection gates defined in .ddt-protection.json. Use with care.",
|
|
140
|
+
false
|
|
141
|
+
).option(
|
|
142
|
+
"--freshness <mode>",
|
|
143
|
+
"Pac age check: warn (log if stale, continue), strict (block if stale), skip (no check). Default warn.",
|
|
144
|
+
"warn"
|
|
145
|
+
).option(
|
|
146
|
+
"--post-deploy-tests <project>",
|
|
147
|
+
"After a successful --apply, discover YAML tests under <project>/tests/ and run them against the same connection. Exits 2 on any blocking (error-severity) DQ failure. Emits POST_DEPLOY_DQ_FAILED hint with --manifest recipe when tests fail."
|
|
148
|
+
);
|
|
149
|
+
addMappingFlags(cmd);
|
|
150
|
+
cmd.action(async (opts) => {
|
|
151
|
+
if (opts.restoreFromSnapshot) {
|
|
152
|
+
await runRestoreFromSnapshot(opts);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!opts.source || !opts.target) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
"--source <path> and --target <path> are required (unless --restore-from-snapshot is given)."
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const nameMapping = await buildMappingFromOptions(opts);
|
|
161
|
+
const sourcePath = String(opts.source);
|
|
162
|
+
const targetPath = String(opts.target);
|
|
163
|
+
const engine = new CompareEngine();
|
|
164
|
+
const src = sourcePath.endsWith(".ddtpac") ? new PacSource(sourcePath, "source") : new ProjectSource(sourcePath, "source");
|
|
165
|
+
const tgt = targetPath.endsWith(".ddtpac") ? new PacSource(targetPath, "target") : new ProjectSource(targetPath, "target");
|
|
166
|
+
let loadedSrcPac;
|
|
167
|
+
if (sourcePath.endsWith(".ddtpac")) {
|
|
168
|
+
loadedSrcPac = await pacNs.readPac(sourcePath);
|
|
169
|
+
const freshnessMode = opts.freshness ?? "warn";
|
|
170
|
+
if (freshnessMode !== "skip") {
|
|
171
|
+
const fr = pacFreshness.checkPacFreshness(loadedSrcPac.manifest.builtAt);
|
|
172
|
+
if (fr.status !== "fresh") {
|
|
173
|
+
const msg = pacFreshness.formatFreshnessWarning(fr, sourcePath);
|
|
174
|
+
if (freshnessMode === "strict") {
|
|
175
|
+
console.error(msg);
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
return;
|
|
178
|
+
} else {
|
|
179
|
+
console.warn(msg);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let slice;
|
|
185
|
+
if (opts.slice !== false) {
|
|
186
|
+
if (sourcePath.endsWith(".ddtpac")) {
|
|
187
|
+
const srcPac = loadedSrcPac ?? await pacNs.readPac(sourcePath);
|
|
188
|
+
if (srcPac.manifest.slice) slice = compileSlice(srcPac.manifest.slice);
|
|
189
|
+
} else {
|
|
190
|
+
const loaded = await loadProject(sourcePath);
|
|
191
|
+
if (loaded.project.slice) slice = compileSlice(loaded.project.slice);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const result = await engine.compare(src, tgt, {
|
|
195
|
+
ignoreCase: !!opts.ignoreCase,
|
|
196
|
+
...nameMapping ? { nameMapping } : {},
|
|
197
|
+
...slice ? { sliceFilter: slice } : {}
|
|
198
|
+
});
|
|
199
|
+
const mappingSummary = describeMapping(nameMapping);
|
|
200
|
+
if (mappingSummary) console.log(`Mapping: ${mappingSummary}`);
|
|
201
|
+
if (slice) {
|
|
202
|
+
const outside = result.outsideScope?.length ?? 0;
|
|
203
|
+
const refs = result.referenced?.length ?? 0;
|
|
204
|
+
console.log(
|
|
205
|
+
`Slice active: ${result.objects.length} owned \xB7 ${outside} outside scope (untouched) \xB7 ${refs} referenced`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (opts.reportHtml) {
|
|
209
|
+
const html = renderHtmlReport(result, {
|
|
210
|
+
title: `Publish ${result.source.label} \u2192 ${result.target.label}`,
|
|
211
|
+
safety: safety.assess(result)
|
|
212
|
+
});
|
|
213
|
+
const htmlPath = path.resolve(String(opts.reportHtml));
|
|
214
|
+
await fs.mkdir(path.dirname(htmlPath), { recursive: true });
|
|
215
|
+
await fs.writeFile(htmlPath, html, "utf8");
|
|
216
|
+
console.log(`Wrote HTML report \u2192 ${htmlPath}`);
|
|
217
|
+
}
|
|
218
|
+
let profileDeployment;
|
|
219
|
+
let profileVariables = {};
|
|
220
|
+
let profileConnection;
|
|
221
|
+
if (opts.profile) {
|
|
222
|
+
const profileName = String(opts.profile);
|
|
223
|
+
if (sourcePath.endsWith(".ddtpac")) {
|
|
224
|
+
const srcPac = loadedSrcPac ?? await pacNs.readPac(sourcePath);
|
|
225
|
+
const block = srcPac.manifest.deploymentProfiles?.[profileName];
|
|
226
|
+
if (!block) {
|
|
227
|
+
const available = Object.keys(srcPac.manifest.deploymentProfiles ?? {});
|
|
228
|
+
throw new Error(
|
|
229
|
+
`--profile ${profileName}: no deploymentProfile by that name in the pac manifest. ` + (available.length === 0 ? "The pac carries no deploymentProfiles (was it built before VARSYNTAX.2, or does the .ddtproj declare any?)." : `Available: ${available.join(", ")}.`)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (block.variables) profileVariables = { ...block.variables };
|
|
233
|
+
if (block.connection) profileConnection = block.connection;
|
|
234
|
+
console.log(
|
|
235
|
+
`Profile: ${profileName}${profileConnection ? ` (connection=${profileConnection})` : ""}`
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
const loaded = await loadProject(sourcePath);
|
|
239
|
+
const resolved = resolveProfile(loaded, profileName);
|
|
240
|
+
profileDeployment = resolved.deployment;
|
|
241
|
+
profileVariables = resolved.variables;
|
|
242
|
+
profileConnection = resolved.connection;
|
|
243
|
+
console.log(`Profile: ${resolved.name} (connection=${resolved.connection})`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const assessment = safety.assess(result);
|
|
247
|
+
const deployment = profileDeployment ?? mergeDeployOptions().deployment;
|
|
248
|
+
const variables = { ...profileVariables, ...parseVariables(opts.variables) ?? {} };
|
|
249
|
+
let ctxProjectName;
|
|
250
|
+
let ctxProjectVersion;
|
|
251
|
+
if (sourcePath.endsWith(".ddtpac")) {
|
|
252
|
+
const srcPac = loadedSrcPac ?? await pacNs.readPac(sourcePath);
|
|
253
|
+
ctxProjectName = srcPac.manifest.projectName;
|
|
254
|
+
ctxProjectVersion = srcPac.manifest.projectVersion;
|
|
255
|
+
} else {
|
|
256
|
+
try {
|
|
257
|
+
const loaded = await loadProject(sourcePath);
|
|
258
|
+
ctxProjectName = loaded.project.name;
|
|
259
|
+
ctxProjectVersion = loaded.project.version;
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const generateContext = {
|
|
264
|
+
...opts.profile ? { profile: String(opts.profile) } : {},
|
|
265
|
+
...ctxProjectName ? { projectName: ctxProjectName } : {},
|
|
266
|
+
...ctxProjectVersion ? { projectVersion: ctxProjectVersion } : {}
|
|
267
|
+
};
|
|
268
|
+
const generator = new ScriptGenerator();
|
|
269
|
+
const script = generator.generate(result, {
|
|
270
|
+
variables,
|
|
271
|
+
deployment,
|
|
272
|
+
context: generateContext
|
|
273
|
+
});
|
|
274
|
+
const plan = defaultSafePlan(result);
|
|
275
|
+
const validated = validatePlan(result, plan);
|
|
276
|
+
const steps = buildExecutionPlan(result, validated, { deployment });
|
|
277
|
+
const protConfig = await protectedObjects.loadProtectionConfig(path.dirname(sourcePath));
|
|
278
|
+
if (protConfig) {
|
|
279
|
+
const ops = result.objects.filter((o) => o.kind !== "unchanged" && o.kind !== "added").map((o) => ({
|
|
280
|
+
fqn: o.identity.fqn,
|
|
281
|
+
objectType: String(o.identity.objectType),
|
|
282
|
+
kind: o.kind
|
|
283
|
+
}));
|
|
284
|
+
const allowedFqns = (opts.allowProtected ?? []).flatMap(
|
|
285
|
+
(s) => s.split(",").map((f) => f.trim())
|
|
286
|
+
);
|
|
287
|
+
const violations = protectedObjects.checkProtectedObjects(ops, protConfig, {
|
|
288
|
+
allowedFqns,
|
|
289
|
+
allowAll: Boolean(opts.allowAllProtected)
|
|
290
|
+
});
|
|
291
|
+
if (violations.length > 0) {
|
|
292
|
+
console.error(protectedObjects.formatProtectionViolations(violations));
|
|
293
|
+
process.exitCode = 1;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
printPlanReport(result.summary, script.summary, assessment, steps);
|
|
298
|
+
if (opts.apply) {
|
|
299
|
+
const connectionName = opts.connection ?? profileConnection;
|
|
300
|
+
if (!connectionName) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
"--apply requires --connection <profile-name> or --profile <env> (supplies connection)."
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (!opts.yes) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
"--apply requires --yes. The CLI does not currently prompt interactively. Pass --yes to confirm."
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (assessment.blocked) {
|
|
311
|
+
console.error(
|
|
312
|
+
`Refusing to apply: safety classifier blocked the deploy (${assessment.blockReason}).`
|
|
313
|
+
);
|
|
314
|
+
process.exitCode = 2;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (opts.requireApprovals !== void 0 && opts.requireApprovals !== false) {
|
|
318
|
+
const required = Number(opts.requireApprovals) || 0;
|
|
319
|
+
const allowed = opts.approver ? String(opts.approver).split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
320
|
+
const deployId = String(opts.deployId ?? `${connectionName}:default`);
|
|
321
|
+
const approvalsRoot = String(opts.approvalsRoot);
|
|
322
|
+
const file = await approval.readApprovalFile(approvalsRoot, deployId);
|
|
323
|
+
const outcome = approval.evaluateApprovalGate(
|
|
324
|
+
{ deployId, required, allowedApprovers: allowed },
|
|
325
|
+
file.approvals
|
|
326
|
+
);
|
|
327
|
+
if (!outcome.satisfied) {
|
|
328
|
+
console.error(
|
|
329
|
+
"Approval gate blocked deploy: " + (outcome.blockReason ?? "gate not satisfied") + `. Record approvals with \`ddt approval add ${deployId} --as <user>\`.`
|
|
330
|
+
);
|
|
331
|
+
process.exitCode = 2;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
console.error(
|
|
335
|
+
` approvals: ${outcome.satisfiedBy.length}/${required} (${outcome.satisfiedBy.join(", ")})`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (opts.aiPreflight) {
|
|
339
|
+
const narrative = await readPreflightNarrative(opts.aiPreflightText);
|
|
340
|
+
if (!narrative) {
|
|
341
|
+
console.error(
|
|
342
|
+
"--ai-preflight needs a narrative. Provide it inline via --ai-preflight-text or via stdin."
|
|
343
|
+
);
|
|
344
|
+
process.exitCode = 2;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
console.log("Grading deploy narrative with AI...");
|
|
348
|
+
try {
|
|
349
|
+
const findings = [
|
|
350
|
+
...assessment.unrecoverable.map((f) => ({
|
|
351
|
+
code: String(f.code),
|
|
352
|
+
fqn: f.fqn,
|
|
353
|
+
reason: f.reason
|
|
354
|
+
})),
|
|
355
|
+
...assessment.destructive.map((f) => ({
|
|
356
|
+
code: String(f.code),
|
|
357
|
+
fqn: f.fqn,
|
|
358
|
+
reason: f.reason
|
|
359
|
+
})),
|
|
360
|
+
...assessment.expensive.map((f) => ({
|
|
361
|
+
code: String(f.code),
|
|
362
|
+
fqn: f.fqn,
|
|
363
|
+
reason: f.reason
|
|
364
|
+
}))
|
|
365
|
+
];
|
|
366
|
+
const compareSummary = `added ${result.summary.added}, modified ${result.summary.modified}, removed ${result.summary.removed}, unchanged ${result.summary.unchanged}`;
|
|
367
|
+
const verdict = await aiPreflight.gradePreflight(
|
|
368
|
+
{
|
|
369
|
+
narrative,
|
|
370
|
+
compareSummary,
|
|
371
|
+
safetyFindings: findings,
|
|
372
|
+
diffSampleDdl: script.sql,
|
|
373
|
+
target: `${result.target.label}@${connectionName ?? "unknown"}`
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
completeFn: async (prompt) => {
|
|
377
|
+
const r = await ai.complete([{ role: "user", content: prompt }], {
|
|
378
|
+
feature: "ai-preflight"
|
|
379
|
+
});
|
|
380
|
+
return r.text;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
console.log(` preflight verdict: ${verdict.verdict}`);
|
|
385
|
+
if (verdict.reasoning) console.log(` preflight reasoning: ${verdict.reasoning}`);
|
|
386
|
+
for (const d of verdict.discrepancies) console.log(` \u2022 ${d}`);
|
|
387
|
+
const isBlocking = verdict.verdict === "mismatch" || opts.strictAiPreflight && verdict.verdict === "partial";
|
|
388
|
+
if (isBlocking && !opts.confirmAfterPreflightMismatch) {
|
|
389
|
+
console.error(
|
|
390
|
+
`Refusing to apply: AI preflight returned "${verdict.verdict}". Re-run with --confirm-after-preflight-mismatch to override, or revise the narrative.`
|
|
391
|
+
);
|
|
392
|
+
process.exitCode = 2;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (verdict.parseFailed) {
|
|
396
|
+
console.warn(
|
|
397
|
+
" preflight verdict could not be parsed \u2014 continuing without a clean alignment signal."
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
402
|
+
console.error(
|
|
403
|
+
`--ai-preflight failed: ${msg}. Configure an AI provider via \`ddt ai status\` or omit --ai-preflight.`
|
|
404
|
+
);
|
|
405
|
+
process.exitCode = 2;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (opts.lint !== false) {
|
|
410
|
+
const lint = lintSql(script.sql);
|
|
411
|
+
if (lint.errorCount > 0) {
|
|
412
|
+
console.error("");
|
|
413
|
+
console.error(
|
|
414
|
+
`Refusing to apply: lint gate caught ${lint.errorCount} ERROR-severity finding(s).`
|
|
415
|
+
);
|
|
416
|
+
for (const f of lint.findings) {
|
|
417
|
+
if (f.severity !== "ERROR") continue;
|
|
418
|
+
console.error(` ${f.rule} line ${f.line}: ${f.message}`);
|
|
419
|
+
if (f.suggestion) console.error(` \u2192 ${f.suggestion}`);
|
|
420
|
+
}
|
|
421
|
+
console.error("");
|
|
422
|
+
console.error("Resolve the findings or re-run with --no-lint to override.");
|
|
423
|
+
process.exitCode = 2;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (lint.warningCount > 0) {
|
|
427
|
+
console.log(`Lint: ${lint.warningCount} warning(s) \u2014 proceeding.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const profile = await getProfile(String(connectionName));
|
|
431
|
+
const conn = createConnection(profile);
|
|
432
|
+
const queryTag = resolveQueryTag(
|
|
433
|
+
opts.queryTag,
|
|
434
|
+
sourcePath,
|
|
435
|
+
result.source.label,
|
|
436
|
+
ctxProjectName,
|
|
437
|
+
ctxProjectVersion
|
|
438
|
+
);
|
|
439
|
+
if (queryTag) console.log(`Query tag: ${queryTag}`);
|
|
440
|
+
try {
|
|
441
|
+
await conn.connect();
|
|
442
|
+
const executor = new DatabricksExecutor(conn, {
|
|
443
|
+
...queryTag ? { queryTag } : {},
|
|
444
|
+
onStatement: (sql) => {
|
|
445
|
+
const head = sql.split("\n")[0]?.slice(0, 100) ?? "";
|
|
446
|
+
console.log(` \u2192 ${head}${sql.length > 100 ? " \u2026" : ""}`);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
console.log("");
|
|
450
|
+
console.log("--- APPLYING ---");
|
|
451
|
+
const report = await executePlan(steps, executor, {
|
|
452
|
+
rollbackOnFailure: opts.rollback !== false,
|
|
453
|
+
requireReversible: !!opts.requireReversible,
|
|
454
|
+
onStepStart: (step) => {
|
|
455
|
+
console.log(`\u25B6 ${step.objectType} ${step.fqn} \u2014 ${step.description}`);
|
|
456
|
+
},
|
|
457
|
+
onStepEnd: (r) => {
|
|
458
|
+
const tag = r.status === "SUCCESS" ? "\u2713" : r.status === "ROLLED_BACK" ? "\u21A9" : r.status === "FAILED" ? "\u2717" : r.status === "SKIPPED" ? "-" : "?";
|
|
459
|
+
console.log(
|
|
460
|
+
` ${tag} ${r.status}${r.durationMs ? ` (${r.durationMs}ms)` : ""}${r.error ? `: ${r.error}` : ""}`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
console.log("");
|
|
465
|
+
console.log("--- DEPLOYMENT REPORT ---");
|
|
466
|
+
console.log(`finalState: ${report.finalState}`);
|
|
467
|
+
if (report.failedStepId) console.log(`failedStepId: ${report.failedStepId}`);
|
|
468
|
+
if (report.preflightErrors?.length) {
|
|
469
|
+
console.log("preflightErrors:");
|
|
470
|
+
for (const e of report.preflightErrors) console.log(` ${e}`);
|
|
471
|
+
}
|
|
472
|
+
const manifestJson = JSON.stringify(
|
|
473
|
+
buildDeployManifest(report, profile.auth.host),
|
|
474
|
+
null,
|
|
475
|
+
2
|
|
476
|
+
);
|
|
477
|
+
if (opts.manifest) {
|
|
478
|
+
const mPath = path.resolve(String(opts.manifest));
|
|
479
|
+
await fs.mkdir(path.dirname(mPath), { recursive: true });
|
|
480
|
+
await fs.writeFile(mPath, manifestJson + "\n", "utf8");
|
|
481
|
+
console.log(`Wrote deploy manifest \u2192 ${mPath}`);
|
|
482
|
+
}
|
|
483
|
+
if (opts.changelog) {
|
|
484
|
+
const catalog = opts.changelog === true ? void 0 : String(opts.changelog);
|
|
485
|
+
const r = await recordDeployChangelog(
|
|
486
|
+
conn,
|
|
487
|
+
report,
|
|
488
|
+
result.source,
|
|
489
|
+
result.target,
|
|
490
|
+
manifestJson,
|
|
491
|
+
"0.3.0",
|
|
492
|
+
catalog ? { catalog } : {}
|
|
493
|
+
);
|
|
494
|
+
if (r.ok) {
|
|
495
|
+
console.log(`Changelog: deploy_id=${r.entry.deployId} appended.`);
|
|
496
|
+
} else {
|
|
497
|
+
console.error(`Changelog write failed (deploy still completed): ${r.error}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const webhookUrl = opts.webhook ?? process.env["DDT_NOTIFY_WEBHOOK"];
|
|
501
|
+
if (webhookUrl) {
|
|
502
|
+
try {
|
|
503
|
+
await postWebhook(String(webhookUrl), {
|
|
504
|
+
workspaceHost: profile.auth.host,
|
|
505
|
+
source: result.source.label,
|
|
506
|
+
target: result.target.label,
|
|
507
|
+
finalState: report.finalState,
|
|
508
|
+
failedStepId: report.failedStepId,
|
|
509
|
+
stepsTotal: report.steps.length,
|
|
510
|
+
stepsSucceeded: report.steps.filter((s) => s.status === "SUCCESS").length,
|
|
511
|
+
stepsFailed: report.steps.filter((s) => s.status === "FAILED").length
|
|
512
|
+
});
|
|
513
|
+
console.log(`Webhook notified \u2192 ${webhookUrl}`);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
console.error(
|
|
516
|
+
`Webhook POST failed (deploy still completed): ${err instanceof Error ? err.message : String(err)}`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (report.finalState === "DIRTY") {
|
|
521
|
+
console.error(
|
|
522
|
+
"DIRTY: rollback could not complete. Manual intervention required \u2014 see report above."
|
|
523
|
+
);
|
|
524
|
+
process.exitCode = 1;
|
|
525
|
+
} else if (report.finalState !== "CLEAN_SUCCESS") {
|
|
526
|
+
process.exitCode = 1;
|
|
527
|
+
}
|
|
528
|
+
if (opts.postDeployTests && report.finalState === "CLEAN_SUCCESS") {
|
|
529
|
+
const projectRoot = path.resolve(String(opts.postDeployTests));
|
|
530
|
+
console.log("Running post-deploy DQ tests from " + projectRoot + "...");
|
|
531
|
+
const testFiles = await testFramework.discoverTests(projectRoot);
|
|
532
|
+
if (testFiles.length === 0) {
|
|
533
|
+
console.warn(
|
|
534
|
+
" POST_DEPLOY_DQ: no tests found under " + projectRoot + "/tests/ \u2014 skipping."
|
|
535
|
+
);
|
|
536
|
+
} else {
|
|
537
|
+
const dqReport = await testFramework.runTestFiles(testFiles, conn);
|
|
538
|
+
process.stdout.write(testFramework.renderTextReport(dqReport));
|
|
539
|
+
if (dqReport.summary.blocking) {
|
|
540
|
+
console.error(
|
|
541
|
+
"POST_DEPLOY_DQ_FAILED: " + dqReport.summary.fail + " fail / " + dqReport.summary.error + " error." + (opts.manifest ? " Recover: `ddt revert --manifest " + String(opts.manifest) + "`" : " Inspect tests/ and re-run after fixing data issues.")
|
|
542
|
+
);
|
|
543
|
+
process.exitCode = 2;
|
|
544
|
+
} else {
|
|
545
|
+
console.log(
|
|
546
|
+
" POST_DEPLOY_DQ: " + dqReport.summary.pass + "/" + dqReport.summary.total + " pass."
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} finally {
|
|
552
|
+
await conn.disconnect();
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const colorMode = opts.color ?? "auto";
|
|
557
|
+
console.log("");
|
|
558
|
+
console.log("--- MIGRATION SCRIPT ---");
|
|
559
|
+
console.log(colorizeMigrationScript(script.sql, { mode: colorMode }));
|
|
560
|
+
});
|
|
561
|
+
attachRelatedOptions(cmd, [
|
|
562
|
+
"deployment.allowDropTable",
|
|
563
|
+
"deployment.allowDropColumn",
|
|
564
|
+
"deployment.allowUnrecoverableDrop",
|
|
565
|
+
"deployment.allowNarrowingTypes",
|
|
566
|
+
"deployment.allowTableRebuild",
|
|
567
|
+
"deployment.blockOnPossibleDataLoss",
|
|
568
|
+
"deployment.preserveTargetOnlyObjects"
|
|
569
|
+
]);
|
|
570
|
+
return cmd;
|
|
571
|
+
}
|
|
572
|
+
function printPlanReport(diffSummary, scriptSummary, assessment, steps) {
|
|
573
|
+
console.log("--- COMPARE SUMMARY ---");
|
|
574
|
+
console.log(
|
|
575
|
+
`+${diffSummary.added} -${diffSummary.removed} ~${diffSummary.modified} =${diffSummary.unchanged}` + (scriptSummary.destructive ? ` (destructive=${scriptSummary.destructive})` : "") + (scriptSummary.refused ? ` (refused=${scriptSummary.refused})` : "")
|
|
576
|
+
);
|
|
577
|
+
console.log("");
|
|
578
|
+
console.log("--- SAFETY ASSESSMENT ---");
|
|
579
|
+
console.log(`unrecoverable=${assessment.unrecoverable.length}`);
|
|
580
|
+
console.log(`destructive=${assessment.destructive.length}`);
|
|
581
|
+
console.log(`expensive=${assessment.expensive.length}`);
|
|
582
|
+
console.log(`warnings=${assessment.warnings.length}`);
|
|
583
|
+
if (assessment.blocked) console.log(`BLOCKED: ${assessment.blockReason}`);
|
|
584
|
+
console.log("");
|
|
585
|
+
console.log("--- DEFAULT-SAFE PLAN ---");
|
|
586
|
+
const reversible = steps.filter((st) => st.reverseSql).length;
|
|
587
|
+
console.log(`${steps.length} step(s) planned (default-safe selection)`);
|
|
588
|
+
console.log(`${reversible} step(s) reversible \xB7 ${steps.length - reversible} irreversible`);
|
|
589
|
+
}
|
|
590
|
+
async function postWebhook(url, payload) {
|
|
591
|
+
const isSlackOrTeams = /hooks\.slack\.com|webhook\.office\.com/.test(url);
|
|
592
|
+
let body;
|
|
593
|
+
if (isSlackOrTeams) {
|
|
594
|
+
const summary = payload;
|
|
595
|
+
body = JSON.stringify({
|
|
596
|
+
text: `DDT publish \u2192 ${summary.target} \xB7 ${summary.finalState} \xB7 ${summary.stepsSucceeded}/${summary.stepsTotal} steps ok${summary.stepsFailed ? ` \xB7 ${summary.stepsFailed} failed` : ""}`
|
|
597
|
+
});
|
|
598
|
+
} else {
|
|
599
|
+
body = JSON.stringify(payload);
|
|
600
|
+
}
|
|
601
|
+
const res = await fetch(url, {
|
|
602
|
+
method: "POST",
|
|
603
|
+
headers: { "Content-Type": "application/json" },
|
|
604
|
+
body
|
|
605
|
+
});
|
|
606
|
+
if (!res.ok) {
|
|
607
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function parseVariables(raw) {
|
|
611
|
+
if (!raw) return void 0;
|
|
612
|
+
const out = {};
|
|
613
|
+
for (const pair of String(raw).split(",")) {
|
|
614
|
+
const eq = pair.indexOf("=");
|
|
615
|
+
if (eq <= 0) continue;
|
|
616
|
+
const k = pair.slice(0, eq).trim();
|
|
617
|
+
const v = pair.slice(eq + 1).trim();
|
|
618
|
+
if (k.length > 0) out[k] = v;
|
|
619
|
+
}
|
|
620
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
621
|
+
}
|
|
622
|
+
function resolveQueryTag(raw, sourcePath, sourceLabel, projectName, projectVersion) {
|
|
623
|
+
if (raw !== void 0) {
|
|
624
|
+
const s = String(raw).trim();
|
|
625
|
+
return s.length === 0 ? void 0 : s;
|
|
626
|
+
}
|
|
627
|
+
const fallback = sourcePath.endsWith(".ddtpac") ? path.basename(sourcePath, ".ddtpac") : sourceLabel;
|
|
628
|
+
const base = projectName ?? fallback;
|
|
629
|
+
const version = projectVersion ? `@${projectVersion}` : "";
|
|
630
|
+
return `ddt:${base}${version}:${Date.now()}`;
|
|
631
|
+
}
|
|
632
|
+
async function runRestoreFromSnapshot(opts) {
|
|
633
|
+
const batchId = String(opts.restoreFromSnapshot);
|
|
634
|
+
const registryDir = path.resolve(String(opts.snapshotDir ?? ".ddt/snapshots"));
|
|
635
|
+
let batch;
|
|
636
|
+
try {
|
|
637
|
+
batch = await safety.readSnapshotBatch(registryDir, batchId);
|
|
638
|
+
} catch (err) {
|
|
639
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
640
|
+
console.error(`Snapshot batch "${batchId}" not found in ${registryDir}: ${msg}`);
|
|
641
|
+
process.exitCode = 1;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const sqlLines = safety.emitRestoreFromSnapshotSql(batch);
|
|
645
|
+
const sql = sqlLines.join("\n");
|
|
646
|
+
console.log(`Restoring from batch ${batch.batchId} (createdAt=${batch.createdAt})`);
|
|
647
|
+
console.log(
|
|
648
|
+
` ${batch.entries.length} object(s) recorded; ${sqlLines.filter((l) => !l.startsWith("--")).length} statement(s) will run.`
|
|
649
|
+
);
|
|
650
|
+
if (opts.out) {
|
|
651
|
+
const out = path.resolve(String(opts.out));
|
|
652
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
653
|
+
await fs.writeFile(out, sql + "\n", "utf8");
|
|
654
|
+
console.log(`Wrote restore SQL \u2192 ${out}`);
|
|
655
|
+
}
|
|
656
|
+
if (!opts.apply) {
|
|
657
|
+
const colorMode = opts.color ?? "auto";
|
|
658
|
+
process.stdout.write(colorizeMigrationScript(sql, { mode: colorMode }) + "\n");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (!opts.yes) {
|
|
662
|
+
console.error(
|
|
663
|
+
"--apply requires --yes (restore replaces target tables via DROP + SHALLOW CLONE)."
|
|
664
|
+
);
|
|
665
|
+
process.exitCode = 1;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (!opts.connection) {
|
|
669
|
+
console.error("--apply --restore-from-snapshot requires --connection <profile>.");
|
|
670
|
+
process.exitCode = 1;
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const profile = await getProfile(String(opts.connection));
|
|
674
|
+
const conn = createConnection(profile);
|
|
675
|
+
let executed = 0;
|
|
676
|
+
let failed = 0;
|
|
677
|
+
try {
|
|
678
|
+
await conn.connect();
|
|
679
|
+
for (const stmt of queryExecution.splitStatements(sql).map((s) => s.sql)) {
|
|
680
|
+
try {
|
|
681
|
+
await conn.executeRows(stmt);
|
|
682
|
+
executed += 1;
|
|
683
|
+
const head = stmt.split("\n")[0]?.slice(0, 100) ?? "";
|
|
684
|
+
console.log(" \u2192 " + head + (stmt.length > 100 ? " \u2026" : ""));
|
|
685
|
+
} catch (err) {
|
|
686
|
+
failed += 1;
|
|
687
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
688
|
+
console.error(` [restore ${batch.batchId}] ${msg}
|
|
689
|
+
${stmt}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
} finally {
|
|
693
|
+
await conn.disconnect();
|
|
694
|
+
}
|
|
695
|
+
if (failed === 0) {
|
|
696
|
+
console.log(`Restore complete. ${executed} statement(s) executed.`);
|
|
697
|
+
} else {
|
|
698
|
+
console.error(
|
|
699
|
+
`Restore finished with failures: ${executed} ok, ${failed} failed. Inspect the snapshot tables under ${registryDir} and recover by hand.`
|
|
700
|
+
);
|
|
701
|
+
process.exitCode = 1;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async function readPreflightNarrative(inlineText) {
|
|
705
|
+
if (typeof inlineText === "string" && inlineText.trim().length > 0) {
|
|
706
|
+
return inlineText.trim();
|
|
707
|
+
}
|
|
708
|
+
if (process.stdin.isTTY) {
|
|
709
|
+
process.stdout.write(
|
|
710
|
+
"\n--ai-preflight is active. In one paragraph, describe what you expect this deploy to do.\nEnd with an empty line (or Ctrl-D):\n> "
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
process.stdin.setEncoding("utf8");
|
|
714
|
+
let buffer = "";
|
|
715
|
+
await new Promise((resolve) => {
|
|
716
|
+
const onData = (chunk) => {
|
|
717
|
+
buffer += chunk;
|
|
718
|
+
if (/\n\s*\n/.test(buffer)) {
|
|
719
|
+
process.stdin.removeListener("data", onData);
|
|
720
|
+
process.stdin.removeListener("end", onEnd);
|
|
721
|
+
resolve();
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
const onEnd = () => {
|
|
725
|
+
process.stdin.removeListener("data", onData);
|
|
726
|
+
resolve();
|
|
727
|
+
};
|
|
728
|
+
process.stdin.on("data", onData);
|
|
729
|
+
process.stdin.on("end", onEnd);
|
|
730
|
+
});
|
|
731
|
+
const trimmed = buffer.trim();
|
|
732
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
733
|
+
}
|
|
734
|
+
export {
|
|
735
|
+
parseVariables,
|
|
736
|
+
publishCommand,
|
|
737
|
+
resolveQueryTag
|
|
738
|
+
};
|
|
739
|
+
//# sourceMappingURL=publish-AYCRMCE2.js.map
|