@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
@@ -79021,6 +79021,7 @@ __export(scripts_exports, {
79021
79021
  createWorktree: () => createWorktree,
79022
79022
  cutBackup: () => cutBackup,
79023
79023
  databricksAuthPrereqMessage: () => databricksAuthPrereqMessage,
79024
+ defaultRefreshPromoteWorkflows: () => defaultRefreshPromoteWorkflows,
79024
79025
  delay: () => delay,
79025
79026
  deleteAppEndpoint: () => deleteAppEndpoint,
79026
79027
  deleteBranch: () => deleteBranch,
@@ -88916,8 +88917,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
88916
88917
  var cached;
88917
88918
  function substrateSelfVersion() {
88918
88919
  if (cached !== void 0) return cached;
88919
- if ("0.2.44".length > 0) {
88920
- cached = "0.2.44";
88920
+ if ("0.2.45".length > 0) {
88921
+ cached = "0.2.45";
88921
88922
  return cached;
88922
88923
  }
88923
88924
  cached = "unknown";
@@ -95218,123 +95219,500 @@ function pickRunUrl(pr) {
95218
95219
 
95219
95220
  // scripts/lakebase/scm-merge.ts
95220
95221
  init_cjs_shims();
95221
- async function reconcileTierToOrigin(args) {
95222
- const { cwd, tier } = args;
95222
+
95223
+ // scripts/git/sync.ts
95224
+ init_cjs_shims();
95225
+ async function currentBranchName(cwd) {
95223
95226
  try {
95224
- const head = await getCurrentBranch({ cwd });
95225
- if (head === tier) {
95226
- await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
95227
- } else {
95228
- await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
95229
- }
95230
- return null;
95231
- } catch (err) {
95232
- 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).`;
95227
+ return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
95228
+ } catch {
95229
+ return "";
95233
95230
  }
95234
95231
  }
95235
- var ScmMergeError = class extends Error {
95236
- constructor(message, code) {
95237
- super(message);
95238
- this.code = code;
95239
- this.name = "ScmMergeError";
95240
- }
95241
- code;
95242
- };
95243
- var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
95244
- var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
95245
- function defaultMigratePredicate(run, mergedAt) {
95246
- if (!run.createdAt) return false;
95247
- const created = Date.parse(run.createdAt);
95248
- if (!Number.isFinite(created)) return false;
95249
- if (run.event && run.event !== "push") return false;
95250
- return created >= mergedAt.getTime() - 5e3;
95232
+ async function push(args) {
95233
+ await exec2("git push", { cwd: args.cwd });
95251
95234
  }
95252
- function shaMigratePredicate(mergeCommitSha) {
95253
- return (run) => {
95254
- if (run.event && run.event !== "push") return false;
95255
- return !!run.headSha && run.headSha === mergeCommitSha;
95256
- };
95235
+ async function pull(args) {
95236
+ await exec2("git pull", { cwd: args.cwd });
95257
95237
  }
95258
- async function mergeFeature(args) {
95259
- const current = readWorkflowState(args.projectDir);
95260
- if (!current) {
95261
- throw new ScmMergeError(
95262
- "No SCM workflow state. wait-ci first.",
95263
- "no-state-file"
95264
- );
95265
- }
95266
- if (current.state !== "ci-green") {
95267
- throw new ScmMergeError(
95268
- `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
95269
- "bad-precondition"
95270
- );
95238
+ async function publishBranch(args) {
95239
+ const remote = args.remote ?? "origin";
95240
+ const branch = await currentBranchName(args.cwd);
95241
+ if (!branch) throw new Error("No current branch");
95242
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
95243
+ cwd: args.cwd
95244
+ });
95245
+ }
95246
+ async function pushCurrentBranchForPr(args) {
95247
+ const remote = args.remote ?? "origin";
95248
+ const branch = await currentBranchName(args.cwd);
95249
+ if (!branch) throw new Error("No current branch");
95250
+ const upstreamSet = await hasUpstream({ cwd: args.cwd });
95251
+ if (!upstreamSet) {
95252
+ await exec2(`git push -u ${remote} ${shq(branch)}`, {
95253
+ cwd: args.cwd
95254
+ });
95255
+ } else {
95256
+ await exec2("git push", { cwd: args.cwd });
95271
95257
  }
95272
- if (!current.pr_url) {
95273
- throw new ScmMergeError(
95274
- "ci-green row is missing pr_url; cannot resolve the PR to merge.",
95275
- "no-pr-url"
95258
+ }
95259
+ async function fetch3(args) {
95260
+ const parts = ["git fetch"];
95261
+ if (args.prune) parts.push("--prune");
95262
+ if (args.all) parts.push("--all");
95263
+ await exec2(parts.join(" "), { cwd: args.cwd });
95264
+ }
95265
+ async function pullFrom(args) {
95266
+ await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
95267
+ cwd: args.cwd
95268
+ });
95269
+ }
95270
+ async function pushTo(args) {
95271
+ await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
95272
+ cwd: args.cwd
95273
+ });
95274
+ }
95275
+ async function sync(args) {
95276
+ await exec2("git pull", { cwd: args.cwd });
95277
+ await exec2("git push", { cwd: args.cwd });
95278
+ }
95279
+
95280
+ // scripts/lakebase/workflow-drift.ts
95281
+ init_cjs_shims();
95282
+ var fs30 = __toESM(require("fs"), 1);
95283
+ var path28 = __toESM(require("path"), 1);
95284
+ function findKitTemplatesDir(start) {
95285
+ let dir = start;
95286
+ for (let i2 = 0; i2 < 6; i2++) {
95287
+ const candidate = path28.join(
95288
+ dir,
95289
+ "templates",
95290
+ "project",
95291
+ "common",
95292
+ ".github",
95293
+ "workflows"
95276
95294
  );
95295
+ if (fs30.existsSync(candidate)) return candidate;
95296
+ const parent = path28.dirname(dir);
95297
+ if (parent === dir) break;
95298
+ dir = parent;
95277
95299
  }
95278
- if (!current.branch || !current.parent_branch) {
95279
- throw new ScmMergeError(
95280
- "ci-green row missing branch / parent_branch; refusing to merge.",
95281
- "bad-precondition"
95282
- );
95300
+ throw new Error(
95301
+ `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
95302
+ );
95303
+ }
95304
+ function unifiedDiff(name, projectContent, templateContent) {
95305
+ if (projectContent === templateContent) return "";
95306
+ const a = projectContent.split("\n");
95307
+ const b = templateContent.split("\n");
95308
+ const out = [`--- project/${name}`, `+++ template/${name}`];
95309
+ const max = Math.max(a.length, b.length);
95310
+ for (let i2 = 0; i2 < max; i2++) {
95311
+ const av = a[i2];
95312
+ const bv = b[i2];
95313
+ if (av === bv) continue;
95314
+ if (av !== void 0) out.push(`-${i2 + 1}: ${av}`);
95315
+ if (bv !== void 0) out.push(`+${i2 + 1}: ${bv}`);
95283
95316
  }
95284
- const ownerRepo = await getOwnerRepo(args.projectDir);
95285
- if (!ownerRepo) {
95286
- throw new ScmMergeError(
95287
- "No GitHub remote found at origin.",
95288
- "no-github-remote"
95289
- );
95317
+ return out.join("\n");
95318
+ }
95319
+ function detectWorkflowDrift(args) {
95320
+ const projectWorkflowsDir = path28.join(
95321
+ args.projectDir,
95322
+ ".github",
95323
+ "workflows"
95324
+ );
95325
+ const here = path28.dirname(new URL(importMetaUrl).pathname);
95326
+ const kitWorkflowsDir = args.kitDir ? path28.join(
95327
+ args.kitDir,
95328
+ "templates",
95329
+ "project",
95330
+ "common",
95331
+ ".github",
95332
+ "workflows"
95333
+ ) : findKitTemplatesDir(here);
95334
+ const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95335
+ const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95336
+ const version = readKitVersion(kitWorkflowsDir);
95337
+ const seen = /* @__PURE__ */ new Set();
95338
+ const files = [];
95339
+ for (const name of templateFiles) {
95340
+ seen.add(name);
95341
+ const projectPath2 = path28.join(projectWorkflowsDir, name);
95342
+ const templatePath = path28.join(kitWorkflowsDir, name);
95343
+ if (!fs30.existsSync(projectPath2)) {
95344
+ files.push({ name, status: "missing" });
95345
+ continue;
95346
+ }
95347
+ const projectContent = fs30.readFileSync(projectPath2, "utf8");
95348
+ const templateContent = applyPlaceholders(fs30.readFileSync(templatePath, "utf8"), version);
95349
+ if (projectContent === templateContent) {
95350
+ files.push({ name, status: "unchanged" });
95351
+ } else {
95352
+ files.push({
95353
+ name,
95354
+ status: "drifted",
95355
+ diff: unifiedDiff(name, projectContent, templateContent)
95356
+ });
95357
+ }
95290
95358
  }
95291
- const pullNumber = extractPullNumber(current.pr_url);
95292
- if (!pullNumber) {
95293
- throw new ScmMergeError(
95294
- `Could not extract PR number from URL: ${current.pr_url}`,
95295
- "bad-pr-url"
95296
- );
95359
+ for (const name of projectFiles) {
95360
+ if (seen.has(name)) continue;
95361
+ files.push({ name, status: "extra" });
95297
95362
  }
95298
- const instance = args.instance ?? current.project_id;
95299
- const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
95300
- const wantMigrate = args.waitMigrate !== false;
95301
- let authVerified = false;
95302
- if (wantMigrate && args.verifyMigrateAuth) {
95303
- const probe = await args.verifyMigrateAuth();
95304
- if (!probe.ok) {
95305
- throw new ScmMergeError(
95306
- `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.`,
95307
- "migrate-auth"
95308
- );
95309
- }
95310
- authVerified = true;
95363
+ const order = {
95364
+ drifted: 0,
95365
+ missing: 1,
95366
+ extra: 2,
95367
+ unchanged: 3
95368
+ };
95369
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
95370
+ const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
95371
+ return {
95372
+ overall: hasDrift ? "drift" : "ok",
95373
+ files
95374
+ };
95375
+ }
95376
+ function readKitVersion(kitWorkflowsDir) {
95377
+ let dir = kitWorkflowsDir;
95378
+ for (let i2 = 0; i2 < 5; i2++) {
95379
+ dir = path28.dirname(dir);
95311
95380
  }
95312
- let paired;
95313
95381
  try {
95314
- paired = await mergePairedPullRequest({
95315
- ownerRepo,
95316
- pullNumber,
95317
- lakebaseInstance: instance,
95318
- method: args.method ?? "squash"
95319
- });
95320
- } catch (err) {
95321
- throw new ScmMergeError(
95322
- `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
95323
- "merge-failed"
95382
+ const raw = fs30.readFileSync(path28.join(dir, "package.json"), "utf-8");
95383
+ const pkg = JSON.parse(raw);
95384
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
95385
+ } catch {
95386
+ return "unknown";
95387
+ }
95388
+ }
95389
+ function applyPlaceholders(content, version) {
95390
+ return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
95391
+ }
95392
+ function applyCommandPlaceholders(content, version) {
95393
+ return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
95394
+ }
95395
+ var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
95396
+ function findKitCommandsDir(start) {
95397
+ let dir = start;
95398
+ for (let i2 = 0; i2 < 6; i2++) {
95399
+ const candidate = path28.join(
95400
+ dir,
95401
+ "templates",
95402
+ "project",
95403
+ "common",
95404
+ ".claude",
95405
+ "commands"
95324
95406
  );
95407
+ if (fs30.existsSync(candidate)) return candidate;
95408
+ const parent = path28.dirname(dir);
95409
+ if (parent === dir) break;
95410
+ dir = parent;
95325
95411
  }
95326
- const warnings = [...paired.warnings];
95327
- let localBranchDeleted = false;
95328
- let headAfter = current.branch;
95329
- if (!args.skipLocalCleanup) {
95330
- const switchTo = args.switchTo ?? gitBase;
95331
- const head = await getCurrentBranch({ cwd: args.projectDir });
95332
- if (head === current.branch) {
95333
- try {
95334
- await exec2(`git checkout ${shellEscape2(switchTo)}`, {
95335
- cwd: args.projectDir,
95336
- timeout: 1e4
95337
- });
95412
+ throw new Error(
95413
+ `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
95414
+ );
95415
+ }
95416
+ function parsePinnedVersion(content) {
95417
+ const m2 = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
95418
+ return m2 ? m2[1] : void 0;
95419
+ }
95420
+ function detectCommandDrift(args) {
95421
+ const projectCommandsDir = path28.join(args.projectDir, ".claude", "commands");
95422
+ const here = path28.dirname(new URL(importMetaUrl).pathname);
95423
+ const kitCommandsDir = args.kitDir ? path28.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
95424
+ const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
95425
+ const templateFiles = fs30.existsSync(kitCommandsDir) ? fs30.readdirSync(kitCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
95426
+ const projectFiles = fs30.existsSync(projectCommandsDir) ? fs30.readdirSync(projectCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
95427
+ const seen = /* @__PURE__ */ new Set();
95428
+ const files = [];
95429
+ for (const name of templateFiles) {
95430
+ seen.add(name);
95431
+ const projectPath2 = path28.join(projectCommandsDir, name);
95432
+ const templatePath = path28.join(kitCommandsDir, name);
95433
+ const templateRaw = fs30.readFileSync(templatePath, "utf8");
95434
+ if (!fs30.existsSync(projectPath2)) {
95435
+ files.push({ name, status: "missing", kit_version: kitVersion });
95436
+ continue;
95437
+ }
95438
+ const projectContent = fs30.readFileSync(projectPath2, "utf8");
95439
+ const pinned = parsePinnedVersion(projectContent);
95440
+ const versionForCompare = pinned ?? kitVersion;
95441
+ const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
95442
+ if (projectContent === templateContent) {
95443
+ files.push({
95444
+ name,
95445
+ status: "unchanged",
95446
+ pinned_version: pinned,
95447
+ kit_version: kitVersion
95448
+ });
95449
+ } else {
95450
+ files.push({
95451
+ name,
95452
+ status: "drifted",
95453
+ pinned_version: pinned,
95454
+ kit_version: kitVersion,
95455
+ diff: unifiedDiff(name, projectContent, templateContent)
95456
+ });
95457
+ }
95458
+ }
95459
+ for (const name of projectFiles) {
95460
+ if (seen.has(name)) continue;
95461
+ files.push({ name, status: "extra", kit_version: kitVersion });
95462
+ }
95463
+ const order = {
95464
+ drifted: 0,
95465
+ missing: 1,
95466
+ extra: 2,
95467
+ unchanged: 3
95468
+ };
95469
+ files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
95470
+ const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
95471
+ return { overall: hasDrift ? "drift" : "ok", files };
95472
+ }
95473
+ function readKitVersionFromCommandsDir(kitCommandsDir) {
95474
+ let dir = kitCommandsDir;
95475
+ for (let i2 = 0; i2 < 5; i2++) {
95476
+ dir = path28.dirname(dir);
95477
+ }
95478
+ try {
95479
+ const raw = fs30.readFileSync(path28.join(dir, "package.json"), "utf-8");
95480
+ const pkg = JSON.parse(raw);
95481
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
95482
+ } catch {
95483
+ return "unknown";
95484
+ }
95485
+ }
95486
+ function detectScaffoldedDrift(args) {
95487
+ const workflows = detectWorkflowDrift(args);
95488
+ const commands = detectCommandDrift(args);
95489
+ return {
95490
+ overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
95491
+ workflows,
95492
+ commands
95493
+ };
95494
+ }
95495
+ function updateWorkflows(args) {
95496
+ const projectWorkflowsDir = path28.join(
95497
+ args.projectDir,
95498
+ ".github",
95499
+ "workflows"
95500
+ );
95501
+ const here = path28.dirname(new URL(importMetaUrl).pathname);
95502
+ const kitWorkflowsDir = args.kitDir ? path28.join(
95503
+ args.kitDir,
95504
+ "templates",
95505
+ "project",
95506
+ "common",
95507
+ ".github",
95508
+ "workflows"
95509
+ ) : findKitTemplatesDir(here);
95510
+ const substitute = args.substitute !== false;
95511
+ const dryRun = args.dryRun === true;
95512
+ const pruneExtras = args.pruneExtras === true;
95513
+ const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95514
+ const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
95515
+ if (!dryRun && templateFiles.length > 0 && !fs30.existsSync(projectWorkflowsDir)) {
95516
+ fs30.mkdirSync(projectWorkflowsDir, { recursive: true });
95517
+ }
95518
+ const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
95519
+ const seen = /* @__PURE__ */ new Set();
95520
+ const files = [];
95521
+ for (const name of templateFiles) {
95522
+ seen.add(name);
95523
+ const projectPath2 = path28.join(projectWorkflowsDir, name);
95524
+ const templatePath = path28.join(kitWorkflowsDir, name);
95525
+ const templateRaw = fs30.readFileSync(templatePath, "utf-8");
95526
+ const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
95527
+ const existed = fs30.existsSync(projectPath2);
95528
+ const current = existed ? fs30.readFileSync(projectPath2, "utf-8") : "";
95529
+ let outcome;
95530
+ if (!existed) {
95531
+ outcome = "added";
95532
+ } else if (current === desired) {
95533
+ outcome = "unchanged";
95534
+ } else {
95535
+ outcome = "updated";
95536
+ }
95537
+ if (!dryRun && outcome !== "unchanged") {
95538
+ fs30.writeFileSync(projectPath2, desired);
95539
+ }
95540
+ files.push({ name, outcome });
95541
+ }
95542
+ if (pruneExtras) {
95543
+ for (const name of projectFiles) {
95544
+ if (seen.has(name)) continue;
95545
+ const projectPath2 = path28.join(projectWorkflowsDir, name);
95546
+ if (!dryRun) {
95547
+ fs30.unlinkSync(projectPath2);
95548
+ }
95549
+ files.push({ name, outcome: "removed" });
95550
+ }
95551
+ }
95552
+ const order = {
95553
+ added: 0,
95554
+ updated: 1,
95555
+ removed: 2,
95556
+ unchanged: 3
95557
+ };
95558
+ files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
95559
+ const changed = files.some((f3) => f3.outcome !== "unchanged");
95560
+ return { files, changed };
95561
+ }
95562
+
95563
+ // scripts/lakebase/scm-merge.ts
95564
+ async function reconcileTierToOrigin(args) {
95565
+ const { cwd, tier } = args;
95566
+ try {
95567
+ const head = await getCurrentBranch({ cwd });
95568
+ if (head === tier) {
95569
+ await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
95570
+ } else {
95571
+ await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
95572
+ }
95573
+ return null;
95574
+ } catch (err) {
95575
+ 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).`;
95576
+ }
95577
+ }
95578
+ var ScmMergeError = class extends Error {
95579
+ constructor(message, code) {
95580
+ super(message);
95581
+ this.code = code;
95582
+ this.name = "ScmMergeError";
95583
+ }
95584
+ code;
95585
+ };
95586
+ var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
95587
+ var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
95588
+ function defaultMigratePredicate(run, mergedAt) {
95589
+ if (!run.createdAt) return false;
95590
+ const created = Date.parse(run.createdAt);
95591
+ if (!Number.isFinite(created)) return false;
95592
+ if (run.event && run.event !== "push") return false;
95593
+ return created >= mergedAt.getTime() - 5e3;
95594
+ }
95595
+ function shaMigratePredicate(mergeCommitSha) {
95596
+ return (run) => {
95597
+ if (run.event && run.event !== "push") return false;
95598
+ return !!run.headSha && run.headSha === mergeCommitSha;
95599
+ };
95600
+ }
95601
+ async function defaultRefreshPromoteWorkflows(projectDir) {
95602
+ const drift = detectWorkflowDrift({ projectDir });
95603
+ const touched = drift.files.filter((f3) => f3.status === "drifted").map((f3) => f3.name);
95604
+ if (touched.length === 0) return { refreshed: false };
95605
+ const result = updateWorkflows({ projectDir });
95606
+ if (!result.changed) return { refreshed: false };
95607
+ await exec2(`git add ${shellEscape2(".github/workflows")}`, { cwd: projectDir, timeout: 1e4 });
95608
+ const staged = await exec2("git diff --cached --name-only -- .github/workflows", {
95609
+ cwd: projectDir,
95610
+ timeout: 1e4
95611
+ });
95612
+ if (!staged.trim()) return { refreshed: false };
95613
+ await exec2(
95614
+ `git commit -m ${shellEscape2(
95615
+ "chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)"
95616
+ )} -- .github/workflows`,
95617
+ { cwd: projectDir, timeout: 15e3 }
95618
+ );
95619
+ await pushCurrentBranchForPr({ cwd: projectDir });
95620
+ return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
95621
+ }
95622
+ async function mergeFeature(args) {
95623
+ const current = readWorkflowState(args.projectDir);
95624
+ if (!current) {
95625
+ throw new ScmMergeError(
95626
+ "No SCM workflow state. wait-ci first.",
95627
+ "no-state-file"
95628
+ );
95629
+ }
95630
+ if (current.state !== "ci-green") {
95631
+ throw new ScmMergeError(
95632
+ `merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
95633
+ "bad-precondition"
95634
+ );
95635
+ }
95636
+ if (!current.pr_url) {
95637
+ throw new ScmMergeError(
95638
+ "ci-green row is missing pr_url; cannot resolve the PR to merge.",
95639
+ "no-pr-url"
95640
+ );
95641
+ }
95642
+ if (!current.branch || !current.parent_branch) {
95643
+ throw new ScmMergeError(
95644
+ "ci-green row missing branch / parent_branch; refusing to merge.",
95645
+ "bad-precondition"
95646
+ );
95647
+ }
95648
+ const ownerRepo = await getOwnerRepo(args.projectDir);
95649
+ if (!ownerRepo) {
95650
+ throw new ScmMergeError(
95651
+ "No GitHub remote found at origin.",
95652
+ "no-github-remote"
95653
+ );
95654
+ }
95655
+ const pullNumber = extractPullNumber(current.pr_url);
95656
+ if (!pullNumber) {
95657
+ throw new ScmMergeError(
95658
+ `Could not extract PR number from URL: ${current.pr_url}`,
95659
+ "bad-pr-url"
95660
+ );
95661
+ }
95662
+ const instance = args.instance ?? current.project_id;
95663
+ const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
95664
+ const wantMigrate = args.waitMigrate !== false;
95665
+ let authVerified = false;
95666
+ if (wantMigrate && args.verifyMigrateAuth) {
95667
+ const probe = await args.verifyMigrateAuth();
95668
+ if (!probe.ok) {
95669
+ throw new ScmMergeError(
95670
+ `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.`,
95671
+ "migrate-auth"
95672
+ );
95673
+ }
95674
+ authVerified = true;
95675
+ }
95676
+ const preflightNotes = [];
95677
+ {
95678
+ const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
95679
+ try {
95680
+ const r2 = await refresh();
95681
+ if (r2.refreshed) {
95682
+ preflightNotes.push(`Refreshed stale CI workflows before merge${r2.detail ? ` (${r2.detail})` : ""}.`);
95683
+ }
95684
+ } catch (err) {
95685
+ preflightNotes.push(
95686
+ `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').`
95687
+ );
95688
+ }
95689
+ }
95690
+ let paired;
95691
+ try {
95692
+ paired = await mergePairedPullRequest({
95693
+ ownerRepo,
95694
+ pullNumber,
95695
+ lakebaseInstance: instance,
95696
+ method: args.method ?? "squash"
95697
+ });
95698
+ } catch (err) {
95699
+ throw new ScmMergeError(
95700
+ `mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
95701
+ "merge-failed"
95702
+ );
95703
+ }
95704
+ const warnings = [...preflightNotes, ...paired.warnings];
95705
+ let localBranchDeleted = false;
95706
+ let headAfter = current.branch;
95707
+ if (!args.skipLocalCleanup) {
95708
+ const switchTo = args.switchTo ?? gitBase;
95709
+ const head = await getCurrentBranch({ cwd: args.projectDir });
95710
+ if (head === current.branch) {
95711
+ try {
95712
+ await exec2(`git checkout ${shellEscape2(switchTo)}`, {
95713
+ cwd: args.projectDir,
95714
+ timeout: 1e4
95715
+ });
95338
95716
  headAfter = switchTo;
95339
95717
  try {
95340
95718
  await exec2(`git fetch origin ${shellEscape2(switchTo)}`, {
@@ -95520,7 +95898,7 @@ init_cjs_shims();
95520
95898
 
95521
95899
  // scripts/git/branches.ts
95522
95900
  init_cjs_shims();
95523
- async function currentBranchName(cwd) {
95901
+ async function currentBranchName2(cwd) {
95524
95902
  try {
95525
95903
  return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
95526
95904
  } catch {
@@ -95539,7 +95917,7 @@ async function listLocalBranches(args) {
95539
95917
  return [];
95540
95918
  }
95541
95919
  if (!raw) return [];
95542
- const current = await currentBranchName(cwd);
95920
+ const current = await currentBranchName2(cwd);
95543
95921
  return raw.split("\n").filter(Boolean).map((line) => {
95544
95922
  const [name, tracking, trackInfo] = line.split("|");
95545
95923
  let ahead = 0;
@@ -95739,15 +96117,15 @@ function parentForTopology(t2, defaultLeaf) {
95739
96117
 
95740
96118
  // scripts/lakebase/scm-doctor.ts
95741
96119
  init_cjs_shims();
95742
- var fs30 = __toESM(require("fs"), 1);
95743
- var path28 = __toESM(require("path"), 1);
96120
+ var fs31 = __toESM(require("fs"), 1);
96121
+ var path29 = __toESM(require("path"), 1);
95744
96122
  var FEATURE_PREFIX = "feature/";
95745
96123
  var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
95746
96124
  function readEnv(projectDir) {
95747
- const envPath = path28.join(projectDir, ".env");
96125
+ const envPath = path29.join(projectDir, ".env");
95748
96126
  const out = /* @__PURE__ */ new Map();
95749
- if (!fs30.existsSync(envPath)) return out;
95750
- const lines = fs30.readFileSync(envPath, "utf8").split("\n");
96127
+ if (!fs31.existsSync(envPath)) return out;
96128
+ const lines = fs31.readFileSync(envPath, "utf8").split("\n");
95751
96129
  for (const line of lines) {
95752
96130
  const m2 = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
95753
96131
  if (m2) out.set(m2[1], m2[2].replace(/^["']|["']$/g, ""));
@@ -95793,622 +96171,337 @@ async function runDoctor(args, deps = {}) {
95793
96171
  findings.push({
95794
96172
  id: "github-actions-disabled",
95795
96173
  severity: "warn",
95796
- 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.`,
95797
- 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."
95798
- });
95799
- }
95800
- }
95801
- } catch {
95802
- }
95803
- for (const wf of ["pr.yml", "merge.yml"]) {
95804
- try {
95805
- const p = path28.join(projectDir, ".github", "workflows", wf);
95806
- if (!fs30.existsSync(p)) continue;
95807
- const body = fs30.readFileSync(p, "utf8");
95808
- if (/lakebase-scm-utils#v\d/.test(body)) {
95809
- findings.push({
95810
- id: "ci-workflow-substrate-pin",
95811
- severity: "warn",
95812
- 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.`,
95813
- 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."
95814
- });
95815
- }
95816
- } catch {
95817
- }
95818
- }
95819
- try {
95820
- if (isGitTracked(projectDir, ".lakebase/workflow-state.json")) {
95821
- findings.push({
95822
- id: "scm-state-git-tracked",
95823
- severity: "warn",
95824
- 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).",
95825
- 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."
95826
- });
95827
- }
95828
- } catch {
95829
- }
95830
- if (!env.has("LAKEBASE_PROJECT_ID")) {
95831
- findings.push({
95832
- id: "env-missing-project-id",
95833
- severity: "fail",
95834
- message: ".env does not contain LAKEBASE_PROJECT_ID. The post-checkout hook will exit early; workflow CLIs will need an explicit --instance.",
95835
- suggestion: "Set LAKEBASE_PROJECT_ID=<your project id> in .env"
95836
- });
95837
- }
95838
- if (!instance) {
95839
- return finalize({
95840
- projectDir,
95841
- workflowStatePresent,
95842
- state: state ?? void 0,
95843
- findings
95844
- });
95845
- }
95846
- let lakebaseBranches = [];
95847
- try {
95848
- lakebaseBranches = await listBranches({ instance });
95849
- } catch (err) {
95850
- findings.push({
95851
- id: "lakebase-unreachable",
95852
- severity: "fail",
95853
- message: `Could not list Lakebase branches for instance ${instance}: ${err instanceof Error ? err.message : String(err)}`,
95854
- suggestion: "databricks auth login (or check DATABRICKS_CONFIG_PROFILE)."
95855
- });
95856
- return finalize({
95857
- projectDir,
95858
- workflowStatePresent,
95859
- state: state ?? void 0,
95860
- findings
95861
- });
95862
- }
95863
- const inferredTopology = inferTierTopology(lakebaseBranches);
95864
- if (state && state.tier_topology !== inferredTopology) {
95865
- findings.push({
95866
- id: "tier-topology-mismatch",
95867
- severity: "warn",
95868
- message: `workflow-state records tier_topology=${state.tier_topology}, but the Lakebase tier inventory suggests ${inferredTopology}.`,
95869
- suggestion: "lakebase-scm-adopt-state --force"
95870
- });
95871
- }
95872
- const headBranch = await getCurrentBranch({ cwd: projectDir });
95873
- if (state && state.state === "feature-claimed") {
95874
- if (state.branch && headBranch && headBranch !== state.branch) {
95875
- findings.push({
95876
- id: "head-branch-drift",
95877
- severity: "warn",
95878
- message: `workflow says feature-claimed for "${state.branch}", but HEAD is on "${headBranch}".`,
95879
- suggestion: `git checkout '${state.branch}'`
95880
- });
95881
- }
95882
- if (state.branch) {
95883
- const sanitized = sanitizeBranchName(state.branch);
95884
- let pair;
95885
- try {
95886
- pair = await getBranchByName(sanitized, { instance });
95887
- } catch {
95888
- pair = void 0;
95889
- }
95890
- if (!pair) {
95891
- findings.push({
95892
- id: "lakebase-pair-missing",
95893
- severity: "fail",
95894
- message: `workflow says feature-claimed for "${state.branch}", but no Lakebase branch "${sanitized}" exists.`,
95895
- suggestion: `lakebase-scm-abandon-feature # reset state; re-claim if needed`
95896
- });
95897
- } else if (state.lakebase_branch_uid && pair.uid !== state.lakebase_branch_uid) {
95898
- findings.push({
95899
- id: "lakebase-uid-drift",
95900
- severity: "warn",
95901
- message: `workflow records lakebase_branch_uid=${state.lakebase_branch_uid}, but the live branch reports ${pair.uid}.`,
95902
- suggestion: "lakebase-scm-adopt-state --force"
95903
- });
95904
- }
95905
- }
95906
- }
95907
- if (state && state.state === "feature-claimed" && state.branch) {
95908
- const envBranchId = env.get("LAKEBASE_BRANCH_ID");
95909
- const sanitized = sanitizeBranchName(state.branch);
95910
- if (envBranchId && envBranchId !== sanitized) {
95911
- findings.push({
95912
- id: "env-branch-drift",
95913
- severity: "warn",
95914
- message: `.env LAKEBASE_BRANCH_ID=${envBranchId} but workflow says ${sanitized}. The post-checkout hook may not have run since the last branch switch.`,
95915
- suggestion: `git checkout '${state.branch}' # re-fires post-checkout`
95916
- });
95917
- }
95918
- }
95919
- if (headBranch && !TIER_LEAFS2.has(headBranch) && headBranch.startsWith(FEATURE_PREFIX)) {
95920
- const sanitized = sanitizeBranchName(headBranch);
95921
- const paired = lakebaseBranches.some((b) => leafOf2(b) === sanitized);
95922
- if (!paired) {
95923
- findings.push({
95924
- id: "orphan-current-branch",
95925
- severity: "fail",
95926
- message: `Current git branch "${headBranch}" has no Lakebase pair (post-checkout fallback retired in phase C).`,
95927
- suggestion: `lakebase-scm-recover-orphans --claim --only-branch '${headBranch}'`
95928
- });
95929
- }
95930
- }
95931
- try {
95932
- const heads = await collapseMigrationHeads({ projectDir, dryRun: true });
95933
- if (heads.headsBefore.length > 1) {
95934
- findings.push({
95935
- id: "multiple-migration-heads",
95936
- severity: "fail",
95937
- 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.`,
95938
- suggestion: "lakebase-collapse-heads"
95939
- });
95940
- }
95941
- } catch {
95942
- }
95943
- if (instance && state?.branch) {
95944
- try {
95945
- const localIds = listSchemaMigrations({ projectDir }).map((m2) => m2.version);
95946
- let orphanRev = null;
95947
- try {
95948
- const status = await schemaMigrationStatus({ instance, branch: state.branch, projectDir });
95949
- if (dbRevisionOrphaned(status.current, localIds)) orphanRev = status.current ?? null;
95950
- } catch (e2) {
95951
- orphanRev = parseAlembicMissingRevision(e2 instanceof Error ? e2.message : String(e2));
95952
- }
95953
- if (orphanRev) {
95954
- findings.push({
95955
- id: "db-ahead-of-code",
95956
- severity: "fail",
95957
- 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".`,
95958
- 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"
95959
- });
95960
- }
95961
- } catch {
95962
- }
95963
- }
95964
- return finalize({
95965
- projectDir,
95966
- workflowStatePresent,
95967
- state: state ?? void 0,
95968
- inferredTierTopology: inferredTopology,
95969
- findings
95970
- });
95971
- }
95972
- function finalize(report) {
95973
- let worst = "ok";
95974
- for (const f3 of report.findings) {
95975
- worst = worstOf(worst, f3.severity);
95976
- }
95977
- return { ...report, worstSeverity: worst };
95978
- }
95979
- var ScmDoctorFixError = class extends Error {
95980
- constructor(message, code) {
95981
- super(message);
95982
- this.code = code;
95983
- this.name = "ScmDoctorFixError";
95984
- }
95985
- code;
95986
- };
95987
- var FIXABLE_FINDING_IDS = [
95988
- "env-branch-drift",
95989
- "head-branch-drift",
95990
- "tier-topology-mismatch",
95991
- "orphan-current-branch",
95992
- "multiple-migration-heads",
95993
- "db-ahead-of-code"
95994
- ];
95995
- async function fixFinding(args) {
95996
- if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
95997
- throw new ScmDoctorFixError(
95998
- `Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
95999
- "unsupported-finding"
96000
- );
96001
- }
96002
- const report = args.report ?? await runDoctor({ projectDir: args.projectDir, instance: args.instance });
96003
- const present = report.findings.find((f3) => f3.id === args.findingId);
96004
- if (!present) {
96005
- throw new ScmDoctorFixError(
96006
- `Finding "${args.findingId}" is not present in the current report. Re-run lakebase-scm-doctor to see what needs fixing.`,
96007
- "finding-not-present"
96008
- );
96009
- }
96010
- let action = "";
96011
- try {
96012
- switch (args.findingId) {
96013
- case "env-branch-drift": {
96014
- const branch = report.state?.branch;
96015
- if (!branch) {
96016
- throw new ScmDoctorFixError(
96017
- "Cannot fix: workflow state has no branch field.",
96018
- "fix-failed"
96019
- );
96020
- }
96021
- const sanitized = sanitizeBranchName(branch);
96022
- const envFile = path28.join(args.projectDir, ".env");
96023
- updateEnvConnection({
96024
- envPath: envFile,
96025
- projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
96026
- branchId: sanitized,
96027
- username: readEnvVar(envFile, "DB_USERNAME") ?? "",
96028
- endpointHost: readEnvVar(envFile, "LAKEBASE_HOST")
96029
- });
96030
- action = `rewrote .env LAKEBASE_BRANCH_ID=${sanitized} (metadata only; the app mints its token at runtime)`;
96031
- break;
96032
- }
96033
- case "head-branch-drift": {
96034
- const branch = report.state?.branch;
96035
- if (!branch) {
96036
- throw new ScmDoctorFixError(
96037
- "Cannot fix: workflow state has no branch field.",
96038
- "fix-failed"
96039
- );
96040
- }
96041
- await exec2(`git checkout ${shellEscape3(branch)}`, {
96042
- cwd: args.projectDir,
96043
- timeout: 15e3
96044
- });
96045
- action = `git checkout ${branch} (re-fires post-checkout to resync HEAD)`;
96046
- break;
96047
- }
96048
- case "tier-topology-mismatch": {
96049
- const instance = args.instance ?? report.state?.project_id;
96050
- if (!instance) {
96051
- throw new ScmDoctorFixError(
96052
- "Cannot fix: missing Lakebase project id.",
96053
- "fix-failed"
96054
- );
96055
- }
96056
- await adoptScmState({
96057
- projectDir: args.projectDir,
96058
- instance,
96059
- force: true
96060
- });
96061
- action = `adopted state with --force to re-infer tier_topology`;
96062
- break;
96063
- }
96064
- case "orphan-current-branch": {
96065
- const instance = args.instance ?? report.state?.project_id;
96066
- if (!instance) {
96067
- throw new ScmDoctorFixError(
96068
- "Cannot fix: missing Lakebase project id.",
96069
- "fix-failed"
96070
- );
96071
- }
96072
- const headBranch = await getCurrentBranch({ cwd: args.projectDir });
96073
- if (!headBranch) {
96074
- throw new ScmDoctorFixError(
96075
- "Cannot fix: detached HEAD or no current branch.",
96076
- "fix-failed"
96077
- );
96078
- }
96079
- await recoverOrphans({
96080
- projectDir: args.projectDir,
96081
- instance,
96082
- claim: true,
96083
- onlyBranch: headBranch
96084
- });
96085
- action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
96086
- break;
96087
- }
96088
- case "multiple-migration-heads": {
96089
- const r2 = await collapseMigrationHeads({ projectDir: args.projectDir });
96090
- if (r2.status !== "ok" || !r2.mergeRevision) {
96091
- throw new ScmDoctorFixError(
96092
- `Expected to create a merge revision but got status="${r2.status}".`,
96093
- "fix-failed"
96094
- );
96095
- }
96096
- action = `collapsed ${r2.headsBefore.length} heads into merge revision ${r2.mergeRevision} (commit it)`;
96097
- break;
96098
- }
96099
- case "db-ahead-of-code": {
96100
- const r2 = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
96101
- action = `abandoned the feature (deleted the polluted paired branch${r2.lakebaseDeleted ? "" : " , Lakebase delete reported not-deleted"}) and reset state to '${r2.state.state}'; re-claim to re-fork a clean branch from the tier`;
96102
- break;
96103
- }
96104
- }
96105
- } catch (err) {
96106
- if (err instanceof ScmDoctorFixError) throw err;
96107
- throw new ScmDoctorFixError(
96108
- `Remediation failed: ${err instanceof Error ? err.message : String(err)}`,
96109
- "fix-failed"
96110
- );
96111
- }
96112
- const postReport = await runDoctor({
96113
- projectDir: args.projectDir,
96114
- instance: args.instance
96115
- });
96116
- return { findingId: args.findingId, action, postReport };
96117
- }
96118
- function shellEscape3(s2) {
96119
- return `'${s2.replace(/'/g, "'\\''")}'`;
96120
- }
96121
-
96122
- // scripts/lakebase/doctor.ts
96123
- init_cjs_shims();
96124
- var fs32 = __toESM(require("fs"), 1);
96125
- var path30 = __toESM(require("path"), 1);
96126
- var cp6 = __toESM(require("child_process"), 1);
96127
-
96128
- // scripts/lakebase/workflow-drift.ts
96129
- init_cjs_shims();
96130
- var fs31 = __toESM(require("fs"), 1);
96131
- var path29 = __toESM(require("path"), 1);
96132
- function findKitTemplatesDir(start) {
96133
- let dir = start;
96134
- for (let i2 = 0; i2 < 6; i2++) {
96135
- const candidate = path29.join(
96136
- dir,
96137
- "templates",
96138
- "project",
96139
- "common",
96140
- ".github",
96141
- "workflows"
96142
- );
96143
- if (fs31.existsSync(candidate)) return candidate;
96144
- const parent = path29.dirname(dir);
96145
- if (parent === dir) break;
96146
- dir = parent;
96147
- }
96148
- throw new Error(
96149
- `Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
96150
- );
96151
- }
96152
- function unifiedDiff(name, projectContent, templateContent) {
96153
- if (projectContent === templateContent) return "";
96154
- const a = projectContent.split("\n");
96155
- const b = templateContent.split("\n");
96156
- const out = [`--- project/${name}`, `+++ template/${name}`];
96157
- const max = Math.max(a.length, b.length);
96158
- for (let i2 = 0; i2 < max; i2++) {
96159
- const av = a[i2];
96160
- const bv = b[i2];
96161
- if (av === bv) continue;
96162
- if (av !== void 0) out.push(`-${i2 + 1}: ${av}`);
96163
- if (bv !== void 0) out.push(`+${i2 + 1}: ${bv}`);
96164
- }
96165
- return out.join("\n");
96166
- }
96167
- function detectWorkflowDrift(args) {
96168
- const projectWorkflowsDir = path29.join(
96169
- args.projectDir,
96170
- ".github",
96171
- "workflows"
96172
- );
96173
- const here = path29.dirname(new URL(importMetaUrl).pathname);
96174
- const kitWorkflowsDir = args.kitDir ? path29.join(
96175
- args.kitDir,
96176
- "templates",
96177
- "project",
96178
- "common",
96179
- ".github",
96180
- "workflows"
96181
- ) : findKitTemplatesDir(here);
96182
- const templateFiles = fs31.existsSync(kitWorkflowsDir) ? fs31.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
96183
- const projectFiles = fs31.existsSync(projectWorkflowsDir) ? fs31.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
96184
- const version = readKitVersion(kitWorkflowsDir);
96185
- const seen = /* @__PURE__ */ new Set();
96186
- const files = [];
96187
- for (const name of templateFiles) {
96188
- seen.add(name);
96189
- const projectPath2 = path29.join(projectWorkflowsDir, name);
96190
- const templatePath = path29.join(kitWorkflowsDir, name);
96191
- if (!fs31.existsSync(projectPath2)) {
96192
- files.push({ name, status: "missing" });
96193
- continue;
96174
+ 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.`,
96175
+ 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."
96176
+ });
96177
+ }
96194
96178
  }
96195
- const projectContent = fs31.readFileSync(projectPath2, "utf8");
96196
- const templateContent = applyPlaceholders(fs31.readFileSync(templatePath, "utf8"), version);
96197
- if (projectContent === templateContent) {
96198
- files.push({ name, status: "unchanged" });
96199
- } else {
96200
- files.push({
96201
- name,
96202
- status: "drifted",
96203
- diff: unifiedDiff(name, projectContent, templateContent)
96179
+ } catch {
96180
+ }
96181
+ for (const wf of ["pr.yml", "merge.yml"]) {
96182
+ try {
96183
+ const p = path29.join(projectDir, ".github", "workflows", wf);
96184
+ if (!fs31.existsSync(p)) continue;
96185
+ const body = fs31.readFileSync(p, "utf8");
96186
+ if (/lakebase-scm-utils#v\d/.test(body)) {
96187
+ findings.push({
96188
+ id: "ci-workflow-substrate-pin",
96189
+ severity: "warn",
96190
+ 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.`,
96191
+ 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."
96192
+ });
96193
+ }
96194
+ } catch {
96195
+ }
96196
+ }
96197
+ try {
96198
+ if (isGitTracked(projectDir, ".lakebase/workflow-state.json")) {
96199
+ findings.push({
96200
+ id: "scm-state-git-tracked",
96201
+ severity: "warn",
96202
+ 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).",
96203
+ 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."
96204
96204
  });
96205
96205
  }
96206
+ } catch {
96206
96207
  }
96207
- for (const name of projectFiles) {
96208
- if (seen.has(name)) continue;
96209
- files.push({ name, status: "extra" });
96208
+ if (!env.has("LAKEBASE_PROJECT_ID")) {
96209
+ findings.push({
96210
+ id: "env-missing-project-id",
96211
+ severity: "fail",
96212
+ message: ".env does not contain LAKEBASE_PROJECT_ID. The post-checkout hook will exit early; workflow CLIs will need an explicit --instance.",
96213
+ suggestion: "Set LAKEBASE_PROJECT_ID=<your project id> in .env"
96214
+ });
96210
96215
  }
96211
- const order = {
96212
- drifted: 0,
96213
- missing: 1,
96214
- extra: 2,
96215
- unchanged: 3
96216
- };
96217
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
96218
- const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
96219
- return {
96220
- overall: hasDrift ? "drift" : "ok",
96221
- files
96222
- };
96223
- }
96224
- function readKitVersion(kitWorkflowsDir) {
96225
- let dir = kitWorkflowsDir;
96226
- for (let i2 = 0; i2 < 5; i2++) {
96227
- dir = path29.dirname(dir);
96216
+ if (!instance) {
96217
+ return finalize({
96218
+ projectDir,
96219
+ workflowStatePresent,
96220
+ state: state ?? void 0,
96221
+ findings
96222
+ });
96228
96223
  }
96224
+ let lakebaseBranches = [];
96229
96225
  try {
96230
- const raw = fs31.readFileSync(path29.join(dir, "package.json"), "utf-8");
96231
- const pkg = JSON.parse(raw);
96232
- return typeof pkg.version === "string" ? pkg.version : "unknown";
96233
- } catch {
96234
- return "unknown";
96226
+ lakebaseBranches = await listBranches({ instance });
96227
+ } catch (err) {
96228
+ findings.push({
96229
+ id: "lakebase-unreachable",
96230
+ severity: "fail",
96231
+ message: `Could not list Lakebase branches for instance ${instance}: ${err instanceof Error ? err.message : String(err)}`,
96232
+ suggestion: "databricks auth login (or check DATABRICKS_CONFIG_PROFILE)."
96233
+ });
96234
+ return finalize({
96235
+ projectDir,
96236
+ workflowStatePresent,
96237
+ state: state ?? void 0,
96238
+ findings
96239
+ });
96235
96240
  }
96236
- }
96237
- function applyPlaceholders(content, version) {
96238
- return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
96239
- }
96240
- function applyCommandPlaceholders(content, version) {
96241
- return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
96242
- }
96243
- var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
96244
- function findKitCommandsDir(start) {
96245
- let dir = start;
96246
- for (let i2 = 0; i2 < 6; i2++) {
96247
- const candidate = path29.join(
96248
- dir,
96249
- "templates",
96250
- "project",
96251
- "common",
96252
- ".claude",
96253
- "commands"
96254
- );
96255
- if (fs31.existsSync(candidate)) return candidate;
96256
- const parent = path29.dirname(dir);
96257
- if (parent === dir) break;
96258
- dir = parent;
96241
+ const inferredTopology = inferTierTopology(lakebaseBranches);
96242
+ if (state && state.tier_topology !== inferredTopology) {
96243
+ findings.push({
96244
+ id: "tier-topology-mismatch",
96245
+ severity: "warn",
96246
+ message: `workflow-state records tier_topology=${state.tier_topology}, but the Lakebase tier inventory suggests ${inferredTopology}.`,
96247
+ suggestion: "lakebase-scm-adopt-state --force"
96248
+ });
96259
96249
  }
96260
- throw new Error(
96261
- `Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
96262
- );
96263
- }
96264
- function parsePinnedVersion(content) {
96265
- const m2 = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
96266
- return m2 ? m2[1] : void 0;
96267
- }
96268
- function detectCommandDrift(args) {
96269
- const projectCommandsDir = path29.join(args.projectDir, ".claude", "commands");
96270
- const here = path29.dirname(new URL(importMetaUrl).pathname);
96271
- const kitCommandsDir = args.kitDir ? path29.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
96272
- const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
96273
- const templateFiles = fs31.existsSync(kitCommandsDir) ? fs31.readdirSync(kitCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
96274
- const projectFiles = fs31.existsSync(projectCommandsDir) ? fs31.readdirSync(projectCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
96275
- const seen = /* @__PURE__ */ new Set();
96276
- const files = [];
96277
- for (const name of templateFiles) {
96278
- seen.add(name);
96279
- const projectPath2 = path29.join(projectCommandsDir, name);
96280
- const templatePath = path29.join(kitCommandsDir, name);
96281
- const templateRaw = fs31.readFileSync(templatePath, "utf8");
96282
- if (!fs31.existsSync(projectPath2)) {
96283
- files.push({ name, status: "missing", kit_version: kitVersion });
96284
- continue;
96285
- }
96286
- const projectContent = fs31.readFileSync(projectPath2, "utf8");
96287
- const pinned = parsePinnedVersion(projectContent);
96288
- const versionForCompare = pinned ?? kitVersion;
96289
- const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
96290
- if (projectContent === templateContent) {
96291
- files.push({
96292
- name,
96293
- status: "unchanged",
96294
- pinned_version: pinned,
96295
- kit_version: kitVersion
96296
- });
96297
- } else {
96298
- files.push({
96299
- name,
96300
- status: "drifted",
96301
- pinned_version: pinned,
96302
- kit_version: kitVersion,
96303
- diff: unifiedDiff(name, projectContent, templateContent)
96250
+ const headBranch = await getCurrentBranch({ cwd: projectDir });
96251
+ if (state && state.state === "feature-claimed") {
96252
+ if (state.branch && headBranch && headBranch !== state.branch) {
96253
+ findings.push({
96254
+ id: "head-branch-drift",
96255
+ severity: "warn",
96256
+ message: `workflow says feature-claimed for "${state.branch}", but HEAD is on "${headBranch}".`,
96257
+ suggestion: `git checkout '${state.branch}'`
96304
96258
  });
96305
96259
  }
96260
+ if (state.branch) {
96261
+ const sanitized = sanitizeBranchName(state.branch);
96262
+ let pair;
96263
+ try {
96264
+ pair = await getBranchByName(sanitized, { instance });
96265
+ } catch {
96266
+ pair = void 0;
96267
+ }
96268
+ if (!pair) {
96269
+ findings.push({
96270
+ id: "lakebase-pair-missing",
96271
+ severity: "fail",
96272
+ message: `workflow says feature-claimed for "${state.branch}", but no Lakebase branch "${sanitized}" exists.`,
96273
+ suggestion: `lakebase-scm-abandon-feature # reset state; re-claim if needed`
96274
+ });
96275
+ } else if (state.lakebase_branch_uid && pair.uid !== state.lakebase_branch_uid) {
96276
+ findings.push({
96277
+ id: "lakebase-uid-drift",
96278
+ severity: "warn",
96279
+ message: `workflow records lakebase_branch_uid=${state.lakebase_branch_uid}, but the live branch reports ${pair.uid}.`,
96280
+ suggestion: "lakebase-scm-adopt-state --force"
96281
+ });
96282
+ }
96283
+ }
96306
96284
  }
96307
- for (const name of projectFiles) {
96308
- if (seen.has(name)) continue;
96309
- files.push({ name, status: "extra", kit_version: kitVersion });
96285
+ if (state && state.state === "feature-claimed" && state.branch) {
96286
+ const envBranchId = env.get("LAKEBASE_BRANCH_ID");
96287
+ const sanitized = sanitizeBranchName(state.branch);
96288
+ if (envBranchId && envBranchId !== sanitized) {
96289
+ findings.push({
96290
+ id: "env-branch-drift",
96291
+ severity: "warn",
96292
+ message: `.env LAKEBASE_BRANCH_ID=${envBranchId} but workflow says ${sanitized}. The post-checkout hook may not have run since the last branch switch.`,
96293
+ suggestion: `git checkout '${state.branch}' # re-fires post-checkout`
96294
+ });
96295
+ }
96310
96296
  }
96311
- const order = {
96312
- drifted: 0,
96313
- missing: 1,
96314
- extra: 2,
96315
- unchanged: 3
96316
- };
96317
- files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
96318
- const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
96319
- return { overall: hasDrift ? "drift" : "ok", files };
96320
- }
96321
- function readKitVersionFromCommandsDir(kitCommandsDir) {
96322
- let dir = kitCommandsDir;
96323
- for (let i2 = 0; i2 < 5; i2++) {
96324
- dir = path29.dirname(dir);
96297
+ if (headBranch && !TIER_LEAFS2.has(headBranch) && headBranch.startsWith(FEATURE_PREFIX)) {
96298
+ const sanitized = sanitizeBranchName(headBranch);
96299
+ const paired = lakebaseBranches.some((b) => leafOf2(b) === sanitized);
96300
+ if (!paired) {
96301
+ findings.push({
96302
+ id: "orphan-current-branch",
96303
+ severity: "fail",
96304
+ message: `Current git branch "${headBranch}" has no Lakebase pair (post-checkout fallback retired in phase C).`,
96305
+ suggestion: `lakebase-scm-recover-orphans --claim --only-branch '${headBranch}'`
96306
+ });
96307
+ }
96325
96308
  }
96326
96309
  try {
96327
- const raw = fs31.readFileSync(path29.join(dir, "package.json"), "utf-8");
96328
- const pkg = JSON.parse(raw);
96329
- return typeof pkg.version === "string" ? pkg.version : "unknown";
96310
+ const heads = await collapseMigrationHeads({ projectDir, dryRun: true });
96311
+ if (heads.headsBefore.length > 1) {
96312
+ findings.push({
96313
+ id: "multiple-migration-heads",
96314
+ severity: "fail",
96315
+ 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.`,
96316
+ suggestion: "lakebase-collapse-heads"
96317
+ });
96318
+ }
96330
96319
  } catch {
96331
- return "unknown";
96332
96320
  }
96321
+ if (instance && state?.branch) {
96322
+ try {
96323
+ const localIds = listSchemaMigrations({ projectDir }).map((m2) => m2.version);
96324
+ let orphanRev = null;
96325
+ try {
96326
+ const status = await schemaMigrationStatus({ instance, branch: state.branch, projectDir });
96327
+ if (dbRevisionOrphaned(status.current, localIds)) orphanRev = status.current ?? null;
96328
+ } catch (e2) {
96329
+ orphanRev = parseAlembicMissingRevision(e2 instanceof Error ? e2.message : String(e2));
96330
+ }
96331
+ if (orphanRev) {
96332
+ findings.push({
96333
+ id: "db-ahead-of-code",
96334
+ severity: "fail",
96335
+ 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".`,
96336
+ 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"
96337
+ });
96338
+ }
96339
+ } catch {
96340
+ }
96341
+ }
96342
+ return finalize({
96343
+ projectDir,
96344
+ workflowStatePresent,
96345
+ state: state ?? void 0,
96346
+ inferredTierTopology: inferredTopology,
96347
+ findings
96348
+ });
96333
96349
  }
96334
- function detectScaffoldedDrift(args) {
96335
- const workflows = detectWorkflowDrift(args);
96336
- const commands = detectCommandDrift(args);
96337
- return {
96338
- overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
96339
- workflows,
96340
- commands
96341
- };
96350
+ function finalize(report) {
96351
+ let worst = "ok";
96352
+ for (const f3 of report.findings) {
96353
+ worst = worstOf(worst, f3.severity);
96354
+ }
96355
+ return { ...report, worstSeverity: worst };
96342
96356
  }
96343
- function updateWorkflows(args) {
96344
- const projectWorkflowsDir = path29.join(
96345
- args.projectDir,
96346
- ".github",
96347
- "workflows"
96348
- );
96349
- const here = path29.dirname(new URL(importMetaUrl).pathname);
96350
- const kitWorkflowsDir = args.kitDir ? path29.join(
96351
- args.kitDir,
96352
- "templates",
96353
- "project",
96354
- "common",
96355
- ".github",
96356
- "workflows"
96357
- ) : findKitTemplatesDir(here);
96358
- const substitute = args.substitute !== false;
96359
- const dryRun = args.dryRun === true;
96360
- const pruneExtras = args.pruneExtras === true;
96361
- const templateFiles = fs31.existsSync(kitWorkflowsDir) ? fs31.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
96362
- const projectFiles = fs31.existsSync(projectWorkflowsDir) ? fs31.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
96363
- if (!dryRun && templateFiles.length > 0 && !fs31.existsSync(projectWorkflowsDir)) {
96364
- fs31.mkdirSync(projectWorkflowsDir, { recursive: true });
96357
+ var ScmDoctorFixError = class extends Error {
96358
+ constructor(message, code) {
96359
+ super(message);
96360
+ this.code = code;
96361
+ this.name = "ScmDoctorFixError";
96365
96362
  }
96366
- const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
96367
- const seen = /* @__PURE__ */ new Set();
96368
- const files = [];
96369
- for (const name of templateFiles) {
96370
- seen.add(name);
96371
- const projectPath2 = path29.join(projectWorkflowsDir, name);
96372
- const templatePath = path29.join(kitWorkflowsDir, name);
96373
- const templateRaw = fs31.readFileSync(templatePath, "utf-8");
96374
- const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
96375
- const existed = fs31.existsSync(projectPath2);
96376
- const current = existed ? fs31.readFileSync(projectPath2, "utf-8") : "";
96377
- let outcome;
96378
- if (!existed) {
96379
- outcome = "added";
96380
- } else if (current === desired) {
96381
- outcome = "unchanged";
96382
- } else {
96383
- outcome = "updated";
96384
- }
96385
- if (!dryRun && outcome !== "unchanged") {
96386
- fs31.writeFileSync(projectPath2, desired);
96387
- }
96388
- files.push({ name, outcome });
96363
+ code;
96364
+ };
96365
+ var FIXABLE_FINDING_IDS = [
96366
+ "env-branch-drift",
96367
+ "head-branch-drift",
96368
+ "tier-topology-mismatch",
96369
+ "orphan-current-branch",
96370
+ "multiple-migration-heads",
96371
+ "db-ahead-of-code"
96372
+ ];
96373
+ async function fixFinding(args) {
96374
+ if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
96375
+ throw new ScmDoctorFixError(
96376
+ `Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
96377
+ "unsupported-finding"
96378
+ );
96389
96379
  }
96390
- if (pruneExtras) {
96391
- for (const name of projectFiles) {
96392
- if (seen.has(name)) continue;
96393
- const projectPath2 = path29.join(projectWorkflowsDir, name);
96394
- if (!dryRun) {
96395
- fs31.unlinkSync(projectPath2);
96380
+ const report = args.report ?? await runDoctor({ projectDir: args.projectDir, instance: args.instance });
96381
+ const present = report.findings.find((f3) => f3.id === args.findingId);
96382
+ if (!present) {
96383
+ throw new ScmDoctorFixError(
96384
+ `Finding "${args.findingId}" is not present in the current report. Re-run lakebase-scm-doctor to see what needs fixing.`,
96385
+ "finding-not-present"
96386
+ );
96387
+ }
96388
+ let action = "";
96389
+ try {
96390
+ switch (args.findingId) {
96391
+ case "env-branch-drift": {
96392
+ const branch = report.state?.branch;
96393
+ if (!branch) {
96394
+ throw new ScmDoctorFixError(
96395
+ "Cannot fix: workflow state has no branch field.",
96396
+ "fix-failed"
96397
+ );
96398
+ }
96399
+ const sanitized = sanitizeBranchName(branch);
96400
+ const envFile = path29.join(args.projectDir, ".env");
96401
+ updateEnvConnection({
96402
+ envPath: envFile,
96403
+ projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
96404
+ branchId: sanitized,
96405
+ username: readEnvVar(envFile, "DB_USERNAME") ?? "",
96406
+ endpointHost: readEnvVar(envFile, "LAKEBASE_HOST")
96407
+ });
96408
+ action = `rewrote .env LAKEBASE_BRANCH_ID=${sanitized} (metadata only; the app mints its token at runtime)`;
96409
+ break;
96410
+ }
96411
+ case "head-branch-drift": {
96412
+ const branch = report.state?.branch;
96413
+ if (!branch) {
96414
+ throw new ScmDoctorFixError(
96415
+ "Cannot fix: workflow state has no branch field.",
96416
+ "fix-failed"
96417
+ );
96418
+ }
96419
+ await exec2(`git checkout ${shellEscape3(branch)}`, {
96420
+ cwd: args.projectDir,
96421
+ timeout: 15e3
96422
+ });
96423
+ action = `git checkout ${branch} (re-fires post-checkout to resync HEAD)`;
96424
+ break;
96425
+ }
96426
+ case "tier-topology-mismatch": {
96427
+ const instance = args.instance ?? report.state?.project_id;
96428
+ if (!instance) {
96429
+ throw new ScmDoctorFixError(
96430
+ "Cannot fix: missing Lakebase project id.",
96431
+ "fix-failed"
96432
+ );
96433
+ }
96434
+ await adoptScmState({
96435
+ projectDir: args.projectDir,
96436
+ instance,
96437
+ force: true
96438
+ });
96439
+ action = `adopted state with --force to re-infer tier_topology`;
96440
+ break;
96441
+ }
96442
+ case "orphan-current-branch": {
96443
+ const instance = args.instance ?? report.state?.project_id;
96444
+ if (!instance) {
96445
+ throw new ScmDoctorFixError(
96446
+ "Cannot fix: missing Lakebase project id.",
96447
+ "fix-failed"
96448
+ );
96449
+ }
96450
+ const headBranch = await getCurrentBranch({ cwd: args.projectDir });
96451
+ if (!headBranch) {
96452
+ throw new ScmDoctorFixError(
96453
+ "Cannot fix: detached HEAD or no current branch.",
96454
+ "fix-failed"
96455
+ );
96456
+ }
96457
+ await recoverOrphans({
96458
+ projectDir: args.projectDir,
96459
+ instance,
96460
+ claim: true,
96461
+ onlyBranch: headBranch
96462
+ });
96463
+ action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
96464
+ break;
96465
+ }
96466
+ case "multiple-migration-heads": {
96467
+ const r2 = await collapseMigrationHeads({ projectDir: args.projectDir });
96468
+ if (r2.status !== "ok" || !r2.mergeRevision) {
96469
+ throw new ScmDoctorFixError(
96470
+ `Expected to create a merge revision but got status="${r2.status}".`,
96471
+ "fix-failed"
96472
+ );
96473
+ }
96474
+ action = `collapsed ${r2.headsBefore.length} heads into merge revision ${r2.mergeRevision} (commit it)`;
96475
+ break;
96476
+ }
96477
+ case "db-ahead-of-code": {
96478
+ const r2 = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
96479
+ action = `abandoned the feature (deleted the polluted paired branch${r2.lakebaseDeleted ? "" : " , Lakebase delete reported not-deleted"}) and reset state to '${r2.state.state}'; re-claim to re-fork a clean branch from the tier`;
96480
+ break;
96396
96481
  }
96397
- files.push({ name, outcome: "removed" });
96398
96482
  }
96483
+ } catch (err) {
96484
+ if (err instanceof ScmDoctorFixError) throw err;
96485
+ throw new ScmDoctorFixError(
96486
+ `Remediation failed: ${err instanceof Error ? err.message : String(err)}`,
96487
+ "fix-failed"
96488
+ );
96399
96489
  }
96400
- const order = {
96401
- added: 0,
96402
- updated: 1,
96403
- removed: 2,
96404
- unchanged: 3
96405
- };
96406
- files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
96407
- const changed = files.some((f3) => f3.outcome !== "unchanged");
96408
- return { files, changed };
96490
+ const postReport = await runDoctor({
96491
+ projectDir: args.projectDir,
96492
+ instance: args.instance
96493
+ });
96494
+ return { findingId: args.findingId, action, postReport };
96495
+ }
96496
+ function shellEscape3(s2) {
96497
+ return `'${s2.replace(/'/g, "'\\''")}'`;
96409
96498
  }
96410
96499
 
96411
96500
  // scripts/lakebase/doctor.ts
96501
+ init_cjs_shims();
96502
+ var fs32 = __toESM(require("fs"), 1);
96503
+ var path30 = __toESM(require("path"), 1);
96504
+ var cp6 = __toESM(require("child_process"), 1);
96412
96505
  function readEnvFile(projectDir) {
96413
96506
  const envPath = path30.join(projectDir, ".env");
96414
96507
  if (!fs32.existsSync(envPath)) return {};
@@ -96841,7 +96934,7 @@ function checkWorkflowDrift(projectDir) {
96841
96934
  status: "warn",
96842
96935
  message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
96843
96936
  detail: { files: report.files.map((f3) => ({ name: f3.name, status: f3.status })) },
96844
- hint: "Inspect via the lakebase_workflow_drift MCP tool (or detectWorkflowDrift import). Refresh manually until updateWorkflows lands."
96937
+ 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."
96845
96938
  };
96846
96939
  } catch (err) {
96847
96940
  return {
@@ -97317,63 +97410,6 @@ async function discardAllChanges(args) {
97317
97410
  await exec2("git clean -fd", { cwd: args.cwd });
97318
97411
  }
97319
97412
 
97320
- // scripts/git/sync.ts
97321
- init_cjs_shims();
97322
- async function currentBranchName2(cwd) {
97323
- try {
97324
- return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
97325
- } catch {
97326
- return "";
97327
- }
97328
- }
97329
- async function push(args) {
97330
- await exec2("git push", { cwd: args.cwd });
97331
- }
97332
- async function pull(args) {
97333
- await exec2("git pull", { cwd: args.cwd });
97334
- }
97335
- async function publishBranch(args) {
97336
- const remote = args.remote ?? "origin";
97337
- const branch = await currentBranchName2(args.cwd);
97338
- if (!branch) throw new Error("No current branch");
97339
- await exec2(`git push -u ${remote} ${shq(branch)}`, {
97340
- cwd: args.cwd
97341
- });
97342
- }
97343
- async function pushCurrentBranchForPr(args) {
97344
- const remote = args.remote ?? "origin";
97345
- const branch = await currentBranchName2(args.cwd);
97346
- if (!branch) throw new Error("No current branch");
97347
- const upstreamSet = await hasUpstream({ cwd: args.cwd });
97348
- if (!upstreamSet) {
97349
- await exec2(`git push -u ${remote} ${shq(branch)}`, {
97350
- cwd: args.cwd
97351
- });
97352
- } else {
97353
- await exec2("git push", { cwd: args.cwd });
97354
- }
97355
- }
97356
- async function fetch3(args) {
97357
- const parts = ["git fetch"];
97358
- if (args.prune) parts.push("--prune");
97359
- if (args.all) parts.push("--all");
97360
- await exec2(parts.join(" "), { cwd: args.cwd });
97361
- }
97362
- async function pullFrom(args) {
97363
- await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
97364
- cwd: args.cwd
97365
- });
97366
- }
97367
- async function pushTo(args) {
97368
- await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
97369
- cwd: args.cwd
97370
- });
97371
- }
97372
- async function sync(args) {
97373
- await exec2("git pull", { cwd: args.cwd });
97374
- await exec2("git push", { cwd: args.cwd });
97375
- }
97376
-
97377
97413
  // scripts/git/branch-tag.ts
97378
97414
  init_cjs_shims();
97379
97415
  var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["production", "main", "master"]);
@@ -97779,6 +97815,7 @@ function withProxyEnv(base = {}) {
97779
97815
  createWorktree,
97780
97816
  cutBackup,
97781
97817
  databricksAuthPrereqMessage,
97818
+ defaultRefreshPromoteWorkflows,
97782
97819
  delay,
97783
97820
  deleteAppEndpoint,
97784
97821
  deleteBranch,