@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
|
@@ -202,6 +202,12 @@ limits:
|
|
|
202
202
|
max_instances: 3
|
|
203
203
|
invoke_rate_per_minute: 60
|
|
204
204
|
|
|
205
|
+
# Optional source-IP gate, checked before invocation permissions. Omit or leave
|
|
206
|
+
# empty for unrestricted source IPs.
|
|
207
|
+
# invocation:
|
|
208
|
+
# allowed_ip_ranges:
|
|
209
|
+
# - 203.0.113.0/24
|
|
210
|
+
|
|
205
211
|
# Env-var names the function expects (UPPER_SNAKE_CASE). Values are read
|
|
206
212
|
# from a local .env file at deploy time — they are never committed or bundled.
|
|
207
213
|
secrets: []
|
|
@@ -325,10 +331,6 @@ export async function functionsInitCommand(args) {
|
|
|
325
331
|
console.log('');
|
|
326
332
|
console.log('Next steps (init is only needed when creating a function from scratch):');
|
|
327
333
|
console.log(` cd ${target}`);
|
|
328
|
-
console.log(' pnpm install # installs deps for local dev + pins versions via Chainguard.');
|
|
329
|
-
console.log(' # Requires Chainguard credentials (Sequence-internal). Without');
|
|
330
|
-
console.log(' # them this 401s — delete the scaffolded .npmrc and install from');
|
|
331
|
-
console.log(' # public npm instead (the deploy worker re-resolves server-side).');
|
|
332
334
|
console.log(' seq-studio functions build # local pre-flight checks');
|
|
333
335
|
console.log(' # Add secret names to managed-function.yml (secrets: [MY_SECRET]) and values to .env');
|
|
334
336
|
console.log(' seq-studio functions deploy -e <env> # upload, apply secrets, then deploy');
|
|
@@ -387,6 +389,15 @@ async function buildFromResolvedSource({ spec, source, }) {
|
|
|
387
389
|
// ---------------------------------------------------------------------------
|
|
388
390
|
// deploy
|
|
389
391
|
// ---------------------------------------------------------------------------
|
|
392
|
+
function buildInvocationIpPreviewLines({ ranges }) {
|
|
393
|
+
if (ranges.length === 0) {
|
|
394
|
+
return [`${LOG} invocation IPs: unrestricted`];
|
|
395
|
+
}
|
|
396
|
+
return [
|
|
397
|
+
`${LOG} invocation IPs:`,
|
|
398
|
+
...ranges.map((range) => `${LOG} ${range}`),
|
|
399
|
+
];
|
|
400
|
+
}
|
|
390
401
|
async function getFunctionDetail(ctx, functionId) {
|
|
391
402
|
try {
|
|
392
403
|
return await getJson({
|
|
@@ -561,7 +572,9 @@ async function deployFromResolvedSource({ args, spec, ctx: earlyCtx, source, })
|
|
|
561
572
|
preview.push(...buildEgressPreviewLines(LOG, [
|
|
562
573
|
...manifestEgressHosts(manifest.egress),
|
|
563
574
|
...manifestEgressIpRanges(manifest.egress),
|
|
564
|
-
])
|
|
575
|
+
]), ...buildInvocationIpPreviewLines({
|
|
576
|
+
ranges: manifest.invocation.allowed_ip_ranges,
|
|
577
|
+
}));
|
|
565
578
|
if (undeclaredEnvKeys.length > 0) {
|
|
566
579
|
preview.push(`${LOG} note: .env has ${undeclaredEnvKeys.join(', ')} — not declared in manifest, ignored`);
|
|
567
580
|
}
|
|
@@ -5,6 +5,20 @@ import { z } from 'zod';
|
|
|
5
5
|
* as artifact-studio's duplicated manifest. Keep the two in sync.
|
|
6
6
|
*/
|
|
7
7
|
export declare const MF_MANIFEST_FILENAME = "managed-function.yml";
|
|
8
|
+
export declare const managedFunctionAuthorizationAdapterSchema: z.ZodEnum<{
|
|
9
|
+
"encompass.loan-read-by-guid": "encompass.loan-read-by-guid";
|
|
10
|
+
"encompass.loan-read-by-number": "encompass.loan-read-by-number";
|
|
11
|
+
"encompass.loan-search": "encompass.loan-search";
|
|
12
|
+
}>;
|
|
13
|
+
export type ManagedFunctionAuthorizationAdapter = z.infer<typeof managedFunctionAuthorizationAdapterSchema>;
|
|
14
|
+
export declare const managedFunctionAuthorizationSchema: z.ZodObject<{
|
|
15
|
+
version: z.ZodLiteral<1>;
|
|
16
|
+
adapter: z.ZodEnum<{
|
|
17
|
+
"encompass.loan-read-by-guid": "encompass.loan-read-by-guid";
|
|
18
|
+
"encompass.loan-read-by-number": "encompass.loan-read-by-number";
|
|
19
|
+
"encompass.loan-search": "encompass.loan-search";
|
|
20
|
+
}>;
|
|
21
|
+
}, z.core.$strict>;
|
|
8
22
|
/**
|
|
9
23
|
* CLI-side copy of the server's egress entry validation (see
|
|
10
24
|
* atlas/src/server/services/managed-functions/egress.ts). Returns the
|
|
@@ -56,9 +70,20 @@ export declare const managedFunctionManifestSchema: z.ZodObject<{
|
|
|
56
70
|
max_instances: z.ZodDefault<z.ZodNumber>;
|
|
57
71
|
invoke_rate_per_minute: z.ZodDefault<z.ZodNumber>;
|
|
58
72
|
}, z.core.$strip>>;
|
|
73
|
+
invocation: z.ZodDefault<z.ZodObject<{
|
|
74
|
+
allowed_ip_ranges: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
75
|
+
}, z.core.$strip>>;
|
|
59
76
|
secrets: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
60
77
|
egress: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
61
78
|
service_account: z.ZodOptional<z.ZodString>;
|
|
79
|
+
authorization: z.ZodOptional<z.ZodObject<{
|
|
80
|
+
version: z.ZodLiteral<1>;
|
|
81
|
+
adapter: z.ZodEnum<{
|
|
82
|
+
"encompass.loan-read-by-guid": "encompass.loan-read-by-guid";
|
|
83
|
+
"encompass.loan-read-by-number": "encompass.loan-read-by-number";
|
|
84
|
+
"encompass.loan-search": "encompass.loan-search";
|
|
85
|
+
}>;
|
|
86
|
+
}, z.core.$strict>>;
|
|
62
87
|
input_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
63
88
|
output_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
64
89
|
capabilities: z.ZodDefault<z.ZodObject<{
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/**
|
|
3
4
|
* CLI-side mirror of the server manifest schema at
|
|
@@ -6,6 +7,22 @@ import { z } from 'zod';
|
|
|
6
7
|
*/
|
|
7
8
|
export const MF_MANIFEST_FILENAME = 'managed-function.yml';
|
|
8
9
|
const SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
10
|
+
export const managedFunctionAuthorizationAdapterSchema = z.enum([
|
|
11
|
+
'encompass.loan-read-by-guid',
|
|
12
|
+
'encompass.loan-read-by-number',
|
|
13
|
+
'encompass.loan-search',
|
|
14
|
+
]);
|
|
15
|
+
export const managedFunctionAuthorizationSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
version: z.literal(1),
|
|
18
|
+
adapter: managedFunctionAuthorizationAdapterSchema,
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
const AUTHORIZATION_ADAPTER_BY_FUNCTION_ID = {
|
|
22
|
+
'encompass-get-common-loan-fields': 'encompass.loan-read-by-guid',
|
|
23
|
+
'encompass-get-loan-by-number': 'encompass.loan-read-by-number',
|
|
24
|
+
'encompass-search-pipeline': 'encompass.loan-search',
|
|
25
|
+
};
|
|
9
26
|
/** RFC 1035 label: alnum, optional inner dashes, max 63 chars. */
|
|
10
27
|
const DNS_LABEL_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
11
28
|
/**
|
|
@@ -499,6 +516,43 @@ const SERVICE_ACCOUNT_REF_RE = /^([a-z][a-z0-9-]{1,98}|[0-9a-fA-F]{8}-[0-9a-fA-F
|
|
|
499
516
|
* atlas/src/server/services/managed-functions/manifest.ts.
|
|
500
517
|
*/
|
|
501
518
|
const GRAPHQL_OPERATION_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
519
|
+
/**
|
|
520
|
+
* Fast local validation for the declarative invocation policy. Atlas remains
|
|
521
|
+
* authoritative and canonicalizes network addresses during version upload;
|
|
522
|
+
* this mirror prevents a malformed bundle from reaching the network.
|
|
523
|
+
*/
|
|
524
|
+
const invocationIpRangeSchema = z
|
|
525
|
+
.string()
|
|
526
|
+
.trim()
|
|
527
|
+
.min(1)
|
|
528
|
+
.max(128)
|
|
529
|
+
.superRefine((value, ctx) => {
|
|
530
|
+
const parts = value.split('/');
|
|
531
|
+
if (parts.length > 2) {
|
|
532
|
+
ctx.addIssue({ code: 'custom', message: `invalid IP address or CIDR range: ${value}` });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const [address, prefixText] = parts;
|
|
536
|
+
const family = isIP(address ?? '');
|
|
537
|
+
if (family === 0) {
|
|
538
|
+
ctx.addIssue({ code: 'custom', message: `invalid IP address or CIDR range: ${value}` });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (prefixText === undefined)
|
|
542
|
+
return;
|
|
543
|
+
if (!/^\d+$/.test(prefixText)) {
|
|
544
|
+
ctx.addIssue({ code: 'custom', message: `invalid CIDR prefix: ${value}` });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const prefix = Number(prefixText);
|
|
548
|
+
const maxPrefix = family === 4 ? 32 : 128;
|
|
549
|
+
if (prefix < 1 || prefix > maxPrefix) {
|
|
550
|
+
ctx.addIssue({
|
|
551
|
+
code: 'custom',
|
|
552
|
+
message: `CIDR prefix must be between 1 and ${maxPrefix}: ${value}`,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
});
|
|
502
556
|
export const managedFunctionManifestSchema = z.object({
|
|
503
557
|
schema_version: z.literal(1).default(1),
|
|
504
558
|
function: z.object({
|
|
@@ -536,6 +590,16 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
536
590
|
max_instances: 3,
|
|
537
591
|
invoke_rate_per_minute: 60,
|
|
538
592
|
}),
|
|
593
|
+
/**
|
|
594
|
+
* Source-IP gate applied by the Atlas invocation gateway before FGA.
|
|
595
|
+
* Empty or omitted means unrestricted. The policy is versioned with the
|
|
596
|
+
* bundle and becomes live atomically with that version.
|
|
597
|
+
*/
|
|
598
|
+
invocation: z
|
|
599
|
+
.object({
|
|
600
|
+
allowed_ip_ranges: z.array(invocationIpRangeSchema).max(64).default([]),
|
|
601
|
+
})
|
|
602
|
+
.default({ allowed_ip_ranges: [] }),
|
|
539
603
|
secrets: z
|
|
540
604
|
.array(z.string().regex(SECRET_NAME_RE, 'secret names must be UPPER_SNAKE_CASE'))
|
|
541
605
|
.max(32)
|
|
@@ -571,6 +635,11 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
571
635
|
.string()
|
|
572
636
|
.regex(SERVICE_ACCOUNT_REF_RE, 'service_account must be a platform service-account slug (lowercase alphanumeric with hyphens) or uuid')
|
|
573
637
|
.optional(),
|
|
638
|
+
/**
|
|
639
|
+
* Server-owned resource authorization. Authors select a reviewed adapter;
|
|
640
|
+
* they cannot provide input/output paths or executable policy.
|
|
641
|
+
*/
|
|
642
|
+
authorization: managedFunctionAuthorizationSchema.optional(),
|
|
574
643
|
input_schema: z.record(z.string(), z.unknown()).optional(),
|
|
575
644
|
output_schema: z.record(z.string(), z.unknown()).optional(),
|
|
576
645
|
/**
|
|
@@ -616,6 +685,30 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
616
685
|
.default({}),
|
|
617
686
|
}).default({ uses: [], gates: [], data: {} }),
|
|
618
687
|
}).superRefine((manifest, ctx) => {
|
|
688
|
+
const expectedAdapter = AUTHORIZATION_ADAPTER_BY_FUNCTION_ID[manifest.function.id];
|
|
689
|
+
if (expectedAdapter !== undefined && manifest.authorization === undefined) {
|
|
690
|
+
ctx.addIssue({
|
|
691
|
+
code: 'custom',
|
|
692
|
+
message: `function "${manifest.function.id}" requires authorization adapter "${expectedAdapter}"`,
|
|
693
|
+
path: ['authorization'],
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
else if (expectedAdapter !== undefined &&
|
|
697
|
+
manifest.authorization?.adapter !== expectedAdapter) {
|
|
698
|
+
ctx.addIssue({
|
|
699
|
+
code: 'custom',
|
|
700
|
+
message: `function "${manifest.function.id}" must use authorization adapter "${expectedAdapter}"`,
|
|
701
|
+
path: ['authorization', 'adapter'],
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
else if (expectedAdapter === undefined &&
|
|
705
|
+
manifest.authorization !== undefined) {
|
|
706
|
+
ctx.addIssue({
|
|
707
|
+
code: 'custom',
|
|
708
|
+
message: `function "${manifest.function.id}" is not registered for a server-owned authorization adapter`,
|
|
709
|
+
path: ['authorization'],
|
|
710
|
+
});
|
|
711
|
+
}
|
|
619
712
|
for (const message of validateCapabilityPins({
|
|
620
713
|
uses: manifest.capabilities.uses,
|
|
621
714
|
roles: manifest.capabilities.roles,
|
package/dist/login.js
CHANGED
|
@@ -5,7 +5,7 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { deleteRealmTokens, realmForEnv, saveTokens, seqapiTokenPath, SEQUENCE_AUTH_REALM, SEQUENCE_REALM, verifyTokenMatchesRealm, } from './auth.js';
|
|
8
|
-
import { manualEnvConfigHint } from './config.js';
|
|
8
|
+
import { localAuthRealmOptions, manualEnvConfigHint } from './config.js';
|
|
9
9
|
import { bootstrapUrl, clearCatalog, fetchCatalog } from './env-catalog.js';
|
|
10
10
|
const DEFAULT_REDIRECT_PORT = 5099;
|
|
11
11
|
const DEFAULT_LOGIN_TIMEOUT_MS = 300_000;
|
|
@@ -216,7 +216,7 @@ export async function refreshCatalogAfterLogin() {
|
|
|
216
216
|
console.log(manualEnvConfigHint());
|
|
217
217
|
}
|
|
218
218
|
export async function login(envName) {
|
|
219
|
-
const realm = await realmForEnv(envName);
|
|
219
|
+
const realm = await realmForEnv(envName, await localAuthRealmOptions(envName));
|
|
220
220
|
if (envName && realm.name === SEQUENCE_REALM && envName !== SEQUENCE_REALM) {
|
|
221
221
|
// A name that only exists in the Sequence catalog (staging, banksouth…)
|
|
222
222
|
// resolves to the Sequence realm — one login covers all of those. A typo'd
|
|
@@ -241,7 +241,7 @@ function legacyArtifactTokenPath() {
|
|
|
241
241
|
return join(homedir(), '.config', 'sequence-artifact-studio', 'tokens.json');
|
|
242
242
|
}
|
|
243
243
|
export async function logout(envName) {
|
|
244
|
-
const realm = await realmForEnv(envName);
|
|
244
|
+
const realm = await realmForEnv(envName, await localAuthRealmOptions(envName));
|
|
245
245
|
await deleteRealmTokens(realm.name);
|
|
246
246
|
if (realm.name === SEQUENCE_REALM) {
|
|
247
247
|
await rm(legacyArtifactTokenPath(), { force: true });
|
package/dist/main.d.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* seq-studio argv router.
|
|
3
3
|
*
|
|
4
4
|
* Top-level commands:
|
|
5
|
+
* seq-studio init scaffold an app monorepo (sequence.app.yml)
|
|
6
|
+
* seq-studio add add a primitive to an app monorepo
|
|
7
|
+
* seq-studio deploy deploy all primitives from sequence.app.yml
|
|
5
8
|
* seq-studio process <sub> manage Lattice processes
|
|
6
9
|
* seq-studio artifact <sub> manage Artifact Studio apps
|
|
7
10
|
* seq-studio functions <sub> manage Managed Functions
|
package/dist/main.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* seq-studio argv router.
|
|
3
3
|
*
|
|
4
4
|
* Top-level commands:
|
|
5
|
+
* seq-studio init scaffold an app monorepo (sequence.app.yml)
|
|
6
|
+
* seq-studio add add a primitive to an app monorepo
|
|
7
|
+
* seq-studio deploy deploy all primitives from sequence.app.yml
|
|
5
8
|
* seq-studio process <sub> manage Lattice processes
|
|
6
9
|
* seq-studio artifact <sub> manage Artifact Studio apps
|
|
7
10
|
* seq-studio functions <sub> manage Managed Functions
|
|
@@ -21,6 +24,9 @@ import { applyCommand, bundleCommand, doctorCommand, initCommand, lintCommand, p
|
|
|
21
24
|
// Lazy-load artifact delegate: published @sequenceholdings/artifact-studio/cli
|
|
22
25
|
// still auto-runs runCli() at module load; importing it here breaks doctor/process.
|
|
23
26
|
const TOP_LEVEL_USAGE = `usage:
|
|
27
|
+
seq-studio init <dir> --with <kinds> scaffold an app monorepo (sequence.app.yml)
|
|
28
|
+
seq-studio add <kind> <name> add a primitive to the current app monorepo
|
|
29
|
+
seq-studio deploy -e <env> deploy primitives from sequence.app.yml
|
|
24
30
|
seq-studio process <sub> [args] lint | plan | apply | test | simulate | bundle | init
|
|
25
31
|
seq-studio artifact <sub> [args] init | build | plan | deploy | dev | list | show | pull | promote | rollback
|
|
26
32
|
seq-studio functions <sub> [args] init | build | deploy | list | show | logs | promote | rollback | delete
|
|
@@ -65,6 +71,21 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
65
71
|
return namespace ? 0 : 1;
|
|
66
72
|
}
|
|
67
73
|
switch (namespace) {
|
|
74
|
+
case 'init': {
|
|
75
|
+
const { runAppInitCommand } = await import('./app/commands.js');
|
|
76
|
+
return runAppInitCommand(parseArgs([sub, ...rest].filter((a) => Boolean(a))));
|
|
77
|
+
}
|
|
78
|
+
case 'add': {
|
|
79
|
+
const { runAppAddCommand } = await import('./app/commands.js');
|
|
80
|
+
return runAppAddCommand({
|
|
81
|
+
kindArg: sub,
|
|
82
|
+
args: parseArgs(rest),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
case 'deploy': {
|
|
86
|
+
const { runAppDeployCommand } = await import('./app/commands.js');
|
|
87
|
+
return runAppDeployCommand(parseArgs([sub, ...rest].filter((a) => Boolean(a))));
|
|
88
|
+
}
|
|
68
89
|
case 'process':
|
|
69
90
|
return runProcessNamespace(sub, rest);
|
|
70
91
|
case 'artifact': {
|
|
@@ -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,52 +19,72 @@ 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]
|
|
41
|
+
enqueue a durable plan, poll progress, then print the completed
|
|
33
42
|
materialize + SDK/graph + compile + live-diff + provision findings
|
|
34
43
|
(no Databricks CLI / DAB validate on Atlas); exit 1 on destructive findings
|
|
35
44
|
|
|
36
|
-
seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha> -e <env>
|
|
37
|
-
[--approved-by <sub>] [--no-wait] [--json]
|
|
45
|
+
seq-studio pipeline deploy --repo pipelines/<slug> --ref <sha|branch> -e <env>
|
|
46
|
+
[--target <id>] [--approved-by <sub>] [--run-now] [--no-wait] [--json]
|
|
38
47
|
plan then enqueue Trigger deploy (DAB bundle validate hard-gates before
|
|
39
|
-
bundle deploy)
|
|
48
|
+
bundle deploy). Default is plant-only: jobs/pipelines are created, not
|
|
49
|
+
run. Pass --run-now to run in-unit producer roots after bundle deploy
|
|
50
|
+
and wait before serving sync create. Use a full 40-hex SHA for explicit
|
|
51
|
+
rollback deployments
|
|
40
52
|
|
|
41
53
|
seq-studio pipeline promote --stage <slug> --version <v> -e <env>
|
|
42
|
-
[--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]
|
|
54
|
+
[--target <id>] [--repo pipelines/<slug>] [--approved-by <you>] [--no-wait]
|
|
43
55
|
promote a validated version to the next environment (approvals are
|
|
44
56
|
self-recorded: --approved-by must name the authenticated caller)
|
|
45
57
|
|
|
46
|
-
seq-studio pipeline rollback --stage <slug> -e <env> [--repo pipelines/<slug>]
|
|
58
|
+
seq-studio pipeline rollback --stage <slug> -e <env> [--target <id>] [--repo pipelines/<slug>]
|
|
47
59
|
[--approved-by <you>] [--no-wait]
|
|
48
60
|
redeploy the previously retired deployment's version (production/
|
|
49
61
|
banksouth require --approved-by — approvals are explicit even for
|
|
50
62
|
rollbacks)
|
|
51
63
|
|
|
64
|
+
Unless --no-wait is set, deploy, promote, and rollback report status or
|
|
65
|
+
status-detail changes while waiting, then emit a 20-second progress heartbeat.
|
|
66
|
+
Terminal output includes the deployment id and elapsed time; it also includes
|
|
67
|
+
a Trigger run id when the deployment provides one.
|
|
68
|
+
|
|
52
69
|
seq-studio pipeline adopt --stage <slug> --ref <sha|branch> -e <env>
|
|
53
|
-
--native-id <id> [--resource-key <key>] [--kind job|dlt_pipeline]
|
|
70
|
+
[--target <id>] --native-id <id> [--resource-key <key>] [--kind job|dlt_pipeline]
|
|
54
71
|
--approved-by <you> [--old-source-removal-pr <url>]
|
|
55
72
|
[--repo pipelines/<slug>] [--json]
|
|
56
73
|
bind a live Databricks job/pipeline to the stage without recreation
|
|
57
74
|
(SEQ-2449). Always requires --approved-by. Monorepo-declared keys also
|
|
58
75
|
require --old-source-removal-pr.
|
|
59
76
|
|
|
60
|
-
seq-studio pipeline unbind --stage <slug> --ref <sha|branch> -e <env>
|
|
77
|
+
seq-studio pipeline unbind --stage <slug> --ref <sha|branch> -e <env> [--target <id>]
|
|
61
78
|
--approved-by <you> [--resource-key <key>]
|
|
62
79
|
[--repo pipelines/<slug>] [--json]
|
|
63
80
|
release an adopted binding; the remote object stays live (never deleted)
|
|
64
81
|
|
|
65
|
-
seq-studio pipeline run-now --stage <slug> -e <env> [--repo pipelines/<slug>] [--json]
|
|
82
|
+
seq-studio pipeline run-now --stage <slug> -e <env> [--target <id>] [--repo pipelines/<slug>] [--json]
|
|
66
83
|
fire the stage's active deployment resource (job run-now / DLT
|
|
67
84
|
start_update) and print the Databricks run URL
|
|
85
|
+
|
|
86
|
+
-e/--env selects the Atlas connection. --target selects the logical target
|
|
87
|
+
advertised by that deployment; it is inferred when the endpoint exposes one target.
|
|
68
88
|
`;
|
|
69
89
|
/**
|
|
70
90
|
* Same classification as the orm delegate: is the pipeline-spec package
|
|
@@ -225,6 +245,34 @@ async function loadExternalAssets(spec, source) {
|
|
|
225
245
|
}
|
|
226
246
|
return spec.externalAssetsExportSchema.parse(JSON.parse(raw));
|
|
227
247
|
}
|
|
248
|
+
async function loadOrmContracts(spec, source) {
|
|
249
|
+
let raw;
|
|
250
|
+
if (/^https?:\/\//.test(source)) {
|
|
251
|
+
const config = await readConfig();
|
|
252
|
+
const token = await tryGetAccessToken();
|
|
253
|
+
const request = buildExternalAssetsRequest({
|
|
254
|
+
source,
|
|
255
|
+
knownOrigins: configuredEnvOrigins(config.envs),
|
|
256
|
+
token,
|
|
257
|
+
});
|
|
258
|
+
const response = await fetch(request.url, request.init);
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
throw new Error(`fetching ORM contracts catalog failed: ${response.status} ${response.statusText}`);
|
|
261
|
+
}
|
|
262
|
+
raw = await response.text();
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
raw = await readFile(resolve(source), 'utf8');
|
|
266
|
+
}
|
|
267
|
+
return spec.ormContractCatalogSchema.parse(JSON.parse(raw));
|
|
268
|
+
}
|
|
269
|
+
/** Resolve ORM contracts from --orm-contracts or conventional orm-contracts.json. */
|
|
270
|
+
export function resolveOrmContractsPath({ dir, flag, }) {
|
|
271
|
+
if (typeof flag === 'string')
|
|
272
|
+
return flag;
|
|
273
|
+
const conventional = join(dir, ORM_CONTRACTS_FILENAME);
|
|
274
|
+
return existsSync(conventional) ? conventional : undefined;
|
|
275
|
+
}
|
|
228
276
|
function printHumanReport(report) {
|
|
229
277
|
for (const finding of report.findings) {
|
|
230
278
|
const prefix = finding.severity === 'error' ? 'error' : 'warning';
|
|
@@ -296,7 +344,28 @@ export async function pipelineValidateCommand(args) {
|
|
|
296
344
|
});
|
|
297
345
|
}
|
|
298
346
|
}
|
|
299
|
-
|
|
347
|
+
let ormContracts;
|
|
348
|
+
const ormContractsPath = resolveOrmContractsPath({ dir, flag: args.flags['orm-contracts'] });
|
|
349
|
+
if (ormContractsPath) {
|
|
350
|
+
try {
|
|
351
|
+
ormContracts = await loadOrmContracts(spec, ormContractsPath);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
return emit({
|
|
355
|
+
ok: false,
|
|
356
|
+
dir: dirArg,
|
|
357
|
+
stages: pipeline.stages.map((stage) => stage.stage),
|
|
358
|
+
findings: [
|
|
359
|
+
{
|
|
360
|
+
severity: 'error',
|
|
361
|
+
code: 'orm_contracts_unreadable',
|
|
362
|
+
message: error instanceof Error ? error.message : String(error),
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const result = spec.validateSpecGraph(pipeline.stages, externalAssets, ormContracts !== undefined ? { ormContracts } : undefined);
|
|
300
369
|
return emit({
|
|
301
370
|
ok: result.ok,
|
|
302
371
|
dir: dirArg,
|
|
@@ -310,6 +379,15 @@ export async function runPipelineCommand(sub, args) {
|
|
|
310
379
|
return pipelineInitCommand(args);
|
|
311
380
|
case 'validate':
|
|
312
381
|
return pipelineValidateCommand(args);
|
|
382
|
+
case 'codegen': {
|
|
383
|
+
const codegenSub = args.positional[0];
|
|
384
|
+
if (codegenSub === 'tables') {
|
|
385
|
+
const { pipelineCodegenTablesCommand } = await import('./codegen.js');
|
|
386
|
+
return pipelineCodegenTablesCommand({ ...args, positional: args.positional.slice(1) });
|
|
387
|
+
}
|
|
388
|
+
console.error(`${LOG} unknown pipeline codegen target '${codegenSub ?? ''}' — expected: tables`);
|
|
389
|
+
return 1;
|
|
390
|
+
}
|
|
313
391
|
case 'adopt': {
|
|
314
392
|
const { pipelineAdoptCommand } = await import('./lifecycle.js');
|
|
315
393
|
return pipelineAdoptCommand(args);
|
|
@@ -37,19 +37,8 @@ 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>;
|