@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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `seq-studio pipeline codegen tables` — emit one `.stage.yml` per table from a
|
|
3
|
+
* typed TypeScript authoring module (SEQ-2485).
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { tsImport } from 'tsx/esm/api';
|
|
10
|
+
const LOG = '[seq-studio]';
|
|
11
|
+
function isAuthoring(value) {
|
|
12
|
+
if (!value || typeof value !== 'object')
|
|
13
|
+
return false;
|
|
14
|
+
const record = value;
|
|
15
|
+
return (typeof record.stagePrefix === 'string' &&
|
|
16
|
+
typeof record.runtime === 'string' &&
|
|
17
|
+
typeof record.entrypoint === 'string' &&
|
|
18
|
+
Array.isArray(record.owners) &&
|
|
19
|
+
Array.isArray(record.tables));
|
|
20
|
+
}
|
|
21
|
+
function findAuthoringExport(value) {
|
|
22
|
+
if (isAuthoring(value))
|
|
23
|
+
return value;
|
|
24
|
+
if (!value || typeof value !== 'object')
|
|
25
|
+
return null;
|
|
26
|
+
const record = value;
|
|
27
|
+
for (const key of ['default', 'authoring', 'silverlakeAuthoring']) {
|
|
28
|
+
const nested = record[key];
|
|
29
|
+
if (isAuthoring(nested))
|
|
30
|
+
return nested;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
async function loadAuthoringModule(fromPath) {
|
|
35
|
+
const absolute = resolve(fromPath);
|
|
36
|
+
if (!existsSync(absolute)) {
|
|
37
|
+
throw new Error(`authoring module not found: ${absolute}`);
|
|
38
|
+
}
|
|
39
|
+
const loaded = await tsImport(pathToFileURL(absolute).href, import.meta.url);
|
|
40
|
+
if (!loaded || typeof loaded !== 'object') {
|
|
41
|
+
throw new Error(`authoring module '${absolute}' did not export an object`);
|
|
42
|
+
}
|
|
43
|
+
const record = loaded;
|
|
44
|
+
const candidate = findAuthoringExport(record.default) ??
|
|
45
|
+
findAuthoringExport(record.authoring) ??
|
|
46
|
+
findAuthoringExport(record.silverlakeAuthoring);
|
|
47
|
+
if (!candidate) {
|
|
48
|
+
throw new Error(`authoring module '${absolute}' must export default, authoring, or silverlakeAuthoring ` +
|
|
49
|
+
`as a PerTableIngestionAuthoring object (exports: ${Object.keys(record).join(', ')})`);
|
|
50
|
+
}
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
export async function pipelineCodegenTablesCommand(args) {
|
|
54
|
+
const spec = await import('@sequenceholdings/pipeline-spec');
|
|
55
|
+
const from = typeof args.flags.from === 'string'
|
|
56
|
+
? args.flags.from
|
|
57
|
+
: join(process.cwd(), 'authoring.tables.ts');
|
|
58
|
+
const dir = resolve(typeof args.flags.dir === 'string' ? args.flags.dir : '.');
|
|
59
|
+
const dryRun = args.flags['dry-run'] === true;
|
|
60
|
+
let authoring;
|
|
61
|
+
try {
|
|
62
|
+
authoring = await loadAuthoringModule(from);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error(`${LOG} ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
console.error('usage: seq-studio pipeline codegen tables --from <authoring.ts> [--dir <dir>] [--dry-run]');
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const files = spec.generatePerTableStageFiles(authoring);
|
|
70
|
+
const parseErrors = [];
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
const parsed = spec.stageSpecSchema.safeParse(file.spec);
|
|
73
|
+
if (!parsed.success) {
|
|
74
|
+
parseErrors.push(`${file.filename}: ${parsed.error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (parseErrors.length > 0) {
|
|
78
|
+
console.error(`${LOG} generated specs failed schema validation:`);
|
|
79
|
+
for (const line of parseErrors)
|
|
80
|
+
console.error(` ${line}`);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
if (dryRun) {
|
|
84
|
+
console.log(`${LOG} dry-run: would write ${files.length} stage file(s) to ${dir}`);
|
|
85
|
+
for (const file of files)
|
|
86
|
+
console.log(` ${file.filename}`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
const targets = files.map((file) => ({
|
|
90
|
+
file,
|
|
91
|
+
target: join(dir, file.filename),
|
|
92
|
+
}));
|
|
93
|
+
const seenTargets = new Set();
|
|
94
|
+
const duplicateTarget = targets.find(({ target }) => {
|
|
95
|
+
if (seenTargets.has(target))
|
|
96
|
+
return true;
|
|
97
|
+
seenTargets.add(target);
|
|
98
|
+
return false;
|
|
99
|
+
});
|
|
100
|
+
if (duplicateTarget) {
|
|
101
|
+
console.error(`${LOG} duplicate output target ${duplicateTarget.target}`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
if (args.flags.force !== true) {
|
|
105
|
+
const existingTarget = targets.find(({ target }) => existsSync(target));
|
|
106
|
+
if (existingTarget) {
|
|
107
|
+
console.error(`${LOG} ${existingTarget.target} already exists — pass --force to overwrite`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
await mkdir(dir, { recursive: true });
|
|
112
|
+
for (const { file, target } of targets) {
|
|
113
|
+
await writeFile(target, file.yaml, 'utf8');
|
|
114
|
+
console.log(`${LOG} wrote ${target}`);
|
|
115
|
+
}
|
|
116
|
+
console.log(`${LOG} next: seq-studio pipeline validate ${dir === process.cwd() ? '.' : dir}`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* the CLI without the package get a clear install hint instead of a crash.
|
|
13
13
|
*/
|
|
14
14
|
import type { ParsedArgs } from '../process/commands.js';
|
|
15
|
+
/** Conventional ORM contract catalog filename beside stage specs (CI auto-loads this). */
|
|
16
|
+
export declare const ORM_CONTRACTS_FILENAME = "orm-contracts.json";
|
|
15
17
|
/**
|
|
16
18
|
* Same classification as the orm delegate: is the pipeline-spec package
|
|
17
19
|
* itself absent (installable) or did one of its dependencies fail to load?
|
|
@@ -53,6 +55,11 @@ export declare function buildExternalAssetsRequest({ source, knownOrigins, token
|
|
|
53
55
|
export declare function configuredEnvOrigins(envs: Readonly<Record<string, {
|
|
54
56
|
url: string;
|
|
55
57
|
}>>): string[];
|
|
58
|
+
/** Resolve ORM contracts from --orm-contracts or conventional orm-contracts.json. */
|
|
59
|
+
export declare function resolveOrmContractsPath({ dir, flag, }: {
|
|
60
|
+
dir: string;
|
|
61
|
+
flag: unknown;
|
|
62
|
+
}): string | undefined;
|
|
56
63
|
export declare function pipelineValidateCommand(args: ParsedArgs): Promise<number>;
|
|
57
64
|
export declare function runPipelineCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
|
58
65
|
export {};
|
|
@@ -19,39 +19,68 @@ import { tryGetAccessToken } from '../auth.js';
|
|
|
19
19
|
import { readConfig } from '../config.js';
|
|
20
20
|
import { renderEntrypointStub, renderStageTemplate, STAGE_TYPES, } from './templates.js';
|
|
21
21
|
const LOG = '[seq-studio]';
|
|
22
|
+
/** Conventional ORM contract catalog filename beside stage specs (CI auto-loads this). */
|
|
23
|
+
export const ORM_CONTRACTS_FILENAME = 'orm-contracts.json';
|
|
22
24
|
const PIPELINE_USAGE = `usage:
|
|
23
25
|
seq-studio pipeline init --type ingestion|transformation|serving <name> [--dir <dir>]
|
|
24
26
|
scaffold <name>.stage.yml (commented template) + src/ entrypoint stub
|
|
25
27
|
|
|
26
|
-
seq-studio pipeline validate [dir] [--assets <file|url>] [--json]
|
|
28
|
+
seq-studio pipeline validate [dir] [--assets <file|url>] [--orm-contracts <file|url>] [--json]
|
|
27
29
|
run the offline spec gate: envelope + body validation, schema_ref
|
|
28
30
|
resolution, and repo-level graph validation (references, single-writer,
|
|
29
31
|
cycles, column subsets). --assets supplies a registry asset export for
|
|
30
|
-
cross-Pipeline references
|
|
32
|
+
cross-Pipeline references; --orm-contracts (or orm-contracts.json in dir)
|
|
33
|
+
supplies ORM synced-table contracts for serving satisfies pins. Exits 1 on
|
|
34
|
+
any error-severity finding.
|
|
31
35
|
|
|
32
|
-
seq-studio pipeline
|
|
36
|
+
seq-studio pipeline codegen tables [--from <authoring.ts>] [--dir <dir>] [--dry-run] [--force]
|
|
37
|
+
emit one <stage>.stage.yml per table from a typed PerTableIngestionAuthoring
|
|
38
|
+
module (default export, authoring, or silverlakeAuthoring export name)
|
|
39
|
+
|
|
40
|
+
seq-studio pipeline plan --repo pipelines/<slug> --ref <sha|branch> -e <env> [--target <id>] [--json]
|
|
33
41
|
materialize + SDK/graph + compile + live-diff + provision findings
|
|
34
42
|
(no Databricks CLI / DAB validate on Atlas); exit 1 on destructive findings
|
|
35
43
|
|
|
36
|
-
seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha> -e <env>
|
|
37
|
-
[--approved-by <sub>] [--no-wait] [--json]
|
|
44
|
+
seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha|branch> -e <env>
|
|
45
|
+
[--target <id>] [--approved-by <sub>] [--no-wait] [--json]
|
|
38
46
|
plan then enqueue Trigger deploy (DAB bundle validate hard-gates before
|
|
39
|
-
bundle deploy);
|
|
47
|
+
bundle deploy); use a full 40-hex SHA for explicit rollback deployments
|
|
40
48
|
|
|
41
49
|
seq-studio pipeline promote --stage <slug> --version <v> -e <env>
|
|
42
|
-
[--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]
|
|
50
|
+
[--target <id>] [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]
|
|
43
51
|
promote a validated version to the next environment (approvals are
|
|
44
52
|
self-recorded: --approved-by must name the authenticated caller)
|
|
45
53
|
|
|
46
|
-
seq-studio pipeline rollback --stage <slug> -e <env> [--repo pipelines/<slug>]
|
|
54
|
+
seq-studio pipeline rollback --stage <slug> -e <env> [--target <id>] [--repo pipelines/<slug>]
|
|
47
55
|
[--approved-by <you>] [--no-wait]
|
|
48
56
|
redeploy the previously retired deployment's version (production/
|
|
49
57
|
banksouth require --approved-by — approvals are explicit even for
|
|
50
58
|
rollbacks)
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
Unless --no-wait is set, deploy, promote, and rollback report status or
|
|
61
|
+
status-detail changes while waiting, then emit a 20-second progress heartbeat.
|
|
62
|
+
Terminal output includes the deployment id and elapsed time; it also includes
|
|
63
|
+
a Trigger run id when the deployment provides one.
|
|
64
|
+
|
|
65
|
+
seq-studio pipeline adopt --stage <slug> --ref <sha|branch> -e <env>
|
|
66
|
+
[--target <id>] --native-id <id> [--resource-key <key>] [--kind job|dlt_pipeline]
|
|
67
|
+
--approved-by <you> [--old-source-removal-pr <url>]
|
|
68
|
+
[--repo pipelines/<slug>] [--json]
|
|
69
|
+
bind a live Databricks job/pipeline to the stage without recreation
|
|
70
|
+
(SEQ-2449). Always requires --approved-by. Monorepo-declared keys also
|
|
71
|
+
require --old-source-removal-pr.
|
|
72
|
+
|
|
73
|
+
seq-studio pipeline unbind --stage <slug> --ref <sha|branch> -e <env> [--target <id>]
|
|
74
|
+
--approved-by <you> [--resource-key <key>]
|
|
75
|
+
[--repo pipelines/<slug>] [--json]
|
|
76
|
+
release an adopted binding; the remote object stays live (never deleted)
|
|
77
|
+
|
|
78
|
+
seq-studio pipeline run-now --stage <slug> -e <env> [--target <id>] [--repo pipelines/<slug>] [--json]
|
|
53
79
|
fire the stage's active deployment resource (job run-now / DLT
|
|
54
80
|
start_update) and print the Databricks run URL
|
|
81
|
+
|
|
82
|
+
-e/--env selects the Atlas connection. --target selects the logical target
|
|
83
|
+
advertised by that deployment; it is inferred when the endpoint exposes one target.
|
|
55
84
|
`;
|
|
56
85
|
/**
|
|
57
86
|
* Same classification as the orm delegate: is the pipeline-spec package
|
|
@@ -212,6 +241,34 @@ async function loadExternalAssets(spec, source) {
|
|
|
212
241
|
}
|
|
213
242
|
return spec.externalAssetsExportSchema.parse(JSON.parse(raw));
|
|
214
243
|
}
|
|
244
|
+
async function loadOrmContracts(spec, source) {
|
|
245
|
+
let raw;
|
|
246
|
+
if (/^https?:\/\//.test(source)) {
|
|
247
|
+
const config = await readConfig();
|
|
248
|
+
const token = await tryGetAccessToken();
|
|
249
|
+
const request = buildExternalAssetsRequest({
|
|
250
|
+
source,
|
|
251
|
+
knownOrigins: configuredEnvOrigins(config.envs),
|
|
252
|
+
token,
|
|
253
|
+
});
|
|
254
|
+
const response = await fetch(request.url, request.init);
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw new Error(`fetching ORM contracts catalog failed: ${response.status} ${response.statusText}`);
|
|
257
|
+
}
|
|
258
|
+
raw = await response.text();
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
raw = await readFile(resolve(source), 'utf8');
|
|
262
|
+
}
|
|
263
|
+
return spec.ormContractCatalogSchema.parse(JSON.parse(raw));
|
|
264
|
+
}
|
|
265
|
+
/** Resolve ORM contracts from --orm-contracts or conventional orm-contracts.json. */
|
|
266
|
+
export function resolveOrmContractsPath({ dir, flag, }) {
|
|
267
|
+
if (typeof flag === 'string')
|
|
268
|
+
return flag;
|
|
269
|
+
const conventional = join(dir, ORM_CONTRACTS_FILENAME);
|
|
270
|
+
return existsSync(conventional) ? conventional : undefined;
|
|
271
|
+
}
|
|
215
272
|
function printHumanReport(report) {
|
|
216
273
|
for (const finding of report.findings) {
|
|
217
274
|
const prefix = finding.severity === 'error' ? 'error' : 'warning';
|
|
@@ -283,7 +340,28 @@ export async function pipelineValidateCommand(args) {
|
|
|
283
340
|
});
|
|
284
341
|
}
|
|
285
342
|
}
|
|
286
|
-
|
|
343
|
+
let ormContracts;
|
|
344
|
+
const ormContractsPath = resolveOrmContractsPath({ dir, flag: args.flags['orm-contracts'] });
|
|
345
|
+
if (ormContractsPath) {
|
|
346
|
+
try {
|
|
347
|
+
ormContracts = await loadOrmContracts(spec, ormContractsPath);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
return emit({
|
|
351
|
+
ok: false,
|
|
352
|
+
dir: dirArg,
|
|
353
|
+
stages: pipeline.stages.map((stage) => stage.stage),
|
|
354
|
+
findings: [
|
|
355
|
+
{
|
|
356
|
+
severity: 'error',
|
|
357
|
+
code: 'orm_contracts_unreadable',
|
|
358
|
+
message: error instanceof Error ? error.message : String(error),
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const result = spec.validateSpecGraph(pipeline.stages, externalAssets, ormContracts !== undefined ? { ormContracts } : undefined);
|
|
287
365
|
return emit({
|
|
288
366
|
ok: result.ok,
|
|
289
367
|
dir: dirArg,
|
|
@@ -297,6 +375,23 @@ export async function runPipelineCommand(sub, args) {
|
|
|
297
375
|
return pipelineInitCommand(args);
|
|
298
376
|
case 'validate':
|
|
299
377
|
return pipelineValidateCommand(args);
|
|
378
|
+
case 'codegen': {
|
|
379
|
+
const codegenSub = args.positional[0];
|
|
380
|
+
if (codegenSub === 'tables') {
|
|
381
|
+
const { pipelineCodegenTablesCommand } = await import('./codegen.js');
|
|
382
|
+
return pipelineCodegenTablesCommand({ ...args, positional: args.positional.slice(1) });
|
|
383
|
+
}
|
|
384
|
+
console.error(`${LOG} unknown pipeline codegen target '${codegenSub ?? ''}' — expected: tables`);
|
|
385
|
+
return 1;
|
|
386
|
+
}
|
|
387
|
+
case 'adopt': {
|
|
388
|
+
const { pipelineAdoptCommand } = await import('./lifecycle.js');
|
|
389
|
+
return pipelineAdoptCommand(args);
|
|
390
|
+
}
|
|
391
|
+
case 'unbind': {
|
|
392
|
+
const { pipelineUnbindCommand } = await import('./lifecycle.js');
|
|
393
|
+
return pipelineUnbindCommand(args);
|
|
394
|
+
}
|
|
300
395
|
case 'plan': {
|
|
301
396
|
const { pipelinePlanCommand } = await import('./lifecycle.js');
|
|
302
397
|
return pipelinePlanCommand(args);
|
|
@@ -37,22 +37,13 @@ export interface DeploymentDetail {
|
|
|
37
37
|
id: string;
|
|
38
38
|
status: string;
|
|
39
39
|
statusDetail: string | null;
|
|
40
|
+
triggerRunId?: string | null;
|
|
40
41
|
}
|
|
41
|
-
/**
|
|
42
|
-
* The CLI's local dev-loop env is named `local` — chosen so `-e local` lines
|
|
43
|
-
* up with `seqapi -e local` / `artifact-studio --env local` (see `config.ts`
|
|
44
|
-
* `BUILT_IN_ENV_URLS`). There is no `local` in the server's deploy-lifecycle
|
|
45
|
-
* enum (`DEPLOY_ENVIRONMENTS` in `atlas/src/server/services/data-pipelines/schema.ts`
|
|
46
|
-
* is `dev | staging | production | banksouth`); the server's name for that
|
|
47
|
-
* same developer-loop target is `dev` (`targetFactsForEnvironment` treats
|
|
48
|
-
* `dev` as the one target exempt from a `lakebase_branch` binding). Map at
|
|
49
|
-
* this one boundary — every lifecycle request body funnels through here —
|
|
50
|
-
* so the CLI never sends the wire-invalid `local` and every other env name
|
|
51
|
-
* passes through unchanged.
|
|
52
|
-
*/
|
|
53
42
|
export declare function deployEnvironmentForEnv(env: ResolvedEnv): string;
|
|
54
43
|
export declare function pipelinePlanCommand(args: ParsedArgs): Promise<number>;
|
|
55
44
|
export declare function pipelineDeployCommand(args: ParsedArgs): Promise<number>;
|
|
56
45
|
export declare function pipelinePromoteCommand(args: ParsedArgs): Promise<number>;
|
|
57
46
|
export declare function pipelineRunNowCommand(args: ParsedArgs): Promise<number>;
|
|
58
47
|
export declare function pipelineRollbackCommand(args: ParsedArgs): Promise<number>;
|
|
48
|
+
export declare function pipelineAdoptCommand(args: ParsedArgs): Promise<number>;
|
|
49
|
+
export declare function pipelineUnbindCommand(args: ParsedArgs): Promise<number>;
|