@databricks-solutions/lakebase-scm-utils 0.2.40 → 0.2.42

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 (74) hide show
  1. package/dist/scripts/github/index.cjs +3 -1
  2. package/dist/scripts/github/index.js +3 -1
  3. package/dist/scripts/github/pr.cli.cjs +3 -1
  4. package/dist/scripts/github/pr.cli.js +3 -1
  5. package/dist/scripts/index.cjs +66 -21
  6. package/dist/scripts/index.d.cts +1 -1
  7. package/dist/scripts/index.d.ts +1 -1
  8. package/dist/scripts/index.js +59 -15
  9. package/dist/scripts/lakebase/branch.cli.cjs +3 -1
  10. package/dist/scripts/lakebase/branch.cli.js +3 -1
  11. package/dist/scripts/lakebase/ci-app-endpoint.cli.cjs +3 -1
  12. package/dist/scripts/lakebase/ci-app-endpoint.cli.js +3 -1
  13. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +5 -3
  14. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +5 -3
  15. package/dist/scripts/lakebase/collapse-heads.cli.cjs +5 -3
  16. package/dist/scripts/lakebase/collapse-heads.cli.js +5 -3
  17. package/dist/scripts/lakebase/cut-backup.cli.cjs +3 -1
  18. package/dist/scripts/lakebase/cut-backup.cli.js +3 -1
  19. package/dist/scripts/lakebase/cut-tier.cli.cjs +3 -1
  20. package/dist/scripts/lakebase/cut-tier.cli.js +3 -1
  21. package/dist/scripts/lakebase/detect-language.cli.cjs +5 -3
  22. package/dist/scripts/lakebase/detect-language.cli.js +5 -3
  23. package/dist/scripts/lakebase/doctor.cli.cjs +5 -3
  24. package/dist/scripts/lakebase/doctor.cli.js +5 -3
  25. package/dist/scripts/lakebase/get-connection.cli.cjs +5 -3
  26. package/dist/scripts/lakebase/get-connection.cli.js +5 -3
  27. package/dist/scripts/lakebase/index.cjs +66 -21
  28. package/dist/scripts/lakebase/index.d.cts +5 -1
  29. package/dist/scripts/lakebase/index.d.ts +5 -1
  30. package/dist/scripts/lakebase/index.js +59 -15
  31. package/dist/scripts/lakebase/infra-runner.cli.cjs +5 -3
  32. package/dist/scripts/lakebase/infra-runner.cli.js +5 -3
  33. package/dist/scripts/lakebase/new-migration.cli.cjs +5 -3
  34. package/dist/scripts/lakebase/new-migration.cli.js +5 -3
  35. package/dist/scripts/lakebase/schema-diff.cli.cjs +5 -3
  36. package/dist/scripts/lakebase/schema-diff.cli.js +5 -3
  37. package/dist/scripts/lakebase/schema-migrate.cli.cjs +5 -3
  38. package/dist/scripts/lakebase/schema-migrate.cli.js +5 -3
  39. package/dist/scripts/lakebase/scm-abandon-feature.cli.cjs +37 -1
  40. package/dist/scripts/lakebase/scm-abandon-feature.cli.js +37 -1
  41. package/dist/scripts/lakebase/scm-adopt-state.cli.cjs +37 -1
  42. package/dist/scripts/lakebase/scm-adopt-state.cli.js +37 -1
  43. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +45 -9
  44. package/dist/scripts/lakebase/scm-claim-feature.cli.js +39 -3
  45. package/dist/scripts/lakebase/scm-cleanup.cli.cjs +3 -1
  46. package/dist/scripts/lakebase/scm-cleanup.cli.js +3 -1
  47. package/dist/scripts/lakebase/scm-doctor.cli.cjs +56 -29
  48. package/dist/scripts/lakebase/scm-doctor.cli.js +50 -23
  49. package/dist/scripts/lakebase/scm-feature-branch.cli.cjs +1 -0
  50. package/dist/scripts/lakebase/scm-feature-branch.cli.js +1 -0
  51. package/dist/scripts/lakebase/scm-merge.cli.cjs +45 -9
  52. package/dist/scripts/lakebase/scm-merge.cli.js +39 -3
  53. package/dist/scripts/lakebase/scm-prepare-pr.cli.cjs +34 -0
  54. package/dist/scripts/lakebase/scm-prepare-pr.cli.js +34 -0
  55. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +6 -3
  56. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +6 -3
  57. package/dist/scripts/lakebase/scm-recover-orphans.cli.cjs +37 -1
  58. package/dist/scripts/lakebase/scm-recover-orphans.cli.js +37 -1
  59. package/dist/scripts/lakebase/scm-state.cli.cjs +5 -4
  60. package/dist/scripts/lakebase/scm-state.cli.js +4 -3
  61. package/dist/scripts/lakebase/scm-wait-ci.cli.cjs +34 -0
  62. package/dist/scripts/lakebase/scm-wait-ci.cli.js +34 -0
  63. package/dist/scripts/util/index.cjs +5 -3
  64. package/dist/scripts/util/index.js +5 -3
  65. package/package.json +1 -1
  66. package/scripts/lakebase/databricks-cli.ts +7 -1
  67. package/scripts/lakebase/enable-e2e.ts +19 -3
  68. package/scripts/lakebase/scm-doctor.ts +1 -12
  69. package/scripts/lakebase/scm-workflow-state.ts +49 -0
  70. package/templates/project/common/.github/workflows/merge.yml +14 -5
  71. package/templates/project/common/.github/workflows/pr.yml +14 -5
  72. package/templates/project/common/.gitignore.base +8 -0
  73. package/templates/project/common/scripts/lk +66 -33
  74. package/templates/project/common/scripts/run-tests.sh +18 -1
@@ -1630,8 +1630,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
1630
1630
  var cached;
1631
1631
  function substrateSelfVersion() {
1632
1632
  if (cached !== void 0) return cached;
1633
- if ("0.2.40".length > 0) {
1634
- cached = "0.2.40";
1633
+ if ("0.2.42".length > 0) {
1634
+ cached = "0.2.42";
1635
1635
  return cached;
1636
1636
  }
1637
1637
  cached = "unknown";
@@ -1866,7 +1866,9 @@ function resolveProfile(opts) {
1866
1866
  function buildInvocation(args, opts) {
1867
1867
  const base = opts.env ?? process.env;
1868
1868
  const trimmedHost = effectiveHost(opts)?.replace(/\/+$/, "");
1869
- const env = trimmedHost ? { ...base, DATABRICKS_HOST: trimmedHost } : base;
1869
+ const env = { ...base };
1870
+ if (trimmedHost) env.DATABRICKS_HOST = trimmedHost;
1871
+ delete env.DATABRICKS_WORKSPACE_ID;
1870
1872
  const profile = resolveProfile(opts);
1871
1873
  const argv = profile && !opts.noProfile && !args.includes("--profile") ? [...args, "--profile", profile] : args;
1872
1874
  return { argv, env, profile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databricks-solutions/lakebase-scm-utils",
3
- "version": "0.2.40",
3
+ "version": "0.2.42",
4
4
  "description": "Lakebase SCM + substrate engine: database branching, paired-branch SCM workflows, connection/credential minting, schema migration, project scaffold + deploy primitives, and the git/github utility layer. Extracted from consort for portable, standalone consumption by lakebase-scm-extension and the SFTDD orchestration.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -149,7 +149,13 @@ export function buildInvocation(args: string[], opts: DatabricksCliOptions): {
149
149
  // so an in-project call pins the project workspace rather than the DEFAULT
150
150
  // profile's default host. Trailing slash stripped for a stable value.
151
151
  const trimmedHost = effectiveHost(opts)?.replace(/\/+$/, "");
152
- const env: NodeJS.ProcessEnv = trimmedHost ? { ...base, DATABRICKS_HOST: trimmedHost } : base;
152
+ const env: NodeJS.ProcessEnv = { ...base };
153
+ if (trimmedHost) env.DATABRICKS_HOST = trimmedHost;
154
+ // A stray DATABRICKS_WORKSPACE_ID (a bare numeric workspace ID, never a URL)
155
+ // makes the CLI's unified-host path dial https://<id> (issue #204.1). The
156
+ // workspace always resolves via the profile/host chain above, so scrub the
157
+ // stray var from the child env (the documented workaround, centralized).
158
+ delete env.DATABRICKS_WORKSPACE_ID;
153
159
  const profile = resolveProfile(opts);
154
160
  const argv =
155
161
  profile && !opts.noProfile && !args.includes("--profile")
@@ -225,12 +225,27 @@ function runTestsE2eBlock(): string {
225
225
  ' export VITE_PROXY_TARGET="http://127.0.0.1:${E2E_BACKEND_PORT}"',
226
226
  ' echo "Local E2E free-port: backend :$E2E_BACKEND_PORT / client :$E2E_CLIENT_PORT"',
227
227
  'fi',
228
+ // Stale-server hygiene (issue #197): CI=1 on the Playwright invocations below
229
+ // forces EVERY config vintage's reuseExistingServer guard OFF (old `!CI` and
230
+ // new `!!BASE_URL` alike), so the verify never reuses a stale server bound to
231
+ // an unmigrated branch (the false-500 misdiagnosis). A lingering uvicorn/vite
232
+ // on the conventional default ports still serves STALE schema to anything
233
+ // that reaches it, so clear it (TERM, not KILL) before the boot - never when
234
+ // the caller pre-set the ports (the deploy gate owns that server; BASE_URL).
235
+ 'if [ -z "${BASE_URL:-}" ] && command -v lsof >/dev/null 2>&1; then',
236
+ ' for p in "${E2E_BACKEND_PORT:-8000}" "${E2E_CLIENT_PORT:-5173}"; do',
237
+ ' if lsof -iTCP:"$p" -sTCP:LISTEN -n -P >/dev/null 2>&1; then',
238
+ ' echo "Local E2E: clearing a stale listener on :$p before the webServer boot"',
239
+ ' lsof -ti tcp:"$p" 2>/dev/null | xargs kill 2>/dev/null || true',
240
+ ' fi',
241
+ ' done',
242
+ 'fi',
228
243
  'if [ -f "$REPO_ROOT/playwright.config.ts" ] || [ -f "$REPO_ROOT/playwright.config.js" ]; then',
229
244
  ' echo "Running Playwright E2E tests..."',
230
245
  ' if [ -f "$REPO_ROOT/package.json" ] && command -v npm >/dev/null 2>&1; then',
231
- ' (cd "$REPO_ROOT" && npm run test:e2e)',
246
+ ' (cd "$REPO_ROOT" && CI=1 npm run test:e2e)',
232
247
  " else",
233
- ' (cd "$REPO_ROOT" && npx --yes playwright test)',
248
+ ' (cd "$REPO_ROOT" && CI=1 npx --yes playwright test)',
234
249
  " fi",
235
250
  // Python E2E: pytest-playwright + the shipped tests/e2e/conftest.py
236
251
  // (live_server). Gated on the conftest + pyproject so it only fires for a
@@ -269,7 +284,8 @@ function runTestsE2eBlock(): string {
269
284
  'if [ -f "$REPO_ROOT/client/playwright.config.ts" ] || [ -f "$REPO_ROOT/client/playwright.config.js" ] || [ -f "$REPO_ROOT/client/playwright.config.mjs" ]; then',
270
285
  ' echo "Running client Playwright E2E tests..."',
271
286
  ' (cd "$REPO_ROOT/client" && npx --yes playwright install chromium)',
272
- ' (cd "$REPO_ROOT/client" && npx --yes playwright test --pass-with-no-tests)',
287
+ // CI=1: no server reuse on any config vintage (see the stale-server note above).
288
+ ' (cd "$REPO_ROOT/client" && CI=1 npx --yes playwright test --pass-with-no-tests)',
273
289
  "fi",
274
290
  "",
275
291
  ].join("\n");
@@ -9,7 +9,6 @@
9
9
  // an error rather than performing a related-but-different remediation.
10
10
 
11
11
  import * as fs from "node:fs";
12
- import { execFileSync } from "node:child_process";
13
12
  import * as path from "node:path";
14
13
  import {
15
14
  getBranchByName,
@@ -25,6 +24,7 @@ import { recoverOrphans } from "./scm-recover-orphans.js";
25
24
  import { abandonFeatureBranch } from "./scm-abandon-feature.js";
26
25
  import {
27
26
  readWorkflowState,
27
+ isGitTracked,
28
28
  type ScmWorkflowState,
29
29
  type TierTopology,
30
30
  } from "./scm-workflow-state.js";
@@ -106,17 +106,6 @@ function leafOf(b: LakebaseBranchInfo): string {
106
106
  return b.name.split("/").pop() ?? b.name;
107
107
  }
108
108
 
109
- /** True iff `rel` (repo-relative) is git-tracked in projectDir. Best-effort: no
110
- * git / not a repo -> false. */
111
- function isGitTracked(projectDir: string, rel: string): boolean {
112
- try {
113
- execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
114
- return true;
115
- } catch {
116
- return false;
117
- }
118
- }
119
-
120
109
  function worstOf(a: DoctorSeverity, b: DoctorSeverity): DoctorSeverity {
121
110
  const order: DoctorSeverity[] = ["ok", "warn", "fail"];
122
111
  return order[Math.max(order.indexOf(a), order.indexOf(b))] as DoctorSeverity;
@@ -13,6 +13,52 @@
13
13
 
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
+ import { execFileSync } from "node:child_process";
17
+
18
+ /** True iff `rel` (repo-relative) is git-tracked in projectDir. Best-effort: no
19
+ * git / not a repo -> false. Shared home (scm-doctor's Finding-28 check reads
20
+ * the same predicate). */
21
+ export function isGitTracked(projectDir: string, rel: string): boolean {
22
+ try {
23
+ execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ /** Ensure the runtime SCM claim state is NOT git-tracked (issue #203 / Finding 28):
31
+ * a tracked .lakebase/workflow-state.json gets a branch-COMMITTED stale claim
32
+ * restored over the live one by every branch checkout / `git reset --hard
33
+ * origin/<tier>`, which then blocks the next feature's claim
34
+ * ("already-claimed-other") until a manual abandon+reclaim. Untrack it
35
+ * (index-only; the working file stays) and cover it in .gitignore, so the live
36
+ * claim survives checkouts. The committed .lakebase/kit-ref + scm-utils-ref stay
37
+ * tracked deliberately (CI resolves them). Idempotent + best-effort (non-git
38
+ * dirs / unwritable .gitignore skip silently – the state write itself still lands). */
39
+ function ensureWorkflowStateUntracked(projectDir: string): void {
40
+ const rel = ".lakebase/workflow-state.json";
41
+ try {
42
+ if (isGitTracked(projectDir, rel)) {
43
+ execFileSync("git", ["rm", "--cached", "--quiet", "--ignore-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
44
+ }
45
+ } catch {
46
+ /* best-effort */
47
+ }
48
+ try {
49
+ const gitignore = path.join(projectDir, ".gitignore");
50
+ const existing = fs.existsSync(gitignore) ? fs.readFileSync(gitignore, "utf8") : "";
51
+ if (!existing.split("\n").some((l) => l.trim() === rel)) {
52
+ const sep = existing === "" || existing.endsWith("\n") ? "" : "\n";
53
+ fs.appendFileSync(
54
+ gitignore,
55
+ `${sep}# Runtime SCM claim state (per working tree): a branch checkout must never restore\n# a stale committed claim over the live one (issue #203 / Finding 28).\n${rel}\n`,
56
+ );
57
+ }
58
+ } catch {
59
+ /* best-effort */
60
+ }
61
+ }
16
62
 
17
63
  /** All SCM states, in canonical progression order. */
18
64
  export const SCM_STATES = [
@@ -133,6 +179,9 @@ export function writeWorkflowState(
133
179
  .join("\n");
134
180
  throw new Error(`Refusing to write invalid SCM state:\n${summary}`);
135
181
  }
182
+ // Self-heal on every write (issue #203): the live claim survives checkouts
183
+ // only while the state file is untracked.
184
+ ensureWorkflowStateUntracked(projectDir);
136
185
  const dir = path.join(projectDir, ".lakebase");
137
186
  fs.mkdirSync(dir, { recursive: true });
138
187
  const target = stateFilePath(projectDir);
@@ -72,7 +72,16 @@ jobs:
72
72
  # the version this project was scaffolded from, when .lakebase/scm-utils-ref is absent.
73
73
  run: |
74
74
  SCM_UTILS_REF="$(tr -d '[:space:]' < .lakebase/scm-utils-ref 2>/dev/null || true)"
75
- echo "SCM_UTILS_REF=${SCM_UTILS_REF:-v{{LAKEBASE_SCM_UTILS_VERSION}}}" >> "$GITHUB_ENV"
75
+ SCM_UTILS_REF="${SCM_UTILS_REF:-v{{LAKEBASE_SCM_UTILS_VERSION}}}"
76
+ echo "SCM_UTILS_REF=${SCM_UTILS_REF}" >> "$GITHUB_ENV"
77
+ # Registry-first: version-tag refs install the published tarball from the
78
+ # npm registry (seconds); branch/SHA refs keep the GitHub source form
79
+ # (their only source).
80
+ case "$SCM_UTILS_REF" in
81
+ v[0-9]*) echo "SCM_UTILS_NPX_PKG=@databricks-solutions/lakebase-scm-utils@${SCM_UTILS_REF#v}" >> "$GITHUB_ENV" ;;
82
+ [0-9]*) echo "SCM_UTILS_NPX_PKG=@databricks-solutions/lakebase-scm-utils@${SCM_UTILS_REF}" >> "$GITHUB_ENV" ;;
83
+ *) echo "SCM_UTILS_NPX_PKG=github:databricks-solutions/lakebase-scm-utils#${SCM_UTILS_REF}" >> "$GITHUB_ENV" ;;
84
+ esac
76
85
 
77
86
  - name: Detect project language
78
87
  id: detect-lang
@@ -84,7 +93,7 @@ jobs:
84
93
  # this project.
85
94
  run: |
86
95
  LANG="$(npx --yes \
87
- --package=github:databricks-solutions/lakebase-scm-utils#"${SCM_UTILS_REF}" \
96
+ --package="${SCM_UTILS_NPX_PKG}" \
88
97
  lakebase-detect-language)"
89
98
  echo "lang=$LANG" >> $GITHUB_OUTPUT
90
99
  echo "Detected language: $LANG"
@@ -268,7 +277,7 @@ jobs:
268
277
  echo " instance: $LAKEBASE_PROJECT_ID"
269
278
  echo " source: $DEFAULT_NAME"
270
279
  echo " backup: $SNAPSHOT_NAME"
271
- if CREATE_OUT="$(npx --yes --package=github:databricks-solutions/lakebase-scm-utils#"${SCM_UTILS_REF}" \
280
+ if CREATE_OUT="$(npx --yes --package="${SCM_UTILS_NPX_PKG}" \
272
281
  lakebase-cut-backup \
273
282
  --instance "$LAKEBASE_PROJECT_ID" \
274
283
  --source "$DEFAULT_NAME" \
@@ -312,7 +321,7 @@ jobs:
312
321
  # is the promotion opt-in (allowTier) + verifies the tier reached head,
313
322
  # so a partial promotion fails the job instead of leaving git ahead of
314
323
  # the Lakebase schema. Single-tier (default branch) also goes through it.
315
- npx --yes --package=github:databricks-solutions/lakebase-scm-utils#"${SCM_UTILS_REF}" \
324
+ npx --yes --package="${SCM_UTILS_NPX_PKG}" \
316
325
  lakebase-schema-migrate apply-tier \
317
326
  --instance "$LAKEBASE_PROJECT_ID" \
318
327
  --branch "$LAKEBASE_BRANCH_NAME" \
@@ -394,7 +403,7 @@ jobs:
394
403
  echo " substrate ref: ${SCM_UTILS_REF}"
395
404
  echo " instance: $LAKEBASE_PROJECT_ID"
396
405
  echo " branch: $LAKEBASE_BRANCH_NAME"
397
- npx --yes --package=github:databricks-solutions/lakebase-scm-utils#"${SCM_UTILS_REF}" \
406
+ npx --yes --package="${SCM_UTILS_NPX_PKG}" \
398
407
  lakebase-schema-migrate status \
399
408
  --instance "$LAKEBASE_PROJECT_ID" \
400
409
  --branch "$LAKEBASE_BRANCH_NAME" \
@@ -31,7 +31,16 @@ jobs:
31
31
  # the version this project was scaffolded from, when .lakebase/scm-utils-ref is absent.
32
32
  run: |
33
33
  SCM_UTILS_REF="$(tr -d '[:space:]' < .lakebase/scm-utils-ref 2>/dev/null || true)"
34
- echo "SCM_UTILS_REF=${SCM_UTILS_REF:-v{{LAKEBASE_SCM_UTILS_VERSION}}}" >> "$GITHUB_ENV"
34
+ SCM_UTILS_REF="${SCM_UTILS_REF:-v{{LAKEBASE_SCM_UTILS_VERSION}}}"
35
+ echo "SCM_UTILS_REF=${SCM_UTILS_REF}" >> "$GITHUB_ENV"
36
+ # Registry-first: version-tag refs install the published tarball from the
37
+ # npm registry (seconds); branch/SHA refs keep the GitHub source form
38
+ # (their only source).
39
+ case "$SCM_UTILS_REF" in
40
+ v[0-9]*) echo "SCM_UTILS_NPX_PKG=@databricks-solutions/lakebase-scm-utils@${SCM_UTILS_REF#v}" >> "$GITHUB_ENV" ;;
41
+ [0-9]*) echo "SCM_UTILS_NPX_PKG=@databricks-solutions/lakebase-scm-utils@${SCM_UTILS_REF}" >> "$GITHUB_ENV" ;;
42
+ *) echo "SCM_UTILS_NPX_PKG=github:databricks-solutions/lakebase-scm-utils#${SCM_UTILS_REF}" >> "$GITHUB_ENV" ;;
43
+ esac
35
44
 
36
45
  - name: Detect project language
37
46
  id: detect-lang
@@ -43,7 +52,7 @@ jobs:
43
52
  # this project.
44
53
  run: |
45
54
  LANG="$(npx --yes \
46
- --package=github:databricks-solutions/lakebase-scm-utils#"${SCM_UTILS_REF}" \
55
+ --package="${SCM_UTILS_NPX_PKG}" \
47
56
  lakebase-detect-language)"
48
57
  echo "lang=$LANG" >> $GITHUB_OUTPUT
49
58
  echo "Detected language: $LANG"
@@ -343,7 +352,7 @@ jobs:
343
352
  echo " substrate ref: ${SCM_UTILS_REF}"
344
353
  echo " instance: $LAKEBASE_PROJECT_ID"
345
354
  echo " branch: $LAKEBASE_BRANCH_NAME"
346
- npx --yes --package=github:databricks-solutions/lakebase-scm-utils#"${SCM_UTILS_REF}" \
355
+ npx --yes --package="${SCM_UTILS_NPX_PKG}" \
347
356
  lakebase-schema-migrate apply \
348
357
  --instance "$LAKEBASE_PROJECT_ID" \
349
358
  --branch "$LAKEBASE_BRANCH_NAME" \
@@ -469,7 +478,7 @@ jobs:
469
478
  # `|| URL=""` clause keeps the workflow green and the env var
470
479
  # unset so the downstream Playwright step degrades cleanly.
471
480
  URL="$(npx --yes \
472
- --package=github:databricks-solutions/lakebase-scm-utils#"${SCM_UTILS_REF}" \
481
+ --package="${SCM_UTILS_NPX_PKG}" \
473
482
  lakebase-ci-app-endpoint \
474
483
  --instance "$LAKEBASE_PROJECT_ID" \
475
484
  --branch "$CI_BRANCH")" || URL=""
@@ -590,7 +599,7 @@ jobs:
590
599
  if [ -n "$PRODUCTION_DATASOURCE_URL" ] && [ -n "$BRANCH" ]; then
591
600
  echo "### Schema diff: \`${BRANCH}\` vs parent (\`${PR_BASE_REF}\`)" >> "$MD"
592
601
  echo "" >> "$MD"
593
- if ! npx --yes --package=github:databricks-solutions/lakebase-scm-utils#"${SCM_UTILS_REF}" \
602
+ if ! npx --yes --package="${SCM_UTILS_NPX_PKG}" \
594
603
  lakebase-schema-diff \
595
604
  --instance "$LAKEBASE_PROJECT_ID" \
596
605
  --branch "$BRANCH" \
@@ -58,6 +58,14 @@ application-local.properties
58
58
  .lakebase/kit-ref.local
59
59
  .lakebase/scm-utils-ref.local
60
60
 
61
+ # The runtime SCM CLAIM state: per working tree, rewritten by every SCM transition
62
+ # (claim / pr-ready / ci-green / merged). If it is git-tracked, a branch checkout
63
+ # or `git reset --hard origin/<tier>` restores a branch-COMMITTED stale claim over
64
+ # the live one, and the next feature's claim then refuses "already-claimed-other"
65
+ # until a manual abandon+reclaim (issue #203 / Finding 28). Never track it; the
66
+ # committed kit-ref / scm-utils-ref pins above stay tracked deliberately.
67
+ .lakebase/workflow-state.json
68
+
61
69
  # Per-agent local memory: each role subagent (memory: project) persists its own
62
70
  # learnings here mid-run. It is local + churny (re-written every run), not feature
63
71
  # code, so it must never be committed nor count as a dirty working tree , an
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env bash
2
2
  # lk , run a substrate/SCM or SFTDD CLI fast, without the npx tax.
3
3
  #
4
- # `npx --package=github:databricks-solutions/<pkg>#<ref> <bin>` re-resolves the
5
- # GitHub ref on EVERY call (~3.5s even warm). This shim resolves a package ONCE
6
- # per ref into a shared cache and runs the bin via `node dist/...` (~0.09s).
4
+ # `npx --package=<pkg>@<version> <bin>` re-resolves on EVERY call (~3.5s even
5
+ # warm). This shim resolves a package ONCE per ref into a shared cache and runs
6
+ # the bin via `node dist/...` (~0.09s). Cold-cache installs pull the published
7
+ # tarball from the npm registry for version-tag refs (seconds; GitHub is the
8
+ # fallback) and clone+install from GitHub for branch/SHA refs (their only source).
7
9
  #
8
10
  # Two packages back a scaffolded project, and lk routes each bin to its owner so
9
11
  # each runs from its OWN dist (no cross-package node_modules paths):
@@ -110,27 +112,20 @@ resolve_pkg() {
110
112
  node -e 'const fs=require("fs"),p=require("path"),r=process.argv[1];let b={};try{b=(require(r+"/package.json").bin)||{}}catch(e){process.stderr.write("lk: cannot read package.json at "+r+"\n");process.exit(1)}const m=Object.values(b).filter(x=>!fs.existsSync(p.join(r,x)));if(m.length){process.stderr.write("lk: "+r+" dist is INCOMPLETE , "+m.length+"/"+Object.keys(b).length+" bin(s) have no file (e.g. "+m.slice(0,3).join(", ")+").\n");process.exit(1)}' "$1"
111
113
  }
112
114
 
113
- install_pkg() {
114
- local committish="$1" sha="$2" tmp
115
- mkdir -p "$cache_root"
116
- tmp="$(mktemp -d "${cache_root}/.tmp.XXXXXX")"
117
- # Tell the consumer what is happening: this is a full npm install from git and
118
- # takes a couple minutes over the network the FIRST time a ref is cached (the
119
- # kit + its runtime deps). Without this, a silent multi-minute install looks
120
- # hung. `--omit=dev` skips the build-toolchain devDeps (tsup/vitest/typescript/
121
- # esbuild) , the kit ships prebuilt dist/, so consumers never build; `--no-audit
122
- # --no-fund` drop registry round-trips. npm's own progress goes to stderr (>&2)
123
- # so an interactive warm/rewarm narrates instead of going dark.
124
- echo "lk: Downloading the Consort toolkit (${pkg}@${committish}) , one-time for this version, ~1-2 min (kit + its dependencies). Later commands are instant." >&2
125
- # SHOW WHAT IS BEING INSTALLED , not just a clock. npm's progress bar is TTY-only
126
- # (silent when piped to a log for the relay), but `--loglevel=http` prints one line
127
- # per package fetched (`npm http fetch GET 200 …/<pkg>/-/<pkg>-<ver>.tgz`) , exactly
128
- # "what is being installed". We run npm to a temp log and FORWARD its new lines
129
- # (prefixed `lk:` so the narrator relays them) every few seconds as they land; only
130
- # when npm is briefly between fetches (e.g. the initial git clone) do we print an
131
- # elapsed-time line. So the relay never goes silent and never looks like a timeout.
115
+ # npm_install_with_progress <spec>: install <spec> into $tmp in the background
116
+ # while narrating. `--omit=dev` skips the build-toolchain devDeps (tsup/vitest/
117
+ # typescript/esbuild) , the kit ships prebuilt dist/, so consumers never build;
118
+ # `--no-audit --no-fund` drop registry round-trips. SHOW WHAT IS BEING INSTALLED,
119
+ # not just a clock: npm's progress bar is TTY-only (silent when piped to a log
120
+ # for the relay), but `--loglevel=http` prints one line per package fetched, so
121
+ # we FORWARD its new lines (prefixed `lk:`) every few seconds as they land; only
122
+ # when npm is briefly between fetches (e.g. a git clone) do we print an elapsed
123
+ # line. The relay never goes silent and never looks like a timeout. Returns
124
+ # npm's exit status.
125
+ npm_install_with_progress() {
126
+ local spec="$1"
132
127
  local npmlog; npmlog="$(mktemp)"
133
- npm install --loglevel=http --no-save --prefer-online --omit=dev --no-audit --no-fund --prefix "$tmp" "${gh}#${committish}" > "$npmlog" 2>&1 &
128
+ npm install --loglevel=http --no-save --prefer-online --omit=dev --no-audit --no-fund --prefix "$tmp" "$spec" > "$npmlog" 2>&1 &
134
129
  local npm_pid=$! start last cur
135
130
  start=$(date +%s); last=0
136
131
  while kill -0 "$npm_pid" 2>/dev/null; do
@@ -140,22 +135,60 @@ resolve_pkg() {
140
135
  tail -c "+$((last + 1))" "$npmlog" | sed 's/^/lk: /' >&2 # forward the real npm progress
141
136
  last="$cur"
142
137
  else
143
- echo "lk: … still installing the Consort TOOLKIT itself (the kit — one-time per version, cached + reused across projects) ($(( $(date +%s) - start ))s elapsed; ~1-2 min typical)…" >&2
138
+ echo "lk: … still installing the Consort TOOLKIT itself (the kit — one-time per version, cached + reused across projects) ($(( $(date +%s) - start ))s elapsed)…" >&2
144
139
  fi
145
140
  done
146
- # Flush any final lines npm wrote between the last poll and exit, then drop the log
147
- # (all branches below are covered , no leaked temp on success OR failure).
141
+ # Flush any final lines npm wrote between the last poll and exit, then drop the
142
+ # log (all branches below are covered , no leaked temp on success OR failure).
148
143
  cur=$(wc -c < "$npmlog" 2>/dev/null || echo 0)
149
144
  [ "$cur" -gt "$last" ] && tail -c "+$((last + 1))" "$npmlog" | sed 's/^/lk: /' >&2
150
145
  rm -f "$npmlog"
151
- if wait "$npm_pid" && [ -d "${tmp}/node_modules/${pkg}/dist" ]; then
152
- rm -rf "$cache"
153
- if mv "$tmp" "$cache" 2>/dev/null; then
154
- [ -n "$sha" ] && printf '%s\n' "$sha" > "${cache}/.pkg-sha" 2>/dev/null || true
155
- echo "lk: Consort toolkit ready (${pkg}@${committish}; subsequent commands are instant)." >&2
156
- return 0
146
+ wait "$npm_pid"
147
+ }
148
+
149
+ # finalize_install <committish> <sha>: promote a completed $tmp install into the
150
+ # version-keyed cache slot.
151
+ finalize_install() {
152
+ local committish="$1" sha="$2"
153
+ rm -rf "$cache"
154
+ if mv "$tmp" "$cache" 2>/dev/null; then
155
+ [ -n "$sha" ] && printf '%s\n' "$sha" > "${cache}/.pkg-sha" 2>/dev/null || true
156
+ echo "lk: Consort toolkit ready (${pkg}@${committish}; subsequent commands are instant)." >&2
157
+ return 0
158
+ fi
159
+ rm -rf "$tmp"; return 1
160
+ }
161
+
162
+ install_pkg() {
163
+ local committish="$1" sha="$2" tmp
164
+ mkdir -p "$cache_root"
165
+ tmp="$(mktemp -d "${cache_root}/.tmp.XXXXXX")"
166
+ # Tell the consumer what is happening: without narration a cold-cache install
167
+ # looks hung (a GitHub install takes a couple of minutes the FIRST time a ref
168
+ # is cached; a registry install is seconds).
169
+ echo "lk: Downloading the Consort toolkit (${pkg}@${committish}) , one-time for this version. Later commands are instant." >&2
170
+ # Registry-first for immutable version tags (v0.2.40 -> ${pkg}@0.2.40): a
171
+ # registry tarball lands in seconds (no git clone, no build). GitHub stays the
172
+ # fallback (e.g. a tag not yet published) and is the only path for branch/SHA
173
+ # refs, which the registry never has.
174
+ local ver=""
175
+ if [ "$is_sha" -eq 0 ]; then
176
+ case "$ref" in
177
+ v[0-9]*) ver="${ref#v}" ;;
178
+ [0-9]*) ver="$ref" ;;
179
+ esac
180
+ fi
181
+ if [ -n "$ver" ]; then
182
+ if npm_install_with_progress "${pkg}@${ver}" && [ -d "${tmp}/node_modules/${pkg}/dist" ]; then
183
+ finalize_install "$committish" "$sha"
184
+ return $?
157
185
  fi
158
- rm -rf "$tmp"; return 1
186
+ echo "lk: registry install of ${pkg}@${ver} did not complete; falling back to GitHub (${gh}#${committish})." >&2
187
+ rm -rf "$tmp"; tmp="$(mktemp -d "${cache_root}/.tmp.XXXXXX")"
188
+ fi
189
+ if npm_install_with_progress "${gh}#${committish}" && [ -d "${tmp}/node_modules/${pkg}/dist" ]; then
190
+ finalize_install "$committish" "$sha"
191
+ return $?
159
192
  fi
160
193
  rm -rf "$tmp"; return 1
161
194
  }
@@ -213,6 +213,20 @@ ensure_local_e2e_ports() {
213
213
  export VITE_PROXY_TARGET="http://127.0.0.1:${E2E_BACKEND_PORT}"
214
214
  echo "Local E2E free-port: backend :$E2E_BACKEND_PORT / client :$E2E_CLIENT_PORT"
215
215
  fi
216
+ # Stale-server hygiene (issue #197): CI=1 (on the Playwright invocation below)
217
+ # means the webServer never REUSES a stale app, and free_port moved us off any
218
+ # collision - but a lingering uvicorn/vite on the conventional default ports (a
219
+ # prior E2E run, or a deploy whose teardown missed) still serves STALE schema to
220
+ # anything that reaches it. Clear it (TERM, not KILL) before the boot. Never
221
+ # when the caller pre-set the ports (the deploy gate owns that server).
222
+ if [ -z "${BASE_URL:-}" ] && command -v lsof >/dev/null 2>&1; then
223
+ for p in "${E2E_BACKEND_PORT:-8000}" "${E2E_CLIENT_PORT:-5173}"; do
224
+ if lsof -iTCP:"$p" -sTCP:LISTEN -n -P >/dev/null 2>&1; then
225
+ echo "Local E2E: clearing a stale listener on :$p before the webServer boot"
226
+ lsof -ti tcp:"$p" 2>/dev/null | xargs kill 2>/dev/null || true
227
+ fi
228
+ done
229
+ fi
216
230
  }
217
231
 
218
232
  if [ "$#" -eq 0 ] && [ -d "$REPO_ROOT/client/tests/e2e" ] \
@@ -240,8 +254,11 @@ if [ "$#" -eq 0 ] && [ -d "$REPO_ROOT/client/tests/e2e" ] \
240
254
  # Use the client's OWN playwright bin (guaranteed present by the check/install above), not npx,
241
255
  # so browser install + the run are deterministic under the deploy gate. install chromium first
242
256
  # (playwright test does not auto-install browsers); a failing E2E fails the run (set -e).
257
+ # CI=1: force EVERY playwright.config vintage's reuseExistingServer guard OFF
258
+ # (old `!CI` and new `!!BASE_URL` alike) so the verify never reuses a stale
259
+ # server bound to an unmigrated branch (issue #197's false-500 misdiagnosis).
243
260
  ensure_local_e2e_ports
244
- ( cd "$REPO_ROOT/client" && ./node_modules/.bin/playwright install chromium && npm run test:e2e )
261
+ ( cd "$REPO_ROOT/client" && ./node_modules/.bin/playwright install chromium && CI=1 npm run test:e2e )
245
262
  fi
246
263
  fi
247
264