@xano/cli 1.3.4 → 1.3.5-beta.0
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 +69 -2
- package/dist/base-command.d.ts +12 -1
- package/dist/base-command.js +36 -22
- package/dist/commands/branch/set_live/index.d.ts +14 -0
- package/dist/commands/branch/set_live/index.js +38 -0
- package/dist/commands/ephemeral/push/index.js +6 -1
- package/dist/commands/function/create/index.d.ts +15 -0
- package/dist/commands/function/create/index.js +39 -0
- package/dist/commands/function/edit/index.d.ts +15 -0
- package/dist/commands/function/edit/index.js +39 -0
- package/dist/commands/function/run/index.js +5 -11
- package/dist/commands/policy/catalogue/index.d.ts +16 -0
- package/dist/commands/policy/catalogue/index.js +60 -0
- package/dist/commands/policy/coverage/index.d.ts +17 -0
- package/dist/commands/policy/coverage/index.js +33 -0
- package/dist/commands/policy/create/index.d.ts +18 -0
- package/dist/commands/policy/create/index.js +44 -0
- package/dist/commands/policy/delete/index.d.ts +19 -0
- package/dist/commands/policy/delete/index.js +61 -0
- package/dist/commands/policy/evaluate/index.d.ts +32 -0
- package/dist/commands/policy/evaluate/index.js +71 -0
- package/dist/commands/policy/list/index.d.ts +14 -0
- package/dist/commands/policy/list/index.js +21 -0
- package/dist/commands/policy/parse/index.d.ts +19 -0
- package/dist/commands/policy/parse/index.js +18 -0
- package/dist/commands/policy/publish/index.d.ts +20 -0
- package/dist/commands/policy/publish/index.js +38 -0
- package/dist/commands/policy/runs/index.d.ts +54 -0
- package/dist/commands/policy/runs/index.js +273 -0
- package/dist/commands/policy/status/index.d.ts +21 -0
- package/dist/commands/policy/status/index.js +54 -0
- package/dist/commands/release/create/index.js +4 -0
- package/dist/commands/release/deploy/index.d.ts +19 -0
- package/dist/commands/release/deploy/index.js +68 -6
- package/dist/commands/release/import/index.js +5 -1
- package/dist/commands/release/list/index.d.ts +7 -0
- package/dist/commands/release/list/index.js +46 -9
- package/dist/commands/release/push/index.js +19 -1
- package/dist/commands/sandbox/push/index.js +6 -1
- package/dist/commands/skills/pull/index.d.ts +30 -0
- package/dist/commands/skills/pull/index.js +118 -0
- package/dist/commands/tenant/create/index.d.ts +3 -0
- package/dist/commands/tenant/create/index.js +21 -0
- package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
- package/dist/commands/tenant/deploy_release/index.js +182 -4
- package/dist/commands/tenant/edit/index.d.ts +2 -0
- package/dist/commands/tenant/edit/index.js +39 -4
- package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
- package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
- package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/create/index.js +108 -0
- package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
- package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
- package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
- package/dist/commands/tenant_deploy_request/get/index.js +113 -0
- package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
- package/dist/commands/tenant_deploy_request/list/index.js +122 -0
- package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
- package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
- package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
- package/dist/commands/workspace/pull/index.d.ts +10 -0
- package/dist/commands/workspace/pull/index.js +70 -7
- package/dist/commands/workspace/push/index.d.ts +19 -0
- package/dist/commands/workspace/push/index.js +135 -6
- package/dist/policy-command.d.ts +91 -0
- package/dist/policy-command.js +95 -0
- package/dist/utils/document-parser.d.ts +10 -0
- package/dist/utils/document-parser.js +21 -0
- package/dist/utils/multidoc-push.d.ts +57 -2
- package/dist/utils/multidoc-push.js +102 -33
- package/dist/utils/policy/catalogue.d.ts +85 -0
- package/dist/utils/policy/catalogue.js +196 -0
- package/dist/utils/policy/coverage.d.ts +19 -0
- package/dist/utils/policy/coverage.js +93 -0
- package/dist/utils/policy/errors.d.ts +20 -0
- package/dist/utils/policy/errors.js +93 -0
- package/dist/utils/policy/feedback.d.ts +90 -0
- package/dist/utils/policy/feedback.js +210 -0
- package/dist/utils/policy/findings.d.ts +29 -0
- package/dist/utils/policy/findings.js +82 -0
- package/dist/utils/policy/gate.d.ts +138 -0
- package/dist/utils/policy/gate.js +193 -0
- package/dist/utils/policy/permission.d.ts +7 -0
- package/dist/utils/policy/permission.js +55 -0
- package/dist/utils/policy/push-gate.d.ts +5 -0
- package/dist/utils/policy/push-gate.js +43 -0
- package/dist/utils/policy/release.d.ts +85 -0
- package/dist/utils/policy/release.js +117 -0
- package/dist/utils/policy/request.d.ts +55 -0
- package/dist/utils/policy/request.js +119 -0
- package/dist/utils/policy/runs.d.ts +30 -0
- package/dist/utils/policy/runs.js +101 -0
- package/dist/utils/policy/status.d.ts +55 -0
- package/dist/utils/policy/status.js +127 -0
- package/dist/utils/policy/template.d.ts +66 -0
- package/dist/utils/policy/template.js +152 -0
- package/dist/utils/policy/types.d.ts +295 -0
- package/dist/utils/policy/types.js +1 -0
- package/oclif.manifest.json +5782 -2573
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -197,6 +197,7 @@ xano workspace delete -w <workspace_id> --force
|
|
|
197
197
|
xano workspace pull
|
|
198
198
|
xano workspace pull -d ./my-workspace # Specify output directory
|
|
199
199
|
xano workspace pull -b dev # Specific branch
|
|
200
|
+
xano workspace pull -b '' # The live branch, whatever the profile's branch
|
|
200
201
|
xano workspace pull --env --records # Include env vars and table records
|
|
201
202
|
xano workspace pull --draft # Include draft changes
|
|
202
203
|
xano workspace pull --sync-state # Write .xano/sync_state.json; later pulls keep it current
|
|
@@ -205,6 +206,7 @@ xano workspace pull --sync-state # Write .xano/sync_stat
|
|
|
205
206
|
xano workspace push
|
|
206
207
|
xano workspace push -d ./my-workspace # Push from a specific directory
|
|
207
208
|
xano workspace push -b dev
|
|
209
|
+
xano workspace push -b '' # The live branch, whatever the profile's branch
|
|
208
210
|
xano workspace push --sync # Full push — send all files, not just changed ones
|
|
209
211
|
xano workspace push --sync --delete # Full push + delete remote objects not included
|
|
210
212
|
xano workspace push --dry-run # Preview changes without pushing
|
|
@@ -222,6 +224,7 @@ xano workspace push --force # Skip preview and conf
|
|
|
222
224
|
xano workspace push -i "function/*" # Push only matching files
|
|
223
225
|
xano workspace push -e "table/*" # Push all files except tables
|
|
224
226
|
xano workspace push -i "function/*" -e "**/test*" # Include functions, exclude tests
|
|
227
|
+
xano workspace push -o json # One JSON document: the preview (with --dry-run) or the import result
|
|
225
228
|
|
|
226
229
|
# Pull from a git repository to local files (defaults to current directory)
|
|
227
230
|
xano workspace git pull -r https://github.com/owner/repo
|
|
@@ -431,8 +434,8 @@ Input flexibility for `function run` (assembled into one JSON `input` object):
|
|
|
431
434
|
to run against `live`; an unknown label is rejected by the server with
|
|
432
435
|
`Invalid data source.`
|
|
433
436
|
|
|
434
|
-
`-t/--tenant <name>` runs the function on that tenant
|
|
435
|
-
|
|
437
|
+
`-t/--tenant <name>` runs the function on that tenant
|
|
438
|
+
(`/workspace/{id}/tenant/{name}/function/run`) instead of the workspace.
|
|
436
439
|
|
|
437
440
|
Merge order is JSON base first, then `--data` overrides. Missing required inputs are
|
|
438
441
|
prompted for on an interactive terminal; in a non-TTY (CI) context the command fails
|
|
@@ -654,9 +657,14 @@ xano tenant get <tenant_name>
|
|
|
654
657
|
xano tenant create "My Tenant"
|
|
655
658
|
xano tenant create "My Tenant" -d "Description" --type tier2 --cluster_id 1 --platform_id 5
|
|
656
659
|
xano tenant create "My Tenant" --type tier2 --cluster_id 1 --license ./license.yaml
|
|
660
|
+
xano tenant create "Production" --required_reviewers 1 --allow_deploy_bypass
|
|
661
|
+
xano tenant create "Staging" --required_reviewers 0 --allow_quick_deploy
|
|
657
662
|
|
|
658
663
|
# Edit a tenant
|
|
659
664
|
xano tenant edit <tenant_name> --display "New Name" -d "New description"
|
|
665
|
+
xano tenant edit <tenant_name> --required_reviewers 1
|
|
666
|
+
xano tenant edit <tenant_name> --required_reviewers 0
|
|
667
|
+
xano tenant edit <tenant_name> --allow_quick_deploy
|
|
660
668
|
|
|
661
669
|
# Delete a tenant (confirmation required)
|
|
662
670
|
xano tenant delete <tenant_name>
|
|
@@ -707,6 +715,65 @@ xano tenant deploy_release <tenant_name> --release v1.0
|
|
|
707
715
|
xano tenant deploy_platform <tenant_name> --platform_id 5 --license ./license.yaml
|
|
708
716
|
```
|
|
709
717
|
|
|
718
|
+
If a tenant has `required_reviewers` set above 0, `tenant deploy_release` fails with an
|
|
719
|
+
error naming the `tenant_deploy_request` command to run instead — see below. A tenant
|
|
720
|
+
with `allow_quick_deploy` enabled skips the gate entirely, regardless of
|
|
721
|
+
`required_reviewers`.
|
|
722
|
+
|
|
723
|
+
#### Tenant Deploy Requests
|
|
724
|
+
|
|
725
|
+
When a tenant has `required_reviewers` set above 0 and `allow_quick_deploy` is off (see
|
|
726
|
+
`tenant create`/`tenant edit` above), a release may only be deployed to it through an
|
|
727
|
+
**approved deploy request**, requiring that many distinct reviewer approvals. An author
|
|
728
|
+
with no deploy permission opens a request; a named reviewer who does hold deploy
|
|
729
|
+
permission on the tenant votes to approve it, and once enough reviewers have approved,
|
|
730
|
+
it deploys as part of that action. `allow_deploy_bypass` (set at `tenant create` time
|
|
731
|
+
only) additionally allows an authorized user to skip a specific request's gate via
|
|
732
|
+
`tenant_deploy_request bypass`, audited with a required reason.
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
# List deploy requests in a workspace (results show the target tenant and release)
|
|
736
|
+
xano tenant_deploy_request list
|
|
737
|
+
xano tenant_deploy_request list --tenant prod --status pending
|
|
738
|
+
xano tenant_deploy_request list --tenant prod --release v1.2
|
|
739
|
+
xano tenant_deploy_request list --to-review
|
|
740
|
+
|
|
741
|
+
# Get details of a specific deploy request
|
|
742
|
+
xano tenant_deploy_request get <id>
|
|
743
|
+
|
|
744
|
+
# Open a deploy request (submits for review immediately unless --draft is passed)
|
|
745
|
+
xano tenant_deploy_request create "Deploy v1.2 to prod" --tenant prod --release v1.2 --reviewers 12,45
|
|
746
|
+
xano tenant_deploy_request create "Deploy v1.2 to prod" --tenant prod --release v1.2 --draft
|
|
747
|
+
|
|
748
|
+
# Edit a deploy request's title, description or reviewers
|
|
749
|
+
xano tenant_deploy_request edit <id> --title "Deploy v1.2.1 to prod"
|
|
750
|
+
xano tenant_deploy_request edit <id> --reviewers 12,45,67
|
|
751
|
+
|
|
752
|
+
# Change status: draft, submit, approve, request_changes, close, or reopen
|
|
753
|
+
# (approve records one reviewer's vote; once enough votes are in it deploys the
|
|
754
|
+
# release as part of that same call)
|
|
755
|
+
xano tenant_deploy_request set_status <id> --status submit
|
|
756
|
+
xano tenant_deploy_request set_status <id> --status approve
|
|
757
|
+
xano tenant_deploy_request set_status <id> --status request_changes --reason "Needs a migration note"
|
|
758
|
+
xano tenant_deploy_request set_status <id> --status close --reason "Superseded by a newer request"
|
|
759
|
+
xano tenant_deploy_request set_status <id> --status reopen
|
|
760
|
+
|
|
761
|
+
# Link a newer release to a request after changes were requested (author only)
|
|
762
|
+
xano tenant_deploy_request revision <id> --release v1.2.1 --note "Added the missing migration"
|
|
763
|
+
|
|
764
|
+
# Deploy past the approval gate under the deploy-bypass permission (requires the
|
|
765
|
+
# tenant's allow_deploy_bypass to be enabled, and a separately-grantable RBAC
|
|
766
|
+
# permission). Every use is audited.
|
|
767
|
+
xano tenant_deploy_request bypass <id> --reason "Prod incident, reviewer unavailable"
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
CI example (e.g. a GitHub Action step):
|
|
771
|
+
|
|
772
|
+
```bash
|
|
773
|
+
xano tenant_deploy_request create "$PR_TITLE" --tenant prod --release v1.2 \
|
|
774
|
+
--description "$PR_BODY" --reviewers 12,45 -o json
|
|
775
|
+
```
|
|
776
|
+
|
|
710
777
|
#### Tenant License
|
|
711
778
|
|
|
712
779
|
```bash
|
package/dist/base-command.d.ts
CHANGED
|
@@ -63,6 +63,16 @@ export default abstract class BaseCommand extends Command {
|
|
|
63
63
|
* Sets NODE_TLS_REJECT_UNAUTHORIZED=0 so all fetch() calls skip cert verification.
|
|
64
64
|
*/
|
|
65
65
|
protected applyInsecureFromProfile(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Report a failure as exit 1 for commands that reserve exit 2 for findings: operational errors,
|
|
68
|
+
* flag errors and oclif's own default exit 2 all become 1, and under `-o json` the failure is also
|
|
69
|
+
* written to stdout as `{"error": {"exit": 1, "message"}}`. A deliberate exit 0 passes through.
|
|
70
|
+
*/
|
|
71
|
+
protected catchAsOperational(error: Error & {
|
|
72
|
+
oclif?: {
|
|
73
|
+
exit?: number;
|
|
74
|
+
};
|
|
75
|
+
}): Promise<void>;
|
|
66
76
|
finally(_: Error | undefined): Promise<void>;
|
|
67
77
|
/**
|
|
68
78
|
* Get the resolved credentials file path, respecting --config flag and XANO_CONFIG env var.
|
|
@@ -78,6 +88,8 @@ export default abstract class BaseCommand extends Command {
|
|
|
78
88
|
protected getOrCreateSandbox(profile: ProfileConfig, verbose: boolean): Promise<SandboxTenant>;
|
|
79
89
|
protected getProfile(): string | undefined;
|
|
80
90
|
init(): Promise<void>;
|
|
91
|
+
/** Raw-argv check: the parsed flags are not available yet when the banner prints. */
|
|
92
|
+
protected isJsonOutput(): boolean;
|
|
81
93
|
protected loadCredentialsFile(): CredentialsFile | null;
|
|
82
94
|
/**
|
|
83
95
|
* Poll a static-host build until it reaches a terminal status (ok | error),
|
|
@@ -129,7 +141,6 @@ export default abstract class BaseCommand extends Command {
|
|
|
129
141
|
verbose: boolean;
|
|
130
142
|
workspaceId: string;
|
|
131
143
|
}): Promise<string>;
|
|
132
|
-
private isJsonOutput;
|
|
133
144
|
/**
|
|
134
145
|
* Find and parse the nearest project-local profile.yaml, unless an explicit
|
|
135
146
|
* -p/XANO_PROFILE was given (in which case the local file is ignored).
|
package/dist/base-command.js
CHANGED
|
@@ -156,6 +156,18 @@ export default class BaseCommand extends Command {
|
|
|
156
156
|
// Don't fail the command if we can't read the profile for insecure check
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Report a failure as exit 1 for commands that reserve exit 2 for findings: operational errors,
|
|
161
|
+
* flag errors and oclif's own default exit 2 all become 1, and under `-o json` the failure is also
|
|
162
|
+
* written to stdout as `{"error": {"exit": 1, "message"}}`. A deliberate exit 0 passes through.
|
|
163
|
+
*/
|
|
164
|
+
async catchAsOperational(error) {
|
|
165
|
+
if (error.oclif?.exit === 0)
|
|
166
|
+
return super.catch(error);
|
|
167
|
+
if (this.isJsonOutput())
|
|
168
|
+
this.log(JSON.stringify({ error: { exit: 1, message: error.message } }, null, 2));
|
|
169
|
+
this.error(error, { exit: 1 });
|
|
170
|
+
}
|
|
159
171
|
async finally(_) {
|
|
160
172
|
if (this.updateNotice && !this.isJsonOutput()) {
|
|
161
173
|
this.log(this.updateNotice);
|
|
@@ -221,6 +233,19 @@ export default class BaseCommand extends Command {
|
|
|
221
233
|
const forceUpdateCheck = process.env.XANO_FORCE_UPDATE_CHECK === '1';
|
|
222
234
|
this.updateNotice = checkForUpdate(this.config.version, forceUpdateCheck);
|
|
223
235
|
}
|
|
236
|
+
/** Raw-argv check: the parsed flags are not available yet when the banner prints. */
|
|
237
|
+
isJsonOutput() {
|
|
238
|
+
const args = this.argv;
|
|
239
|
+
for (let i = 0; i < args.length; i++) {
|
|
240
|
+
if (args[i] === '--output' && args[i + 1] === 'json')
|
|
241
|
+
return true;
|
|
242
|
+
if (args[i] === '-o' && args[i + 1] === 'json')
|
|
243
|
+
return true;
|
|
244
|
+
if (args[i] === '--output=json' || args[i] === '-o=json')
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
224
249
|
loadCredentialsFile() {
|
|
225
250
|
const credentialsPath = this.getCredentialsPath();
|
|
226
251
|
if (!fs.existsSync(credentialsPath)) {
|
|
@@ -354,18 +379,19 @@ export default class BaseCommand extends Command {
|
|
|
354
379
|
...(timeoutMs > 0 && !options.signal ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
|
|
355
380
|
});
|
|
356
381
|
const contentType = headers['Content-Type'] || 'application/json';
|
|
382
|
+
const logDiagnostic = this.isJsonOutput() ? this.logToStderr.bind(this) : this.log.bind(this);
|
|
357
383
|
if (verbose) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
384
|
+
logDiagnostic('');
|
|
385
|
+
logDiagnostic('─'.repeat(60));
|
|
386
|
+
logDiagnostic(`→ ${method} ${url}`);
|
|
387
|
+
logDiagnostic(` Content-Type: ${contentType}`);
|
|
362
388
|
if (authToken) {
|
|
363
|
-
|
|
389
|
+
logDiagnostic(` Authorization: Bearer ${authToken.slice(0, 8)}...${authToken.slice(-4)}`);
|
|
364
390
|
}
|
|
365
391
|
if (options.body) {
|
|
366
392
|
const bodyStr = typeof options.body === 'string' ? options.body : String(options.body);
|
|
367
393
|
const bodyPreview = bodyStr.length > 500 ? bodyStr.slice(0, 500) + '...' : bodyStr;
|
|
368
|
-
|
|
394
|
+
logDiagnostic(` Body: ${bodyPreview}`);
|
|
369
395
|
}
|
|
370
396
|
}
|
|
371
397
|
const startTime = Date.now();
|
|
@@ -381,7 +407,7 @@ export default class BaseCommand extends Command {
|
|
|
381
407
|
if (dispatcherSupported && isInvalidDispatcherError(error)) {
|
|
382
408
|
dispatcherSupported = false;
|
|
383
409
|
if (verbose) {
|
|
384
|
-
|
|
410
|
+
logDiagnostic(' (this Node runtime rejected undici dispatcher; retrying without it — see DEV-7773)');
|
|
385
411
|
}
|
|
386
412
|
response = await fetch(url, buildFetchOptions(false));
|
|
387
413
|
}
|
|
@@ -391,9 +417,9 @@ export default class BaseCommand extends Command {
|
|
|
391
417
|
}
|
|
392
418
|
const elapsed = Date.now() - startTime;
|
|
393
419
|
if (verbose) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
420
|
+
logDiagnostic(`← ${response.status} ${response.statusText} (${elapsed}ms)`);
|
|
421
|
+
logDiagnostic('─'.repeat(60));
|
|
422
|
+
logDiagnostic('');
|
|
397
423
|
}
|
|
398
424
|
return response;
|
|
399
425
|
}
|
|
@@ -468,18 +494,6 @@ export default class BaseCommand extends Command {
|
|
|
468
494
|
conclude(`stopped waiting after ${Math.round(timeoutMs / 1000)}s (last status: ${stage || 'unknown'})`);
|
|
469
495
|
return stage;
|
|
470
496
|
}
|
|
471
|
-
isJsonOutput() {
|
|
472
|
-
const args = process.argv;
|
|
473
|
-
for (let i = 0; i < args.length; i++) {
|
|
474
|
-
if (args[i] === '--output' && args[i + 1] === 'json')
|
|
475
|
-
return true;
|
|
476
|
-
if (args[i] === '-o' && args[i + 1] === 'json')
|
|
477
|
-
return true;
|
|
478
|
-
if (args[i] === '--output=json' || args[i] === '-o=json')
|
|
479
|
-
return true;
|
|
480
|
-
}
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
497
|
/**
|
|
484
498
|
* Find and parse the nearest project-local profile.yaml, unless an explicit
|
|
485
499
|
* -p/XANO_PROFILE was given (in which case the local file is ignored).
|
|
@@ -8,11 +8,25 @@ export default class BranchSetLive extends BaseCommand {
|
|
|
8
8
|
static flags: {
|
|
9
9
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
16
|
};
|
|
17
|
+
/** Set once the set-live policy gate refuses, the one failure that exits 2. */
|
|
18
|
+
private refusedByPolicyGate;
|
|
19
|
+
/** Exit 2 is the set-live gate's refusal; every other failure, flag errors included, exits 1. */
|
|
20
|
+
protected catch(error: Error & {
|
|
21
|
+
oclif?: {
|
|
22
|
+
exit?: number;
|
|
23
|
+
};
|
|
24
|
+
}): Promise<void>;
|
|
16
25
|
run(): Promise<void>;
|
|
17
26
|
private confirm;
|
|
27
|
+
/**
|
|
28
|
+
* The set-live gate refused: the verdict on stdout (under `-o json`, `{set_live: false, message,
|
|
29
|
+
* policy_gate}`), then the platform's message and how to proceed, exiting 2.
|
|
30
|
+
*/
|
|
31
|
+
private refuseSetLive;
|
|
18
32
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { BLANK_POLICY_OVERRIDE, blankPolicyOverride, gateOverrideHint, gateRefusal, liveGateLines, quoted, } from '../../../utils/policy/gate.js';
|
|
3
4
|
export default class BranchSetLive extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
branch_label: Args.string({
|
|
@@ -40,14 +41,25 @@ Branch 'v1' is now live
|
|
|
40
41
|
options: ['summary', 'json'],
|
|
41
42
|
required: false,
|
|
42
43
|
}),
|
|
44
|
+
'policy-override': Flags.string({ description: 'Set the branch live past a blocking set-live policy gate with an audited reason (requires workspace:policy update)', hidden: true }),
|
|
43
45
|
workspace: Flags.integer({
|
|
44
46
|
char: 'w',
|
|
45
47
|
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
46
48
|
required: false,
|
|
47
49
|
}),
|
|
48
50
|
};
|
|
51
|
+
/** Set once the set-live policy gate refuses, the one failure that exits 2. */
|
|
52
|
+
refusedByPolicyGate = false;
|
|
53
|
+
/** Exit 2 is the set-live gate's refusal; every other failure, flag errors included, exits 1. */
|
|
54
|
+
async catch(error) {
|
|
55
|
+
if (this.refusedByPolicyGate)
|
|
56
|
+
return super.catch(error);
|
|
57
|
+
return this.catchAsOperational(error);
|
|
58
|
+
}
|
|
49
59
|
async run() {
|
|
50
60
|
const { args, flags } = await this.parse(BranchSetLive);
|
|
61
|
+
if (blankPolicyOverride(flags['policy-override']))
|
|
62
|
+
this.error(BLANK_POLICY_OVERRIDE);
|
|
51
63
|
const { profile } = this.resolveProfile(flags);
|
|
52
64
|
// Get workspace ID from flag or profile
|
|
53
65
|
const workspaceId = flags.workspace || profile.workspace;
|
|
@@ -66,16 +78,23 @@ Branch 'v1' is now live
|
|
|
66
78
|
}
|
|
67
79
|
// Construct the API URL
|
|
68
80
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/branch/${encodeURIComponent(branchLabel)}/live`;
|
|
81
|
+
// A reason from someone with workspace:policy update sets the branch live past a blocking gate.
|
|
82
|
+
const overrideReason = flags['policy-override']?.trim();
|
|
69
83
|
// Set branch as live via the API
|
|
70
84
|
try {
|
|
71
85
|
const response = await this.verboseFetch(apiUrl, {
|
|
86
|
+
...(overrideReason ? { body: JSON.stringify({ override_reason: overrideReason }) } : {}),
|
|
72
87
|
headers: {
|
|
73
88
|
'accept': 'application/json',
|
|
74
89
|
'Authorization': `Bearer ${profile.access_token}`,
|
|
90
|
+
...(overrideReason ? { 'Content-Type': 'application/json' } : {}),
|
|
75
91
|
},
|
|
76
92
|
method: 'POST',
|
|
77
93
|
}, flags.verbose, profile.access_token);
|
|
78
94
|
if (!response.ok) {
|
|
95
|
+
const refused = await gateRefusal(response);
|
|
96
|
+
if (refused)
|
|
97
|
+
this.refuseSetLive(refused, flags, branchLabel);
|
|
79
98
|
const errorText = await response.text();
|
|
80
99
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
81
100
|
}
|
|
@@ -89,6 +108,8 @@ Branch 'v1' is now live
|
|
|
89
108
|
}
|
|
90
109
|
}
|
|
91
110
|
catch (error) {
|
|
111
|
+
if (this.refusedByPolicyGate)
|
|
112
|
+
throw error;
|
|
92
113
|
if (error instanceof Error) {
|
|
93
114
|
this.error(`Failed to set branch as live: ${error.message}`);
|
|
94
115
|
}
|
|
@@ -111,4 +132,21 @@ Branch 'v1' is now live
|
|
|
111
132
|
});
|
|
112
133
|
});
|
|
113
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The set-live gate refused: the verdict on stdout (under `-o json`, `{set_live: false, message,
|
|
137
|
+
* policy_gate}`), then the platform's message and how to proceed, exiting 2.
|
|
138
|
+
*/
|
|
139
|
+
refuseSetLive(refused, flags, branchLabel) {
|
|
140
|
+
if (flags.output === 'json') {
|
|
141
|
+
this.log(JSON.stringify({ message: refused.message, policy_gate: refused.answer, set_live: false }, null, 2));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
for (const line of liveGateLines(refused.answer, 'branch'))
|
|
145
|
+
this.log(line);
|
|
146
|
+
}
|
|
147
|
+
const workspace = flags.workspace ? ` -w ${flags.workspace}` : '';
|
|
148
|
+
const hint = gateOverrideHint(refused.answer, `xano branch set_live ${quoted(branchLabel)}${workspace} --policy-override "<why>"`);
|
|
149
|
+
this.refusedByPolicyGate = true;
|
|
150
|
+
this.error(`${refused.message}${hint}`, { exit: 2 });
|
|
151
|
+
}
|
|
114
152
|
}
|
|
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import BaseCommand from '../../../base-command.js';
|
|
5
5
|
import { isAwaited, waitForMicroservices, } from '../../../utils/microservice-wait.js';
|
|
6
6
|
import { executePush } from '../../../utils/multidoc-push.js';
|
|
7
|
+
import { policiesSkippedNotice } from '../../../utils/policy/feedback.js';
|
|
7
8
|
/** Status glyph for one microservice row in the `--wait` progress block. */
|
|
8
9
|
const icon = (e) => {
|
|
9
10
|
if (!isAwaited(e))
|
|
@@ -168,13 +169,17 @@ Skip preview and push immediately
|
|
|
168
169
|
truncate: flags.truncate,
|
|
169
170
|
verbose: flags.verbose,
|
|
170
171
|
};
|
|
171
|
-
await executePush({
|
|
172
|
+
const result = await executePush({
|
|
172
173
|
accessToken: profile.access_token,
|
|
173
174
|
branch: '',
|
|
174
175
|
command: this,
|
|
175
176
|
inputDir,
|
|
176
177
|
verboseFetch: this.verboseFetch.bind(this),
|
|
177
178
|
}, target, pushFlags);
|
|
179
|
+
// When the platform leaves policy files out, it says so once.
|
|
180
|
+
const skipped = policiesSkippedNotice(result.response ?? result.preview);
|
|
181
|
+
if (skipped)
|
|
182
|
+
this.warn(skipped);
|
|
178
183
|
// --wait: after a real push, poll the tenant's live microservice status until
|
|
179
184
|
// every auto-deployed microservice (tenant_deploy="auto") is ready or the
|
|
180
185
|
// wait times out. Skipped on --dry-run (nothing was deployed).
|
|
@@ -7,13 +7,28 @@ export default class FunctionCreate extends BaseCommand {
|
|
|
7
7
|
edit: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
8
|
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
9
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
stdin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
16
|
};
|
|
17
|
+
/** Set once the publish policy gate refuses, the one failure that exits 2. */
|
|
18
|
+
private refusedByPolicyGate;
|
|
19
|
+
/** Exit 2 is the publish gate's refusal; every other failure, flag errors included, exits 1. */
|
|
20
|
+
protected catch(error: Error & {
|
|
21
|
+
oclif?: {
|
|
22
|
+
exit?: number;
|
|
23
|
+
};
|
|
24
|
+
}): Promise<void>;
|
|
16
25
|
run(): Promise<void>;
|
|
17
26
|
private editFile;
|
|
18
27
|
private readStdin;
|
|
28
|
+
/**
|
|
29
|
+
* The live-branch publish gate refused the new function: the verdict on stdout (under `-o json`,
|
|
30
|
+
* `{created: false, message, policy_gate}`), then the platform's message and how to proceed,
|
|
31
|
+
* exiting 2.
|
|
32
|
+
*/
|
|
33
|
+
private refusePublish;
|
|
19
34
|
}
|
|
@@ -4,6 +4,7 @@ import * as fs from 'node:fs';
|
|
|
4
4
|
import * as os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import BaseCommand from '../../../base-command.js';
|
|
7
|
+
import { BLANK_POLICY_OVERRIDE, blankPolicyOverride, gateRefusal, publishRefusal, quoted } from '../../../utils/policy/gate.js';
|
|
7
8
|
export default class FunctionCreate extends BaseCommand {
|
|
8
9
|
static args = {};
|
|
9
10
|
static description = 'Create a new function in a workspace';
|
|
@@ -59,6 +60,7 @@ Name: my_function
|
|
|
59
60
|
options: ['summary', 'json'],
|
|
60
61
|
required: false,
|
|
61
62
|
}),
|
|
63
|
+
'policy-override': Flags.string({ description: 'Create the function past a blocking live-branch publish policy gate with an audited reason (requires workspace:policy update)', hidden: true }),
|
|
62
64
|
stdin: Flags.boolean({
|
|
63
65
|
char: 's',
|
|
64
66
|
default: false,
|
|
@@ -72,8 +74,18 @@ Name: my_function
|
|
|
72
74
|
required: false,
|
|
73
75
|
}),
|
|
74
76
|
};
|
|
77
|
+
/** Set once the publish policy gate refuses, the one failure that exits 2. */
|
|
78
|
+
refusedByPolicyGate = false;
|
|
79
|
+
/** Exit 2 is the publish gate's refusal; every other failure, flag errors included, exits 1. */
|
|
80
|
+
async catch(error) {
|
|
81
|
+
if (this.refusedByPolicyGate)
|
|
82
|
+
return super.catch(error);
|
|
83
|
+
return this.catchAsOperational(error);
|
|
84
|
+
}
|
|
75
85
|
async run() {
|
|
76
86
|
const { flags } = await this.parse(FunctionCreate);
|
|
87
|
+
if (blankPolicyOverride(flags['policy-override']))
|
|
88
|
+
this.error(BLANK_POLICY_OVERRIDE);
|
|
77
89
|
const { profile, profileName } = this.resolveProfile(flags);
|
|
78
90
|
// Determine workspace_id from flag or profile
|
|
79
91
|
let workspaceId;
|
|
@@ -133,6 +145,10 @@ Name: my_function
|
|
|
133
145
|
const queryParams = new URLSearchParams({
|
|
134
146
|
include_xanoscript: 'false',
|
|
135
147
|
});
|
|
148
|
+
// A reason from someone with workspace:policy update saves past a blocking publish gate.
|
|
149
|
+
const overrideReason = flags['policy-override']?.trim();
|
|
150
|
+
if (overrideReason)
|
|
151
|
+
queryParams.set('override_reason', overrideReason);
|
|
136
152
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${queryParams.toString()}`;
|
|
137
153
|
// Create function via API
|
|
138
154
|
try {
|
|
@@ -146,6 +162,9 @@ Name: my_function
|
|
|
146
162
|
method: 'POST',
|
|
147
163
|
}, flags.verbose, profile.access_token);
|
|
148
164
|
if (!response.ok) {
|
|
165
|
+
const refused = await gateRefusal(response);
|
|
166
|
+
if (refused)
|
|
167
|
+
this.refusePublish(refused, flags);
|
|
149
168
|
const errorText = await response.text();
|
|
150
169
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
151
170
|
}
|
|
@@ -166,6 +185,8 @@ Name: my_function
|
|
|
166
185
|
}
|
|
167
186
|
}
|
|
168
187
|
catch (error) {
|
|
188
|
+
if (this.refusedByPolicyGate)
|
|
189
|
+
throw error;
|
|
169
190
|
if (error instanceof Error) {
|
|
170
191
|
this.error(`Failed to create function: ${error.message}`);
|
|
171
192
|
}
|
|
@@ -239,4 +260,22 @@ Name: my_function
|
|
|
239
260
|
process.stdin.resume();
|
|
240
261
|
});
|
|
241
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* The live-branch publish gate refused the new function: the verdict on stdout (under `-o json`,
|
|
265
|
+
* `{created: false, message, policy_gate}`), then the platform's message and how to proceed,
|
|
266
|
+
* exiting 2.
|
|
267
|
+
*/
|
|
268
|
+
refusePublish(refused, flags) {
|
|
269
|
+
const workspace = flags.workspace ? ` -w ${quoted(flags.workspace)}` : '';
|
|
270
|
+
const source = flags.file ? ` -f ${quoted(flags.file)}` : ' --stdin';
|
|
271
|
+
const { lines, message } = publishRefusal(refused, {
|
|
272
|
+
json: flags.output === 'json',
|
|
273
|
+
outcome: 'created',
|
|
274
|
+
rerun: `xano function create${workspace}${source} --policy-override "<why>"`,
|
|
275
|
+
});
|
|
276
|
+
for (const line of lines)
|
|
277
|
+
this.log(line);
|
|
278
|
+
this.refusedByPolicyGate = true;
|
|
279
|
+
this.error(message, { exit: 2 });
|
|
280
|
+
}
|
|
242
281
|
}
|
|
@@ -9,6 +9,7 @@ export default class FunctionEdit extends BaseCommand {
|
|
|
9
9
|
edit: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
publish: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
stdin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -16,10 +17,24 @@ export default class FunctionEdit extends BaseCommand {
|
|
|
16
17
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
18
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
19
|
};
|
|
20
|
+
/** Set once the publish policy gate refuses, the one failure that exits 2. */
|
|
21
|
+
private refusedByPolicyGate;
|
|
22
|
+
/** Exit 2 is the publish gate's refusal; every other failure, flag errors included, exits 1. */
|
|
23
|
+
protected catch(error: Error & {
|
|
24
|
+
oclif?: {
|
|
25
|
+
exit?: number;
|
|
26
|
+
};
|
|
27
|
+
}): Promise<void>;
|
|
19
28
|
run(): Promise<void>;
|
|
20
29
|
private editFile;
|
|
21
30
|
private editFunctionContent;
|
|
22
31
|
private fetchFunctionCode;
|
|
23
32
|
private promptForFunctionId;
|
|
24
33
|
private readStdin;
|
|
34
|
+
/**
|
|
35
|
+
* The live-branch publish gate refused the update: the verdict on stdout (under `-o json`,
|
|
36
|
+
* `{updated: false, message, policy_gate}`), then the platform's message and how to proceed,
|
|
37
|
+
* exiting 2.
|
|
38
|
+
*/
|
|
39
|
+
private refusePublish;
|
|
25
40
|
}
|
|
@@ -5,6 +5,7 @@ import * as fs from 'node:fs';
|
|
|
5
5
|
import * as os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import BaseCommand, { buildUserAgent } from '../../../base-command.js';
|
|
8
|
+
import { BLANK_POLICY_OVERRIDE, blankPolicyOverride, gateRefusal, publishRefusal, quoted } from '../../../utils/policy/gate.js';
|
|
8
9
|
export default class FunctionEdit extends BaseCommand {
|
|
9
10
|
static args = {
|
|
10
11
|
function_id: Args.string({
|
|
@@ -81,6 +82,7 @@ Name: my_function
|
|
|
81
82
|
options: ['summary', 'json'],
|
|
82
83
|
required: false,
|
|
83
84
|
}),
|
|
85
|
+
'policy-override': Flags.string({ description: 'Publish past a blocking live-branch publish policy gate with an audited reason (requires workspace:policy update)', hidden: true }),
|
|
84
86
|
publish: Flags.boolean({
|
|
85
87
|
default: true,
|
|
86
88
|
description: 'Publish the function after editing',
|
|
@@ -99,8 +101,18 @@ Name: my_function
|
|
|
99
101
|
required: false,
|
|
100
102
|
}),
|
|
101
103
|
};
|
|
104
|
+
/** Set once the publish policy gate refuses, the one failure that exits 2. */
|
|
105
|
+
refusedByPolicyGate = false;
|
|
106
|
+
/** Exit 2 is the publish gate's refusal; every other failure, flag errors included, exits 1. */
|
|
107
|
+
async catch(error) {
|
|
108
|
+
if (this.refusedByPolicyGate)
|
|
109
|
+
return super.catch(error);
|
|
110
|
+
return this.catchAsOperational(error);
|
|
111
|
+
}
|
|
102
112
|
async run() {
|
|
103
113
|
const { args, flags } = await this.parse(FunctionEdit);
|
|
114
|
+
if (blankPolicyOverride(flags['policy-override']))
|
|
115
|
+
this.error(BLANK_POLICY_OVERRIDE);
|
|
104
116
|
const { profile, profileName } = this.resolveProfile(flags);
|
|
105
117
|
// Validate flag combinations
|
|
106
118
|
if (flags.edit && !flags.file) {
|
|
@@ -175,6 +187,10 @@ Name: my_function
|
|
|
175
187
|
include_xanoscript: 'false',
|
|
176
188
|
publish: flags.publish ? 'true' : 'false',
|
|
177
189
|
});
|
|
190
|
+
// A reason from someone with workspace:policy update publishes past a blocking publish gate.
|
|
191
|
+
const overrideReason = flags['policy-override']?.trim();
|
|
192
|
+
if (overrideReason)
|
|
193
|
+
queryParams.set('override_reason', overrideReason);
|
|
178
194
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function/${functionId}?${queryParams.toString()}`;
|
|
179
195
|
// Update function via API
|
|
180
196
|
try {
|
|
@@ -188,6 +204,9 @@ Name: my_function
|
|
|
188
204
|
method: 'PUT',
|
|
189
205
|
}, flags.verbose, profile.access_token);
|
|
190
206
|
if (!response.ok) {
|
|
207
|
+
const refused = await gateRefusal(response);
|
|
208
|
+
if (refused)
|
|
209
|
+
this.refusePublish(refused, flags, functionId);
|
|
191
210
|
const errorText = await response.text();
|
|
192
211
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
193
212
|
}
|
|
@@ -208,6 +227,8 @@ Name: my_function
|
|
|
208
227
|
}
|
|
209
228
|
}
|
|
210
229
|
catch (error) {
|
|
230
|
+
if (this.refusedByPolicyGate)
|
|
231
|
+
throw error;
|
|
211
232
|
if (error instanceof Error) {
|
|
212
233
|
this.error(`Failed to update function: ${error.message}`);
|
|
213
234
|
}
|
|
@@ -428,4 +449,22 @@ Name: my_function
|
|
|
428
449
|
process.stdin.resume();
|
|
429
450
|
});
|
|
430
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* The live-branch publish gate refused the update: the verdict on stdout (under `-o json`,
|
|
454
|
+
* `{updated: false, message, policy_gate}`), then the platform's message and how to proceed,
|
|
455
|
+
* exiting 2.
|
|
456
|
+
*/
|
|
457
|
+
refusePublish(refused, flags, functionId) {
|
|
458
|
+
const workspace = flags.workspace ? ` -w ${quoted(flags.workspace)}` : '';
|
|
459
|
+
const source = flags.file ? ` -f ${quoted(flags.file)}` : flags.stdin ? ' --stdin' : '';
|
|
460
|
+
const { lines, message } = publishRefusal(refused, {
|
|
461
|
+
json: flags.output === 'json',
|
|
462
|
+
outcome: 'updated',
|
|
463
|
+
rerun: `xano function edit ${quoted(functionId)}${workspace}${source} --policy-override "<why>"`,
|
|
464
|
+
});
|
|
465
|
+
for (const line of lines)
|
|
466
|
+
this.log(line);
|
|
467
|
+
this.refusedByPolicyGate = true;
|
|
468
|
+
this.error(message, { exit: 2 });
|
|
469
|
+
}
|
|
431
470
|
}
|