@databricks-solutions/lakebase-scm-utils 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scripts/index.cjs +40 -21
- package/dist/scripts/index.d.cts +1 -1
- package/dist/scripts/index.d.ts +1 -1
- package/dist/scripts/index.js +39 -21
- 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 +40 -21
- package/dist/scripts/lakebase/index.d.cts +13 -4
- package/dist/scripts/lakebase/index.d.ts +13 -4
- package/dist/scripts/lakebase/index.js +39 -21
- 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-abandon-feature.cli.cjs +37 -22
- package/dist/scripts/lakebase/scm-abandon-feature.cli.js +34 -22
- 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 +36 -21
- package/dist/scripts/lakebase/scm-doctor.cli.js +36 -21
- package/dist/scripts/lakebase/scm-merge.cli.cjs +2 -2
- package/dist/scripts/lakebase/scm-merge.cli.js +2 -2
- package/dist/scripts/lakebase/scm-park-feature.cli.cjs +586 -0
- package/dist/scripts/lakebase/scm-park-feature.cli.d.cts +4 -0
- package/dist/scripts/lakebase/scm-park-feature.cli.d.ts +4 -0
- package/dist/scripts/lakebase/scm-park-feature.cli.js +546 -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/util/index.cjs +2 -2
- package/dist/scripts/util/index.js +2 -2
- package/package.json +2 -1
- package/scripts/lakebase/scm-abandon-feature.cli.ts +3 -3
- package/scripts/lakebase/scm-abandon-feature.ts +19 -83
- package/scripts/lakebase/scm-park-feature.cli.ts +150 -0
- package/scripts/lakebase/scm-park-feature.ts +71 -0
- package/scripts/lakebase/scm-release-claim.ts +146 -0
- package/templates/project/common/consort-capture.ts +121 -3
- package/templates/project/python/tests/__pycache__/conftest.cpython-314.pyc +0 -0
- package/templates/project/python/tests/conftest.py +90 -1
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// scripts/lakebase/scm-park-feature.cli.ts
|
|
32
|
+
var scm_park_feature_cli_exports = {};
|
|
33
|
+
__export(scm_park_feature_cli_exports, {
|
|
34
|
+
runScmParkFeatureCli: () => runScmParkFeatureCli
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(scm_park_feature_cli_exports);
|
|
37
|
+
|
|
38
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
39
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
40
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
41
|
+
|
|
42
|
+
// scripts/lakebase/scm-park-feature.cli.ts
|
|
43
|
+
var path2 = __toESM(require("path"), 1);
|
|
44
|
+
|
|
45
|
+
// scripts/util/cli-entry.ts
|
|
46
|
+
var import_node_fs = require("fs");
|
|
47
|
+
var import_node_url = require("url");
|
|
48
|
+
function isCliEntry(importMetaUrl2) {
|
|
49
|
+
const invokedRaw = process.argv[1];
|
|
50
|
+
if (!invokedRaw) return false;
|
|
51
|
+
let invokedResolved;
|
|
52
|
+
let moduleResolved;
|
|
53
|
+
try {
|
|
54
|
+
invokedResolved = (0, import_node_fs.realpathSync)(invokedRaw);
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
moduleResolved = (0, import_node_fs.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl2));
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return invokedResolved === moduleResolved;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// scripts/util/exec.ts
|
|
67
|
+
var cp = __toESM(require("child_process"), 1);
|
|
68
|
+
function shq(s) {
|
|
69
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
70
|
+
}
|
|
71
|
+
function exec2(command, opts = {}) {
|
|
72
|
+
return new Promise((resolve2, reject) => {
|
|
73
|
+
const options = {
|
|
74
|
+
cwd: opts.cwd,
|
|
75
|
+
timeout: opts.timeout ?? 6e4
|
|
76
|
+
};
|
|
77
|
+
if (opts.env) {
|
|
78
|
+
options.env = { ...process.env, ...opts.env };
|
|
79
|
+
}
|
|
80
|
+
cp.exec(command, options, (err, stdout, stderr) => {
|
|
81
|
+
if (err) {
|
|
82
|
+
const msg = String(stderr || err.message);
|
|
83
|
+
reject(new Error(`${command}: ${msg}`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
resolve2(String(stdout).trim());
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// scripts/git/inspect.ts
|
|
92
|
+
async function getCurrentBranch(args) {
|
|
93
|
+
try {
|
|
94
|
+
const name = await exec2("git rev-parse --abbrev-ref HEAD", {
|
|
95
|
+
cwd: args.cwd
|
|
96
|
+
});
|
|
97
|
+
return name === "HEAD" ? "" : name;
|
|
98
|
+
} catch {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// scripts/git/status.ts
|
|
104
|
+
async function isDirty(args) {
|
|
105
|
+
try {
|
|
106
|
+
const ignore = args.ignore ?? [];
|
|
107
|
+
const untrackedFlag = args.untracked === false ? " --untracked-files=no" : "";
|
|
108
|
+
let command = `git status --porcelain${untrackedFlag}`;
|
|
109
|
+
if (ignore.length > 0) {
|
|
110
|
+
const excludes = ignore.map((p) => shq(`:(exclude)${p.replace(/\/+$/, "")}`)).join(" ");
|
|
111
|
+
command = `git status --porcelain${untrackedFlag} -- . ${excludes}`;
|
|
112
|
+
}
|
|
113
|
+
const out = await exec2(command, { cwd: args.cwd });
|
|
114
|
+
return out.trim().length > 0;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// scripts/lakebase/scm-workflow-state.ts
|
|
121
|
+
var fs = __toESM(require("fs"), 1);
|
|
122
|
+
var path = __toESM(require("path"), 1);
|
|
123
|
+
var import_node_child_process = require("child_process");
|
|
124
|
+
function isGitTracked(projectDir, rel) {
|
|
125
|
+
try {
|
|
126
|
+
(0, import_node_child_process.execFileSync)("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function ensureWorkflowStateUntracked(projectDir) {
|
|
133
|
+
const rel = ".lakebase/workflow-state.json";
|
|
134
|
+
try {
|
|
135
|
+
if (isGitTracked(projectDir, rel)) {
|
|
136
|
+
(0, import_node_child_process.execFileSync)("git", ["rm", "--cached", "--quiet", "--ignore-unmatch", "--", rel], { cwd: projectDir, stdio: "ignore" });
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const gitignore = path.join(projectDir, ".gitignore");
|
|
142
|
+
const existing = fs.existsSync(gitignore) ? fs.readFileSync(gitignore, "utf8") : "";
|
|
143
|
+
if (!existing.split("\n").some((l) => l.trim() === rel)) {
|
|
144
|
+
const sep = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
145
|
+
fs.appendFileSync(
|
|
146
|
+
gitignore,
|
|
147
|
+
`${sep}# Runtime SCM claim state (per working tree): a branch checkout must never restore
|
|
148
|
+
# a stale committed claim over the live one (issue #203 / Finding 28).
|
|
149
|
+
${rel}
|
|
150
|
+
`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
var SCM_STATES = [
|
|
157
|
+
"scaffold-complete",
|
|
158
|
+
"feature-claimed",
|
|
159
|
+
"pr-ready",
|
|
160
|
+
"ci-green",
|
|
161
|
+
"merged"
|
|
162
|
+
];
|
|
163
|
+
var STATE_INDEX = SCM_STATES.reduce(
|
|
164
|
+
(acc, s, i) => ({ ...acc, [s]: i }),
|
|
165
|
+
{}
|
|
166
|
+
);
|
|
167
|
+
var STATE_FILE_REL = ".lakebase/workflow-state.json";
|
|
168
|
+
function stateFilePath(projectDir) {
|
|
169
|
+
return path.join(projectDir, STATE_FILE_REL);
|
|
170
|
+
}
|
|
171
|
+
function readWorkflowState(projectDir) {
|
|
172
|
+
const p = stateFilePath(projectDir);
|
|
173
|
+
if (!fs.existsSync(p)) return null;
|
|
174
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
175
|
+
let parsed;
|
|
176
|
+
try {
|
|
177
|
+
parsed = JSON.parse(raw);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Failed to parse ${STATE_FILE_REL}: ${e.message}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const result = validateWorkflowState(parsed);
|
|
184
|
+
if (!result.ok) {
|
|
185
|
+
const summary = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Invalid ${STATE_FILE_REL}:
|
|
188
|
+
${summary}
|
|
189
|
+
|
|
190
|
+
Fix the file or delete it to re-init.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return result.value;
|
|
194
|
+
}
|
|
195
|
+
function writeWorkflowState(projectDir, state) {
|
|
196
|
+
const result = validateWorkflowState(state);
|
|
197
|
+
if (!result.ok) {
|
|
198
|
+
const summary = result.errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
|
|
199
|
+
throw new Error(`Refusing to write invalid SCM state:
|
|
200
|
+
${summary}`);
|
|
201
|
+
}
|
|
202
|
+
ensureWorkflowStateUntracked(projectDir);
|
|
203
|
+
const dir = path.join(projectDir, ".lakebase");
|
|
204
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
205
|
+
const target = stateFilePath(projectDir);
|
|
206
|
+
const tmp = `${target}.tmp`;
|
|
207
|
+
const ordered = orderForOutput(result.value);
|
|
208
|
+
fs.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}
|
|
209
|
+
`, "utf8");
|
|
210
|
+
fs.renameSync(tmp, target);
|
|
211
|
+
}
|
|
212
|
+
function validateWorkflowState(value) {
|
|
213
|
+
const errors = [];
|
|
214
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
errors: [{ path: "$", message: "must be an object" }]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const v = value;
|
|
221
|
+
if (v.version !== 1) {
|
|
222
|
+
errors.push({ path: "version", message: `must be 1, got ${String(v.version)}` });
|
|
223
|
+
}
|
|
224
|
+
if (typeof v.state !== "string" || !SCM_STATES.includes(v.state)) {
|
|
225
|
+
errors.push({
|
|
226
|
+
path: "state",
|
|
227
|
+
message: `must be one of ${SCM_STATES.join(" | ")}`
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (v.tier_topology !== 1 && v.tier_topology !== 2 && v.tier_topology !== 3) {
|
|
231
|
+
errors.push({
|
|
232
|
+
path: "tier_topology",
|
|
233
|
+
message: "must be 1, 2, or 3"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (typeof v.project_id !== "string" || v.project_id.length === 0) {
|
|
237
|
+
errors.push({
|
|
238
|
+
path: "project_id",
|
|
239
|
+
message: "must be a non-empty string"
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const stringFields = [
|
|
243
|
+
"feature_id",
|
|
244
|
+
"branch",
|
|
245
|
+
"parent_branch",
|
|
246
|
+
"lakebase_branch_uid",
|
|
247
|
+
"claimed_at",
|
|
248
|
+
"pr_url",
|
|
249
|
+
"pushed_at",
|
|
250
|
+
"ci_run_url",
|
|
251
|
+
"ci_green_at",
|
|
252
|
+
"merged_at",
|
|
253
|
+
"migrate_run_url",
|
|
254
|
+
"migrate_completed_at",
|
|
255
|
+
"$schema"
|
|
256
|
+
];
|
|
257
|
+
for (const key of stringFields) {
|
|
258
|
+
if (v[key] === void 0) continue;
|
|
259
|
+
if (typeof v[key] !== "string" || v[key].length === 0) {
|
|
260
|
+
errors.push({
|
|
261
|
+
path: key,
|
|
262
|
+
message: "must be a non-empty string when present"
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const requiredForState = {
|
|
267
|
+
"scaffold-complete": [],
|
|
268
|
+
"feature-claimed": [
|
|
269
|
+
"feature_id",
|
|
270
|
+
"branch",
|
|
271
|
+
"parent_branch",
|
|
272
|
+
"lakebase_branch_uid",
|
|
273
|
+
"claimed_at"
|
|
274
|
+
],
|
|
275
|
+
"pr-ready": [
|
|
276
|
+
"feature_id",
|
|
277
|
+
"branch",
|
|
278
|
+
"parent_branch",
|
|
279
|
+
"lakebase_branch_uid",
|
|
280
|
+
"claimed_at",
|
|
281
|
+
"pr_url",
|
|
282
|
+
"pushed_at"
|
|
283
|
+
],
|
|
284
|
+
"ci-green": [
|
|
285
|
+
"feature_id",
|
|
286
|
+
"branch",
|
|
287
|
+
"parent_branch",
|
|
288
|
+
"lakebase_branch_uid",
|
|
289
|
+
"claimed_at",
|
|
290
|
+
"pr_url",
|
|
291
|
+
"pushed_at",
|
|
292
|
+
"ci_run_url",
|
|
293
|
+
"ci_green_at"
|
|
294
|
+
],
|
|
295
|
+
merged: [
|
|
296
|
+
"feature_id",
|
|
297
|
+
"branch",
|
|
298
|
+
"parent_branch",
|
|
299
|
+
"lakebase_branch_uid",
|
|
300
|
+
"claimed_at",
|
|
301
|
+
"pr_url",
|
|
302
|
+
"pushed_at",
|
|
303
|
+
"ci_run_url",
|
|
304
|
+
"ci_green_at",
|
|
305
|
+
"merged_at"
|
|
306
|
+
]
|
|
307
|
+
};
|
|
308
|
+
if (typeof v.state === "string" && SCM_STATES.includes(v.state)) {
|
|
309
|
+
for (const key of requiredForState[v.state]) {
|
|
310
|
+
if (v[key] === void 0) {
|
|
311
|
+
errors.push({
|
|
312
|
+
path: key,
|
|
313
|
+
message: `required when state is "${v.state}"`
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const allowedKeys = /* @__PURE__ */ new Set([
|
|
319
|
+
"$schema",
|
|
320
|
+
"version",
|
|
321
|
+
"state",
|
|
322
|
+
"tier_topology",
|
|
323
|
+
"project_id",
|
|
324
|
+
"feature_id",
|
|
325
|
+
"branch",
|
|
326
|
+
"parent_branch",
|
|
327
|
+
"lakebase_branch_uid",
|
|
328
|
+
"claimed_at",
|
|
329
|
+
"pr_url",
|
|
330
|
+
"pushed_at",
|
|
331
|
+
"ci_run_url",
|
|
332
|
+
"ci_green_at",
|
|
333
|
+
"merged_at",
|
|
334
|
+
"migrate_run_url",
|
|
335
|
+
"migrate_completed_at"
|
|
336
|
+
]);
|
|
337
|
+
for (const key of Object.keys(v)) {
|
|
338
|
+
if (!allowedKeys.has(key)) {
|
|
339
|
+
errors.push({ path: key, message: "unknown property" });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
343
|
+
return { ok: true, value: v };
|
|
344
|
+
}
|
|
345
|
+
function orderForOutput(state) {
|
|
346
|
+
const keyOrder = [
|
|
347
|
+
"$schema",
|
|
348
|
+
"version",
|
|
349
|
+
"state",
|
|
350
|
+
"tier_topology",
|
|
351
|
+
"project_id",
|
|
352
|
+
"feature_id",
|
|
353
|
+
"branch",
|
|
354
|
+
"parent_branch",
|
|
355
|
+
"lakebase_branch_uid",
|
|
356
|
+
"claimed_at",
|
|
357
|
+
"pr_url",
|
|
358
|
+
"pushed_at",
|
|
359
|
+
"ci_run_url",
|
|
360
|
+
"ci_green_at",
|
|
361
|
+
"merged_at",
|
|
362
|
+
"migrate_run_url",
|
|
363
|
+
"migrate_completed_at"
|
|
364
|
+
];
|
|
365
|
+
const out = {};
|
|
366
|
+
for (const k of keyOrder) {
|
|
367
|
+
if (state[k] !== void 0) {
|
|
368
|
+
out[k] = state[k];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// scripts/lakebase/scm-release-claim.ts
|
|
375
|
+
var ScmReleaseError = class extends Error {
|
|
376
|
+
constructor(message, code) {
|
|
377
|
+
super(message);
|
|
378
|
+
this.code = code;
|
|
379
|
+
this.name = "ScmReleaseError";
|
|
380
|
+
}
|
|
381
|
+
code;
|
|
382
|
+
};
|
|
383
|
+
async function prepareClaimRelease(args, verb) {
|
|
384
|
+
const current = readWorkflowState(args.projectDir);
|
|
385
|
+
if (!current) {
|
|
386
|
+
throw new ScmReleaseError(
|
|
387
|
+
`No SCM workflow state found at ${args.projectDir}/.lakebase/workflow-state.json.`,
|
|
388
|
+
"no-state-file"
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (current.state !== "feature-claimed") {
|
|
392
|
+
throw new ScmReleaseError(
|
|
393
|
+
`${verb} refuses state "${current.state}". Only feature-claimed is ${verb}able; later states must complete or be reverted via gh.`,
|
|
394
|
+
"bad-precondition"
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (!current.feature_id || !current.branch || !current.parent_branch || !current.lakebase_branch_uid) {
|
|
398
|
+
throw new ScmReleaseError(
|
|
399
|
+
`feature-claimed row is missing required invariants. Cannot ${verb} safely; consider re-adopting state first.`,
|
|
400
|
+
"missing-claim-fields"
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
if (!args.force) {
|
|
404
|
+
const dirty = await isDirty({ cwd: args.projectDir });
|
|
405
|
+
if (dirty) {
|
|
406
|
+
throw new ScmReleaseError(
|
|
407
|
+
`Working tree has uncommitted changes; refusing to ${verb} (switching HEAD off the feature branch would strand them). Commit / stash / discard first, or pass --force.`,
|
|
408
|
+
"dirty-working-tree"
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const switchTo = args.switchTo ?? current.parent_branch;
|
|
413
|
+
const warnings = [];
|
|
414
|
+
const headBranch = await getCurrentBranch({ cwd: args.projectDir });
|
|
415
|
+
if (headBranch === current.branch) {
|
|
416
|
+
try {
|
|
417
|
+
await exec2(`git checkout ${JSON.stringify(switchTo)}`, {
|
|
418
|
+
cwd: args.projectDir,
|
|
419
|
+
timeout: 1e4
|
|
420
|
+
});
|
|
421
|
+
} catch (err) {
|
|
422
|
+
warnings.push(
|
|
423
|
+
`git checkout ${switchTo} failed: ${err instanceof Error ? err.message : String(err)}. Local branch state may be left on the feature branch.`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return { current, switchTo, warnings };
|
|
428
|
+
}
|
|
429
|
+
function resetToScaffoldComplete(projectDir, current) {
|
|
430
|
+
const reset = {
|
|
431
|
+
$schema: current.$schema,
|
|
432
|
+
version: 1,
|
|
433
|
+
state: "scaffold-complete",
|
|
434
|
+
tier_topology: current.tier_topology,
|
|
435
|
+
project_id: current.project_id
|
|
436
|
+
};
|
|
437
|
+
writeWorkflowState(projectDir, reset);
|
|
438
|
+
return reset;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// scripts/lakebase/scm-park-feature.ts
|
|
442
|
+
async function parkFeatureBranch(args) {
|
|
443
|
+
const { current, switchTo, warnings } = await prepareClaimRelease(
|
|
444
|
+
{ projectDir: args.projectDir, force: args.force, switchTo: args.switchTo },
|
|
445
|
+
"park"
|
|
446
|
+
);
|
|
447
|
+
const reset = resetToScaffoldComplete(args.projectDir, current);
|
|
448
|
+
return {
|
|
449
|
+
state: reset,
|
|
450
|
+
keptBranch: current.branch,
|
|
451
|
+
parkedFeatureId: current.feature_id,
|
|
452
|
+
switchedTo: switchTo,
|
|
453
|
+
warnings
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// scripts/lakebase/scm-park-feature.cli.ts
|
|
458
|
+
function parseArgs(argv) {
|
|
459
|
+
const out = {};
|
|
460
|
+
for (let i = 0; i < argv.length; i++) {
|
|
461
|
+
const a = argv[i];
|
|
462
|
+
switch (a) {
|
|
463
|
+
case "--project-dir":
|
|
464
|
+
case "--cwd":
|
|
465
|
+
out.projectDir = argv[++i];
|
|
466
|
+
break;
|
|
467
|
+
case "--switch-to":
|
|
468
|
+
out.switchTo = argv[++i];
|
|
469
|
+
break;
|
|
470
|
+
case "--force":
|
|
471
|
+
out.force = true;
|
|
472
|
+
break;
|
|
473
|
+
case "--json":
|
|
474
|
+
out.json = true;
|
|
475
|
+
break;
|
|
476
|
+
case "--pretty":
|
|
477
|
+
out.pretty = true;
|
|
478
|
+
break;
|
|
479
|
+
case "--help":
|
|
480
|
+
case "-h":
|
|
481
|
+
out.help = true;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
var HELP = `lakebase-scm-park-feature (phase B+)
|
|
488
|
+
|
|
489
|
+
Release a feature claim but KEEP its paired branch: switch HEAD to the parent
|
|
490
|
+
tier, reset workflow-state to scaffold-complete, leave the Lakebase + git
|
|
491
|
+
branch intact. Frees the single SCM claim so another feature can be claimed
|
|
492
|
+
and driven; resume the parked feature later with
|
|
493
|
+
'lakebase-scm-claim-feature-branch <feature>' (it reuses the kept branch).
|
|
494
|
+
|
|
495
|
+
Usage:
|
|
496
|
+
lakebase-scm-park-feature [flags]
|
|
497
|
+
|
|
498
|
+
Flags:
|
|
499
|
+
--project-dir <dir> Project root (default: cwd)
|
|
500
|
+
--switch-to <branch> Branch to checkout after releasing (default: parent_branch)
|
|
501
|
+
--force Allow park even with a dirty working tree
|
|
502
|
+
(edits are kept but stranded on the feature branch)
|
|
503
|
+
--json Machine-readable JSON output
|
|
504
|
+
--pretty Pretty-print JSON (only with --json)
|
|
505
|
+
-h, --help Show this help
|
|
506
|
+
|
|
507
|
+
Exit codes:
|
|
508
|
+
0 = parked (claim freed, branch kept, state reset to scaffold-complete)
|
|
509
|
+
1 = no state file
|
|
510
|
+
2 = precondition refused (wrong state, dirty tree without --force, missing fields)
|
|
511
|
+
3 = substrate failure
|
|
512
|
+
`;
|
|
513
|
+
function renderHuman(report) {
|
|
514
|
+
if (!report.ok) {
|
|
515
|
+
return `lakebase-scm-park-feature: ${report.error?.code}
|
|
516
|
+
|
|
517
|
+
${report.error?.message}`;
|
|
518
|
+
}
|
|
519
|
+
const r = report.result;
|
|
520
|
+
const lines = ["Feature parked (branch kept):"];
|
|
521
|
+
lines.push(` state : ${r.state.state}`);
|
|
522
|
+
lines.push(` parked_feature : ${r.parkedFeatureId}`);
|
|
523
|
+
lines.push(` kept_branch : ${r.keptBranch}`);
|
|
524
|
+
lines.push(` switched_to : ${r.switchedTo}`);
|
|
525
|
+
lines.push("");
|
|
526
|
+
lines.push(` resume with : lakebase-scm-claim-feature-branch ${r.parkedFeatureId}`);
|
|
527
|
+
if (r.warnings.length > 0) {
|
|
528
|
+
lines.push("");
|
|
529
|
+
lines.push("warnings:");
|
|
530
|
+
for (const w of r.warnings) lines.push(` - ${w}`);
|
|
531
|
+
}
|
|
532
|
+
return lines.join("\n");
|
|
533
|
+
}
|
|
534
|
+
function exitCodeForError(err) {
|
|
535
|
+
if (err instanceof ScmReleaseError) {
|
|
536
|
+
if (err.code === "no-state-file") return 1;
|
|
537
|
+
return 2;
|
|
538
|
+
}
|
|
539
|
+
return 3;
|
|
540
|
+
}
|
|
541
|
+
async function runScmParkFeatureCli(argv) {
|
|
542
|
+
const args = parseArgs(argv);
|
|
543
|
+
if (args.help) {
|
|
544
|
+
process.stdout.write(`${HELP}
|
|
545
|
+
`);
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
const projectDir = path2.resolve(args.projectDir ?? process.cwd());
|
|
549
|
+
try {
|
|
550
|
+
const result = await parkFeatureBranch({
|
|
551
|
+
projectDir,
|
|
552
|
+
switchTo: args.switchTo,
|
|
553
|
+
force: args.force
|
|
554
|
+
});
|
|
555
|
+
const report = { ok: true, result };
|
|
556
|
+
if (args.json) {
|
|
557
|
+
const indent = args.pretty ? 2 : 0;
|
|
558
|
+
process.stdout.write(`${JSON.stringify(report, null, indent)}
|
|
559
|
+
`);
|
|
560
|
+
} else {
|
|
561
|
+
process.stdout.write(`${renderHuman(report)}
|
|
562
|
+
`);
|
|
563
|
+
}
|
|
564
|
+
return 0;
|
|
565
|
+
} catch (e) {
|
|
566
|
+
const err = e;
|
|
567
|
+
const code = err instanceof ScmReleaseError ? err.code : "substrate-failure";
|
|
568
|
+
const report = { ok: false, error: { code, message: err.message } };
|
|
569
|
+
if (args.json) {
|
|
570
|
+
const indent = args.pretty ? 2 : 0;
|
|
571
|
+
process.stdout.write(`${JSON.stringify(report, null, indent)}
|
|
572
|
+
`);
|
|
573
|
+
} else {
|
|
574
|
+
process.stderr.write(`${renderHuman(report)}
|
|
575
|
+
`);
|
|
576
|
+
}
|
|
577
|
+
return exitCodeForError(err);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (isCliEntry(importMetaUrl)) {
|
|
581
|
+
void runScmParkFeatureCli(process.argv.slice(2)).then((c) => process.exit(c));
|
|
582
|
+
}
|
|
583
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
584
|
+
0 && (module.exports = {
|
|
585
|
+
runScmParkFeatureCli
|
|
586
|
+
});
|