@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.
Files changed (79) hide show
  1. package/README.md +10 -4
  2. package/dist/scripts/index.cjs +2813 -2244
  3. package/dist/scripts/index.d.cts +1 -1
  4. package/dist/scripts/index.d.ts +1 -1
  5. package/dist/scripts/index.js +857 -298
  6. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2460 -2030
  7. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +508 -81
  8. package/dist/scripts/lakebase/collapse-heads.cli.cjs +2429 -2007
  9. package/dist/scripts/lakebase/collapse-heads.cli.js +477 -58
  10. package/dist/scripts/lakebase/detect-language.cli.cjs +2441 -2016
  11. package/dist/scripts/lakebase/detect-language.cli.js +489 -67
  12. package/dist/scripts/lakebase/doctor.cli.cjs +2484 -2040
  13. package/dist/scripts/lakebase/doctor.cli.js +532 -91
  14. package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
  15. package/dist/scripts/lakebase/get-connection.cli.js +2 -2
  16. package/dist/scripts/lakebase/index.cjs +2810 -2205
  17. package/dist/scripts/lakebase/index.d.cts +59 -10
  18. package/dist/scripts/lakebase/index.d.ts +59 -10
  19. package/dist/scripts/lakebase/index.js +851 -256
  20. package/dist/scripts/lakebase/infra-runner.cli.cjs +2435 -2013
  21. package/dist/scripts/lakebase/infra-runner.cli.js +483 -64
  22. package/dist/scripts/lakebase/new-migration.cli.cjs +2429 -2007
  23. package/dist/scripts/lakebase/new-migration.cli.js +477 -58
  24. package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
  25. package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
  26. package/dist/scripts/lakebase/schema-migrate.cli.cjs +2461 -2029
  27. package/dist/scripts/lakebase/schema-migrate.cli.js +509 -80
  28. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2460 -2030
  29. package/dist/scripts/lakebase/scm-claim-feature.cli.js +508 -81
  30. package/dist/scripts/lakebase/scm-doctor.cli.cjs +2486 -2056
  31. package/dist/scripts/lakebase/scm-doctor.cli.js +537 -107
  32. package/dist/scripts/lakebase/scm-merge.cli.cjs +2443 -2021
  33. package/dist/scripts/lakebase/scm-merge.cli.js +494 -72
  34. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2464 -2034
  35. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +511 -84
  36. package/dist/scripts/util/index.cjs +2 -2
  37. package/dist/scripts/util/index.js +2 -2
  38. package/package.json +1 -1
  39. package/scripts/lakebase/adapters/efcore-adapter.ts +158 -0
  40. package/scripts/lakebase/adapters/flyway-adapter.ts +8 -5
  41. package/scripts/lakebase/client-lockfile.ts +102 -0
  42. package/scripts/lakebase/collapse-heads.cli.ts +1 -1
  43. package/scripts/lakebase/create-project.ts +25 -2
  44. package/scripts/lakebase/detect-language.cli.ts +2 -2
  45. package/scripts/lakebase/doctor.ts +29 -0
  46. package/scripts/lakebase/dotnet-project.ts +112 -0
  47. package/scripts/lakebase/index.ts +1 -0
  48. package/scripts/lakebase/migration-layout.ts +43 -2
  49. package/scripts/lakebase/new-migration.cli.ts +1 -1
  50. package/scripts/lakebase/scaffold-language.ts +23 -2
  51. package/scripts/lakebase/scaffold.ts +4 -1
  52. package/scripts/lakebase/schema-migrate-runners/efcore.ts +399 -0
  53. package/scripts/lakebase/schema-migrate-runners/flyway.ts +6 -4
  54. package/scripts/lakebase/schema-migrate.cli.ts +3 -1
  55. package/scripts/lakebase/schema-migrate.ts +24 -10
  56. package/templates/project/common/.github/workflows/merge.yml +13 -1
  57. package/templates/project/common/.github/workflows/pr.yml +25 -5
  58. package/templates/project/common/scripts/flyway-migrate.sh +54 -1
  59. package/templates/project/common/scripts/prepare-schema-diff.sh +5 -0
  60. package/templates/project/common/scripts/run-dev.sh +10 -1
  61. package/templates/project/common/scripts/run-tests.sh +6 -1
  62. package/templates/project/dotnet/.gitignore.extra +8 -0
  63. package/templates/project/dotnet/App.sln +56 -0
  64. package/templates/project/dotnet/Makefile +13 -0
  65. package/templates/project/dotnet/global.json +6 -0
  66. package/templates/project/dotnet/src/App/Data/AppDbContext.cs +12 -0
  67. package/templates/project/dotnet/src/App/Lakebase/DotEnv.cs +48 -0
  68. package/templates/project/dotnet/src/App/Lakebase/LakebaseConnection.cs +225 -0
  69. package/templates/project/dotnet/src/App/Program.cs +34 -0
  70. package/templates/project/dotnet/src/App/appsettings.json +9 -0
  71. package/templates/project/dotnet/tests/App.Tests/App.Tests.csproj +25 -0
  72. package/templates/project/dotnet/tests/App.Tests/HealthTests.cs +18 -0
  73. package/templates/project/dotnet/tests/App.Tests/OpenApiTests.cs +23 -0
  74. package/templates/project/dotnet-efcore/.config/dotnet-tools.json +13 -0
  75. package/templates/project/dotnet-efcore/src/App/App.csproj +22 -0
  76. package/templates/project/dotnet-efcore/src/App/Data/DesignTimeDbContextFactory.cs +24 -0
  77. package/templates/project/dotnet-efcore/src/App/Migrations/README.md +23 -0
  78. package/templates/project/dotnet-flyway/db/migrations/README.md +23 -0
  79. package/templates/project/dotnet-flyway/src/App/App.csproj +17 -0
@@ -30,6 +30,9 @@ import { normalizeTierName, protectedTierNamesFromEnv } from "./branch-utils.js"
30
30
  import "./adapters/alembic-adapter.js";
31
31
  import "./adapters/flyway-adapter.js";
32
32
  import "./adapters/knex-adapter.js";
33
+ import "./adapters/efcore-adapter.js";
34
+ import { isDotnetFlywayProject, flywayMigrationsDir } from "./dotnet-project.js";
35
+ import { listEfCoreFiles } from "./adapters/efcore-adapter.js";
33
36
  import {
34
37
  resolveSchemaMigrationAdapter,
35
38
  type SchemaMigrationAdapterId,
@@ -38,9 +41,9 @@ import {
38
41
  type CollapseHeadsResult,
39
42
  } from "./schema-migration-adapter.js";
40
43
 
41
- export type SchemaMigrationLanguage = "java" | "kotlin" | "python" | "nodejs";
44
+ export type SchemaMigrationLanguage = "java" | "kotlin" | "python" | "nodejs" | "dotnet";
42
45
 
43
- export type SchemaMigrationToolName = "flyway" | "alembic" | "knex";
46
+ export type SchemaMigrationToolName = "flyway" | "alembic" | "knex" | "efcore";
44
47
 
45
48
  export interface SchemaMigrationFile {
46
49
  /** Stable identifier sortable in apply-order: Flyway `V<n>`, Alembic
@@ -48,7 +51,7 @@ export interface SchemaMigrationFile {
48
51
  version: string;
49
52
  filename: string;
50
53
  description: string;
51
- type: "SQL" | "Python" | "JavaScript" | "TypeScript";
54
+ type: "SQL" | "Python" | "JavaScript" | "TypeScript" | "CSharp";
52
55
  /** Tool that should run this file. */
53
56
  tool: SchemaMigrationToolName;
54
57
  }
@@ -149,16 +152,24 @@ export function detectLanguage(projectDir: string): SchemaMigrationLanguage {
149
152
  if (lang === "unknown") {
150
153
  throw new SchemaMigrationError(
151
154
  `Could not detect project language in ${projectDir}. ` +
152
- `Expected one of: pom.xml (java/kotlin), pyproject.toml or alembic.ini (python), package.json (nodejs). ` +
155
+ `Expected one of: pom.xml (java/kotlin), pyproject.toml or alembic.ini (python), package.json (nodejs), *.sln or *.csproj (dotnet). ` +
153
156
  `Pass {language} explicitly to override.`
154
157
  );
155
158
  }
156
159
  return lang;
157
160
  }
158
161
 
159
- /** Map a language to the migration tool the kit invokes for it. */
160
- export function toolForLanguage(language: SchemaMigrationLanguage): SchemaMigrationToolName {
162
+ /**
163
+ * Map a language to the migration tool the kit invokes for it. .NET supports
164
+ * two tools, so it also needs the project directory: EF Core when a project
165
+ * references Microsoft.EntityFrameworkCore.Design, Flyway when SQL migrations
166
+ * live in db/migrations/ (Flyway wins when both are present), else EF Core.
167
+ */
168
+ export function toolForLanguage(language: SchemaMigrationLanguage, projectDir?: string): SchemaMigrationToolName {
161
169
  switch (language) {
170
+ case "dotnet":
171
+ if (projectDir && isDotnetFlywayProject(projectDir)) return "flyway";
172
+ return "efcore";
162
173
  case "java":
163
174
  case "kotlin":
164
175
  return "flyway";
@@ -182,9 +193,11 @@ export function toolForLanguage(language: SchemaMigrationLanguage): SchemaMigrat
182
193
  export function listSchemaMigrations(args: ListSchemaMigrationsArgs = {}): SchemaMigrationFile[] {
183
194
  const projectDir = args.projectDir ?? process.cwd();
184
195
  const language = args.language ?? detectLanguage(projectDir);
185
- const tool = toolForLanguage(language);
196
+ const tool = toolForLanguage(language, projectDir);
186
197
 
187
198
  switch (tool) {
199
+ case "efcore":
200
+ return listEfCoreFiles(projectDir);
188
201
  case "flyway":
189
202
  return listFlywayMigrations(projectDir);
190
203
  case "alembic":
@@ -195,8 +208,9 @@ export function listSchemaMigrations(args: ListSchemaMigrationsArgs = {}): Schem
195
208
  }
196
209
 
197
210
  function listFlywayMigrations(projectDir: string): SchemaMigrationFile[] {
198
- // Flyway convention: src/main/resources/db/migration/V<n>__<desc>.sql
199
- const dir = path.join(projectDir, "src", "main", "resources", "db", "migration");
211
+ // Flyway convention: V<n>__<desc>.sql under the Maven layout (Java/Kotlin)
212
+ // or db/migrations/ (.NET); see flywayMigrationsDir.
213
+ const dir = flywayMigrationsDir(projectDir);
200
214
  if (!fs.existsSync(dir)) return [];
201
215
  const files = fs.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
202
216
  return files
@@ -291,7 +305,7 @@ async function dsnFor(args: {
291
305
  /** Resolve an adapter, honoring an explicit language override. */
292
306
  function adapterFor(projectDir: string, language?: SchemaMigrationLanguage) {
293
307
  const override: SchemaMigrationAdapterId | undefined = language
294
- ? toolForLanguage(language)
308
+ ? toolForLanguage(language, projectDir)
295
309
  : undefined;
296
310
  return resolveSchemaMigrationAdapter(projectDir, override);
297
311
  }
@@ -143,6 +143,18 @@ jobs:
143
143
  fi
144
144
  npm install
145
145
 
146
+ - name: Set up .NET
147
+ if: steps.detect-lang.outputs.lang == 'dotnet'
148
+ uses: actions/setup-dotnet@v4
149
+ with:
150
+ global-json-file: global.json
151
+
152
+ - name: Restore .NET packages and tools
153
+ if: steps.detect-lang.outputs.lang == 'dotnet'
154
+ run: |
155
+ dotnet restore
156
+ if [ -f .config/dotnet-tools.json ]; then dotnet tool restore; fi
157
+
146
158
  # The official action installs to a user-writable path (no sudo). It's
147
159
  # a no-op if a compatible databricks CLI is already on PATH.
148
160
  - name: Install Databricks CLI
@@ -218,7 +230,7 @@ jobs:
218
230
  # 3. Default https://repo1.maven.org/maven2 for GitHub-hosted
219
231
  # runners with public internet.
220
232
  - name: Install Flyway CLI
221
- if: hashFiles('pom.xml') != ''
233
+ if: (hashFiles('pom.xml') != '' || hashFiles('db/migrations/**') != '')
222
234
  env:
223
235
  FLYWAY_DOWNLOAD_BASE_URL: ${{ vars.FLYWAY_DOWNLOAD_BASE_URL }}
224
236
  run: |
@@ -102,6 +102,18 @@ jobs:
102
102
  fi
103
103
  npm install
104
104
 
105
+ - name: Set up .NET
106
+ if: steps.detect-lang.outputs.lang == 'dotnet'
107
+ uses: actions/setup-dotnet@v4
108
+ with:
109
+ global-json-file: global.json
110
+
111
+ - name: Restore .NET packages and tools
112
+ if: steps.detect-lang.outputs.lang == 'dotnet'
113
+ run: |
114
+ dotnet restore
115
+ if [ -f .config/dotnet-tools.json ]; then dotnet tool restore; fi
116
+
105
117
  # Install frontend dependencies when a `client/` workspace is present
106
118
  # (Python+React layouts etc.). Gated on client/package.json so projects
107
119
  # without a frontend skip this transparently.
@@ -301,10 +313,12 @@ jobs:
301
313
  BODY="⚠️ **No Lakebase CI branch** was created. This run is not using a dedicated database branch. Open the [workflow run](${RUN_URL}), expand **Create CI database branch (Lakebase)** and check the step **Summary** (or log) for the reason and how to fix it."
302
314
  gh pr comment "$PR" --body "$BODY" 2>/dev/null || true
303
315
 
304
- # Install Flyway CLI for Java/Kotlin projects only. The substrate's
305
- # migrate runner spawns the standalone `flyway` binary; non-Java
306
- # projects don't need it. Runtime-gated on pom.xml so the YAML is
307
- # one shape for every language.
316
+ # Install Flyway CLI for Java/Kotlin projects and .NET projects that use
317
+ # Flyway (db/migrations/). The substrate's migrate runner spawns the
318
+ # standalone `flyway` binary; other projects don't need it. Runtime-gated
319
+ # on pom.xml / any file under db/migrations/ (what the adapter checks; a
320
+ # fresh scaffold has only its README there) so the YAML is one shape for
321
+ # every language.
308
322
  #
309
323
  # Resolution order:
310
324
  # 1. `flyway` already on PATH (self-hosted runner with brew /
@@ -315,7 +329,7 @@ jobs:
315
329
  # 3. Default https://repo1.maven.org/maven2 for GitHub-hosted
316
330
  # runners with public internet.
317
331
  - name: Install Flyway CLI
318
- if: steps.lakebase-db.outputs.url != '' && hashFiles('pom.xml') != ''
332
+ if: steps.lakebase-db.outputs.url != '' && (hashFiles('pom.xml') != '' || hashFiles('db/migrations/**') != '')
319
333
  env:
320
334
  FLYWAY_DOWNLOAD_BASE_URL: ${{ vars.FLYWAY_DOWNLOAD_BASE_URL }}
321
335
  run: |
@@ -388,6 +402,8 @@ jobs:
388
402
  uv run pytest
389
403
  elif [ "$LANG" = "nodejs" ]; then
390
404
  npm test
405
+ elif [ "$LANG" = "dotnet" ]; then
406
+ dotnet test --no-restore
391
407
  fi
392
408
  else
393
409
  echo "::error::No CI branch URL – cannot run tests without a database. Check the 'Create CI database branch' step for details."
@@ -570,6 +586,10 @@ jobs:
570
586
  desc=$(echo "$base" | sed 's/^[^_]*_//' | sed 's/\.py$//' | tr '_' ' ')
571
587
  [ -n "$rev" ] && echo "| $rev | $desc |" >> "$MD"
572
588
  done
589
+ elif [ "$LANG" = "dotnet" ]; then
590
+ # EF Core or Flyway: the kit's canonical discovery picks the tool + files.
591
+ npx --yes --package="${SCM_UTILS_NPX_PKG}" lakebase-schema-migrate list \
592
+ | jq -r '.[] | if .tool == "flyway" then "| V\(.version) | \(.description) |" else "| \(.version) | \(.description) (EF Core) |" end' >> "$MD"
573
593
  elif [ "$LANG" = "nodejs" ]; then
574
594
  for f in migrations/*.js; do
575
595
  [ -f "$f" ] || continue
@@ -66,10 +66,63 @@ elif [ -f "$REPO_ROOT/requirements.txt" ] || [ -f "$REPO_ROOT/pyproject.toml" ];
66
66
  source .venv/bin/activate
67
67
  fi
68
68
  alembic upgrade head "$@"
69
+ elif [ -n "$(find "$REPO_ROOT" -maxdepth 1 \( -name '*.sln' -o -name '*.slnx' -o -name '*.csproj' \) -print -quit 2>/dev/null)" ]; then
70
+ # .NET: EF Core or Flyway (db/migrations/).
71
+ if [ -n "${DATABASE_URL:-}" ]; then
72
+ # Explicit target (CI secret, ephemeral-verify DSN): run the tool directly.
73
+ if [ -d "$REPO_ROOT/db/migrations" ]; then
74
+ _np="${DATABASE_URL#postgresql://}"; _cr="${_np%%@*}"; _hp="${_np#*@}"
75
+ # Decode %XX only ('+' is literal in URL userinfo, and backslashes in a
76
+ # credential must not be interpreted as escapes).
77
+ _urldecode() {
78
+ local s="$1" out="" c
79
+ while [ -n "$s" ]; do
80
+ c="${s:0:1}"; s="${s:1}"
81
+ if [ "$c" = "%" ] && [ "${#s}" -ge 2 ]; then
82
+ out+="$(printf "\\x${s:0:2}")"; s="${s:2}"
83
+ else
84
+ out+="$c"
85
+ fi
86
+ done
87
+ printf '%s' "$out"
88
+ }
89
+ FLYWAY_URL="jdbc:postgresql://${_hp}" \
90
+ FLYWAY_USER="$(_urldecode "${_cr%%:*}")" \
91
+ FLYWAY_PASSWORD="$(_urldecode "${_cr#*:}")" \
92
+ flyway "-locations=filesystem:$REPO_ROOT/db/migrations" \
93
+ -baselineOnMigrate=true -baselineVersion=0 migrate "$@"
94
+ else
95
+ # Relative to the repo root (the cwd here): an absolute path through a
96
+ # symlinked directory (e.g. macOS /tmp -> /private/tmp) makes MSBuild see
97
+ # two different project paths and dotnet ef then loses the build output.
98
+ _csproj="$(grep -l "Microsoft.EntityFrameworkCore.Design" \
99
+ $(find . -maxdepth 3 -name '*.csproj' -not -path '*/tests/*' -not -path '*/test/*' \
100
+ -not -path '*/client/*' -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' | sort) \
101
+ 2>/dev/null | head -1)"
102
+ [ -n "$_csproj" ] || { echo "No *.csproj references Microsoft.EntityFrameworkCore.Design." >&2; exit 1; }
103
+ # dotnet ef reads project metadata before it builds, so restore first
104
+ # (a fresh clone or a cleaned obj/ has no assets file).
105
+ if [ -f .config/dotnet-tools.json ]; then dotnet tool restore >/dev/null; fi
106
+ dotnet restore "$_csproj" >/dev/null
107
+ # The design-time DbContext factory reads DATABASE_URL, so no password
108
+ # is passed on the command line.
109
+ dotnet ef database update --project "$_csproj" "$@"
110
+ fi
111
+ else
112
+ # Paired branch from .env metadata: the kit's tool-agnostic migrate CLI
113
+ # picks the tool, mints a fresh credential, and refuses protected tiers,
114
+ # exactly as CI does.
115
+ if [ ! -x "$SCRIPT_DIR/lk" ] || [ -z "${LAKEBASE_PROJECT_ID:-}" ] || [ -z "${LAKEBASE_BRANCH_ID:-}" ]; then
116
+ echo "Cannot migrate: need scripts/lk plus LAKEBASE_PROJECT_ID and LAKEBASE_BRANCH_ID (written to .env by the post-checkout hook), or DATABASE_URL." >&2
117
+ exit 1
118
+ fi
119
+ "$SCRIPT_DIR/lk" lakebase-schema-migrate apply \
120
+ --instance "$LAKEBASE_PROJECT_ID" --branch "$LAKEBASE_BRANCH_ID" "$@"
121
+ fi
69
122
  elif [ -f "$REPO_ROOT/package.json" ]; then
70
123
  # Node.js / Knex
71
124
  npx knex migrate:latest "$@"
72
125
  else
73
- echo "Could not detect project language. Expected pom.xml (Java), pyproject.toml/requirements.txt (Python), or package.json (Node.js)."
126
+ echo "Could not detect project language. Expected pom.xml (Java), pyproject.toml/requirements.txt (Python), *.sln/*.csproj (.NET), or package.json (Node.js)."
74
127
  exit 1
75
128
  fi
@@ -107,6 +107,11 @@ if [ -d src/main/resources/db/migration ]; then
107
107
  desc="$(echo "$base" | sed -n 's/^V[0-9.]*__\(.*\)\.sql$/\1/p' | tr '_' ' ')"
108
108
  [ -n "$ver" ] && echo "| V$ver | $desc |" >> "$MD"
109
109
  done
110
+ elif [ -n "$(find . -maxdepth 1 \( -name '*.sln' -o -name '*.slnx' -o -name '*.csproj' \) -print -quit 2>/dev/null)" ] \
111
+ && [ -x "$WORK_TREE/scripts/lk" ]; then
112
+ # .NET (EF Core or Flyway): the kit's canonical discovery picks the tool + files.
113
+ "$WORK_TREE/scripts/lk" lakebase-schema-migrate list 2>/dev/null \
114
+ | jq -r '.[] | if .tool == "flyway" then "| V\(.version) | \(.description) |" else "| \(.version) | \(.description) (EF Core) |" end' >> "$MD" || true
110
115
  elif [ -d alembic/versions ]; then
111
116
  # Alembic / Python
112
117
  for f in alembic/versions/*.py; do
@@ -209,6 +209,15 @@ for r in getattr(app, 'routes', []):
209
209
  fi
210
210
 
211
211
  uv run uvicorn app.main:app --reload --host "$HOST" --port "$PORT" "$@"
212
+ elif [ -n "$(find "$REPO_ROOT" -maxdepth 1 \( -name '*.sln' -o -name '*.slnx' -o -name '*.csproj' \) -print -quit 2>/dev/null)" ]; then
213
+ # .NET / ASP.NET Core. The app reads .env itself and mints its own credential.
214
+ PORT="$(resolve_port 8000)"
215
+ export PORT HOST
216
+ # Relative paths (cwd is the repo root): see the symlink note in flyway-migrate.sh.
217
+ _app="$(find src -maxdepth 2 -name '*.csproj' 2>/dev/null | head -1)"
218
+ _app="${_app:-$(find . -maxdepth 1 -name '*.csproj' 2>/dev/null | head -1)}"
219
+ echo "Serving on http://${HOST}:${PORT} (Ctrl-C to stop). Open it in your browser."
220
+ dotnet watch --project "$_app" run -- --urls "http://${HOST}:${PORT}" "$@"
212
221
  elif [ -f "$REPO_ROOT/package.json" ]; then
213
222
  # Node.js – prefer a "dev" script, fall back to "start".
214
223
  PORT="$(resolve_port 8000)"
@@ -220,6 +229,6 @@ elif [ -f "$REPO_ROOT/package.json" ]; then
220
229
  npm start -- "$@"
221
230
  fi
222
231
  else
223
- echo "Could not detect project language. Expected pom.xml (Java), pyproject.toml/requirements.txt (Python), or package.json (Node.js)."
232
+ echo "Could not detect project language. Expected pom.xml (Java), pyproject.toml/requirements.txt (Python), *.sln/*.csproj (.NET), or package.json (Node.js)."
224
233
  exit 1
225
234
  fi
@@ -140,13 +140,18 @@ elif [ -f "$REPO_ROOT/requirements.txt" ] || [ -f "$REPO_ROOT/pyproject.toml" ];
140
140
  else
141
141
  uv run --extra dev pytest "$@"
142
142
  fi
143
+ elif [ -n "$(find "$REPO_ROOT" -maxdepth 1 \( -name '*.sln' -o -name '*.slnx' -o -name '*.csproj' \) -print -quit 2>/dev/null)" ]; then
144
+ # .NET / EF Core or Flyway + xUnit
145
+ echo "Running migrations..."
146
+ "$REPO_ROOT/scripts/flyway-migrate.sh"
147
+ dotnet test "$@"
143
148
  elif [ -f "$REPO_ROOT/package.json" ]; then
144
149
  # Node.js / Knex + Jest
145
150
  echo "Running Knex migrations..."
146
151
  npx knex migrate:latest
147
152
  npm test "$@"
148
153
  else
149
- echo "Could not detect project language. Expected pom.xml (Java), pyproject.toml/requirements.txt (Python), or package.json (Node.js)."
154
+ echo "Could not detect project language. Expected pom.xml (Java), pyproject.toml/requirements.txt (Python), *.sln/*.csproj (.NET), or package.json (Node.js)."
150
155
  exit 1
151
156
  fi
152
157
 
@@ -0,0 +1,8 @@
1
+ # .NET
2
+ bin/
3
+ obj/
4
+ *.user
5
+ *.suo
6
+ .vs/
7
+ TestResults/
8
+ *.trx
@@ -0,0 +1,56 @@
1
+ 
2
+ Microsoft Visual Studio Solution File, Format Version 12.00
3
+ # Visual Studio Version 17
4
+ VisualStudioVersion = 17.0.31903.59
5
+ MinimumVisualStudioVersion = 10.0.40219.1
6
+ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
7
+ EndProject
8
+ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{DCBDD7E2-BB16-483D-92E7-913AB5681AA2}"
9
+ EndProject
10
+ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
11
+ EndProject
12
+ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App.Tests", "tests\App.Tests\App.Tests.csproj", "{3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}"
13
+ EndProject
14
+ Global
15
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
16
+ Debug|Any CPU = Debug|Any CPU
17
+ Debug|x64 = Debug|x64
18
+ Debug|x86 = Debug|x86
19
+ Release|Any CPU = Release|Any CPU
20
+ Release|x64 = Release|x64
21
+ Release|x86 = Release|x86
22
+ EndGlobalSection
23
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
24
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
26
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Debug|x64.ActiveCfg = Debug|Any CPU
27
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Debug|x64.Build.0 = Debug|Any CPU
28
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Debug|x86.ActiveCfg = Debug|Any CPU
29
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Debug|x86.Build.0 = Debug|Any CPU
30
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
31
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Release|Any CPU.Build.0 = Release|Any CPU
32
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Release|x64.ActiveCfg = Release|Any CPU
33
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Release|x64.Build.0 = Release|Any CPU
34
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Release|x86.ActiveCfg = Release|Any CPU
35
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2}.Release|x86.Build.0 = Release|Any CPU
36
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
38
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Debug|x64.ActiveCfg = Debug|Any CPU
39
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Debug|x64.Build.0 = Debug|Any CPU
40
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Debug|x86.ActiveCfg = Debug|Any CPU
41
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Debug|x86.Build.0 = Debug|Any CPU
42
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
43
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Release|Any CPU.Build.0 = Release|Any CPU
44
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Release|x64.ActiveCfg = Release|Any CPU
45
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Release|x64.Build.0 = Release|Any CPU
46
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Release|x86.ActiveCfg = Release|Any CPU
47
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E}.Release|x86.Build.0 = Release|Any CPU
48
+ EndGlobalSection
49
+ GlobalSection(SolutionProperties) = preSolution
50
+ HideSolutionNode = FALSE
51
+ EndGlobalSection
52
+ GlobalSection(NestedProjects) = preSolution
53
+ {DCBDD7E2-BB16-483D-92E7-913AB5681AA2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
54
+ {3E88FEDA-D8D3-4939-BD9C-CFDD1EEBBC1E} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
55
+ EndGlobalSection
56
+ EndGlobal
@@ -0,0 +1,13 @@
1
+ .PHONY: install test migrate run
2
+
3
+ install:
4
+ dotnet restore App.sln
5
+
6
+ test:
7
+ dotnet test App.sln
8
+
9
+ migrate:
10
+ ./scripts/flyway-migrate.sh
11
+
12
+ run:
13
+ dotnet run --project src/App -- --urls "http://127.0.0.1:$${PORT:-8000}"
@@ -0,0 +1,6 @@
1
+ {
2
+ "sdk": {
3
+ "version": "10.0.100",
4
+ "rollForward": "latestFeature"
5
+ }
6
+ }
@@ -0,0 +1,12 @@
1
+ using Microsoft.EntityFrameworkCore;
2
+
3
+ namespace App.Data;
4
+
5
+ /// <summary>
6
+ /// Application DbContext. Add DbSet&lt;TEntity&gt; properties for your tables.
7
+ /// Schema changes go through migrations (see the project README), never
8
+ /// EnsureCreated().
9
+ /// </summary>
10
+ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
11
+ {
12
+ }
@@ -0,0 +1,48 @@
1
+ namespace App.Lakebase;
2
+
3
+ /// <summary>
4
+ /// Minimal .env loader (KEY=VALUE lines, # comments, optional quotes). Never
5
+ /// overwrites variables already set in the process environment.
6
+ /// </summary>
7
+ public static class DotEnv
8
+ {
9
+ public static void LoadFromRepoRoot()
10
+ {
11
+ foreach (var start in new[] { Directory.GetCurrentDirectory(), AppContext.BaseDirectory })
12
+ {
13
+ for (var dir = new DirectoryInfo(start); dir is not null; dir = dir.Parent)
14
+ {
15
+ var candidate = Path.Combine(dir.FullName, ".env");
16
+ if (File.Exists(candidate))
17
+ {
18
+ Load(candidate);
19
+ return;
20
+ }
21
+ }
22
+ }
23
+ }
24
+
25
+ public static void Load(string path)
26
+ {
27
+ foreach (var raw in File.ReadAllLines(path))
28
+ {
29
+ var line = raw.Trim();
30
+ if (line.Length == 0 || line.StartsWith('#'))
31
+ continue;
32
+ if (line.StartsWith("export ", StringComparison.Ordinal))
33
+ line = line[7..].TrimStart();
34
+
35
+ var eq = line.IndexOf('=');
36
+ if (eq <= 0)
37
+ continue;
38
+
39
+ var key = line[..eq].Trim();
40
+ var value = line[(eq + 1)..].Trim();
41
+ if (value.Length >= 2 && (value[0] == '"' || value[0] == '\'') && value[^1] == value[0])
42
+ value = value[1..^1];
43
+
44
+ if (Environment.GetEnvironmentVariable(key) is null)
45
+ Environment.SetEnvironmentVariable(key, value);
46
+ }
47
+ }
48
+ }