@databricks-solutions/lakebase-scm-utils 0.2.48 → 0.2.49

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 (51) hide show
  1. package/dist/ci-secrets-CnGrdQV-.d.cts +84 -0
  2. package/dist/ci-secrets-CnGrdQV-.d.ts +84 -0
  3. package/dist/scripts/index.cjs +2177 -2052
  4. package/dist/scripts/index.d.cts +2 -1
  5. package/dist/scripts/index.d.ts +2 -1
  6. package/dist/scripts/index.js +253 -134
  7. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2 -2
  8. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +2 -2
  9. package/dist/scripts/lakebase/collapse-heads.cli.cjs +2 -2
  10. package/dist/scripts/lakebase/collapse-heads.cli.js +2 -2
  11. package/dist/scripts/lakebase/detect-language.cli.cjs +2 -2
  12. package/dist/scripts/lakebase/detect-language.cli.js +2 -2
  13. package/dist/scripts/lakebase/doctor.cli.cjs +2 -2
  14. package/dist/scripts/lakebase/doctor.cli.js +2 -2
  15. package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
  16. package/dist/scripts/lakebase/get-connection.cli.js +2 -2
  17. package/dist/scripts/lakebase/index.cjs +2163 -2050
  18. package/dist/scripts/lakebase/index.d.cts +8 -0
  19. package/dist/scripts/lakebase/index.d.ts +8 -0
  20. package/dist/scripts/lakebase/index.js +243 -130
  21. package/dist/scripts/lakebase/infra-runner.cli.cjs +2 -2
  22. package/dist/scripts/lakebase/infra-runner.cli.js +2 -2
  23. package/dist/scripts/lakebase/new-migration.cli.cjs +2 -2
  24. package/dist/scripts/lakebase/new-migration.cli.js +2 -2
  25. package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
  26. package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
  27. package/dist/scripts/lakebase/schema-migrate.cli.cjs +2 -2
  28. package/dist/scripts/lakebase/schema-migrate.cli.js +2 -2
  29. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2 -2
  30. package/dist/scripts/lakebase/scm-claim-feature.cli.js +2 -2
  31. package/dist/scripts/lakebase/scm-doctor.cli.cjs +2 -2
  32. package/dist/scripts/lakebase/scm-doctor.cli.js +2 -2
  33. package/dist/scripts/lakebase/scm-merge.cli.cjs +2216 -1994
  34. package/dist/scripts/lakebase/scm-merge.cli.js +296 -74
  35. package/dist/scripts/lakebase/scm-prepare-pr.cli.cjs +479 -86
  36. package/dist/scripts/lakebase/scm-prepare-pr.cli.js +479 -86
  37. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2 -2
  38. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +2 -2
  39. package/dist/scripts/lakebase/sync-ci-secrets.cli.cjs +56 -30
  40. package/dist/scripts/lakebase/sync-ci-secrets.cli.js +56 -30
  41. package/dist/scripts/util/index.cjs +142 -25
  42. package/dist/scripts/util/index.d.cts +2 -27
  43. package/dist/scripts/util/index.d.ts +2 -27
  44. package/dist/scripts/util/index.js +136 -25
  45. package/package.json +1 -1
  46. package/scripts/lakebase/create-project.ts +2 -2
  47. package/scripts/lakebase/scm-merge.ts +21 -0
  48. package/scripts/lakebase/scm-prepare-pr.ts +10 -1
  49. package/scripts/lakebase/sync-ci-secrets.cli.ts +2 -2
  50. package/scripts/util/ci-secrets.ts +212 -26
  51. package/templates/project/common/scripts/create-token-and-sync-secrets.sh +9 -86
@@ -1,4 +1,5 @@
1
1
  export { C as CwdOnly, E as ExecOptions, e as exec, s as shq } from '../../exec-CwxSWhlz.cjs';
2
+ export { C as CI_TOKEN_LIFETIME_SECONDS, a as CI_TOKEN_REMINT_MARGIN_SECONDS, b as CiFreshnessResult, c as CiTokenExpiryArgs, E as EnsureCiSecretsFreshArgs, R as REQUIRED_CI_SECRETS, S as SyncCiSecretsArgs, d as ciTokenComment, e as ciTokenExpiry, f as ensureCiSecretsFresh, g as ensureCiSecretsFreshFromEnv, m as missingCiSecrets, s as syncCiSecrets } from '../../ci-secrets-CnGrdQV-.cjs';
2
3
 
3
4
  /** Promise-based sleep – replaces shell `sleep` in poll loops. */
4
5
  declare function delay(ms: number): Promise<void>;
@@ -33,32 +34,6 @@ declare function copyDirSubstituted(srcDir: string, destDir: string, args?: Copy
33
34
  declare const LAKEBASE_BRANCH_NAME_MAX = 63;
34
35
  declare function sanitizeBranchName(gitBranch: string): string;
35
36
 
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[]>;
44
- interface SyncCiSecretsArgs {
45
- /** Project root (used to resolve ownerRepo from `git remote` when not given,
46
- * and as the cwd for the `databricks tokens create` call). */
47
- projectDir: string;
48
- /** Workspace host (DATABRICKS_HOST secret). Required. */
49
- databricksHost: string;
50
- /** Lakebase project id (LAKEBASE_PROJECT_ID secret). Required. */
51
- lakebaseProjectId: string;
52
- /** Token comment for `databricks tokens create`. */
53
- comment?: string;
54
- /** Token lifetime in seconds (default: 24h). */
55
- lifetimeSeconds?: number;
56
- /** Override the auto-detected ownerRepo (defaults to origin remote). */
57
- ownerRepo?: string;
58
- }
59
- /** Synchronize Databricks + Lakebase CI secrets to the repo's Actions secrets. */
60
- declare function syncCiSecrets(args: SyncCiSecretsArgs): Promise<void>;
61
-
62
37
  /**
63
38
  * Return true iff the current module is being executed as a Node.js
64
39
  * entry point (vs. imported as a library). Pass `import.meta.url`
@@ -169,4 +144,4 @@ declare function pollUntilDefined<T>(probe: (ctx: {
169
144
  elapsedMs: number;
170
145
  }) => Promise<T | undefined>, opts: Omit<PollUntilArgs<T>, "probe">): Promise<PollUntilResult<T>>;
171
146
 
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 };
147
+ 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, copyDirSubstituted, delay, extractZipToDir, formatOwnerRepo, isCliEntry, parseOwnerRepo, patchPomForLakebase, pollUntil, pollUntilDefined, proxyEnvSubset, sanitizeArtifactId, sanitizeBranchName, withProxyEnv };
@@ -1,4 +1,5 @@
1
1
  export { C as CwdOnly, E as ExecOptions, e as exec, s as shq } from '../../exec-CwxSWhlz.js';
2
+ export { C as CI_TOKEN_LIFETIME_SECONDS, a as CI_TOKEN_REMINT_MARGIN_SECONDS, b as CiFreshnessResult, c as CiTokenExpiryArgs, E as EnsureCiSecretsFreshArgs, R as REQUIRED_CI_SECRETS, S as SyncCiSecretsArgs, d as ciTokenComment, e as ciTokenExpiry, f as ensureCiSecretsFresh, g as ensureCiSecretsFreshFromEnv, m as missingCiSecrets, s as syncCiSecrets } from '../../ci-secrets-CnGrdQV-.js';
2
3
 
3
4
  /** Promise-based sleep – replaces shell `sleep` in poll loops. */
4
5
  declare function delay(ms: number): Promise<void>;
@@ -33,32 +34,6 @@ declare function copyDirSubstituted(srcDir: string, destDir: string, args?: Copy
33
34
  declare const LAKEBASE_BRANCH_NAME_MAX = 63;
34
35
  declare function sanitizeBranchName(gitBranch: string): string;
35
36
 
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[]>;
44
- interface SyncCiSecretsArgs {
45
- /** Project root (used to resolve ownerRepo from `git remote` when not given,
46
- * and as the cwd for the `databricks tokens create` call). */
47
- projectDir: string;
48
- /** Workspace host (DATABRICKS_HOST secret). Required. */
49
- databricksHost: string;
50
- /** Lakebase project id (LAKEBASE_PROJECT_ID secret). Required. */
51
- lakebaseProjectId: string;
52
- /** Token comment for `databricks tokens create`. */
53
- comment?: string;
54
- /** Token lifetime in seconds (default: 24h). */
55
- lifetimeSeconds?: number;
56
- /** Override the auto-detected ownerRepo (defaults to origin remote). */
57
- ownerRepo?: string;
58
- }
59
- /** Synchronize Databricks + Lakebase CI secrets to the repo's Actions secrets. */
60
- declare function syncCiSecrets(args: SyncCiSecretsArgs): Promise<void>;
61
-
62
37
  /**
63
38
  * Return true iff the current module is being executed as a Node.js
64
39
  * entry point (vs. imported as a library). Pass `import.meta.url`
@@ -169,4 +144,4 @@ declare function pollUntilDefined<T>(probe: (ctx: {
169
144
  elapsedMs: number;
170
145
  }) => Promise<T | undefined>, opts: Omit<PollUntilArgs<T>, "probe">): Promise<PollUntilResult<T>>;
171
146
 
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 };
147
+ 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, copyDirSubstituted, delay, extractZipToDir, formatOwnerRepo, isCliEntry, parseOwnerRepo, patchPomForLakebase, pollUntil, pollUntilDefined, proxyEnvSubset, sanitizeArtifactId, sanitizeBranchName, 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.48".length > 0) {
1634
- cached = "0.2.48";
1633
+ if ("0.2.49".length > 0) {
1634
+ cached = "0.2.49";
1635
1635
  return cached;
1636
1636
  }
1637
1637
  cached = "unknown";
@@ -1691,6 +1691,7 @@ function sanitizeBranchName(gitBranch) {
1691
1691
 
1692
1692
  // scripts/util/ci-secrets.ts
1693
1693
  init_esm_shims();
1694
+ import * as path6 from "path";
1694
1695
 
1695
1696
  // scripts/lakebase/databricks-cli.ts
1696
1697
  init_esm_shims();
@@ -2992,7 +2993,7 @@ paginateRest.VERSION = VERSION5;
2992
2993
 
2993
2994
  // node_modules/@octokit/plugin-paginate-graphql/dist-bundle/index.js
2994
2995
  init_esm_shims();
2995
- var generateMessage = (path6, cursorValue) => `The cursor at "${path6.join(
2996
+ var generateMessage = (path7, cursorValue) => `The cursor at "${path7.join(
2996
2997
  ","
2997
2998
  )}" did not change its value "${cursorValue}" after a page transition. Please make sure your that your query is set up correctly.`;
2998
2999
  var MissingCursorChange = class extends Error {
@@ -3033,9 +3034,9 @@ function findPaginatedResourcePath(responseData) {
3033
3034
  }
3034
3035
  return paginatedResourcePath;
3035
3036
  }
3036
- var deepFindPathToProperty = (object, searchProp, path6 = []) => {
3037
+ var deepFindPathToProperty = (object, searchProp, path7 = []) => {
3037
3038
  for (const key of Object.keys(object)) {
3038
- const currentPath = [...path6, key];
3039
+ const currentPath = [...path7, key];
3039
3040
  const currentValue = object[key];
3040
3041
  if (isObject(currentValue)) {
3041
3042
  if (currentValue.hasOwnProperty(searchProp)) {
@@ -3053,12 +3054,12 @@ var deepFindPathToProperty = (object, searchProp, path6 = []) => {
3053
3054
  }
3054
3055
  return [];
3055
3056
  };
3056
- var get = (object, path6) => {
3057
- return path6.reduce((current, nextProperty) => current[nextProperty], object);
3057
+ var get = (object, path7) => {
3058
+ return path7.reduce((current, nextProperty) => current[nextProperty], object);
3058
3059
  };
3059
- var set = (object, path6, mutator) => {
3060
- const lastProperty = path6[path6.length - 1];
3061
- const parentPath = [...path6].slice(0, -1);
3060
+ var set = (object, path7, mutator) => {
3061
+ const lastProperty = path7[path7.length - 1];
3062
+ const parentPath = [...path7].slice(0, -1);
3062
3063
  const parent = get(object, parentPath);
3063
3064
  if (typeof mutator === "function") {
3064
3065
  parent[lastProperty] = mutator(parent[lastProperty]);
@@ -3110,22 +3111,22 @@ var mergeResponses = (response1, response2) => {
3110
3111
  if (Object.keys(response1).length === 0) {
3111
3112
  return Object.assign(response1, response2);
3112
3113
  }
3113
- const path6 = findPaginatedResourcePath(response1);
3114
- const nodesPath = [...path6, "nodes"];
3114
+ const path7 = findPaginatedResourcePath(response1);
3115
+ const nodesPath = [...path7, "nodes"];
3115
3116
  const newNodes = get(response2, nodesPath);
3116
3117
  if (newNodes) {
3117
3118
  set(response1, nodesPath, (values) => {
3118
3119
  return [...values, ...newNodes];
3119
3120
  });
3120
3121
  }
3121
- const edgesPath = [...path6, "edges"];
3122
+ const edgesPath = [...path7, "edges"];
3122
3123
  const newEdges = get(response2, edgesPath);
3123
3124
  if (newEdges) {
3124
3125
  set(response1, edgesPath, (values) => {
3125
3126
  return [...values, ...newEdges];
3126
3127
  });
3127
3128
  }
3128
- const pageInfoPath = [...path6, "pageInfo"];
3129
+ const pageInfoPath = [...path7, "pageInfo"];
3129
3130
  set(response1, pageInfoPath, get(response2, pageInfoPath));
3130
3131
  return response1;
3131
3132
  };
@@ -5546,7 +5547,7 @@ var triggers_notification_paths_default = [
5546
5547
  ];
5547
5548
  function routeMatcher(paths) {
5548
5549
  const regexes = paths.map(
5549
- (path6) => path6.split("/").map((c) => c.startsWith("{") ? "(?:.+?)" : c).join("/")
5550
+ (path7) => path7.split("/").map((c) => c.startsWith("{") ? "(?:.+?)" : c).join("/")
5550
5551
  );
5551
5552
  const regex2 = `^(?:${regexes.map((r) => `(?:${r})`).join("|")})[^/]*$`;
5552
5553
  return new RegExp(regex2, "i");
@@ -8601,13 +8602,40 @@ async function getOwnerRepo(cwd) {
8601
8602
 
8602
8603
  // scripts/util/ci-secrets.ts
8603
8604
  var REQUIRED_CI_SECRETS = ["DATABRICKS_HOST", "LAKEBASE_PROJECT_ID", "DATABRICKS_TOKEN"];
8605
+ var CI_TOKEN_LIFETIME_SECONDS = 7776e3;
8606
+ var CI_TOKEN_REMINT_MARGIN_SECONDS = 86400;
8607
+ function ciTokenComment(ownerRepo) {
8608
+ const repoName = ownerRepo.includes("/") ? ownerRepo.slice(ownerRepo.lastIndexOf("/") + 1) : ownerRepo;
8609
+ return `GitHub Actions (${repoName})`;
8610
+ }
8604
8611
  async function missingCiSecrets(ownerRepo) {
8605
8612
  const present = new Set(await listSecretNames(ownerRepo));
8606
8613
  return REQUIRED_CI_SECRETS.filter((n) => !present.has(n));
8607
8614
  }
8615
+ async function mintCiToken(args) {
8616
+ try {
8617
+ const raw = await runDatabricks(
8618
+ ["tokens", "create", "--comment", args.comment, "--lifetime-seconds", String(args.lifetimeSeconds), "-o", "json"],
8619
+ { host: args.databricksHost, cwd: args.projectDir, timeout: 3e4 }
8620
+ );
8621
+ const parsed = JSON.parse(raw.slice(Math.max(0, raw.indexOf("{"))));
8622
+ const token = parsed.token_value || parsed.token || "";
8623
+ if (token) return token;
8624
+ } catch {
8625
+ }
8626
+ try {
8627
+ const raw = await runDatabricks(["auth", "token", "-o", "json"], {
8628
+ host: args.databricksHost,
8629
+ cwd: args.projectDir,
8630
+ timeout: 3e4
8631
+ });
8632
+ const parsed = JSON.parse(raw.slice(Math.max(0, raw.indexOf("{"))));
8633
+ return parsed.access_token || "";
8634
+ } catch {
8635
+ return "";
8636
+ }
8637
+ }
8608
8638
  async function syncCiSecrets(args) {
8609
- const lifetime = args.lifetimeSeconds ?? 86400;
8610
- const comment = args.comment ?? "GitHub Actions CI";
8611
8639
  const ownerRepo = args.ownerRepo ?? await getOwnerRepo(args.projectDir);
8612
8640
  if (!ownerRepo) {
8613
8641
  throw new Error("Could not resolve GitHub repository from git remote");
@@ -8618,21 +8646,98 @@ async function syncCiSecrets(args) {
8618
8646
  if (!args.lakebaseProjectId) {
8619
8647
  throw new Error("syncCiSecrets: lakebaseProjectId is required");
8620
8648
  }
8649
+ const lifetime = args.lifetimeSeconds ?? CI_TOKEN_LIFETIME_SECONDS;
8650
+ const comment = args.comment ?? ciTokenComment(ownerRepo);
8621
8651
  const secrets = {
8622
8652
  DATABRICKS_HOST: args.databricksHost,
8623
8653
  LAKEBASE_PROJECT_ID: args.lakebaseProjectId
8624
8654
  };
8655
+ const token = await mintCiToken({
8656
+ databricksHost: args.databricksHost,
8657
+ projectDir: args.projectDir,
8658
+ comment,
8659
+ lifetimeSeconds: lifetime
8660
+ });
8661
+ if (token) secrets.DATABRICKS_TOKEN = token;
8662
+ await setRepoSecrets(ownerRepo, secrets);
8663
+ }
8664
+ async function ciTokenExpiry(args) {
8665
+ const comment = ciTokenComment(args.ownerRepo);
8666
+ let raw;
8625
8667
  try {
8626
- const tokenRaw = await runDatabricks(
8627
- ["tokens", "create", "--comment", comment, "--lifetime-seconds", String(lifetime), "-o", "json"],
8628
- { host: args.databricksHost, cwd: args.projectDir, timeout: 3e4 }
8629
- );
8630
- const parsed = JSON.parse(tokenRaw);
8631
- const token = parsed.token_value || parsed.token || "";
8632
- if (token) secrets.DATABRICKS_TOKEN = token;
8668
+ raw = await runDatabricks(["tokens", "list", "-o", "json"], {
8669
+ host: args.databricksHost,
8670
+ cwd: args.projectDir,
8671
+ timeout: 3e4
8672
+ });
8633
8673
  } catch {
8674
+ return null;
8675
+ }
8676
+ let infos;
8677
+ try {
8678
+ const parsed = JSON.parse(raw.slice(Math.max(0, raw.indexOf("["))));
8679
+ infos = Array.isArray(parsed) ? parsed : [];
8680
+ } catch {
8681
+ return null;
8682
+ }
8683
+ const expiries = infos.filter((t) => t.comment === comment).map((t) => t.expiry_time === -1 ? Number.POSITIVE_INFINITY : Number(t.expiry_time)).filter((n) => Number.isFinite(n) || n === Number.POSITIVE_INFINITY);
8684
+ if (expiries.length === 0) return null;
8685
+ return Math.max(...expiries);
8686
+ }
8687
+ async function ensureCiSecretsFresh(args) {
8688
+ const now = args.now ?? Date.now;
8689
+ const marginMs = (args.marginSeconds ?? CI_TOKEN_REMINT_MARGIN_SECONDS) * 1e3;
8690
+ const ownerRepo = args.ownerRepo ?? await getOwnerRepo(args.projectDir);
8691
+ if (!ownerRepo) {
8692
+ throw new Error("Could not resolve GitHub repository from git remote");
8693
+ }
8694
+ const sync = () => syncCiSecrets({
8695
+ projectDir: args.projectDir,
8696
+ databricksHost: args.databricksHost,
8697
+ lakebaseProjectId: args.lakebaseProjectId,
8698
+ ownerRepo
8699
+ });
8700
+ const missing = await missingCiSecrets(ownerRepo);
8701
+ if (missing.length > 0) {
8702
+ await sync();
8703
+ return { action: "provisioned", reason: `CI secret(s) were missing (${missing.join(", ")}); provisioned.` };
8704
+ }
8705
+ const expiry = await ciTokenExpiry({ projectDir: args.projectDir, databricksHost: args.databricksHost, ownerRepo });
8706
+ if (expiry === null) {
8707
+ await sync();
8708
+ return { action: "reminted", reason: "no live CI token found (expired or minted under a different identity); re-minted." };
8709
+ }
8710
+ if (expiry !== Number.POSITIVE_INFINITY && expiry < now() + marginMs) {
8711
+ await sync();
8712
+ const hrs = Math.max(0, Math.round((expiry - now()) / 36e5));
8713
+ return { action: "reminted", reason: `CI token expires in ~${hrs}h (within the re-mint margin); re-minted.` };
8714
+ }
8715
+ return { action: "ok", reason: "CI token is current." };
8716
+ }
8717
+ async function ensureCiSecretsFreshFromEnv(projectDir, opts) {
8718
+ const envPath = path6.join(projectDir, ".env");
8719
+ const databricksHost = readEnvVar(envPath, "DATABRICKS_HOST");
8720
+ const lakebaseProjectId = readEnvVar(envPath, "LAKEBASE_PROJECT_ID");
8721
+ if (!databricksHost || !lakebaseProjectId) {
8722
+ return {
8723
+ action: "skipped",
8724
+ reason: "CI-auth preflight skipped: .env is missing DATABRICKS_HOST / LAKEBASE_PROJECT_ID."
8725
+ };
8726
+ }
8727
+ try {
8728
+ return await ensureCiSecretsFresh({
8729
+ projectDir,
8730
+ databricksHost,
8731
+ lakebaseProjectId,
8732
+ ownerRepo: opts?.ownerRepo,
8733
+ marginSeconds: opts?.marginSeconds
8734
+ });
8735
+ } catch (err) {
8736
+ return {
8737
+ action: "failed",
8738
+ reason: `CI-auth preflight could not re-mint (${err instanceof Error ? err.message : String(err)}); if CI fails on auth, run lakebase-sync-ci-secrets.`
8739
+ };
8634
8740
  }
8635
- await setRepoSecrets(ownerRepo, secrets);
8636
8741
  }
8637
8742
 
8638
8743
  // scripts/util/cli-entry.ts
@@ -8740,11 +8845,17 @@ async function pollUntilDefined(probe, opts) {
8740
8845
  });
8741
8846
  }
8742
8847
  export {
8848
+ CI_TOKEN_LIFETIME_SECONDS,
8849
+ CI_TOKEN_REMINT_MARGIN_SECONDS,
8743
8850
  LAKEBASE_BRANCH_NAME_MAX,
8744
8851
  PROXY_ENV_KEYS,
8745
8852
  REQUIRED_CI_SECRETS,
8853
+ ciTokenComment,
8854
+ ciTokenExpiry,
8746
8855
  copyDirSubstituted,
8747
8856
  delay,
8857
+ ensureCiSecretsFresh,
8858
+ ensureCiSecretsFreshFromEnv,
8748
8859
  exec2 as exec,
8749
8860
  extractZipToDir,
8750
8861
  formatOwnerRepo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databricks-solutions/lakebase-scm-utils",
3
- "version": "0.2.48",
3
+ "version": "0.2.49",
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,
@@ -429,12 +429,12 @@ export async function createProject(
429
429
  if (useGithub) {
430
430
  report("Setting up CI auth (service principal)...");
431
431
  try {
432
+ // Canonical CI-token identity (durable 90-day PAT, project-scoped comment) — defaults
433
+ // live in ci-secrets.ts so every mint path (create, repair, pre-push, preflight) matches.
432
434
  await syncCiSecrets({
433
435
  projectDir,
434
436
  databricksHost: host,
435
437
  lakebaseProjectId,
436
- comment: "GitHub Actions CI",
437
- lifetimeSeconds: 86_400,
438
438
  ownerRepo: fullRepoName,
439
439
  });
440
440
  // VERIFY the secrets actually landed — syncCiSecrets fail-softs a dropped PAT, and a
@@ -18,6 +18,7 @@ import {
18
18
  type WorkflowRunSummary,
19
19
  } from "../github/pr.js";
20
20
  import { getOwnerRepo } from "../git/remote.js";
21
+ import { ensureCiSecretsFreshFromEnv, type CiFreshnessResult } from "../util/ci-secrets.js";
21
22
  import { pollUntil } from "../util/poll-until.js";
22
23
  import { exec } from "../util/exec.js";
23
24
  import { getCurrentBranch } from "../git/inspect.js";
@@ -175,6 +176,11 @@ export interface MergeArgs {
175
176
  /** Fetch the PR's mergeability (to gate the reconcile). Defaults to the real getPullRequest;
176
177
  * injected in tests so the gate is exercised without the network. */
177
178
  fetchPrState?: (ownerRepo: string, headBranch: string) => Promise<{ mergeableState?: string } | undefined>;
179
+ /** Expiry-aware CI-auth preflight run before the merge: the promote triggers merge.yml, which
180
+ * needs a live DATABRICKS_TOKEN secret, and a merge is not a push so the pre-push hook does not
181
+ * fire. Re-mints the canonical token when missing / near expiry. BEST-EFFORT: never blocks the
182
+ * merge. Defaults to {@link ensureCiSecretsFreshFromEnv}; injected in tests. */
183
+ ensureCiFresh?: (ownerRepo: string) => Promise<CiFreshnessResult>;
178
184
  }
179
185
 
180
186
  export interface MergeResult {
@@ -450,6 +456,21 @@ export async function mergeFeature(args: MergeArgs): Promise<MergeResult> {
450
456
  }
451
457
  }
452
458
 
459
+ // ─── Pre-merge CI-auth freshness preflight ───
460
+ // The promote merges to the tier branch, which triggers merge.yml (migrate-target) — that needs
461
+ // a live DATABRICKS_TOKEN repo secret. A merge is not a push, so the pre-push hook does NOT
462
+ // refresh it here; if the token has expired (or was never provisioned) the promote CI fails on
463
+ // empty auth. Re-mint the canonical token when missing / near expiry. BEST-EFFORT: never blocks.
464
+ {
465
+ const ensure = args.ensureCiFresh ?? ((repo: string) => ensureCiSecretsFreshFromEnv(args.projectDir, { ownerRepo: repo }));
466
+ try {
467
+ const r = await ensure(ownerRepo);
468
+ if (r.action !== "ok" && r.action !== "skipped") preflightNotes.push(`CI-auth preflight: ${r.reason}`);
469
+ } catch (err) {
470
+ preflightNotes.push(`CI-auth preflight skipped (${err instanceof Error ? err.message : String(err)}); if the promote CI fails on auth, run lakebase-sync-ci-secrets.`);
471
+ }
472
+ }
473
+
453
474
  // ─── Pre-merge pipeline.json-ledger reconcile ───
454
475
  // The per-feature pipeline.json ledger is TRACKED (durable run-state) and can diverge between the
455
476
  // feature branch and its tier, so the promotion PR lands CONFLICTING — GitHub won't dispatch CI or
@@ -9,6 +9,7 @@ import { exec } from "../util/exec.js";
9
9
  import { getCurrentBranch } from "../git/inspect.js";
10
10
  import { getAheadBehind, isDirty } from "../git/status.js";
11
11
  import { getOwnerRepo } from "../git/remote.js";
12
+ import { ensureCiSecretsFreshFromEnv, type CiFreshnessResult } from "../util/ci-secrets.js";
12
13
  import { resolveGitBase } from "./scm-git-base.js";
13
14
  import { createPullRequest, getPullRequest } from "../github/pr.js";
14
15
  import { RUNTIME_ARTIFACT_IGNORE } from "./constants.js";
@@ -63,6 +64,8 @@ export interface PreparePrResult {
63
64
  prUrl: string;
64
65
  /** True iff createPullRequest was invoked (vs. reusing an existing open PR). */
65
66
  prCreated: boolean;
67
+ /** Outcome of the expiry-aware CI-auth preflight run before the push (fail-soft). */
68
+ ciFreshness: CiFreshnessResult;
66
69
  }
67
70
 
68
71
  export async function preparePr(
@@ -138,6 +141,12 @@ export async function preparePr(
138
141
  );
139
142
  }
140
143
 
144
+ // Expiry-aware CI-auth preflight BEFORE the push: opening/updating the PR triggers
145
+ // build-and-test, which needs a live DATABRICKS_TOKEN secret. Re-mint the canonical token
146
+ // when it is missing or near expiry so the run isn't DOA. Fail-soft — never blocks the PR
147
+ // (the push's pre-push hook also refreshes; this is the belt-and-suspenders guarantee).
148
+ const ciFreshness = await ensureCiSecretsFreshFromEnv(args.projectDir, { ownerRepo });
149
+
141
150
  const now = (args.now ?? (() => new Date()))();
142
151
  let prUrl = args.prUrlOverride ?? "";
143
152
  let prCreated = false;
@@ -191,7 +200,7 @@ export async function preparePr(
191
200
  };
192
201
  writeWorkflowState(args.projectDir, next);
193
202
 
194
- return { state: next, prUrl, prCreated };
203
+ return { state: next, prUrl, prCreated, ciFreshness };
195
204
  }
196
205
 
197
206
  async function ensureAheadOfParent(
@@ -57,8 +57,8 @@ Flags:
57
57
  --host <url> DATABRICKS_HOST (default: .env DATABRICKS_HOST)
58
58
  --project-id <id> LAKEBASE_PROJECT_ID (default: .env LAKEBASE_PROJECT_ID)
59
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")
60
+ --lifetime-sec <n> CI PAT lifetime (default: 7776000 = 90d, the canonical durable lifetime)
61
+ --comment <text> Token comment (default: canonical "GitHub Actions (<repo>)")
62
62
  -h, --help Show this help
63
63
 
64
64
  Exit codes: