@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
@@ -9719,8 +9719,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
9719
9719
  var cached;
9720
9720
  function substrateSelfVersion() {
9721
9721
  if (cached !== void 0) return cached;
9722
- if ("0.2.44".length > 0) {
9723
- cached = "0.2.44";
9722
+ if ("0.2.45".length > 0) {
9723
+ cached = "0.2.45";
9724
9724
  return cached;
9725
9725
  }
9726
9726
  cached = "unknown";
@@ -16021,123 +16021,500 @@ function pickRunUrl(pr) {
16021
16021
 
16022
16022
  // scripts/lakebase/scm-merge.ts
16023
16023
  init_esm_shims();
16024
- async function reconcileTierToOrigin(args) {
16025
- const { cwd, tier } = args;
16024
+
16025
+ // scripts/git/sync.ts
16026
+ init_esm_shims();
16027
+ async function currentBranchName(cwd) {
16026
16028
  try {
16027
- const head = await getCurrentBranch({ cwd });
16028
- if (head === tier) {
16029
- await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
16030
- } else {
16031
- await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
16032
- }
16033
- return null;
16034
- } catch (err) {
16035
- 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).`;
16029
+ return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
16030
+ } catch {
16031
+ return "";
16036
16032
  }
16037
16033
  }
16038
- var ScmMergeError = class extends Error {
16039
- constructor(message, code) {
16040
- super(message);
16041
- this.code = code;
16042
- this.name = "ScmMergeError";
16043
- }
16044
- code;
16045
- };
16046
- var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
16047
- var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
16048
- function defaultMigratePredicate(run, mergedAt) {
16049
- if (!run.createdAt) return false;
16050
- const created = Date.parse(run.createdAt);
16051
- if (!Number.isFinite(created)) return false;
16052
- if (run.event && run.event !== "push") return false;
16053
- return created >= mergedAt.getTime() - 5e3;
16034
+ async function push(args) {
16035
+ await exec2("git push", { cwd: args.cwd });
16054
16036
  }
16055
- function shaMigratePredicate(mergeCommitSha) {
16056
- return (run) => {
16057
- if (run.event && run.event !== "push") return false;
16058
- return !!run.headSha && run.headSha === mergeCommitSha;
16059
- };
16037
+ async function pull(args) {
16038
+ await exec2("git pull", { cwd: args.cwd });
16060
16039
  }
16061
- async function mergeFeature(args) {
16062
- const current = readWorkflowState(args.projectDir);
16063
- if (!current) {
16064
- throw new ScmMergeError(
16065
- "No SCM workflow state. wait-ci first.",
16066
- "no-state-file"
16067
- );
16068
- }
16069
- if (current.state !== "ci-green") {
16070
- throw new ScmMergeError(
16071
- `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
16072
- "bad-precondition"
16073
- );
16040
+ async function publishBranch(args) {
16041
+ const remote = args.remote ?? "origin";
16042
+ const branch = await currentBranchName(args.cwd);
16043
+ if (!branch) throw new Error("No current branch");
16044
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
16045
+ cwd: args.cwd
16046
+ });
16047
+ }
16048
+ async function pushCurrentBranchForPr(args) {
16049
+ const remote = args.remote ?? "origin";
16050
+ const branch = await currentBranchName(args.cwd);
16051
+ if (!branch) throw new Error("No current branch");
16052
+ const upstreamSet = await hasUpstream({ cwd: args.cwd });
16053
+ if (!upstreamSet) {
16054
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
16055
+ cwd: args.cwd
16056
+ });
16057
+ } else {
16058
+ await exec2("git push", { cwd: args.cwd });
16074
16059
  }
16075
- if (!current.pr_url) {
16076
- throw new ScmMergeError(
16077
- "ci-green row is missing pr_url; cannot resolve the PR to merge.",
16078
- "no-pr-url"
16060
+ }
16061
+ async function fetch2(args) {
16062
+ const parts = ["git fetch"];
16063
+ if (args.prune) parts.push("--prune");
16064
+ if (args.all) parts.push("--all");
16065
+ await exec2(parts.join(" "), { cwd: args.cwd });
16066
+ }
16067
+ async function pullFrom(args) {
16068
+ await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
16069
+ cwd: args.cwd
16070
+ });
16071
+ }
16072
+ async function pushTo(args) {
16073
+ await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
16074
+ cwd: args.cwd
16075
+ });
16076
+ }
16077
+ async function sync(args) {
16078
+ await exec2("git pull", { cwd: args.cwd });
16079
+ await exec2("git push", { cwd: args.cwd });
16080
+ }
16081
+
16082
+ // scripts/lakebase/workflow-drift.ts
16083
+ init_esm_shims();
16084
+ import * as fs29 from "fs";
16085
+ import * as path29 from "path";
16086
+ function findKitTemplatesDir(start) {
16087
+ let dir = start;
16088
+ for (let i = 0; i < 6; i++) {
16089
+ const candidate = path29.join(
16090
+ dir,
16091
+ "templates",
16092
+ "project",
16093
+ "common",
16094
+ ".github",
16095
+ "workflows"
16079
16096
  );
16097
+ if (fs29.existsSync(candidate)) return candidate;
16098
+ const parent = path29.dirname(dir);
16099
+ if (parent === dir) break;
16100
+ dir = parent;
16080
16101
  }
16081
- if (!current.branch || !current.parent_branch) {
16082
- throw new ScmMergeError(
16083
- "ci-green row missing branch / parent_branch; refusing to merge.",
16084
- "bad-precondition"
16085
- );
16102
+ throw new Error(
16103
+ `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
16104
+ );
16105
+ }
16106
+ function unifiedDiff(name, projectContent, templateContent) {
16107
+ if (projectContent === templateContent) return "";
16108
+ const a = projectContent.split("\n");
16109
+ const b = templateContent.split("\n");
16110
+ const out = [`--- project/${name}`, `+++ template/${name}`];
16111
+ const max = Math.max(a.length, b.length);
16112
+ for (let i = 0; i < max; i++) {
16113
+ const av = a[i];
16114
+ const bv = b[i];
16115
+ if (av === bv) continue;
16116
+ if (av !== void 0) out.push(`-${i + 1}: ${av}`);
16117
+ if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
16086
16118
  }
16087
- const ownerRepo = await getOwnerRepo(args.projectDir);
16088
- if (!ownerRepo) {
16089
- throw new ScmMergeError(
16090
- "No GitHub remote found at origin.",
16091
- "no-github-remote"
16092
- );
16119
+ return out.join("\n");
16120
+ }
16121
+ function detectWorkflowDrift(args) {
16122
+ const projectWorkflowsDir = path29.join(
16123
+ args.projectDir,
16124
+ ".github",
16125
+ "workflows"
16126
+ );
16127
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16128
+ const kitWorkflowsDir = args.kitDir ? path29.join(
16129
+ args.kitDir,
16130
+ "templates",
16131
+ "project",
16132
+ "common",
16133
+ ".github",
16134
+ "workflows"
16135
+ ) : findKitTemplatesDir(here);
16136
+ const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16137
+ const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16138
+ const version = readKitVersion(kitWorkflowsDir);
16139
+ const seen = /* @__PURE__ */ new Set();
16140
+ const files = [];
16141
+ for (const name of templateFiles) {
16142
+ seen.add(name);
16143
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16144
+ const templatePath = path29.join(kitWorkflowsDir, name);
16145
+ if (!fs29.existsSync(projectPath2)) {
16146
+ files.push({ name, status: "missing" });
16147
+ continue;
16148
+ }
16149
+ const projectContent = fs29.readFileSync(projectPath2, "utf8");
16150
+ const templateContent = applyPlaceholders(fs29.readFileSync(templatePath, "utf8"), version);
16151
+ if (projectContent === templateContent) {
16152
+ files.push({ name, status: "unchanged" });
16153
+ } else {
16154
+ files.push({
16155
+ name,
16156
+ status: "drifted",
16157
+ diff: unifiedDiff(name, projectContent, templateContent)
16158
+ });
16159
+ }
16093
16160
  }
16094
- const pullNumber = extractPullNumber(current.pr_url);
16095
- if (!pullNumber) {
16096
- throw new ScmMergeError(
16097
- `Could not extract PR number from URL: ${current.pr_url}`,
16098
- "bad-pr-url"
16099
- );
16161
+ for (const name of projectFiles) {
16162
+ if (seen.has(name)) continue;
16163
+ files.push({ name, status: "extra" });
16100
16164
  }
16101
- const instance = args.instance ?? current.project_id;
16102
- const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
16103
- const wantMigrate = args.waitMigrate !== false;
16104
- let authVerified = false;
16105
- if (wantMigrate && args.verifyMigrateAuth) {
16106
- const probe = await args.verifyMigrateAuth();
16107
- if (!probe.ok) {
16108
- throw new ScmMergeError(
16109
- `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.`,
16110
- "migrate-auth"
16111
- );
16112
- }
16113
- authVerified = true;
16165
+ const order = {
16166
+ drifted: 0,
16167
+ missing: 1,
16168
+ extra: 2,
16169
+ unchanged: 3
16170
+ };
16171
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16172
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16173
+ return {
16174
+ overall: hasDrift ? "drift" : "ok",
16175
+ files
16176
+ };
16177
+ }
16178
+ function readKitVersion(kitWorkflowsDir) {
16179
+ let dir = kitWorkflowsDir;
16180
+ for (let i = 0; i < 5; i++) {
16181
+ dir = path29.dirname(dir);
16114
16182
  }
16115
- let paired;
16116
16183
  try {
16117
- paired = await mergePairedPullRequest({
16118
- ownerRepo,
16119
- pullNumber,
16120
- lakebaseInstance: instance,
16121
- method: args.method ?? "squash"
16122
- });
16123
- } catch (err) {
16124
- throw new ScmMergeError(
16125
- `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
16126
- "merge-failed"
16184
+ const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
16185
+ const pkg = JSON.parse(raw);
16186
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
16187
+ } catch {
16188
+ return "unknown";
16189
+ }
16190
+ }
16191
+ function applyPlaceholders(content, version) {
16192
+ return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
16193
+ }
16194
+ function applyCommandPlaceholders(content, version) {
16195
+ return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
16196
+ }
16197
+ var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
16198
+ function findKitCommandsDir(start) {
16199
+ let dir = start;
16200
+ for (let i = 0; i < 6; i++) {
16201
+ const candidate = path29.join(
16202
+ dir,
16203
+ "templates",
16204
+ "project",
16205
+ "common",
16206
+ ".claude",
16207
+ "commands"
16127
16208
  );
16209
+ if (fs29.existsSync(candidate)) return candidate;
16210
+ const parent = path29.dirname(dir);
16211
+ if (parent === dir) break;
16212
+ dir = parent;
16128
16213
  }
16129
- const warnings = [...paired.warnings];
16130
- let localBranchDeleted = false;
16131
- let headAfter = current.branch;
16132
- if (!args.skipLocalCleanup) {
16133
- const switchTo = args.switchTo ?? gitBase;
16134
- const head = await getCurrentBranch({ cwd: args.projectDir });
16135
- if (head === current.branch) {
16136
- try {
16137
- await exec2(`git checkout ${shellEscape2(switchTo)}`, {
16138
- cwd: args.projectDir,
16139
- timeout: 1e4
16140
- });
16214
+ throw new Error(
16215
+ `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
16216
+ );
16217
+ }
16218
+ function parsePinnedVersion(content) {
16219
+ const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
16220
+ return m ? m[1] : void 0;
16221
+ }
16222
+ function detectCommandDrift(args) {
16223
+ const projectCommandsDir = path29.join(args.projectDir, ".claude", "commands");
16224
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16225
+ const kitCommandsDir = args.kitDir ? path29.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
16226
+ const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
16227
+ const templateFiles = fs29.existsSync(kitCommandsDir) ? fs29.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16228
+ const projectFiles = fs29.existsSync(projectCommandsDir) ? fs29.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
16229
+ const seen = /* @__PURE__ */ new Set();
16230
+ const files = [];
16231
+ for (const name of templateFiles) {
16232
+ seen.add(name);
16233
+ const projectPath2 = path29.join(projectCommandsDir, name);
16234
+ const templatePath = path29.join(kitCommandsDir, name);
16235
+ const templateRaw = fs29.readFileSync(templatePath, "utf8");
16236
+ if (!fs29.existsSync(projectPath2)) {
16237
+ files.push({ name, status: "missing", kit_version: kitVersion });
16238
+ continue;
16239
+ }
16240
+ const projectContent = fs29.readFileSync(projectPath2, "utf8");
16241
+ const pinned = parsePinnedVersion(projectContent);
16242
+ const versionForCompare = pinned ?? kitVersion;
16243
+ const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
16244
+ if (projectContent === templateContent) {
16245
+ files.push({
16246
+ name,
16247
+ status: "unchanged",
16248
+ pinned_version: pinned,
16249
+ kit_version: kitVersion
16250
+ });
16251
+ } else {
16252
+ files.push({
16253
+ name,
16254
+ status: "drifted",
16255
+ pinned_version: pinned,
16256
+ kit_version: kitVersion,
16257
+ diff: unifiedDiff(name, projectContent, templateContent)
16258
+ });
16259
+ }
16260
+ }
16261
+ for (const name of projectFiles) {
16262
+ if (seen.has(name)) continue;
16263
+ files.push({ name, status: "extra", kit_version: kitVersion });
16264
+ }
16265
+ const order = {
16266
+ drifted: 0,
16267
+ missing: 1,
16268
+ extra: 2,
16269
+ unchanged: 3
16270
+ };
16271
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
16272
+ const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
16273
+ return { overall: hasDrift ? "drift" : "ok", files };
16274
+ }
16275
+ function readKitVersionFromCommandsDir(kitCommandsDir) {
16276
+ let dir = kitCommandsDir;
16277
+ for (let i = 0; i < 5; i++) {
16278
+ dir = path29.dirname(dir);
16279
+ }
16280
+ try {
16281
+ const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
16282
+ const pkg = JSON.parse(raw);
16283
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
16284
+ } catch {
16285
+ return "unknown";
16286
+ }
16287
+ }
16288
+ function detectScaffoldedDrift(args) {
16289
+ const workflows = detectWorkflowDrift(args);
16290
+ const commands = detectCommandDrift(args);
16291
+ return {
16292
+ overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
16293
+ workflows,
16294
+ commands
16295
+ };
16296
+ }
16297
+ function updateWorkflows(args) {
16298
+ const projectWorkflowsDir = path29.join(
16299
+ args.projectDir,
16300
+ ".github",
16301
+ "workflows"
16302
+ );
16303
+ const here = path29.dirname(new URL(import.meta.url).pathname);
16304
+ const kitWorkflowsDir = args.kitDir ? path29.join(
16305
+ args.kitDir,
16306
+ "templates",
16307
+ "project",
16308
+ "common",
16309
+ ".github",
16310
+ "workflows"
16311
+ ) : findKitTemplatesDir(here);
16312
+ const substitute = args.substitute !== false;
16313
+ const dryRun = args.dryRun === true;
16314
+ const pruneExtras = args.pruneExtras === true;
16315
+ const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16316
+ const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16317
+ if (!dryRun && templateFiles.length > 0 && !fs29.existsSync(projectWorkflowsDir)) {
16318
+ fs29.mkdirSync(projectWorkflowsDir, { recursive: true });
16319
+ }
16320
+ const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
16321
+ const seen = /* @__PURE__ */ new Set();
16322
+ const files = [];
16323
+ for (const name of templateFiles) {
16324
+ seen.add(name);
16325
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16326
+ const templatePath = path29.join(kitWorkflowsDir, name);
16327
+ const templateRaw = fs29.readFileSync(templatePath, "utf-8");
16328
+ const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
16329
+ const existed = fs29.existsSync(projectPath2);
16330
+ const current = existed ? fs29.readFileSync(projectPath2, "utf-8") : "";
16331
+ let outcome;
16332
+ if (!existed) {
16333
+ outcome = "added";
16334
+ } else if (current === desired) {
16335
+ outcome = "unchanged";
16336
+ } else {
16337
+ outcome = "updated";
16338
+ }
16339
+ if (!dryRun && outcome !== "unchanged") {
16340
+ fs29.writeFileSync(projectPath2, desired);
16341
+ }
16342
+ files.push({ name, outcome });
16343
+ }
16344
+ if (pruneExtras) {
16345
+ for (const name of projectFiles) {
16346
+ if (seen.has(name)) continue;
16347
+ const projectPath2 = path29.join(projectWorkflowsDir, name);
16348
+ if (!dryRun) {
16349
+ fs29.unlinkSync(projectPath2);
16350
+ }
16351
+ files.push({ name, outcome: "removed" });
16352
+ }
16353
+ }
16354
+ const order = {
16355
+ added: 0,
16356
+ updated: 1,
16357
+ removed: 2,
16358
+ unchanged: 3
16359
+ };
16360
+ files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
16361
+ const changed = files.some((f) => f.outcome !== "unchanged");
16362
+ return { files, changed };
16363
+ }
16364
+
16365
+ // scripts/lakebase/scm-merge.ts
16366
+ async function reconcileTierToOrigin(args) {
16367
+ const { cwd, tier } = args;
16368
+ try {
16369
+ const head = await getCurrentBranch({ cwd });
16370
+ if (head === tier) {
16371
+ await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
16372
+ } else {
16373
+ await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
16374
+ }
16375
+ return null;
16376
+ } catch (err) {
16377
+ 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).`;
16378
+ }
16379
+ }
16380
+ var ScmMergeError = class extends Error {
16381
+ constructor(message, code) {
16382
+ super(message);
16383
+ this.code = code;
16384
+ this.name = "ScmMergeError";
16385
+ }
16386
+ code;
16387
+ };
16388
+ var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
16389
+ var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
16390
+ function defaultMigratePredicate(run, mergedAt) {
16391
+ if (!run.createdAt) return false;
16392
+ const created = Date.parse(run.createdAt);
16393
+ if (!Number.isFinite(created)) return false;
16394
+ if (run.event && run.event !== "push") return false;
16395
+ return created >= mergedAt.getTime() - 5e3;
16396
+ }
16397
+ function shaMigratePredicate(mergeCommitSha) {
16398
+ return (run) => {
16399
+ if (run.event && run.event !== "push") return false;
16400
+ return !!run.headSha && run.headSha === mergeCommitSha;
16401
+ };
16402
+ }
16403
+ async function defaultRefreshPromoteWorkflows(projectDir) {
16404
+ const drift = detectWorkflowDrift({ projectDir });
16405
+ const touched = drift.files.filter((f) => f.status === "drifted").map((f) => f.name);
16406
+ if (touched.length === 0) return { refreshed: false };
16407
+ const result = updateWorkflows({ projectDir });
16408
+ if (!result.changed) return { refreshed: false };
16409
+ await exec2(`git add ${shellEscape2(".github/workflows")}`, { cwd: projectDir, timeout: 1e4 });
16410
+ const staged = await exec2("git diff --cached --name-only -- .github/workflows", {
16411
+ cwd: projectDir,
16412
+ timeout: 1e4
16413
+ });
16414
+ if (!staged.trim()) return { refreshed: false };
16415
+ await exec2(
16416
+ `git commit -m ${shellEscape2(
16417
+ "chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)"
16418
+ )} -- .github/workflows`,
16419
+ { cwd: projectDir, timeout: 15e3 }
16420
+ );
16421
+ await pushCurrentBranchForPr({ cwd: projectDir });
16422
+ return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
16423
+ }
16424
+ async function mergeFeature(args) {
16425
+ const current = readWorkflowState(args.projectDir);
16426
+ if (!current) {
16427
+ throw new ScmMergeError(
16428
+ "No SCM workflow state. wait-ci first.",
16429
+ "no-state-file"
16430
+ );
16431
+ }
16432
+ if (current.state !== "ci-green") {
16433
+ throw new ScmMergeError(
16434
+ `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
16435
+ "bad-precondition"
16436
+ );
16437
+ }
16438
+ if (!current.pr_url) {
16439
+ throw new ScmMergeError(
16440
+ "ci-green row is missing pr_url; cannot resolve the PR to merge.",
16441
+ "no-pr-url"
16442
+ );
16443
+ }
16444
+ if (!current.branch || !current.parent_branch) {
16445
+ throw new ScmMergeError(
16446
+ "ci-green row missing branch / parent_branch; refusing to merge.",
16447
+ "bad-precondition"
16448
+ );
16449
+ }
16450
+ const ownerRepo = await getOwnerRepo(args.projectDir);
16451
+ if (!ownerRepo) {
16452
+ throw new ScmMergeError(
16453
+ "No GitHub remote found at origin.",
16454
+ "no-github-remote"
16455
+ );
16456
+ }
16457
+ const pullNumber = extractPullNumber(current.pr_url);
16458
+ if (!pullNumber) {
16459
+ throw new ScmMergeError(
16460
+ `Could not extract PR number from URL: ${current.pr_url}`,
16461
+ "bad-pr-url"
16462
+ );
16463
+ }
16464
+ const instance = args.instance ?? current.project_id;
16465
+ const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
16466
+ const wantMigrate = args.waitMigrate !== false;
16467
+ let authVerified = false;
16468
+ if (wantMigrate && args.verifyMigrateAuth) {
16469
+ const probe = await args.verifyMigrateAuth();
16470
+ if (!probe.ok) {
16471
+ throw new ScmMergeError(
16472
+ `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.`,
16473
+ "migrate-auth"
16474
+ );
16475
+ }
16476
+ authVerified = true;
16477
+ }
16478
+ const preflightNotes = [];
16479
+ {
16480
+ const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
16481
+ try {
16482
+ const r = await refresh();
16483
+ if (r.refreshed) {
16484
+ preflightNotes.push(`Refreshed stale CI workflows before merge${r.detail ? ` (${r.detail})` : ""}.`);
16485
+ }
16486
+ } catch (err) {
16487
+ preflightNotes.push(
16488
+ `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').`
16489
+ );
16490
+ }
16491
+ }
16492
+ let paired;
16493
+ try {
16494
+ paired = await mergePairedPullRequest({
16495
+ ownerRepo,
16496
+ pullNumber,
16497
+ lakebaseInstance: instance,
16498
+ method: args.method ?? "squash"
16499
+ });
16500
+ } catch (err) {
16501
+ throw new ScmMergeError(
16502
+ `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
16503
+ "merge-failed"
16504
+ );
16505
+ }
16506
+ const warnings = [...preflightNotes, ...paired.warnings];
16507
+ let localBranchDeleted = false;
16508
+ let headAfter = current.branch;
16509
+ if (!args.skipLocalCleanup) {
16510
+ const switchTo = args.switchTo ?? gitBase;
16511
+ const head = await getCurrentBranch({ cwd: args.projectDir });
16512
+ if (head === current.branch) {
16513
+ try {
16514
+ await exec2(`git checkout ${shellEscape2(switchTo)}`, {
16515
+ cwd: args.projectDir,
16516
+ timeout: 1e4
16517
+ });
16141
16518
  headAfter = switchTo;
16142
16519
  try {
16143
16520
  await exec2(`git fetch origin ${shellEscape2(switchTo)}`, {
@@ -16323,7 +16700,7 @@ init_esm_shims();
16323
16700
 
16324
16701
  // scripts/git/branches.ts
16325
16702
  init_esm_shims();
16326
- async function currentBranchName(cwd) {
16703
+ async function currentBranchName2(cwd) {
16327
16704
  try {
16328
16705
  return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
16329
16706
  } catch {
@@ -16342,7 +16719,7 @@ async function listLocalBranches(args) {
16342
16719
  return [];
16343
16720
  }
16344
16721
  if (!raw) return [];
16345
- const current = await currentBranchName(cwd);
16722
+ const current = await currentBranchName2(cwd);
16346
16723
  return raw.split("\n").filter(Boolean).map((line) => {
16347
16724
  const [name, tracking, trackInfo] = line.split("|");
16348
16725
  let ahead = 0;
@@ -16542,15 +16919,15 @@ function parentForTopology(t, defaultLeaf) {
16542
16919
 
16543
16920
  // scripts/lakebase/scm-doctor.ts
16544
16921
  init_esm_shims();
16545
- import * as fs29 from "fs";
16546
- import * as path29 from "path";
16922
+ import * as fs30 from "fs";
16923
+ import * as path30 from "path";
16547
16924
  var FEATURE_PREFIX = "feature/";
16548
16925
  var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
16549
16926
  function readEnv(projectDir) {
16550
- const envPath = path29.join(projectDir, ".env");
16927
+ const envPath = path30.join(projectDir, ".env");
16551
16928
  const out = /* @__PURE__ */ new Map();
16552
- if (!fs29.existsSync(envPath)) return out;
16553
- const lines = fs29.readFileSync(envPath, "utf8").split("\n");
16929
+ if (!fs30.existsSync(envPath)) return out;
16930
+ const lines = fs30.readFileSync(envPath, "utf8").split("\n");
16554
16931
  for (const line of lines) {
16555
16932
  const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
16556
16933
  if (m) out.set(m[1], m[2].replace(/^["']|["']$/g, ""));
@@ -16596,622 +16973,337 @@ async function runDoctor(args, deps = {}) {
16596
16973
  findings.push({
16597
16974
  id: "github-actions-disabled",
16598
16975
  severity: "warn",
16599
- message: `GitHub Actions is disabled for ${ownerRepo}, so the kit's CI workflows (pr.yml / merge.yml) will never run , the PR branch's migrations, tests, and schema-diff comment are all skipped. This is commonly an org-level policy on EMU repos, which a repo admin cannot override.`,
16600
- suggestion: "Have an org owner enable Actions for this repo (repo Settings -> Actions -> General; if it says 'disabled by the organization', it must be enabled in the org's Actions policy). Until then, run the workflow steps locally: scripts/run-tests.sh against a Lakebase branch."
16601
- });
16602
- }
16603
- }
16604
- } catch {
16605
- }
16606
- for (const wf of ["pr.yml", "merge.yml"]) {
16607
- try {
16608
- const p = path29.join(projectDir, ".github", "workflows", wf);
16609
- if (!fs29.existsSync(p)) continue;
16610
- const body = fs29.readFileSync(p, "utf8");
16611
- if (/lakebase-scm-utils#v\d/.test(body)) {
16612
- findings.push({
16613
- id: "ci-workflow-substrate-pin",
16614
- severity: "warn",
16615
- message: `.github/workflows/${wf} hardcodes a substrate version pin (github:databricks-solutions/lakebase-scm-utils#v<ver>) instead of resolving .lakebase/scm-utils-ref at runtime, so bumping .lakebase/scm-utils-ref does NOT change the substrate CI actually runs.`,
16616
- suggestion: "Re-emit the workflows from the current substrate templates so they resolve SCM_UTILS_REF from .lakebase/scm-utils-ref at CI time (updateWorkflows in scripts/lakebase/workflow-drift.ts; lakebase-doctor reports this as workflow-drift). Until then a scm-utils-ref bump will not reach CI."
16617
- });
16618
- }
16619
- } catch {
16620
- }
16621
- }
16622
- try {
16623
- if (isGitTracked(projectDir, ".lakebase/workflow-state.json")) {
16624
- findings.push({
16625
- id: "scm-state-git-tracked",
16626
- severity: "warn",
16627
- message: ".lakebase/workflow-state.json (the per-working-tree SCM claim state) is git-tracked, so a branch checkout or `git reset --hard origin/<tier>` can restore a stale committed claim over the live one (the foreign-claim guard then refuses to drive until you abandon + reclaim).",
16628
- suggestion: "If a wrong-feature-claim refusal follows a checkout, run lakebase-scm-adopt-state (or abandon + reclaim) to re-establish the live claim. The kit-ref run pin (.lakebase/kit-ref.local) already protects the kit version from the same checkout revert."
16629
- });
16630
- }
16631
- } catch {
16632
- }
16633
- if (!env.has("LAKEBASE_PROJECT_ID")) {
16634
- findings.push({
16635
- id: "env-missing-project-id",
16636
- severity: "fail",
16637
- message: ".env does not contain LAKEBASE_PROJECT_ID. The post-checkout hook will exit early; workflow CLIs will need an explicit --instance.",
16638
- suggestion: "Set LAKEBASE_PROJECT_ID=<your project id> in .env"
16639
- });
16640
- }
16641
- if (!instance) {
16642
- return finalize({
16643
- projectDir,
16644
- workflowStatePresent,
16645
- state: state ?? void 0,
16646
- findings
16647
- });
16648
- }
16649
- let lakebaseBranches = [];
16650
- try {
16651
- lakebaseBranches = await listBranches({ instance });
16652
- } catch (err) {
16653
- findings.push({
16654
- id: "lakebase-unreachable",
16655
- severity: "fail",
16656
- message: `Could not list Lakebase branches for instance ${instance}: ${err instanceof Error ? err.message : String(err)}`,
16657
- suggestion: "databricks auth login (or check DATABRICKS_CONFIG_PROFILE)."
16658
- });
16659
- return finalize({
16660
- projectDir,
16661
- workflowStatePresent,
16662
- state: state ?? void 0,
16663
- findings
16664
- });
16665
- }
16666
- const inferredTopology = inferTierTopology(lakebaseBranches);
16667
- if (state && state.tier_topology !== inferredTopology) {
16668
- findings.push({
16669
- id: "tier-topology-mismatch",
16670
- severity: "warn",
16671
- message: `workflow-state records tier_topology=${state.tier_topology}, but the Lakebase tier inventory suggests ${inferredTopology}.`,
16672
- suggestion: "lakebase-scm-adopt-state --force"
16673
- });
16674
- }
16675
- const headBranch = await getCurrentBranch({ cwd: projectDir });
16676
- if (state && state.state === "feature-claimed") {
16677
- if (state.branch && headBranch && headBranch !== state.branch) {
16678
- findings.push({
16679
- id: "head-branch-drift",
16680
- severity: "warn",
16681
- message: `workflow says feature-claimed for "${state.branch}", but HEAD is on "${headBranch}".`,
16682
- suggestion: `git checkout '${state.branch}'`
16683
- });
16684
- }
16685
- if (state.branch) {
16686
- const sanitized = sanitizeBranchName(state.branch);
16687
- let pair;
16688
- try {
16689
- pair = await getBranchByName(sanitized, { instance });
16690
- } catch {
16691
- pair = void 0;
16692
- }
16693
- if (!pair) {
16694
- findings.push({
16695
- id: "lakebase-pair-missing",
16696
- severity: "fail",
16697
- message: `workflow says feature-claimed for "${state.branch}", but no Lakebase branch "${sanitized}" exists.`,
16698
- suggestion: `lakebase-scm-abandon-feature # reset state; re-claim if needed`
16699
- });
16700
- } else if (state.lakebase_branch_uid && pair.uid !== state.lakebase_branch_uid) {
16701
- findings.push({
16702
- id: "lakebase-uid-drift",
16703
- severity: "warn",
16704
- message: `workflow records lakebase_branch_uid=${state.lakebase_branch_uid}, but the live branch reports ${pair.uid}.`,
16705
- suggestion: "lakebase-scm-adopt-state --force"
16706
- });
16707
- }
16708
- }
16709
- }
16710
- if (state && state.state === "feature-claimed" && state.branch) {
16711
- const envBranchId = env.get("LAKEBASE_BRANCH_ID");
16712
- const sanitized = sanitizeBranchName(state.branch);
16713
- if (envBranchId && envBranchId !== sanitized) {
16714
- findings.push({
16715
- id: "env-branch-drift",
16716
- severity: "warn",
16717
- message: `.env LAKEBASE_BRANCH_ID=${envBranchId} but workflow says ${sanitized}. The post-checkout hook may not have run since the last branch switch.`,
16718
- suggestion: `git checkout '${state.branch}' # re-fires post-checkout`
16719
- });
16720
- }
16721
- }
16722
- if (headBranch && !TIER_LEAFS2.has(headBranch) && headBranch.startsWith(FEATURE_PREFIX)) {
16723
- const sanitized = sanitizeBranchName(headBranch);
16724
- const paired = lakebaseBranches.some((b) => leafOf2(b) === sanitized);
16725
- if (!paired) {
16726
- findings.push({
16727
- id: "orphan-current-branch",
16728
- severity: "fail",
16729
- message: `Current git branch "${headBranch}" has no Lakebase pair (post-checkout fallback retired in phase C).`,
16730
- suggestion: `lakebase-scm-recover-orphans --claim --only-branch '${headBranch}'`
16731
- });
16732
- }
16733
- }
16734
- try {
16735
- const heads = await collapseMigrationHeads({ projectDir, dryRun: true });
16736
- if (heads.headsBefore.length > 1) {
16737
- findings.push({
16738
- id: "multiple-migration-heads",
16739
- severity: "fail",
16740
- message: `Migrations have ${heads.headsBefore.length} heads (${heads.headsBefore.join(", ")}); a sibling-feature merge left them un-collapsed. \`upgrade head\` will refuse until they are unified.`,
16741
- suggestion: "lakebase-collapse-heads"
16742
- });
16743
- }
16744
- } catch {
16745
- }
16746
- if (instance && state?.branch) {
16747
- try {
16748
- const localIds = listSchemaMigrations({ projectDir }).map((m) => m.version);
16749
- let orphanRev = null;
16750
- try {
16751
- const status = await schemaMigrationStatus({ instance, branch: state.branch, projectDir });
16752
- if (dbRevisionOrphaned(status.current, localIds)) orphanRev = status.current ?? null;
16753
- } catch (e) {
16754
- orphanRev = parseAlembicMissingRevision(e instanceof Error ? e.message : String(e));
16755
- }
16756
- if (orphanRev) {
16757
- findings.push({
16758
- id: "db-ahead-of-code",
16759
- severity: "fail",
16760
- message: `The paired branch DB is AHEAD of code: applied revision '${orphanRev}' has no local migration file. An aborted build likely migrated this branch and a git reset removed the migration file; alembic accept/deploy/promote will fail "Can't locate revision".`,
16761
- suggestion: "reset the paired branch DB to the code head (downgrade/stamp + drop reset-created tables), or delete the branch and re-cut it clean from its tier"
16762
- });
16763
- }
16764
- } catch {
16765
- }
16766
- }
16767
- return finalize({
16768
- projectDir,
16769
- workflowStatePresent,
16770
- state: state ?? void 0,
16771
- inferredTierTopology: inferredTopology,
16772
- findings
16773
- });
16774
- }
16775
- function finalize(report) {
16776
- let worst = "ok";
16777
- for (const f of report.findings) {
16778
- worst = worstOf(worst, f.severity);
16779
- }
16780
- return { ...report, worstSeverity: worst };
16781
- }
16782
- var ScmDoctorFixError = class extends Error {
16783
- constructor(message, code) {
16784
- super(message);
16785
- this.code = code;
16786
- this.name = "ScmDoctorFixError";
16787
- }
16788
- code;
16789
- };
16790
- var FIXABLE_FINDING_IDS = [
16791
- "env-branch-drift",
16792
- "head-branch-drift",
16793
- "tier-topology-mismatch",
16794
- "orphan-current-branch",
16795
- "multiple-migration-heads",
16796
- "db-ahead-of-code"
16797
- ];
16798
- async function fixFinding(args) {
16799
- if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
16800
- throw new ScmDoctorFixError(
16801
- `Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
16802
- "unsupported-finding"
16803
- );
16804
- }
16805
- const report = args.report ?? await runDoctor({ projectDir: args.projectDir, instance: args.instance });
16806
- const present = report.findings.find((f) => f.id === args.findingId);
16807
- if (!present) {
16808
- throw new ScmDoctorFixError(
16809
- `Finding "${args.findingId}" is not present in the current report. Re-run lakebase-scm-doctor to see what needs fixing.`,
16810
- "finding-not-present"
16811
- );
16812
- }
16813
- let action = "";
16814
- try {
16815
- switch (args.findingId) {
16816
- case "env-branch-drift": {
16817
- const branch = report.state?.branch;
16818
- if (!branch) {
16819
- throw new ScmDoctorFixError(
16820
- "Cannot fix: workflow state has no branch field.",
16821
- "fix-failed"
16822
- );
16823
- }
16824
- const sanitized = sanitizeBranchName(branch);
16825
- const envFile = path29.join(args.projectDir, ".env");
16826
- updateEnvConnection({
16827
- envPath: envFile,
16828
- projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
16829
- branchId: sanitized,
16830
- username: readEnvVar(envFile, "DB_USERNAME") ?? "",
16831
- endpointHost: readEnvVar(envFile, "LAKEBASE_HOST")
16832
- });
16833
- action = `rewrote .env LAKEBASE_BRANCH_ID=${sanitized} (metadata only; the app mints its token at runtime)`;
16834
- break;
16835
- }
16836
- case "head-branch-drift": {
16837
- const branch = report.state?.branch;
16838
- if (!branch) {
16839
- throw new ScmDoctorFixError(
16840
- "Cannot fix: workflow state has no branch field.",
16841
- "fix-failed"
16842
- );
16843
- }
16844
- await exec2(`git checkout ${shellEscape3(branch)}`, {
16845
- cwd: args.projectDir,
16846
- timeout: 15e3
16847
- });
16848
- action = `git checkout ${branch} (re-fires post-checkout to resync HEAD)`;
16849
- break;
16850
- }
16851
- case "tier-topology-mismatch": {
16852
- const instance = args.instance ?? report.state?.project_id;
16853
- if (!instance) {
16854
- throw new ScmDoctorFixError(
16855
- "Cannot fix: missing Lakebase project id.",
16856
- "fix-failed"
16857
- );
16858
- }
16859
- await adoptScmState({
16860
- projectDir: args.projectDir,
16861
- instance,
16862
- force: true
16863
- });
16864
- action = `adopted state with --force to re-infer tier_topology`;
16865
- break;
16866
- }
16867
- case "orphan-current-branch": {
16868
- const instance = args.instance ?? report.state?.project_id;
16869
- if (!instance) {
16870
- throw new ScmDoctorFixError(
16871
- "Cannot fix: missing Lakebase project id.",
16872
- "fix-failed"
16873
- );
16874
- }
16875
- const headBranch = await getCurrentBranch({ cwd: args.projectDir });
16876
- if (!headBranch) {
16877
- throw new ScmDoctorFixError(
16878
- "Cannot fix: detached HEAD or no current branch.",
16879
- "fix-failed"
16880
- );
16881
- }
16882
- await recoverOrphans({
16883
- projectDir: args.projectDir,
16884
- instance,
16885
- claim: true,
16886
- onlyBranch: headBranch
16887
- });
16888
- action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
16889
- break;
16890
- }
16891
- case "multiple-migration-heads": {
16892
- const r = await collapseMigrationHeads({ projectDir: args.projectDir });
16893
- if (r.status !== "ok" || !r.mergeRevision) {
16894
- throw new ScmDoctorFixError(
16895
- `Expected to create a merge revision but got status="${r.status}".`,
16896
- "fix-failed"
16897
- );
16898
- }
16899
- action = `collapsed ${r.headsBefore.length} heads into merge revision ${r.mergeRevision} (commit it)`;
16900
- break;
16901
- }
16902
- case "db-ahead-of-code": {
16903
- const r = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
16904
- action = `abandoned the feature (deleted the polluted paired branch${r.lakebaseDeleted ? "" : " , Lakebase delete reported not-deleted"}) and reset state to '${r.state.state}'; re-claim to re-fork a clean branch from the tier`;
16905
- break;
16906
- }
16907
- }
16908
- } catch (err) {
16909
- if (err instanceof ScmDoctorFixError) throw err;
16910
- throw new ScmDoctorFixError(
16911
- `Remediation failed: ${err instanceof Error ? err.message : String(err)}`,
16912
- "fix-failed"
16913
- );
16914
- }
16915
- const postReport = await runDoctor({
16916
- projectDir: args.projectDir,
16917
- instance: args.instance
16918
- });
16919
- return { findingId: args.findingId, action, postReport };
16920
- }
16921
- function shellEscape3(s) {
16922
- return `'${s.replace(/'/g, "'\\''")}'`;
16923
- }
16924
-
16925
- // scripts/lakebase/doctor.ts
16926
- init_esm_shims();
16927
- import * as fs31 from "fs";
16928
- import * as path31 from "path";
16929
- import * as cp6 from "child_process";
16930
-
16931
- // scripts/lakebase/workflow-drift.ts
16932
- init_esm_shims();
16933
- import * as fs30 from "fs";
16934
- import * as path30 from "path";
16935
- function findKitTemplatesDir(start) {
16936
- let dir = start;
16937
- for (let i = 0; i < 6; i++) {
16938
- const candidate = path30.join(
16939
- dir,
16940
- "templates",
16941
- "project",
16942
- "common",
16943
- ".github",
16944
- "workflows"
16945
- );
16946
- if (fs30.existsSync(candidate)) return candidate;
16947
- const parent = path30.dirname(dir);
16948
- if (parent === dir) break;
16949
- dir = parent;
16950
- }
16951
- throw new Error(
16952
- `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
16953
- );
16954
- }
16955
- function unifiedDiff(name, projectContent, templateContent) {
16956
- if (projectContent === templateContent) return "";
16957
- const a = projectContent.split("\n");
16958
- const b = templateContent.split("\n");
16959
- const out = [`--- project/${name}`, `+++ template/${name}`];
16960
- const max = Math.max(a.length, b.length);
16961
- for (let i = 0; i < max; i++) {
16962
- const av = a[i];
16963
- const bv = b[i];
16964
- if (av === bv) continue;
16965
- if (av !== void 0) out.push(`-${i + 1}: ${av}`);
16966
- if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
16967
- }
16968
- return out.join("\n");
16969
- }
16970
- function detectWorkflowDrift(args) {
16971
- const projectWorkflowsDir = path30.join(
16972
- args.projectDir,
16973
- ".github",
16974
- "workflows"
16975
- );
16976
- const here = path30.dirname(new URL(import.meta.url).pathname);
16977
- const kitWorkflowsDir = args.kitDir ? path30.join(
16978
- args.kitDir,
16979
- "templates",
16980
- "project",
16981
- "common",
16982
- ".github",
16983
- "workflows"
16984
- ) : findKitTemplatesDir(here);
16985
- const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16986
- const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
16987
- const version = readKitVersion(kitWorkflowsDir);
16988
- const seen = /* @__PURE__ */ new Set();
16989
- const files = [];
16990
- for (const name of templateFiles) {
16991
- seen.add(name);
16992
- const projectPath2 = path30.join(projectWorkflowsDir, name);
16993
- const templatePath = path30.join(kitWorkflowsDir, name);
16994
- if (!fs30.existsSync(projectPath2)) {
16995
- files.push({ name, status: "missing" });
16996
- continue;
16976
+ message: `GitHub Actions is disabled for ${ownerRepo}, so the kit's CI workflows (pr.yml / merge.yml) will never run , the PR branch's migrations, tests, and schema-diff comment are all skipped. This is commonly an org-level policy on EMU repos, which a repo admin cannot override.`,
16977
+ suggestion: "Have an org owner enable Actions for this repo (repo Settings -> Actions -> General; if it says 'disabled by the organization', it must be enabled in the org's Actions policy). Until then, run the workflow steps locally: scripts/run-tests.sh against a Lakebase branch."
16978
+ });
16979
+ }
16997
16980
  }
16998
- const projectContent = fs30.readFileSync(projectPath2, "utf8");
16999
- const templateContent = applyPlaceholders(fs30.readFileSync(templatePath, "utf8"), version);
17000
- if (projectContent === templateContent) {
17001
- files.push({ name, status: "unchanged" });
17002
- } else {
17003
- files.push({
17004
- name,
17005
- status: "drifted",
17006
- diff: unifiedDiff(name, projectContent, templateContent)
16981
+ } catch {
16982
+ }
16983
+ for (const wf of ["pr.yml", "merge.yml"]) {
16984
+ try {
16985
+ const p = path30.join(projectDir, ".github", "workflows", wf);
16986
+ if (!fs30.existsSync(p)) continue;
16987
+ const body = fs30.readFileSync(p, "utf8");
16988
+ if (/lakebase-scm-utils#v\d/.test(body)) {
16989
+ findings.push({
16990
+ id: "ci-workflow-substrate-pin",
16991
+ severity: "warn",
16992
+ message: `.github/workflows/${wf} hardcodes a substrate version pin (github:databricks-solutions/lakebase-scm-utils#v<ver>) instead of resolving .lakebase/scm-utils-ref at runtime, so bumping .lakebase/scm-utils-ref does NOT change the substrate CI actually runs.`,
16993
+ suggestion: "Re-emit the workflows from the current substrate templates so they resolve SCM_UTILS_REF from .lakebase/scm-utils-ref at CI time (updateWorkflows in scripts/lakebase/workflow-drift.ts; lakebase-doctor reports this as workflow-drift). Until then a scm-utils-ref bump will not reach CI."
16994
+ });
16995
+ }
16996
+ } catch {
16997
+ }
16998
+ }
16999
+ try {
17000
+ if (isGitTracked(projectDir, ".lakebase/workflow-state.json")) {
17001
+ findings.push({
17002
+ id: "scm-state-git-tracked",
17003
+ severity: "warn",
17004
+ message: ".lakebase/workflow-state.json (the per-working-tree SCM claim state) is git-tracked, so a branch checkout or `git reset --hard origin/<tier>` can restore a stale committed claim over the live one (the foreign-claim guard then refuses to drive until you abandon + reclaim).",
17005
+ suggestion: "If a wrong-feature-claim refusal follows a checkout, run lakebase-scm-adopt-state (or abandon + reclaim) to re-establish the live claim. The kit-ref run pin (.lakebase/kit-ref.local) already protects the kit version from the same checkout revert."
17007
17006
  });
17008
17007
  }
17008
+ } catch {
17009
17009
  }
17010
- for (const name of projectFiles) {
17011
- if (seen.has(name)) continue;
17012
- files.push({ name, status: "extra" });
17010
+ if (!env.has("LAKEBASE_PROJECT_ID")) {
17011
+ findings.push({
17012
+ id: "env-missing-project-id",
17013
+ severity: "fail",
17014
+ message: ".env does not contain LAKEBASE_PROJECT_ID. The post-checkout hook will exit early; workflow CLIs will need an explicit --instance.",
17015
+ suggestion: "Set LAKEBASE_PROJECT_ID=<your project id> in .env"
17016
+ });
17013
17017
  }
17014
- const order = {
17015
- drifted: 0,
17016
- missing: 1,
17017
- extra: 2,
17018
- unchanged: 3
17019
- };
17020
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
17021
- const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
17022
- return {
17023
- overall: hasDrift ? "drift" : "ok",
17024
- files
17025
- };
17026
- }
17027
- function readKitVersion(kitWorkflowsDir) {
17028
- let dir = kitWorkflowsDir;
17029
- for (let i = 0; i < 5; i++) {
17030
- dir = path30.dirname(dir);
17018
+ if (!instance) {
17019
+ return finalize({
17020
+ projectDir,
17021
+ workflowStatePresent,
17022
+ state: state ?? void 0,
17023
+ findings
17024
+ });
17031
17025
  }
17026
+ let lakebaseBranches = [];
17032
17027
  try {
17033
- const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
17034
- const pkg = JSON.parse(raw);
17035
- return typeof pkg.version === "string" ? pkg.version : "unknown";
17036
- } catch {
17037
- return "unknown";
17028
+ lakebaseBranches = await listBranches({ instance });
17029
+ } catch (err) {
17030
+ findings.push({
17031
+ id: "lakebase-unreachable",
17032
+ severity: "fail",
17033
+ message: `Could not list Lakebase branches for instance ${instance}: ${err instanceof Error ? err.message : String(err)}`,
17034
+ suggestion: "databricks auth login (or check DATABRICKS_CONFIG_PROFILE)."
17035
+ });
17036
+ return finalize({
17037
+ projectDir,
17038
+ workflowStatePresent,
17039
+ state: state ?? void 0,
17040
+ findings
17041
+ });
17038
17042
  }
17039
- }
17040
- function applyPlaceholders(content, version) {
17041
- return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
17042
- }
17043
- function applyCommandPlaceholders(content, version) {
17044
- return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
17045
- }
17046
- var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
17047
- function findKitCommandsDir(start) {
17048
- let dir = start;
17049
- for (let i = 0; i < 6; i++) {
17050
- const candidate = path30.join(
17051
- dir,
17052
- "templates",
17053
- "project",
17054
- "common",
17055
- ".claude",
17056
- "commands"
17057
- );
17058
- if (fs30.existsSync(candidate)) return candidate;
17059
- const parent = path30.dirname(dir);
17060
- if (parent === dir) break;
17061
- dir = parent;
17043
+ const inferredTopology = inferTierTopology(lakebaseBranches);
17044
+ if (state && state.tier_topology !== inferredTopology) {
17045
+ findings.push({
17046
+ id: "tier-topology-mismatch",
17047
+ severity: "warn",
17048
+ message: `workflow-state records tier_topology=${state.tier_topology}, but the Lakebase tier inventory suggests ${inferredTopology}.`,
17049
+ suggestion: "lakebase-scm-adopt-state --force"
17050
+ });
17062
17051
  }
17063
- throw new Error(
17064
- `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
17065
- );
17066
- }
17067
- function parsePinnedVersion(content) {
17068
- const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
17069
- return m ? m[1] : void 0;
17070
- }
17071
- function detectCommandDrift(args) {
17072
- const projectCommandsDir = path30.join(args.projectDir, ".claude", "commands");
17073
- const here = path30.dirname(new URL(import.meta.url).pathname);
17074
- const kitCommandsDir = args.kitDir ? path30.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
17075
- const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
17076
- const templateFiles = fs30.existsSync(kitCommandsDir) ? fs30.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
17077
- const projectFiles = fs30.existsSync(projectCommandsDir) ? fs30.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
17078
- const seen = /* @__PURE__ */ new Set();
17079
- const files = [];
17080
- for (const name of templateFiles) {
17081
- seen.add(name);
17082
- const projectPath2 = path30.join(projectCommandsDir, name);
17083
- const templatePath = path30.join(kitCommandsDir, name);
17084
- const templateRaw = fs30.readFileSync(templatePath, "utf8");
17085
- if (!fs30.existsSync(projectPath2)) {
17086
- files.push({ name, status: "missing", kit_version: kitVersion });
17087
- continue;
17088
- }
17089
- const projectContent = fs30.readFileSync(projectPath2, "utf8");
17090
- const pinned = parsePinnedVersion(projectContent);
17091
- const versionForCompare = pinned ?? kitVersion;
17092
- const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
17093
- if (projectContent === templateContent) {
17094
- files.push({
17095
- name,
17096
- status: "unchanged",
17097
- pinned_version: pinned,
17098
- kit_version: kitVersion
17099
- });
17100
- } else {
17101
- files.push({
17102
- name,
17103
- status: "drifted",
17104
- pinned_version: pinned,
17105
- kit_version: kitVersion,
17106
- diff: unifiedDiff(name, projectContent, templateContent)
17052
+ const headBranch = await getCurrentBranch({ cwd: projectDir });
17053
+ if (state && state.state === "feature-claimed") {
17054
+ if (state.branch && headBranch && headBranch !== state.branch) {
17055
+ findings.push({
17056
+ id: "head-branch-drift",
17057
+ severity: "warn",
17058
+ message: `workflow says feature-claimed for "${state.branch}", but HEAD is on "${headBranch}".`,
17059
+ suggestion: `git checkout '${state.branch}'`
17107
17060
  });
17108
17061
  }
17062
+ if (state.branch) {
17063
+ const sanitized = sanitizeBranchName(state.branch);
17064
+ let pair;
17065
+ try {
17066
+ pair = await getBranchByName(sanitized, { instance });
17067
+ } catch {
17068
+ pair = void 0;
17069
+ }
17070
+ if (!pair) {
17071
+ findings.push({
17072
+ id: "lakebase-pair-missing",
17073
+ severity: "fail",
17074
+ message: `workflow says feature-claimed for "${state.branch}", but no Lakebase branch "${sanitized}" exists.`,
17075
+ suggestion: `lakebase-scm-abandon-feature # reset state; re-claim if needed`
17076
+ });
17077
+ } else if (state.lakebase_branch_uid && pair.uid !== state.lakebase_branch_uid) {
17078
+ findings.push({
17079
+ id: "lakebase-uid-drift",
17080
+ severity: "warn",
17081
+ message: `workflow records lakebase_branch_uid=${state.lakebase_branch_uid}, but the live branch reports ${pair.uid}.`,
17082
+ suggestion: "lakebase-scm-adopt-state --force"
17083
+ });
17084
+ }
17085
+ }
17109
17086
  }
17110
- for (const name of projectFiles) {
17111
- if (seen.has(name)) continue;
17112
- files.push({ name, status: "extra", kit_version: kitVersion });
17087
+ if (state && state.state === "feature-claimed" && state.branch) {
17088
+ const envBranchId = env.get("LAKEBASE_BRANCH_ID");
17089
+ const sanitized = sanitizeBranchName(state.branch);
17090
+ if (envBranchId && envBranchId !== sanitized) {
17091
+ findings.push({
17092
+ id: "env-branch-drift",
17093
+ severity: "warn",
17094
+ message: `.env LAKEBASE_BRANCH_ID=${envBranchId} but workflow says ${sanitized}. The post-checkout hook may not have run since the last branch switch.`,
17095
+ suggestion: `git checkout '${state.branch}' # re-fires post-checkout`
17096
+ });
17097
+ }
17113
17098
  }
17114
- const order = {
17115
- drifted: 0,
17116
- missing: 1,
17117
- extra: 2,
17118
- unchanged: 3
17119
- };
17120
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
17121
- const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
17122
- return { overall: hasDrift ? "drift" : "ok", files };
17123
- }
17124
- function readKitVersionFromCommandsDir(kitCommandsDir) {
17125
- let dir = kitCommandsDir;
17126
- for (let i = 0; i < 5; i++) {
17127
- dir = path30.dirname(dir);
17099
+ if (headBranch && !TIER_LEAFS2.has(headBranch) && headBranch.startsWith(FEATURE_PREFIX)) {
17100
+ const sanitized = sanitizeBranchName(headBranch);
17101
+ const paired = lakebaseBranches.some((b) => leafOf2(b) === sanitized);
17102
+ if (!paired) {
17103
+ findings.push({
17104
+ id: "orphan-current-branch",
17105
+ severity: "fail",
17106
+ message: `Current git branch "${headBranch}" has no Lakebase pair (post-checkout fallback retired in phase C).`,
17107
+ suggestion: `lakebase-scm-recover-orphans --claim --only-branch '${headBranch}'`
17108
+ });
17109
+ }
17128
17110
  }
17129
17111
  try {
17130
- const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
17131
- const pkg = JSON.parse(raw);
17132
- return typeof pkg.version === "string" ? pkg.version : "unknown";
17112
+ const heads = await collapseMigrationHeads({ projectDir, dryRun: true });
17113
+ if (heads.headsBefore.length > 1) {
17114
+ findings.push({
17115
+ id: "multiple-migration-heads",
17116
+ severity: "fail",
17117
+ message: `Migrations have ${heads.headsBefore.length} heads (${heads.headsBefore.join(", ")}); a sibling-feature merge left them un-collapsed. \`upgrade head\` will refuse until they are unified.`,
17118
+ suggestion: "lakebase-collapse-heads"
17119
+ });
17120
+ }
17133
17121
  } catch {
17134
- return "unknown";
17135
17122
  }
17123
+ if (instance && state?.branch) {
17124
+ try {
17125
+ const localIds = listSchemaMigrations({ projectDir }).map((m) => m.version);
17126
+ let orphanRev = null;
17127
+ try {
17128
+ const status = await schemaMigrationStatus({ instance, branch: state.branch, projectDir });
17129
+ if (dbRevisionOrphaned(status.current, localIds)) orphanRev = status.current ?? null;
17130
+ } catch (e) {
17131
+ orphanRev = parseAlembicMissingRevision(e instanceof Error ? e.message : String(e));
17132
+ }
17133
+ if (orphanRev) {
17134
+ findings.push({
17135
+ id: "db-ahead-of-code",
17136
+ severity: "fail",
17137
+ message: `The paired branch DB is AHEAD of code: applied revision '${orphanRev}' has no local migration file. An aborted build likely migrated this branch and a git reset removed the migration file; alembic accept/deploy/promote will fail "Can't locate revision".`,
17138
+ suggestion: "reset the paired branch DB to the code head (downgrade/stamp + drop reset-created tables), or delete the branch and re-cut it clean from its tier"
17139
+ });
17140
+ }
17141
+ } catch {
17142
+ }
17143
+ }
17144
+ return finalize({
17145
+ projectDir,
17146
+ workflowStatePresent,
17147
+ state: state ?? void 0,
17148
+ inferredTierTopology: inferredTopology,
17149
+ findings
17150
+ });
17136
17151
  }
17137
- function detectScaffoldedDrift(args) {
17138
- const workflows = detectWorkflowDrift(args);
17139
- const commands = detectCommandDrift(args);
17140
- return {
17141
- overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
17142
- workflows,
17143
- commands
17144
- };
17152
+ function finalize(report) {
17153
+ let worst = "ok";
17154
+ for (const f of report.findings) {
17155
+ worst = worstOf(worst, f.severity);
17156
+ }
17157
+ return { ...report, worstSeverity: worst };
17145
17158
  }
17146
- function updateWorkflows(args) {
17147
- const projectWorkflowsDir = path30.join(
17148
- args.projectDir,
17149
- ".github",
17150
- "workflows"
17151
- );
17152
- const here = path30.dirname(new URL(import.meta.url).pathname);
17153
- const kitWorkflowsDir = args.kitDir ? path30.join(
17154
- args.kitDir,
17155
- "templates",
17156
- "project",
17157
- "common",
17158
- ".github",
17159
- "workflows"
17160
- ) : findKitTemplatesDir(here);
17161
- const substitute = args.substitute !== false;
17162
- const dryRun = args.dryRun === true;
17163
- const pruneExtras = args.pruneExtras === true;
17164
- const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
17165
- const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
17166
- if (!dryRun && templateFiles.length > 0 && !fs30.existsSync(projectWorkflowsDir)) {
17167
- fs30.mkdirSync(projectWorkflowsDir, { recursive: true });
17159
+ var ScmDoctorFixError = class extends Error {
17160
+ constructor(message, code) {
17161
+ super(message);
17162
+ this.code = code;
17163
+ this.name = "ScmDoctorFixError";
17168
17164
  }
17169
- const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
17170
- const seen = /* @__PURE__ */ new Set();
17171
- const files = [];
17172
- for (const name of templateFiles) {
17173
- seen.add(name);
17174
- const projectPath2 = path30.join(projectWorkflowsDir, name);
17175
- const templatePath = path30.join(kitWorkflowsDir, name);
17176
- const templateRaw = fs30.readFileSync(templatePath, "utf-8");
17177
- const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
17178
- const existed = fs30.existsSync(projectPath2);
17179
- const current = existed ? fs30.readFileSync(projectPath2, "utf-8") : "";
17180
- let outcome;
17181
- if (!existed) {
17182
- outcome = "added";
17183
- } else if (current === desired) {
17184
- outcome = "unchanged";
17185
- } else {
17186
- outcome = "updated";
17187
- }
17188
- if (!dryRun && outcome !== "unchanged") {
17189
- fs30.writeFileSync(projectPath2, desired);
17190
- }
17191
- files.push({ name, outcome });
17165
+ code;
17166
+ };
17167
+ var FIXABLE_FINDING_IDS = [
17168
+ "env-branch-drift",
17169
+ "head-branch-drift",
17170
+ "tier-topology-mismatch",
17171
+ "orphan-current-branch",
17172
+ "multiple-migration-heads",
17173
+ "db-ahead-of-code"
17174
+ ];
17175
+ async function fixFinding(args) {
17176
+ if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
17177
+ throw new ScmDoctorFixError(
17178
+ `Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
17179
+ "unsupported-finding"
17180
+ );
17192
17181
  }
17193
- if (pruneExtras) {
17194
- for (const name of projectFiles) {
17195
- if (seen.has(name)) continue;
17196
- const projectPath2 = path30.join(projectWorkflowsDir, name);
17197
- if (!dryRun) {
17198
- fs30.unlinkSync(projectPath2);
17182
+ const report = args.report ?? await runDoctor({ projectDir: args.projectDir, instance: args.instance });
17183
+ const present = report.findings.find((f) => f.id === args.findingId);
17184
+ if (!present) {
17185
+ throw new ScmDoctorFixError(
17186
+ `Finding "${args.findingId}" is not present in the current report. Re-run lakebase-scm-doctor to see what needs fixing.`,
17187
+ "finding-not-present"
17188
+ );
17189
+ }
17190
+ let action = "";
17191
+ try {
17192
+ switch (args.findingId) {
17193
+ case "env-branch-drift": {
17194
+ const branch = report.state?.branch;
17195
+ if (!branch) {
17196
+ throw new ScmDoctorFixError(
17197
+ "Cannot fix: workflow state has no branch field.",
17198
+ "fix-failed"
17199
+ );
17200
+ }
17201
+ const sanitized = sanitizeBranchName(branch);
17202
+ const envFile = path30.join(args.projectDir, ".env");
17203
+ updateEnvConnection({
17204
+ envPath: envFile,
17205
+ projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
17206
+ branchId: sanitized,
17207
+ username: readEnvVar(envFile, "DB_USERNAME") ?? "",
17208
+ endpointHost: readEnvVar(envFile, "LAKEBASE_HOST")
17209
+ });
17210
+ action = `rewrote .env LAKEBASE_BRANCH_ID=${sanitized} (metadata only; the app mints its token at runtime)`;
17211
+ break;
17212
+ }
17213
+ case "head-branch-drift": {
17214
+ const branch = report.state?.branch;
17215
+ if (!branch) {
17216
+ throw new ScmDoctorFixError(
17217
+ "Cannot fix: workflow state has no branch field.",
17218
+ "fix-failed"
17219
+ );
17220
+ }
17221
+ await exec2(`git checkout ${shellEscape3(branch)}`, {
17222
+ cwd: args.projectDir,
17223
+ timeout: 15e3
17224
+ });
17225
+ action = `git checkout ${branch} (re-fires post-checkout to resync HEAD)`;
17226
+ break;
17227
+ }
17228
+ case "tier-topology-mismatch": {
17229
+ const instance = args.instance ?? report.state?.project_id;
17230
+ if (!instance) {
17231
+ throw new ScmDoctorFixError(
17232
+ "Cannot fix: missing Lakebase project id.",
17233
+ "fix-failed"
17234
+ );
17235
+ }
17236
+ await adoptScmState({
17237
+ projectDir: args.projectDir,
17238
+ instance,
17239
+ force: true
17240
+ });
17241
+ action = `adopted state with --force to re-infer tier_topology`;
17242
+ break;
17243
+ }
17244
+ case "orphan-current-branch": {
17245
+ const instance = args.instance ?? report.state?.project_id;
17246
+ if (!instance) {
17247
+ throw new ScmDoctorFixError(
17248
+ "Cannot fix: missing Lakebase project id.",
17249
+ "fix-failed"
17250
+ );
17251
+ }
17252
+ const headBranch = await getCurrentBranch({ cwd: args.projectDir });
17253
+ if (!headBranch) {
17254
+ throw new ScmDoctorFixError(
17255
+ "Cannot fix: detached HEAD or no current branch.",
17256
+ "fix-failed"
17257
+ );
17258
+ }
17259
+ await recoverOrphans({
17260
+ projectDir: args.projectDir,
17261
+ instance,
17262
+ claim: true,
17263
+ onlyBranch: headBranch
17264
+ });
17265
+ action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
17266
+ break;
17267
+ }
17268
+ case "multiple-migration-heads": {
17269
+ const r = await collapseMigrationHeads({ projectDir: args.projectDir });
17270
+ if (r.status !== "ok" || !r.mergeRevision) {
17271
+ throw new ScmDoctorFixError(
17272
+ `Expected to create a merge revision but got status="${r.status}".`,
17273
+ "fix-failed"
17274
+ );
17275
+ }
17276
+ action = `collapsed ${r.headsBefore.length} heads into merge revision ${r.mergeRevision} (commit it)`;
17277
+ break;
17278
+ }
17279
+ case "db-ahead-of-code": {
17280
+ const r = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
17281
+ action = `abandoned the feature (deleted the polluted paired branch${r.lakebaseDeleted ? "" : " , Lakebase delete reported not-deleted"}) and reset state to '${r.state.state}'; re-claim to re-fork a clean branch from the tier`;
17282
+ break;
17199
17283
  }
17200
- files.push({ name, outcome: "removed" });
17201
17284
  }
17285
+ } catch (err) {
17286
+ if (err instanceof ScmDoctorFixError) throw err;
17287
+ throw new ScmDoctorFixError(
17288
+ `Remediation failed: ${err instanceof Error ? err.message : String(err)}`,
17289
+ "fix-failed"
17290
+ );
17202
17291
  }
17203
- const order = {
17204
- added: 0,
17205
- updated: 1,
17206
- removed: 2,
17207
- unchanged: 3
17208
- };
17209
- files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
17210
- const changed = files.some((f) => f.outcome !== "unchanged");
17211
- return { files, changed };
17292
+ const postReport = await runDoctor({
17293
+ projectDir: args.projectDir,
17294
+ instance: args.instance
17295
+ });
17296
+ return { findingId: args.findingId, action, postReport };
17297
+ }
17298
+ function shellEscape3(s) {
17299
+ return `'${s.replace(/'/g, "'\\''")}'`;
17212
17300
  }
17213
17301
 
17214
17302
  // scripts/lakebase/doctor.ts
17303
+ init_esm_shims();
17304
+ import * as fs31 from "fs";
17305
+ import * as path31 from "path";
17306
+ import * as cp6 from "child_process";
17215
17307
  function readEnvFile(projectDir) {
17216
17308
  const envPath = path31.join(projectDir, ".env");
17217
17309
  if (!fs31.existsSync(envPath)) return {};
@@ -17644,7 +17736,7 @@ function checkWorkflowDrift(projectDir) {
17644
17736
  status: "warn",
17645
17737
  message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
17646
17738
  detail: { files: report.files.map((f) => ({ name: f.name, status: f.status })) },
17647
- hint: "Inspect via the lakebase_workflow_drift MCP tool (or detectWorkflowDrift import). Refresh manually until updateWorkflows lands."
17739
+ 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."
17648
17740
  };
17649
17741
  } catch (err) {
17650
17742
  return {
@@ -18120,63 +18212,6 @@ async function discardAllChanges(args) {
18120
18212
  await exec2("git clean -fd", { cwd: args.cwd });
18121
18213
  }
18122
18214
 
18123
- // scripts/git/sync.ts
18124
- init_esm_shims();
18125
- async function currentBranchName2(cwd) {
18126
- try {
18127
- return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
18128
- } catch {
18129
- return "";
18130
- }
18131
- }
18132
- async function push(args) {
18133
- await exec2("git push", { cwd: args.cwd });
18134
- }
18135
- async function pull(args) {
18136
- await exec2("git pull", { cwd: args.cwd });
18137
- }
18138
- async function publishBranch(args) {
18139
- const remote = args.remote ?? "origin";
18140
- const branch = await currentBranchName2(args.cwd);
18141
- if (!branch) throw new Error("No current branch");
18142
- await exec2(`git push -u ${remote} ${shq(branch)}`, {
18143
- cwd: args.cwd
18144
- });
18145
- }
18146
- async function pushCurrentBranchForPr(args) {
18147
- const remote = args.remote ?? "origin";
18148
- const branch = await currentBranchName2(args.cwd);
18149
- if (!branch) throw new Error("No current branch");
18150
- const upstreamSet = await hasUpstream({ cwd: args.cwd });
18151
- if (!upstreamSet) {
18152
- await exec2(`git push -u ${remote} ${shq(branch)}`, {
18153
- cwd: args.cwd
18154
- });
18155
- } else {
18156
- await exec2("git push", { cwd: args.cwd });
18157
- }
18158
- }
18159
- async function fetch2(args) {
18160
- const parts = ["git fetch"];
18161
- if (args.prune) parts.push("--prune");
18162
- if (args.all) parts.push("--all");
18163
- await exec2(parts.join(" "), { cwd: args.cwd });
18164
- }
18165
- async function pullFrom(args) {
18166
- await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
18167
- cwd: args.cwd
18168
- });
18169
- }
18170
- async function pushTo(args) {
18171
- await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
18172
- cwd: args.cwd
18173
- });
18174
- }
18175
- async function sync(args) {
18176
- await exec2("git pull", { cwd: args.cwd });
18177
- await exec2("git push", { cwd: args.cwd });
18178
- }
18179
-
18180
18215
  // scripts/git/branch-tag.ts
18181
18216
  init_esm_shims();
18182
18217
  var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["production", "main", "master"]);
@@ -18581,6 +18616,7 @@ export {
18581
18616
  createWorktree,
18582
18617
  cutBackup,
18583
18618
  databricksAuthPrereqMessage,
18619
+ defaultRefreshPromoteWorkflows,
18584
18620
  delay,
18585
18621
  deleteAppEndpoint,
18586
18622
  deleteBranch,