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

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 (39) hide show
  1. package/dist/scripts/index.cjs +795 -758
  2. package/dist/scripts/index.d.cts +1 -1
  3. package/dist/scripts/index.d.ts +1 -1
  4. package/dist/scripts/index.js +794 -758
  5. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2 -2
  6. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +2 -2
  7. package/dist/scripts/lakebase/collapse-heads.cli.cjs +2 -2
  8. package/dist/scripts/lakebase/collapse-heads.cli.js +2 -2
  9. package/dist/scripts/lakebase/detect-language.cli.cjs +2 -2
  10. package/dist/scripts/lakebase/detect-language.cli.js +2 -2
  11. package/dist/scripts/lakebase/doctor.cli.cjs +3 -3
  12. package/dist/scripts/lakebase/doctor.cli.js +3 -3
  13. package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
  14. package/dist/scripts/lakebase/get-connection.cli.js +2 -2
  15. package/dist/scripts/lakebase/index.cjs +463 -395
  16. package/dist/scripts/lakebase/index.d.cts +33 -1
  17. package/dist/scripts/lakebase/index.d.ts +33 -1
  18. package/dist/scripts/lakebase/index.js +462 -395
  19. package/dist/scripts/lakebase/infra-runner.cli.cjs +2 -2
  20. package/dist/scripts/lakebase/infra-runner.cli.js +2 -2
  21. package/dist/scripts/lakebase/new-migration.cli.cjs +2 -2
  22. package/dist/scripts/lakebase/new-migration.cli.js +2 -2
  23. package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
  24. package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
  25. package/dist/scripts/lakebase/schema-migrate.cli.cjs +2 -2
  26. package/dist/scripts/lakebase/schema-migrate.cli.js +2 -2
  27. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2 -2
  28. package/dist/scripts/lakebase/scm-claim-feature.cli.js +2 -2
  29. package/dist/scripts/lakebase/scm-doctor.cli.cjs +2 -2
  30. package/dist/scripts/lakebase/scm-doctor.cli.js +2 -2
  31. package/dist/scripts/lakebase/scm-merge.cli.cjs +2284 -2033
  32. package/dist/scripts/lakebase/scm-merge.cli.js +339 -88
  33. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2 -2
  34. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +2 -2
  35. package/dist/scripts/util/index.cjs +2 -2
  36. package/dist/scripts/util/index.js +2 -2
  37. package/package.json +1 -1
  38. package/scripts/lakebase/doctor.ts +1 -1
  39. package/scripts/lakebase/scm-merge.ts +80 -1
@@ -2517,8 +2517,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
2517
2517
  var cached;
2518
2518
  function substrateSelfVersion() {
2519
2519
  if (cached !== void 0) return cached;
2520
- if ("0.2.44".length > 0) {
2521
- cached = "0.2.44";
2520
+ if ("0.2.45".length > 0) {
2521
+ cached = "0.2.45";
2522
2522
  return cached;
2523
2523
  }
2524
2524
  cached = "unknown";
@@ -2746,6 +2746,14 @@ async function getCredential(args) {
2746
2746
 
2747
2747
  // scripts/git/status.ts
2748
2748
  init_esm_shims();
2749
+ async function hasUpstream(args) {
2750
+ try {
2751
+ await exec2("git rev-parse --abbrev-ref @{u}", { cwd: args.cwd });
2752
+ return true;
2753
+ } catch {
2754
+ return false;
2755
+ }
2756
+ }
2749
2757
  async function getAheadBehind(args) {
2750
2758
  const { cwd } = args;
2751
2759
  try {
@@ -15849,118 +15857,461 @@ function pickRunUrl(pr) {
15849
15857
 
15850
15858
  // scripts/lakebase/scm-merge.ts
15851
15859
  init_esm_shims();
15852
- async function reconcileTierToOrigin(args) {
15853
- const { cwd, tier } = args;
15860
+
15861
+ // scripts/git/sync.ts
15862
+ init_esm_shims();
15863
+ async function currentBranchName(cwd) {
15854
15864
  try {
15855
- const head = await getCurrentBranch({ cwd });
15856
- if (head === tier) {
15857
- await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
15858
- } else {
15859
- await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
15860
- }
15861
- return null;
15862
- } catch (err) {
15863
- return `local reconcile of ${tier} to origin/${tier} failed: ${err instanceof Error ? err.message : String(err)}. Reconcile it by hand (git checkout ${tier} && git pull --ff-only).`;
15865
+ return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
15866
+ } catch {
15867
+ return "";
15864
15868
  }
15865
15869
  }
15866
- var ScmMergeError = class extends Error {
15867
- constructor(message, code) {
15868
- super(message);
15869
- this.code = code;
15870
- this.name = "ScmMergeError";
15870
+ async function pushCurrentBranchForPr(args) {
15871
+ const remote = args.remote ?? "origin";
15872
+ const branch = await currentBranchName(args.cwd);
15873
+ if (!branch) throw new Error("No current branch");
15874
+ const upstreamSet = await hasUpstream({ cwd: args.cwd });
15875
+ if (!upstreamSet) {
15876
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
15877
+ cwd: args.cwd
15878
+ });
15879
+ } else {
15880
+ await exec2("git push", { cwd: args.cwd });
15871
15881
  }
15872
- code;
15873
- };
15874
- var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
15875
- var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
15876
- function defaultMigratePredicate(run, mergedAt) {
15877
- if (!run.createdAt) return false;
15878
- const created = Date.parse(run.createdAt);
15879
- if (!Number.isFinite(created)) return false;
15880
- if (run.event && run.event !== "push") return false;
15881
- return created >= mergedAt.getTime() - 5e3;
15882
- }
15883
- function shaMigratePredicate(mergeCommitSha) {
15884
- return (run) => {
15885
- if (run.event && run.event !== "push") return false;
15886
- return !!run.headSha && run.headSha === mergeCommitSha;
15887
- };
15888
15882
  }
15889
- async function mergeFeature(args) {
15890
- const current = readWorkflowState(args.projectDir);
15891
- if (!current) {
15892
- throw new ScmMergeError(
15893
- "No SCM workflow state. wait-ci first.",
15894
- "no-state-file"
15883
+
15884
+ // scripts/lakebase/workflow-drift.ts
15885
+ init_esm_shims();
15886
+ import * as fs29 from "fs";
15887
+ import * as path29 from "path";
15888
+ function findKitTemplatesDir(start) {
15889
+ let dir = start;
15890
+ for (let i = 0; i < 6; i++) {
15891
+ const candidate = path29.join(
15892
+ dir,
15893
+ "templates",
15894
+ "project",
15895
+ "common",
15896
+ ".github",
15897
+ "workflows"
15895
15898
  );
15899
+ if (fs29.existsSync(candidate)) return candidate;
15900
+ const parent = path29.dirname(dir);
15901
+ if (parent === dir) break;
15902
+ dir = parent;
15896
15903
  }
15897
- if (current.state !== "ci-green") {
15898
- throw new ScmMergeError(
15899
- `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
15900
- "bad-precondition"
15901
- );
15904
+ throw new Error(
15905
+ `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
15906
+ );
15907
+ }
15908
+ function unifiedDiff(name, projectContent, templateContent) {
15909
+ if (projectContent === templateContent) return "";
15910
+ const a = projectContent.split("\n");
15911
+ const b = templateContent.split("\n");
15912
+ const out = [`--- project/${name}`, `+++ template/${name}`];
15913
+ const max = Math.max(a.length, b.length);
15914
+ for (let i = 0; i < max; i++) {
15915
+ const av = a[i];
15916
+ const bv = b[i];
15917
+ if (av === bv) continue;
15918
+ if (av !== void 0) out.push(`-${i + 1}: ${av}`);
15919
+ if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
15902
15920
  }
15903
- if (!current.pr_url) {
15904
- throw new ScmMergeError(
15905
- "ci-green row is missing pr_url; cannot resolve the PR to merge.",
15906
- "no-pr-url"
15907
- );
15921
+ return out.join("\n");
15922
+ }
15923
+ function detectWorkflowDrift(args) {
15924
+ const projectWorkflowsDir = path29.join(
15925
+ args.projectDir,
15926
+ ".github",
15927
+ "workflows"
15928
+ );
15929
+ const here = path29.dirname(new URL(import.meta.url).pathname);
15930
+ const kitWorkflowsDir = args.kitDir ? path29.join(
15931
+ args.kitDir,
15932
+ "templates",
15933
+ "project",
15934
+ "common",
15935
+ ".github",
15936
+ "workflows"
15937
+ ) : findKitTemplatesDir(here);
15938
+ const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
15939
+ const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
15940
+ const version = readKitVersion(kitWorkflowsDir);
15941
+ const seen = /* @__PURE__ */ new Set();
15942
+ const files = [];
15943
+ for (const name of templateFiles) {
15944
+ seen.add(name);
15945
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
15946
+ const templatePath = path29.join(kitWorkflowsDir, name);
15947
+ if (!fs29.existsSync(projectPath2)) {
15948
+ files.push({ name, status: "missing" });
15949
+ continue;
15950
+ }
15951
+ const projectContent = fs29.readFileSync(projectPath2, "utf8");
15952
+ const templateContent = applyPlaceholders(fs29.readFileSync(templatePath, "utf8"), version);
15953
+ if (projectContent === templateContent) {
15954
+ files.push({ name, status: "unchanged" });
15955
+ } else {
15956
+ files.push({
15957
+ name,
15958
+ status: "drifted",
15959
+ diff: unifiedDiff(name, projectContent, templateContent)
15960
+ });
15961
+ }
15908
15962
  }
15909
- if (!current.branch || !current.parent_branch) {
15910
- throw new ScmMergeError(
15911
- "ci-green row missing branch / parent_branch; refusing to merge.",
15912
- "bad-precondition"
15913
- );
15963
+ for (const name of projectFiles) {
15964
+ if (seen.has(name)) continue;
15965
+ files.push({ name, status: "extra" });
15914
15966
  }
15915
- const ownerRepo = await getOwnerRepo(args.projectDir);
15916
- if (!ownerRepo) {
15917
- throw new ScmMergeError(
15918
- "No GitHub remote found at origin.",
15919
- "no-github-remote"
15920
- );
15967
+ const order = {
15968
+ drifted: 0,
15969
+ missing: 1,
15970
+ extra: 2,
15971
+ unchanged: 3
15972
+ };
15973
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
15974
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
15975
+ return {
15976
+ overall: hasDrift ? "drift" : "ok",
15977
+ files
15978
+ };
15979
+ }
15980
+ function readKitVersion(kitWorkflowsDir) {
15981
+ let dir = kitWorkflowsDir;
15982
+ for (let i = 0; i < 5; i++) {
15983
+ dir = path29.dirname(dir);
15921
15984
  }
15922
- const pullNumber = extractPullNumber(current.pr_url);
15923
- if (!pullNumber) {
15924
- throw new ScmMergeError(
15925
- `Could not extract PR number from URL: ${current.pr_url}`,
15926
- "bad-pr-url"
15985
+ try {
15986
+ const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
15987
+ const pkg = JSON.parse(raw);
15988
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
15989
+ } catch {
15990
+ return "unknown";
15991
+ }
15992
+ }
15993
+ function applyPlaceholders(content, version) {
15994
+ return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
15995
+ }
15996
+ function applyCommandPlaceholders(content, version) {
15997
+ return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
15998
+ }
15999
+ var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
16000
+ function findKitCommandsDir(start) {
16001
+ let dir = start;
16002
+ for (let i = 0; i < 6; i++) {
16003
+ const candidate = path29.join(
16004
+ dir,
16005
+ "templates",
16006
+ "project",
16007
+ "common",
16008
+ ".claude",
16009
+ "commands"
15927
16010
  );
16011
+ if (fs29.existsSync(candidate)) return candidate;
16012
+ const parent = path29.dirname(dir);
16013
+ if (parent === dir) break;
16014
+ dir = parent;
15928
16015
  }
15929
- const instance = args.instance ?? current.project_id;
15930
- const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
15931
- const wantMigrate = args.waitMigrate !== false;
15932
- let authVerified = false;
15933
- if (wantMigrate && args.verifyMigrateAuth) {
15934
- const probe = await args.verifyMigrateAuth();
15935
- if (!probe.ok) {
15936
- throw new ScmMergeError(
15937
- `Migrate-auth precondition failed: ${probe.detail ?? "the Databricks credential is not usable"}. Refusing to merge: the downstream migrate would promote git without applying the schema. Refresh your credential (databricks auth login), then re-run.`,
15938
- "migrate-auth"
15939
- );
16016
+ throw new Error(
16017
+ `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
16018
+ );
16019
+ }
16020
+ function parsePinnedVersion(content) {
16021
+ const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
16022
+ return m ? m[1] : void 0;
16023
+ }
16024
+ function detectCommandDrift(args) {
16025
+ const projectCommandsDir = path29.join(args.projectDir, ".claude", "commands");
16026
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16027
+ const kitCommandsDir = args.kitDir ? path29.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
16028
+ const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
16029
+ const templateFiles = fs29.existsSync(kitCommandsDir) ? fs29.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16030
+ const projectFiles = fs29.existsSync(projectCommandsDir) ? fs29.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16031
+ const seen = /* @__PURE__ */ new Set();
16032
+ const files = [];
16033
+ for (const name of templateFiles) {
16034
+ seen.add(name);
16035
+ const projectPath2 = path29.join(projectCommandsDir, name);
16036
+ const templatePath = path29.join(kitCommandsDir, name);
16037
+ const templateRaw = fs29.readFileSync(templatePath, "utf8");
16038
+ if (!fs29.existsSync(projectPath2)) {
16039
+ files.push({ name, status: "missing", kit_version: kitVersion });
16040
+ continue;
16041
+ }
16042
+ const projectContent = fs29.readFileSync(projectPath2, "utf8");
16043
+ const pinned = parsePinnedVersion(projectContent);
16044
+ const versionForCompare = pinned ?? kitVersion;
16045
+ const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
16046
+ if (projectContent === templateContent) {
16047
+ files.push({
16048
+ name,
16049
+ status: "unchanged",
16050
+ pinned_version: pinned,
16051
+ kit_version: kitVersion
16052
+ });
16053
+ } else {
16054
+ files.push({
16055
+ name,
16056
+ status: "drifted",
16057
+ pinned_version: pinned,
16058
+ kit_version: kitVersion,
16059
+ diff: unifiedDiff(name, projectContent, templateContent)
16060
+ });
15940
16061
  }
15941
- authVerified = true;
15942
16062
  }
15943
- let paired;
15944
- try {
15945
- paired = await mergePairedPullRequest({
15946
- ownerRepo,
15947
- pullNumber,
15948
- lakebaseInstance: instance,
15949
- method: args.method ?? "squash"
15950
- });
15951
- } catch (err) {
15952
- throw new ScmMergeError(
15953
- `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
15954
- "merge-failed"
15955
- );
16063
+ for (const name of projectFiles) {
16064
+ if (seen.has(name)) continue;
16065
+ files.push({ name, status: "extra", kit_version: kitVersion });
15956
16066
  }
15957
- const warnings = [...paired.warnings];
15958
- let localBranchDeleted = false;
15959
- let headAfter = current.branch;
15960
- if (!args.skipLocalCleanup) {
15961
- const switchTo = args.switchTo ?? gitBase;
15962
- const head = await getCurrentBranch({ cwd: args.projectDir });
15963
- if (head === current.branch) {
16067
+ const order = {
16068
+ drifted: 0,
16069
+ missing: 1,
16070
+ extra: 2,
16071
+ unchanged: 3
16072
+ };
16073
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16074
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16075
+ return { overall: hasDrift ? "drift" : "ok", files };
16076
+ }
16077
+ function readKitVersionFromCommandsDir(kitCommandsDir) {
16078
+ let dir = kitCommandsDir;
16079
+ for (let i = 0; i < 5; i++) {
16080
+ dir = path29.dirname(dir);
16081
+ }
16082
+ try {
16083
+ const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
16084
+ const pkg = JSON.parse(raw);
16085
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
16086
+ } catch {
16087
+ return "unknown";
16088
+ }
16089
+ }
16090
+ function detectScaffoldedDrift(args) {
16091
+ const workflows = detectWorkflowDrift(args);
16092
+ const commands = detectCommandDrift(args);
16093
+ return {
16094
+ overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
16095
+ workflows,
16096
+ commands
16097
+ };
16098
+ }
16099
+ function updateWorkflows(args) {
16100
+ const projectWorkflowsDir = path29.join(
16101
+ args.projectDir,
16102
+ ".github",
16103
+ "workflows"
16104
+ );
16105
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16106
+ const kitWorkflowsDir = args.kitDir ? path29.join(
16107
+ args.kitDir,
16108
+ "templates",
16109
+ "project",
16110
+ "common",
16111
+ ".github",
16112
+ "workflows"
16113
+ ) : findKitTemplatesDir(here);
16114
+ const substitute = args.substitute !== false;
16115
+ const dryRun = args.dryRun === true;
16116
+ const pruneExtras = args.pruneExtras === true;
16117
+ const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16118
+ const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16119
+ if (!dryRun && templateFiles.length > 0 && !fs29.existsSync(projectWorkflowsDir)) {
16120
+ fs29.mkdirSync(projectWorkflowsDir, { recursive: true });
16121
+ }
16122
+ const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
16123
+ const seen = /* @__PURE__ */ new Set();
16124
+ const files = [];
16125
+ for (const name of templateFiles) {
16126
+ seen.add(name);
16127
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16128
+ const templatePath = path29.join(kitWorkflowsDir, name);
16129
+ const templateRaw = fs29.readFileSync(templatePath, "utf-8");
16130
+ const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
16131
+ const existed = fs29.existsSync(projectPath2);
16132
+ const current = existed ? fs29.readFileSync(projectPath2, "utf-8") : "";
16133
+ let outcome;
16134
+ if (!existed) {
16135
+ outcome = "added";
16136
+ } else if (current === desired) {
16137
+ outcome = "unchanged";
16138
+ } else {
16139
+ outcome = "updated";
16140
+ }
16141
+ if (!dryRun && outcome !== "unchanged") {
16142
+ fs29.writeFileSync(projectPath2, desired);
16143
+ }
16144
+ files.push({ name, outcome });
16145
+ }
16146
+ if (pruneExtras) {
16147
+ for (const name of projectFiles) {
16148
+ if (seen.has(name)) continue;
16149
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16150
+ if (!dryRun) {
16151
+ fs29.unlinkSync(projectPath2);
16152
+ }
16153
+ files.push({ name, outcome: "removed" });
16154
+ }
16155
+ }
16156
+ const order = {
16157
+ added: 0,
16158
+ updated: 1,
16159
+ removed: 2,
16160
+ unchanged: 3
16161
+ };
16162
+ files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
16163
+ const changed = files.some((f) => f.outcome !== "unchanged");
16164
+ return { files, changed };
16165
+ }
16166
+
16167
+ // scripts/lakebase/scm-merge.ts
16168
+ async function reconcileTierToOrigin(args) {
16169
+ const { cwd, tier } = args;
16170
+ try {
16171
+ const head = await getCurrentBranch({ cwd });
16172
+ if (head === tier) {
16173
+ await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
16174
+ } else {
16175
+ await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
16176
+ }
16177
+ return null;
16178
+ } catch (err) {
16179
+ return `local reconcile of ${tier} to origin/${tier} failed: ${err instanceof Error ? err.message : String(err)}. Reconcile it by hand (git checkout ${tier} && git pull --ff-only).`;
16180
+ }
16181
+ }
16182
+ var ScmMergeError = class extends Error {
16183
+ constructor(message, code) {
16184
+ super(message);
16185
+ this.code = code;
16186
+ this.name = "ScmMergeError";
16187
+ }
16188
+ code;
16189
+ };
16190
+ var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
16191
+ var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
16192
+ function defaultMigratePredicate(run, mergedAt) {
16193
+ if (!run.createdAt) return false;
16194
+ const created = Date.parse(run.createdAt);
16195
+ if (!Number.isFinite(created)) return false;
16196
+ if (run.event && run.event !== "push") return false;
16197
+ return created >= mergedAt.getTime() - 5e3;
16198
+ }
16199
+ function shaMigratePredicate(mergeCommitSha) {
16200
+ return (run) => {
16201
+ if (run.event && run.event !== "push") return false;
16202
+ return !!run.headSha && run.headSha === mergeCommitSha;
16203
+ };
16204
+ }
16205
+ async function defaultRefreshPromoteWorkflows(projectDir) {
16206
+ const drift = detectWorkflowDrift({ projectDir });
16207
+ const touched = drift.files.filter((f) => f.status === "drifted").map((f) => f.name);
16208
+ if (touched.length === 0) return { refreshed: false };
16209
+ const result = updateWorkflows({ projectDir });
16210
+ if (!result.changed) return { refreshed: false };
16211
+ await exec2(`git add ${shellEscape2(".github/workflows")}`, { cwd: projectDir, timeout: 1e4 });
16212
+ const staged = await exec2("git diff --cached --name-only -- .github/workflows", {
16213
+ cwd: projectDir,
16214
+ timeout: 1e4
16215
+ });
16216
+ if (!staged.trim()) return { refreshed: false };
16217
+ await exec2(
16218
+ `git commit -m ${shellEscape2(
16219
+ "chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)"
16220
+ )} -- .github/workflows`,
16221
+ { cwd: projectDir, timeout: 15e3 }
16222
+ );
16223
+ await pushCurrentBranchForPr({ cwd: projectDir });
16224
+ return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
16225
+ }
16226
+ async function mergeFeature(args) {
16227
+ const current = readWorkflowState(args.projectDir);
16228
+ if (!current) {
16229
+ throw new ScmMergeError(
16230
+ "No SCM workflow state. wait-ci first.",
16231
+ "no-state-file"
16232
+ );
16233
+ }
16234
+ if (current.state !== "ci-green") {
16235
+ throw new ScmMergeError(
16236
+ `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
16237
+ "bad-precondition"
16238
+ );
16239
+ }
16240
+ if (!current.pr_url) {
16241
+ throw new ScmMergeError(
16242
+ "ci-green row is missing pr_url; cannot resolve the PR to merge.",
16243
+ "no-pr-url"
16244
+ );
16245
+ }
16246
+ if (!current.branch || !current.parent_branch) {
16247
+ throw new ScmMergeError(
16248
+ "ci-green row missing branch / parent_branch; refusing to merge.",
16249
+ "bad-precondition"
16250
+ );
16251
+ }
16252
+ const ownerRepo = await getOwnerRepo(args.projectDir);
16253
+ if (!ownerRepo) {
16254
+ throw new ScmMergeError(
16255
+ "No GitHub remote found at origin.",
16256
+ "no-github-remote"
16257
+ );
16258
+ }
16259
+ const pullNumber = extractPullNumber(current.pr_url);
16260
+ if (!pullNumber) {
16261
+ throw new ScmMergeError(
16262
+ `Could not extract PR number from URL: ${current.pr_url}`,
16263
+ "bad-pr-url"
16264
+ );
16265
+ }
16266
+ const instance = args.instance ?? current.project_id;
16267
+ const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
16268
+ const wantMigrate = args.waitMigrate !== false;
16269
+ let authVerified = false;
16270
+ if (wantMigrate && args.verifyMigrateAuth) {
16271
+ const probe = await args.verifyMigrateAuth();
16272
+ if (!probe.ok) {
16273
+ throw new ScmMergeError(
16274
+ `Migrate-auth precondition failed: ${probe.detail ?? "the Databricks credential is not usable"}. Refusing to merge: the downstream migrate would promote git without applying the schema. Refresh your credential (databricks auth login), then re-run.`,
16275
+ "migrate-auth"
16276
+ );
16277
+ }
16278
+ authVerified = true;
16279
+ }
16280
+ const preflightNotes = [];
16281
+ {
16282
+ const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
16283
+ try {
16284
+ const r = await refresh();
16285
+ if (r.refreshed) {
16286
+ preflightNotes.push(`Refreshed stale CI workflows before merge${r.detail ? ` (${r.detail})` : ""}.`);
16287
+ }
16288
+ } catch (err) {
16289
+ preflightNotes.push(
16290
+ `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').`
16291
+ );
16292
+ }
16293
+ }
16294
+ let paired;
16295
+ try {
16296
+ paired = await mergePairedPullRequest({
16297
+ ownerRepo,
16298
+ pullNumber,
16299
+ lakebaseInstance: instance,
16300
+ method: args.method ?? "squash"
16301
+ });
16302
+ } catch (err) {
16303
+ throw new ScmMergeError(
16304
+ `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
16305
+ "merge-failed"
16306
+ );
16307
+ }
16308
+ const warnings = [...preflightNotes, ...paired.warnings];
16309
+ let localBranchDeleted = false;
16310
+ let headAfter = current.branch;
16311
+ if (!args.skipLocalCleanup) {
16312
+ const switchTo = args.switchTo ?? gitBase;
16313
+ const head = await getCurrentBranch({ cwd: args.projectDir });
16314
+ if (head === current.branch) {
15964
16315
  try {
15965
16316
  await exec2(`git checkout ${shellEscape2(switchTo)}`, {
15966
16317
  cwd: args.projectDir,
@@ -16151,7 +16502,7 @@ init_esm_shims();
16151
16502
 
16152
16503
  // scripts/git/branches.ts
16153
16504
  init_esm_shims();
16154
- async function currentBranchName(cwd) {
16505
+ async function currentBranchName2(cwd) {
16155
16506
  try {
16156
16507
  return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
16157
16508
  } catch {
@@ -16170,7 +16521,7 @@ async function listLocalBranches(args) {
16170
16521
  return [];
16171
16522
  }
16172
16523
  if (!raw) return [];
16173
- const current = await currentBranchName(cwd);
16524
+ const current = await currentBranchName2(cwd);
16174
16525
  return raw.split("\n").filter(Boolean).map((line) => {
16175
16526
  const [name, tracking, trackInfo] = line.split("|");
16176
16527
  let ahead = 0;
@@ -16335,15 +16686,15 @@ function parentForTopology(t, defaultLeaf) {
16335
16686
 
16336
16687
  // scripts/lakebase/scm-doctor.ts
16337
16688
  init_esm_shims();
16338
- import * as fs29 from "fs";
16339
- import * as path29 from "path";
16689
+ import * as fs30 from "fs";
16690
+ import * as path30 from "path";
16340
16691
  var FEATURE_PREFIX = "feature/";
16341
16692
  var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
16342
16693
  function readEnv(projectDir) {
16343
- const envPath = path29.join(projectDir, ".env");
16694
+ const envPath = path30.join(projectDir, ".env");
16344
16695
  const out = /* @__PURE__ */ new Map();
16345
- if (!fs29.existsSync(envPath)) return out;
16346
- const lines = fs29.readFileSync(envPath, "utf8").split("\n");
16696
+ if (!fs30.existsSync(envPath)) return out;
16697
+ const lines = fs30.readFileSync(envPath, "utf8").split("\n");
16347
16698
  for (const line of lines) {
16348
16699
  const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
16349
16700
  if (m) out.set(m[1], m[2].replace(/^["']|["']$/g, ""));
@@ -16398,9 +16749,9 @@ async function runDoctor(args, deps = {}) {
16398
16749
  }
16399
16750
  for (const wf of ["pr.yml", "merge.yml"]) {
16400
16751
  try {
16401
- const p = path29.join(projectDir, ".github", "workflows", wf);
16402
- if (!fs29.existsSync(p)) continue;
16403
- const body = fs29.readFileSync(p, "utf8");
16752
+ const p = path30.join(projectDir, ".github", "workflows", wf);
16753
+ if (!fs30.existsSync(p)) continue;
16754
+ const body = fs30.readFileSync(p, "utf8");
16404
16755
  if (/lakebase-scm-utils#v\d/.test(body)) {
16405
16756
  findings.push({
16406
16757
  id: "ci-workflow-substrate-pin",
@@ -16615,7 +16966,7 @@ async function fixFinding(args) {
16615
16966
  );
16616
16967
  }
16617
16968
  const sanitized = sanitizeBranchName(branch);
16618
- const envFile = path29.join(args.projectDir, ".env");
16969
+ const envFile = path30.join(args.projectDir, ".env");
16619
16970
  updateEnvConnection({
16620
16971
  envPath: envFile,
16621
16972
  projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
@@ -16720,291 +17071,6 @@ init_esm_shims();
16720
17071
  import * as fs31 from "fs";
16721
17072
  import * as path31 from "path";
16722
17073
  import * as cp6 from "child_process";
16723
-
16724
- // scripts/lakebase/workflow-drift.ts
16725
- init_esm_shims();
16726
- import * as fs30 from "fs";
16727
- import * as path30 from "path";
16728
- function findKitTemplatesDir(start) {
16729
- let dir = start;
16730
- for (let i = 0; i < 6; i++) {
16731
- const candidate = path30.join(
16732
- dir,
16733
- "templates",
16734
- "project",
16735
- "common",
16736
- ".github",
16737
- "workflows"
16738
- );
16739
- if (fs30.existsSync(candidate)) return candidate;
16740
- const parent = path30.dirname(dir);
16741
- if (parent === dir) break;
16742
- dir = parent;
16743
- }
16744
- throw new Error(
16745
- `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
16746
- );
16747
- }
16748
- function unifiedDiff(name, projectContent, templateContent) {
16749
- if (projectContent === templateContent) return "";
16750
- const a = projectContent.split("\n");
16751
- const b = templateContent.split("\n");
16752
- const out = [`--- project/${name}`, `+++ template/${name}`];
16753
- const max = Math.max(a.length, b.length);
16754
- for (let i = 0; i < max; i++) {
16755
- const av = a[i];
16756
- const bv = b[i];
16757
- if (av === bv) continue;
16758
- if (av !== void 0) out.push(`-${i + 1}: ${av}`);
16759
- if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
16760
- }
16761
- return out.join("\n");
16762
- }
16763
- function detectWorkflowDrift(args) {
16764
- const projectWorkflowsDir = path30.join(
16765
- args.projectDir,
16766
- ".github",
16767
- "workflows"
16768
- );
16769
- const here = path30.dirname(new URL(import.meta.url).pathname);
16770
- const kitWorkflowsDir = args.kitDir ? path30.join(
16771
- args.kitDir,
16772
- "templates",
16773
- "project",
16774
- "common",
16775
- ".github",
16776
- "workflows"
16777
- ) : findKitTemplatesDir(here);
16778
- const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16779
- const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16780
- const version = readKitVersion(kitWorkflowsDir);
16781
- const seen = /* @__PURE__ */ new Set();
16782
- const files = [];
16783
- for (const name of templateFiles) {
16784
- seen.add(name);
16785
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16786
- const templatePath = path30.join(kitWorkflowsDir, name);
16787
- if (!fs30.existsSync(projectPath2)) {
16788
- files.push({ name, status: "missing" });
16789
- continue;
16790
- }
16791
- const projectContent = fs30.readFileSync(projectPath2, "utf8");
16792
- const templateContent = applyPlaceholders(fs30.readFileSync(templatePath, "utf8"), version);
16793
- if (projectContent === templateContent) {
16794
- files.push({ name, status: "unchanged" });
16795
- } else {
16796
- files.push({
16797
- name,
16798
- status: "drifted",
16799
- diff: unifiedDiff(name, projectContent, templateContent)
16800
- });
16801
- }
16802
- }
16803
- for (const name of projectFiles) {
16804
- if (seen.has(name)) continue;
16805
- files.push({ name, status: "extra" });
16806
- }
16807
- const order = {
16808
- drifted: 0,
16809
- missing: 1,
16810
- extra: 2,
16811
- unchanged: 3
16812
- };
16813
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16814
- const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16815
- return {
16816
- overall: hasDrift ? "drift" : "ok",
16817
- files
16818
- };
16819
- }
16820
- function readKitVersion(kitWorkflowsDir) {
16821
- let dir = kitWorkflowsDir;
16822
- for (let i = 0; i < 5; i++) {
16823
- dir = path30.dirname(dir);
16824
- }
16825
- try {
16826
- const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
16827
- const pkg = JSON.parse(raw);
16828
- return typeof pkg.version === "string" ? pkg.version : "unknown";
16829
- } catch {
16830
- return "unknown";
16831
- }
16832
- }
16833
- function applyPlaceholders(content, version) {
16834
- return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
16835
- }
16836
- function applyCommandPlaceholders(content, version) {
16837
- return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
16838
- }
16839
- var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
16840
- function findKitCommandsDir(start) {
16841
- let dir = start;
16842
- for (let i = 0; i < 6; i++) {
16843
- const candidate = path30.join(
16844
- dir,
16845
- "templates",
16846
- "project",
16847
- "common",
16848
- ".claude",
16849
- "commands"
16850
- );
16851
- if (fs30.existsSync(candidate)) return candidate;
16852
- const parent = path30.dirname(dir);
16853
- if (parent === dir) break;
16854
- dir = parent;
16855
- }
16856
- throw new Error(
16857
- `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
16858
- );
16859
- }
16860
- function parsePinnedVersion(content) {
16861
- const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
16862
- return m ? m[1] : void 0;
16863
- }
16864
- function detectCommandDrift(args) {
16865
- const projectCommandsDir = path30.join(args.projectDir, ".claude", "commands");
16866
- const here = path30.dirname(new URL(import.meta.url).pathname);
16867
- const kitCommandsDir = args.kitDir ? path30.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
16868
- const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
16869
- const templateFiles = fs30.existsSync(kitCommandsDir) ? fs30.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16870
- const projectFiles = fs30.existsSync(projectCommandsDir) ? fs30.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16871
- const seen = /* @__PURE__ */ new Set();
16872
- const files = [];
16873
- for (const name of templateFiles) {
16874
- seen.add(name);
16875
- const projectPath2 = path30.join(projectCommandsDir, name);
16876
- const templatePath = path30.join(kitCommandsDir, name);
16877
- const templateRaw = fs30.readFileSync(templatePath, "utf8");
16878
- if (!fs30.existsSync(projectPath2)) {
16879
- files.push({ name, status: "missing", kit_version: kitVersion });
16880
- continue;
16881
- }
16882
- const projectContent = fs30.readFileSync(projectPath2, "utf8");
16883
- const pinned = parsePinnedVersion(projectContent);
16884
- const versionForCompare = pinned ?? kitVersion;
16885
- const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
16886
- if (projectContent === templateContent) {
16887
- files.push({
16888
- name,
16889
- status: "unchanged",
16890
- pinned_version: pinned,
16891
- kit_version: kitVersion
16892
- });
16893
- } else {
16894
- files.push({
16895
- name,
16896
- status: "drifted",
16897
- pinned_version: pinned,
16898
- kit_version: kitVersion,
16899
- diff: unifiedDiff(name, projectContent, templateContent)
16900
- });
16901
- }
16902
- }
16903
- for (const name of projectFiles) {
16904
- if (seen.has(name)) continue;
16905
- files.push({ name, status: "extra", kit_version: kitVersion });
16906
- }
16907
- const order = {
16908
- drifted: 0,
16909
- missing: 1,
16910
- extra: 2,
16911
- unchanged: 3
16912
- };
16913
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16914
- const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16915
- return { overall: hasDrift ? "drift" : "ok", files };
16916
- }
16917
- function readKitVersionFromCommandsDir(kitCommandsDir) {
16918
- let dir = kitCommandsDir;
16919
- for (let i = 0; i < 5; i++) {
16920
- dir = path30.dirname(dir);
16921
- }
16922
- try {
16923
- const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
16924
- const pkg = JSON.parse(raw);
16925
- return typeof pkg.version === "string" ? pkg.version : "unknown";
16926
- } catch {
16927
- return "unknown";
16928
- }
16929
- }
16930
- function detectScaffoldedDrift(args) {
16931
- const workflows = detectWorkflowDrift(args);
16932
- const commands = detectCommandDrift(args);
16933
- return {
16934
- overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
16935
- workflows,
16936
- commands
16937
- };
16938
- }
16939
- function updateWorkflows(args) {
16940
- const projectWorkflowsDir = path30.join(
16941
- args.projectDir,
16942
- ".github",
16943
- "workflows"
16944
- );
16945
- const here = path30.dirname(new URL(import.meta.url).pathname);
16946
- const kitWorkflowsDir = args.kitDir ? path30.join(
16947
- args.kitDir,
16948
- "templates",
16949
- "project",
16950
- "common",
16951
- ".github",
16952
- "workflows"
16953
- ) : findKitTemplatesDir(here);
16954
- const substitute = args.substitute !== false;
16955
- const dryRun = args.dryRun === true;
16956
- const pruneExtras = args.pruneExtras === true;
16957
- const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16958
- const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16959
- if (!dryRun && templateFiles.length > 0 && !fs30.existsSync(projectWorkflowsDir)) {
16960
- fs30.mkdirSync(projectWorkflowsDir, { recursive: true });
16961
- }
16962
- const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
16963
- const seen = /* @__PURE__ */ new Set();
16964
- const files = [];
16965
- for (const name of templateFiles) {
16966
- seen.add(name);
16967
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16968
- const templatePath = path30.join(kitWorkflowsDir, name);
16969
- const templateRaw = fs30.readFileSync(templatePath, "utf-8");
16970
- const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
16971
- const existed = fs30.existsSync(projectPath2);
16972
- const current = existed ? fs30.readFileSync(projectPath2, "utf-8") : "";
16973
- let outcome;
16974
- if (!existed) {
16975
- outcome = "added";
16976
- } else if (current === desired) {
16977
- outcome = "unchanged";
16978
- } else {
16979
- outcome = "updated";
16980
- }
16981
- if (!dryRun && outcome !== "unchanged") {
16982
- fs30.writeFileSync(projectPath2, desired);
16983
- }
16984
- files.push({ name, outcome });
16985
- }
16986
- if (pruneExtras) {
16987
- for (const name of projectFiles) {
16988
- if (seen.has(name)) continue;
16989
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16990
- if (!dryRun) {
16991
- fs30.unlinkSync(projectPath2);
16992
- }
16993
- files.push({ name, outcome: "removed" });
16994
- }
16995
- }
16996
- const order = {
16997
- added: 0,
16998
- updated: 1,
16999
- removed: 2,
17000
- unchanged: 3
17001
- };
17002
- files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
17003
- const changed = files.some((f) => f.outcome !== "unchanged");
17004
- return { files, changed };
17005
- }
17006
-
17007
- // scripts/lakebase/doctor.ts
17008
17074
  function readEnvFile(projectDir) {
17009
17075
  const envPath = path31.join(projectDir, ".env");
17010
17076
  if (!fs31.existsSync(envPath)) return {};
@@ -17437,7 +17503,7 @@ function checkWorkflowDrift(projectDir) {
17437
17503
  status: "warn",
17438
17504
  message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
17439
17505
  detail: { files: report.files.map((f) => ({ name: f.name, status: f.status })) },
17440
- hint: "Inspect via the lakebase_workflow_drift MCP tool (or detectWorkflowDrift import). Refresh manually until updateWorkflows lands."
17506
+ hint: "Refresh with updateWorkflows (or the lakebase_workflow_drift MCP tool). A stale merge.yml calling 'lakebase-schema-migrate apply' (not 'apply-tier') makes the promote's migrate-target fail on the tier guard; the scm-merge promote also self-heals drifted workflows onto the PR head before merging."
17441
17507
  };
17442
17508
  } catch (err) {
17443
17509
  return {
@@ -17745,6 +17811,7 @@ export {
17745
17811
  createUatPairedBranch,
17746
17812
  cutBackup,
17747
17813
  databricksAuthPrereqMessage,
17814
+ defaultRefreshPromoteWorkflows,
17748
17815
  deleteAppEndpoint,
17749
17816
  deleteBranch,
17750
17817
  deleteLakebaseProject,