@databricks-solutions/lakebase-scm-utils 0.2.46 → 0.2.47

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 (62) hide show
  1. package/dist/{pr-D3CteJaf.d.cts → pr-e-E4aP4j.d.cts} +6 -0
  2. package/dist/{pr-D3CteJaf.d.ts → pr-e-E4aP4j.d.ts} +6 -0
  3. package/dist/scripts/github/index.cjs +2 -0
  4. package/dist/scripts/github/index.d.cts +1 -1
  5. package/dist/scripts/github/index.d.ts +1 -1
  6. package/dist/scripts/github/index.js +2 -0
  7. package/dist/scripts/github/pr.cli.cjs +2 -0
  8. package/dist/scripts/github/pr.cli.js +2 -0
  9. package/dist/scripts/index.cjs +28 -4
  10. package/dist/scripts/index.d.cts +2 -2
  11. package/dist/scripts/index.d.ts +2 -2
  12. package/dist/scripts/index.js +26 -4
  13. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2 -2
  14. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +2 -2
  15. package/dist/scripts/lakebase/collapse-heads.cli.cjs +2 -2
  16. package/dist/scripts/lakebase/collapse-heads.cli.js +2 -2
  17. package/dist/scripts/lakebase/detect-language.cli.cjs +2 -2
  18. package/dist/scripts/lakebase/detect-language.cli.js +2 -2
  19. package/dist/scripts/lakebase/doctor.cli.cjs +2 -2
  20. package/dist/scripts/lakebase/doctor.cli.js +2 -2
  21. package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
  22. package/dist/scripts/lakebase/get-connection.cli.js +2 -2
  23. package/dist/scripts/lakebase/index.cjs +34 -4
  24. package/dist/scripts/lakebase/index.d.cts +3 -3
  25. package/dist/scripts/lakebase/index.d.ts +3 -3
  26. package/dist/scripts/lakebase/index.js +34 -4
  27. package/dist/scripts/lakebase/infra-runner.cli.cjs +2 -2
  28. package/dist/scripts/lakebase/infra-runner.cli.js +2 -2
  29. package/dist/scripts/lakebase/new-migration.cli.cjs +2 -2
  30. package/dist/scripts/lakebase/new-migration.cli.js +2 -2
  31. package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
  32. package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
  33. package/dist/scripts/lakebase/schema-migrate.cli.cjs +2 -2
  34. package/dist/scripts/lakebase/schema-migrate.cli.js +2 -2
  35. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2 -2
  36. package/dist/scripts/lakebase/scm-claim-feature.cli.js +2 -2
  37. package/dist/scripts/lakebase/scm-doctor.cli.cjs +2 -2
  38. package/dist/scripts/lakebase/scm-doctor.cli.js +2 -2
  39. package/dist/scripts/lakebase/scm-merge.cli.cjs +2 -2
  40. package/dist/scripts/lakebase/scm-merge.cli.js +2 -2
  41. package/dist/scripts/lakebase/scm-prepare-pr.cli.cjs +2 -0
  42. package/dist/scripts/lakebase/scm-prepare-pr.cli.js +2 -0
  43. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2 -2
  44. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +2 -2
  45. package/dist/scripts/lakebase/scm-wait-ci.cli.cjs +8 -0
  46. package/dist/scripts/lakebase/scm-wait-ci.cli.js +8 -0
  47. package/dist/scripts/lakebase/sync-ci-secrets.cli.cjs +8610 -0
  48. package/dist/scripts/lakebase/sync-ci-secrets.cli.d.cts +4 -0
  49. package/dist/scripts/lakebase/sync-ci-secrets.cli.d.ts +4 -0
  50. package/dist/scripts/lakebase/sync-ci-secrets.cli.js +8597 -0
  51. package/dist/scripts/util/index.cjs +21 -2
  52. package/dist/scripts/util/index.d.cts +9 -1
  53. package/dist/scripts/util/index.d.ts +9 -1
  54. package/dist/scripts/util/index.js +19 -2
  55. package/package.json +3 -2
  56. package/scripts/github/pr.ts +8 -0
  57. package/scripts/lakebase/create-project.ts +14 -3
  58. package/scripts/lakebase/scm-wait-ci.ts +13 -0
  59. package/scripts/lakebase/sync-ci-secrets.cli.ts +122 -0
  60. package/scripts/util/ci-secrets.ts +14 -1
  61. package/templates/project/common/.gitignore.base +6 -0
  62. package/templates/project/common/scripts/lk +22 -4
@@ -1466,12 +1466,14 @@ var util_exports = {};
1466
1466
  __export(util_exports, {
1467
1467
  LAKEBASE_BRANCH_NAME_MAX: () => LAKEBASE_BRANCH_NAME_MAX,
1468
1468
  PROXY_ENV_KEYS: () => PROXY_ENV_KEYS,
1469
+ REQUIRED_CI_SECRETS: () => REQUIRED_CI_SECRETS,
1469
1470
  copyDirSubstituted: () => copyDirSubstituted,
1470
1471
  delay: () => delay,
1471
1472
  exec: () => exec2,
1472
1473
  extractZipToDir: () => extractZipToDir,
1473
1474
  formatOwnerRepo: () => formatOwnerRepo,
1474
1475
  isCliEntry: () => isCliEntry,
1476
+ missingCiSecrets: () => missingCiSecrets,
1475
1477
  parseOwnerRepo: () => parseOwnerRepo,
1476
1478
  patchPomForLakebase: () => patchPomForLakebase,
1477
1479
  pollUntil: () => pollUntil,
@@ -1659,8 +1661,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
1659
1661
  var cached;
1660
1662
  function substrateSelfVersion() {
1661
1663
  if (cached !== void 0) return cached;
1662
- if ("0.2.46".length > 0) {
1663
- cached = "0.2.46";
1664
+ if ("0.2.47".length > 0) {
1665
+ cached = "0.2.47";
1664
1666
  return cached;
1665
1667
  }
1666
1668
  cached = "unknown";
@@ -8580,6 +8582,16 @@ async function setRepoSecrets(ownerRepo, secrets) {
8580
8582
  await setRepoSecret(ownerRepo, name, value);
8581
8583
  }
8582
8584
  }
8585
+ async function listSecretNames(ownerRepo) {
8586
+ try {
8587
+ const { owner, repo } = parseOwnerRepo(ownerRepo);
8588
+ const octokit = await getOctokit();
8589
+ const { data } = await octokit.rest.actions.listRepoSecrets({ owner, repo });
8590
+ return data.secrets.map((s) => s.name);
8591
+ } catch {
8592
+ return [];
8593
+ }
8594
+ }
8583
8595
 
8584
8596
  // scripts/git/remote.ts
8585
8597
  init_cjs_shims();
@@ -8619,6 +8631,11 @@ async function getOwnerRepo(cwd) {
8619
8631
  }
8620
8632
 
8621
8633
  // scripts/util/ci-secrets.ts
8634
+ var REQUIRED_CI_SECRETS = ["DATABRICKS_HOST", "LAKEBASE_PROJECT_ID", "DATABRICKS_TOKEN"];
8635
+ async function missingCiSecrets(ownerRepo) {
8636
+ const present = new Set(await listSecretNames(ownerRepo));
8637
+ return REQUIRED_CI_SECRETS.filter((n) => !present.has(n));
8638
+ }
8622
8639
  async function syncCiSecrets(args) {
8623
8640
  const lifetime = args.lifetimeSeconds ?? 86400;
8624
8641
  const comment = args.comment ?? "GitHub Actions CI";
@@ -8757,12 +8774,14 @@ async function pollUntilDefined(probe, opts) {
8757
8774
  0 && (module.exports = {
8758
8775
  LAKEBASE_BRANCH_NAME_MAX,
8759
8776
  PROXY_ENV_KEYS,
8777
+ REQUIRED_CI_SECRETS,
8760
8778
  copyDirSubstituted,
8761
8779
  delay,
8762
8780
  exec,
8763
8781
  extractZipToDir,
8764
8782
  formatOwnerRepo,
8765
8783
  isCliEntry,
8784
+ missingCiSecrets,
8766
8785
  parseOwnerRepo,
8767
8786
  patchPomForLakebase,
8768
8787
  pollUntil,
@@ -33,6 +33,14 @@ declare function copyDirSubstituted(srcDir: string, destDir: string, args?: Copy
33
33
  declare const LAKEBASE_BRANCH_NAME_MAX = 63;
34
34
  declare function sanitizeBranchName(gitBranch: string): string;
35
35
 
36
+ /** The repo Actions secrets CI needs to provision a per-PR Lakebase branch. Missing ANY of
37
+ * these leaves CI unable to produce a DATABASE_URL, so the test step aborts — the failure
38
+ * that otherwise only surfaces at promotion, an entire feature after creation. */
39
+ declare const REQUIRED_CI_SECRETS: readonly ["DATABRICKS_HOST", "LAKEBASE_PROJECT_ID", "DATABRICKS_TOKEN"];
40
+ /** Which required CI secrets are NOT present on the repo (empty = fully provisioned). Used to
41
+ * VERIFY a sync landed — a `databricks tokens create` or `gh secret set` that fails must be
42
+ * caught here, not swallowed, so CI-auth is never silently absent. */
43
+ declare function missingCiSecrets(ownerRepo: string): Promise<string[]>;
36
44
  interface SyncCiSecretsArgs {
37
45
  /** Project root (used to resolve ownerRepo from `git remote` when not given,
38
46
  * and as the cwd for the `databricks tokens create` call). */
@@ -161,4 +169,4 @@ declare function pollUntilDefined<T>(probe: (ctx: {
161
169
  elapsedMs: number;
162
170
  }) => Promise<T | undefined>, opts: Omit<PollUntilArgs<T>, "probe">): Promise<PollUntilResult<T>>;
163
171
 
164
- export { type CopyDirSubstitutedArgs, LAKEBASE_BRANCH_NAME_MAX, type OwnerRepo, PROXY_ENV_KEYS, type PollProbeDone, type PollProbePending, type PollProbeResult, type PollUntilArgs, type PollUntilDoneResult, type PollUntilResult, type PollUntilTimeoutResult, type SyncCiSecretsArgs, copyDirSubstituted, delay, extractZipToDir, formatOwnerRepo, isCliEntry, parseOwnerRepo, patchPomForLakebase, pollUntil, pollUntilDefined, proxyEnvSubset, sanitizeArtifactId, sanitizeBranchName, syncCiSecrets, withProxyEnv };
172
+ export { type CopyDirSubstitutedArgs, LAKEBASE_BRANCH_NAME_MAX, type OwnerRepo, PROXY_ENV_KEYS, type PollProbeDone, type PollProbePending, type PollProbeResult, type PollUntilArgs, type PollUntilDoneResult, type PollUntilResult, type PollUntilTimeoutResult, REQUIRED_CI_SECRETS, type SyncCiSecretsArgs, copyDirSubstituted, delay, extractZipToDir, formatOwnerRepo, isCliEntry, missingCiSecrets, parseOwnerRepo, patchPomForLakebase, pollUntil, pollUntilDefined, proxyEnvSubset, sanitizeArtifactId, sanitizeBranchName, syncCiSecrets, withProxyEnv };
@@ -33,6 +33,14 @@ declare function copyDirSubstituted(srcDir: string, destDir: string, args?: Copy
33
33
  declare const LAKEBASE_BRANCH_NAME_MAX = 63;
34
34
  declare function sanitizeBranchName(gitBranch: string): string;
35
35
 
36
+ /** The repo Actions secrets CI needs to provision a per-PR Lakebase branch. Missing ANY of
37
+ * these leaves CI unable to produce a DATABASE_URL, so the test step aborts — the failure
38
+ * that otherwise only surfaces at promotion, an entire feature after creation. */
39
+ declare const REQUIRED_CI_SECRETS: readonly ["DATABRICKS_HOST", "LAKEBASE_PROJECT_ID", "DATABRICKS_TOKEN"];
40
+ /** Which required CI secrets are NOT present on the repo (empty = fully provisioned). Used to
41
+ * VERIFY a sync landed — a `databricks tokens create` or `gh secret set` that fails must be
42
+ * caught here, not swallowed, so CI-auth is never silently absent. */
43
+ declare function missingCiSecrets(ownerRepo: string): Promise<string[]>;
36
44
  interface SyncCiSecretsArgs {
37
45
  /** Project root (used to resolve ownerRepo from `git remote` when not given,
38
46
  * and as the cwd for the `databricks tokens create` call). */
@@ -161,4 +169,4 @@ declare function pollUntilDefined<T>(probe: (ctx: {
161
169
  elapsedMs: number;
162
170
  }) => Promise<T | undefined>, opts: Omit<PollUntilArgs<T>, "probe">): Promise<PollUntilResult<T>>;
163
171
 
164
- export { type CopyDirSubstitutedArgs, LAKEBASE_BRANCH_NAME_MAX, type OwnerRepo, PROXY_ENV_KEYS, type PollProbeDone, type PollProbePending, type PollProbeResult, type PollUntilArgs, type PollUntilDoneResult, type PollUntilResult, type PollUntilTimeoutResult, type SyncCiSecretsArgs, copyDirSubstituted, delay, extractZipToDir, formatOwnerRepo, isCliEntry, parseOwnerRepo, patchPomForLakebase, pollUntil, pollUntilDefined, proxyEnvSubset, sanitizeArtifactId, sanitizeBranchName, syncCiSecrets, withProxyEnv };
172
+ export { type CopyDirSubstitutedArgs, LAKEBASE_BRANCH_NAME_MAX, type OwnerRepo, PROXY_ENV_KEYS, type PollProbeDone, type PollProbePending, type PollProbeResult, type PollUntilArgs, type PollUntilDoneResult, type PollUntilResult, type PollUntilTimeoutResult, REQUIRED_CI_SECRETS, type SyncCiSecretsArgs, copyDirSubstituted, delay, extractZipToDir, formatOwnerRepo, isCliEntry, missingCiSecrets, parseOwnerRepo, patchPomForLakebase, pollUntil, pollUntilDefined, proxyEnvSubset, sanitizeArtifactId, sanitizeBranchName, syncCiSecrets, withProxyEnv };
@@ -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.46".length > 0) {
1634
- cached = "0.2.46";
1633
+ if ("0.2.47".length > 0) {
1634
+ cached = "0.2.47";
1635
1635
  return cached;
1636
1636
  }
1637
1637
  cached = "unknown";
@@ -8551,6 +8551,16 @@ async function setRepoSecrets(ownerRepo, secrets) {
8551
8551
  await setRepoSecret(ownerRepo, name, value);
8552
8552
  }
8553
8553
  }
8554
+ async function listSecretNames(ownerRepo) {
8555
+ try {
8556
+ const { owner, repo } = parseOwnerRepo(ownerRepo);
8557
+ const octokit = await getOctokit();
8558
+ const { data } = await octokit.rest.actions.listRepoSecrets({ owner, repo });
8559
+ return data.secrets.map((s) => s.name);
8560
+ } catch {
8561
+ return [];
8562
+ }
8563
+ }
8554
8564
 
8555
8565
  // scripts/git/remote.ts
8556
8566
  init_esm_shims();
@@ -8590,6 +8600,11 @@ async function getOwnerRepo(cwd) {
8590
8600
  }
8591
8601
 
8592
8602
  // scripts/util/ci-secrets.ts
8603
+ var REQUIRED_CI_SECRETS = ["DATABRICKS_HOST", "LAKEBASE_PROJECT_ID", "DATABRICKS_TOKEN"];
8604
+ async function missingCiSecrets(ownerRepo) {
8605
+ const present = new Set(await listSecretNames(ownerRepo));
8606
+ return REQUIRED_CI_SECRETS.filter((n) => !present.has(n));
8607
+ }
8593
8608
  async function syncCiSecrets(args) {
8594
8609
  const lifetime = args.lifetimeSeconds ?? 86400;
8595
8610
  const comment = args.comment ?? "GitHub Actions CI";
@@ -8727,12 +8742,14 @@ async function pollUntilDefined(probe, opts) {
8727
8742
  export {
8728
8743
  LAKEBASE_BRANCH_NAME_MAX,
8729
8744
  PROXY_ENV_KEYS,
8745
+ REQUIRED_CI_SECRETS,
8730
8746
  copyDirSubstituted,
8731
8747
  delay,
8732
8748
  exec2 as exec,
8733
8749
  extractZipToDir,
8734
8750
  formatOwnerRepo,
8735
8751
  isCliEntry,
8752
+ missingCiSecrets,
8736
8753
  parseOwnerRepo,
8737
8754
  patchPomForLakebase,
8738
8755
  pollUntil,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databricks-solutions/lakebase-scm-utils",
3
- "version": "0.2.46",
3
+ "version": "0.2.47",
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,
@@ -105,7 +105,8 @@
105
105
  "lakebase-scm-prepare-pr": "./dist/scripts/lakebase/scm-prepare-pr.cli.js",
106
106
  "lakebase-scm-recover-orphans": "./dist/scripts/lakebase/scm-recover-orphans.cli.js",
107
107
  "lakebase-scm-state": "./dist/scripts/lakebase/scm-state.cli.js",
108
- "lakebase-scm-wait-ci": "./dist/scripts/lakebase/scm-wait-ci.cli.js"
108
+ "lakebase-scm-wait-ci": "./dist/scripts/lakebase/scm-wait-ci.cli.js",
109
+ "lakebase-sync-ci-secrets": "./dist/scripts/lakebase/sync-ci-secrets.cli.js"
109
110
  },
110
111
  "scripts": {
111
112
  "build": "tsup",
@@ -67,6 +67,12 @@ export interface PullRequestInfo {
67
67
  isDraft: boolean;
68
68
  ciStatus: "pending" | "success" | "failure";
69
69
  checks: PullRequestCheck[];
70
+ /** GitHub's mergeability triple: true/false/null (null = still computing). */
71
+ mergeable?: boolean | null;
72
+ /** GitHub's `mergeable_state`: `clean`/`unstable`/`behind`/`blocked`/`dirty`/`unknown`.
73
+ * `dirty` = merge conflict with the base — a `pull_request` CI run can't be dispatched
74
+ * (there is no mergeable ref), so a waiter must surface this, not poll until timeout. */
75
+ mergeableState?: string;
70
76
  headBranch: string;
71
77
  baseBranch: string;
72
78
  body?: string;
@@ -184,6 +190,8 @@ export async function getPullRequest(
184
190
  isDraft: pr.draft || false,
185
191
  ciStatus,
186
192
  checks,
193
+ mergeable: pr.mergeable,
194
+ mergeableState: pr.mergeable_state,
187
195
  headBranch: pr.head?.ref || headBranch,
188
196
  baseBranch: pr.base?.ref || "",
189
197
  body: pr.body || undefined,
@@ -30,7 +30,7 @@ import { createLongRunningBranch } from "./long-running-branch.js";
30
30
  import { enableE2eForProject } from "./enable-e2e.js";
31
31
  import { enableInfraForProject } from "./enable-infra.js";
32
32
  import { setupRunner } from "./runner-setup.js";
33
- import { syncCiSecrets } from "../util/ci-secrets.js";
33
+ import { syncCiSecrets, missingCiSecrets } from "../util/ci-secrets.js";
34
34
  import { delay } from "../util/delay.js";
35
35
  import {
36
36
  initWorkflowState,
@@ -437,10 +437,21 @@ export async function createProject(
437
437
  lifetimeSeconds: 86_400,
438
438
  ownerRepo: fullRepoName,
439
439
  });
440
+ // VERIFY the secrets actually landed — syncCiSecrets fail-softs a dropped PAT, and a
441
+ // swallowed failure here only surfaces at the first promotion CI run (no DATABASE_URL),
442
+ // an entire feature later. Name the exact gaps + the one-command repair so it is actionable
443
+ // NOW, not a buried warning.
444
+ const missing = await missingCiSecrets(fullRepoName);
445
+ if (missing.length > 0) {
446
+ const m = `CI auth incomplete — repo secret(s) MISSING: ${missing.join(", ")}. CI cannot provision a per-PR Lakebase branch until fixed. Repair with: lakebase-sync-ci-secrets (run in the project).`;
447
+ warnings.push(m);
448
+ report(`WARNING: ${m}`);
449
+ }
440
450
  } catch (err) {
441
451
  const msg = err instanceof Error ? err.message : String(err);
442
- warnings.push(`CI auth setup failed: ${msg}`);
443
- report(`Warning: CI auth setup failed (${msg})`);
452
+ const m = `CI auth setup failed: ${msg}. CI will fail (no DATABASE_URL) until repaired — run: lakebase-sync-ci-secrets (in the project).`;
453
+ warnings.push(m);
454
+ report(`WARNING: ${m}`);
444
455
  }
445
456
  }
446
457
 
@@ -24,6 +24,7 @@ export class ScmWaitCiError extends Error {
24
24
  | "bad-precondition"
25
25
  | "no-github-remote"
26
26
  | "ci-failed"
27
+ | "pr-conflicting"
27
28
  | "timeout"
28
29
  | "pr-not-found",
29
30
  ) {
@@ -123,6 +124,18 @@ export async function waitForCi(args: WaitCiArgs): Promise<WaitCiResult> {
123
124
  if (lastPr.ciStatus === "success") {
124
125
  return { done: true, value: lastPr };
125
126
  }
127
+ // A CONFLICTING PR (merge conflict with base) has no mergeable ref, so GitHub never
128
+ // dispatches the `pull_request` workflow — CI stays at 0 runs forever. Detect it and
129
+ // surface at once (resolve + re-push re-triggers), rather than spinning to the timeout
130
+ // on a `pending` that can never advance. Guard on `mergeableState==="dirty"` AND no
131
+ // checks yet, so a transient null/unknown while GitHub computes mergeability doesn't
132
+ // false-trip, and a PR that already has runs is judged by ciStatus as before.
133
+ if (lastPr.mergeableState === "dirty" && lastPr.checks.length === 0) {
134
+ throw new ScmWaitCiError(
135
+ `PR ${lastPr.url} is CONFLICTING with ${lastPr.baseBranch} — GitHub will not dispatch CI (0 runs) until the conflict is resolved. Reconcile the branch and re-push; the synchronize event will trigger CI.`,
136
+ "pr-conflicting",
137
+ );
138
+ }
126
139
  if (lastPr.ciStatus === "failure") {
127
140
  const failed = lastPr.checks
128
141
  .filter((c) => /(FAILURE|TIMED_OUT|CANCELLED|ACTION_REQUIRED)/i.test(c.conclusion))
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ // CLI: (re)provision the repo's CI auth secrets — the standalone repair path.
3
+ //
4
+ // create-project runs syncCiSecrets ONCE, fail-soft (a failed PAT mint or `gh secret set` is
5
+ // swallowed as a warning), and there was no way to retry without recreating the whole project.
6
+ // So a repo could sit with ZERO CI secrets until a promotion CI run failed an entire feature
7
+ // later (empty DATABRICKS_* → no DATABASE_URL → the test step aborts). This bin mints a fresh
8
+ // CI PAT and sets DATABRICKS_HOST / LAKEBASE_PROJECT_ID / DATABRICKS_TOKEN, then VERIFIES all
9
+ // three landed — exiting non-zero (naming the gaps) instead of leaving CI silently broken.
10
+
11
+ import * as path from "node:path";
12
+ import { isCliEntry } from "../util/cli-entry.js";
13
+ import { readEnvVar } from "./env-file.js";
14
+ import { getOwnerRepo } from "../git/remote.js";
15
+ import { syncCiSecrets, missingCiSecrets, REQUIRED_CI_SECRETS } from "../util/ci-secrets.js";
16
+
17
+ interface ParsedArgs {
18
+ projectDir?: string;
19
+ host?: string;
20
+ projectId?: string;
21
+ repo?: string;
22
+ lifetimeSec?: number;
23
+ comment?: string;
24
+ help?: boolean;
25
+ }
26
+
27
+ function parseArgs(argv: string[]): ParsedArgs {
28
+ const out: ParsedArgs = {};
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const a = argv[i];
31
+ switch (a) {
32
+ case "--project-dir":
33
+ case "--cwd": out.projectDir = argv[++i]; break;
34
+ case "--host": out.host = argv[++i]; break;
35
+ case "--project-id": out.projectId = argv[++i]; break;
36
+ case "--repo": out.repo = argv[++i]; break;
37
+ case "--lifetime-sec": out.lifetimeSec = Number.parseInt(argv[++i], 10); break;
38
+ case "--comment": out.comment = argv[++i]; break;
39
+ case "--help": case "-h": out.help = true; break;
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+
45
+ const HELP = `lakebase-sync-ci-secrets
46
+
47
+ (Re)provision the GitHub repo's CI auth: mint a fresh Databricks CI PAT and set
48
+ DATABRICKS_HOST, LAKEBASE_PROJECT_ID, and DATABRICKS_TOKEN as repo Actions secrets,
49
+ then verify all three are present. The standalone repair for the create-time
50
+ "CI auth setup failed" warning — run it from the project, no recreate needed.
51
+
52
+ Usage:
53
+ lakebase-sync-ci-secrets [flags]
54
+
55
+ Flags:
56
+ --project-dir <dir> Project root (default: cwd); resolves host/project-id from its .env
57
+ --host <url> DATABRICKS_HOST (default: .env DATABRICKS_HOST)
58
+ --project-id <id> LAKEBASE_PROJECT_ID (default: .env LAKEBASE_PROJECT_ID)
59
+ --repo <owner/name> Target repo (default: origin remote)
60
+ --lifetime-sec <n> CI PAT lifetime (default: 86400 = 24h)
61
+ --comment <text> Token comment (default: "GitHub Actions CI")
62
+ -h, --help Show this help
63
+
64
+ Exit codes:
65
+ 0 = all three secrets present after sync
66
+ 2 = could not resolve host / project-id / repo
67
+ 3 = sync ran but a required secret is still missing (e.g. PAT mint failed)
68
+ `;
69
+
70
+ export async function runSyncCiSecretsCli(argv: string[]): Promise<number> {
71
+ const args = parseArgs(argv);
72
+ if (args.help) {
73
+ process.stdout.write(`${HELP}\n`);
74
+ return 0;
75
+ }
76
+ const projectDir = path.resolve(args.projectDir ?? process.cwd());
77
+ const envPath = path.join(projectDir, ".env");
78
+ const host = args.host ?? readEnvVar(envPath, "DATABRICKS_HOST");
79
+ const projectId = args.projectId ?? readEnvVar(envPath, "LAKEBASE_PROJECT_ID");
80
+ const ownerRepo = args.repo ?? (await getOwnerRepo(projectDir));
81
+
82
+ if (!ownerRepo) {
83
+ process.stderr.write("lakebase-sync-ci-secrets: no GitHub repo (pass --repo or run inside a repo with an origin remote).\n");
84
+ return 2;
85
+ }
86
+ if (!host || !projectId) {
87
+ process.stderr.write(
88
+ `lakebase-sync-ci-secrets: missing ${!host ? "DATABRICKS_HOST" : ""}${!host && !projectId ? " and " : ""}${!projectId ? "LAKEBASE_PROJECT_ID" : ""}` +
89
+ ` — not in ${envPath}; pass --host / --project-id.\n`,
90
+ );
91
+ return 2;
92
+ }
93
+
94
+ process.stdout.write(`Provisioning CI auth for ${ownerRepo} (host ${host}, project ${projectId})…\n`);
95
+ await syncCiSecrets({
96
+ projectDir,
97
+ databricksHost: host,
98
+ lakebaseProjectId: projectId,
99
+ ownerRepo,
100
+ ...(args.comment ? { comment: args.comment } : {}),
101
+ ...(args.lifetimeSec ? { lifetimeSeconds: args.lifetimeSec } : {}),
102
+ });
103
+
104
+ // VERIFY — the whole point of the repair. A silently-dropped PAT (the create-time failure
105
+ // mode) leaves DATABRICKS_TOKEN absent; catch it here and exit non-zero rather than report success.
106
+ const missing = await missingCiSecrets(ownerRepo);
107
+ if (missing.length > 0) {
108
+ process.stderr.write(
109
+ `lakebase-sync-ci-secrets: sync ran but these secrets are still MISSING on ${ownerRepo}: ${missing.join(", ")}.\n` +
110
+ (missing.includes("DATABRICKS_TOKEN")
111
+ ? ` DATABRICKS_TOKEN missing usually means 'databricks tokens create' failed — check your auth for ${host} (e.g. 'databricks auth login --host ${host}'), then re-run.\n`
112
+ : ""),
113
+ );
114
+ return 3;
115
+ }
116
+ process.stdout.write(`CI auth ready: ${REQUIRED_CI_SECRETS.join(", ")} set on ${ownerRepo}.\n`);
117
+ return 0;
118
+ }
119
+
120
+ if (isCliEntry(import.meta.url)) {
121
+ void runSyncCiSecretsCli(process.argv.slice(2)).then((c) => process.exit(c));
122
+ }
@@ -7,9 +7,22 @@
7
7
  // be set), the values have to come from the create-project caller's scope.
8
8
 
9
9
  import { runDatabricks } from "../lakebase/databricks-cli.js";
10
- import { setRepoSecrets } from "../github/secrets.js";
10
+ import { setRepoSecrets, listSecretNames } from "../github/secrets.js";
11
11
  import { getOwnerRepo } from "../git/remote.js";
12
12
 
13
+ /** The repo Actions secrets CI needs to provision a per-PR Lakebase branch. Missing ANY of
14
+ * these leaves CI unable to produce a DATABASE_URL, so the test step aborts — the failure
15
+ * that otherwise only surfaces at promotion, an entire feature after creation. */
16
+ export const REQUIRED_CI_SECRETS = ["DATABRICKS_HOST", "LAKEBASE_PROJECT_ID", "DATABRICKS_TOKEN"] as const;
17
+
18
+ /** Which required CI secrets are NOT present on the repo (empty = fully provisioned). Used to
19
+ * VERIFY a sync landed — a `databricks tokens create` or `gh secret set` that fails must be
20
+ * caught here, not swallowed, so CI-auth is never silently absent. */
21
+ export async function missingCiSecrets(ownerRepo: string): Promise<string[]> {
22
+ const present = new Set(await listSecretNames(ownerRepo));
23
+ return REQUIRED_CI_SECRETS.filter((n) => !present.has(n));
24
+ }
25
+
13
26
  export interface SyncCiSecretsArgs {
14
27
  /** Project root (used to resolve ownerRepo from `git remote` when not given,
15
28
  * and as the cwd for the `databricks tokens create` call). */
@@ -29,7 +29,13 @@ application-local.properties
29
29
  # it on the feature branch at each cut so the spec history survives for later stories).
30
30
  .consort/cycles/
31
31
  .consort/experiments/
32
+ # The live build-pipeline pointer. It lives PER-FEATURE (.consort/features/<id>/pipeline.json);
33
+ # the bare `.consort/pipeline.json` is kept for older layouts. Matching only the top-level path
34
+ # left the per-feature file TRACKED — so it travelled onto staging/main and collided at the
35
+ # promotion merge (two branches editing one run's gate-bookkeeping), the exact "pipeline.json
36
+ # conflict blocks promotion" defect. Both forms are run-state, never the design corpus.
32
37
  .consort/pipeline.json
38
+ .consort/features/*/pipeline.json
33
39
  # The turn recorder's per-run observability: its internal file-hash map
34
40
  # (.recorder-state.json, used for delta computation), the routing-decision
35
41
  # diagnostic stream (routing-decisions.jsonl), and the per-turn record dirs
@@ -297,6 +297,14 @@ if [ "$first" = "--warm" ] || [ "$first" = "--rewarm" ]; then
297
297
  fi
298
298
 
299
299
  bin="$first"; shift
300
+ # A help request (`-h`/`--help`) must NEVER trigger a one-time cold-cache install:
301
+ # printing usage should have no side effects. Treat it like LK_NO_INSTALL so a cold
302
+ # cache SHORT-CIRCUITS with a clear "warm first" note instead of silently kicking off a
303
+ # multi-minute download (the "my --help refreshed the cache?!" surprise). When the cache
304
+ # is already warm, help still prints instantly, exactly as before.
305
+ help_req=0
306
+ for _a in "$@"; do case "$_a" in -h|--help) help_req=1; break ;; esac; done
307
+ [ "$help_req" -eq 1 ] && export LK_NO_INSTALL=1
300
308
  # Deprecation: lakebase-sftdd-* / lakebase-tdd-* are legacy aliases from the
301
309
  # pre-Consort skill names. They still run (routed to the kit below), but are
302
310
  # scheduled for removal in consort v0.4.0 , prefer the consort-* / lakebase-*
@@ -306,8 +314,15 @@ case "$bin" in
306
314
  echo "lk: [deprecated] '${bin}' is a legacy sftdd/tdd-era alias; use the consort-* / lakebase-* name (removed in consort v0.4.0). Still running." >&2 ;;
307
315
  esac
308
316
  slug="$(pkg_for_bin "$bin")"
309
- resolve_pkg "$slug" run; rc=$?
310
- [ "$rc" -eq 97 ] && exit 97
317
+ # Capture the status with `|| rc=$?` so `set -e` does NOT abort on resolve_pkg's
318
+ # non-zero returns (notably 97 = cold cache under LK_NO_INSTALL / a help request) —
319
+ # a bare `resolve_pkg …; rc=$?` exits before rc is ever read.
320
+ rc=0; resolve_pkg "$slug" run || rc=$?
321
+ if [ "$rc" -eq 97 ]; then
322
+ # Cold cache + a help request: do NOT install just to print usage.
323
+ [ "$help_req" -eq 1 ] && { echo "lk: not downloading the toolkit just to show '--help'. The ${slug} cache for this ref is cold; run 'lk --warm' to install it (one-time), then retry '${bin} --help'. A help request never triggers a download." >&2; exit 0; }
324
+ exit 97
325
+ fi
311
326
  [ "$rc" -ne 0 ] && exit "$rc"
312
327
  rel="$(bin_rel "$pkg_dir" "$bin")"
313
328
 
@@ -317,8 +332,11 @@ rel="$(bin_rel "$pkg_dir" "$bin")"
317
332
  # route to the kit up front and never reach here, so an unknown kit bin still fails
318
333
  # fast with no substrate round-trip.
319
334
  if [ -z "$rel" ] && [ "$slug" = "scm-utils" ] && project_uses_kit; then
320
- resolve_pkg kit run; krc=$?
321
- [ "$krc" -eq 97 ] && exit 97
335
+ krc=0; resolve_pkg kit run || krc=$?
336
+ if [ "$krc" -eq 97 ]; then
337
+ [ "$help_req" -eq 1 ] && { echo "lk: not downloading the toolkit just to show '--help'. The kit cache for this ref is cold; run 'lk --warm' to install it (one-time), then retry '${bin} --help'. A help request never triggers a download." >&2; exit 0; }
338
+ exit 97
339
+ fi
322
340
  if [ "$krc" -eq 0 ]; then
323
341
  rel="$(bin_rel "$pkg_dir" "$bin")"
324
342
  [ -n "$rel" ] && slug="kit"