@databricks-solutions/lakebase-scm-utils 0.2.44 → 0.2.46
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 +13 -4
- package/dist/scripts/index.cjs +651 -605
- package/dist/scripts/index.d.cts +1 -1
- package/dist/scripts/index.d.ts +1 -1
- package/dist/scripts/index.js +650 -605
- package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +12 -3
- package/dist/scripts/lakebase/ci-resolve-branch.cli.js +12 -3
- package/dist/scripts/lakebase/collapse-heads.cli.cjs +12 -3
- package/dist/scripts/lakebase/collapse-heads.cli.js +12 -3
- package/dist/scripts/lakebase/detect-language.cli.cjs +12 -3
- package/dist/scripts/lakebase/detect-language.cli.js +12 -3
- package/dist/scripts/lakebase/doctor.cli.cjs +13 -4
- package/dist/scripts/lakebase/doctor.cli.js +13 -4
- package/dist/scripts/lakebase/get-connection.cli.cjs +12 -3
- package/dist/scripts/lakebase/get-connection.cli.js +12 -3
- package/dist/scripts/lakebase/index.cjs +472 -395
- package/dist/scripts/lakebase/index.d.cts +33 -12
- package/dist/scripts/lakebase/index.d.ts +33 -12
- package/dist/scripts/lakebase/index.js +471 -395
- package/dist/scripts/lakebase/infra-runner.cli.cjs +12 -3
- package/dist/scripts/lakebase/infra-runner.cli.js +12 -3
- package/dist/scripts/lakebase/new-migration.cli.cjs +12 -3
- package/dist/scripts/lakebase/new-migration.cli.js +12 -3
- package/dist/scripts/lakebase/schema-diff.cli.cjs +12 -3
- package/dist/scripts/lakebase/schema-diff.cli.js +12 -3
- package/dist/scripts/lakebase/schema-migrate.cli.cjs +12 -3
- package/dist/scripts/lakebase/schema-migrate.cli.js +12 -3
- package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +12 -3
- package/dist/scripts/lakebase/scm-claim-feature.cli.js +12 -3
- package/dist/scripts/lakebase/scm-doctor.cli.cjs +12 -3
- package/dist/scripts/lakebase/scm-doctor.cli.js +12 -3
- package/dist/scripts/lakebase/scm-merge.cli.cjs +2294 -2034
- package/dist/scripts/lakebase/scm-merge.cli.js +349 -89
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +12 -3
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +12 -3
- package/dist/scripts/util/index.cjs +2 -2
- package/dist/scripts/util/index.js +2 -2
- package/package.json +1 -1
- package/scripts/lakebase/constants.ts +23 -5
- package/scripts/lakebase/doctor.ts +1 -1
- package/scripts/lakebase/get-connection.ts +17 -3
- package/scripts/lakebase/scm-merge.ts +80 -1
- package/skills/lakebase-scm-workflows/README.md +2 -1
- package/templates/project/nodejs/package.json +3 -1
- package/templates/project/nodejs/src/index.js +8 -0
- package/templates/project/nodejs/src/openapi.js +20 -0
- package/templates/project/nodejs/src/routes/health.js +18 -0
- package/templates/project/nodejs/tests/app.test.js +16 -0
|
@@ -79276,6 +79276,7 @@ var DEFAULT_DATABASE = "databricks_postgres";
|
|
|
79276
79276
|
var DEFAULT_ENDPOINT = "primary";
|
|
79277
79277
|
var CONSORT_APPLICATION_NAME = "consort";
|
|
79278
79278
|
var SCM_UTILS_APPLICATION_NAME = "scm-utils";
|
|
79279
|
+
var SCM_CLIENT_ENV = "LAKEBASE_SCM_CLIENT";
|
|
79279
79280
|
var CONSORT_VERSION_ENV = "CONSORT_VERSION";
|
|
79280
79281
|
|
|
79281
79282
|
// scripts/lakebase/self-version.ts
|
|
@@ -79287,8 +79288,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
79287
79288
|
var cached;
|
|
79288
79289
|
function substrateSelfVersion() {
|
|
79289
79290
|
if (cached !== void 0) return cached;
|
|
79290
|
-
if ("0.2.
|
|
79291
|
-
cached = "0.2.
|
|
79291
|
+
if ("0.2.46".length > 0) {
|
|
79292
|
+
cached = "0.2.46";
|
|
79292
79293
|
return cached;
|
|
79293
79294
|
}
|
|
79294
79295
|
cached = "unknown";
|
|
@@ -79316,9 +79317,17 @@ function substrateSelfVersion() {
|
|
|
79316
79317
|
}
|
|
79317
79318
|
|
|
79318
79319
|
// scripts/lakebase/get-connection.ts
|
|
79320
|
+
function sanitizeApplicationName(raw) {
|
|
79321
|
+
const cleaned = raw.replace(/[^\x21-\x7e]+/g, "-").replace(/^-+|-+$/g, "");
|
|
79322
|
+
if (!cleaned) return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
|
|
79323
|
+
return cleaned.length > 63 ? cleaned.slice(0, 63) : cleaned;
|
|
79324
|
+
}
|
|
79319
79325
|
function connectionApplicationName() {
|
|
79326
|
+
const client = process.env[SCM_CLIENT_ENV]?.trim();
|
|
79327
|
+
if (client) return sanitizeApplicationName(client);
|
|
79320
79328
|
const consortVersion = process.env[CONSORT_VERSION_ENV]?.trim();
|
|
79321
|
-
|
|
79329
|
+
if (consortVersion) return `${CONSORT_APPLICATION_NAME}/${consortVersion}`;
|
|
79330
|
+
return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
|
|
79322
79331
|
}
|
|
79323
79332
|
async function getConnection(args) {
|
|
79324
79333
|
const endpointName = args.endpointName ?? DEFAULT_ENDPOINT;
|
|
@@ -378,6 +378,7 @@ var DEFAULT_DATABASE = "databricks_postgres";
|
|
|
378
378
|
var DEFAULT_ENDPOINT = "primary";
|
|
379
379
|
var CONSORT_APPLICATION_NAME = "consort";
|
|
380
380
|
var SCM_UTILS_APPLICATION_NAME = "scm-utils";
|
|
381
|
+
var SCM_CLIENT_ENV = "LAKEBASE_SCM_CLIENT";
|
|
381
382
|
var CONSORT_VERSION_ENV = "CONSORT_VERSION";
|
|
382
383
|
|
|
383
384
|
// scripts/lakebase/self-version.ts
|
|
@@ -388,8 +389,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
388
389
|
var cached;
|
|
389
390
|
function substrateSelfVersion() {
|
|
390
391
|
if (cached !== void 0) return cached;
|
|
391
|
-
if ("0.2.
|
|
392
|
-
cached = "0.2.
|
|
392
|
+
if ("0.2.46".length > 0) {
|
|
393
|
+
cached = "0.2.46";
|
|
393
394
|
return cached;
|
|
394
395
|
}
|
|
395
396
|
cached = "unknown";
|
|
@@ -417,9 +418,17 @@ function substrateSelfVersion() {
|
|
|
417
418
|
}
|
|
418
419
|
|
|
419
420
|
// scripts/lakebase/get-connection.ts
|
|
421
|
+
function sanitizeApplicationName(raw) {
|
|
422
|
+
const cleaned = raw.replace(/[^\x21-\x7e]+/g, "-").replace(/^-+|-+$/g, "");
|
|
423
|
+
if (!cleaned) return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
|
|
424
|
+
return cleaned.length > 63 ? cleaned.slice(0, 63) : cleaned;
|
|
425
|
+
}
|
|
420
426
|
function connectionApplicationName() {
|
|
427
|
+
const client = process.env[SCM_CLIENT_ENV]?.trim();
|
|
428
|
+
if (client) return sanitizeApplicationName(client);
|
|
421
429
|
const consortVersion = process.env[CONSORT_VERSION_ENV]?.trim();
|
|
422
|
-
|
|
430
|
+
if (consortVersion) return `${CONSORT_APPLICATION_NAME}/${consortVersion}`;
|
|
431
|
+
return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
|
|
423
432
|
}
|
|
424
433
|
async function getConnection(args) {
|
|
425
434
|
const endpointName = args.endpointName ?? DEFAULT_ENDPOINT;
|
|
@@ -1659,8 +1659,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
1659
1659
|
var cached;
|
|
1660
1660
|
function substrateSelfVersion() {
|
|
1661
1661
|
if (cached !== void 0) return cached;
|
|
1662
|
-
if ("0.2.
|
|
1663
|
-
cached = "0.2.
|
|
1662
|
+
if ("0.2.46".length > 0) {
|
|
1663
|
+
cached = "0.2.46";
|
|
1664
1664
|
return cached;
|
|
1665
1665
|
}
|
|
1666
1666
|
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.2.
|
|
1634
|
-
cached = "0.2.
|
|
1633
|
+
if ("0.2.46".length > 0) {
|
|
1634
|
+
cached = "0.2.46";
|
|
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.2.
|
|
3
|
+
"version": "0.2.46",
|
|
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,
|
|
@@ -58,10 +58,15 @@ export const DEFAULT_ENDPOINT = "primary";
|
|
|
58
58
|
// branch/schema/ping connections this package opens). The full label is
|
|
59
59
|
// `<brand>/<version>` , see `connectionApplicationName()` in get-connection.ts , and it
|
|
60
60
|
// reflects WHICH tool opened the connection:
|
|
61
|
-
// -
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
61
|
+
// - ANY consumer that sets SCM_CLIENT_ENV (`LAKEBASE_SCM_CLIENT="<brand>/<version>"`) — the
|
|
62
|
+
// GENERIC, extensible path: a new consumer self-brands with NO scm-utils change. scm-utils
|
|
63
|
+
// sanitizes the value and uses it verbatim (e.g. the VS Code / Cursor extension sets
|
|
64
|
+
// `scm-extension/<ext-version>`);
|
|
65
|
+
// - `consort/<consort-version>` — BACK-COMPAT for Consort, which sets CONSORT_VERSION_ENV
|
|
66
|
+
// (older Consorts predate the generic env; keeping this means Consort needs no change);
|
|
67
|
+
// - `scm-utils/<scm-utils-version>` when nothing is set — a bare `lakebase-*` CLI falls back
|
|
68
|
+
// to this package's own brand.
|
|
69
|
+
// Precedence: SCM_CLIENT_ENV > CONSORT_VERSION_ENV > scm-utils default.
|
|
65
70
|
// A TRANSPARENT label , standard practice (psql, ORMs set one) , visible to the database OWNER
|
|
66
71
|
// in their own `pg_stat_activity`, so support + the owner's own diagnostics can tell which
|
|
67
72
|
// tooling connected versus their application. Reads no table contents; carries only brand + version.
|
|
@@ -69,9 +74,22 @@ export const DEFAULT_ENDPOINT = "primary";
|
|
|
69
74
|
/** Brand when the connection is made under a Consort run (CONSORT_VERSION_ENV set). */
|
|
70
75
|
export const CONSORT_APPLICATION_NAME = "consort";
|
|
71
76
|
|
|
72
|
-
/** Brand when scm-utils is used directly
|
|
77
|
+
/** Brand when scm-utils is used directly via a bare `lakebase-*` CLI; the default. */
|
|
73
78
|
export const SCM_UTILS_APPLICATION_NAME = "scm-utils";
|
|
74
79
|
|
|
80
|
+
/**
|
|
81
|
+
* GENERIC consumer-brand env: any tool that opens substrate connections can set
|
|
82
|
+
* `LAKEBASE_SCM_CLIENT="<brand>/<version>"` (e.g. the VS Code / Cursor extension sets
|
|
83
|
+
* `scm-extension/<ext-version>`), and connectionApplicationName() uses it (sanitized) as the
|
|
84
|
+
* Postgres application_name. This is the extensibility point: adding a NEW consumer needs ZERO
|
|
85
|
+
* scm-utils changes — the consumer just sets this env before its first connection. Takes
|
|
86
|
+
* precedence over the CONSORT_VERSION back-compat path. The value is a full `<brand>/<version>`
|
|
87
|
+
* label owned by the consumer (scm-utils no longer hardcodes per-consumer brand constants).
|
|
88
|
+
* Brand convention: drop the `lakebase-` package prefix (lakebase-scm-utils -> scm-utils,
|
|
89
|
+
* lakebase-scm-extension -> scm-extension).
|
|
90
|
+
*/
|
|
91
|
+
export const SCM_CLIENT_ENV = "LAKEBASE_SCM_CLIENT";
|
|
92
|
+
|
|
75
93
|
/**
|
|
76
94
|
* Env var Consort sets to its OWN version so a connection opened under a Consort run is
|
|
77
95
|
* labelled `consort/<consort-version>` rather than `scm-utils/<scm-utils-version>`. This is
|
|
@@ -628,7 +628,7 @@ function checkWorkflowDrift(projectDir: string): CheckResult {
|
|
|
628
628
|
status: "warn",
|
|
629
629
|
message: `Scaffolded workflows drift from kit: ${drifted} drifted, ${missing} missing`,
|
|
630
630
|
detail: { files: report.files.map((f) => ({ name: f.name, status: f.status })) },
|
|
631
|
-
hint: "
|
|
631
|
+
hint: "Refresh with updateWorkflows (or the lakebase_workflow_drift MCP tool). A stale merge.yml calling 'lakebase-schema-migrate apply' (not 'apply-tier') makes the promote's migrate-target fail on the tier guard; the scm-merge promote also self-heals drifted workflows onto the PR head before merging.",
|
|
632
632
|
};
|
|
633
633
|
} catch (err) {
|
|
634
634
|
return {
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
DEFAULT_DATABASE,
|
|
24
24
|
DEFAULT_ENDPOINT,
|
|
25
25
|
POSTGRES_PORT,
|
|
26
|
+
SCM_CLIENT_ENV,
|
|
26
27
|
SCM_UTILS_APPLICATION_NAME,
|
|
27
28
|
} from "./constants.js";
|
|
28
29
|
import { substrateSelfVersion } from "./self-version.js";
|
|
@@ -39,11 +40,24 @@ import { KIT_TIMEOUTS } from "./kit-config.js";
|
|
|
39
40
|
* `pg_stat_activity`. Never throws (an unreadable scm-utils version -> `scm-utils/unknown`; a
|
|
40
41
|
* blank env is ignored), so labelling can never break a connection.
|
|
41
42
|
*/
|
|
43
|
+
/** Sanitize a consumer-provided application_name label: printable ASCII only (runs of control
|
|
44
|
+
* chars / whitespace collapse to a single '-'), trimmed of leading/trailing '-', and capped to
|
|
45
|
+
* Postgres's 63-byte application_name limit. Empty after cleaning -> the scm-utils default. */
|
|
46
|
+
function sanitizeApplicationName(raw: string): string {
|
|
47
|
+
const cleaned = raw.replace(/[^\x21-\x7e]+/g, "-").replace(/^-+|-+$/g, "");
|
|
48
|
+
if (!cleaned) return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
|
|
49
|
+
// Printable-ASCII only, so byte length == char length; trim to the 63-byte cap.
|
|
50
|
+
return cleaned.length > 63 ? cleaned.slice(0, 63) : cleaned;
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
export function connectionApplicationName(): string {
|
|
54
|
+
// Precedence: a consumer's generic self-brand > Consort back-compat > scm-utils default.
|
|
55
|
+
// A NEW consumer just sets LAKEBASE_SCM_CLIENT="<brand>/<version>" — no scm-utils change needed.
|
|
56
|
+
const client = process.env[SCM_CLIENT_ENV]?.trim();
|
|
57
|
+
if (client) return sanitizeApplicationName(client);
|
|
43
58
|
const consortVersion = process.env[CONSORT_VERSION_ENV]?.trim();
|
|
44
|
-
return consortVersion
|
|
45
|
-
|
|
46
|
-
: `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
|
|
59
|
+
if (consortVersion) return `${CONSORT_APPLICATION_NAME}/${consortVersion}`;
|
|
60
|
+
return `${SCM_UTILS_APPLICATION_NAME}/${substrateSelfVersion()}`;
|
|
47
61
|
}
|
|
48
62
|
// AppKit / @databricks/lakebase re-exports a WorkspaceClient type that
|
|
49
63
|
// matches what createLakebasePool expects. We accept `unknown` at the API
|
|
@@ -20,7 +20,9 @@ import { getOwnerRepo } from "../git/remote.js";
|
|
|
20
20
|
import { pollUntil } from "../util/poll-until.js";
|
|
21
21
|
import { exec } from "../util/exec.js";
|
|
22
22
|
import { getCurrentBranch } from "../git/inspect.js";
|
|
23
|
+
import { pushCurrentBranchForPr } from "../git/sync.js";
|
|
23
24
|
import { resolveGitBase } from "./scm-git-base.js";
|
|
25
|
+
import { detectWorkflowDrift, updateWorkflows } from "./workflow-drift.js";
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Fast-forward the local tier ref to origin/<tier> after a remote merge, WITHOUT
|
|
@@ -149,6 +151,21 @@ export interface MergeArgs {
|
|
|
149
151
|
* already on the parent branch after the merge).
|
|
150
152
|
*/
|
|
151
153
|
localMigrateFallback?: () => Promise<{ ok: boolean; detail?: string }>;
|
|
154
|
+
/**
|
|
155
|
+
* Pre-merge workflow self-heal. Bumping SCM_UTILS_REF upgrades the CLI (e.g.
|
|
156
|
+
* the tier-migration guard) but leaves a scaffolded project's committed
|
|
157
|
+
* `.github/workflows/*.yml` at their older shape — so the promote's
|
|
158
|
+
* `migrate-target` can run a stale `lakebase-schema-migrate apply` against the
|
|
159
|
+
* parent TIER, which the guard refuses (CI red, cleanup skipped). This step
|
|
160
|
+
* refreshes drifted workflow files to the pinned template (so the promote uses
|
|
161
|
+
* `apply-tier`) and commits+pushes them onto the PR head BEFORE the merge, so
|
|
162
|
+
* the merge commit carries the corrected workflow and the triggered `<tier>
|
|
163
|
+
* Merge` run passes. BEST-EFFORT: it never blocks the merge — a detect/refresh/
|
|
164
|
+
* push failure is recorded as a warning and the merge proceeds (the local
|
|
165
|
+
* migrate fallback still keeps git and schema in sync). Defaults to the built-in
|
|
166
|
+
* implementation ({@link defaultRefreshPromoteWorkflows}); injected in tests.
|
|
167
|
+
*/
|
|
168
|
+
refreshWorkflows?: () => Promise<{ refreshed: boolean; detail?: string }>;
|
|
152
169
|
}
|
|
153
170
|
|
|
154
171
|
export interface MergeResult {
|
|
@@ -221,6 +238,43 @@ function shaMigratePredicate(
|
|
|
221
238
|
};
|
|
222
239
|
}
|
|
223
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Built-in {@link MergeArgs.refreshWorkflows}. Refreshes drifted scaffolded
|
|
243
|
+
* `.github/workflows/*.yml` to the pinned template and commits + pushes them onto
|
|
244
|
+
* the PR head, so the merge commit carries the corrected promote workflow (the
|
|
245
|
+
* `migrate-target` step's `lakebase-schema-migrate apply-tier`) and the triggered
|
|
246
|
+
* `<tier> Merge` run passes instead of failing on the tier guard. No-op when
|
|
247
|
+
* nothing drifted. Scoped to `.github/workflows` so it never sweeps unrelated
|
|
248
|
+
* working-tree changes. Callers invoke it BEST-EFFORT — a failure here must never
|
|
249
|
+
* block the merge (the local migrate fallback still keeps git + schema in sync).
|
|
250
|
+
*/
|
|
251
|
+
export async function defaultRefreshPromoteWorkflows(
|
|
252
|
+
projectDir: string,
|
|
253
|
+
): Promise<{ refreshed: boolean; detail?: string }> {
|
|
254
|
+
const drift = detectWorkflowDrift({ projectDir });
|
|
255
|
+
// Only self-heal a stale EXISTING workflow (drifted). A project with no
|
|
256
|
+
// scaffolded workflows at all (all "missing") is not the stale-apply case, so
|
|
257
|
+
// the promote never scaffolds files it doesn't own.
|
|
258
|
+
const touched = drift.files.filter((f) => f.status === "drifted").map((f) => f.name);
|
|
259
|
+
if (touched.length === 0) return { refreshed: false };
|
|
260
|
+
const result = updateWorkflows({ projectDir });
|
|
261
|
+
if (!result.changed) return { refreshed: false };
|
|
262
|
+
await exec(`git add ${shellEscape(".github/workflows")}`, { cwd: projectDir, timeout: 10_000 });
|
|
263
|
+
const staged = await exec("git diff --cached --name-only -- .github/workflows", {
|
|
264
|
+
cwd: projectDir,
|
|
265
|
+
timeout: 10_000,
|
|
266
|
+
});
|
|
267
|
+
if (!staged.trim()) return { refreshed: false };
|
|
268
|
+
await exec(
|
|
269
|
+
`git commit -m ${shellEscape(
|
|
270
|
+
"chore(ci): refresh scaffolded workflows to pinned substrate (promote uses apply-tier)",
|
|
271
|
+
)} -- .github/workflows`,
|
|
272
|
+
{ cwd: projectDir, timeout: 15_000 },
|
|
273
|
+
);
|
|
274
|
+
await pushCurrentBranchForPr({ cwd: projectDir });
|
|
275
|
+
return { refreshed: true, detail: `refreshed + pushed ${touched.join(", ") || "workflow(s)"} onto the PR head` };
|
|
276
|
+
}
|
|
277
|
+
|
|
224
278
|
export async function mergeFeature(args: MergeArgs): Promise<MergeResult> {
|
|
225
279
|
const current = readWorkflowState(args.projectDir);
|
|
226
280
|
if (!current) {
|
|
@@ -297,6 +351,31 @@ export async function mergeFeature(args: MergeArgs): Promise<MergeResult> {
|
|
|
297
351
|
authVerified = true;
|
|
298
352
|
}
|
|
299
353
|
|
|
354
|
+
// ─── Pre-merge workflow self-heal ───
|
|
355
|
+
// A scaffolded project's committed workflows can lag the pinned substrate ref
|
|
356
|
+
// (SCM_UTILS_REF bumped, workflow files not regenerated), so the promote's
|
|
357
|
+
// migrate-target runs a stale `lakebase-schema-migrate apply` against the parent
|
|
358
|
+
// TIER, which the guard refuses (CI red, cleanup skipped). Refresh drifted
|
|
359
|
+
// workflows onto the PR head BEFORE the merge so the merge commit carries the
|
|
360
|
+
// corrected `apply-tier` step. BEST-EFFORT: never blocks the merge.
|
|
361
|
+
// Runs regardless of waitMigrate: the promote workflow triggers on the tier
|
|
362
|
+
// push either way, so a stale merge.yml fails CI whether or not the caller waits.
|
|
363
|
+
const preflightNotes: string[] = [];
|
|
364
|
+
{
|
|
365
|
+
const refresh = args.refreshWorkflows ?? (() => defaultRefreshPromoteWorkflows(args.projectDir));
|
|
366
|
+
try {
|
|
367
|
+
const r = await refresh();
|
|
368
|
+
if (r.refreshed) {
|
|
369
|
+
preflightNotes.push(`Refreshed stale CI workflows before merge${r.detail ? ` (${r.detail})` : ""}.`);
|
|
370
|
+
}
|
|
371
|
+
} catch (err) {
|
|
372
|
+
preflightNotes.push(
|
|
373
|
+
`Workflow self-heal skipped (${err instanceof Error ? err.message : String(err)}); if the promote's ` +
|
|
374
|
+
`migrate-target fails on the tier guard, refresh .github/workflows (merge.yml must call 'lakebase-schema-migrate apply-tier').`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
300
379
|
let paired: Awaited<ReturnType<typeof mergePairedPullRequest>>;
|
|
301
380
|
try {
|
|
302
381
|
paired = await mergePairedPullRequest({
|
|
@@ -312,7 +391,7 @@ export async function mergeFeature(args: MergeArgs): Promise<MergeResult> {
|
|
|
312
391
|
);
|
|
313
392
|
}
|
|
314
393
|
|
|
315
|
-
const warnings: string[] = [...paired.warnings];
|
|
394
|
+
const warnings: string[] = [...preflightNotes, ...paired.warnings];
|
|
316
395
|
let localBranchDeleted = false;
|
|
317
396
|
let headAfter = current.branch;
|
|
318
397
|
if (!args.skipLocalCleanup) {
|
|
@@ -228,7 +228,8 @@ The same flow applies to the `--enable-e2e`-scaffolded `playwright.config.ts` +
|
|
|
228
228
|
| `lakebase-infra-runner` | Run the `[Infra]`-tag suite (schema-diff + migration status + endpoint readiness) for a branch. Used by scaffolded `test:infra` scripts. |
|
|
229
229
|
| `lakebase-update-commands` | Refresh a scaffolded project's `.claude/commands/{design,build}.md` from the kit's current templates. Interactive per-file confirm by default; `--force` skips prompts, `--dry-run` previews, `--json` emits a structured report. Hook files (`<name>.{pre,post}-hook.md`) are NEVER touched. |
|
|
230
230
|
| `lakebase-feature-status` | One-screen snapshot of a TDD feature's workflow state. Pairs with `consort`. |
|
|
231
|
-
|
|
231
|
+
|
|
232
|
+
> An MCP presentation of this same substrate surface ships in the `consort` package as `lakebase-mcp-server` (stdio; for Claude Desktop / OpenAI Codex / Cursor-via-MCP / Genie Code consumers). It is not an scm-utils bin.
|
|
232
233
|
|
|
233
234
|
## JS/TS exports
|
|
234
235
|
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
require('dotenv').config();
|
|
2
2
|
const express = require('express');
|
|
3
|
+
const swaggerUi = require('swagger-ui-express');
|
|
4
|
+
const openapiSpec = require('./openapi');
|
|
3
5
|
const healthRoutes = require('./routes/health');
|
|
4
6
|
|
|
5
7
|
const app = express();
|
|
6
8
|
app.use(express.json());
|
|
7
9
|
app.use('/', healthRoutes);
|
|
8
10
|
|
|
11
|
+
// Swagger / OpenAPI UI for acceptance review (parity with FastAPI's /docs): the interactive UI at
|
|
12
|
+
// /docs and the raw spec at /openapi.json, both derived from the route @openapi annotations. For an
|
|
13
|
+
// API-only story, the acceptance gate points the reviewer here to exercise the endpoints.
|
|
14
|
+
app.use('/docs', swaggerUi.serve, swaggerUi.setup(openapiSpec));
|
|
15
|
+
app.get('/openapi.json', (req, res) => res.json(openapiSpec));
|
|
16
|
+
|
|
9
17
|
const PORT = process.env.PORT || 3000;
|
|
10
18
|
|
|
11
19
|
if (require.main === module) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const swaggerJsdoc = require('swagger-jsdoc');
|
|
3
|
+
|
|
4
|
+
// The OpenAPI spec is built from the `@openapi` JSDoc blocks on the route files, so the Swagger UI
|
|
5
|
+
// at /docs reflects the real endpoints as they are added — the Express analogue of FastAPI's
|
|
6
|
+
// automatic /docs. Annotate every route you add with an `@openapi` block (see src/routes/health.js
|
|
7
|
+
// for the pattern); it then appears in /docs and /openapi.json with no extra wiring.
|
|
8
|
+
const spec = swaggerJsdoc({
|
|
9
|
+
definition: {
|
|
10
|
+
openapi: '3.0.3',
|
|
11
|
+
info: {
|
|
12
|
+
title: '{{PROJECT_NAME}}',
|
|
13
|
+
version: '1.0.0',
|
|
14
|
+
description: 'API surface for acceptance review (Swagger UI at /docs).',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
apis: [path.join(__dirname, 'routes', '*.js')],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
module.exports = spec;
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @openapi
|
|
6
|
+
* /health:
|
|
7
|
+
* get:
|
|
8
|
+
* summary: Liveness probe
|
|
9
|
+
* description: Returns ok when the service is up. Pattern for annotating routes so they appear in /docs.
|
|
10
|
+
* responses:
|
|
11
|
+
* 200:
|
|
12
|
+
* description: Service is up
|
|
13
|
+
* content:
|
|
14
|
+
* application/json:
|
|
15
|
+
* schema:
|
|
16
|
+
* type: object
|
|
17
|
+
* properties:
|
|
18
|
+
* status:
|
|
19
|
+
* type: string
|
|
20
|
+
* example: ok
|
|
21
|
+
*/
|
|
4
22
|
router.get('/health', (req, res) => {
|
|
5
23
|
res.json({ status: 'ok' });
|
|
6
24
|
});
|
|
@@ -8,3 +8,19 @@ describe('Health check', () => {
|
|
|
8
8
|
expect(res.body.status).toBe('ok');
|
|
9
9
|
});
|
|
10
10
|
});
|
|
11
|
+
|
|
12
|
+
describe('API docs (Swagger UI for acceptance review)', () => {
|
|
13
|
+
test('GET /openapi.json exposes the OpenAPI spec, including annotated routes', async () => {
|
|
14
|
+
const res = await request(app).get('/openapi.json');
|
|
15
|
+
expect(res.statusCode).toBe(200);
|
|
16
|
+
expect(res.body.openapi).toBeDefined();
|
|
17
|
+
// The @openapi annotation on the health route is picked up by swagger-jsdoc.
|
|
18
|
+
expect(res.body.paths['/health']).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('GET /docs/ serves the Swagger UI', async () => {
|
|
22
|
+
const res = await request(app).get('/docs/');
|
|
23
|
+
expect(res.statusCode).toBe(200);
|
|
24
|
+
expect(res.text).toMatch(/swagger-ui/i);
|
|
25
|
+
});
|
|
26
|
+
});
|