@sequenceholdings/studio-cli 0.1.22 → 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 +115 -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/functions/manifest.d.ts +22 -0
- package/dist/functions/manifest.js +45 -0
- 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 +86 -12
- package/dist/pipeline/lifecycle.d.ts +1 -12
- package/dist/pipeline/lifecycle.js +140 -35
- package/dist/pipeline/templates.js +3 -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,24 +368,40 @@ 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
|
}
|
|
318
407
|
export async function pipelineAdoptCommand(args) {
|
|
@@ -328,6 +417,14 @@ export async function pipelineAdoptCommand(args) {
|
|
|
328
417
|
}
|
|
329
418
|
const { env, token } = await envAndToken(args);
|
|
330
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
|
+
}
|
|
331
428
|
const repo = flagString(args.flags, 'repo');
|
|
332
429
|
const resourceKey = flagString(args.flags, 'resource-key') ?? stage.replace(/-/g, '_');
|
|
333
430
|
const kind = flagString(args.flags, 'kind') ?? 'job';
|
|
@@ -348,7 +445,7 @@ export async function pipelineAdoptCommand(args) {
|
|
|
348
445
|
token,
|
|
349
446
|
path: `/api/data-pipelines/stages/${stageId}/adopt`,
|
|
350
447
|
body: {
|
|
351
|
-
environment:
|
|
448
|
+
environment: target.id,
|
|
352
449
|
ref,
|
|
353
450
|
bindings: [{ resourceKey, nativeId, kind }],
|
|
354
451
|
approvedBy,
|
|
@@ -384,6 +481,14 @@ export async function pipelineUnbindCommand(args) {
|
|
|
384
481
|
}
|
|
385
482
|
const { env, token } = await envAndToken(args);
|
|
386
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
|
+
}
|
|
387
492
|
const repo = flagString(args.flags, 'repo');
|
|
388
493
|
const resourceKey = flagString(args.flags, 'resource-key');
|
|
389
494
|
const stageId = await resolveStageIdBySlug({
|
|
@@ -398,7 +503,7 @@ export async function pipelineUnbindCommand(args) {
|
|
|
398
503
|
token,
|
|
399
504
|
path: `/api/data-pipelines/stages/${stageId}/unbind`,
|
|
400
505
|
body: {
|
|
401
|
-
environment:
|
|
506
|
+
environment: target.id,
|
|
402
507
|
ref,
|
|
403
508
|
approvedBy,
|
|
404
509
|
...(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
|
|
@@ -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>;
|
package/dist/secrets/commands.js
CHANGED
|
@@ -41,6 +41,25 @@ function parseCommaList(value) {
|
|
|
41
41
|
.map((part) => part.trim())
|
|
42
42
|
.filter(Boolean);
|
|
43
43
|
}
|
|
44
|
+
function orgFlag(args) {
|
|
45
|
+
return typeof args.flags.org === 'string' ? args.flags.org.trim().toLowerCase() : undefined;
|
|
46
|
+
}
|
|
47
|
+
/** First-party Sequence Atlas envs — their Lakebase registry is org=sequence. */
|
|
48
|
+
const SEQUENCE_OWNED_ENVS = new Set(['local', 'staging', 'production']);
|
|
49
|
+
/** Refuse --org that would write a tenant row into the wrong deployment registry. */
|
|
50
|
+
function orgDeploymentError(org, envName) {
|
|
51
|
+
if (SEQUENCE_OWNED_ENVS.has(envName) && org !== 'sequence') {
|
|
52
|
+
return `--org ${org} must be used with -e ${org} (${envName} only holds the sequence secret registry)`;
|
|
53
|
+
}
|
|
54
|
+
if (!SEQUENCE_OWNED_ENVS.has(envName) && org === 'sequence') {
|
|
55
|
+
return `--org sequence is not valid on -e ${envName} (that deployment does not own the Sequence registry)`;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** Drop one trailing LF or CRLF so Windows-authored --from-file values match Unix. */
|
|
60
|
+
export function stripOneTrailingNewline(value) {
|
|
61
|
+
return value.replace(/\r?\n$/, '');
|
|
62
|
+
}
|
|
44
63
|
async function listSecrets(ctx) {
|
|
45
64
|
const { secrets } = await getJson({
|
|
46
65
|
...clientOptions(ctx),
|
|
@@ -48,11 +67,24 @@ async function listSecrets(ctx) {
|
|
|
48
67
|
});
|
|
49
68
|
return secrets;
|
|
50
69
|
}
|
|
51
|
-
async function resolveSecretByName({ ctx, name, }) {
|
|
70
|
+
async function resolveSecretByName({ ctx, name, org, }) {
|
|
52
71
|
const secrets = await listSecrets(ctx);
|
|
53
|
-
const
|
|
72
|
+
const matches = secrets.filter((row) => row.name === name);
|
|
73
|
+
const scoped = org ? matches.filter((row) => row.orgId === org) : matches;
|
|
74
|
+
if (scoped.length === 0) {
|
|
75
|
+
throw new Error(`Managed secret "${name}" not found on ${ctx.env.name}` +
|
|
76
|
+
(org ? ` org=${org}` : '') +
|
|
77
|
+
` (or you don't have access).`);
|
|
78
|
+
}
|
|
79
|
+
if (scoped.length > 1) {
|
|
80
|
+
const orgs = scoped.map((row) => row.orgId).join(', ');
|
|
81
|
+
throw new Error(`Managed secret "${name}" exists in multiple orgs (${orgs}). Pass --org <slug>.`);
|
|
82
|
+
}
|
|
83
|
+
const [secret] = scoped;
|
|
54
84
|
if (!secret) {
|
|
55
|
-
throw new Error(`Managed secret "${name}" not found on ${ctx.env.name}
|
|
85
|
+
throw new Error(`Managed secret "${name}" not found on ${ctx.env.name}` +
|
|
86
|
+
(org ? ` org=${org}` : '') +
|
|
87
|
+
` (or you don't have access).`);
|
|
56
88
|
}
|
|
57
89
|
return secret;
|
|
58
90
|
}
|
|
@@ -128,20 +160,29 @@ async function functionExistsBySlugEventually({ ctx, slug, attempts = 3, retryDe
|
|
|
128
160
|
export async function secretsCreateCommand(args) {
|
|
129
161
|
const name = args.positional[0];
|
|
130
162
|
if (!name) {
|
|
131
|
-
console.error('usage: seq-studio secrets create <NAME> -e <env> [--description text]');
|
|
163
|
+
console.error('usage: seq-studio secrets create <NAME> -e <env> [--org <slug>] [--description text]');
|
|
132
164
|
return 1;
|
|
133
165
|
}
|
|
134
166
|
const ctx = await buildContext(args);
|
|
135
167
|
const description = typeof args.flags.description === 'string' ? args.flags.description : undefined;
|
|
168
|
+
const org = orgFlag(args);
|
|
169
|
+
if (org) {
|
|
170
|
+
const mismatch = orgDeploymentError(org, ctx.env.name);
|
|
171
|
+
if (mismatch) {
|
|
172
|
+
console.error(`${LOG} ${mismatch}`);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
136
176
|
const secret = await postJson({
|
|
137
177
|
...clientOptions(ctx),
|
|
138
178
|
path: '/api/managed-secrets',
|
|
139
179
|
body: {
|
|
140
180
|
name,
|
|
141
181
|
description: description ?? null,
|
|
182
|
+
...(org ? { orgId: org } : {}),
|
|
142
183
|
},
|
|
143
184
|
});
|
|
144
|
-
console.log(`${LOG} created managed secret "${secret.name}" (${secret.id}) on ${ctx.env.name}`);
|
|
185
|
+
console.log(`${LOG} created managed secret "${secret.name}" (${secret.id}) org=${secret.orgId} on ${ctx.env.name}`);
|
|
145
186
|
return 0;
|
|
146
187
|
}
|
|
147
188
|
// ---------------------------------------------------------------------------
|
|
@@ -150,12 +191,24 @@ export async function secretsCreateCommand(args) {
|
|
|
150
191
|
export async function secretsSetCommand(args) {
|
|
151
192
|
const name = args.positional[0];
|
|
152
193
|
if (!name) {
|
|
153
|
-
console.error('usage: seq-studio secrets set <NAME> -e <env>');
|
|
194
|
+
console.error('usage: seq-studio secrets set <NAME> -e <env> [--org <slug>] [--from-file <path>]');
|
|
154
195
|
return 1;
|
|
155
196
|
}
|
|
156
197
|
const ctx = await buildContext(args);
|
|
157
|
-
const secret = await resolveSecretByName({ ctx, name });
|
|
158
|
-
const
|
|
198
|
+
const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
|
|
199
|
+
const fromFile = typeof args.flags['from-file'] === 'string' ? args.flags['from-file'] : undefined;
|
|
200
|
+
let value;
|
|
201
|
+
if (fromFile) {
|
|
202
|
+
const filePath = isAbsolute(fromFile) ? fromFile : resolve(workDir(args), fromFile);
|
|
203
|
+
if (!existsSync(filePath)) {
|
|
204
|
+
console.error(`${LOG} --from-file not found: ${filePath}`);
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
value = stripOneTrailingNewline(await readFile(filePath, 'utf8'));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
value = await promptHidden(`Default value for ${name} (input hidden): `);
|
|
211
|
+
}
|
|
159
212
|
if (!value) {
|
|
160
213
|
console.error(`${LOG} empty value — aborted`);
|
|
161
214
|
return 1;
|
|
@@ -183,7 +236,7 @@ export async function secretsListCommand(args) {
|
|
|
183
236
|
for (const secret of secrets) {
|
|
184
237
|
const defaultState = secret.hasDefaultValue ? 'default set' : 'no default';
|
|
185
238
|
const access = secret.currentUserAccess ?? 'none';
|
|
186
|
-
console.log(` ${secret.name} [${defaultState}] access=${access} attachments=${secret.attachmentCount}`);
|
|
239
|
+
console.log(` ${secret.name} org=${secret.orgId} [${defaultState}] access=${access} attachments=${secret.attachmentCount}`);
|
|
187
240
|
}
|
|
188
241
|
return 0;
|
|
189
242
|
}
|
|
@@ -194,11 +247,11 @@ export async function secretsAttachCommand(args) {
|
|
|
194
247
|
const name = args.positional[0];
|
|
195
248
|
const fnSlug = typeof args.flags.fn === 'string' ? args.flags.fn : args.positional[1];
|
|
196
249
|
if (!name || !fnSlug) {
|
|
197
|
-
console.error('usage: seq-studio secrets attach <NAME> --fn <slug> -e <env> [--env-var NAME]');
|
|
250
|
+
console.error('usage: seq-studio secrets attach <NAME> --fn <slug> -e <env> [--org <slug>] [--env-var NAME]');
|
|
198
251
|
return 1;
|
|
199
252
|
}
|
|
200
253
|
const ctx = await buildContext(args);
|
|
201
|
-
const secret = await resolveSecretByName({ ctx, name });
|
|
254
|
+
const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
|
|
202
255
|
const fn = await resolveFunctionBySlug({ ctx, slug: fnSlug });
|
|
203
256
|
const envVarName = (typeof args.flags['env-var'] === 'string' ? args.flags['env-var'] : undefined) ?? name;
|
|
204
257
|
if (secret.hasDefaultValue) {
|
|
@@ -229,11 +282,11 @@ export async function secretsDetachCommand(args) {
|
|
|
229
282
|
const name = args.positional[0];
|
|
230
283
|
const fnSlug = typeof args.flags.fn === 'string' ? args.flags.fn : args.positional[1];
|
|
231
284
|
if (!name || !fnSlug) {
|
|
232
|
-
console.error('usage: seq-studio secrets detach <NAME> --fn <slug> -e <env>');
|
|
285
|
+
console.error('usage: seq-studio secrets detach <NAME> --fn <slug> -e <env> [--org <slug>]');
|
|
233
286
|
return 1;
|
|
234
287
|
}
|
|
235
288
|
const ctx = await buildContext(args);
|
|
236
|
-
const secret = await resolveSecretByName({ ctx, name });
|
|
289
|
+
const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
|
|
237
290
|
const fn = await resolveFunctionBySlug({ ctx, slug: fnSlug });
|
|
238
291
|
await deleteJson({
|
|
239
292
|
...clientOptions(ctx),
|
|
@@ -464,11 +517,11 @@ async function attachedFunctionSlugs(ctx, secretId) {
|
|
|
464
517
|
export async function secretsVersionsCommand(args) {
|
|
465
518
|
const name = args.positional[0];
|
|
466
519
|
if (!name) {
|
|
467
|
-
console.error('usage: seq-studio secrets versions <NAME> -e <env>');
|
|
520
|
+
console.error('usage: seq-studio secrets versions <NAME> -e <env> [--org <slug>]');
|
|
468
521
|
return 1;
|
|
469
522
|
}
|
|
470
523
|
const ctx = await buildContext(args);
|
|
471
|
-
const secret = await resolveSecretByName({ ctx, name });
|
|
524
|
+
const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
|
|
472
525
|
const versions = await listSecretVersions(ctx, secret.id);
|
|
473
526
|
if (versions.length === 0) {
|
|
474
527
|
console.log(`${LOG} no versions recorded for "${name}" on ${ctx.env.name}`);
|
|
@@ -492,11 +545,11 @@ export async function secretsVersionsCommand(args) {
|
|
|
492
545
|
export async function secretsSetDefaultCommand(args) {
|
|
493
546
|
const name = args.positional[0];
|
|
494
547
|
if (!name) {
|
|
495
|
-
console.error('usage: seq-studio secrets set-default <NAME> [version] -e <env> [--yes]');
|
|
548
|
+
console.error('usage: seq-studio secrets set-default <NAME> [version] -e <env> [--org <slug>] [--yes]');
|
|
496
549
|
return 1;
|
|
497
550
|
}
|
|
498
551
|
const ctx = await buildContext(args);
|
|
499
|
-
const secret = await resolveSecretByName({ ctx, name });
|
|
552
|
+
const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
|
|
500
553
|
const versions = await listSecretVersions(ctx, secret.id);
|
|
501
554
|
if (versions.length === 0) {
|
|
502
555
|
console.error(`${LOG} "${name}" has no recorded versions`);
|
|
@@ -558,11 +611,11 @@ export async function secretsPinCommand(args) {
|
|
|
558
611
|
const name = args.positional[0];
|
|
559
612
|
const fnSlug = typeof args.flags.fn === 'string' ? args.flags.fn : undefined;
|
|
560
613
|
if (!name || !fnSlug) {
|
|
561
|
-
console.error('usage: seq-studio secrets pin <NAME> --fn <slug> [version] [--unpin] -e <env> [--yes] [--no-redeploy]');
|
|
614
|
+
console.error('usage: seq-studio secrets pin <NAME> --fn <slug> [version] [--unpin] -e <env> [--org <slug>] [--yes] [--no-redeploy]');
|
|
562
615
|
return 1;
|
|
563
616
|
}
|
|
564
617
|
const ctx = await buildContext(args);
|
|
565
|
-
const secret = await resolveSecretByName({ ctx, name });
|
|
618
|
+
const secret = await resolveSecretByName({ ctx, name, org: orgFlag(args) });
|
|
566
619
|
const fn = await resolveFunctionBySlug({ ctx, slug: fnSlug });
|
|
567
620
|
if (flagBool(args.flags, 'unpin')) {
|
|
568
621
|
const preview = [
|
|
@@ -643,15 +696,21 @@ export async function secretsPinCommand(args) {
|
|
|
643
696
|
// dispatcher
|
|
644
697
|
// ---------------------------------------------------------------------------
|
|
645
698
|
export const SECRETS_USAGE = `usage:
|
|
646
|
-
seq-studio secrets create <NAME> -e <env> [--description text]
|
|
647
|
-
|
|
699
|
+
seq-studio secrets create <NAME> -e <env> [--org <slug>] [--description text]
|
|
700
|
+
register an org-owned secret
|
|
701
|
+
seq-studio secrets set <NAME> -e <env> [--org <slug>] [--from-file <path>]
|
|
702
|
+
set the shared default value (write-only)
|
|
648
703
|
seq-studio secrets list -e <env> secrets you can see (never values)
|
|
649
|
-
seq-studio secrets attach <NAME> --fn <slug> -e <env>
|
|
650
|
-
|
|
704
|
+
seq-studio secrets attach <NAME> --fn <slug> -e <env> [--org <slug>]
|
|
705
|
+
mount default on a function env var
|
|
706
|
+
seq-studio secrets detach <NAME> --fn <slug> -e <env> [--org <slug>]
|
|
707
|
+
remove attachment
|
|
651
708
|
seq-studio secrets apply --from-env-file .env -e <env> push defaults + attach (keys default from manifest)
|
|
652
|
-
seq-studio secrets versions <NAME> -e <env>
|
|
653
|
-
seq-studio secrets set-default <NAME> [version] -e <env>
|
|
654
|
-
|
|
709
|
+
seq-studio secrets versions <NAME> -e <env> [--org <slug>] value version history (never values)
|
|
710
|
+
seq-studio secrets set-default <NAME> [version] -e <env> [--org <slug>]
|
|
711
|
+
point the default at a prior version (editor)
|
|
712
|
+
seq-studio secrets pin <NAME> --fn <slug> [version] -e <env> [--org <slug>]
|
|
713
|
+
pin one function to a version (sticky; --unpin clears)
|
|
655
714
|
|
|
656
715
|
Note: for the common deploy loop, secrets declared in managed-function.yml are
|
|
657
716
|
reconciled automatically by \`seq-studio functions deploy\` using a local .env.
|
|
@@ -659,6 +718,8 @@ export const SECRETS_USAGE = `usage:
|
|
|
659
718
|
value changes without a redeploy.
|
|
660
719
|
|
|
661
720
|
Flags: -e/--env <env> (required; see: seq-studio envs list)
|
|
721
|
+
--org <slug> (target/select managed-scope org; required when a name exists in multiple orgs)
|
|
722
|
+
set: --from-file <path> (non-interactive; file is not echoed)
|
|
662
723
|
--fn <slug> · --env-var <NAME> · --yes
|
|
663
724
|
--functions <f1,f2> · --keys <K1,K2> · --all (override key selection)
|
|
664
725
|
set-default: [version|version-row-id] (defaults to the most recent non-default) · --yes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequenceholdings/studio-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Unified Sequence Studio CLI — `seq-studio
|
|
3
|
+
"version": "0.1.24",
|
|
4
|
+
"description": "Unified Sequence Studio CLI — `seq-studio init` / `add` / `deploy` (app monorepos), `seq-studio agents`, `seq-studio process` (Lattice), `seq-studio artifact`, `seq-studio functions` / `secrets`, `seq-studio repos`, and `seq-studio auth pat`. Includes Auth0 browser login shared with seqapi.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"smol-toml": "^1.4.2",
|
|
41
41
|
"tsx": "^4.20.3",
|
|
42
42
|
"zod": "^4.1.13",
|
|
43
|
-
"@sequenceholdings/
|
|
44
|
-
"@sequenceholdings/
|
|
45
|
-
"@sequenceholdings/
|
|
43
|
+
"@sequenceholdings/artifact-studio": "0.2.1",
|
|
44
|
+
"@sequenceholdings/lattice": "0.1.2",
|
|
45
|
+
"@sequenceholdings/agent-spec": "0.1.1"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@sequenceholdings/orm": "0.1.
|
|
48
|
+
"@sequenceholdings/orm": "0.1.3",
|
|
49
49
|
"@sequenceholdings/pipeline-spec": "0.1.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependenciesMeta": {
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"@types/node": "^22.0.0",
|
|
62
62
|
"typescript": "^5.6.0",
|
|
63
63
|
"vitest": "^4.1.5",
|
|
64
|
-
"@sequenceholdings/
|
|
65
|
-
"@sequenceholdings/
|
|
64
|
+
"@sequenceholdings/pipeline-spec": "0.1.0",
|
|
65
|
+
"@sequenceholdings/orm": "0.1.3"
|
|
66
66
|
},
|
|
67
67
|
"engines": {
|
|
68
68
|
"node": ">=20"
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/** Client-side pin rules mirrored from the server deploy adapter (SEQ-2450). */
|
|
2
|
-
export declare const FULL_SHA_PATTERN: RegExp;
|
|
3
|
-
export declare const PINNED_ENVIRONMENTS: Set<string>;
|
|
4
|
-
export declare function isFullSha(ref: string): boolean;
|
|
5
|
-
export declare function requiresPinnedSha(environment: string): boolean;
|