@sequenceholdings/studio-cli 0.1.21 → 0.1.24
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 +184 -16
- package/dist/agents/commands.d.ts +1 -1
- package/dist/agents/commands.js +24 -4
- package/dist/agents/source.d.ts +3 -1
- package/dist/agents/source.js +27 -4
- 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/atlas-client.js +29 -0
- package/dist/auth.js +2 -0
- package/dist/functions/commands.js +1 -0
- package/dist/functions/manifest.d.ts +24 -0
- package/dist/functions/manifest.js +84 -9
- 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 +105 -10
- package/dist/pipeline/lifecycle.d.ts +3 -12
- package/dist/pipeline/lifecycle.js +245 -33
- package/dist/pipeline/templates.js +3 -1
- package/dist/repos/commands.d.ts +53 -1
- package/dist/repos/commands.js +258 -1
- package/dist/secrets/commands.d.ts +3 -1
- package/dist/secrets/commands.js +87 -26
- package/package.json +8 -8
- package/dist/pipeline/pinning.d.ts +0 -5
- package/dist/pipeline/pinning.js +0 -9
|
@@ -5,9 +5,34 @@
|
|
|
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 FIRST_PARTY_TARGETS = [
|
|
13
|
+
{ id: 'dev', label: 'Development', requiresApproval: false },
|
|
14
|
+
{ id: 'staging', label: 'Staging', requiresApproval: false },
|
|
15
|
+
{ id: 'production', label: 'Production', requiresApproval: true },
|
|
16
|
+
{ id: 'banksouth', label: 'BankSouth', requiresApproval: true },
|
|
17
|
+
];
|
|
18
|
+
function formatElapsed(ms) {
|
|
19
|
+
const totalSec = Math.floor(ms / 1000);
|
|
20
|
+
if (totalSec < 60)
|
|
21
|
+
return `${totalSec}s`;
|
|
22
|
+
const min = Math.floor(totalSec / 60);
|
|
23
|
+
const sec = totalSec % 60;
|
|
24
|
+
return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
|
|
25
|
+
}
|
|
26
|
+
function deploymentProgressSuffix(detail) {
|
|
27
|
+
if (detail.triggerRunId)
|
|
28
|
+
return ` (trigger ${detail.triggerRunId})`;
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
function deploymentStatusLabel(detail) {
|
|
32
|
+
if (detail.statusDetail)
|
|
33
|
+
return `${detail.status} — ${detail.statusDetail}`;
|
|
34
|
+
return detail.status;
|
|
35
|
+
}
|
|
11
36
|
async function envAndToken(args) {
|
|
12
37
|
const requested = typeof args.flags.e === 'string' ? args.flags.e : typeof args.flags.env === 'string' ? args.flags.env : undefined;
|
|
13
38
|
if (!requested) {
|
|
@@ -31,21 +56,32 @@ function flagString(flags, key) {
|
|
|
31
56
|
throw new Error(`--${key} requires a value`);
|
|
32
57
|
return value;
|
|
33
58
|
}
|
|
34
|
-
/**
|
|
35
|
-
* The CLI's local dev-loop env is named `local` — chosen so `-e local` lines
|
|
36
|
-
* up with `seqapi -e local` / `artifact-studio --env local` (see `config.ts`
|
|
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
|
-
*/
|
|
46
59
|
export function deployEnvironmentForEnv(env) {
|
|
47
60
|
return env.name === 'local' ? 'dev' : env.name;
|
|
48
61
|
}
|
|
62
|
+
async function resolvePipelineTarget({ args, env, token, }) {
|
|
63
|
+
const requested = flagString(args.flags, 'target');
|
|
64
|
+
const inferredId = requested ?? deployEnvironmentForEnv(env);
|
|
65
|
+
if (!requested) {
|
|
66
|
+
const firstPartyTarget = FIRST_PARTY_TARGETS.find((target) => target.id === inferredId);
|
|
67
|
+
if (firstPartyTarget)
|
|
68
|
+
return firstPartyTarget;
|
|
69
|
+
}
|
|
70
|
+
const response = await getJson({
|
|
71
|
+
baseUrl: env.url,
|
|
72
|
+
token,
|
|
73
|
+
path: '/api/data-pipelines/targets',
|
|
74
|
+
});
|
|
75
|
+
const targets = response.targets;
|
|
76
|
+
const matchingTarget = targets.find((target) => target.id === inferredId);
|
|
77
|
+
if (matchingTarget)
|
|
78
|
+
return matchingTarget;
|
|
79
|
+
if (!requested && targets.length === 1)
|
|
80
|
+
return targets[0];
|
|
81
|
+
const available = targets.map((target) => target.id).join(', ') || 'none';
|
|
82
|
+
throw new Error(`could not select a pipeline target for '${env.name}'; available targets: ${available}. ` +
|
|
83
|
+
'Pass --target <id> explicitly.');
|
|
84
|
+
}
|
|
49
85
|
export async function pipelinePlanCommand(args) {
|
|
50
86
|
const repo = flagString(args.flags, 'repo');
|
|
51
87
|
const ref = flagString(args.flags, 'ref');
|
|
@@ -55,13 +91,21 @@ export async function pipelinePlanCommand(args) {
|
|
|
55
91
|
}
|
|
56
92
|
const { env, token } = await envAndToken(args);
|
|
57
93
|
const json = args.flags.json === true || args.flags.json === 'true';
|
|
94
|
+
let target;
|
|
95
|
+
try {
|
|
96
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error(`${LOG} plan failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
58
102
|
let response;
|
|
59
103
|
try {
|
|
60
104
|
response = await postJson({
|
|
61
105
|
baseUrl: env.url,
|
|
62
106
|
token,
|
|
63
107
|
path: '/api/data-pipelines/pipelines/plan',
|
|
64
|
-
body: { repo, ref, environment:
|
|
108
|
+
body: { repo, ref, environment: target.id },
|
|
65
109
|
});
|
|
66
110
|
}
|
|
67
111
|
catch (error) {
|
|
@@ -86,8 +130,12 @@ export async function pipelineDeployCommand(args) {
|
|
|
86
130
|
const deploymentId = flagString(args.flags, 'deployment-id');
|
|
87
131
|
const approvedBy = flagString(args.flags, 'approved-by');
|
|
88
132
|
const { env, token } = await envAndToken(args);
|
|
89
|
-
|
|
90
|
-
|
|
133
|
+
let target;
|
|
134
|
+
try {
|
|
135
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
console.error(`${LOG} deploy failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
91
139
|
return 1;
|
|
92
140
|
}
|
|
93
141
|
let enqueue;
|
|
@@ -96,19 +144,19 @@ export async function pipelineDeployCommand(args) {
|
|
|
96
144
|
// Direct execute of a persisted plan.
|
|
97
145
|
const stageId = flagString(args.flags, 'stage-id');
|
|
98
146
|
if (!stageId) {
|
|
99
|
-
console.error('usage: seq-studio pipeline deploy --deployment-id <id> --stage-id <id> -e <env> [--approved-by <sub>] [--no-wait]');
|
|
147
|
+
console.error('usage: seq-studio pipeline deploy --deployment-id <id> --stage-id <id> -e <env> [--target <id>] [--approved-by <sub>] [--no-wait]');
|
|
100
148
|
return 1;
|
|
101
149
|
}
|
|
102
150
|
enqueue = await postJson({
|
|
103
151
|
baseUrl: env.url,
|
|
104
152
|
token,
|
|
105
153
|
path: `/api/data-pipelines/stages/${stageId}/deploy`,
|
|
106
|
-
body: { deploymentId, ...(approvedBy ? { approvedBy } : {}) },
|
|
154
|
+
body: { deploymentId, environment: target.id, ...(approvedBy ? { approvedBy } : {}) },
|
|
107
155
|
});
|
|
108
156
|
}
|
|
109
157
|
else {
|
|
110
158
|
if (!repo || !ref) {
|
|
111
|
-
console.error('usage: seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha> -e <env> [--approved-by <sub>] [--no-wait]');
|
|
159
|
+
console.error('usage: seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha|branch> -e <env> [--target <id>] [--approved-by <sub>] [--no-wait]');
|
|
112
160
|
return 1;
|
|
113
161
|
}
|
|
114
162
|
// Plan then deploy the first planning deployment.
|
|
@@ -116,7 +164,7 @@ export async function pipelineDeployCommand(args) {
|
|
|
116
164
|
baseUrl: env.url,
|
|
117
165
|
token,
|
|
118
166
|
path: '/api/data-pipelines/pipelines/plan',
|
|
119
|
-
body: { repo, ref, environment:
|
|
167
|
+
body: { repo, ref, environment: target.id },
|
|
120
168
|
});
|
|
121
169
|
if (plan.plan.hasDestructive) {
|
|
122
170
|
console.error(`${LOG} plan has destructive findings — refusing to deploy`);
|
|
@@ -140,6 +188,7 @@ export async function pipelineDeployCommand(args) {
|
|
|
140
188
|
path: `/api/data-pipelines/stages/${stageId}/deploy`,
|
|
141
189
|
body: {
|
|
142
190
|
deploymentId: firstId,
|
|
191
|
+
environment: target.id,
|
|
143
192
|
...(approvedBy ? { approvedBy } : {}),
|
|
144
193
|
},
|
|
145
194
|
});
|
|
@@ -168,8 +217,16 @@ export async function pipelinePromoteCommand(args) {
|
|
|
168
217
|
return 1;
|
|
169
218
|
}
|
|
170
219
|
const { env, token } = await envAndToken(args);
|
|
171
|
-
|
|
172
|
-
|
|
220
|
+
let target;
|
|
221
|
+
try {
|
|
222
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.error(`${LOG} promote failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
if (target.requiresApproval && !approvedBy) {
|
|
229
|
+
console.error(`${LOG} promote to ${target.id} requires --approved-by <your sub/email> (approvals are self-recorded)`);
|
|
173
230
|
return 1;
|
|
174
231
|
}
|
|
175
232
|
const stageId = await resolveStageIdBySlug({ baseUrl: env.url, token, slug: stage, repo });
|
|
@@ -181,7 +238,7 @@ export async function pipelinePromoteCommand(args) {
|
|
|
181
238
|
path: `/api/data-pipelines/stages/${stageId}/promote`,
|
|
182
239
|
body: {
|
|
183
240
|
version,
|
|
184
|
-
environment:
|
|
241
|
+
environment: target.id,
|
|
185
242
|
...(approvedBy ? { approvedBy } : {}),
|
|
186
243
|
},
|
|
187
244
|
});
|
|
@@ -207,6 +264,14 @@ export async function pipelineRunNowCommand(args) {
|
|
|
207
264
|
}
|
|
208
265
|
const { env, token } = await envAndToken(args);
|
|
209
266
|
const json = args.flags.json === true || args.flags.json === 'true';
|
|
267
|
+
let target;
|
|
268
|
+
try {
|
|
269
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
console.error(`${LOG} run-now failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
210
275
|
let stageId;
|
|
211
276
|
try {
|
|
212
277
|
stageId = await resolveStageIdBySlug({ baseUrl: env.url, token, slug: stage, repo });
|
|
@@ -221,7 +286,7 @@ export async function pipelineRunNowCommand(args) {
|
|
|
221
286
|
baseUrl: env.url,
|
|
222
287
|
token,
|
|
223
288
|
path: `/api/data-pipelines/stages/${stageId}/run`,
|
|
224
|
-
body: { environment:
|
|
289
|
+
body: { environment: target.id },
|
|
225
290
|
});
|
|
226
291
|
}
|
|
227
292
|
catch (error) {
|
|
@@ -235,7 +300,7 @@ export async function pipelineRunNowCommand(args) {
|
|
|
235
300
|
console.log(JSON.stringify(response, null, 2));
|
|
236
301
|
}
|
|
237
302
|
else {
|
|
238
|
-
console.log(`${LOG} run-now ${stage} → ${
|
|
303
|
+
console.log(`${LOG} run-now ${stage} → ${target.id}`);
|
|
239
304
|
console.log(`${LOG} invocation ${response.invocationId} (${response.status})`);
|
|
240
305
|
console.log(`${LOG} ${response.databricks_url}`);
|
|
241
306
|
}
|
|
@@ -250,8 +315,16 @@ export async function pipelineRollbackCommand(args) {
|
|
|
250
315
|
return 1;
|
|
251
316
|
}
|
|
252
317
|
const { env, token } = await envAndToken(args);
|
|
253
|
-
|
|
254
|
-
|
|
318
|
+
let target;
|
|
319
|
+
try {
|
|
320
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
console.error(`${LOG} rollback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
326
|
+
if (target.requiresApproval && !approvedBy) {
|
|
327
|
+
console.error(`${LOG} rollback in ${target.id} requires --approved-by <your sub/email> (approvals are explicit even for rollbacks)`);
|
|
255
328
|
return 1;
|
|
256
329
|
}
|
|
257
330
|
const stageId = await resolveStageIdBySlug({ baseUrl: env.url, token, slug: stage, repo });
|
|
@@ -262,7 +335,7 @@ export async function pipelineRollbackCommand(args) {
|
|
|
262
335
|
token,
|
|
263
336
|
path: `/api/data-pipelines/stages/${stageId}/rollback`,
|
|
264
337
|
body: {
|
|
265
|
-
environment:
|
|
338
|
+
environment: target.id,
|
|
266
339
|
...(approvedBy ? { approvedBy } : {}),
|
|
267
340
|
},
|
|
268
341
|
});
|
|
@@ -295,26 +368,165 @@ function printFindingsTable(findings) {
|
|
|
295
368
|
async function pollDeployment({ baseUrl, token, deploymentId, }) {
|
|
296
369
|
const started = Date.now();
|
|
297
370
|
const timeoutMs = 15 * 60 * 1000;
|
|
371
|
+
let lastLoggedLabel = null;
|
|
372
|
+
let lastLogAt = started;
|
|
298
373
|
while (Date.now() - started < timeoutMs) {
|
|
299
374
|
const detail = await getJson({
|
|
300
375
|
baseUrl,
|
|
301
376
|
token,
|
|
302
377
|
path: `/api/data-pipelines/deployments/${deploymentId}`,
|
|
303
378
|
});
|
|
304
|
-
|
|
379
|
+
const elapsedMs = Date.now() - started;
|
|
305
380
|
if (TERMINAL.has(detail.status)) {
|
|
381
|
+
const elapsed = formatElapsed(elapsedMs);
|
|
382
|
+
const progressSuffix = deploymentProgressSuffix(detail);
|
|
306
383
|
if (detail.status === 'active') {
|
|
307
|
-
console.log(`${LOG}
|
|
384
|
+
console.log(`${LOG} deployment ${deploymentId} succeeded (${elapsed})${progressSuffix}`);
|
|
308
385
|
return 0;
|
|
309
386
|
}
|
|
310
|
-
|
|
387
|
+
const detailText = detail.statusDetail ? `: ${detail.statusDetail}` : '';
|
|
388
|
+
console.error(`${LOG} deployment ${deploymentId} ended ${detail.status} (${elapsed})${detailText}${progressSuffix}`);
|
|
311
389
|
return 1;
|
|
312
390
|
}
|
|
313
|
-
|
|
391
|
+
const progressSuffix = deploymentProgressSuffix(detail);
|
|
392
|
+
const statusLabel = deploymentStatusLabel(detail);
|
|
393
|
+
if (statusLabel !== lastLoggedLabel) {
|
|
394
|
+
console.log(`${LOG} deployment ${deploymentId}: ${statusLabel} (${formatElapsed(elapsedMs)})${progressSuffix}`);
|
|
395
|
+
lastLoggedLabel = statusLabel;
|
|
396
|
+
lastLogAt = Date.now();
|
|
397
|
+
}
|
|
398
|
+
else if (Date.now() - lastLogAt >= HEARTBEAT_INTERVAL_MS) {
|
|
399
|
+
console.log(`${LOG} deployment ${deploymentId}: still ${statusLabel} (${formatElapsed(elapsedMs)})${progressSuffix}`);
|
|
400
|
+
lastLogAt = Date.now();
|
|
401
|
+
}
|
|
402
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
314
403
|
}
|
|
315
|
-
console.error(`${LOG} timed out waiting for deployment ${deploymentId}`);
|
|
404
|
+
console.error(`${LOG} timed out waiting for deployment ${deploymentId} (${formatElapsed(Date.now() - started)})`);
|
|
316
405
|
return 1;
|
|
317
406
|
}
|
|
407
|
+
export async function pipelineAdoptCommand(args) {
|
|
408
|
+
const stage = flagString(args.flags, 'stage');
|
|
409
|
+
const ref = flagString(args.flags, 'ref');
|
|
410
|
+
const nativeId = flagString(args.flags, 'native-id');
|
|
411
|
+
const approvedBy = flagString(args.flags, 'approved-by');
|
|
412
|
+
if (!stage || !ref || !nativeId || !approvedBy) {
|
|
413
|
+
console.error('usage: seq-studio pipeline adopt --stage <slug> --ref <sha|branch> -e <env> ' +
|
|
414
|
+
'--native-id <id> --approved-by <you> [--resource-key <key>] [--kind job|dlt_pipeline] ' +
|
|
415
|
+
'[--old-source-removal-pr <url>] [--repo pipelines/<slug>] [--json]');
|
|
416
|
+
return 1;
|
|
417
|
+
}
|
|
418
|
+
const { env, token } = await envAndToken(args);
|
|
419
|
+
const json = args.flags.json === true || args.flags.json === 'true';
|
|
420
|
+
let target;
|
|
421
|
+
try {
|
|
422
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
console.error(`${LOG} adopt failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
426
|
+
return 1;
|
|
427
|
+
}
|
|
428
|
+
const repo = flagString(args.flags, 'repo');
|
|
429
|
+
const resourceKey = flagString(args.flags, 'resource-key') ?? stage.replace(/-/g, '_');
|
|
430
|
+
const kind = flagString(args.flags, 'kind') ?? 'job';
|
|
431
|
+
if (kind !== 'job' && kind !== 'dlt_pipeline') {
|
|
432
|
+
console.error(`${LOG} --kind must be job or dlt_pipeline`);
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
const oldSourceRemovalPr = flagString(args.flags, 'old-source-removal-pr');
|
|
436
|
+
const stageId = await resolveStageIdBySlug({
|
|
437
|
+
baseUrl: env.url,
|
|
438
|
+
token,
|
|
439
|
+
slug: stage,
|
|
440
|
+
repo,
|
|
441
|
+
});
|
|
442
|
+
try {
|
|
443
|
+
const response = await postJson({
|
|
444
|
+
baseUrl: env.url,
|
|
445
|
+
token,
|
|
446
|
+
path: `/api/data-pipelines/stages/${stageId}/adopt`,
|
|
447
|
+
body: {
|
|
448
|
+
environment: target.id,
|
|
449
|
+
ref,
|
|
450
|
+
bindings: [{ resourceKey, nativeId, kind }],
|
|
451
|
+
approvedBy,
|
|
452
|
+
...(oldSourceRemovalPr ? { oldSourceRemovalPr } : {}),
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
if (json) {
|
|
456
|
+
console.log(JSON.stringify(response, null, 2));
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
console.log(`${LOG} enqueued adopt for ${stage} → ${resourceKey}=${nativeId} ` +
|
|
460
|
+
`(trigger=${response.triggerRunId}). Worker binds on Trigger; ` +
|
|
461
|
+
`confirm with plan or stage_resources once the run completes.`);
|
|
462
|
+
}
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
if (error instanceof AtlasApiError) {
|
|
467
|
+
console.error(`${LOG} adopt failed: ${error.message}`);
|
|
468
|
+
return 1;
|
|
469
|
+
}
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
export async function pipelineUnbindCommand(args) {
|
|
474
|
+
const stage = flagString(args.flags, 'stage');
|
|
475
|
+
const ref = flagString(args.flags, 'ref');
|
|
476
|
+
const approvedBy = flagString(args.flags, 'approved-by');
|
|
477
|
+
if (!stage || !ref || !approvedBy) {
|
|
478
|
+
console.error('usage: seq-studio pipeline unbind --stage <slug> --ref <sha|branch> -e <env> ' +
|
|
479
|
+
'--approved-by <you> [--resource-key <key>] [--repo pipelines/<slug>] [--json]');
|
|
480
|
+
return 1;
|
|
481
|
+
}
|
|
482
|
+
const { env, token } = await envAndToken(args);
|
|
483
|
+
const json = args.flags.json === true || args.flags.json === 'true';
|
|
484
|
+
let target;
|
|
485
|
+
try {
|
|
486
|
+
target = await resolvePipelineTarget({ args, env, token });
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
console.error(`${LOG} unbind failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
490
|
+
return 1;
|
|
491
|
+
}
|
|
492
|
+
const repo = flagString(args.flags, 'repo');
|
|
493
|
+
const resourceKey = flagString(args.flags, 'resource-key');
|
|
494
|
+
const stageId = await resolveStageIdBySlug({
|
|
495
|
+
baseUrl: env.url,
|
|
496
|
+
token,
|
|
497
|
+
slug: stage,
|
|
498
|
+
repo,
|
|
499
|
+
});
|
|
500
|
+
try {
|
|
501
|
+
const response = await postJson({
|
|
502
|
+
baseUrl: env.url,
|
|
503
|
+
token,
|
|
504
|
+
path: `/api/data-pipelines/stages/${stageId}/unbind`,
|
|
505
|
+
body: {
|
|
506
|
+
environment: target.id,
|
|
507
|
+
ref,
|
|
508
|
+
approvedBy,
|
|
509
|
+
...(resourceKey ? { resourceKeys: [resourceKey] } : {}),
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
if (json) {
|
|
513
|
+
console.log(JSON.stringify(response, null, 2));
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
console.log(`${LOG} enqueued unbind for ${stage}` +
|
|
517
|
+
(resourceKey ? ` (${resourceKey})` : '') +
|
|
518
|
+
` (trigger=${response.triggerRunId}). Remote object is never deleted.`);
|
|
519
|
+
}
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
if (error instanceof AtlasApiError) {
|
|
524
|
+
console.error(`${LOG} unbind failed: ${error.message}`);
|
|
525
|
+
return 1;
|
|
526
|
+
}
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
318
530
|
/**
|
|
319
531
|
* Stage identity is `(repo, slug)` — a bare slug can be ambiguous across
|
|
320
532
|
* Pipelines. `--repo pipelines/<domain>` scopes the lookup server-side; an
|
|
@@ -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/repos/commands.d.ts
CHANGED
|
@@ -44,6 +44,58 @@ export declare function reposCloneCommand(args: ParsedArgs, deps?: {
|
|
|
44
44
|
* so askpass never sends ATLAS_GIT_PAT to an attacker-controlled host.
|
|
45
45
|
*/
|
|
46
46
|
export declare function normalizeCloneUrl(raw: string, env: ResolvedEnv): string;
|
|
47
|
+
type DiscoveredCliCheck = {
|
|
48
|
+
name: string;
|
|
49
|
+
script?: string;
|
|
50
|
+
command?: string[];
|
|
51
|
+
source: 'autodiscover' | 'ci.json';
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Pure discovery shared by `repos ci show` / `import`. Mirrors atlas
|
|
55
|
+
* `discoverCiChecks` / `parseCiJson` so CLI preview matches execution.
|
|
56
|
+
*/
|
|
57
|
+
export declare function discoverCliChecks({ ciJson, packageScripts, }: {
|
|
58
|
+
ciJson?: unknown;
|
|
59
|
+
packageScripts?: Record<string, string>;
|
|
60
|
+
}): {
|
|
61
|
+
ok: true;
|
|
62
|
+
checks: DiscoveredCliCheck[];
|
|
63
|
+
} | {
|
|
64
|
+
ok: false;
|
|
65
|
+
error: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Load `.seq/ci.json` / `package.json` for CI preview.
|
|
69
|
+
*
|
|
70
|
+
* Matches the runner: only a true 404 means "file absent". Invalid JSON and
|
|
71
|
+
* non-404 API errors fail closed (no silent package.json fallback over a bad
|
|
72
|
+
* `.seq/ci.json`).
|
|
73
|
+
*/
|
|
74
|
+
export declare function loadRemoteCiSources({ ctx, repoId, ref, }: {
|
|
75
|
+
ctx: CommandContext;
|
|
76
|
+
repoId: string;
|
|
77
|
+
ref: string;
|
|
78
|
+
}): Promise<{
|
|
79
|
+
ok: true;
|
|
80
|
+
ciJson?: unknown;
|
|
81
|
+
packageScripts?: Record<string, string>;
|
|
82
|
+
} | {
|
|
83
|
+
ok: false;
|
|
84
|
+
error: string;
|
|
85
|
+
}>;
|
|
86
|
+
/**
|
|
87
|
+
* Discover CI checks from a remote repo tip (pure preview — no Settings write).
|
|
88
|
+
* Uses the same autodiscovery rules as the platform runner (PLA-378).
|
|
89
|
+
*/
|
|
90
|
+
export declare function reposCiShowCommand(args: ParsedArgs): Promise<number>;
|
|
91
|
+
/** Add a check-run name to Settings `requiredChecks` (merge-blocking). */
|
|
92
|
+
export declare function reposCiRequireCommand(args: ParsedArgs): Promise<number>;
|
|
93
|
+
/**
|
|
94
|
+
* Set requiredChecks to every check discovered on the tip (opt-in bulk require).
|
|
95
|
+
*/
|
|
96
|
+
export declare function reposCiImportCommand(args: ParsedArgs): Promise<number>;
|
|
97
|
+
export declare function reposCiCommand(args: ParsedArgs): Promise<number>;
|
|
47
98
|
export declare function reposDeleteCommand(args: ParsedArgs): Promise<number>;
|
|
48
|
-
export declare const REPOS_USAGE = "usage:\n seq-studio repos list -e <env> [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] -e <env> list or create namespaces\n seq-studio repos show <ns>/<name> -e <env> repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> -e <env> [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> -e <env> [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> -e <env> [--yes] delete a repo (confirm prompt)\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
|
|
99
|
+
export declare const REPOS_USAGE = "usage:\n seq-studio repos list -e <env> [--namespace <slug>] [--mine] repos visible on the environment\n seq-studio repos namespaces [create <slug>] -e <env> list or create namespaces\n seq-studio repos show <ns>/<name> -e <env> repo detail (branches, clone URL)\n seq-studio repos create <ns>/<name> -e <env> [--default-branch b] create an empty repo\n seq-studio repos clone <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n seq-studio repos clone --url <https://\u2026/repos/<id>/git> -e <env> [--ref r] [--out dir]\n seq-studio repos clone --id <uuid> -e <env> [--ref r] [--out dir]\n smart-HTTP when ATLAS_GIT_PAT is set;\n otherwise JSON materialize (<ns>/<name>)\n seq-studio repos pull <ns>/<name> -e <env> [--ref r] [--out dir] [--force]\n materialize the tree at a ref (JSON API)\n seq-studio repos delete <ns>/<name> -e <env> [--yes] delete a repo (confirm prompt)\n seq-studio repos ci show <ns>/<name> -e <env> [--ref r] preview discovered CI checks\n seq-studio repos ci require <ns>/<name> --check <name> -e <env> reserved (refuses write until sandboxed runner)\n seq-studio repos ci import <ns>/<name> -e <env> [--ref r] reserved (refuses write until sandboxed runner)\n\n Flags: -e/--env <env> (required; see: seq-studio envs list)\n\n clone prefers real git clone (PAT via askpass \u2014 never written into the remote\n URL). Without ATLAS_GIT_PAT, <ns>/<name> falls back to the JSON API and prints\n how to get a PAT (seq-studio or Atlas UI /settings/tokens).\n PAT without Auth0 login: use --url from Repositories \u2192 Clone (or --id <uuid>).\n --ref accepts a branch, tag, or commit SHA (SHA \u2192 clone then checkout).\n\n Authenticate JSON API calls with: seq-studio login\n Authenticate git clone/push with: ATLAS_GIT_PAT (from Atlas Settings \u2192 Tokens)\n";
|
|
49
100
|
export declare function runReposCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
|
101
|
+
export {};
|