@databricks-solutions/lakebase-scm-utils 0.2.41 → 0.2.43
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 +3 -1
- package/dist/scripts/github/index.js +3 -1
- package/dist/scripts/github/pr.cli.cjs +3 -1
- package/dist/scripts/github/pr.cli.js +3 -1
- package/dist/scripts/index.cjs +84 -21
- package/dist/scripts/index.d.cts +1 -1
- package/dist/scripts/index.d.ts +1 -1
- package/dist/scripts/index.js +76 -15
- package/dist/scripts/lakebase/branch.cli.cjs +3 -1
- package/dist/scripts/lakebase/branch.cli.js +3 -1
- package/dist/scripts/lakebase/ci-app-endpoint.cli.cjs +3 -1
- package/dist/scripts/lakebase/ci-app-endpoint.cli.js +3 -1
- package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +5 -3
- package/dist/scripts/lakebase/ci-resolve-branch.cli.js +5 -3
- package/dist/scripts/lakebase/collapse-heads.cli.cjs +5 -3
- package/dist/scripts/lakebase/collapse-heads.cli.js +5 -3
- package/dist/scripts/lakebase/cut-backup.cli.cjs +3 -1
- package/dist/scripts/lakebase/cut-backup.cli.js +3 -1
- package/dist/scripts/lakebase/cut-tier.cli.cjs +3 -1
- package/dist/scripts/lakebase/cut-tier.cli.js +3 -1
- package/dist/scripts/lakebase/detect-language.cli.cjs +5 -3
- package/dist/scripts/lakebase/detect-language.cli.js +5 -3
- package/dist/scripts/lakebase/doctor.cli.cjs +5 -3
- package/dist/scripts/lakebase/doctor.cli.js +5 -3
- package/dist/scripts/lakebase/get-connection.cli.cjs +5 -3
- package/dist/scripts/lakebase/get-connection.cli.js +5 -3
- package/dist/scripts/lakebase/index.cjs +84 -21
- package/dist/scripts/lakebase/index.d.cts +21 -1
- package/dist/scripts/lakebase/index.d.ts +21 -1
- package/dist/scripts/lakebase/index.js +76 -15
- package/dist/scripts/lakebase/infra-runner.cli.cjs +5 -3
- package/dist/scripts/lakebase/infra-runner.cli.js +5 -3
- package/dist/scripts/lakebase/new-migration.cli.cjs +5 -3
- package/dist/scripts/lakebase/new-migration.cli.js +5 -3
- package/dist/scripts/lakebase/schema-diff.cli.cjs +5 -3
- package/dist/scripts/lakebase/schema-diff.cli.js +5 -3
- package/dist/scripts/lakebase/schema-migrate.cli.cjs +5 -3
- package/dist/scripts/lakebase/schema-migrate.cli.js +5 -3
- package/dist/scripts/lakebase/scm-abandon-feature.cli.cjs +37 -1
- package/dist/scripts/lakebase/scm-abandon-feature.cli.js +37 -1
- package/dist/scripts/lakebase/scm-adopt-state.cli.cjs +37 -1
- package/dist/scripts/lakebase/scm-adopt-state.cli.js +37 -1
- package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +45 -9
- package/dist/scripts/lakebase/scm-claim-feature.cli.js +39 -3
- package/dist/scripts/lakebase/scm-cleanup.cli.cjs +3 -1
- package/dist/scripts/lakebase/scm-cleanup.cli.js +3 -1
- package/dist/scripts/lakebase/scm-doctor.cli.cjs +56 -29
- package/dist/scripts/lakebase/scm-doctor.cli.js +50 -23
- package/dist/scripts/lakebase/scm-feature-branch.cli.cjs +1 -0
- package/dist/scripts/lakebase/scm-feature-branch.cli.js +1 -0
- package/dist/scripts/lakebase/scm-merge.cli.cjs +61 -9
- package/dist/scripts/lakebase/scm-merge.cli.js +55 -3
- package/dist/scripts/lakebase/scm-prepare-pr.cli.cjs +34 -0
- package/dist/scripts/lakebase/scm-prepare-pr.cli.js +34 -0
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +6 -3
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +6 -3
- package/dist/scripts/lakebase/scm-recover-orphans.cli.cjs +37 -1
- package/dist/scripts/lakebase/scm-recover-orphans.cli.js +37 -1
- package/dist/scripts/lakebase/scm-state.cli.cjs +5 -4
- package/dist/scripts/lakebase/scm-state.cli.js +4 -3
- package/dist/scripts/lakebase/scm-wait-ci.cli.cjs +34 -0
- package/dist/scripts/lakebase/scm-wait-ci.cli.js +34 -0
- package/dist/scripts/util/index.cjs +5 -3
- package/dist/scripts/util/index.js +5 -3
- package/package.json +1 -1
- package/scripts/lakebase/databricks-cli.ts +7 -1
- package/scripts/lakebase/enable-e2e.ts +19 -3
- package/scripts/lakebase/scm-doctor.ts +1 -12
- package/scripts/lakebase/scm-merge.ts +36 -0
- package/scripts/lakebase/scm-workflow-state.ts +49 -0
- package/templates/project/common/.github/workflows/merge.yml +17 -6
- package/templates/project/common/.github/workflows/pr.yml +17 -6
- package/templates/project/common/.gitignore.base +8 -0
- package/templates/project/common/scripts/run-tests.sh +18 -1
- package/templates/project/python/tests/__pycache__/conftest.cpython-314.pyc +0 -0
- package/templates/project/python/tests/conftest.py +10 -3
|
@@ -288,7 +288,9 @@ function resolveProfile(opts) {
|
|
|
288
288
|
function buildInvocation(args, opts) {
|
|
289
289
|
const base = opts.env ?? process.env;
|
|
290
290
|
const trimmedHost = effectiveHost(opts)?.replace(/\/+$/, "");
|
|
291
|
-
const env =
|
|
291
|
+
const env = { ...base };
|
|
292
|
+
if (trimmedHost) env.DATABRICKS_HOST = trimmedHost;
|
|
293
|
+
delete env.DATABRICKS_WORKSPACE_ID;
|
|
292
294
|
const profile = resolveProfile(opts);
|
|
293
295
|
const argv = profile && !opts.noProfile && !args.includes("--profile") ? [...args, "--profile", profile] : args;
|
|
294
296
|
return { argv, env, profile };
|
|
@@ -1159,6 +1161,39 @@ async function listLocalBranches(args) {
|
|
|
1159
1161
|
// scripts/lakebase/scm-workflow-state.ts
|
|
1160
1162
|
import * as fs5 from "fs";
|
|
1161
1163
|
import * as path4 from "path";
|
|
1164
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1165
|
+
function isGitTracked(projectDir, rel) {
|
|
1166
|
+
try {
|
|
1167
|
+
execFileSync4("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
1168
|
+
return true;
|
|
1169
|
+
} catch {
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function ensureWorkflowStateUntracked(projectDir) {
|
|
1174
|
+
const rel = ".lakebase/workflow-state.json";
|
|
1175
|
+
try {
|
|
1176
|
+
if (isGitTracked(projectDir, rel)) {
|
|
1177
|
+
execFileSync4("git", ["rm", "--cached", "--quiet", "--ignore-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
1178
|
+
}
|
|
1179
|
+
} catch {
|
|
1180
|
+
}
|
|
1181
|
+
try {
|
|
1182
|
+
const gitignore = path4.join(projectDir, ".gitignore");
|
|
1183
|
+
const existing = fs5.existsSync(gitignore) ? fs5.readFileSync(gitignore, "utf8") : "";
|
|
1184
|
+
if (!existing.split("\n").some((l) => l.trim() === rel)) {
|
|
1185
|
+
const sep = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
1186
|
+
fs5.appendFileSync(
|
|
1187
|
+
gitignore,
|
|
1188
|
+
`${sep}# Runtime SCM claim state (per working tree): a branch checkout must never restore
|
|
1189
|
+
# a stale committed claim over the live one (issue #203 / Finding 28).
|
|
1190
|
+
${rel}
|
|
1191
|
+
`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
} catch {
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1162
1197
|
var SCM_STATES = [
|
|
1163
1198
|
"scaffold-complete",
|
|
1164
1199
|
"feature-claimed",
|
|
@@ -1205,6 +1240,7 @@ function writeWorkflowState(projectDir, state) {
|
|
|
1205
1240
|
throw new Error(`Refusing to write invalid SCM state:
|
|
1206
1241
|
${summary}`);
|
|
1207
1242
|
}
|
|
1243
|
+
ensureWorkflowStateUntracked(projectDir);
|
|
1208
1244
|
const dir = path4.join(projectDir, ".lakebase");
|
|
1209
1245
|
fs5.mkdirSync(dir, { recursive: true });
|
|
1210
1246
|
const target = stateFilePath(projectDir);
|
|
@@ -77545,6 +77545,7 @@ function isCliEntry(importMetaUrl2) {
|
|
|
77545
77545
|
init_cjs_shims();
|
|
77546
77546
|
var fs = __toESM(require("fs"), 1);
|
|
77547
77547
|
var path = __toESM(require("path"), 1);
|
|
77548
|
+
var import_node_child_process = require("child_process");
|
|
77548
77549
|
var SCM_STATES = [
|
|
77549
77550
|
"scaffold-complete",
|
|
77550
77551
|
"feature-claimed",
|
|
@@ -77821,21 +77822,21 @@ var KIT_REGISTRIES = {
|
|
|
77821
77822
|
init_cjs_shims();
|
|
77822
77823
|
var fs6 = __toESM(require("fs"), 1);
|
|
77823
77824
|
var path4 = __toESM(require("path"), 1);
|
|
77824
|
-
var
|
|
77825
|
+
var import_node_child_process4 = require("child_process");
|
|
77825
77826
|
|
|
77826
77827
|
// scripts/lakebase/branch-create.ts
|
|
77827
77828
|
init_cjs_shims();
|
|
77828
77829
|
|
|
77829
77830
|
// scripts/lakebase/databricks-cli.ts
|
|
77830
77831
|
init_cjs_shims();
|
|
77831
|
-
var
|
|
77832
|
+
var import_node_child_process3 = require("child_process");
|
|
77832
77833
|
var import_node_util = require("util");
|
|
77833
77834
|
var import_node_path = require("path");
|
|
77834
77835
|
|
|
77835
77836
|
// scripts/lakebase/databricks-profile.ts
|
|
77836
77837
|
init_cjs_shims();
|
|
77837
77838
|
var fs2 = __toESM(require("fs"), 1);
|
|
77838
|
-
var
|
|
77839
|
+
var import_node_child_process2 = require("child_process");
|
|
77839
77840
|
|
|
77840
77841
|
// scripts/util/exec.ts
|
|
77841
77842
|
init_cjs_shims();
|
|
@@ -77847,7 +77848,7 @@ var fs3 = __toESM(require("fs"), 1);
|
|
|
77847
77848
|
var path2 = __toESM(require("path"), 1);
|
|
77848
77849
|
|
|
77849
77850
|
// scripts/lakebase/databricks-cli.ts
|
|
77850
|
-
var execFileP = (0, import_node_util.promisify)(
|
|
77851
|
+
var execFileP = (0, import_node_util.promisify)(import_node_child_process3.execFile);
|
|
77851
77852
|
|
|
77852
77853
|
// scripts/util/poll-until.ts
|
|
77853
77854
|
init_cjs_shims();
|
|
@@ -27,6 +27,7 @@ function isCliEntry(importMetaUrl) {
|
|
|
27
27
|
// scripts/lakebase/scm-workflow-state.ts
|
|
28
28
|
import * as fs from "fs";
|
|
29
29
|
import * as path from "path";
|
|
30
|
+
import { execFileSync } from "child_process";
|
|
30
31
|
var SCM_STATES = [
|
|
31
32
|
"scaffold-complete",
|
|
32
33
|
"feature-claimed",
|
|
@@ -297,16 +298,16 @@ var KIT_REGISTRIES = {
|
|
|
297
298
|
// scripts/lakebase/paired-branch.ts
|
|
298
299
|
import * as fs5 from "fs";
|
|
299
300
|
import * as path4 from "path";
|
|
300
|
-
import { execFileSync as
|
|
301
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
301
302
|
|
|
302
303
|
// scripts/lakebase/databricks-cli.ts
|
|
303
|
-
import { execFile, execFileSync as
|
|
304
|
+
import { execFile, execFileSync as execFileSync3 } from "child_process";
|
|
304
305
|
import { promisify } from "util";
|
|
305
306
|
import { join as join3 } from "path";
|
|
306
307
|
|
|
307
308
|
// scripts/lakebase/databricks-profile.ts
|
|
308
309
|
import * as fs2 from "fs";
|
|
309
|
-
import { execFileSync } from "child_process";
|
|
310
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
310
311
|
|
|
311
312
|
// scripts/util/exec.ts
|
|
312
313
|
import * as cp from "child_process";
|
|
@@ -8351,6 +8351,39 @@ async function pollUntil(args) {
|
|
|
8351
8351
|
init_cjs_shims();
|
|
8352
8352
|
var fs3 = __toESM(require("fs"), 1);
|
|
8353
8353
|
var path2 = __toESM(require("path"), 1);
|
|
8354
|
+
var import_node_child_process4 = require("child_process");
|
|
8355
|
+
function isGitTracked(projectDir, rel) {
|
|
8356
|
+
try {
|
|
8357
|
+
(0, import_node_child_process4.execFileSync)("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
8358
|
+
return true;
|
|
8359
|
+
} catch {
|
|
8360
|
+
return false;
|
|
8361
|
+
}
|
|
8362
|
+
}
|
|
8363
|
+
function ensureWorkflowStateUntracked(projectDir) {
|
|
8364
|
+
const rel = ".lakebase/workflow-state.json";
|
|
8365
|
+
try {
|
|
8366
|
+
if (isGitTracked(projectDir, rel)) {
|
|
8367
|
+
(0, import_node_child_process4.execFileSync)("git", ["rm", "--cached", "--quiet", "--ignore-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
8368
|
+
}
|
|
8369
|
+
} catch {
|
|
8370
|
+
}
|
|
8371
|
+
try {
|
|
8372
|
+
const gitignore = path2.join(projectDir, ".gitignore");
|
|
8373
|
+
const existing = fs3.existsSync(gitignore) ? fs3.readFileSync(gitignore, "utf8") : "";
|
|
8374
|
+
if (!existing.split("\n").some((l) => l.trim() === rel)) {
|
|
8375
|
+
const sep2 = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
8376
|
+
fs3.appendFileSync(
|
|
8377
|
+
gitignore,
|
|
8378
|
+
`${sep2}# Runtime SCM claim state (per working tree): a branch checkout must never restore
|
|
8379
|
+
# a stale committed claim over the live one (issue #203 / Finding 28).
|
|
8380
|
+
${rel}
|
|
8381
|
+
`
|
|
8382
|
+
);
|
|
8383
|
+
}
|
|
8384
|
+
} catch {
|
|
8385
|
+
}
|
|
8386
|
+
}
|
|
8354
8387
|
var SCM_STATES = [
|
|
8355
8388
|
"scaffold-complete",
|
|
8356
8389
|
"feature-claimed",
|
|
@@ -8397,6 +8430,7 @@ function writeWorkflowState(projectDir, state) {
|
|
|
8397
8430
|
throw new Error(`Refusing to write invalid SCM state:
|
|
8398
8431
|
${summary}`);
|
|
8399
8432
|
}
|
|
8433
|
+
ensureWorkflowStateUntracked(projectDir);
|
|
8400
8434
|
const dir = path2.join(projectDir, ".lakebase");
|
|
8401
8435
|
fs3.mkdirSync(dir, { recursive: true });
|
|
8402
8436
|
const target = stateFilePath(projectDir);
|
|
@@ -8339,6 +8339,39 @@ async function pollUntil(args) {
|
|
|
8339
8339
|
init_esm_shims();
|
|
8340
8340
|
import * as fs3 from "fs";
|
|
8341
8341
|
import * as path3 from "path";
|
|
8342
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
8343
|
+
function isGitTracked(projectDir, rel) {
|
|
8344
|
+
try {
|
|
8345
|
+
execFileSync4("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
8346
|
+
return true;
|
|
8347
|
+
} catch {
|
|
8348
|
+
return false;
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
8351
|
+
function ensureWorkflowStateUntracked(projectDir) {
|
|
8352
|
+
const rel = ".lakebase/workflow-state.json";
|
|
8353
|
+
try {
|
|
8354
|
+
if (isGitTracked(projectDir, rel)) {
|
|
8355
|
+
execFileSync4("git", ["rm", "--cached", "--quiet", "--ignore-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
8356
|
+
}
|
|
8357
|
+
} catch {
|
|
8358
|
+
}
|
|
8359
|
+
try {
|
|
8360
|
+
const gitignore = path3.join(projectDir, ".gitignore");
|
|
8361
|
+
const existing = fs3.existsSync(gitignore) ? fs3.readFileSync(gitignore, "utf8") : "";
|
|
8362
|
+
if (!existing.split("\n").some((l) => l.trim() === rel)) {
|
|
8363
|
+
const sep2 = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
8364
|
+
fs3.appendFileSync(
|
|
8365
|
+
gitignore,
|
|
8366
|
+
`${sep2}# Runtime SCM claim state (per working tree): a branch checkout must never restore
|
|
8367
|
+
# a stale committed claim over the live one (issue #203 / Finding 28).
|
|
8368
|
+
${rel}
|
|
8369
|
+
`
|
|
8370
|
+
);
|
|
8371
|
+
}
|
|
8372
|
+
} catch {
|
|
8373
|
+
}
|
|
8374
|
+
}
|
|
8342
8375
|
var SCM_STATES = [
|
|
8343
8376
|
"scaffold-complete",
|
|
8344
8377
|
"feature-claimed",
|
|
@@ -8385,6 +8418,7 @@ function writeWorkflowState(projectDir, state) {
|
|
|
8385
8418
|
throw new Error(`Refusing to write invalid SCM state:
|
|
8386
8419
|
${summary}`);
|
|
8387
8420
|
}
|
|
8421
|
+
ensureWorkflowStateUntracked(projectDir);
|
|
8388
8422
|
const dir = path3.join(projectDir, ".lakebase");
|
|
8389
8423
|
fs3.mkdirSync(dir, { recursive: true });
|
|
8390
8424
|
const target = stateFilePath(projectDir);
|
|
@@ -1659,8 +1659,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
1659
1659
|
var cached;
|
|
1660
1660
|
function substrateSelfVersion() {
|
|
1661
1661
|
if (cached !== void 0) return cached;
|
|
1662
|
-
if ("0.2.
|
|
1663
|
-
cached = "0.2.
|
|
1662
|
+
if ("0.2.43".length > 0) {
|
|
1663
|
+
cached = "0.2.43";
|
|
1664
1664
|
return cached;
|
|
1665
1665
|
}
|
|
1666
1666
|
cached = "unknown";
|
|
@@ -1895,7 +1895,9 @@ function resolveProfile(opts) {
|
|
|
1895
1895
|
function buildInvocation(args, opts) {
|
|
1896
1896
|
const base = opts.env ?? process.env;
|
|
1897
1897
|
const trimmedHost = effectiveHost(opts)?.replace(/\/+$/, "");
|
|
1898
|
-
const env =
|
|
1898
|
+
const env = { ...base };
|
|
1899
|
+
if (trimmedHost) env.DATABRICKS_HOST = trimmedHost;
|
|
1900
|
+
delete env.DATABRICKS_WORKSPACE_ID;
|
|
1899
1901
|
const profile = resolveProfile(opts);
|
|
1900
1902
|
const argv = profile && !opts.noProfile && !args.includes("--profile") ? [...args, "--profile", profile] : args;
|
|
1901
1903
|
return { argv, env, profile };
|
|
@@ -1630,8 +1630,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
1630
1630
|
var cached;
|
|
1631
1631
|
function substrateSelfVersion() {
|
|
1632
1632
|
if (cached !== void 0) return cached;
|
|
1633
|
-
if ("0.2.
|
|
1634
|
-
cached = "0.2.
|
|
1633
|
+
if ("0.2.43".length > 0) {
|
|
1634
|
+
cached = "0.2.43";
|
|
1635
1635
|
return cached;
|
|
1636
1636
|
}
|
|
1637
1637
|
cached = "unknown";
|
|
@@ -1866,7 +1866,9 @@ function resolveProfile(opts) {
|
|
|
1866
1866
|
function buildInvocation(args, opts) {
|
|
1867
1867
|
const base = opts.env ?? process.env;
|
|
1868
1868
|
const trimmedHost = effectiveHost(opts)?.replace(/\/+$/, "");
|
|
1869
|
-
const env =
|
|
1869
|
+
const env = { ...base };
|
|
1870
|
+
if (trimmedHost) env.DATABRICKS_HOST = trimmedHost;
|
|
1871
|
+
delete env.DATABRICKS_WORKSPACE_ID;
|
|
1870
1872
|
const profile = resolveProfile(opts);
|
|
1871
1873
|
const argv = profile && !opts.noProfile && !args.includes("--profile") ? [...args, "--profile", profile] : args;
|
|
1872
1874
|
return { argv, env, profile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@databricks-solutions/lakebase-scm-utils",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.43",
|
|
4
4
|
"description": "Lakebase SCM + substrate engine: database branching, paired-branch SCM workflows, connection/credential minting, schema migration, project scaffold + deploy primitives, and the git/github utility layer. Extracted from consort for portable, standalone consumption by lakebase-scm-extension and the SFTDD orchestration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -149,7 +149,13 @@ export function buildInvocation(args: string[], opts: DatabricksCliOptions): {
|
|
|
149
149
|
// so an in-project call pins the project workspace rather than the DEFAULT
|
|
150
150
|
// profile's default host. Trailing slash stripped for a stable value.
|
|
151
151
|
const trimmedHost = effectiveHost(opts)?.replace(/\/+$/, "");
|
|
152
|
-
const env: NodeJS.ProcessEnv =
|
|
152
|
+
const env: NodeJS.ProcessEnv = { ...base };
|
|
153
|
+
if (trimmedHost) env.DATABRICKS_HOST = trimmedHost;
|
|
154
|
+
// A stray DATABRICKS_WORKSPACE_ID (a bare numeric workspace ID, never a URL)
|
|
155
|
+
// makes the CLI's unified-host path dial https://<id> (issue #204.1). The
|
|
156
|
+
// workspace always resolves via the profile/host chain above, so scrub the
|
|
157
|
+
// stray var from the child env (the documented workaround, centralized).
|
|
158
|
+
delete env.DATABRICKS_WORKSPACE_ID;
|
|
153
159
|
const profile = resolveProfile(opts);
|
|
154
160
|
const argv =
|
|
155
161
|
profile && !opts.noProfile && !args.includes("--profile")
|
|
@@ -225,12 +225,27 @@ function runTestsE2eBlock(): string {
|
|
|
225
225
|
' export VITE_PROXY_TARGET="http://127.0.0.1:${E2E_BACKEND_PORT}"',
|
|
226
226
|
' echo "Local E2E free-port: backend :$E2E_BACKEND_PORT / client :$E2E_CLIENT_PORT"',
|
|
227
227
|
'fi',
|
|
228
|
+
// Stale-server hygiene (issue #197): CI=1 on the Playwright invocations below
|
|
229
|
+
// forces EVERY config vintage's reuseExistingServer guard OFF (old `!CI` and
|
|
230
|
+
// new `!!BASE_URL` alike), so the verify never reuses a stale server bound to
|
|
231
|
+
// an unmigrated branch (the false-500 misdiagnosis). A lingering uvicorn/vite
|
|
232
|
+
// on the conventional default ports still serves STALE schema to anything
|
|
233
|
+
// that reaches it, so clear it (TERM, not KILL) before the boot - never when
|
|
234
|
+
// the caller pre-set the ports (the deploy gate owns that server; BASE_URL).
|
|
235
|
+
'if [ -z "${BASE_URL:-}" ] && command -v lsof >/dev/null 2>&1; then',
|
|
236
|
+
' for p in "${E2E_BACKEND_PORT:-8000}" "${E2E_CLIENT_PORT:-5173}"; do',
|
|
237
|
+
' if lsof -iTCP:"$p" -sTCP:LISTEN -n -P >/dev/null 2>&1; then',
|
|
238
|
+
' echo "Local E2E: clearing a stale listener on :$p before the webServer boot"',
|
|
239
|
+
' lsof -ti tcp:"$p" 2>/dev/null | xargs kill 2>/dev/null || true',
|
|
240
|
+
' fi',
|
|
241
|
+
' done',
|
|
242
|
+
'fi',
|
|
228
243
|
'if [ -f "$REPO_ROOT/playwright.config.ts" ] || [ -f "$REPO_ROOT/playwright.config.js" ]; then',
|
|
229
244
|
' echo "Running Playwright E2E tests..."',
|
|
230
245
|
' if [ -f "$REPO_ROOT/package.json" ] && command -v npm >/dev/null 2>&1; then',
|
|
231
|
-
' (cd "$REPO_ROOT" && npm run test:e2e)',
|
|
246
|
+
' (cd "$REPO_ROOT" && CI=1 npm run test:e2e)',
|
|
232
247
|
" else",
|
|
233
|
-
' (cd "$REPO_ROOT" && npx --yes playwright test)',
|
|
248
|
+
' (cd "$REPO_ROOT" && CI=1 npx --yes playwright test)',
|
|
234
249
|
" fi",
|
|
235
250
|
// Python E2E: pytest-playwright + the shipped tests/e2e/conftest.py
|
|
236
251
|
// (live_server). Gated on the conftest + pyproject so it only fires for a
|
|
@@ -269,7 +284,8 @@ function runTestsE2eBlock(): string {
|
|
|
269
284
|
'if [ -f "$REPO_ROOT/client/playwright.config.ts" ] || [ -f "$REPO_ROOT/client/playwright.config.js" ] || [ -f "$REPO_ROOT/client/playwright.config.mjs" ]; then',
|
|
270
285
|
' echo "Running client Playwright E2E tests..."',
|
|
271
286
|
' (cd "$REPO_ROOT/client" && npx --yes playwright install chromium)',
|
|
272
|
-
|
|
287
|
+
// CI=1: no server reuse on any config vintage (see the stale-server note above).
|
|
288
|
+
' (cd "$REPO_ROOT/client" && CI=1 npx --yes playwright test --pass-with-no-tests)',
|
|
273
289
|
"fi",
|
|
274
290
|
"",
|
|
275
291
|
].join("\n");
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
// an error rather than performing a related-but-different remediation.
|
|
10
10
|
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
|
-
import { execFileSync } from "node:child_process";
|
|
13
12
|
import * as path from "node:path";
|
|
14
13
|
import {
|
|
15
14
|
getBranchByName,
|
|
@@ -25,6 +24,7 @@ import { recoverOrphans } from "./scm-recover-orphans.js";
|
|
|
25
24
|
import { abandonFeatureBranch } from "./scm-abandon-feature.js";
|
|
26
25
|
import {
|
|
27
26
|
readWorkflowState,
|
|
27
|
+
isGitTracked,
|
|
28
28
|
type ScmWorkflowState,
|
|
29
29
|
type TierTopology,
|
|
30
30
|
} from "./scm-workflow-state.js";
|
|
@@ -106,17 +106,6 @@ function leafOf(b: LakebaseBranchInfo): string {
|
|
|
106
106
|
return b.name.split("/").pop() ?? b.name;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/** True iff `rel` (repo-relative) is git-tracked in projectDir. Best-effort: no
|
|
110
|
-
* git / not a repo -> false. */
|
|
111
|
-
function isGitTracked(projectDir: string, rel: string): boolean {
|
|
112
|
-
try {
|
|
113
|
-
execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
114
|
-
return true;
|
|
115
|
-
} catch {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
109
|
function worstOf(a: DoctorSeverity, b: DoctorSeverity): DoctorSeverity {
|
|
121
110
|
const order: DoctorSeverity[] = ["ok", "warn", "fail"];
|
|
122
111
|
return order[Math.max(order.indexOf(a), order.indexOf(b))] as DoctorSeverity;
|
|
@@ -21,6 +21,35 @@ import { pollUntil } from "../util/poll-until.js";
|
|
|
21
21
|
import { exec } from "../util/exec.js";
|
|
22
22
|
import { getCurrentBranch } from "../git/inspect.js";
|
|
23
23
|
import { resolveGitBase } from "./scm-git-base.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fast-forward the local tier ref to origin/<tier> after a remote merge, WITHOUT
|
|
27
|
+
* needing a checkout. A promote interrupted after the remote merge (e.g. a
|
|
28
|
+
* downstream migrate failure) otherwise leaves the local tier BEHIND, so the
|
|
29
|
+
* next sprint plans on stale code and the next push rejects non-fast-forward.
|
|
30
|
+
* Mechanism by head position: when HEAD IS the tier, `git pull --ff-only` syncs
|
|
31
|
+
* ref + working tree; otherwise `git fetch origin <tier>:<tier>` fast-forwards
|
|
32
|
+
* the local ref in place (git refuses fetch-into-current-branch, hence the
|
|
33
|
+
* split). Returns null on success; a warning string when the sync fails (e.g.
|
|
34
|
+
* not a fast-forward because the local tier has its own commits), never throws.
|
|
35
|
+
*/
|
|
36
|
+
export async function reconcileTierToOrigin(args: { cwd: string; tier: string }): Promise<string | null> {
|
|
37
|
+
const { cwd, tier } = args;
|
|
38
|
+
try {
|
|
39
|
+
const head = await getCurrentBranch({ cwd });
|
|
40
|
+
if (head === tier) {
|
|
41
|
+
await exec(`git pull --ff-only`, { cwd, timeout: 30_000 });
|
|
42
|
+
} else {
|
|
43
|
+
await exec(`git fetch origin ${shellEscape(`${tier}:${tier}`)}`, { cwd, timeout: 30_000 });
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return (
|
|
48
|
+
`local reconcile of ${tier} to origin/${tier} failed: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
49
|
+
`Reconcile it by hand (git checkout ${tier} && git pull --ff-only).`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
24
53
|
import {
|
|
25
54
|
readWorkflowState,
|
|
26
55
|
writeWorkflowState,
|
|
@@ -328,6 +357,13 @@ export async function mergeFeature(args: MergeArgs): Promise<MergeResult> {
|
|
|
328
357
|
}
|
|
329
358
|
} else {
|
|
330
359
|
headAfter = head || current.branch;
|
|
360
|
+
// HEAD isn't the feature branch, so the checkout+ff path above didn't run.
|
|
361
|
+
// Reconcile the local tier ref to origin anyway (no checkout needed): a
|
|
362
|
+
// promote interrupted after the remote merge must not leave the tier behind
|
|
363
|
+
// (the stockflow F1 case: local staging missed the merge, the next push
|
|
364
|
+
// rejected non-fast-forward). Best-effort; a failure is a warning, not a throw.
|
|
365
|
+
const reconcileWarning = await reconcileTierToOrigin({ cwd: args.projectDir, tier: switchTo });
|
|
366
|
+
if (reconcileWarning) warnings.push(reconcileWarning);
|
|
331
367
|
}
|
|
332
368
|
if (headAfter !== current.branch) {
|
|
333
369
|
try {
|
|
@@ -13,6 +13,52 @@
|
|
|
13
13
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
/** True iff `rel` (repo-relative) is git-tracked in projectDir. Best-effort: no
|
|
19
|
+
* git / not a repo -> false. Shared home (scm-doctor's Finding-28 check reads
|
|
20
|
+
* the same predicate). */
|
|
21
|
+
export function isGitTracked(projectDir: string, rel: string): boolean {
|
|
22
|
+
try {
|
|
23
|
+
execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Ensure the runtime SCM claim state is NOT git-tracked (issue #203 / Finding 28):
|
|
31
|
+
* a tracked .lakebase/workflow-state.json gets a branch-COMMITTED stale claim
|
|
32
|
+
* restored over the live one by every branch checkout / `git reset --hard
|
|
33
|
+
* origin/<tier>`, which then blocks the next feature's claim
|
|
34
|
+
* ("already-claimed-other") until a manual abandon+reclaim. Untrack it
|
|
35
|
+
* (index-only; the working file stays) and cover it in .gitignore, so the live
|
|
36
|
+
* claim survives checkouts. The committed .lakebase/kit-ref + scm-utils-ref stay
|
|
37
|
+
* tracked deliberately (CI resolves them). Idempotent + best-effort (non-git
|
|
38
|
+
* dirs / unwritable .gitignore skip silently – the state write itself still lands). */
|
|
39
|
+
function ensureWorkflowStateUntracked(projectDir: string): void {
|
|
40
|
+
const rel = ".lakebase/workflow-state.json";
|
|
41
|
+
try {
|
|
42
|
+
if (isGitTracked(projectDir, rel)) {
|
|
43
|
+
execFileSync("git", ["rm", "--cached", "--quiet", "--ignore-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
/* best-effort */
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const gitignore = path.join(projectDir, ".gitignore");
|
|
50
|
+
const existing = fs.existsSync(gitignore) ? fs.readFileSync(gitignore, "utf8") : "";
|
|
51
|
+
if (!existing.split("\n").some((l) => l.trim() === rel)) {
|
|
52
|
+
const sep = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
53
|
+
fs.appendFileSync(
|
|
54
|
+
gitignore,
|
|
55
|
+
`${sep}# Runtime SCM claim state (per working tree): a branch checkout must never restore\n# a stale committed claim over the live one (issue #203 / Finding 28).\n${rel}\n`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
/* best-effort */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
16
62
|
|
|
17
63
|
/** All SCM states, in canonical progression order. */
|
|
18
64
|
export const SCM_STATES = [
|
|
@@ -133,6 +179,9 @@ export function writeWorkflowState(
|
|
|
133
179
|
.join("\n");
|
|
134
180
|
throw new Error(`Refusing to write invalid SCM state:\n${summary}`);
|
|
135
181
|
}
|
|
182
|
+
// Self-heal on every write (issue #203): the live claim survives checkouts
|
|
183
|
+
// only while the state file is untracked.
|
|
184
|
+
ensureWorkflowStateUntracked(projectDir);
|
|
136
185
|
const dir = path.join(projectDir, ".lakebase");
|
|
137
186
|
fs.mkdirSync(dir, { recursive: true });
|
|
138
187
|
const target = stateFilePath(projectDir);
|
|
@@ -74,14 +74,25 @@ jobs:
|
|
|
74
74
|
SCM_UTILS_REF="$(tr -d '[:space:]' < .lakebase/scm-utils-ref 2>/dev/null || true)"
|
|
75
75
|
SCM_UTILS_REF="${SCM_UTILS_REF:-v{{LAKEBASE_SCM_UTILS_VERSION}}}"
|
|
76
76
|
echo "SCM_UTILS_REF=${SCM_UTILS_REF}" >> "$GITHUB_ENV"
|
|
77
|
-
# Registry-first
|
|
78
|
-
#
|
|
79
|
-
# (
|
|
77
|
+
# Registry-first for version tags, with a GitHub fallback when the registry
|
|
78
|
+
# cannot serve the tarball yet: a freshly-published version is blocked by
|
|
79
|
+
# the proxy's same-day security screen for ~24h (a staging merge then fails
|
|
80
|
+
# on npx ETARGET), and a branch/SHA ref only ever has GitHub. Probe the
|
|
81
|
+
# tarball URL once and pick the source that actually serves it, so a
|
|
82
|
+
# same-day publish never stalls CI.
|
|
83
|
+
SPEC=""
|
|
80
84
|
case "$SCM_UTILS_REF" in
|
|
81
|
-
v[0-9]*)
|
|
82
|
-
[0-9]*) echo "SCM_UTILS_NPX_PKG=@databricks-solutions/lakebase-scm-utils@${SCM_UTILS_REF}" >> "$GITHUB_ENV" ;;
|
|
83
|
-
*) echo "SCM_UTILS_NPX_PKG=github:databricks-solutions/lakebase-scm-utils#${SCM_UTILS_REF}" >> "$GITHUB_ENV" ;;
|
|
85
|
+
v[0-9]*|[0-9]*) SPEC="@databricks-solutions/lakebase-scm-utils@${SCM_UTILS_REF#v}" ;;
|
|
84
86
|
esac
|
|
87
|
+
if [ -n "$SPEC" ]; then
|
|
88
|
+
TARBALL_URL="$(npm view "${SPEC}" dist.tarball 2>/dev/null || true)"
|
|
89
|
+
if [ -z "$TARBALL_URL" ] || ! curl -fsSI --max-time 15 "$TARBALL_URL" >/dev/null 2>&1; then
|
|
90
|
+
echo "resolve-substrate: registry cannot serve ${SPEC} yet (same-day publish screen); falling back to GitHub."
|
|
91
|
+
SPEC=""
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
[ -z "$SPEC" ] && SPEC="github:databricks-solutions/lakebase-scm-utils#${SCM_UTILS_REF}"
|
|
95
|
+
echo "SCM_UTILS_NPX_PKG=${SPEC}" >> "$GITHUB_ENV"
|
|
85
96
|
|
|
86
97
|
- name: Detect project language
|
|
87
98
|
id: detect-lang
|
|
@@ -33,14 +33,25 @@ jobs:
|
|
|
33
33
|
SCM_UTILS_REF="$(tr -d '[:space:]' < .lakebase/scm-utils-ref 2>/dev/null || true)"
|
|
34
34
|
SCM_UTILS_REF="${SCM_UTILS_REF:-v{{LAKEBASE_SCM_UTILS_VERSION}}}"
|
|
35
35
|
echo "SCM_UTILS_REF=${SCM_UTILS_REF}" >> "$GITHUB_ENV"
|
|
36
|
-
# Registry-first
|
|
37
|
-
#
|
|
38
|
-
# (
|
|
36
|
+
# Registry-first for version tags, with a GitHub fallback when the registry
|
|
37
|
+
# cannot serve the tarball yet: a freshly-published version is blocked by
|
|
38
|
+
# the proxy's same-day security screen for ~24h (a staging merge then fails
|
|
39
|
+
# on npx ETARGET), and a branch/SHA ref only ever has GitHub. Probe the
|
|
40
|
+
# tarball URL once and pick the source that actually serves it, so a
|
|
41
|
+
# same-day publish never stalls CI.
|
|
42
|
+
SPEC=""
|
|
39
43
|
case "$SCM_UTILS_REF" in
|
|
40
|
-
v[0-9]*)
|
|
41
|
-
[0-9]*) echo "SCM_UTILS_NPX_PKG=@databricks-solutions/lakebase-scm-utils@${SCM_UTILS_REF}" >> "$GITHUB_ENV" ;;
|
|
42
|
-
*) echo "SCM_UTILS_NPX_PKG=github:databricks-solutions/lakebase-scm-utils#${SCM_UTILS_REF}" >> "$GITHUB_ENV" ;;
|
|
44
|
+
v[0-9]*|[0-9]*) SPEC="@databricks-solutions/lakebase-scm-utils@${SCM_UTILS_REF#v}" ;;
|
|
43
45
|
esac
|
|
46
|
+
if [ -n "$SPEC" ]; then
|
|
47
|
+
TARBALL_URL="$(npm view "${SPEC}" dist.tarball 2>/dev/null || true)"
|
|
48
|
+
if [ -z "$TARBALL_URL" ] || ! curl -fsSI --max-time 15 "$TARBALL_URL" >/dev/null 2>&1; then
|
|
49
|
+
echo "resolve-substrate: registry cannot serve ${SPEC} yet (same-day publish screen); falling back to GitHub."
|
|
50
|
+
SPEC=""
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
[ -z "$SPEC" ] && SPEC="github:databricks-solutions/lakebase-scm-utils#${SCM_UTILS_REF}"
|
|
54
|
+
echo "SCM_UTILS_NPX_PKG=${SPEC}" >> "$GITHUB_ENV"
|
|
44
55
|
|
|
45
56
|
- name: Detect project language
|
|
46
57
|
id: detect-lang
|
|
@@ -58,6 +58,14 @@ application-local.properties
|
|
|
58
58
|
.lakebase/kit-ref.local
|
|
59
59
|
.lakebase/scm-utils-ref.local
|
|
60
60
|
|
|
61
|
+
# The runtime SCM CLAIM state: per working tree, rewritten by every SCM transition
|
|
62
|
+
# (claim / pr-ready / ci-green / merged). If it is git-tracked, a branch checkout
|
|
63
|
+
# or `git reset --hard origin/<tier>` restores a branch-COMMITTED stale claim over
|
|
64
|
+
# the live one, and the next feature's claim then refuses "already-claimed-other"
|
|
65
|
+
# until a manual abandon+reclaim (issue #203 / Finding 28). Never track it; the
|
|
66
|
+
# committed kit-ref / scm-utils-ref pins above stay tracked deliberately.
|
|
67
|
+
.lakebase/workflow-state.json
|
|
68
|
+
|
|
61
69
|
# Per-agent local memory: each role subagent (memory: project) persists its own
|
|
62
70
|
# learnings here mid-run. It is local + churny (re-written every run), not feature
|
|
63
71
|
# code, so it must never be committed nor count as a dirty working tree , an
|
|
@@ -213,6 +213,20 @@ ensure_local_e2e_ports() {
|
|
|
213
213
|
export VITE_PROXY_TARGET="http://127.0.0.1:${E2E_BACKEND_PORT}"
|
|
214
214
|
echo "Local E2E free-port: backend :$E2E_BACKEND_PORT / client :$E2E_CLIENT_PORT"
|
|
215
215
|
fi
|
|
216
|
+
# Stale-server hygiene (issue #197): CI=1 (on the Playwright invocation below)
|
|
217
|
+
# means the webServer never REUSES a stale app, and free_port moved us off any
|
|
218
|
+
# collision - but a lingering uvicorn/vite on the conventional default ports (a
|
|
219
|
+
# prior E2E run, or a deploy whose teardown missed) still serves STALE schema to
|
|
220
|
+
# anything that reaches it. Clear it (TERM, not KILL) before the boot. Never
|
|
221
|
+
# when the caller pre-set the ports (the deploy gate owns that server).
|
|
222
|
+
if [ -z "${BASE_URL:-}" ] && command -v lsof >/dev/null 2>&1; then
|
|
223
|
+
for p in "${E2E_BACKEND_PORT:-8000}" "${E2E_CLIENT_PORT:-5173}"; do
|
|
224
|
+
if lsof -iTCP:"$p" -sTCP:LISTEN -n -P >/dev/null 2>&1; then
|
|
225
|
+
echo "Local E2E: clearing a stale listener on :$p before the webServer boot"
|
|
226
|
+
lsof -ti tcp:"$p" 2>/dev/null | xargs kill 2>/dev/null || true
|
|
227
|
+
fi
|
|
228
|
+
done
|
|
229
|
+
fi
|
|
216
230
|
}
|
|
217
231
|
|
|
218
232
|
if [ "$#" -eq 0 ] && [ -d "$REPO_ROOT/client/tests/e2e" ] \
|
|
@@ -240,8 +254,11 @@ if [ "$#" -eq 0 ] && [ -d "$REPO_ROOT/client/tests/e2e" ] \
|
|
|
240
254
|
# Use the client's OWN playwright bin (guaranteed present by the check/install above), not npx,
|
|
241
255
|
# so browser install + the run are deterministic under the deploy gate. install chromium first
|
|
242
256
|
# (playwright test does not auto-install browsers); a failing E2E fails the run (set -e).
|
|
257
|
+
# CI=1: force EVERY playwright.config vintage's reuseExistingServer guard OFF
|
|
258
|
+
# (old `!CI` and new `!!BASE_URL` alike) so the verify never reuses a stale
|
|
259
|
+
# server bound to an unmigrated branch (issue #197's false-500 misdiagnosis).
|
|
243
260
|
ensure_local_e2e_ports
|
|
244
|
-
( cd "$REPO_ROOT/client" && ./node_modules/.bin/playwright install chromium && npm run test:e2e )
|
|
261
|
+
( cd "$REPO_ROOT/client" && ./node_modules/.bin/playwright install chromium && CI=1 npm run test:e2e )
|
|
245
262
|
fi
|
|
246
263
|
fi
|
|
247
264
|
|
|
@@ -41,9 +41,16 @@ def _restore_migration_head_after_each_test():
|
|
|
41
41
|
|
|
42
42
|
from alembic import command
|
|
43
43
|
from alembic.config import Config
|
|
44
|
+
from alembic.util.exc import CommandError
|
|
45
|
+
from sqlalchemy.exc import OperationalError
|
|
44
46
|
|
|
45
47
|
ini = str(Path(__file__).resolve().parent.parent / "alembic.ini")
|
|
46
48
|
command.upgrade(Config(ini), "head")
|
|
47
|
-
except
|
|
48
|
-
# Best-effort isolation
|
|
49
|
-
|
|
49
|
+
except (CommandError, OperationalError) as exc:
|
|
50
|
+
# Best-effort isolation, but never SILENT and never an umbrella swallow:
|
|
51
|
+
# surface a restore failure instead of hiding it (a broken migration must
|
|
52
|
+
# be visible, not masked as routine teardown noise). CommandError wraps
|
|
53
|
+
# alembic's own failures; OperationalError covers DBAPI/connection errors.
|
|
54
|
+
# Everything else (a real schema error) propagates and fails loudly, which
|
|
55
|
+
# is the correct behavior for a genuinely broken restore.
|
|
56
|
+
print(f"WARNING: migration-head restore failed during teardown: {exc}")
|