@databricks-solutions/lakebase-scm-utils 0.2.51 → 0.3.1
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 +2813 -2244
- package/dist/scripts/index.d.cts +1 -1
- package/dist/scripts/index.d.ts +1 -1
- package/dist/scripts/index.js +857 -298
- package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2460 -2030
- package/dist/scripts/lakebase/ci-resolve-branch.cli.js +508 -81
- package/dist/scripts/lakebase/collapse-heads.cli.cjs +2429 -2007
- package/dist/scripts/lakebase/collapse-heads.cli.js +477 -58
- package/dist/scripts/lakebase/detect-language.cli.cjs +2441 -2016
- package/dist/scripts/lakebase/detect-language.cli.js +489 -67
- package/dist/scripts/lakebase/doctor.cli.cjs +2484 -2040
- package/dist/scripts/lakebase/doctor.cli.js +532 -91
- 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 +2810 -2205
- package/dist/scripts/lakebase/index.d.cts +59 -10
- package/dist/scripts/lakebase/index.d.ts +59 -10
- package/dist/scripts/lakebase/index.js +851 -256
- package/dist/scripts/lakebase/infra-runner.cli.cjs +2435 -2013
- package/dist/scripts/lakebase/infra-runner.cli.js +483 -64
- package/dist/scripts/lakebase/new-migration.cli.cjs +2429 -2007
- package/dist/scripts/lakebase/new-migration.cli.js +477 -58
- 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 +2461 -2029
- package/dist/scripts/lakebase/schema-migrate.cli.js +509 -80
- package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2460 -2030
- package/dist/scripts/lakebase/scm-claim-feature.cli.js +508 -81
- package/dist/scripts/lakebase/scm-doctor.cli.cjs +2486 -2056
- package/dist/scripts/lakebase/scm-doctor.cli.js +537 -107
- package/dist/scripts/lakebase/scm-merge.cli.cjs +2443 -2021
- package/dist/scripts/lakebase/scm-merge.cli.js +494 -72
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2464 -2034
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +511 -84
- package/dist/scripts/util/index.cjs +2 -2
- package/dist/scripts/util/index.js +2 -2
- package/package.json +1 -1
- package/scripts/lakebase/adapters/efcore-adapter.ts +158 -0
- package/scripts/lakebase/adapters/flyway-adapter.ts +8 -5
- package/scripts/lakebase/client-lockfile.ts +102 -0
- package/scripts/lakebase/collapse-heads.cli.ts +1 -1
- package/scripts/lakebase/create-project.ts +25 -2
- package/scripts/lakebase/detect-language.cli.ts +2 -2
- package/scripts/lakebase/doctor.ts +29 -0
- package/scripts/lakebase/dotnet-project.ts +112 -0
- package/scripts/lakebase/index.ts +1 -0
- package/scripts/lakebase/migration-layout.ts +43 -2
- package/scripts/lakebase/new-migration.cli.ts +1 -1
- package/scripts/lakebase/scaffold-language.ts +23 -2
- package/scripts/lakebase/scaffold.ts +4 -1
- package/scripts/lakebase/schema-migrate-runners/efcore.ts +399 -0
- package/scripts/lakebase/schema-migrate-runners/flyway.ts +6 -4
- package/scripts/lakebase/schema-migrate.cli.ts +3 -1
- package/scripts/lakebase/schema-migrate.ts +24 -10
- package/templates/project/common/.github/workflows/merge.yml +13 -1
- package/templates/project/common/.github/workflows/pr.yml +25 -5
- package/templates/project/common/scripts/flyway-migrate.sh +54 -1
- package/templates/project/common/scripts/prepare-schema-diff.sh +5 -0
- package/templates/project/common/scripts/run-dev.sh +10 -1
- package/templates/project/common/scripts/run-tests.sh +6 -1
- package/templates/project/dotnet/.gitignore.extra +8 -0
- package/templates/project/dotnet/App.sln +56 -0
- package/templates/project/dotnet/Makefile +13 -0
- package/templates/project/dotnet/global.json +6 -0
- package/templates/project/dotnet/src/App/Data/AppDbContext.cs +12 -0
- package/templates/project/dotnet/src/App/Lakebase/DotEnv.cs +48 -0
- package/templates/project/dotnet/src/App/Lakebase/LakebaseConnection.cs +225 -0
- package/templates/project/dotnet/src/App/Program.cs +34 -0
- package/templates/project/dotnet/src/App/appsettings.json +9 -0
- package/templates/project/dotnet/tests/App.Tests/App.Tests.csproj +25 -0
- package/templates/project/dotnet/tests/App.Tests/HealthTests.cs +18 -0
- package/templates/project/dotnet/tests/App.Tests/OpenApiTests.cs +23 -0
- package/templates/project/dotnet-efcore/.config/dotnet-tools.json +13 -0
- package/templates/project/dotnet-efcore/src/App/App.csproj +22 -0
- package/templates/project/dotnet-efcore/src/App/Data/DesignTimeDbContextFactory.cs +24 -0
- package/templates/project/dotnet-efcore/src/App/Migrations/README.md +23 -0
- package/templates/project/dotnet-flyway/db/migrations/README.md +23 -0
- package/templates/project/dotnet-flyway/src/App/App.csproj +17 -0
|
@@ -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.
|
|
1671
|
-
cached = "0.
|
|
1670
|
+
if ("0.3.1".length > 0) {
|
|
1671
|
+
cached = "0.3.1";
|
|
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.
|
|
1634
|
-
cached = "0.
|
|
1633
|
+
if ("0.3.1".length > 0) {
|
|
1634
|
+
cached = "0.3.1";
|
|
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
|
+
"version": "0.3.1",
|
|
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,158 @@
|
|
|
1
|
+
// EfCoreAdapter: SchemaMigrationAdapter implementation for .NET projects
|
|
2
|
+
// using EF Core migrations.
|
|
3
|
+
//
|
|
4
|
+
// Wraps scripts/lakebase/schema-migrate-runners/efcore.ts in the cross-tool
|
|
5
|
+
// adapter contract from ADR-0005. Supports rollback (EF generates Down()) and
|
|
6
|
+
// stamp (writes "__EFMigrationsHistory" rows without running migrations).
|
|
7
|
+
// collapseHeads is omitted: EF history is linear (flat-list, like Flyway).
|
|
8
|
+
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
|
|
11
|
+
import { getConnection } from "../get-connection.js";
|
|
12
|
+
import { findEfCoreProject, isDotnetProject } from "../dotnet-project.js";
|
|
13
|
+
import {
|
|
14
|
+
applyEfcore,
|
|
15
|
+
createEfcoreMigration,
|
|
16
|
+
efDescription,
|
|
17
|
+
efVersion,
|
|
18
|
+
localEfMigrationIds,
|
|
19
|
+
rollbackEfcore,
|
|
20
|
+
stampEfcore,
|
|
21
|
+
statusEfcore,
|
|
22
|
+
} from "../schema-migrate-runners/efcore.js";
|
|
23
|
+
import type { AppliedSchemaMigration, PendingSchemaMigration, SchemaMigrationFile } from "../schema-migrate.js";
|
|
24
|
+
import {
|
|
25
|
+
registerSchemaMigrationAdapter,
|
|
26
|
+
type ApplyArgs,
|
|
27
|
+
type ApplyResult,
|
|
28
|
+
type ListArgs,
|
|
29
|
+
type ListResult,
|
|
30
|
+
type NewMigrationArgs,
|
|
31
|
+
type NewMigrationResult,
|
|
32
|
+
type RollbackArgs,
|
|
33
|
+
type RollbackResult,
|
|
34
|
+
type SchemaMigrationAdapter,
|
|
35
|
+
type StampArgs,
|
|
36
|
+
type StampResult,
|
|
37
|
+
type StatusArgs,
|
|
38
|
+
type StatusResult,
|
|
39
|
+
} from "../schema-migration-adapter.js";
|
|
40
|
+
|
|
41
|
+
async function buildDsn(args: {
|
|
42
|
+
instance: string;
|
|
43
|
+
branch: string;
|
|
44
|
+
database?: string;
|
|
45
|
+
endpointName?: string;
|
|
46
|
+
}): Promise<string> {
|
|
47
|
+
const result = await getConnection({
|
|
48
|
+
output: "dsn",
|
|
49
|
+
instance: args.instance,
|
|
50
|
+
branch: args.branch,
|
|
51
|
+
database: args.database,
|
|
52
|
+
endpointName: args.endpointName,
|
|
53
|
+
});
|
|
54
|
+
return result.url;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function errMsg(err: unknown): string {
|
|
58
|
+
return err instanceof Error ? err.message : String(err);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Migration files in apply order: Migrations/<timestamp>_<Name>.cs (not .Designer.cs / the snapshot). */
|
|
62
|
+
export function listEfCoreFiles(projectDir: string): SchemaMigrationFile[] {
|
|
63
|
+
return localEfMigrationIds(projectDir).map((id) => ({
|
|
64
|
+
version: efVersion(id),
|
|
65
|
+
filename: `${id}.cs`,
|
|
66
|
+
description: efDescription(id),
|
|
67
|
+
type: "CSharp" as const,
|
|
68
|
+
tool: "efcore" as const,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const EfCoreAdapter: SchemaMigrationAdapter = {
|
|
73
|
+
id: "efcore",
|
|
74
|
+
languages: ["dotnet"],
|
|
75
|
+
|
|
76
|
+
/** A .NET project (root marker) with a *.csproj that references Microsoft.EntityFrameworkCore.Design. */
|
|
77
|
+
detect(projectDir: string): boolean {
|
|
78
|
+
return isDotnetProject(projectDir) && findEfCoreProject(projectDir) !== null;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async apply(args: ApplyArgs): Promise<ApplyResult> {
|
|
82
|
+
try {
|
|
83
|
+
const dsn = await buildDsn(args);
|
|
84
|
+
const r = await applyEfcore({ projectDir: args.projectDir, dsn });
|
|
85
|
+
return {
|
|
86
|
+
applied_migrations: r.applied,
|
|
87
|
+
status: r.alreadyAtLatest ? "noop" : "ok",
|
|
88
|
+
tool_specific: { alreadyAtLatest: r.alreadyAtLatest, tool: r.tool },
|
|
89
|
+
};
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return { applied_migrations: [], status: "error", error: errMsg(err) };
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async rollback(args: RollbackArgs): Promise<RollbackResult> {
|
|
96
|
+
try {
|
|
97
|
+
const dsn = await buildDsn(args);
|
|
98
|
+
const r = await rollbackEfcore({ projectDir: args.projectDir, dsn, target: args.target });
|
|
99
|
+
return {
|
|
100
|
+
rolled_back: r.rolledBack as AppliedSchemaMigration[],
|
|
101
|
+
status: r.rolledBack.length === 0 ? "noop" : "ok",
|
|
102
|
+
tool_specific: { tool: r.tool },
|
|
103
|
+
};
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return { rolled_back: [], status: "error", error: errMsg(err) };
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async stamp(args: StampArgs): Promise<StampResult> {
|
|
110
|
+
try {
|
|
111
|
+
const dsn = await buildDsn(args);
|
|
112
|
+
const stamped = await stampEfcore({ projectDir: args.projectDir, dsn, revision: args.revision });
|
|
113
|
+
return { status: "ok", stamped_revision: stamped, tool_specific: { tool: "efcore" } };
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return { status: "error", stamped_revision: null, error: errMsg(err) };
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async status(args: StatusArgs): Promise<StatusResult> {
|
|
120
|
+
try {
|
|
121
|
+
const dsn = await buildDsn(args);
|
|
122
|
+
const r = await statusEfcore({ projectDir: args.projectDir, dsn });
|
|
123
|
+
return {
|
|
124
|
+
applied_version: r.current ?? null,
|
|
125
|
+
pending: r.pending as PendingSchemaMigration[],
|
|
126
|
+
applied: [],
|
|
127
|
+
status: "ok",
|
|
128
|
+
tool_specific: { tool: r.tool },
|
|
129
|
+
};
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return { applied_version: null, pending: [], applied: [], status: "error", error: errMsg(err) };
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async list(args: ListArgs): Promise<ListResult> {
|
|
136
|
+
return { files: listEfCoreFiles(args.projectDir) };
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// baseline: use stamp (EF has no separate baseline concept).
|
|
140
|
+
|
|
141
|
+
async newMigration(args: NewMigrationArgs): Promise<NewMigrationResult> {
|
|
142
|
+
// `dotnet ef migrations add` always generates from the model diff, so
|
|
143
|
+
// autogenerate is implicit and needs no database connection.
|
|
144
|
+
try {
|
|
145
|
+
const created = await createEfcoreMigration({ projectDir: args.projectDir, slug: args.slug });
|
|
146
|
+
return {
|
|
147
|
+
status: "ok",
|
|
148
|
+
version: efVersion(created.id),
|
|
149
|
+
filename: path.basename(created.path),
|
|
150
|
+
path: created.path,
|
|
151
|
+
};
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return { status: "error", version: "", filename: "", path: "", error: errMsg(err) };
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
registerSchemaMigrationAdapter(EfCoreAdapter);
|
|
@@ -14,6 +14,7 @@ import * as fs from "node:fs";
|
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
|
|
16
16
|
import { getConnection } from "../get-connection.js";
|
|
17
|
+
import { flywayMigrationsDir, isDotnetFlywayProject } from "../dotnet-project.js";
|
|
17
18
|
import {
|
|
18
19
|
applyFlyway,
|
|
19
20
|
statusFlyway,
|
|
@@ -55,8 +56,9 @@ async function buildDsn(args: {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function listFlywayFiles(projectDir: string): SchemaMigrationFile[] {
|
|
58
|
-
// Flyway convention:
|
|
59
|
-
|
|
59
|
+
// Flyway convention: V<n>__<desc>.sql under the Maven layout (Java/Kotlin)
|
|
60
|
+
// or db/migrations/ (.NET); see flywayMigrationsDir.
|
|
61
|
+
const dir = flywayMigrationsDir(projectDir);
|
|
60
62
|
if (!fs.existsSync(dir)) return [];
|
|
61
63
|
const files = fs.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
|
|
62
64
|
return files
|
|
@@ -83,10 +85,11 @@ function versionCompare(a: string, b: string): number {
|
|
|
83
85
|
|
|
84
86
|
export const FlywayAdapter: SchemaMigrationAdapter = {
|
|
85
87
|
id: "flyway",
|
|
86
|
-
languages: ["java", "kotlin"],
|
|
88
|
+
languages: ["java", "kotlin", "dotnet"],
|
|
87
89
|
|
|
88
90
|
detect(projectDir: string): boolean {
|
|
89
|
-
|
|
91
|
+
// Maven project, or a .NET project that keeps Flyway SQL in db/migrations/.
|
|
92
|
+
return fs.existsSync(path.join(projectDir, "pom.xml")) || isDotnetFlywayProject(projectDir);
|
|
90
93
|
},
|
|
91
94
|
|
|
92
95
|
async apply(args: ApplyArgs): Promise<ApplyResult> {
|
|
@@ -155,7 +158,7 @@ export const FlywayAdapter: SchemaMigrationAdapter = {
|
|
|
155
158
|
// so a flat Flyway list never collides across sibling branches. Flyway has
|
|
156
159
|
// no DAG, so no merge-time head collapse is needed. autogenerate is ignored.
|
|
157
160
|
try {
|
|
158
|
-
const dir =
|
|
161
|
+
const dir = flywayMigrationsDir(args.projectDir);
|
|
159
162
|
fs.mkdirSync(dir, { recursive: true });
|
|
160
163
|
const version = migrationTimestamp();
|
|
161
164
|
const slug = migrationSlug(args.slug);
|
|
@@ -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
|
+
}
|
|
@@ -67,7 +67,7 @@ Usage:
|
|
|
67
67
|
|
|
68
68
|
Flags:
|
|
69
69
|
--project-dir <dir> Project root (default: cwd)
|
|
70
|
-
--language <lang> java | kotlin | python | nodejs (default: auto-detect)
|
|
70
|
+
--language <lang> java | kotlin | python | nodejs | dotnet (default: auto-detect)
|
|
71
71
|
--message, -m <txt> Message for the merge revision (Alembic; default: "merge heads")
|
|
72
72
|
--dry-run Report the heads without creating a merge revision
|
|
73
73
|
--pretty Pretty-print JSON output
|
|
@@ -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";
|
|
@@ -54,7 +55,7 @@ export interface ConsortSetupHooks {
|
|
|
54
55
|
agentModels?: Record<string, string>;
|
|
55
56
|
uiTrack?: boolean;
|
|
56
57
|
clientFramework?: string;
|
|
57
|
-
language?: "java" | "kotlin" | "python" | "nodejs";
|
|
58
|
+
language?: "java" | "kotlin" | "python" | "nodejs" | "dotnet";
|
|
58
59
|
},
|
|
59
60
|
): void;
|
|
60
61
|
}
|
|
@@ -77,7 +78,9 @@ export interface CreateProjectArgs {
|
|
|
77
78
|
/** Whether to make the GitHub repo private (default: true). */
|
|
78
79
|
privateRepo?: boolean;
|
|
79
80
|
/** Project language stack (default: 'java'). */
|
|
80
|
-
language?: "java" | "kotlin" | "python" | "nodejs";
|
|
81
|
+
language?: "java" | "kotlin" | "python" | "nodejs" | "dotnet";
|
|
82
|
+
/** .NET only: migration tool, "efcore" (default) or "flyway". */
|
|
83
|
+
migrationTool?: "efcore" | "flyway";
|
|
81
84
|
/** CI runner type (default: 'self-hosted'). */
|
|
82
85
|
runnerType?: "self-hosted" | "github-hosted";
|
|
83
86
|
/**
|
|
@@ -365,6 +368,7 @@ export async function createProject(
|
|
|
365
368
|
databricksHost: host,
|
|
366
369
|
lakebaseProjectId,
|
|
367
370
|
language,
|
|
371
|
+
migrationTool: input.migrationTool,
|
|
368
372
|
runnerType,
|
|
369
373
|
skipCommands,
|
|
370
374
|
clientFramework,
|
|
@@ -581,8 +585,27 @@ export async function createProject(
|
|
|
581
585
|
kotlin: "Kotlin/Spring Boot",
|
|
582
586
|
python: "Python/FastAPI",
|
|
583
587
|
nodejs: "Node.js/Express",
|
|
588
|
+
dotnet: `.NET/ASP.NET Core (${input.migrationTool === "flyway" ? "Flyway" : "EF Core"})`,
|
|
584
589
|
};
|
|
585
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
|
+
|
|
586
609
|
report("Creating initial commit...");
|
|
587
610
|
await commitAndPush({
|
|
588
611
|
projectDir,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// pr.yml + merge.yml to populate `steps.detect-lang.outputs.lang` without
|
|
4
4
|
// duplicating the detection bash inline.
|
|
5
5
|
//
|
|
6
|
-
// Output: prints "java" | "python" | "nodejs" to stdout. Exits non-zero
|
|
6
|
+
// Output: prints "java" | "kotlin" | "python" | "nodejs" | "dotnet" to stdout. Exits non-zero
|
|
7
7
|
// if no recognised marker file is present.
|
|
8
8
|
//
|
|
9
9
|
// Optional flag: --project-dir <path> (defaults to cwd).
|
|
@@ -38,7 +38,7 @@ function printHelpAndExit(): never {
|
|
|
38
38
|
`lakebase-detect-language – print the project's language\n\n` +
|
|
39
39
|
`Usage:\n` +
|
|
40
40
|
` lakebase-detect-language [--project-dir <path>]\n\n` +
|
|
41
|
-
`Output (stdout): one of "java", "python", "nodejs"\n` +
|
|
41
|
+
`Output (stdout): one of "java", "kotlin", "python", "nodejs", "dotnet"\n` +
|
|
42
42
|
`Exits 1 with an error message on stderr if no marker found.\n`,
|
|
43
43
|
);
|
|
44
44
|
process.exit(0);
|
|
@@ -285,6 +285,33 @@ async function checkPrereq(
|
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/** .NET SDK floor for `dotnet` projects (the scaffold targets net10.0). */
|
|
289
|
+
const DOTNET_PREREQ: PrereqSpec = {
|
|
290
|
+
name: "dotnet-sdk",
|
|
291
|
+
cmd: "dotnet",
|
|
292
|
+
versionArgs: ["--version"],
|
|
293
|
+
minMajor: 10,
|
|
294
|
+
label: ".NET SDK",
|
|
295
|
+
hint: "Install the .NET 10 SDK (e.g. `brew install --cask dotnet-sdk` or https://dot.net). EF Core's dotnet-ef is restored per project from .config/dotnet-tools.json.",
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Language-gated prerequisites: checked only when the project is that
|
|
300
|
+
* language, so a Java or Python user never sees a missing-.NET failure.
|
|
301
|
+
*/
|
|
302
|
+
export async function checkLanguagePrerequisites(
|
|
303
|
+
projectDir: string,
|
|
304
|
+
run: VersionRunner = defaultVersionRunner
|
|
305
|
+
): Promise<CheckResult[]> {
|
|
306
|
+
let lang: string;
|
|
307
|
+
try {
|
|
308
|
+
lang = detectLanguage(projectDir);
|
|
309
|
+
} catch {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
return lang === "dotnet" ? [await checkPrereq(DOTNET_PREREQ, run)] : [];
|
|
313
|
+
}
|
|
314
|
+
|
|
288
315
|
/** All cold-start prerequisite checks, in README order. */
|
|
289
316
|
export async function checkPrerequisites(
|
|
290
317
|
run: VersionRunner = defaultVersionRunner
|
|
@@ -712,6 +739,7 @@ export async function runDoctor(args: DoctorArgs = {}): Promise<DoctorReport> {
|
|
|
712
739
|
// Cold-start prerequisites (Node/Python/JDK/gh/npm): always run, they are
|
|
713
740
|
// local-environment checks with no auth dependency.
|
|
714
741
|
const prereqs = await checkPrerequisites();
|
|
742
|
+
const languagePrereqs = await checkLanguagePrerequisites(projectDir);
|
|
715
743
|
|
|
716
744
|
// Lakebase-enabled probe: needs a reachable workspace, so gate on auth like
|
|
717
745
|
// the identity + lakebase-project checks.
|
|
@@ -727,6 +755,7 @@ export async function runDoctor(args: DoctorArgs = {}): Promise<DoctorReport> {
|
|
|
727
755
|
const checks: CheckResult[] = [
|
|
728
756
|
cli,
|
|
729
757
|
...prereqs,
|
|
758
|
+
...languagePrereqs,
|
|
730
759
|
auth,
|
|
731
760
|
identity,
|
|
732
761
|
lakebaseEnabled,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// .NET project discovery shared by language detection, the Flyway adapter
|
|
2
|
+
// (dotnet layout), and the EF Core adapter. Pure filesystem scans; no
|
|
3
|
+
// `dotnet` invocation, so detection stays fast and runtime-light.
|
|
4
|
+
//
|
|
5
|
+
// Layout the kit scaffolds (templates/project/dotnet*):
|
|
6
|
+
// App.sln
|
|
7
|
+
// src/App/App.csproj <- application project
|
|
8
|
+
// src/App/Migrations/ <- EF Core migrations (efcore variant)
|
|
9
|
+
// db/migrations/V1__*.sql <- Flyway migrations (flyway variant)
|
|
10
|
+
// tests/App.Tests/App.Tests.csproj
|
|
11
|
+
//
|
|
12
|
+
// A .NET project is identified by a *.sln / *.slnx / *.csproj at the ROOT
|
|
13
|
+
// (the same rule language detection uses, so the kit never half-recognizes a
|
|
14
|
+
// repo). Once identified, the EF Core project may live up to two levels down
|
|
15
|
+
// (src/<Name>/, <Name>/).
|
|
16
|
+
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
|
|
20
|
+
/** Package whose reference marks a project as the EF Core migrations owner. */
|
|
21
|
+
export const EF_DESIGN_PACKAGE = "Microsoft.EntityFrameworkCore.Design";
|
|
22
|
+
|
|
23
|
+
/** Flyway migrations directory for .NET projects (relative to the project root). */
|
|
24
|
+
export const DOTNET_FLYWAY_DIR = path.join("db", "migrations");
|
|
25
|
+
|
|
26
|
+
const SKIP_DIRS = new Set(["bin", "obj", "node_modules", ".git", "client", "tests", "test"]);
|
|
27
|
+
|
|
28
|
+
function safeReaddir(dir: string): fs.Dirent[] {
|
|
29
|
+
try {
|
|
30
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when `dir` itself carries a .NET marker (*.sln, *.slnx, *.csproj). */
|
|
37
|
+
export function hasDotnetMarker(dir: string): boolean {
|
|
38
|
+
return safeReaddir(dir).some(
|
|
39
|
+
(e) => e.isFile() && /\.(sln|slnx|csproj)$/i.test(e.name),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Find *.csproj files at the root and up to two directory levels below it,
|
|
45
|
+
* skipping build output and test folders. Sorted for deterministic results.
|
|
46
|
+
*/
|
|
47
|
+
export function findCsprojFiles(projectDir: string): string[] {
|
|
48
|
+
const found: string[] = [];
|
|
49
|
+
const walk = (dir: string, depth: number) => {
|
|
50
|
+
for (const e of safeReaddir(dir)) {
|
|
51
|
+
const full = path.join(dir, e.name);
|
|
52
|
+
if (e.isFile() && e.name.toLowerCase().endsWith(".csproj")) {
|
|
53
|
+
found.push(full);
|
|
54
|
+
} else if (e.isDirectory() && depth < 2 && !SKIP_DIRS.has(e.name.toLowerCase()) && !e.name.startsWith(".")) {
|
|
55
|
+
walk(full, depth + 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(projectDir, 0);
|
|
60
|
+
return found.sort();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface EfCoreProject {
|
|
64
|
+
/** Absolute path to the .csproj that references EF Core Design. */
|
|
65
|
+
csproj: string;
|
|
66
|
+
/** Directory containing the .csproj. */
|
|
67
|
+
projectDir: string;
|
|
68
|
+
/** Absolute path to the project's Migrations/ directory (may not exist yet). */
|
|
69
|
+
migrationsDir: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Locate the project that owns EF Core migrations, or null. */
|
|
73
|
+
export function findEfCoreProject(projectDir: string): EfCoreProject | null {
|
|
74
|
+
for (const csproj of findCsprojFiles(projectDir)) {
|
|
75
|
+
let content = "";
|
|
76
|
+
try {
|
|
77
|
+
content = fs.readFileSync(csproj, "utf-8");
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (content.includes(EF_DESIGN_PACKAGE)) {
|
|
82
|
+
const dir = path.dirname(csproj);
|
|
83
|
+
return { csproj, projectDir: dir, migrationsDir: path.join(dir, "Migrations") };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** True for a .NET repo: a *.sln / *.slnx / *.csproj at the root. */
|
|
90
|
+
export function isDotnetProject(projectDir: string): boolean {
|
|
91
|
+
return hasDotnetMarker(projectDir);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** True for a .NET repo that uses Flyway SQL migrations under db/migrations/. */
|
|
95
|
+
export function isDotnetFlywayProject(projectDir: string): boolean {
|
|
96
|
+
return isDotnetProject(projectDir) && fs.existsSync(path.join(projectDir, DOTNET_FLYWAY_DIR));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Maven/Spring Flyway layout (Java/Kotlin). */
|
|
100
|
+
export const MAVEN_FLYWAY_DIR = path.join("src", "main", "resources", "db", "migration");
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Absolute Flyway migrations directory for a project: the Maven layout for
|
|
104
|
+
* Java/Kotlin (and as the historical default), or `db/migrations` for a .NET
|
|
105
|
+
* project that uses Flyway.
|
|
106
|
+
*/
|
|
107
|
+
export function flywayMigrationsDir(projectDir: string): string {
|
|
108
|
+
const maven = path.join(projectDir, MAVEN_FLYWAY_DIR);
|
|
109
|
+
if (fs.existsSync(maven) || fs.existsSync(path.join(projectDir, "pom.xml"))) return maven;
|
|
110
|
+
if (isDotnetFlywayProject(projectDir)) return path.join(projectDir, DOTNET_FLYWAY_DIR);
|
|
111
|
+
return maven;
|
|
112
|
+
}
|
|
@@ -73,6 +73,7 @@ export * from "./workflow-drift.js";
|
|
|
73
73
|
export * from "./spring-initializr.js";
|
|
74
74
|
export * from "./uc-resources.js";
|
|
75
75
|
export * from "./migration-layout.js";
|
|
76
|
+
export * from "./dotnet-project.js";
|
|
76
77
|
export {
|
|
77
78
|
applySchemaMigrations,
|
|
78
79
|
rollbackSchemaMigration,
|