@databricks-solutions/lakebase-scm-utils 0.2.43 → 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.
- package/dist/scripts/github/index.cjs +21 -4
- package/dist/scripts/github/index.js +21 -4
- package/dist/scripts/github/pr.cli.cjs +21 -4
- package/dist/scripts/github/pr.cli.js +21 -4
- package/dist/scripts/index.cjs +1224 -1157
- package/dist/scripts/index.d.cts +1 -1
- package/dist/scripts/index.d.ts +1 -1
- package/dist/scripts/index.js +1223 -1157
- package/dist/scripts/lakebase/branch.cli.cjs +21 -4
- package/dist/scripts/lakebase/branch.cli.js +21 -4
- package/dist/scripts/lakebase/ci-app-endpoint.cli.cjs +21 -4
- package/dist/scripts/lakebase/ci-app-endpoint.cli.js +21 -4
- package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +23 -6
- package/dist/scripts/lakebase/ci-resolve-branch.cli.js +23 -6
- package/dist/scripts/lakebase/collapse-heads.cli.cjs +23 -6
- package/dist/scripts/lakebase/collapse-heads.cli.js +23 -6
- package/dist/scripts/lakebase/cut-backup.cli.cjs +21 -4
- package/dist/scripts/lakebase/cut-backup.cli.js +21 -4
- package/dist/scripts/lakebase/cut-tier.cli.cjs +21 -4
- package/dist/scripts/lakebase/cut-tier.cli.js +21 -4
- package/dist/scripts/lakebase/detect-language.cli.cjs +23 -6
- package/dist/scripts/lakebase/detect-language.cli.js +23 -6
- package/dist/scripts/lakebase/doctor.cli.cjs +24 -7
- package/dist/scripts/lakebase/doctor.cli.js +24 -7
- package/dist/scripts/lakebase/get-connection.cli.cjs +23 -6
- package/dist/scripts/lakebase/get-connection.cli.js +23 -6
- package/dist/scripts/lakebase/index.cjs +489 -391
- package/dist/scripts/lakebase/index.d.cts +33 -1
- package/dist/scripts/lakebase/index.d.ts +33 -1
- package/dist/scripts/lakebase/index.js +488 -391
- package/dist/scripts/lakebase/infra-runner.cli.cjs +23 -6
- package/dist/scripts/lakebase/infra-runner.cli.js +23 -6
- package/dist/scripts/lakebase/new-migration.cli.cjs +23 -6
- package/dist/scripts/lakebase/new-migration.cli.js +23 -6
- package/dist/scripts/lakebase/schema-diff.cli.cjs +23 -6
- package/dist/scripts/lakebase/schema-diff.cli.js +23 -6
- package/dist/scripts/lakebase/schema-migrate.cli.cjs +23 -6
- package/dist/scripts/lakebase/schema-migrate.cli.js +23 -6
- package/dist/scripts/lakebase/scm-abandon-feature.cli.cjs +21 -4
- package/dist/scripts/lakebase/scm-abandon-feature.cli.js +21 -4
- package/dist/scripts/lakebase/scm-adopt-state.cli.cjs +21 -4
- package/dist/scripts/lakebase/scm-adopt-state.cli.js +21 -4
- package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +23 -6
- package/dist/scripts/lakebase/scm-claim-feature.cli.js +23 -6
- package/dist/scripts/lakebase/scm-cleanup.cli.cjs +21 -4
- package/dist/scripts/lakebase/scm-cleanup.cli.js +21 -4
- package/dist/scripts/lakebase/scm-doctor.cli.cjs +23 -6
- package/dist/scripts/lakebase/scm-doctor.cli.js +23 -6
- package/dist/scripts/lakebase/scm-merge.cli.cjs +2305 -2037
- package/dist/scripts/lakebase/scm-merge.cli.js +360 -92
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +23 -6
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +23 -6
- package/dist/scripts/lakebase/scm-recover-orphans.cli.cjs +21 -4
- package/dist/scripts/lakebase/scm-recover-orphans.cli.js +21 -4
- package/dist/scripts/util/index.cjs +23 -6
- package/dist/scripts/util/index.js +23 -6
- package/package.json +1 -1
- package/scripts/lakebase/databricks-cli.ts +44 -4
- package/scripts/lakebase/doctor.ts +1 -1
- package/scripts/lakebase/runner-setup.ts +18 -2
- package/scripts/lakebase/scm-merge.ts +80 -1
- package/scripts/lakebase/secret-auth.ts +9 -2
- package/templates/project/common/scripts/run-tests.sh +10 -0
package/dist/scripts/index.cjs
CHANGED
|
@@ -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,
|
|
@@ -86478,16 +86479,33 @@ stdout: ${stdout}` : killed || signal ? `
|
|
|
86478
86479
|
}
|
|
86479
86480
|
async function runDatabricks(args, opts = {}) {
|
|
86480
86481
|
const { argv, env, profile } = buildInvocation(args, opts);
|
|
86482
|
+
const timeout = opts.timeout ?? KIT_TIMEOUTS.cliDefault;
|
|
86481
86483
|
try {
|
|
86482
|
-
|
|
86483
|
-
env,
|
|
86484
|
-
|
|
86485
|
-
});
|
|
86484
|
+
if (opts.input !== void 0) {
|
|
86485
|
+
return await execDatabricksWithStdin(argv, opts.input, env, timeout);
|
|
86486
|
+
}
|
|
86487
|
+
const { stdout } = await execFileP("databricks", argv, { env, timeout });
|
|
86486
86488
|
return stdout.toString();
|
|
86487
86489
|
} catch (err) {
|
|
86488
86490
|
throw classifyDatabricksError(err, argv, profile);
|
|
86489
86491
|
}
|
|
86490
86492
|
}
|
|
86493
|
+
function execDatabricksWithStdin(argv, input, env, timeout) {
|
|
86494
|
+
return new Promise((resolve2, reject) => {
|
|
86495
|
+
const child = (0, import_node_child_process3.execFile)("databricks", argv, { env, timeout }, (err, stdout, stderr) => {
|
|
86496
|
+
if (err) {
|
|
86497
|
+
err.stdout = String(stdout ?? "");
|
|
86498
|
+
err.stderr = String(stderr ?? "");
|
|
86499
|
+
reject(err);
|
|
86500
|
+
return;
|
|
86501
|
+
}
|
|
86502
|
+
resolve2(String(stdout ?? ""));
|
|
86503
|
+
});
|
|
86504
|
+
child.stdin?.on("error", () => {
|
|
86505
|
+
});
|
|
86506
|
+
child.stdin?.end(input);
|
|
86507
|
+
});
|
|
86508
|
+
}
|
|
86491
86509
|
function runDatabricksSync(args, opts = {}) {
|
|
86492
86510
|
const { argv, env, profile } = buildInvocation(args, opts);
|
|
86493
86511
|
try {
|
|
@@ -88899,8 +88917,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
88899
88917
|
var cached;
|
|
88900
88918
|
function substrateSelfVersion() {
|
|
88901
88919
|
if (cached !== void 0) return cached;
|
|
88902
|
-
if ("0.2.
|
|
88903
|
-
cached = "0.2.
|
|
88920
|
+
if ("0.2.45".length > 0) {
|
|
88921
|
+
cached = "0.2.45";
|
|
88904
88922
|
return cached;
|
|
88905
88923
|
}
|
|
88906
88924
|
cached = "unknown";
|
|
@@ -92166,8 +92184,20 @@ async function setupRunner(args) {
|
|
|
92166
92184
|
if (needsConfig) {
|
|
92167
92185
|
report("Registering runner with GitHub...");
|
|
92168
92186
|
const regToken = await createRegistrationToken(args.fullRepoName);
|
|
92169
|
-
cp4.
|
|
92170
|
-
|
|
92187
|
+
cp4.execFileSync(
|
|
92188
|
+
"./config.sh",
|
|
92189
|
+
[
|
|
92190
|
+
"--url",
|
|
92191
|
+
`https://github.com/${args.fullRepoName}`,
|
|
92192
|
+
"--token",
|
|
92193
|
+
regToken,
|
|
92194
|
+
"--name",
|
|
92195
|
+
name,
|
|
92196
|
+
"--labels",
|
|
92197
|
+
"self-hosted",
|
|
92198
|
+
"--unattended",
|
|
92199
|
+
"--replace"
|
|
92200
|
+
],
|
|
92171
92201
|
{ cwd: dir, timeout: KIT_TIMEOUTS.cliLong }
|
|
92172
92202
|
);
|
|
92173
92203
|
}
|
|
@@ -95189,1197 +95219,1289 @@ function pickRunUrl(pr) {
|
|
|
95189
95219
|
|
|
95190
95220
|
// scripts/lakebase/scm-merge.ts
|
|
95191
95221
|
init_cjs_shims();
|
|
95192
|
-
|
|
95193
|
-
|
|
95222
|
+
|
|
95223
|
+
// scripts/git/sync.ts
|
|
95224
|
+
init_cjs_shims();
|
|
95225
|
+
async function currentBranchName(cwd) {
|
|
95194
95226
|
try {
|
|
95195
|
-
|
|
95196
|
-
|
|
95197
|
-
|
|
95198
|
-
} else {
|
|
95199
|
-
await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
|
|
95200
|
-
}
|
|
95201
|
-
return null;
|
|
95202
|
-
} catch (err) {
|
|
95203
|
-
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 "";
|
|
95204
95230
|
}
|
|
95205
95231
|
}
|
|
95206
|
-
|
|
95207
|
-
|
|
95208
|
-
super(message);
|
|
95209
|
-
this.code = code;
|
|
95210
|
-
this.name = "ScmMergeError";
|
|
95211
|
-
}
|
|
95212
|
-
code;
|
|
95213
|
-
};
|
|
95214
|
-
var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
95215
|
-
var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
|
|
95216
|
-
function defaultMigratePredicate(run, mergedAt) {
|
|
95217
|
-
if (!run.createdAt) return false;
|
|
95218
|
-
const created = Date.parse(run.createdAt);
|
|
95219
|
-
if (!Number.isFinite(created)) return false;
|
|
95220
|
-
if (run.event && run.event !== "push") return false;
|
|
95221
|
-
return created >= mergedAt.getTime() - 5e3;
|
|
95232
|
+
async function push(args) {
|
|
95233
|
+
await exec2("git push", { cwd: args.cwd });
|
|
95222
95234
|
}
|
|
95223
|
-
function
|
|
95224
|
-
|
|
95225
|
-
if (run.event && run.event !== "push") return false;
|
|
95226
|
-
return !!run.headSha && run.headSha === mergeCommitSha;
|
|
95227
|
-
};
|
|
95235
|
+
async function pull(args) {
|
|
95236
|
+
await exec2("git pull", { cwd: args.cwd });
|
|
95228
95237
|
}
|
|
95229
|
-
async function
|
|
95230
|
-
const
|
|
95231
|
-
|
|
95232
|
-
|
|
95233
|
-
|
|
95234
|
-
|
|
95235
|
-
|
|
95236
|
-
|
|
95237
|
-
|
|
95238
|
-
|
|
95239
|
-
|
|
95240
|
-
|
|
95241
|
-
|
|
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 });
|
|
95242
95257
|
}
|
|
95243
|
-
|
|
95244
|
-
|
|
95245
|
-
|
|
95246
|
-
|
|
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"
|
|
95247
95294
|
);
|
|
95295
|
+
if (fs30.existsSync(candidate)) return candidate;
|
|
95296
|
+
const parent = path28.dirname(dir);
|
|
95297
|
+
if (parent === dir) break;
|
|
95298
|
+
dir = parent;
|
|
95248
95299
|
}
|
|
95249
|
-
|
|
95250
|
-
|
|
95251
|
-
|
|
95252
|
-
|
|
95253
|
-
|
|
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}`);
|
|
95254
95316
|
}
|
|
95255
|
-
|
|
95256
|
-
|
|
95257
|
-
|
|
95258
|
-
|
|
95259
|
-
|
|
95260
|
-
|
|
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
|
+
}
|
|
95261
95358
|
}
|
|
95262
|
-
const
|
|
95263
|
-
|
|
95264
|
-
|
|
95265
|
-
`Could not extract PR number from URL: ${current.pr_url}`,
|
|
95266
|
-
"bad-pr-url"
|
|
95267
|
-
);
|
|
95359
|
+
for (const name of projectFiles) {
|
|
95360
|
+
if (seen.has(name)) continue;
|
|
95361
|
+
files.push({ name, status: "extra" });
|
|
95268
95362
|
}
|
|
95269
|
-
const
|
|
95270
|
-
|
|
95271
|
-
|
|
95272
|
-
|
|
95273
|
-
|
|
95274
|
-
|
|
95275
|
-
|
|
95276
|
-
|
|
95277
|
-
|
|
95278
|
-
|
|
95279
|
-
|
|
95280
|
-
|
|
95281
|
-
|
|
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);
|
|
95282
95380
|
}
|
|
95283
|
-
let paired;
|
|
95284
95381
|
try {
|
|
95285
|
-
|
|
95286
|
-
|
|
95287
|
-
|
|
95288
|
-
|
|
95289
|
-
|
|
95290
|
-
});
|
|
95291
|
-
} catch (err) {
|
|
95292
|
-
throw new ScmMergeError(
|
|
95293
|
-
`mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
95294
|
-
"merge-failed"
|
|
95295
|
-
);
|
|
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";
|
|
95296
95387
|
}
|
|
95297
|
-
const warnings = [...paired.warnings];
|
|
95298
|
-
let localBranchDeleted = false;
|
|
95299
|
-
let headAfter = current.branch;
|
|
95300
|
-
if (!args.skipLocalCleanup) {
|
|
95301
|
-
const switchTo = args.switchTo ?? gitBase;
|
|
95302
|
-
const head = await getCurrentBranch({ cwd: args.projectDir });
|
|
95303
|
-
if (head === current.branch) {
|
|
95304
|
-
try {
|
|
95305
|
-
await exec2(`git checkout ${shellEscape2(switchTo)}`, {
|
|
95306
|
-
cwd: args.projectDir,
|
|
95307
|
-
timeout: 1e4
|
|
95308
|
-
});
|
|
95309
|
-
headAfter = switchTo;
|
|
95310
|
-
try {
|
|
95311
|
-
await exec2(`git fetch origin ${shellEscape2(switchTo)}`, {
|
|
95312
|
-
cwd: args.projectDir,
|
|
95313
|
-
timeout: 3e4
|
|
95314
|
-
});
|
|
95315
|
-
await exec2(`git merge --ff-only ${shellEscape2(`origin/${switchTo}`)}`, {
|
|
95316
|
-
cwd: args.projectDir,
|
|
95317
|
-
timeout: 1e4
|
|
95318
|
-
});
|
|
95319
|
-
} catch (err) {
|
|
95320
|
-
warnings.push(
|
|
95321
|
-
`local fast-forward of ${switchTo} to origin/${switchTo} failed: ${err instanceof Error ? err.message : String(err)}. The PR merged remotely; your local ${switchTo} may be stale, run \`git pull --ff-only\`.`
|
|
95322
|
-
);
|
|
95323
|
-
}
|
|
95324
|
-
} catch (err) {
|
|
95325
|
-
warnings.push(
|
|
95326
|
-
`git checkout ${switchTo} failed: ${err instanceof Error ? err.message : String(err)}. Local branch was NOT deleted.`
|
|
95327
|
-
);
|
|
95328
|
-
}
|
|
95329
|
-
} else {
|
|
95330
|
-
headAfter = head || current.branch;
|
|
95331
|
-
const reconcileWarning = await reconcileTierToOrigin({ cwd: args.projectDir, tier: switchTo });
|
|
95332
|
-
if (reconcileWarning) warnings.push(reconcileWarning);
|
|
95333
|
-
}
|
|
95334
|
-
if (headAfter !== current.branch) {
|
|
95335
|
-
try {
|
|
95336
|
-
await exec2(
|
|
95337
|
-
`git branch -D ${shellEscape2(current.branch)}`,
|
|
95338
|
-
{ cwd: args.projectDir, timeout: 1e4 }
|
|
95339
|
-
);
|
|
95340
|
-
localBranchDeleted = true;
|
|
95341
|
-
} catch (err) {
|
|
95342
|
-
warnings.push(
|
|
95343
|
-
`git branch -D ${current.branch} failed: ${err instanceof Error ? err.message : String(err)}.`
|
|
95344
|
-
);
|
|
95345
|
-
}
|
|
95346
|
-
}
|
|
95347
|
-
}
|
|
95348
|
-
const nowFn = args.now ?? (() => /* @__PURE__ */ new Date());
|
|
95349
|
-
const mergedAt = nowFn();
|
|
95350
|
-
let next = {
|
|
95351
|
-
...current,
|
|
95352
|
-
state: "merged",
|
|
95353
|
-
merged_at: mergedAt.toISOString()
|
|
95354
|
-
};
|
|
95355
|
-
writeWorkflowState(args.projectDir, next);
|
|
95356
|
-
let migrate;
|
|
95357
|
-
const waitMigrate = args.waitMigrate !== false;
|
|
95358
|
-
if (waitMigrate) {
|
|
95359
|
-
const timeoutMs = args.migrateTimeoutMs ?? DEFAULT_MIGRATE_TIMEOUT_MS;
|
|
95360
|
-
const pollMs = args.migratePollMs ?? DEFAULT_MIGRATE_POLL_MS;
|
|
95361
|
-
const fetchRuns = args.fetchRuns ?? listWorkflowRuns;
|
|
95362
|
-
const predicate = args.migrateRunPredicate ?? (paired.mergeCommitSha ? shaMigratePredicate(paired.mergeCommitSha) : defaultMigratePredicate);
|
|
95363
|
-
const elapsedSinceMerge = nowFn().getTime() - mergedAt.getTime();
|
|
95364
|
-
const remainingTimeoutMs = Math.max(0, timeoutMs - elapsedSinceMerge);
|
|
95365
|
-
let polls = 0;
|
|
95366
|
-
let matched;
|
|
95367
|
-
let lastSeen;
|
|
95368
|
-
try {
|
|
95369
|
-
const result = await pollUntil({
|
|
95370
|
-
timeoutMs: remainingTimeoutMs,
|
|
95371
|
-
intervalMs: pollMs,
|
|
95372
|
-
now: nowFn,
|
|
95373
|
-
sleep: args.sleep,
|
|
95374
|
-
probe: async () => {
|
|
95375
|
-
const runs = await fetchRuns(ownerRepo, 20);
|
|
95376
|
-
const candidates = runs.filter((r2) => r2.branch === gitBase).filter((r2) => predicate(r2, mergedAt));
|
|
95377
|
-
if (candidates.length === 0) {
|
|
95378
|
-
return { done: false };
|
|
95379
|
-
}
|
|
95380
|
-
candidates.sort(
|
|
95381
|
-
(a, b) => Date.parse(b.createdAt ?? "0") - Date.parse(a.createdAt ?? "0")
|
|
95382
|
-
);
|
|
95383
|
-
lastSeen = candidates[0];
|
|
95384
|
-
const status = (lastSeen.status ?? "").toLowerCase();
|
|
95385
|
-
return status === "completed" ? { done: true, value: lastSeen } : { done: false };
|
|
95386
|
-
}
|
|
95387
|
-
});
|
|
95388
|
-
polls = result.polls;
|
|
95389
|
-
if (result.outcome === "done") {
|
|
95390
|
-
matched = result.value;
|
|
95391
|
-
}
|
|
95392
|
-
} catch (err) {
|
|
95393
|
-
warnings.push(
|
|
95394
|
-
`Downstream migrate poll errored: ${err instanceof Error ? err.message : String(err)}. Treating as advisory.`
|
|
95395
|
-
);
|
|
95396
|
-
}
|
|
95397
|
-
const applyLocalFallback = async (reason) => {
|
|
95398
|
-
if (!args.localMigrateFallback) return false;
|
|
95399
|
-
const r2 = await args.localMigrateFallback();
|
|
95400
|
-
if (!r2.ok) {
|
|
95401
|
-
warnings.push(
|
|
95402
|
-
`Local-migrate fallback FAILED after ${reason}: ${r2.detail ?? "unknown error"}. git promoted but the parent schema is NOT applied (partial promotion); apply it manually.`
|
|
95403
|
-
);
|
|
95404
|
-
return false;
|
|
95405
|
-
}
|
|
95406
|
-
next = { ...next, migrate_completed_at: nowFn().toISOString() };
|
|
95407
|
-
writeWorkflowState(args.projectDir, next);
|
|
95408
|
-
migrate = { ...migrate ?? { waited: true, polls }, appliedLocally: true, authVerified };
|
|
95409
|
-
warnings.push(
|
|
95410
|
-
`Downstream migrate did not confirm (${reason}); applied the parent migrations LOCALLY instead. git and Lakebase schema are in sync${r2.detail ? ` (${r2.detail})` : ""}.`
|
|
95411
|
-
);
|
|
95412
|
-
return true;
|
|
95413
|
-
};
|
|
95414
|
-
if (matched) {
|
|
95415
|
-
const runUrl = workflowRunUrl(ownerRepo, matched);
|
|
95416
|
-
const conclusion = (matched.conclusion ?? "").toLowerCase();
|
|
95417
|
-
migrate = {
|
|
95418
|
-
waited: true,
|
|
95419
|
-
runUrl,
|
|
95420
|
-
conclusion,
|
|
95421
|
-
polls,
|
|
95422
|
-
authVerified
|
|
95423
|
-
};
|
|
95424
|
-
if (conclusion === "success") {
|
|
95425
|
-
next = {
|
|
95426
|
-
...next,
|
|
95427
|
-
migrate_run_url: runUrl,
|
|
95428
|
-
migrate_completed_at: nowFn().toISOString()
|
|
95429
|
-
};
|
|
95430
|
-
writeWorkflowState(args.projectDir, next);
|
|
95431
|
-
} else {
|
|
95432
|
-
const applied = await applyLocalFallback(`the downstream migrate finished conclusion=${conclusion} (${runUrl})`);
|
|
95433
|
-
if (!applied) {
|
|
95434
|
-
throw new ScmMergeError(
|
|
95435
|
-
`Downstream migrate workflow finished with conclusion=${conclusion}. Run ${runUrl} for details.`,
|
|
95436
|
-
"migrate-failed"
|
|
95437
|
-
);
|
|
95438
|
-
}
|
|
95439
|
-
}
|
|
95440
|
-
} else {
|
|
95441
|
-
const budgetSec = Math.round(
|
|
95442
|
-
(args.migrateTimeoutMs ?? DEFAULT_MIGRATE_TIMEOUT_MS) / 1e3
|
|
95443
|
-
);
|
|
95444
|
-
const lastStatus = lastSeen?.status ?? "(no matching run)";
|
|
95445
|
-
const timeoutFatal = args.migrateTimeoutFatal !== false;
|
|
95446
|
-
if (timeoutFatal) {
|
|
95447
|
-
migrate = { waited: true, polls, authVerified };
|
|
95448
|
-
const applied = await applyLocalFallback(
|
|
95449
|
-
`the downstream migrate did not complete within ${budgetSec}s (last seen: ${lastStatus})`
|
|
95450
|
-
);
|
|
95451
|
-
if (!applied) {
|
|
95452
|
-
throw new ScmMergeError(
|
|
95453
|
-
`Timed out after ${budgetSec}s waiting for the downstream migrate workflow on "${gitBase}". Last seen status: ${lastStatus}.`,
|
|
95454
|
-
"migrate-timeout"
|
|
95455
|
-
);
|
|
95456
|
-
}
|
|
95457
|
-
} else {
|
|
95458
|
-
migrate = { waited: true, polls, timedOut: true, authVerified };
|
|
95459
|
-
warnings.push(
|
|
95460
|
-
`Downstream migrate workflow on "${gitBase}" was not confirmed within ${budgetSec}s (last seen status: ${lastStatus}). The PR merged and your local ${gitBase} is synced; the migrate run may still be pending or running. Confirm it later via the Actions tab or re-run with --wait-migrate.`
|
|
95461
|
-
);
|
|
95462
|
-
}
|
|
95463
|
-
}
|
|
95464
|
-
} else {
|
|
95465
|
-
migrate = { waited: false, polls: 0 };
|
|
95466
|
-
}
|
|
95467
|
-
return {
|
|
95468
|
-
state: next,
|
|
95469
|
-
paired,
|
|
95470
|
-
localBranchDeleted,
|
|
95471
|
-
headAfter,
|
|
95472
|
-
migrate,
|
|
95473
|
-
warnings
|
|
95474
|
-
};
|
|
95475
|
-
}
|
|
95476
|
-
function workflowRunUrl(ownerRepo, run) {
|
|
95477
|
-
return `https://github.com/${ownerRepo}/actions/runs/${run.id}`;
|
|
95478
|
-
}
|
|
95479
|
-
function extractPullNumber(prUrl) {
|
|
95480
|
-
const m2 = prUrl.match(/\/pull\/(\d+)(?:[\/?#].*)?$/);
|
|
95481
|
-
if (!m2) return void 0;
|
|
95482
|
-
const n = Number.parseInt(m2[1], 10);
|
|
95483
|
-
return Number.isFinite(n) ? n : void 0;
|
|
95484
95388
|
}
|
|
95485
|
-
function
|
|
95486
|
-
return
|
|
95389
|
+
function applyPlaceholders(content, version) {
|
|
95390
|
+
return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
|
|
95487
95391
|
}
|
|
95488
|
-
|
|
95489
|
-
|
|
95490
|
-
init_cjs_shims();
|
|
95491
|
-
|
|
95492
|
-
// scripts/git/branches.ts
|
|
95493
|
-
init_cjs_shims();
|
|
95494
|
-
async function currentBranchName(cwd) {
|
|
95495
|
-
try {
|
|
95496
|
-
return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
95497
|
-
} catch {
|
|
95498
|
-
return "";
|
|
95499
|
-
}
|
|
95392
|
+
function applyCommandPlaceholders(content, version) {
|
|
95393
|
+
return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
|
|
95500
95394
|
}
|
|
95501
|
-
|
|
95502
|
-
|
|
95503
|
-
let
|
|
95504
|
-
|
|
95505
|
-
|
|
95506
|
-
|
|
95507
|
-
|
|
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"
|
|
95508
95406
|
);
|
|
95509
|
-
|
|
95510
|
-
|
|
95511
|
-
|
|
95512
|
-
|
|
95513
|
-
const current = await currentBranchName(cwd);
|
|
95514
|
-
return raw.split("\n").filter(Boolean).map((line) => {
|
|
95515
|
-
const [name, tracking, trackInfo] = line.split("|");
|
|
95516
|
-
let ahead = 0;
|
|
95517
|
-
let behind = 0;
|
|
95518
|
-
if (trackInfo) {
|
|
95519
|
-
const aheadMatch = trackInfo.match(/ahead (\d+)/);
|
|
95520
|
-
const behindMatch = trackInfo.match(/behind (\d+)/);
|
|
95521
|
-
if (aheadMatch) ahead = parseInt(aheadMatch[1], 10);
|
|
95522
|
-
if (behindMatch) behind = parseInt(behindMatch[1], 10);
|
|
95523
|
-
}
|
|
95524
|
-
return {
|
|
95525
|
-
name,
|
|
95526
|
-
isCurrent: name === current,
|
|
95527
|
-
isRemote: false,
|
|
95528
|
-
tracking: tracking || void 0,
|
|
95529
|
-
ahead,
|
|
95530
|
-
behind
|
|
95531
|
-
};
|
|
95532
|
-
});
|
|
95533
|
-
}
|
|
95534
|
-
async function listRemoteBranches(args) {
|
|
95535
|
-
const { cwd, remote = "origin" } = args;
|
|
95536
|
-
try {
|
|
95537
|
-
const localBranches = await listLocalBranches({ cwd });
|
|
95538
|
-
const localNames = new Set(localBranches.map((b) => b.name));
|
|
95539
|
-
const raw = await exec2(`git branch -r --format="%(refname:short)"`, {
|
|
95540
|
-
cwd
|
|
95541
|
-
});
|
|
95542
|
-
if (!raw) return [];
|
|
95543
|
-
const remotePrefix = `${remote}/`;
|
|
95544
|
-
return raw.split("\n").filter(Boolean).filter((name) => !name.includes("HEAD")).map((name) => {
|
|
95545
|
-
const shortName = name.startsWith(remotePrefix) ? name.slice(remotePrefix.length) : name;
|
|
95546
|
-
return { fullName: name, shortName };
|
|
95547
|
-
}).filter(({ shortName }) => !localNames.has(shortName)).map(({ fullName, shortName }) => ({
|
|
95548
|
-
name: shortName,
|
|
95549
|
-
isCurrent: false,
|
|
95550
|
-
isRemote: true,
|
|
95551
|
-
tracking: fullName
|
|
95552
|
-
}));
|
|
95553
|
-
} catch {
|
|
95554
|
-
return [];
|
|
95407
|
+
if (fs30.existsSync(candidate)) return candidate;
|
|
95408
|
+
const parent = path28.dirname(dir);
|
|
95409
|
+
if (parent === dir) break;
|
|
95410
|
+
dir = parent;
|
|
95555
95411
|
}
|
|
95412
|
+
throw new Error(
|
|
95413
|
+
`Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
|
|
95414
|
+
);
|
|
95556
95415
|
}
|
|
95557
|
-
|
|
95558
|
-
const
|
|
95559
|
-
|
|
95560
|
-
const out = await exec2(
|
|
95561
|
-
`git ls-remote --heads "${remote}" "${branch}"`,
|
|
95562
|
-
{ cwd }
|
|
95563
|
-
);
|
|
95564
|
-
return out.trim().length > 0;
|
|
95565
|
-
} catch {
|
|
95566
|
-
return false;
|
|
95567
|
-
}
|
|
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;
|
|
95568
95419
|
}
|
|
95569
|
-
|
|
95570
|
-
|
|
95571
|
-
|
|
95572
|
-
|
|
95573
|
-
|
|
95574
|
-
|
|
95575
|
-
|
|
95576
|
-
|
|
95577
|
-
|
|
95578
|
-
|
|
95579
|
-
|
|
95580
|
-
|
|
95581
|
-
|
|
95582
|
-
|
|
95583
|
-
|
|
95584
|
-
"missing
|
|
95585
|
-
);
|
|
95586
|
-
}
|
|
95587
|
-
const lakebaseBranches = await listBranches({ instance: args.instance });
|
|
95588
|
-
const tierTopology = inferTierTopology(lakebaseBranches);
|
|
95589
|
-
const lakebaseLeafs = new Set(
|
|
95590
|
-
lakebaseBranches.map((b) => leafName2(b))
|
|
95591
|
-
);
|
|
95592
|
-
const defaultLeaf = leafName2(
|
|
95593
|
-
lakebaseBranches.find((b) => b.isDefault === true)
|
|
95594
|
-
);
|
|
95595
|
-
const gitBranches = await listLocalBranches({ cwd: args.projectDir });
|
|
95596
|
-
const orphans = [];
|
|
95597
|
-
const skipped = [];
|
|
95598
|
-
for (const gb of gitBranches) {
|
|
95599
|
-
if (gb.isRemote) continue;
|
|
95600
|
-
const name = gb.name;
|
|
95601
|
-
if (TIER_LEAFS.has(name)) {
|
|
95602
|
-
skipped.push({ gitBranch: name, reason: "tier branch" });
|
|
95603
|
-
continue;
|
|
95604
|
-
}
|
|
95605
|
-
if (defaultLeaf && name === defaultLeaf) {
|
|
95606
|
-
skipped.push({ gitBranch: name, reason: "default branch" });
|
|
95607
|
-
continue;
|
|
95608
|
-
}
|
|
95609
|
-
const sanitized = sanitizeBranchName(name);
|
|
95610
|
-
if (lakebaseLeafs.has(sanitized)) {
|
|
95611
|
-
skipped.push({
|
|
95612
|
-
gitBranch: name,
|
|
95613
|
-
reason: `paired Lakebase branch "${sanitized}" exists`
|
|
95614
|
-
});
|
|
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 });
|
|
95615
95436
|
continue;
|
|
95616
95437
|
}
|
|
95617
|
-
|
|
95618
|
-
|
|
95619
|
-
|
|
95620
|
-
|
|
95621
|
-
|
|
95622
|
-
|
|
95623
|
-
|
|
95624
|
-
|
|
95625
|
-
|
|
95626
|
-
|
|
95627
|
-
|
|
95628
|
-
|
|
95629
|
-
|
|
95630
|
-
|
|
95631
|
-
|
|
95632
|
-
|
|
95633
|
-
|
|
95634
|
-
|
|
95635
|
-
const candidates = args.onlyBranch ? orphans.filter((o) => o.gitBranch === args.onlyBranch) : orphans;
|
|
95636
|
-
if (args.onlyBranch && candidates.length === 0) {
|
|
95637
|
-
throw new ScmRecoverError(
|
|
95638
|
-
`No orphan found for --only-branch ${args.onlyBranch}.`,
|
|
95639
|
-
"claim-conflict"
|
|
95640
|
-
);
|
|
95641
|
-
}
|
|
95642
|
-
const headOrphan = candidates.find((o) => o.isCurrent);
|
|
95643
|
-
const stateTargetOrphan = headOrphan ?? candidates[0];
|
|
95644
|
-
for (const orphan of candidates) {
|
|
95645
|
-
try {
|
|
95646
|
-
const paired = await createFeaturePairedBranch({
|
|
95647
|
-
instance: args.instance,
|
|
95648
|
-
branch: orphan.gitBranch,
|
|
95649
|
-
parentBranch,
|
|
95650
|
-
cwd: args.projectDir
|
|
95651
|
-
// The git branch already exists on disk; the substrate primitive
|
|
95652
|
-
// is idempotent on the git side (it'll checkout the existing
|
|
95653
|
-
// branch rather than fail) but if the project is not on this
|
|
95654
|
-
// branch, we want a no-op git side. Leaving the default true is
|
|
95655
|
-
// OK: if the git branch already matches, the checkout is a
|
|
95656
|
-
// no-op; if the branch isn't HEAD, the substrate switches to it
|
|
95657
|
-
// which is what the user implicitly asked for by including the
|
|
95658
|
-
// branch.
|
|
95659
|
-
});
|
|
95660
|
-
let stateUpdated = false;
|
|
95661
|
-
if (orphan === stateTargetOrphan) {
|
|
95662
|
-
const next = {
|
|
95663
|
-
...currentState ?? {
|
|
95664
|
-
$schema: "./scm-workflow-state.schema.json",
|
|
95665
|
-
version: 1,
|
|
95666
|
-
state: "scaffold-complete",
|
|
95667
|
-
tier_topology: tierTopology,
|
|
95668
|
-
project_id: args.instance
|
|
95669
|
-
},
|
|
95670
|
-
state: "feature-claimed",
|
|
95671
|
-
feature_id: orphan.gitBranch.replace(/^feature\//, ""),
|
|
95672
|
-
branch: paired.gitBranch,
|
|
95673
|
-
parent_branch: parentBranch,
|
|
95674
|
-
lakebase_branch_uid: paired.branch.uid,
|
|
95675
|
-
claimed_at: (args.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
95676
|
-
pr_url: void 0,
|
|
95677
|
-
pushed_at: void 0,
|
|
95678
|
-
ci_run_url: void 0,
|
|
95679
|
-
ci_green_at: void 0,
|
|
95680
|
-
merged_at: void 0
|
|
95681
|
-
};
|
|
95682
|
-
writeWorkflowState(args.projectDir, next);
|
|
95683
|
-
stateUpdated = true;
|
|
95684
|
-
result.stateUpdatedFor = orphan.gitBranch;
|
|
95685
|
-
}
|
|
95686
|
-
result.claimed.push({
|
|
95687
|
-
candidate: orphan,
|
|
95688
|
-
lakebaseBranchUid: paired.branch.uid,
|
|
95689
|
-
stateUpdated,
|
|
95690
|
-
warnings: paired.warnings
|
|
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)
|
|
95691
95456
|
});
|
|
95692
|
-
} catch (err) {
|
|
95693
|
-
throw new ScmRecoverError(
|
|
95694
|
-
`Substrate claim failed for ${orphan.gitBranch}: ${err instanceof Error ? err.message : String(err)}`,
|
|
95695
|
-
"substrate-failure"
|
|
95696
|
-
);
|
|
95697
95457
|
}
|
|
95698
95458
|
}
|
|
95699
|
-
|
|
95700
|
-
|
|
95701
|
-
|
|
95702
|
-
if (!b) return "";
|
|
95703
|
-
return b.name.split("/").pop() ?? b.name;
|
|
95704
|
-
}
|
|
95705
|
-
function parentForTopology(t2, defaultLeaf) {
|
|
95706
|
-
if (t2 === 3) return "dev";
|
|
95707
|
-
if (t2 === 2) return "staging";
|
|
95708
|
-
return defaultLeaf || "main";
|
|
95709
|
-
}
|
|
95710
|
-
|
|
95711
|
-
// scripts/lakebase/scm-doctor.ts
|
|
95712
|
-
init_cjs_shims();
|
|
95713
|
-
var fs30 = __toESM(require("fs"), 1);
|
|
95714
|
-
var path28 = __toESM(require("path"), 1);
|
|
95715
|
-
var FEATURE_PREFIX = "feature/";
|
|
95716
|
-
var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
|
|
95717
|
-
function readEnv(projectDir) {
|
|
95718
|
-
const envPath = path28.join(projectDir, ".env");
|
|
95719
|
-
const out = /* @__PURE__ */ new Map();
|
|
95720
|
-
if (!fs30.existsSync(envPath)) return out;
|
|
95721
|
-
const lines = fs30.readFileSync(envPath, "utf8").split("\n");
|
|
95722
|
-
for (const line of lines) {
|
|
95723
|
-
const m2 = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
|
|
95724
|
-
if (m2) out.set(m2[1], m2[2].replace(/^["']|["']$/g, ""));
|
|
95459
|
+
for (const name of projectFiles) {
|
|
95460
|
+
if (seen.has(name)) continue;
|
|
95461
|
+
files.push({ name, status: "extra", kit_version: kitVersion });
|
|
95725
95462
|
}
|
|
95726
|
-
|
|
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 };
|
|
95727
95472
|
}
|
|
95728
|
-
function
|
|
95729
|
-
|
|
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
|
+
}
|
|
95730
95485
|
}
|
|
95731
|
-
function
|
|
95732
|
-
const
|
|
95733
|
-
|
|
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
|
+
};
|
|
95734
95494
|
}
|
|
95735
|
-
|
|
95736
|
-
const
|
|
95737
|
-
|
|
95738
|
-
|
|
95739
|
-
|
|
95740
|
-
|
|
95741
|
-
const
|
|
95742
|
-
|
|
95743
|
-
|
|
95744
|
-
|
|
95745
|
-
|
|
95746
|
-
|
|
95747
|
-
|
|
95748
|
-
|
|
95749
|
-
|
|
95750
|
-
|
|
95751
|
-
|
|
95752
|
-
|
|
95753
|
-
|
|
95754
|
-
|
|
95755
|
-
|
|
95756
|
-
|
|
95757
|
-
});
|
|
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 });
|
|
95758
95517
|
}
|
|
95759
|
-
|
|
95760
|
-
|
|
95761
|
-
|
|
95762
|
-
|
|
95763
|
-
|
|
95764
|
-
|
|
95765
|
-
|
|
95766
|
-
|
|
95767
|
-
|
|
95768
|
-
|
|
95769
|
-
|
|
95770
|
-
|
|
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";
|
|
95771
95536
|
}
|
|
95772
|
-
|
|
95537
|
+
if (!dryRun && outcome !== "unchanged") {
|
|
95538
|
+
fs30.writeFileSync(projectPath2, desired);
|
|
95539
|
+
}
|
|
95540
|
+
files.push({ name, outcome });
|
|
95773
95541
|
}
|
|
95774
|
-
|
|
95775
|
-
|
|
95776
|
-
|
|
95777
|
-
|
|
95778
|
-
|
|
95779
|
-
|
|
95780
|
-
findings.push({
|
|
95781
|
-
id: "ci-workflow-substrate-pin",
|
|
95782
|
-
severity: "warn",
|
|
95783
|
-
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.`,
|
|
95784
|
-
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."
|
|
95785
|
-
});
|
|
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);
|
|
95786
95548
|
}
|
|
95787
|
-
|
|
95549
|
+
files.push({ name, outcome: "removed" });
|
|
95788
95550
|
}
|
|
95789
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;
|
|
95790
95566
|
try {
|
|
95791
|
-
|
|
95792
|
-
|
|
95793
|
-
|
|
95794
|
-
|
|
95795
|
-
|
|
95796
|
-
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."
|
|
95797
|
-
});
|
|
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 });
|
|
95798
95572
|
}
|
|
95799
|
-
|
|
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).`;
|
|
95800
95576
|
}
|
|
95801
|
-
|
|
95802
|
-
|
|
95803
|
-
|
|
95804
|
-
|
|
95805
|
-
|
|
95806
|
-
|
|
95807
|
-
|
|
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;
|
|
95808
95675
|
}
|
|
95809
|
-
|
|
95810
|
-
|
|
95811
|
-
|
|
95812
|
-
|
|
95813
|
-
|
|
95814
|
-
|
|
95815
|
-
|
|
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
|
+
}
|
|
95816
95689
|
}
|
|
95817
|
-
let
|
|
95690
|
+
let paired;
|
|
95818
95691
|
try {
|
|
95819
|
-
|
|
95820
|
-
|
|
95821
|
-
|
|
95822
|
-
|
|
95823
|
-
|
|
95824
|
-
message: `Could not list Lakebase branches for instance ${instance}: ${err instanceof Error ? err.message : String(err)}`,
|
|
95825
|
-
suggestion: "databricks auth login (or check DATABRICKS_CONFIG_PROFILE)."
|
|
95826
|
-
});
|
|
95827
|
-
return finalize({
|
|
95828
|
-
projectDir,
|
|
95829
|
-
workflowStatePresent,
|
|
95830
|
-
state: state ?? void 0,
|
|
95831
|
-
findings
|
|
95832
|
-
});
|
|
95833
|
-
}
|
|
95834
|
-
const inferredTopology = inferTierTopology(lakebaseBranches);
|
|
95835
|
-
if (state && state.tier_topology !== inferredTopology) {
|
|
95836
|
-
findings.push({
|
|
95837
|
-
id: "tier-topology-mismatch",
|
|
95838
|
-
severity: "warn",
|
|
95839
|
-
message: `workflow-state records tier_topology=${state.tier_topology}, but the Lakebase tier inventory suggests ${inferredTopology}.`,
|
|
95840
|
-
suggestion: "lakebase-scm-adopt-state --force"
|
|
95692
|
+
paired = await mergePairedPullRequest({
|
|
95693
|
+
ownerRepo,
|
|
95694
|
+
pullNumber,
|
|
95695
|
+
lakebaseInstance: instance,
|
|
95696
|
+
method: args.method ?? "squash"
|
|
95841
95697
|
});
|
|
95698
|
+
} catch (err) {
|
|
95699
|
+
throw new ScmMergeError(
|
|
95700
|
+
`mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
95701
|
+
"merge-failed"
|
|
95702
|
+
);
|
|
95842
95703
|
}
|
|
95843
|
-
const
|
|
95844
|
-
|
|
95845
|
-
|
|
95846
|
-
|
|
95847
|
-
|
|
95848
|
-
|
|
95849
|
-
|
|
95850
|
-
suggestion: `git checkout '${state.branch}'`
|
|
95851
|
-
});
|
|
95852
|
-
}
|
|
95853
|
-
if (state.branch) {
|
|
95854
|
-
const sanitized = sanitizeBranchName(state.branch);
|
|
95855
|
-
let pair;
|
|
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) {
|
|
95856
95711
|
try {
|
|
95857
|
-
|
|
95858
|
-
|
|
95859
|
-
|
|
95860
|
-
}
|
|
95861
|
-
if (!pair) {
|
|
95862
|
-
findings.push({
|
|
95863
|
-
id: "lakebase-pair-missing",
|
|
95864
|
-
severity: "fail",
|
|
95865
|
-
message: `workflow says feature-claimed for "${state.branch}", but no Lakebase branch "${sanitized}" exists.`,
|
|
95866
|
-
suggestion: `lakebase-scm-abandon-feature # reset state; re-claim if needed`
|
|
95867
|
-
});
|
|
95868
|
-
} else if (state.lakebase_branch_uid && pair.uid !== state.lakebase_branch_uid) {
|
|
95869
|
-
findings.push({
|
|
95870
|
-
id: "lakebase-uid-drift",
|
|
95871
|
-
severity: "warn",
|
|
95872
|
-
message: `workflow records lakebase_branch_uid=${state.lakebase_branch_uid}, but the live branch reports ${pair.uid}.`,
|
|
95873
|
-
suggestion: "lakebase-scm-adopt-state --force"
|
|
95712
|
+
await exec2(`git checkout ${shellEscape2(switchTo)}`, {
|
|
95713
|
+
cwd: args.projectDir,
|
|
95714
|
+
timeout: 1e4
|
|
95874
95715
|
});
|
|
95716
|
+
headAfter = switchTo;
|
|
95717
|
+
try {
|
|
95718
|
+
await exec2(`git fetch origin ${shellEscape2(switchTo)}`, {
|
|
95719
|
+
cwd: args.projectDir,
|
|
95720
|
+
timeout: 3e4
|
|
95721
|
+
});
|
|
95722
|
+
await exec2(`git merge --ff-only ${shellEscape2(`origin/${switchTo}`)}`, {
|
|
95723
|
+
cwd: args.projectDir,
|
|
95724
|
+
timeout: 1e4
|
|
95725
|
+
});
|
|
95726
|
+
} catch (err) {
|
|
95727
|
+
warnings.push(
|
|
95728
|
+
`local fast-forward of ${switchTo} to origin/${switchTo} failed: ${err instanceof Error ? err.message : String(err)}. The PR merged remotely; your local ${switchTo} may be stale, run \`git pull --ff-only\`.`
|
|
95729
|
+
);
|
|
95730
|
+
}
|
|
95731
|
+
} catch (err) {
|
|
95732
|
+
warnings.push(
|
|
95733
|
+
`git checkout ${switchTo} failed: ${err instanceof Error ? err.message : String(err)}. Local branch was NOT deleted.`
|
|
95734
|
+
);
|
|
95875
95735
|
}
|
|
95736
|
+
} else {
|
|
95737
|
+
headAfter = head || current.branch;
|
|
95738
|
+
const reconcileWarning = await reconcileTierToOrigin({ cwd: args.projectDir, tier: switchTo });
|
|
95739
|
+
if (reconcileWarning) warnings.push(reconcileWarning);
|
|
95876
95740
|
}
|
|
95877
|
-
|
|
95878
|
-
|
|
95879
|
-
|
|
95880
|
-
|
|
95881
|
-
|
|
95882
|
-
|
|
95883
|
-
|
|
95884
|
-
|
|
95885
|
-
|
|
95886
|
-
|
|
95887
|
-
|
|
95888
|
-
|
|
95889
|
-
}
|
|
95890
|
-
if (headBranch && !TIER_LEAFS2.has(headBranch) && headBranch.startsWith(FEATURE_PREFIX)) {
|
|
95891
|
-
const sanitized = sanitizeBranchName(headBranch);
|
|
95892
|
-
const paired = lakebaseBranches.some((b) => leafOf2(b) === sanitized);
|
|
95893
|
-
if (!paired) {
|
|
95894
|
-
findings.push({
|
|
95895
|
-
id: "orphan-current-branch",
|
|
95896
|
-
severity: "fail",
|
|
95897
|
-
message: `Current git branch "${headBranch}" has no Lakebase pair (post-checkout fallback retired in phase C).`,
|
|
95898
|
-
suggestion: `lakebase-scm-recover-orphans --claim --only-branch '${headBranch}'`
|
|
95899
|
-
});
|
|
95741
|
+
if (headAfter !== current.branch) {
|
|
95742
|
+
try {
|
|
95743
|
+
await exec2(
|
|
95744
|
+
`git branch -D ${shellEscape2(current.branch)}`,
|
|
95745
|
+
{ cwd: args.projectDir, timeout: 1e4 }
|
|
95746
|
+
);
|
|
95747
|
+
localBranchDeleted = true;
|
|
95748
|
+
} catch (err) {
|
|
95749
|
+
warnings.push(
|
|
95750
|
+
`git branch -D ${current.branch} failed: ${err instanceof Error ? err.message : String(err)}.`
|
|
95751
|
+
);
|
|
95752
|
+
}
|
|
95900
95753
|
}
|
|
95901
95754
|
}
|
|
95902
|
-
|
|
95903
|
-
|
|
95904
|
-
|
|
95905
|
-
|
|
95906
|
-
|
|
95907
|
-
|
|
95908
|
-
|
|
95909
|
-
|
|
95755
|
+
const nowFn = args.now ?? (() => /* @__PURE__ */ new Date());
|
|
95756
|
+
const mergedAt = nowFn();
|
|
95757
|
+
let next = {
|
|
95758
|
+
...current,
|
|
95759
|
+
state: "merged",
|
|
95760
|
+
merged_at: mergedAt.toISOString()
|
|
95761
|
+
};
|
|
95762
|
+
writeWorkflowState(args.projectDir, next);
|
|
95763
|
+
let migrate;
|
|
95764
|
+
const waitMigrate = args.waitMigrate !== false;
|
|
95765
|
+
if (waitMigrate) {
|
|
95766
|
+
const timeoutMs = args.migrateTimeoutMs ?? DEFAULT_MIGRATE_TIMEOUT_MS;
|
|
95767
|
+
const pollMs = args.migratePollMs ?? DEFAULT_MIGRATE_POLL_MS;
|
|
95768
|
+
const fetchRuns = args.fetchRuns ?? listWorkflowRuns;
|
|
95769
|
+
const predicate = args.migrateRunPredicate ?? (paired.mergeCommitSha ? shaMigratePredicate(paired.mergeCommitSha) : defaultMigratePredicate);
|
|
95770
|
+
const elapsedSinceMerge = nowFn().getTime() - mergedAt.getTime();
|
|
95771
|
+
const remainingTimeoutMs = Math.max(0, timeoutMs - elapsedSinceMerge);
|
|
95772
|
+
let polls = 0;
|
|
95773
|
+
let matched;
|
|
95774
|
+
let lastSeen;
|
|
95775
|
+
try {
|
|
95776
|
+
const result = await pollUntil({
|
|
95777
|
+
timeoutMs: remainingTimeoutMs,
|
|
95778
|
+
intervalMs: pollMs,
|
|
95779
|
+
now: nowFn,
|
|
95780
|
+
sleep: args.sleep,
|
|
95781
|
+
probe: async () => {
|
|
95782
|
+
const runs = await fetchRuns(ownerRepo, 20);
|
|
95783
|
+
const candidates = runs.filter((r2) => r2.branch === gitBase).filter((r2) => predicate(r2, mergedAt));
|
|
95784
|
+
if (candidates.length === 0) {
|
|
95785
|
+
return { done: false };
|
|
95786
|
+
}
|
|
95787
|
+
candidates.sort(
|
|
95788
|
+
(a, b) => Date.parse(b.createdAt ?? "0") - Date.parse(a.createdAt ?? "0")
|
|
95789
|
+
);
|
|
95790
|
+
lastSeen = candidates[0];
|
|
95791
|
+
const status = (lastSeen.status ?? "").toLowerCase();
|
|
95792
|
+
return status === "completed" ? { done: true, value: lastSeen } : { done: false };
|
|
95793
|
+
}
|
|
95910
95794
|
});
|
|
95795
|
+
polls = result.polls;
|
|
95796
|
+
if (result.outcome === "done") {
|
|
95797
|
+
matched = result.value;
|
|
95798
|
+
}
|
|
95799
|
+
} catch (err) {
|
|
95800
|
+
warnings.push(
|
|
95801
|
+
`Downstream migrate poll errored: ${err instanceof Error ? err.message : String(err)}. Treating as advisory.`
|
|
95802
|
+
);
|
|
95911
95803
|
}
|
|
95912
|
-
|
|
95913
|
-
|
|
95914
|
-
|
|
95915
|
-
|
|
95916
|
-
|
|
95917
|
-
|
|
95918
|
-
|
|
95919
|
-
|
|
95920
|
-
|
|
95921
|
-
|
|
95922
|
-
|
|
95804
|
+
const applyLocalFallback = async (reason) => {
|
|
95805
|
+
if (!args.localMigrateFallback) return false;
|
|
95806
|
+
const r2 = await args.localMigrateFallback();
|
|
95807
|
+
if (!r2.ok) {
|
|
95808
|
+
warnings.push(
|
|
95809
|
+
`Local-migrate fallback FAILED after ${reason}: ${r2.detail ?? "unknown error"}. git promoted but the parent schema is NOT applied (partial promotion); apply it manually.`
|
|
95810
|
+
);
|
|
95811
|
+
return false;
|
|
95812
|
+
}
|
|
95813
|
+
next = { ...next, migrate_completed_at: nowFn().toISOString() };
|
|
95814
|
+
writeWorkflowState(args.projectDir, next);
|
|
95815
|
+
migrate = { ...migrate ?? { waited: true, polls }, appliedLocally: true, authVerified };
|
|
95816
|
+
warnings.push(
|
|
95817
|
+
`Downstream migrate did not confirm (${reason}); applied the parent migrations LOCALLY instead. git and Lakebase schema are in sync${r2.detail ? ` (${r2.detail})` : ""}.`
|
|
95818
|
+
);
|
|
95819
|
+
return true;
|
|
95820
|
+
};
|
|
95821
|
+
if (matched) {
|
|
95822
|
+
const runUrl = workflowRunUrl(ownerRepo, matched);
|
|
95823
|
+
const conclusion = (matched.conclusion ?? "").toLowerCase();
|
|
95824
|
+
migrate = {
|
|
95825
|
+
waited: true,
|
|
95826
|
+
runUrl,
|
|
95827
|
+
conclusion,
|
|
95828
|
+
polls,
|
|
95829
|
+
authVerified
|
|
95830
|
+
};
|
|
95831
|
+
if (conclusion === "success") {
|
|
95832
|
+
next = {
|
|
95833
|
+
...next,
|
|
95834
|
+
migrate_run_url: runUrl,
|
|
95835
|
+
migrate_completed_at: nowFn().toISOString()
|
|
95836
|
+
};
|
|
95837
|
+
writeWorkflowState(args.projectDir, next);
|
|
95838
|
+
} else {
|
|
95839
|
+
const applied = await applyLocalFallback(`the downstream migrate finished conclusion=${conclusion} (${runUrl})`);
|
|
95840
|
+
if (!applied) {
|
|
95841
|
+
throw new ScmMergeError(
|
|
95842
|
+
`Downstream migrate workflow finished with conclusion=${conclusion}. Run ${runUrl} for details.`,
|
|
95843
|
+
"migrate-failed"
|
|
95844
|
+
);
|
|
95845
|
+
}
|
|
95923
95846
|
}
|
|
95924
|
-
|
|
95925
|
-
|
|
95926
|
-
|
|
95927
|
-
|
|
95928
|
-
|
|
95929
|
-
|
|
95930
|
-
|
|
95847
|
+
} else {
|
|
95848
|
+
const budgetSec = Math.round(
|
|
95849
|
+
(args.migrateTimeoutMs ?? DEFAULT_MIGRATE_TIMEOUT_MS) / 1e3
|
|
95850
|
+
);
|
|
95851
|
+
const lastStatus = lastSeen?.status ?? "(no matching run)";
|
|
95852
|
+
const timeoutFatal = args.migrateTimeoutFatal !== false;
|
|
95853
|
+
if (timeoutFatal) {
|
|
95854
|
+
migrate = { waited: true, polls, authVerified };
|
|
95855
|
+
const applied = await applyLocalFallback(
|
|
95856
|
+
`the downstream migrate did not complete within ${budgetSec}s (last seen: ${lastStatus})`
|
|
95857
|
+
);
|
|
95858
|
+
if (!applied) {
|
|
95859
|
+
throw new ScmMergeError(
|
|
95860
|
+
`Timed out after ${budgetSec}s waiting for the downstream migrate workflow on "${gitBase}". Last seen status: ${lastStatus}.`,
|
|
95861
|
+
"migrate-timeout"
|
|
95862
|
+
);
|
|
95863
|
+
}
|
|
95864
|
+
} else {
|
|
95865
|
+
migrate = { waited: true, polls, timedOut: true, authVerified };
|
|
95866
|
+
warnings.push(
|
|
95867
|
+
`Downstream migrate workflow on "${gitBase}" was not confirmed within ${budgetSec}s (last seen status: ${lastStatus}). The PR merged and your local ${gitBase} is synced; the migrate run may still be pending or running. Confirm it later via the Actions tab or re-run with --wait-migrate.`
|
|
95868
|
+
);
|
|
95931
95869
|
}
|
|
95932
|
-
} catch {
|
|
95933
95870
|
}
|
|
95871
|
+
} else {
|
|
95872
|
+
migrate = { waited: false, polls: 0 };
|
|
95934
95873
|
}
|
|
95935
|
-
return
|
|
95936
|
-
|
|
95937
|
-
|
|
95938
|
-
|
|
95939
|
-
|
|
95940
|
-
|
|
95941
|
-
|
|
95874
|
+
return {
|
|
95875
|
+
state: next,
|
|
95876
|
+
paired,
|
|
95877
|
+
localBranchDeleted,
|
|
95878
|
+
headAfter,
|
|
95879
|
+
migrate,
|
|
95880
|
+
warnings
|
|
95881
|
+
};
|
|
95942
95882
|
}
|
|
95943
|
-
function
|
|
95944
|
-
|
|
95945
|
-
for (const f3 of report.findings) {
|
|
95946
|
-
worst = worstOf(worst, f3.severity);
|
|
95947
|
-
}
|
|
95948
|
-
return { ...report, worstSeverity: worst };
|
|
95883
|
+
function workflowRunUrl(ownerRepo, run) {
|
|
95884
|
+
return `https://github.com/${ownerRepo}/actions/runs/${run.id}`;
|
|
95949
95885
|
}
|
|
95950
|
-
|
|
95951
|
-
|
|
95952
|
-
|
|
95953
|
-
|
|
95954
|
-
|
|
95886
|
+
function extractPullNumber(prUrl) {
|
|
95887
|
+
const m2 = prUrl.match(/\/pull\/(\d+)(?:[\/?#].*)?$/);
|
|
95888
|
+
if (!m2) return void 0;
|
|
95889
|
+
const n = Number.parseInt(m2[1], 10);
|
|
95890
|
+
return Number.isFinite(n) ? n : void 0;
|
|
95891
|
+
}
|
|
95892
|
+
function shellEscape2(s2) {
|
|
95893
|
+
return `'${s2.replace(/'/g, "'\\''")}'`;
|
|
95894
|
+
}
|
|
95895
|
+
|
|
95896
|
+
// scripts/lakebase/scm-recover-orphans.ts
|
|
95897
|
+
init_cjs_shims();
|
|
95898
|
+
|
|
95899
|
+
// scripts/git/branches.ts
|
|
95900
|
+
init_cjs_shims();
|
|
95901
|
+
async function currentBranchName2(cwd) {
|
|
95902
|
+
try {
|
|
95903
|
+
return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
95904
|
+
} catch {
|
|
95905
|
+
return "";
|
|
95955
95906
|
}
|
|
95956
|
-
|
|
95957
|
-
|
|
95958
|
-
|
|
95959
|
-
|
|
95960
|
-
|
|
95961
|
-
|
|
95962
|
-
|
|
95963
|
-
|
|
95964
|
-
"db-ahead-of-code"
|
|
95965
|
-
];
|
|
95966
|
-
async function fixFinding(args) {
|
|
95967
|
-
if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
|
|
95968
|
-
throw new ScmDoctorFixError(
|
|
95969
|
-
`Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
|
|
95970
|
-
"unsupported-finding"
|
|
95907
|
+
}
|
|
95908
|
+
async function listLocalBranches(args) {
|
|
95909
|
+
const { cwd } = args;
|
|
95910
|
+
let raw;
|
|
95911
|
+
try {
|
|
95912
|
+
raw = await exec2(
|
|
95913
|
+
'git branch --format="%(refname:short)|%(upstream:short)|%(upstream:track)"',
|
|
95914
|
+
{ cwd }
|
|
95971
95915
|
);
|
|
95916
|
+
} catch {
|
|
95917
|
+
return [];
|
|
95972
95918
|
}
|
|
95973
|
-
|
|
95974
|
-
const
|
|
95975
|
-
|
|
95976
|
-
|
|
95977
|
-
|
|
95978
|
-
|
|
95979
|
-
)
|
|
95919
|
+
if (!raw) return [];
|
|
95920
|
+
const current = await currentBranchName2(cwd);
|
|
95921
|
+
return raw.split("\n").filter(Boolean).map((line) => {
|
|
95922
|
+
const [name, tracking, trackInfo] = line.split("|");
|
|
95923
|
+
let ahead = 0;
|
|
95924
|
+
let behind = 0;
|
|
95925
|
+
if (trackInfo) {
|
|
95926
|
+
const aheadMatch = trackInfo.match(/ahead (\d+)/);
|
|
95927
|
+
const behindMatch = trackInfo.match(/behind (\d+)/);
|
|
95928
|
+
if (aheadMatch) ahead = parseInt(aheadMatch[1], 10);
|
|
95929
|
+
if (behindMatch) behind = parseInt(behindMatch[1], 10);
|
|
95930
|
+
}
|
|
95931
|
+
return {
|
|
95932
|
+
name,
|
|
95933
|
+
isCurrent: name === current,
|
|
95934
|
+
isRemote: false,
|
|
95935
|
+
tracking: tracking || void 0,
|
|
95936
|
+
ahead,
|
|
95937
|
+
behind
|
|
95938
|
+
};
|
|
95939
|
+
});
|
|
95940
|
+
}
|
|
95941
|
+
async function listRemoteBranches(args) {
|
|
95942
|
+
const { cwd, remote = "origin" } = args;
|
|
95943
|
+
try {
|
|
95944
|
+
const localBranches = await listLocalBranches({ cwd });
|
|
95945
|
+
const localNames = new Set(localBranches.map((b) => b.name));
|
|
95946
|
+
const raw = await exec2(`git branch -r --format="%(refname:short)"`, {
|
|
95947
|
+
cwd
|
|
95948
|
+
});
|
|
95949
|
+
if (!raw) return [];
|
|
95950
|
+
const remotePrefix = `${remote}/`;
|
|
95951
|
+
return raw.split("\n").filter(Boolean).filter((name) => !name.includes("HEAD")).map((name) => {
|
|
95952
|
+
const shortName = name.startsWith(remotePrefix) ? name.slice(remotePrefix.length) : name;
|
|
95953
|
+
return { fullName: name, shortName };
|
|
95954
|
+
}).filter(({ shortName }) => !localNames.has(shortName)).map(({ fullName, shortName }) => ({
|
|
95955
|
+
name: shortName,
|
|
95956
|
+
isCurrent: false,
|
|
95957
|
+
isRemote: true,
|
|
95958
|
+
tracking: fullName
|
|
95959
|
+
}));
|
|
95960
|
+
} catch {
|
|
95961
|
+
return [];
|
|
95980
95962
|
}
|
|
95981
|
-
|
|
95963
|
+
}
|
|
95964
|
+
async function hasRemoteBranch(args) {
|
|
95965
|
+
const { cwd, branch, remote = "origin" } = args;
|
|
95982
95966
|
try {
|
|
95983
|
-
|
|
95984
|
-
|
|
95985
|
-
|
|
95986
|
-
|
|
95987
|
-
|
|
95988
|
-
|
|
95989
|
-
|
|
95990
|
-
|
|
95991
|
-
|
|
95992
|
-
|
|
95993
|
-
|
|
95994
|
-
|
|
95995
|
-
|
|
95996
|
-
|
|
95997
|
-
|
|
95998
|
-
|
|
95999
|
-
|
|
96000
|
-
|
|
96001
|
-
|
|
96002
|
-
|
|
96003
|
-
|
|
96004
|
-
|
|
96005
|
-
|
|
96006
|
-
|
|
96007
|
-
|
|
96008
|
-
|
|
96009
|
-
|
|
96010
|
-
|
|
96011
|
-
|
|
96012
|
-
|
|
96013
|
-
|
|
96014
|
-
|
|
96015
|
-
|
|
96016
|
-
|
|
96017
|
-
|
|
96018
|
-
|
|
96019
|
-
|
|
96020
|
-
|
|
96021
|
-
|
|
96022
|
-
|
|
96023
|
-
|
|
96024
|
-
|
|
96025
|
-
|
|
96026
|
-
|
|
96027
|
-
await adoptScmState({
|
|
96028
|
-
projectDir: args.projectDir,
|
|
96029
|
-
instance,
|
|
96030
|
-
force: true
|
|
96031
|
-
});
|
|
96032
|
-
action = `adopted state with --force to re-infer tier_topology`;
|
|
96033
|
-
break;
|
|
96034
|
-
}
|
|
96035
|
-
case "orphan-current-branch": {
|
|
96036
|
-
const instance = args.instance ?? report.state?.project_id;
|
|
96037
|
-
if (!instance) {
|
|
96038
|
-
throw new ScmDoctorFixError(
|
|
96039
|
-
"Cannot fix: missing Lakebase project id.",
|
|
96040
|
-
"fix-failed"
|
|
96041
|
-
);
|
|
96042
|
-
}
|
|
96043
|
-
const headBranch = await getCurrentBranch({ cwd: args.projectDir });
|
|
96044
|
-
if (!headBranch) {
|
|
96045
|
-
throw new ScmDoctorFixError(
|
|
96046
|
-
"Cannot fix: detached HEAD or no current branch.",
|
|
96047
|
-
"fix-failed"
|
|
96048
|
-
);
|
|
96049
|
-
}
|
|
96050
|
-
await recoverOrphans({
|
|
96051
|
-
projectDir: args.projectDir,
|
|
96052
|
-
instance,
|
|
96053
|
-
claim: true,
|
|
96054
|
-
onlyBranch: headBranch
|
|
96055
|
-
});
|
|
96056
|
-
action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
|
|
96057
|
-
break;
|
|
96058
|
-
}
|
|
96059
|
-
case "multiple-migration-heads": {
|
|
96060
|
-
const r2 = await collapseMigrationHeads({ projectDir: args.projectDir });
|
|
96061
|
-
if (r2.status !== "ok" || !r2.mergeRevision) {
|
|
96062
|
-
throw new ScmDoctorFixError(
|
|
96063
|
-
`Expected to create a merge revision but got status="${r2.status}".`,
|
|
96064
|
-
"fix-failed"
|
|
96065
|
-
);
|
|
96066
|
-
}
|
|
96067
|
-
action = `collapsed ${r2.headsBefore.length} heads into merge revision ${r2.mergeRevision} (commit it)`;
|
|
96068
|
-
break;
|
|
96069
|
-
}
|
|
96070
|
-
case "db-ahead-of-code": {
|
|
96071
|
-
const r2 = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
|
|
96072
|
-
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`;
|
|
96073
|
-
break;
|
|
96074
|
-
}
|
|
95967
|
+
const out = await exec2(
|
|
95968
|
+
`git ls-remote --heads "${remote}" "${branch}"`,
|
|
95969
|
+
{ cwd }
|
|
95970
|
+
);
|
|
95971
|
+
return out.trim().length > 0;
|
|
95972
|
+
} catch {
|
|
95973
|
+
return false;
|
|
95974
|
+
}
|
|
95975
|
+
}
|
|
95976
|
+
|
|
95977
|
+
// scripts/lakebase/scm-recover-orphans.ts
|
|
95978
|
+
var ScmRecoverError = class extends Error {
|
|
95979
|
+
constructor(message, code) {
|
|
95980
|
+
super(message);
|
|
95981
|
+
this.code = code;
|
|
95982
|
+
this.name = "ScmRecoverError";
|
|
95983
|
+
}
|
|
95984
|
+
code;
|
|
95985
|
+
};
|
|
95986
|
+
var TIER_LEAFS = /* @__PURE__ */ new Set(["staging", "dev", "main", "master"]);
|
|
95987
|
+
async function recoverOrphans(args) {
|
|
95988
|
+
if (!args.instance) {
|
|
95989
|
+
throw new ScmRecoverError(
|
|
95990
|
+
"Lakebase project id required (--instance / LAKEBASE_PROJECT_ID).",
|
|
95991
|
+
"missing-instance"
|
|
95992
|
+
);
|
|
95993
|
+
}
|
|
95994
|
+
const lakebaseBranches = await listBranches({ instance: args.instance });
|
|
95995
|
+
const tierTopology = inferTierTopology(lakebaseBranches);
|
|
95996
|
+
const lakebaseLeafs = new Set(
|
|
95997
|
+
lakebaseBranches.map((b) => leafName2(b))
|
|
95998
|
+
);
|
|
95999
|
+
const defaultLeaf = leafName2(
|
|
96000
|
+
lakebaseBranches.find((b) => b.isDefault === true)
|
|
96001
|
+
);
|
|
96002
|
+
const gitBranches = await listLocalBranches({ cwd: args.projectDir });
|
|
96003
|
+
const orphans = [];
|
|
96004
|
+
const skipped = [];
|
|
96005
|
+
for (const gb of gitBranches) {
|
|
96006
|
+
if (gb.isRemote) continue;
|
|
96007
|
+
const name = gb.name;
|
|
96008
|
+
if (TIER_LEAFS.has(name)) {
|
|
96009
|
+
skipped.push({ gitBranch: name, reason: "tier branch" });
|
|
96010
|
+
continue;
|
|
96075
96011
|
}
|
|
96076
|
-
|
|
96077
|
-
|
|
96078
|
-
|
|
96079
|
-
|
|
96080
|
-
|
|
96012
|
+
if (defaultLeaf && name === defaultLeaf) {
|
|
96013
|
+
skipped.push({ gitBranch: name, reason: "default branch" });
|
|
96014
|
+
continue;
|
|
96015
|
+
}
|
|
96016
|
+
const sanitized = sanitizeBranchName(name);
|
|
96017
|
+
if (lakebaseLeafs.has(sanitized)) {
|
|
96018
|
+
skipped.push({
|
|
96019
|
+
gitBranch: name,
|
|
96020
|
+
reason: `paired Lakebase branch "${sanitized}" exists`
|
|
96021
|
+
});
|
|
96022
|
+
continue;
|
|
96023
|
+
}
|
|
96024
|
+
orphans.push({
|
|
96025
|
+
gitBranch: name,
|
|
96026
|
+
sanitized,
|
|
96027
|
+
isCurrent: gb.isCurrent === true,
|
|
96028
|
+
reason: name.startsWith("feature/") ? "feature/<slug> branch with no Lakebase pair" : `non-tier git branch "${name}" with no Lakebase pair`
|
|
96029
|
+
});
|
|
96030
|
+
}
|
|
96031
|
+
const result = {
|
|
96032
|
+
tierTopology,
|
|
96033
|
+
orphans,
|
|
96034
|
+
skipped,
|
|
96035
|
+
claimed: []
|
|
96036
|
+
};
|
|
96037
|
+
if (!args.claim || orphans.length === 0) {
|
|
96038
|
+
return result;
|
|
96039
|
+
}
|
|
96040
|
+
const parentBranch = parentForTopology(tierTopology, defaultLeaf);
|
|
96041
|
+
const currentState = readWorkflowState(args.projectDir);
|
|
96042
|
+
const candidates = args.onlyBranch ? orphans.filter((o) => o.gitBranch === args.onlyBranch) : orphans;
|
|
96043
|
+
if (args.onlyBranch && candidates.length === 0) {
|
|
96044
|
+
throw new ScmRecoverError(
|
|
96045
|
+
`No orphan found for --only-branch ${args.onlyBranch}.`,
|
|
96046
|
+
"claim-conflict"
|
|
96081
96047
|
);
|
|
96082
96048
|
}
|
|
96083
|
-
const
|
|
96084
|
-
|
|
96085
|
-
|
|
96086
|
-
|
|
96087
|
-
|
|
96049
|
+
const headOrphan = candidates.find((o) => o.isCurrent);
|
|
96050
|
+
const stateTargetOrphan = headOrphan ?? candidates[0];
|
|
96051
|
+
for (const orphan of candidates) {
|
|
96052
|
+
try {
|
|
96053
|
+
const paired = await createFeaturePairedBranch({
|
|
96054
|
+
instance: args.instance,
|
|
96055
|
+
branch: orphan.gitBranch,
|
|
96056
|
+
parentBranch,
|
|
96057
|
+
cwd: args.projectDir
|
|
96058
|
+
// The git branch already exists on disk; the substrate primitive
|
|
96059
|
+
// is idempotent on the git side (it'll checkout the existing
|
|
96060
|
+
// branch rather than fail) but if the project is not on this
|
|
96061
|
+
// branch, we want a no-op git side. Leaving the default true is
|
|
96062
|
+
// OK: if the git branch already matches, the checkout is a
|
|
96063
|
+
// no-op; if the branch isn't HEAD, the substrate switches to it
|
|
96064
|
+
// which is what the user implicitly asked for by including the
|
|
96065
|
+
// branch.
|
|
96066
|
+
});
|
|
96067
|
+
let stateUpdated = false;
|
|
96068
|
+
if (orphan === stateTargetOrphan) {
|
|
96069
|
+
const next = {
|
|
96070
|
+
...currentState ?? {
|
|
96071
|
+
$schema: "./scm-workflow-state.schema.json",
|
|
96072
|
+
version: 1,
|
|
96073
|
+
state: "scaffold-complete",
|
|
96074
|
+
tier_topology: tierTopology,
|
|
96075
|
+
project_id: args.instance
|
|
96076
|
+
},
|
|
96077
|
+
state: "feature-claimed",
|
|
96078
|
+
feature_id: orphan.gitBranch.replace(/^feature\//, ""),
|
|
96079
|
+
branch: paired.gitBranch,
|
|
96080
|
+
parent_branch: parentBranch,
|
|
96081
|
+
lakebase_branch_uid: paired.branch.uid,
|
|
96082
|
+
claimed_at: (args.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
96083
|
+
pr_url: void 0,
|
|
96084
|
+
pushed_at: void 0,
|
|
96085
|
+
ci_run_url: void 0,
|
|
96086
|
+
ci_green_at: void 0,
|
|
96087
|
+
merged_at: void 0
|
|
96088
|
+
};
|
|
96089
|
+
writeWorkflowState(args.projectDir, next);
|
|
96090
|
+
stateUpdated = true;
|
|
96091
|
+
result.stateUpdatedFor = orphan.gitBranch;
|
|
96092
|
+
}
|
|
96093
|
+
result.claimed.push({
|
|
96094
|
+
candidate: orphan,
|
|
96095
|
+
lakebaseBranchUid: paired.branch.uid,
|
|
96096
|
+
stateUpdated,
|
|
96097
|
+
warnings: paired.warnings
|
|
96098
|
+
});
|
|
96099
|
+
} catch (err) {
|
|
96100
|
+
throw new ScmRecoverError(
|
|
96101
|
+
`Substrate claim failed for ${orphan.gitBranch}: ${err instanceof Error ? err.message : String(err)}`,
|
|
96102
|
+
"substrate-failure"
|
|
96103
|
+
);
|
|
96104
|
+
}
|
|
96105
|
+
}
|
|
96106
|
+
return result;
|
|
96088
96107
|
}
|
|
96089
|
-
function
|
|
96090
|
-
|
|
96108
|
+
function leafName2(b) {
|
|
96109
|
+
if (!b) return "";
|
|
96110
|
+
return b.name.split("/").pop() ?? b.name;
|
|
96111
|
+
}
|
|
96112
|
+
function parentForTopology(t2, defaultLeaf) {
|
|
96113
|
+
if (t2 === 3) return "dev";
|
|
96114
|
+
if (t2 === 2) return "staging";
|
|
96115
|
+
return defaultLeaf || "main";
|
|
96091
96116
|
}
|
|
96092
96117
|
|
|
96093
|
-
// scripts/lakebase/doctor.ts
|
|
96094
|
-
init_cjs_shims();
|
|
96095
|
-
var fs32 = __toESM(require("fs"), 1);
|
|
96096
|
-
var path30 = __toESM(require("path"), 1);
|
|
96097
|
-
var cp6 = __toESM(require("child_process"), 1);
|
|
96098
|
-
|
|
96099
|
-
// scripts/lakebase/workflow-drift.ts
|
|
96118
|
+
// scripts/lakebase/scm-doctor.ts
|
|
96100
96119
|
init_cjs_shims();
|
|
96101
96120
|
var fs31 = __toESM(require("fs"), 1);
|
|
96102
96121
|
var path29 = __toESM(require("path"), 1);
|
|
96103
|
-
|
|
96104
|
-
|
|
96105
|
-
|
|
96106
|
-
|
|
96107
|
-
|
|
96108
|
-
|
|
96109
|
-
|
|
96110
|
-
|
|
96111
|
-
|
|
96112
|
-
|
|
96113
|
-
);
|
|
96114
|
-
if (fs31.existsSync(candidate)) return candidate;
|
|
96115
|
-
const parent = path29.dirname(dir);
|
|
96116
|
-
if (parent === dir) break;
|
|
96117
|
-
dir = parent;
|
|
96122
|
+
var FEATURE_PREFIX = "feature/";
|
|
96123
|
+
var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
|
|
96124
|
+
function readEnv(projectDir) {
|
|
96125
|
+
const envPath = path29.join(projectDir, ".env");
|
|
96126
|
+
const out = /* @__PURE__ */ new Map();
|
|
96127
|
+
if (!fs31.existsSync(envPath)) return out;
|
|
96128
|
+
const lines = fs31.readFileSync(envPath, "utf8").split("\n");
|
|
96129
|
+
for (const line of lines) {
|
|
96130
|
+
const m2 = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
|
|
96131
|
+
if (m2) out.set(m2[1], m2[2].replace(/^["']|["']$/g, ""));
|
|
96118
96132
|
}
|
|
96119
|
-
|
|
96120
|
-
`Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
|
|
96121
|
-
);
|
|
96133
|
+
return out;
|
|
96122
96134
|
}
|
|
96123
|
-
function
|
|
96124
|
-
|
|
96125
|
-
const a = projectContent.split("\n");
|
|
96126
|
-
const b = templateContent.split("\n");
|
|
96127
|
-
const out = [`--- project/${name}`, `+++ template/${name}`];
|
|
96128
|
-
const max = Math.max(a.length, b.length);
|
|
96129
|
-
for (let i2 = 0; i2 < max; i2++) {
|
|
96130
|
-
const av = a[i2];
|
|
96131
|
-
const bv = b[i2];
|
|
96132
|
-
if (av === bv) continue;
|
|
96133
|
-
if (av !== void 0) out.push(`-${i2 + 1}: ${av}`);
|
|
96134
|
-
if (bv !== void 0) out.push(`+${i2 + 1}: ${bv}`);
|
|
96135
|
-
}
|
|
96136
|
-
return out.join("\n");
|
|
96135
|
+
function leafOf2(b) {
|
|
96136
|
+
return b.name.split("/").pop() ?? b.name;
|
|
96137
96137
|
}
|
|
96138
|
-
function
|
|
96139
|
-
const
|
|
96140
|
-
|
|
96141
|
-
|
|
96142
|
-
|
|
96143
|
-
|
|
96144
|
-
const
|
|
96145
|
-
const
|
|
96146
|
-
|
|
96147
|
-
|
|
96148
|
-
|
|
96149
|
-
|
|
96150
|
-
|
|
96151
|
-
|
|
96152
|
-
|
|
96153
|
-
|
|
96154
|
-
|
|
96155
|
-
|
|
96156
|
-
|
|
96157
|
-
|
|
96158
|
-
|
|
96159
|
-
|
|
96160
|
-
|
|
96161
|
-
|
|
96162
|
-
|
|
96163
|
-
|
|
96164
|
-
|
|
96138
|
+
function worstOf(a, b) {
|
|
96139
|
+
const order = ["ok", "warn", "fail"];
|
|
96140
|
+
return order[Math.max(order.indexOf(a), order.indexOf(b))];
|
|
96141
|
+
}
|
|
96142
|
+
async function runDoctor(args, deps = {}) {
|
|
96143
|
+
const projectDir = args.projectDir;
|
|
96144
|
+
const findings = [];
|
|
96145
|
+
const env = readEnv(projectDir);
|
|
96146
|
+
const instance = args.instance ?? env.get("LAKEBASE_PROJECT_ID");
|
|
96147
|
+
const state = readWorkflowState(projectDir);
|
|
96148
|
+
const workflowStatePresent = state !== null;
|
|
96149
|
+
for (const stale of deps.findStaleBranches?.() ?? []) {
|
|
96150
|
+
const where = stale.feature_id ? ` ${stale.feature_id}/${stale.story_id}` : "";
|
|
96151
|
+
findings.push({
|
|
96152
|
+
id: `stale-${stale.kind}`,
|
|
96153
|
+
severity: "warn",
|
|
96154
|
+
message: `Stale ${stale.kind}${where} "${stale.slug}"${stale.branch ? ` (branch ${stale.branch})` : ""}: ${stale.reason}.`,
|
|
96155
|
+
suggestion: stale.kind === "experiment" ? `consort-experiment discard --feature ${stale.feature_id} --story ${stale.story_id} --slug ${stale.slug} --instance <id> --approver <you> --reason "doctor: stale experiment"` : "consort-spike teardown (or delete the spike's paired branch) once its learning has carried forward"
|
|
96156
|
+
});
|
|
96157
|
+
}
|
|
96158
|
+
if (!workflowStatePresent) {
|
|
96159
|
+
findings.push({
|
|
96160
|
+
id: "no-state-file",
|
|
96161
|
+
severity: "fail",
|
|
96162
|
+
message: "No .lakebase/workflow-state.json. Either the project pre-dates the SCM workflow or scaffold did not seed it.",
|
|
96163
|
+
suggestion: "lakebase-scm-adopt-state"
|
|
96164
|
+
});
|
|
96165
|
+
}
|
|
96166
|
+
try {
|
|
96167
|
+
const ownerRepo = await getOwnerRepo(projectDir);
|
|
96168
|
+
if (ownerRepo) {
|
|
96169
|
+
const enabled = await getActionsEnabled(ownerRepo);
|
|
96170
|
+
if (enabled === false) {
|
|
96171
|
+
findings.push({
|
|
96172
|
+
id: "github-actions-disabled",
|
|
96173
|
+
severity: "warn",
|
|
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
|
+
}
|
|
96165
96178
|
}
|
|
96166
|
-
|
|
96167
|
-
|
|
96168
|
-
|
|
96169
|
-
|
|
96170
|
-
|
|
96171
|
-
|
|
96172
|
-
|
|
96173
|
-
|
|
96174
|
-
|
|
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."
|
|
96175
96204
|
});
|
|
96176
96205
|
}
|
|
96206
|
+
} catch {
|
|
96177
96207
|
}
|
|
96178
|
-
|
|
96179
|
-
|
|
96180
|
-
|
|
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
|
+
});
|
|
96181
96215
|
}
|
|
96182
|
-
|
|
96183
|
-
|
|
96184
|
-
|
|
96185
|
-
|
|
96186
|
-
|
|
96187
|
-
|
|
96188
|
-
|
|
96189
|
-
const hasDrift = files.some((f3) => f3.status === "drifted" || f3.status === "missing");
|
|
96190
|
-
return {
|
|
96191
|
-
overall: hasDrift ? "drift" : "ok",
|
|
96192
|
-
files
|
|
96193
|
-
};
|
|
96194
|
-
}
|
|
96195
|
-
function readKitVersion(kitWorkflowsDir) {
|
|
96196
|
-
let dir = kitWorkflowsDir;
|
|
96197
|
-
for (let i2 = 0; i2 < 5; i2++) {
|
|
96198
|
-
dir = path29.dirname(dir);
|
|
96216
|
+
if (!instance) {
|
|
96217
|
+
return finalize({
|
|
96218
|
+
projectDir,
|
|
96219
|
+
workflowStatePresent,
|
|
96220
|
+
state: state ?? void 0,
|
|
96221
|
+
findings
|
|
96222
|
+
});
|
|
96199
96223
|
}
|
|
96224
|
+
let lakebaseBranches = [];
|
|
96200
96225
|
try {
|
|
96201
|
-
|
|
96202
|
-
|
|
96203
|
-
|
|
96204
|
-
|
|
96205
|
-
|
|
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
|
+
});
|
|
96206
96240
|
}
|
|
96207
|
-
|
|
96208
|
-
|
|
96209
|
-
|
|
96210
|
-
|
|
96211
|
-
|
|
96212
|
-
|
|
96213
|
-
|
|
96214
|
-
|
|
96215
|
-
function findKitCommandsDir(start) {
|
|
96216
|
-
let dir = start;
|
|
96217
|
-
for (let i2 = 0; i2 < 6; i2++) {
|
|
96218
|
-
const candidate = path29.join(
|
|
96219
|
-
dir,
|
|
96220
|
-
"templates",
|
|
96221
|
-
"project",
|
|
96222
|
-
"common",
|
|
96223
|
-
".claude",
|
|
96224
|
-
"commands"
|
|
96225
|
-
);
|
|
96226
|
-
if (fs31.existsSync(candidate)) return candidate;
|
|
96227
|
-
const parent = path29.dirname(dir);
|
|
96228
|
-
if (parent === dir) break;
|
|
96229
|
-
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
|
+
});
|
|
96230
96249
|
}
|
|
96231
|
-
|
|
96232
|
-
|
|
96233
|
-
|
|
96234
|
-
|
|
96235
|
-
|
|
96236
|
-
|
|
96237
|
-
|
|
96238
|
-
}
|
|
96239
|
-
function detectCommandDrift(args) {
|
|
96240
|
-
const projectCommandsDir = path29.join(args.projectDir, ".claude", "commands");
|
|
96241
|
-
const here = path29.dirname(new URL(importMetaUrl).pathname);
|
|
96242
|
-
const kitCommandsDir = args.kitDir ? path29.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
|
|
96243
|
-
const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
|
|
96244
|
-
const templateFiles = fs31.existsSync(kitCommandsDir) ? fs31.readdirSync(kitCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
|
|
96245
|
-
const projectFiles = fs31.existsSync(projectCommandsDir) ? fs31.readdirSync(projectCommandsDir).filter((f3) => f3.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f3)) : [];
|
|
96246
|
-
const seen = /* @__PURE__ */ new Set();
|
|
96247
|
-
const files = [];
|
|
96248
|
-
for (const name of templateFiles) {
|
|
96249
|
-
seen.add(name);
|
|
96250
|
-
const projectPath2 = path29.join(projectCommandsDir, name);
|
|
96251
|
-
const templatePath = path29.join(kitCommandsDir, name);
|
|
96252
|
-
const templateRaw = fs31.readFileSync(templatePath, "utf8");
|
|
96253
|
-
if (!fs31.existsSync(projectPath2)) {
|
|
96254
|
-
files.push({ name, status: "missing", kit_version: kitVersion });
|
|
96255
|
-
continue;
|
|
96256
|
-
}
|
|
96257
|
-
const projectContent = fs31.readFileSync(projectPath2, "utf8");
|
|
96258
|
-
const pinned = parsePinnedVersion(projectContent);
|
|
96259
|
-
const versionForCompare = pinned ?? kitVersion;
|
|
96260
|
-
const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
|
|
96261
|
-
if (projectContent === templateContent) {
|
|
96262
|
-
files.push({
|
|
96263
|
-
name,
|
|
96264
|
-
status: "unchanged",
|
|
96265
|
-
pinned_version: pinned,
|
|
96266
|
-
kit_version: kitVersion
|
|
96267
|
-
});
|
|
96268
|
-
} else {
|
|
96269
|
-
files.push({
|
|
96270
|
-
name,
|
|
96271
|
-
status: "drifted",
|
|
96272
|
-
pinned_version: pinned,
|
|
96273
|
-
kit_version: kitVersion,
|
|
96274
|
-
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}'`
|
|
96275
96258
|
});
|
|
96276
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
|
+
}
|
|
96277
96284
|
}
|
|
96278
|
-
|
|
96279
|
-
|
|
96280
|
-
|
|
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
|
+
}
|
|
96281
96296
|
}
|
|
96282
|
-
|
|
96283
|
-
|
|
96284
|
-
|
|
96285
|
-
|
|
96286
|
-
|
|
96287
|
-
|
|
96288
|
-
|
|
96289
|
-
|
|
96290
|
-
|
|
96291
|
-
}
|
|
96292
|
-
|
|
96293
|
-
let dir = kitCommandsDir;
|
|
96294
|
-
for (let i2 = 0; i2 < 5; i2++) {
|
|
96295
|
-
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
|
+
}
|
|
96296
96308
|
}
|
|
96297
96309
|
try {
|
|
96298
|
-
const
|
|
96299
|
-
|
|
96300
|
-
|
|
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
|
+
}
|
|
96301
96319
|
} catch {
|
|
96302
|
-
return "unknown";
|
|
96303
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
|
+
});
|
|
96304
96349
|
}
|
|
96305
|
-
function
|
|
96306
|
-
|
|
96307
|
-
const
|
|
96308
|
-
|
|
96309
|
-
|
|
96310
|
-
|
|
96311
|
-
commands
|
|
96312
|
-
};
|
|
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 };
|
|
96313
96356
|
}
|
|
96314
|
-
|
|
96315
|
-
|
|
96316
|
-
|
|
96317
|
-
|
|
96318
|
-
"
|
|
96319
|
-
);
|
|
96320
|
-
const here = path29.dirname(new URL(importMetaUrl).pathname);
|
|
96321
|
-
const kitWorkflowsDir = args.kitDir ? path29.join(
|
|
96322
|
-
args.kitDir,
|
|
96323
|
-
"templates",
|
|
96324
|
-
"project",
|
|
96325
|
-
"common",
|
|
96326
|
-
".github",
|
|
96327
|
-
"workflows"
|
|
96328
|
-
) : findKitTemplatesDir(here);
|
|
96329
|
-
const substitute = args.substitute !== false;
|
|
96330
|
-
const dryRun = args.dryRun === true;
|
|
96331
|
-
const pruneExtras = args.pruneExtras === true;
|
|
96332
|
-
const templateFiles = fs31.existsSync(kitWorkflowsDir) ? fs31.readdirSync(kitWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
|
|
96333
|
-
const projectFiles = fs31.existsSync(projectWorkflowsDir) ? fs31.readdirSync(projectWorkflowsDir).filter((f3) => f3.endsWith(".yml")) : [];
|
|
96334
|
-
if (!dryRun && templateFiles.length > 0 && !fs31.existsSync(projectWorkflowsDir)) {
|
|
96335
|
-
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";
|
|
96336
96362
|
}
|
|
96337
|
-
|
|
96338
|
-
|
|
96339
|
-
|
|
96340
|
-
|
|
96341
|
-
|
|
96342
|
-
|
|
96343
|
-
|
|
96344
|
-
|
|
96345
|
-
|
|
96346
|
-
|
|
96347
|
-
|
|
96348
|
-
|
|
96349
|
-
|
|
96350
|
-
|
|
96351
|
-
|
|
96352
|
-
|
|
96353
|
-
} else {
|
|
96354
|
-
outcome = "updated";
|
|
96355
|
-
}
|
|
96356
|
-
if (!dryRun && outcome !== "unchanged") {
|
|
96357
|
-
fs31.writeFileSync(projectPath2, desired);
|
|
96358
|
-
}
|
|
96359
|
-
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
|
+
);
|
|
96360
96379
|
}
|
|
96361
|
-
|
|
96362
|
-
|
|
96363
|
-
|
|
96364
|
-
|
|
96365
|
-
|
|
96366
|
-
|
|
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;
|
|
96367
96481
|
}
|
|
96368
|
-
files.push({ name, outcome: "removed" });
|
|
96369
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
|
+
);
|
|
96370
96489
|
}
|
|
96371
|
-
const
|
|
96372
|
-
|
|
96373
|
-
|
|
96374
|
-
|
|
96375
|
-
|
|
96376
|
-
|
|
96377
|
-
|
|
96378
|
-
|
|
96379
|
-
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, "'\\''")}'`;
|
|
96380
96498
|
}
|
|
96381
96499
|
|
|
96382
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);
|
|
96383
96505
|
function readEnvFile(projectDir) {
|
|
96384
96506
|
const envPath = path30.join(projectDir, ".env");
|
|
96385
96507
|
if (!fs32.existsSync(envPath)) return {};
|
|
@@ -96812,7 +96934,7 @@ function checkWorkflowDrift(projectDir) {
|
|
|
96812
96934
|
status: "warn",
|
|
96813
96935
|
message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
|
|
96814
96936
|
detail: { files: report.files.map((f3) => ({ name: f3.name, status: f3.status })) },
|
|
96815
|
-
hint: "
|
|
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."
|
|
96816
96938
|
};
|
|
96817
96939
|
} catch (err) {
|
|
96818
96940
|
return {
|
|
@@ -96923,9 +97045,10 @@ async function ensureLakebaseSecretAuth(args) {
|
|
|
96923
97045
|
if (typeof pat !== "string" || !pat) {
|
|
96924
97046
|
throw new Error("databricks tokens create returned no token_value");
|
|
96925
97047
|
}
|
|
96926
|
-
await runDatabricks(["secrets", "put-secret",
|
|
97048
|
+
await runDatabricks(["secrets", "put-secret", "--json", "@/dev/stdin"], {
|
|
96927
97049
|
profile,
|
|
96928
|
-
timeout: timeoutMs
|
|
97050
|
+
timeout: timeoutMs,
|
|
97051
|
+
input: JSON.stringify({ scope: scopeName, key: keyName, string_value: pat })
|
|
96929
97052
|
});
|
|
96930
97053
|
let aclGranted = false;
|
|
96931
97054
|
if (servicePrincipalClientId) {
|
|
@@ -97287,63 +97410,6 @@ async function discardAllChanges(args) {
|
|
|
97287
97410
|
await exec2("git clean -fd", { cwd: args.cwd });
|
|
97288
97411
|
}
|
|
97289
97412
|
|
|
97290
|
-
// scripts/git/sync.ts
|
|
97291
|
-
init_cjs_shims();
|
|
97292
|
-
async function currentBranchName2(cwd) {
|
|
97293
|
-
try {
|
|
97294
|
-
return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
97295
|
-
} catch {
|
|
97296
|
-
return "";
|
|
97297
|
-
}
|
|
97298
|
-
}
|
|
97299
|
-
async function push(args) {
|
|
97300
|
-
await exec2("git push", { cwd: args.cwd });
|
|
97301
|
-
}
|
|
97302
|
-
async function pull(args) {
|
|
97303
|
-
await exec2("git pull", { cwd: args.cwd });
|
|
97304
|
-
}
|
|
97305
|
-
async function publishBranch(args) {
|
|
97306
|
-
const remote = args.remote ?? "origin";
|
|
97307
|
-
const branch = await currentBranchName2(args.cwd);
|
|
97308
|
-
if (!branch) throw new Error("No current branch");
|
|
97309
|
-
await exec2(`git push -u ${remote} ${shq(branch)}`, {
|
|
97310
|
-
cwd: args.cwd
|
|
97311
|
-
});
|
|
97312
|
-
}
|
|
97313
|
-
async function pushCurrentBranchForPr(args) {
|
|
97314
|
-
const remote = args.remote ?? "origin";
|
|
97315
|
-
const branch = await currentBranchName2(args.cwd);
|
|
97316
|
-
if (!branch) throw new Error("No current branch");
|
|
97317
|
-
const upstreamSet = await hasUpstream({ cwd: args.cwd });
|
|
97318
|
-
if (!upstreamSet) {
|
|
97319
|
-
await exec2(`git push -u ${remote} ${shq(branch)}`, {
|
|
97320
|
-
cwd: args.cwd
|
|
97321
|
-
});
|
|
97322
|
-
} else {
|
|
97323
|
-
await exec2("git push", { cwd: args.cwd });
|
|
97324
|
-
}
|
|
97325
|
-
}
|
|
97326
|
-
async function fetch3(args) {
|
|
97327
|
-
const parts = ["git fetch"];
|
|
97328
|
-
if (args.prune) parts.push("--prune");
|
|
97329
|
-
if (args.all) parts.push("--all");
|
|
97330
|
-
await exec2(parts.join(" "), { cwd: args.cwd });
|
|
97331
|
-
}
|
|
97332
|
-
async function pullFrom(args) {
|
|
97333
|
-
await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
|
|
97334
|
-
cwd: args.cwd
|
|
97335
|
-
});
|
|
97336
|
-
}
|
|
97337
|
-
async function pushTo(args) {
|
|
97338
|
-
await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
|
|
97339
|
-
cwd: args.cwd
|
|
97340
|
-
});
|
|
97341
|
-
}
|
|
97342
|
-
async function sync(args) {
|
|
97343
|
-
await exec2("git pull", { cwd: args.cwd });
|
|
97344
|
-
await exec2("git push", { cwd: args.cwd });
|
|
97345
|
-
}
|
|
97346
|
-
|
|
97347
97413
|
// scripts/git/branch-tag.ts
|
|
97348
97414
|
init_cjs_shims();
|
|
97349
97415
|
var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["production", "main", "master"]);
|
|
@@ -97749,6 +97815,7 @@ function withProxyEnv(base = {}) {
|
|
|
97749
97815
|
createWorktree,
|
|
97750
97816
|
cutBackup,
|
|
97751
97817
|
databricksAuthPrereqMessage,
|
|
97818
|
+
defaultRefreshPromoteWorkflows,
|
|
97752
97819
|
delay,
|
|
97753
97820
|
deleteAppEndpoint,
|
|
97754
97821
|
deleteBranch,
|