@databricks-solutions/lakebase-scm-utils 0.2.44 → 0.2.46

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.
Files changed (48) hide show
  1. package/README.md +13 -4
  2. package/dist/scripts/index.cjs +651 -605
  3. package/dist/scripts/index.d.cts +1 -1
  4. package/dist/scripts/index.d.ts +1 -1
  5. package/dist/scripts/index.js +650 -605
  6. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +12 -3
  7. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +12 -3
  8. package/dist/scripts/lakebase/collapse-heads.cli.cjs +12 -3
  9. package/dist/scripts/lakebase/collapse-heads.cli.js +12 -3
  10. package/dist/scripts/lakebase/detect-language.cli.cjs +12 -3
  11. package/dist/scripts/lakebase/detect-language.cli.js +12 -3
  12. package/dist/scripts/lakebase/doctor.cli.cjs +13 -4
  13. package/dist/scripts/lakebase/doctor.cli.js +13 -4
  14. package/dist/scripts/lakebase/get-connection.cli.cjs +12 -3
  15. package/dist/scripts/lakebase/get-connection.cli.js +12 -3
  16. package/dist/scripts/lakebase/index.cjs +472 -395
  17. package/dist/scripts/lakebase/index.d.cts +33 -12
  18. package/dist/scripts/lakebase/index.d.ts +33 -12
  19. package/dist/scripts/lakebase/index.js +471 -395
  20. package/dist/scripts/lakebase/infra-runner.cli.cjs +12 -3
  21. package/dist/scripts/lakebase/infra-runner.cli.js +12 -3
  22. package/dist/scripts/lakebase/new-migration.cli.cjs +12 -3
  23. package/dist/scripts/lakebase/new-migration.cli.js +12 -3
  24. package/dist/scripts/lakebase/schema-diff.cli.cjs +12 -3
  25. package/dist/scripts/lakebase/schema-diff.cli.js +12 -3
  26. package/dist/scripts/lakebase/schema-migrate.cli.cjs +12 -3
  27. package/dist/scripts/lakebase/schema-migrate.cli.js +12 -3
  28. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +12 -3
  29. package/dist/scripts/lakebase/scm-claim-feature.cli.js +12 -3
  30. package/dist/scripts/lakebase/scm-doctor.cli.cjs +12 -3
  31. package/dist/scripts/lakebase/scm-doctor.cli.js +12 -3
  32. package/dist/scripts/lakebase/scm-merge.cli.cjs +2294 -2034
  33. package/dist/scripts/lakebase/scm-merge.cli.js +349 -89
  34. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +12 -3
  35. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +12 -3
  36. package/dist/scripts/util/index.cjs +2 -2
  37. package/dist/scripts/util/index.js +2 -2
  38. package/package.json +1 -1
  39. package/scripts/lakebase/constants.ts +23 -5
  40. package/scripts/lakebase/doctor.ts +1 -1
  41. package/scripts/lakebase/get-connection.ts +17 -3
  42. package/scripts/lakebase/scm-merge.ts +80 -1
  43. package/skills/lakebase-scm-workflows/README.md +2 -1
  44. package/templates/project/nodejs/package.json +3 -1
  45. package/templates/project/nodejs/src/index.js +8 -0
  46. package/templates/project/nodejs/src/openapi.js +20 -0
  47. package/templates/project/nodejs/src/routes/health.js +18 -0
  48. package/templates/project/nodejs/tests/app.test.js +16 -0
@@ -1457,7 +1457,7 @@ var require_light = __commonJS({
1457
1457
 
1458
1458
  // scripts/lakebase/scm-merge.cli.ts
1459
1459
  init_esm_shims();
1460
- import * as path12 from "path";
1460
+ import * as path13 from "path";
1461
1461
 
1462
1462
  // scripts/util/cli-entry.ts
1463
1463
  init_esm_shims();
@@ -2549,7 +2549,7 @@ paginateRest.VERSION = VERSION5;
2549
2549
 
2550
2550
  // node_modules/@octokit/plugin-paginate-graphql/dist-bundle/index.js
2551
2551
  init_esm_shims();
2552
- var generateMessage = (path13, cursorValue) => `The cursor at "${path13.join(
2552
+ var generateMessage = (path14, cursorValue) => `The cursor at "${path14.join(
2553
2553
  ","
2554
2554
  )}" did not change its value "${cursorValue}" after a page transition. Please make sure your that your query is set up correctly.`;
2555
2555
  var MissingCursorChange = class extends Error {
@@ -2590,9 +2590,9 @@ function findPaginatedResourcePath(responseData) {
2590
2590
  }
2591
2591
  return paginatedResourcePath;
2592
2592
  }
2593
- var deepFindPathToProperty = (object, searchProp, path13 = []) => {
2593
+ var deepFindPathToProperty = (object, searchProp, path14 = []) => {
2594
2594
  for (const key of Object.keys(object)) {
2595
- const currentPath = [...path13, key];
2595
+ const currentPath = [...path14, key];
2596
2596
  const currentValue = object[key];
2597
2597
  if (isObject(currentValue)) {
2598
2598
  if (currentValue.hasOwnProperty(searchProp)) {
@@ -2610,12 +2610,12 @@ var deepFindPathToProperty = (object, searchProp, path13 = []) => {
2610
2610
  }
2611
2611
  return [];
2612
2612
  };
2613
- var get = (object, path13) => {
2614
- return path13.reduce((current, nextProperty) => current[nextProperty], object);
2613
+ var get = (object, path14) => {
2614
+ return path14.reduce((current, nextProperty) => current[nextProperty], object);
2615
2615
  };
2616
- var set = (object, path13, mutator) => {
2617
- const lastProperty = path13[path13.length - 1];
2618
- const parentPath = [...path13].slice(0, -1);
2616
+ var set = (object, path14, mutator) => {
2617
+ const lastProperty = path14[path14.length - 1];
2618
+ const parentPath = [...path14].slice(0, -1);
2619
2619
  const parent = get(object, parentPath);
2620
2620
  if (typeof mutator === "function") {
2621
2621
  parent[lastProperty] = mutator(parent[lastProperty]);
@@ -2667,22 +2667,22 @@ var mergeResponses = (response1, response2) => {
2667
2667
  if (Object.keys(response1).length === 0) {
2668
2668
  return Object.assign(response1, response2);
2669
2669
  }
2670
- const path13 = findPaginatedResourcePath(response1);
2671
- const nodesPath = [...path13, "nodes"];
2670
+ const path14 = findPaginatedResourcePath(response1);
2671
+ const nodesPath = [...path14, "nodes"];
2672
2672
  const newNodes = get(response2, nodesPath);
2673
2673
  if (newNodes) {
2674
2674
  set(response1, nodesPath, (values) => {
2675
2675
  return [...values, ...newNodes];
2676
2676
  });
2677
2677
  }
2678
- const edgesPath = [...path13, "edges"];
2678
+ const edgesPath = [...path14, "edges"];
2679
2679
  const newEdges = get(response2, edgesPath);
2680
2680
  if (newEdges) {
2681
2681
  set(response1, edgesPath, (values) => {
2682
2682
  return [...values, ...newEdges];
2683
2683
  });
2684
2684
  }
2685
- const pageInfoPath = [...path13, "pageInfo"];
2685
+ const pageInfoPath = [...path14, "pageInfo"];
2686
2686
  set(response1, pageInfoPath, get(response2, pageInfoPath));
2687
2687
  return response1;
2688
2688
  };
@@ -5103,7 +5103,7 @@ var triggers_notification_paths_default = [
5103
5103
  ];
5104
5104
  function routeMatcher(paths) {
5105
5105
  const regexes = paths.map(
5106
- (path13) => path13.split("/").map((c) => c.startsWith("{") ? "(?:.+?)" : c).join("/")
5106
+ (path14) => path14.split("/").map((c) => c.startsWith("{") ? "(?:.+?)" : c).join("/")
5107
5107
  );
5108
5108
  const regex2 = `^(?:${regexes.map((r) => `(?:${r})`).join("|")})[^/]*$`;
5109
5109
  return new RegExp(regex2, "i");
@@ -8378,9 +8378,9 @@ function asBranchUid(s) {
8378
8378
  }
8379
8379
  return s;
8380
8380
  }
8381
- function branchNameFromResourcePath(path13) {
8382
- if (!path13.includes("/branches/")) return null;
8383
- const leaf = path13.split("/branches/").pop();
8381
+ function branchNameFromResourcePath(path14) {
8382
+ if (!path14.includes("/branches/")) return null;
8383
+ const leaf = path14.split("/branches/").pop();
8384
8384
  if (!leaf) return null;
8385
8385
  try {
8386
8386
  return asBranchName(leaf);
@@ -8785,6 +8785,42 @@ async function pollUntil(args) {
8785
8785
  }
8786
8786
  }
8787
8787
 
8788
+ // scripts/git/sync.ts
8789
+ init_esm_shims();
8790
+
8791
+ // scripts/git/status.ts
8792
+ init_esm_shims();
8793
+ async function hasUpstream(args) {
8794
+ try {
8795
+ await exec2("git rev-parse --abbrev-ref @{u}", { cwd: args.cwd });
8796
+ return true;
8797
+ } catch {
8798
+ return false;
8799
+ }
8800
+ }
8801
+
8802
+ // scripts/git/sync.ts
8803
+ async function currentBranchName(cwd) {
8804
+ try {
8805
+ return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
8806
+ } catch {
8807
+ return "";
8808
+ }
8809
+ }
8810
+ async function pushCurrentBranchForPr(args) {
8811
+ const remote = args.remote ?? "origin";
8812
+ const branch = await currentBranchName(args.cwd);
8813
+ if (!branch) throw new Error("No current branch");
8814
+ const upstreamSet = await hasUpstream({ cwd: args.cwd });
8815
+ if (!upstreamSet) {
8816
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
8817
+ cwd: args.cwd
8818
+ });
8819
+ } else {
8820
+ await exec2("git push", { cwd: args.cwd });
8821
+ }
8822
+ }
8823
+
8788
8824
  // scripts/lakebase/scm-git-base.ts
8789
8825
  init_esm_shims();
8790
8826
  async function resolveGitBase(parentBranch, cwd) {
@@ -8794,10 +8830,190 @@ async function resolveGitBase(parentBranch, cwd) {
8794
8830
  return resolveDefaultBranch({ cwd });
8795
8831
  }
8796
8832
 
8797
- // scripts/lakebase/scm-workflow-state.ts
8833
+ // scripts/lakebase/workflow-drift.ts
8798
8834
  init_esm_shims();
8799
8835
  import * as fs3 from "fs";
8800
8836
  import * as path3 from "path";
8837
+ function findKitTemplatesDir(start) {
8838
+ let dir = start;
8839
+ for (let i = 0; i < 6; i++) {
8840
+ const candidate = path3.join(
8841
+ dir,
8842
+ "templates",
8843
+ "project",
8844
+ "common",
8845
+ ".github",
8846
+ "workflows"
8847
+ );
8848
+ if (fs3.existsSync(candidate)) return candidate;
8849
+ const parent = path3.dirname(dir);
8850
+ if (parent === dir) break;
8851
+ dir = parent;
8852
+ }
8853
+ throw new Error(
8854
+ `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
8855
+ );
8856
+ }
8857
+ function unifiedDiff(name, projectContent, templateContent) {
8858
+ if (projectContent === templateContent) return "";
8859
+ const a = projectContent.split("\n");
8860
+ const b = templateContent.split("\n");
8861
+ const out = [`--- project/${name}`, `+++ template/${name}`];
8862
+ const max = Math.max(a.length, b.length);
8863
+ for (let i = 0; i < max; i++) {
8864
+ const av = a[i];
8865
+ const bv = b[i];
8866
+ if (av === bv) continue;
8867
+ if (av !== void 0) out.push(`-${i + 1}: ${av}`);
8868
+ if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
8869
+ }
8870
+ return out.join("\n");
8871
+ }
8872
+ function detectWorkflowDrift(args) {
8873
+ const projectWorkflowsDir = path3.join(
8874
+ args.projectDir,
8875
+ ".github",
8876
+ "workflows"
8877
+ );
8878
+ const here = path3.dirname(new URL(import.meta.url).pathname);
8879
+ const kitWorkflowsDir = args.kitDir ? path3.join(
8880
+ args.kitDir,
8881
+ "templates",
8882
+ "project",
8883
+ "common",
8884
+ ".github",
8885
+ "workflows"
8886
+ ) : findKitTemplatesDir(here);
8887
+ const templateFiles = fs3.existsSync(kitWorkflowsDir) ? fs3.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
8888
+ const projectFiles = fs3.existsSync(projectWorkflowsDir) ? fs3.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
8889
+ const version = readKitVersion(kitWorkflowsDir);
8890
+ const seen = /* @__PURE__ */ new Set();
8891
+ const files = [];
8892
+ for (const name of templateFiles) {
8893
+ seen.add(name);
8894
+ const projectPath2 = path3.join(projectWorkflowsDir, name);
8895
+ const templatePath = path3.join(kitWorkflowsDir, name);
8896
+ if (!fs3.existsSync(projectPath2)) {
8897
+ files.push({ name, status: "missing" });
8898
+ continue;
8899
+ }
8900
+ const projectContent = fs3.readFileSync(projectPath2, "utf8");
8901
+ const templateContent = applyPlaceholders(fs3.readFileSync(templatePath, "utf8"), version);
8902
+ if (projectContent === templateContent) {
8903
+ files.push({ name, status: "unchanged" });
8904
+ } else {
8905
+ files.push({
8906
+ name,
8907
+ status: "drifted",
8908
+ diff: unifiedDiff(name, projectContent, templateContent)
8909
+ });
8910
+ }
8911
+ }
8912
+ for (const name of projectFiles) {
8913
+ if (seen.has(name)) continue;
8914
+ files.push({ name, status: "extra" });
8915
+ }
8916
+ const order = {
8917
+ drifted: 0,
8918
+ missing: 1,
8919
+ extra: 2,
8920
+ unchanged: 3
8921
+ };
8922
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
8923
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
8924
+ return {
8925
+ overall: hasDrift ? "drift" : "ok",
8926
+ files
8927
+ };
8928
+ }
8929
+ function readKitVersion(kitWorkflowsDir) {
8930
+ let dir = kitWorkflowsDir;
8931
+ for (let i = 0; i < 5; i++) {
8932
+ dir = path3.dirname(dir);
8933
+ }
8934
+ try {
8935
+ const raw = fs3.readFileSync(path3.join(dir, "package.json"), "utf-8");
8936
+ const pkg = JSON.parse(raw);
8937
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
8938
+ } catch {
8939
+ return "unknown";
8940
+ }
8941
+ }
8942
+ function applyPlaceholders(content, version) {
8943
+ return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
8944
+ }
8945
+ function updateWorkflows(args) {
8946
+ const projectWorkflowsDir = path3.join(
8947
+ args.projectDir,
8948
+ ".github",
8949
+ "workflows"
8950
+ );
8951
+ const here = path3.dirname(new URL(import.meta.url).pathname);
8952
+ const kitWorkflowsDir = args.kitDir ? path3.join(
8953
+ args.kitDir,
8954
+ "templates",
8955
+ "project",
8956
+ "common",
8957
+ ".github",
8958
+ "workflows"
8959
+ ) : findKitTemplatesDir(here);
8960
+ const substitute = args.substitute !== false;
8961
+ const dryRun = args.dryRun === true;
8962
+ const pruneExtras = args.pruneExtras === true;
8963
+ const templateFiles = fs3.existsSync(kitWorkflowsDir) ? fs3.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
8964
+ const projectFiles = fs3.existsSync(projectWorkflowsDir) ? fs3.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
8965
+ if (!dryRun && templateFiles.length > 0 && !fs3.existsSync(projectWorkflowsDir)) {
8966
+ fs3.mkdirSync(projectWorkflowsDir, { recursive: true });
8967
+ }
8968
+ const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
8969
+ const seen = /* @__PURE__ */ new Set();
8970
+ const files = [];
8971
+ for (const name of templateFiles) {
8972
+ seen.add(name);
8973
+ const projectPath2 = path3.join(projectWorkflowsDir, name);
8974
+ const templatePath = path3.join(kitWorkflowsDir, name);
8975
+ const templateRaw = fs3.readFileSync(templatePath, "utf-8");
8976
+ const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
8977
+ const existed = fs3.existsSync(projectPath2);
8978
+ const current = existed ? fs3.readFileSync(projectPath2, "utf-8") : "";
8979
+ let outcome;
8980
+ if (!existed) {
8981
+ outcome = "added";
8982
+ } else if (current === desired) {
8983
+ outcome = "unchanged";
8984
+ } else {
8985
+ outcome = "updated";
8986
+ }
8987
+ if (!dryRun && outcome !== "unchanged") {
8988
+ fs3.writeFileSync(projectPath2, desired);
8989
+ }
8990
+ files.push({ name, outcome });
8991
+ }
8992
+ if (pruneExtras) {
8993
+ for (const name of projectFiles) {
8994
+ if (seen.has(name)) continue;
8995
+ const projectPath2 = path3.join(projectWorkflowsDir, name);
8996
+ if (!dryRun) {
8997
+ fs3.unlinkSync(projectPath2);
8998
+ }
8999
+ files.push({ name, outcome: "removed" });
9000
+ }
9001
+ }
9002
+ const order = {
9003
+ added: 0,
9004
+ updated: 1,
9005
+ removed: 2,
9006
+ unchanged: 3
9007
+ };
9008
+ files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
9009
+ const changed = files.some((f) => f.outcome !== "unchanged");
9010
+ return { files, changed };
9011
+ }
9012
+
9013
+ // scripts/lakebase/scm-workflow-state.ts
9014
+ init_esm_shims();
9015
+ import * as fs4 from "fs";
9016
+ import * as path4 from "path";
8801
9017
  import { execFileSync as execFileSync4 } from "child_process";
8802
9018
  function isGitTracked(projectDir, rel) {
8803
9019
  try {
@@ -8816,11 +9032,11 @@ function ensureWorkflowStateUntracked(projectDir) {
8816
9032
  } catch {
8817
9033
  }
8818
9034
  try {
8819
- const gitignore = path3.join(projectDir, ".gitignore");
8820
- const existing = fs3.existsSync(gitignore) ? fs3.readFileSync(gitignore, "utf8") : "";
9035
+ const gitignore = path4.join(projectDir, ".gitignore");
9036
+ const existing = fs4.existsSync(gitignore) ? fs4.readFileSync(gitignore, "utf8") : "";
8821
9037
  if (!existing.split("\n").some((l) => l.trim() === rel)) {
8822
9038
  const sep2 = existing === "" || existing.endsWith("\n") ? "" : "\n";
8823
- fs3.appendFileSync(
9039
+ fs4.appendFileSync(
8824
9040
  gitignore,
8825
9041
  `${sep2}# Runtime SCM claim state (per working tree): a branch checkout must never restore
8826
9042
  # a stale committed claim over the live one (issue #203 / Finding 28).
@@ -8844,12 +9060,12 @@ var STATE_INDEX = SCM_STATES.reduce(
8844
9060
  );
8845
9061
  var STATE_FILE_REL = ".lakebase/workflow-state.json";
8846
9062
  function stateFilePath(projectDir) {
8847
- return path3.join(projectDir, STATE_FILE_REL);
9063
+ return path4.join(projectDir, STATE_FILE_REL);
8848
9064
  }
8849
9065
  function readWorkflowState(projectDir) {
8850
9066
  const p = stateFilePath(projectDir);
8851
- if (!fs3.existsSync(p)) return null;
8852
- const raw = fs3.readFileSync(p, "utf8");
9067
+ if (!fs4.existsSync(p)) return null;
9068
+ const raw = fs4.readFileSync(p, "utf8");
8853
9069
  let parsed;
8854
9070
  try {
8855
9071
  parsed = JSON.parse(raw);
@@ -8878,14 +9094,14 @@ function writeWorkflowState(projectDir, state) {
8878
9094
  ${summary}`);
8879
9095
  }
8880
9096
  ensureWorkflowStateUntracked(projectDir);
8881
- const dir = path3.join(projectDir, ".lakebase");
8882
- fs3.mkdirSync(dir, { recursive: true });
9097
+ const dir = path4.join(projectDir, ".lakebase");
9098
+ fs4.mkdirSync(dir, { recursive: true });
8883
9099
  const target = stateFilePath(projectDir);
8884
9100
  const tmp = `${target}.tmp`;
8885
9101
  const ordered = orderForOutput(result.value);
8886
- fs3.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}
9102
+ fs4.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}
8887
9103
  `, "utf8");
8888
- fs3.renameSync(tmp, target);
9104
+ fs4.renameSync(tmp, target);
8889
9105
  }
8890
9106
  function validateWorkflowState(value) {
8891
9107
  const errors = [];
@@ -9087,6 +9303,27 @@ function shaMigratePredicate(mergeCommitSha) {
9087
9303
  return !!run.headSha && run.headSha === mergeCommitSha;
9088
9304
  };
9089
9305
  }
9306
+ async function defaultRefreshPromoteWorkflows(projectDir) {
9307
+ const drift = detectWorkflowDrift({ projectDir });
9308
+ const touched = drift.files.filter((f) => f.status === "drifted").map((f) => f.name);
9309
+ if (touched.length === 0) return { refreshed: false };
9310
+ const result = updateWorkflows({ projectDir });
9311
+ if (!result.changed) return { refreshed: false };
9312
+ await exec2(`git add ${shellEscape(".github/workflows")}`, { cwd: projectDir, timeout: 1e4 });
9313
+ const staged = await exec2("git diff --cached --name-only -- .github/workflows", {
9314
+ cwd: projectDir,
9315
+ timeout: 1e4
9316
+ });
9317
+ if (!staged.trim()) return { refreshed: false };
9318
+ await exec2(
9319
+ `git commit -m ${shellEscape(
9320
+ "chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)"
9321
+ )} -- .github/workflows`,
9322
+ { cwd: projectDir, timeout: 15e3 }
9323
+ );
9324
+ await pushCurrentBranchForPr({ cwd: projectDir });
9325
+ return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
9326
+ }
9090
9327
  async function mergeFeature(args) {
9091
9328
  const current = readWorkflowState(args.projectDir);
9092
9329
  if (!current) {
@@ -9141,6 +9378,20 @@ async function mergeFeature(args) {
9141
9378
  }
9142
9379
  authVerified = true;
9143
9380
  }
9381
+ const preflightNotes = [];
9382
+ {
9383
+ const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
9384
+ try {
9385
+ const r = await refresh();
9386
+ if (r.refreshed) {
9387
+ preflightNotes.push(`Refreshed stale CI workflows before merge${r.detail ? ` (${r.detail})` : ""}.`);
9388
+ }
9389
+ } catch (err) {
9390
+ preflightNotes.push(
9391
+ `Workflow self-heal skipped (${err instanceof Error ? err.message : String(err)}); if the promote's migrate-target fails on the tier guard, refresh .github/workflows (merge.yml must call 'lakebase-schema-migrate apply-tier').`
9392
+ );
9393
+ }
9394
+ }
9144
9395
  let paired;
9145
9396
  try {
9146
9397
  paired = await mergePairedPullRequest({
@@ -9155,7 +9406,7 @@ async function mergeFeature(args) {
9155
9406
  "merge-failed"
9156
9407
  );
9157
9408
  }
9158
- const warnings = [...paired.warnings];
9409
+ const warnings = [...preflightNotes, ...paired.warnings];
9159
9410
  let localBranchDeleted = false;
9160
9411
  let headAfter = current.branch;
9161
9412
  if (!args.skipLocalCleanup) {
@@ -9349,8 +9600,8 @@ function shellEscape(s) {
9349
9600
 
9350
9601
  // scripts/lakebase/schema-migrate.ts
9351
9602
  init_esm_shims();
9352
- import * as fs10 from "fs";
9353
- import * as path11 from "path";
9603
+ import * as fs11 from "fs";
9604
+ import * as path12 from "path";
9354
9605
 
9355
9606
  // scripts/lakebase/get-connection.ts
9356
9607
  init_esm_shims();
@@ -9364,29 +9615,30 @@ var DEFAULT_DATABASE = "databricks_postgres";
9364
9615
  var DEFAULT_ENDPOINT = "primary";
9365
9616
  var CONSORT_APPLICATION_NAME = "consort";
9366
9617
  var SCM_UTILS_APPLICATION_NAME = "scm-utils";
9618
+ var SCM_CLIENT_ENV = "LAKEBASE_SCM_CLIENT";
9367
9619
  var CONSORT_VERSION_ENV = "CONSORT_VERSION";
9368
9620
 
9369
9621
  // scripts/lakebase/self-version.ts
9370
9622
  init_esm_shims();
9371
- import * as fs4 from "fs";
9372
- import * as path4 from "path";
9623
+ import * as fs5 from "fs";
9624
+ import * as path5 from "path";
9373
9625
  import { fileURLToPath as fileURLToPath3 } from "url";
9374
9626
  var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
9375
9627
  var cached;
9376
9628
  function substrateSelfVersion() {
9377
9629
  if (cached !== void 0) return cached;
9378
- if ("0.2.44".length > 0) {
9379
- cached = "0.2.44";
9630
+ if ("0.2.46".length > 0) {
9631
+ cached = "0.2.46";
9380
9632
  return cached;
9381
9633
  }
9382
9634
  cached = "unknown";
9383
9635
  try {
9384
- let dir = path4.dirname(fileURLToPath3(import.meta.url));
9636
+ let dir = path5.dirname(fileURLToPath3(import.meta.url));
9385
9637
  for (let i = 0; i < 8; i++) {
9386
- const pkgPath = path4.join(dir, "package.json");
9387
- if (fs4.existsSync(pkgPath)) {
9638
+ const pkgPath = path5.join(dir, "package.json");
9639
+ if (fs5.existsSync(pkgPath)) {
9388
9640
  try {
9389
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
9641
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf8"));
9390
9642
  if (pkg.name === PKG_NAME && typeof pkg.version === "string" && pkg.version.length > 0) {
9391
9643
  cached = pkg.version;
9392
9644
  return cached;
@@ -9394,7 +9646,7 @@ function substrateSelfVersion() {
9394
9646
  } catch {
9395
9647
  }
9396
9648
  }
9397
- const parent = path4.dirname(dir);
9649
+ const parent = path5.dirname(dir);
9398
9650
  if (parent === dir) break;
9399
9651
  dir = parent;
9400
9652
  }
@@ -9404,9 +9656,17 @@ function substrateSelfVersion() {
9404
9656
  }
9405
9657
 
9406
9658
  // scripts/lakebase/get-connection.ts
9659
+ function sanitizeApplicationName(raw) {
9660
+ const cleaned = raw.replace(/[^\x21-\x7e]+/g, "-").replace(/^-+|-+$/g, "");
9661
+ if (!cleaned) return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
9662
+ return cleaned.length > 63 ? cleaned.slice(0, 63) : cleaned;
9663
+ }
9407
9664
  function connectionApplicationName() {
9665
+ const client = process.env[SCM_CLIENT_ENV]?.trim();
9666
+ if (client) return sanitizeApplicationName(client);
9408
9667
  const consortVersion = process.env[CONSORT_VERSION_ENV]?.trim();
9409
- return consortVersion ? `${CONSORT_APPLICATION_NAME}/${consortVersion}` : `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
9668
+ if (consortVersion) return `${CONSORT_APPLICATION_NAME}/${consortVersion}`;
9669
+ return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
9410
9670
  }
9411
9671
  async function getConnection(args) {
9412
9672
  const endpointName = args.endpointName ?? DEFAULT_ENDPOINT;
@@ -9481,22 +9741,22 @@ init_esm_shims();
9481
9741
 
9482
9742
  // scripts/lakebase/adapters/alembic-adapter.ts
9483
9743
  init_esm_shims();
9484
- import * as fs6 from "fs";
9485
- import * as path6 from "path";
9744
+ import * as fs7 from "fs";
9745
+ import * as path7 from "path";
9486
9746
 
9487
9747
  // scripts/lakebase/schema-migrate-runners/alembic.ts
9488
9748
  init_esm_shims();
9489
9749
  import { spawn } from "child_process";
9490
- import * as fs5 from "fs";
9491
- import * as path5 from "path";
9750
+ import * as fs6 from "fs";
9751
+ import * as path6 from "path";
9492
9752
  function resolveAlembicBin(projectDir) {
9493
9753
  const candidates = [
9494
- path5.join(projectDir, ".venv", "bin", "alembic"),
9495
- path5.join(projectDir, "venv", "bin", "alembic")
9754
+ path6.join(projectDir, ".venv", "bin", "alembic"),
9755
+ path6.join(projectDir, "venv", "bin", "alembic")
9496
9756
  ];
9497
9757
  for (const candidate of candidates) {
9498
9758
  try {
9499
- if (fs5.existsSync(candidate)) return candidate;
9759
+ if (fs6.existsSync(candidate)) return candidate;
9500
9760
  } catch {
9501
9761
  }
9502
9762
  }
@@ -9506,7 +9766,7 @@ function spawnAlembic(projectDir, args, dsn) {
9506
9766
  return new Promise((resolve2, reject) => {
9507
9767
  const bin = resolveAlembicBin(projectDir);
9508
9768
  const env = { ...process.env };
9509
- env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path5.delimiter);
9769
+ env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path6.delimiter);
9510
9770
  if (dsn) env.DATABASE_URL = dsn;
9511
9771
  const child = spawn(bin, args, {
9512
9772
  cwd: projectDir,
@@ -9554,10 +9814,10 @@ async function createAlembicRevision(opts) {
9554
9814
  const m = stdout.match(/Generating\s+(\S+\.py)/);
9555
9815
  if (m) return m[1].trim();
9556
9816
  for (const rel of ["migrations/versions", "alembic/versions"]) {
9557
- const dir = path5.join(opts.projectDir, rel);
9558
- if (!fs5.existsSync(dir)) continue;
9559
- const hit = fs5.readdirSync(dir).find((f) => f.startsWith(`${opts.revId}_`) && f.endsWith(".py"));
9560
- if (hit) return path5.join(dir, hit);
9817
+ const dir = path6.join(opts.projectDir, rel);
9818
+ if (!fs6.existsSync(dir)) continue;
9819
+ const hit = fs6.readdirSync(dir).find((f) => f.startsWith(`${opts.revId}_`) && f.endsWith(".py"));
9820
+ if (hit) return path6.join(dir, hit);
9561
9821
  }
9562
9822
  throw new SchemaMigrationError(
9563
9823
  `alembic revision succeeded but the created file could not be located.
@@ -9692,15 +9952,15 @@ async function buildDsn(args) {
9692
9952
  }
9693
9953
  function findVersionsDir(projectDir) {
9694
9954
  const candidates = [
9695
- path6.join(projectDir, "migrations", "versions"),
9696
- path6.join(projectDir, "alembic", "versions")
9955
+ path7.join(projectDir, "migrations", "versions"),
9956
+ path7.join(projectDir, "alembic", "versions")
9697
9957
  ];
9698
- return candidates.find((p) => fs6.existsSync(p));
9958
+ return candidates.find((p) => fs7.existsSync(p));
9699
9959
  }
9700
9960
  function listAlembicFiles(projectDir) {
9701
9961
  const dir = findVersionsDir(projectDir);
9702
9962
  if (!dir) return [];
9703
- const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".py") && !f.startsWith("__"));
9963
+ const files = fs7.readdirSync(dir).filter((f) => f.endsWith(".py") && !f.startsWith("__"));
9704
9964
  return files.map((filename) => {
9705
9965
  const stem = filename.replace(/\.py$/, "");
9706
9966
  const sep2 = stem.indexOf("_");
@@ -9725,9 +9985,9 @@ var AlembicAdapter = {
9725
9985
  * here. Callers can still force-select via project.yaml#migration_tool.
9726
9986
  */
9727
9987
  detect(projectDir) {
9728
- if (fs6.existsSync(path6.join(projectDir, "alembic.ini"))) return true;
9729
- if (fs6.existsSync(path6.join(projectDir, "migrations", "env.py"))) return true;
9730
- if (fs6.existsSync(path6.join(projectDir, "alembic", "env.py"))) return true;
9988
+ if (fs7.existsSync(path7.join(projectDir, "alembic.ini"))) return true;
9989
+ if (fs7.existsSync(path7.join(projectDir, "migrations", "env.py"))) return true;
9990
+ if (fs7.existsSync(path7.join(projectDir, "alembic", "env.py"))) return true;
9731
9991
  return false;
9732
9992
  },
9733
9993
  async apply(args) {
@@ -9833,7 +10093,7 @@ var AlembicAdapter = {
9833
10093
  autogenerate: !!args.autogenerate,
9834
10094
  dsn
9835
10095
  });
9836
- return { status: "ok", version: revId, filename: path6.basename(created), path: created };
10096
+ return { status: "ok", version: revId, filename: path7.basename(created), path: created };
9837
10097
  } catch (err) {
9838
10098
  return {
9839
10099
  status: "error",
@@ -9850,7 +10110,7 @@ var AlembicAdapter = {
9850
10110
  if (heads.length <= 1) return { status: "noop", headsBefore: heads };
9851
10111
  if (args.dryRun) return { status: "ok", headsBefore: heads };
9852
10112
  const created = await mergeAlembicHeads(args.projectDir, args.message ?? "merge heads");
9853
- const mergeRevision = path6.basename(created).replace(/\.py$/, "").split("_")[0];
10113
+ const mergeRevision = path7.basename(created).replace(/\.py$/, "").split("_")[0];
9854
10114
  return { status: "ok", headsBefore: heads, mergeRevision, path: created };
9855
10115
  } catch (err) {
9856
10116
  return {
@@ -9865,13 +10125,13 @@ registerSchemaMigrationAdapter(AlembicAdapter);
9865
10125
 
9866
10126
  // scripts/lakebase/adapters/flyway-adapter.ts
9867
10127
  init_esm_shims();
9868
- import * as fs7 from "fs";
9869
- import * as path8 from "path";
10128
+ import * as fs8 from "fs";
10129
+ import * as path9 from "path";
9870
10130
 
9871
10131
  // scripts/lakebase/schema-migrate-runners/flyway.ts
9872
10132
  init_esm_shims();
9873
10133
  import { spawn as spawn2 } from "child_process";
9874
- import * as path7 from "path";
10134
+ import * as path8 from "path";
9875
10135
  function dsnToFlywayEnv(dsn) {
9876
10136
  const u = new URL(dsn);
9877
10137
  const user = decodeURIComponent(u.username);
@@ -9881,7 +10141,7 @@ function dsnToFlywayEnv(dsn) {
9881
10141
  return { url, user, password };
9882
10142
  }
9883
10143
  function migrationsLocation(projectDir) {
9884
- return `filesystem:${path7.join(projectDir, "src", "main", "resources", "db", "migration")}`;
10144
+ return `filesystem:${path8.join(projectDir, "src", "main", "resources", "db", "migration")}`;
9885
10145
  }
9886
10146
  function runFlyway(ctx, args) {
9887
10147
  const { url, user, password } = dsnToFlywayEnv(ctx.dsn);
@@ -9982,7 +10242,7 @@ async function statusFlyway(ctx) {
9982
10242
  if (state === "SUCCESS" || state === "BASELINE") {
9983
10243
  current = m.version;
9984
10244
  } else if (state === "PENDING") {
9985
- const filename = m.filepath ? path7.basename(m.filepath) : `V${m.version}__migration.sql`;
10245
+ const filename = m.filepath ? path8.basename(m.filepath) : `V${m.version}__migration.sql`;
9986
10246
  pending.push({
9987
10247
  version: m.version,
9988
10248
  filename,
@@ -10005,9 +10265,9 @@ async function buildDsn2(args) {
10005
10265
  return result.url;
10006
10266
  }
10007
10267
  function listFlywayFiles(projectDir) {
10008
- const dir = path8.join(projectDir, "src", "main", "resources", "db", "migration");
10009
- if (!fs7.existsSync(dir)) return [];
10010
- const files = fs7.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
10268
+ const dir = path9.join(projectDir, "src", "main", "resources", "db", "migration");
10269
+ if (!fs8.existsSync(dir)) return [];
10270
+ const files = fs8.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
10011
10271
  return files.map((filename) => {
10012
10272
  const m = filename.match(/^V(\d+(?:\.\d+)*)__(.+)\.sql$/);
10013
10273
  const version = m[1];
@@ -10030,7 +10290,7 @@ var FlywayAdapter = {
10030
10290
  id: "flyway",
10031
10291
  languages: ["java", "kotlin"],
10032
10292
  detect(projectDir) {
10033
- return fs7.existsSync(path8.join(projectDir, "pom.xml"));
10293
+ return fs8.existsSync(path9.join(projectDir, "pom.xml"));
10034
10294
  },
10035
10295
  async apply(args) {
10036
10296
  const dsn = await buildDsn2(args);
@@ -10088,14 +10348,14 @@ var FlywayAdapter = {
10088
10348
  // optional-protocol shape makes this additive.
10089
10349
  async newMigration(args) {
10090
10350
  try {
10091
- const dir = path8.join(args.projectDir, "src", "main", "resources", "db", "migration");
10092
- fs7.mkdirSync(dir, { recursive: true });
10351
+ const dir = path9.join(args.projectDir, "src", "main", "resources", "db", "migration");
10352
+ fs8.mkdirSync(dir, { recursive: true });
10093
10353
  const version = migrationTimestamp();
10094
10354
  const slug = migrationSlug2(args.slug);
10095
10355
  const filename = `V${version}__${slug}.sql`;
10096
- const full = path8.join(dir, filename);
10097
- if (fs7.existsSync(full)) throw new Error(`${filename} already exists`);
10098
- fs7.writeFileSync(
10356
+ const full = path9.join(dir, filename);
10357
+ if (fs8.existsSync(full)) throw new Error(`${filename} already exists`);
10358
+ fs8.writeFileSync(
10099
10359
  full,
10100
10360
  `-- V${version}: ${args.slug}
10101
10361
  -- Flyway migration (write your DDL/DML below).
@@ -10118,19 +10378,19 @@ registerSchemaMigrationAdapter(FlywayAdapter);
10118
10378
 
10119
10379
  // scripts/lakebase/adapters/knex-adapter.ts
10120
10380
  init_esm_shims();
10121
- import * as fs9 from "fs";
10122
- import * as path10 from "path";
10381
+ import * as fs10 from "fs";
10382
+ import * as path11 from "path";
10123
10383
 
10124
10384
  // scripts/lakebase/schema-migrate-runners/knex.ts
10125
10385
  init_esm_shims();
10126
10386
  import { spawn as spawn3 } from "child_process";
10127
- import * as fs8 from "fs";
10128
- import * as path9 from "path";
10387
+ import * as fs9 from "fs";
10388
+ import * as path10 from "path";
10129
10389
  var KNEXFILE_VARIANTS = ["knexfile.js", "knexfile.ts", "knexfile.mjs", "knexfile.cjs"];
10130
10390
  function findKnexfile(projectDir) {
10131
10391
  for (const name of KNEXFILE_VARIANTS) {
10132
- const p = path9.join(projectDir, name);
10133
- if (fs8.existsSync(p)) return p;
10392
+ const p = path10.join(projectDir, name);
10393
+ if (fs9.existsSync(p)) return p;
10134
10394
  }
10135
10395
  return void 0;
10136
10396
  }
@@ -10282,9 +10542,9 @@ async function buildDsn3(args) {
10282
10542
  }
10283
10543
  var KNEXFILE_VARIANTS2 = ["knexfile.js", "knexfile.ts", "knexfile.mjs", "knexfile.cjs"];
10284
10544
  function listKnexFiles(projectDir) {
10285
- const dir = path10.join(projectDir, "migrations");
10286
- if (!fs9.existsSync(dir)) return [];
10287
- const files = fs9.readdirSync(dir).filter((f) => (f.endsWith(".js") || f.endsWith(".ts")) && !f.startsWith("."));
10545
+ const dir = path11.join(projectDir, "migrations");
10546
+ if (!fs10.existsSync(dir)) return [];
10547
+ const files = fs10.readdirSync(dir).filter((f) => (f.endsWith(".js") || f.endsWith(".ts")) && !f.startsWith("."));
10288
10548
  return files.map((filename) => {
10289
10549
  const stem = filename.replace(/\.(js|ts)$/, "");
10290
10550
  const m = stem.match(/^(\d{14})_(.+)$/);
@@ -10304,7 +10564,7 @@ var KnexAdapter = {
10304
10564
  * project.yaml#migration_tool.
10305
10565
  */
10306
10566
  detect(projectDir) {
10307
- return KNEXFILE_VARIANTS2.some((name) => fs9.existsSync(path10.join(projectDir, name)));
10567
+ return KNEXFILE_VARIANTS2.some((name) => fs10.existsSync(path11.join(projectDir, name)));
10308
10568
  },
10309
10569
  async apply(args) {
10310
10570
  const dsn = await buildDsn3(args);
@@ -10377,9 +10637,9 @@ var KnexAdapter = {
10377
10637
  async newMigration(args) {
10378
10638
  try {
10379
10639
  const created = await createKnexMigration({ projectDir: args.projectDir, slug: migrationSlug2(args.slug) });
10380
- const stem = path10.basename(created).replace(/\.(js|ts)$/, "");
10640
+ const stem = path11.basename(created).replace(/\.(js|ts)$/, "");
10381
10641
  const version = stem.match(/^(\d{14})_/)?.[1] ?? stem;
10382
- return { status: "ok", version, filename: path10.basename(created), path: created };
10642
+ return { status: "ok", version, filename: path11.basename(created), path: created };
10383
10643
  } catch (err) {
10384
10644
  return {
10385
10645
  status: "error",
@@ -10672,7 +10932,7 @@ async function runScmMergeCli(argv) {
10672
10932
  `);
10673
10933
  return 0;
10674
10934
  }
10675
- const projectDir = path12.resolve(args.projectDir ?? process.cwd());
10935
+ const projectDir = path13.resolve(args.projectDir ?? process.cwd());
10676
10936
  const waitMigrate = args.noWaitMigrate ? false : true;
10677
10937
  const verifyMigrateAuth = waitMigrate && !args.noVerifyMigrateAuth ? async () => {
10678
10938
  try {