@xano/cli 1.0.6-beta.19 → 1.0.6-beta.20

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 CHANGED
@@ -576,9 +576,12 @@ xano tenant get <tenant_name>
576
576
  xano tenant create "My Tenant"
577
577
  xano tenant create "My Tenant" -d "Description" --type tier2 --cluster_id 1 --platform_id 5
578
578
  xano tenant create "My Tenant" --type tier2 --cluster_id 1 --license ./license.yaml
579
+ xano tenant create "Production" --required_reviewers 1 --allow_deploy_bypass
579
580
 
580
581
  # Edit a tenant
581
582
  xano tenant edit <tenant_name> --display "New Name" -d "New description"
583
+ xano tenant edit <tenant_name> --required_reviewers 1
584
+ xano tenant edit <tenant_name> --required_reviewers 0
582
585
 
583
586
  # Delete a tenant (confirmation required)
584
587
  xano tenant delete <tenant_name>
@@ -629,6 +632,60 @@ xano tenant deploy_release <tenant_name> --release v1.0
629
632
  xano tenant deploy_platform <tenant_name> --platform_id 5 --license ./license.yaml
630
633
  ```
631
634
 
635
+ If a tenant has `required_reviewers` set above 0, `tenant deploy_release` fails with an
636
+ error naming the `tenant_deploy_request` command to run instead — see below.
637
+
638
+ #### Tenant Deploy Requests
639
+
640
+ When a tenant has `required_reviewers` set above 0 (see `tenant create`/`tenant edit`
641
+ above), a release may only be deployed to it through an **approved deploy request**,
642
+ requiring that many distinct reviewer approvals. An author with no deploy permission
643
+ opens a request; a named reviewer who does hold deploy permission on the tenant votes to
644
+ approve it, and once enough reviewers have approved, it deploys as part of that action.
645
+
646
+ ```bash
647
+ # List deploy requests in a workspace (results show the target tenant and release)
648
+ xano tenant_deploy_request list
649
+ xano tenant_deploy_request list --tenant prod --status pending
650
+ xano tenant_deploy_request list --tenant prod --release v1.2
651
+ xano tenant_deploy_request list --to-review
652
+
653
+ # Get details of a specific deploy request
654
+ xano tenant_deploy_request get <id>
655
+
656
+ # Open a deploy request (submits for review immediately unless --draft is passed)
657
+ xano tenant_deploy_request create "Deploy v1.2 to prod" --tenant prod --release v1.2 --reviewers 12,45
658
+ xano tenant_deploy_request create "Deploy v1.2 to prod" --tenant prod --release v1.2 --draft
659
+
660
+ # Edit a deploy request's title, description or reviewers
661
+ xano tenant_deploy_request edit <id> --title "Deploy v1.2.1 to prod"
662
+ xano tenant_deploy_request edit <id> --reviewers 12,45,67
663
+
664
+ # Change status: draft, submit, approve, request_changes, close, or reopen
665
+ # (approve records one reviewer's vote; once enough votes are in it deploys the
666
+ # release as part of that same call)
667
+ xano tenant_deploy_request set_status <id> --status submit
668
+ xano tenant_deploy_request set_status <id> --status approve
669
+ xano tenant_deploy_request set_status <id> --status request_changes --reason "Needs a migration note"
670
+ xano tenant_deploy_request set_status <id> --status close --reason "Superseded by a newer request"
671
+ xano tenant_deploy_request set_status <id> --status reopen
672
+
673
+ # Link a newer release to a request after changes were requested (author only)
674
+ xano tenant_deploy_request revision <id> --release v1.2.1 --note "Added the missing migration"
675
+
676
+ # Deploy past the approval gate under the deploy-bypass permission (requires the
677
+ # tenant's allow_deploy_bypass to be enabled, and a separately-grantable RBAC
678
+ # permission). Every use is audited.
679
+ xano tenant_deploy_request bypass <id> --reason "Prod incident, reviewer unavailable"
680
+ ```
681
+
682
+ CI example (e.g. a GitHub Action step):
683
+
684
+ ```bash
685
+ xano tenant_deploy_request create "$PR_TITLE" --tenant prod --release v1.2 \
686
+ --description "$PR_BODY" --reviewers 12,45 -o json
687
+ ```
688
+
632
689
  #### Tenant License
633
690
 
634
691
  ```bash
@@ -6,6 +6,8 @@ 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>;
9
11
  cluster_id: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
12
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
13
  domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -13,6 +15,7 @@ export default class TenantCreate extends BaseCommand {
13
15
  license: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
16
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
17
  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>;
16
19
  type: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
20
  tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
21
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -17,9 +17,20 @@ 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`,
20
21
  ];
21
22
  static flags = {
22
23
  ...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
+ }),
23
34
  cluster_id: Flags.integer({
24
35
  description: 'Cluster ID to deploy to (required for tier2/tier3)',
25
36
  required: false,
@@ -54,6 +65,11 @@ Created tenant: Production (production) - ID: 42
54
65
  description: 'Platform ID to use',
55
66
  required: false,
56
67
  }),
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
+ }),
57
73
  type: Flags.string({
58
74
  default: 'tier1',
59
75
  description: 'Tenant type',
@@ -79,6 +95,11 @@ Created tenant: Production (production) - ID: 42
79
95
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
80
96
  }
81
97
  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
+ },
82
103
  display: args.display,
83
104
  ingress: flags.ingress,
84
105
  license: flags.type,
@@ -14,4 +14,25 @@ export default class TenantDeployRelease extends BaseCommand {
14
14
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
15
  };
16
16
  run(): Promise<void>;
17
+ /**
18
+ * Does an approved tenant_deploy_request already exist for this (tenant, release)
19
+ * pair? The list endpoint has no release filter, so this fetches approved
20
+ * requests for the tenant and filters client-side by the resolved release name.
21
+ */
22
+ private hasApprovedDeployRequest;
23
+ /**
24
+ * Does this tenant have the deploy approval gate enabled?
25
+ *
26
+ * Returns false if the tenant has `allow_quick_deploy` set — that flag lets a
27
+ * tenant skip the approval gate entirely for quick deploys, regardless of
28
+ * `required_reviewers`.
29
+ *
30
+ * Deliberately uses `tenant list` rather than `tenant get`: the single-tenant
31
+ * GET route's output whitelist (tenant-detail.yaml) omits `deploy_settings`
32
+ * (which holds `required_reviewers`, `allow_deploy_bypass`, and
33
+ * `allow_quick_deploy`) even though the list route's output includes it
34
+ * — confirmed live against a running instance. Filed as a backend bug; this
35
+ * is the workaround until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
36
+ */
37
+ private tenantRequiresApproval;
17
38
  }
@@ -43,6 +43,27 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
43
43
  }
44
44
  const releaseName = flags.release;
45
45
  const tenantName = args.tenant_name;
46
+ // Pre-flight: the "approval required" error must always be shown before any
47
+ // permission/RBAC error. The backend's own deploy route checks RBAC first
48
+ // and the approval gate second (inside mvp:tenant_deploy_release), so a
49
+ // caller who lacks deploy permission would otherwise never learn a request
50
+ // is what's actually needed. Checking here first, with an endpoint that
51
+ // only needs read scope, guarantees the right error is shown regardless of
52
+ // whether the caller can deploy at all.
53
+ const requiresApproval = await this.tenantRequiresApproval(profile, workspaceId, tenantName, flags.verbose);
54
+ if (requiresApproval) {
55
+ const hasApprovedRequest = await this.hasApprovedDeployRequest({
56
+ profile,
57
+ releaseName,
58
+ tenantName,
59
+ verbose: flags.verbose,
60
+ workspaceId,
61
+ });
62
+ if (!hasApprovedRequest) {
63
+ this.error(`Tenant "${tenantName}" requires an approved deploy request before "${releaseName}" can be deployed.\n` +
64
+ `Run: xano tenant_deploy_request create "Deploy ${releaseName} to ${tenantName}" --tenant ${tenantName} --release ${releaseName} --reviewers <id,id,...>`);
65
+ }
66
+ }
46
67
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/deploy`;
47
68
  this.warn('This may take a few minutes. Please be patient.');
48
69
  const startTime = Date.now();
@@ -57,8 +78,17 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
57
78
  method: 'POST',
58
79
  }, flags.verbose, profile.access_token);
59
80
  if (!response.ok) {
60
- const errorText = await response.text();
61
- this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
81
+ let message = await this.parseApiError(response, 'API request failed');
82
+ // Only reached once the pre-flight above has already confirmed the
83
+ // approval gate is satisfied or not applicable — so a permission error
84
+ // here is genuinely about RBAC, not about a missing approval. If the
85
+ // tenant supports deploy requests, mention that path as an alternative
86
+ // to direct deploy, since opening a request doesn't require deploy
87
+ // permission.
88
+ if ((response.status === 401 || response.status === 403) && requiresApproval) {
89
+ 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,...>`;
90
+ }
91
+ this.error(message);
62
92
  }
63
93
  const tenant = (await response.json());
64
94
  if (flags.output === 'json') {
@@ -83,4 +113,66 @@ Deployed release "v1.0" to tenant: My Tenant (my-tenant)
83
113
  }
84
114
  }
85
115
  }
116
+ /**
117
+ * Does an approved tenant_deploy_request already exist for this (tenant, release)
118
+ * pair? The list endpoint has no release filter, so this fetches approved
119
+ * requests for the tenant and filters client-side by the resolved release name.
120
+ */
121
+ async hasApprovedDeployRequest(opts) {
122
+ const { profile, releaseName, tenantName, verbose, workspaceId } = opts;
123
+ const params = new URLSearchParams({ per_page: '100', status: 'approved', tenant_name: tenantName });
124
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request?${params}`;
125
+ const response = await this.verboseFetch(apiUrl, {
126
+ headers: {
127
+ accept: 'application/json',
128
+ Authorization: `Bearer ${profile.access_token}`,
129
+ },
130
+ method: 'GET',
131
+ }, verbose, profile.access_token);
132
+ if (!response.ok) {
133
+ // A read-scope failure here means something is more broadly wrong (e.g.
134
+ // no workspace access at all) — surface it rather than silently treating
135
+ // it as "no approval found", which would misreport as approval-required.
136
+ const message = await this.parseApiError(response, 'Failed to check for an approved deploy request');
137
+ this.error(message);
138
+ }
139
+ const data = (await response.json());
140
+ const items = Array.isArray(data) ? data : (data.items ?? []);
141
+ return items.some((item) => item.status === 'approved' && item._release?.name === releaseName);
142
+ }
143
+ /**
144
+ * Does this tenant have the deploy approval gate enabled?
145
+ *
146
+ * Returns false if the tenant has `allow_quick_deploy` set — that flag lets a
147
+ * tenant skip the approval gate entirely for quick deploys, regardless of
148
+ * `required_reviewers`.
149
+ *
150
+ * Deliberately uses `tenant list` rather than `tenant get`: the single-tenant
151
+ * GET route's output whitelist (tenant-detail.yaml) omits `deploy_settings`
152
+ * (which holds `required_reviewers`, `allow_deploy_bypass`, and
153
+ * `allow_quick_deploy`) even though the list route's output includes it
154
+ * — confirmed live against a running instance. Filed as a backend bug; this
155
+ * is the workaround until it's fixed. See TENANT_DEPLOY_REQUEST_TEST.md.
156
+ */
157
+ async tenantRequiresApproval(profile, workspaceId, tenantName, verbose) {
158
+ const params = new URLSearchParams({ per_page: '100' });
159
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant?${params}`;
160
+ const response = await this.verboseFetch(apiUrl, {
161
+ headers: {
162
+ accept: 'application/json',
163
+ Authorization: `Bearer ${profile.access_token}`,
164
+ },
165
+ method: 'GET',
166
+ }, verbose, profile.access_token);
167
+ if (!response.ok) {
168
+ const message = await this.parseApiError(response, 'Failed to look up tenant');
169
+ this.error(message);
170
+ }
171
+ const data = (await response.json());
172
+ const items = Array.isArray(data) ? data : (data.items ?? []);
173
+ const tenant = items.find((item) => item.name === tenantName);
174
+ if (tenant?.deploy_settings?.allow_quick_deploy)
175
+ return false;
176
+ return (tenant?.deploy_settings?.required_reviewers ?? 0) > 0;
177
+ }
86
178
  }
@@ -6,6 +6,7 @@ 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>;
9
10
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  display: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -13,6 +14,7 @@ export default class TenantEdit extends BaseCommand {
13
14
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
15
  proxy: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
16
  rbac: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ required_reviewers: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
18
  tasks: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
19
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
20
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -13,9 +13,15 @@ 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`,
16
17
  ];
17
18
  static flags = {
18
19
  ...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
+ }),
19
25
  description: Flags.string({
20
26
  char: 'd',
21
27
  description: 'New description',
@@ -50,6 +56,10 @@ Updated tenant: New Name (my-tenant) - ID: 42
50
56
  description: '[CRITICAL] NEVER disable RBAC without explicit user confirmation; this removes role-based access controls on the tenant. Enables/disables RBAC.',
51
57
  required: false,
52
58
  }),
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
+ }),
53
63
  tasks: Flags.boolean({
54
64
  allowNo: true,
55
65
  description: '[IMPORTANT] ALWAYS confirm with the user before disabling background tasks; this stops scheduled jobs on the tenant. Enables/disables background tasks.',
@@ -76,8 +86,17 @@ Updated tenant: New Name (my-tenant) - ID: 42
76
86
  'Content-Type': 'application/json',
77
87
  };
78
88
  try {
79
- // Fetch current tenant state (PUT requires all fields)
80
- const getResponse = await this.verboseFetch(baseUrl, {
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}`, {
81
100
  headers: {
82
101
  'accept': 'application/json',
83
102
  'Authorization': `Bearer ${profile.access_token}`,
@@ -88,9 +107,25 @@ Updated tenant: New Name (my-tenant) - ID: 42
88
107
  const errorText = await getResponse.text();
89
108
  this.error(`Failed to fetch tenant: ${getResponse.status} ${getResponse.statusText}\n${errorText}`);
90
109
  }
91
- const current = await getResponse.json();
92
- // Merge in user-provided values
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.
93
119
  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
+ ? flags.allow_quick_deploy
124
+ : (current.deploy_settings?.allow_quick_deploy ?? false),
125
+ required_reviewers: flags.required_reviewers !== undefined
126
+ ? flags.required_reviewers
127
+ : (current.deploy_settings?.required_reviewers ?? 0),
128
+ },
94
129
  description: flags.description !== undefined ? flags.description : (current.description ?? ''),
95
130
  display: flags.display !== undefined ? flags.display : (current.display ?? current.name),
96
131
  domain: flags.domain !== undefined ? flags.domain : (current.domain ?? ''),
@@ -0,0 +1,20 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class TenantDeployRequestBypass extends BaseCommand {
3
+ static args: {
4
+ id: import("@oclif/core/interfaces").Arg<number, {
5
+ max?: number;
6
+ min?: number;
7
+ }>;
8
+ };
9
+ static description: string;
10
+ static examples: string[];
11
+ static flags: {
12
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ reason: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ };
19
+ run(): Promise<void>;
20
+ }
@@ -0,0 +1,79 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../base-command.js';
3
+ export default class TenantDeployRequestBypass extends BaseCommand {
4
+ static args = {
5
+ id: Args.integer({
6
+ description: 'Tenant deploy request ID',
7
+ required: true,
8
+ }),
9
+ };
10
+ static description = "[CRITICAL] STOP and confirm with the user before running this; it deploys the release immediately, past the review workflow. Deploy the request's release past the approval gate under the tenant_center:deploy:bypass_approval permission. Only available if the tenant has enabled bypass (allow_deploy_bypass), and every use is logged with the given reason. The approval request's own status is left untouched.";
11
+ static examples = [
12
+ `$ xano tenant_deploy_request bypass 12 --reason "Prod incident, reviewer unavailable"
13
+ Deployed tenant release, bypassing deploy request #12's approval gate.
14
+ `,
15
+ ];
16
+ static flags = {
17
+ ...BaseCommand.baseFlags,
18
+ output: Flags.string({
19
+ char: 'o',
20
+ default: 'summary',
21
+ description: 'Output format',
22
+ options: ['summary', 'json'],
23
+ required: false,
24
+ }),
25
+ reason: Flags.string({
26
+ description: 'Reason for bypassing the approval gate (required, audited)',
27
+ required: true,
28
+ }),
29
+ workspace: Flags.string({
30
+ char: 'w',
31
+ description: 'Workspace ID (uses profile workspace if not provided)',
32
+ required: false,
33
+ }),
34
+ };
35
+ async run() {
36
+ const { args, flags } = await this.parse(TenantDeployRequestBypass);
37
+ const { profile } = this.resolveProfile(flags);
38
+ const workspaceId = flags.workspace || profile.workspace;
39
+ if (!workspaceId) {
40
+ this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
41
+ }
42
+ const body = {
43
+ reason: flags.reason,
44
+ };
45
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request/${args.id}/override`;
46
+ try {
47
+ const response = await this.verboseFetch(apiUrl, {
48
+ body: JSON.stringify(body),
49
+ headers: {
50
+ accept: 'application/json',
51
+ Authorization: `Bearer ${profile.access_token}`,
52
+ 'Content-Type': 'application/json',
53
+ },
54
+ method: 'POST',
55
+ }, flags.verbose, profile.access_token);
56
+ if (!response.ok) {
57
+ const message = await this.parseApiError(response, 'Failed to bypass tenant deploy request approval gate');
58
+ this.error(message);
59
+ }
60
+ const deploy = (await response.json());
61
+ if (flags.output === 'json') {
62
+ this.log(JSON.stringify(deploy, null, 2));
63
+ }
64
+ else {
65
+ this.log(`Deployed tenant release, bypassing deploy request #${args.id}'s approval gate.`);
66
+ if (deploy.duration_ms !== undefined)
67
+ this.log(` Time: ${(deploy.duration_ms / 1000).toFixed(1)}s`);
68
+ }
69
+ }
70
+ catch (error) {
71
+ if (error instanceof Error) {
72
+ this.error(`Failed to bypass tenant deploy request approval gate: ${error.message}`);
73
+ }
74
+ else {
75
+ this.error(`Failed to bypass tenant deploy request approval gate: ${String(error)}`);
76
+ }
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,21 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class TenantDeployRequestCreate extends BaseCommand {
3
+ static args: {
4
+ title: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ release: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ reviewers: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ };
20
+ run(): Promise<void>;
21
+ }
@@ -0,0 +1,108 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import BaseCommand from '../../../base-command.js';
3
+ export default class TenantDeployRequestCreate extends BaseCommand {
4
+ static args = {
5
+ title: Args.string({
6
+ description: 'Short title for the deploy request',
7
+ required: true,
8
+ }),
9
+ };
10
+ static description = 'Open a tenant deploy request asking named reviewers to approve deploying a release to a tenant. Submits for review immediately unless --draft is passed.';
11
+ static examples = [
12
+ `$ xano tenant_deploy_request create "Deploy v1.2 to prod" --tenant prod --release v1.2 --reviewers 12,45
13
+ Created deploy request #12: "Deploy v1.2 to prod" [pending]
14
+ `,
15
+ `$ xano tenant_deploy_request create "Deploy v1.2 to prod" --tenant prod --release v1.2 --draft`,
16
+ `$ xano tenant_deploy_request create "$PR_TITLE" --tenant prod --release v1.2 --description "$PR_BODY" --reviewers 12,45 -o json`,
17
+ ];
18
+ static flags = {
19
+ ...BaseCommand.baseFlags,
20
+ description: Flags.string({
21
+ char: 'd',
22
+ description: 'Longer description of what is being deployed and why',
23
+ required: false,
24
+ }),
25
+ draft: Flags.boolean({
26
+ default: false,
27
+ description: 'Save as a draft instead of submitting immediately (reviewers are not notified until submitted)',
28
+ required: false,
29
+ }),
30
+ output: Flags.string({
31
+ char: 'o',
32
+ default: 'summary',
33
+ description: 'Output format',
34
+ options: ['summary', 'json'],
35
+ required: false,
36
+ }),
37
+ release: Flags.string({
38
+ char: 'r',
39
+ description: 'Name of the release to deploy',
40
+ required: true,
41
+ }),
42
+ reviewers: Flags.string({
43
+ description: 'Comma-separated user IDs to request review from',
44
+ required: false,
45
+ }),
46
+ tenant: Flags.string({
47
+ char: 't',
48
+ description: 'Name of the target tenant',
49
+ required: true,
50
+ }),
51
+ workspace: Flags.string({
52
+ char: 'w',
53
+ description: 'Workspace ID (uses profile workspace if not provided)',
54
+ required: false,
55
+ }),
56
+ };
57
+ async run() {
58
+ const { args, flags } = await this.parse(TenantDeployRequestCreate);
59
+ const { profile } = this.resolveProfile(flags);
60
+ const workspaceId = flags.workspace || profile.workspace;
61
+ if (!workspaceId) {
62
+ this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
63
+ }
64
+ const reviewerIds = flags.reviewers
65
+ ? flags.reviewers.split(',').map((id) => Number.parseInt(id.trim(), 10))
66
+ : [];
67
+ const body = {
68
+ release_name: flags.release,
69
+ reviewer_ids: reviewerIds,
70
+ status: flags.draft ? 'draft' : 'pending',
71
+ tenant_name: flags.tenant,
72
+ title: args.title,
73
+ };
74
+ if (flags.description)
75
+ body.description = flags.description;
76
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request`;
77
+ try {
78
+ const response = await this.verboseFetch(apiUrl, {
79
+ body: JSON.stringify(body),
80
+ headers: {
81
+ accept: 'application/json',
82
+ Authorization: `Bearer ${profile.access_token}`,
83
+ 'Content-Type': 'application/json',
84
+ },
85
+ method: 'POST',
86
+ }, flags.verbose, profile.access_token);
87
+ if (!response.ok) {
88
+ const message = await this.parseApiError(response, 'Failed to create tenant deploy request');
89
+ this.error(message);
90
+ }
91
+ const item = (await response.json());
92
+ if (flags.output === 'json') {
93
+ this.log(JSON.stringify(item, null, 2));
94
+ }
95
+ else {
96
+ this.log(`Created deploy request #${item.id}: "${item.title}" [${item.status}]`);
97
+ }
98
+ }
99
+ catch (error) {
100
+ if (error instanceof Error) {
101
+ this.error(`Failed to create tenant deploy request: ${error.message}`);
102
+ }
103
+ else {
104
+ this.error(`Failed to create tenant deploy request: ${String(error)}`);
105
+ }
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,22 @@
1
+ import BaseCommand from '../../../base-command.js';
2
+ export default class TenantDeployRequestEdit extends BaseCommand {
3
+ static args: {
4
+ id: import("@oclif/core/interfaces").Arg<number, {
5
+ max?: number;
6
+ min?: number;
7
+ }>;
8
+ };
9
+ static description: string;
10
+ static examples: string[];
11
+ static flags: {
12
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
+ reviewers: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
+ };
21
+ run(): Promise<void>;
22
+ }