@databricks-solutions/lakebase-scm-utils 0.3.4 → 0.3.6
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/dist/scripts/github/index.cjs +14 -0
- package/dist/scripts/github/index.d.cts +19 -1
- package/dist/scripts/github/index.d.ts +19 -1
- package/dist/scripts/github/index.js +13 -0
- package/dist/scripts/index.cjs +94 -6
- package/dist/scripts/index.d.cts +2 -2
- package/dist/scripts/index.d.ts +2 -2
- package/dist/scripts/index.js +91 -6
- package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2 -2
- package/dist/scripts/lakebase/ci-resolve-branch.cli.js +2 -2
- package/dist/scripts/lakebase/collapse-heads.cli.cjs +2 -2
- package/dist/scripts/lakebase/collapse-heads.cli.js +2 -2
- package/dist/scripts/lakebase/detect-language.cli.cjs +2 -2
- package/dist/scripts/lakebase/detect-language.cli.js +2 -2
- package/dist/scripts/lakebase/doctor.cli.cjs +15664 -7598
- package/dist/scripts/lakebase/doctor.cli.js +8243 -106
- 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 +187 -99
- package/dist/scripts/lakebase/index.d.cts +22 -1
- package/dist/scripts/lakebase/index.d.ts +22 -1
- package/dist/scripts/lakebase/index.js +185 -99
- package/dist/scripts/lakebase/infra-runner.cli.cjs +2 -2
- package/dist/scripts/lakebase/infra-runner.cli.js +2 -2
- package/dist/scripts/lakebase/new-migration.cli.cjs +2 -2
- package/dist/scripts/lakebase/new-migration.cli.js +2 -2
- 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 +2 -2
- package/dist/scripts/lakebase/schema-migrate.cli.js +2 -2
- package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2 -2
- package/dist/scripts/lakebase/scm-claim-feature.cli.js +2 -2
- package/dist/scripts/lakebase/scm-doctor.cli.cjs +2 -2
- package/dist/scripts/lakebase/scm-doctor.cli.js +2 -2
- package/dist/scripts/lakebase/scm-merge.cli.cjs +2 -2
- package/dist/scripts/lakebase/scm-merge.cli.js +2 -2
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2 -2
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +2 -2
- package/dist/scripts/util/index.cjs +2 -2
- package/dist/scripts/util/index.js +2 -2
- package/package.json +1 -1
- package/scripts/github/repo.ts +37 -1
- package/scripts/lakebase/create-preflight.ts +45 -0
- package/scripts/lakebase/create-project.ts +15 -0
- package/scripts/lakebase/doctor.ts +50 -0
- package/scripts/lakebase/enable-e2e.ts +34 -4
- package/templates/project/common/.github/workflows/pr.yml +14 -5
|
@@ -19,6 +19,7 @@ import * as fs from "node:fs";
|
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { deleteLakebaseProject } from "./lakebase-project.js";
|
|
21
21
|
import { runDatabricks } from "./databricks-cli.js";
|
|
22
|
+
import { checkGitHubScopes } from "../github/repo.js";
|
|
22
23
|
|
|
23
24
|
export interface PreflightResult {
|
|
24
25
|
ok: boolean;
|
|
@@ -113,6 +114,50 @@ export async function checkDatabricksAuth(
|
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/** Injectable scope probe for checkGitHubWorkflowScope (defaults to the real GitHub API read). */
|
|
118
|
+
type ScopeProbe = () => Promise<{ granted: string[] | null; missing: string[] }>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* W-workflow-scope: preflight the GitHub token's `workflow` OAuth scope BEFORE
|
|
122
|
+
* any repo / Lakebase project is created. The scaffold's FINAL push commits
|
|
123
|
+
* `.github/workflows/*`, which GitHub REJECTS unless the token carries
|
|
124
|
+
* `workflow` — without this, that rejection only surfaces at the very end,
|
|
125
|
+
* after all the provisioning, forcing a full rollback. Gating the ONE scope the
|
|
126
|
+
* push needs here turns that into an immediate, actionable failure.
|
|
127
|
+
*
|
|
128
|
+
* Non-blocking in two cases (ok:true): a token that exposes NO OAuth scopes
|
|
129
|
+
* (fine-grained PAT / GitHub App token — `granted` null; its workflow
|
|
130
|
+
* permission isn't visible this way, so we can't assert), and any probe error
|
|
131
|
+
* (a genuinely bad/absent token surfaces clearly at repo creation, not here).
|
|
132
|
+
*/
|
|
133
|
+
export async function checkGitHubWorkflowScope(
|
|
134
|
+
probe: ScopeProbe = () => checkGitHubScopes(["workflow"]),
|
|
135
|
+
): Promise<PreflightResult> {
|
|
136
|
+
try {
|
|
137
|
+
const { granted, missing } = await probe();
|
|
138
|
+
if (granted === null) return { ok: true }; // scopes not visible → can't assert, don't false-fail
|
|
139
|
+
if (missing.includes("workflow")) {
|
|
140
|
+
return { ok: false, reason: `token grants [${granted.join(", ") || "none"}] but not \`workflow\`` };
|
|
141
|
+
}
|
|
142
|
+
return { ok: true };
|
|
143
|
+
} catch {
|
|
144
|
+
return { ok: true }; // best-effort: a probe failure must not block scaffold
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The actionable prereq message for a GitHub token missing the `workflow` scope. */
|
|
149
|
+
export function githubWorkflowScopePrereqMessage(reason?: string): string {
|
|
150
|
+
return (
|
|
151
|
+
"The GitHub token lacks the `workflow` OAuth scope, required to push the project's " +
|
|
152
|
+
"`.github/workflows/*` CI files. (Without this check, scaffolding runs all the way to the " +
|
|
153
|
+
"final push, is rejected, and rolls back.) Grant the scope, then re-run:\n" +
|
|
154
|
+
" - gh CLI: gh auth refresh -h github.com -s workflow\n" +
|
|
155
|
+
" - VS Code: re-sign in to GitHub and approve the workflow scope\n" +
|
|
156
|
+
" - token: set GITHUB_TOKEN to a PAT that includes the `workflow` scope" +
|
|
157
|
+
(reason ? `\n(${reason})` : "")
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
116
161
|
/** The actionable prereq message for a failed auth probe (W5). */
|
|
117
162
|
export function databricksAuthPrereqMessage(host?: string, reason?: string): string {
|
|
118
163
|
const hostFlag = host ? ` --host ${host.replace(/\/+$/, "")}` : "";
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
checkDatabricksAuth,
|
|
22
22
|
databricksAuthPrereqMessage,
|
|
23
|
+
checkGitHubWorkflowScope,
|
|
24
|
+
githubWorkflowScopePrereqMessage,
|
|
23
25
|
withLakebaseRollback,
|
|
24
26
|
validateCreateInputs,
|
|
25
27
|
dirIsEmpty,
|
|
@@ -289,6 +291,19 @@ export async function createProject(
|
|
|
289
291
|
throw new Error(databricksAuthPrereqMessage(host, auth.reason));
|
|
290
292
|
}
|
|
291
293
|
|
|
294
|
+
// ── GitHub token `workflow` scope precondition ────────────────
|
|
295
|
+
// The scaffold's final push commits .github/workflows/*, which GitHub rejects
|
|
296
|
+
// unless the token carries the `workflow` OAuth scope. Check it HERE, before any
|
|
297
|
+
// GitHub repo or Lakebase project exists, so a missing scope fails in seconds with
|
|
298
|
+
// a fix instead of at the final push after all provisioning (which then rolls back).
|
|
299
|
+
if (useGithub) {
|
|
300
|
+
report("Checking GitHub token scopes...");
|
|
301
|
+
const scope = await checkGitHubWorkflowScope();
|
|
302
|
+
if (!scope.ok) {
|
|
303
|
+
throw new Error(githubWorkflowScopePrereqMessage(scope.reason));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
292
307
|
// ── Step 1+2: GitHub repo + clone, OR local-only setup ────────
|
|
293
308
|
if (useGithub) {
|
|
294
309
|
report("Creating GitHub repository...", fullRepoName);
|
|
@@ -14,6 +14,7 @@ import { listBranches } from "./branch-utils.js";
|
|
|
14
14
|
import { verifyHooks } from "./project-verify.js";
|
|
15
15
|
import { detectLanguage } from "./schema-migrate.js";
|
|
16
16
|
import { detectWorkflowDrift } from "./workflow-drift.js";
|
|
17
|
+
import { checkGitHubScopes } from "../github/repo.js";
|
|
17
18
|
|
|
18
19
|
export type CheckStatus = "ok" | "warn" | "fail" | "skip";
|
|
19
20
|
|
|
@@ -593,6 +594,53 @@ async function checkGitRemote(projectDir: string): Promise<CheckResult> {
|
|
|
593
594
|
}
|
|
594
595
|
}
|
|
595
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Probe whether the GitHub token carries the `workflow` scope needed to push
|
|
599
|
+
* `.github/workflows/*`. A missing scope is exactly why a scaffold push (or any
|
|
600
|
+
* later workflow edit) gets rejected at the very end and rolls back. Best-effort:
|
|
601
|
+
* no token, OR a fine-grained / GitHub App token (no visible OAuth scopes), OR
|
|
602
|
+
* an API error → `skip`, never a false `fail`. `probe` is injectable for tests.
|
|
603
|
+
*/
|
|
604
|
+
export async function checkGitHubTokenScope(
|
|
605
|
+
probe: () => Promise<{ granted: string[] | null; missing: string[] }> = () =>
|
|
606
|
+
checkGitHubScopes(["workflow"])
|
|
607
|
+
): Promise<CheckResult> {
|
|
608
|
+
try {
|
|
609
|
+
const { granted, missing } = await probe();
|
|
610
|
+
if (granted === null) {
|
|
611
|
+
return {
|
|
612
|
+
name: "github-token-scope",
|
|
613
|
+
status: "skip",
|
|
614
|
+
message:
|
|
615
|
+
"GitHub token scopes not visible (fine-grained PAT / App token) - `workflow` scope unverified",
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
if (missing.includes("workflow")) {
|
|
619
|
+
return {
|
|
620
|
+
name: "github-token-scope",
|
|
621
|
+
status: "fail",
|
|
622
|
+
message:
|
|
623
|
+
"GitHub token lacks the `workflow` scope - pushes touching .github/workflows/* will be rejected",
|
|
624
|
+
detail: { granted },
|
|
625
|
+
hint: "Grant it with `gh auth refresh -h github.com -s workflow`, re-sign in to GitHub in VS Code and approve the workflow scope, or set GITHUB_TOKEN to a PAT that includes `workflow`.",
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
name: "github-token-scope",
|
|
630
|
+
status: "ok",
|
|
631
|
+
message: "GitHub token has the `workflow` scope",
|
|
632
|
+
detail: { granted },
|
|
633
|
+
};
|
|
634
|
+
} catch (err) {
|
|
635
|
+
return {
|
|
636
|
+
name: "github-token-scope",
|
|
637
|
+
status: "skip",
|
|
638
|
+
message: "GitHub token scope check skipped (no token resolved, or GitHub unreachable)",
|
|
639
|
+
detail: { error: (err as Error).message },
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
596
644
|
function checkLanguage(projectDir: string): CheckResult {
|
|
597
645
|
try {
|
|
598
646
|
const lang = detectLanguage(projectDir);
|
|
@@ -732,6 +780,7 @@ export async function runDoctor(args: DoctorArgs = {}): Promise<DoctorReport> {
|
|
|
732
780
|
host
|
|
733
781
|
);
|
|
734
782
|
const gitRemote = await checkGitRemote(projectDir);
|
|
783
|
+
const githubTokenScope = await checkGitHubTokenScope();
|
|
735
784
|
const language = checkLanguage(projectDir);
|
|
736
785
|
const hooks = checkHooks(projectDir);
|
|
737
786
|
const workflowDrift = checkWorkflowDrift(projectDir);
|
|
@@ -763,6 +812,7 @@ export async function runDoctor(args: DoctorArgs = {}): Promise<DoctorReport> {
|
|
|
763
812
|
configProfile,
|
|
764
813
|
lakebaseProject,
|
|
765
814
|
gitRemote,
|
|
815
|
+
githubTokenScope,
|
|
766
816
|
language,
|
|
767
817
|
hooks,
|
|
768
818
|
workflowDrift,
|
|
@@ -202,6 +202,19 @@ const RUN_TESTS_E2E_MARKER = "# run Playwright E2E suite when configured";
|
|
|
202
202
|
*/
|
|
203
203
|
const CLIENT_E2E_MARKER = "# run client Playwright E2E suite when configured";
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* The markers matched ONLY as standalone lines (the appended block's own opening
|
|
207
|
+
* comment lines), never as substrings. The base run-tests.sh TEMPLATE documents the
|
|
208
|
+
* E2E-assembly contract in a header comment that QUOTES RUN_TESTS_E2E_MARKER verbatim
|
|
209
|
+
* inside backticks; a substring `includes`/`indexOf` match there matched the prose
|
|
210
|
+
* mention and truncated the ENTIRE primary-suite body (the run-tests.sh clobber that
|
|
211
|
+
* silently made every honest-GREEN + deploy verify exit 0). A line-anchored (`^…$`,
|
|
212
|
+
* multiline) match only ever finds the real block, never the documentation comment.
|
|
213
|
+
*/
|
|
214
|
+
const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
215
|
+
const RUN_TESTS_E2E_MARKER_LINE = new RegExp(`^${escapeRe(RUN_TESTS_E2E_MARKER)}$`, "m");
|
|
216
|
+
const CLIENT_E2E_MARKER_LINE = new RegExp(`^${escapeRe(CLIENT_E2E_MARKER)}$`, "m");
|
|
217
|
+
|
|
205
218
|
/** The full E2E block appended to run-tests.sh (project-root layout + Python +
|
|
206
219
|
* the client-workspace layout). Built once so the fresh-insert and the in-place
|
|
207
220
|
* upgrade write byte-identical content. */
|
|
@@ -307,16 +320,33 @@ export function addE2eToRunTestsScript(
|
|
|
307
320
|
return { patched: false, inserted: false };
|
|
308
321
|
}
|
|
309
322
|
const original = fs.readFileSync(scriptPath, "utf8");
|
|
310
|
-
|
|
311
|
-
|
|
323
|
+
// Match markers as standalone LINES, not substrings (see RUN_TESTS_E2E_MARKER_LINE):
|
|
324
|
+
// a prose mention in the template header must never be treated as the block boundary.
|
|
325
|
+
const baseMarker = RUN_TESTS_E2E_MARKER_LINE.exec(original);
|
|
326
|
+
const hasBase = baseMarker !== null;
|
|
327
|
+
const hasClient = CLIENT_E2E_MARKER_LINE.test(original);
|
|
312
328
|
if (hasBase && hasClient) {
|
|
313
329
|
return { patched: true, inserted: false }; // already the current block
|
|
314
330
|
}
|
|
315
331
|
// Truncate an OLD block (base marker present, client branch missing) so we replace
|
|
316
332
|
// rather than duplicate; else append to the end after the language-specific suite.
|
|
317
|
-
const cut = hasBase ? original.slice(0,
|
|
333
|
+
const cut = hasBase ? original.slice(0, baseMarker!.index) : original;
|
|
318
334
|
const base = cut.replace(/\n+$/, "\n");
|
|
319
|
-
|
|
335
|
+
const assembled = base + runTestsE2eBlock();
|
|
336
|
+
// FAIL-CLOSED: never ship a run-tests.sh that lost its primary suite. The base
|
|
337
|
+
// template assigns REPO_ROOT and runs the pytest/vitest body; if the ORIGINAL
|
|
338
|
+
// carried that body but the ASSEMBLED result would not, the cut dropped real
|
|
339
|
+
// content — throw rather than write a hollow, always-exit-0 harness (the clobber
|
|
340
|
+
// defect). A genuinely body-less original (nothing to preserve) is untouched.
|
|
341
|
+
const BODY_SENTINEL = 'REPO_ROOT="$(cd';
|
|
342
|
+
if (original.includes(BODY_SENTINEL) && !assembled.includes(BODY_SENTINEL)) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`addE2eToRunTestsScript: refusing to write ${scriptPath} — the E2E assembly would drop the ` +
|
|
345
|
+
`primary-suite body (REPO_ROOT/pytest/vitest). This is the marker-match clobber; the marker ` +
|
|
346
|
+
`must anchor to the appended block's own line, not a prose mention.`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
fs.writeFileSync(scriptPath, assembled, "utf8");
|
|
320
350
|
return { patched: true, inserted: true };
|
|
321
351
|
}
|
|
322
352
|
|
|
@@ -148,11 +148,6 @@ jobs:
|
|
|
148
148
|
# the CI branch is provisioned and fail fast. `--if-present` no-ops a
|
|
149
149
|
# client with no `test` script. The client's Playwright E2E runs later
|
|
150
150
|
# (its own step, gated on client/playwright.config.ts).
|
|
151
|
-
- name: Run client unit tests
|
|
152
|
-
if: hashFiles('client/package.json') != ''
|
|
153
|
-
working-directory: client
|
|
154
|
-
run: npm run test --if-present
|
|
155
|
-
|
|
156
151
|
# The official action installs to a user-writable path (no sudo). It's
|
|
157
152
|
# a no-op if a compatible databricks CLI is already on PATH.
|
|
158
153
|
- name: Install Databricks CLI
|
|
@@ -384,6 +379,20 @@ jobs:
|
|
|
384
379
|
--pretty
|
|
385
380
|
echo "Migrations completed."
|
|
386
381
|
|
|
382
|
+
- name: Run client unit tests
|
|
383
|
+
# The kit's client/vitest.globalSetup.ts boots a REAL backend over the /api
|
|
384
|
+
# boundary (never mock when a real counterpart exists), so the component suite
|
|
385
|
+
# NEEDS the CI Lakebase branch + DATABASE_URL. Run it AFTER datasource config +
|
|
386
|
+
# migrations, not before. No-op when there are no component tests (globalSetup
|
|
387
|
+
# returns early); skipped when there is no CI Lakebase branch (url empty).
|
|
388
|
+
if: hashFiles('client/package.json') != '' && steps.lakebase-db.outputs.url != ''
|
|
389
|
+
working-directory: client
|
|
390
|
+
env:
|
|
391
|
+
DATABASE_URL: ${{ env.DATABASE_URL }}
|
|
392
|
+
DB_USERNAME: ${{ env.DB_USERNAME }}
|
|
393
|
+
DB_PASSWORD: ${{ env.DB_PASSWORD }}
|
|
394
|
+
run: npm run test --if-present
|
|
395
|
+
|
|
387
396
|
- name: Run tests
|
|
388
397
|
env:
|
|
389
398
|
DATABASE_URL: ${{ env.DATABASE_URL }}
|