@xano/cli 1.3.1-beta.1 → 1.3.1

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.
Files changed (96) hide show
  1. package/README.md +1 -269
  2. package/dist/base-command.d.ts +1 -12
  3. package/dist/base-command.js +22 -36
  4. package/dist/commands/branch/set_live/index.d.ts +0 -22
  5. package/dist/commands/branch/set_live/index.js +1 -60
  6. package/dist/commands/ephemeral/push/index.js +1 -6
  7. package/dist/commands/function/create/index.d.ts +0 -15
  8. package/dist/commands/function/create/index.js +0 -40
  9. package/dist/commands/function/edit/index.d.ts +0 -15
  10. package/dist/commands/function/edit/index.js +0 -40
  11. package/dist/commands/release/create/index.js +0 -7
  12. package/dist/commands/release/deploy/index.d.ts +0 -28
  13. package/dist/commands/release/deploy/index.js +6 -90
  14. package/dist/commands/release/import/index.js +1 -5
  15. package/dist/commands/release/list/index.d.ts +0 -7
  16. package/dist/commands/release/list/index.js +10 -54
  17. package/dist/commands/release/push/index.js +1 -19
  18. package/dist/commands/sandbox/push/index.js +1 -6
  19. package/dist/commands/tenant/create/index.d.ts +0 -3
  20. package/dist/commands/tenant/create/index.js +0 -21
  21. package/dist/commands/tenant/deploy_release/index.d.ts +0 -40
  22. package/dist/commands/tenant/deploy_release/index.js +4 -186
  23. package/dist/commands/tenant/edit/index.d.ts +0 -2
  24. package/dist/commands/tenant/edit/index.js +4 -39
  25. package/dist/commands/workspace/pull/index.d.ts +0 -10
  26. package/dist/commands/workspace/pull/index.js +7 -70
  27. package/dist/commands/workspace/push/index.d.ts +0 -19
  28. package/dist/commands/workspace/push/index.js +5 -124
  29. package/dist/utils/document-parser.d.ts +0 -10
  30. package/dist/utils/document-parser.js +0 -21
  31. package/dist/utils/multidoc-push.d.ts +2 -55
  32. package/dist/utils/multidoc-push.js +27 -103
  33. package/oclif.manifest.json +3142 -6141
  34. package/package.json +1 -7
  35. package/dist/commands/policy/catalogue/index.d.ts +0 -15
  36. package/dist/commands/policy/catalogue/index.js +0 -29
  37. package/dist/commands/policy/create/index.d.ts +0 -18
  38. package/dist/commands/policy/create/index.js +0 -39
  39. package/dist/commands/policy/delete/index.d.ts +0 -19
  40. package/dist/commands/policy/delete/index.js +0 -61
  41. package/dist/commands/policy/evaluate/index.d.ts +0 -32
  42. package/dist/commands/policy/evaluate/index.js +0 -71
  43. package/dist/commands/policy/list/index.d.ts +0 -14
  44. package/dist/commands/policy/list/index.js +0 -21
  45. package/dist/commands/policy/parse/index.d.ts +0 -19
  46. package/dist/commands/policy/parse/index.js +0 -18
  47. package/dist/commands/policy/publish/index.d.ts +0 -20
  48. package/dist/commands/policy/publish/index.js +0 -38
  49. package/dist/commands/policy/runs/index.d.ts +0 -54
  50. package/dist/commands/policy/runs/index.js +0 -273
  51. package/dist/commands/policy/status/index.d.ts +0 -21
  52. package/dist/commands/policy/status/index.js +0 -54
  53. package/dist/commands/skills/pull/index.d.ts +0 -30
  54. package/dist/commands/skills/pull/index.js +0 -118
  55. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +0 -31
  56. package/dist/commands/tenant_deploy_request/bypass/index.js +0 -109
  57. package/dist/commands/tenant_deploy_request/create/index.d.ts +0 -21
  58. package/dist/commands/tenant_deploy_request/create/index.js +0 -108
  59. package/dist/commands/tenant_deploy_request/edit/index.d.ts +0 -22
  60. package/dist/commands/tenant_deploy_request/edit/index.js +0 -103
  61. package/dist/commands/tenant_deploy_request/get/index.d.ts +0 -25
  62. package/dist/commands/tenant_deploy_request/get/index.js +0 -116
  63. package/dist/commands/tenant_deploy_request/list/index.d.ts +0 -20
  64. package/dist/commands/tenant_deploy_request/list/index.js +0 -122
  65. package/dist/commands/tenant_deploy_request/revision/index.d.ts +0 -21
  66. package/dist/commands/tenant_deploy_request/revision/index.js +0 -83
  67. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +0 -38
  68. package/dist/commands/tenant_deploy_request/set_status/index.js +0 -159
  69. package/dist/policy-command.d.ts +0 -90
  70. package/dist/policy-command.js +0 -94
  71. package/dist/utils/policy/catalogue.d.ts +0 -7
  72. package/dist/utils/policy/catalogue.js +0 -72
  73. package/dist/utils/policy/errors.d.ts +0 -12
  74. package/dist/utils/policy/errors.js +0 -61
  75. package/dist/utils/policy/feedback.d.ts +0 -68
  76. package/dist/utils/policy/feedback.js +0 -165
  77. package/dist/utils/policy/findings.d.ts +0 -23
  78. package/dist/utils/policy/findings.js +0 -65
  79. package/dist/utils/policy/gate.d.ts +0 -161
  80. package/dist/utils/policy/gate.js +0 -201
  81. package/dist/utils/policy/goal.d.ts +0 -46
  82. package/dist/utils/policy/goal.js +0 -95
  83. package/dist/utils/policy/permission.d.ts +0 -23
  84. package/dist/utils/policy/permission.js +0 -84
  85. package/dist/utils/policy/push-gate.d.ts +0 -5
  86. package/dist/utils/policy/push-gate.js +0 -29
  87. package/dist/utils/policy/release.d.ts +0 -85
  88. package/dist/utils/policy/release.js +0 -115
  89. package/dist/utils/policy/request.d.ts +0 -55
  90. package/dist/utils/policy/request.js +0 -115
  91. package/dist/utils/policy/runs.d.ts +0 -30
  92. package/dist/utils/policy/runs.js +0 -101
  93. package/dist/utils/policy/status.d.ts +0 -47
  94. package/dist/utils/policy/status.js +0 -117
  95. package/dist/utils/policy/types.d.ts +0 -185
  96. package/dist/utils/policy/types.js +0 -1
@@ -10,11 +10,4 @@ export default class ReleaseList extends BaseCommand {
10
10
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  };
12
12
  run(): Promise<void>;
13
- /**
14
- * The releases' stored policy checks, read once for the whole list. The list never fails or
15
- * changes because of this read: it answers `null` when the checks cannot be read (the Policies
16
- * feature is off, the credential lacks `workspace:policy` read, or the request failed), and the
17
- * list then prints as it would without policies.
18
- */
19
- private policyChecks;
20
13
  }
@@ -1,21 +1,13 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
3
  import { buildPagingJson } from '../../../utils/paging.js';
4
- import { readReleasePolicyChecks, releasePolicyCheckTag } from '../../../utils/policy/release.js';
5
4
  export default class ReleaseList extends BaseCommand {
6
- static description = "List all releases in a workspace, each with its stored policy check when the credential can read policies (the release's own check, as Studio's Policies column shows it)";
5
+ static description = 'List all releases in a workspace';
7
6
  static examples = [
8
7
  `$ xano release list
9
8
  Releases in workspace 5:
10
9
  - v1.0 (ID: 10) - main
11
10
  - v1.1-hotfix (ID: 11) - main [hotfix]
12
- `,
13
- `$ xano release list
14
- Releases in workspace 5:
15
- - v1.2 (ID: 12) - main (9/28/2026, 4:12:03 PM PDT) [policies: 2 blocking, 1 advisory]
16
- - v1.1-hotfix (ID: 11) - main [hotfix] (9/27/2026, 9:40:15 AM PDT) [policies: passed]
17
- - v1.0 (ID: 10) - main (9/20/2026, 1:05:44 PM PDT) [policies: not checked]
18
- Policy check details: xano policy runs --release <name>
19
11
  `,
20
12
  `$ xano release list -w 5 --output json`,
21
13
  ];
@@ -65,19 +57,22 @@ Policy check details: xano policy runs --release <name>
65
57
  else {
66
58
  this.error('Unexpected API response format');
67
59
  }
68
- const checks = await this.policyChecks(profile, String(workspaceId), releases, flags.verbose);
69
60
  if (flags.output === 'json') {
70
- this.log(JSON.stringify(buildPagingJson({ items: withPolicyChecks(releases, checks) }, { tier: 'none' }), null, 2));
61
+ this.log(JSON.stringify(buildPagingJson({ items: releases }, { tier: 'none' }), null, 2));
71
62
  }
72
63
  else if (releases.length === 0) {
73
64
  this.log('No releases found');
74
65
  }
75
66
  else {
76
67
  this.log(`Releases in workspace ${workspaceId}:`);
77
- for (const release of releases)
78
- this.log(releaseLine(release, checks));
79
- if (checks)
80
- this.log('Policy check details: xano policy runs --release <name>');
68
+ for (const release of releases) {
69
+ const branch = release.branch ? ` - ${release.branch}` : '';
70
+ const hotfix = release.hotfix ? ' [hotfix]' : '';
71
+ const createdAt = release.created_at
72
+ ? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
73
+ : '';
74
+ this.log(` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}`);
75
+ }
81
76
  }
82
77
  }
83
78
  catch (error) {
@@ -89,43 +84,4 @@ Policy check details: xano policy runs --release <name>
89
84
  }
90
85
  }
91
86
  }
92
- /**
93
- * The releases' stored policy checks, read once for the whole list. The list never fails or
94
- * changes because of this read: it answers `null` when the checks cannot be read (the Policies
95
- * feature is off, the credential lacks `workspace:policy` read, or the request failed), and the
96
- * list then prints as it would without policies.
97
- */
98
- async policyChecks(profile, workspace, releases, verbose) {
99
- if (releases.length === 0)
100
- return null;
101
- return readReleasePolicyChecks({ logToStderr: (message) => this.logToStderr(message), verboseFetch: (...args) => this.verboseFetch(...args) }, { branch: '', profile, verbose, workspace }, releases.map(release => release.id));
102
- }
103
- }
104
- /** A release's summary line, tagged with its stored check when the checks were read. */
105
- function releaseLine(release, checks) {
106
- const branch = release.branch ? ` - ${release.branch}` : '';
107
- const hotfix = release.hotfix ? ' [hotfix]' : '';
108
- const createdAt = release.created_at
109
- ? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
110
- : '';
111
- const policies = checks ? ` ${releasePolicyCheckTag(checks.get(release.id))}` : '';
112
- return ` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}${policies}`;
113
- }
114
- /**
115
- * The releases for `-o json`: each gains `policy_check` (its stored check as the platform lists
116
- * it, without the release id it is keyed by, or `null` for a release with no stored check) when
117
- * the checks were read, and stays as served when they could not be, so a reader can tell an
118
- * unchecked release (`null`) from checks the CLI could not read (absent).
119
- */
120
- function withPolicyChecks(releases, checks) {
121
- if (!checks)
122
- return releases;
123
- return releases.map(release => {
124
- const check = checks.get(release.id);
125
- if (!check)
126
- return { ...release, policy_check: null };
127
- const stored = { ...check };
128
- delete stored.release_id;
129
- return { ...release, policy_check: stored };
130
- });
131
87
  }
@@ -2,9 +2,7 @@ import { Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import BaseCommand from '../../../base-command.js';
5
- import { findFilesWithGuid, parseDocument } from '../../../utils/document-parser.js';
6
- import { policiesSkippedNotice } from '../../../utils/policy/feedback.js';
7
- import { releasePolicyRunLines } from '../../../utils/policy/release.js';
5
+ import { findFilesWithGuid } from '../../../utils/document-parser.js';
8
6
  export default class ReleasePush extends BaseCommand {
9
7
  static description = '[IMPORTANT] ALWAYS confirm with the user before creating a release from local files. Creates a new release from local XanoScript files via the multidoc endpoint.';
10
8
  static examples = [
@@ -112,12 +110,6 @@ Output release details as JSON
112
110
  if (documentEntries.length === 0) {
113
111
  this.error(`All .xs files in ${flags.directory} are empty`);
114
112
  }
115
- // A release carries its policy files like its code.
116
- const policyKeys = documentEntries
117
- .map((entry) => parseDocument(entry.content))
118
- .filter((document) => document?.type === 'policy')
119
- .map((document) => document.name)
120
- .sort();
121
113
  const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
122
114
  // Construct the API URL with query params
123
115
  const queryParams = new URLSearchParams({
@@ -166,10 +158,6 @@ Output release details as JSON
166
158
  this.error(errorMessage);
167
159
  }
168
160
  const release = (await response.json());
169
- // The platform says which policy files it left out: all of them while the Policies feature is
170
- // off, and those of a caller who may not author policies, whose release carries the live
171
- // branch's policies instead.
172
- const skipped = policiesSkippedNotice(release);
173
161
  if (flags.output === 'json') {
174
162
  this.log(JSON.stringify(release, null, 2));
175
163
  }
@@ -183,14 +171,8 @@ Output release details as JSON
183
171
  this.log(` Description: ${release.description}`);
184
172
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
185
173
  this.log(` Documents: ${documentEntries.length}`);
186
- if (policyKeys.length > 0 && !skipped)
187
- this.log(` Policy documents: ${policyKeys.length} (${policyKeys.join(', ')})`);
188
- for (const line of releasePolicyRunLines(release.policy_run, release.name ?? flags.name))
189
- this.log(line);
190
174
  this.log(` Time: ${elapsed}s`);
191
175
  }
192
- if (skipped)
193
- this.warn(skipped);
194
176
  }
195
177
  catch (error) {
196
178
  if (error instanceof Error) {
@@ -4,7 +4,6 @@ import { resolve } from 'node:path';
4
4
  import open from 'open';
5
5
  import BaseCommand from '../../../base-command.js';
6
6
  import { executePush } from '../../../utils/multidoc-push.js';
7
- import { policiesSkippedNotice } from '../../../utils/policy/feedback.js';
8
7
  export default class SandboxPush extends BaseCommand {
9
8
  static description = '[IMPORTANT] ALWAYS run --dry-run first and show the user the output before pushing. Push local documents to your sandbox environment via multidoc import. By default, only changed files are pushed (partial mode). Use --sync to push all files. Shows a preview of changes before pushing unless --force is specified. Use --dry-run to preview only. Include/exclude glob filters are intentionally not supported on sandbox push — partial pushes can hide deletions during review and lead to data loss when promoted to the workspace. Pushing into a sandbox that currently holds a different workspace will prompt for confirmation; run `xano sandbox reset` first to start clean.';
10
9
  static examples = [
@@ -127,7 +126,7 @@ Push and open sandbox review in the browser
127
126
  truncate: flags.truncate,
128
127
  verbose: flags.verbose,
129
128
  };
130
- const result = await executePush({
129
+ await executePush({
131
130
  accessToken: profile.access_token,
132
131
  branch: '',
133
132
  command: this,
@@ -138,10 +137,6 @@ Push and open sandbox review in the browser
138
137
  },
139
138
  verboseFetch: this.verboseFetch.bind(this),
140
139
  }, target, pushFlags);
141
- // When the platform leaves policy files out, it says so once.
142
- const skipped = policiesSkippedNotice(result.response ?? result.preview);
143
- if (skipped)
144
- this.warn(skipped);
145
140
  if (flags.review) {
146
141
  await this.openReview(profile.instance_origin, profile.access_token, flags.verbose);
147
142
  }
@@ -6,8 +6,6 @@ export default class TenantCreate extends BaseCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
- allow_deploy_bypass: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
- allow_quick_deploy: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
9
  cluster_id: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
10
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
11
  domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -15,7 +13,6 @@ export default class TenantCreate extends BaseCommand {
15
13
  license: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
14
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
15
  platform_id: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
- required_reviewers: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
19
16
  tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
17
  type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
21
18
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -17,20 +17,9 @@ Created tenant: Production (production) - ID: 42
17
17
  `,
18
18
  `$ xano tenant create "Staging" --description "Staging env" --cluster_id 1 --platform_id 1 --type tier2 -o json`,
19
19
  `$ xano tenant create "Staging" --type tier2 --cluster_id 1 --license ./license.yaml`,
20
- `$ xano tenant create "Production" --required_reviewers 1 --allow_deploy_bypass`,
21
20
  ];
22
21
  static flags = {
23
22
  ...BaseCommand.baseFlags,
24
- allow_deploy_bypass: Flags.boolean({
25
- default: false,
26
- description: 'Allow authorized users to bypass the deploy approval gate on this tenant (via tenant_deploy_request bypass). Has no effect unless --required_reviewers is also set above 0.',
27
- required: false,
28
- }),
29
- allow_quick_deploy: Flags.boolean({
30
- default: false,
31
- description: 'Allow this tenant to skip the deploy approval gate entirely for quick deploys, regardless of --required_reviewers.',
32
- required: false,
33
- }),
34
23
  cluster_id: Flags.integer({
35
24
  description: 'Cluster ID to deploy to (required for tier2/tier3)',
36
25
  required: false,
@@ -65,11 +54,6 @@ Created tenant: Production (production) - ID: 42
65
54
  description: 'Platform ID to use',
66
55
  required: false,
67
56
  }),
68
- required_reviewers: Flags.integer({
69
- default: 0,
70
- description: 'When greater than 0, that many distinct reviewers must approve a tenant_deploy_request before a release may be deployed to this tenant. 0 (default) means not gated.',
71
- required: false,
72
- }),
73
57
  tasks: Flags.boolean({
74
58
  allowNo: true,
75
59
  default: true,
@@ -95,11 +79,6 @@ Created tenant: Production (production) - ID: 42
95
79
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
96
80
  }
97
81
  const body = {
98
- deploy_settings: {
99
- allow_deploy_bypass: flags.allow_deploy_bypass,
100
- allow_quick_deploy: flags.allow_quick_deploy,
101
- required_reviewers: flags.required_reviewers,
102
- },
103
82
  display: args.display,
104
83
  ingress: flags.ingress,
105
84
  license: flags.type,
@@ -6,52 +6,12 @@ export default class TenantDeployRelease extends BaseCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
- check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
9
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
- 'override-reason': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
10
  release: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
11
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
12
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
13
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
14
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
15
  };
18
- /** Set once the policy gate refuses the deploy, the one failure that exits 2. */
19
- private refusedByPolicyGate;
20
- /** Exit 2 is the policy gate's refusal; every other failure, flag errors included, exits 1. */
21
- protected catch(error: Error & {
22
- oclif?: {
23
- exit?: number;
24
- };
25
- }): Promise<void>;
26
16
  run(): Promise<void>;
27
- /**
28
- * `--check`: what the policy gate would decide for this release on this tenant, without deploying.
29
- * Exits 2 when the deploy would be refused; a preview the platform cannot answer exits 1.
30
- */
31
- private checkPolicyGate;
32
- /**
33
- * Does an approved tenant_deploy_request already exist for this (tenant, release)
34
- * pair? The list endpoint has no release filter, so this fetches approved
35
- * requests for the tenant and filters client-side by the resolved release name.
36
- */
37
- private hasApprovedDeployRequest;
38
- /** A deploy the policy gate refused: its verdict, then the refusal and how to proceed, exiting 2. */
39
- private refuseByPolicyGate;
40
- /**
41
- * Does this tenant have the deploy approval gate enabled?
42
- *
43
- * Returns false if the tenant has `allow_quick_deploy` set — that flag lets a
44
- * tenant skip the approval gate entirely for quick deploys, regardless of
45
- * `required_reviewers`.
46
- *
47
- * Deliberately uses `tenant list` rather than `tenant get`: the single-tenant
48
- * GET route's output whitelist (tenant-detail.yaml) omits `deploy_settings`
49
- * (which holds `required_reviewers`, `allow_deploy_bypass`, and
50
- * `allow_quick_deploy`) even though the list route's output includes it
51
- * — confirmed live against a running instance. Filed as a backend bug; this
52
- * is the workaround until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
53
- */
54
- private tenantRequiresApproval;
55
- /** A release policy the tenant's import could not land (its key is held by another policy) is never dropped silently. */
56
- private warnPoliciesSkipped;
57
17
  }
@@ -1,7 +1,5 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
- import { deployedGateLine, fetchPolicyGate, gateExitCode, gateLines, gateOverrideHint, gateRefusal, gateRefusalOutput, quoted, } from '../../../utils/policy/gate.js';
4
- import { policyPermissionGuidance } from '../../../utils/policy/permission.js';
5
3
  export default class TenantDeployRelease extends BaseCommand {
6
4
  static args = {
7
5
  tenant_name: Args.string({
@@ -9,26 +7,15 @@ export default class TenantDeployRelease extends BaseCommand {
9
7
  required: true,
10
8
  }),
11
9
  };
12
- static description = "[CRITICAL] STOP and confirm with the user before deploying a release to a tenant; this mutates the live tenant. Deploys a release to a tenant. A deploy is gated on the release's own policy check: any blocking finding in the release refuses it (exit 2) unless --override-reason is given, whatever the tenant runs today, so the release gets the same verdict on every tenant. --check previews that verdict without deploying.";
10
+ static description = '[CRITICAL] STOP and confirm with the user before deploying a release to a tenant; this mutates the live tenant. Deploys a release to a tenant.';
13
11
  static examples = [
14
12
  `$ xano tenant deploy_release t1234-abcd-xyz1 --release v1.0
15
13
  Deployed release "v1.0" to tenant: My Tenant (my-tenant)
16
14
  `,
17
15
  `$ xano tenant deploy_release t1234-abcd-xyz1 --release v1.0 -o json`,
18
- `$ xano tenant deploy_release prod --release v1.2 --check
19
- Checked release "v1.2" for tenant "prod"; nothing was deployed.
20
- Policy gate: blocked
21
- Deploy blocked: this release has 1 blocking policy finding.
22
- `,
23
- `$ xano tenant deploy_release prod --release v1.2 --override-reason "Hotfix; AUTH-001 finding tracked in JIRA-12"`,
24
16
  ];
25
17
  static flags = {
26
18
  ...BaseCommand.baseFlags,
27
- check: Flags.boolean({
28
- default: false,
29
- description: "Preview the release's policy gate on this tenant without deploying; exits 2 when the deploy would be refused",
30
- exclusive: ['override-reason'],
31
- }),
32
19
  output: Flags.string({
33
20
  char: 'o',
34
21
  default: 'summary',
@@ -36,10 +23,6 @@ Policy gate: blocked
36
23
  options: ['summary', 'json'],
37
24
  required: false,
38
25
  }),
39
- 'override-reason': Flags.string({
40
- description: 'Deploy past a blocking policy gate, recording this reason (needs the workspace:policy update permission; audited)',
41
- exclusive: ['check'],
42
- }),
43
26
  release: Flags.string({
44
27
  char: 'r',
45
28
  description: 'Release name to deploy',
@@ -51,14 +34,6 @@ Policy gate: blocked
51
34
  required: false,
52
35
  }),
53
36
  };
54
- /** Set once the policy gate refuses the deploy, the one failure that exits 2. */
55
- refusedByPolicyGate = false;
56
- /** Exit 2 is the policy gate's refusal; every other failure, flag errors included, exits 1. */
57
- async catch(error) {
58
- if (this.refusedByPolicyGate)
59
- return super.catch(error);
60
- return this.catchAsOperational(error);
61
- }
62
37
  async run() {
63
38
  const { args, flags } = await this.parse(TenantDeployRelease);
64
39
  const { profile } = this.resolveProfile(flags);
@@ -68,46 +43,12 @@ Policy gate: blocked
68
43
  }
69
44
  const releaseName = flags.release;
70
45
  const tenantName = args.tenant_name;
71
- if (flags.check) {
72
- await this.checkPolicyGate({ flags, profile, releaseName, tenantName, workspaceId });
73
- return;
74
- }
75
- const overrideReason = flags['override-reason']?.trim();
76
- if (flags['override-reason'] !== undefined && !overrideReason) {
77
- this.error('--override-reason needs a reason: say why this deploy may proceed past the policy gate.');
78
- }
79
- // Pre-flight: the "approval required" error must always be shown before any
80
- // permission/RBAC error. The backend's own deploy route checks RBAC first
81
- // and the approval gate second (inside mvp:tenant_deploy_release), so a
82
- // caller who lacks deploy permission would otherwise never learn a request
83
- // is what's actually needed. Checking here first, with an endpoint that
84
- // only needs read scope, guarantees the right error is shown regardless of
85
- // whether the caller can deploy at all.
86
- const requiresApproval = await this.tenantRequiresApproval(profile, workspaceId, tenantName, flags.verbose);
87
- if (requiresApproval) {
88
- const hasApprovedRequest = await this.hasApprovedDeployRequest({
89
- profile,
90
- releaseName,
91
- tenantName,
92
- verbose: flags.verbose,
93
- workspaceId,
94
- });
95
- if (!hasApprovedRequest) {
96
- this.error(`Tenant "${tenantName}" requires an approved deploy request before "${releaseName}" can be deployed.\n` +
97
- `Run: xano tenant_deploy_request create "Deploy ${releaseName} to ${tenantName}" --tenant ${tenantName} --release ${releaseName} --reviewers <id,id,...>`);
98
- }
99
- }
100
46
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/deploy`;
101
47
  this.warn('This may take a few minutes. Please be patient.');
102
48
  const startTime = Date.now();
103
- // A refusal by the policy gate exits 2 with its verdict; the catch that rewords other failures keeps it.
104
- let refused = null;
105
49
  try {
106
50
  const response = await this.verboseFetch(apiUrl, {
107
- body: JSON.stringify({
108
- release_name: releaseName,
109
- ...(overrideReason ? { override_policy: true, override_reason: overrideReason } : {}),
110
- }),
51
+ body: JSON.stringify({ release_name: releaseName }),
111
52
  headers: {
112
53
  accept: 'application/json',
113
54
  Authorization: `Bearer ${profile.access_token}`,
@@ -116,22 +57,8 @@ Policy gate: blocked
116
57
  method: 'POST',
117
58
  }, flags.verbose, profile.access_token);
118
59
  if (!response.ok) {
119
- refused = await gateRefusal(response);
120
- if (refused) {
121
- const override = `xano tenant deploy_release ${quoted(tenantName)} --release ${quoted(releaseName)} --override-reason "<why>"`;
122
- this.refuseByPolicyGate(refused, flags, override);
123
- }
124
- let message = await this.parseApiError(response, 'API request failed');
125
- // Only reached once the pre-flight above has already confirmed the
126
- // approval gate is satisfied or not applicable — so a permission error
127
- // here is genuinely about RBAC, not about a missing approval. If the
128
- // tenant supports deploy requests, mention that path as an alternative
129
- // to direct deploy, since opening a request doesn't require deploy
130
- // permission.
131
- if ((response.status === 401 || response.status === 403) && requiresApproval) {
132
- message += `\n\nYou don't have permission to deploy to this tenant directly, but it supports deploy requests: xano tenant_deploy_request create "Deploy ${releaseName} to ${tenantName}" --tenant ${tenantName} --release ${releaseName} --reviewers <id,id,...>`;
133
- }
134
- this.error(message);
60
+ const errorText = await response.text();
61
+ this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
135
62
  }
136
63
  const tenant = (await response.json());
137
64
  if (flags.output === 'json') {
@@ -144,15 +71,10 @@ Policy gate: blocked
144
71
  this.log(` State: ${tenant.state}`);
145
72
  if (tenant.release?.name)
146
73
  this.log(` Release: ${tenant.release.name}`);
147
- if (tenant.policy_gate?.status)
148
- this.log(` ${deployedGateLine(tenant.policy_gate)}`);
149
74
  this.log(` Time: ${elapsed}s`);
150
75
  }
151
- this.warnPoliciesSkipped(tenant);
152
76
  }
153
77
  catch (error) {
154
- if (refused)
155
- throw error;
156
78
  if (error instanceof Error) {
157
79
  this.error(`Failed to deploy to tenant: ${error.message}`);
158
80
  }
@@ -161,108 +83,4 @@ Policy gate: blocked
161
83
  }
162
84
  }
163
85
  }
164
- /**
165
- * `--check`: what the policy gate would decide for this release on this tenant, without deploying.
166
- * Exits 2 when the deploy would be refused; a preview the platform cannot answer exits 1.
167
- */
168
- async checkPolicyGate(opts) {
169
- const { flags, profile, releaseName, tenantName, workspaceId } = opts;
170
- const result = await fetchPolicyGate({ verboseFetch: (...args) => this.verboseFetch(...args) }, { profile, release: releaseName, tenant: tenantName, verbose: flags.verbose, workspace: workspaceId });
171
- if ('unavailable' in result) {
172
- const { message, payload, status } = result.unavailable;
173
- this.error(`Policy gate preview failed (${status}): ${message}${policyPermissionGuidance(status, payload)}`);
174
- }
175
- const { answer } = result;
176
- const exit = gateExitCode(answer);
177
- if (flags.output === 'json') {
178
- this.log(JSON.stringify(answer, null, 2));
179
- }
180
- else {
181
- this.log(`Checked release "${releaseName}" for tenant "${tenantName}"; nothing was deployed.`);
182
- for (const line of gateLines(answer))
183
- this.log(line);
184
- if (exit) {
185
- this.log(answer.can_override
186
- ? `To deploy past the gate: xano tenant deploy_release ${quoted(tenantName)} --release ${quoted(releaseName)} --override-reason "<why>"`
187
- : 'A deploy is refused unless someone with the workspace:policy update permission overrides the gate.');
188
- }
189
- }
190
- if (exit)
191
- process.exitCode = exit;
192
- }
193
- /**
194
- * Does an approved tenant_deploy_request already exist for this (tenant, release)
195
- * pair? The list endpoint has no release filter, so this fetches approved
196
- * requests for the tenant and filters client-side by the resolved release name.
197
- */
198
- async hasApprovedDeployRequest(opts) {
199
- const { profile, releaseName, tenantName, verbose, workspaceId } = opts;
200
- const params = new URLSearchParams({ per_page: '100', status: 'approved', tenant_name: tenantName });
201
- const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request?${params}`;
202
- const response = await this.verboseFetch(apiUrl, {
203
- headers: {
204
- accept: 'application/json',
205
- Authorization: `Bearer ${profile.access_token}`,
206
- },
207
- method: 'GET',
208
- }, verbose, profile.access_token);
209
- if (!response.ok) {
210
- // A read-scope failure here means something is more broadly wrong (e.g.
211
- // no workspace access at all) — surface it rather than silently treating
212
- // it as "no approval found", which would misreport as approval-required.
213
- const message = await this.parseApiError(response, 'Failed to check for an approved deploy request');
214
- this.error(message);
215
- }
216
- const data = (await response.json());
217
- const items = Array.isArray(data) ? data : (data.items ?? []);
218
- return items.some((item) => item.status === 'approved' && item._release?.name === releaseName);
219
- }
220
- /** A deploy the policy gate refused: its verdict, then the refusal and how to proceed, exiting 2. */
221
- refuseByPolicyGate(refused, flags, override) {
222
- for (const line of gateRefusalOutput(refused, flags.output === 'json'))
223
- this.log(line);
224
- this.refusedByPolicyGate = true;
225
- this.error(`${refused.message}${gateOverrideHint(refused.answer, override)}`, { exit: 2 });
226
- }
227
- /**
228
- * Does this tenant have the deploy approval gate enabled?
229
- *
230
- * Returns false if the tenant has `allow_quick_deploy` set — that flag lets a
231
- * tenant skip the approval gate entirely for quick deploys, regardless of
232
- * `required_reviewers`.
233
- *
234
- * Deliberately uses `tenant list` rather than `tenant get`: the single-tenant
235
- * GET route's output whitelist (tenant-detail.yaml) omits `deploy_settings`
236
- * (which holds `required_reviewers`, `allow_deploy_bypass`, and
237
- * `allow_quick_deploy`) even though the list route's output includes it
238
- * — confirmed live against a running instance. Filed as a backend bug; this
239
- * is the workaround until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
240
- */
241
- async tenantRequiresApproval(profile, workspaceId, tenantName, verbose) {
242
- const params = new URLSearchParams({ per_page: '100' });
243
- const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant?${params}`;
244
- const response = await this.verboseFetch(apiUrl, {
245
- headers: {
246
- accept: 'application/json',
247
- Authorization: `Bearer ${profile.access_token}`,
248
- },
249
- method: 'GET',
250
- }, verbose, profile.access_token);
251
- if (!response.ok) {
252
- const message = await this.parseApiError(response, 'Failed to look up tenant');
253
- this.error(message);
254
- }
255
- const data = (await response.json());
256
- const items = Array.isArray(data) ? data : (data.items ?? []);
257
- const tenant = items.find((item) => item.name === tenantName);
258
- if (tenant?.deploy_settings?.allow_quick_deploy)
259
- return false;
260
- return (tenant?.deploy_settings?.required_reviewers ?? 0) > 0;
261
- }
262
- /** A release policy the tenant's import could not land (its key is held by another policy) is never dropped silently. */
263
- warnPoliciesSkipped(tenant) {
264
- const message = tenant.policies_skipped?.message;
265
- if (message)
266
- this.warn(message);
267
- }
268
86
  }
@@ -6,7 +6,6 @@ export default class TenantEdit extends BaseCommand {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
- allow_quick_deploy: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
9
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
10
  display: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
11
  domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -14,7 +13,6 @@ export default class TenantEdit extends BaseCommand {
14
13
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
14
  proxy: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
15
  rbac: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
- required_reviewers: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
16
  tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
17
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
18
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -13,15 +13,9 @@ export default class TenantEdit extends BaseCommand {
13
13
  Updated tenant: New Name (my-tenant) - ID: 42
14
14
  `,
15
15
  `$ xano tenant edit t1234-abcd-xyz1 --no-tasks --no-ingress -o json`,
16
- `$ xano tenant edit t1234-abcd-xyz1 --required_reviewers 1`,
17
16
  ];
18
17
  static flags = {
19
18
  ...BaseCommand.baseFlags,
20
- allow_quick_deploy: Flags.boolean({
21
- allowNo: true,
22
- description: 'Allow this tenant to skip the deploy approval gate entirely for quick deploys, regardless of --required_reviewers.',
23
- required: false,
24
- }),
25
19
  description: Flags.string({
26
20
  char: 'd',
27
21
  description: 'New description',
@@ -56,10 +50,6 @@ Updated tenant: New Name (my-tenant) - ID: 42
56
50
  description: '[CRITICAL] NEVER disable RBAC without explicit user confirmation; this removes role-based access controls on the tenant. Enables/disables RBAC.',
57
51
  required: false,
58
52
  }),
59
- required_reviewers: Flags.integer({
60
- description: 'When greater than 0, that many distinct reviewers must approve a tenant_deploy_request before a release may be deployed to this tenant. 0 means not gated.',
61
- required: false,
62
- }),
63
53
  tasks: Flags.boolean({
64
54
  allowNo: true,
65
55
  description: '[IMPORTANT] ALWAYS confirm with the user before disabling background tasks; this stops scheduled jobs on the tenant. Enables/disables background tasks.',
@@ -86,17 +76,8 @@ Updated tenant: New Name (my-tenant) - ID: 42
86
76
  'Content-Type': 'application/json',
87
77
  };
88
78
  try {
89
- // Fetch current tenant state (PUT requires all fields). Deliberately uses
90
- // `tenant list` rather than `tenant get`: the single-tenant GET route's
91
- // output whitelist (tenant-detail.yaml) omits `deploy_settings` (which
92
- // holds `required_reviewers`, `allow_deploy_bypass`, and
93
- // `allow_quick_deploy`) even though the list route's output includes it
94
- // — confirmed live against a running instance. Using GET here would
95
- // silently reset those fields to their defaults on every edit that doesn't
96
- // explicitly pass them. Filed as a backend bug; this is the workaround
97
- // until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
98
- const listParams = new URLSearchParams({ per_page: '100' });
99
- const getResponse = await this.verboseFetch(`${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant?${listParams}`, {
79
+ // Fetch current tenant state (PUT requires all fields)
80
+ const getResponse = await this.verboseFetch(baseUrl, {
100
81
  headers: {
101
82
  'accept': 'application/json',
102
83
  'Authorization': `Bearer ${profile.access_token}`,
@@ -107,25 +88,9 @@ Updated tenant: New Name (my-tenant) - ID: 42
107
88
  const errorText = await getResponse.text();
108
89
  this.error(`Failed to fetch tenant: ${getResponse.status} ${getResponse.statusText}\n${errorText}`);
109
90
  }
110
- const listData = (await getResponse.json());
111
- const listItems = Array.isArray(listData) ? listData : (listData.items ?? []);
112
- const current = listItems.find((item) => item.name === tenantName);
113
- if (!current) {
114
- this.error(`Tenant "${tenantName}" not found`);
115
- }
116
- // Merge in user-provided values. allow_deploy_bypass has no edit flag (it's
117
- // fixed at `tenant create` time), but the PUT still requires the full body,
118
- // so its current value is preserved here rather than reset.
91
+ const current = await getResponse.json();
92
+ // Merge in user-provided values
119
93
  const body = {
120
- deploy_settings: {
121
- allow_deploy_bypass: current.deploy_settings?.allow_deploy_bypass ?? false,
122
- allow_quick_deploy: flags.allow_quick_deploy === undefined
123
- ? (current.deploy_settings?.allow_quick_deploy ?? false)
124
- : flags.allow_quick_deploy,
125
- required_reviewers: flags.required_reviewers === undefined
126
- ? (current.deploy_settings?.required_reviewers ?? 0)
127
- : flags.required_reviewers,
128
- },
129
94
  description: flags.description === undefined ? (current.description ?? '') : flags.description,
130
95
  display: flags.display === undefined ? (current.display ?? current.name) : flags.display,
131
96
  domain: flags.domain === undefined ? (current.domain ?? '') : flags.domain,