@databricks-solutions/lakebase-scm-utils 0.3.0 → 0.3.2
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 +10 -4
- package/dist/scripts/index.cjs +2336 -2262
- package/dist/scripts/index.js +395 -321
- package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +26 -9
- package/dist/scripts/lakebase/ci-resolve-branch.cli.js +27 -10
- package/dist/scripts/lakebase/collapse-heads.cli.cjs +26 -9
- package/dist/scripts/lakebase/collapse-heads.cli.js +27 -10
- package/dist/scripts/lakebase/detect-language.cli.cjs +26 -9
- package/dist/scripts/lakebase/detect-language.cli.js +27 -10
- package/dist/scripts/lakebase/doctor.cli.cjs +26 -9
- package/dist/scripts/lakebase/doctor.cli.js +27 -10
- package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
- package/dist/scripts/lakebase/get-connection.cli.js +2 -2
- package/dist/scripts/lakebase/index.cjs +2333 -2223
- package/dist/scripts/lakebase/index.js +389 -279
- package/dist/scripts/lakebase/infra-runner.cli.cjs +26 -9
- package/dist/scripts/lakebase/infra-runner.cli.js +27 -10
- package/dist/scripts/lakebase/new-migration.cli.cjs +26 -9
- package/dist/scripts/lakebase/new-migration.cli.js +27 -10
- package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
- package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
- package/dist/scripts/lakebase/schema-migrate.cli.cjs +26 -9
- package/dist/scripts/lakebase/schema-migrate.cli.js +27 -10
- package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +26 -9
- package/dist/scripts/lakebase/scm-claim-feature.cli.js +27 -10
- package/dist/scripts/lakebase/scm-doctor.cli.cjs +26 -9
- package/dist/scripts/lakebase/scm-doctor.cli.js +27 -10
- package/dist/scripts/lakebase/scm-merge.cli.cjs +26 -9
- package/dist/scripts/lakebase/scm-merge.cli.js +27 -10
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +26 -9
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +28 -11
- package/dist/scripts/util/index.cjs +2 -2
- package/dist/scripts/util/index.js +2 -2
- package/package.json +1 -1
- package/scripts/lakebase/client-lockfile.ts +102 -0
- package/scripts/lakebase/create-project.ts +19 -0
- package/scripts/lakebase/schema-migrate-runners/alembic.ts +58 -14
- package/templates/project/client/playwright.config.ts +35 -10
|
@@ -88825,8 +88825,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
88825
88825
|
var cached;
|
|
88826
88826
|
function substrateSelfVersion() {
|
|
88827
88827
|
if (cached !== void 0) return cached;
|
|
88828
|
-
if ("0.3.
|
|
88829
|
-
cached = "0.3.
|
|
88828
|
+
if ("0.3.2".length > 0) {
|
|
88829
|
+
cached = "0.3.2";
|
|
88830
88830
|
return cached;
|
|
88831
88831
|
}
|
|
88832
88832
|
cached = "unknown";
|
|
@@ -89010,26 +89010,43 @@ init_cjs_shims();
|
|
|
89010
89010
|
var import_node_child_process5 = require("child_process");
|
|
89011
89011
|
var fs8 = __toESM(require("fs"), 1);
|
|
89012
89012
|
var path7 = __toESM(require("path"), 1);
|
|
89013
|
-
|
|
89014
|
-
|
|
89013
|
+
var uvOnPath;
|
|
89014
|
+
function hasUv() {
|
|
89015
|
+
if (uvOnPath !== void 0) return uvOnPath;
|
|
89016
|
+
try {
|
|
89017
|
+
(0, import_node_child_process5.execFileSync)("uv", ["--version"], { stdio: "ignore" });
|
|
89018
|
+
uvOnPath = true;
|
|
89019
|
+
} catch {
|
|
89020
|
+
uvOnPath = false;
|
|
89021
|
+
}
|
|
89022
|
+
return uvOnPath;
|
|
89023
|
+
}
|
|
89024
|
+
function resolveAlembicInvocation(projectDir, uvAvailable = hasUv()) {
|
|
89025
|
+
const venvCandidates = [
|
|
89015
89026
|
path7.join(projectDir, ".venv", "bin", "alembic"),
|
|
89016
89027
|
path7.join(projectDir, "venv", "bin", "alembic")
|
|
89017
89028
|
];
|
|
89018
|
-
for (const candidate of
|
|
89029
|
+
for (const candidate of venvCandidates) {
|
|
89019
89030
|
try {
|
|
89020
|
-
if (fs8.existsSync(candidate)) return candidate;
|
|
89031
|
+
if (fs8.existsSync(candidate)) return { bin: candidate, prefixArgs: [] };
|
|
89021
89032
|
} catch {
|
|
89022
89033
|
}
|
|
89023
89034
|
}
|
|
89024
|
-
|
|
89035
|
+
try {
|
|
89036
|
+
if (uvAvailable && fs8.existsSync(path7.join(projectDir, "pyproject.toml"))) {
|
|
89037
|
+
return { bin: "uv", prefixArgs: ["run", "alembic"] };
|
|
89038
|
+
}
|
|
89039
|
+
} catch {
|
|
89040
|
+
}
|
|
89041
|
+
return { bin: "alembic", prefixArgs: [] };
|
|
89025
89042
|
}
|
|
89026
89043
|
function spawnAlembic(projectDir, args, dsn) {
|
|
89027
89044
|
return new Promise((resolve2, reject) => {
|
|
89028
|
-
const bin =
|
|
89045
|
+
const { bin, prefixArgs } = resolveAlembicInvocation(projectDir);
|
|
89029
89046
|
const env = { ...process.env };
|
|
89030
89047
|
env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path7.delimiter);
|
|
89031
89048
|
if (dsn) env.DATABASE_URL = dsn;
|
|
89032
|
-
const child = (0, import_node_child_process5.spawn)(bin, args, {
|
|
89049
|
+
const child = (0, import_node_child_process5.spawn)(bin, [...prefixArgs, ...args], {
|
|
89033
89050
|
cwd: projectDir,
|
|
89034
89051
|
env,
|
|
89035
89052
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -9978,8 +9978,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
9978
9978
|
var cached;
|
|
9979
9979
|
function substrateSelfVersion() {
|
|
9980
9980
|
if (cached !== void 0) return cached;
|
|
9981
|
-
if ("0.3.
|
|
9982
|
-
cached = "0.3.
|
|
9981
|
+
if ("0.3.2".length > 0) {
|
|
9982
|
+
cached = "0.3.2";
|
|
9983
9983
|
return cached;
|
|
9984
9984
|
}
|
|
9985
9985
|
cached = "unknown";
|
|
@@ -10160,29 +10160,46 @@ import * as path9 from "path";
|
|
|
10160
10160
|
|
|
10161
10161
|
// scripts/lakebase/schema-migrate-runners/alembic.ts
|
|
10162
10162
|
init_esm_shims();
|
|
10163
|
-
import { spawn } from "child_process";
|
|
10163
|
+
import { spawn, execFileSync as execFileSync5 } from "child_process";
|
|
10164
10164
|
import * as fs7 from "fs";
|
|
10165
10165
|
import * as path8 from "path";
|
|
10166
|
-
|
|
10167
|
-
|
|
10166
|
+
var uvOnPath;
|
|
10167
|
+
function hasUv() {
|
|
10168
|
+
if (uvOnPath !== void 0) return uvOnPath;
|
|
10169
|
+
try {
|
|
10170
|
+
execFileSync5("uv", ["--version"], { stdio: "ignore" });
|
|
10171
|
+
uvOnPath = true;
|
|
10172
|
+
} catch {
|
|
10173
|
+
uvOnPath = false;
|
|
10174
|
+
}
|
|
10175
|
+
return uvOnPath;
|
|
10176
|
+
}
|
|
10177
|
+
function resolveAlembicInvocation(projectDir, uvAvailable = hasUv()) {
|
|
10178
|
+
const venvCandidates = [
|
|
10168
10179
|
path8.join(projectDir, ".venv", "bin", "alembic"),
|
|
10169
10180
|
path8.join(projectDir, "venv", "bin", "alembic")
|
|
10170
10181
|
];
|
|
10171
|
-
for (const candidate of
|
|
10182
|
+
for (const candidate of venvCandidates) {
|
|
10172
10183
|
try {
|
|
10173
|
-
if (fs7.existsSync(candidate)) return candidate;
|
|
10184
|
+
if (fs7.existsSync(candidate)) return { bin: candidate, prefixArgs: [] };
|
|
10174
10185
|
} catch {
|
|
10175
10186
|
}
|
|
10176
10187
|
}
|
|
10177
|
-
|
|
10188
|
+
try {
|
|
10189
|
+
if (uvAvailable && fs7.existsSync(path8.join(projectDir, "pyproject.toml"))) {
|
|
10190
|
+
return { bin: "uv", prefixArgs: ["run", "alembic"] };
|
|
10191
|
+
}
|
|
10192
|
+
} catch {
|
|
10193
|
+
}
|
|
10194
|
+
return { bin: "alembic", prefixArgs: [] };
|
|
10178
10195
|
}
|
|
10179
10196
|
function spawnAlembic(projectDir, args, dsn) {
|
|
10180
10197
|
return new Promise((resolve2, reject) => {
|
|
10181
|
-
const bin =
|
|
10198
|
+
const { bin, prefixArgs } = resolveAlembicInvocation(projectDir);
|
|
10182
10199
|
const env = { ...process.env };
|
|
10183
10200
|
env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path8.delimiter);
|
|
10184
10201
|
if (dsn) env.DATABASE_URL = dsn;
|
|
10185
|
-
const child = spawn(bin, args, {
|
|
10202
|
+
const child = spawn(bin, [...prefixArgs, ...args], {
|
|
10186
10203
|
cwd: projectDir,
|
|
10187
10204
|
env,
|
|
10188
10205
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -79288,8 +79288,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
79288
79288
|
var cached;
|
|
79289
79289
|
function substrateSelfVersion() {
|
|
79290
79290
|
if (cached !== void 0) return cached;
|
|
79291
|
-
if ("0.3.
|
|
79292
|
-
cached = "0.3.
|
|
79291
|
+
if ("0.3.2".length > 0) {
|
|
79292
|
+
cached = "0.3.2";
|
|
79293
79293
|
return cached;
|
|
79294
79294
|
}
|
|
79295
79295
|
cached = "unknown";
|
|
@@ -79547,26 +79547,43 @@ init_cjs_shims();
|
|
|
79547
79547
|
var import_node_child_process3 = require("child_process");
|
|
79548
79548
|
var fs7 = __toESM(require("fs"), 1);
|
|
79549
79549
|
var path5 = __toESM(require("path"), 1);
|
|
79550
|
-
|
|
79551
|
-
|
|
79550
|
+
var uvOnPath;
|
|
79551
|
+
function hasUv() {
|
|
79552
|
+
if (uvOnPath !== void 0) return uvOnPath;
|
|
79553
|
+
try {
|
|
79554
|
+
(0, import_node_child_process3.execFileSync)("uv", ["--version"], { stdio: "ignore" });
|
|
79555
|
+
uvOnPath = true;
|
|
79556
|
+
} catch {
|
|
79557
|
+
uvOnPath = false;
|
|
79558
|
+
}
|
|
79559
|
+
return uvOnPath;
|
|
79560
|
+
}
|
|
79561
|
+
function resolveAlembicInvocation(projectDir, uvAvailable = hasUv()) {
|
|
79562
|
+
const venvCandidates = [
|
|
79552
79563
|
path5.join(projectDir, ".venv", "bin", "alembic"),
|
|
79553
79564
|
path5.join(projectDir, "venv", "bin", "alembic")
|
|
79554
79565
|
];
|
|
79555
|
-
for (const candidate of
|
|
79566
|
+
for (const candidate of venvCandidates) {
|
|
79556
79567
|
try {
|
|
79557
|
-
if (fs7.existsSync(candidate)) return candidate;
|
|
79568
|
+
if (fs7.existsSync(candidate)) return { bin: candidate, prefixArgs: [] };
|
|
79558
79569
|
} catch {
|
|
79559
79570
|
}
|
|
79560
79571
|
}
|
|
79561
|
-
|
|
79572
|
+
try {
|
|
79573
|
+
if (uvAvailable && fs7.existsSync(path5.join(projectDir, "pyproject.toml"))) {
|
|
79574
|
+
return { bin: "uv", prefixArgs: ["run", "alembic"] };
|
|
79575
|
+
}
|
|
79576
|
+
} catch {
|
|
79577
|
+
}
|
|
79578
|
+
return { bin: "alembic", prefixArgs: [] };
|
|
79562
79579
|
}
|
|
79563
79580
|
function spawnAlembic(projectDir, args, dsn) {
|
|
79564
79581
|
return new Promise((resolve2, reject) => {
|
|
79565
|
-
const bin =
|
|
79582
|
+
const { bin, prefixArgs } = resolveAlembicInvocation(projectDir);
|
|
79566
79583
|
const env = { ...process.env };
|
|
79567
79584
|
env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path5.delimiter);
|
|
79568
79585
|
if (dsn) env.DATABASE_URL = dsn;
|
|
79569
|
-
const child = (0, import_node_child_process3.spawn)(bin, args, {
|
|
79586
|
+
const child = (0, import_node_child_process3.spawn)(bin, [...prefixArgs, ...args], {
|
|
79570
79587
|
cwd: projectDir,
|
|
79571
79588
|
env,
|
|
79572
79589
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -389,8 +389,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
389
389
|
var cached;
|
|
390
390
|
function substrateSelfVersion() {
|
|
391
391
|
if (cached !== void 0) return cached;
|
|
392
|
-
if ("0.3.
|
|
393
|
-
cached = "0.3.
|
|
392
|
+
if ("0.3.2".length > 0) {
|
|
393
|
+
cached = "0.3.2";
|
|
394
394
|
return cached;
|
|
395
395
|
}
|
|
396
396
|
cached = "unknown";
|
|
@@ -640,29 +640,46 @@ import * as fs7 from "fs";
|
|
|
640
640
|
import * as path6 from "path";
|
|
641
641
|
|
|
642
642
|
// scripts/lakebase/schema-migrate-runners/alembic.ts
|
|
643
|
-
import { spawn } from "child_process";
|
|
643
|
+
import { spawn, execFileSync as execFileSync3 } from "child_process";
|
|
644
644
|
import * as fs6 from "fs";
|
|
645
645
|
import * as path5 from "path";
|
|
646
|
-
|
|
647
|
-
|
|
646
|
+
var uvOnPath;
|
|
647
|
+
function hasUv() {
|
|
648
|
+
if (uvOnPath !== void 0) return uvOnPath;
|
|
649
|
+
try {
|
|
650
|
+
execFileSync3("uv", ["--version"], { stdio: "ignore" });
|
|
651
|
+
uvOnPath = true;
|
|
652
|
+
} catch {
|
|
653
|
+
uvOnPath = false;
|
|
654
|
+
}
|
|
655
|
+
return uvOnPath;
|
|
656
|
+
}
|
|
657
|
+
function resolveAlembicInvocation(projectDir, uvAvailable = hasUv()) {
|
|
658
|
+
const venvCandidates = [
|
|
648
659
|
path5.join(projectDir, ".venv", "bin", "alembic"),
|
|
649
660
|
path5.join(projectDir, "venv", "bin", "alembic")
|
|
650
661
|
];
|
|
651
|
-
for (const candidate of
|
|
662
|
+
for (const candidate of venvCandidates) {
|
|
652
663
|
try {
|
|
653
|
-
if (fs6.existsSync(candidate)) return candidate;
|
|
664
|
+
if (fs6.existsSync(candidate)) return { bin: candidate, prefixArgs: [] };
|
|
654
665
|
} catch {
|
|
655
666
|
}
|
|
656
667
|
}
|
|
657
|
-
|
|
668
|
+
try {
|
|
669
|
+
if (uvAvailable && fs6.existsSync(path5.join(projectDir, "pyproject.toml"))) {
|
|
670
|
+
return { bin: "uv", prefixArgs: ["run", "alembic"] };
|
|
671
|
+
}
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
return { bin: "alembic", prefixArgs: [] };
|
|
658
675
|
}
|
|
659
676
|
function spawnAlembic(projectDir, args, dsn) {
|
|
660
677
|
return new Promise((resolve2, reject) => {
|
|
661
|
-
const bin =
|
|
678
|
+
const { bin, prefixArgs } = resolveAlembicInvocation(projectDir);
|
|
662
679
|
const env = { ...process.env };
|
|
663
680
|
env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path5.delimiter);
|
|
664
681
|
if (dsn) env.DATABASE_URL = dsn;
|
|
665
|
-
const child = spawn(bin, args, {
|
|
682
|
+
const child = spawn(bin, [...prefixArgs, ...args], {
|
|
666
683
|
cwd: projectDir,
|
|
667
684
|
env,
|
|
668
685
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -2137,7 +2154,7 @@ async function reconcileTierBranch(args, deps = {}) {
|
|
|
2137
2154
|
// scripts/lakebase/scm-workflow-state.ts
|
|
2138
2155
|
import * as fs13 from "fs";
|
|
2139
2156
|
import * as path14 from "path";
|
|
2140
|
-
import { execFileSync as
|
|
2157
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
2141
2158
|
var SCM_STATES = [
|
|
2142
2159
|
"scaffold-complete",
|
|
2143
2160
|
"feature-claimed",
|
|
@@ -1667,8 +1667,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
1667
1667
|
var cached;
|
|
1668
1668
|
function substrateSelfVersion() {
|
|
1669
1669
|
if (cached !== void 0) return cached;
|
|
1670
|
-
if ("0.3.
|
|
1671
|
-
cached = "0.3.
|
|
1670
|
+
if ("0.3.2".length > 0) {
|
|
1671
|
+
cached = "0.3.2";
|
|
1672
1672
|
return cached;
|
|
1673
1673
|
}
|
|
1674
1674
|
cached = "unknown";
|
|
@@ -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.3.
|
|
1634
|
-
cached = "0.3.
|
|
1633
|
+
if ("0.3.2".length > 0) {
|
|
1634
|
+
cached = "0.3.2";
|
|
1635
1635
|
return cached;
|
|
1636
1636
|
}
|
|
1637
1637
|
cached = "unknown";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@databricks-solutions/lakebase-scm-utils",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Generate + normalize the client's package-lock.json at scaffold time, so a
|
|
2
|
+
// scaffolded UI project ships a COMMITTED lockfile that resolves from the
|
|
3
|
+
// public npm registry.
|
|
4
|
+
//
|
|
5
|
+
// WHY: createProject writes the client template + wires Playwright but never
|
|
6
|
+
// runs `npm install` in client/, so no package-lock.json exists at the initial
|
|
7
|
+
// commit. The user had to generate it by hand (README: `cd client && npm
|
|
8
|
+
// install`), and when they did it on the Databricks network the lock pinned
|
|
9
|
+
// npm-proxy.cloud.databricks.com in its `resolved` URLs — which hangs a cold
|
|
10
|
+
// install off-network (the lockfile-registry-guard hazard: the proxy host
|
|
11
|
+
// resolves but never accepts a TCP connection off the Databricks network).
|
|
12
|
+
//
|
|
13
|
+
// WHAT: `npm install --package-lock-only` resolves the dep tree and writes
|
|
14
|
+
// client/package-lock.json WITHOUT installing node_modules (fast, no tarball
|
|
15
|
+
// downloads), then we host-swap any Databricks-proxy `resolved` host to
|
|
16
|
+
// registry.npmjs.org (integrity hashes are unchanged — the proxy is a
|
|
17
|
+
// passthrough mirror of the same tarballs). The scaffold's `git add -A` initial
|
|
18
|
+
// commit then picks the normalized lock up for free.
|
|
19
|
+
//
|
|
20
|
+
// Non-fatal: a resolve failure (offline, registry down) is a warning, not a
|
|
21
|
+
// create abort — the user can still `cd client && npm install` as before.
|
|
22
|
+
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import { exec } from "../util/exec.js";
|
|
26
|
+
import { withProxyEnv } from "../util/proxy-env.js";
|
|
27
|
+
|
|
28
|
+
/** The Databricks npm proxy host a cold off-network install hangs on. */
|
|
29
|
+
const PROXY_HOST = "npm-proxy.cloud.databricks.com";
|
|
30
|
+
/** The public registry every tracked lockfile must resolve from. */
|
|
31
|
+
const PUBLIC_HOST = "registry.npmjs.org";
|
|
32
|
+
|
|
33
|
+
export interface EnsureClientLockfileArgs {
|
|
34
|
+
/** The project root (the client dir lives at <projectDir>/<clientDir>). */
|
|
35
|
+
projectDir: string;
|
|
36
|
+
/** The client dir name under projectDir. Default "client". */
|
|
37
|
+
clientDir?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EnsureClientLockfileResult {
|
|
41
|
+
/** True when a package-lock.json exists in the client dir after this ran. */
|
|
42
|
+
generated: boolean;
|
|
43
|
+
/** Count of proxy-host `resolved` URLs rewritten to the public registry. */
|
|
44
|
+
swaps: number;
|
|
45
|
+
/** Why it did not run / generate (no client package.json, exec error). */
|
|
46
|
+
reason?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Host-swap any Databricks-proxy `resolved` URL to the public registry. A pure
|
|
51
|
+
* string substitution — integrity hashes are left untouched because the proxy
|
|
52
|
+
* is a passthrough mirror of the same tarballs. Returns the rewritten text and
|
|
53
|
+
* the number of swaps.
|
|
54
|
+
*/
|
|
55
|
+
export function normalizeLockfileRegistry(lockText: string): { text: string; swaps: number } {
|
|
56
|
+
let swaps = 0;
|
|
57
|
+
const text = lockText.replace(new RegExp(PROXY_HOST.replace(/\./g, "\\."), "g"), () => {
|
|
58
|
+
swaps++;
|
|
59
|
+
return PUBLIC_HOST;
|
|
60
|
+
});
|
|
61
|
+
return { text, swaps };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* For a UI project, generate client/package-lock.json (lock only, no install)
|
|
66
|
+
* and normalize its registry host. Idempotent, non-fatal: callers push the
|
|
67
|
+
* reason onto their warnings and continue.
|
|
68
|
+
*/
|
|
69
|
+
export async function ensureClientLockfile(
|
|
70
|
+
args: EnsureClientLockfileArgs,
|
|
71
|
+
): Promise<EnsureClientLockfileResult> {
|
|
72
|
+
const clientDir = path.join(args.projectDir, args.clientDir ?? "client");
|
|
73
|
+
const pkg = path.join(clientDir, "package.json");
|
|
74
|
+
if (!fs.existsSync(pkg)) {
|
|
75
|
+
return { generated: false, swaps: 0, reason: "no client package.json" };
|
|
76
|
+
}
|
|
77
|
+
const lockPath = path.join(clientDir, "package-lock.json");
|
|
78
|
+
try {
|
|
79
|
+
// --package-lock-only resolves the dep tree + writes the lock, skipping
|
|
80
|
+
// node_modules (no tarball downloads), so the scaffold stays fast.
|
|
81
|
+
await exec("npm install --package-lock-only", {
|
|
82
|
+
cwd: clientDir,
|
|
83
|
+
env: withProxyEnv(),
|
|
84
|
+
timeout: 180_000,
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
return { generated: fs.existsSync(lockPath), swaps: 0, reason: `npm resolve failed: ${msg}` };
|
|
89
|
+
}
|
|
90
|
+
if (!fs.existsSync(lockPath)) {
|
|
91
|
+
return { generated: false, swaps: 0, reason: "npm wrote no package-lock.json" };
|
|
92
|
+
}
|
|
93
|
+
// Normalize proxy -> public registry so a cold off-network install never hangs.
|
|
94
|
+
try {
|
|
95
|
+
const { text, swaps } = normalizeLockfileRegistry(fs.readFileSync(lockPath, "utf8"));
|
|
96
|
+
if (swaps > 0) fs.writeFileSync(lockPath, text);
|
|
97
|
+
return { generated: true, swaps };
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
return { generated: true, swaps: 0, reason: `lock normalize failed: ${msg}` };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -28,6 +28,7 @@ import { scaffoldAll, substrateVersion } from "./scaffold.js";
|
|
|
28
28
|
import type { ClientFramework } from "./scaffold-language.js";
|
|
29
29
|
import { createLongRunningBranch } from "./long-running-branch.js";
|
|
30
30
|
import { enableE2eForProject } from "./enable-e2e.js";
|
|
31
|
+
import { ensureClientLockfile } from "./client-lockfile.js";
|
|
31
32
|
import { enableInfraForProject } from "./enable-infra.js";
|
|
32
33
|
import { setupRunner } from "./runner-setup.js";
|
|
33
34
|
import { syncCiSecrets, missingCiSecrets } from "../util/ci-secrets.js";
|
|
@@ -587,6 +588,24 @@ export async function createProject(
|
|
|
587
588
|
dotnet: `.NET/ASP.NET Core (${input.migrationTool === "flyway" ? "Flyway" : "EF Core"})`,
|
|
588
589
|
};
|
|
589
590
|
const langLabel = langLabels[language] ?? language;
|
|
591
|
+
|
|
592
|
+
// ── Step 7f: client lockfile (UI projects) ───────────────────
|
|
593
|
+
// Generate + registry-normalize client/package-lock.json BEFORE the commit so
|
|
594
|
+
// `git add -A` tracks it. Without this the scaffold ships no client lock, and
|
|
595
|
+
// the user's hand-generated one (made on the Databricks network) pins the npm
|
|
596
|
+
// proxy and hangs a cold off-network install. Non-fatal: warn + continue.
|
|
597
|
+
if (clientFramework !== "none") {
|
|
598
|
+
report("Generating client package-lock.json (registry-normalized)...");
|
|
599
|
+
const lock = await ensureClientLockfile({ projectDir });
|
|
600
|
+
if (lock.generated) {
|
|
601
|
+
report(` wrote client/package-lock.json${lock.swaps > 0 ? ` (normalized ${lock.swaps} proxy URL(s) to the public registry)` : ""}`);
|
|
602
|
+
} else {
|
|
603
|
+
const m = `Could not generate client/package-lock.json (${lock.reason ?? "unknown"}). Generate it with: (cd client && npm install) — the lock is committed on first push.`;
|
|
604
|
+
warnings.push(m);
|
|
605
|
+
report(`WARNING: ${m}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
590
609
|
report("Creating initial commit...");
|
|
591
610
|
await commitAndPush({
|
|
592
611
|
projectDir,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// stdout/stderr is not load-bearing here, which keeps the runner robust
|
|
15
15
|
// to logger config drift in the consumer's `alembic.ini`.
|
|
16
16
|
|
|
17
|
-
import { spawn } from "node:child_process";
|
|
17
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import {
|
|
@@ -32,26 +32,70 @@ interface RunnerCtx {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* runner's PATH. Spawning bare `alembic` fails with ENOENT in CI even
|
|
38
|
-
* after `uv sync` succeeded. Prefer the project-local venv when it
|
|
39
|
-
* exists; fall back to bare `alembic` for projects with a pre-activated
|
|
40
|
-
* shell venv or a system-wide install.
|
|
35
|
+
* Whether `uv` is on PATH. Memoized: resolution runs per alembic subcommand and
|
|
36
|
+
* the answer cannot change within a process.
|
|
41
37
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
let uvOnPath: boolean | undefined;
|
|
39
|
+
function hasUv(): boolean {
|
|
40
|
+
if (uvOnPath !== undefined) return uvOnPath;
|
|
41
|
+
try {
|
|
42
|
+
execFileSync("uv", ["--version"], { stdio: "ignore" });
|
|
43
|
+
uvOnPath = true;
|
|
44
|
+
} catch {
|
|
45
|
+
uvOnPath = false;
|
|
46
|
+
}
|
|
47
|
+
return uvOnPath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve HOW to invoke alembic in a project: an executable plus the args that
|
|
52
|
+
* must PREFIX the alembic subcommand.
|
|
53
|
+
*
|
|
54
|
+
* uv-managed Python projects install alembic into a per-project `.venv/bin/alembic`,
|
|
55
|
+
* which is NOT on the runner's PATH, so that venv-local bin is preferred when it
|
|
56
|
+
* exists (it is also what CI has after `uv sync`). But a FRESH clone / rehydrated
|
|
57
|
+
* project has no `.venv` yet (nothing has run `uv sync`), and spawning bare
|
|
58
|
+
* `alembic` then fails ENOENT — the failure the `lk lakebase-new-migration` path
|
|
59
|
+
* hit on a never-synced project, before any test run created the venv. For such a
|
|
60
|
+
* uv project (pyproject.toml present and `uv` on PATH) invoke via `uv run alembic`,
|
|
61
|
+
* which auto-creates the venv and syncs the declared deps (alembic is a top-level
|
|
62
|
+
* dep) BEFORE running — exactly what scripts/run-tests.sh relies on. CI is
|
|
63
|
+
* unaffected: it hits the venv-local bin first. Last resort is bare `alembic`
|
|
64
|
+
* (a pre-activated shell venv or a system-wide install).
|
|
65
|
+
*/
|
|
66
|
+
export function resolveAlembicInvocation(
|
|
67
|
+
projectDir: string,
|
|
68
|
+
uvAvailable: boolean = hasUv()
|
|
69
|
+
): { bin: string; prefixArgs: string[] } {
|
|
70
|
+
const venvCandidates = [
|
|
44
71
|
path.join(projectDir, ".venv", "bin", "alembic"),
|
|
45
72
|
path.join(projectDir, "venv", "bin", "alembic"),
|
|
46
73
|
];
|
|
47
|
-
for (const candidate of
|
|
74
|
+
for (const candidate of venvCandidates) {
|
|
48
75
|
try {
|
|
49
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
76
|
+
if (fs.existsSync(candidate)) return { bin: candidate, prefixArgs: [] };
|
|
50
77
|
} catch {
|
|
51
78
|
// best-effort; keep checking
|
|
52
79
|
}
|
|
53
80
|
}
|
|
54
|
-
|
|
81
|
+
try {
|
|
82
|
+
if (uvAvailable && fs.existsSync(path.join(projectDir, "pyproject.toml"))) {
|
|
83
|
+
return { bin: "uv", prefixArgs: ["run", "alembic"] };
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// best-effort; fall through to bare alembic
|
|
87
|
+
}
|
|
88
|
+
return { bin: "alembic", prefixArgs: [] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Back-compat: the executable that would run alembic (the venv-local path or bare
|
|
93
|
+
* `alembic`). Prefer resolveAlembicInvocation, which also models the `uv run`
|
|
94
|
+
* prefix for a never-synced uv project.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveAlembicBin(projectDir: string): string {
|
|
97
|
+
const { bin, prefixArgs } = resolveAlembicInvocation(projectDir);
|
|
98
|
+
return prefixArgs.length ? `${bin} ${prefixArgs.join(" ")}` : bin;
|
|
55
99
|
}
|
|
56
100
|
|
|
57
101
|
/**
|
|
@@ -65,7 +109,7 @@ function spawnAlembic(
|
|
|
65
109
|
dsn?: string
|
|
66
110
|
): Promise<{ stdout: string; stderr: string }> {
|
|
67
111
|
return new Promise((resolve, reject) => {
|
|
68
|
-
const bin =
|
|
112
|
+
const { bin, prefixArgs } = resolveAlembicInvocation(projectDir);
|
|
69
113
|
// Put the project root on PYTHONPATH so `app` is importable for EVERY alembic
|
|
70
114
|
// subcommand. `upgrade` runs env.py (which prepends sys.path), but `history`
|
|
71
115
|
// and other read-only commands do not, yet they still import every migration
|
|
@@ -74,7 +118,7 @@ function spawnAlembic(
|
|
|
74
118
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
75
119
|
env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter);
|
|
76
120
|
if (dsn) env.DATABASE_URL = dsn;
|
|
77
|
-
const child = spawn(bin, args, {
|
|
121
|
+
const child = spawn(bin, [...prefixArgs, ...args], {
|
|
78
122
|
cwd: projectDir,
|
|
79
123
|
env,
|
|
80
124
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1,21 +1,47 @@
|
|
|
1
1
|
import { defineConfig, devices } from "@playwright/test";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
|
|
6
|
+
// The backend lives at the project root, one level up from this client/ config
|
|
7
|
+
// (Playwright runs from client/, hence cwd:".." on the webServers below).
|
|
8
|
+
const ROOT = join(process.cwd(), "..");
|
|
9
|
+
|
|
5
10
|
// End-to-end tests drive the rendered SPA through a real browser against the
|
|
6
11
|
// real backend, backed by the paired Lakebase branch. Playwright boots BOTH
|
|
7
12
|
// servers: the backend FIRST (so Vite's /api proxy has a target), then Vite.
|
|
8
13
|
// Adapt the backend `command`/`url` to your stack if it is not Python/FastAPI.
|
|
9
14
|
//
|
|
10
|
-
// Resiliency: the ports are env-driven (E2E_BACKEND_PORT / E2E_CLIENT_PORT)
|
|
11
|
-
//
|
|
12
|
-
// left on 8000/5173 by a prior run
|
|
13
|
-
// "port already used".
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
// Resiliency: the ports are env-driven (E2E_BACKEND_PORT / E2E_CLIENT_PORT).
|
|
16
|
+
// run-tests.sh and CI pre-allocate FREE ports (scripts/port-utils.sh) and hand them
|
|
17
|
+
// in via those vars, so a stale server left on 8000/5173 by a prior run never makes
|
|
18
|
+
// webServer hard-fail with "port already used". When NOTHING hands them in — a direct
|
|
19
|
+
// `npx playwright test` (e.g. the build driver running a targeted spec, not through
|
|
20
|
+
// run-tests.sh) — resolve a free port HERE the SAME way, by sourcing that one shared
|
|
21
|
+
// free_port helper, so a direct run is just as collision-proof and never defaults onto
|
|
22
|
+
// a busy :8000 / :5173. One free-port owner (port-utils.sh), two entry points. The Vite
|
|
23
|
+
// proxy is pointed at the resolved backend port via VITE_PROXY_TARGET so /api + /health
|
|
24
|
+
// still reach the backend wherever it landed.
|
|
25
|
+
function resolvePort(envVar: string, fallback: number): string {
|
|
26
|
+
const fromEnv = process.env[envVar];
|
|
27
|
+
if (fromEnv) return fromEnv;
|
|
28
|
+
const portUtils = join(ROOT, "scripts", "port-utils.sh");
|
|
29
|
+
if (existsSync(portUtils)) {
|
|
30
|
+
try {
|
|
31
|
+
const out = execFileSync(
|
|
32
|
+
"bash",
|
|
33
|
+
["-c", `source ${JSON.stringify(portUtils)} && free_port ${fallback}`],
|
|
34
|
+
{ encoding: "utf8" }
|
|
35
|
+
).trim();
|
|
36
|
+
if (/^\d+$/.test(out)) return out;
|
|
37
|
+
} catch {
|
|
38
|
+
// fall through to the conventional default
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return String(fallback);
|
|
42
|
+
}
|
|
43
|
+
const BACKEND_PORT = resolvePort("E2E_BACKEND_PORT", 8000);
|
|
44
|
+
const CLIENT_PORT = resolvePort("E2E_CLIENT_PORT", 5173);
|
|
19
45
|
// Use 127.0.0.1, NOT localhost, and bind both servers to it (below). `localhost`
|
|
20
46
|
// is ambiguous: on macOS it resolves to IPv6 ::1 first, but uvicorn binds IPv4
|
|
21
47
|
// 127.0.0.1 only (its default) and Vite's dev server is likewise IPv4 — so a
|
|
@@ -31,7 +57,6 @@ const CLIENT_URL = `http://127.0.0.1:${CLIENT_PORT}`;
|
|
|
31
57
|
// one level up from this client/ config (Playwright runs from client/, hence cwd:".." below). Pick
|
|
32
58
|
// the migrate-before-serve command + health URL for the detected stack so E2E boots on ANY language
|
|
33
59
|
// , a nodejs scaffold otherwise inherited the Python (alembic/uvicorn) command and could not start.
|
|
34
|
-
const ROOT = join(process.cwd(), "..");
|
|
35
60
|
function backendWebServer(): { command: string; url: string } {
|
|
36
61
|
if (existsSync(join(ROOT, "pom.xml"))) {
|
|
37
62
|
// Java/Kotlin (Spring): flyway migrate, then boot on the resolved port.
|