@sequenceholdings/studio-cli 0.1.22 → 0.1.25
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/README.md +136 -12
- package/dist/app/commands.d.ts +16 -0
- package/dist/app/commands.js +227 -0
- package/dist/app/deploy.d.ts +49 -0
- package/dist/app/deploy.js +197 -0
- package/dist/app/kinds.d.ts +10 -0
- package/dist/app/kinds.js +36 -0
- package/dist/app/manifest.d.ts +94 -0
- package/dist/app/manifest.js +273 -0
- package/dist/app/scaffold.d.ts +28 -0
- package/dist/app/scaffold.js +263 -0
- package/dist/auth.d.ts +9 -5
- package/dist/auth.js +121 -8
- package/dist/config.d.ts +7 -3
- package/dist/config.js +77 -5
- package/dist/envs/commands.js +9 -6
- package/dist/functions/bundle.js +3 -3
- package/dist/functions/commands.js +18 -5
- package/dist/functions/manifest.d.ts +25 -0
- package/dist/functions/manifest.js +93 -0
- package/dist/login.js +3 -3
- package/dist/main.d.ts +3 -0
- package/dist/main.js +21 -0
- package/dist/pipeline/codegen.d.ts +2 -0
- package/dist/pipeline/codegen.js +118 -0
- package/dist/pipeline/commands.d.ts +7 -0
- package/dist/pipeline/commands.js +90 -12
- package/dist/pipeline/lifecycle.d.ts +1 -12
- package/dist/pipeline/lifecycle.js +228 -46
- package/dist/pipeline/templates.js +3 -1
- package/dist/preview.d.ts +4 -3
- package/dist/preview.js +5 -4
- package/dist/secrets/commands.d.ts +3 -1
- package/dist/secrets/commands.js +87 -26
- package/package.json +7 -7
- package/dist/pipeline/pinning.d.ts +0 -5
- package/dist/pipeline/pinning.js +0 -9
|
@@ -5,9 +5,35 @@
|
|
|
5
5
|
import { getAccessToken, NotLoggedInError } from '../auth.js';
|
|
6
6
|
import { AtlasApiError, getJson, postJson } from '../atlas-client.js';
|
|
7
7
|
import { resolveEnvWithDiscovery } from '../config.js';
|
|
8
|
-
import { isFullSha, requiresPinnedSha } from './pinning.js';
|
|
9
8
|
const LOG = '[seq-studio]';
|
|
10
9
|
const TERMINAL = new Set(['active', 'failed', 'retired']);
|
|
10
|
+
const POLL_INTERVAL_MS = 2000;
|
|
11
|
+
const HEARTBEAT_INTERVAL_MS = 20_000;
|
|
12
|
+
const PLAN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
13
|
+
const FIRST_PARTY_TARGETS = [
|
|
14
|
+
{ id: 'dev', label: 'Development', requiresApproval: false },
|
|
15
|
+
{ id: 'staging', label: 'Staging', requiresApproval: false },
|
|
16
|
+
{ id: 'production', label: 'Production', requiresApproval: true },
|
|
17
|
+
{ id: 'banksouth', label: 'BankSouth', requiresApproval: true },
|
|
18
|
+
];
|
|
19
|
+
function formatElapsed(ms) {
|
|
20
|
+
const totalSec = Math.floor(ms / 1000);
|
|
21
|
+
if (totalSec < 60)
|
|
22
|
+
return `${totalSec}s`;
|
|
23
|
+
const min = Math.floor(totalSec / 60);
|
|
24
|
+
const sec = totalSec % 60;
|
|
25
|
+
return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
|
|
26
|
+
}
|
|
27
|
+
function deploymentProgressSuffix(detail) {
|
|
28
|
+
if (detail.triggerRunId)
|
|
29
|
+
return ` (trigger ${detail.triggerRunId})`;
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
function deploymentStatusLabel(detail) {
|
|
33
|
+
if (detail.statusDetail)
|
|
34
|
+
return `${detail.status} — ${detail.statusDetail}`;
|
|
35
|
+
return detail.status;
|
|
36
|
+
}
|
|
11
37
|
async function envAndToken(args) {
|
|
12
38
|
const requested = typeof args.flags.e === 'string' ? args.flags.e : typeof args.flags.env === 'string' ? args.flags.env : undefined;
|
|
13
39
|
if (!requested) {
|
|
@@ -31,21 +57,35 @@ function flagString(flags, key) {
|
|
|
31
57
|
throw new Error(`--${key} requires a value`);
|
|
32
58
|
return value;
|
|
33
59
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* `BUILT_IN_ENV_URLS`). There is no `local` in the server's deploy-lifecycle
|
|
38
|
-
* enum (`DEPLOY_ENVIRONMENTS` in `atlas/src/server/services/data-pipelines/schema.ts`
|
|
39
|
-
* is `dev | staging | production | banksouth`); the server's name for that
|
|
40
|
-
* same developer-loop target is `dev` (`targetFactsForEnvironment` treats
|
|
41
|
-
* `dev` as the one target exempt from a `lakebase_branch` binding). Map at
|
|
42
|
-
* this one boundary — every lifecycle request body funnels through here —
|
|
43
|
-
* so the CLI never sends the wire-invalid `local` and every other env name
|
|
44
|
-
* passes through unchanged.
|
|
45
|
-
*/
|
|
60
|
+
function flagOn(flags, key) {
|
|
61
|
+
return flags[key] === true || flags[key] === 'true';
|
|
62
|
+
}
|
|
46
63
|
export function deployEnvironmentForEnv(env) {
|
|
47
64
|
return env.name === 'local' ? 'dev' : env.name;
|
|
48
65
|
}
|
|
66
|
+
async function resolvePipelineTarget({ args, env, token, }) {
|
|
67
|
+
const requested = flagString(args.flags, 'target');
|
|
68
|
+
const inferredId = requested ?? deployEnvironmentForEnv(env);
|
|
69
|
+
if (!requested) {
|
|
70
|
+
const firstPartyTarget = FIRST_PARTY_TARGETS.find((target) => target.id === inferredId);
|
|
71
|
+
if (firstPartyTarget)
|
|
72
|
+
return firstPartyTarget;
|
|
73
|
+
}
|
|
74
|
+
const response = await getJson({
|
|
75
|
+
baseUrl: env.url,
|
|
76
|
+
token,
|
|
77
|
+
path: '/api/data-pipelines/targets',
|
|
78
|
+
});
|
|
79
|
+
const targets = response.targets;
|
|
80
|
+
const matchingTarget = targets.find((target) => target.id === inferredId);
|
|
81
|
+
if (matchingTarget)
|
|
82
|
+
return matchingTarget;
|
|
83
|
+
if (!requested && targets.length === 1)
|
|
84
|
+
return targets[0];
|
|
85
|
+
const available = targets.map((target) => target.id).join(', ') || 'none';
|
|
86
|
+
throw new Error(`could not select a pipeline target for '${env.name}'; available targets: ${available}. ` +
|
|
87
|
+
'Pass --target <id> explicitly.');
|
|
88
|
+
}
|
|
49
89
|
export async function pipelinePlanCommand(args) {
|
|
50
90
|
const repo = flagString(args.flags, 'repo');
|
|
51
91
|
const ref = flagString(args.flags, 'ref');
|
|
@@ -55,21 +95,27 @@ export async function pipelinePlanCommand(args) {
|
|
|
55
95
|
}
|
|
56
96
|
const { env, token } = await envAndToken(args);
|
|
57
97
|
const json = args.flags.json === true || args.flags.json === 'true';
|
|
98
|
+
let target;
|
|
99
|
+
try {
|
|
100
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error(`${LOG} plan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
58
106
|
let response;
|
|
59
107
|
try {
|
|
60
|
-
response = await
|
|
108
|
+
response = await enqueueAndWaitForPlan({
|
|
61
109
|
baseUrl: env.url,
|
|
62
110
|
token,
|
|
63
|
-
|
|
64
|
-
|
|
111
|
+
repo,
|
|
112
|
+
ref,
|
|
113
|
+
environment: target.id,
|
|
65
114
|
});
|
|
66
115
|
}
|
|
67
116
|
catch (error) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return 1;
|
|
71
|
-
}
|
|
72
|
-
throw error;
|
|
117
|
+
console.error(`${LOG} plan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
return 1;
|
|
73
119
|
}
|
|
74
120
|
if (json) {
|
|
75
121
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -85,9 +131,14 @@ export async function pipelineDeployCommand(args) {
|
|
|
85
131
|
const ref = flagString(args.flags, 'ref');
|
|
86
132
|
const deploymentId = flagString(args.flags, 'deployment-id');
|
|
87
133
|
const approvedBy = flagString(args.flags, 'approved-by');
|
|
134
|
+
const runNow = flagOn(args.flags, 'run-now');
|
|
88
135
|
const { env, token } = await envAndToken(args);
|
|
89
|
-
|
|
90
|
-
|
|
136
|
+
let target;
|
|
137
|
+
try {
|
|
138
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error(`${LOG} deploy failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
91
142
|
return 1;
|
|
92
143
|
}
|
|
93
144
|
let enqueue;
|
|
@@ -96,27 +147,33 @@ export async function pipelineDeployCommand(args) {
|
|
|
96
147
|
// Direct execute of a persisted plan.
|
|
97
148
|
const stageId = flagString(args.flags, 'stage-id');
|
|
98
149
|
if (!stageId) {
|
|
99
|
-
console.error('usage: seq-studio pipeline deploy --deployment-id <id> --stage-id <id> -e <env> [--approved-by <sub>] [--no-wait]');
|
|
150
|
+
console.error('usage: seq-studio pipeline deploy --deployment-id <id> --stage-id <id> -e <env> [--target <id>] [--approved-by <sub>] [--run-now] [--no-wait]');
|
|
100
151
|
return 1;
|
|
101
152
|
}
|
|
102
153
|
enqueue = await postJson({
|
|
103
154
|
baseUrl: env.url,
|
|
104
155
|
token,
|
|
105
156
|
path: `/api/data-pipelines/stages/${stageId}/deploy`,
|
|
106
|
-
body: {
|
|
157
|
+
body: {
|
|
158
|
+
deploymentId,
|
|
159
|
+
environment: target.id,
|
|
160
|
+
...(approvedBy ? { approvedBy } : {}),
|
|
161
|
+
runNow,
|
|
162
|
+
},
|
|
107
163
|
});
|
|
108
164
|
}
|
|
109
165
|
else {
|
|
110
166
|
if (!repo || !ref) {
|
|
111
|
-
console.error('usage: seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha> -e <env> [--approved-by <sub>] [--no-wait]');
|
|
167
|
+
console.error('usage: seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha|branch> -e <env> [--target <id>] [--approved-by <sub>] [--run-now] [--no-wait]');
|
|
112
168
|
return 1;
|
|
113
169
|
}
|
|
114
170
|
// Plan then deploy the first planning deployment.
|
|
115
|
-
const plan = await
|
|
171
|
+
const plan = await enqueueAndWaitForPlan({
|
|
116
172
|
baseUrl: env.url,
|
|
117
173
|
token,
|
|
118
|
-
|
|
119
|
-
|
|
174
|
+
repo,
|
|
175
|
+
ref,
|
|
176
|
+
environment: target.id,
|
|
120
177
|
});
|
|
121
178
|
if (plan.plan.hasDestructive) {
|
|
122
179
|
console.error(`${LOG} plan has destructive findings — refusing to deploy`);
|
|
@@ -140,7 +197,9 @@ export async function pipelineDeployCommand(args) {
|
|
|
140
197
|
path: `/api/data-pipelines/stages/${stageId}/deploy`,
|
|
141
198
|
body: {
|
|
142
199
|
deploymentId: firstId,
|
|
200
|
+
environment: target.id,
|
|
143
201
|
...(approvedBy ? { approvedBy } : {}),
|
|
202
|
+
runNow,
|
|
144
203
|
},
|
|
145
204
|
});
|
|
146
205
|
}
|
|
@@ -168,8 +227,16 @@ export async function pipelinePromoteCommand(args) {
|
|
|
168
227
|
return 1;
|
|
169
228
|
}
|
|
170
229
|
const { env, token } = await envAndToken(args);
|
|
171
|
-
|
|
172
|
-
|
|
230
|
+
let target;
|
|
231
|
+
try {
|
|
232
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
console.error(`${LOG} promote failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
if (target.requiresApproval && !approvedBy) {
|
|
239
|
+
console.error(`${LOG} promote to ${target.id} requires --approved-by <your sub/email> (approvals are self-recorded)`);
|
|
173
240
|
return 1;
|
|
174
241
|
}
|
|
175
242
|
const stageId = await resolveStageIdBySlug({ baseUrl: env.url, token, slug: stage, repo });
|
|
@@ -181,7 +248,7 @@ export async function pipelinePromoteCommand(args) {
|
|
|
181
248
|
path: `/api/data-pipelines/stages/${stageId}/promote`,
|
|
182
249
|
body: {
|
|
183
250
|
version,
|
|
184
|
-
environment:
|
|
251
|
+
environment: target.id,
|
|
185
252
|
...(approvedBy ? { approvedBy } : {}),
|
|
186
253
|
},
|
|
187
254
|
});
|
|
@@ -193,10 +260,20 @@ export async function pipelinePromoteCommand(args) {
|
|
|
193
260
|
}
|
|
194
261
|
throw error;
|
|
195
262
|
}
|
|
196
|
-
console.log(`${LOG} enqueued promote ${enqueue.
|
|
263
|
+
console.log(`${LOG} enqueued promote plan ${enqueue.planRequestId} (trigger=${enqueue.triggerRunId ?? 'n/a'})`);
|
|
197
264
|
if (args.flags['no-wait'] === true || args.flags['no-wait'] === 'true')
|
|
198
265
|
return 0;
|
|
199
|
-
|
|
266
|
+
const plan = await waitForPlanRequest({
|
|
267
|
+
baseUrl: env.url,
|
|
268
|
+
token,
|
|
269
|
+
planRequestId: enqueue.planRequestId,
|
|
270
|
+
});
|
|
271
|
+
const deploymentId = plan.deploymentIds[plan.stageIds.indexOf(stageId)] ?? plan.deploymentIds[0];
|
|
272
|
+
if (!deploymentId) {
|
|
273
|
+
console.error(`${LOG} promote plan completed without a deployment id`);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
return pollDeployment({ baseUrl: env.url, token, deploymentId });
|
|
200
277
|
}
|
|
201
278
|
export async function pipelineRunNowCommand(args) {
|
|
202
279
|
const stage = flagString(args.flags, 'stage');
|
|
@@ -207,6 +284,14 @@ export async function pipelineRunNowCommand(args) {
|
|
|
207
284
|
}
|
|
208
285
|
const { env, token } = await envAndToken(args);
|
|
209
286
|
const json = args.flags.json === true || args.flags.json === 'true';
|
|
287
|
+
let target;
|
|
288
|
+
try {
|
|
289
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
console.error(`${LOG} run-now failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
210
295
|
let stageId;
|
|
211
296
|
try {
|
|
212
297
|
stageId = await resolveStageIdBySlug({ baseUrl: env.url, token, slug: stage, repo });
|
|
@@ -221,7 +306,7 @@ export async function pipelineRunNowCommand(args) {
|
|
|
221
306
|
baseUrl: env.url,
|
|
222
307
|
token,
|
|
223
308
|
path: `/api/data-pipelines/stages/${stageId}/run`,
|
|
224
|
-
body: { environment:
|
|
309
|
+
body: { environment: target.id },
|
|
225
310
|
});
|
|
226
311
|
}
|
|
227
312
|
catch (error) {
|
|
@@ -235,7 +320,7 @@ export async function pipelineRunNowCommand(args) {
|
|
|
235
320
|
console.log(JSON.stringify(response, null, 2));
|
|
236
321
|
}
|
|
237
322
|
else {
|
|
238
|
-
console.log(`${LOG} run-now ${stage} → ${
|
|
323
|
+
console.log(`${LOG} run-now ${stage} → ${target.id}`);
|
|
239
324
|
console.log(`${LOG} invocation ${response.invocationId} (${response.status})`);
|
|
240
325
|
console.log(`${LOG} ${response.databricks_url}`);
|
|
241
326
|
}
|
|
@@ -250,8 +335,16 @@ export async function pipelineRollbackCommand(args) {
|
|
|
250
335
|
return 1;
|
|
251
336
|
}
|
|
252
337
|
const { env, token } = await envAndToken(args);
|
|
253
|
-
|
|
254
|
-
|
|
338
|
+
let target;
|
|
339
|
+
try {
|
|
340
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
console.error(`${LOG} rollback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
if (target.requiresApproval && !approvedBy) {
|
|
347
|
+
console.error(`${LOG} rollback in ${target.id} requires --approved-by <your sub/email> (approvals are explicit even for rollbacks)`);
|
|
255
348
|
return 1;
|
|
256
349
|
}
|
|
257
350
|
const stageId = await resolveStageIdBySlug({ baseUrl: env.url, token, slug: stage, repo });
|
|
@@ -262,7 +355,7 @@ export async function pipelineRollbackCommand(args) {
|
|
|
262
355
|
token,
|
|
263
356
|
path: `/api/data-pipelines/stages/${stageId}/rollback`,
|
|
264
357
|
body: {
|
|
265
|
-
environment:
|
|
358
|
+
environment: target.id,
|
|
266
359
|
...(approvedBy ? { approvedBy } : {}),
|
|
267
360
|
},
|
|
268
361
|
});
|
|
@@ -295,26 +388,99 @@ function printFindingsTable(findings) {
|
|
|
295
388
|
async function pollDeployment({ baseUrl, token, deploymentId, }) {
|
|
296
389
|
const started = Date.now();
|
|
297
390
|
const timeoutMs = 15 * 60 * 1000;
|
|
391
|
+
let lastLoggedLabel = null;
|
|
392
|
+
let lastLogAt = started;
|
|
298
393
|
while (Date.now() - started < timeoutMs) {
|
|
299
394
|
const detail = await getJson({
|
|
300
395
|
baseUrl,
|
|
301
396
|
token,
|
|
302
397
|
path: `/api/data-pipelines/deployments/${deploymentId}`,
|
|
303
398
|
});
|
|
304
|
-
|
|
399
|
+
const elapsedMs = Date.now() - started;
|
|
305
400
|
if (TERMINAL.has(detail.status)) {
|
|
401
|
+
const elapsed = formatElapsed(elapsedMs);
|
|
402
|
+
const progressSuffix = deploymentProgressSuffix(detail);
|
|
306
403
|
if (detail.status === 'active') {
|
|
307
|
-
console.log(`${LOG}
|
|
404
|
+
console.log(`${LOG} deployment ${deploymentId} succeeded (${elapsed})${progressSuffix}`);
|
|
308
405
|
return 0;
|
|
309
406
|
}
|
|
310
|
-
|
|
407
|
+
const detailText = detail.statusDetail ? `: ${detail.statusDetail}` : '';
|
|
408
|
+
console.error(`${LOG} deployment ${deploymentId} ended ${detail.status} (${elapsed})${detailText}${progressSuffix}`);
|
|
311
409
|
return 1;
|
|
312
410
|
}
|
|
313
|
-
|
|
411
|
+
const progressSuffix = deploymentProgressSuffix(detail);
|
|
412
|
+
const statusLabel = deploymentStatusLabel(detail);
|
|
413
|
+
if (statusLabel !== lastLoggedLabel) {
|
|
414
|
+
console.log(`${LOG} deployment ${deploymentId}: ${statusLabel} (${formatElapsed(elapsedMs)})${progressSuffix}`);
|
|
415
|
+
lastLoggedLabel = statusLabel;
|
|
416
|
+
lastLogAt = Date.now();
|
|
417
|
+
}
|
|
418
|
+
else if (Date.now() - lastLogAt >= HEARTBEAT_INTERVAL_MS) {
|
|
419
|
+
console.log(`${LOG} deployment ${deploymentId}: still ${statusLabel} (${formatElapsed(elapsedMs)})${progressSuffix}`);
|
|
420
|
+
lastLogAt = Date.now();
|
|
421
|
+
}
|
|
422
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
314
423
|
}
|
|
315
|
-
console.error(`${LOG} timed out waiting for deployment ${deploymentId}`);
|
|
424
|
+
console.error(`${LOG} timed out waiting for deployment ${deploymentId} (${formatElapsed(Date.now() - started)})`);
|
|
316
425
|
return 1;
|
|
317
426
|
}
|
|
427
|
+
async function enqueueAndWaitForPlan({ baseUrl, token, repo, ref, environment, }) {
|
|
428
|
+
const enqueue = await postJson({
|
|
429
|
+
baseUrl,
|
|
430
|
+
token,
|
|
431
|
+
path: '/api/data-pipelines/pipelines/plan',
|
|
432
|
+
body: { repo, ref, environment },
|
|
433
|
+
});
|
|
434
|
+
console.log(`${LOG} enqueued plan ${enqueue.planRequestId} (trigger=${enqueue.triggerRunId ?? 'n/a'})`);
|
|
435
|
+
const detail = await waitForPlanRequest({
|
|
436
|
+
baseUrl,
|
|
437
|
+
token,
|
|
438
|
+
planRequestId: enqueue.planRequestId,
|
|
439
|
+
});
|
|
440
|
+
if (!detail.plan || !detail.text) {
|
|
441
|
+
throw new Error(`plan request ${detail.planRequestId} succeeded without a plan result`);
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
plan: detail.plan,
|
|
445
|
+
deploymentIds: detail.deploymentIds,
|
|
446
|
+
stageIds: detail.stageIds,
|
|
447
|
+
text: detail.text,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async function waitForPlanRequest({ baseUrl, token, planRequestId, }) {
|
|
451
|
+
const started = Date.now();
|
|
452
|
+
let lastStatus = null;
|
|
453
|
+
let lastLoggedAt = started;
|
|
454
|
+
while (Date.now() - started < PLAN_TIMEOUT_MS) {
|
|
455
|
+
const detail = await getJson({
|
|
456
|
+
baseUrl,
|
|
457
|
+
token,
|
|
458
|
+
path: `/api/data-pipelines/plan-requests/${planRequestId}`,
|
|
459
|
+
});
|
|
460
|
+
const elapsed = Date.now() - started;
|
|
461
|
+
const statusLabel = detail.statusDetail
|
|
462
|
+
? `${detail.status} — ${detail.statusDetail}`
|
|
463
|
+
: detail.status;
|
|
464
|
+
if (detail.status === 'failed') {
|
|
465
|
+
throw new Error(`plan request ${detail.planRequestId} failed after ${formatElapsed(elapsed)}: ` +
|
|
466
|
+
`${detail.statusDetail ?? 'unknown error'}`);
|
|
467
|
+
}
|
|
468
|
+
if (detail.status === 'succeeded')
|
|
469
|
+
return detail;
|
|
470
|
+
if (statusLabel !== lastStatus) {
|
|
471
|
+
console.log(`${LOG} plan ${detail.planRequestId}: ${statusLabel} (${formatElapsed(elapsed)})`);
|
|
472
|
+
lastStatus = statusLabel;
|
|
473
|
+
lastLoggedAt = Date.now();
|
|
474
|
+
}
|
|
475
|
+
else if (Date.now() - lastLoggedAt >= HEARTBEAT_INTERVAL_MS) {
|
|
476
|
+
console.log(`${LOG} plan ${detail.planRequestId}: still ${statusLabel} (${formatElapsed(elapsed)})`);
|
|
477
|
+
lastLoggedAt = Date.now();
|
|
478
|
+
}
|
|
479
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
480
|
+
}
|
|
481
|
+
throw new Error(`timed out waiting for plan request ${planRequestId} ` +
|
|
482
|
+
`(${formatElapsed(Date.now() - started)})`);
|
|
483
|
+
}
|
|
318
484
|
export async function pipelineAdoptCommand(args) {
|
|
319
485
|
const stage = flagString(args.flags, 'stage');
|
|
320
486
|
const ref = flagString(args.flags, 'ref');
|
|
@@ -328,6 +494,14 @@ export async function pipelineAdoptCommand(args) {
|
|
|
328
494
|
}
|
|
329
495
|
const { env, token } = await envAndToken(args);
|
|
330
496
|
const json = args.flags.json === true || args.flags.json === 'true';
|
|
497
|
+
let target;
|
|
498
|
+
try {
|
|
499
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
console.error(`${LOG} adopt failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
503
|
+
return 1;
|
|
504
|
+
}
|
|
331
505
|
const repo = flagString(args.flags, 'repo');
|
|
332
506
|
const resourceKey = flagString(args.flags, 'resource-key') ?? stage.replace(/-/g, '_');
|
|
333
507
|
const kind = flagString(args.flags, 'kind') ?? 'job';
|
|
@@ -348,7 +522,7 @@ export async function pipelineAdoptCommand(args) {
|
|
|
348
522
|
token,
|
|
349
523
|
path: `/api/data-pipelines/stages/${stageId}/adopt`,
|
|
350
524
|
body: {
|
|
351
|
-
environment:
|
|
525
|
+
environment: target.id,
|
|
352
526
|
ref,
|
|
353
527
|
bindings: [{ resourceKey, nativeId, kind }],
|
|
354
528
|
approvedBy,
|
|
@@ -384,6 +558,14 @@ export async function pipelineUnbindCommand(args) {
|
|
|
384
558
|
}
|
|
385
559
|
const { env, token } = await envAndToken(args);
|
|
386
560
|
const json = args.flags.json === true || args.flags.json === 'true';
|
|
561
|
+
let target;
|
|
562
|
+
try {
|
|
563
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
console.error(`${LOG} unbind failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
567
|
+
return 1;
|
|
568
|
+
}
|
|
387
569
|
const repo = flagString(args.flags, 'repo');
|
|
388
570
|
const resourceKey = flagString(args.flags, 'resource-key');
|
|
389
571
|
const stageId = await resolveStageIdBySlug({
|
|
@@ -398,7 +580,7 @@ export async function pipelineUnbindCommand(args) {
|
|
|
398
580
|
token,
|
|
399
581
|
path: `/api/data-pipelines/stages/${stageId}/unbind`,
|
|
400
582
|
body: {
|
|
401
|
-
environment:
|
|
583
|
+
environment: target.id,
|
|
402
584
|
ref,
|
|
403
585
|
approvedBy,
|
|
404
586
|
...(resourceKey ? { resourceKeys: [resourceKey] } : {}),
|
|
@@ -116,6 +116,7 @@ const SERVING_BODY = `
|
|
|
116
116
|
source:
|
|
117
117
|
stage: my-gold-stage
|
|
118
118
|
output: gold.my_table
|
|
119
|
+
# catalog: src_example # optional; omit to use the deploy catalog
|
|
119
120
|
projection:
|
|
120
121
|
# The served contract (inline columns or schema_ref):
|
|
121
122
|
columns:
|
|
@@ -139,7 +140,8 @@ expose:
|
|
|
139
140
|
# table: my_table
|
|
140
141
|
# mode: referenced
|
|
141
142
|
`;
|
|
142
|
-
const ENTRYPOINT_STUB =
|
|
143
|
+
const ENTRYPOINT_STUB = `# Databricks notebook source
|
|
144
|
+
"""Entrypoint stub for the {{name}} stage.
|
|
143
145
|
|
|
144
146
|
The platform invokes this per the stage spec ({{name}}.stage.yml).
|
|
145
147
|
Decoders and transforms stay pure — bronze materialization, envelope
|
package/dist/preview.d.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* hand-editing `~/.config/lattice/config.toml`. See
|
|
7
7
|
* `docs/preview-environments.md` for the full lifecycle.
|
|
8
8
|
*
|
|
9
|
-
* SLUG ALGORITHM — must stay byte-for-byte in sync with the
|
|
9
|
+
* SLUG ALGORITHM — must stay byte-for-byte in sync with the other
|
|
10
10
|
* places that compute the same slug from a branch name:
|
|
11
|
-
* - `.github/workflows/preview-deploy.yml` (
|
|
12
|
-
* -
|
|
11
|
+
* - `.github/workflows/preview-deploy.yml` (replace non-alnum, lowercase, trim hyphens)
|
|
12
|
+
* - `.github/workflows/trigger-preview.yml` (same bash pipeline)
|
|
13
|
+
* - `atlas/src/server/db.ts:sanitize` (`.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase().replace(/^-+|-+$/g, '')`)
|
|
13
14
|
* - `databricks/lakebase/atlas-db/pr-lifecycle.ts:sanitize`
|
|
14
15
|
* Changing it here without changing those produces a host that does NOT
|
|
15
16
|
* match what `preview-deploy.yml` actually deployed.
|
package/dist/preview.js
CHANGED
|
@@ -8,10 +8,11 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
* hand-editing `~/.config/lattice/config.toml`. See
|
|
9
9
|
* `docs/preview-environments.md` for the full lifecycle.
|
|
10
10
|
*
|
|
11
|
-
* SLUG ALGORITHM — must stay byte-for-byte in sync with the
|
|
11
|
+
* SLUG ALGORITHM — must stay byte-for-byte in sync with the other
|
|
12
12
|
* places that compute the same slug from a branch name:
|
|
13
|
-
* - `.github/workflows/preview-deploy.yml` (
|
|
14
|
-
* -
|
|
13
|
+
* - `.github/workflows/preview-deploy.yml` (replace non-alnum, lowercase, trim hyphens)
|
|
14
|
+
* - `.github/workflows/trigger-preview.yml` (same bash pipeline)
|
|
15
|
+
* - `atlas/src/server/db.ts:sanitize` (`.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase().replace(/^-+|-+$/g, '')`)
|
|
15
16
|
* - `databricks/lakebase/atlas-db/pr-lifecycle.ts:sanitize`
|
|
16
17
|
* Changing it here without changing those produces a host that does NOT
|
|
17
18
|
* match what `preview-deploy.yml` actually deployed.
|
|
@@ -45,7 +46,7 @@ export const PREVIEW_PROTECTED_SLUGS = [
|
|
|
45
46
|
export const MAX_LABEL_LENGTH = 63;
|
|
46
47
|
/** Apply the canonical branch → slug transform. */
|
|
47
48
|
export function previewSlug(branchOrSlug) {
|
|
48
|
-
return branchOrSlug.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
|
|
49
|
+
return branchOrSlug.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase().replace(/^-+|-+$/g, '');
|
|
49
50
|
}
|
|
50
51
|
/** The `studio-atlas-git-<slug>` DNS label Vercel assigns the preview. */
|
|
51
52
|
export function previewLabel(slug) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ParsedArgs } from '../process/commands.js';
|
|
2
2
|
export declare const LOG = "[seq-studio]";
|
|
3
|
+
/** Drop one trailing LF or CRLF so Windows-authored --from-file values match Unix. */
|
|
4
|
+
export declare function stripOneTrailingNewline(value: string): string;
|
|
3
5
|
export declare function secretsCreateCommand(args: ParsedArgs): Promise<number>;
|
|
4
6
|
export declare function secretsSetCommand(args: ParsedArgs): Promise<number>;
|
|
5
7
|
export declare function secretsListCommand(args: ParsedArgs): Promise<number>;
|
|
@@ -20,5 +22,5 @@ export declare function secretsSetDefaultCommand(args: ParsedArgs): Promise<numb
|
|
|
20
22
|
* UI: pin + redeploy the function's active version / pin only / cancel.
|
|
21
23
|
*/
|
|
22
24
|
export declare function secretsPinCommand(args: ParsedArgs): Promise<number>;
|
|
23
|
-
export declare const SECRETS_USAGE = "usage:\n seq-studio secrets create <NAME> -e <env> [--description text]
|
|
25
|
+
export declare const SECRETS_USAGE = "usage:\n seq-studio secrets create <NAME> -e <env> [--org <slug>] [--description text]\n register an org-owned secret\n seq-studio secrets set <NAME> -e <env> [--org <slug>] [--from-file <path>]\n set the shared default value (write-only)\n seq-studio secrets list -e <env> secrets you can see (never values)\n seq-studio secrets attach <NAME> --fn <slug> -e <env> [--org <slug>]\n mount default on a function env var\n seq-studio secrets detach <NAME> --fn <slug> -e <env> [--org <slug>]\n remove attachment\n seq-studio secrets apply --from-env-file .env -e <env> push defaults + attach (keys default from manifest)\n seq-studio secrets versions <NAME> -e <env> [--org <slug>] value version history (never values)\n seq-studio secrets set-default <NAME> [version] -e <env> [--org <slug>]\n point the default at a prior version (editor)\n seq-studio secrets pin <NAME> --fn <slug> [version] -e <env> [--org <slug>]\n pin one function to a version (sticky; --unpin clears)\n\n Note: for the common deploy loop, secrets declared in managed-function.yml are\n reconciled automatically by `seq-studio functions deploy` using a local .env.\n Use `secrets` commands for CI (no .env), bulk/multi-function ops, or write-only\n value changes without a redeploy.\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n --org <slug> (target/select managed-scope org; required when a name exists in multiple orgs)\n set: --from-file <path> (non-interactive; file is not echoed)\n --fn <slug> \u00B7 --env-var <NAME> \u00B7 --yes\n --functions <f1,f2> \u00B7 --keys <K1,K2> \u00B7 --all (override key selection)\n set-default: [version|version-row-id] (defaults to the most recent non-default) \u00B7 --yes\n pin: [version|version-row-id] (defaults to the current default) \u00B7 --unpin \u00B7 --yes (redeploy) \u00B7 --yes --no-redeploy\n";
|
|
24
26
|
export declare function runSecretsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|