@databricks-solutions/lakebase-scm-utils 0.2.46 → 0.2.48
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/{pr-D3CteJaf.d.cts → pr-e-E4aP4j.d.cts} +6 -0
- package/dist/{pr-D3CteJaf.d.ts → pr-e-E4aP4j.d.ts} +6 -0
- package/dist/scripts/github/index.cjs +2 -0
- package/dist/scripts/github/index.d.cts +1 -1
- package/dist/scripts/github/index.d.ts +1 -1
- package/dist/scripts/github/index.js +2 -0
- package/dist/scripts/github/pr.cli.cjs +2 -0
- package/dist/scripts/github/pr.cli.js +2 -0
- package/dist/scripts/index.cjs +81 -4
- package/dist/scripts/index.d.cts +3 -3
- package/dist/scripts/index.d.ts +3 -3
- package/dist/scripts/index.js +78 -4
- 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 +2 -2
- package/dist/scripts/lakebase/doctor.cli.js +2 -2
- 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 +87 -4
- package/dist/scripts/lakebase/index.d.cts +32 -14
- package/dist/scripts/lakebase/index.d.ts +32 -14
- package/dist/scripts/lakebase/index.js +86 -4
- 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 +131 -2
- package/dist/scripts/lakebase/scm-merge.cli.js +131 -2
- package/dist/scripts/lakebase/scm-prepare-pr.cli.cjs +2 -0
- package/dist/scripts/lakebase/scm-prepare-pr.cli.js +2 -0
- 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/lakebase/scm-wait-ci.cli.cjs +8 -0
- package/dist/scripts/lakebase/scm-wait-ci.cli.js +8 -0
- package/dist/scripts/lakebase/sync-ci-secrets.cli.cjs +8610 -0
- package/dist/scripts/lakebase/sync-ci-secrets.cli.d.cts +4 -0
- package/dist/scripts/lakebase/sync-ci-secrets.cli.d.ts +4 -0
- package/dist/scripts/lakebase/sync-ci-secrets.cli.js +8597 -0
- package/dist/scripts/util/index.cjs +21 -2
- package/dist/scripts/util/index.d.cts +9 -1
- package/dist/scripts/util/index.d.ts +9 -1
- package/dist/scripts/util/index.js +19 -2
- package/package.json +3 -2
- package/scripts/github/pr.ts +8 -0
- package/scripts/lakebase/create-project.ts +14 -3
- package/scripts/lakebase/scm-merge.ts +91 -0
- package/scripts/lakebase/scm-wait-ci.ts +13 -0
- package/scripts/lakebase/sync-ci-secrets.cli.ts +122 -0
- package/scripts/util/ci-secrets.ts +14 -1
- package/templates/project/common/.gitignore.base +4 -0
- 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.
|
|
1663
|
-
cached = "0.2.
|
|
1664
|
+
if ("0.2.48".length > 0) {
|
|
1665
|
+
cached = "0.2.48";
|
|
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.
|
|
1634
|
-
cached = "0.2.
|
|
1633
|
+
if ("0.2.48".length > 0) {
|
|
1634
|
+
cached = "0.2.48";
|
|
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.
|
|
3
|
+
"version": "0.2.48",
|
|
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",
|
package/scripts/github/pr.ts
CHANGED
|
@@ -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
|
-
|
|
443
|
-
|
|
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
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// stable surface for that signal.
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
+
getPullRequest,
|
|
15
16
|
listWorkflowRuns,
|
|
16
17
|
mergePairedPullRequest,
|
|
17
18
|
type WorkflowRunSummary,
|
|
@@ -68,6 +69,7 @@ export class ScmMergeError extends Error {
|
|
|
68
69
|
| "no-pr-url"
|
|
69
70
|
| "bad-pr-url"
|
|
70
71
|
| "merge-failed"
|
|
72
|
+
| "promote-conflict"
|
|
71
73
|
| "migrate-failed"
|
|
72
74
|
| "migrate-timeout"
|
|
73
75
|
| "migrate-auth",
|
|
@@ -166,6 +168,13 @@ export interface MergeArgs {
|
|
|
166
168
|
* implementation ({@link defaultRefreshPromoteWorkflows}); injected in tests.
|
|
167
169
|
*/
|
|
168
170
|
refreshWorkflows?: () => Promise<{ refreshed: boolean; detail?: string }>;
|
|
171
|
+
/** Pre-merge reconcile of a pipeline.json-ledger promotion conflict to the run side (keeps it
|
|
172
|
+
* tracked). Called ONLY when the PR is CONFLICTING. Defaults to {@link reconcilePromoteRunState};
|
|
173
|
+
* injected in tests. */
|
|
174
|
+
reconcileRunState?: (baseBranch: string) => Promise<{ reconciled: boolean; detail?: string }>;
|
|
175
|
+
/** Fetch the PR's mergeability (to gate the reconcile). Defaults to the real getPullRequest;
|
|
176
|
+
* injected in tests so the gate is exercised without the network. */
|
|
177
|
+
fetchPrState?: (ownerRepo: string, headBranch: string) => Promise<{ mergeableState?: string } | undefined>;
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
export interface MergeResult {
|
|
@@ -248,6 +257,71 @@ function shaMigratePredicate(
|
|
|
248
257
|
* working-tree changes. Callers invoke it BEST-EFFORT — a failure here must never
|
|
249
258
|
* block the merge (the local migrate fallback still keeps git + schema in sync).
|
|
250
259
|
*/
|
|
260
|
+
/** The ONLY paths a promotion merge may auto-resolve to the RUN (feature/head) side: the per-story
|
|
261
|
+
* pipeline.json ledger (per-feature, and the legacy top-level pointer). It is the feature branch's
|
|
262
|
+
* own durable bookkeeping — the tier has no authoritative copy — so on a promotion conflict the
|
|
263
|
+
* head side always wins. Everything else is real divergence and must surface, never auto-resolve. */
|
|
264
|
+
const PROMOTE_RUNSTATE_CONFLICT = /(^|\/)\.consort\/(features\/[^/]+\/)?pipeline\.json$/;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Clear a promotion-merge conflict that is ONLY the pipeline.json ledger, so GitHub can dispatch CI
|
|
268
|
+
* and merge — WITHOUT untracking the ledger (it's durable run-state that must persist). Merges the
|
|
269
|
+
* base into the PR head and resolves the ledger path(s) to the head (run) side, then pushes. This is
|
|
270
|
+
* the merge-layer fix for the "pipeline.json conflict blocks promotion" defect, replacing the wrong
|
|
271
|
+
* earlier approach of gitignoring/untracking it. Behavior:
|
|
272
|
+
* - a dirty tree under `.consort/` only is checkpointed first (the run's own bookkeeping);
|
|
273
|
+
* - a dirty tree OUTSIDE `.consort/` aborts (never auto-commit app code);
|
|
274
|
+
* - a conflict on any NON-ledger path aborts the merge + surfaces (real divergence isn't resolved);
|
|
275
|
+
* - a clean merge (no conflict) is a no-op (reconciled:false).
|
|
276
|
+
* Only call this when the PR is actually CONFLICTING — a clean promote needs no merge commit.
|
|
277
|
+
*/
|
|
278
|
+
export async function reconcilePromoteRunState(
|
|
279
|
+
projectDir: string,
|
|
280
|
+
baseBranch: string,
|
|
281
|
+
): Promise<{ reconciled: boolean; detail?: string }> {
|
|
282
|
+
const o = { cwd: projectDir, timeout: 30_000 };
|
|
283
|
+
// Working tree must be clean to merge. Checkpoint run-state (.consort/) if that's all that's dirty;
|
|
284
|
+
// refuse on anything else so we never auto-commit app code during a promotion.
|
|
285
|
+
const dirty = (await exec("git status --porcelain", o).catch(() => "")).split("\n").map((l) => l.slice(3).trim()).filter(Boolean);
|
|
286
|
+
const nonRunStateDirty = dirty.filter((p) => !p.startsWith(".consort/"));
|
|
287
|
+
if (nonRunStateDirty.length > 0) {
|
|
288
|
+
throw new ScmMergeError(
|
|
289
|
+
`working tree has uncommitted non-run-state changes (${nonRunStateDirty.slice(0, 5).join(", ")}${nonRunStateDirty.length > 5 ? ", …" : ""}); commit or stash before promoting.`,
|
|
290
|
+
"promote-conflict",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (dirty.length > 0) {
|
|
294
|
+
await exec(`git add ${shellEscape(".consort")}`, o);
|
|
295
|
+
await exec(`git commit -m ${shellEscape("chore(run): checkpoint run state before promotion")}`, o);
|
|
296
|
+
}
|
|
297
|
+
await exec(`git fetch origin ${shellEscape(baseBranch)}`, o).catch(() => {});
|
|
298
|
+
let conflicted = false;
|
|
299
|
+
try {
|
|
300
|
+
await exec(`git merge --no-ff -m ${shellEscape(`merge ${baseBranch} for promotion`)} ${shellEscape(`origin/${baseBranch}`)}`, o);
|
|
301
|
+
} catch {
|
|
302
|
+
conflicted = true;
|
|
303
|
+
}
|
|
304
|
+
if (!conflicted) return { reconciled: false }; // clean merge (or already up to date) — nothing to resolve
|
|
305
|
+
const u = (await exec("git diff --name-only --diff-filter=U", o).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
306
|
+
const other = u.filter((p) => !PROMOTE_RUNSTATE_CONFLICT.test(p));
|
|
307
|
+
if (u.length === 0 || other.length > 0) {
|
|
308
|
+
await exec("git merge --abort", o).catch(() => {});
|
|
309
|
+
throw new ScmMergeError(
|
|
310
|
+
other.length > 0
|
|
311
|
+
? `promotion conflicts on non-ledger file(s): ${other.join(", ")}. Resolve the real divergence manually, then re-run the promote.`
|
|
312
|
+
: `promotion merge failed with no resolvable conflicts; resolve manually and re-run.`,
|
|
313
|
+
"promote-conflict",
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
for (const p of u) {
|
|
317
|
+
await exec(`git checkout --ours -- ${shellEscape(p)}`, o); // keep the run (feature) ledger
|
|
318
|
+
await exec(`git add -- ${shellEscape(p)}`, o);
|
|
319
|
+
}
|
|
320
|
+
await exec(`git commit -m ${shellEscape(`merge ${baseBranch} for promotion (pipeline.json ledger kept at run state)`)}`, o);
|
|
321
|
+
await pushCurrentBranchForPr({ cwd: projectDir });
|
|
322
|
+
return { reconciled: true, detail: `reconciled ${u.length} ledger path(s) to the run side + pushed` };
|
|
323
|
+
}
|
|
324
|
+
|
|
251
325
|
export async function defaultRefreshPromoteWorkflows(
|
|
252
326
|
projectDir: string,
|
|
253
327
|
): Promise<{ refreshed: boolean; detail?: string }> {
|
|
@@ -376,6 +450,23 @@ export async function mergeFeature(args: MergeArgs): Promise<MergeResult> {
|
|
|
376
450
|
}
|
|
377
451
|
}
|
|
378
452
|
|
|
453
|
+
// ─── Pre-merge pipeline.json-ledger reconcile ───
|
|
454
|
+
// The per-feature pipeline.json ledger is TRACKED (durable run-state) and can diverge between the
|
|
455
|
+
// feature branch and its tier, so the promotion PR lands CONFLICTING — GitHub won't dispatch CI or
|
|
456
|
+
// merge. When that happens, reconcile the ledger to the run (head) side at the MERGE layer and push,
|
|
457
|
+
// so the PR goes mergeable WITHOUT untracking the ledger. Only runs when the PR is actually dirty;
|
|
458
|
+
// a non-ledger conflict throws (real divergence surfaces). This is the correct replacement for the
|
|
459
|
+
// withdrawn gitignore/untrack approach.
|
|
460
|
+
{
|
|
461
|
+
const fetchState = args.fetchPrState ?? ((o, h) => getPullRequest(o, h));
|
|
462
|
+
const prState = await fetchState(ownerRepo, current.branch).catch(() => undefined);
|
|
463
|
+
if (prState?.mergeableState === "dirty") {
|
|
464
|
+
const reconcile = args.reconcileRunState ?? ((base: string) => reconcilePromoteRunState(args.projectDir, base));
|
|
465
|
+
const r = await reconcile(gitBase); // throws ScmMergeError on a non-ledger conflict (surfaces)
|
|
466
|
+
if (r.reconciled) preflightNotes.push(`Reconciled a pipeline.json-ledger promotion conflict to the run side${r.detail ? ` (${r.detail})` : ""}.`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
379
470
|
let paired: Awaited<ReturnType<typeof mergePairedPullRequest>>;
|
|
380
471
|
try {
|
|
381
472
|
paired = await mergePairedPullRequest({
|
|
@@ -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). */
|
|
@@ -30,6 +30,10 @@ application-local.properties
|
|
|
30
30
|
.consort/cycles/
|
|
31
31
|
.consort/experiments/
|
|
32
32
|
.consort/pipeline.json
|
|
33
|
+
# NOTE: the PER-FEATURE ledger .consort/features/<id>/pipeline.json is deliberately NOT ignored —
|
|
34
|
+
# it is the durable per-story pipeline state (persisted on the feature branch so it survives across
|
|
35
|
+
# sessions). Ignoring it lost the ledger. A promotion-merge conflict on it is resolved at the MERGE
|
|
36
|
+
# layer (scm-merge reconciles it to the run side), never by untracking it.
|
|
33
37
|
# The turn recorder's per-run observability: its internal file-hash map
|
|
34
38
|
# (.recorder-state.json, used for delta computation), the routing-decision
|
|
35
39
|
# 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
|
-
|
|
310
|
-
|
|
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
|
|
321
|
-
[ "$krc" -eq 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"
|