@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
|
@@ -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
|
-
|
|
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
|
|
92
|
-
|
|
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
|
+
? (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
|
+
},
|
|
94
129
|
description: flags.description === undefined ? (current.description ?? '') : flags.description,
|
|
95
130
|
display: flags.display === undefined ? (current.display ?? current.name) : flags.display,
|
|
96
131
|
domain: flags.domain === undefined ? (current.domain ?? '') : flags.domain,
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
'override-policy': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
reason: 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
|
+
/** Set once the policy gate refuses the bypass deploy, the one failure that exits 2. */
|
|
21
|
+
private refusedByPolicyGate;
|
|
22
|
+
/** Exit 2 is the policy 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>;
|
|
28
|
+
run(): Promise<void>;
|
|
29
|
+
/** The bypass deploy was refused by the policy gate: its verdict, then the refusal, exiting 2. */
|
|
30
|
+
private refuseByPolicyGate;
|
|
31
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { gateOverrideHint, gateRefusal, gateRefusalOutput } from '../../../utils/policy/gate.js';
|
|
4
|
+
export default class TenantDeployRequestBypass extends BaseCommand {
|
|
5
|
+
static args = {
|
|
6
|
+
id: Args.integer({
|
|
7
|
+
description: 'Tenant deploy request ID',
|
|
8
|
+
required: true,
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
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.";
|
|
12
|
+
static examples = [
|
|
13
|
+
`$ xano tenant_deploy_request bypass 12 --reason "Prod incident, reviewer unavailable"
|
|
14
|
+
Deployed tenant release, bypassing deploy request #12's approval gate.
|
|
15
|
+
`,
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
...BaseCommand.baseFlags,
|
|
19
|
+
output: Flags.string({
|
|
20
|
+
char: 'o',
|
|
21
|
+
default: 'summary',
|
|
22
|
+
description: 'Output format',
|
|
23
|
+
options: ['summary', 'json'],
|
|
24
|
+
required: false,
|
|
25
|
+
}),
|
|
26
|
+
'override-policy': Flags.boolean({
|
|
27
|
+
default: false,
|
|
28
|
+
description: 'Also proceed past a blocking policy gate, recording --reason as the override reason (needs the workspace:policy update permission; audited)',
|
|
29
|
+
hidden: true,
|
|
30
|
+
}),
|
|
31
|
+
reason: Flags.string({
|
|
32
|
+
description: 'Reason for bypassing the approval gate (required, audited)',
|
|
33
|
+
required: true,
|
|
34
|
+
}),
|
|
35
|
+
workspace: Flags.string({
|
|
36
|
+
char: 'w',
|
|
37
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
/** Set once the policy gate refuses the bypass deploy, the one failure that exits 2. */
|
|
42
|
+
refusedByPolicyGate = false;
|
|
43
|
+
/** Exit 2 is the policy gate's refusal; every other failure, flag errors included, exits 1. */
|
|
44
|
+
async catch(error) {
|
|
45
|
+
if (this.refusedByPolicyGate)
|
|
46
|
+
return super.catch(error);
|
|
47
|
+
return this.catchAsOperational(error);
|
|
48
|
+
}
|
|
49
|
+
async run() {
|
|
50
|
+
const { args, flags } = await this.parse(TenantDeployRequestBypass);
|
|
51
|
+
const { profile } = this.resolveProfile(flags);
|
|
52
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
53
|
+
if (!workspaceId) {
|
|
54
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
55
|
+
}
|
|
56
|
+
const body = {
|
|
57
|
+
reason: flags.reason,
|
|
58
|
+
...(flags['override-policy'] ? { override_policy: true } : {}),
|
|
59
|
+
};
|
|
60
|
+
// A refusal by the policy gate exits 2 with its verdict; the catch that rewords other failures keeps it.
|
|
61
|
+
let refused = null;
|
|
62
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request/${args.id}/override`;
|
|
63
|
+
try {
|
|
64
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
65
|
+
body: JSON.stringify(body),
|
|
66
|
+
headers: {
|
|
67
|
+
accept: 'application/json',
|
|
68
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
},
|
|
71
|
+
method: 'POST',
|
|
72
|
+
}, flags.verbose, profile.access_token);
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
refused = await gateRefusal(response);
|
|
75
|
+
if (refused)
|
|
76
|
+
this.refuseByPolicyGate(refused, args.id, flags.output === 'json');
|
|
77
|
+
const message = await this.parseApiError(response, 'Failed to bypass tenant deploy request approval gate');
|
|
78
|
+
this.error(message);
|
|
79
|
+
}
|
|
80
|
+
const deploy = (await response.json());
|
|
81
|
+
if (flags.output === 'json') {
|
|
82
|
+
this.log(JSON.stringify(deploy, null, 2));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.log(`Deployed tenant release, bypassing deploy request #${args.id}'s approval gate.`);
|
|
86
|
+
if (deploy.duration_ms !== undefined)
|
|
87
|
+
this.log(` Time: ${(deploy.duration_ms / 1000).toFixed(1)}s`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (refused)
|
|
92
|
+
throw error;
|
|
93
|
+
if (error instanceof Error) {
|
|
94
|
+
this.error(`Failed to bypass tenant deploy request approval gate: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.error(`Failed to bypass tenant deploy request approval gate: ${String(error)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** The bypass deploy was refused by the policy gate: its verdict, then the refusal, exiting 2. */
|
|
102
|
+
refuseByPolicyGate(refused, id, json) {
|
|
103
|
+
for (const line of gateRefusalOutput(refused, json))
|
|
104
|
+
this.log(line);
|
|
105
|
+
const override = `xano tenant_deploy_request bypass ${id} --reason "<why>" --override-policy`;
|
|
106
|
+
this.refusedByPolicyGate = true;
|
|
107
|
+
this.error(`${refused.message}${gateOverrideHint(refused.answer, override)}`, { exit: 2 });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
export default class TenantDeployRequestEdit extends BaseCommand {
|
|
4
|
+
static args = {
|
|
5
|
+
id: Args.integer({
|
|
6
|
+
description: 'Tenant deploy request ID',
|
|
7
|
+
required: true,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static description = "Edit a tenant deploy request's title, description or reviewers. The author or an existing reviewer may do this.";
|
|
11
|
+
static examples = [
|
|
12
|
+
`$ xano tenant_deploy_request edit 12 --title "Deploy v1.2.1 to prod"
|
|
13
|
+
Updated deploy request #12: "Deploy v1.2.1 to prod" [pending]
|
|
14
|
+
`,
|
|
15
|
+
`$ xano tenant_deploy_request edit 12 --reviewers 12,45,67 -o json`,
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
...BaseCommand.baseFlags,
|
|
19
|
+
description: Flags.string({
|
|
20
|
+
char: 'd',
|
|
21
|
+
description: 'New description',
|
|
22
|
+
required: false,
|
|
23
|
+
}),
|
|
24
|
+
output: Flags.string({
|
|
25
|
+
char: 'o',
|
|
26
|
+
default: 'summary',
|
|
27
|
+
description: 'Output format',
|
|
28
|
+
options: ['summary', 'json'],
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
31
|
+
reviewers: Flags.string({
|
|
32
|
+
description: 'Replacement comma-separated user IDs to request review from',
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
35
|
+
title: Flags.string({
|
|
36
|
+
description: 'New title',
|
|
37
|
+
required: false,
|
|
38
|
+
}),
|
|
39
|
+
workspace: Flags.string({
|
|
40
|
+
char: 'w',
|
|
41
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
42
|
+
required: false,
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
async run() {
|
|
46
|
+
const { args, flags } = await this.parse(TenantDeployRequestEdit);
|
|
47
|
+
const { profile } = this.resolveProfile(flags);
|
|
48
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
49
|
+
if (!workspaceId) {
|
|
50
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
51
|
+
}
|
|
52
|
+
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request/${args.id}`;
|
|
53
|
+
try {
|
|
54
|
+
const getResponse = await this.verboseFetch(baseUrl, {
|
|
55
|
+
headers: {
|
|
56
|
+
accept: 'application/json',
|
|
57
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
58
|
+
},
|
|
59
|
+
method: 'GET',
|
|
60
|
+
}, flags.verbose, profile.access_token);
|
|
61
|
+
if (!getResponse.ok) {
|
|
62
|
+
const message = await this.parseApiError(getResponse, 'Failed to fetch tenant deploy request');
|
|
63
|
+
this.error(message);
|
|
64
|
+
}
|
|
65
|
+
const current = (await getResponse.json());
|
|
66
|
+
const body = {
|
|
67
|
+
description: flags.description === undefined ? (current.description ?? '') : flags.description,
|
|
68
|
+
reviewer_ids: flags.reviewers
|
|
69
|
+
? flags.reviewers.split(',').map((id) => Number.parseInt(id.trim(), 10))
|
|
70
|
+
: (current.reviewers ?? []).map((r) => r.id),
|
|
71
|
+
title: flags.title === undefined ? current.title : flags.title,
|
|
72
|
+
};
|
|
73
|
+
const putResponse = await this.verboseFetch(baseUrl, {
|
|
74
|
+
body: JSON.stringify(body),
|
|
75
|
+
headers: {
|
|
76
|
+
accept: 'application/json',
|
|
77
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
},
|
|
80
|
+
method: 'PUT',
|
|
81
|
+
}, flags.verbose, profile.access_token);
|
|
82
|
+
if (!putResponse.ok) {
|
|
83
|
+
const message = await this.parseApiError(putResponse, 'Failed to edit tenant deploy request');
|
|
84
|
+
this.error(message);
|
|
85
|
+
}
|
|
86
|
+
const item = (await putResponse.json());
|
|
87
|
+
if (flags.output === 'json') {
|
|
88
|
+
this.log(JSON.stringify(item, null, 2));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.log(`Updated deploy request #${item.id}: "${item.title}" [${item.status}]`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
this.error(`Failed to edit tenant deploy request: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.error(`Failed to edit tenant deploy request: ${String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class TenantDeployRequestGet 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
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* The policy gate's verdict on the request's release for its tenant, as a deploy now would meet it.
|
|
21
|
+
* Only a policy reader gets one; any refusal or failure is a reason it is not available, never an
|
|
22
|
+
* error of this command.
|
|
23
|
+
*/
|
|
24
|
+
private policyCheck;
|
|
25
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { fetchPolicyGate, gateLines, unavailableReason } from '../../../utils/policy/gate.js';
|
|
4
|
+
export default class TenantDeployRequestGet extends BaseCommand {
|
|
5
|
+
static args = {
|
|
6
|
+
id: Args.integer({
|
|
7
|
+
description: 'Tenant deploy request ID',
|
|
8
|
+
required: true,
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
static description = 'Get details of a specific tenant deploy request';
|
|
12
|
+
static examples = [
|
|
13
|
+
`$ xano tenant_deploy_request get 12
|
|
14
|
+
Deploy request #12: "Deploy v1.2 to prod" [pending]
|
|
15
|
+
Tenant: prod
|
|
16
|
+
Release: v1.2
|
|
17
|
+
`,
|
|
18
|
+
`$ xano tenant_deploy_request get 12 -o json`,
|
|
19
|
+
];
|
|
20
|
+
static flags = {
|
|
21
|
+
...BaseCommand.baseFlags,
|
|
22
|
+
output: Flags.string({
|
|
23
|
+
char: 'o',
|
|
24
|
+
default: 'summary',
|
|
25
|
+
description: 'Output format',
|
|
26
|
+
options: ['summary', 'json'],
|
|
27
|
+
required: false,
|
|
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(TenantDeployRequestGet);
|
|
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 apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/approval_request/${args.id}`;
|
|
43
|
+
try {
|
|
44
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
45
|
+
headers: {
|
|
46
|
+
accept: 'application/json',
|
|
47
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
48
|
+
},
|
|
49
|
+
method: 'GET',
|
|
50
|
+
}, flags.verbose, profile.access_token);
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const message = await this.parseApiError(response, 'Failed to get tenant deploy request');
|
|
53
|
+
this.error(message);
|
|
54
|
+
}
|
|
55
|
+
const item = (await response.json());
|
|
56
|
+
const policyCheck = await this.policyCheck(item, { profile, verbose: flags.verbose, workspaceId });
|
|
57
|
+
if (flags.output === 'json') {
|
|
58
|
+
this.log(JSON.stringify({ ...item, policy_gate: policyCheck.answer ?? null,
|
|
59
|
+
...(policyCheck.unavailable ? { policy_gate_unavailable: policyCheck.unavailable } : {}) }, null, 2));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.log(`Deploy request #${item.id}: "${item.title}" [${item.status}]`);
|
|
63
|
+
this.log(` Tenant: ${item._tenant?.name || (item.deployment?.tenant?.id ? `#${item.deployment.tenant.id}` : 'unavailable')}`);
|
|
64
|
+
this.log(` Release: ${item._release?.name || (item.deployment?.release?.id ? `#${item.deployment.release.id}` : 'unavailable')}`);
|
|
65
|
+
if (item.description)
|
|
66
|
+
this.log(` Description: ${item.description}`);
|
|
67
|
+
if (item.reviewers?.length)
|
|
68
|
+
this.log(` Reviewers: ${item.reviewers.map((r) => r.id).join(', ')}`);
|
|
69
|
+
if (item.can_review !== undefined)
|
|
70
|
+
this.log(` Can review: ${item.can_review}`);
|
|
71
|
+
if (policyCheck.answer) {
|
|
72
|
+
this.log(`Policy check of release "${item._release?.name}" on tenant "${item._tenant?.name}":`);
|
|
73
|
+
for (const line of gateLines(policyCheck.answer))
|
|
74
|
+
this.log(line);
|
|
75
|
+
}
|
|
76
|
+
else if (policyCheck.unavailable) {
|
|
77
|
+
this.log(`Policy checks: not available (${policyCheck.unavailable})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof Error) {
|
|
83
|
+
this.error(`Failed to get tenant deploy request: ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
this.error(`Failed to get tenant deploy request: ${String(error)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The policy gate's verdict on the request's release for its tenant, as a deploy now would meet it.
|
|
92
|
+
* Only a policy reader gets one; any refusal or failure is a reason it is not available, never an
|
|
93
|
+
* error of this command.
|
|
94
|
+
*/
|
|
95
|
+
async policyCheck(item, target) {
|
|
96
|
+
// Terminal requests are historical records: previewing now can check a different base release.
|
|
97
|
+
if (['approved', 'closed'].includes(item.status))
|
|
98
|
+
return {};
|
|
99
|
+
const tenant = item._tenant?.name?.trim();
|
|
100
|
+
const release = item._release?.name?.trim();
|
|
101
|
+
if (!tenant || !release)
|
|
102
|
+
return { unavailable: 'the request does not name its tenant and release' };
|
|
103
|
+
try {
|
|
104
|
+
const result = await fetchPolicyGate({ verboseFetch: (...args) => this.verboseFetch(...args) }, { profile: target.profile, release, tenant, verbose: target.verbose, workspace: String(target.workspaceId) });
|
|
105
|
+
if ('answer' in result)
|
|
106
|
+
return result.answer.status === 'disabled' ? {} : { answer: result.answer };
|
|
107
|
+
return result.unavailable.payload?.code === 'policy_feature_disabled' ? {} : { unavailable: unavailableReason(result.unavailable) };
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return { unavailable: error instanceof Error ? error.message : String(error) };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class TenantDeployRequestList extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
mine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'per-page': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
release: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
tenant: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'to-review': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
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
|
+
}
|