@databricks-solutions/lakebase-scm-utils 0.2.44 → 0.2.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/dist/scripts/index.cjs +651 -605
- package/dist/scripts/index.d.cts +1 -1
- package/dist/scripts/index.d.ts +1 -1
- package/dist/scripts/index.js +650 -605
- package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +12 -3
- package/dist/scripts/lakebase/ci-resolve-branch.cli.js +12 -3
- package/dist/scripts/lakebase/collapse-heads.cli.cjs +12 -3
- package/dist/scripts/lakebase/collapse-heads.cli.js +12 -3
- package/dist/scripts/lakebase/detect-language.cli.cjs +12 -3
- package/dist/scripts/lakebase/detect-language.cli.js +12 -3
- package/dist/scripts/lakebase/doctor.cli.cjs +13 -4
- package/dist/scripts/lakebase/doctor.cli.js +13 -4
- package/dist/scripts/lakebase/get-connection.cli.cjs +12 -3
- package/dist/scripts/lakebase/get-connection.cli.js +12 -3
- package/dist/scripts/lakebase/index.cjs +472 -395
- package/dist/scripts/lakebase/index.d.cts +33 -12
- package/dist/scripts/lakebase/index.d.ts +33 -12
- package/dist/scripts/lakebase/index.js +471 -395
- package/dist/scripts/lakebase/infra-runner.cli.cjs +12 -3
- package/dist/scripts/lakebase/infra-runner.cli.js +12 -3
- package/dist/scripts/lakebase/new-migration.cli.cjs +12 -3
- package/dist/scripts/lakebase/new-migration.cli.js +12 -3
- package/dist/scripts/lakebase/schema-diff.cli.cjs +12 -3
- package/dist/scripts/lakebase/schema-diff.cli.js +12 -3
- package/dist/scripts/lakebase/schema-migrate.cli.cjs +12 -3
- package/dist/scripts/lakebase/schema-migrate.cli.js +12 -3
- package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +12 -3
- package/dist/scripts/lakebase/scm-claim-feature.cli.js +12 -3
- package/dist/scripts/lakebase/scm-doctor.cli.cjs +12 -3
- package/dist/scripts/lakebase/scm-doctor.cli.js +12 -3
- package/dist/scripts/lakebase/scm-merge.cli.cjs +2294 -2034
- package/dist/scripts/lakebase/scm-merge.cli.js +349 -89
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +12 -3
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +12 -3
- package/dist/scripts/util/index.cjs +2 -2
- package/dist/scripts/util/index.js +2 -2
- package/package.json +1 -1
- package/scripts/lakebase/constants.ts +23 -5
- package/scripts/lakebase/doctor.ts +1 -1
- package/scripts/lakebase/get-connection.ts +17 -3
- package/scripts/lakebase/scm-merge.ts +80 -1
- package/skills/lakebase-scm-workflows/README.md +2 -1
- package/templates/project/nodejs/package.json +3 -1
- package/templates/project/nodejs/src/index.js +8 -0
- package/templates/project/nodejs/src/openapi.js +20 -0
- package/templates/project/nodejs/src/routes/health.js +18 -0
- package/templates/project/nodejs/tests/app.test.js +16 -0
package/dist/scripts/index.js
CHANGED
|
@@ -9708,6 +9708,7 @@ var RUNTIME_ARTIFACT_IGNORE = [
|
|
|
9708
9708
|
var DEFAULT_ENDPOINT = "primary";
|
|
9709
9709
|
var CONSORT_APPLICATION_NAME = "consort";
|
|
9710
9710
|
var SCM_UTILS_APPLICATION_NAME = "scm-utils";
|
|
9711
|
+
var SCM_CLIENT_ENV = "LAKEBASE_SCM_CLIENT";
|
|
9711
9712
|
var CONSORT_VERSION_ENV = "CONSORT_VERSION";
|
|
9712
9713
|
|
|
9713
9714
|
// scripts/lakebase/self-version.ts
|
|
@@ -9719,8 +9720,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
9719
9720
|
var cached;
|
|
9720
9721
|
function substrateSelfVersion() {
|
|
9721
9722
|
if (cached !== void 0) return cached;
|
|
9722
|
-
if ("0.2.
|
|
9723
|
-
cached = "0.2.
|
|
9723
|
+
if ("0.2.46".length > 0) {
|
|
9724
|
+
cached = "0.2.46";
|
|
9724
9725
|
return cached;
|
|
9725
9726
|
}
|
|
9726
9727
|
cached = "unknown";
|
|
@@ -9748,9 +9749,17 @@ function substrateSelfVersion() {
|
|
|
9748
9749
|
}
|
|
9749
9750
|
|
|
9750
9751
|
// scripts/lakebase/get-connection.ts
|
|
9752
|
+
function sanitizeApplicationName(raw) {
|
|
9753
|
+
const cleaned = raw.replace(/[^\x21-\x7e]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9754
|
+
if (!cleaned) return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
|
|
9755
|
+
return cleaned.length > 63 ? cleaned.slice(0, 63) : cleaned;
|
|
9756
|
+
}
|
|
9751
9757
|
function connectionApplicationName() {
|
|
9758
|
+
const client = process.env[SCM_CLIENT_ENV]?.trim();
|
|
9759
|
+
if (client) return sanitizeApplicationName(client);
|
|
9752
9760
|
const consortVersion = process.env[CONSORT_VERSION_ENV]?.trim();
|
|
9753
|
-
|
|
9761
|
+
if (consortVersion) return `${CONSORT_APPLICATION_NAME}/${consortVersion}`;
|
|
9762
|
+
return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
|
|
9754
9763
|
}
|
|
9755
9764
|
async function getConnection(args) {
|
|
9756
9765
|
const endpointName = args.endpointName ?? DEFAULT_ENDPOINT;
|
|
@@ -16021,122 +16030,499 @@ function pickRunUrl(pr) {
|
|
|
16021
16030
|
|
|
16022
16031
|
// scripts/lakebase/scm-merge.ts
|
|
16023
16032
|
init_esm_shims();
|
|
16024
|
-
|
|
16025
|
-
|
|
16033
|
+
|
|
16034
|
+
// scripts/git/sync.ts
|
|
16035
|
+
init_esm_shims();
|
|
16036
|
+
async function currentBranchName(cwd) {
|
|
16026
16037
|
try {
|
|
16027
|
-
|
|
16028
|
-
|
|
16029
|
-
|
|
16030
|
-
} else {
|
|
16031
|
-
await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
|
|
16032
|
-
}
|
|
16033
|
-
return null;
|
|
16034
|
-
} catch (err) {
|
|
16035
|
-
return `local reconcile of ${tier} to origin/${tier} failed: ${err instanceof Error ? err.message : String(err)}. Reconcile it by hand (git checkout ${tier} && git pull --ff-only).`;
|
|
16038
|
+
return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
16039
|
+
} catch {
|
|
16040
|
+
return "";
|
|
16036
16041
|
}
|
|
16037
16042
|
}
|
|
16038
|
-
|
|
16039
|
-
|
|
16040
|
-
super(message);
|
|
16041
|
-
this.code = code;
|
|
16042
|
-
this.name = "ScmMergeError";
|
|
16043
|
-
}
|
|
16044
|
-
code;
|
|
16045
|
-
};
|
|
16046
|
-
var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
16047
|
-
var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
|
|
16048
|
-
function defaultMigratePredicate(run, mergedAt) {
|
|
16049
|
-
if (!run.createdAt) return false;
|
|
16050
|
-
const created = Date.parse(run.createdAt);
|
|
16051
|
-
if (!Number.isFinite(created)) return false;
|
|
16052
|
-
if (run.event && run.event !== "push") return false;
|
|
16053
|
-
return created >= mergedAt.getTime() - 5e3;
|
|
16043
|
+
async function push(args) {
|
|
16044
|
+
await exec2("git push", { cwd: args.cwd });
|
|
16054
16045
|
}
|
|
16055
|
-
function
|
|
16056
|
-
|
|
16057
|
-
if (run.event && run.event !== "push") return false;
|
|
16058
|
-
return !!run.headSha && run.headSha === mergeCommitSha;
|
|
16059
|
-
};
|
|
16046
|
+
async function pull(args) {
|
|
16047
|
+
await exec2("git pull", { cwd: args.cwd });
|
|
16060
16048
|
}
|
|
16061
|
-
async function
|
|
16062
|
-
const
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
16066
|
-
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
16049
|
+
async function publishBranch(args) {
|
|
16050
|
+
const remote = args.remote ?? "origin";
|
|
16051
|
+
const branch = await currentBranchName(args.cwd);
|
|
16052
|
+
if (!branch) throw new Error("No current branch");
|
|
16053
|
+
await exec2(`git push -u ${remote} ${shq(branch)}`, {
|
|
16054
|
+
cwd: args.cwd
|
|
16055
|
+
});
|
|
16056
|
+
}
|
|
16057
|
+
async function pushCurrentBranchForPr(args) {
|
|
16058
|
+
const remote = args.remote ?? "origin";
|
|
16059
|
+
const branch = await currentBranchName(args.cwd);
|
|
16060
|
+
if (!branch) throw new Error("No current branch");
|
|
16061
|
+
const upstreamSet = await hasUpstream({ cwd: args.cwd });
|
|
16062
|
+
if (!upstreamSet) {
|
|
16063
|
+
await exec2(`git push -u ${remote} ${shq(branch)}`, {
|
|
16064
|
+
cwd: args.cwd
|
|
16065
|
+
});
|
|
16066
|
+
} else {
|
|
16067
|
+
await exec2("git push", { cwd: args.cwd });
|
|
16074
16068
|
}
|
|
16075
|
-
|
|
16076
|
-
|
|
16077
|
-
|
|
16078
|
-
|
|
16069
|
+
}
|
|
16070
|
+
async function fetch2(args) {
|
|
16071
|
+
const parts = ["git fetch"];
|
|
16072
|
+
if (args.prune) parts.push("--prune");
|
|
16073
|
+
if (args.all) parts.push("--all");
|
|
16074
|
+
await exec2(parts.join(" "), { cwd: args.cwd });
|
|
16075
|
+
}
|
|
16076
|
+
async function pullFrom(args) {
|
|
16077
|
+
await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
|
|
16078
|
+
cwd: args.cwd
|
|
16079
|
+
});
|
|
16080
|
+
}
|
|
16081
|
+
async function pushTo(args) {
|
|
16082
|
+
await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
|
|
16083
|
+
cwd: args.cwd
|
|
16084
|
+
});
|
|
16085
|
+
}
|
|
16086
|
+
async function sync(args) {
|
|
16087
|
+
await exec2("git pull", { cwd: args.cwd });
|
|
16088
|
+
await exec2("git push", { cwd: args.cwd });
|
|
16089
|
+
}
|
|
16090
|
+
|
|
16091
|
+
// scripts/lakebase/workflow-drift.ts
|
|
16092
|
+
init_esm_shims();
|
|
16093
|
+
import * as fs29 from "fs";
|
|
16094
|
+
import * as path29 from "path";
|
|
16095
|
+
function findKitTemplatesDir(start) {
|
|
16096
|
+
let dir = start;
|
|
16097
|
+
for (let i = 0; i < 6; i++) {
|
|
16098
|
+
const candidate = path29.join(
|
|
16099
|
+
dir,
|
|
16100
|
+
"templates",
|
|
16101
|
+
"project",
|
|
16102
|
+
"common",
|
|
16103
|
+
".github",
|
|
16104
|
+
"workflows"
|
|
16079
16105
|
);
|
|
16106
|
+
if (fs29.existsSync(candidate)) return candidate;
|
|
16107
|
+
const parent = path29.dirname(dir);
|
|
16108
|
+
if (parent === dir) break;
|
|
16109
|
+
dir = parent;
|
|
16080
16110
|
}
|
|
16081
|
-
|
|
16082
|
-
|
|
16083
|
-
|
|
16084
|
-
|
|
16085
|
-
|
|
16111
|
+
throw new Error(
|
|
16112
|
+
`Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
|
|
16113
|
+
);
|
|
16114
|
+
}
|
|
16115
|
+
function unifiedDiff(name, projectContent, templateContent) {
|
|
16116
|
+
if (projectContent === templateContent) return "";
|
|
16117
|
+
const a = projectContent.split("\n");
|
|
16118
|
+
const b = templateContent.split("\n");
|
|
16119
|
+
const out = [`--- project/${name}`, `+++ template/${name}`];
|
|
16120
|
+
const max = Math.max(a.length, b.length);
|
|
16121
|
+
for (let i = 0; i < max; i++) {
|
|
16122
|
+
const av = a[i];
|
|
16123
|
+
const bv = b[i];
|
|
16124
|
+
if (av === bv) continue;
|
|
16125
|
+
if (av !== void 0) out.push(`-${i + 1}: ${av}`);
|
|
16126
|
+
if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
|
|
16086
16127
|
}
|
|
16087
|
-
|
|
16088
|
-
|
|
16089
|
-
|
|
16090
|
-
|
|
16091
|
-
|
|
16092
|
-
|
|
16128
|
+
return out.join("\n");
|
|
16129
|
+
}
|
|
16130
|
+
function detectWorkflowDrift(args) {
|
|
16131
|
+
const projectWorkflowsDir = path29.join(
|
|
16132
|
+
args.projectDir,
|
|
16133
|
+
".github",
|
|
16134
|
+
"workflows"
|
|
16135
|
+
);
|
|
16136
|
+
const here = path29.dirname(new URL(import.meta.url).pathname);
|
|
16137
|
+
const kitWorkflowsDir = args.kitDir ? path29.join(
|
|
16138
|
+
args.kitDir,
|
|
16139
|
+
"templates",
|
|
16140
|
+
"project",
|
|
16141
|
+
"common",
|
|
16142
|
+
".github",
|
|
16143
|
+
"workflows"
|
|
16144
|
+
) : findKitTemplatesDir(here);
|
|
16145
|
+
const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
|
|
16146
|
+
const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
|
|
16147
|
+
const version = readKitVersion(kitWorkflowsDir);
|
|
16148
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16149
|
+
const files = [];
|
|
16150
|
+
for (const name of templateFiles) {
|
|
16151
|
+
seen.add(name);
|
|
16152
|
+
const projectPath2 = path29.join(projectWorkflowsDir, name);
|
|
16153
|
+
const templatePath = path29.join(kitWorkflowsDir, name);
|
|
16154
|
+
if (!fs29.existsSync(projectPath2)) {
|
|
16155
|
+
files.push({ name, status: "missing" });
|
|
16156
|
+
continue;
|
|
16157
|
+
}
|
|
16158
|
+
const projectContent = fs29.readFileSync(projectPath2, "utf8");
|
|
16159
|
+
const templateContent = applyPlaceholders(fs29.readFileSync(templatePath, "utf8"), version);
|
|
16160
|
+
if (projectContent === templateContent) {
|
|
16161
|
+
files.push({ name, status: "unchanged" });
|
|
16162
|
+
} else {
|
|
16163
|
+
files.push({
|
|
16164
|
+
name,
|
|
16165
|
+
status: "drifted",
|
|
16166
|
+
diff: unifiedDiff(name, projectContent, templateContent)
|
|
16167
|
+
});
|
|
16168
|
+
}
|
|
16093
16169
|
}
|
|
16094
|
-
const
|
|
16095
|
-
|
|
16096
|
-
|
|
16097
|
-
`Could not extract PR number from URL: ${current.pr_url}`,
|
|
16098
|
-
"bad-pr-url"
|
|
16099
|
-
);
|
|
16170
|
+
for (const name of projectFiles) {
|
|
16171
|
+
if (seen.has(name)) continue;
|
|
16172
|
+
files.push({ name, status: "extra" });
|
|
16100
16173
|
}
|
|
16101
|
-
const
|
|
16102
|
-
|
|
16103
|
-
|
|
16104
|
-
|
|
16105
|
-
|
|
16106
|
-
|
|
16107
|
-
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
|
|
16112
|
-
|
|
16113
|
-
|
|
16174
|
+
const order = {
|
|
16175
|
+
drifted: 0,
|
|
16176
|
+
missing: 1,
|
|
16177
|
+
extra: 2,
|
|
16178
|
+
unchanged: 3
|
|
16179
|
+
};
|
|
16180
|
+
files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
|
|
16181
|
+
const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
|
|
16182
|
+
return {
|
|
16183
|
+
overall: hasDrift ? "drift" : "ok",
|
|
16184
|
+
files
|
|
16185
|
+
};
|
|
16186
|
+
}
|
|
16187
|
+
function readKitVersion(kitWorkflowsDir) {
|
|
16188
|
+
let dir = kitWorkflowsDir;
|
|
16189
|
+
for (let i = 0; i < 5; i++) {
|
|
16190
|
+
dir = path29.dirname(dir);
|
|
16114
16191
|
}
|
|
16115
|
-
let paired;
|
|
16116
16192
|
try {
|
|
16117
|
-
|
|
16118
|
-
|
|
16119
|
-
|
|
16120
|
-
|
|
16121
|
-
|
|
16122
|
-
|
|
16123
|
-
|
|
16124
|
-
|
|
16125
|
-
|
|
16126
|
-
|
|
16193
|
+
const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
|
|
16194
|
+
const pkg = JSON.parse(raw);
|
|
16195
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
16196
|
+
} catch {
|
|
16197
|
+
return "unknown";
|
|
16198
|
+
}
|
|
16199
|
+
}
|
|
16200
|
+
function applyPlaceholders(content, version) {
|
|
16201
|
+
return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
|
|
16202
|
+
}
|
|
16203
|
+
function applyCommandPlaceholders(content, version) {
|
|
16204
|
+
return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
|
|
16205
|
+
}
|
|
16206
|
+
var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
|
|
16207
|
+
function findKitCommandsDir(start) {
|
|
16208
|
+
let dir = start;
|
|
16209
|
+
for (let i = 0; i < 6; i++) {
|
|
16210
|
+
const candidate = path29.join(
|
|
16211
|
+
dir,
|
|
16212
|
+
"templates",
|
|
16213
|
+
"project",
|
|
16214
|
+
"common",
|
|
16215
|
+
".claude",
|
|
16216
|
+
"commands"
|
|
16127
16217
|
);
|
|
16218
|
+
if (fs29.existsSync(candidate)) return candidate;
|
|
16219
|
+
const parent = path29.dirname(dir);
|
|
16220
|
+
if (parent === dir) break;
|
|
16221
|
+
dir = parent;
|
|
16128
16222
|
}
|
|
16129
|
-
|
|
16130
|
-
|
|
16131
|
-
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
|
|
16137
|
-
|
|
16138
|
-
|
|
16139
|
-
|
|
16223
|
+
throw new Error(
|
|
16224
|
+
`Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
|
|
16225
|
+
);
|
|
16226
|
+
}
|
|
16227
|
+
function parsePinnedVersion(content) {
|
|
16228
|
+
const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
|
|
16229
|
+
return m ? m[1] : void 0;
|
|
16230
|
+
}
|
|
16231
|
+
function detectCommandDrift(args) {
|
|
16232
|
+
const projectCommandsDir = path29.join(args.projectDir, ".claude", "commands");
|
|
16233
|
+
const here = path29.dirname(new URL(import.meta.url).pathname);
|
|
16234
|
+
const kitCommandsDir = args.kitDir ? path29.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
|
|
16235
|
+
const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
|
|
16236
|
+
const templateFiles = fs29.existsSync(kitCommandsDir) ? fs29.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
|
|
16237
|
+
const projectFiles = fs29.existsSync(projectCommandsDir) ? fs29.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
|
|
16238
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16239
|
+
const files = [];
|
|
16240
|
+
for (const name of templateFiles) {
|
|
16241
|
+
seen.add(name);
|
|
16242
|
+
const projectPath2 = path29.join(projectCommandsDir, name);
|
|
16243
|
+
const templatePath = path29.join(kitCommandsDir, name);
|
|
16244
|
+
const templateRaw = fs29.readFileSync(templatePath, "utf8");
|
|
16245
|
+
if (!fs29.existsSync(projectPath2)) {
|
|
16246
|
+
files.push({ name, status: "missing", kit_version: kitVersion });
|
|
16247
|
+
continue;
|
|
16248
|
+
}
|
|
16249
|
+
const projectContent = fs29.readFileSync(projectPath2, "utf8");
|
|
16250
|
+
const pinned = parsePinnedVersion(projectContent);
|
|
16251
|
+
const versionForCompare = pinned ?? kitVersion;
|
|
16252
|
+
const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
|
|
16253
|
+
if (projectContent === templateContent) {
|
|
16254
|
+
files.push({
|
|
16255
|
+
name,
|
|
16256
|
+
status: "unchanged",
|
|
16257
|
+
pinned_version: pinned,
|
|
16258
|
+
kit_version: kitVersion
|
|
16259
|
+
});
|
|
16260
|
+
} else {
|
|
16261
|
+
files.push({
|
|
16262
|
+
name,
|
|
16263
|
+
status: "drifted",
|
|
16264
|
+
pinned_version: pinned,
|
|
16265
|
+
kit_version: kitVersion,
|
|
16266
|
+
diff: unifiedDiff(name, projectContent, templateContent)
|
|
16267
|
+
});
|
|
16268
|
+
}
|
|
16269
|
+
}
|
|
16270
|
+
for (const name of projectFiles) {
|
|
16271
|
+
if (seen.has(name)) continue;
|
|
16272
|
+
files.push({ name, status: "extra", kit_version: kitVersion });
|
|
16273
|
+
}
|
|
16274
|
+
const order = {
|
|
16275
|
+
drifted: 0,
|
|
16276
|
+
missing: 1,
|
|
16277
|
+
extra: 2,
|
|
16278
|
+
unchanged: 3
|
|
16279
|
+
};
|
|
16280
|
+
files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
|
|
16281
|
+
const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
|
|
16282
|
+
return { overall: hasDrift ? "drift" : "ok", files };
|
|
16283
|
+
}
|
|
16284
|
+
function readKitVersionFromCommandsDir(kitCommandsDir) {
|
|
16285
|
+
let dir = kitCommandsDir;
|
|
16286
|
+
for (let i = 0; i < 5; i++) {
|
|
16287
|
+
dir = path29.dirname(dir);
|
|
16288
|
+
}
|
|
16289
|
+
try {
|
|
16290
|
+
const raw = fs29.readFileSync(path29.join(dir, "package.json"), "utf-8");
|
|
16291
|
+
const pkg = JSON.parse(raw);
|
|
16292
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
16293
|
+
} catch {
|
|
16294
|
+
return "unknown";
|
|
16295
|
+
}
|
|
16296
|
+
}
|
|
16297
|
+
function detectScaffoldedDrift(args) {
|
|
16298
|
+
const workflows = detectWorkflowDrift(args);
|
|
16299
|
+
const commands = detectCommandDrift(args);
|
|
16300
|
+
return {
|
|
16301
|
+
overall: workflows.overall === "drift" || commands.overall === "drift" ? "drift" : "ok",
|
|
16302
|
+
workflows,
|
|
16303
|
+
commands
|
|
16304
|
+
};
|
|
16305
|
+
}
|
|
16306
|
+
function updateWorkflows(args) {
|
|
16307
|
+
const projectWorkflowsDir = path29.join(
|
|
16308
|
+
args.projectDir,
|
|
16309
|
+
".github",
|
|
16310
|
+
"workflows"
|
|
16311
|
+
);
|
|
16312
|
+
const here = path29.dirname(new URL(import.meta.url).pathname);
|
|
16313
|
+
const kitWorkflowsDir = args.kitDir ? path29.join(
|
|
16314
|
+
args.kitDir,
|
|
16315
|
+
"templates",
|
|
16316
|
+
"project",
|
|
16317
|
+
"common",
|
|
16318
|
+
".github",
|
|
16319
|
+
"workflows"
|
|
16320
|
+
) : findKitTemplatesDir(here);
|
|
16321
|
+
const substitute = args.substitute !== false;
|
|
16322
|
+
const dryRun = args.dryRun === true;
|
|
16323
|
+
const pruneExtras = args.pruneExtras === true;
|
|
16324
|
+
const templateFiles = fs29.existsSync(kitWorkflowsDir) ? fs29.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
|
|
16325
|
+
const projectFiles = fs29.existsSync(projectWorkflowsDir) ? fs29.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
|
|
16326
|
+
if (!dryRun && templateFiles.length > 0 && !fs29.existsSync(projectWorkflowsDir)) {
|
|
16327
|
+
fs29.mkdirSync(projectWorkflowsDir, { recursive: true });
|
|
16328
|
+
}
|
|
16329
|
+
const version = substitute ? readKitVersion(kitWorkflowsDir) : "";
|
|
16330
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16331
|
+
const files = [];
|
|
16332
|
+
for (const name of templateFiles) {
|
|
16333
|
+
seen.add(name);
|
|
16334
|
+
const projectPath2 = path29.join(projectWorkflowsDir, name);
|
|
16335
|
+
const templatePath = path29.join(kitWorkflowsDir, name);
|
|
16336
|
+
const templateRaw = fs29.readFileSync(templatePath, "utf-8");
|
|
16337
|
+
const desired = substitute ? applyPlaceholders(templateRaw, version) : templateRaw;
|
|
16338
|
+
const existed = fs29.existsSync(projectPath2);
|
|
16339
|
+
const current = existed ? fs29.readFileSync(projectPath2, "utf-8") : "";
|
|
16340
|
+
let outcome;
|
|
16341
|
+
if (!existed) {
|
|
16342
|
+
outcome = "added";
|
|
16343
|
+
} else if (current === desired) {
|
|
16344
|
+
outcome = "unchanged";
|
|
16345
|
+
} else {
|
|
16346
|
+
outcome = "updated";
|
|
16347
|
+
}
|
|
16348
|
+
if (!dryRun && outcome !== "unchanged") {
|
|
16349
|
+
fs29.writeFileSync(projectPath2, desired);
|
|
16350
|
+
}
|
|
16351
|
+
files.push({ name, outcome });
|
|
16352
|
+
}
|
|
16353
|
+
if (pruneExtras) {
|
|
16354
|
+
for (const name of projectFiles) {
|
|
16355
|
+
if (seen.has(name)) continue;
|
|
16356
|
+
const projectPath2 = path29.join(projectWorkflowsDir, name);
|
|
16357
|
+
if (!dryRun) {
|
|
16358
|
+
fs29.unlinkSync(projectPath2);
|
|
16359
|
+
}
|
|
16360
|
+
files.push({ name, outcome: "removed" });
|
|
16361
|
+
}
|
|
16362
|
+
}
|
|
16363
|
+
const order = {
|
|
16364
|
+
added: 0,
|
|
16365
|
+
updated: 1,
|
|
16366
|
+
removed: 2,
|
|
16367
|
+
unchanged: 3
|
|
16368
|
+
};
|
|
16369
|
+
files.sort((a, b) => order[a.outcome] - order[b.outcome] || a.name.localeCompare(b.name));
|
|
16370
|
+
const changed = files.some((f) => f.outcome !== "unchanged");
|
|
16371
|
+
return { files, changed };
|
|
16372
|
+
}
|
|
16373
|
+
|
|
16374
|
+
// scripts/lakebase/scm-merge.ts
|
|
16375
|
+
async function reconcileTierToOrigin(args) {
|
|
16376
|
+
const { cwd, tier } = args;
|
|
16377
|
+
try {
|
|
16378
|
+
const head = await getCurrentBranch({ cwd });
|
|
16379
|
+
if (head === tier) {
|
|
16380
|
+
await exec2(`git pull --ff-only`, { cwd, timeout: 3e4 });
|
|
16381
|
+
} else {
|
|
16382
|
+
await exec2(`git fetch origin ${shellEscape2(`${tier}:${tier}`)}`, { cwd, timeout: 3e4 });
|
|
16383
|
+
}
|
|
16384
|
+
return null;
|
|
16385
|
+
} catch (err) {
|
|
16386
|
+
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).`;
|
|
16387
|
+
}
|
|
16388
|
+
}
|
|
16389
|
+
var ScmMergeError = class extends Error {
|
|
16390
|
+
constructor(message, code) {
|
|
16391
|
+
super(message);
|
|
16392
|
+
this.code = code;
|
|
16393
|
+
this.name = "ScmMergeError";
|
|
16394
|
+
}
|
|
16395
|
+
code;
|
|
16396
|
+
};
|
|
16397
|
+
var DEFAULT_MIGRATE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
16398
|
+
var DEFAULT_MIGRATE_POLL_MS = 30 * 1e3;
|
|
16399
|
+
function defaultMigratePredicate(run, mergedAt) {
|
|
16400
|
+
if (!run.createdAt) return false;
|
|
16401
|
+
const created = Date.parse(run.createdAt);
|
|
16402
|
+
if (!Number.isFinite(created)) return false;
|
|
16403
|
+
if (run.event && run.event !== "push") return false;
|
|
16404
|
+
return created >= mergedAt.getTime() - 5e3;
|
|
16405
|
+
}
|
|
16406
|
+
function shaMigratePredicate(mergeCommitSha) {
|
|
16407
|
+
return (run) => {
|
|
16408
|
+
if (run.event && run.event !== "push") return false;
|
|
16409
|
+
return !!run.headSha && run.headSha === mergeCommitSha;
|
|
16410
|
+
};
|
|
16411
|
+
}
|
|
16412
|
+
async function defaultRefreshPromoteWorkflows(projectDir) {
|
|
16413
|
+
const drift = detectWorkflowDrift({ projectDir });
|
|
16414
|
+
const touched = drift.files.filter((f) => f.status === "drifted").map((f) => f.name);
|
|
16415
|
+
if (touched.length === 0) return { refreshed: false };
|
|
16416
|
+
const result = updateWorkflows({ projectDir });
|
|
16417
|
+
if (!result.changed) return { refreshed: false };
|
|
16418
|
+
await exec2(`git add ${shellEscape2(".github/workflows")}`, { cwd: projectDir, timeout: 1e4 });
|
|
16419
|
+
const staged = await exec2("git diff --cached --name-only -- .github/workflows", {
|
|
16420
|
+
cwd: projectDir,
|
|
16421
|
+
timeout: 1e4
|
|
16422
|
+
});
|
|
16423
|
+
if (!staged.trim()) return { refreshed: false };
|
|
16424
|
+
await exec2(
|
|
16425
|
+
`git commit -m ${shellEscape2(
|
|
16426
|
+
"chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)"
|
|
16427
|
+
)} -- .github/workflows`,
|
|
16428
|
+
{ cwd: projectDir, timeout: 15e3 }
|
|
16429
|
+
);
|
|
16430
|
+
await pushCurrentBranchForPr({ cwd: projectDir });
|
|
16431
|
+
return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
|
|
16432
|
+
}
|
|
16433
|
+
async function mergeFeature(args) {
|
|
16434
|
+
const current = readWorkflowState(args.projectDir);
|
|
16435
|
+
if (!current) {
|
|
16436
|
+
throw new ScmMergeError(
|
|
16437
|
+
"No SCM workflow state. wait-ci first.",
|
|
16438
|
+
"no-state-file"
|
|
16439
|
+
);
|
|
16440
|
+
}
|
|
16441
|
+
if (current.state !== "ci-green") {
|
|
16442
|
+
throw new ScmMergeError(
|
|
16443
|
+
`merge refuses state "${current.state}". Allowed predecessor: ci-green.`,
|
|
16444
|
+
"bad-precondition"
|
|
16445
|
+
);
|
|
16446
|
+
}
|
|
16447
|
+
if (!current.pr_url) {
|
|
16448
|
+
throw new ScmMergeError(
|
|
16449
|
+
"ci-green row is missing pr_url; cannot resolve the PR to merge.",
|
|
16450
|
+
"no-pr-url"
|
|
16451
|
+
);
|
|
16452
|
+
}
|
|
16453
|
+
if (!current.branch || !current.parent_branch) {
|
|
16454
|
+
throw new ScmMergeError(
|
|
16455
|
+
"ci-green row missing branch / parent_branch; refusing to merge.",
|
|
16456
|
+
"bad-precondition"
|
|
16457
|
+
);
|
|
16458
|
+
}
|
|
16459
|
+
const ownerRepo = await getOwnerRepo(args.projectDir);
|
|
16460
|
+
if (!ownerRepo) {
|
|
16461
|
+
throw new ScmMergeError(
|
|
16462
|
+
"No GitHub remote found at origin.",
|
|
16463
|
+
"no-github-remote"
|
|
16464
|
+
);
|
|
16465
|
+
}
|
|
16466
|
+
const pullNumber = extractPullNumber(current.pr_url);
|
|
16467
|
+
if (!pullNumber) {
|
|
16468
|
+
throw new ScmMergeError(
|
|
16469
|
+
`Could not extract PR number from URL: ${current.pr_url}`,
|
|
16470
|
+
"bad-pr-url"
|
|
16471
|
+
);
|
|
16472
|
+
}
|
|
16473
|
+
const instance = args.instance ?? current.project_id;
|
|
16474
|
+
const gitBase = await resolveGitBase(current.parent_branch, args.projectDir);
|
|
16475
|
+
const wantMigrate = args.waitMigrate !== false;
|
|
16476
|
+
let authVerified = false;
|
|
16477
|
+
if (wantMigrate && args.verifyMigrateAuth) {
|
|
16478
|
+
const probe = await args.verifyMigrateAuth();
|
|
16479
|
+
if (!probe.ok) {
|
|
16480
|
+
throw new ScmMergeError(
|
|
16481
|
+
`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.`,
|
|
16482
|
+
"migrate-auth"
|
|
16483
|
+
);
|
|
16484
|
+
}
|
|
16485
|
+
authVerified = true;
|
|
16486
|
+
}
|
|
16487
|
+
const preflightNotes = [];
|
|
16488
|
+
{
|
|
16489
|
+
const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
|
|
16490
|
+
try {
|
|
16491
|
+
const r = await refresh();
|
|
16492
|
+
if (r.refreshed) {
|
|
16493
|
+
preflightNotes.push(`Refreshed stale CI workflows before merge${r.detail ? ` (${r.detail})` : ""}.`);
|
|
16494
|
+
}
|
|
16495
|
+
} catch (err) {
|
|
16496
|
+
preflightNotes.push(
|
|
16497
|
+
`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').`
|
|
16498
|
+
);
|
|
16499
|
+
}
|
|
16500
|
+
}
|
|
16501
|
+
let paired;
|
|
16502
|
+
try {
|
|
16503
|
+
paired = await mergePairedPullRequest({
|
|
16504
|
+
ownerRepo,
|
|
16505
|
+
pullNumber,
|
|
16506
|
+
lakebaseInstance: instance,
|
|
16507
|
+
method: args.method ?? "squash"
|
|
16508
|
+
});
|
|
16509
|
+
} catch (err) {
|
|
16510
|
+
throw new ScmMergeError(
|
|
16511
|
+
`mergePairedPullRequest failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
16512
|
+
"merge-failed"
|
|
16513
|
+
);
|
|
16514
|
+
}
|
|
16515
|
+
const warnings = [...preflightNotes, ...paired.warnings];
|
|
16516
|
+
let localBranchDeleted = false;
|
|
16517
|
+
let headAfter = current.branch;
|
|
16518
|
+
if (!args.skipLocalCleanup) {
|
|
16519
|
+
const switchTo = args.switchTo ?? gitBase;
|
|
16520
|
+
const head = await getCurrentBranch({ cwd: args.projectDir });
|
|
16521
|
+
if (head === current.branch) {
|
|
16522
|
+
try {
|
|
16523
|
+
await exec2(`git checkout ${shellEscape2(switchTo)}`, {
|
|
16524
|
+
cwd: args.projectDir,
|
|
16525
|
+
timeout: 1e4
|
|
16140
16526
|
});
|
|
16141
16527
|
headAfter = switchTo;
|
|
16142
16528
|
try {
|
|
@@ -16323,7 +16709,7 @@ init_esm_shims();
|
|
|
16323
16709
|
|
|
16324
16710
|
// scripts/git/branches.ts
|
|
16325
16711
|
init_esm_shims();
|
|
16326
|
-
async function
|
|
16712
|
+
async function currentBranchName2(cwd) {
|
|
16327
16713
|
try {
|
|
16328
16714
|
return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
16329
16715
|
} catch {
|
|
@@ -16342,7 +16728,7 @@ async function listLocalBranches(args) {
|
|
|
16342
16728
|
return [];
|
|
16343
16729
|
}
|
|
16344
16730
|
if (!raw) return [];
|
|
16345
|
-
const current = await
|
|
16731
|
+
const current = await currentBranchName2(cwd);
|
|
16346
16732
|
return raw.split("\n").filter(Boolean).map((line) => {
|
|
16347
16733
|
const [name, tracking, trackInfo] = line.split("|");
|
|
16348
16734
|
let ahead = 0;
|
|
@@ -16542,15 +16928,15 @@ function parentForTopology(t, defaultLeaf) {
|
|
|
16542
16928
|
|
|
16543
16929
|
// scripts/lakebase/scm-doctor.ts
|
|
16544
16930
|
init_esm_shims();
|
|
16545
|
-
import * as
|
|
16546
|
-
import * as
|
|
16931
|
+
import * as fs30 from "fs";
|
|
16932
|
+
import * as path30 from "path";
|
|
16547
16933
|
var FEATURE_PREFIX = "feature/";
|
|
16548
16934
|
var TIER_LEAFS2 = DEFAULT_PROTECTED_TIER_NAMES;
|
|
16549
16935
|
function readEnv(projectDir) {
|
|
16550
|
-
const envPath =
|
|
16936
|
+
const envPath = path30.join(projectDir, ".env");
|
|
16551
16937
|
const out = /* @__PURE__ */ new Map();
|
|
16552
|
-
if (!
|
|
16553
|
-
const lines =
|
|
16938
|
+
if (!fs30.existsSync(envPath)) return out;
|
|
16939
|
+
const lines = fs30.readFileSync(envPath, "utf8").split("\n");
|
|
16554
16940
|
for (const line of lines) {
|
|
16555
16941
|
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
|
|
16556
16942
|
if (m) out.set(m[1], m[2].replace(/^["']|["']$/g, ""));
|
|
@@ -16605,9 +16991,9 @@ async function runDoctor(args, deps = {}) {
|
|
|
16605
16991
|
}
|
|
16606
16992
|
for (const wf of ["pr.yml", "merge.yml"]) {
|
|
16607
16993
|
try {
|
|
16608
|
-
const p =
|
|
16609
|
-
if (!
|
|
16610
|
-
const body =
|
|
16994
|
+
const p = path30.join(projectDir, ".github", "workflows", wf);
|
|
16995
|
+
if (!fs30.existsSync(p)) continue;
|
|
16996
|
+
const body = fs30.readFileSync(p, "utf8");
|
|
16611
16997
|
if (/lakebase-scm-utils#v\d/.test(body)) {
|
|
16612
16998
|
findings.push({
|
|
16613
16999
|
id: "ci-workflow-substrate-pin",
|
|
@@ -16769,449 +17155,164 @@ async function runDoctor(args, deps = {}) {
|
|
|
16769
17155
|
workflowStatePresent,
|
|
16770
17156
|
state: state ?? void 0,
|
|
16771
17157
|
inferredTierTopology: inferredTopology,
|
|
16772
|
-
findings
|
|
16773
|
-
});
|
|
16774
|
-
}
|
|
16775
|
-
function finalize(report) {
|
|
16776
|
-
let worst = "ok";
|
|
16777
|
-
for (const f of report.findings) {
|
|
16778
|
-
worst = worstOf(worst, f.severity);
|
|
16779
|
-
}
|
|
16780
|
-
return { ...report, worstSeverity: worst };
|
|
16781
|
-
}
|
|
16782
|
-
var ScmDoctorFixError = class extends Error {
|
|
16783
|
-
constructor(message, code) {
|
|
16784
|
-
super(message);
|
|
16785
|
-
this.code = code;
|
|
16786
|
-
this.name = "ScmDoctorFixError";
|
|
16787
|
-
}
|
|
16788
|
-
code;
|
|
16789
|
-
};
|
|
16790
|
-
var FIXABLE_FINDING_IDS = [
|
|
16791
|
-
"env-branch-drift",
|
|
16792
|
-
"head-branch-drift",
|
|
16793
|
-
"tier-topology-mismatch",
|
|
16794
|
-
"orphan-current-branch",
|
|
16795
|
-
"multiple-migration-heads",
|
|
16796
|
-
"db-ahead-of-code"
|
|
16797
|
-
];
|
|
16798
|
-
async function fixFinding(args) {
|
|
16799
|
-
if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
|
|
16800
|
-
throw new ScmDoctorFixError(
|
|
16801
|
-
`Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
|
|
16802
|
-
"unsupported-finding"
|
|
16803
|
-
);
|
|
16804
|
-
}
|
|
16805
|
-
const report = args.report ?? await runDoctor({ projectDir: args.projectDir, instance: args.instance });
|
|
16806
|
-
const present = report.findings.find((f) => f.id === args.findingId);
|
|
16807
|
-
if (!present) {
|
|
16808
|
-
throw new ScmDoctorFixError(
|
|
16809
|
-
`Finding "${args.findingId}" is not present in the current report. Re-run lakebase-scm-doctor to see what needs fixing.`,
|
|
16810
|
-
"finding-not-present"
|
|
16811
|
-
);
|
|
16812
|
-
}
|
|
16813
|
-
let action = "";
|
|
16814
|
-
try {
|
|
16815
|
-
switch (args.findingId) {
|
|
16816
|
-
case "env-branch-drift": {
|
|
16817
|
-
const branch = report.state?.branch;
|
|
16818
|
-
if (!branch) {
|
|
16819
|
-
throw new ScmDoctorFixError(
|
|
16820
|
-
"Cannot fix: workflow state has no branch field.",
|
|
16821
|
-
"fix-failed"
|
|
16822
|
-
);
|
|
16823
|
-
}
|
|
16824
|
-
const sanitized = sanitizeBranchName(branch);
|
|
16825
|
-
const envFile = path29.join(args.projectDir, ".env");
|
|
16826
|
-
updateEnvConnection({
|
|
16827
|
-
envPath: envFile,
|
|
16828
|
-
projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
|
|
16829
|
-
branchId: sanitized,
|
|
16830
|
-
username: readEnvVar(envFile, "DB_USERNAME") ?? "",
|
|
16831
|
-
endpointHost: readEnvVar(envFile, "LAKEBASE_HOST")
|
|
16832
|
-
});
|
|
16833
|
-
action = `rewrote .env LAKEBASE_BRANCH_ID=${sanitized} (metadata only; the app mints its token at runtime)`;
|
|
16834
|
-
break;
|
|
16835
|
-
}
|
|
16836
|
-
case "head-branch-drift": {
|
|
16837
|
-
const branch = report.state?.branch;
|
|
16838
|
-
if (!branch) {
|
|
16839
|
-
throw new ScmDoctorFixError(
|
|
16840
|
-
"Cannot fix: workflow state has no branch field.",
|
|
16841
|
-
"fix-failed"
|
|
16842
|
-
);
|
|
16843
|
-
}
|
|
16844
|
-
await exec2(`git checkout ${shellEscape3(branch)}`, {
|
|
16845
|
-
cwd: args.projectDir,
|
|
16846
|
-
timeout: 15e3
|
|
16847
|
-
});
|
|
16848
|
-
action = `git checkout ${branch} (re-fires post-checkout to resync HEAD)`;
|
|
16849
|
-
break;
|
|
16850
|
-
}
|
|
16851
|
-
case "tier-topology-mismatch": {
|
|
16852
|
-
const instance = args.instance ?? report.state?.project_id;
|
|
16853
|
-
if (!instance) {
|
|
16854
|
-
throw new ScmDoctorFixError(
|
|
16855
|
-
"Cannot fix: missing Lakebase project id.",
|
|
16856
|
-
"fix-failed"
|
|
16857
|
-
);
|
|
16858
|
-
}
|
|
16859
|
-
await adoptScmState({
|
|
16860
|
-
projectDir: args.projectDir,
|
|
16861
|
-
instance,
|
|
16862
|
-
force: true
|
|
16863
|
-
});
|
|
16864
|
-
action = `adopted state with --force to re-infer tier_topology`;
|
|
16865
|
-
break;
|
|
16866
|
-
}
|
|
16867
|
-
case "orphan-current-branch": {
|
|
16868
|
-
const instance = args.instance ?? report.state?.project_id;
|
|
16869
|
-
if (!instance) {
|
|
16870
|
-
throw new ScmDoctorFixError(
|
|
16871
|
-
"Cannot fix: missing Lakebase project id.",
|
|
16872
|
-
"fix-failed"
|
|
16873
|
-
);
|
|
16874
|
-
}
|
|
16875
|
-
const headBranch = await getCurrentBranch({ cwd: args.projectDir });
|
|
16876
|
-
if (!headBranch) {
|
|
16877
|
-
throw new ScmDoctorFixError(
|
|
16878
|
-
"Cannot fix: detached HEAD or no current branch.",
|
|
16879
|
-
"fix-failed"
|
|
16880
|
-
);
|
|
16881
|
-
}
|
|
16882
|
-
await recoverOrphans({
|
|
16883
|
-
projectDir: args.projectDir,
|
|
16884
|
-
instance,
|
|
16885
|
-
claim: true,
|
|
16886
|
-
onlyBranch: headBranch
|
|
16887
|
-
});
|
|
16888
|
-
action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
|
|
16889
|
-
break;
|
|
16890
|
-
}
|
|
16891
|
-
case "multiple-migration-heads": {
|
|
16892
|
-
const r = await collapseMigrationHeads({ projectDir: args.projectDir });
|
|
16893
|
-
if (r.status !== "ok" || !r.mergeRevision) {
|
|
16894
|
-
throw new ScmDoctorFixError(
|
|
16895
|
-
`Expected to create a merge revision but got status="${r.status}".`,
|
|
16896
|
-
"fix-failed"
|
|
16897
|
-
);
|
|
16898
|
-
}
|
|
16899
|
-
action = `collapsed ${r.headsBefore.length} heads into merge revision ${r.mergeRevision} (commit it)`;
|
|
16900
|
-
break;
|
|
16901
|
-
}
|
|
16902
|
-
case "db-ahead-of-code": {
|
|
16903
|
-
const r = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
|
|
16904
|
-
action = `abandoned the feature (deleted the polluted paired branch${r.lakebaseDeleted ? "" : " , Lakebase delete reported not-deleted"}) and reset state to '${r.state.state}'; re-claim to re-fork a clean branch from the tier`;
|
|
16905
|
-
break;
|
|
16906
|
-
}
|
|
16907
|
-
}
|
|
16908
|
-
} catch (err) {
|
|
16909
|
-
if (err instanceof ScmDoctorFixError) throw err;
|
|
16910
|
-
throw new ScmDoctorFixError(
|
|
16911
|
-
`Remediation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
16912
|
-
"fix-failed"
|
|
16913
|
-
);
|
|
16914
|
-
}
|
|
16915
|
-
const postReport = await runDoctor({
|
|
16916
|
-
projectDir: args.projectDir,
|
|
16917
|
-
instance: args.instance
|
|
16918
|
-
});
|
|
16919
|
-
return { findingId: args.findingId, action, postReport };
|
|
16920
|
-
}
|
|
16921
|
-
function shellEscape3(s) {
|
|
16922
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
16923
|
-
}
|
|
16924
|
-
|
|
16925
|
-
// scripts/lakebase/doctor.ts
|
|
16926
|
-
init_esm_shims();
|
|
16927
|
-
import * as fs31 from "fs";
|
|
16928
|
-
import * as path31 from "path";
|
|
16929
|
-
import * as cp6 from "child_process";
|
|
16930
|
-
|
|
16931
|
-
// scripts/lakebase/workflow-drift.ts
|
|
16932
|
-
init_esm_shims();
|
|
16933
|
-
import * as fs30 from "fs";
|
|
16934
|
-
import * as path30 from "path";
|
|
16935
|
-
function findKitTemplatesDir(start) {
|
|
16936
|
-
let dir = start;
|
|
16937
|
-
for (let i = 0; i < 6; i++) {
|
|
16938
|
-
const candidate = path30.join(
|
|
16939
|
-
dir,
|
|
16940
|
-
"templates",
|
|
16941
|
-
"project",
|
|
16942
|
-
"common",
|
|
16943
|
-
".github",
|
|
16944
|
-
"workflows"
|
|
16945
|
-
);
|
|
16946
|
-
if (fs30.existsSync(candidate)) return candidate;
|
|
16947
|
-
const parent = path30.dirname(dir);
|
|
16948
|
-
if (parent === dir) break;
|
|
16949
|
-
dir = parent;
|
|
16950
|
-
}
|
|
16951
|
-
throw new Error(
|
|
16952
|
-
`Could not locate templates/project/common/.github/workflows/ relative to ${start}. Pass explicit kitDir.`
|
|
16953
|
-
);
|
|
16954
|
-
}
|
|
16955
|
-
function unifiedDiff(name, projectContent, templateContent) {
|
|
16956
|
-
if (projectContent === templateContent) return "";
|
|
16957
|
-
const a = projectContent.split("\n");
|
|
16958
|
-
const b = templateContent.split("\n");
|
|
16959
|
-
const out = [`--- project/${name}`, `+++ template/${name}`];
|
|
16960
|
-
const max = Math.max(a.length, b.length);
|
|
16961
|
-
for (let i = 0; i < max; i++) {
|
|
16962
|
-
const av = a[i];
|
|
16963
|
-
const bv = b[i];
|
|
16964
|
-
if (av === bv) continue;
|
|
16965
|
-
if (av !== void 0) out.push(`-${i + 1}: ${av}`);
|
|
16966
|
-
if (bv !== void 0) out.push(`+${i + 1}: ${bv}`);
|
|
16967
|
-
}
|
|
16968
|
-
return out.join("\n");
|
|
16969
|
-
}
|
|
16970
|
-
function detectWorkflowDrift(args) {
|
|
16971
|
-
const projectWorkflowsDir = path30.join(
|
|
16972
|
-
args.projectDir,
|
|
16973
|
-
".github",
|
|
16974
|
-
"workflows"
|
|
16975
|
-
);
|
|
16976
|
-
const here = path30.dirname(new URL(import.meta.url).pathname);
|
|
16977
|
-
const kitWorkflowsDir = args.kitDir ? path30.join(
|
|
16978
|
-
args.kitDir,
|
|
16979
|
-
"templates",
|
|
16980
|
-
"project",
|
|
16981
|
-
"common",
|
|
16982
|
-
".github",
|
|
16983
|
-
"workflows"
|
|
16984
|
-
) : findKitTemplatesDir(here);
|
|
16985
|
-
const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
|
|
16986
|
-
const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
|
|
16987
|
-
const version = readKitVersion(kitWorkflowsDir);
|
|
16988
|
-
const seen = /* @__PURE__ */ new Set();
|
|
16989
|
-
const files = [];
|
|
16990
|
-
for (const name of templateFiles) {
|
|
16991
|
-
seen.add(name);
|
|
16992
|
-
const projectPath2 = path30.join(projectWorkflowsDir, name);
|
|
16993
|
-
const templatePath = path30.join(kitWorkflowsDir, name);
|
|
16994
|
-
if (!fs30.existsSync(projectPath2)) {
|
|
16995
|
-
files.push({ name, status: "missing" });
|
|
16996
|
-
continue;
|
|
16997
|
-
}
|
|
16998
|
-
const projectContent = fs30.readFileSync(projectPath2, "utf8");
|
|
16999
|
-
const templateContent = applyPlaceholders(fs30.readFileSync(templatePath, "utf8"), version);
|
|
17000
|
-
if (projectContent === templateContent) {
|
|
17001
|
-
files.push({ name, status: "unchanged" });
|
|
17002
|
-
} else {
|
|
17003
|
-
files.push({
|
|
17004
|
-
name,
|
|
17005
|
-
status: "drifted",
|
|
17006
|
-
diff: unifiedDiff(name, projectContent, templateContent)
|
|
17007
|
-
});
|
|
17008
|
-
}
|
|
17009
|
-
}
|
|
17010
|
-
for (const name of projectFiles) {
|
|
17011
|
-
if (seen.has(name)) continue;
|
|
17012
|
-
files.push({ name, status: "extra" });
|
|
17013
|
-
}
|
|
17014
|
-
const order = {
|
|
17015
|
-
drifted: 0,
|
|
17016
|
-
missing: 1,
|
|
17017
|
-
extra: 2,
|
|
17018
|
-
unchanged: 3
|
|
17019
|
-
};
|
|
17020
|
-
files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
|
|
17021
|
-
const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
|
|
17022
|
-
return {
|
|
17023
|
-
overall: hasDrift ? "drift" : "ok",
|
|
17024
|
-
files
|
|
17025
|
-
};
|
|
17026
|
-
}
|
|
17027
|
-
function readKitVersion(kitWorkflowsDir) {
|
|
17028
|
-
let dir = kitWorkflowsDir;
|
|
17029
|
-
for (let i = 0; i < 5; i++) {
|
|
17030
|
-
dir = path30.dirname(dir);
|
|
17031
|
-
}
|
|
17032
|
-
try {
|
|
17033
|
-
const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
|
|
17034
|
-
const pkg = JSON.parse(raw);
|
|
17035
|
-
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
17036
|
-
} catch {
|
|
17037
|
-
return "unknown";
|
|
17038
|
-
}
|
|
17039
|
-
}
|
|
17040
|
-
function applyPlaceholders(content, version) {
|
|
17041
|
-
return content.replace(/\{\{LAKEBASE_SCM_UTILS_VERSION\}\}/g, version);
|
|
17042
|
-
}
|
|
17043
|
-
function applyCommandPlaceholders(content, version) {
|
|
17044
|
-
return content.replace(/\$\{KIT_VERSION_AT_SCAFFOLD\}/g, version);
|
|
17045
|
-
}
|
|
17046
|
-
var COMMAND_HOOK_FILE_PATTERN = /\.(pre|post)-hook\.md$/;
|
|
17047
|
-
function findKitCommandsDir(start) {
|
|
17048
|
-
let dir = start;
|
|
17049
|
-
for (let i = 0; i < 6; i++) {
|
|
17050
|
-
const candidate = path30.join(
|
|
17051
|
-
dir,
|
|
17052
|
-
"templates",
|
|
17053
|
-
"project",
|
|
17054
|
-
"common",
|
|
17055
|
-
".claude",
|
|
17056
|
-
"commands"
|
|
17057
|
-
);
|
|
17058
|
-
if (fs30.existsSync(candidate)) return candidate;
|
|
17059
|
-
const parent = path30.dirname(dir);
|
|
17060
|
-
if (parent === dir) break;
|
|
17061
|
-
dir = parent;
|
|
17062
|
-
}
|
|
17063
|
-
throw new Error(
|
|
17064
|
-
`Could not locate templates/project/common/.claude/commands/ relative to ${start}. Pass explicit kitDir.`
|
|
17065
|
-
);
|
|
17066
|
-
}
|
|
17067
|
-
function parsePinnedVersion(content) {
|
|
17068
|
-
const m = content.match(/^\s*[*_>`\s]*pinned\s+to\s*:\s*[`*_]*([^\s`*_]+)[`*_]*\s*$/im);
|
|
17069
|
-
return m ? m[1] : void 0;
|
|
17070
|
-
}
|
|
17071
|
-
function detectCommandDrift(args) {
|
|
17072
|
-
const projectCommandsDir = path30.join(args.projectDir, ".claude", "commands");
|
|
17073
|
-
const here = path30.dirname(new URL(import.meta.url).pathname);
|
|
17074
|
-
const kitCommandsDir = args.kitDir ? path30.join(args.kitDir, "templates", "project", "common", ".claude", "commands") : findKitCommandsDir(here);
|
|
17075
|
-
const kitVersion = readKitVersionFromCommandsDir(kitCommandsDir);
|
|
17076
|
-
const templateFiles = fs30.existsSync(kitCommandsDir) ? fs30.readdirSync(kitCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
|
|
17077
|
-
const projectFiles = fs30.existsSync(projectCommandsDir) ? fs30.readdirSync(projectCommandsDir).filter((f) => f.endsWith(".md") && !COMMAND_HOOK_FILE_PATTERN.test(f)) : [];
|
|
17078
|
-
const seen = /* @__PURE__ */ new Set();
|
|
17079
|
-
const files = [];
|
|
17080
|
-
for (const name of templateFiles) {
|
|
17081
|
-
seen.add(name);
|
|
17082
|
-
const projectPath2 = path30.join(projectCommandsDir, name);
|
|
17083
|
-
const templatePath = path30.join(kitCommandsDir, name);
|
|
17084
|
-
const templateRaw = fs30.readFileSync(templatePath, "utf8");
|
|
17085
|
-
if (!fs30.existsSync(projectPath2)) {
|
|
17086
|
-
files.push({ name, status: "missing", kit_version: kitVersion });
|
|
17087
|
-
continue;
|
|
17088
|
-
}
|
|
17089
|
-
const projectContent = fs30.readFileSync(projectPath2, "utf8");
|
|
17090
|
-
const pinned = parsePinnedVersion(projectContent);
|
|
17091
|
-
const versionForCompare = pinned ?? kitVersion;
|
|
17092
|
-
const templateContent = applyCommandPlaceholders(templateRaw, versionForCompare);
|
|
17093
|
-
if (projectContent === templateContent) {
|
|
17094
|
-
files.push({
|
|
17095
|
-
name,
|
|
17096
|
-
status: "unchanged",
|
|
17097
|
-
pinned_version: pinned,
|
|
17098
|
-
kit_version: kitVersion
|
|
17099
|
-
});
|
|
17100
|
-
} else {
|
|
17101
|
-
files.push({
|
|
17102
|
-
name,
|
|
17103
|
-
status: "drifted",
|
|
17104
|
-
pinned_version: pinned,
|
|
17105
|
-
kit_version: kitVersion,
|
|
17106
|
-
diff: unifiedDiff(name, projectContent, templateContent)
|
|
17107
|
-
});
|
|
17108
|
-
}
|
|
17109
|
-
}
|
|
17110
|
-
for (const name of projectFiles) {
|
|
17111
|
-
if (seen.has(name)) continue;
|
|
17112
|
-
files.push({ name, status: "extra", kit_version: kitVersion });
|
|
17113
|
-
}
|
|
17114
|
-
const order = {
|
|
17115
|
-
drifted: 0,
|
|
17116
|
-
missing: 1,
|
|
17117
|
-
extra: 2,
|
|
17118
|
-
unchanged: 3
|
|
17119
|
-
};
|
|
17120
|
-
files.sort((a, b) => order[a.status] - order[b.status] || a.name.localeCompare(b.name));
|
|
17121
|
-
const hasDrift = files.some((f) => f.status === "drifted" || f.status === "missing");
|
|
17122
|
-
return { overall: hasDrift ? "drift" : "ok", files };
|
|
17123
|
-
}
|
|
17124
|
-
function readKitVersionFromCommandsDir(kitCommandsDir) {
|
|
17125
|
-
let dir = kitCommandsDir;
|
|
17126
|
-
for (let i = 0; i < 5; i++) {
|
|
17127
|
-
dir = path30.dirname(dir);
|
|
17128
|
-
}
|
|
17129
|
-
try {
|
|
17130
|
-
const raw = fs30.readFileSync(path30.join(dir, "package.json"), "utf-8");
|
|
17131
|
-
const pkg = JSON.parse(raw);
|
|
17132
|
-
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
17133
|
-
} catch {
|
|
17134
|
-
return "unknown";
|
|
17135
|
-
}
|
|
17158
|
+
findings
|
|
17159
|
+
});
|
|
17136
17160
|
}
|
|
17137
|
-
function
|
|
17138
|
-
|
|
17139
|
-
const
|
|
17140
|
-
|
|
17141
|
-
|
|
17142
|
-
|
|
17143
|
-
commands
|
|
17144
|
-
};
|
|
17161
|
+
function finalize(report) {
|
|
17162
|
+
let worst = "ok";
|
|
17163
|
+
for (const f of report.findings) {
|
|
17164
|
+
worst = worstOf(worst, f.severity);
|
|
17165
|
+
}
|
|
17166
|
+
return { ...report, worstSeverity: worst };
|
|
17145
17167
|
}
|
|
17146
|
-
|
|
17147
|
-
|
|
17148
|
-
|
|
17149
|
-
|
|
17150
|
-
"
|
|
17151
|
-
);
|
|
17152
|
-
const here = path30.dirname(new URL(import.meta.url).pathname);
|
|
17153
|
-
const kitWorkflowsDir = args.kitDir ? path30.join(
|
|
17154
|
-
args.kitDir,
|
|
17155
|
-
"templates",
|
|
17156
|
-
"project",
|
|
17157
|
-
"common",
|
|
17158
|
-
".github",
|
|
17159
|
-
"workflows"
|
|
17160
|
-
) : findKitTemplatesDir(here);
|
|
17161
|
-
const substitute = args.substitute !== false;
|
|
17162
|
-
const dryRun = args.dryRun === true;
|
|
17163
|
-
const pruneExtras = args.pruneExtras === true;
|
|
17164
|
-
const templateFiles = fs30.existsSync(kitWorkflowsDir) ? fs30.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
|
|
17165
|
-
const projectFiles = fs30.existsSync(projectWorkflowsDir) ? fs30.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
|
|
17166
|
-
if (!dryRun && templateFiles.length > 0 && !fs30.existsSync(projectWorkflowsDir)) {
|
|
17167
|
-
fs30.mkdirSync(projectWorkflowsDir, { recursive: true });
|
|
17168
|
+
var ScmDoctorFixError = class extends Error {
|
|
17169
|
+
constructor(message, code) {
|
|
17170
|
+
super(message);
|
|
17171
|
+
this.code = code;
|
|
17172
|
+
this.name = "ScmDoctorFixError";
|
|
17168
17173
|
}
|
|
17169
|
-
|
|
17170
|
-
|
|
17171
|
-
|
|
17172
|
-
|
|
17173
|
-
|
|
17174
|
-
|
|
17175
|
-
|
|
17176
|
-
|
|
17177
|
-
|
|
17178
|
-
|
|
17179
|
-
|
|
17180
|
-
|
|
17181
|
-
|
|
17182
|
-
|
|
17183
|
-
|
|
17184
|
-
|
|
17185
|
-
} else {
|
|
17186
|
-
outcome = "updated";
|
|
17187
|
-
}
|
|
17188
|
-
if (!dryRun && outcome !== "unchanged") {
|
|
17189
|
-
fs30.writeFileSync(projectPath2, desired);
|
|
17190
|
-
}
|
|
17191
|
-
files.push({ name, outcome });
|
|
17174
|
+
code;
|
|
17175
|
+
};
|
|
17176
|
+
var FIXABLE_FINDING_IDS = [
|
|
17177
|
+
"env-branch-drift",
|
|
17178
|
+
"head-branch-drift",
|
|
17179
|
+
"tier-topology-mismatch",
|
|
17180
|
+
"orphan-current-branch",
|
|
17181
|
+
"multiple-migration-heads",
|
|
17182
|
+
"db-ahead-of-code"
|
|
17183
|
+
];
|
|
17184
|
+
async function fixFinding(args) {
|
|
17185
|
+
if (!FIXABLE_FINDING_IDS.includes(args.findingId)) {
|
|
17186
|
+
throw new ScmDoctorFixError(
|
|
17187
|
+
`Finding "${args.findingId}" is not supported by --fix. Supported: ${FIXABLE_FINDING_IDS.join(", ")}.`,
|
|
17188
|
+
"unsupported-finding"
|
|
17189
|
+
);
|
|
17192
17190
|
}
|
|
17193
|
-
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
|
|
17197
|
-
|
|
17198
|
-
|
|
17191
|
+
const report = args.report ?? await runDoctor({ projectDir: args.projectDir, instance: args.instance });
|
|
17192
|
+
const present = report.findings.find((f) => f.id === args.findingId);
|
|
17193
|
+
if (!present) {
|
|
17194
|
+
throw new ScmDoctorFixError(
|
|
17195
|
+
`Finding "${args.findingId}" is not present in the current report. Re-run lakebase-scm-doctor to see what needs fixing.`,
|
|
17196
|
+
"finding-not-present"
|
|
17197
|
+
);
|
|
17198
|
+
}
|
|
17199
|
+
let action = "";
|
|
17200
|
+
try {
|
|
17201
|
+
switch (args.findingId) {
|
|
17202
|
+
case "env-branch-drift": {
|
|
17203
|
+
const branch = report.state?.branch;
|
|
17204
|
+
if (!branch) {
|
|
17205
|
+
throw new ScmDoctorFixError(
|
|
17206
|
+
"Cannot fix: workflow state has no branch field.",
|
|
17207
|
+
"fix-failed"
|
|
17208
|
+
);
|
|
17209
|
+
}
|
|
17210
|
+
const sanitized = sanitizeBranchName(branch);
|
|
17211
|
+
const envFile = path30.join(args.projectDir, ".env");
|
|
17212
|
+
updateEnvConnection({
|
|
17213
|
+
envPath: envFile,
|
|
17214
|
+
projectId: readEnvVar(envFile, "LAKEBASE_PROJECT_ID") ?? "",
|
|
17215
|
+
branchId: sanitized,
|
|
17216
|
+
username: readEnvVar(envFile, "DB_USERNAME") ?? "",
|
|
17217
|
+
endpointHost: readEnvVar(envFile, "LAKEBASE_HOST")
|
|
17218
|
+
});
|
|
17219
|
+
action = `rewrote .env LAKEBASE_BRANCH_ID=${sanitized} (metadata only; the app mints its token at runtime)`;
|
|
17220
|
+
break;
|
|
17221
|
+
}
|
|
17222
|
+
case "head-branch-drift": {
|
|
17223
|
+
const branch = report.state?.branch;
|
|
17224
|
+
if (!branch) {
|
|
17225
|
+
throw new ScmDoctorFixError(
|
|
17226
|
+
"Cannot fix: workflow state has no branch field.",
|
|
17227
|
+
"fix-failed"
|
|
17228
|
+
);
|
|
17229
|
+
}
|
|
17230
|
+
await exec2(`git checkout ${shellEscape3(branch)}`, {
|
|
17231
|
+
cwd: args.projectDir,
|
|
17232
|
+
timeout: 15e3
|
|
17233
|
+
});
|
|
17234
|
+
action = `git checkout ${branch} (re-fires post-checkout to resync HEAD)`;
|
|
17235
|
+
break;
|
|
17236
|
+
}
|
|
17237
|
+
case "tier-topology-mismatch": {
|
|
17238
|
+
const instance = args.instance ?? report.state?.project_id;
|
|
17239
|
+
if (!instance) {
|
|
17240
|
+
throw new ScmDoctorFixError(
|
|
17241
|
+
"Cannot fix: missing Lakebase project id.",
|
|
17242
|
+
"fix-failed"
|
|
17243
|
+
);
|
|
17244
|
+
}
|
|
17245
|
+
await adoptScmState({
|
|
17246
|
+
projectDir: args.projectDir,
|
|
17247
|
+
instance,
|
|
17248
|
+
force: true
|
|
17249
|
+
});
|
|
17250
|
+
action = `adopted state with --force to re-infer tier_topology`;
|
|
17251
|
+
break;
|
|
17252
|
+
}
|
|
17253
|
+
case "orphan-current-branch": {
|
|
17254
|
+
const instance = args.instance ?? report.state?.project_id;
|
|
17255
|
+
if (!instance) {
|
|
17256
|
+
throw new ScmDoctorFixError(
|
|
17257
|
+
"Cannot fix: missing Lakebase project id.",
|
|
17258
|
+
"fix-failed"
|
|
17259
|
+
);
|
|
17260
|
+
}
|
|
17261
|
+
const headBranch = await getCurrentBranch({ cwd: args.projectDir });
|
|
17262
|
+
if (!headBranch) {
|
|
17263
|
+
throw new ScmDoctorFixError(
|
|
17264
|
+
"Cannot fix: detached HEAD or no current branch.",
|
|
17265
|
+
"fix-failed"
|
|
17266
|
+
);
|
|
17267
|
+
}
|
|
17268
|
+
await recoverOrphans({
|
|
17269
|
+
projectDir: args.projectDir,
|
|
17270
|
+
instance,
|
|
17271
|
+
claim: true,
|
|
17272
|
+
onlyBranch: headBranch
|
|
17273
|
+
});
|
|
17274
|
+
action = `recovered orphan ${headBranch} via createFeaturePairedBranch`;
|
|
17275
|
+
break;
|
|
17276
|
+
}
|
|
17277
|
+
case "multiple-migration-heads": {
|
|
17278
|
+
const r = await collapseMigrationHeads({ projectDir: args.projectDir });
|
|
17279
|
+
if (r.status !== "ok" || !r.mergeRevision) {
|
|
17280
|
+
throw new ScmDoctorFixError(
|
|
17281
|
+
`Expected to create a merge revision but got status="${r.status}".`,
|
|
17282
|
+
"fix-failed"
|
|
17283
|
+
);
|
|
17284
|
+
}
|
|
17285
|
+
action = `collapsed ${r.headsBefore.length} heads into merge revision ${r.mergeRevision} (commit it)`;
|
|
17286
|
+
break;
|
|
17287
|
+
}
|
|
17288
|
+
case "db-ahead-of-code": {
|
|
17289
|
+
const r = await abandonFeatureBranch({ projectDir: args.projectDir, instance: args.instance, force: true });
|
|
17290
|
+
action = `abandoned the feature (deleted the polluted paired branch${r.lakebaseDeleted ? "" : " , Lakebase delete reported not-deleted"}) and reset state to '${r.state.state}'; re-claim to re-fork a clean branch from the tier`;
|
|
17291
|
+
break;
|
|
17199
17292
|
}
|
|
17200
|
-
files.push({ name, outcome: "removed" });
|
|
17201
17293
|
}
|
|
17294
|
+
} catch (err) {
|
|
17295
|
+
if (err instanceof ScmDoctorFixError) throw err;
|
|
17296
|
+
throw new ScmDoctorFixError(
|
|
17297
|
+
`Remediation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
17298
|
+
"fix-failed"
|
|
17299
|
+
);
|
|
17202
17300
|
}
|
|
17203
|
-
const
|
|
17204
|
-
|
|
17205
|
-
|
|
17206
|
-
|
|
17207
|
-
|
|
17208
|
-
|
|
17209
|
-
|
|
17210
|
-
|
|
17211
|
-
return { files, changed };
|
|
17301
|
+
const postReport = await runDoctor({
|
|
17302
|
+
projectDir: args.projectDir,
|
|
17303
|
+
instance: args.instance
|
|
17304
|
+
});
|
|
17305
|
+
return { findingId: args.findingId, action, postReport };
|
|
17306
|
+
}
|
|
17307
|
+
function shellEscape3(s) {
|
|
17308
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
17212
17309
|
}
|
|
17213
17310
|
|
|
17214
17311
|
// scripts/lakebase/doctor.ts
|
|
17312
|
+
init_esm_shims();
|
|
17313
|
+
import * as fs31 from "fs";
|
|
17314
|
+
import * as path31 from "path";
|
|
17315
|
+
import * as cp6 from "child_process";
|
|
17215
17316
|
function readEnvFile(projectDir) {
|
|
17216
17317
|
const envPath = path31.join(projectDir, ".env");
|
|
17217
17318
|
if (!fs31.existsSync(envPath)) return {};
|
|
@@ -17644,7 +17745,7 @@ function checkWorkflowDrift(projectDir) {
|
|
|
17644
17745
|
status: "warn",
|
|
17645
17746
|
message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
|
|
17646
17747
|
detail: { files: report.files.map((f) => ({ name: f.name, status: f.status })) },
|
|
17647
|
-
hint: "
|
|
17748
|
+
hint: "Refresh with updateWorkflows (or the lakebase_workflow_drift MCP tool). A stale merge.yml calling 'lakebase-schema-migrate apply' (not 'apply-tier') makes the promote's migrate-target fail on the tier guard; the scm-merge promote also self-heals drifted workflows onto the PR head before merging."
|
|
17648
17749
|
};
|
|
17649
17750
|
} catch (err) {
|
|
17650
17751
|
return {
|
|
@@ -18120,63 +18221,6 @@ async function discardAllChanges(args) {
|
|
|
18120
18221
|
await exec2("git clean -fd", { cwd: args.cwd });
|
|
18121
18222
|
}
|
|
18122
18223
|
|
|
18123
|
-
// scripts/git/sync.ts
|
|
18124
|
-
init_esm_shims();
|
|
18125
|
-
async function currentBranchName2(cwd) {
|
|
18126
|
-
try {
|
|
18127
|
-
return await exec2("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
18128
|
-
} catch {
|
|
18129
|
-
return "";
|
|
18130
|
-
}
|
|
18131
|
-
}
|
|
18132
|
-
async function push(args) {
|
|
18133
|
-
await exec2("git push", { cwd: args.cwd });
|
|
18134
|
-
}
|
|
18135
|
-
async function pull(args) {
|
|
18136
|
-
await exec2("git pull", { cwd: args.cwd });
|
|
18137
|
-
}
|
|
18138
|
-
async function publishBranch(args) {
|
|
18139
|
-
const remote = args.remote ?? "origin";
|
|
18140
|
-
const branch = await currentBranchName2(args.cwd);
|
|
18141
|
-
if (!branch) throw new Error("No current branch");
|
|
18142
|
-
await exec2(`git push -u ${remote} ${shq(branch)}`, {
|
|
18143
|
-
cwd: args.cwd
|
|
18144
|
-
});
|
|
18145
|
-
}
|
|
18146
|
-
async function pushCurrentBranchForPr(args) {
|
|
18147
|
-
const remote = args.remote ?? "origin";
|
|
18148
|
-
const branch = await currentBranchName2(args.cwd);
|
|
18149
|
-
if (!branch) throw new Error("No current branch");
|
|
18150
|
-
const upstreamSet = await hasUpstream({ cwd: args.cwd });
|
|
18151
|
-
if (!upstreamSet) {
|
|
18152
|
-
await exec2(`git push -u ${remote} ${shq(branch)}`, {
|
|
18153
|
-
cwd: args.cwd
|
|
18154
|
-
});
|
|
18155
|
-
} else {
|
|
18156
|
-
await exec2("git push", { cwd: args.cwd });
|
|
18157
|
-
}
|
|
18158
|
-
}
|
|
18159
|
-
async function fetch2(args) {
|
|
18160
|
-
const parts = ["git fetch"];
|
|
18161
|
-
if (args.prune) parts.push("--prune");
|
|
18162
|
-
if (args.all) parts.push("--all");
|
|
18163
|
-
await exec2(parts.join(" "), { cwd: args.cwd });
|
|
18164
|
-
}
|
|
18165
|
-
async function pullFrom(args) {
|
|
18166
|
-
await exec2(`git pull ${shq(args.remote)} ${shq(args.branch)}`, {
|
|
18167
|
-
cwd: args.cwd
|
|
18168
|
-
});
|
|
18169
|
-
}
|
|
18170
|
-
async function pushTo(args) {
|
|
18171
|
-
await exec2(`git push ${shq(args.remote)} ${shq(args.branch)}`, {
|
|
18172
|
-
cwd: args.cwd
|
|
18173
|
-
});
|
|
18174
|
-
}
|
|
18175
|
-
async function sync(args) {
|
|
18176
|
-
await exec2("git pull", { cwd: args.cwd });
|
|
18177
|
-
await exec2("git push", { cwd: args.cwd });
|
|
18178
|
-
}
|
|
18179
|
-
|
|
18180
18224
|
// scripts/git/branch-tag.ts
|
|
18181
18225
|
init_esm_shims();
|
|
18182
18226
|
var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["production", "main", "master"]);
|
|
@@ -18581,6 +18625,7 @@ export {
|
|
|
18581
18625
|
createWorktree,
|
|
18582
18626
|
cutBackup,
|
|
18583
18627
|
databricksAuthPrereqMessage,
|
|
18628
|
+
defaultRefreshPromoteWorkflows,
|
|
18584
18629
|
delay,
|
|
18585
18630
|
deleteAppEndpoint,
|
|
18586
18631
|
deleteBranch,
|